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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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/
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
max-inline-size: unset;
|
||||
min-inline-size: 0;
|
||||
|
||||
.workflow-stage {
|
||||
.card__column-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
module CollectionsHelper
|
||||
def referenced_collections_from(records)
|
||||
Current.user.collections.where id: records.pluck(:collection_id).uniq
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<% referenced_collections_from(records).each do |collection| %>
|
||||
<%= turbo_stream_from collection %>
|
||||
<% end %>
|
||||
@@ -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 there’s no activity.
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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" %>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<% if filter.collections.any? %>
|
||||
<% filter.collections.each do |collection| %>
|
||||
<%= turbo_stream_from collection %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= turbo_stream_from :collections %>
|
||||
<% end %>
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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| %>
|
||||
|
||||
@@ -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? %>
|
||||
|
||||
Executable
+231
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user