Skip to content

Round 13: honor login intent, complete rate-limit coverage, localize every reachable error#80

Merged
bejranonda merged 15 commits into
mainfrom
claude/webapp-review-improve-dlhdzr
Jun 10, 2026
Merged

Round 13: honor login intent, complete rate-limit coverage, localize every reachable error#80
bejranonda merged 15 commits into
mainfrom
claude/webapp-review-improve-dlhdzr

Conversation

@bejranonda

Copy link
Copy Markdown
Owner

Round 13, iteration 1 — review + fix pass

Baseline before changes: 97/97 e2e green against production, 171/171 unit, type-check + i18n clean. This round hunted what the green suites can't see.

1. /login opened on "Create your account" even when the user clicked "Log in" (fix)

The register-default tab is a deliberate Round-7 call for fresh visitors — but the header CTA labelled Log in and the auth-gate redirects from /dashboard and /chat (users who almost certainly have an account) landed on the Register form too. The page now honors ?mode=login from those three entry points; the default for everyone else is unchanged. Pinned both ways by two new e2e cases in ui-ux.spec.ts.

2. /api/analyze and /api/promo/redeem had no per-IP rate limit (fix)

KNOWN_ISSUES' plan (analyze 20/min, promo/redeem 5/min) was never wired for these two — and the doc entry itself was stale, still claiming login/register were unthrottled two rounds after they were wired. Both routes now throttle; a new rateLimit route wiring suite in tests/rate-limit.test.ts walks all six route sources so the throttled list is a CI-pinned invariant instead of a doc claim.

3. Localize every error a real user can reach (fix, 3 commits)

The project's own zero-error bar says "every user-facing action returns a localized, branded error" — but:

  • Auth: a Thai pilot user registering without a voucher saw raw English A voucher code is required to register during the pilot. Same for wrong password, email-in-use, 429, 500. The store now derives a stable errorCode (server reason preferred — login 401 / register 409 now carry one — status-class fallback otherwise) and the login page maps known codes to new auth.server_errors.* strings in th/en/de/da. res.json() on error paths is hardened so a non-JSON body can't surface a SyntaxError as the user-facing message (the suspected crash class behind KNOWN_ISSUES 2a).
  • Scan upload: Unsupported format: … / File too large (…MB) / Skipped N file(s) were hardcoded English on the primary flow → new scan.upload_* keys, all locales.
  • Scan analyze: 403 monthly-quota, 503 AI-overload, and 429 throttle responses rendered raw server English → mapped to scan.error_quota / error_overloaded / error_rate_limited. Diagnostic shapes (400/500) keep raw detail for operators.
  • Bonus: pricing.score_breakdown.free_badge was untranslated "Free" in th/de/da.

Validation

  • npm run check:all: type-check + i18n parity + 185 unit tests (was 171) — green
  • ESLint: 0 errors
  • Full Playwright suite re-run against production after merge (including the 2 new specs) — will report in the round summary

Per ITERATION_PROCESS.md §4: merge with Merge, not Squash.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS


Generated by Claude Code

claude added 5 commits June 10, 2026 05:06
…og in'

The auth page defaults to the Register tab (a deliberate Round-7 call
for fresh visitors arriving from 'Get started' CTAs). But the header
CTA labelled 'Log in' and the auth-gate redirects from /dashboard and
/chat — surfaces whose users almost certainly already have an account —
landed on 'Create your account' too. The nav promised one thing and the
page delivered another.

- login/page.tsx reads ?mode=login in a post-mount effect (no
  useSearchParams, avoiding the Suspense/hydration cost on this
  statically-prerendered page); default stays register.
- SiteHeader anonymous CTA, dashboard redirect, and chat redirect now
  link to /login?mode=login.
- pricing: removed a dead ternary whose two branches were identical.
- e2e: ui-ux.spec.ts pins both behaviours structurally (display-name
  field presence) so the tab default can't silently flip either way.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
…m (5/min)

Closes the two remaining holes in the per-IP throttling plan from
KNOWN_ISSUES 'Ongoing Follow-ups #2'. /api/analyze is the most
expensive route in the app — every call burns Workers-AI plus Gemini
free-tier quota, and one scripted IP could drain the day's AI budget.
/api/promo/redeem is session-gated but allowed a logged-in attacker to
enumerate promo codes; /api/voucher/check already throttled the
anonymous path, this closes the authenticated one.

Limits follow the documented plan (analyze 20/min, promo/redeem 5/min)
— far above organic usage, both call sites sit before the route
try/catch like every other rateLimit caller.

