From 64059e2351b42a85ffbcfc7c0067aa43d924448b Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Thu, 15 Jan 2026 22:13:41 -0500 Subject: [PATCH 01/13] Add test for webhook URL with trailing whitespace Currently fails because URI.parse rejects URLs with trailing spaces. --- test/models/webhook_test.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/models/webhook_test.rb b/test/models/webhook_test.rb index 52c43383d..121c1a651 100644 --- a/test/models/webhook_test.rb +++ b/test/models/webhook_test.rb @@ -35,6 +35,10 @@ class WebhookTest < ActiveSupport::TestCase webhook = Webhook.new name: "HTTPS", board: boards(:writebook), url: "https://example.com/webhook" assert webhook.valid? + + webhook = Webhook.new name: "TRAILING SPACE", board: boards(:writebook), url: "https://example.com/webhook " + assert webhook.valid? + assert_equal "https://example.com/webhook", webhook.url end test "deactivate" do From d8b6ea8bbadb0467793f052c6b96d77492f6b637 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Thu, 15 Jan 2026 22:13:47 -0500 Subject: [PATCH 02/13] Strip whitespace from webhook URLs Prevents validation errors when users accidentally include trailing spaces in webhook URLs. --- app/models/webhook.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 3b34af041..f008aa934 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -36,6 +36,7 @@ class Webhook < ApplicationRecord after_create :create_delinquency_tracker! normalizes :subscribed_actions, with: ->(value) { Array.wrap(value).map(&:to_s).uniq & PERMITTED_ACTIONS } + normalizes :url, with: -> { it.strip } validates :name, presence: true validate :validate_url From f50b0bca5a0cd1bab1b703cad77b0f4ee006583e Mon Sep 17 00:00:00 2001 From: Henrik Nyh Date: Sun, 8 Feb 2026 19:51:41 +0000 Subject: [PATCH 03/13] billing.rb: destroy -> destroy! --- saas/app/models/account/billing.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/saas/app/models/account/billing.rb b/saas/app/models/account/billing.rb index 02886581a..c124fd64c 100644 --- a/saas/app/models/account/billing.rb +++ b/saas/app/models/account/billing.rb @@ -27,7 +27,7 @@ module Account::Billing end def uncomp - billing_waiver&.destroy + billing_waiver&.destroy! reload_billing_waiver end From 4e52b9b1d9c6180dbfa211bed605af7d87ed0e2c Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Sat, 28 Feb 2026 05:18:48 -0500 Subject: [PATCH 04/13] Display the save form button on Board Settings page as a bridge button --- app/views/boards/edit.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/boards/edit.html.erb b/app/views/boards/edit.html.erb index cd28b29a4..5895ef65f 100644 --- a/app/views/boards/edit.html.erb +++ b/app/views/boards/edit.html.erb @@ -16,13 +16,13 @@
<%= form_with model: @board, class: "display-contents", data: { - controller: "form boards-form", + controller: "form boards-form bridge--form", boards_form_self_removal_prompt_message_value: "Are you sure you want to remove yourself from this board? You won’t be able to get back in unless someone invites you.", action: "turbo:submit-start->boards-form#submitWithWarning" } do |form| %> <%= render "boards/edit/name", form: form, board: @board %> <%= render "boards/edit/users", board: @board, selected_users: @selected_users, unselected_users: @unselected_users, form: form %> - From bdf4459716908bf185d86fe279addeb31d5137f1 Mon Sep 17 00:00:00 2001 From: Dain Miller Date: Sat, 28 Feb 2026 05:41:55 -0500 Subject: [PATCH 05/13] Fix small documentation typos --- docs/kamal-deployment.md | 2 +- saas/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/kamal-deployment.md b/docs/kamal-deployment.md index 19aa208c8..60e1b345a 100644 --- a/docs/kamal-deployment.md +++ b/docs/kamal-deployment.md @@ -11,7 +11,7 @@ The steps to configure your very own Fizzy are: 1. Fork the repo 2. Initialize Kamal by running `kamal init`. This command generates the `.kamal` directory along with the required configuration files, including `.kamal/secrets`. -3. Edit few things in config/deploy.yml and .kamal/secrets +3. Edit a few things in config/deploy.yml and .kamal/secrets 4. Run `kamal setup` to do your first deploy. We'll go through each of these in turn. diff --git a/saas/README.md b/saas/README.md index b71afff08..bec3b279f 100644 --- a/saas/README.md +++ b/saas/README.md @@ -14,7 +14,7 @@ To go back to open source mode: bin/rails saas:disable ``` -Then you can work do [Fizzy development as usual](https://github.com/basecamp/fizzy). +Then you can do [Fizzy development as usual](https://github.com/basecamp/fizzy). ## How to update Fizzy From ef980a0d483d23f7578e762710c1530c8c8c3512 Mon Sep 17 00:00:00 2001 From: Adrien Maston Date: Mon, 2 Mar 2026 14:50:58 +0100 Subject: [PATCH 06/13] Allow notice to take more inline space --- app/assets/stylesheets/flash.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/flash.css b/app/assets/stylesheets/flash.css index a717838e4..0d369dfbb 100644 --- a/app/assets/stylesheets/flash.css +++ b/app/assets/stylesheets/flash.css @@ -16,7 +16,9 @@ color: var(--flash-color, var(--color-ink-inverted)); display: inline-flex; font-size: var(--font-size-medium); + inline-size: max-content; margin: 0 auto; + max-inline-size: 90vw; padding: 0.7em 1.4em; } } From 143508fc221c6a28864153a1ac046b4f4ffc3d8d Mon Sep 17 00:00:00 2001 From: Joseph Hale Date: Mon, 2 Mar 2026 14:13:00 -0700 Subject: [PATCH 07/13] Skip AnalyzeJob when blob has no attachments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upload-then-delete races cause AnalyzeJob to hit S3 after PurgeJob has already removed the object, producing Aws::S3::Errors::NoSuchKey noise in solid_queue_failed_executions. Since PurgeOnLastAttachment destroys the attachment row before enqueueing PurgeJob, checking blob.attachments.exists? catches this — a fast DB query that avoids the S3 round-trip entirely. Co-Authored-By: Claude Opus 4.6 --- ...storage_analyze_job_suppress_broadcasts.rb | 2 ++ ...ge_analyze_job_suppress_broadcasts_test.rb | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 test/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts_test.rb diff --git a/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts.rb b/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts.rb index ba22603b6..030232ee4 100644 --- a/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts.rb +++ b/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts.rb @@ -4,6 +4,8 @@ # there is currently a bug https://github.com/rails/rails/issues/55144 module ActiveStorageAnalyzeJobSuppressBroadcasts def perform(blob) + return unless blob.attachments.exists? + Board.suppressing_turbo_broadcasts do Card.suppressing_turbo_broadcasts do super diff --git a/test/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts_test.rb b/test/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts_test.rb new file mode 100644 index 000000000..1015c957c --- /dev/null +++ b/test/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts_test.rb @@ -0,0 +1,23 @@ +require "test_helper" + +class ActiveStorageAnalyzeJobSuppressBroadcastsTest < ActiveSupport::TestCase + test "skips analysis when blob has no attachments" do + blob = ActiveStorage::Blob.create_and_upload!( + io: StringIO.new("x" * 1024), filename: "orphan.txt", content_type: "text/plain" + ) + + blob.expects(:analyze).never + + ActiveStorage::AnalyzeJob.perform_now(blob) + end + + test "performs analysis when blob has attachments" do + card = cards(:logo) + card.image.attach io: StringIO.new("x" * 1024), filename: "test.png", content_type: "image/png" + blob = card.image.blob + + blob.expects(:analyze).once + + ActiveStorage::AnalyzeJob.perform_now(blob) + end +end From c61f49cce97560dde49d00c66dca4df618e69374 Mon Sep 17 00:00:00 2001 From: Joseph Hale Date: Mon, 2 Mar 2026 14:38:19 -0700 Subject: [PATCH 08/13] Extract detached-blob guard into its own file Separate the attachment existence check from the broadcast suppression override so each file has a single responsibility. The guard now lives in ActiveStorageAnalyzeJobSkipDetached with its own documentation explaining the upload-then-delete race condition. Co-Authored-By: Claude Opus 4.6 --- .../active_storage_analyze_job_skip_detached.rb | 17 +++++++++++++++++ ...e_storage_analyze_job_suppress_broadcasts.rb | 2 -- ...e_storage_analyze_job_skip_detached_test.rb} | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 lib/rails_ext/active_storage_analyze_job_skip_detached.rb rename test/lib/rails_ext/{active_storage_analyze_job_suppress_broadcasts_test.rb => active_storage_analyze_job_skip_detached_test.rb} (88%) diff --git a/lib/rails_ext/active_storage_analyze_job_skip_detached.rb b/lib/rails_ext/active_storage_analyze_job_skip_detached.rb new file mode 100644 index 000000000..efa671335 --- /dev/null +++ b/lib/rails_ext/active_storage_analyze_job_skip_detached.rb @@ -0,0 +1,17 @@ +# Skip analysis for blobs whose attachments have already been destroyed. +# +# When an upload is deleted before AnalyzeJob runs, PurgeOnLastAttachment has +# already destroyed the attachment row and enqueued PurgeJob for the blob's S3 +# object. Analyzing at this point hits a missing key. Same check as +# PurgeOnLastAttachment#purge_blob_if_last. +module ActiveStorageAnalyzeJobSkipDetached + def perform(blob) + return unless blob.attachments.exists? + + super + end +end + +ActiveSupport.on_load :active_storage_blob do + ActiveStorage::AnalyzeJob.prepend ActiveStorageAnalyzeJobSkipDetached +end diff --git a/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts.rb b/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts.rb index 030232ee4..ba22603b6 100644 --- a/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts.rb +++ b/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts.rb @@ -4,8 +4,6 @@ # there is currently a bug https://github.com/rails/rails/issues/55144 module ActiveStorageAnalyzeJobSuppressBroadcasts def perform(blob) - return unless blob.attachments.exists? - Board.suppressing_turbo_broadcasts do Card.suppressing_turbo_broadcasts do super diff --git a/test/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts_test.rb b/test/lib/rails_ext/active_storage_analyze_job_skip_detached_test.rb similarity index 88% rename from test/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts_test.rb rename to test/lib/rails_ext/active_storage_analyze_job_skip_detached_test.rb index 1015c957c..5ab5edaff 100644 --- a/test/lib/rails_ext/active_storage_analyze_job_suppress_broadcasts_test.rb +++ b/test/lib/rails_ext/active_storage_analyze_job_skip_detached_test.rb @@ -1,6 +1,6 @@ require "test_helper" -class ActiveStorageAnalyzeJobSuppressBroadcastsTest < ActiveSupport::TestCase +class ActiveStorageAnalyzeJobSkipDetachedTest < ActiveSupport::TestCase test "skips analysis when blob has no attachments" do blob = ActiveStorage::Blob.create_and_upload!( io: StringIO.new("x" * 1024), filename: "orphan.txt", content_type: "text/plain" From 7611236a67a61afae4688924a7577843efa78022 Mon Sep 17 00:00:00 2001 From: Joseph Hale Date: Mon, 2 Mar 2026 14:40:21 -0700 Subject: [PATCH 09/13] Simplify comment in skip-detached override Co-Authored-By: Claude Opus 4.6 --- lib/rails_ext/active_storage_analyze_job_skip_detached.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/rails_ext/active_storage_analyze_job_skip_detached.rb b/lib/rails_ext/active_storage_analyze_job_skip_detached.rb index efa671335..e48154652 100644 --- a/lib/rails_ext/active_storage_analyze_job_skip_detached.rb +++ b/lib/rails_ext/active_storage_analyze_job_skip_detached.rb @@ -1,9 +1,5 @@ -# Skip analysis for blobs whose attachments have already been destroyed. -# -# When an upload is deleted before AnalyzeJob runs, PurgeOnLastAttachment has -# already destroyed the attachment row and enqueued PurgeJob for the blob's S3 -# object. Analyzing at this point hits a missing key. Same check as -# PurgeOnLastAttachment#purge_blob_if_last. +# Skip analysis for blobs whose attachments have already been destroyed. Avoids +# Aws::S3::Errors::NoSuchKey when an upload is deleted before AnalyzeJob runs. module ActiveStorageAnalyzeJobSkipDetached def perform(blob) return unless blob.attachments.exists? From 451920adc7ae3508fc04b17f34f89bd818b23958 Mon Sep 17 00:00:00 2001 From: Joseph Hale Date: Mon, 2 Mar 2026 15:25:11 -0700 Subject: [PATCH 10/13] Include concrete example motivating this module file --- lib/rails_ext/active_storage_analyze_job_skip_detached.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/rails_ext/active_storage_analyze_job_skip_detached.rb b/lib/rails_ext/active_storage_analyze_job_skip_detached.rb index e48154652..0df49f0cf 100644 --- a/lib/rails_ext/active_storage_analyze_job_skip_detached.rb +++ b/lib/rails_ext/active_storage_analyze_job_skip_detached.rb @@ -1,5 +1,6 @@ -# Skip analysis for blobs whose attachments have already been destroyed. Avoids -# Aws::S3::Errors::NoSuchKey when an upload is deleted before AnalyzeJob runs. +# Skip analysis for blobs whose attachments have already been destroyed. +# e.g. when a user uploads a file but deletes it before the analysis runs. +# Avoids `Aws::S3::Errors::NoSuchKey` when an upload is deleted before AnalyzeJob runs. module ActiveStorageAnalyzeJobSkipDetached def perform(blob) return unless blob.attachments.exists? From 9cd1167f8569b15a3b8339bdf441a287ef6e9eb2 Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Tue, 3 Mar 2026 03:28:38 -0600 Subject: [PATCH 11/13] Change exceeding and nearing limits to account for the 1000th card (#2345) * Change exceeding and nearing limits to account for the 1000th card (instead of 1001) * Add boundary condition tests for card limit checks Tests now verify behavior at exactly the limit (1000 cards) and at the nearing threshold (remaining = 100), covering the off-by-one fix. --------- Co-authored-by: Jorge Manrubia --- saas/app/models/account/limited.rb | 4 ++-- saas/test/models/account/limited_test.rb | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/saas/app/models/account/limited.rb b/saas/app/models/account/limited.rb index 8e846a117..2918e90a3 100644 --- a/saas/app/models/account/limited.rb +++ b/saas/app/models/account/limited.rb @@ -21,11 +21,11 @@ module Account::Limited end def nearing_plan_cards_limit? - plan.limit_cards? && remaining_cards_count < NEAR_CARD_LIMIT_THRESHOLD + plan.limit_cards? && remaining_cards_count <= NEAR_CARD_LIMIT_THRESHOLD end def exceeding_card_limit? - plan.limit_cards? && billed_cards_count > plan.card_limit + plan.limit_cards? && billed_cards_count >= plan.card_limit end def nearing_plan_storage_limit? diff --git a/saas/test/models/account/limited_test.rb b/saas/test/models/account/limited_test.rb index 828386023..fff154451 100644 --- a/saas/test/models/account/limited_test.rb +++ b/saas/test/models/account/limited_test.rb @@ -10,10 +10,11 @@ class Account::LimitedTest < ActiveSupport::TestCase accounts(:initech).update_column(:cards_count, 899) assert_not accounts(:initech).nearing_plan_cards_limit? - # Free plan near limit + # Free plan at exactly the threshold (remaining = 100) accounts(:initech).update_column(:cards_count, 900) - assert_not accounts(:initech).nearing_plan_cards_limit? + assert accounts(:initech).nearing_plan_cards_limit? + # Free plan over the threshold accounts(:initech).update_column(:cards_count, 901) assert accounts(:initech).nearing_plan_cards_limit? end @@ -27,6 +28,10 @@ class Account::LimitedTest < ActiveSupport::TestCase accounts(:initech).update_column(:cards_count, 999) assert_not accounts(:initech).exceeding_card_limit? + # Free plan at exactly the limit + accounts(:initech).update_column(:cards_count, 1000) + assert accounts(:initech).exceeding_card_limit? + # Free plan over limit accounts(:initech).update_column(:cards_count, 1001) assert accounts(:initech).exceeding_card_limit? From 3cb5e29552c0b47c73d04f8e4bce475e02ef2672 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 3 Mar 2026 04:30:31 -0500 Subject: [PATCH 12/13] Document has_attachments in API card responses (#2523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #2455 — add the has_attachments field to the card JSON examples for the cards index, cards show, and pins index endpoints. --- docs/API.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/API.md b/docs/API.md index 47965372c..7ac8dae02 100644 --- a/docs/API.md +++ b/docs/API.md @@ -539,6 +539,7 @@ __Response:__ "description": "Hello, World!", "description_html": "

