Skip to content

Add admin tables' missing columns and fix QA round 3 UI nits (T19: DB-01, bug 7) - #225

Merged
Programcode merged 2 commits into
mainfrom
fix/t19-db01-ui-nits
Oct 23, 2026
Merged

Programcode merged 2 commits into
mainfrom
fix/t19-db01-ui-nits

Conversation

@Programcode

@Programcode Programcode commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Batch T19: DB-01 (admin Set limit 500 on production) and the QA round 3 UI nits (bug 7). One PR, scoped to this batch.

To apply and deploy

  • Migration to apply to production: supabase/migrations/20261023120000_reconcile_admin_table_drift.sql. It's additive and safe to rerun. Not applied by me.
  • Edge Functions changed: none. admin/index.ts is untouched, so nothing to deploy. The migration makes the existing function's writes work.
  • Merging deploys the frontend only.

DB-01: rate_limits.updated_at

Production's public.rate_limits predates 20260204000000_admin_improvements.sql, whose CREATE TABLE IF NOT EXISTS skipped it, so it has no updated_at. The admin function writes updated_at on Set limit and orders global limits by it. 20261022180000 had moved the readers to created_at and chose not to add the column; this batch asked for the column, so the new migration adds it instead of changing the function.

The migration checks the catalog before each step:

  • rate_limits.updated_at and global_prompts.updated_at: added only when missing, then backfilled from created_at. Existing values on local and staging are left alone.
  • global_prompts.is_active and created_by (Set prompt writes both): ADD COLUMN IF NOT EXISTS.
  • The unique keys the upserts infer, rate_limits (user_id, limit_type) and global_prompts (feature): created only when no unique index on exactly those columns exists. If duplicate rows would block a key, it raises a warning and skips it; nothing is deleted.
  • A BEFORE UPDATE trigger on rate_limits using the existing public.set_updated_at().

Drift survey of every table admin/index.ts touches:

  • rate_limits: missing updated_at on production (known). Fixed here.
  • global_prompts: created by the same skipped migration, so it may be legacy too; Set prompt writes updated_at, is_active and created_by and upserts on feature. Guarded here. Production's actual shape is unverified (no production access).
  • user_roles: same drift, already fixed by 20260907155821_reconcile_user_roles_updated_at.sql.
  • demo_access_codes: written from a production dump (20260820070000); every column the admin reads is there.
  • admin_announcements, promo_codes, subscriptions: created by plain CREATE TABLE in repo migrations, so the shape matches. ai_requests and user_settings: their admin-read columns are used by every function on production.
  • admin_grants: CREATE TABLE IF NOT EXISTS, but the admin only inserts columns the migration defines, and a failed audit insert is only a warning.

Proof:

  • scripts/test-admin-table-drift-migration.sh (now part of npm run test:db): rebuilds the legacy shape in a rolled-back transaction, runs the migration with a duplicate pair (columns land, key skipped, no row deleted), resolves the duplicate, reruns (key created), runs the exact Set limit (per-user upsert, global update) and Set prompt SQL, then reruns again and checks no row changed.
  • Real reproduction on the local stack: with production's shape (column and trigger dropped), POST admin?action=set-limit returned 500. After applying the migration it returned 200.
  • New full-stack test e2e/full-stack/admin.spec.ts › "an admin sets a per-user and a global rate limit through the admin function": both Set limit paths twice, one row each, values updated.
  • ./.superset/db-gate.sh --db-only: pgTAP Result: PASS, and every test:db script passed, the new one included.

