From 91263556a9df83e3998dfe4efca556b982839894 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 31 Oct 2025 21:57:17 +0100 Subject: [PATCH 01/98] Ensure there is a limit of 100 events per column per day Otherwise these pages can get very heavy --- app/helpers/events_helper.rb | 18 ++++++++++++------ app/views/events/day_timeline/_column.html.erb | 5 +++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 14c12c5cd..8bd479784 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -3,24 +3,30 @@ module EventsHelper case event_type when "added" events = day_timeline.events.where(action: [ "card_published", "card_reopened" ]) + full_events_count = events.count { - title: event_column_title("Added", events.count, day_timeline.day), + title: event_column_title("Added", full_events_count, day_timeline.day), index: 1, - events: events + events: events.limit(100).load, + full_events_count: full_events_count } when "closed" events = day_timeline.events.where(action: "card_closed") + full_events_count = events.count { - title: event_column_title("Closed", events.count, day_timeline.day), + title: event_column_title("Closed", full_events_count, day_timeline.day), index: 3, - events: events + events: events.limit(100).load, + full_events_count: full_events_count } else events = day_timeline.events.where.not(action: [ "card_published", "card_closed", "card_reopened" ]) + full_events_count = events.count { - title: event_column_title("Updated", events.count, day_timeline.day), + title: event_column_title("Updated", full_events_count, day_timeline.day), index: 2, - events: events + events: events.limit(100).load, + full_events_count: full_events_count } end end diff --git a/app/views/events/day_timeline/_column.html.erb b/app/views/events/day_timeline/_column.html.erb index a1186da6b..2249766c6 100644 --- a/app/views/events/day_timeline/_column.html.erb +++ b/app/views/events/day_timeline/_column.html.erb @@ -8,4 +8,9 @@ <%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %> <% end %> <% end %> + + <%# FIXME: Need to style this cut-off %> + <% if column[:events].count < column[:full_events_count] %> +

...and <%= column[:full_events_count] - column[:events].count %> more

+ <% end %> From 4b7cd8828fb1c4a7ad68af5fb347aeb9ec3aef21 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Fri, 31 Oct 2025 16:58:21 -0500 Subject: [PATCH 02/98] Style notice --- app/views/events/day_timeline/_column.html.erb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/events/day_timeline/_column.html.erb b/app/views/events/day_timeline/_column.html.erb index 2249766c6..ab7f0aa15 100644 --- a/app/views/events/day_timeline/_column.html.erb +++ b/app/views/events/day_timeline/_column.html.erb @@ -9,8 +9,7 @@ <% end %> <% end %> - <%# FIXME: Need to style this cut-off %> <% if column[:events].count < column[:full_events_count] %> -

...and <%= column[:full_events_count] - column[:events].count %> more

+
Showing the first 100 items (<%= column[:full_events_count] - column[:events].count %> are hidden)
From 3bba72bf191fed66c0d158b4124f317c9bdf470f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 08:19:22 +0100 Subject: [PATCH 03/98] Wait a little to ensure attachments have loaded --- test/system/smoke_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/system/smoke_test.rb b/test/system/smoke_test.rb index 870438f72..8c80c165e 100644 --- a/test/system/smoke_test.rb +++ b/test/system/smoke_test.rb @@ -61,8 +61,8 @@ class SmokeTest < ApplicationSystemTestCase def assert_image_figure_attachment(content_type: "image/png", caption:) assert_figure_attachment(content_type: content_type) do - assert_selector("img[src*='/rails/active_storage']") - assert_selector "figcaption input[placeholder='#{caption}']" + assert_selector "img[src*='/rails/active_storage']", wait: 5 + assert_selector "figcaption input[placeholder='#{caption}']", wait: 5 end end end From d6deb3ac5f7b672512e604ffd95360653692ac3d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 08:22:32 +0100 Subject: [PATCH 04/98] Setup signoff too --- bin/setup | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/setup b/bin/setup index 90d68e323..61ab69995 100755 --- a/bin/setup +++ b/bin/setup @@ -62,6 +62,9 @@ if which pacman >/dev/null 2>&1; then fi fi +# Ensure gh-signoff is installed and up to date +step "Set up gh-signoff" bash -c "gh extension install basecamp/gh-signoff || gh extension upgrade basecamp/gh-signoff" + bundle config set --local auto_install true step "Installing RubyGems" bundle install From e6459cede3bbcda6a91795771349859397163b39 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 08:34:04 +0100 Subject: [PATCH 05/98] Use existing authorization method --- app/controllers/account/settings_controller.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/controllers/account/settings_controller.rb b/app/controllers/account/settings_controller.rb index 9acebbae4..c2f9672c9 100644 --- a/app/controllers/account/settings_controller.rb +++ b/app/controllers/account/settings_controller.rb @@ -1,16 +1,14 @@ class Account::SettingsController < ApplicationController + before_action :ensure_admin, only: :update + def show @account = Account.sole @users = User.active.alphabetically end def update - if Current.user.can_administer? - Account.sole.update!(account_params) - redirect_to account_settings_path - else - head :forbidden - end + Account.sole.update!(account_params) + redirect_to account_settings_path end private From 72d0008df0c65ed039a015fc3032082c25b15689 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 12:49:17 +0100 Subject: [PATCH 06/98] Only restrict resource actions when there's a specific story to tell --- config/routes.rb | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 9a663d70d..9b7e1ddb6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ Rails.application.routes.draw do namespace :account do post :enter, to: "entries#create" - resource :join_code, only: %i[ show edit update destroy ] + resource :join_code resource :settings resource :entropy_configuration end @@ -27,11 +27,11 @@ Rails.application.routes.draw do resources :columns end - resources :cards, only: %i[ create ] + resources :cards, only: :create resources :webhooks do scope module: "webhooks" do - resource :activation, only: %i[ create ] + resource :activation, only: :create end end end @@ -58,7 +58,7 @@ Rails.application.routes.draw do resources :previews end - resources :cards, only: %i[ index show edit update destroy ] do + resources :cards do scope module: :cards do resource :goldness resource :image @@ -95,7 +95,7 @@ Rails.application.routes.draw do scope module: :notifications do get "tray", to: "trays#show", on: :collection - resource :reading, only: %i[ create destroy ] + resource :reading collection do resource :bulk_reading, only: :create end @@ -130,9 +130,9 @@ Rails.application.routes.draw do resource :session do scope module: "sessions" do - resources :transfers, only: %i[ show update ] - resource :magic_link, only: %i[ show create ] - resource :menu, only: %i[ show create ] + resources :transfers + resource :magic_link + resource :menu end end @@ -144,17 +144,17 @@ Rails.application.routes.draw do resources :commands - resource :conversation, only: %i[ show create ] do + resource :conversation do scope module: :conversations do - resources :messages, only: %i[ index create ] + resources :messages end end scope module: :memberships, path: "memberships/:membership_id" do - resource :unlink, only: %i[ show create ], controller: :unlink, as: :unlink_membership + resource :unlink, controller: :unlink, as: :unlink_membership - resources :email_addresses, only: %i[ new create ], param: :token do - resource :confirmation, only: %i[ show create ], module: :email_addresses + resources :email_addresses, param: :token do + resource :confirmation, module: :email_addresses end end From d29e4dcdcf44e21d8a3924363392a547579564af Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 12:49:54 +0100 Subject: [PATCH 07/98] No CR when there are only two statements in total --- app/controllers/account/entropy_configurations_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/account/entropy_configurations_controller.rb b/app/controllers/account/entropy_configurations_controller.rb index 53b1f54e8..9de3ff04c 100644 --- a/app/controllers/account/entropy_configurations_controller.rb +++ b/app/controllers/account/entropy_configurations_controller.rb @@ -1,7 +1,6 @@ class Account::EntropyConfigurationsController < ApplicationController def update Entropy::Configuration.default.update!(entropy_configuration_params) - redirect_to account_settings_path, notice: "Account updated" end From b3474ec97d555a83fc015ec8be4fdf7f44598aa4 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 13:11:41 +0100 Subject: [PATCH 08/98] Rename Entropy::Configuration to just Entropy --- .../account/entropies_controller.rb | 11 +++++++++ .../entropy_configurations_controller.rb | 11 --------- .../collections/entropies_controller.rb | 17 +++++++++++++ .../entropy_configurations_controller.rb | 17 ------------- app/models/account.rb | 2 +- app/models/account/entropic.rb | 8 +++---- app/models/card/entropic.rb | 12 +++++----- app/models/collection.rb | 2 +- app/models/collection/entropic.rb | 10 ++++---- app/models/entropy.rb | 17 ++++++++++--- app/models/entropy/configuration.rb | 16 ------------- ...nfiguration.html.erb => _entropy.html.erb} | 2 +- app/views/account/settings/show.html.erb | 2 +- .../collections/edit/_auto_close.html.erb | 4 ++-- config/routes.rb | 4 ++-- ...ame_entropy_configurations_to_entropies.rb | 5 ++++ db/schema.rb | 6 ++--- db/schema_cache.yml | 16 ++++++------- .../accounts/entropies_controller_test.rb | 15 ++++++++++++ .../entropy_configurations_controller_test.rb | 15 ------------ .../collections/entropies_controller_test.rb | 16 +++++++++++++ .../entropy_configurations_controller_test.rb | 16 ------------- .../collections_controller_test.rb | 2 +- .../configurations.yml => entropies.yml} | 0 test/models/card/entropic_test.rb | 24 +++++++++---------- test/models/card/stallable_test.rb | 2 +- test/models/entropy/configuration_test.rb | 20 ---------------- test/models/entropy_test.rb | 20 ++++++++++++++++ test/routes_test.rb | 4 ++-- 29 files changed, 148 insertions(+), 148 deletions(-) create mode 100644 app/controllers/account/entropies_controller.rb delete mode 100644 app/controllers/account/entropy_configurations_controller.rb create mode 100644 app/controllers/collections/entropies_controller.rb delete mode 100644 app/controllers/collections/entropy_configurations_controller.rb delete mode 100644 app/models/entropy/configuration.rb rename app/views/account/settings/{_entropy_configuration.html.erb => _entropy.html.erb} (89%) create mode 100644 db/migrate/20251102115338_rename_entropy_configurations_to_entropies.rb create mode 100644 test/controllers/accounts/entropies_controller_test.rb delete mode 100644 test/controllers/accounts/entropy_configurations_controller_test.rb create mode 100644 test/controllers/collections/entropies_controller_test.rb delete mode 100644 test/controllers/collections/entropy_configurations_controller_test.rb rename test/fixtures/{entropy/configurations.yml => entropies.yml} (100%) delete mode 100644 test/models/entropy/configuration_test.rb create mode 100644 test/models/entropy_test.rb diff --git a/app/controllers/account/entropies_controller.rb b/app/controllers/account/entropies_controller.rb new file mode 100644 index 000000000..75fa01e20 --- /dev/null +++ b/app/controllers/account/entropies_controller.rb @@ -0,0 +1,11 @@ +class Account::EntropiesController < ApplicationController + def update + Entropy.default.update!(entropy_params) + redirect_to account_settings_path, notice: "Account updated" + end + + private + def entropy_params + params.expect(entropy: [ :auto_postpone_period ]) + end +end diff --git a/app/controllers/account/entropy_configurations_controller.rb b/app/controllers/account/entropy_configurations_controller.rb deleted file mode 100644 index 9de3ff04c..000000000 --- a/app/controllers/account/entropy_configurations_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Account::EntropyConfigurationsController < ApplicationController - def update - Entropy::Configuration.default.update!(entropy_configuration_params) - redirect_to account_settings_path, notice: "Account updated" - end - - private - def entropy_configuration_params - params.expect(entropy_configuration: [ :auto_postpone_period ]) - end -end diff --git a/app/controllers/collections/entropies_controller.rb b/app/controllers/collections/entropies_controller.rb new file mode 100644 index 000000000..bb0d2940a --- /dev/null +++ b/app/controllers/collections/entropies_controller.rb @@ -0,0 +1,17 @@ +class Collections::EntropiesController < ApplicationController + include CollectionScoped + + def update + @collection.entropy.update!(entropy_params) + + render turbo_stream: [ + turbo_stream.replace([ @collection, :entropy], partial: "collections/edit/auto_close", locals: { collection: @collection }), + turbo_stream_flash(notice: "Saved") + ] + end + + private + def entropy_params + params.expect(collection: [ :auto_postpone_period ]) + end +end diff --git a/app/controllers/collections/entropy_configurations_controller.rb b/app/controllers/collections/entropy_configurations_controller.rb deleted file mode 100644 index 76957627d..000000000 --- a/app/controllers/collections/entropy_configurations_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -class Collections::EntropyConfigurationsController < ApplicationController - include CollectionScoped - - def update - @collection.entropy_configuration.update!(entropy_configuration_params) - - render turbo_stream: [ - turbo_stream.replace([ @collection, :entropy_configuration ], partial: "collections/edit/auto_close", locals: { collection: @collection }), - turbo_stream_flash(notice: "Saved") - ] - end - - private - def entropy_configuration_params - params.expect(collection: [ :auto_postpone_period ]) - end -end diff --git a/app/models/account.rb b/app/models/account.rb index 357d0cc29..6c42bcdfb 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -16,7 +16,7 @@ class Account < ApplicationRecord end end - # To use the account as a generic card container. See +Entropy::Configuration+. + # To use the account as a generic card container. See +Entropy+. def cards Card.all end diff --git a/app/models/account/entropic.rb b/app/models/account/entropic.rb index b82b1051b..de6e3a7e6 100644 --- a/app/models/account/entropic.rb +++ b/app/models/account/entropic.rb @@ -2,16 +2,16 @@ module Account::Entropic extend ActiveSupport::Concern included do - has_one :default_entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy + has_one :default_entropy, class_name: "Entropy", as: :container, dependent: :destroy - before_save :set_default_entropy_configuration + before_save :set_default_entropy end private DEFAULT_ENTROPY_PERIOD = 30.days - def set_default_entropy_configuration - self.default_entropy_configuration ||= build_default_entropy_configuration \ + def set_default_entropy + self.default_entropy ||= build_default_entropy \ auto_postpone_period: DEFAULT_ENTROPY_PERIOD end end diff --git a/app/models/card/entropic.rb b/app/models/card/entropic.rb index 46acd8da8..e938e232e 100644 --- a/app/models/card/entropic.rb +++ b/app/models/card/entropic.rb @@ -4,16 +4,16 @@ module Card::Entropic included do scope :due_to_be_postponed, -> do active - .left_outer_joins(collection: :entropy_configuration) - .where("last_active_at <= DATETIME('now', '-' || COALESCE(entropy_configurations.auto_postpone_period, ?) || ' seconds')", - Entropy::Configuration.default.auto_postpone_period) + .left_outer_joins(collection: :entropy) + .where("last_active_at <= DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", + Entropy.default.auto_postpone_period) end scope :postponing_soon, -> do active - .left_outer_joins(collection: :entropy_configuration) - .where("last_active_at > DATETIME('now', '-' || COALESCE(entropy_configurations.auto_postpone_period, ?) || ' seconds')", Entropy::Configuration.default.auto_postpone_period) - .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropy_configurations.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy::Configuration.default.auto_postpone_period) + .left_outer_joins(collection: :entropy) + .where("last_active_at > DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", Entropy.default.auto_postpone_period) + .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropies.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy.default.auto_postpone_period) end delegate :auto_postpone_period, to: :collection diff --git a/app/models/collection.rb b/app/models/collection.rb index 504439e2e..f1aef0fb7 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -8,7 +8,7 @@ class Collection < ApplicationRecord has_many :tags, -> { distinct }, through: :cards has_many :events has_many :webhooks, dependent: :destroy - has_one :entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy + has_one :entropy, as: :container, dependent: :destroy scope :alphabetically, -> { order("lower(name)") } scope :ordered_by_recently_accessed, -> { merge(Access.ordered_by_recently_accessed) } diff --git a/app/models/collection/entropic.rb b/app/models/collection/entropic.rb index a3879955b..dbfb6a981 100644 --- a/app/models/collection/entropic.rb +++ b/app/models/collection/entropic.rb @@ -2,15 +2,15 @@ module Collection::Entropic extend ActiveSupport::Concern included do - delegate :auto_postpone_period, to: :entropy_configuration + delegate :auto_postpone_period, to: :entropy end - def entropy_configuration - super || Entropy::Configuration.default + def entropy + super || Entropy.default end def auto_postpone_period=(new_value) - entropy_configuration ||= association(:entropy_configuration).reader || self.build_entropy_configuration - entropy_configuration.update auto_postpone_period: new_value + entropy ||= association(:entropy).reader || self.build_entropy + entropy.update auto_postpone_period: new_value end end diff --git a/app/models/entropy.rb b/app/models/entropy.rb index 42e730a09..613d1a315 100644 --- a/app/models/entropy.rb +++ b/app/models/entropy.rb @@ -1,5 +1,16 @@ -module Entropy - def self.table_name_prefix - "entropy_" +class Entropy < ApplicationRecord + belongs_to :container, polymorphic: true + + after_commit :touch_all_cards_later + + class << self + def default + Account.sole.default_entropy + end end + + private + def touch_all_cards_later + Card::TouchAllJob.perform_later(container) + end end diff --git a/app/models/entropy/configuration.rb b/app/models/entropy/configuration.rb deleted file mode 100644 index 1884cb112..000000000 --- a/app/models/entropy/configuration.rb +++ /dev/null @@ -1,16 +0,0 @@ -class Entropy::Configuration < ApplicationRecord - belongs_to :container, polymorphic: true - - after_commit :touch_all_cards_later - - class << self - def default - Account.sole.default_entropy_configuration - end - end - - private - def touch_all_cards_later - Card::TouchAllJob.perform_later(container) - end -end diff --git a/app/views/account/settings/_entropy_configuration.html.erb b/app/views/account/settings/_entropy.html.erb similarity index 89% rename from app/views/account/settings/_entropy_configuration.html.erb rename to app/views/account/settings/_entropy.html.erb index 9ecc64db6..316097898 100644 --- a/app/views/account/settings/_entropy_configuration.html.erb +++ b/app/views/account/settings/_entropy.html.erb @@ -4,5 +4,5 @@

