Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .claude/skills/verify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
name: verify
description: Drive Motzi's running dev server end-to-end to verify changes — login shortcuts, order flows, admin, jobs, emails
---

# Verifying Motzi changes at runtime

## Handle

- `bin/dev` runs foreman: Rails web + `bun run build:watch` + Solid Queue worker. If port 3000 is busy it offers an alternate port — check `lsof -iTCP:3001 -sTCP:LISTEN` before starting another server.
- For a throwaway server: `bun run build` (populates `app/assets/builds`), then `DISABLE_SPRING=1 bin/rails server -p <port>`. Worktrees need `bun install` first (no shared node_modules).
- Auth: `GET /dev/login_as_admin` signs in as admin — works for both curl cookie jars (`curl -c jar -L`) and Playwright.
- Admin pages need `-H "Accept: text/html"` with curl or they 302.

## Flows worth driving

- **Member order**: `/menu` (React) → pick a day on an item → Add to cart → Submit/Update Order. Credits counter must move; "We've got your order!" confirms. Edit Order → × an item → Update refunds the credit.
- **Guest buy**: `/menu` logged out shows cash prices; cart total fills the "You pay $" box; tip % updates the charge button. Card charge stays disabled in dev — no `STRIPE_PUBLISHABLE_KEY` in `.env` (expect 2 Stripe IntegrationErrors in console; pre-existing, ignore, along with 1 React hooks-order warning in Layout).
- **Admin**: `/admin/orders` (find the order), `/admin/orders/<id>/edit` → Update round-trip. `/admin/activity_feed`, `/admin/error_events` (+ `.txt` format), `/admin/uptime_checks`, engines `/blazer`, `/jobs`, `/letter_opener`.
- **Email pipeline**: order updates enqueue `ActionMailer::MailDeliveryJob`; confirm delivery in `/letter_opener` (rich view shows the MJML-rendered email).
- **Jobs**: `psql -d motzi -c "select * from solid_queue_processes"` — supervisor/worker/dispatcher/scheduler heartbeats prove the worker is up. `solid_queue_jobs` timestamps are UTC — don't compare against local `now()`.
- **Probes**: `/nonexistent` → custom 404 via ErrorsController; `/health/admin` without token → 404; `POST /reply_ingress` bad secret → 401; `POST /error_events` JSON → 204 + row in `error_events`.

## Gotchas

