Repository navigation
Remove demo mode - #414
Remove demo mode#414
Conversation
Deletes the "Demo Account" card from /pricing and everything behind it: the
one-click server action, the email gate, the 5-minute countdown, the upgrade
page, the cleanup cron and the seeded shared household.
app/(marketing)/demo/ the server action and the upgrade page
components/demo/ the email gate, the countdown pill, the choices
lib/demo/ config, session get-or-create, lead capture,
and the 27KB household seeder
app/api/cron/demo-cleanup/ expiry sweep, plus its vercel.json schedule
and its entry in scripts/cron-dispatch.mjs
supabase/seed_demo_account.sql
docs/demo-mode.md
tests/demo-config.test.ts
Unwired rather than stubbed. AppContext loses its DemoState field, so app-frame
no longer reads demo_sessions on every authenticated page load and app-shell no
longer branches on a demo that cannot exist. The home page drops the "Welcome
Bubaly Demo Account" greeting and the !isDemoAccount condition on the setup
nudge — with no demo account the condition was always true.
The CRM lead score loses its "Tried the demo" factor (+18). Its only source was
demo_email_uses, so keeping the signal would have meant a field that can never
be true silently reweighting every score.
25 catalogue keys removed from each of the six base languages.
THE DATABASE IS NOT TOUCHED BY THIS COMMIT, DELIBERATELY. The teardown lives in
docs/audit/demo-mode-teardown.sql as an operator script, not a migration,
because tests/migrations-are-additive.test.ts holds the entire migration history
to zero DROP TABLE / DROP COLUMN / TRUNCATE / DROP TYPE, and says why: an agent
authors migrations and a human applies the pending set later, so a destructive
statement sitting in that set destroys production data the moment they run
`supabase db push`. This teardown is destructive twice over — two dropped tables
and a families delete that cascades into all 174 family-scoped tables — so it
follows 0275's path (LB-016 §5) and is run by hand, after its read-only step 1.
Weakening that guard to let the migration through was the alternative and was
not taken. Nothing breaks if the script is never run: with the code gone both
tables take writes from nobody and the demo household is unreachable.
On disk this is ~86KB of source and 175 catalogue lines. It is not a
disk-space lever and was not treated as one — the tracked repository is 44MB
in total, and the 3.2GB working tree is .next (1.8GB) plus node_modules
(836MB root, 455MB mobile), all three gitignored and regenerable.
tsc --noEmit clean, next lint clean, i18n gate clean on all four surfaces,
899 files / 7,674 tests pass, and `next build` succeeds with no demo route in
the output.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Updates to Preview Branch (claude/bubaly-repo-connect-etzqg7) ↗︎
Tasks are run on every commit but only new migration files are pushed.
❌ Branch Error • Mon, 07 Sep 2026 17:34:08 UTC View logs for this Workflow Run ↗︎. |
|
The failureWhy it is not this PRThis PR adds no migration. Its only The repo carries 17 duplicate migration version numbers, all pre-existing on
The decisive evidence that the SQL itself is fine: Why it started now
Not fixing it here, and no re-runThe fix is renaming one file in each of 17 colliding pairs to a free version number. That touches applied migration history, invalidates No re-run either: a primary-key violation at a fixed statement index is deterministic, not a flake, and This is worth its own issue. Until those 17 collisions are resolved, Supabase Branching cannot produce a preview database for any branch that touches Everything else
Locally on this commit: Generated by Claude Code |
The "Bubaly Handled" band's primary button linked to /pricing#demo and read "Try a 5-minute live demo". Demo mode was removed in #414: there is no app/(marketing)/demo, no lib/demo, no TestAccountCard and no id="demo" under the pricing route, so the chip was both an inert link and a public claim of a capability the product does not have — on the one band whose whole job is to keep real and illustrative evidence visibly apart. Remove the chip, delete handledProof.tryDemo from all seven catalogues, and promote the Trust Center link (a page that exists) to the band's primary action. The band's header comment no longer advertises a third "LIVE" tier, and the stale "demo family excluded" notes in stats.ts / format.ts go with it. tests/marketing-handled-honesty.test.ts pinned href="/pricing#demo", which would have kept the dead link green; it now asserts the opposite, so the CTA cannot come back without the test noticing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
A French visitor was getting a translated header above an English page, and a
raw catalogue key in the footer reading siteFooter.acceptableUse. This makes the
whole public site render in the reader's language, and fixes the tooling that
reported it clean the entire time.
What was showing English: the site footer (every heading and link, on every
marketing page), the Knowledge Center block, all of /mobile, all four legal
pages (123 strings), the FAQ page, and inLanguage: 'en-US' in the JSON-LD, which
told every crawler the French page was English.
Why nothing caught it — three defects in the i18n tooling, each fixed:
* walk() skipped any directory named mobile/ios/android at ANY depth. The
intent was the sibling native projects at the repo root, so app/(marketing)/mobile
— a real public page — was invisible to the gate. Root-anchored now.
* the scanner matched copy against an allowlist of property names, so every
gap had to be found by reading English off a shipped page. It now recognises
copy by VALUE, with a list of properties that are never copy — wrong in the
over-reporting direction, which is the direction to be wrong in.
* i18n-lift stored the string exactly as it sat between the quotes, so a JS
escape survived into the catalogue and 29 values held a literal backslash.
Repaired, and a catalogue test now fails on any value carrying one.
`everything` is removed from the gated set, deliberately. It was gated on a zero
from a scanner that could not see data literals, could not see props outside its
allowlist, and skipped every directory named mobile. Widening it turned that
zero into 2,343 real strings, so the promise had never been true. It goes back
when it scans clean — a surface gated while dirty teaches everyone to ignore the
gate.
Brought up to date with main twice during review, across #414, #416, #417, #419
and #420. Conflicts in site-footer, structured-data and en-US resolved by
keeping main's titleKey/labelKey naming (the shape that makes a key in a
copy field visible on sight, which is what this PR exists to fix) and main's
relocated social row, while keeping BOTH sides in structured-data: the cached
social-links reader and this branch's translations.
The merged tree then failed catalogue-key-not-rendered-raw with 20 offenders,
and the test was right: those lists were half-lifted, a key beside a literal in
the same field. The render sites were already correct, so the literals were the
defect. Nine lifted — Children's privacy, two security commitments, the GDPR /
HIPAA / CCPA labels, two aiShowcase strings and three pricing tier labels.
Footer spacing: the language pill and social row share one wrapping flex row,
and gap-y-2 left them reading as one crowded block once wrapped. gap-y-4.
Known gaps, stated rather than hidden: 293 lifted keys are en-US only and render
English by the documented fallback; 2,343 strings remain across app/(app) and
components/modules; and the FAQ answers are database rows that no amount of code
translation reaches.
All six checks green. E2E needed one re-run: 9 iPad page.goto timeouts, no
assertion failures, 395 passing at three other viewports — the same signature
seen on #412, which contained no application code at all.
… word keyed (W3) The pricing page said what each plan cost. It now says what the price buys, in numbers the app can produce: a per-day framing under the monthly price, the outcomes each tier hands back, and a value block whose three sources stay visibly apart — the real cross-family aggregate (omitted below the public floor, never rounded up to a number), the illustrative sample computed by the app's own brief composer and badged as such, and the published case studies. The highlight rail is rebuilt from the homepage's own heroOutcomes.* keys so the two never drift; the upgrade modal reads the same registry. Held out of #424 because the branch's pricing-content.tsx predated main's i18n sweep of that file; landed here onto the keyed file with 51 new keys in all seven catalogues. The section's demo-seed clause is out of scope — demo mode was removed in #414 — and no replacement was invented. The reviewer verified 55 marketing/i18n test files on an export of the sha. Merged from worktree-wf_62bfdf60-bdb-1 (93d1940), the branch the agent's own report named. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…ARTIAL S-20 came back 6 of 7. Six clauses met: per-day amounts derived with ceil, the real card omitted below HANDLED_PUBLIC_MIN and never rendering a 0, the illustrative card's sample badge, the matrix stacking on phones, the toggle's touch targets, no competitor price. The seventh asks that "the demo family gets persisted completed runs with step events". There is no demo family. 3994805 "Remove demo mode (#414)" deliberately deleted lib/demo/ - including the 27KB household seeder the clause depends on - along with components/demo/, app/(marketing)/demo/, app/api/cron/demo-cleanup/, supabase/seed_demo_account.sql, docs/demo-mode.md and the pricing card, and unwired AppContext's DemoState rather than stubbing it. Verified in the tree (all four paths absent) and against the replayed database (no family matching '%demo%'). lib/demo/seed.ts survives only inside .claude/worktrees/*, which is the same stale-worktree trap that produced S-02's false finding. So the clause is stale, not unbuilt, and the remedy is to delete it. Recorded as SPEC-002 rather than deleted silently: the clause lives in a queue other agents read, and an agent that takes S-20 and tries to satisfy it would rebuild demo mode - re-adding a one-click shared household, an email gate and a cleanup cron that were removed on purpose. That cost is larger than the cost of the row. Three sections came back PARTIAL and all three meant something different: S-15 a genuine defect the acceptance tests missed -> fix the code S-14 a clause the code was RIGHT to disobey -> amend it, or migrate S-20 a clause describing a deleted feature -> delete the clause A pass that assumed any single shape would have been wrong two times in three: reporting a shipped feature as missing, or a deleted one as a regression, or quietly "fixing" a spec to match a real bug. Each clause was read rather than scored, which is the only reason all three came out right. Prose counts and row totals are cross-checked against the generated index, so the two derivations cannot drift apart silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…demo clause
The verification pass ended with a synthesiser that spot-checked clauses
independently rather than trusting the nineteen verdicts. It overturned S-11's
LANDED, and was half right.
HALF THAT WAS WRONG - "web tsc and mobile tsc both clean". The verifier marked
this met using an off-tree workaround (fetching types into a scratchpad), which
is not evidence, and on a bare checkout mobile/ really did typecheck red:
app/(tabs)/assistant.tsx(5,105): Cannot find module 'expo-audio'
mobile/node_modules/expo-audio was missing while mobile/package.json:27
declares expo-audio ~57.0.3. npm install in mobile/ restores it and
npx tsc --noEmit exits 0. So the clause holds - a stale sandbox node_modules,
not a code defect - and CI's "Mobile (Expo) · Typecheck · Config" job already
proves it on a fresh install. The synthesiser's proposed remedy, "add a mobile
install+typecheck step to CI", was already done.
HALF THAT WAS REAL - a named test had never been written. The queue's "Tests to
add" line asks for tests/display-render.test.ts extended with "ask tile renders
without a mic in SSR"; the file contained neither 'ask' nor 'mic'.
Now written, four cases. It needed a next/navigation mock, because AskBubaly
calls useRouter() and the Ask tile could not be server-rendered in that harness
at all - "invariant expected app router to be mounted" - which is the practical
reason nobody had written it. The mock is inert for the ten tests above it and
they pass identically.
Why the assertion is built the way it is: it asserts an ABSENCE, and a tile
that failed to render would also contain no mic. So each case first proves the
tile is really there, and a last case proves 'lucide-' survives SSR at all, or
not.toContain('lucide-mic') would pass on markup that renders a mic and the
whole block would be decoration. Mutation-tested: deleting mic-button.tsx:148's
`if (!micAvailable(support)) return null` turns exactly the two mic assertions
red and leaves both controls green.
S-20 also loses the demo requirements outright rather than only being annotated
(SPEC-002's own stated remedy): the summary line, the owned file lib/demo/seed.ts,
the TestAccountCard skeleton line, and the "Done when" clause. They asked for
work on a feature 3994805 "Remove demo mode (#414)" had already deleted, and
an agent taking S-20 would have rebuilt it. Section 6's W2+W7+W3 ask still says
public_stats() "excludes the demo family" - FLAGGED there, not edited, because
that list is owner-approved and the exclusion is a harmless no-op rather than a
bug. Corrected one more stale path the synthesiser caught: the section claimed
components/billing/upgrade-modal.tsx, which does not exist; the per-day-consuming
modal is components/app/upgrade-modal.tsx.
The lesson the synthesiser is the evidence for: a verifier that wants its own
section to pass will accept evidence it gathered off-tree. Nineteen agents
reading carefully still left one verdict a twentieth, reading only to disagree,
could overturn.
web tsc clean; mobile tsc clean after install; i18n gate clean; display-render
13/13 and launch-audit-docs 4/4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
Deletes the "Demo Account" card from
/pricingand everything behind it.What goes
app/(marketing)/demo/components/demo/lib/demo/app/api/cron/demo-cleanup/vercel.jsonschedule and its entry inscripts/cron-dispatch.mjssupabase/seed_demo_account.sqldocs/demo-mode.mdtests/demo-config.test.tsUnwired, not stubbed
AppContextloses itsDemoStatefield, soapp-frameno longer readsdemo_sessionson every authenticated page load andapp-shellno longer branches on a demo that cannot exist. The home page drops the "Welcome Bubaly Demo Account" greeting and the!isDemoAccountcondition on the setup nudge — with no demo account, that condition was always true.The CRM lead score loses its "Tried the demo" factor (+18). Its only source was
demo_email_uses, so keeping it would have meant a field that can never be true silently reweighting every score.25 catalogue keys removed from each of the six base languages.
The database is deliberately untouched by this PR
The teardown ships as
docs/audit/demo-mode-teardown.sql— an operator script, not a migration.tests/migrations-are-additive.test.tsholds the entire migration history to zeroDROP TABLE/DROP COLUMN/TRUNCATE/DROP TYPE, and states why: an agent authors migrations and a human applies the pending set later, so a destructive statement sitting in that set destroys production data the moment they runsupabase db push. I wrote the migration first and that guard caught it — correctly. The teardown is destructive twice over: two dropped tables, and afamiliesdelete that cascades into all 174 family-scoped tables (every one declaresON DELETE CASCADE; none usesRESTRICT).So it follows
0275's path (LB-016 §5): read-only step 1 first, then a guarded, idempotent step 2 that asserts its end state and raises rather than reporting success. Named*-teardown.sqlsorun-probes.sh, which globs*-check.sql, does not pull a mutating script into CI.Weakening that guard to let the migration through was the alternative and was not taken. Nothing breaks if the script is never run — with the code gone, both tables take writes from nobody and the demo household is unreachable.
On disk space
This is ~86KB of source and 175 catalogue lines. It is not a disk-space lever and wasn't treated as one. For the record, measured on this checkout:
.next1.8GB,node_modules836MB,mobile/node_modules455MBAll three are gitignored and regenerable; deleting
.nextreclaimed 1.8GB immediately.Verification
tsc --noEmitclean ·next lintclean · i18n gate clean on all four surfaces · 899 files / 7,674 tests pass ·next buildsucceeds with no demo route in the output.🤖 Generated with Claude Code
https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
Generated by Claude Code