BOXCAR doesn’t let stale cards stick around forever. Cards automatically close as “Not Now” without activity for specific period of time. This is the default, global setting — you can override it on each board.

- <%= render "entropy/auto_close", model: account.default_entropy_configuration, url: account_entropy_configuration_path %> + <%= render "entropy/auto_close", model: account.default_entropy, url: account_entropy_path %> diff --git a/app/views/account/settings/show.html.erb b/app/views/account/settings/show.html.erb index 53533bba4..7e2dbbfbf 100644 --- a/app/views/account/settings/show.html.erb +++ b/app/views/account/settings/show.html.erb @@ -10,5 +10,5 @@ <%= render "account/settings/users", users: @users %> - <%= render "account/settings/entropy_configuration", account: @account %> + <%= render "account/settings/entropy", account: @account %> diff --git a/app/views/collections/edit/_auto_close.html.erb b/app/views/collections/edit/_auto_close.html.erb index fba5f8771..4a86005a8 100644 --- a/app/views/collections/edit/_auto_close.html.erb +++ b/app/views/collections/edit/_auto_close.html.erb @@ -1,7 +1,7 @@ -<%= turbo_frame_tag @collection, :entropy_configuration do %> +<%= turbo_frame_tag @collection, :entropy do %>

Auto close

BOXCAR doesn’t let stale cards stick around forever. Cards automatically close as “Not now” if no one updates, comments, or moves a card for…

- <%= render "entropy/auto_close", model: collection, url: collection_entropy_configuration_path(collection) %> + <%= render "entropy/auto_close", model: collection, url: collection_entropy_path(collection) %>
<% end %> diff --git a/config/routes.rb b/config/routes.rb index 9b7e1ddb6..b8c481b53 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,7 +3,7 @@ Rails.application.routes.draw do post :enter, to: "entries#create" resource :join_code resource :settings - resource :entropy_configuration + resource :entropy end resources :users do @@ -16,7 +16,7 @@ Rails.application.routes.draw do resource :subscriptions resource :involvement resource :publication - resource :entropy_configuration + resource :entropy namespace :columns do resource :not_now diff --git a/db/migrate/20251102115338_rename_entropy_configurations_to_entropies.rb b/db/migrate/20251102115338_rename_entropy_configurations_to_entropies.rb new file mode 100644 index 000000000..2e1e5d1e8 --- /dev/null +++ b/db/migrate/20251102115338_rename_entropy_configurations_to_entropies.rb @@ -0,0 +1,5 @@ +class RenameEntropyConfigurationsToEntropies < ActiveRecord::Migration[8.2] + def change + rename_table :entropy_configurations, :entropies + end +end diff --git a/db/schema.rb b/db/schema.rb index 987f8eb9d..3a6bf3b4c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.2].define(version: 2025_10_29_161222) do +ActiveRecord::Schema[8.2].define(version: 2025_11_02_115338) do create_table "accesses", force: :cascade do |t| t.datetime "accessed_at" t.integer "collection_id", null: false @@ -220,13 +220,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_10_29_161222) do t.index ["filter_id"], name: "index_creators_filters_on_filter_id" end - create_table "entropy_configurations", force: :cascade do |t| + create_table "entropies", force: :cascade do |t| t.bigint "auto_postpone_period", default: 2592000, null: false t.integer "container_id", null: false t.string "container_type", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["container_type", "container_id", "auto_postpone_period"], name: "idx_on_container_type_container_id_auto_postpone_pe_47f82c5b73" + t.index ["container_type", "container_id", "auto_postpone_period"], name: "idx_on_container_type_container_id_auto_postpone_pe_3d79b50517" t.index ["container_type", "container_id"], name: "index_entropy_configurations_on_container", unique: true end diff --git a/db/schema_cache.yml b/db/schema_cache.yml index 704ed83a2..755b7cf07 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -606,7 +606,7 @@ columns: creators_filters: - *25 - *19 - entropy_configurations: + entropies: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: auto_postpone_period @@ -1254,7 +1254,7 @@ primary_keys: columns: id comments: id creators_filters: - entropy_configurations: id + entropies: id events: id filters: id filters_tags: @@ -1301,7 +1301,7 @@ data_sources: columns: true comments: true creators_filters: true - entropy_configurations: true + entropies: true events: true filters: true filters_tags: true @@ -2031,10 +2031,10 @@ indexes: nulls_not_distinct: comment: valid: true - entropy_configurations: + entropies: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - table: entropy_configurations - name: idx_on_container_type_container_id_auto_postpone_pe_47f82c5b73 + table: entropies + name: idx_on_container_type_container_id_auto_postpone_pe_3d79b50517 unique: false columns: - container_type @@ -2051,7 +2051,7 @@ indexes: comment: valid: true - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - table: entropy_configurations + table: entropies name: index_entropy_configurations_on_container unique: true columns: @@ -2879,4 +2879,4 @@ indexes: nulls_not_distinct: comment: valid: true -version: 20251029161222 +version: 20251102115338 diff --git a/test/controllers/accounts/entropies_controller_test.rb b/test/controllers/accounts/entropies_controller_test.rb new file mode 100644 index 000000000..9fda2c1ed --- /dev/null +++ b/test/controllers/accounts/entropies_controller_test.rb @@ -0,0 +1,15 @@ +require "test_helper" + +class Account::EntropiesControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + end + + test "update" do + put account_entropy_path, params: { entropy: { auto_postpone_period: 1.day } } + + assert_equal 1.day, entropies("37s_account").auto_postpone_period + + assert_redirected_to account_settings_path + end +end diff --git a/test/controllers/accounts/entropy_configurations_controller_test.rb b/test/controllers/accounts/entropy_configurations_controller_test.rb deleted file mode 100644 index 309e6712e..000000000 --- a/test/controllers/accounts/entropy_configurations_controller_test.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "test_helper" - -class Account::EntropyConfigurationsControllerTest < ActionDispatch::IntegrationTest - setup do - sign_in_as :kevin - end - - test "update" do - put account_entropy_configuration_path, params: { entropy_configuration: { auto_postpone_period: 1.day } } - - assert_equal 1.day, entropy_configurations("37s_account").auto_postpone_period - - assert_redirected_to account_settings_path - end -end diff --git a/test/controllers/collections/entropies_controller_test.rb b/test/controllers/collections/entropies_controller_test.rb new file mode 100644 index 000000000..118ba0b7c --- /dev/null +++ b/test/controllers/collections/entropies_controller_test.rb @@ -0,0 +1,16 @@ +require "test_helper" + +class Collections::EntropiesControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + @collection = collections(:writebook) + end + + test "update" do + put collection_entropy_path(@collection), params: { collection: { auto_postpone_period: 1.day } } + + assert_equal 1.day, @collection.entropy.reload.auto_postpone_period + + assert_turbo_stream action: :replace, target: dom_id(@collection, :entropy) + end +end diff --git a/test/controllers/collections/entropy_configurations_controller_test.rb b/test/controllers/collections/entropy_configurations_controller_test.rb deleted file mode 100644 index 3fc24fe8a..000000000 --- a/test/controllers/collections/entropy_configurations_controller_test.rb +++ /dev/null @@ -1,16 +0,0 @@ -require "test_helper" - -class Collections::EntropyConfigurationsControllerTest < ActionDispatch::IntegrationTest - setup do - sign_in_as :kevin - @collection = collections(:writebook) - end - - test "update" do - put collection_entropy_configuration_path(@collection), params: { collection: { auto_postpone_period: 1.day } } - - assert_equal 1.day, @collection.entropy_configuration.reload.auto_postpone_period - - assert_turbo_stream action: :replace, target: dom_id(@collection, :entropy_configuration) - end -end diff --git a/test/controllers/collections_controller_test.rb b/test/controllers/collections_controller_test.rb index 4193cbbdd..92bb2237c 100644 --- a/test/controllers/collections_controller_test.rb +++ b/test/controllers/collections_controller_test.rb @@ -44,7 +44,7 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to edit_collection_path(collections(:writebook)) assert_equal "Writebook bugs", collections(:writebook).reload.name assert_equal users(:kevin, :jz).sort, collections(:writebook).users.sort - assert_equal 1.day, entropy_configurations(:writebook_collection).auto_postpone_period + assert_equal 1.day, entropies(:writebook_collection).auto_postpone_period assert_not collections(:writebook).all_access? end diff --git a/test/fixtures/entropy/configurations.yml b/test/fixtures/entropies.yml similarity index 100% rename from test/fixtures/entropy/configurations.yml rename to test/fixtures/entropies.yml diff --git a/test/models/card/entropic_test.rb b/test/models/card/entropic_test.rb index 4953ee24e..0512b1c2b 100644 --- a/test/models/card/entropic_test.rb +++ b/test/models/card/entropic_test.rb @@ -8,8 +8,8 @@ class Card::EntropicTest < ActiveSupport::TestCase test "auto_postpone_at uses the period defined in the account by default" do freeze_time - entropy_configurations(:writebook_collection).destroy - entropy_configurations("37s_account").reload.update! auto_postpone_period: 456.days + entropies(:writebook_collection).destroy + entropies("37s_account").reload.update! auto_postpone_period: 456.days cards(:layout).update! last_active_at: 2.day.ago assert_equal (456 - 2).days.from_now, cards(:layout).entropy.auto_clean_at end @@ -17,16 +17,16 @@ class Card::EntropicTest < ActiveSupport::TestCase test "auto_postpone_at infers the period from the collection when present" do freeze_time - entropy_configurations(:writebook_collection).update! auto_postpone_period: 123.days + entropies(:writebook_collection).update! auto_postpone_period: 123.days cards(:layout).update! last_active_at: 2.day.ago assert_equal (123 - 2).days.from_now, cards(:layout).entropy.auto_clean_at end - test "auto postpone all due using the default account entropy configuration" do - entropy_configurations(:writebook_collection).destroy + test "auto postpone all due using the default account entropy" do + entropies(:writebook_collection).destroy - cards(:logo).update!(last_active_at: 1.day.ago - entropy_configurations("37s_account").auto_postpone_period) - cards(:shipping).update!(last_active_at: 1.day.from_now - entropy_configurations("37s_account").auto_postpone_period) + cards(:logo).update!(last_active_at: 1.day.ago - entropies("37s_account").auto_postpone_period) + cards(:shipping).update!(last_active_at: 1.day.from_now - entropies("37s_account").auto_postpone_period) assert_difference -> { Card.postponed.count }, +1 do Card.auto_postpone_all_due @@ -37,9 +37,9 @@ class Card::EntropicTest < ActiveSupport::TestCase assert_not cards(:shipping).reload.postponed? end - test "auto postpone all due using entropy configuration defined at the collection level" do - cards(:logo).update!(last_active_at: 1.day.ago - entropy_configurations(:writebook_collection).auto_postpone_period) - cards(:shipping).update!(last_active_at: 1.day.from_now - entropy_configurations(:writebook_collection).auto_postpone_period) + test "auto postpone all due using entropy defined at the collection level" do + cards(:logo).update!(last_active_at: 1.day.ago - entropies(:writebook_collection).auto_postpone_period) + cards(:shipping).update!(last_active_at: 1.day.from_now - entropies(:writebook_collection).auto_postpone_period) assert_difference -> { Card.postponed.count }, +1 do Card.auto_postpone_all_due @@ -52,8 +52,8 @@ class Card::EntropicTest < ActiveSupport::TestCase test "postponing soon scope" do cards(:logo, :shipping).each(&:published!) - cards(:logo).update!(last_active_at: entropy_configurations(:writebook_collection).auto_postpone_period.seconds.ago + 2.days) - cards(:shipping).update!(last_active_at: entropy_configurations(:writebook_collection).auto_postpone_period.seconds.ago - 2.days) + cards(:logo).update!(last_active_at: entropies(:writebook_collection).auto_postpone_period.seconds.ago + 2.days) + cards(:shipping).update!(last_active_at: entropies(:writebook_collection).auto_postpone_period.seconds.ago - 2.days) assert_includes Card.postponing_soon, cards(:logo) assert_not_includes Card.postponing_soon, cards(:shipping) diff --git a/test/models/card/stallable_test.rb b/test/models/card/stallable_test.rb index a76ff1d69..13ae49972 100644 --- a/test/models/card/stallable_test.rb +++ b/test/models/card/stallable_test.rb @@ -28,7 +28,7 @@ class Card::StallableTest < ActiveSupport::TestCase test "a card with an old activity spike is not stalled after being postponed" do card = cards(:logo) - card.update!(last_active_at: 1.day.ago - card.collection.entropy_configuration.auto_postpone_period) + card.update!(last_active_at: 1.day.ago - card.collection.entropy.auto_postpone_period) card.create_activity_spike!(updated_at: 3.months.ago) assert card.stalled? diff --git a/test/models/entropy/configuration_test.rb b/test/models/entropy/configuration_test.rb deleted file mode 100644 index 015e8b1de..000000000 --- a/test/models/entropy/configuration_test.rb +++ /dev/null @@ -1,20 +0,0 @@ -require "test_helper" - -class Entropy::ConfigurationTest < ActiveSupport::TestCase - test "touch cards when configuration changes for collection container" do - collection = collections(:writebook) - config = collection.entropy_configuration || collection.create_entropy_configuration!(auto_postpone_period: 30.days) - - assert_enqueued_with(job: Card::TouchAllJob, args: [ collection ]) do - config.update!(auto_postpone_period: 15.days) - end - end - - test "touch cards when configuration changes for account container" do - account = Account.sole - - assert_enqueued_with(job: Card::TouchAllJob, args: [ account ]) do - account.default_entropy_configuration.update!(auto_postpone_period: 45.days) - end - end -end diff --git a/test/models/entropy_test.rb b/test/models/entropy_test.rb new file mode 100644 index 000000000..08fbf8600 --- /dev/null +++ b/test/models/entropy_test.rb @@ -0,0 +1,20 @@ +require "test_helper" + +class Entropy::Test < ActiveSupport::TestCase + test "touch cards when entropy changes for collection container" do + collection = collections(:writebook) + config = collection.entropy || collection.create_entropy!(auto_postpone_period: 30.days) + + assert_enqueued_with(job: Card::TouchAllJob, args: [ collection ]) do + config.update!(auto_postpone_period: 15.days) + end + end + + test "touch cards when entropy changes for account container" do + account = Account.sole + + assert_enqueued_with(job: Card::TouchAllJob, args: [ account ]) do + account.default_entropy.update!(auto_postpone_period: 45.days) + end + end +end diff --git a/test/routes_test.rb b/test/routes_test.rb index d1663037b..1bdc1ae5c 100644 --- a/test/routes_test.rb +++ b/test/routes_test.rb @@ -9,7 +9,7 @@ class RouteTest < ActionDispatch::IntegrationTest assert_recognizes({ controller: "account/settings", action: "show" }, "/account/settings") end - test "account/entropy_configuration" do - assert_recognizes({ controller: "account/entropy_configurations", action: "show" }, "/account/entropy_configuration") + test "account/entropy" do + assert_recognizes({ controller: "account/entropies", action: "show" }, "/account/entropy") end end From fb395c8b079213450a5cc59aec65f2e7d3099d08 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 13:17:55 +0100 Subject: [PATCH 09/98] Keep all entropy definitions together in the concern --- app/models/collection.rb | 1 - app/models/collection/entropic.rb | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/collection.rb b/app/models/collection.rb index f1aef0fb7..d9ed3d3ec 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -8,7 +8,6 @@ class Collection < ApplicationRecord has_many :tags, -> { distinct }, through: :cards has_many :events has_many :webhooks, dependent: :destroy - has_one :entropy, as: :container, dependent: :destroy scope :alphabetically, -> { order("lower(name)") } scope :ordered_by_recently_accessed, -> { merge(Access.ordered_by_recently_accessed) } diff --git a/app/models/collection/entropic.rb b/app/models/collection/entropic.rb index dbfb6a981..851da2e37 100644 --- a/app/models/collection/entropic.rb +++ b/app/models/collection/entropic.rb @@ -3,6 +3,7 @@ module Collection::Entropic included do delegate :auto_postpone_period, to: :entropy + has_one :entropy, as: :container, dependent: :destroy end def entropy From 2c4159d11ae673d6ba5ca1a8baf3686316ef9809 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 13:29:07 +0100 Subject: [PATCH 10/98] Style --- app/models/collection.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/collection.rb b/app/models/collection.rb index 504439e2e..756d987cf 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -17,8 +17,6 @@ class Collection < ApplicationRecord private def ensure_default_collection - if Collection.count.zero? - Collection.create!(name: "Cards") - end + Collection.create!(name: "Cards") if Collection.none? end end From 7052d6ecec446ec9148f228f844e0b4db7ca2e24 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 13:54:46 +0100 Subject: [PATCH 11/98] Fix style --- app/controllers/collections/entropies_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/collections/entropies_controller.rb b/app/controllers/collections/entropies_controller.rb index bb0d2940a..d9ed904f8 100644 --- a/app/controllers/collections/entropies_controller.rb +++ b/app/controllers/collections/entropies_controller.rb @@ -5,7 +5,7 @@ class Collections::EntropiesController < ApplicationController @collection.entropy.update!(entropy_params) render turbo_stream: [ - turbo_stream.replace([ @collection, :entropy], partial: "collections/edit/auto_close", locals: { collection: @collection }), + turbo_stream.replace([ @collection, :entropy ], partial: "collections/edit/auto_close", locals: { collection: @collection }), turbo_stream_flash(notice: "Saved") ] end From b1c8dc1e64d26f6999eedc52c79ae4bc2155ed35 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 13:56:31 +0100 Subject: [PATCH 12/98] Need even more grace to ensure success --- test/system/smoke_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/system/smoke_test.rb b/test/system/smoke_test.rb index 8c80c165e..7e0b5e74b 100644 --- a/test/system/smoke_test.rb +++ b/test/system/smoke_test.rb @@ -61,8 +61,8 @@ class SmokeTest < ApplicationSystemTestCase def assert_image_figure_attachment(content_type: "image/png", caption:) assert_figure_attachment(content_type: content_type) do - assert_selector "img[src*='/rails/active_storage']", wait: 5 - assert_selector "figcaption input[placeholder='#{caption}']", wait: 5 + assert_selector "img[src*='/rails/active_storage']", wait: 10 + assert_selector "figcaption input[placeholder='#{caption}']", wait: 10 end end end From 966aa51a37cd32928bec988c5d1238285d7393cb Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 14:15:53 +0100 Subject: [PATCH 13/98] Get rid of unnecessary touch jobs touch_all on 10K cards took 6ms in testing --- app/jobs/card/touch_all_job.rb | 7 ------- app/models/collection/cards.rb | 7 +------ app/models/column.rb | 16 ++-------------- app/models/entropy.rb | 7 +------ test/models/collection/cards_test.rb | 4 ++-- test/models/column_test.rb | 8 ++++---- test/models/entropy_test.rb | 13 +++++-------- 7 files changed, 15 insertions(+), 47 deletions(-) delete mode 100644 app/jobs/card/touch_all_job.rb diff --git a/app/jobs/card/touch_all_job.rb b/app/jobs/card/touch_all_job.rb deleted file mode 100644 index 7b96fa5b7..000000000 --- a/app/jobs/card/touch_all_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Card::TouchAllJob < ApplicationJob - queue_as :backend - - def perform(container) - container.cards.in_batches(&:touch_all) - end -end diff --git a/app/models/collection/cards.rb b/app/models/collection/cards.rb index 17ed592aa..105f4912b 100644 --- a/app/models/collection/cards.rb +++ b/app/models/collection/cards.rb @@ -4,11 +4,6 @@ module Collection::Cards included do has_many :cards, dependent: :destroy - after_update_commit :touch_all_cards_later, if: :saved_change_to_name? + after_update_commit -> { cards.touch_all }, if: :saved_change_to_name? end - - private - def touch_all_cards_later - Card::TouchAllJob.perform_later(self) - end end diff --git a/app/models/column.rb b/app/models/column.rb index 0285ab185..d0c1ce102 100644 --- a/app/models/column.rb +++ b/app/models/column.rb @@ -8,23 +8,11 @@ class Column < ApplicationRecord validates :color, presence: true before_validation :set_default_color - after_save_commit :touch_all_cards_later, if: :should_invalidate_cards? - after_destroy_commit :touch_all_collection_cards + after_save_commit -> { cards.touch_all }, if: -> { saved_change_to_name? || saved_change_to_color? } + after_destroy_commit -> { collection.cards.touch_all } private def set_default_color self.color ||= Card::DEFAULT_COLOR end - - def touch_all_cards_later - Card::TouchAllJob.perform_later(self) - end - - def should_invalidate_cards? - saved_change_to_name? || saved_change_to_color? - end - - def touch_all_collection_cards - Card::TouchAllJob.perform_later(collection) - end end diff --git a/app/models/entropy.rb b/app/models/entropy.rb index 613d1a315..6940ed813 100644 --- a/app/models/entropy.rb +++ b/app/models/entropy.rb @@ -1,16 +1,11 @@ class Entropy < ApplicationRecord belongs_to :container, polymorphic: true - after_commit :touch_all_cards_later + after_commit -> { container.cards.touch_all } class << self def default Account.sole.default_entropy end end - - private - def touch_all_cards_later - Card::TouchAllJob.perform_later(container) - end end diff --git a/test/models/collection/cards_test.rb b/test/models/collection/cards_test.rb index f2ec9b9db..34bed8d8c 100644 --- a/test/models/collection/cards_test.rb +++ b/test/models/collection/cards_test.rb @@ -4,11 +4,11 @@ class Collection::CardsTest < ActiveSupport::TestCase test "touch cards when the name changes" do collection = collections(:writebook) - assert_enqueued_with(job: Card::TouchAllJob) do + assert_changes -> { collection.cards.first.updated_at } do collection.update!(name: "New Name") end - assert_no_enqueued_jobs(only: Card::TouchAllJob) do + assert_no_changes -> { collection.cards.first.updated_at } do collection.update!(updated_at: 1.hour.from_now) end end diff --git a/test/models/column_test.rb b/test/models/column_test.rb index d924de308..e4786abfb 100644 --- a/test/models/column_test.rb +++ b/test/models/column_test.rb @@ -10,15 +10,15 @@ class ColumnTest < ActiveSupport::TestCase test "touch all the cards when the name or color changes" do column = columns(:writebook_triage) - assert_enqueued_with(job: Card::TouchAllJob) do + assert_changes -> { column.cards.first.updated_at } do column.update!(name: "New Name") end - assert_enqueued_with(job: Card::TouchAllJob) do + assert_changes -> { column.cards.first.updated_at } do column.update!(color: "#FF0000") end - assert_no_enqueued_jobs(only: Card::TouchAllJob) do + assert_no_changes -> { column.cards.first.updated_at } do column.update!(updated_at: 1.hour.from_now) end end @@ -26,7 +26,7 @@ class ColumnTest < ActiveSupport::TestCase test "touch all collection cards when column is destroyed" do column = columns(:writebook_triage) - assert_enqueued_with(job: Card::TouchAllJob, args: [ column.collection ]) do + assert_changes -> { column.collection.cards.first.updated_at } do column.destroy end end diff --git a/test/models/entropy_test.rb b/test/models/entropy_test.rb index 08fbf8600..f5056cf76 100644 --- a/test/models/entropy_test.rb +++ b/test/models/entropy_test.rb @@ -1,20 +1,17 @@ require "test_helper" class Entropy::Test < ActiveSupport::TestCase - test "touch cards when entropy changes for collection container" do - collection = collections(:writebook) - config = collection.entropy || collection.create_entropy!(auto_postpone_period: 30.days) - - assert_enqueued_with(job: Card::TouchAllJob, args: [ collection ]) do - config.update!(auto_postpone_period: 15.days) + test "touch cards when entropy changes for collection" do + assert_changes -> { collections(:writebook).cards.first.updated_at } do + collections(:writebook).entropy.update!(auto_postpone_period: 15.days) end end test "touch cards when entropy changes for account container" do account = Account.sole - assert_enqueued_with(job: Card::TouchAllJob, args: [ account ]) do - account.default_entropy.update!(auto_postpone_period: 45.days) + assert_changes -> { account.cards.first.updated_at } do + collections(:writebook).entropy.update!(auto_postpone_period: 15.days) end end end From 0af74bfa11830a9b38d9582db37df690a4740fef Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 14:17:46 +0100 Subject: [PATCH 14/98] Style --- app/controllers/collections/columns_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/collections/columns_controller.rb b/app/controllers/collections/columns_controller.rb index b8f4d4de1..ca06d825f 100644 --- a/app/controllers/collections/columns_controller.rb +++ b/app/controllers/collections/columns_controller.rb @@ -1,7 +1,7 @@ class Collections::ColumnsController < ApplicationController include ActionView::RecordIdentifier, CollectionScoped - before_action :set_column, only: [ :show, :update, :destroy ] + before_action :set_column, only: %i[ show update destroy ] def show set_page_and_extract_portion_from @column.cards.active.latest.with_golden_first From d6c88736bd19b233b39ac38df5ae7df733ca92cf Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 14:19:26 +0100 Subject: [PATCH 15/98] No need for validation when we have rely on update!/create! on db constraints --- app/models/column.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/models/column.rb b/app/models/column.rb index d0c1ce102..10ab6f3df 100644 --- a/app/models/column.rb +++ b/app/models/column.rb @@ -4,9 +4,6 @@ class Column < ApplicationRecord belongs_to :collection, touch: true has_many :cards, dependent: :nullify - validates :name, presence: true - validates :color, presence: true - before_validation :set_default_color after_save_commit -> { cards.touch_all }, if: -> { saved_change_to_name? || saved_change_to_color? } after_destroy_commit -> { collection.cards.touch_all } From a1543f2660c7e62151b40048cb8e84d4128fb170 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 14:20:07 +0100 Subject: [PATCH 16/98] Inline anemic helper method with no additional explaining purpose --- app/models/column.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/models/column.rb b/app/models/column.rb index 10ab6f3df..87772367f 100644 --- a/app/models/column.rb +++ b/app/models/column.rb @@ -4,12 +4,7 @@ class Column < ApplicationRecord belongs_to :collection, touch: true has_many :cards, dependent: :nullify - before_validation :set_default_color + before_validation -> { self.color ||= Card::DEFAULT_COLOR } after_save_commit -> { cards.touch_all }, if: -> { saved_change_to_name? || saved_change_to_color? } after_destroy_commit -> { collection.cards.touch_all } - - private - def set_default_color - self.color ||= Card::DEFAULT_COLOR - end end From 2c9845eb62e08b288c4199a667054f9d9f22f21b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 15:24:55 +0100 Subject: [PATCH 17/98] No need to hide that the top-level entropy is tied to the Account --- app/controllers/account/entropies_controller.rb | 2 +- app/models/account/entropic.rb | 15 ++++----------- app/models/card/entropic.rb | 6 +++--- app/models/collection/entropic.rb | 2 +- app/models/entropy.rb | 6 ------ app/views/account/settings/_entropy.html.erb | 2 +- 6 files changed, 10 insertions(+), 23 deletions(-) diff --git a/app/controllers/account/entropies_controller.rb b/app/controllers/account/entropies_controller.rb index 75fa01e20..f45f3ef5f 100644 --- a/app/controllers/account/entropies_controller.rb +++ b/app/controllers/account/entropies_controller.rb @@ -1,6 +1,6 @@ class Account::EntropiesController < ApplicationController def update - Entropy.default.update!(entropy_params) + Account.sole.entropy.update!(entropy_params) redirect_to account_settings_path, notice: "Account updated" end diff --git a/app/models/account/entropic.rb b/app/models/account/entropic.rb index de6e3a7e6..e54af1432 100644 --- a/app/models/account/entropic.rb +++ b/app/models/account/entropic.rb @@ -1,17 +1,10 @@ module Account::Entropic extend ActiveSupport::Concern + DEFAULT_ENTROPY_PERIOD = 30.days + included do - has_one :default_entropy, class_name: "Entropy", as: :container, dependent: :destroy - - before_save :set_default_entropy + has_one :entropy, as: :container, dependent: :destroy + after_create -> { create_entropy!(auto_postpone_period: DEFAULT_ENTROPY_PERIOD) } end - - private - DEFAULT_ENTROPY_PERIOD = 30.days - - def set_default_entropy - self.default_entropy ||= build_default_entropy \ - auto_postpone_period: DEFAULT_ENTROPY_PERIOD - end end diff --git a/app/models/card/entropic.rb b/app/models/card/entropic.rb index e938e232e..68c0c6aa1 100644 --- a/app/models/card/entropic.rb +++ b/app/models/card/entropic.rb @@ -6,14 +6,14 @@ module Card::Entropic active .left_outer_joins(collection: :entropy) .where("last_active_at <= DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", - Entropy.default.auto_postpone_period) + Account.sole.entropy.auto_postpone_period) end scope :postponing_soon, -> do active .left_outer_joins(collection: :entropy) - .where("last_active_at > DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", Entropy.default.auto_postpone_period) - .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropies.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy.default.auto_postpone_period) + .where("last_active_at > DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", Account.sole.entropy.auto_postpone_period) + .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropies.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Account.sole.entropy.auto_postpone_period) end delegate :auto_postpone_period, to: :collection diff --git a/app/models/collection/entropic.rb b/app/models/collection/entropic.rb index 851da2e37..a84e7eb62 100644 --- a/app/models/collection/entropic.rb +++ b/app/models/collection/entropic.rb @@ -7,7 +7,7 @@ module Collection::Entropic end def entropy - super || Entropy.default + super || Account.sole.entropy end def auto_postpone_period=(new_value) diff --git a/app/models/entropy.rb b/app/models/entropy.rb index 6940ed813..6be6675ee 100644 --- a/app/models/entropy.rb +++ b/app/models/entropy.rb @@ -2,10 +2,4 @@ class Entropy < ApplicationRecord belongs_to :container, polymorphic: true after_commit -> { container.cards.touch_all } - - class << self - def default - Account.sole.default_entropy - end - end end diff --git a/app/views/account/settings/_entropy.html.erb b/app/views/account/settings/_entropy.html.erb index 316097898..fb3709e4d 100644 --- a/app/views/account/settings/_entropy.html.erb +++ b/app/views/account/settings/_entropy.html.erb @@ -4,5 +4,5 @@