- Dev DB `motzi` is shared across worktrees and the main checkout — other sessions' migrations show up in your server (PendingMigrationError) and in `db:migrate` schema dumps.
- `AnalyzeAnomaliesJob` fails nightly in dev (Anthropic 400) — pre-existing, not your change.
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ gem "pg", ">= 0.18", "< 2.0"
gem "progress_bar"
gem "puma", "~> 7.0" # web/app server
gem "rack-attack" # block scanner traffic / rate limit
gem "rails", "~> 7.2.0"
gem "rails", "~> 8.0.3"
gem "rails-settings-cached" # site-wide settings
gem "redcarpet" # markdown the baker's note in admin
gem "sass-rails", "~> 6" # css
Expand Down
117 changes: 57 additions & 60 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,49 +1,46 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.2.3.1)
actionpack (= 7.2.3.1)
activesupport (= 7.2.3.1)
actioncable (8.0.5)
actionpack (= 8.0.5)
activesupport (= 8.0.5)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.3.1)
actionpack (= 7.2.3.1)
activejob (= 7.2.3.1)
activerecord (= 7.2.3.1)
activestorage (= 7.2.3.1)
activesupport (= 7.2.3.1)
actionmailbox (8.0.5)
actionpack (= 8.0.5)
activejob (= 8.0.5)
activerecord (= 8.0.5)
activestorage (= 8.0.5)
activesupport (= 8.0.5)
mail (>= 2.8.0)
actionmailer (7.2.3.1)
actionpack (= 7.2.3.1)
actionview (= 7.2.3.1)
activejob (= 7.2.3.1)
activesupport (= 7.2.3.1)
actionmailer (8.0.5)
actionpack (= 8.0.5)
actionview (= 8.0.5)
activejob (= 8.0.5)
activesupport (= 8.0.5)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.3.1)
actionview (= 7.2.3.1)
activesupport (= 7.2.3.1)
cgi
actionpack (8.0.5)
actionview (= 8.0.5)
activesupport (= 8.0.5)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.3)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.3.1)
actionpack (= 7.2.3.1)
activerecord (= 7.2.3.1)
activestorage (= 7.2.3.1)
activesupport (= 7.2.3.1)
actiontext (8.0.5)
actionpack (= 8.0.5)
activerecord (= 8.0.5)
activestorage (= 8.0.5)
activesupport (= 8.0.5)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.2.3.1)
activesupport (= 7.2.3.1)
actionview (8.0.5)
activesupport (= 8.0.5)
builder (~> 3.1)
cgi
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
Expand All @@ -57,22 +54,22 @@ GEM
kaminari (>= 1.2.1)
railties (>= 6.1)
ransack (>= 4.0)
activejob (7.2.3.1)
activesupport (= 7.2.3.1)
activejob (8.0.5)
activesupport (= 8.0.5)
globalid (>= 0.3.6)
activemodel (7.2.3.1)
activesupport (= 7.2.3.1)
activerecord (7.2.3.1)
activemodel (= 7.2.3.1)
activesupport (= 7.2.3.1)
activemodel (8.0.5)
activesupport (= 8.0.5)
activerecord (8.0.5)
activemodel (= 8.0.5)
activesupport (= 8.0.5)
timeout (>= 0.4.0)
activestorage (7.2.3.1)
actionpack (= 7.2.3.1)
activejob (= 7.2.3.1)
activerecord (= 7.2.3.1)
activesupport (= 7.2.3.1)
activestorage (8.0.5)
actionpack (= 8.0.5)
activejob (= 8.0.5)
activerecord (= 8.0.5)
activesupport (= 8.0.5)
marcel (~> 1.0)
activesupport (7.2.3.1)
activesupport (8.0.5)
base64
benchmark (>= 0.3)
bigdecimal
Expand All @@ -81,9 +78,10 @@ GEM
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1, < 6)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
ahoy_email (1.1.1)
Expand Down Expand Up @@ -437,20 +435,20 @@ GEM
rack (>= 1.3)
rackup (2.3.1)
rack (>= 3)
rails (7.2.3.1)
actioncable (= 7.2.3.1)
actionmailbox (= 7.2.3.1)
actionmailer (= 7.2.3.1)
actionpack (= 7.2.3.1)
actiontext (= 7.2.3.1)
actionview (= 7.2.3.1)
activejob (= 7.2.3.1)
activemodel (= 7.2.3.1)
activerecord (= 7.2.3.1)
activestorage (= 7.2.3.1)
activesupport (= 7.2.3.1)
rails (8.0.5)
actioncable (= 8.0.5)
actionmailbox (= 8.0.5)
actionmailer (= 8.0.5)
actionpack (= 8.0.5)
actiontext (= 8.0.5)
actionview (= 8.0.5)
activejob (= 8.0.5)
activemodel (= 8.0.5)
activerecord (= 8.0.5)
activestorage (= 8.0.5)
activesupport (= 8.0.5)
bundler (>= 1.15.0)
railties (= 7.2.3.1)
railties (= 8.0.5)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
Expand All @@ -461,10 +459,9 @@ GEM
rails-settings-cached (2.9.6)
activerecord (>= 5.0.0)
railties (>= 5.0.0)
railties (7.2.3.1)
actionpack (= 7.2.3.1)
activesupport (= 7.2.3.1)
cgi
railties (8.0.5)
actionpack (= 8.0.5)
activesupport (= 8.0.5)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
Expand Down Expand Up @@ -706,7 +703,7 @@ DEPENDENCIES
puma (~> 7.0)
rack-attack
rack-mini-profiler (~> 4.0)
rails (~> 7.2.0)
rails (~> 8.0.3)
rails-settings-cached
rcodetools
redcarpet
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/error_events_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ def create
end