Hello, World!

", "image_url": null, + "has_attachments": false, "tags": ["programming"], "golden": false, "last_active_at": "2025-12-05T19:38:48.553Z", @@ -590,6 +591,7 @@ __Response:__ "description": "Hello, World!", "description_html": "

Hello, World!

", "image_url": null, + "has_attachments": false, "tags": ["programming"], "closed": false, "golden": false, @@ -857,6 +859,7 @@ __Response:__ "description": "Hello, World!", "description_html": "

Hello, World!

", "image_url": null, + "has_attachments": false, "tags": ["programming"], "golden": false, "last_active_at": "2025-12-05T19:38:48.553Z", From 2e6d06afa7a0ae0df7a76510f9fd409d152eaf7e Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Tue, 3 Mar 2026 05:22:28 -0500 Subject: [PATCH 13/13] Mobile: improve search screen (#2636) * Disable search results nested scrolling behavior for mobile apps * Display the native search input clear button in the native apps --- app/assets/stylesheets/native.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/assets/stylesheets/native.css b/app/assets/stylesheets/native.css index 92c2ef72d..e51304e9d 100644 --- a/app/assets/stylesheets/native.css +++ b/app/assets/stylesheets/native.css @@ -100,6 +100,14 @@ /* Search /* ------------------------------------------------------------------------ */ + .search { + overscroll-behavior: auto; + } + + .search__input::-webkit-search-cancel-button { + display: inline-block; + } + .search__title { text-decoration: none; }