BOXCAR doesn’t let stale cards stick around forever. Cards automatically close as “Not Now” without activity for specific period of time. This is the default, global setting — you can override it on each board.

- <%= render "entropy/auto_close", model: account.default_entropy, url: account_entropy_path %> + <%= render "entropy/auto_close", model: account.entropy, url: account_entropy_path %> From f09183bc9ac50b89200452953de0695ded1bf3a8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 15:46:05 +0100 Subject: [PATCH 18/98] Don't want to balloon txn, but also don't want to end up with a state with broken data --- app/models/collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/collection.rb b/app/models/collection.rb index ac9003ed1..1bfc4be2a 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -12,7 +12,7 @@ class Collection < ApplicationRecord scope :alphabetically, -> { order("lower(name)") } scope :ordered_by_recently_accessed, -> { merge(Access.ordered_by_recently_accessed) } - after_destroy_commit :ensure_default_collection + after_destroy :ensure_default_collection private def ensure_default_collection From c834cce504e31149e2266a30c01e1e9220933e3c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 15:58:33 +0100 Subject: [PATCH 19/98] Simplify join code creation and validation and test controller cc @monorkin --- .../account/join_codes_controller.rb | 7 ++--- app/models/account/join_code.rb | 6 +---- .../accounts/join_codes_controller_test.rb | 26 +++++++++++++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 test/controllers/accounts/join_codes_controller_test.rb diff --git a/app/controllers/account/join_codes_controller.rb b/app/controllers/account/join_codes_controller.rb index b67246b80..026c692f9 100644 --- a/app/controllers/account/join_codes_controller.rb +++ b/app/controllers/account/join_codes_controller.rb @@ -8,11 +8,8 @@ class Account::JoinCodesController < ApplicationController end def update - if @join_code.update(join_code_params) - redirect_to account_join_code_path - else - render :edit, status: :unprocessable_entity - end + @join_code.update!(join_code_params) + redirect_to account_join_code_path end def destroy diff --git a/app/models/account/join_code.rb b/app/models/account/join_code.rb index ddf4cf822..69d681197 100644 --- a/app/models/account/join_code.rb +++ b/app/models/account/join_code.rb @@ -3,11 +3,7 @@ class Account::JoinCode < ApplicationRecord scope :active, -> { where("usage_count < usage_limit") } - before_validation :generate_code, on: :create, if: -> { code.blank? } - - validates :code, presence: true, uniqueness: true - validates :usage_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0 } - validates :usage_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + before_create :generate_code, if: -> { code.blank? } def redeem transaction do diff --git a/test/controllers/accounts/join_codes_controller_test.rb b/test/controllers/accounts/join_codes_controller_test.rb new file mode 100644 index 000000000..438904d2a --- /dev/null +++ b/test/controllers/accounts/join_codes_controller_test.rb @@ -0,0 +1,26 @@ +require "test_helper" + +class Account::JoinCodesControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + end + + test "reset" do + get account_join_code_path + assert_response :success + + assert_changes -> { Account::JoinCode.sole.code } do + delete account_join_code_path + assert_redirected_to account_join_code_path + end + end + + test "update" do + get edit_account_join_code_path + assert_response :success + + put account_join_code_path, params: { account_join_code: { usage_limit: 5 } } + assert_equal 5, Account::JoinCode.sole.usage_limit + assert_redirected_to account_join_code_path + end +end From 02bd57089dabb49fd4efea5674c52418f31bc1db Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 16:04:44 +0100 Subject: [PATCH 20/98] Use before_action to pull up account assignment --- app/controllers/account/settings_controller.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/account/settings_controller.rb b/app/controllers/account/settings_controller.rb index c2f9672c9..42a6a60f7 100644 --- a/app/controllers/account/settings_controller.rb +++ b/app/controllers/account/settings_controller.rb @@ -1,17 +1,21 @@ class Account::SettingsController < ApplicationController before_action :ensure_admin, only: :update + before_action :set_account def show - @account = Account.sole @users = User.active.alphabetically end def update - Account.sole.update!(account_params) + @account.update!(account_params) redirect_to account_settings_path end private + def set_account + @account = Account.sole + end + def account_params params.expect account: %i[ name ] end From 76c87f6b67e059f83fe20e8400d36cad6829e002 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 16:04:51 +0100 Subject: [PATCH 21/98] Test updating account settings --- test/controllers/accounts/settings_controller_test.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/controllers/accounts/settings_controller_test.rb b/test/controllers/accounts/settings_controller_test.rb index d475ad40d..78b9f13ac 100644 --- a/test/controllers/accounts/settings_controller_test.rb +++ b/test/controllers/accounts/settings_controller_test.rb @@ -9,4 +9,10 @@ class Account::SettingsControllerTest < ActionDispatch::IntegrationTest get account_settings_path assert_response :success end + + test "update" do + put account_settings_path, params: { account: { name: "New Account Name" }} + assert_equal "New Account Name", Account.sole.name + assert_redirected_to account_settings_path + end end From 877e62dec5bc52774b1eb0b8268913073f39d210 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 16:30:49 +0100 Subject: [PATCH 22/98] No CR when there are only two statements --- app/controllers/sessions_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 25cb98408..10a3c9773 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -10,7 +10,6 @@ class SessionsController < ApplicationController def create Identity.find_by_email_address(email_address)&.send_magic_link - redirect_to session_magic_link_path end From 6a62df470cfcdacbe193716344a3b26bbb766fba Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 16:34:29 +0100 Subject: [PATCH 23/98] No longer used --- app/controllers/uploads_controller.rb | 29 --------------------------- config/routes.rb | 3 --- 2 files changed, 32 deletions(-) delete mode 100644 app/controllers/uploads_controller.rb diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb deleted file mode 100644 index 3a240af09..000000000 --- a/app/controllers/uploads_controller.rb +++ /dev/null @@ -1,29 +0,0 @@ -class UploadsController < ApplicationController - include ActiveStorage::SetCurrent - - before_action :set_file, only: :create - before_action :set_attachment, only: :show - - def create - # FIXME: Try to get upload attachments on root - @upload = Account.sole.uploads_attachments.create! blob: create_blob! - end - - def show - expires_in 5.minutes, public: true - redirect_to @attachment.url, allow_other_host: true - end - - private - def set_file - @file = params[:file] - end - - def set_attachment - @attachment = ActiveStorage::Attachment.find_by! slug: "#{params[:slug]}.#{params[:format]}" - end - - def create_blob! - ActiveStorage::Blob.create_and_upload! io: @file, filename: @file.original_filename, content_type: @file.content_type - end -end diff --git a/config/routes.rb b/config/routes.rb index b8c481b53..dc8df0d44 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -120,9 +120,6 @@ Rails.application.routes.draw do resources :days end - resources :uploads, only: :create - get "/u/*slug" => "uploads#show", as: :upload - resources :qr_codes get "join/:tenant/:code", to: "join_codes#new", as: :join From bced7405dfd587e94fef43841090f8255b372dd6 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 17:31:28 +0100 Subject: [PATCH 24/98] Extract events loading from overloaded UsersController --- app/controllers/users/events_controller.rb | 23 +++++++++++++++++++ app/controllers/users_controller.rb | 17 +------------- app/views/users/events/show.html.erb | 14 +++++++++++ app/views/users/show.html.erb | 19 +++++---------- config/routes.rb | 7 ++++-- .../users/events_controller_test.rb | 17 ++++++++++++++ 6 files changed, 66 insertions(+), 31 deletions(-) create mode 100644 app/controllers/users/events_controller.rb create mode 100644 app/views/users/events/show.html.erb create mode 100644 test/controllers/users/events_controller_test.rb diff --git a/app/controllers/users/events_controller.rb b/app/controllers/users/events_controller.rb new file mode 100644 index 000000000..76b85dd58 --- /dev/null +++ b/app/controllers/users/events_controller.rb @@ -0,0 +1,23 @@ +class Users::EventsController < ApplicationController + include FilterScoped + + before_action :set_user, :set_filter, :set_user_filtering + + def show + @filter = Current.user.filters.new(creator_ids: [ @user.id ]) + @day_timeline = Current.user.timeline_for(day_param, filter: @filter) + end + + private + def set_user + @user = User.active.find(params[:user_id]) + end + + def day_param + if params[:day].present? + Time.zone.parse(params[:day]) + else + Time.current + end + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 811261c87..8b89fc0e4 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,14 +1,9 @@ class UsersController < ApplicationController - include FilterScoped - require_access_without_a_user only: %i[ new create ] - before_action :set_join_code, only: %i[ new create] - before_action :ensure_join_code_is_valid, only: %i[ new create ] + before_action :set_join_code, :ensure_join_code_is_valid, only: %i[ new create ] before_action :set_user, only: %i[ show edit update destroy ] before_action :ensure_permission_to_change_user, only: %i[ update destroy ] - before_action :set_filter, only: %i[ edit show ] - before_action :set_user_filtering, only: %i[ edit show] def new end @@ -25,8 +20,6 @@ class UsersController < ApplicationController end def show - @filter = Current.user.filters.new(creator_ids: [ @user.id ]) - @day_timeline = Current.user.timeline_for(day_param, filter: @filter) end def update @@ -58,14 +51,6 @@ class UsersController < ApplicationController head :forbidden unless Current.user.can_change?(@user) end - def day_param - if params[:day].present? - Time.zone.parse(params[:day]) - else - Time.current - end - end - def user_params params.expect(user: [ :name, :avatar ]) end diff --git a/app/views/users/events/show.html.erb b/app/views/users/events/show.html.erb new file mode 100644 index 000000000..17ec2c1b3 --- /dev/null +++ b/app/views/users/events/show.html.erb @@ -0,0 +1,14 @@ + +