tests/rate-limit.test.ts gains a route-wiring suite that walks all six
route sources and asserts rateLimit() + tooManyResponse() are present,
so the throttled-route list is a CI-pinned invariant instead of a doc
claim. The doc claim had in fact drifted: KNOWN_ISSUES still said
login/register were unthrottled two rounds after they were wired.
Entry updated.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
…cales)

The login/register form rendered raw English server strings for every
failure: a Thai pilot user registering without a voucher saw 'A voucher
code is required to register during the pilot.' verbatim — violating
the project's own zero-error bar ('every user-facing action returns a
localized, branded error').

- Server: /api/auth/login 401 and /api/auth/register 409 now carry a
  machine-readable reason ('invalid_credentials' / 'email_in_use'),
  matching the reason field register 400s already had. Additive — the
  error string is unchanged for any consumer that reads it.
- Store: auth-store derives a stable errorCode per failure (server
  reason preferred, status-class fallback: 401/409/429/5xx/network) and
  hardens res.json() so a truncated or non-JSON error body can no
  longer surface a SyntaxError as the user-facing message — the
  suspected crash class behind KNOWN_ISSUES 2a.
- UI: login page maps known codes to new auth.server_errors.* strings
  (th/en/de/da, Shinny voice — Thai with polite particles, German in
  du-form); voucher rejection codes reuse the existing
  voucher_invalid.* strings so the inline check and the submit error
  always say the same thing. Unknown codes fall back to the raw
  message.
- Also: pricing.score_breakdown.free_badge was untranslated 'Free' in
  th/de/da — now ฟรี / Kostenlos / Gratis.
- Tests: 8 new cases pin deriveErrorCode rules and the store lifecycle
  (185 total).

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
The scan flow's file-rejection errors ('Unsupported format: …', 'File
too large (…MB)', 'Skipped N unsupported/oversized file(s).') were
hardcoded English inside useScanUpload while everything around them
went through t(). A Thai user dropping a 20 MB photo or a PDF got an
untranslated error on the product's primary flow.

New scan.upload_unsupported / upload_too_large / upload_skipped keys in
all four locales, Shinny voice. Single rejected file shows the specific
reason; a partly-rejected batch shows a localized count summary.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
The three scan failures a real user can hit surfaced raw English from
the server: 403 'You have exhausted your free scans for this month…'
(every free-tier user at 10 scans), 503 'Food analysis is temporarily
unavailable…' (any cascade exhaustion), and 429 from the new per-IP
throttle. useScanAnalysis now maps those statuses to localized
Shinny-voice strings (scan.error_quota / error_rate_limited /
error_overloaded, th/en/de/da); diagnostic shapes (400 invalid image,
500 with phase info) keep the raw server detail, which is worth more
to operators than a generic translated line. The logger still records
the raw server message either way.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@bejranonda, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 25 minutes and 29 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 45ce9146-39b3-4259-9b2a-ddfa89ba949e

📥 Commits

Reviewing files that changed from the base of the PR and between efda8d6 and 37bcc29.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (28)
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • backend/requirements.txt
  • docs/ITERATION_PROCESS.md
  • docs/KNOWN_ISSUES.md
  • frontend/package.json
  • frontend/src/app/[locale]/chat/page.tsx
  • frontend/src/app/[locale]/dashboard/page.tsx
  • frontend/src/app/[locale]/login/page.tsx
  • frontend/src/app/[locale]/page.tsx
  • frontend/src/app/[locale]/pricing/page.tsx
  • frontend/src/app/api/analyze/route.ts
  • frontend/src/app/api/auth/login/route.ts
  • frontend/src/app/api/auth/register/route.ts
  • frontend/src/app/api/promo/redeem/route.ts
  • frontend/src/components/SiteHeader.tsx
  • frontend/src/hooks/scan/useScanAnalysis.ts
  • frontend/src/hooks/scan/useScanUpload.ts
  • frontend/src/lib/auth-store.ts
  • frontend/src/lib/image-stitcher.ts
  • frontend/src/messages/da.json
  • frontend/src/messages/de.json
  • frontend/src/messages/en.json
  • frontend/src/messages/th.json
  • frontend/tests/auth-store.test.ts
  • frontend/tests/e2e/ui-ux.spec.ts
  • frontend/tests/rate-limit.test.ts
  • package.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/webapp-review-improve-dlhdzr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

The 'Recent Scans' card rendered hardcoded fake entries (Pad Thai /
Som Tam / Green Curry, invented scores, 'Today') whenever the server
scan counter was > 0 — fabricated data presented as the user's own
meals, while the real device-local history that the scan flow already
writes (lib/scan-history, with names, scores and thumbnails) sat
unused.

- Renders up to 3 real history entries: thumbnail (or neutral fallback
  icon), food name, localized score label, locale-formatted date.
- Empty state now keys on actual history presence, not the server
  counter — a user who scanned on another device gets the honest empty
  state instead of three invented meals.