payload = params.permit(:error_class, :message, :stack, :url, context: {}).to_h
# Clients may nest the payload under error_event (the shape ParamsWrapper
# advertises). Top level wins when present: the auto-wrapped copy drops
# keys that aren't ErrorEvent columns (stack, context).
if payload.empty? && params[:error_event].is_a?(ActionController::Parameters)
payload = params[:error_event].permit(:error_class, :message, :stack, :url, context: {}).to_h
end

ErrorEvent.record_browser_exception(
error_class: payload["error_class"].presence || "Error",
Expand Down
5 changes: 4 additions & 1 deletion app/javascript/packs/menu/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export default function Layout({
const [showEmailSettings, setShowEmailSettings] = useState(
initialTab === "email"
);
// useContext under the hood — must run before the early returns below so
// the hook order doesn't change once the menu finishes loading.
const deadlineContext = getDeadlineContext();

if (error) {
return (
Expand All @@ -62,7 +65,7 @@ export default function Layout({
);
}

const orderingClosed = getDeadlineContext().allClosed(menu);
const orderingClosed = deadlineContext.allClosed(menu);

if (order && !isEditingOrder) {
const handleEditOrder =
Expand Down
2 changes: 1 addition & 1 deletion bin/dev
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ if lsof -iTCP:"$web_port" -sTCP:LISTEN -t > /dev/null 2>&1; then
alt_port=$((alt_port + 1))
done
echo "==> Port ${web_port} in use (pid $pid)"
read -rp "==> [u]se port ${alt_port} instead (default) / [k]ill $pid / [q]uit? " answer
read -rp "==> [U]se port ${alt_port} instead (default) / [k]ill $pid / [q]uit? " answer
case "${answer:-u}" in
[Kk]*)
kill "$pid" 2>/dev/null && echo "==> Killed $pid" || echo "==> Could not kill $pid"
Expand Down
8 changes: 8 additions & 0 deletions bin/rubocop
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env ruby
require "rubygems"
require "bundler/setup"

# explicit rubocop config increases performance slightly while avoiding config confusion.
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))

load Gem.bin_path("rubocop", "rubocop")
2 changes: 1 addition & 1 deletion config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
module Motzi
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.2
config.load_defaults 8.0

# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
Expand Down
6 changes: 1 addition & 5 deletions config/brakeman.ignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@
{
"fingerprint": "19ec190c996eca43da675de0907235ecb61b31a18d1b162444c5e6033a901ea4",
"note": "Open3.capture2 with array form — no shell injection possible"
},
{
"fingerprint": "98b26f60d776fd41ee6f088c833725145be9aac2d7c5b33780241c273622db42",
"note": "Rails 7.2 EOL 2026-08-09 — upgrade tracked separately; don't block CI on the countdown. Fingerprint changes with the patch version; re-run bin/brakeman -f json for the new one when it goes stale."
}
],
"updated": "2026-07-13",
"updated": "2026-07-14",
"brakeman_version": "8.0.4"
}
47 changes: 21 additions & 26 deletions config/initializers/content_security_policy.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
# Be sure to restart your server when you modify this file.

# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header

# Rails.application.config.content_security_policy do |policy|
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https
# # If you are using webpack-dev-server then specify webpack-dev-server host
# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?

# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# Rails.application.configure do
# config.content_security_policy do |policy|
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# end
#
# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
# config.content_security_policy_nonce_directives = %w(script-src style-src)
#
# # Report violations without enforcing the policy.
# # config.content_security_policy_report_only = true
# end

# If you are using UJS then enable automatic nonce generation
# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }

# Set the nonce only to specific directives
# Rails.application.config.content_security_policy_nonce_directives = %w(script-src)

# Report CSP violations to a specified URI
# For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true
6 changes: 4 additions & 2 deletions config/initializers/filter_parameter_logging.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Be sure to restart your server when you modify this file.

# Configure sensitive parameters which will be filtered from the log file.
# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
:passw, :uid
:passw, :uid, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
]
8 changes: 4 additions & 4 deletions config/initializers/inflections.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, '\1en'
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.plural /^(ox)$/i, "\\1en"
# inflect.singular /^(ox)en/i, "\\1"
# inflect.irregular "person", "people"
# inflect.uncountable %w( fish sheep )
# end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym 'RESTful'
# inflect.acronym "RESTful"
# end
Loading
Loading