<%= "What #{Current.user == @user ? "have you" : "has #{@user.first_name}"} been up to?" %>

+ +
+ <%= day_timeline_pagination_frame_tag @day_timeline do %> + <%= render "events/day", day_timeline: @day_timeline %> + + <% if @day_timeline.next_day %> + <%= link_to "Load more…", user_events_path(@user, day: @day_timeline.next_day.strftime("%Y-%m-%d"), **@filter.as_params), + class: "day-timeline-pagination-link", data: { frame: day_timeline_pagination_frame_id_for(@day_timeline.next_day), pagination_target: "paginationLink" } %> + <% end %> + <% end %> +
+
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 9e49972f1..1ae06e2c0 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -1,8 +1,5 @@ <% @page_title = @user.name %> - -<% content_for :header do %> - <%= render "my/menus/menu" %> -<% end %> +<% me_or_you = Current.user == @user ? "me" : @user.first_name %>
@@ -30,8 +27,10 @@
- <%= link_to "Which cards are assigned to #{ Current.user == @user ? "me" : @user.first_name }?", cards_path(assignee_ids: [@user.id], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %> - <%= link_to "Which cards were added by #{ Current.user == @user ? "me" : @user.first_name }?", cards_path(creator_ids: [@user.id], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %> + <%= link_to "Which cards are assigned to #{me_or_you}?", + cards_path(assignee_ids: [ @user.id ], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %> + <%= link_to "Which cards were added by #{me_or_you}?", + cards_path(creator_ids: [ @user.id ], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %>
@@ -49,10 +48,4 @@ <% end %> - - - - -

<%= "What #{ Current.user == @user ? "have you" : "has #{ @user.first_name }" } been up to?" %>

- -<%= render "users/activity_timeline", user: @user, day_timeline: @day_timeline, filter: @filter %> +<%= turbo_frame_tag "user_events", src: user_events_path(@user) %> diff --git a/config/routes.rb b/config/routes.rb index dc8df0d44..15c162207 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,8 +7,11 @@ Rails.application.routes.draw do end resources :users do - resource :role, module: :users - resources :push_subscriptions, module: :users + scope module: :users do + resource :role + resource :events + resources :push_subscriptions + end end resources :collections do diff --git a/test/controllers/users/events_controller_test.rb b/test/controllers/users/events_controller_test.rb new file mode 100644 index 000000000..97cb85b2e --- /dev/null +++ b/test/controllers/users/events_controller_test.rb @@ -0,0 +1,17 @@ +require "test_helper" + +class Users::EventsControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + end + + test "show self" do + get user_events_path(users(:kevin)) + assert_in_body "What have you been up to?" + end + + test "show other" do + get user_events_path(users(:david)) + assert_in_body "What has David been up to?" + end +end From ada599f6a21f54a9988f60705905284aef748f92 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 17:31:35 +0100 Subject: [PATCH 25/98] Use latest Rails --- Gemfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index eb6ae5aa8..32494ed59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,7 +36,7 @@ GIT GIT remote: https://github.com/rails/rails.git - revision: def13087d55a6dbbfa80d114c847555927ce2ed7 + revision: d3d17c297a70d3e255a34693a1fb0074712e9930 branch: main specs: actioncable (8.2.0.alpha) @@ -267,7 +267,7 @@ GEM activesupport (>= 6.0.0) railties (>= 6.0.0) io-console (0.8.1) - irb (1.15.2) + irb (1.15.3) pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) @@ -345,7 +345,7 @@ GEM net-smtp (0.5.1) net-protocol net-ssh (7.3.0) - nio4r (2.7.4) + nio4r (2.7.5) nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) @@ -407,7 +407,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.3) + rack (3.2.4) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) @@ -427,7 +427,7 @@ GEM rake-compiler-dock (1.9.1) rb_sys (0.9.117) rake-compiler-dock (= 1.9.1) - rdoc (6.15.0) + rdoc (6.15.1) erb psych (>= 4.0.0) tsort @@ -543,7 +543,7 @@ GEM unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) - uri (1.0.4) + uri (1.1.0) useragent (0.16.11) vcr (6.3.1) base64 From 43069b1ea4b4c229a87793a4815e66a6042ee4c1 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 17:49:25 +0100 Subject: [PATCH 26/98] Extract Users::JoinsController from overloaded UsersController Good code smell is when the before_action callbacks stack up but can't be shared across actions --- app/controllers/concerns/authorization.rb | 2 +- app/controllers/users/joins_controller.rb | 32 +++++++++++++++++++ app/controllers/users_controller.rb | 24 -------------- app/views/users/{ => joins}/new.html.erb | 3 +- config/routes.rb | 4 +++ .../users/joins_controller_test.rb | 32 +++++++++++++++++++ test/controllers/users_controller_test.rb | 29 ----------------- 7 files changed, 71 insertions(+), 55 deletions(-) create mode 100644 app/controllers/users/joins_controller.rb rename app/views/users/{ => joins}/new.html.erb (84%) create mode 100644 test/controllers/users/joins_controller_test.rb diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb index df48f62f5..0e5e9240d 100644 --- a/app/controllers/concerns/authorization.rb +++ b/app/controllers/concerns/authorization.rb @@ -29,7 +29,7 @@ module Authorization if Current.membership.blank? redirect_to session_menu_url(script_name: nil) elsif Current.user.nil? && Current.membership.join_code.present? - redirect_to new_user_path + redirect_to new_users_join_path elsif !Current.user&.active? redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking)) end diff --git a/app/controllers/users/joins_controller.rb b/app/controllers/users/joins_controller.rb new file mode 100644 index 000000000..630c579bc --- /dev/null +++ b/app/controllers/users/joins_controller.rb @@ -0,0 +1,32 @@ +class Users::JoinsController < ApplicationController + require_access_without_a_user + + before_action :set_join_code, :ensure_join_code_is_valid + + def new + end + + def create + @join_code.redeem do + User.create!(user_params.merge(membership: Current.membership)) + end + + redirect_to root_path + end + + private + def set_join_code + @join_code = Account::JoinCode.active.find_by(code: Current.membership.join_code) + end + + def ensure_join_code_is_valid + unless @join_code&.active? + redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking)) + end + end + + def user_params + params.expect(user: [ :name, :avatar ]) + end +end + diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8b89fc0e4..2ba535fef 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,21 +1,7 @@ class UsersController < ApplicationController - require_access_without_a_user only: %i[ new create ] - - before_action :set_join_code, :ensure_join_code_is_valid, only: %i[ new create ] before_action :set_user, only: %i[ show edit update destroy ] before_action :ensure_permission_to_change_user, only: %i[ update destroy ] - def new - end - - def create - @join_code.redeem do - User.create!(user_params.merge(membership: Current.membership)) - end - - redirect_to root_path - end - def edit end @@ -33,16 +19,6 @@ class UsersController < ApplicationController end private - def set_join_code - @join_code = Account::JoinCode.active.find_by(code: Current.membership.join_code) - end - - def ensure_join_code_is_valid - unless @join_code&.active? - redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking)) - end - end - def set_user @user = User.active.find(params[:id]) end diff --git a/app/views/users/new.html.erb b/app/views/users/joins/new.html.erb similarity index 84% rename from app/views/users/new.html.erb rename to app/views/users/joins/new.html.erb index 9239786fe..adf795ece 100644 --- a/app/views/users/new.html.erb +++ b/app/views/users/joins/new.html.erb @@ -5,10 +5,11 @@

<%= @page_title %>

+

Enter your name to continue

- <%= form_with scope: "user", url: users_path(membership: params[:membership]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %> + <%= form_with scope: "user", url: users_joins_path(membership: params[:membership]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %>
- - <% if card.tags.any? || card.creating? %> + <% if card.tags.any? %>
<% card.tags.each_with_index do |tag, index| %> <%= link_to cards_path(collection_ids: [ card.collection ], tag_ids: [ tag.id ]), diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index 5c5549fa4..a300bafff 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -15,7 +15,7 @@
<%= render "cards/container", card: @card %> - <%= render "cards/messages", card: @card unless @card.creating? %> + <%= render "cards/messages", card: @card unless @card.drafted? %> <%= render "layouts/lightbox" do %> <%= button_to_remove_card_image(@card) if @card.image.attached? %> diff --git a/config/recurring.yml b/config/recurring.yml index 89b43c567..15067065b 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -8,9 +8,6 @@ production: &production auto_postpone_all_due: class: Card::AutoPostponeAllDueJob schedule: every hour at minute 50 - remove_abandoned_creations: - class: RemoveAbandonedCreationsJob - schedule: every hour at minute 59 delete_unused_tags: class: DeleteUnusedTagsJob schedule: every day at 04:02 diff --git a/config/routes.rb b/config/routes.rb index 043c48a03..c8e89fb66 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -71,7 +71,6 @@ Rails.application.routes.draw do resource :triage resource :publish resource :reading - resource :recover resource :watch resource :collection resource :column diff --git a/db/migrate/20251102095019_change_cards_status_default_to_drafted.rb b/db/migrate/20251102095019_change_cards_status_default_to_drafted.rb new file mode 100644 index 000000000..b6b960298 --- /dev/null +++ b/db/migrate/20251102095019_change_cards_status_default_to_drafted.rb @@ -0,0 +1,5 @@ +class ChangeCardsStatusDefaultToDrafted < ActiveRecord::Migration[8.2] + def change + change_column_default :cards, :status, from: "creating", to: "drafted" + end +end diff --git a/db/schema.rb b/db/schema.rb index 3a6bf3b4c..049e45a1a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -144,7 +144,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_02_115338) do t.integer "creator_id", null: false t.date "due_on" t.datetime "last_active_at", null: false - t.text "status", default: "creating", null: false + t.text "status", default: "drafted", null: false t.string "title" t.datetime "updated_at", null: false t.index ["collection_id"], name: "index_cards_on_collection_id" diff --git a/db/schema_cache.yml b/db/schema_cache.yml index 755b7cf07..e4c5ab87c 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -496,7 +496,7 @@ columns: cast_type: *15 sql_type_metadata: *16 'null': false - default: creating + default: drafted default_function: collation: comment: diff --git a/test/controllers/cards/publishes_controller_test.rb b/test/controllers/cards/publishes_controller_test.rb index bddb6e088..c2129d161 100644 --- a/test/controllers/cards/publishes_controller_test.rb +++ b/test/controllers/cards/publishes_controller_test.rb @@ -21,11 +21,12 @@ class Cards::PublishesControllerTest < ActionDispatch::IntegrationTest card.drafted! assert_changes -> { card.reload.published? }, from: false, to: true do - assert_difference -> { Card.creating.count }, +1 do + assert_difference -> { Card.count }, +1 do post card_publish_path(card, creation_type: "add_another") end end - assert_redirected_to Card.creating.last + assert Card.last.drafted? + assert_redirected_to Card.last end end diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index 4ee37cc2c..507aac5ed 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -15,11 +15,23 @@ class CardsControllerTest < ActionDispatch::IntegrationTest assert_response :success end - test "create" do + test "create a new draft" do assert_difference -> { Card.count }, 1 do post collection_cards_path(collections(:writebook)) end - assert_redirected_to card_path(Card.last) + + assert Card.last.drafted? + assert_redirected_to Card.last + end + + test "create resumes existing draft if it exists" do + draft = collections(:writebook).cards.create!(creator: users(:kevin), status: :drafted) + + assert_no_difference -> { Card.count } do + post collection_cards_path(collections(:writebook)) + end + + assert_redirected_to draft end test "show" do diff --git a/test/controllers/recovers_controller_test.rb b/test/controllers/recovers_controller_test.rb deleted file mode 100644 index 91fbf4cf2..000000000 --- a/test/controllers/recovers_controller_test.rb +++ /dev/null @@ -1,18 +0,0 @@ -require "test_helper" - -class Cards::RecoversControllerTest < ActionDispatch::IntegrationTest - setup do - sign_in_as :jz - end - - test "create" do - abandoned_card = collections(:writebook).cards.create! creator: users(:kevin) - abandoned_card.update!(title: "An edited title") - unsaved_card = collections(:writebook).cards.create! creator: users(:kevin) - - post card_recover_path(unsaved_card) - - assert_redirected_to abandoned_card - assert_equal [ abandoned_card ], Card.creating - end -end diff --git a/test/models/card/statuses_test.rb b/test/models/card/statuses_test.rb index 5a3e2cb8b..2ad983790 100644 --- a/test/models/card/statuses_test.rb +++ b/test/models/card/statuses_test.rb @@ -1,12 +1,10 @@ require "test_helper" class Card::StatusesTest < ActiveSupport::TestCase - test "cards start out in a `creating` state" do + test "cards start out in a `drafted` state" do card = collections(:writebook).cards.create! creator: users(:kevin), title: "Newly created card" - assert card.creating? - assert_not_includes Card.published_or_drafted_by(users(:kevin)), card - assert_not_includes Card.published_or_drafted_by(users(:jz)), card + assert card.drafted? end test "cards are only visible to the creator when drafted" do @@ -51,38 +49,4 @@ class Card::StatusesTest < ActiveSupport::TestCase assert_equal card, Event.last.eventable assert_equal "card_published", Event.last.action end - - test "can_recover_abandoned_creation?" do - card = collections(:writebook).cards.create! creator: users(:kevin) - unsaved_card = collections(:writebook).cards.new creator: users(:kevin) - - assert_not unsaved_card.can_recover_abandoned_creation? - - card.update!(title: "Something worth keeping") - assert unsaved_card.can_recover_abandoned_creation? - end - - test "recover_abandoned_creation" do - card_edited = collections(:writebook).cards.create! creator: users(:kevin) - card_edited.update!(title: "Something worth keeping") - - card_not_edited = collections(:writebook).cards.create! creator: users(:kevin) - - assert card_not_edited.can_recover_abandoned_creation? - - assert_equal card_edited, card_not_edited.recover_abandoned_creation - - assert_raises(ActiveRecord::RecordNotFound) { card_not_edited.reload } - end - - test "remove_abandoned_creations" do - card_old = collections(:writebook).cards.create! creator: users(:kevin), updated_at: 2.days.ago - card_recent = collections(:writebook).cards.create! creator: users(:kevin) - - assert_equal 2, Card.creating.count - - Card.remove_abandoned_creations - - assert_equal [ card_recent ], Card.creating - end end From 1df8d428114160a1900204579104c295d32ab990 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 2 Nov 2025 11:29:35 +0100 Subject: [PATCH 39/98] Create cards with CTRL/CMD enter --- app/views/cards/container/footer/_draft.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/cards/container/footer/_draft.html.erb b/app/views/cards/container/footer/_draft.html.erb index 1c39e6ad0..a2317ce08 100644 --- a/app/views/cards/container/footer/_draft.html.erb +++ b/app/views/cards/container/footer/_draft.html.erb @@ -1,5 +1,6 @@
- <%= button_to "Create card", card_publish_path(card), name: "creation_type", value: "add", class: "btn" %> + <%= button_to "Create card", card_publish_path(card), name: "creation_type", value: "add", class: "btn", + form: { data: { controller: "form", action: "keydown.ctrl+enter@document->form#submit:prevent keydown.meta+enter@document->form#submit:prevent" } } %> <%= button_to "Create and add another", card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another", title: "Create and add another (ctrl+shift+enter)", form: { data: { turbo: false } }, data: { controller: "hotkey", action: "keydown.ctrl+shift+enter@document->hotkey#click" } %> From 1d6cc37bb4dc50978cc9b840e031611184d60b9d Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 3 Nov 2025 10:57:35 +0100 Subject: [PATCH 40/98] Add missing blank content validation --- app/models/step.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/step.rb b/app/models/step.rb index c4e35f33e..c2063dfaa 100644 --- a/app/models/step.rb +++ b/app/models/step.rb @@ -3,6 +3,8 @@ class Step < ApplicationRecord scope :completed, -> { where(completed: true) } + validates :content, presence: true + def completed? completed end From da6d1011fd60cfeef3ff9c6b5c3377851e123b12 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 3 Nov 2025 10:57:58 +0100 Subject: [PATCH 41/98] Replace card replacement with stream actions to prevent flickering when managing steps --- app/controllers/cards/steps_controller.rb | 3 --- app/views/cards/display/perma/_steps.html.erb | 4 ++-- app/views/cards/steps/create.turbo_stream.erb | 3 +++ app/views/cards/steps/destroy.turbo_stream.erb | 1 + app/views/cards/steps/update.turbo_stream.erb | 3 +++ test/controllers/cards/steps_controller_test.rb | 10 +++++----- 6 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 app/views/cards/steps/create.turbo_stream.erb create mode 100644 app/views/cards/steps/destroy.turbo_stream.erb create mode 100644 app/views/cards/steps/update.turbo_stream.erb diff --git a/app/controllers/cards/steps_controller.rb b/app/controllers/cards/steps_controller.rb index 4f3434632..305890508 100644 --- a/app/controllers/cards/steps_controller.rb +++ b/app/controllers/cards/steps_controller.rb @@ -5,7 +5,6 @@ class Cards::StepsController < ApplicationController def create @step = @card.steps.create!(step_params) - render_card_replacement end def show @@ -16,12 +15,10 @@ class Cards::StepsController < ApplicationController def update @step.update!(step_params) - render_card_replacement end def destroy @step.destroy! - render_card_replacement end private diff --git a/app/views/cards/display/perma/_steps.html.erb b/app/views/cards/display/perma/_steps.html.erb index 5ce71f721..6cca251d2 100644 --- a/app/views/cards/display/perma/_steps.html.erb +++ b/app/views/cards/display/perma/_steps.html.erb @@ -1,9 +1,9 @@
    <%= render partial: "cards/steps/step", collection: card.steps, as: :step %> -
  1. +
  2. - <%= form_with model: [card, Step.new], url: card_steps_path(card), data: { controller: "form", action: "submit->form#preventEmptySubmit" } do |form| %> + <%= form_with model: [card, Step.new], url: card_steps_path(card), data: { controller: "form", action: "submit->form#preventEmptySubmit turbo:submit-end->form#reset" } do |form| %> <%= form.text_field :content, class: "input step__content hide-focus-ring", placeholder: "Add a step…", autocomplete: "off", data: { form_target: "input" }, aria: { label: "Add a step" } %> <% end %>
  3. diff --git a/app/views/cards/steps/create.turbo_stream.erb b/app/views/cards/steps/create.turbo_stream.erb new file mode 100644 index 000000000..399593809 --- /dev/null +++ b/app/views/cards/steps/create.turbo_stream.erb @@ -0,0 +1,3 @@ +<%= turbo_stream.before dom_id(@card, :new_step) do %> + <%= render "cards/steps/step", step: @step %> +<% end %> diff --git a/app/views/cards/steps/destroy.turbo_stream.erb b/app/views/cards/steps/destroy.turbo_stream.erb new file mode 100644 index 000000000..a5c5deefb --- /dev/null +++ b/app/views/cards/steps/destroy.turbo_stream.erb @@ -0,0 +1 @@ +<%= turbo_stream.remove @step %> diff --git a/app/views/cards/steps/update.turbo_stream.erb b/app/views/cards/steps/update.turbo_stream.erb new file mode 100644 index 000000000..24fd7f934 --- /dev/null +++ b/app/views/cards/steps/update.turbo_stream.erb @@ -0,0 +1,3 @@ +<%= turbo_stream.replace @step do %> + <%= render "cards/steps/step", step: @step %> +<% end %> diff --git a/test/controllers/cards/steps_controller_test.rb b/test/controllers/cards/steps_controller_test.rb index 677279a2a..72c24f071 100644 --- a/test/controllers/cards/steps_controller_test.rb +++ b/test/controllers/cards/steps_controller_test.rb @@ -10,7 +10,7 @@ class Cards::StepsControllerTest < ActionDispatch::IntegrationTest assert_difference -> { card.steps.count }, +1 do post card_steps_path(card), params: { step: { content: "Research alternatives" } }, as: :turbo_stream - assert_card_container_rerendered(card) + assert_turbo_stream action: :before, target: dom_id(card, :new_step) end assert_equal "Research alternatives", card.steps.last.content @@ -22,7 +22,7 @@ class Cards::StepsControllerTest < ActionDispatch::IntegrationTest assert_changes -> { step.reload.content }, from: "Original content", to: "Updated content" do put card_step_path(card, step), params: { step: { content: "Updated content" } }, as: :turbo_stream - assert_card_container_rerendered(card) + assert_turbo_stream action: :replace, target: dom_id(step) end end @@ -32,7 +32,7 @@ class Cards::StepsControllerTest < ActionDispatch::IntegrationTest assert_difference -> { card.steps.count }, -1 do delete card_step_path(card, step), as: :turbo_stream - assert_card_container_rerendered(card) + assert_turbo_stream action: :remove, target: dom_id(step) end end @@ -43,13 +43,13 @@ class Cards::StepsControllerTest < ActionDispatch::IntegrationTest # Toggle to completed assert_changes -> { step.reload.completed? }, from: false, to: true do put card_step_path(card, step), params: { step: { completed: "1" } }, as: :turbo_stream - assert_card_container_rerendered(card) + assert_turbo_stream action: :replace, target: dom_id(step) end # Toggle back to incomplete assert_changes -> { step.reload.completed? }, from: true, to: false do put card_step_path(card, step), params: { step: { completed: "0" } }, as: :turbo_stream - assert_card_container_rerendered(card) + assert_turbo_stream action: :replace, target: dom_id(step) end end end From 5f7feed6c4b98b26c83c426f74d985f6e4245cf1 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 3 Nov 2025 12:48:57 +0100 Subject: [PATCH 42/98] Refresh activity-related bits in the card perma dynamically: - Remove entropy bubble - Refresh the auto close notice https://app.box-car.com/5986089/cards/2683 --- app/models/card/eventable.rb | 5 +++++ .../card/display/_refresh_activity.turbo_stream.erb | 4 ++++ app/views/cards/container/_closure.html.erb | 13 ++++++++----- app/views/cards/display/preview/_bubble.html.erb | 5 ++++- app/views/cards/show.html.erb | 2 +- 5 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 app/views/card/display/_refresh_activity.turbo_stream.erb diff --git a/app/models/card/eventable.rb b/app/models/card/eventable.rb index 520fd95d4..c52fb7a29 100644 --- a/app/models/card/eventable.rb +++ b/app/models/card/eventable.rb @@ -19,6 +19,7 @@ module Card::Eventable def touch_last_active_at # Not using touch so that we can detect attribute change on callbacks update!(last_active_at: Time.current) + broadcast_activity end private @@ -35,4 +36,8 @@ module Card::Eventable def create_system_comment_for(event) SystemCommenter.new(self, event).comment end + + def broadcast_activity + broadcast_render_later_to self, :activity, partial: "card/display/refresh_activity", locals: { card: self } + end end diff --git a/app/views/card/display/_refresh_activity.turbo_stream.erb b/app/views/card/display/_refresh_activity.turbo_stream.erb new file mode 100644 index 000000000..a5a603252 --- /dev/null +++ b/app/views/card/display/_refresh_activity.turbo_stream.erb @@ -0,0 +1,4 @@ +<%= turbo_stream.remove dom_id(card, "bubble") %> +<%= turbo_stream.replace dom_id(card, :card_closure_toggle) do %> + <%= render "cards/container/closure", card: card %> +<% end %> diff --git a/app/views/cards/container/_closure.html.erb b/app/views/cards/container/_closure.html.erb index bad02f3cd..07d227c4f 100644 --- a/app/views/cards/container/_closure.html.erb +++ b/app/views/cards/container/_closure.html.erb @@ -10,10 +10,13 @@ <%= button_to card_closure_path(card), class: "btn borderless" do %> Mark as Done <% end %> - <% if card.entropic? && card.open? %> -
    - Closes as “Not Now” <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if there’s no activity. -
    - <% end %> + +
    + <% if card.entropic? && card.open? %> +
    + Closes as "Not Now" <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if there's no activity. +
    + <% end %> +
    <% end %>
diff --git a/app/views/cards/display/preview/_bubble.html.erb b/app/views/cards/display/preview/_bubble.html.erb index af2d71d04..c39772090 100644 --- a/app/views/cards/display/preview/_bubble.html.erb +++ b/app/views/cards/display/preview/_bubble.html.erb @@ -1,4 +1,7 @@ -<%= tag.div hidden: true, class: "bubble", +<%= tag.div \ + id: dom_id(card, "bubble"), + hidden: true, + class: "bubble", data: { controller: "bubble", action: "turbo:morph-element->bubble#update", diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index a300bafff..9ce4cf342 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -11,7 +11,7 @@
<% end %> -<%= turbo_stream_from @card %> +<%= turbo_stream_from @card, :activity %>
<%= render "cards/container", card: @card %> From 37ce2c98d10d573c2ba1cceb27738a4375ab7600 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 3 Nov 2025 13:04:21 +0100 Subject: [PATCH 43/98] Refresh the cards perma dynamically with page refreshes --- app/models/card.rb | 6 +++--- app/models/card/broadcastable.rb | 7 +++++++ app/views/cards/display/common/_assignees.html.erb | 2 +- app/views/cards/display/perma/_assignees.html.erb | 2 +- app/views/cards/display/perma/_collection.html.erb | 4 ++-- app/views/cards/display/perma/_tags.html.erb | 4 ++-- app/views/cards/show.html.erb | 1 + app/views/cards/triage/_columns.html.erb | 2 +- 8 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 app/models/card/broadcastable.rb diff --git a/app/models/card.rb b/app/models/card.rb index 3b455a424..cdc32824d 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,7 +1,7 @@ class Card < ApplicationRecord - include Assignable, Attachments, Closeable, Colored, Entropic, Eventable, - Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, Readable, - Searchable, Stallable, Statuses, Taggable, Triageable, Watchable + include Assignable, Attachments, Broadcastable, Closeable, Colored, Entropic, + Eventable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, + Readable, Searchable, Stallable, Statuses, Taggable, Triageable, Watchable belongs_to :collection, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } diff --git a/app/models/card/broadcastable.rb b/app/models/card/broadcastable.rb new file mode 100644 index 000000000..aa24b776c --- /dev/null +++ b/app/models/card/broadcastable.rb @@ -0,0 +1,7 @@ +module Card::Broadcastable + extend ActiveSupport::Concern + + included do + broadcasts_refreshes + end +end diff --git a/app/views/cards/display/common/_assignees.html.erb b/app/views/cards/display/common/_assignees.html.erb index 19aebd6a9..c07ec9e6d 100644 --- a/app/views/cards/display/common/_assignees.html.erb +++ b/app/views/cards/display/common/_assignees.html.erb @@ -9,7 +9,7 @@ Assign - + <%= yield %>
diff --git a/app/views/cards/display/perma/_assignees.html.erb b/app/views/cards/display/perma/_assignees.html.erb index 1c530263e..a115f39b2 100644 --- a/app/views/cards/display/perma/_assignees.html.erb +++ b/app/views/cards/display/perma/_assignees.html.erb @@ -1,3 +1,3 @@ <%= render "cards/display/common/assignees", card: card do %> - <%= turbo_frame_tag card, :assignment, src: new_card_assignment_path(card) %> + <%= turbo_frame_tag card, :assignment, src: new_card_assignment_path(card), refresh: "morph" %> <% end %> diff --git a/app/views/cards/display/perma/_collection.html.erb b/app/views/cards/display/perma/_collection.html.erb index a9e0cbbfa..d660f252d 100644 --- a/app/views/cards/display/perma/_collection.html.erb +++ b/app/views/cards/display/perma/_collection.html.erb @@ -6,8 +6,8 @@ Choose a collection for this card - - <%= turbo_frame_tag "collection_picker", src: edit_card_collection_path(card), target: "_top" %> + + <%= turbo_frame_tag "collection_picker", src: edit_card_collection_path(card), target: "_top", refresh: "morph" %>
<% end %> diff --git a/app/views/cards/display/perma/_tags.html.erb b/app/views/cards/display/perma/_tags.html.erb index 91c112540..e72041211 100644 --- a/app/views/cards/display/perma/_tags.html.erb +++ b/app/views/cards/display/perma/_tags.html.erb @@ -5,8 +5,8 @@ Add a tag - - <%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card) %> + + <%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card), refresh: "morph" %> diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index 9ce4cf342..c33a2c40b 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -11,6 +11,7 @@ <% end %> +<%= turbo_stream_from @card %> <%= turbo_stream_from @card, :activity %>
diff --git a/app/views/cards/triage/_columns.html.erb b/app/views/cards/triage/_columns.html.erb index 2ceaab98b..591a8e256 100644 --- a/app/views/cards/triage/_columns.html.erb +++ b/app/views/cards/triage/_columns.html.erb @@ -1 +1 @@ -<%= turbo_frame_tag dom_id(card, :columns), src: edit_card_column_path(card), target: "_top" %> +<%= turbo_frame_tag dom_id(card, :columns), src: edit_card_column_path(card), target: "_top", refresh: "morph" %> From 38a7a1446603e935560beba1bc0ce5cadc05b48a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 13:40:02 +0100 Subject: [PATCH 44/98] Remove vector extension that we are no longer using --- Gemfile | 3 --- Gemfile.lock | 5 ----- config/database.yml | 4 ---- 3 files changed, 12 deletions(-) diff --git a/Gemfile b/Gemfile index 533a87e9b..30f8a6483 100644 --- a/Gemfile +++ b/Gemfile @@ -55,9 +55,6 @@ gem "prometheus-client-mmap", "~> 1.1" gem "autotuner" gem "benchmark" # indirect dependency, being removed from Ruby 3.5 stdlib so here to quash warnings -# AI -gem "sqlite-vec", "0.1.7.alpha.2" - group :development, :test do gem "debug" gem "bundler-audit", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 32494ed59..09e284b0b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -508,10 +508,6 @@ GEM fugit (~> 1.11) railties (>= 7.1) thor (>= 1.3.1) - sqlite-vec (0.1.7.alpha.2-arm64-darwin) - sqlite-vec (0.1.7.alpha.2-arm64-linux) - sqlite-vec (0.1.7.alpha.2-x86_64-darwin) - sqlite-vec (0.1.7.alpha.2-x86_64-linux) sqlite3 (2.7.4) mini_portile2 (~> 2.8.0) sqlite3 (2.7.4-arm64-darwin) @@ -646,7 +642,6 @@ DEPENDENCIES solid_cable (>= 3.0) solid_cache (~> 1.0) solid_queue (~> 1.2) - sqlite-vec (= 0.1.7.alpha.2) sqlite3 (>= 2.0) stimulus-rails thruster diff --git a/config/database.yml b/config/database.yml index b4b295cce..de3b50a9b 100644 --- a/config/database.yml +++ b/config/database.yml @@ -8,8 +8,6 @@ default: &default adapter: sqlite3 pool: 50 timeout: 5000 - extensions: - - <%= SqliteVec.loadable_path %> development: primary: @@ -54,8 +52,6 @@ production: &production adapter: beamer database: storage/tenants/<%= Rails.env %>/%{tenant}/db/main.sqlite3 tenanted: true - extensions: - - <%= SqliteVec.loadable_path %> untenanted: <<: *default adapter: beamer From 225a70fc527a7b2de9895b1cdc9afddaded7b325 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 13:40:20 +0100 Subject: [PATCH 45/98] Remove here too --- config/application.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/application.rb b/config/application.rb index c39007fb2..6fe42e9ef 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,8 +2,6 @@ require_relative "boot" require "rails/all" -require "sqlite_vec" # Referenced in database.yml - # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) From 0a9e1620a0fd0bf916e7165a50c80ea2b8017175 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 13:46:22 +0100 Subject: [PATCH 46/98] No longer needed --- app/controllers/collections/columns_controller.rb | 2 +- app/controllers/public/collections/columns_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/collections/columns_controller.rb b/app/controllers/collections/columns_controller.rb index 461b5bd1a..a6c9103c5 100644 --- a/app/controllers/collections/columns_controller.rb +++ b/app/controllers/collections/columns_controller.rb @@ -1,5 +1,5 @@ class Collections::ColumnsController < ApplicationController - include ActionView::RecordIdentifier, CollectionScoped + include CollectionScoped before_action :set_column, only: %i[ show update destroy ] diff --git a/app/controllers/public/collections/columns_controller.rb b/app/controllers/public/collections/columns_controller.rb index dcc2b46a3..6d882bc16 100644 --- a/app/controllers/public/collections/columns_controller.rb +++ b/app/controllers/public/collections/columns_controller.rb @@ -1,5 +1,5 @@ class Public::Collections::ColumnsController < ApplicationController - include ActionView::RecordIdentifier, CachedPublicly, PublicCollectionScoped + include CachedPublicly, PublicCollectionScoped allow_unauthenticated_access only: :show allow_unauthorized_access only: :show From aeb23b691c2a2d3e79a66961db739e5c5f4105d8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 13:46:34 +0100 Subject: [PATCH 47/98] Always use templates for turbo streaming to stay consistentn --- app/controllers/columns/cards/drops/columns_controller.rb | 8 +++----- .../columns/cards/drops/columns/create.turbo_stream.erb | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 app/views/columns/cards/drops/columns/create.turbo_stream.erb diff --git a/app/controllers/columns/cards/drops/columns_controller.rb b/app/controllers/columns/cards/drops/columns_controller.rb index cb2c3b4a8..3de47992e 100644 --- a/app/controllers/columns/cards/drops/columns_controller.rb +++ b/app/controllers/columns/cards/drops/columns_controller.rb @@ -1,10 +1,8 @@ class Columns::Cards::Drops::ColumnsController < ApplicationController - include ActionView::RecordIdentifier, CardScoped + include CardScoped def create - column = @card.collection.columns.find(params[:column_id]) - @card.triage_into(column) - - render turbo_stream: turbo_stream.replace(dom_id(column), partial: "collections/show/column", method: :morph, locals: { column: column }) + @column = @card.collection.columns.find(params[:column_id]) + @card.triage_into(@column) end end diff --git a/app/views/columns/cards/drops/columns/create.turbo_stream.erb b/app/views/columns/cards/drops/columns/create.turbo_stream.erb new file mode 100644 index 000000000..5ab9915a0 --- /dev/null +++ b/app/views/columns/cards/drops/columns/create.turbo_stream.erb @@ -0,0 +1 @@ +<%= turbo_stream.replace(dom_id(@column), partial: "collections/show/column", method: :morph, locals: { column: @column }) %> From 2f1ee30a2b4cd1fb6a91004483c8d14139bab96f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:20:57 +0100 Subject: [PATCH 48/98] Remove duplicated nesting --- config/routes.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index c8e89fb66..bff2cfcfd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,7 @@ Rails.application.routes.draw do resources :users do scope module: :users do + resource :avatar resource :role resource :events resources :push_subscriptions @@ -139,12 +140,6 @@ Rails.application.routes.draw do end end - resources :users do - scope module: :users do - resource :avatar - end - end - resources :commands resource :conversation do From 3ec9308345a7f901e9edac7b9fa915727a238a0c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:21:06 +0100 Subject: [PATCH 49/98] Always bang for exceptions --- app/controllers/users/roles_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/users/roles_controller.rb b/app/controllers/users/roles_controller.rb index 8cffa232b..f9eb7d232 100644 --- a/app/controllers/users/roles_controller.rb +++ b/app/controllers/users/roles_controller.rb @@ -3,7 +3,7 @@ class Users::RolesController < ApplicationController before_action :ensure_permission_to_administer_user def update - @user.update(role_params) + @user.update!(role_params) redirect_to users_path end From 4c2f3ef0b84e474e320207342ac0d974528c34b5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:21:25 +0100 Subject: [PATCH 50/98] Style --- app/controllers/users/avatars_controller.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/controllers/users/avatars_controller.rb b/app/controllers/users/avatars_controller.rb index 180a7c506..5e29250c0 100644 --- a/app/controllers/users/avatars_controller.rb +++ b/app/controllers/users/avatars_controller.rb @@ -6,25 +6,17 @@ class Users::AvatarsController < ApplicationController before_action :set_user def show - if stale? @user, cache_control: { max_age: cache_max_age, stale_while_revalidate: 1.week } + if stale? @user, cache_control: { max_age: (30.minutes unless Current.user == @user), stale_while_revalidate: 1.week }.compact render_avatar_or_initials end end def destroy @user.avatar.destroy - redirect_to user_path(@user) + redirect_to @user end private - def cache_max_age - if Current.user == @user - 0 - else - 30.minutes - end - end - def set_user @user = User.find(params[:user_id]) end From b05e7c04e16104bc6779bf36e2e7c29692cc058f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:21:48 +0100 Subject: [PATCH 51/98] Ensure you can only delete your own avatar or else you have to be admin --- app/controllers/users/avatars_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/users/avatars_controller.rb b/app/controllers/users/avatars_controller.rb index 5e29250c0..8be049cc3 100644 --- a/app/controllers/users/avatars_controller.rb +++ b/app/controllers/users/avatars_controller.rb @@ -4,6 +4,7 @@ class Users::AvatarsController < ApplicationController allow_unauthenticated_access only: :show before_action :set_user + before_action :ensure_permission_to_administer_user, only: :destroy def show if stale? @user, cache_control: { max_age: (30.minutes unless Current.user == @user), stale_while_revalidate: 1.week }.compact @@ -21,6 +22,10 @@ class Users::AvatarsController < ApplicationController @user = User.find(params[:user_id]) end + def ensure_permission_to_administer_user + head :forbidden unless Current.user.admin? || Current.user == @user + end + def render_avatar_or_initials if @user.avatar.attached? send_blob_stream @user.avatar From c25fcc087a876aab1c0518510dca528791c80314 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:21:53 +0100 Subject: [PATCH 52/98] Test avatars controller --- .../users/avatars_controller_test.rb | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/controllers/users/avatars_controller_test.rb diff --git a/test/controllers/users/avatars_controller_test.rb b/test/controllers/users/avatars_controller_test.rb new file mode 100644 index 000000000..4fa45c2c9 --- /dev/null +++ b/test/controllers/users/avatars_controller_test.rb @@ -0,0 +1,29 @@ +require "test_helper" + +class Users::AvatarsControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :david + end + + test "show self without caching" do + get user_avatar_path(users(:david)) + assert_match "image/svg+xml", @response.content_type + assert_nil @response.cache_control[:max_age] + end + + test "show other with caching" do + get user_avatar_path(users(:kevin)) + assert_match "image/svg+xml", @response.content_type + assert_equal 30.minutes.to_s, @response.cache_control[:max_age] + end + + test "delete self" do + delete user_avatar_path(users(:david)) + assert_redirected_to users(:david) + end + + test "unable to delete other" do + delete user_avatar_path(users(:kevin)) + assert_response :forbidden + end +end From f9ff13cdc4181413984915641041abe471e0e1f5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:25:32 +0100 Subject: [PATCH 53/98] Add http caching for user events --- app/controllers/users/events_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/users/events_controller.rb b/app/controllers/users/events_controller.rb index 76b85dd58..2f1e3afb0 100644 --- a/app/controllers/users/events_controller.rb +++ b/app/controllers/users/events_controller.rb @@ -6,6 +6,8 @@ class Users::EventsController < ApplicationController def show @filter = Current.user.filters.new(creator_ids: [ @user.id ]) @day_timeline = Current.user.timeline_for(day_param, filter: @filter) + + fresh_when @day_timeline end private From 992b149395ee96f51639b09dd5b79e170fc8ac3c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:29:01 +0100 Subject: [PATCH 54/98] No longer used --- app/models/user/filtering.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/user/filtering.rb b/app/models/user/filtering.rb index c9f049ab3..fdc7f2bcb 100644 --- a/app/models/user/filtering.rb +++ b/app/models/user/filtering.rb @@ -1,6 +1,4 @@ class User::Filtering - include Rails.application.routes.url_helpers - attr_reader :user, :filter, :expanded delegate :as_params, :single_collection, to: :filter From 61d05ee073cabcb029fa93a64be09ca7a2ecb82f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:31:16 +0100 Subject: [PATCH 55/98] No longer used --- app/helpers/ai_helper.rb | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 app/helpers/ai_helper.rb diff --git a/app/helpers/ai_helper.rb b/app/helpers/ai_helper.rb deleted file mode 100644 index 171dcda01..000000000 --- a/app/helpers/ai_helper.rb +++ /dev/null @@ -1,20 +0,0 @@ -require "zlib" - -module AiHelper - LLM_MODELS = %w[ - gpt-5-chat-latest - gpt-5-mini - gpt-5-nano - gpt-5 - chatgpt-4o-latest - gpt-4.1 - gpt-3.5-turbo - gpt-4.1-mini - gpt-4.1-nano - gpt-4o-mini - ] - - def llm_model_options - LLM_MODELS.map { |model| [ model, model ] } - end -end From c007176ed046aaf59459c5f26bc9035cf2254769 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 3 Nov 2025 14:35:06 +0100 Subject: [PATCH 56/98] Don't broadcast card changes when touching activity spikes We do this from jobs, so there is no request id to discard the page refresh in the client. We don't want page refreshes in these cases, as those are normally handled via the change that originated this detection, in any case. --- app/models/card/activity_spike/detector.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/models/card/activity_spike/detector.rb b/app/models/card/activity_spike/detector.rb index 6721b946f..63f246ca0 100644 --- a/app/models/card/activity_spike/detector.rb +++ b/app/models/card/activity_spike/detector.rb @@ -20,10 +20,12 @@ class Card::ActivitySpike::Detector end def register_activity_spike - if card.activity_spike - card.activity_spike.touch - else - card.create_activity_spike! + Card.suppressing_turbo_broadcasts do + if card.activity_spike + card.activity_spike.touch + else + card.create_activity_spike! + end end end From 1277ec53475684447fdd4974ea8132933e326091 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:34:08 +0100 Subject: [PATCH 57/98] Extract jump_field_tag helper --- app/helpers/my/menu_helper.rb | 19 +++++++++++++++++++ app/views/my/menus/show.html.erb | 17 ++--------------- 2 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 app/helpers/my/menu_helper.rb diff --git a/app/helpers/my/menu_helper.rb b/app/helpers/my/menu_helper.rb new file mode 100644 index 000000000..a972a3408 --- /dev/null +++ b/app/helpers/my/menu_helper.rb @@ -0,0 +1,19 @@ +module My::MenuHelper + def jump_field_tag + text_field_tag :search, nil, + type: "search", + role: "combobox", + placeholder: "Type to jump to a collection, person, place, or tag…", + class: "input input--transparent txt-small", + autofocus: true, + autocorrect: "off", + autocomplete: "off", + aria: { activedescendant: "" }, + data: { + "1p-ignore": "true", + filter_target: "input", + nav_section_expander_target: "input", + navigable_list_target: "input", + action: "input->filter#filter" } + end +end diff --git a/app/views/my/menus/show.html.erb b/app/views/my/menus/show.html.erb index 077139170..4490d1088 100644 --- a/app/views/my/menus/show.html.erb +++ b/app/views/my/menus/show.html.erb @@ -1,20 +1,7 @@ <%= turbo_frame_tag "my_menu", target: "_top" do %>
- <%= text_field_tag :search, nil, - type: "search", - role: "combobox", - placeholder: "Type to jump to a collection, person, place, or tag…", - class: "input input--transparent txt-small", - autofocus: true, - autocorrect: "off", - autocomplete: "off", - aria: { activedescendant: "" }, - data: { - "1p-ignore": "true", - filter_target: "input", - nav_section_expander_target: "input", - navigable_list_target: "input", - action: "input->filter#filter" } %> + <%= jump_field_tag %> + +
> + - - <%= turbo_frame_tag "collection_picker", src: edit_card_collection_path(card), target: "_top", refresh: "morph" %> - -
- <% end %> + + <%= turbo_frame_tag "collection_picker", src: edit_card_collection_path(card), target: "_top", refresh: "morph" %> + +
<% end %> From d3cb3a3a2da55349d4e24b6fba0de287993647ed Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 17:17:44 +0100 Subject: [PATCH 86/98] Use etags for my pins --- app/controllers/my/pins_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/my/pins_controller.rb b/app/controllers/my/pins_controller.rb index f361b6af7..27e14535b 100644 --- a/app/controllers/my/pins_controller.rb +++ b/app/controllers/my/pins_controller.rb @@ -1,5 +1,6 @@ class My::PinsController < ApplicationController def index @pins = Current.user.pins.includes(:card).ordered.limit(20) + fresh_when @pins end end From 6570f5fc04ebf60d3b18e69c1e10034ef64755f3 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 17:22:38 +0100 Subject: [PATCH 87/98] Use etag for assignments/new --- app/controllers/cards/assignments_controller.rb | 2 ++ app/views/cards/assignments/new.html.erb | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/cards/assignments_controller.rb b/app/controllers/cards/assignments_controller.rb index 9516ebc2f..80b6c8149 100644 --- a/app/controllers/cards/assignments_controller.rb +++ b/app/controllers/cards/assignments_controller.rb @@ -2,6 +2,8 @@ class Cards::AssignmentsController < ApplicationController include CardScoped def new + @users = @collection.users.active.alphabetically + fresh_when @users end def create diff --git a/app/views/cards/assignments/new.html.erb b/app/views/cards/assignments/new.html.erb index ee7f8a74c..8642e2c0a 100644 --- a/app/views/cards/assignments/new.html.erb +++ b/app/views/cards/assignments/new.html.erb @@ -7,13 +7,13 @@ navigable_list_actionable_items_value: true } do %> Assign this to… - <% if @collection.users.active.count > 1 %> + <% if @users.many? %> <%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small margin-block-half", autofocus: true, type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %> <% end %>
<% end %> From 886e38547e347fa4907cf878ce7765a245fef1d4 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 11:50:36 -0600 Subject: [PATCH 93/98] This shouldn't be visible when the card is already postponed Fixes: https://app.box-car.com/5986089/cards/128 --- app/views/cards/container/_closure.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/cards/container/_closure.html.erb b/app/views/cards/container/_closure.html.erb index 07d227c4f..c40da47b9 100644 --- a/app/views/cards/container/_closure.html.erb +++ b/app/views/cards/container/_closure.html.erb @@ -12,9 +12,9 @@ <% end %>
- <% if card.entropic? && card.open? %> + <% if card.entropic? && card.open? && !card.postponed? %>
- Closes as "Not Now" <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if there's no activity. + Closes as “Not Now” <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if there’s no activity.
<% end %>
From 8538c21d4ec667a1bfc835dd1d816bec8aa6a561 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 3 Nov 2025 13:39:55 -0500 Subject: [PATCH 94/98] Copy Cloudflare's True-Client-IP header to X-Forwarded-For ref: https://3.basecamp.com/2914079/buckets/37331921/todos/9243121525 --- config/initializers/true_client_ip.rb | 24 +++++++++++++++++++ test/lib/true_client_ip_test.rb | 34 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 config/initializers/true_client_ip.rb create mode 100644 test/lib/true_client_ip_test.rb diff --git a/config/initializers/true_client_ip.rb b/config/initializers/true_client_ip.rb new file mode 100644 index 000000000..cac696107 --- /dev/null +++ b/config/initializers/true_client_ip.rb @@ -0,0 +1,24 @@ +# +# Cloudflare sets a True-Client-IP header, which for most 37signals apps gets copied to +# X-Forwarded-For by an iRule on the F5 load balancers: +# +# https://github.com/basecamp/f5-tf/blob/1543f7bfa3961a79e397f80cf041d75567f1b2f8/ams-base/iRules/manage_x_forwarded.tcl +# +# However, for Fizzy the F5s are configured to do passthrough, so the header value isn't being +# copied for us. Let's do that bit of work here, before Rails' RemoteIp middleware. +# +class TrackTrueClientIp + def initialize(app) + @app = app + end + + def call(env) + if env["HTTP_TRUE_CLIENT_IP"].present? + env["HTTP_X_FORWARDED_FOR"] = env["HTTP_TRUE_CLIENT_IP"] + end + + @app.call(env) + end +end + +Rails.application.config.middleware.insert_before ActionDispatch::RemoteIp, TrackTrueClientIp diff --git a/test/lib/true_client_ip_test.rb b/test/lib/true_client_ip_test.rb new file mode 100644 index 000000000..5c8c2e4d9 --- /dev/null +++ b/test/lib/true_client_ip_test.rb @@ -0,0 +1,34 @@ +require "test_helper" + +class TrackTrueClientIpTest < ActiveSupport::TestCase + setup do + @app = ->(env) { [ 200, {}, [ "OK" ] ] } + @middleware = TrackTrueClientIp.new(@app) + end + + test "sets X-Forwarded-For header when True-Client-IP header is present" do + env = { "HTTP_TRUE_CLIENT_IP" => "123.123.123.123" } + @middleware.call(env) + assert_equal "123.123.123.123", env["HTTP_X_FORWARDED_FOR"] + end + + test "does not modify environment when True-Client-IP header is absent" do + env = {} + @middleware.call(env) + assert_nil env["HTTP_X_FORWARDED_FOR"] + + env = { "HTTP_X_FORWARDED_FOR" => "234.234.234.234" } + @middleware.call(env) + assert_equal "234.234.234.234", env["HTTP_X_FORWARDED_FOR"] + end + + test "calls the next middleware in the stack" do + called = false + app = ->(env) { called = true; [ 200, {}, [ "OK" ] ] } + middleware = TrackTrueClientIp.new(app) + + middleware.call({}) + + assert called + end +end From e682f2742d38fc585c594135e67975fef7aec6c3 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 3 Nov 2025 15:12:39 -0500 Subject: [PATCH 95/98] test: Fix the flaky smoke test on attachments --- test/system/smoke_test.rb | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/test/system/smoke_test.rb b/test/system/smoke_test.rb index 7e0b5e74b..cc84bfef2 100644 --- a/test/system/smoke_test.rb +++ b/test/system/smoke_test.rb @@ -22,11 +22,17 @@ class SmokeTest < ApplicationSystemTestCase click_on "Upload file" end - assert_image_figure_attachment content_type: "image/jpeg", caption: "moon.jpg" + within("form lexxy-editor figure.attachment[data-content-type='image/jpeg']") do + assert_selector "img[src*='/rails/active_storage']" + assert_selector "figcaption input[placeholder='moon.jpg']" + end click_on "Post this comment" - assert_image_figure_attachment content_type: "image/jpeg", caption: "moon.jpg" + within("action-text-attachment") do + assert_selector "a img[src*='/rails/active_storage']" + assert_selector "figcaption span.attachment__name", text: "moon.jpg" + end end test "dismissing notifications" do @@ -53,16 +59,4 @@ class SmokeTest < ApplicationSystemTestCase editor_element.set with page.execute_script("arguments[0].value = '#{with}'", editor_element) end - - def assert_figure_attachment(content_type:, &block) - figure = find("figure.attachment[data-content-type='#{content_type}']") - within(figure, &block) if block_given? - end - - def assert_image_figure_attachment(content_type: "image/png", caption:) - assert_figure_attachment(content_type: content_type) do - assert_selector "img[src*='/rails/active_storage']", wait: 10 - assert_selector "figcaption input[placeholder='#{caption}']", wait: 10 - end - end end From 605955fdaffebfe6d3505e6a493f9f52b2f1d754 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 3 Nov 2025 17:29:25 -0500 Subject: [PATCH 96/98] Make old migration that depends on the sqlite vec0 module a no-op Once the sqlite 'vec0' module was removed from the codebase in 38a7a144 (following the table removal in 875a298f), this migration became unrunnable. So to make sure we can reconstruct the schema if necessary by running all the migrations, I'm replacing it with the creation of an empty table. --- .../20250723165724_create_search_embeddings.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/db/migrate/20250723165724_create_search_embeddings.rb b/db/migrate/20250723165724_create_search_embeddings.rb index 688d4a082..ad91e507e 100644 --- a/db/migrate/20250723165724_create_search_embeddings.rb +++ b/db/migrate/20250723165724_create_search_embeddings.rb @@ -1,10 +1,16 @@ class CreateSearchEmbeddings < ActiveRecord::Migration[7.1] def change - create_virtual_table :search_embeddings, :vec0, [ - "id INTEGER PRIMARY KEY", - "record_type TEXT NOT NULL", - "record_id INTEGER NOT NULL", - "embedding FLOAT[1536] distance_metric=cosine" - ] + # create_virtual_table :search_embeddings, :vec0, [ + # "id INTEGER PRIMARY KEY", + # "record_type TEXT NOT NULL", + # "record_id INTEGER NOT NULL", + # "embedding FLOAT[1536] distance_metric=cosine" + # ] + + # Above is the original migration. Once the sqlite 'vec0' module was removed from the codebase + # in 38a7a144 (following the table removal in 875a298f), this migration became unrunnable. So to + # make sure we can reconstruct the schema if necessary by running all the migrations, I'm + # replacing it with this empty table. + create_table :search_embeddings end end From 7d154b682eb4e7f42530bfeaeb1b1e333a952e25 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 16:29:57 -0600 Subject: [PATCH 97/98] Rename 'BOXCAR' to 'Fizzy' Missed files --- app/helpers/application_helper.rb | 2 +- app/mailers/application_mailer.rb | 2 +- app/mailers/magic_link_mailer.rb | 2 +- app/mailers/notification/bundle_mailer.rb | 2 +- app/views/account/settings/_entropy.html.erb | 2 +- app/views/collections/edit/_auto_close.html.erb | 2 +- app/views/collections/edit/_publication.html.erb | 2 +- app/views/events/index.html.erb | 2 +- app/views/layouts/public.html.erb | 2 +- .../email_change_confirmation.html.erb | 2 +- .../email_change_confirmation.text.erb | 2 +- .../sign_in_instructions.html.erb | 6 +++--- .../sign_in_instructions.text.erb | 2 +- .../bundle_mailer/notification.html.erb | 2 +- .../bundle_mailer/notification.text.erb | 2 +- app/views/my/menus/_button.html.erb | 2 +- app/views/my/menus/_places.html.erb | 2 +- app/views/my/menus/show.html.erb | 2 +- app/views/notifications/settings/_install.html.erb | 8 ++++---- app/views/notifications/settings/_system.html.erb | 12 ++++++------ app/views/notifications/unsubscribes/show.html.erb | 4 ++-- app/views/pwa/manifest.json.erb | 2 +- app/views/searches/_form.html.erb | 2 +- app/views/sessions/_footer.html.erb | 2 +- app/views/sessions/menus/show.html.erb | 6 +++--- app/views/webhooks/index.html.erb | 4 ++-- app/views/webhooks/new.html.erb | 2 +- .../app/models/signup/account_name_generator.rb | 2 +- gems/fizzy-saas/app/views/signups/new.html.erb | 2 +- .../signups/memberships_controller_test.rb | 2 +- .../models/signup/account_name_generator_test.rb | 14 +++++++------- test/mailers/magic_link_mailer_test.rb | 2 +- 32 files changed, 52 insertions(+), 52 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8447f024d..f6afd0ba4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,6 +1,6 @@ module ApplicationHelper def page_title_tag - tag.title @page_title || "BOXCAR" + tag.title @page_title || "Fizzy" end def icon_tag(name, **options) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d6db07d61..4e7878b62 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,5 +1,5 @@ class ApplicationMailer < ActionMailer::Base - default from: "The BOXCAR team " + default from: "The Fizzy team " layout "mailer" append_view_path Rails.root.join("app/views/mailers") diff --git a/app/mailers/magic_link_mailer.rb b/app/mailers/magic_link_mailer.rb index 52b9be031..391e64f6d 100644 --- a/app/mailers/magic_link_mailer.rb +++ b/app/mailers/magic_link_mailer.rb @@ -3,7 +3,7 @@ class MagicLinkMailer < ApplicationMailer @magic_link = magic_link @identity = @magic_link.identity - mail to: @identity.email_address, subject: "Sign in to BOXCAR" + mail to: @identity.email_address, subject: "Sign in to Fizzy" end private diff --git a/app/mailers/notification/bundle_mailer.rb b/app/mailers/notification/bundle_mailer.rb index 32f883efd..c821a1274 100644 --- a/app/mailers/notification/bundle_mailer.rb +++ b/app/mailers/notification/bundle_mailer.rb @@ -11,6 +11,6 @@ class Notification::BundleMailer < ApplicationMailer mail \ to: bundle.user.identity.email_address, - subject: "Latest Activity in BOXCAR" + subject: "Latest Activity in Fizzy" end end diff --git a/app/views/account/settings/_entropy.html.erb b/app/views/account/settings/_entropy.html.erb index fb3709e4d..f6056ab37 100644 --- a/app/views/account/settings/_entropy.html.erb +++ b/app/views/account/settings/_entropy.html.erb @@ -1,7 +1,7 @@

Auto close

-

BOXCAR doesn’t let stale cards stick around forever. Cards automatically close as “Not Now” without activity for specific period of time. This is the default, global setting — you can override it on each board.

+

Fizzy doesn’t let stale cards stick around forever. Cards automatically close as “Not Now” without activity for specific period of time. This is the default, global setting — you can override it on each board.

<%= render "entropy/auto_close", model: account.entropy, url: account_entropy_path %> diff --git a/app/views/collections/edit/_auto_close.html.erb b/app/views/collections/edit/_auto_close.html.erb index 4a86005a8..e7e135d8d 100644 --- a/app/views/collections/edit/_auto_close.html.erb +++ b/app/views/collections/edit/_auto_close.html.erb @@ -1,7 +1,7 @@ <%= turbo_frame_tag @collection, :entropy do %>

Auto close

-

BOXCAR doesn’t let stale cards stick around forever. Cards automatically close as “Not now” if no one updates, comments, or moves a card for…

+

Fizzy doesn’t let stale cards stick around forever. Cards automatically close as “Not now” if no one updates, comments, or moves a card for…

<%= render "entropy/auto_close", model: collection, url: collection_entropy_path(collection) %>
<% end %> diff --git a/app/views/collections/edit/_publication.html.erb b/app/views/collections/edit/_publication.html.erb index 6f90bb0b0..0673bd043 100644 --- a/app/views/collections/edit/_publication.html.erb +++ b/app/views/collections/edit/_publication.html.erb @@ -1,7 +1,7 @@ <%= turbo_frame_tag @collection, :publication do %>

Public link

-
Turn on the Public link to share this board with anyone in the world. They won’t need to log in and they won’t be able to see anything else in BOXCAR.
+
Turn on the Public link to share this board with anyone in the world. They won’t need to log in and they won’t be able to see anything else in Fizzy.
<% if collection.published? %> diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 4bdc6b8c7..1355ca229 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -28,7 +28,7 @@ diff --git a/app/views/layouts/public.html.erb b/app/views/layouts/public.html.erb index 6df1eabeb..c217f44bb 100644 --- a/app/views/layouts/public.html.erb +++ b/app/views/layouts/public.html.erb @@ -8,7 +8,7 @@ diff --git a/app/views/mailers/identity_mailer/email_change_confirmation.html.erb b/app/views/mailers/identity_mailer/email_change_confirmation.html.erb index 0c46017ff..438dc18e9 100644 --- a/app/views/mailers/identity_mailer/email_change_confirmation.html.erb +++ b/app/views/mailers/identity_mailer/email_change_confirmation.html.erb @@ -1,5 +1,5 @@

Confirm your email address change

-

Hit the button below to use this email address in BOXCAR.

+

Hit the button below to use this email address in Fizzy.

<%= link_to "Yes use use this email address", email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token), class: "btn" %> diff --git a/app/views/mailers/identity_mailer/email_change_confirmation.text.erb b/app/views/mailers/identity_mailer/email_change_confirmation.text.erb index 7d669a4d4..265c9cbf2 100644 --- a/app/views/mailers/identity_mailer/email_change_confirmation.text.erb +++ b/app/views/mailers/identity_mailer/email_change_confirmation.text.erb @@ -1,7 +1,7 @@ Confirm your email address change <%= "=" * 80 %> -Hit the link below to use this email address in BOXCAR: +Hit the link below to use this email address in Fizzy: <%= email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token) %> diff --git a/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb b/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb index 7a193eb08..b966ecfa6 100644 --- a/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb +++ b/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb @@ -1,7 +1,7 @@ -

