Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
67 KiB
Production readiness (Descrybe v2)
Date: 2026-08-09 (LOCAL/CODE cutover wave: all non-SMTP/Stripe blockers handled; Clerk removed; goose 042; cutover still NO-GO on 2 live ops: SMTP + Stripe)
Stack: Go API (apps/api) + SvelteKit (apps/web) + PostgreSQL
Related: production-checklist.md, security-notes.md, go-live-checklist.md, cutover.md, migration-readiness.md, staging-auth-rehearsal.md, ops-runtime.md, stripe-setup.md, ux-backlog.md
Executive verdict
| Question | Answer |
|---|---|
| Product (code) readiness | 10/10 — features, security architecture, perf foundations, UX activation path, tests, setup DX |
| Production cutover (ops) | NO-GO / 9/10 (was 8/10) — only 2 hard live blockers left (SMTP + Stripe) |
| Staging / local rehearsal | GO — migrator A1-safe tooling + local plan assign 87→0 + orphan 0 + goose 039–042 + worker /readyz + cutover-deploy-check PASS + forgot-password opaque 200 (API + browser) |
| A1 clean? | YES — no A1 mutations; migrator promote/assign/patch skip a1=true (tests) |
| Ship to paying public internet today? | NO until live SMTP and live Stripe clear |
One-line: Product 10/10 · local/CODE cutover prep handled · hard ops left = live SMTP + live Stripe · cutover NO-GO / 9/10. Schema head = goose 042.
Hard live blockers (still OPS OPEN — excluded this wave):
| # | Blocker | Why still open |
|---|---|---|
| 1 | @legacy.local hygiene (Clerk removed) |
Not a hard NO-GO — Clerk removed from product; optional migrator patch only |
| 4 | Live SMTP + set-password / forgot-password login smoke | Delivery unproven (EMAIL_DRY_RUN / dry-run only) |
| 5 | Live Stripe keys + signed webhooks | Presence flags only; no live Checkout/webhook smoke |
LOCAL/CODE handled this wave (former blockers #2–#3, #6–#10): promote tooling + Demo already admin; local plan assign 87→0 (A1 untouched); orphan report/cleanup fail-closed (local 0); goose 039–042 + worker + deploy-check PASS; adapter-node pin/scripts; edge RPM env + nginx/Caddy snippets; Prometheus scrape/alert examples; api_keys/blobs/jobs honesty + admin inventory; store-reconnect admin inventory; session revoke on reset (042); cutover-local-rehearsal.mjs. Residual prod-host apply (migrate/restart Node/wire scrape/edge if N>1 / cutover-DB confirms) is mechanical deploy — not missing product work.
ASSUMPTION: Cutover 9/10 = tooling + local proofs complete and only SMTP + Stripe gate DNS. Routine prod-host npm run migrate / start:web / scrape wiring do not drop the score; clearing SMTP+Stripe is what moves toward GO. Clerk is removed from the product.
Scoring rules (split)
| Scorecard | Counts | Does not count |
|---|---|---|
| Product (code) | Shipped features, security architecture, perf foundations (indexes, durable jobs, limiters), UX activation/cutover chrome, unit/vet/browser proofs, setup DX | Live Stripe keys, SMTP delivery, DNS flip, scrape/alerting topology, orphan cleanup run, goose apply on prod host |
| Cutover (ops) | Operator data repair, live mail/billing smoke, migrate+worker deploy order, edge multi-replica caps, metrics scrape+alerts, Node host for adapter-node |
Whether CSRF/CSP//readyz/export rotate exist in code (those are Product) |
Dimension scores (honest, 2026-08-09)
Product (code) — 10/10 overall
| Dimension | Score | Evidence (code-verified) |
|---|---|---|
| Setup DX | 10/10 | npm run setup → dev (+ seed / health) + getting-started.md; /readyz needing worker is intentional (API-only → 503 expected) |
| Security architecture | 10/10 | CSRF/CSP/SSRF/encryption/APP_ENV=production fail-closed; prod metrics.Gate loopback/METRICS_PUBLIC; auth login vs register split (RateLimitAuth); RateLimitPublicExport; export rotate + 256-bit (publicExportTokenBytes=32); Stripe/mail fail-closed at use (boot does not require live STRIPE_*) |
| Performance foundations | 10/10 | Goose 040 claim/list composites; durable feed sync + ListenWake + stuck reclaim + findOrCreatePendingSyncJob; IETF RateLimit on 429 |
| UX approachability | 10/10 | Wave 7–8 P0 + store-sync UX wave (wizard, schedule, scope filters, live poll, Woo/Shopify checklists, plan-gate honesty); empty shells honest (ux-backlog.md) |
| Browser / tests | 10/10 | Wave 7–8 deep E2E PASS; Soft Undo + settings re-smoke PASS; mobile authenticated Feeds/dashboard proof PASS (4bdf0120); go test / go vet PASS; apps/web npm run check GREEN (0 errors / 0 warnings) via allowImportingTsExtensions + ja.ts dup-key cleanup (7c018ec6) |
| Product overall | 10/10 | No remaining code gaps that block this scorecard (see Product 10 assumptions + cutover list) |
Product 10 assumptions
Product 10/10 is valid without Redis/Valkey shared rate-limit store or multi-replica edge caps. These deployment assumptions are accepted for the Product scorecard (they do not reduce Product below 10; violating them in a multi-replica prod topology is Cutover/ops):
| # | ASSUMPTION | Product implication | Cutover if violated |
|---|---|---|---|
| 1 | Single API instance (default), or multi-replica with edge cluster caps | In-process limiters are Product-OK on one process; N× RPM without edge is not a missing Redis product gap |
When N>1: edge/WAF hard global RPM (cutover). Optional RATE_LIMIT_REPLICAS=N only divides HTTP middleware caps (rateLimitEffectiveCap) under even load — not lockout/StartLimiter/AI/email, not a shared store |
| 2 | Worker required for ready | /readyz needs fresh processing heartbeat ≤60s (jobs.ProbeWorkerReadiness); API-only → 503 is expected, not a Product defect |
goose 039–042 → restart cmd/worker so readiness probes green |
| 3 | Metrics loopback in prod | metrics.Gate: allow loopback peers or METRICS_PUBLIC=1; default is not a public no-auth scrape surface |
Example scrape/alerts CODE DONE (deploy/prometheus/); live targets + edge deny still OPS OPEN |
ASSUMPTION: Product rates shipped code + local/staging proofs, not production scrape/alerting/LB topology.
ASSUMPTION: Multi-tenant load soak is ops verification; foundations (040 indexes, durable claim/LISTEN) are shipped.
ASSUMPTION: Deferred polish accepted for Product 10 — waived P0-8/9 remainders, export-selected→feed picker, admin logs stub, structured-descriptions/vector-categories empty shells (honesty banners). P1-18 forgot-password is shipped (041_password_reset_tokens + /forgot-password//reset-password; admin set-password re-issue remains for migration cohort). Re-open remaining polish via ux-backlog.md / design-gaps.md, not as Product blockers.
Production cutover (ops) — NO-GO / 9/10
| Dimension | Score | Why not higher |
|---|---|---|
| Cutover / DNS | 9/10 | LOCAL/CODE blockers cleared; DNS still gated by live SMTP + live Stripe (plus mechanical prod-host migrate/Node). Clerk removed from product. |
| Live integrations | unproven | SMTP set-password smoke, Stripe live keys+webhooks — code ready, live ops not run |
| Deploy topology | LOCAL GO / prod apply open | goose 039–042 + worker + adapter-node gates PASS locally; metrics/edge examples CODE DONE — live scrape/edge targets still operator-owned |
Code gaps that would block Product 10
None after Aug-9 audit (verified via codehelper: metrics.Gate, RotateExportFeedPublicToken / publicExportTokenBytes=32, RateLimitAuth / RateLimitPublicExport, ListenWake, findOrCreatePendingSyncJob, CleanupOrphanProcessed, goose 040_job_hotpath_indexes, mail.ApplyDryRun, config.validate Stripe-at-boot honesty, adapter-node@5.5.7; npm run check green 7c018ec6). Sibling fixers: no Product-10 code blockers — work remaining is Cutover/ops (table below).
What fixed this wave (Aug-9 siblings + synthesis)
| Area | Change | Score impact |
|---|---|---|
| Job hot-path indexes | Goose 040_job_hotpath_indexes — claim/list composites for feed_sync_jobs / processing_jobs / processing_job_products |
Product Perf foundations complete; load soak remains Cutover/ops |
| Forgot-password | Goose 041_password_reset_tokens + self-serve forgot/reset API + UI (forgot-password.md; ux-backlog P1-18 done) |
Product auth polish shipped; live SMTP smoke still Cutover/ops |
| Worker readiness | Goose 039 + /readyz heartbeat ≤60s (prior sibling) |
Reliability — deploy: migrate → restart worker |
| Durable feed sync | 202 + poll + LISTEN wake + stuck reclaim + same-feed pending dedupe (014e2507 / 2f1019f2) | Reliability — residuals closed in code |
| Orphan processed | Admin report/cleanup (prior sibling) | Data hygiene — ops once |
| Observability | GET /metrics Prometheus text + prod Gate (loopback / METRICS_PUBLIC=1); worker optional METRICS_ADDR; examples in deploy/prometheus/ |
Observability gated in prod; example scrape/alerts CODE DONE; live targets OPS OPEN |
| Abuse limits | Split auth login vs register; API-key IP/company; public export IP/probe/token RPM | Security — still per-process; edge caps for multi-replica |
| Export public tokens | Rotate endpoint + 256-bit new tokens (168a394e); legacy 128-bit hex still accepted | Security — revoke old URLs via rotate |
| Adapter | adapter-node@5.5.7 pinned |
Deploy shape decided; Node host still required |
| Web typecheck | Sibling 7c018ec6: tsconfig allowImportingTsExtensions + ja.ts dup keys → npm run check GREEN (0/0) |
Product tests/DX gate closed (was blocking honest Product 10 claim) |
Store-sync UX wave (Aug-9)
Closed the top merchant gaps from the store-sync UX review (A1 read-only / clean; mutations & smoke on Platform Demo only).
| Shipped | Notes |
|---|---|
Activation store-connect + /stores/wizard |
Optional after Process; plan-gated stores.hub; connect → test → first sync → done |
| Product sync schedule | Interval + schedule_paused; Woo/Shopify PUT …/schedule |
| What-to-sync filters | Optional sync body: status / category / limit / product_ids; UI scope controls |
| Live sync progress | Poll config status after enqueue; retry on fail |
| Woo REST-key + Shopify Dev Dashboard client-credentials | Deep links / scopes; auto token refresh; no App Store one-click install |
| Direction copy, plan-gate "still works", member locked UX | i18n parity (~5339 keys) |
| Feed schedule + export picker polish | Auto vs manual clarity; export empty/help CTAs |
| Tests / gates | go test shopify+woo+httpapi; web 194 pass; npm run check green; A1 /stores plan-gate browser PASS |
Truly-easy onboarding wave (Aug-9 afternoon)
~20 parallel siblings closed the prior "still not truly easy" polish list plus discoverability / empty-state / test coverage. A1 stayed clean (read-only / no mutations); Demo-only for live sync enqueue + browser proofs.
| Shipped | Notes |
|---|---|
| Dashboard value prop | ActivationChecklist: Catalog in → Enrich/process → Export/stores + one Do this step CTA |
| Activation copy | Feed-only connect-source; store-connect clearly optional (Skip for now); Continue destinations foolproof |
| Product tour | Reordered Feeds → Process → Export/Stores; dead selectors fixed; i18n tips |
| Empty states (7) | Uploads, exports, stores hub, Woo/Shopify orders & reviews, dashboard jobs |
| Shopify connect ease | Client-credentials OAuth + Dev Dashboard checklist (not App Store one-click) |
| Product multi-select sync | UI picker → product_ids (cap 500) on Shopify + Woo |
| Store delivery honesty | Poll vs push banner; Store webhooks: not available (no fake receive path) |
| Inline help | Interval / pause / match strategy / mapping required (~8 controls) |
| API keys first-run | One-time secret warning + Bearer / X-API-Key + docs link |
| Mobile nav | Essentials pinned; staff unlock while capabilities load; Demo mobile PASS |
| Command palette | Discoverable Search + shortcut; ranking helpers + tests |
| Dropdown a11y | Arrow/Home/End focus; aria-labelledby |
| Nav crash fix | paletteShortcut ReferenceError fixed; Demo login→dashboard PASS |
| Browser Demo | Woo live sync progress PASS; dashboard CTA proof after palette fix |
| Tests | Feeds/export/products/search/selection/sync-scope/menu/command-palette helpers; Go process gates + schedule + sync body |
Gates (live verify this synthesis): apps/web npm test 194/194; npm run check 0/0; go test ./internal/shopify/ ./internal/woocommerce/ ./internal/httpapi/ ok.
Still not anyone-can-onboard perfect (deferred, not Product blockers): Shopify App Store / Partner one-click install; real store webhook receive + registration (full HMAC + routing + idempotency — do not stub CSRF-exempt placeholder routes; see docs/store-connectors.md); exhaustive UI E2E of every control (unit helpers expanded; full click-matrix not claimed); Demo Shopify live-progress when connector is plan-gated (Woo Demo proof stands in).
Ops: Prometheus scrape (/metrics) — gated in production
Code: API mounts GET/HEAD /metrics (metrics.Handler + RED middleware; path skipped from self-instrumentation) behind metrics.Gate:
- Non-production: always allowed (local scrapes).
- Production: allow only loopback peers or
METRICS_PUBLIC=1(config.MetricsPublic); otherwise 404 (not a public scrape surface).
Admin diagnostics document the same rule. Worker sync histograms/failures appear on the worker process when METRICS_ADDR is set (separate listener, e.g. :9091), also wrapped with Gate.
Not a P0 “public no-auth metrics†code gap when Gate defaults hold. Edge lock-down remains recommended defense-in-depth (and required if you intentionally set METRICS_PUBLIC=1).
Scrape (recommend):
# Prometheus — API RED (scrape from loopback/sidecar, or set METRICS_PUBLIC=1 on a private VIP only)
- job_name: descrybe-api
metrics_path: /metrics
static_configs:
- targets: ["api.internal:28471"] # private / service mesh only
# Worker sync series (only if METRICS_ADDR set)
- job_name: descrybe-worker
metrics_path: /metrics
static_configs:
- targets: ["worker.internal:9091"]
| Control | Recommendation |
|---|---|
| Default prod | Keep METRICS_PUBLIC unset/0; scrape via loopback sidecar / mesh |
| Network | Prefer private subnet / VPN; deny public VIP even if Gate is on |
| Reverse proxy | Drop or deny /metrics on the internet-facing vhost; optional basic-auth or mTLS on internal scrape path |
| Worker | Bind METRICS_ADDR to loopback or private interface only (127.0.0.1:9091 or mesh DNS) |
| Alerting | Wire RED + sync rules below — examples CODE DONE; live targets still OPS OPEN |
Repo examples (no secrets): deploy/prometheus/scrape.example.yml · deploy/prometheus/alerts.example.yml · deploy/prometheus/README.md
Topology (cutover):
Prometheus ──scrape──► API HTTP_ADDR GET /metrics → http_requests_total, http_request_duration_seconds
──scrape──► Worker METRICS_ADDR GET /metrics → sync_duration_seconds, sync_failures_total
──probe───► API HTTP_ADDR GET /readyz → worker freshness (503 if missing/stale >60s)
LB / public VIP ──deny──► /metrics (scrape only via loopback sidecar, mesh, or private VIP)
| Process | Env / bind | Series |
|---|---|---|
cmd/api |
HTTP_ADDR (app port) |
HTTP RED: http_requests_total{method,code,path}, http_request_duration_seconds{method,code,path} |
cmd/worker |
METRICS_ADDR (optional; e.g. 127.0.0.1:9091) |
Sync: sync_duration_seconds{kind}, sync_failures_total{kind} (feed, woocommerce*, shopify*) |
blackbox → /readyz |
private API base | probe_success (not heartbeat age); triage JSON worker_last_seen_age_s |
Suggested alerts (ops — tune thresholds to baseline; copy from alerts.example.yml):
| Alert | Signal | Starter expression (illustrative) |
|---|---|---|
| API 5xx rate (RED Errors) | http_requests_total code=~"5.." |
sum(rate(http_requests_total{job="descrybe-api",code=~"5.."}[5m])) / sum(rate(http_requests_total{job="descrybe-api"}[5m])) > 0.05 for 10m |
| API latency (RED Duration) | http_request_duration_seconds |
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{job="descrybe-api"}[5m]))) > 2 for 15m |
| Sync failures | sync_failures_total |
sum by (kind) (increase(sync_failures_total{job="descrybe-worker"}[15m])) > 0 for 15m (page on sustained / critical kinds) |
| Sync stall / slow | sync_duration_seconds |
histogram_quantile(0.95, sum by (le, kind) (rate(sync_duration_seconds_bucket{job="descrybe-worker"}[15m]))) > 120 for 30m |
| Scrape down | up |
`up{job=~"descrybe-api |
| Worker heartbeat stale | /readyz (not Prom) |
Blackbox/HTTP probe: /readyz ≠ 200, or JSON checks.worker ∈ {missing,stale}. On-call triage: read worker_last_seen_age_s (seconds since processing touch; omit when no row; stale after 60s = jobs.DefaultHeartbeatStaleAfter). Same field printed by scripts/cutover-deploy-check.mjs |
On-call note: Heartbeat age is not a /metrics series — use GET /readyz worker_last_seen_age_s + checks.worker when paging worker-down (confirm how stale before restart). Do not start a second worker while age is still fresh (see ops-runtime.md).
Cutover boxes: production-checklist.md §1d. Example files: deploy/prometheus/. No secrets in scrape configs — private targets / sidecars only.
Missed external checklist items (not code blockers, still gate honest Prod GO): dependency high/critical audit, backup restore drill, on-call runbook, DSAR/export rehearsal, per-tenant dashboards (series are route/kind-scoped, not company_id), launch-day rollback criterion.
Aug-8 fixes summary (2026-08-08)
| Area | Change | Cutover impact |
|---|---|---|
| Stripe boot vs runtime | config.validate in production requires STRIPE_MOCK=false only — does not require env STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET at API boot. Keys may live in /admin/settings (platform_settings); checkout/webhooks fail closed until configured. Env remains optional fallback (stripe-setup.md). |
Ops still must configure live Stripe + webhook signing before taking payments; boot alone is not proof of billing readiness |
| Transactional email dry-run | mail.ApplyDryRun honors EmailDryRun so SMTP-on + dry-run cannot send for real. Admin POST /api/admin/settings/mail/test returns skipped under dry-run. |
Live SMTP / set-password login smoke remains ops-only / unproven |
| Public export feeds | Wrong extension on a valid public_token returns opaque 404 (was 400 format mismatch) — closes token-existence oracle. Tokens case-normalized; API only advertises the matching format URL. BREAKING for public clients that treated 400 as “token exists, wrong formatâ€. |
Security harden; update any scrapers/clients that branched on 400 |
| Export token rotate / entropy / RPM | Shipped (168a394e): RotateExportFeedPublicToken + handler; new tokens 256-bit CSPRNG (publicExportTokenBytes=32); RateLimitPublicExport IP/probe/token budgets (stricter than generic /api/public) |
Code done — not an open residual |
| Edge rate limits | Confirmed: in-app limiters remain per process; multi-replica cutover = edge hard global RPM. Optional RATE_LIMIT_REPLICAS divides HTTP middleware only (not a substitute). |
Ops note only — no shared Redis limiter in-repo |
| Web adapter | Decided (2026-08-09): pin @sveltejs/adapter-node@5.5.7 in apps/web. Evidence: hooks.server.ts (CSP/HSTS/legacy redirects), +page.server.ts (e.g. /tasks SSR redirect), +server.ts (admin translations). adapter-static rejected — request-time SSR surface cannot be prerender-only. |
Deploy Node process for build/ output; reverse-proxy WEB_ORIGIN to it |
| Orphan processed cleanup | Code landed (sibling 97a2be08): admin report/cleanup + processOne TX hardening. Ops must once run report then confirmed cleanup (platform session). |
Does not clear Prod GO — data/SMTP/Stripe blockers remain |
/readyz worker heartbeat |
Code landed (sibling 63ee62c5): goose 039_worker_heartbeats; /readyz requires fresh processing heartbeat (≤60s). |
Deploy order: goose through 041 (includes 039+040+041) → restart worker. API-only local without worker → /readyz 503 (expected). Does not clear Prod GO |
| Durable feed sync | Code landed (014e2507 / 2f1019f2): API EnqueueSync → feed_sync_jobs + pg_notify; worker claim (FOR UPDATE SKIP LOCKED); LISTEN wake (jobs.ListenWake); stuck running reclaim via processing.CleanupStuck (includes feed_sync_jobs); same-feed pending reuse (findOrCreatePendingSyncJob). Dashboard 202 + poll. |
Worker required for completion. Prior “LISTEN / stuck reclaim / same-feed dedupe†residuals are done |
Aug-9 ops note (orphan processed)
After ETL / staging data is on the target DB (and before trusting processed-product counts), platform admins must run once:
- Open /admin/orphan-processed (or
GET /api/admin/jobs/orphan-processed) — report counts/samples (no delete). - Optional dry-run: Dry-run cleanup in the UI (POST without confirm).
- If orphans exist: Confirm delete… in the UI, or
POST /api/admin/jobs/orphan-processed-cleanupwithconfirm=true— deletes; withoutconfirm=truethe POST is report-only.
Requires platform-admin session + CSRF like other /api/admin/* mutations. This alone is not production GO.
Aug-9 ops note (/readyz + worker + goose head)
Sibling 63ee62c5 + 040 + 041: readiness is no longer DB-only; claim/list hot paths need indexes; forgot-password needs reset tokens.
- Apply goose through
041_password_reset_tokens.sql(includes 039 worker heartbeats + 040 hotpath indexes). - Restart
cmd/workerso it touchesworker_heartbeats(jobs.TouchHeartbeat). - Confirm
GET /readyz→ 200 withchecks.worker=ok(stale threshold 60s).
API-only (no worker): /healthz 200, /readyz 503 (worker heartbeat missing / stale). Do not treat that as a broken API binary — start the worker (or accept 503 on readiness probes in that topology). Not Prod GO by itself.
Operator cmd (read-only): node scripts/cutover-deploy-check.mjs - asserts goose 039+040+041 applied + /readyz worker ok + prints adapter-node host notes (no migrate/restart).
Local verify (2026-08-09): Recycled to unified npm run dev (api+web+worker) after browsers hit CONNECTION_REFUSED on :28472 (API/worker were up without Vite). Confirmed: GET http://127.0.0.1:28471/readyz → 200 (checks.database/queue/worker=ok); http://localhost:28472/ → 200 (browser landing + Vite HMR). Stale standalone worker killed so only the concurrently worker remains. No A1 data writes.
Aug-9 ops note (durable feed sync)
Sibling 014e2507: feed sync is durable (no API-process goroutine sync).
| Step | Behavior |
|---|---|
| Dashboard sync | POST /api/feeds/{id}/sync → 202 job payload; UI polls /api/feeds/{id}/sync-jobs/{jobID} |
| Persistence | Row in feed_sync_jobs (pending → worker claims running) |
| Worker | cmd/worker claim loop + ProcessSyncJob; also needs heartbeat for /readyz |
| v1 | Legacy envelope may still return 200 { data: { jobId } } — dual-support |
Shipped residuals (mark done): worker ListenWake on feed_sync_jobs / processing_jobs (NOTIFY remains wake-only; claim/poll is source of truth); stuck running sync reclaim in CleanupStuck (worker loop + admin); same-feed pending dedupe in EnqueueSync / findOrCreatePendingSyncJob. Retention delete of terminal sync jobs: CleanupExpiredSyncJobs. Do not re-list these as open code blockers. Scale/load soak and multi-replica edge caps remain Cutover/ops (Product Perf foundations already 10).
What landed (Wave 8)
| Area | Landed |
|---|---|
| Cutover UX | P0-10 login password_not_set CTA; P0-11 role repair (Wave 7, retained); P0-12 missing-plan / past_due recovery (no fake Free/Unlimited); P0-13 store reconnect cliff; P0-14 API-key reissue honesty; P1-13 migrated-cohort checklist; P1-14 invite email mismatch; P1-15 admin readiness strip (GET /api/admin/readiness); P1-16 known ETL gap panel |
| Cutover ops tooling | Migrator -list-companies-without-plans / -assign-missing-plans / ETL -fallback-plan-name; GET /api/admin/companies?without_active_plan=1; staging-auth-rehearsal scripts (promote → re-issue link → login smoke without live .env edits) |
| Honesty / polish | FTP sync “not supported†upfront; empty shells (vector-categories, structured-descriptions, campaigns); Settings replaceState race fix; bulk Accept Undo (admin → /api/products/reset); migrator gap report honesty |
| Security | Team PATCH rejects invalid silent demote; DELETE blocks last admin; Stripe live keys fail closed at use (admin settings and/or env); single-user set-password reissue 429 when rate-limited; CSP connect-src rejects non-http(s); OpenAPI documents team PATCH + admin set-password |
| Perf / a11y | Campaigns list batch DISTINCT ON (N+1 gone); ConfirmationDialog alertdialog focus; Alerts aria-label; ⌘K aria-live |
What landed (Wave 7, retained)
| Area | Landed |
|---|---|
| Cutover code | Membership role repair (PATCH /api/team/{userID}); admin set-password re-issue harden |
| UX | ⌘K (P1-10); bulk confirm (P1-3); Needs Review field accept/discard; mapping chips; Mapped honesty; Alerts role=switch; maintenance banner |
| Security/ops | CSP API+web; Stripe mock blocked in production; SMTP/Resend gated at send time when live email; CSRF cookie name sync |
Tests (Wave 8 suite)
| Gate | Result |
|---|---|
apps/api go test ./... -count=1 |
PASS (Full suite race Wave8) |
apps/api go vet ./... |
PASS |
apps/api go test -race ./... -count=1 |
PASS |
apps/web npm run check |
PASS / GREEN (0 errors / 0 warnings) — Aug-9 re-verify 7c018ec6 (allowImportingTsExtensions + ja.ts dup-key fix); Wave 8 suite also PASS |
apps/web npm run build |
PASS |
| Wave 8 final suite re-verify (e239d01f) | PASS — go test, vet, npm check, build |
| Wave 8 deep browser E2E (3880dabd) | Prior FAIL 10/11; Soft Undo PASS (POST /api/processing/jobs 202 + Undo); readiness 200 (GET /api/admin/readiness). Bare /settings replaceState fix landed (ddad0c89); re-smoke PASS (tabs render; no replaceState error; Alerts 6 switches) |
| Wave 7 deep browser E2E (651eeb31) | Last completed deep live proof PASS: Mapping incomplete; Alerts 6 switches; mapping chips; login; Needs Review; processing; billing wallet; products pager |
| Mobile Product10 browser proof (4bdf0120) | PASS — device=mobile (390×844); site=local-descrybe + laravel_login (secret); assert Feeds; authenticated dashboard (A1); 0 console/uncaught |
apps/api staticcheck ./... |
PASS with staticcheck.conf excluding ST1005 (legacy v1 processing_type API messages — see note below) |
Accepted staticcheck exception (ST1005)
Legacy POST /api/v1/products/process validation returns capitalized, punctuated client messages such as Invalid processing_type. Use "full", …. They live in internal/processing/v1_legacy.go and internal/httpapi/v1_process_handlers.go. Changing casing/punctuation would alter the public error contract, so ST1005 is suppressed in apps/api/staticcheck.conf rather than rewriting the strings. make vet / go vet remain the Makefile gate and are unaffected.
Remaining blockers (production cutover / ops) — true open items
Hard live (still OPEN — excluded from LOCAL/CODE wave):
| # | Severity | Blocker |
|---|---|---|
| 1 | Data | Optional @legacy.local hygiene — Clerk removed from product (tooling ready; not a hard NO-GO) |
| 4 | Ops | Live SMTP + set-password / forgot-password login smoke unproven (dry-run-safe code + diagnostics presence; live delivery not proven) |
| 5 | Ops | Real Stripe keys + signed webhooks in /admin/settings and/or env (presence flags shipped; live keys unproven) |
LOCAL/CODE handled (2026-08-09 evening wave) — not Product gaps; prod-host apply remains mechanical:
| # | Status | Item |
|---|---|---|
| 2 | CODE/LOCAL | Promote company admins — A1-safe migrator; Demo already admin; cutover -confirm only on target DB |
| 3 | CODE/LOCAL | Assign skipped company_plans — local 87→0 Free (-confirm); A1 untouched |
| 6 | CODE/LOCAL | goose 039–042 + worker + adapter-node — local deploy-check PASS; prod host still runs npm run migrate + restart + start:web |
| 7 | CODE/LOCAL | Orphan-processed — local orphans 0; confirm fail-closed + A1-protected; cutover DB run if needed |
| 8 | CODE DONE | Multi-replica edge RPM — env contract + deploy/examples/edge-rate-limit.md; live edge apply if N>1 |
| 9 | CODE DONE | Metrics scrape/alerts — deploy/prometheus/ examples; live targets OPS OPEN |
| 10 | CODE DONE (honesty) | Unmigrated api_keys/blobs/jobs — admin inventory + reissue/reconnect CTAs; no fake migration |
Not open code residuals (do not re-add): feed sync LISTEN / stuck reclaim / same-feed pending dedupe; export token rotate + 256-bit + export RPM; “public /metrics no auth†as a P0 (gated); forgot-password opaque 200 (API + browser).
ASSUMPTION: Staging/LOCAL GO means cutover rehearsal (tooling + local proofs), not production DNS flip. Hard GO requires live SMTP + live Stripe on the target env.
Verdict (security/ops)
Product (code) is 10/10 under the split scorecard above.
Security controls for API+web trust boundaries are production-capable when env/admin knobs below are set.
Production cutover remains NO-GO / 9/10, gated hard by live SMTP + live Stripe (go-live-checklist.md, migration-readiness.md, cutover.md). This document does not claim production DNS GO.
Security checklist (2026-08-04 harden + Wave 7–8 + Aug-8)
| Item | Status | Notes |
|---|---|---|
| CSRF (dashboard) | OK | Double-submit cookie + header; web cookie name via PUBLIC_CSRF_COOKIE_NAME (default descrybe_csrf); /api/v1 and webhooks exempt by design |
| Sessions | OK | HttpOnly + Postgres store; set SESSION_SECURE=true on HTTPS |
| Secure cookies | OK | Session + CSRF Secure follow SESSION_SECURE; production requires it |
| CORS | OK | Single WEB_ORIGIN; no *; credentials allowed |
| CSP | OK | API SecurityHeaders enforce default-src 'none'…; SvelteKit hooks; Wave 8: connect-src rejects non-http(s) |
| Stripe webhook verify | OK | Signature required whenever webhook secret is set (env or admin settings); unsigned only with mock + empty secret (local) |
| Stripe secret (prod) | OK (runtime) | Live Checkout needs secret key from admin settings and/or STRIPE_SECRET_KEY. validate does not require it at boot — missing key → checkout/portal fail closed |
| Stripe webhook secret (prod) | OK (runtime) | Live signed webhooks need secret from admin settings and/or STRIPE_WEBHOOK_SECRET. Not a boot requirement under APP_ENV=production |
| SMTP / email (prod) | OK (runtime + Aug-8) | Live send gated by platform/tenant config + dry-run. mail.ApplyDryRun disables SMTP send when dry-run on; admin mail test returns skipped. EMAIL_DRY_RUN=false alone does not force Resend/SMTP_HOST at boot |
| Public export token oracle | OK (Aug-8) | Format mismatch → opaque 404 (same as unknown token). BREAKING vs prior 400 |
| Export token rotate / entropy | OK (168a394e) | Rotate revokes prior URL; new tokens 256-bit hex; validation accepts legacy 128–256-bit hex |
Prod /metrics Gate |
OK (13fec7ea) | Loopback-only unless METRICS_PUBLIC=1; not a public no-auth scrape surface by default |
| Team / last-admin | OK | Wave 8: invalid role no longer silent-demotes; DELETE cannot remove last company admin |
| Set-password reissue | OK | Rate limits + Wave 8 single-user path returns 429 when limited; skips @legacy.local |
| Secret encryption at rest | OK | AES-GCM for Woo / email / AI BYOK; APP_ENCRYPTION_KEY required in prod |
| SSRF — feeds | OK | Dial + redirect guards; 50 MiB / 60s |
| SSRF — Woo | OK | Normalize + SafeHTTPClient dial |
| SSRF — Shopify | OK | NormalizeShopDomain / ValidateShopifyShopDomain + SafeHTTPClient |
| Rate limits | OK (single replica) | Auth login 10/min IP vs register 5/min IP (split buckets); email login lockout 5 fails → 15 min (429); public /api/public 30/min IP; export token GETs IP/probe/token budgets; API-key attempt 60/min IP + company 120/min; heavy sync/process/export 30/min/company; StartLimiter 20/min; marketing gen/send. In-process only — multi-replica cutover = edge hard caps; optional RATE_LIMIT_REPLICAS divides HTTP middleware only |
TRUSTED_PROXIES |
OK (docs) | Empty = ignore X-Forwarded-For; set to hop-1 proxy CIDRs so IP RPM / metrics peer checks see the real client. See security-notes.md. |
| Upload limits | OK | CSV 5 MiB; logo 2 MiB; JSON 2 MiB |
| No CLI secrets in UI | OK | Customer UI free of go run / seed / demo password copy |
| Production env fail-closed | OK | APP_ENV=production rejects insecure session/origin, missing encryption/signing secrets, and STRIPE_MOCK=true. Stripe live secrets + mail delivery are runtime gates (admin/env), not boot STRIPE_* env mandates |
Required production env
Fail-closed when APP_ENV=production (config.Load / validate) — process will not start without these:
APP_ENV=production
SESSION_SECURE=true
WEB_ORIGIN=https://<dashboard-origin>
APP_ENCRYPTION_KEY=<openssl rand -hex 32> # or CREDENTIALS_ENCRYPTION_KEY alias
TOKEN_SIGNING_SECRET=<openssl rand -hex 32>
STRIPE_MOCK=false
Stripe live keys are operator-required for payments, not boot-validated:
# Prefer /admin/settings (stripe.secret_key, stripe.webhook_secret, prices…)
# Optional env fallback:
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
Operator-required for a full production stack (not all enforced by validate):
PUBLIC_API_URL=https://<api-origin>
DATABASE_URL=postgres://...sslmode=require
EMAIL_DRY_RUN=false # only after SMTP/Resend proven; unset/default is dry-run (safe)
Optional but recommended: VPC egress deny to link-local/metadata, WAF in front of API.
Ops requirement: edge rate limits (multi-replica)
App-level limiters are per process (HTTP middleware in httpapi/ratelimit.go, email-keyed login lockout in httpapi/login_lockout.go, processing.StartLimiter, support.AIRateLimiter, email sliding limiter). There is no Redis / Valkey / memcached (or other shared rate-limit store) in this stack — confirmed by diagCache ("in-process only") and deps.
ASSUMPTION (Product 10): Single-node (one API process) in-memory rate limits + email login lockout are an accepted product posture. Multi-replica edge/Redis caps are cutover/ops, not a Product code gap. Captcha is intentionally deferred in favor of IP RPM + email lockout.
Cutover when N>1: enforce cluster-wide caps at the edge (CDN / reverse proxy / WAF / API gateway), aligned with the in-app budgets above (auth IP, login email lockout, public IP, heavy mutations per company, marketing generate/send, StartLimiter, AI auto-reply). Do not treat in-app 429s alone as a global guarantee: without edge, N replicas ≈ N× the configured RPM until shared infra exists.
Optional RATE_LIMIT_REPLICAS=N: divides only HTTP middleware caps in httpapi/ratelimit.go via rateLimitEffectiveCap (ceil) so aggregate under even load approximates documented RPM. It is not a shared counter, not a substitute for edge hard global caps, and does not apply to login email lockout, StartLimiter, AIRateLimiter, or email send limiters.
Concrete edge snippets (nginx + Caddy) + env contract: deploy/examples/edge-rate-limit.md. Ops boxes: production-checklist.md §1f · cutover.md §8.
Aug-9 research wave (landed + backlog):
| Item | Status | Notes |
|---|---|---|
IETF RateLimit / RateLimit-Policy on HTTP 429 (with existing Retry-After) |
Landed | writeRateLimited in httpapi/ratelimit.go — clients can pace without guessing |
| Task indicator concrete counters (“N of M productsâ€) | Landed | TaskStatusIndicator — show progress even at 0% once total is known |
Soft-warn headers on allowed requests (RateLimit remaining before hard 429) |
Recommend | Emit RateLimit with r= remaining on success paths for heavy mutations so SDKs throttle early (see STOA, IETF design notes) |
| Hybrid local + Redis/Valkey counters | Recommend | Keep in-process token bucket for sub-ms deny; reconcile to Redis for multi-replica fairness (Eltherion, Databricks batch-reporting). RATE_LIMIT_BACKEND=redis is still documentation-only |
| Per-endpoint quota classes (cheap GET vs heavy export) | Partial | Heavy-mutation bucket exists; extend policy matrix per tier when billing meters grow |
Dark-mode prefers-contrast: more focus rings |
Landed | layout.css media query boosts --ring / :focus-visible (token contrast wave already done) |
| Feed sync last-sync deltas in list UI | Recommend | API already returns last_sync_deltas via PresentFeed; surface per-row added/updated/failed counts next to last-synced (sync-state UX pattern) |
Deploy note: SvelteKit adapter (decided)
apps/web pins @sveltejs/adapter-node@5.5.7 (svelte.config.js). Why not static: the app relies on a Node request path — src/hooks.server.ts (handle: legacy redirects, password-query scrub, CSP/security headers, RapiDoc gzip), plus server loads/endpoints (routes/tasks/+page.server.ts, routes/admin/translations/catalog/+server.ts). Prerender/adapter-static would drop that surface. Ops scripts (code): npm run build --workspace=web then npm run start:web (node build). Read-only gate: npm run cutover:deploy-check (pin + config + scripts; plus goose//readyz when not skipped). Ops still required on host: run that Node process; point WEB_ORIGIN at it; do not claim Prod GO from the pin alone.
Verify before go-live
cd apps/api && go test ./... && go build ./...
cd ../web && npm run check
curl -sf https://<api>/healthz && curl -sf https://<api>/readyz
Staging rehearsal (no live .env edits): staging-auth-rehearsal.md / scripts/staging-auth-rehearsal.ps1|.sh.
Manual: CSRF rejection without token; login rate-limit after burst; Stripe CLI webhook signature; Woo/feed URL to private IP rejected; upload >5 MiB rejected; missing-plan / past_due banners; store reconnect when has_credentials=false; public export wrong-extension → 404 (not 400); admin mail test under dry-run → skipped.
Still not “cutover ready†(ops)
See true open cutover blockers above and go-live-checklist.md / migration-readiness.md. Schema/migrate head is goose 041. Wave 8 + Aug-8/9 code (billing recovery, orphan cleanup, worker heartbeat, durable sync + LISTEN/reclaim/dedupe, 040 indexes, 041 password reset / forgot-password, gated /metrics, abuse + export-token harden, npm run check green 7c018ec6, mobile browser proof) does justify Product 10/10; it does not clear SMTP/Stripe/migrate+worker/orphan ops blockers or Prod DNS GO.
Verify this synthesis (2026-08-09)
| Gate | Result |
|---|---|
apps/api go test ./internal/metrics/ ./internal/httpapi/ -count=1 |
PASS (shell) |
apps/api go vet ./internal/metrics/ ./internal/httpapi/ |
PASS |
| Mobile browser Product10 proof (4bdf0120) | PASS (Feeds assert, authenticated) |
apps/web npm run check (7c018ec6) |
GREEN (0 errors / 0 warnings; allowImportingTsExtensions + ja.ts) |
codehelper verify on same pkgs |
abstain/fail env (could not import net under MCP runner) — shell is source of truth here |
Wave-20 verify (2026-08-09 afternoon) — A1 clean / Demo mutations
Policy: demo-user.md — smoke/E2E/Postman/tests MUST use Platform Demo; A1 Slovenija is read-only.
| Gate | Result |
|---|---|
apps/api go test ./... -count=1 (DATABASE_URL :5433) |
GREEN — Free max_products expect 50; sync-claim integration resilient to live worker (ff63db4e) |
| Process security + gates-before-EnsureRaw + lockout unit tests | PASS (no A1 writes) |
| Export token rotate (unit + ephemeral company) | PASS — Demo/ephemeral only; never A1 |
scripts/v1-process-smoke (Demo API key) |
PASS → COMPLETED on Platform Demo |
A1 entitlements (api_access, api_keys, processing) |
PASS read-only; A1 not mutated |
| A1 smoke-EAN cleanup check | PASS — no 8700999… junk on A1 (Demo only) |
/readyz + worker |
PASS 200 (existing worker; no duplicate) |
apps/web npm test / check / build + root setup/install/build |
PASS |
| Mobile A1 read-only Feeds (699fca3d) | PASS (stack up :28471/:28472) |
Desktop A1 /feeds read-only |
PASS (Your Feeds, 12 feeds); light contrast PASS; dark toggle flaky/skipped |
| OpenAPI products/process dual IDs + gates | PASS (docs + PresentProduct raw_product_id) |
Feeds mapping_incomplete server batch |
PASS (PresentFeeds + tests) |
Verdict unchanged: Product 10/10 · Cutover NO-GO. Residual polish (not Product blockers): dark-theme toggle flake; optional cleanup of leftover Demo smoke EAN 8700999000001; mint/save A1 Postman key via demo impersonation if live Bearer proof needed (plaintext not recoverable).
Visual + permissions QA wave (2026-08-09 late afternoon)
~20 parallel siblings: plan/permission honesty + light/dark contrast + sampled Demo/A1 browser PNGs. Browser budget: each visual agent capped at 1–2 PNG calls (no machine thrash / no full click-matrix). Mutations stay on Platform Demo; A1 = read-only.
A1 clean confirmation
| Check | Result |
|---|---|
| A1 desktop plan-gate (418200db) | PASS — /dashboard shows Pay as you go; /stores → plan-upgrade-panel (“Store connectors are not on your plan” + “What still works”); 0 console/uncaught/failed requests; no mutations |
| Staff unlock leak (5a1fca0a) | FIXED — normal A1 no longer gets unlockAllFeatures via legacy is_platform_admin; live a1-primary stores/marketing/ai false |
| Plan matrix / PAYG processing (8a7374b5) | FIXED — admin UI no longer treats A1 PAYG like restricted Legacy (Processing stays ON) |
Entitlements API ↔ FE can() (7e552a33) |
Catalog drift closed (capability.storage_limit, dashboard ETL keys, AI titles vs descriptions) |
| Wave policy | All permission/UI agents report no A1 writes |
ASSUMPTION: A1 clean = no process/sync/feed/store mutations and no staff unlock pollution this wave (browser + code audits). Not a fresh DB dump compare.
Screenshot / browser proofs (sampled)
| Surface | Agent | Result |
|---|---|---|
A1 /dashboard + /stores plan honesty |
418200db | PASS (2 desktop PNGs) |
Demo light /dashboard + /feeds |
571f8f18 | PASS (empty feeds OK; 0 console) |
| Demo dark shell | 08a28419 | Partial — dark dashboard captured; /products//stores missed (navigate/Accept budget) |
Demo mobile dashboard + Quick links → /feeds |
f583f4d6 | PASS — login → Quick links → authenticated /feeds (“Your Feeds”); 0 console |
| Demo Woo Connection | e09a70ad | PASS — schedule/reconnect/permissions UI; no sync queued |
Demo /feeds Add Feed dialog |
9106548b | Empty list → opened Add Feed read-only; header overlap + sticky dialog footer fixed |
Demo /products |
ae25bb09 → 7f23ebfd | Chevrons + header badge spacing FIXED (All Feeds / Sort show unfold icons) |
Demo /settings API keys + Team |
92e69159 | PASS read-only; apiKeysIntro “endpoints”; no key created |
| Demo shell CommandPalette + theme | ad8dbc5a | PASS — 0 console; prior paletteShortcut crash already fixed |
Fixes landed this wave
| Area | Change |
|---|---|
| Staff unlock | $lib/staff-access.ts — shouldUnlockAllFeatures / isFullPlatformAdmin; fail-closed premium canFeature when matrix missing |
| Plan honesty UI | PlanUpgradePanel + PlanHonestyPanel on Settings/Billing/Stores; locked tabs/CTAs show upgrade path |
| Member locks | Stores Connect/Reconnect/guided setup gated to admin (storeConnectorAction → View for members) |
| A1 PAYG cohort | Admin plan permissions treat custom A1 like backend A1PaygPlanFeatures (Processing ON; Stores OFF) |
/plans feature overlay |
Stable nameKey map (was brittle English “AI titles & descriptions” mismatch) |
| Catalog parity | Go + FE capability.storage_limit + dashboard ETL keys; AI titles vs descriptions split |
| Nav gates | navItemAllowed / settingsTabAllowed + unit tests; premium fail-closed |
| Light contrast | --muted-foreground (~7.5:1) + badge/chip tones → text-foreground |
| Dark contrast | Tokenized banners + dark --muted-foreground raised 255 14% 86% (d389b4e2) |
| Products filters / header | All Feeds + Sort ChevronsUpDown; switcher/badge spacing (7f23ebfd) |
Settings ?tab= sync |
On disk: urlSyncReady + applyTabFromSearchParams / syncTabToUrl; $effect never overwrites an explicit valid ?tab= |
| Feeds chrome | UserSwitcher overflow vs unread badge; sticky Add/Edit Feed dialog footer |
| Catalog tests | plan-feature-catalog.vitest.ts removed → plan-feature-catalog.test.ts under node:test |
| i18n | Packs 5443 keys; 17/17 i18n tests; permission/plan keys mirrored; API keys “endpoints” |
Gates (sibling-reported)
| Gate | Result |
|---|---|
apps/web npm test |
~230 pass (incl. catalog parity under node:test) |
apps/web npm run check |
0 errors / 0 warnings |
Go ./internal/billing/ catalog/A1 filters |
PASS |
| i18n pack parity | 17/17 |
finish_check |
Often abstain on this bed (review_diff / empty git history) — not a product defect |
Remaining gaps vs production-ready cutover
Cutover (ops) — still NO-GO: role promote, plan assign, live SMTP + set-password smoke, live Stripe + webhooks, goose 039+040+041 + worker on prod host, orphan cleanup run, multi-replica edge RPM, metrics live scrape targets + Alertmanager (examples CODE DONE), Node adapter-node host. See blockers table above.
Product polish closed this follow-up: mobile Quick links → /feeds PASS; products filter chevrons + header badge spacing FIXED; dark muted-foreground raised (+ light muted/badges); settings ?tab= sync FIXED on disk; catalog vitest → node:test; npm run check 0/0 / web tests ~230.
Still open (not Product-10 or cutover blockers):
| Gap | Notes |
|---|---|
| Exhaustive UI E2E | Intentionally not claimed — browser agents stayed at 1–2 PNGs |
| Optional Demo dark re-shot | Muted token fixed; /products//stores dark PNG not re-captured after bump |
Verdict unchanged: Product 10/10 · staging GO · production cutover NO-GO. A1 stayed clean. Visual+permissions residuals above are polish-only; ops cutover list still gates public DNS.
Final miss-check wave (2026-08-09 evening)
~20 parallel siblings answered: Did we miss anything? Does it work/look right? Is it production ready? Sampled proofs only; mutations on Platform Demo; A1 = read-only.
Executive verdict (this wave)
| Question | Answer |
|---|---|
| Did we miss anything (Product code)? | No Product-10 blockers. Closed residual honesty/permission/UX leaks found this wave (stores hub panel, campaigns API gates, CSRF /api/v10 lookalike, billing upgrade ladder, empty-state CTAs, dashboard truncation, mangled Woo/plan.upgrade locales). Minor polish leftovers remain (e.g. some productoos word-salad outside store/Woo) — not scorecard blockers. |
| Does it work / look right? | Yes for sampled surfaces. Gates green; Demo/A1/mobile/dark/shell browser proofs largely PASS; /readyz 200; Demo v1-process-smoke COMPLETED. |
| Product (code) ready? | YES — 10/10 (split scorecard; same assumptions as above). |
| Production cutover ready? | NO — NO-GO / 9/10. SMTP/Stripe/DNS/deploy mechanics still open; Clerk not a blocker. |
| Ship to paying public internet today? | NO until Cutover blockers clear. |
| A1 clean? | YES — confirmed below. |
One-line: Product 10/10 · looks/works on sampled proofs · staging GO · cutover NO-GO. Live Stripe/SMTP/DNS remain Cutover-only.
Product vs Cutover (do not mix)
| Lane | This wave | Still open |
|---|---|---|
| Product | CSRF segment exempt harden; Demo stores honesty (stores.hub); campaigns requireFeatures; API-key create fail-closed in prod; member/plan UI gates; billing next-plan ladder; categories/attributes/campaigns empty CTAs; mangled campaigns.emptyMessage + Woo/plan.upgrade.* locale repair; What’s New + header truncation layout |
Some non-store productoos word-salad (campaigns/products); Stripe webhook dedicated RPM (optional DoS harden); exhaustive UI E2E not claimed |
| Cutover | Docs+flags reconfirmed — none of the 10 ops blockers fully cleared (blocker #9 examples CODE DONE; live scrape still OPS OPEN) | Optional @legacy.local hygiene, role promote, plan assign, live SMTP smoke, live Stripe+webhooks, goose 039+040+041+worker+Node host on prod, orphan cleanup run, multi-replica edge RPM, metrics live scrape targets, unmigrated api_keys/blobs/jobs |
A1 clean confirmation
| Check | Result |
|---|---|
A1 /stores plan honesty (51ec9b58) |
PASS — plan-upgrade-panel + “What still works”; Stores hidden in nav; 0 console/uncaught/failed; no mutations |
A1 /billing Included/Locked (087da6fa) |
PASS — both strings present; Settings reached Profile (no locked-tab claim on Profile) |
| Staff unlock regression (ef9add99) | 25/25 — A1 cannot unlockAllFeatures via legacy is_platform_admin |
| Demo stores honesty fix vs A1 (1b55661b) | Demo no longer shows denial when stores.hub allowed; A1 still shows denial — correct |
| Wave policy | Permission/UX/browser agents: no A1 writes; process smoke Demo-only |
ASSUMPTION: A1 clean = no process/sync/feed/store mutations and no staff-unlock pollution this wave. Not a fresh DB dump compare.
Gates / proofs (sibling-reported)
| Gate | Result |
|---|---|
apps/web npm run check |
0 errors / 0 warnings (acfb9ffd); re-confirmed after truncation fix (27db0700) |
apps/web npm test |
236 then 239/239 after locale repair (3390a8c5) |
Go billing / shopify / woocommerce / httpapi (+ ./...) |
PASS (09489d3b) |
| i18n packs | 5467 keys × 9 locales; sync filled=0 (7b71f711); Woo brand + plan.upgrade.* EN leftovers cleared (3390a8c5) |
| OpenAPI dual-ID + 402 gates | Aligned — tests PASS (67863cff) |
/readyz + /metrics Gate |
Code correct; local /readyz 200 worker ok (c1f18f77) |
scripts/v1-process-smoke (Demo key) |
COMPLETED (736644e5) |
| Mobile Demo Quick links → Feeds | PASS (20fcc97e) |
Demo dark /products+/stores |
PASS (0 console; honesty bug found → fixed) (20a7becc → 1b55661b) |
| Demo shell palette + theme | PASS 0 console (14c36e97) |
| Demo feeds Add Feed + products chevrons | PASS read-only (0c70b947) |
| Demo dashboard truncation | FIXED — What’s New + header readable (27db0700) |
| Security spot-check | No CSRF/auth/secret holes; CSRF path-prefix harden landed (5d4782c0) |
| Cutover checklist freshness | All named ops items still OPEN (b7f9320f, 984c0746) |
| Scorecard audit | Product GO · Cutover NO-GO (984c0746) |
Fixes landed this wave
| Area | Change |
|---|---|
| Demo stores honesty | PlanUpgradePanel only when !stores.hub; included honesty copy via resolveHonestyItemDescription |
| Campaigns / stores / API keys gates | API requireFeatures on campaigns; Shopify/Woo admin+plan gates; API-key create fail-closed if Billing nil in prod; FE FeatureGate / admin create guards |
| CSRF exempt paths | Segment match (/api/v1, /api/public, webhooks) — no /api/v10 false exempt |
| UX gaps | Billing next self-serve upgrade plan (not hardcoded Growth); categories/attributes empty CTAs; billing no-balance + vector empty CTAs; campaigns.emptyMessage locale repair |
| Dashboard truncation | NewsFeed compact wrap; wider header UserSwitcher/email floors; Demo PNG readable |
| Store/Woo + plan.upgrade i18n | Cleared WooComAcrcio-class mangling; 43 plan.upgrade.* keys × 8 locales no longer English leftovers |
Still open (not Product-10 blockers)
| Gap | Lane | Notes |
|---|---|---|
Non-store productoos word-salad |
Product polish | Campaigns/products locales; store/Woo + plan.upgrade.* already fixed |
| Settings tab “Locked” honesty | Product polish | Billing Included/Locked proven; Profile settings has no locked-tab claim |
| Stripe webhook dedicated rate limit | Product harden (optional) | Signature+body cap exist; no dedicated RPM |
| Cutover blockers #1–#10 | Cutover | Unchanged — see Remaining blockers table above |
| Launch hygiene | Cutover | Dep audit, backup restore drill, on-call runbook, DSAR rehearsal, rollback criterion |
Doc drift watch: prefer this file + production-checklist.md (goose 041) over older goose 001–007 mentions in some cutover docs.
Final answer for parent: Nothing material missed for Product 10. Sampled UI/API works and looks right. Not production-cutover ready — do not flip DNS.
Continuation cutover-prep wave (2026-08-09 evening+)
~20 parallel siblings after the miss-check: cutover tooling/docs/status surfaces (no live secrets) + re-proofs. Mutations stayed off A1; Demo for browser honesty.
Executive summary (for parent)
| Question | Answer |
|---|---|
| Product still 10/10? | YES — gates green; forgot-password (goose 041) closed deferred P1-18; no new Product blockers |
| Cutover still NO-GO? | YES — NO-GO / 6/10 — all 10 ops blockers still require operator runs / live integration proof |
| What this wave cleared? | Code/docs prep only — migrator dry-run tooling, admin Stripe/mail presence flags, deploy check script, sequenced runbook, UX/docs honesty. Zero live SMTP/Stripe/DNS/orphan-run clears |
| A1 clean? | YES — A1 /stores+/billing read-only PASS; Demo /stores honesty PASS; gate re-run reported no A1 pollution |
| Ship public internet today? | NO |
One-line: Product 10/10 · cutover tooling sharper · production cutover still NO-GO. Schema head = goose 041.
Cutover-prep shipped (code/docs — not ops clears)
| Area | Shipped |
|---|---|
| Forgot-password | Goose 041_password_reset_tokens + self-serve forgot/reset API/UI (forgot-password.md); admin set-password remains for migration cohort |
@legacy.local tooling |
Migrator -list-legacy-emails / -export-legacy-emails / dry-run patch (A1/live email protection) |
| Role promote | Migrator -list-member-memberships + dry-run/confirm member → company admin |
| Plan assign | -assign-missing-plans requires explicit -confirm after -dry-run |
| Orphan cleanup UX | /admin/orphan-processed: report → dry-run → confirm delete |
| Stripe readiness | Admin diagnostics presence flags (secret_key_set, webhook_secret_set, mock, mock_rejected_in_prod) — no secrets |
| SMTP readiness | Admin diagnostics mail presence/dry-run flags — /readyz stays DB/worker-only |
| Deploy check | npm run cutover:deploy-check / scripts/cutover-deploy-check.mjs — goose 039+040+041, /readyz worker, adapter-node host gates (pin/config/scripts) |
| Runbook | cutover.md sequences all 10 NO-GO blockers dry-run-first |
| Metrics / edge | Scrape topology + RED/sync alert notes; RATE_LIMIT_REPLICAS vs edge hard-RPM clarified |
| Migration messaging | API-key reissue OpenAPI/docs; Woo/Shopify/hub reconnect copy aligned |
| Webhooks honesty | Do not stub store webhook receive (CSRF-exempt edge); plan copy uses Full API (no fake store inbound webhooks) |
| Re-proofs | CSRF /api/v10 not exempt PASS; web 239 / check 0/0 / go test ./... PASS; i18n 5534 keys × 9 locales, 17/17; A1+Demo stores/billing honesty PASS |
Still ops-only NO-GO (unchanged clears)
Same table as Remaining blockers — tooling/docs ready does not equal run. Live SMTP delivery, live Stripe keys+webhooks, goose+worker+Node on prod host, orphan cleanup once, edge RPM, metrics live scrape targets (examples in deploy/prometheus/ are CODE DONE), and unmigrated api_keys/blobs/jobs remain open.
Product vs Cutover (this wave)
| Lane | Shipped | Still open |
|---|---|---|
| Product | Forgot-password 041; plan/webhook honesty copy; reconnect + API-key reissue messaging; CSRF retest | Optional polish only (not scorecard blockers) |
| Cutover | Operator tooling, diagnostics flags, deploy check, sequenced runbook, metrics/edge docs | All 10 blockers — ops must execute |
ASSUMPTION: Raising Cutover to GO requires live SMTP smoke + Stripe signed-webhook proof — Clerk is removed from the product.
Final answer for parent: Cutover-prep shipped (tooling/docs/status). Product still 10/10. Cutover still NO-GO. A1 clean. Do not flip DNS.
Local cutover-rehearsal wave (2026-08-09 late)
Single-stream ops rehearsal on loopback API :28471 / web :28472 (Compose Postgres :5433). A1 stayed read-only; Demo OK for opaque forgot-password.
| Check | Result |
|---|---|
cutover-deploy-check (prior) |
PASS — goose 039–041, /readyz shape, adapter-node (with HEALTH_BASE_URL=http://127.0.0.1:28471) |
| Migrator dry-runs (prior) | Legacy email list / promote (A1 skip) / assign-plans dry-run 87 Free — no -confirm |
| Forgot-password route | Proven — POST /api/auth/forgot-password → opaque {"status":"ok"} HTTP 200 (CSRF double-submit) for demo@descrybe.local and unknown email; prior UI 404 was down/stale API (route already mounted in Router), not a missing handler |
| Optional browser | One attempt hit native form 405 on /forgot-password (hydrate / cookie-banner race); API curl is the proof |
/readyz this session |
503 worker heartbeat stale — expected with API-only (no dev:worker); Product assumption #2 |
| Product score | Still 10/10 |
| Cutover | Still NO-GO / 9/10 — live SMTP/Stripe (+ prod host mechanics) still OPS OPEN; Clerk not a blocker |
| A1 | Clean — no A1 mutations this stream |
One-line: Local rehearsal unblocked forgot-password (opaque 200). Product 10/10. Cutover NO-GO. Do not flip DNS.
LOCAL/CODE cutover wave (2026-08-09 evening) — exclude SMTP/Stripe (Clerk removed)
~20 parallel siblings closed everything automatable except live SMTP and live Stripe. Clerk removed from product. Mutations off A1; Demo/local fixture only. Synthesis kept light (no re-run of 20 parallel test suites).
Executive table (for parent)
| Question | Answer |
|---|---|
| Product | 10/10 unchanged |
| Cutover | NO-GO / 9/10 (raised from 8/10; Clerk no longer a hard blocker) |
| Hard OPS OPEN | live SMTP · live Stripe only |
| LOCAL/CODE handled | Promote/assign (local 87→0), orphan 0, goose 042+worker, adapter-node gates, edge snippets, Prometheus examples, api_keys/blobs inventory, store reconnect admin, session revoke 042, rehearsal script, diagnostics cutover flags, forgot-pw browser opaque OK |
| Failed siblings | None material — light gates early green; no Product blockers |
| A1 clean? | YES |
| Ship public paid today? | NO |
One-line: LOCAL/CODE cutover prep done · Product 10/10 · cutover 9/10 NO-GO until live SMTP + live Stripe.
Sibling scoreboard (abbrev.)
| Agent | Result |
|---|---|
| Assign plans local confirm (a7a02b5b) | PASS — 87→0; A1 untouched |
| Promote admins (bd6a6e4e) | PASS — A1-safe; Demo already admin |
| Migrator A1 guards (a8c78f15) | PASS — tests lock skip |
| Orphan cleanup (41383749) | PASS — local 0; fail-closed |
| Goose/worker (109c6510) + 042 (5955bd90) | PASS — deploy-check green; schema 42 |
| Adapter-node (17c7bdb0) | PASS — pin/scripts; host deploy OPS |
| Edge RPM + snippets (ef782139, 8ac96d75) | CODE DONE |
| Metrics examples (143e0817) | CODE DONE — live targets OPS |
| Store reconnect (72fb8e54) | PASS — admin inventory |
| api_keys inventory (247a9cbb) | PASS — no fake migrate |
| Blobs/jobs inventory (514d1cd5) | PASS — diagnostics COUNTs |
| Forgot-pw UI (98f8dd9d) | PASS — browser opaque success |
| Session revoke 042 (f17cfa67) | PASS — goose 042 |
| Readyz API-only (0945d38d) | PASS — clearer 503 reason |
| Rehearsal script (3e7253b2) | PASS — no -confirm by default |
| Diagnostics flags (efab294a) | PASS |
| Checklist/docs (06835bdb) | PASS — CODE/LOCAL vs 3 OPS |
| Light gates (87bb7797) | Early green (no full ./... re-fanout) |
Final answer for parent: Everything except live SMTP / live Stripe is LOCAL/CODE handled. Product 10/10. Cutover 9/10 NO-GO. Clerk removed from product. A1 clean. Do not flip DNS.