From 440e96cfe16353f7964115e414150668b0747023 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 10 Oct 2025 22:44:40 -0700 Subject: [PATCH 01/41] bin/setup: drop spinners since they interfere with bash trap and error output --- bin/setup | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/bin/setup b/bin/setup index 1abc9b0cc..c1466d58c 100755 --- a/bin/setup +++ b/bin/setup @@ -21,23 +21,13 @@ if ! command -v gum &> /dev/null; then fi step() { - local no_spin=false - if [[ "$1" == "--no-spin" ]]; then - no_spin=true - shift - fi - local step_name="$1" shift gum style --foreground 135 --bold "▸ $step_name" gum style --foreground 240 "$*" - if $no_spin; then - "$@" - else - gum spin --spinner dot --title "" --show-output -- "$@" - fi + "$@" local exit_code=$? echo @@ -49,7 +39,7 @@ gum style --foreground 153 " ˚ ∘ ∘ ˚ " gum style --foreground 111 --bold " ∘˚˳°∘° 𝒻𝒾𝓏𝓏𝓎 °∘°˳˚∘ " echo -step --no-spin "Installing Ruby" mise install --yes +step "Installing Ruby" mise install --yes eval "$(mise hook-env)" if which pacman >/dev/null 2>&1; then From 956962de7f460f7ffa122cc8fcc11a3b87fdd6e6 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 11 Oct 2025 11:15:54 -0400 Subject: [PATCH 02/41] Additional Autotuner suggestions The following suggestions reduce the number of minor garbage collection cycles, specifically a cycle called "malloc". Your app runs malloc cycles in approximately 18.52% of all minor garbage collection cycles. Reducing minor garbage collection cycles can help reduce response times. The following tuning values aim to reduce malloc garbage collection cycles by setting it to a higher value. This may cause a slight increase in memory usage. You should monitor memory usage carefully to ensure your app is not running out of memory. Suggested tuning values: RUBY_GC_MALLOC_LIMIT=33554432 RUBY_GC_MALLOC_LIMIT_MAX=67108864 --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 26bd97953..3e21f7f38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,7 +75,9 @@ ENV RUBY_GC_HEAP_0_INIT_SLOTS=692636 \ RUBY_GC_HEAP_1_INIT_SLOTS=175943 \ RUBY_GC_HEAP_2_INIT_SLOTS=148807 \ RUBY_GC_HEAP_3_INIT_SLOTS=9169 \ - RUBY_GC_HEAP_4_INIT_SLOTS=3054 + RUBY_GC_HEAP_4_INIT_SLOTS=3054 \ + RUBY_GC_MALLOC_LIMIT=33554432 \ + RUBY_GC_MALLOC_LIMIT_MAX=67108864 # Start the server by default, this can be overwritten at runtime EXPOSE 80 443 9394 From b9a616b083cdc601ca6314b594f4282eb4287835 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 11 Oct 2025 11:29:21 -0400 Subject: [PATCH 03/41] Increase the number of job workers to match CPUs JOB_CONCURRENCY would still override this number. --- config/queue.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/queue.yml b/config/queue.yml index 8b04d23f8..aaed725e0 100644 --- a/config/queue.yml +++ b/config/queue.yml @@ -4,8 +4,8 @@ default: &default batch_size: 500 workers: - queues: [ "default", "solid_queue_recurring", "backend", "webhooks" ] - threads: 3 - processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + threads: 1 + processes: <%= ENV.fetch("JOB_CONCURRENCY", Concurrent.physical_processor_count) %> polling_interval: 0.1 development: *default From 5c651a7f94b33fc8894074c464d350df55f4e2ad Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 11 Oct 2025 13:23:25 -0400 Subject: [PATCH 04/41] Tidy recurring.yml --- config/recurring.yml | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/config/recurring.yml b/config/recurring.yml index 14d058592..cf70b28f7 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -1,31 +1,36 @@ production: &production - auto_postpone_all_due: - class: Card::AutoPostponeAllDueJob - schedule: every hour - remove_abandoned_creations: - class: RemoveAbandonedCreationsJob - schedule: every hour - sqlite_backups: - class: SQLiteBackupsJob - schedule: every day at 05:00 - delete_unused_tags: - class: DeleteUnusedTagsJob - schedule: every day at 04:00 - clear_solid_queue_finished_jobs: - command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" - schedule: every hour at minute 12 - clear_solid_queue_recurring_executions: - command: "SolidQueue::RecurringExecution.clear_in_batches" - schedule: every hour at minute 52 + # Application functionality: notifications and highlights deliver_bundled_notifications: command: "Notification::Bundle.deliver_all_later" schedule: every 30 minutes generate_weekly_highlights: command: "User.generate_all_weekly_highlights_later" schedule: every sunday at noon + + # Application cleanup + auto_postpone_all_due: + class: Card::AutoPostponeAllDueJob + schedule: every hour + remove_abandoned_creations: + class: RemoveAbandonedCreationsJob + schedule: every hour + delete_unused_tags: + class: DeleteUnusedTagsJob + schedule: every day at 04:00 + + # Operations cleanup and backups + clear_solid_queue_finished_jobs: + command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" + schedule: every hour at minute 12 + clear_solid_queue_recurring_executions: + command: "SolidQueue::RecurringExecution.clear_in_batches" + schedule: every hour at minute 52 cleanup_webhook_deliveries: command: "Webhook::CleanupDeliveriesJob" schedule: every 4 hours + sqlite_backups: + class: SQLiteBackupsJob + schedule: every day at 05:00 beta: *production staging: *production From 4f981e9c2f414aa665508189c9cac8f14585bb2c Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 11 Oct 2025 13:35:31 -0400 Subject: [PATCH 05/41] Add yabeda-actioncable metrics --- Gemfile | 1 + Gemfile.lock | 6 ++++++ config/initializers/yabeda.rb | 5 +++++ config/recurring.yml | 5 +++++ 4 files changed, 17 insertions(+) diff --git a/Gemfile b/Gemfile index 566fc6326..ac8f14fa3 100644 --- a/Gemfile +++ b/Gemfile @@ -40,6 +40,7 @@ gem "sentry-ruby" gem "sentry-rails" gem "rails_structured_logging", bc: "rails-structured-logging" gem "yabeda" +gem "yabeda-actioncable" gem "yabeda-activejob", github: "rosa/yabeda-activejob", branch: "support-end-time-in-seconds" gem "yabeda-gc" gem "yabeda-http_requests" diff --git a/Gemfile.lock b/Gemfile.lock index 6acd98a6e..056b9f26e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -576,6 +576,11 @@ GEM anyway_config (>= 1.0, < 3) concurrent-ruby dry-initializer + yabeda-actioncable (0.2.2) + actioncable (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + yabeda (~> 0.8) yabeda-gc (0.4.0) yabeda (~> 0.6) yabeda-http_requests (0.2.1) @@ -655,6 +660,7 @@ DEPENDENCIES webmock webrick yabeda + yabeda-actioncable yabeda-activejob! yabeda-gc yabeda-http_requests diff --git a/config/initializers/yabeda.rb b/config/initializers/yabeda.rb index cf1adbd4f..cad88f80a 100644 --- a/config/initializers/yabeda.rb +++ b/config/initializers/yabeda.rb @@ -8,3 +8,8 @@ Yabeda::ActiveJob.install! require "yabeda/solid_queue" Yabeda::SolidQueue.install! + +Yabeda::ActionCable.configure do |config| + # Fizzy relies primarily on Turbo::StreamsChannel + config.channel_class_name = "ActionCable::Channel::Base" +end diff --git a/config/recurring.yml b/config/recurring.yml index cf70b28f7..8a121e3fd 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -32,5 +32,10 @@ production: &production class: SQLiteBackupsJob schedule: every day at 05:00 + # Metrics + yabeda_actioncable: + command: "Yabeda::ActionCable.measure" + schedule: every 60 seconds + beta: *production staging: *production From 28e9b4a496889825abdd437562c3c875f70d6b2a Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 11 Oct 2025 13:57:19 -0400 Subject: [PATCH 06/41] Fix recurring job config for webhook cleanup --- config/recurring.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/recurring.yml b/config/recurring.yml index 8a121e3fd..dd8b5a1e5 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -26,7 +26,7 @@ production: &production command: "SolidQueue::RecurringExecution.clear_in_batches" schedule: every hour at minute 52 cleanup_webhook_deliveries: - command: "Webhook::CleanupDeliveriesJob" + class: Webhook::CleanupDeliveriesJob schedule: every 4 hours sqlite_backups: class: SQLiteBackupsJob From b8f09b7f75874af8316d3667cf52eb615bd22dee Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 11 Oct 2025 15:57:47 -0700 Subject: [PATCH 07/41] Deploy to fully-qualified domain names to eliminate DNS search path driven query amplification (#1293) --- .kamal/hooks/pre-connect | 29 +++++++++++++++++++++++++++-- config/deploy.beta.yml | 12 ++++++------ config/deploy.production.yml | 12 ++++++------ config/deploy.staging.yml | 12 ++++++------ 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/.kamal/hooks/pre-connect b/.kamal/hooks/pre-connect index 11cbb0a33..2b6d9af42 100755 --- a/.kamal/hooks/pre-connect +++ b/.kamal/hooks/pre-connect @@ -1,7 +1,32 @@ #!/usr/bin/env bash -# -# Verify Tailscale connection and SSH authentication before deploying. +# Validate hostnames are FQDNs ending in -int.37signals.com +if command -v yq >/dev/null 2>&1; then + declare -A SUGGESTIONS + while IFS= read -r host; do + if [[ ! $host =~ -int\.37signals\.com$ ]]; then + if [[ $host =~ -4[0-9]{2}$ ]]; then + SUGGESTIONS["$host"]="$host.df-ams-int.37signals.com" + elif [[ $host =~ -1[0-9]{2}$ ]]; then + SUGGESTIONS["$host"]="$host.df-iad-int.37signals.com" + else + SUGGESTIONS["$host"]="$host.sc-chi-int.37signals.com" + fi + fi + done < <(bin/kamal config -d "${KAMAL_DESTINATION:-production}" 2>/dev/null | yq '.":hosts"[]') + + if [ ${#SUGGESTIONS[@]} -gt 0 ]; then + echo "Unqualified hostnames found in config/deploy.${KAMAL_DESTINATION:-production}.yml:" >&2 + echo "" >&2 + echo "Update to use fully-qualified hostnames:" >&2 + for host in "${!SUGGESTIONS[@]}"; do + echo " $host → ${SUGGESTIONS[$host]}" >&2 + done + exit 1 + fi +fi + +# Verify Tailscale connection and SSH authentication before deploying. tailscale_cmd() { if command -v tailscale >/dev/null 2>&1; then tailscale "$@" diff --git a/config/deploy.beta.yml b/config/deploy.beta.yml index 711730945..8dfb783ff 100644 --- a/config/deploy.beta.yml +++ b/config/deploy.beta.yml @@ -1,13 +1,13 @@ servers: web: hosts: - - fizzy-beta-app-01 - - fizzy-beta-app-101 + - fizzy-beta-app-01.sc-chi-int.37signals.com + - fizzy-beta-app-101.df-iad-int.37signals.com labels: otel_scrape_enabled: true jobs: hosts: - - fizzy-beta-app-01 + - fizzy-beta-app-01.sc-chi-int.37signals.com labels: otel_scrape_enabled: true @@ -42,12 +42,12 @@ accessories: - fizzy:/home/beamer cmd: beamer run --retention=1h --metrics-port=9000 hosts: - - fizzy-beta-app-01 - - fizzy-beta-app-101 + - fizzy-beta-app-01.sc-chi-int.37signals.com + - fizzy-beta-app-101.df-iad-int.37signals.com load-balancer: image: basecamp/kamal-proxy:lb - host: fizzy-beta-lb-01 + host: fizzy-beta-lb-01.sc-chi-int.37signals.com options: publish: - 80:80 diff --git a/config/deploy.production.yml b/config/deploy.production.yml index b21749c26..cc92bf005 100644 --- a/config/deploy.production.yml +++ b/config/deploy.production.yml @@ -1,13 +1,13 @@ servers: web: hosts: - - fizzy-app-101 - - fizzy-app-102 + - fizzy-app-101.df-iad-int.37signals.com + - fizzy-app-102.df-iad-int.37signals.com labels: otel_scrape_enabled: true jobs: hosts: - - fizzy-app-101 + - fizzy-app-101.df-iad-int.37signals.com labels: otel_scrape_enabled: true @@ -42,12 +42,12 @@ accessories: - fizzy:/home/beamer cmd: beamer run --retention=1h --metrics-port=9000 hosts: - - fizzy-app-101 - - fizzy-app-102 + - fizzy-app-101.df-iad-int.37signals.com + - fizzy-app-102.df-iad-int.37signals.com load-balancer: image: basecamp/kamal-proxy:lb - host: fizzy-lb-101 + host: fizzy-lb-101.df-iad-int.37signals.com labels: otel_role: load-balancer otel_service: fizzy-load-balancer diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index 7335abd97..fe2d3bf01 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -1,13 +1,13 @@ servers: web: hosts: - - fizzy-staging-app-01 - - fizzy-staging-app-101 + - fizzy-staging-app-01.sc-chi-int.37signals.com + - fizzy-staging-app-101.df-iad-int.37signals.com labels: otel_scrape_enabled: true jobs: hosts: - - fizzy-staging-app-01 + - fizzy-staging-app-01.sc-chi-int.37signals.com labels: otel_scrape_enabled: true @@ -42,12 +42,12 @@ accessories: - fizzy:/home/beamer cmd: beamer run --retention=1h --metrics-port=9000 hosts: - - fizzy-staging-app-01 - - fizzy-staging-app-101 + - fizzy-staging-app-01.sc-chi-int.37signals.com + - fizzy-staging-app-101.df-iad-int.37signals.com load-balancer: image: basecamp/kamal-proxy:lb - host: fizzy-staging-lb-01 + host: fizzy-staging-lb-01.sc-chi-int.37signals.com labels: otel_role: load-balancer otel_service: fizzy-load-balancer From 0e40a28b25b55bb87f779e6cf95d9734bfd0908e Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sun, 12 Oct 2025 14:34:51 -0400 Subject: [PATCH 08/41] dep: update activerecord-tenanted --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 056b9f26e..94806da54 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -157,7 +157,7 @@ GEM activemodel (>= 5.0.0.a) activesupport (>= 5.0.0.a) builder (~> 3.1) - activerecord-tenanted (0.4.1) + activerecord-tenanted (0.5.0) activerecord (>= 8.1.beta) railties (>= 8.1.beta) zeitwerk @@ -194,7 +194,7 @@ GEM bcrypt (3.1.20) bcrypt_pbkdf (1.1.1) benchmark (0.4.1) - bigdecimal (3.3.0) + bigdecimal (3.3.1) bootsnap (1.18.6) msgpack (~> 1.2) brakeman (7.1.0) @@ -229,7 +229,7 @@ GEM drb (2.2.3) dry-initializer (3.2.0) ed25519 (1.4.0) - erb (5.0.3) + erb (5.1.1) erubi (1.13.1) et-orbi (1.4.0) tzinfo @@ -410,7 +410,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.2) + rack (3.2.3) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) From 824c62b4b00300eb5869ac749bd60a98f4597b4d Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sun, 12 Oct 2025 14:36:11 -0400 Subject: [PATCH 09/41] dep: update rails --- Gemfile.lock | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 94806da54..1e973712f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -32,7 +32,7 @@ GIT GIT remote: https://github.com/rails/rails.git - revision: d8795bd762ab2716b6e7fd07a403566b28159cdc + revision: 36da3909403a9ca9b5e9c4a2da9e89bef1500d85 branch: main specs: actioncable (8.1.0.beta1) @@ -96,12 +96,12 @@ GIT marcel (~> 1.0) activesupport (8.1.0.beta1) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) @@ -193,7 +193,6 @@ GEM base64 (0.3.0) bcrypt (3.1.20) bcrypt_pbkdf (1.1.1) - benchmark (0.4.1) bigdecimal (3.3.1) bootsnap (1.18.6) msgpack (~> 1.2) From f100eb89ee40845f9edccb1bad3a6d44246b3ffa Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sun, 12 Oct 2025 14:38:54 -0400 Subject: [PATCH 10/41] dep: update selenium-webdriver --- Gemfile.lock | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1e973712f..94f322698 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -275,7 +275,7 @@ GEM actionview (>= 7.0.0) activesupport (>= 7.0.0) jmespath (1.6.2) - json (2.15.1) + json (2.13.2) jwt (3.1.2) base64 kamal (2.7.0) @@ -479,9 +479,11 @@ GEM ruby2_keywords (0.0.5) rubyzip (3.1.1) securerandom (0.4.1) - selenium-webdriver (4.35.0) + selenium-webdriver (4.36.0) base64 (~> 0.2) + json (<= 2.13.2) logger (~> 1.4) + prism (~> 1.0, < 1.5) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) From 9b00d59e57a6c0cefbcb6e5ef2f1e388af19698a Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sun, 12 Oct 2025 14:40:02 -0400 Subject: [PATCH 11/41] dep: update solid_cache and aws_partitions --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 94f322698..c60db773f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -172,7 +172,7 @@ GEM ast (2.4.3) autotuner (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1170.0) + aws-partitions (1.1172.0) aws-sdk-core (3.233.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -501,7 +501,7 @@ GEM activejob (>= 7.2) activerecord (>= 7.2) railties (>= 7.2) - solid_cache (1.0.7) + solid_cache (1.0.8) activejob (>= 7.2) activerecord (>= 7.2) railties (>= 7.2) From d3816bc21281f80d640ccc2a315097a3fcc567fa Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Mon, 13 Oct 2025 13:46:00 +0200 Subject: [PATCH 12/41] Create testbed to test beamer scenarios in Fizzy --- Dockerfile | 1 + Dockerfile.dev | 46 ++++++ Procfile.beamer-testbed | 3 + bin/beamer-testbed | 217 +++++++++++++++++++++++++++++ config/database.yml | 4 +- config/environments/development.rb | 2 +- config/initializers/beamer.rb | 16 ++- docker-compose.beamer-testbed.yml | 51 +++++++ 8 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 Dockerfile.dev create mode 100644 Procfile.beamer-testbed create mode 100755 bin/beamer-testbed create mode 100644 docker-compose.beamer-testbed.yml diff --git a/Dockerfile b/Dockerfile index 3e21f7f38..e26566140 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,6 +61,7 @@ RUN apt-get update -qq && \ COPY --from=build /usr/local/bundle /usr/local/bundle COPY --from=build /rails /rails COPY --from=beamer /home/beamer/bin/beamer.so /rails/bin/lib/beamer.so +# COPY --from=beamer /home/beamer/bin/beamer /rails/bin/beamer # Run and own only the runtime files as a non-root user for security RUN useradd rails --create-home --shell /bin/bash && \ diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 000000000..8ec772ce1 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,46 @@ +# syntax = docker/dockerfile:1 + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.4.7 +FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Set development environment +ENV RAILS_ENV="development" \ + BUNDLE_PATH="/usr/local/bundle" + +# Install packages needed for development +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential pkg-config git ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools libyaml-dev libssl-dev && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY Gemfile Gemfile.lock .ruby-version ./ +COPY lib/bootstrap.rb ./lib/bootstrap.rb +COPY gems ./gems/ +RUN --mount=type=secret,id=GITHUB_TOKEN --mount=type=cache,id=fizzy-devbundle-${RUBY_VERSION},sharing=locked,target=/devbundle \ + gem install bundler && \ + BUNDLE_PATH=/devbundle BUNDLE_GITHUB__COM="$(cat /run/secrets/GITHUB_TOKEN):x-oauth-basic" bundle install && \ + cp -a /devbundle/. "$BUNDLE_PATH"/ && \ + bundle clean --force + +# Fetch beamer library +FROM registry.37signals.com/basecamp/beamer:vfs AS beamer + +# Final stage +FROM base + +# Copy beamer +COPY --from=beamer /home/beamer/bin/beamer.so /usr/local/lib/beamer/beamer.so +COPY --from=beamer /usr/local/bin/beamer /usr/local/bin/beamer + +# Entrypoint prepares the database +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start the server by default +EXPOSE 3000 +EXPOSE 3006 +EXPOSE 5001 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Procfile.beamer-testbed b/Procfile.beamer-testbed new file mode 100644 index 000000000..e24b485ff --- /dev/null +++ b/Procfile.beamer-testbed @@ -0,0 +1,3 @@ +web: bin/thrust bin/rails server +beamer: beamer --debug --directory ./storage/ run --primary ${BEAMER_PRIMARY:-$(hostname)} +migrations: ./bin/beamer-testbed run-migrations --debug --directory ./storage/ diff --git a/bin/beamer-testbed b/bin/beamer-testbed new file mode 100755 index 000000000..8f9b6b64c --- /dev/null +++ b/bin/beamer-testbed @@ -0,0 +1,217 @@ +#!/usr/bin/env bash + +set -e + +COMPOSE_OPTIONS="-f docker-compose.beamer-testbed.yml" + +# Show usage +show_usage() { + echo "Usage: $0 {start|stop|logs|console|bash|server|run-migrations} [options]" + echo "" + echo "Commands:" + echo " start Build and start all services" + echo " stop Stop all services" + echo " logs Show logs (pass -f to follow)" + echo " console [node] Open Rails console (default: fizzy-01)" + echo " bash [node] Open bash shell (default: fizzy-01)" + echo " server Start web server and beamer using foreman" + echo " run-migrations Wait for beamer, then run migrations if primary" + exit 1 +} + +# Get all fizzy-* services from docker compose +get_fizzy_services() { + fizzy_services=$(docker compose $COMPOSE_OPTIONS config --services | grep '^fizzy-') + + if [ -z "$fizzy_services" ]; then + echo "Error: No fizzy-* services found in docker-compose file" + exit 1 + fi + + echo "$fizzy_services" +} + +# Function to wait for a container to be healthy +wait_for_container() { + local container=$1 + local max_attempts=60 + local attempt=0 + + echo "Waiting for $container to be ready..." + + while [ $attempt -lt $max_attempts ]; do + if docker compose $COMPOSE_OPTIONS exec -T "$container" curl -f -s http://localhost:3000/up >/dev/null 2>&1; then + echo "$container is ready!" + return 0 + fi + attempt=$((attempt + 1)) + echo "$container isn't yet ready on attempt $attempt/$max_attempts" + sleep 2 + done + + echo "Error: $container failed to become ready after $max_attempts attempts" + return 1 +} + +# Function to register a container with kamal-proxy +register_with_proxy() { + local container=$1 + local target_name="fizzy-${container##*-}" + + echo "Registering $container with kamal-proxy as $target_name..." + + docker compose $COMPOSE_OPTIONS exec -T kamal-proxy \ + kamal-proxy deploy fizzy \ + --target "$container:3000" \ + --host "fizzy.localhost" + + echo "$container registered successfully!" +} + +# Start command +cmd_start() { + local fizzy_services=$(get_fizzy_services) + local first_fizzy=$(echo "$fizzy_services" | head -n 1) + + # Get GitHub token from gh CLI + echo "Getting GitHub token from gh CLI..." + export GITHUB_TOKEN=$(gh auth token) + + if [ -z "$GITHUB_TOKEN" ]; then + echo "Error: Failed to get GitHub token from gh CLI" + echo "Make sure you're authenticated with: gh auth login" + exit 1 + fi + + # Create tmp directories with proper permissions + echo "Creating temporary directories..." + for service in $fizzy_services; do + mkdir -p "./tmp/beamer-testbed/$service/storage" + mkdir -p "./tmp/beamer-testbed/$service/tmp" + done + + echo "Building Docker image for $first_fizzy..." + docker compose $COMPOSE_OPTIONS build "$first_fizzy" + + echo "Starting services in detached mode..." + docker compose $COMPOSE_OPTIONS down + docker compose $COMPOSE_OPTIONS up -d + + # Wait for and register each fizzy container + for container in $fizzy_services; do + wait_for_container "$container" + register_with_proxy "$container" + done + + echo "" + echo "All containers are ready and registered!" + echo "You can access the application at: http://fizzy.localhost" +} + +# Stop command +cmd_stop() { + echo "Stopping services..." + docker compose $COMPOSE_OPTIONS down + echo "Services stopped." +} + +# Logs command +cmd_logs() { + docker compose $COMPOSE_OPTIONS logs "$@" +} + +# Console command +cmd_console() { + local node="${1:-fizzy-01}" + + echo "Opening Rails console in $node..." + docker compose $COMPOSE_OPTIONS exec "$node" bin/rails console +} + +# Bash command +cmd_bash() { + local node="${1:-fizzy-01}" + + echo "Opening bash shell in $node..." + docker compose $COMPOSE_OPTIONS exec "$node" bash +} + +# Server command (runs inside container) +cmd_server() { + # Check if foreman is installed + if ! command -v foreman >/dev/null 2>&1; then + echo "Foreman not found, installing..." + gem install foreman + fi + + # Clean up any stale .beamer.sock files + echo "Cleaning up stale socket files..." + find . -path '*/.beamer/*/.beamer.sock' -type s -delete 2>/dev/null || true + + echo "Starting web server and beamer with foreman..." + exec foreman start -f Procfile.beamer-testbed +} + +# Run migrations command (runs inside container) +cmd_run_migrations() { + # Store all arguments as BEAMER_OPTIONS + BEAMER_OPTIONS="$*" + + echo "Waiting for beamer to be ready..." + + # Wait for beamer status to succeed + while ! beamer $BEAMER_OPTIONS status >/dev/null 2>&1; do + echo "Beamer not ready yet, waiting..." + sleep 1 + done + + echo "Beamer is ready!" + sleep 1 + + # Check if this is the primary node + if beamer $BEAMER_OPTIONS is-primary; then + echo "This is the primary node, running migrations..." + ./bin/rails db:prepare + echo "Migrations complete!" + else + echo "This is not the primary node, skipping migrations." + fi + + # Wait indefinitely until interrupted + echo "Waiting indefinitely (press Ctrl-C to exit)..." + while true; do + sleep 3600 + done +} + +# Main command dispatch +case "${1:-}" in +start) + cmd_start + ;; +stop) + cmd_stop + ;; +logs) + shift + cmd_logs "$@" + ;; +console) + shift + cmd_console "$@" + ;; +bash) + shift + cmd_bash "$@" + ;; +server) + cmd_server + ;; +run-migrations) + shift + cmd_run_migrations "$@" + ;; +*) + show_usage + ;; +esac diff --git a/config/database.yml b/config/database.yml index d0b4e53ab..96a868d59 100644 --- a/config/database.yml +++ b/config/database.yml @@ -14,7 +14,9 @@ default: &default development: primary: <<: *default - database: storage/tenants/<%= Rails.env %>/%{tenant}/db/main.sqlite3 + <% database_path = "storage/tenants/#{Rails.env}/%{tenant}/db/main.sqlite3" %> + <% database_path = "file:#{database_path}?vfs=beamer" if %w[1 true].include?(ENV["BEAMER"]&.downcase) %> + database: <%= database_path %> tenanted: true untenanted: <<: *default diff --git a/config/environments/development.rb b/config/environments/development.rb index 3d202d140..4606d6e6d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -76,7 +76,7 @@ Rails.application.configure do config.action_mailer.raise_delivery_errors = false end - config.hosts = %w[ fizzy.localhost localhost 127.0.0.1 ] + config.hosts = %w[ fizzy.localhost localhost 127.0.0.1 ] + [ /^fizzy-\d+(:\d+)$/ ] # Set host to be used by links generated in mailer and notification view templates. config.action_controller.default_url_options = { host: config.hosts.first, port: 3006 } diff --git a/config/initializers/beamer.rb b/config/initializers/beamer.rb index 9bc841182..7b5cc182e 100644 --- a/config/initializers/beamer.rb +++ b/config/initializers/beamer.rb @@ -9,11 +9,17 @@ # file will be removed. Rails.application.config.after_initialize do - beamer_extension_path = Rails.root.join("bin/lib/beamer.so") + paths = [ + Rails.root.join("bin/lib/beamer.so"), + Pathname("/usr/local/lib/beamer/beamer.so") + ] - if beamer_extension_path.exist? - db = SQLite3::Database.new ":memory:" - db.enable_load_extension(true) - db.load_extension(beamer_extension_path) + paths.each do |beamer_extension_path| + if beamer_extension_path.exist? + db = SQLite3::Database.new ":memory:" + db.enable_load_extension(true) + db.load_extension(beamer_extension_path) + break + end end end diff --git a/docker-compose.beamer-testbed.yml b/docker-compose.beamer-testbed.yml new file mode 100644 index 000000000..c6d7d1c80 --- /dev/null +++ b/docker-compose.beamer-testbed.yml @@ -0,0 +1,51 @@ +x-app: &app + image: "basecamp/fizzy:dev" + build: + context: . + dockerfile: Dockerfile.dev + secrets: + - GITHUB_TOKEN + volumes: + - .:/rails + environment: + - RAILS_ENV=development + - BINDING=0.0.0.0 + - BEAMER=true + - BEAMER_PRIMARY=fizzy-01 + command: [ "./bin/beamer-testbed", "server" ] + +services: + kamal-proxy: + image: basecamp/kamal-proxy:next + ports: + - 127.0.0.1:80:80 + - 127.0.0.1:443:443 + command: [ "kamal-proxy", "run", "--debug" ] + + fizzy-01: + <<: *app + hostname: fizzy-01 + volumes: + - .:/rails + - ./tmp/beamer-testbed/fizzy-01/storage:/rails/storage + - ./tmp/beamer-testbed/fizzy-01/:/rails/tmp + + fizzy-02: + <<: *app + hostname: fizzy-02 + volumes: + - .:/rails + - ./tmp/beamer-testbed/fizzy-02/storage:/rails/storage + - ./tmp/beamer-testbed/fizzy-02/:/rails/tmp + + fizzy-03: + <<: *app + hostname: fizzy-03 + volumes: + - .:/rails + - ./tmp/beamer-testbed/fizzy-03/storage:/rails/storage + - ./tmp/beamer-testbed/fizzy-03/:/rails/tmp + +secrets: + GITHUB_TOKEN: + environment: GITHUB_TOKEN From d3d0871b7750995611181d79d33fc3159ac337aa Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Mon, 13 Oct 2025 13:48:46 +0200 Subject: [PATCH 13/41] Remove unused COPY --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e26566140..3e21f7f38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,7 +61,6 @@ RUN apt-get update -qq && \ COPY --from=build /usr/local/bundle /usr/local/bundle COPY --from=build /rails /rails COPY --from=beamer /home/beamer/bin/beamer.so /rails/bin/lib/beamer.so -# COPY --from=beamer /home/beamer/bin/beamer /rails/bin/beamer # Run and own only the runtime files as a non-root user for security RUN useradd rails --create-home --shell /bin/bash && \ From c3b336ee1eb1f137316b58a3e41b002fca067d37 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Mon, 13 Oct 2025 13:57:58 +0200 Subject: [PATCH 14/41] Separate temp dirs --- docker-compose.beamer-testbed.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.beamer-testbed.yml b/docker-compose.beamer-testbed.yml index c6d7d1c80..f643604b4 100644 --- a/docker-compose.beamer-testbed.yml +++ b/docker-compose.beamer-testbed.yml @@ -28,7 +28,7 @@ services: volumes: - .:/rails - ./tmp/beamer-testbed/fizzy-01/storage:/rails/storage - - ./tmp/beamer-testbed/fizzy-01/:/rails/tmp + - ./tmp/beamer-testbed/fizzy-01/tmp:/rails/tmp fizzy-02: <<: *app @@ -36,7 +36,7 @@ services: volumes: - .:/rails - ./tmp/beamer-testbed/fizzy-02/storage:/rails/storage - - ./tmp/beamer-testbed/fizzy-02/:/rails/tmp + - ./tmp/beamer-testbed/fizzy-02/tmp:/rails/tmp fizzy-03: <<: *app @@ -44,7 +44,7 @@ services: volumes: - .:/rails - ./tmp/beamer-testbed/fizzy-03/storage:/rails/storage - - ./tmp/beamer-testbed/fizzy-03/:/rails/tmp + - ./tmp/beamer-testbed/fizzy-03/tmp:/rails/tmp secrets: GITHUB_TOKEN: From b016bd7cae19837cc8970d4fce79ce86ac25e914 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Mon, 13 Oct 2025 14:09:37 +0200 Subject: [PATCH 15/41] Fix replicaiton wiht the untenanted DB --- bin/beamer-testbed | 3 +++ config/database.yml | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/bin/beamer-testbed b/bin/beamer-testbed index 8f9b6b64c..a4bdc3dd3 100755 --- a/bin/beamer-testbed +++ b/bin/beamer-testbed @@ -148,6 +148,9 @@ cmd_server() { echo "Cleaning up stale socket files..." find . -path '*/.beamer/*/.beamer.sock' -type s -delete 2>/dev/null || true + mkdir -p ./storage/tenants + mkdir -p ./storage/untenanted + echo "Starting web server and beamer with foreman..." exec foreman start -f Procfile.beamer-testbed } diff --git a/config/database.yml b/config/database.yml index 96a868d59..78a8c31fe 100644 --- a/config/database.yml +++ b/config/database.yml @@ -20,7 +20,9 @@ development: tenanted: true untenanted: <<: *default - database: storage/untenanted/development.sqlite3 + <% database_path = "storage/untenanted/development.sqlite3" %> + <% database_path = "file:#{database_path}?vfs=beamer" if %w[1 true].include?(ENV["BEAMER"]&.downcase) %> + database: <%= database_path %> migrations_paths: db/untenanted_migrate cable: <<: *default From 63458ceb877df510686152a9315ec356442700f6 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 13 Oct 2025 08:47:16 -0400 Subject: [PATCH 16/41] Fix SQLiteBackupsJob to work with AR::Tenanted v0.5.0 ref: https://github.com/basecamp/activerecord-tenanted/pull/215 --- app/jobs/sqlite_backups_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/sqlite_backups_job.rb b/app/jobs/sqlite_backups_job.rb index 10b0efe37..66b7894ca 100644 --- a/app/jobs/sqlite_backups_job.rb +++ b/app/jobs/sqlite_backups_job.rb @@ -92,7 +92,7 @@ class SQLiteBackupsJob < ApplicationJob end def db_path(tenant) - db_config.database_path + db_config.config_adapter.database_path end def db_config From bffaedc604bddf0f12bc51448ae33de85d6883e8 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 13 Oct 2025 08:50:55 -0400 Subject: [PATCH 17/41] dep: Add indirect dependency "benchmark" to quash Ruby 3.5 warnings ``` gems/sniffer-0.5.0/lib/sniffer/adapters/net_http_adapter.rb:4: warning: lib/ruby/3.4.0/benchmark.rb was loaded from the standard library, but will no longer be part of the default gems starting from Ruby 3.5.0. You can add benchmark to your Gemfile or gemspec to silence this warning. Also please contact the author of sniffer-0.5.0 to request adding benchmark into its gemspec. ``` Sadly it looks like sniffer is not really maintained, so I'm adding this here for now. --- Gemfile | 1 + Gemfile.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Gemfile b/Gemfile index ac8f14fa3..083d72afd 100644 --- a/Gemfile +++ b/Gemfile @@ -50,6 +50,7 @@ gem "yabeda-rails" gem "webrick" # required for yabeda-prometheus metrics server 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 "ruby_llm", git: "https://github.com/crmne/ruby_llm.git" diff --git a/Gemfile.lock b/Gemfile.lock index c60db773f..d0c656f0d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -193,6 +193,7 @@ GEM base64 (0.3.0) bcrypt (3.1.20) bcrypt_pbkdf (1.1.1) + benchmark (0.4.1) bigdecimal (3.3.1) bootsnap (1.18.6) msgpack (~> 1.2) @@ -616,6 +617,7 @@ DEPENDENCIES autotuner aws-sdk-s3 bcrypt (~> 3.1.7) + benchmark bootsnap brakeman bundler-audit From b682427df7f83979997f1c6e3fc379c964b9f4fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20=C3=81lvarez?= Date: Mon, 13 Oct 2025 17:59:41 +0200 Subject: [PATCH 18/41] Add AMS to Staging --- config/deploy.staging.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index fe2d3bf01..f1e55be3f 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -3,6 +3,7 @@ servers: hosts: - fizzy-staging-app-01.sc-chi-int.37signals.com - fizzy-staging-app-101.df-iad-int.37signals.com + - fizzy-staging-app-401.df-ams-int.37signals.com labels: otel_scrape_enabled: true jobs: @@ -44,10 +45,14 @@ accessories: hosts: - fizzy-staging-app-01.sc-chi-int.37signals.com - fizzy-staging-app-101.df-iad-int.37signals.com + - fizzy-staging-app-401.df-ams-int.37signals.com load-balancer: image: basecamp/kamal-proxy:lb - host: fizzy-staging-lb-01.sc-chi-int.37signals.com + hosts: + - fizzy-staging-lb-01.sc-chi-int.37signals.com + - fizzy-staging-lb-101.df-iad-int.37signals.com + - fizzy-staging-lb-401.df-ams-int.37signals.com labels: otel_role: load-balancer otel_service: fizzy-load-balancer From 0670645b7f8a6f651d748f9dcdedee8da256d17f Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 13 Oct 2025 12:51:14 -0400 Subject: [PATCH 19/41] dep: update Rails to the branch with the sqlite uri fixes --- Gemfile | 3 ++- Gemfile.lock | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 083d72afd..745645f32 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,8 @@ source "https://rubygems.org" git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" } -gem "rails", github: "rails/rails", branch: "main" +# gem "rails", github: "rails/rails", branch: "main" +gem "rails", github: "flavorjones/rails", branch: "flavorjones/improve-sqlite-url-support" # Assets & front end gem "importmap-rails" diff --git a/Gemfile.lock b/Gemfile.lock index d0c656f0d..a834863d3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -31,9 +31,9 @@ GIT zeitwerk (~> 2) GIT - remote: https://github.com/rails/rails.git - revision: 36da3909403a9ca9b5e9c4a2da9e89bef1500d85 - branch: main + remote: https://github.com/flavorjones/rails.git + revision: aa964b6012c614909d8524a497e3e79b956b3ade + branch: flavorjones/improve-sqlite-url-support specs: actioncable (8.1.0.beta1) actionpack (= 8.1.0.beta1) From ad3a16f8e609a1f6937b368b0dd5bf7bc85c235b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20=C3=81lvarez?= Date: Mon, 13 Oct 2025 19:35:11 +0200 Subject: [PATCH 20/41] Add reader nodes to Staging --- config/deploy.staging.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index f1e55be3f..c6cf2df65 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -2,8 +2,11 @@ servers: web: hosts: - fizzy-staging-app-01.sc-chi-int.37signals.com + - fizzy-staging-app-02.sc-chi-int.37signals.com - fizzy-staging-app-101.df-iad-int.37signals.com + - fizzy-staging-app-102.df-iad-int.37signals.com - fizzy-staging-app-401.df-ams-int.37signals.com + - fizzy-staging-app-402.df-ams-int.37signals.com labels: otel_scrape_enabled: true jobs: @@ -44,8 +47,11 @@ accessories: cmd: beamer run --retention=1h --metrics-port=9000 hosts: - fizzy-staging-app-01.sc-chi-int.37signals.com + - fizzy-staging-app-02.sc-chi-int.37signals.com - fizzy-staging-app-101.df-iad-int.37signals.com + - fizzy-staging-app-102.df-iad-int.37signals.com - fizzy-staging-app-401.df-ams-int.37signals.com + - fizzy-staging-app-402.df-ams-int.37signals.com load-balancer: image: basecamp/kamal-proxy:lb From fb865effac48ba4a74c8747415a46aaa95a2b06b Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 13 Oct 2025 15:39:14 -0500 Subject: [PATCH 21/41] Activity bumps cards to the top of the stream --- app/controllers/collections/columns/streams_controller.rb | 2 +- app/controllers/collections_controller.rb | 2 +- .../public/collections/columns/streams_controller.rb | 2 +- app/controllers/public/collections_controller.rb | 2 +- app/models/card.rb | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/controllers/collections/columns/streams_controller.rb b/app/controllers/collections/columns/streams_controller.rb index 16a2bf671..6ff567c9b 100644 --- a/app/controllers/collections/columns/streams_controller.rb +++ b/app/controllers/collections/columns/streams_controller.rb @@ -2,7 +2,7 @@ class Collections::Columns::StreamsController < ApplicationController include CollectionScoped def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @collection.cards.awaiting_triage.by_last_activity.with_golden_first fresh_when etag: [ @collection, @page.records ] end end diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index d14f26c32..005bf595c 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -51,7 +51,7 @@ class CollectionsController < ApplicationController end def show_columns - set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @collection.cards.awaiting_triage.by_last_activity.with_golden_first fresh_when etag: [ @collection, @page.records, @user_filtering ] end diff --git a/app/controllers/public/collections/columns/streams_controller.rb b/app/controllers/public/collections/columns/streams_controller.rb index 18a90aa83..904cab2e5 100644 --- a/app/controllers/public/collections/columns/streams_controller.rb +++ b/app/controllers/public/collections/columns/streams_controller.rb @@ -6,7 +6,7 @@ class Public::Collections::Columns::StreamsController < ApplicationController layout "public" def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @collection.cards.awaiting_triage.by_last_activity.with_golden_first # To enable caching at intermediate proxies during traffic spikes expires_in 5.seconds, public: true diff --git a/app/controllers/public/collections_controller.rb b/app/controllers/public/collections_controller.rb index 0aacb1f3f..1e208f766 100644 --- a/app/controllers/public/collections_controller.rb +++ b/app/controllers/public/collections_controller.rb @@ -6,7 +6,7 @@ class Public::CollectionsController < ApplicationController layout "public" def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @collection.cards.awaiting_triage.by_last_activity.with_golden_first # To enable caching at intermediate proxies during traffic spikes expires_in 5.seconds, public: true diff --git a/app/models/card.rb b/app/models/card.rb index 2368d10ed..83a96dd6a 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -17,6 +17,7 @@ class Card < ApplicationRecord scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order updated_at: :desc, id: :desc } + scope :by_last_activity, -> { order last_active_at: :desc, id: :desc } scope :indexed_by, ->(index) do case index From 94b5b6fc9e5d9ff57d08e55c22ef49d3f8927f99 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 13 Oct 2025 15:51:35 -0500 Subject: [PATCH 22/41] Activity bumps cards to the top of triaged columns, too --- 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 01f1704bf..30c5d38fd 100644 --- a/app/controllers/collections/columns_controller.rb +++ b/app/controllers/collections/columns_controller.rb @@ -4,7 +4,7 @@ class Collections::ColumnsController < ApplicationController before_action :set_column, only: [ :show, :update, :destroy ] def show - set_page_and_extract_portion_from @column.cards.active.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @column.cards.active.by_last_activity.with_golden_first fresh_when etag: [ @column, @page.records ] end diff --git a/app/controllers/public/collections/columns_controller.rb b/app/controllers/public/collections/columns_controller.rb index c2ed5fba3..bd9341234 100644 --- a/app/controllers/public/collections/columns_controller.rb +++ b/app/controllers/public/collections/columns_controller.rb @@ -8,7 +8,7 @@ class Public::Collections::ColumnsController < ApplicationController before_action :set_column, only: :show def show - set_page_and_extract_portion_from @column.cards.active.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @column.cards.active.by_last_activity.with_golden_first # To enable caching at intermediate proxies during traffic spikes expires_in 5.seconds, public: true From d266f1bbfffdaea6080caf01f6671b1c98ed8b57 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 13 Oct 2025 16:34:13 -0400 Subject: [PATCH 23/41] beamer-testbed runs on host port 3006 --- bin/beamer-testbed | 2 +- docker-compose.beamer-testbed.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/beamer-testbed b/bin/beamer-testbed index a4bdc3dd3..679047bb1 100755 --- a/bin/beamer-testbed +++ b/bin/beamer-testbed @@ -105,7 +105,7 @@ cmd_start() { echo "" echo "All containers are ready and registered!" - echo "You can access the application at: http://fizzy.localhost" + echo "You can access the application at: http://fizzy.localhost:3006" } # Stop command diff --git a/docker-compose.beamer-testbed.yml b/docker-compose.beamer-testbed.yml index f643604b4..fcc26c205 100644 --- a/docker-compose.beamer-testbed.yml +++ b/docker-compose.beamer-testbed.yml @@ -18,7 +18,7 @@ services: kamal-proxy: image: basecamp/kamal-proxy:next ports: - - 127.0.0.1:80:80 + - 127.0.0.1:3006:80 - 127.0.0.1:443:443 command: [ "kamal-proxy", "run", "--debug" ] From 8c60986362bd904f3e25b680ab04f37fe5094c50 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 13 Oct 2025 16:54:41 -0400 Subject: [PATCH 24/41] beamer-testbed can connect to queenbee on the host to set up new tenants --- docker-compose.beamer-testbed.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.beamer-testbed.yml b/docker-compose.beamer-testbed.yml index fcc26c205..f4b474718 100644 --- a/docker-compose.beamer-testbed.yml +++ b/docker-compose.beamer-testbed.yml @@ -7,6 +7,8 @@ x-app: &app - GITHUB_TOKEN volumes: - .:/rails + extra_hosts: + - queenbee.localhost:host-gateway environment: - RAILS_ENV=development - BINDING=0.0.0.0 From 7a4e885fd562cee66635b32d856c1280a971bac0 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 13 Oct 2025 16:54:27 -0400 Subject: [PATCH 25/41] beamer-testbed configures fizzy-01 to be the writer and everything else is listed as a read-target. Note that we need to set SECRET_BASE so all the app instances agree --- bin/beamer-testbed | 31 +++++++++++++++++++++---------- docker-compose.beamer-testbed.yml | 1 + 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/bin/beamer-testbed b/bin/beamer-testbed index 679047bb1..401f1720d 100755 --- a/bin/beamer-testbed +++ b/bin/beamer-testbed @@ -21,7 +21,7 @@ show_usage() { # Get all fizzy-* services from docker compose get_fizzy_services() { - fizzy_services=$(docker compose $COMPOSE_OPTIONS config --services | grep '^fizzy-') + fizzy_services=$(docker compose $COMPOSE_OPTIONS config --services | grep '^fizzy-' | sort) if [ -z "$fizzy_services" ]; then echo "Error: No fizzy-* services found in docker-compose file" @@ -53,19 +53,30 @@ wait_for_container() { return 1 } -# Function to register a container with kamal-proxy -register_with_proxy() { - local container=$1 - local target_name="fizzy-${container##*-}" +# Function to configure kamal-proxy +# TODO: set up the other containers as read targets +configure_proxy() { + local writer=$1 + local readers=${@:2} - echo "Registering $container with kamal-proxy as $target_name..." + echo "Configuring kamal-proxy..." + + local writer_target="$writer:3000" + local reader_target="" + for reader in $readers; do + if [ -n "$reader_target" ]; then + reader_target+="," + fi + reader_target+="$reader:3000" + done docker compose $COMPOSE_OPTIONS exec -T kamal-proxy \ kamal-proxy deploy fizzy \ - --target "$container:3000" \ - --host "fizzy.localhost" + --target $writer_target \ + --read-target $reader_target \ + --host fizzy.localhost - echo "$container registered successfully!" + echo "Configured kamal-proxy with target=${writer_target} and read-target=${reader_target}" } # Start command @@ -100,8 +111,8 @@ cmd_start() { # Wait for and register each fizzy container for container in $fizzy_services; do wait_for_container "$container" - register_with_proxy "$container" done + configure_proxy $fizzy_services echo "" echo "All containers are ready and registered!" diff --git a/docker-compose.beamer-testbed.yml b/docker-compose.beamer-testbed.yml index f4b474718..5b034c64c 100644 --- a/docker-compose.beamer-testbed.yml +++ b/docker-compose.beamer-testbed.yml @@ -14,6 +14,7 @@ x-app: &app - BINDING=0.0.0.0 - BEAMER=true - BEAMER_PRIMARY=fizzy-01 + - SECRET_KEY_BASE=beamer_testbed_shared_secret_key_base_for_development_only command: [ "./bin/beamer-testbed", "server" ] services: From b49275508d70ad5d37d4b5e946af21199c864a4b Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 13 Oct 2025 16:05:27 -0500 Subject: [PATCH 26/41] `esc` clears filters --- app/views/filters/settings/_manage.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/filters/settings/_manage.html.erb b/app/views/filters/settings/_manage.html.erb index 4a3e8dc17..8a13490ad 100644 --- a/app/views/filters/settings/_manage.html.erb +++ b/app/views/filters/settings/_manage.html.erb @@ -3,7 +3,7 @@
<%= render "filters/filter_toggle", filter: filter %> - <%= link_to no_filtering_url, class: "btn btn--remove txt-x-small" do %> + <%= link_to no_filtering_url, class: "btn btn--remove txt-x-small", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click"} do %> <%= icon_tag "close" %> Clear all <% end %> From 679b4d2c946c611cef05d81e196ce3ad9dce7ba9 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 13 Oct 2025 16:24:57 -0500 Subject: [PATCH 27/41] Focus filter input with a hotkey, adjust sizing --- app/assets/stylesheets/filters.css | 13 ++++++++++--- app/javascript/controllers/hotkey_controller.js | 7 +++++++ app/views/filters/settings/_terms.html.erb | 5 +++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/filters.css b/app/assets/stylesheets/filters.css index 5999d2598..0ca86f4ee 100644 --- a/app/assets/stylesheets/filters.css +++ b/app/assets/stylesheets/filters.css @@ -106,12 +106,19 @@ .filter__terms:is(.input) { --input-background: var(--color-canvas); --input-border-radius: 5em; - --input-padding: 0.5em 2.3em 0.5em 1.3em; + --input-padding: 0.5em 1.3em; + --input-width: 24ch; + --collapsed-filter-space: calc(var(--btn-size) + var(--inline-space-half) + 0.25em); - inline-size: 25ch; - min-inline-size: 25ch; + inline-size: var(--input-width); + min-inline-size: var(--input-width); .filters:not(.filters--expanded, .filters--has-filters-set) & { + --input-padding: 0.5em 2.7em 0.5em 1.3em; + + inline-size: calc(var(--input-width) + var(--collapsed-filter-space)); + min-inline-size: calc(var(--input-width) + var(--collapsed-filter-space)); + margin-inline-end: calc((var(--btn-size) + var(--inline-space-half) + 0.25em) * -1); } } diff --git a/app/javascript/controllers/hotkey_controller.js b/app/javascript/controllers/hotkey_controller.js index a6b8192c9..813c69ef6 100644 --- a/app/javascript/controllers/hotkey_controller.js +++ b/app/javascript/controllers/hotkey_controller.js @@ -8,6 +8,13 @@ export default class extends Controller { } } + focus(event) { + if (this.#isClickable && !this.#shouldIgnore(event)) { + event.preventDefault() + this.element.focus() + } + } + #shouldIgnore(event) { return event.defaultPrevented || event.target.closest("input, textarea, lexxy-editor") } diff --git a/app/views/filters/settings/_terms.html.erb b/app/views/filters/settings/_terms.html.erb index 0d2f48b4e..dbc005b02 100644 --- a/app/views/filters/settings/_terms.html.erb +++ b/app/views/filters/settings/_terms.html.erb @@ -1,8 +1,9 @@ -<%= form.search_field "terms[]", placeholder: "Filter these cards…", class: "filter__terms input txt-x-small", +<%= form.search_field "terms[]", placeholder: "Filter these cards… [F]", class: "filter__terms input txt-x-small", autofocus: false, autocomplete: :off, autocorrect: "off", data: { "1p-ignore": "true", + controller: "hotkey", filter_settings_target: "field", - action: "input->filter-settings#resetIfNoFiltering input->form#debouncedSubmit keydown.enter->form#submitToTopTarget" } %> + action: "keydown.f@document->hotkey#focus input->filter-settings#resetIfNoFiltering input->form#debouncedSubmit keydown.enter->form#submitToTopTarget" } %> <% if filter.terms.present? %> <% filter.terms.each do |term| %> From 03dcfe8964463387ab6a3b16962b3f76c12d3a1c Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 13 Oct 2025 16:25:50 -0500 Subject: [PATCH 28/41] Style --- app/assets/stylesheets/filters.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/filters.css b/app/assets/stylesheets/filters.css index 0ca86f4ee..9ae1a4b3b 100644 --- a/app/assets/stylesheets/filters.css +++ b/app/assets/stylesheets/filters.css @@ -117,9 +117,8 @@ --input-padding: 0.5em 2.7em 0.5em 1.3em; inline-size: calc(var(--input-width) + var(--collapsed-filter-space)); - min-inline-size: calc(var(--input-width) + var(--collapsed-filter-space)); - margin-inline-end: calc((var(--btn-size) + var(--inline-space-half) + 0.25em) * -1); + min-inline-size: calc(var(--input-width) + var(--collapsed-filter-space)); } } From 8d82a1557b6bb0fdc82883f2186a71845870ef8e Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 13 Oct 2025 16:55:51 -0500 Subject: [PATCH 29/41] Allow selecting default color - Adjust swatch layout to be more resilliant and space efficient - Select text on focus - Update placeholder --- app/assets/stylesheets/color-picker.css | 44 +++++-------------- .../show/menu/_column_form.html.erb | 12 ++--- 2 files changed, 16 insertions(+), 40 deletions(-) diff --git a/app/assets/stylesheets/color-picker.css b/app/assets/stylesheets/color-picker.css index 7519bfe50..af4601181 100644 --- a/app/assets/stylesheets/color-picker.css +++ b/app/assets/stylesheets/color-picker.css @@ -1,40 +1,16 @@ @layer components { - .color-picker { - --color-picker-origin: calc(var(--btn-size) / 2 + var(--color-picker-spacer)); - --color-picker-spacer: 0.5ch; - - background-color: var(--color-canvas); - border-radius: calc(var(--btn-size) / 2 + var(--color-picker-spacer)); - gap: var(--color-picker-spacer); - inline-size: auto !important; - inset: calc(-1 * var(--color-picker-spacer)) auto auto calc(-1 * var(--color-picker-spacer)); - max-inline-size: var(--panel-size) !important; - padding: var(--color-picker-spacer); - position: absolute; - transform: scale(0.5); - transform-origin: var(--color-picker-origin) var(--color-picker-origin); - transition: 150ms allow-discrete; - transition-property: display, opacity, overlay, transform; - z-index: var(--z-popup);opacity: 0; - - &[open] { - display: flex; - opacity: 1; - transform: scale(1); - } - - @starting-style { - &[open] { - opacity: 0; - transform: scale(0.5); - } - } - } - .color-picker__colors { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: var(--color-picker-spacer); + grid-template-columns: repeat(3, 1fr); + gap: var(--inline-space-half); + + .btn { + --btn-border-radius: 0.1em; + --btn-size: 2em; + --icon-size: 1.3em; + + inline-size: 100%; + } } } diff --git a/app/views/collections/show/menu/_column_form.html.erb b/app/views/collections/show/menu/_column_form.html.erb index 0b4cb854c..1382d4745 100644 --- a/app/views/collections/show/menu/_column_form.html.erb +++ b/app/views/collections/show/menu/_column_form.html.erb @@ -1,10 +1,10 @@ <%= form_with model: [collection, column], data: { controller: "form", action: "turbo:submit-end->dialog#close turbo:submit-end->form#reset" } do |form| %> - <%= form.text_field :name, class: "input", placeholder: "Column name", value: column.name, - required: true, autocomplete: "off", pattern: ".*\\S.*", title: "Column name cannot be blank" %> -
- <% Card::COLORS.each do |color| %> -