Sign in to BOXCAR

-

Hit the button below to sign in to BOXCAR on this device

+

Sign in to Fizzy

+

Hit the button below to sign in to Fizzy on this device

-<%= link_to "Sign in to BOXCAR", session_magic_link_url(code: @magic_link.code), class: "btn" %> +<%= link_to "Sign in to Fizzy", session_magic_link_url(code: @magic_link.code), class: "btn" %>

If you’re signing in on another device, enter this special code:
<%= @magic_link.code %> diff --git a/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb b/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb index 037cda365..f44e1c908 100644 --- a/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb +++ b/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb @@ -1,4 +1,4 @@ -Sign in to BOXCAR +Sign in to Fizzy <%= "=" * 80 %> Open this link in your browser to login on this device: diff --git a/app/views/mailers/notification/bundle_mailer/notification.html.erb b/app/views/mailers/notification/bundle_mailer/notification.html.erb index 5983a2bc1..e7478b6b7 100644 --- a/app/views/mailers/notification/bundle_mailer/notification.html.erb +++ b/app/views/mailers/notification/bundle_mailer/notification.html.erb @@ -3,7 +3,7 @@

Everything since <%= @bundle.starts_at.strftime("%-l%P on %A, %B %-d") %>

You have <%= link_to pluralize(@notifications.count, "new notification"), notifications_url %>.

