Merge branch 'main' into tooltip-safari-fix

* main: (41 commits)
  Mount our backup vol.
  "Postpones" -> "Closes in"
  Prevent view transition animations on page refreshes
  dep: update AR::Tenanted to allow config adapter registration
  More fine-grained approach to listen for broadcasted refresh signals
  Reuse existing cache invalidation logic that replaced this helper
  Show completed cards when filtering cards by creator
  Refactor to remove workflows styles
  Display column when viewing cards in a grid (filtering)
  Dial back title size on perma
  Add bubble when The Stream has bubbled cards
  This got too wide in the collapsed state
  Allow selecting default color
  Style
  Focus filter input with a hotkey, adjust sizing
  `esc` clears filters
  beamer-testbed configures fizzy-01 to be the writer
  beamer-testbed can connect to queenbee on the host
  beamer-testbed runs on host port 3006
  Activity bumps cards to the top of triaged columns, too
  ...
This commit is contained in:
Andy Smith
2025-10-14 15:20:04 -05:00
57 changed files with 645 additions and 269 deletions
+27 -2
View File
@@ -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 "$@"
+3 -1
View File
@@ -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
+46
View File
@@ -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"]
+5 -2
View File
@@ -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"
@@ -10,7 +11,7 @@ gem "stimulus-rails"
gem "turbo-rails"
# Deployment and drivers
gem "activerecord-tenanted"
gem "activerecord-tenanted", bc: "activerecord-tenanted"
gem "bootsnap", require: false
gem "kamal", require: false
gem "puma", ">= 5.0"
@@ -40,6 +41,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"
@@ -49,6 +51,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"
+30 -16
View File
@@ -1,3 +1,12 @@
GIT
remote: https://github.com/basecamp/activerecord-tenanted
revision: f412b25ff0096064a4763dc0c8d21b6593678137
specs:
activerecord-tenanted (0.5.0)
activerecord (>= 8.1.beta)
railties (>= 8.1.beta)
zeitwerk
GIT
remote: https://github.com/basecamp/queenbee-plugin
revision: eb01c697de1ad028afc65cc7d9b5345a7a8e849f
@@ -31,9 +40,9 @@ GIT
zeitwerk (~> 2)
GIT
remote: https://github.com/rails/rails.git
revision: d8795bd762ab2716b6e7fd07a403566b28159cdc
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)
@@ -96,12 +105,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)
@@ -157,10 +166,6 @@ GEM
activemodel (>= 5.0.0.a)
activesupport (>= 5.0.0.a)
builder (~> 3.1)
activerecord-tenanted (0.4.1)
activerecord (>= 8.1.beta)
railties (>= 8.1.beta)
zeitwerk
activeresource (6.2.0)
activemodel (>= 7.0)
activemodel-serializers-xml (~> 1.0)
@@ -172,7 +177,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)
@@ -194,7 +199,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 +234,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
@@ -276,7 +281,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)
@@ -410,7 +415,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)
@@ -480,9 +485,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)
@@ -500,7 +507,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)
@@ -576,6 +583,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)
@@ -605,11 +617,12 @@ PLATFORMS
x86_64-mingw32
DEPENDENCIES
activerecord-tenanted
activerecord-tenanted!
activeresource
autotuner
aws-sdk-s3
bcrypt (~> 3.1.7)
benchmark
bootsnap
brakeman
bundler-audit
@@ -655,6 +668,7 @@ DEPENDENCIES
webmock
webrick
yabeda
yabeda-actioncable
yabeda-activejob!
yabeda-gc
yabeda-http_requests
+3
View File
@@ -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/
+64 -2
View File
@@ -142,6 +142,18 @@
.card {
inline-size: calc((100% - var(--cards-gap) * (var(--card-grid-columns) - 1) ) / var(--card-grid-columns));
}
.card__header .card__column-name--current {
--btn-padding: 0.1em 0.5em;
background: none;
border: 1px solid currentColor;
color: var(--card-color);
display: inline-flex;
flex: 0 1 auto;
inline-size: fit-content;
margin: 0 0 0 auto;
}
}
/* Column Elements
@@ -261,7 +273,7 @@
writing-mode: vertical-rl;
}
.cards:not(.is-collapsed) & {
.cards:not(.is-collapsed, .cards--considering) & {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -399,7 +411,7 @@
.card:has(.card__background img:not([src=""])) {
.card__content,
.card__meta,
.workflow-stage:not(.workflow-stage--current) {
.card__column-name:not(.card__column-name--current) {
opacity: 0;
transition: opacity 0.2s ease-in-out;
@@ -651,6 +663,7 @@
/* -------------------------------------------------------------------------- */
/* Surface a mini bubble if there are cards with bubbles inside */
.cards--considering:has(.bubble:not([hidden])) .cards__expander-title,
.cards--doing.is-collapsed:has(.bubble:not([hidden])) {
--bubble-color: var(--card-color, oklch(var(--lch-blue-medium)));
--bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%;
@@ -670,4 +683,53 @@
z-index: 1;
}
}
.cards--considering:has(.bubble:not([hidden])) .cards__expander-title:before {
translate: 120% 50%;
}
/* Card column indicators
/* -------------------------------------------------------------------------- */
.card__column-name {
--btn-background: transparent;
--btn-padding: 0.2em 0.5em;
--btn-border-size: 0;
--btn-border-radius: 0.2em;
color: inherit;
inline-size: 100%;
justify-content: flex-start;
text-transform: uppercase;
@media (hover: hover) {
&:hover {
--btn-background: color(from var(--card-color) srgb r g b / 0.15);
}
}
}
.card__column-name--current {
--btn-background: var(--card-color);
color: var(--color-ink-inverted);
@media (hover: hover) {
&:hover {
--btn-background: var(--card-color);
}
}
}
@supports(mask: radial-gradient(black)) {
.card__column-name--stream:before {
background: white;
content: "";
inset: 0;
mask: var(--wavy-background-mask);
position: absolute;
z-index: 0;
opacity: 0.25;
}
}
}
+2 -2
View File
@@ -52,7 +52,7 @@
}
.card__title {
font-size: clamp(var(--text-medium), 6vw, var(--text-xx-large));
font-size: clamp(var(--text-medium), 6vw, var(--text-x-large));
}
.card__meta,
@@ -74,7 +74,7 @@
}
}
.workflow-stage {
.card__column-name {
@media (max-width: 639px) {
--btn-border-radius: 0;
}
+1 -1
View File
@@ -50,7 +50,7 @@
max-inline-size: unset;
min-inline-size: 0;
.workflow-stage {
.card__column-name {
display: none;
}
}
+10 -34
View File
@@ -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%;
}
}
}
+9 -3
View File
@@ -106,13 +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) + (0.25 * 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) + (0.25 * var(--collapsed-filter-space)));
}
}
+1 -1
View File
@@ -423,7 +423,7 @@
margin-block-start: 0.3em;
}
.workflow-stage--current {
.card__column-name--current {
--btn-padding: 0.1em 0.5em;
background: none !important;
-101
View File
@@ -1,101 +0,0 @@
@layer components {
.workflow-stage {
--btn-background: transparent;
--btn-padding: 0.2em 0.5em;
--btn-border-size: 0;
--btn-border-radius: 0.2em;
color: inherit;
inline-size: 100%;
justify-content: flex-start;
text-transform: uppercase;
@media (hover: hover) {
&:hover {
--btn-background: color(from var(--card-color) srgb r g b / 0.15);
}
}
}
.workflow-stage--current {
--btn-background: var(--card-color);
color: var(--color-ink-inverted);
@media (hover: hover) {
&:hover {
--btn-background: var(--card-color);
}
}
}
@supports(mask: radial-gradient(black)) {
.workflow-stage--stream:before {
background: white;
content: "";
inset: 0;
mask: var(--wavy-background-mask);
position: absolute;
z-index: 0;
opacity: 0.25;
}
}
/* Workflow settings
/* ------------------------------------------------------------------------ */
.workflow-preview {
--border-size: 0.2em;
aspect-ratio: 1;
border: 0.2em solid var(--color-ink-lighter);
flex: 1 1 30%;
max-inline-size: 30%;
@media (any-hover: hover) {
&:hover:not(.workflow-preview--selected) {
border-color: var(--color-ink-light);
}
}
button {
appearance: none;
aspect-ratio: 1;
background: transparent;
block-size: 100%;
border: none;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 0.5em;
inline-size: 100%;
margin: 0;
max-inline-size: 100%;
outline: 0;
overflow: hidden;
padding: var(--inline-space);
text-align: start;
}
}
.workflow-preview--selected {
border-color: var(--color-link);
}
.workflow-preview__swatch {
--btn-size: 1em;
--btn-border-radius: 50%;
--btn-border-size: 0;
cursor: auto;
}
.workflow__swatch-btn {
@media (any-hover: hover) {
&:hover {
filter: brightness(0.9);
}
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
class ApplicationController < ActionController::Base
include Authentication, Authorization, CurrentRequest, CurrentTimezone, SetPlatform, TurboFlash, WriterAffinity
include Authentication, Authorization, CurrentRequest, CurrentTimezone, SetPlatform, TurboFlash, ViewTransitions, WriterAffinity
stale_when_importmap_changes
allow_browser versions: :modern, block: -> { render "errors/not_acceptable", layout: "error" }
@@ -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
@@ -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
+1 -1
View File
@@ -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
@@ -0,0 +1,16 @@
module ViewTransitions
extend ActiveSupport::Concern
included do
before_action :disable_view_transitions, if: :page_refresh?
end
private
def disable_view_transitions
@disable_view_transition = true
end
def page_refresh?
request.referrer.present? && request.referrer == request.url
end
end
@@ -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
@@ -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
@@ -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
-4
View File
@@ -33,8 +33,4 @@ module CardsHelper
title << "assigned to #{card.assignees.map(&:name).to_sentence}" if card.assignees.any?
title.join(" ")
end
def cacheable_preview_parts_for(card, *options)
[ card, card.collection, card.collection.entropy_configuration, card.collection.publication, card.column&.color, *options ]
end
end
+5
View File
@@ -0,0 +1,5 @@
module CollectionsHelper
def referenced_collections_from(records)
Current.user.collections.where id: records.pluck(:collection_id).uniq
end
end
+1 -1
View File
@@ -4,7 +4,7 @@ module ColumnsHelper
tag.span(column.name, class: "overflow-ellipsis"),
card_triage_path(card, column_id: column),
method: :post,
class: [ "workflow-stage btn", { "workflow-stage--current": column == card.column } ],
class: [ "card__column-name btn", { "card__column-name--current": column == card.column } ],
form_class: "flex align-center gap-half",
data: { turbo_frame: "_top" }
end
+1 -1
View File
@@ -7,7 +7,7 @@ module EntropyHelper
{
daysBeforeReminder: card.entropy.days_before_reminder,
closesAt: card.entropy.auto_clean_at.iso8601,
action: "Postpones"
action: "Closes"
}
end
@@ -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")
}
+1 -1
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -65,6 +65,6 @@ class Filter < ApplicationRecord
private
def include_closed_cards?
only_closed? || card_ids.present?
only_closed? || card_ids.present? || creator_ids.present?
end
end
+3
View File
@@ -0,0 +1,3 @@
<% referenced_collections_from(records).each do |collection| %>
<%= turbo_stream_from collection %>
<% end %>
+1 -1
View File
@@ -22,7 +22,7 @@
</details>
<% if card.entropic? && card.open? %>
<div class="card-perma__closure-message">
Moves to "Not now" if no activity <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%>.
Closes as “Not Now <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if theres no activity.
</div>
<% end %>
<% end %>
+1 -1
View File
@@ -9,7 +9,7 @@
<%= icon_tag "attachment", class: "card__attachments-indicator translucent" if card.has_attachments? %>
<% if card.triaged? %>
<span class="btn justify-start workflow-stage workflow-stage--current txt-uppercase min-width max-width">
<span class="btn justify-start card__column-name card__column-name--current txt-uppercase min-width max-width">
<span class="overflow-ellipsis "><%= card.column.name %></span>
</span>
<% end %>
@@ -1,5 +1,5 @@
<div class="card__stages">
<% card.collection.columns.each do |column| %>
<%= tag.span column.name, class: ["workflow-stage btn overflow-ellipsis", { "workflow-stage--current": column == card.column }] %>
<%= tag.span column.name, class: ["card__column-name btn overflow-ellipsis", { "card__column-name--current": column == card.column }] %>
<% end %>
</div>
@@ -1,6 +1,6 @@
<div id="<%= dom_id(card, :columns) %>" class="card__stages">
<% card.collection.columns.each do |column| %>
<%= tag.div column.name,
class: ["workflow-stage overflow-ellipsis flex align-center gap-half btn non-clickable no-hover", { "workflow-stage--current": column == card.column }] %>
class: ["card__column-name overflow-ellipsis flex align-center gap-half btn non-clickable no-hover", { "card__column-name--current": column == card.column }] %>
<% end %>
</div>
+2 -2
View File
@@ -1,7 +1,7 @@
<% @page_title = @user_filtering.selected_collections_label %>
<% turbo_exempts_page_from_cache %>
<%= render "filters/broadcasts", filter: @filter %>
<%= render "cards/broadcasts", records: @page.records %>
<% content_for :head do %>
<%= tag.meta property: "og:title", content: @page_title %>
@@ -28,7 +28,7 @@
<%= turbo_frame_tag :cards_container do %>
<section class="cards cards--grid">
<%= with_automatic_pagination :cards_paginated_container, @page do %>
<%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { cacheable_preview_parts_for(card) } %>
<%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { card.cache_invalidation_parts.for_preview } %>
<% end %>
</section>
<% end %>
+3 -3
View File
@@ -2,13 +2,13 @@
<legend class="for-screen-reader">Choose a column for this card</legend>
<%= button_to "Not now", card_not_now_path(card),
class: [ "workflow-stage btn", { "workflow-stage--current": card.postponed? } ],
class: [ "card__column-name btn", { "card__column-name--current": card.postponed? } ],
role: "radio",
aria: { checked: card.postponed? },
form_class: "flex align-center gap-half" %>
<%= button_to "The Stream", card_triage_path(card), method: :delete,
class: [ "workflow-stage workflow-stage--stream btn", { "workflow-stage--current": card.awaiting_triage? } ],
class: [ "card__column-name card__column-name--stream btn", { "card__column-name--current": card.awaiting_triage? } ],
role: "radio",
aria: { checked: card.awaiting_triage? },
form_class: "flex align-center gap-half" %>
@@ -18,7 +18,7 @@
<% end %>
<%= button_to "Done", card_closure_path(card, reason: "Completed"),
class: [ "workflow-stage btn", { "workflow-stage--current": card.closed? } ],
class: [ "card__column-name btn", { "card__column-name--current": card.closed? } ],
role: "radio",
aria: { checked: card.closed? },
form_class: "flex align-center gap-half" %>
@@ -1,5 +1,5 @@
<section class="cards cards--grid">
<%= with_automatic_pagination :filtered_cards_paginated_container, page do %>
<%= render partial: "cards/display/preview", collection: page.records, as: :card, locals: { draggable: true }, cached: ->(card) { cacheable_preview_parts_for(card) } %>
<%= render partial: "cards/display/preview", collection: page.records, as: :card, locals: { draggable: true }, cached: ->(card) { card.cache_invalidation_parts.for_preview } %>
<% end %>
</section>
+1 -1
View File
@@ -5,7 +5,7 @@
<div class="cards__decoration"></div>
<header class="cards__expander">
<h2 class="cards__expander-title">The Stream</h2>
<h2 class="cards__expander-title position-relative">The Stream</h2>
<%= link_to collection_columns_stream_path(collection), class: "btn btn--circle txt-x-small borderless cards__expander-button", data: { turbo_frame: "_top" } do %>
<%= icon_tag "expand", class: "translucent" %>
<span class="for-screen-reader">Expand column</span>
@@ -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" %>
<div class="flex flex-wrap gap-half justify-space-between margin-block-half">
<% Card::COLORS.each do |color| %>
<label class="btn btn--circle txt-small borderless" style="--btn-background: <%= color %>" title="<%= color %>">
<%= form.radio_button :color, color, checked: (column.color == color) %>
<%= form.text_field :name, class: "input", placeholder: "Name this column", value: column.name,
required: true, autocomplete: "off", pattern: ".*\\S.*", title: "Column name cannot be blank", data: { action: "focus->form#select" } %>
<div class="color-picker__colors">
<% ([Card::DEFAULT_COLOR] + Card::COLORS).each do |color| %>
<label class="btn txt-small borderless" style="--btn-background: <%= color %>" title="<%= color %>">
<%= form.radio_button :color, color, checked: (column.color == color || (column.new_record? && color == Card::DEFAULT_COLOR)) %>
<%= icon_tag "check", class: "checked" %>
<span class="for-screen-reader"><%= color %></span>
</label>
+1 -1
View File
@@ -1,7 +1,7 @@
<% @page_title = "Home" %>
<% @header_class = "header--events" %>
<%= render "filters/broadcasts", filter: @filter %>
<%= render "cards/broadcasts", records: @day_timeline.events %>
<% content_for :header do %>
<%= render "filters/menu" %>
-7
View File
@@ -1,7 +0,0 @@
<% if filter.collections.any? %>
<% filter.collections.each do |collection| %>
<%= turbo_stream_from collection %>
<% end %>
<% else %>
<%= turbo_stream_from :collections %>
<% end %>
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="filters__manage gap-half">
<%= 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" %>
<span class="for-screen-reader">Clear all</span>
<% end %>
+3 -2
View File
@@ -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| %>
+4 -1
View File
@@ -2,7 +2,10 @@
<%= page_title_tag %>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="view-transition" content="same-origin">
<% unless @disable_view_transition %>
<meta name="view-transition" content="same-origin">
<% end %>
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
@@ -3,7 +3,7 @@
<div class="cards__decoration"></div>
<header class="cards__expander">
<h2 class="cards__expander-title">The Stream</h2>
<h2 class="cards__expander-title position-relative"">The Stream</h2>
</header>
<% if page.used? %>
+231
View File
@@ -0,0 +1,231 @@
#!/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-' | sort)
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 configure kamal-proxy
# TODO: set up the other containers as read targets
configure_proxy() {
local writer=$1
local readers=${@:2}
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 $writer_target \
--read-target $reader_target \
--host fizzy.localhost
echo "Configured kamal-proxy with target=${writer_target} and read-target=${reader_target}"
}
# 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"
done
configure_proxy $fizzy_services
echo ""
echo "All containers are ready and registered!"
echo "You can access the application at: http://fizzy.localhost:3006"
}
# 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
mkdir -p ./storage/tenants
mkdir -p ./storage/untenanted
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
+2 -12
View File
@@ -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
+6 -2
View File
@@ -14,11 +14,15 @@ 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
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
+6 -6
View File
@@ -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
+10 -6
View File
@@ -1,13 +1,17 @@
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
options:
# NFS mount for backups
# See https://3.basecamp.com/2914079/buckets/37331921/todos/9175385847
mount: type=volume,src=backups,dst=/backups,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/fizzy-production-backups,"volume-opt=o=addr=purestorage.sc-chi-int.37signals.com,nfsvers=3,rw,noatime,nconnect=8,soft,timeo=30,retrans=2"
labels:
otel_scrape_enabled: true
@@ -42,12 +46,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
+17 -6
View File
@@ -1,13 +1,17 @@
servers:
web:
hosts:
- fizzy-staging-app-01
- fizzy-staging-app-101
- 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:
hosts:
- fizzy-staging-app-01
- fizzy-staging-app-01.sc-chi-int.37signals.com
labels:
otel_scrape_enabled: true
@@ -42,12 +46,19 @@ 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-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
host: fizzy-staging-lb-01
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
+1 -1
View File
@@ -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 }
+11 -5
View File
@@ -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
+5
View File
@@ -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
+2 -2
View File
@@ -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
+29 -19
View File
@@ -1,31 +1,41 @@
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"
class: Webhook::CleanupDeliveriesJob
schedule: every 4 hours
sqlite_backups:
class: SQLiteBackupsJob
schedule: every day at 05:00
# Metrics
yabeda_actioncable:
command: "Yabeda::ActionCable.measure"
schedule: every 60 seconds
beta: *production
staging: *production
+54
View File
@@ -0,0 +1,54 @@
x-app: &app
image: "basecamp/fizzy:dev"
build:
context: .
dockerfile: Dockerfile.dev
secrets:
- GITHUB_TOKEN
volumes:
- .:/rails
extra_hosts:
- queenbee.localhost:host-gateway
environment:
- RAILS_ENV=development
- 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:
kamal-proxy:
image: basecamp/kamal-proxy:next
ports:
- 127.0.0.1:3006: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/tmp:/rails/tmp
fizzy-02:
<<: *app
hostname: fizzy-02
volumes:
- .:/rails
- ./tmp/beamer-testbed/fizzy-02/storage:/rails/storage
- ./tmp/beamer-testbed/fizzy-02/tmp:/rails/tmp
fizzy-03:
<<: *app
hostname: fizzy-03
volumes:
- .:/rails
- ./tmp/beamer-testbed/fizzy-03/storage:/rails/storage
- ./tmp/beamer-testbed/fizzy-03/tmp:/rails/tmp
secrets:
GITHUB_TOKEN:
environment: GITHUB_TOKEN