Skip to main content

max / makenotwork

todo: trim completed items in sando and server Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-03 17:05 UTC
Commit: 168b193eaaa9e37ee8ec437eb7363b5f62debc5e
Parent: 1318230
2 files changed, +0 insertions, -214 deletions
@@ -55,19 +55,8 @@ Sando has zero automated tests today — daemon + TUI have been validated by run
55 55
56 56 55 tests passing as of 2026-05-31 (14 TUI + 41 daemon). Remaining gaps:
57 57
58 - - [x] `gates::reset_scratch` — verifies dropping every non-system schema (planted `foo` + `tower_sessions`, ran reset, asserted only `public` remains). Gated by `SANDO_TEST_PG_URL` env var so it skips on hosts without postgres. Run on pop-os with `SANDO_TEST_PG_URL=postgres:///sando_scratch?host=/var/run/postgresql cargo test`.
59 - - [x] `deploy::deploy_local` — copies multiple binaries (`PRIMARY`/`ADMIN`), swaps symlink atomically across two consecutive deploys, gc_local_releases keeps last N by mtime + handles missing dir + noop under threshold. `sh_quote` round-trip.
60 - - [x] `deploy::deploy_remote` failure path — against unroutable `192.0.2.1`, verifies clean ssh-attributed error (no panic / hang); ConnectTimeout bounds the test wallclock to ~10s. Plus `deploy_node` with `ssh_target="local"` short-circuits to symlink swap.
61 - - [x] `backup::fetch` URL parsing — extracted `parse_source` → `BackupSource` enum. 10 tests: file://, rsync://, ssh:// with/without port, multi-segment ssh path, non-numeric `:foo` colon treated as part of host (not port), and all malformed-input rejections (empty, scheme-only, ftp, no path on ssh, empty user@host).
62 - - [x] `events::emit` no-subscribers no-op; `emit_reaches_a_subscriber`; envelope serializes with flat `kind` field (locks the WS/TUI contract); `lagged_subscriber_observes_recv_error_lagged` exercises broadcast capacity.
63 58 - [ ] `events_ws` handler end-to-end — drive WS through a slow client, assert `{"kind":"lagged",...}` frame arrives. Possible (bind axum to ephemeral port + tungstenite client) but the bus-level lag detection is already locked in by `lagged_subscriber_observes_recv_error_lagged`. Diminishing returns vs effort. Deferred.
64 59 - [ ] `build` mutex behavior — requires real cargo or a slow stub. Treated as a manual checklist item under "TUI hands-on" instead. (Already validated by hand 2026-05-31.)
65 - - [x] `routes::confirm` — rejects when tier has no `current_version` (409 Conflict — surfaced that GateBlocked maps to 409 not 400, locked in), accepts + inserts a passing gate_runs row when set, 404 on unknown tier.
66 - - [x] `routes::promote` — refuses promote-to-first-tier (409), errors when neither body nor predecessor has a version, 404 when explicit version's `versions` row is missing.
67 - - [x] `unsatisfied_gates` — 6 tests: empty, failed-kind flagging, latest-row-wins (red→green flap clears), hotfix skips burn_in only, ignores other tiers/versions, **null `passed` treated as failing** (locks the in-flight-race safety property).
68 - - [x] `run_migrator` errors on missing migrations dir.
69 - - [x] sqlx migrations exercised via existing `sync` tests.
70 -
71 60 ### End-to-end harness
72 61
73 62 - [ ] Single-binary smoke: spin up sandod against tmpdir config + a tmp postgres; push a fixture commit; assert the full pipeline (build → gates → MM tier_state advance) completes in under 30s. Run on CI for every sando PR.
@@ -75,12 +64,6 @@ Sando has zero automated tests today — daemon + TUI have been validated by run
75 64
76 65 ### TUI unit tests
77 66
78 - - [x] `format_event` — golden tests for build_ok, gate_done (pass+fail), backup_fetched, deploy_failed, unknown kind, malformed JSON.
79 - - [x] `ws_url_from`: `http://` → `ws://`, `https://` → `wss://`, only replaces scheme once, unknown scheme passes through.
80 - - [x] `Action::Display` impl produces `backup/fetch`, `promote/<tier>`, etc.
81 - - [x] `Shared::push_event` ring-buffer cap at 200; oldest entries drop in FIFO order.
82 - - [x] `truncate` short-string passthrough vs long-string ellipsis.
83 -
84 67 ---
85 68
86 69 Roadmap target: replace `server/deploy/deploy.sh` and astra-hosted `server/deploy/run-ci.sh` with Sando running on **pop-os**, gating Hetzner prod through testnot.work.
@@ -109,12 +92,6 @@ Read these to orient before working on Sando:
109 92
110 93 ## Phase 0 — pop-os bootstrap
111 94
112 - - [x] Provision `sando` system user on pop-os; lock down home dir; generate SSH keypair at `/srv/sando/.ssh/id_ed25519` for outbound deploys.
113 - - [x] Install scratch Postgres locally on pop-os; create `sando_scratch` role + DB used by `migration_dry_run`. (Owner of own DB; non-superuser.)
114 - - [x] Write systemd unit for `sandod` (long-run service, restart on failure, env from `/etc/sando/sando.env`). Installed at `/etc/systemd/system/sandod.service`.
115 - - [x] Write the production `sando.toml`; bare repo path under `/srv/sando/mnw.git`. Installed at `/etc/sando/sando.toml`; daemon config at `/etc/sando/sando-daemon.toml`.
116 - - [x] Install `sandod` binary at `/usr/local/bin/sandod`; enable + start the service. Live on `100.103.89.95:7766`; bare repo auto-bootstrapped at `/srv/sando/mnw.git`.
117 - - [x] Verify MNW server builds reproducibly on pop-os. `makenotwork` 0.8.12 built in 132s; sqlx online mode against `sando_scratch` postgres (sandod prep-resets all non-system schemas + applies all 133 MNW migrations before invoking cargo).
118 95 - [ ] Register sando pubkey with Hetzner prod (`deploy@alpha-west-1`) and testnot.work once that node exists. Pubkey: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEK+vhpr1V8VnsEemN9x6tAA2S05kmv/mQ3eVgSXSkJ8 sando@pop-os`. (Moved to Phase 1 — not blocking Phase 0 exit.)
119 96
120 97 ### Phase 0 follow-ups (not blocking, but visible)
@@ -128,12 +105,6 @@ Read these to orient before working on Sando:
128 105
129 106 The MVP only deploys to `ssh_target=local`. Production needs real SSH/rsync.
130 107
131 - - [x] Implement `deploy::deploy_node` remote path: rsync staged binary to `<ssh_target>:<release_root>/releases/<version>/<bin_name>`, then `ssh <ssh_target>` does `mv -Tf` symlink swap + `sudo systemctl reload-or-restart <service>`. First real promote landed 2026-05-31: pop-os → testnot, version 0.8.12.
132 - - [x] Add `node.service_name` to `sando.toml` (default `makenotwork.service`).
133 - - [x] Bootstrap script for adding a fresh node: `MNW/sando/deploy/bootstrap-node.sh`. (See Phase 3 — node-bootstrap script for full details.)
134 - - [x] Garbage-collect old releases on the remote: keep last N=5 per node, sorted by mtime. Runs at end of each successful deploy (local + remote variants). Tied via `RELEASES_TO_KEEP` const.
135 - - [x] Handle `rsync` failure mid-deploy: leave the previous `current` symlink intact; mark `deploys.outcome = 'failed'`; do not advance `tier_state`. (Verified the routes.rs path; rsync runs before symlink swap so failure naturally leaves `current` untouched.)
136 -
137 108 ### Phase 1 — Track B: testnot live-app setup (NOT blocking Phase 2)
138 109
139 110 Sando's deploy machinery is done, but testnot's MNW runtime needs the rest before its `makenotwork.service` can stay up:
@@ -148,33 +119,14 @@ Sando's deploy machinery is done, but testnot's MNW runtime needs the rest befor
148 119
149 120 `migration_dry_run` is the load-bearing gate. It needs a real backup source, not a fixture.
150 121
151 - - [x] ~~Confirm astra's offsite replica writes a deterministic latest-link path.~~ Pivoted: pull direct from prod (`backup-puller@alpha-west-1:2200`, rrsync-locked to `/opt/makenotwork/backups/`). Astra offsite is separately broken — see carryover below.
152 - - [x] Wire the production `sando.toml` `backup.source` — `ssh://backup-puller@alpha-west-1:2200/latest.sql.gz` with `latest.sql.gz` as a hard link on prod.
153 - - [x] Schedule a daily `POST /backup/fetch` (systemd timer on pop-os). `sandod-backup-fetch.{service,timer}` in `MNW/sando/deploy/`. Runs daily at 04:00 UTC (one hour after prod's 03:00 UTC backup-db.sh). Service uses `EnvironmentFile=/etc/sando/sando.env` for `$SANDO_DAEMON`. Verified 2026-05-31: one-shot test pulled 36MB backup, recorded in `backups` table.
154 - - [x] First end-to-end `migration_dry_run` against a real prod backup. Passed 2026-05-31 for sha 4541ebc in 1.2s: restored 36MB dump + applied all 133 migrations cleanly. Sha eee96a7 correctly failed `migration_dry_run` because it lacked migrations 123-132 that prod has applied — exactly the prod-vs-repo drift the gate is designed to catch.
155 - - [x] Document the failure modes: `plans/migration-dryrun-failures.md`. Covers all 7 fail modes (no backup, scratch_url unset, scratch reset, restore, drift, checksum mismatch, content broken against prod data) with operator playbook.
156 - - [x] Decide retention on `backups` table. 30 days; pruned at end of `backup::fetch`. `DELETE FROM backups WHERE fetched_at < datetime('now', '-30 days')`.
157 -
158 122 ### Phase 2 carryovers / adjacent fires
159 123
160 124 - [ ] **Offsite backup sync from prod → astra still broken.** Diagnosed 2026-05-31: `sync-backup-offsite.sh` was never deployed to prod (`deploy.sh` gap when it was added). `makenotwork@prod` had no SSH key. Generated key + installed pubkey on `max@astra:~/.ssh/authorized_keys`, created `/opt/backups/mnw` on astra. **Blocked** on Tailscale ACL: astra runs only Tailscale SSH (no regular sshd on a bypass port), and the ACL denies `tag:tagged-devices` (alpha-west-1) → astra as user `max`. Needs ACL update in the Tailscale admin console, then deploy `sync-backup-offsite.sh` to `/opt/makenotwork/` and test. Makenotwork@prod pubkey: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzyQQ7pmBIZat8fABlpG/opwh4w5GhLIfkX2qxKxuT0 makenotwork@alpha-west-1`.
161 - - [x] **Prod backup `latest.sql.gz` hard link.** `backup-db.sh` now maintains `latest.sql.gz` atomically (`ln -f $LATEST.new && mv -Tf .new latest.sql.gz`). Deployed 2026-05-31; manual run verified (nlinks=2).
162 -
163 125 ## Phase 3 — Parity with current `deploy.sh`
164 126
165 127 Decisions captured in `plans/config-artifacts.md`. Summary: Caddyfile / systemd unit / backup script / security configs all move to **one-time node-bootstrap**, not per-deploy. error-pages bake into binary (MNW PR) with sibling fallback. mnw-admin ships alongside server via `bin_names: Vec<String>`. Restart warning is Phase 5, prod-tier-only. Prod migrations: server self-applies on startup (`main.rs:73`), sando does not.
166 128
167 - - [x] **Caddyfile** — decided: bootstrap-only. Not per-deploy. (`plans/config-artifacts.md` §1.)
168 - - [x] **systemd unit** — decided: bootstrap-only. (§4.)
169 - - [x] **Backup script** — decided: bootstrap-only. (§6.)
170 - - [x] **Error pages** — short-term done: ship as release-dir sibling. `build_and_run_mm` `cp -a` from `worktree/server/deploy/error-pages/` into the staged release dir; deploy_node's rsync of the whole dir picks it up. Verified on testnot 2026-05-31. Long-term `include_dir!` bake-in still a separate MNW PR.
171 - - [x] **mnw-admin binary** — `cfg.bin_names: Vec<String>` (default `["server"]`, MNW uses `["makenotwork","mnw-admin"]`). `deploy_local` copies each from worktree's `target/release/<bin>`; `deploy_node` rsyncs the whole staged dir. `Config::primary_bin()` returns first entry for systemd reference. `versions.artifact_path` stores the primary; release dir is derived as `.parent()`. Verified on testnot 2026-05-31.
172 - - [x] **Security configs** — decided: bootstrap-only. (§5.)
173 129 - [ ] **Restart warning** — Phase 5, prod-tier only via `tier.restart_warning_seconds` in `sando.toml`; needs `CLI_SERVICE_TOKEN` in `/etc/sando/sando.env`. (§7.)
174 - - [x] **Cross-compile from macOS** — decided: retire after one sprint of testnot parity verification. Pop-os builds natively. (§8.)
175 - - [x] **Prod migrations** — decided: server self-applies on startup. Sando does NOT run them. `migration_dry_run` gate is the prod safety net. (§9.)
176 - - [x] **Node-bootstrap script** — `MNW/sando/deploy/bootstrap-node.sh`. Idempotent. Takes `SANDO_PUBKEY` (required), `BIN_NAME`, `SERVICE_NAME`, `SERVICE_USER`, `DEPLOY_ROOT` env. Installs base packages (rsync/ufw/fail2ban), optionally postgres/tailscale/caddy, creates deploy user + dirs + sudoers entry + systemd unit, sets up UFW. Deliberately does NOT touch Caddyfile content, certs, postgres role/db, or secrets — those are operator-decisions per-node. testnot was done by hand and matches roughly what the script produces. Test by re-running on the next node added (tier B Hetzner prod move or tier C).
177 -
178 130 ## Phase 4 — Cutover
179 131
180 132 Run Sando in parallel with `deploy.sh` until trust is built, then retire the old path.
@@ -190,13 +142,6 @@ Run Sando in parallel with `deploy.sh` until trust is built, then retire the old
190 142
191 143 The TUI polls. The MVP requires you to hand-insert a row for `manual_confirm`. Both are fine for one operator but rough.
192 144
193 - - [x] Build mutex: single-slot `AppState.active_build: Mutex<Option<AbortHandle>>`; newer `/rebuild` aborts any in-flight build. Cargo commands set `.kill_on_drop(true)` so abort propagates SIGKILL to cargo + rustc children. (Landed 2026-05-31 after observing two concurrent builds racing the scratch DB.)
194 - - [x] Implement `WS /events`: tail of gate starts/finishes, deploy events, build logs. Event enum in `daemon/src/events.rs`; `broadcast::channel(256)` in `AppState`; emit sites in build.rs, gates.rs, routes.rs (rebuild, promote, rollback, confirm, backup_fetch). Verified 2026-05-31: live JSON envelopes stream to a python `websockets` client.
195 - - [x] TUI: actions pane. `↑↓`/`jk` select tier; `p` promote (no body — defaults version); `R` rollback; `b` backup fetch; `c` manual_confirm. Action results land in the events log. Daemon URL via `$SANDO_DAEMON`. Built in `tui/src/main.rs` 2026-05-31.
196 - - [x] `POST /confirm/{tier}` endpoint — inserts `gate_runs` row with `passed=1, gate_kind='manual_confirm'` for the tier's `current_version`. Replaces hand-SQL workaround. Verified 2026-05-31 against tier `a`.
197 - - [x] TUI live log pane that follows the most recent build / gate run; backed by `WS /events`. 200-event ring buffer, human-formatted per kind. WS auto-reconnects every 3s. Header shows ws connection state.
198 - - [x] `POST /promote` body — `version` now optional; defaults to predecessor tier's `current_version`. (Unblocks the "promote what just baked" flow.)
199 -
200 145 ## Phase 6 — Monitoring + alerting
201 146
202 147 - [ ] Wire pop-os `/metrics` endpoint into the existing MNW Prometheus scrape config; record where the scrape config lives in `_meta/` or wherever monitoring already runs.
@@ -91,24 +91,12 @@ Live state: working tree has 104+ Run #8 files plus 4 Run #9 files (`join_wizard
91 91
92 92 ### Above-MED items to address before launch (or defer with rationale)
93 93
94 - - [x] **Perf SERIOUS — Webhook hot-path unbounded `tokio::spawn`.** Fixed 2026-05-31. New `src/background.rs` with `BackgroundTx` + bounded mpsc (1024) + semaphore-bounded concurrency (8 workers vs 25 pool conns). `spawn_email!` macro refactored to use the bg queue (covers 17 callers). 5 manual webhook spawns migrated (`checkout_helpers.rs:58, 96, 124, 290` + `checkout.rs:618`). Same-disease per-request email spawns also migrated: postmark issue replies (×2), guest-claim email, join-wizard signup (×2). `cargo test --lib` 1654 / 0. Deferred (different shapes): import pipeline (long-running), MT community create (HTTP not pool), departure/status broadcast (broadcast-class), idempotency store (trivial).
95 -
96 94 ### New MED-tier findings (all closed 2026-05-31)
97 95
98 - - [x] **Payments MED — Cart `min_price_cents` bypass.** Fixed. Both cart paths (`process_seller_checkout` and `create_cart_checkout`) now check `pc.min_price_cents` for non-platform Discount codes before applying. Skips the ineligible item (others may still qualify) rather than rejecting the whole cart — matches the existing scope-skip pattern.
99 - - [x] **Payments MED — Cart-all chain-break on all-free first seller.** Fixed. `process_seller_checkout` signature changed `Result<String>` → `Result<Option<String>>`; all-free path now returns `Ok(None)` instead of `Err(BadRequest)`. New `drain_to_paid` helper loops through the queued sellers until a paid one is reached (returns URL) or queue exhausted (returns `Ok(None)` → library redirect). Both callers (`create_cart_checkout_all` and `checkout_success`) updated.
100 - - [x] **UX MED — Item wizard `pricing_model` silent fallback.** Fixed. `save_pricing` now rejects missing pricing_model with `AppError::validation("Select a pricing model")` and rejects unknown values with `format!("Unknown pricing model: {other}")`. Same shape as project wizard Run #6 fix.
101 - - [x] **UX MED — Inline-JS template duplication.** Fixed. Added delegated `data-copy-link` handler to `static/mnw.js` with proper `.catch()` (falls back to `window.prompt` in non-secure contexts). 8 templates migrated from `onclick="navigator.clipboard.writeText(...).then(...)"` to `<a href="..." data-copy-link>Copy link</a>` (audio_player, blog_post, collection, item, project, text_reader, user, video_player). `href` is the real URL so middle-click / no-JS / share menus still work. Cache-bust bumped to `v=0531`.
102 - - [x] **Perf MED — Cart free-claim N+1.** Fixed. Extended `CartItem` with `enable_license_keys` + `default_max_activations` (both cart queries pull them through). Three free-claim loops (single-seller paid path, discount-zeroed promo path, chain-flow path) drop the per-item `get_item_by_id` (saves N roundtrips) and replace per-item `remove_from_cart` DELETE with a single bulk `remove_from_cart_bulk(..., ANY($2))` at the end of each loop. Per-item tx for `claim_free_item` stays (per-item claim-vs-already-purchased return value). Roundtrips per free item: was ~5-7 → now ~3-4; bulk delete = +1 roundtrip total per loop (was N).
103 -
104 96 All 5 MEDs landed. `cargo test --lib` 1654 / 0.
105 97
106 98 ### Verified closed this run
107 99
108 - - [x] **Storage H1** — `confirm_upload` silent zero-rows + side-effects-already-fired (uploads.rs:295-337). Three-arm match, zero-rows arm rolls back storage + enqueue_s3_orphan.
109 - - [x] **Storage S1** — `media_confirm` three-write atomicity (media.rs:241-293). Single tx wraps storage credit + pending_uploads clear + media_files INSERT.
110 - - [x] DB helpers genericized to `impl PgExecutor<'e>` — all 12 callers (including `synckit/blobs.rs:157`) verified backwards-compatible.
111 -
112 100 ### Storage A- standing — remaining MED/LOW (Phase 4 polish or defer)
113 101 Carried from Storage code-fuzz 2026-05-31 — see below. All still MED, none A- blockers.
114 102
@@ -119,49 +107,15 @@ Carried from Storage code-fuzz 2026-05-31 — see below. All still MED, none A-
119 107 Sorted by file locality and difficulty. Tests: 1655 / 0 throughout.
120 108
121 109 ### Wave 1 — auth/security cluster (8 tiny)
122 - - [x] `synckit_auth.rs:147` `<` → `<=` closes 1-second JWT-revocation collision window.
123 - - [x] `routes/auth.rs:128, 137` malformed-email + invalid-username branches now run DUMMY_HASH equalizer — closes timing oracle.
124 - - [x] `routes/auth.rs:331-336` `validate_username` length switched from `len()` bytes to `chars().count()` — multi-byte usernames treated correctly.
125 - - [x] `git_ssh.rs:162` `parse_repo_path` rejects lone-dot segments.
126 - - [x] `routes/oauth.rs:206` `validate_token` → `validate_token_consuming` (sealed witness type).
127 - - [x] `routes/oauth.rs:213-222` OAuth `state` ≤ 1024 bytes + `code_challenge` ≤ 44 chars.
128 - - [x] `helpers.rs::ip_advisory_lock_key` `DefaultHasher` → SHA-256 (stable across Rust versions).
129 - - [x] `helpers.rs::extract_client_ip` one-shot WARN after 100 cumulative missing `cf-connecting-ip` requests.
130 -
131 110 ### Wave 2 — scanning (3)
132 - - [x] `scanning/clamav.rs::ping` + `ScanPipeline::assert_live` at startup; refuses to boot if scanning configured but no AV layer live.
133 - - [x] `scanning/clamav.rs` 16 KB INSTREAM truncation → `LayerVerdict::Fail` (was Error → FailOpen → Pass).
134 - - [x] `scanning/worker.rs:251` inline media UPDATE swapped to `db::scanning::update_media_file_scan_status` helper.
135 -
136 111 ### Wave 3 — DB layer polish (4)
137 - - [x] `db/pending_uploads.rs::remove_pending_upload` signature now requires `user_id`; 12 callers updated.
138 - - [x] `db/pending_s3_deletions.rs::is_s3_key_live` now covers `projects.cover_image_url` and `items.cover_image_url` via `LIKE %s3_key`.
139 - - [x] `db/projects.rs::update_project_image_url` returns `Result<bool>`; `images.rs::project_image_confirm` three-arm match fires rollback + orphan-queue on `Ok(false)`.
140 - - [x] `db/items/media.rs::update_item_cover` same shape; same caller treatment in `images.rs::item_image_confirm`.
141 -
142 112 ### Wave 4 — storage handlers + admin rescan seal + downloads (5)
143 - - [x] **Migration 133** `items_duration_seconds_nonnegative.sql` CHECK on `duration_seconds` + `video_duration_seconds`.
144 - - [x] `routes/storage/downloads.rs:120` defensive clamp: `duration.max(0) as u64 → saturating_mul(2) → clamp(3600, 86_400)`.
145 - - [x] `routes/storage/mod.rs::commit_rescan` new sibling to `commit_upload` for admin-rescan paths.
146 - - [x] `routes/admin/uploads.rs::rescan_{version,item}_inner` migrated to `commit_rescan`; chronic-disease seal now covers admin paths.
147 - - [x] `routes/pages/dashboard/wizards/item/save.rs:95` wizard `update_item_cover_image_url` call dropped (confirm authoritative, hidden-field desync risk closed).
148 - - [x] `routes/storage/mod.rs` `enqueue_s3_orphan` doc rewritten to match reality (post-credit failures only).
149 -
150 113 ### Wave 5 — UX polish (2)
151 - - [x] `pricing.rs::parse_dollars_to_cents` strips `$`, `,`, whitespace; new `strips_clipboard_decoration` test.
152 - - [x] `routes/admin/users.rs:37, 77` page `clamp(1, 1_000_000_000)` to prevent OFFSET overflow → sqlx 500.
153 -
154 114 ### Wave 6 — Performance (3 of 5; 2 deferred)
155 - - [x] `metrics::idempotency_middleware` in-memory negative cache (OnceLock<DashMap>, 60s TTL, periodic GC). Skips per-POST `get_cached_response` SELECT for keys recently confirmed not-cached.
156 - - [x] `monitor.rs` `record_storage_fill_stats` gated by 5-min TTL — 60× reduction at 10k+ creators.
157 - - [x] `scheduler/cleanup.rs::cleanup_sandbox_accounts` serial loop → JoinSet `CLEANUP_PARALLELISM=4`.
158 115 - [ ] **DEFERRED** `build_runner.rs:151` serial-target loop. LOW; refactor touches denominator + error agg + log order. Post-launch.
159 116 - [ ] **DEFERRED** `scheduler/mod.rs:92-279` advisory-lock granularity. Multi-replica concern; defer until multi-replica is real.
160 117
161 118 ### Wave 7 — Payments LOW (2)
162 - - [x] `routes/stripe/webhook/mod.rs:73-87` `insert_failed_event` failure → 503 (Stripe redelivers) instead of dropping event with 200.
163 - - [x] `routes/stripe/checkout/cart.rs:73-82, 535-543` `remove_from_cart` already-owned cleanup now logs WARN on Err.
164 -
165 119 ---
166 120
167 121 ## Storage code-fuzz 2026-05-31 (post-Run #7)
@@ -169,10 +123,6 @@ Sorted by file locality and difficulty. Tests: 1655 / 0 throughout.
169 123 Targeted Storage-axis fuzz to verify A- before triggering full Run #8.
170 124
171 125 ### Above-MED fixes that landed
172 - - [x] **Storage HIGH — `confirm_upload` silent zero-rows + side-effects-already-fired.** `routes/storage/uploads.rs:295-336`. Three-arm match on UPDATE result; zero-rows case rolls storage back, routes new S3 key through `enqueue_s3_orphan`, returns BadRequest. Same shape as Run #7 HIGH-2, one step further along the same handler family.
173 - - [x] **Storage SERIOUS — `media_confirm` three-write atomicity (Run #5 plan #12 reopened).** `routes/storage/media.rs:235-294`. Three writes (storage credit + pending_uploads clear + media_files INSERT) now in a single tx; tx drop rolls all three back on interruption. Only S3 object needs explicit cleanup. 23505 duplicate-filename detection moved outside the tx — same SQLSTATE check, runs after rollback.
174 - - [x] DB-layer support: `creator_tiers::try_increment_storage_on(&mut PgConnection)` new tx-friendly variant; `pending_uploads::remove_pending_upload` and `media_files::create` signatures genericized to `impl PgExecutor<'e>` (backwards compatible — all existing `&PgPool` call sites still compile).
175 -
176 126 ### Remaining MED/LOW (below A- bar; defer or Phase 4 polish)
177 127 - [ ] Storage MED — `update_project_image_url` / `update_item_cover` ignore `rows_affected()`. Same shape as H1 but only follow-on side-effect is `bump_cache_generation`, so blast radius is small.
178 128 - [ ] Storage MED — `downloads.rs:120` `((duration as u64) * 2).max(3600)` with no DB CHECK on `duration_seconds`. Add `CHECK (duration_seconds >= 0)` migration + cap in code (`duration.max(0).saturating_mul(2).clamp(3600, 86400)`).
@@ -188,29 +138,8 @@ Targeted Storage-axis fuzz to verify A- before triggering full Run #8.
188 138 ## Ultra Fuzz 2026-05-31 (Runs #6, #7 + S1)
189 139
190 140 ### Structural / chronic-disease fixes that landed
191 - - [x] `routes/storage/mod.rs::commit_upload(target, ...)` + `CommitTarget` enum; `enqueue_scan_for` demoted to module-private. All 7 confirm handlers (uploads, versions, project_image, item_image, media, internal/uploads, content_insertions) converted.
192 - - [x] `crate::pricing::parse_dollars_to_cents` + `validate_dollars_f64` shared helpers; 5 callsites converted (item save, project wizard ×2, bulk price, projects API).
193 - - [x] Dead `completion_effects` outbox deleted (`db/completion_effects.rs`, `scheduler/completion_effects.rs`, `routes/stripe/webhook/effects.rs` were orphaned files, no module declarations). Migrations 124/125 left in place — empty table, harmless. Drop-table migration is a future cleanup.
194 -
195 141 ### Bug-level fixes that landed
196 - - [x] Storage CRIT Run #6 — `enqueue_s3_orphan` wired at `uploads.rs:325` post-commit old-key delete.
197 - - [x] Storage HIGH Run #6 — `images.rs::project_image_confirm` idempotency check added.
198 - - [x] Storage HIGH Run #6 — `images.rs::item_image_confirm` scan-ordering fixed via commit_upload.
199 - - [x] Storage HIGH Run #7 — 4 idempotent-early-return sites now call `remove_pending_upload` before returning (uploads.rs, versions.rs, images.rs project + item).
200 - - [x] Storage HIGH Run #7 — `update_project_image_url` + `update_item_cover` failures now refund storage + queue new key for orphan deletion.
201 - - [x] Storage MED Run #6 — `media_delete` enqueue moved after `tx.commit()`.
202 - - [x] UX HIGH Run #6 — `routes/api/projects.rs` float-parse via `validate_dollars_f64`.
203 - - [x] UX HIGH Run #6 — project wizard tier-row loop bubbles errors instead of silently `continue`-ing.
204 - - [x] UX HIGH Run #7 — `pricing_model` silent fallback to Free fixed; missing/malformed now rejects.
205 - - [x] Payments S Run #6 — promo `try_increment_use_count` moved after Stripe-readiness checks in item.rs, cart.rs::create_cart_checkout, and cart.rs::process_seller_checkout.
206 - - [x] Payments S Run #6 — `process_seller_checkout` uses bulk `purchased_subset`.
207 - - [x] Payments H Run #6 — `project_members::add_project_member` + `update_member_split` reject split_percent outside [0,100]; upsert subtracts existing row before cap check.
208 - - [x] Payments H Run #6 — `CodePurpose::Discount` with NULL discount_type/value rejected at validation (item.rs, cart.rs:184, cart.rs::process_seller_checkout — third copy fixed in Run #7).
209 - - [x] Payments H Run #6 — free PWYW project checkout clears contact revocation when share_contact=true.
210 - - [x] Payments SERIOUS Run #7 — cart 23505 swallow → buyer charged for unfulfilled items. New `db::transactions::pending_subset` bulk pre-check; both cart paths pre-check before Stripe session; remaining 23505 catch is now hard-error (release promo + abort).
211 -
212 142 ### Deferred (with rationale)
213 - - [x] **Payments H2 (Run #7) — `claim_free_project` race.** Fixed 2026-05-31. `db::transactions::claim_free_project` now returns `Result<bool>` (mirrors `claim_free_item`). Caller in `routes/stripe/checkout/project.rs` gates `clear_contact_revocation` on the `claimed` winner — the loser of a concurrent-claim race no longer fires the side-effect. Same shape ready for any future side-effects added below the claim (sale-notification email, split recording, etc).
214 143 - [ ] Drop unused `completion_effects` table — schema-only cleanup; harmless empty table.
215 144
216 145 ### Notes on remaining MED/LOW (per Run #7 axis reports)
@@ -305,108 +234,20 @@ Full report: `docs/audit_review.md`. Plan target: lift every axis back to A- or
305 234
306 235 ### Phase 1 — clear HIGH/CRITICAL caps (must do before launch)
307 236
308 - - [x] **Creator-tier forms missing CSRF token (UX CRITICAL)** — `templates/partials/tabs/user_creator.html:80,85`. Add `{% if let Some(token) = csrf_token %}<input type="hidden" name="_csrf" value="{{ token }}">{% endif %}` to both forms. Verify the page handler populates `csrf_token` in the template context. Path `/stripe/creator-tier` is not in any exempt prefix, so without the token every authenticated click returns 403.
309 - - [x] **`git_ssh.rs` repo-name validation gap (Security HIGH)** — `git_ssh.rs:90-102` + `db/git_repos.rs:21-35`. Call `validate_git_repo_name(repo_name).map_err(|_| anyhow!("repository not found"))?` immediately after `parse_repo_path` succeeds in the dispatch path; add the same in `db::git_repos::create_repo`. Without this, the raw name flows into `format!("{op} '/{owner}/{repo_name}.git'")` fed to `git-shell -c`. `cmd_ssh_repo_delete` already validates at line 354 — backport.
310 - - [x] **Login lockout `just_locked` per-attempt email flood (Security HIGH)** — `db/auth.rs:30-54`. Change the `RETURNING` clause from `(failed_login_attempts >= $2) AS just_locked` to `(failed_login_attempts = $2) AS just_locked` (exact equality, fires only on the crossing attempt). Follow-up: idempotency key on the lockout-email outbox so a regression cannot inbox-flood a known email.
311 - - [x] **`cancel_pending_item_checkout` CSRF gap (UX HIGH)** — `routes/stripe/checkout/item.rs:390-407`. Either narrow the `/stripe/checkout` CSRF exempt prefix so this path isn't in it, or add explicit `csrf::validate_token` like `create_tip_checkout` does in `stripe/checkout/tips.rs:49-50`. Document the rule at the exempt-prefix definition site so future "I'll just add a quick mutation handler" landings fail review.
312 - - [x] **Promo `use_count` over-release on cart cleanup (Payments SERIOUS)** — `scheduler/cleanup.rs:223` + `db/transactions.rs:1011-1029`. Cart checkouts produce N pending-tx rows carrying the same `promo_code_id`; the loop calls `release_use_count` N times for a single reservation. Either dedupe with `HashSet<PromoCodeId>` before the release loop, or have `cleanup_stale_pending` return `DISTINCT promo_code_id` from the DELETE.
313 - - [x] **Scanner streams instead of `Vec<u8>` (Storage SERIOUS)** — `storage.rs:179,404,629` + `scanning/worker.rs:175`. Add `download_stream`/`get_object_stream` to the S3 trait returning a `ByteStream`; pipe through ClamAV instead of buffering. A 20 GB media scan currently OOMs the worker; concurrent scans amplify. (plan: `../../_private/docs/mnw/server-docs/plans/scanner-streaming.md`)
314 - - [x] **`scan_jobs` retention (Perf CRITICAL)** — `db/scan_jobs.rs`. Add a scheduler tier (hourly) that deletes rows older than 30 days where status ∈ {Clean, Quarantined, Failed}. Keep Pending/Running. Default retention constant in `constants.rs`. (plan: `../../_private/docs/mnw/server-docs/plans/scan-jobs-retention.md`)
315 - - [x] **Scanner DB-pool permit across S3 download (Perf CRITICAL)** — `scanning/worker.rs`. Drop the pool permit before `download_object` (or `download_stream` after the SERIOUS fix above); reacquire after bytes are local. Under any scan backlog the pool starves request traffic today. (plan: `../../_private/docs/mnw/server-docs/plans/scanner-pool-permit.md`)
316 - - [x] **`broadcast.rs` unbounded sequential loop (Perf HIGH)** — `routes/api/users/broadcast.rs`. Wrap recipient fan-out in `tokio::spawn` with a bounded `JoinSet` (cap 16) over chunks; preserve the 100ms per-recipient SMTP shape. (plan: `../../_private/docs/mnw/server-docs/plans/broadcast-bounded-fanout.md`)
317 -
318 237 ### Phase 2 — close axis-dragging SERIOUS items
319 238
320 - - [x] **Cart template price math (UX MED)** — `templates/pages/cart.html:71-76,95,102`. Move `effective_price_cents() / 100` etc. out of templates; pass pre-formatted strings from the handler, or expose `format_price` as an Askama filter. Unify with `format_revenue` so thousands separators match across dashboards (`format_revenue(1_000_000)` returns `"$10000.00"`).
321 - - [x] **`media.rs` delete-then-reupload race (Storage MED)** — `routes/storage/media.rs:418-456`. Worker must re-check `SELECT 1 FROM media_files/items/versions WHERE s3_key = $1` before each S3 delete, OR queue inserts carry the entity-ID and worker confirms absence. Today a delete-then-reupload within worker latency deletes the fresh file.
322 - - [x] **`pending_uploads` reaper-bump owner pinning (Storage MED)** — `db/pending_uploads.rs:26-34`. ON CONFLICT only refresh `created_at` when `user_id` matches; otherwise treat as a fresh row. Closes the slow-leak where re-presigning every minute keeps an orphan S3 object alive indefinitely.
323 - - [x] **TOTP step-replay across re-enable (Security MED)** — `db/totp.rs:60-81`. `disable_totp` adds `totp_last_used_step = NULL`. `set_totp_secret` resets to 0 / NULL. Closes the false-reject DoS when a user disables and re-enables TOTP.
324 - - [x] **`delete_other_sessions` cache eviction (Security MED)** — `db/sessions.rs:178-192`. Change return to `RETURNING id`; callers iterate and call `state.session_cache.remove(&id)`. Today "log out other sessions" leaves cached `AuthUser` extractor entries valid up to `SESSION_TOUCH_CACHE_SECS`.
325 - - [x] **CSRF on `/login` (Security MED)** — `csrf.rs:166-181`. Move `/login` out of `exempt_prefixes`; have `login_handler` call `csrf::validate_token` like `authorize_post` does. The login template already renders the token; only middleware bypass is keeping it from doing work. Defense-in-depth for SameSite-Lax-only login-CSRF.
326 - - [x] **`is_registered_redirect_uri` `fetch_one` brittleness (Security MED)** — `db/oauth.rs:84-97`. Switch to `fetch_optional`, return `Ok(false)` on `None`. Document trailing-slash matching policy explicitly to close the developer-onboarding foot-gun.
327 -
328 239 ### Phase 3 — resilience & infra hardening
329 240
330 - - [x] **Multi-replica `claim_pending_build` race (Storage MED)** — `db/builds.rs:262-281`. Add `CREATE UNIQUE INDEX … ON ota_builds(status) WHERE status='running'` partial unique index, OR wrap with `pg_advisory_xact_lock`. Today's single-replica prod is safe; the bug fires the moment a second builder joins.
331 - - [x] **Build status reaper race (Storage MED)** — `db/builds.rs:216-252,287`. Add `WHERE status='running'` to the success UPDATE and check `rows_affected`. Avoids the case where the stale-build reaper marks a successful build failed at the exact second it completes.
332 - - [x] ~~**`KNOWN_SYNC_APPS` deletion path (Perf surprise)**~~ — registry removed; no `KNOWN_SYNC_APPS` in `rate_limit.rs` (only JWT extractors). Item moot. — `rate_limit.rs:65-95`. Add `unregister_known_sync_app(app_id)` called from sync-app delete. Long-term: replace `OnceLock<DashSet>` with a DB-backed cache + TTL so multi-replica deploys don't diverge on app inventory.
333 - - [x] **`extract_s3_key_from_url` host pinning (Storage SERIOUS)** — `storage.rs:582-593`. Require the `https://` host portion to be the configured S3 endpoint host or a known CDN domain before extracting the key. Today path-style branch returns `Some("foo")` for `https://attacker.example/my-bucket/foo`.
334 - - [x] **TOTP `pending_2fa_*` tracking row (Security surprise)** — `routes/auth.rs:196-202`. Insert a temporary tracking row scoped via `kind='pending_2fa'` (or separate table) at the moment 2FA-pending begins, so `delete_all_sessions_for_user` can sweep a phisher mid-2FA-prompt. Today that intermediate authenticated state is invisible to "log out everywhere".
335 -
336 241 ### Phase 4 — polish
337 242
338 - - [x] **`MaybeUserUnverified` rename or short-circuit (Security LOW)** — `auth.rs:229-249`. Either rename to make the danger boundary impossible to forget (e.g. `SessionUserStaleAllowed`), or short-circuit to `None` when the session lacks `SESSION_TRACKING_KEY` so legacy sessions don't quietly survive `/logout-everywhere`.
339 - - [x] **`sum_file_sizes_for_item` clamp direction (Storage LOW)** — `db/versions.rs:320-332`. Replace `COALESCE(LEAST(SUM, i64::MAX)::BIGINT, 0)` with `COALESCE(GREATEST(0, LEAST(SUM, i64::MAX))::BIGINT, 0)`. Today the protection is one-sided; negative values flow through.
340 - - [x] **License-key 23505 collision retry (Payments LOW)** — `crypto.rs:30-40` + `db/transactions.rs:411-422`. Retry once on UNIQUE violation in `create_license_key` and the promo-claim arm so an unlucky generator collision doesn't surface as a 500.
341 - - [x] **Stripe v1 multi-signature rotation (Payments LOW)** — `payments/webhooks.rs:350-389`. Collect all `v1=` values from the header; accept on any match against any configured secret. Today only the last `v1=` value is tried, which breaks Stripe's documented secret-rotation procedure.
342 - - [x] **`has_active_subscription_to_project` period-end check (Payments LOW)** — `db/subscriptions.rs:394-408`. Add `AND (current_period_end IS NULL OR current_period_end > NOW())`. Defense-in-depth for a missed/delayed `customer.subscription.deleted` webhook.
343 - - [x] **Download HTML sniff strengthening (Security LOW)** — `scanning/content_type.rs:119-146`. For Download, add a lightweight string sniff for `<!--`, `<script`, `<svg`, `<?xml`, BOM-stripped `<html` after `infer` returns None. Pair with `Content-Disposition: attachment` on all served downloads so the browser never renders inline regardless of scan verdict.
344 - - [x] **Profile-link `rel="ugc nofollow"` (UX LOW)** — `templates/pages/user.html:84`. Add `rel="ugc nofollow"` to user-supplied profile links.
345 - - [x] **`format_revenue` thousands separator (UX LOW)** — `formatting.rs:43-51`. Make `format_revenue` use thousands separators consistent with `format_price`. Dashboards mixing both currently render "$1,234" and "$10000.00" side by side.
346 - - [x] **Third-party credits / attributions page (UX, brand)** — Build a public page that credits every library, vendor, and dependency the platform leans on (Rust crates from server + multithreaded + pom + mnw-cli + shared/, JS libs, fonts, SDKs, payment / object-storage / email vendors). Pull crate names + licenses out of `cargo metadata`, group by tier (runtime infra, scanning, payments, etc.), and write a short human-readable paragraph for the big ones (tokio, axum, sqlx, aws-sdk-s3, async-stripe, yara-x, fs2, etc.). Lives somewhere discoverable from the footer or the `/about` tree. Worth real effort — this is one of the visible ways MNW shows respect for the OSS ecosystem it stands on.
347 -
348 243 ### Phase 5 — chronic
349 244
350 245 - [~] **Invariant-in-prose / policy-not-in-types — third consecutive run (CHRONIC)** — scan_status-ordering half closed 2026-05-26 (see Phase 1 entry for `images.rs::item_image_confirm`). The constructive-impossibility shape from the chronic-remediation rubric: `commit_*_upload` is the only handler-reachable path that writes both row + scan_status; the lower-level scan_status writes were renamed `set_*_scan_status_standalone` and documented as worker- and admin-override-only. Compiler-driven migration found one additional handler with the same bug (CLI internal upload) — that's the test the rubric wants: structural change exposes drift, not human review. Remaining: `/stripe/*` CSRF policy patchwork — same disease, different organ. Track as Landing 2 below.
351 246
352 - - [x] **Per-route CSRF posture (CHRONIC, second half) — Landing 2** — landed 2026-05-26. `csrf.rs::exempt_prefixes` allowlist replaced with per-route helpers + a `CsrfRouter<S>` newtype that only accepts `PostureMethodRouter<S>` values produced by the `{post,put,patch,delete}_csrf*` helpers. A bare `Router::route(path, post(handler))` no longer compiles inside any of the 14 mutation-bearing route files — the structural guarantee replaces the originally-planned runtime trip wire, which was impossible because per-route layers run *inside* global ones (the global writer would have run after the global reader). Compiler-driven migration surfaced one real regression (Manual-posture tip handler was form-only; old middleware was header-then-form) — fixed by routing the Manual handler through `extract_token_from_request` to match the standard precedence.
353 -
354 - Sub-steps:
355 - - [x] L2.1 Helpers + sealed marker. `CsrfPosture::{Auto, Manual(&'static str), Skip(&'static str)}`, `CsrfManuallyValidated` ZST with sealed constructor, `validate_token_consuming` as the only producer.
356 - - [x] L2.2 Per-route layers replace global middleware. The Auto helper attaches a per-route `from_fn` that runs `validate_auto`; Skip/Manual attach nothing at runtime (the posture lives in the helper signature for source-level grep, not in request extensions).
357 - - [x] L2.3 Inventory: 290 mutation routes across 14 files (208 POST · 36 PUT · 2 PATCH · 45 DELETE).
358 - - [x] L2.4–L2.6 Migrate all routes. Skip for webhooks / pre-auth / HMAC-internal / bearer-sync / guest-checkout; Manual for the tip handler; Auto for the rest. Reason strings are at the call site (`STRIPE_SESSION_SKIP`, `SYNCKIT_API_KEY_SKIP`, etc.).
359 - - [x] L2.7 Allowlist + `csrf_middleware` deleted from `csrf.rs` and `lib.rs`.
360 - - [x] L2.8 Structural enforcement via `CsrfRouter<S>` + `PostureMethodRouter<S>`. `route_get` for read-only methods; `merge`, `nest`, `layer`, `route_layer` mirror axum's `Router`. `finalize()` is the single escape hatch, called once in `build_app`.
361 -
362 247 Follow-ups:
363 248 - [ ] **Manual-posture runtime assertion (dev builds).** Today `*_csrf_manual` requires no compile-time proof that the handler called `validate_token_consuming`. Only the tip handler is Manual, and `_validated` is bound only as documentation. In dev/test builds, set a flag in `validate_token_consuming` and debug-assert it after the handler runs; mismatched routes panic loudly in CI without affecting prod. Not blocking — only matters if Manual grows beyond one route.
364 249 - [ ] **Phase 1 entries still open:** `cancel_pending_item_checkout` Skip reason is `"Phase 1 todo: tighten to post_csrf"` (grep "Phase 1 todo" to find). `/login` and `creator-tier` template tightening tracked separately above.
365 250
366 - - [x] **Integration test drift — 42 pre-existing failures (snapshot 2026-05-26 post-Landing 2).** Cleared 2026-05-27. Three landings (harness helpers, KeyCode validator + fixtures, password_reset body asserts) + per-test cleanup. Real code fixes that fell out: `validate_key_code` accepts 5 or 6 words (generator was emitting 6 but validator pinned at 5); `update_build_status` source-status gate now `IN ('pending','running')` (Phase 3 gate inadvertently blocked cancel of pending builds); `validate_auto` short-circuits safe methods (GET on multi-method `with_csrf` routes was 403); `media_confirm` matches `AppError::Database(sqlx::Error::Database)` for 23505 instead of substring on the wrapper Display. All 803 integration tests now pass. Net delta from the CSRF migration is 0 (tip-handler regression was found and fixed; the remaining 42 match the pre-migration baseline modulo flakes). These are not CSRF-related and need triage in their own landings. Re-snapshot before each push.
367 - - [ ] workflows::admin::admin_approve_item_upload
368 - - [ ] workflows::admin::admin_approve_version_upload
369 - - [ ] workflows::admin::admin_reject_item_upload
370 - - [ ] workflows::admin::admin_reject_version_upload
371 - - [ ] workflows::adversarial_auth::suspended_user_login_ok_writes_blocked
372 - - [ ] workflows::adversarial_business::exhausted_promo_code_rejected
373 - - [ ] workflows::auth::lockout_after_failed_attempts
374 - - [ ] workflows::auth::nonexistent_user_rejected
375 - - [ ] workflows::auth::password_change_flow
376 - - [ ] workflows::auth::wrong_password_rejected
377 - - [ ] workflows::creator_media::scan_real_flac_passes
378 - - [ ] workflows::creator_media::scan_real_mp3_passes
379 - - [ ] workflows::creator_media::scan_real_wav_passes
380 - - [ ] workflows::fingerprinting::license_deactivate_frees_slot
381 - - [ ] workflows::fingerprinting::license_verify_lifecycle
382 - - [ ] workflows::fingerprinting::license_verify_requires_verification_enabled
383 - - [ ] workflows::fingerprinting::license_verify_revoked_key
384 - - [ ] workflows::license_keys::license_key_lifecycle
385 - - [ ] workflows::license_keys::max_activations_enforced
386 - - [ ] workflows::license_keys::revoke_key_then_validate_fails
387 - - [ ] workflows::license_keys::v1_license_endpoints
388 - - [ ] workflows::lifecycle::sandbox_lifecycle_create_use_expire_cleanup
389 - - [ ] workflows::media_library::filename_collision_rejected
390 - - [ ] workflows::password_reset::password_reset_expired_link
391 - - [ ] workflows::password_reset::password_reset_passwords_must_match
392 - - [ ] workflows::password_reset::password_reset_tampered_signature
393 - - [ ] workflows::promo_codes_free_access::free_access_code_claim_already_owned
394 - - [ ] workflows::promo_codes_free_access::free_access_code_claim_by_buyer
395 - - [ ] workflows::promo_codes_free_access::free_access_code_generate_list_delete
396 - - [ ] workflows::sandbox::create_sandbox_account
397 - - [ ] workflows::sandbox::sandbox_blocks_restricted_endpoints
398 - - [ ] workflows::sandbox::sandbox_blog_no_email
399 - - [ ] workflows::sandbox::sandbox_content_not_visible_on_item_page
400 - - [ ] workflows::sandbox::sandbox_rss_returns_404
401 - - [ ] workflows::scanning::admin_approve_held_upload
402 - - [ ] workflows::scanning::confirm_upload_bad_magic_quarantined
403 - - [ ] workflows::scanning::confirm_upload_clean_file_passes
404 - - [ ] workflows::scanning::trusted_creator_upload_auto_publishes
405 - - [ ] workflows::scanning::untrusted_creator_upload_held_for_review
406 - - [ ] workflows::subscriptions::sandbox_tier_uses_fake_stripe_ids
407 - - [ ] workflows::suspension::suspended_user_claim_promo_code_blocked
408 - - [ ] workflows::wizards::wizard_back_navigation
409 -
410 251 ### Notes & non-actions
411 252
412 253 - Status-notification fan-out cooldown across overlapping tasks (`monitor.rs:213-237`) — single-replica today; harmless. Reconsider when adding a second instance.