<%= render partial: "notification/bundle_mailer/notification", collection: @notifications, as: :notification %> - diff --git a/app/views/mailers/notification/bundle_mailer/notification.text.erb b/app/views/mailers/notification/bundle_mailer/notification.text.erb index ce383e249..0149767e6 100644 --- a/app/views/mailers/notification/bundle_mailer/notification.text.erb +++ b/app/views/mailers/notification/bundle_mailer/notification.text.erb @@ -4,7 +4,7 @@ You have <%= pluralize @notifications.count, "new notification" %>. <%= render partial: "notification/bundle_mailer/notification", collection: @notifications, as: :notification %> -------------------------------------------------------------------------------- -BOXCAR emails you about new notifications every few hours. +Fizzy emails you about new notifications every few hours. Change how often you get these: <%= notifications_settings_url %> diff --git a/app/views/my/menus/_button.html.erb b/app/views/my/menus/_button.html.erb index 589c69bf2..2034f6a40 100644 --- a/app/views/my/menus/_button.html.erb +++ b/app/views/my/menus/_button.html.erb @@ -3,5 +3,5 @@ action: "click->dialog#open:stop keydown.j@document->hotkey#click keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click", controller: "hotkey" } do %> <%= image_tag "logo.png" %> - BOXCAR + Fizzy <% end %> diff --git a/app/views/my/menus/_places.html.erb b/app/views/my/menus/_places.html.erb index 8b7be5b73..ee7b23de0 100644 --- a/app/views/my/menus/_places.html.erb +++ b/app/views/my/menus/_places.html.erb @@ -1,5 +1,5 @@ <%= collapsible_nav_section "Jump to…" do %> - <%= filter_place_menu_item "https://www.youtube.com/watch?v=rNKRdk7DLHM", "BOXCAR beta orientation", "youtube", new_window: true %> + <%= filter_place_menu_item "https://www.youtube.com/watch?v=rNKRdk7DLHM", "Fizzy beta orientation", "youtube", new_window: true %> <%= filter_place_menu_item account_settings_path, "Account Settings", "settings" %> <%= filter_place_menu_item user_path(Current.user), "My Profile", "person" %> <%= filter_place_menu_item notifications_path, "Notifications", "bell" %> diff --git a/app/views/my/menus/show.html.erb b/app/views/my/menus/show.html.erb index 4490d1088..bac6a9422 100644 --- a/app/views/my/menus/show.html.erb +++ b/app/views/my/menus/show.html.erb @@ -11,7 +11,7 @@ <%= render "hotkeys" %>
- BOXCAR is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals. + Fizzy is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals.
<%= render "saved_filter", filters: @user_filtering.filters %> diff --git a/app/views/notifications/settings/_install.html.erb b/app/views/notifications/settings/_install.html.erb index be618ae0d..f8d664043 100644 --- a/app/views/notifications/settings/_install.html.erb +++ b/app/views/notifications/settings/_install.html.erb @@ -1,10 +1,10 @@ <% unless (platform.chrome? && !platform.ios?) || (platform.firefox? && !platform.android?) %>
-