Bug 7 UI items

  1. Sparring Start pending state. Start now shows a spinner with aria-busy, and a status line ("Checking your AI uses for today…") for the quota check, which can take up to 8 s. A ref guard means a second click or Enter sends no second check. try/finally means the button can't stay spinning, and a failed check is treated as unknown (the round opens, as on a timeout). If opening the round throws, a visible error replaces the silent no-op. Tests: sparring-quota-start.test.tsx (pending state, one check for repeated presses, failed check) and recipes/practice.spec.ts › spar-start (the quota check held in the real app; one check, round opens without a reload).
  2. Cost copy. "AI requests 2 / Covers 4 replies + the judge" is now "Round cost: 2 AI uses / For the whole round: 4 replies + the judge" (ES "Coste de la ronda … Por toda la ronda", HU "A forduló ára … Az egész fordulóra").
  3. Set Up Billing. Not reproduced. I drove Settings › Set Up Billing (and Upgrade) after each Settings Select (interface language, output language, theme), after the header language popover, and after arriving from another page, on desktop and mobile: the dialog opened every time. Chunk loading is already handled: PricingModal is a static import inside the Settings chunk, and installStaleChunkReload plus RouteErrorBoundary already reload once and then offer a Reload button. So I didn't add a second reload prompt. Added aria-haspopup="dialog" to the button. Promo copy: Pro without a Stripe customer now says "Your Pro access comes from a promo code and runs until {date}. Billing is optional until then; set it up only to keep Pro after that date." (or "…so billing is optional." for a lifetime promo), in EN, ES and HU. Covered by SubscriptionStatus.test.tsx and a new promo-pro state in recipes/billing-states.spec.ts (EN and HU, light and dark, 360, 375, 768 and 1280 px).
  4. Ask bubble. The prose class painted its own gray text over the bubble's colour. The user's message, its bold text, and its time line now take the bubble's foreground. Measured in the real app: 5.08:1 light, 6.72:1 dark (AA needs 4.5). By my token math it was about 1.9:1 light and 1.4:1 dark before. The HU placeholder was already shortened by T13 (Unify translations, plurals and number/date formats (T13: IN-02, IN-05, IN-06, IN-08, IN-09, IN-10, IN-12, IN-13, IN-15) #223) to "Mit kérdeznél?", and the recipe confirms it fits at 375 px. The drive also found the chat header overflowing by 48 px at 375 px in HU; it now wraps. Recipe: recipes/ai-tools.spec.ts › ask-bubble.
  5. HU strings. Radar chart axis labels are now translated (they were passed raw to recharts): Logika / Bizonyíték / Előadás / Stratégia, checked in spar-verdict. History "Chatek" and related chat wording is now "Beszélgetések". Already fixed on main by T13 (Unify translations, plurals and number/date formats (T13: IN-02, IN-05, IN-06, IN-08, IN-09, IN-10, IN-12, IN-13, IN-15) #223): the format description "bevezető, R1, R2 és záró", "Az írásstílust", Kíméletlen everywhere (it's the glossary term), and the Load/Loading split (Megnyitás / Betöltés). Left as is: "Cáfolat 1/2" in the Timer vs "R1/R2" in Sparring is the glossary rule T13 added ("the Timer spells out R1 and R2"; EN does the same). ES Introductor/Conclusor are the Spanish role names, per the glossary.
  6. Generate model picker at 375 px. The field shows only the model name, and an Auto choice's description sits below it. List items wrap. Recipe: recipes/model-defaults.spec.ts › model-picker-narrow (HU, 375 px, nothing clipped).
  7. Landing after email-code sign-in. Cause: Sign out exists only on Settings. RequireAuth stamped from: /app/settings with the same user id, so that account signing back in returned to Settings. A deliberate sign-out now sets a flag (src/lib/supabase/sign-out-intent.ts) so the redirect carries no return path, and LoginPage clears the flag on arrival. An expired session still returns the account to its page, and explicit deep links still work. Tests: account-switch-route.test.tsx (deliberate sign-out → /app; self-ended session keeps Settings; a later deep link keeps its page) and recipes/auth.spec.ts › auth-same-account-after-sign-out (email code through Mailpit, lands on /app).

Verify

Full npm run verify run locally, once, at b2e70927 (exit 0): docs:check, lint and typecheck passed; npm test 88 files, 640 tests passed; test:collaboration 30 passed; test:edge 457 passed, 0 failed; build passed; test:sw passed (all offline checks true); e2e smoke (desktop Chromium, @smoke) 47 passed.

Other checks at this head: npm run lint, npm run typecheck, npm test, npm run test:edge (457 passed), npm run test:collaboration (30 passed), and ./.superset/db-gate.sh --db-only (pgTAP PASS plus all test:db scripts).

verify-debatelab recipes driven on desktop-chromium and mobile-chromium (fake mode): practice (spar-), ai-tools (ask-), model-defaults (all), auth (all, 12/12), billing-states (all 14, one desktop test rerun after a Vite dev-server stale-module error), and e2e/full-stack/admin. GitHub Actions minutes are used up, so this is a local pass, not CI.

Screenshots are in .verify/artifacts/ locally (not attached). Covered: Sparring setup and Start pending (EN and HU, 375 and 1280 px), the Ask bubble light and dark, HU chart labels, the Generate picker in HU at 375 px, promo-Pro Settings (EN and HU, four widths), and the post-sign-in landing.

Review

One fresh-context review round (Claude Opus 5.5, full diff against main): no High or Medium findings. Per the deadline, no further rounds. The Low findings were not fixed and are listed for follow-up:

  1. If production has duplicate (user_id, limit_type) rate_limits rows or duplicate feature global_prompts rows, the migration only warns and skips the unique key, and per-user Set limit or Set prompt would still 500 (no constraint matching ON CONFLICT). Before applying, run read-only on production: SELECT user_id, limit_type, count(*) FROM public.rate_limits WHERE user_id IS NOT NULL GROUP BY 1, 2 HAVING count(*) > 1; and SELECT feature, count(*) FROM public.global_prompts GROUP BY 1 HAVING count(*) > 1;. After applying, confirm both unique indexes exist. Also suggested: require indisvalid in the key check, and add SET lock_timeout.
  2. In sparring-quota-start.test.tsx, the "one check however often Start is pressed" test also passes without startingRef: the disabled button already blocks the second press. The "check fails outright" test is a real regression test.
  3. The new e2e/full-stack/admin.spec.ts Set limit test passes on main too, because the local schema is already correct. DB-01 is proven by scripts/test-admin-table-drift-migration.sh and by the manual 500 → 200 reproduction above, not by that test.
  4. The drift script does not drop global_prompts.is_active and created_by, so those ADD COLUMN paths are not exercised against the legacy shape.
  5. If supabase.auth.signOut returns { error } without throwing, the sign-out flag stays set while the user stays signed in. A later expiry would then drop the return path. Suggested: check the returned error, or mark the intent only after sign-out succeeds.

Not done or open

  • Set Up Billing's original failure was not reproduced; see item 3. Worth a manual check on production in a long-lived tab.
  • Production's actual global_prompts shape is unverified. The migration handles both shapes.
  • No Stripe, staging or production checks were run.

🤖 Generated with Claude Code

Programcode and others added 2 commits October 23, 2026 13:20
…: DB-01, bug 7)