- Upgrade CTA subtitle was hardcoded mixed-English ('THB 199/month →
  … 8-dimension scores'); now a localized dashboard.upgrade_subtitle
  string with the price read from TIER_PRICING (single source with
  /pricing).
- Logout icon button: hardcoded title='Logout' → localized
  title + aria-label.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 10, 2026

Copy link
Copy Markdown

Deploying eatinorder with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8ed7d26
Status: ✅  Deploy successful!
Preview URL: https://7cc6cc43.eatinorder.pages.dev
Branch Preview URL: https://claude-webapp-review-improve.eatinorder.pages.dev

View logs

claude added 9 commits June 10, 2026 05:20
The non-429 failure branch showed body.error (an English diagnostic
string) when present, falling back to the localized generic message
only when the body had none — backwards: the localized line is the
user copy, the server string is for the console.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
'Meal', 'Menu Scan', 'Drink/Snack' and the '+ N more' suffix were
hardcoded English in the history entries the scan flow writes — and
the dashboard's recent-scans list (now real data) displays them. New
scan.history_* keys in all four locales; labels are resolved at write
time since history is device-local and the scan-time locale is the
right one. The logger's telemetry label stays English by design.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
…gle-survivor collapse

When all but one photo fail to load during collage stitching, the
single-survivor shortcut returned validImages[0] — which is the broken
image whenever the first photo was the one that failed. Resolve the
survivor's index from the load results instead.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
Every run of ci.yml — including on main — has failed since the
workflow was introduced, and with no branch protection enforcing the
check nobody noticed. The ITERATION_PROCESS §2 table lists GitHub
Actions CI as a must-pass gate; in reality it was a permanently red
light that proved nothing. Two independent infra bugs:

- Frontend job: setup-node pointed cache-dependency-path at
  frontend/package-lock.json, which doesn't exist — npm workspaces
  hoists the only lockfile to the repo root. setup-node failed before
  npm ci even ran (and npm ci inside frontend/ would have failed next
  for the same reason). Install now runs at the repo root; the
  check:all step keeps the frontend working directory.

- Backend job: requirements.txt pinned pydantic==2.5.0 alongside
  pydantic-settings>=2.3.0 — every pydantic-settings release in that
  range requires pydantic>=2.7.0, so pip's resolver correctly refused
  (ResolutionImpossible). Relaxed to pydantic>=2.7.0,<3, which also
  satisfies fastapi 0.104.1 and pydantic-extra-types 2.1.0. Verified
  in a clean venv: resolution succeeds and all 129 backend tests pass.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
The login page absolutely-positions SiteHeader over a vertically
centered card; on mobile the card is taller than the viewport, so its
top — the brand icon — rendered half-clipped behind the header bar on
every load. Top padding clears the header before centering.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
The pt-28 comment landed in expression position between return( and
the JSX root, which is a syntax error. Same class of mistake as the
pricing edit earlier this round — and it got pushed because the local
gate greps for error lines and proceeds when grep SUCCEEDS at finding
them. Gate now keyed on the check's exit code.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
- CHANGELOG: Round 13 entry — login intent honoured via ?mode=login,
  complete per-IP rate-limit coverage with CI-pinned route wiring,
  localization of every reachable error (auth, scan upload, scan
  failures, history labels, chat), real dashboard recent-scans instead
  of mock data, login-card clipping fix, stitcher single-survivor fix,
  and the headline finding: ci.yml had never passed a single run since
  Round 11 — fixed (lockfile path + pydantic pin) and now green.
- ITERATION_PROCESS §2: records the lesson — a gate is only real once
  you've seen it green; no branch protection meant a month of red runs
  went unnoticed.
- Version: 2.1.14 (frontend + root kept in sync; root had drifted to
  2.1.9 while production reported 2.1.13).

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
The homepage showed anonymous visitors a 7-day streak, 1,250 points
and a Practitioner level with nothing marking them as sample numbers —
the code comment says 'Gamification Preview' but the rendered UI
didn't. Same dishonest-affordance class as the dashboard's mock recent
scans. A localized caption now invites the visitor to earn their own.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
…free tier

currentTier fell back to 'free' for visitors with no account, so the
Starter card showed a dead 'Current Plan' chip to people who have no
plan — confusing first-impression copy. Anonymous visitors now get the
'Get Started' CTA into registration. Also: the promo-redeem 'login
first' message was a th/en-only hardcoded ternary (de/da users got
English) — now a pricing.promo_login_first string in all four locales.

https://claude.ai/code/session_01RaziyjCxHgKcqvTqss7bXS
@bejranonda bejranonda merged commit 85dc782 into main Jun 10, 2026
4 of 5 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