<%= platform.safari? && platform.desktop? ? "…or install" : "Install " -%> BOXCAR as a web app.

+

<%= platform.safari? && platform.desktop? ? "…or install" : "Install " -%> Fizzy as a web app.

<% case when platform.edge? %>
    -
  1. Click <%= icon_tag "install-edge", alt: "the app available - install BOXCAR button" %>in the address bar.
  2. +
  3. Click <%= icon_tag "install-edge", alt: "the app available - install Fizzy button" %>in the address bar.
  4. Click Install.
<% when platform.chrome? && platform.android? %> @@ -23,13 +23,13 @@
  • Click Add to Dock….
  • <% when (platform.safari? || platform.chrome?) && platform.ios? %> -

    To receive push notifications in <%= platform.browser.capitalize %> for <%= platform.operating_system %>, you must first install BOXCAR as a web app.

    +

    To receive push notifications in <%= platform.browser.capitalize %> for <%= platform.operating_system %>, you must first install Fizzy as a web app.

    1. Tap <%= icon_tag "share", alt: "the share button" %>
    2. Tap Add to Home Screen.
    <% else %> -

    Some platforms require you to install BOXCAR as a web app to receive push notifications.

    +

    Some platforms require you to install Fizzy as a web app to receive push notifications.

    <% end %>
    <% end %> diff --git a/app/views/notifications/settings/_system.html.erb b/app/views/notifications/settings/_system.html.erb index 23741ee66..afe0efd45 100644 --- a/app/views/notifications/settings/_system.html.erb +++ b/app/views/notifications/settings/_system.html.erb @@ -12,19 +12,19 @@
    1. Click Start, then Settings.
    2. Go to System > Notification.
    3. -
    4. Click <%= icon_tag "switch", alt: "the toggle button" %> ON for BOXCAR.
    5. +
    6. Click <%= icon_tag "switch", alt: "the toggle button" %> ON for Fizzy.
    <% when (platform.firefox? || platform.chrome?) && platform.desktop? %>
      <% if platform.windows? %>
    1. Click Start, then Settings.
    2. Go to System > Notification.
    3. -
    4. Click <%= icon_tag "switch", alt: "the toggle button" %> ON for BOXCAR.
    5. +
    6. Click <%= icon_tag "switch", alt: "the toggle button" %> ON for Fizzy.
    7. <% else %>
    8. Click in the top left.
    9. Click System Settings….
    10. Click Notifications.
    11. -
    12. Click BOXCAR.
    13. +
    14. Click Fizzy.
    15. Click <%= icon_tag "switch", alt: "the allow notifications switch" %> to Allow notifications.
    16. <% end %>
    @@ -33,13 +33,13 @@
  • Click in the top left.
  • Click System Settings….
  • Click Notifications.
  • -
  • Click BOXCAR.
  • +
  • Click Fizzy.
  • Click <%= icon_tag "switch", alt: "the allow notifications switch" %> to Allow notifications.
  • <% when (platform.safari? || platform.chrome?) && platform.ios? %>
    1. Open the <%= icon_tag "gear", aria: { hidden: "true" } %> Settings app.
    2. -
    3. Scroll to and tap BOXCAR.
    4. +
    5. Scroll to and tap Fizzy.
    6. Tap Notifications.
    7. Tap <%= icon_tag "switch", alt: "the allow notifications switch button" %> to Allow Notifications.
    @@ -48,7 +48,7 @@
  • Open the <%= icon_tag "gear", aria: { hidden: "true" } %> Settings app.
  • Tap Notifications.
  • Tap App notifications.
  • -
  • Scroll to BOXCAR.
  • +
  • Scroll to Fizzy.
  • Tap <%= icon_tag "switch", alt: "the switch" %> to Allow Notifications.
  • <% else %> diff --git a/app/views/notifications/unsubscribes/show.html.erb b/app/views/notifications/unsubscribes/show.html.erb index b41fa6431..3e100b14d 100644 --- a/app/views/notifications/unsubscribes/show.html.erb +++ b/app/views/notifications/unsubscribes/show.html.erb @@ -1,9 +1,9 @@

    You’re unsubscribed

    -

    Thanks, <%= @user.first_name %>! BOXCAR won’t send you any more email notifications. If you change your mind you can turn them back on in <%= link_to "Notifications Settings", notifications_settings_path %>.

    +

    Thanks, <%= @user.first_name %>! Fizzy won’t send you any more email notifications. If you change your mind you can turn them back on in <%= link_to "Notifications Settings", notifications_settings_path %>.

    <%= link_to root_path, class: "btn btn--link margin-block-start" do %> <%= icon_tag "arrow-left" %> - Back to BOXCAR + Back to Fizzy <% end %>
    diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb index c697f8eba..ff66cb7b3 100644 --- a/app/views/pwa/manifest.json.erb +++ b/app/views/pwa/manifest.json.erb @@ -1,5 +1,5 @@ { - "name": "<%= [ Account.sole.name, "BOXCAR", Rails.env.production? ? nil : Rails.env ].compact.join(" - ") %>", + "name": "<%= [ Account.sole.name, "Fizzy", Rails.env.production? ? nil : Rails.env ].compact.join(" - ") %>", "icons": [ { "src": "/app-icon-192.png", diff --git a/app/views/searches/_form.html.erb b/app/views/searches/_form.html.erb index 379126465..f9a1b778a 100644 --- a/app/views/searches/_form.html.erb +++ b/app/views/searches/_form.html.erb @@ -1,5 +1,5 @@ <%= form_with url: search_path, method: :get, class: "search__form flex align-center justify-center gap-half", data: { bar_target: "form", turbo_frame: "bar_content" } do |form| %> - <%= form.label :q, "Search BOXCAR", class: "font-weight-black txt-nowrap" %> + <%= form.label :q, "Search Fizzy", class: "font-weight-black txt-nowrap" %> <%= text_field_tag :q, query_terms, class: "search__input input", type: "search", diff --git a/app/views/sessions/_footer.html.erb b/app/views/sessions/_footer.html.erb index 208f47a3f..33d604824 100644 --- a/app/views/sessions/_footer.html.erb +++ b/app/views/sessions/_footer.html.erb @@ -1,3 +1,3 @@
    - BOXCAR beta. <%= mail_to "support@37signals.com", "Need help?", class: "txt-link" %> + Fizzy beta. <%= mail_to "support@37signals.com", "Need help?", class: "txt-link" %>
    \ No newline at end of file diff --git a/app/views/sessions/menus/show.html.erb b/app/views/sessions/menus/show.html.erb index a61c415ca..535d2b903 100644 --- a/app/views/sessions/menus/show.html.erb +++ b/app/views/sessions/menus/show.html.erb @@ -7,7 +7,7 @@ > <% if @memberships.present? %>

    - Your BOXCAR accounts + Your Fizzy accounts

    <% @memberships.each do |membership| %> @@ -21,13 +21,13 @@ <% else %>

    Hmm...

    -

    You don’t have any BOXCAR accounts.

    +

    You don’t have any Fizzy accounts.

    <% end %> <% if defined?(saas) %>
    <%= link_to saas.new_signup_membership_path, class: "btn btn--plain txt-link center txt-small" do %> - Sign up for a new BOXCAR account + Sign up for a new Fizzy account <% end %>
    <% end %> diff --git a/app/views/webhooks/index.html.erb b/app/views/webhooks/index.html.erb index 8c0387510..a891b8bc8 100644 --- a/app/views/webhooks/index.html.erb +++ b/app/views/webhooks/index.html.erb @@ -26,8 +26,8 @@ <%= render partial: "webhooks/webhook", collection: @page.records %> <% else %> -

    Webhooks can notify another application when something happens in this BOXCAR board. You’ll choose which events to subscribe to and provide a URL to receive the data.

    -

    For example, you could create a webhook that posts to a Campfire chat in Basecamp when new cards are added to BOXCAR.

    +

    Webhooks can notify another application when something happens in this Fizzy board. You’ll choose which events to subscribe to and provide a URL to receive the data.

    +

    For example, you could create a webhook that posts to a Campfire chat in Basecamp when new cards are added to Fizzy.

    <%= link_to new_collection_webhook_path, class: "btn btn--link" do %> <%= icon_tag "add" %> Set up a webhook diff --git a/app/views/webhooks/new.html.erb b/app/views/webhooks/new.html.erb index aafe035e6..f71781935 100644 --- a/app/views/webhooks/new.html.erb +++ b/app/views/webhooks/new.html.erb @@ -22,7 +22,7 @@
    <%= form.label :url do %> Payload URL
    -

    This is the URL for the app that will receive payloads from BOXCAR.

    +

    This is the URL for the app that will receive payloads from Fizzy.

    <% end %> <%= form.url_field :url, required: true, diff --git a/gems/fizzy-saas/app/models/signup/account_name_generator.rb b/gems/fizzy-saas/app/models/signup/account_name_generator.rb index 90104fe63..2ec57d534 100644 --- a/gems/fizzy-saas/app/models/signup/account_name_generator.rb +++ b/gems/fizzy-saas/app/models/signup/account_name_generator.rb @@ -1,5 +1,5 @@ class Signup::AccountNameGenerator - SUFFIX = "BOXCAR".freeze + SUFFIX = "Fizzy".freeze attr_reader :identity, :name diff --git a/gems/fizzy-saas/app/views/signups/new.html.erb b/gems/fizzy-saas/app/views/signups/new.html.erb index 7512ec120..93166dd2f 100644 --- a/gems/fizzy-saas/app/views/signups/new.html.erb +++ b/gems/fizzy-saas/app/views/signups/new.html.erb @@ -1,4 +1,4 @@ -<% @page_title = "Sign up for BOXCAR" %> +<% @page_title = "Sign up for Fizzy" %>
    ">

    Sign up

    diff --git a/gems/fizzy-saas/test/controllers/signups/memberships_controller_test.rb b/gems/fizzy-saas/test/controllers/signups/memberships_controller_test.rb index b0eb590fe..f1ca13093 100644 --- a/gems/fizzy-saas/test/controllers/signups/memberships_controller_test.rb +++ b/gems/fizzy-saas/test/controllers/signups/memberships_controller_test.rb @@ -45,7 +45,7 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest signup: { membership_id: membership.signed_id(purpose: :account_creation), full_name: "New User", - account_name: "New's BOXCAR" + account_name: "New's Fizzy" } ) end diff --git a/gems/fizzy-saas/test/models/signup/account_name_generator_test.rb b/gems/fizzy-saas/test/models/signup/account_name_generator_test.rb index 4c5516c61..fdaac96ce 100644 --- a/gems/fizzy-saas/test/models/signup/account_name_generator_test.rb +++ b/gems/fizzy-saas/test/models/signup/account_name_generator_test.rb @@ -9,38 +9,38 @@ class Signup::AccountNameGeneratorTest < ActiveSupport::TestCase test "generate" do account_name = @generator.generate - assert_equal "Newart's BOXCAR", account_name, "The 1st account doesn't have 1st in the name" + assert_equal "Newart's Fizzy", account_name, "The 1st account doesn't have 1st in the name" first_membership = @identity.memberships.create(tenant: "1st") first_membership.stubs(:account_name).returns(account_name) account_name = @generator.generate - assert_equal "Newart's 2nd BOXCAR", account_name + assert_equal "Newart's 2nd Fizzy", account_name second_membership = @identity.memberships.create(tenant: "2nd") second_membership.stubs(:account_name).returns(account_name) account_name = @generator.generate - assert_equal "Newart's 3rd BOXCAR", account_name + assert_equal "Newart's 3rd Fizzy", account_name third_membership = @identity.memberships.create(tenant: "3nd") third_membership.stubs(:account_name).returns(account_name) account_name = @generator.generate - assert_equal "Newart's 4th BOXCAR", account_name + assert_equal "Newart's 4th Fizzy", account_name fourth_membership = @identity.memberships.create(tenant: "4th") fourth_membership.stubs(:account_name).returns(account_name) account_name = @generator.generate - assert_equal "Newart's 5th BOXCAR", account_name + assert_equal "Newart's 5th Fizzy", account_name end test "generate continues from the previous highest index" do membership = @identity.memberships.create(tenant: "12th") - membership.stubs(:account_name).returns("Newart's 12th BOXCAR") + membership.stubs(:account_name).returns("Newart's 12th Fizzy") account_name = @generator.generate - assert_equal "Newart's 13th BOXCAR", account_name + assert_equal "Newart's 13th Fizzy", account_name end end diff --git a/test/mailers/magic_link_mailer_test.rb b/test/mailers/magic_link_mailer_test.rb index 34ba265f9..ee3d56abc 100644 --- a/test/mailers/magic_link_mailer_test.rb +++ b/test/mailers/magic_link_mailer_test.rb @@ -10,7 +10,7 @@ class MagicLinkMailerTest < ActionMailer::TestCase end assert_equal [ "kevin@37signals.com" ], email.to - assert_equal "Sign in to BOXCAR", email.subject + assert_equal "Sign in to Fizzy", email.subject assert_match magic_link.code, email.body.encoded end end From 38ead57f4e0980d35e8c5cec1fb6bf429f532beb Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 16:55:24 -0600 Subject: [PATCH 98/98] Just put the hotkey on the button No need to over explain the keyboard navigation, anyone who cares about this will figure it out --- app/views/my/menus/_button.html.erb | 3 ++- app/views/my/menus/_footer.html.erb | 3 +++ app/views/my/menus/_hotkeys.html.erb | 2 +- app/views/my/menus/_shortcut_notice.html.erb | 5 ----- app/views/my/menus/show.html.erb | 8 +------- 5 files changed, 7 insertions(+), 14 deletions(-) create mode 100644 app/views/my/menus/_footer.html.erb delete mode 100644 app/views/my/menus/_shortcut_notice.html.erb diff --git a/app/views/my/menus/_button.html.erb b/app/views/my/menus/_button.html.erb index 2034f6a40..52305cf60 100644 --- a/app/views/my/menus/_button.html.erb +++ b/app/views/my/menus/_button.html.erb @@ -1,7 +1,8 @@ -<%= tag.button class:"boxcar-menu-trigger input input--select center flex-inline align-center txt-normal", +<%= tag.button class:"boxcar-menu-trigger input input--select center flex-inline gap-half align-center txt-normal", data: { action: "click->dialog#open:stop keydown.j@document->hotkey#click keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click", controller: "hotkey" } do %> <%= image_tag "logo.png" %> Fizzy + J <% end %> diff --git a/app/views/my/menus/_footer.html.erb b/app/views/my/menus/_footer.html.erb new file mode 100644 index 000000000..22cfc379d --- /dev/null +++ b/app/views/my/menus/_footer.html.erb @@ -0,0 +1,3 @@ +
    + Fizzy is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals. +
    diff --git a/app/views/my/menus/_hotkeys.html.erb b/app/views/my/menus/_hotkeys.html.erb index b722cf3fa..1f6494c0d 100644 --- a/app/views/my/menus/_hotkeys.html.erb +++ b/app/views/my/menus/_hotkeys.html.erb @@ -1,4 +1,4 @@ -
    +
    <%= filter_hotkey_link "Home", root_path, 1, "home" %> <%= filter_hotkey_link "New board", new_collection_path, 2, "collection-add" %> <%= filter_hotkey_link "Assigned to me", cards_path(assignee_ids: [Current.user.id]), 3, "clipboard" %> diff --git a/app/views/my/menus/_shortcut_notice.html.erb b/app/views/my/menus/_shortcut_notice.html.erb deleted file mode 100644 index 70f5f5fb7..000000000 --- a/app/views/my/menus/_shortcut_notice.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% if platform.desktop? %> -
    - Press J anytime to open this, esc to close, to move, enter to navigate. -
    -<% end %> diff --git a/app/views/my/menus/show.html.erb b/app/views/my/menus/show.html.erb index bac6a9422..d5eaa1dd5 100644 --- a/app/views/my/menus/show.html.erb +++ b/app/views/my/menus/show.html.erb @@ -9,17 +9,11 @@
    <%= render "hotkeys" %> - -
    - Fizzy is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals. -
    - <%= render "saved_filter", filters: @user_filtering.filters %> <%= render "collections", user_filtering: @user_filtering %> <%= render "tags", user_filtering: @user_filtering %> <%= render "users", user_filtering: @user_filtering %> <%= render "places", user_filtering: @user_filtering %> <%= render "accounts", user_filtering: @user_filtering %> - - <%= render "shortcut_notice" %> + <%= render "footer" %> <% end %>