Production's rate_limits has no updated_at, so the admin function's Set limit
returned 500. A new additive migration adds it (backfilled from created_at
only when missing), the same for global_prompts, and the unique keys both
admin upserts infer, skipping a key with a warning when duplicates would
block it. No Edge Function changes.

Sparring Start shows a spinner and what it is waiting for, sends one quota
check however often it is pressed, and never stays spinning. The setup says
the whole round costs N AI uses. The radar chart translates its axis.

Ask's user bubble takes the bubble's color instead of prose gray (5.1:1
light, 6.7:1 dark) and its header wraps at 375 px in Hungarian. The model
picker field shows only the model name; an Auto description sits below.

Promo Pro in Settings says where Pro came from and that billing is optional
until it ends. Sign out on Settings no longer makes Settings the next
sign-in's landing page. Hungarian History says Beszélgetések, not Chatek.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e shifts nothing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 13:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 2ac31e6 and b2e7092.

📒 Files selected for processing: 31
  • .claude/skills/verify-debatelab/features/README.md
  • .claude/skills/verify-debatelab/features/ai-tools.md
  • .claude/skills/verify-debatelab/features/auth-demo-login.md
  • .claude/skills/verify-debatelab/features/model-defaults.md
  • .claude/skills/verify-debatelab/features/practice.md
  • .claude/skills/verify-debatelab/recipes/ai-tools.spec.ts
  • .claude/skills/verify-debatelab/recipes/auth.spec.ts
  • .claude/skills/verify-debatelab/recipes/billing-states.spec.ts
  • .claude/skills/verify-debatelab/recipes/model-defaults.spec.ts
  • .claude/skills/verify-debatelab/recipes/practice.spec.ts
  • e2e/full-stack/admin.spec.ts
  • package.json
  • scripts/test-admin-table-drift-migration.sh
  • src/components/ModelSelector.tsx
  • src/components/auth/RequireAuth.tsx
  • src/components/features/ask/AskChat.tsx
  • src/components/features/sparring/SparringScorecard.tsx
  • src/components/features/sparring/SparringSetup.tsx
  • src/components/features/subscription/SubscriptionStatus.tsx
  • src/lib/supabase/AuthProvider.tsx
  • src/lib/supabase/sign-out-intent.ts
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/hu.json
  • src/pages/LoginPage.tsx
  • src/pages/SparringPage.tsx
  • src/test/auth/account-switch-route.test.tsx
  • src/test/components/SubscriptionStatus.test.tsx
  • src/test/sparring/sparring-quota-start.test.tsx
  • supabase/README.md
  • supabase/migrations/20261023120000_reconcile_admin_table_drift.sql
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@Programcode
Programcode merged commit 2cffa27 into main Oct 23, 2026
3 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants