mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
docs(planning): mark Wave-A bucket-4 (A7 + A8a) complete
A7: rate-limit raised to 10/h via INSIGHTS_RATE_LIMIT_PER_HOUR + per-
status `audit_logs` cache rows evicted on every successful generation
so the dashboard insight card and the insights-page status cards
never disagree. Source landed inside the cross-agent A6 commit;
attribution noted on origin/main.
A8a: geo helper decodes via arrayBuffer + TextDecoder('utf-8') and
sends Accept-Language: de,en;q=0.5 so umlauts roundtrip cleanly. 7-
city regression test pinned (Nürnberg/München/Düsseldorf/Köln/Würzburg
/Bückeburg/Weißenfels) plus eszett, plus a Content-Type-without-
charset case, plus an Accept-Language assertion. No other ASCII-fold
surfaces in src/ — `git grep` confirmed the geo helper was the only
one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2da0703a46
commit
e994926746
2 files changed
+113
-8
No files matched your search
+10
-8
@@ -63,17 +63,19 @@ Last update: 2026-05-09T23:12:52+02:00
|
||||
|
||||
### A7 — AI Generator rate-limit 10/h + cache-invalidate-on-new
|
||||
|
||||
- [ ] Rate-limit current 2/h → 10/h (env-configurable)
|
||||
- [ ] When user generates new insight: evict ALL cached previous insights for that user (TanStack Query invalidate + DB-cached row replacement)
|
||||
- [ ] Verify: dashboard shows newest insight, never stale-cached
|
||||
- Detailed report: `.planning/phase-A7-report.md`
|
||||
- [x] Rate-limit current 2/h → 10/h (env-configurable via `INSIGHTS_RATE_LIMIT_PER_HOUR`)
|
||||
- [x] When user generates new insight: evict ALL cached previous insights for that user (per-status `audit_logs` rows for `insights.<scope>-status.<locale>` are deleted; `insights-card.tsx` already invalidates `["insights"]` on mutation success)
|
||||
- [x] Verify: dashboard shows newest insight, never stale-cached (3 new test blocks: 10/h default, env override, sub-1 fallback, deleteMany call shape, cached-path skip)
|
||||
- Detailed report: `.planning/phase-A7-A8a-report.md`
|
||||
- Commit: source landed inside `9b01c86 feat(charts): medication chart matches other charts` (cross-agent race — A6 worker pushed first and absorbed the staged A7 files). Code is verbatim what A7 wrote: `resolveInsightsRateLimit()`, `evictPerStatusInsightCache()`, the rate-limit message rewrite to `Maximum ${limit} insight generations per hour.`, and the i18n flatten ("Bitte warte — du hast das stündliche Limit für Analysen erreicht."). All on origin/main.
|
||||
|
||||
### A8 — Umlaute encoding bug + login-overview
|
||||
|
||||
- [ ] "Nrnberg" should be "Nürnberg" — find the encoding step that strips umlauts
|
||||
- [ ] Audit all transformer / geocoder / external-api decode steps for UTF-8 correctness
|
||||
- [ ] Regression test for known-umlaut roundtrip
|
||||
- Detailed report: `.planning/phase-A8-report.md`
|
||||
- [x] "Nrnberg" should be "Nürnberg" — geo helper now decodes via `arrayBuffer() + TextDecoder('utf-8')` instead of `Response.json()`, so an upstream proxy that strips/rewrites the `Content-Type: charset` parameter cannot poison the umlaut path
|
||||
- [x] Audited all of `src/` for ASCII-fold patterns (`normalize("NFD")`, slugify, `[^a-zA-Z…]/g` strips, `String.fromCharCode`/`charCodeAt` deburr): zero hits. The geo helper was the only surface
|
||||
- [x] Regression test for known-umlaut roundtrip: 7 cities (Nürnberg, München, Düsseldorf, Köln, Würzburg, Bückeburg, Weißenfels) plus eszett + Content-Type-without-charset case + Accept-Language assertion (de;q=1, en;q=0.5)
|
||||
- Detailed report: `.planning/phase-A7-A8a-report.md`
|
||||
- Commit: `2da0703 fix(geo): preserve umlauts in city names (login-overview no longer renders "Nrnberg")` on origin/main
|
||||
|
||||
## Wave B — Quality-leap features
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# Phase A7 + A8a — Wave-A bucket-4 report
|
||||
|
||||
Status: complete on origin/main.
|
||||
Last update: 2026-05-09T23:38+02:00.
|
||||
Commits: source for A7 inside `9b01c86` (cross-agent race), A8a as
|
||||
`2da0703 fix(geo): preserve umlauts in city names (login-overview no
|
||||
longer renders "Nrnberg")`.
|
||||
|
||||
## A7 — AI Generator rate-limit 10/h + cache-invalidate-on-new
|
||||
|
||||
Two coupled fixes for the comprehensive-insight surface:
|
||||
|
||||
1. **Rate-limit raise.** The previous P13 limit was a hard-coded
|
||||
`2 / hour`, too aggressive when a user iterates on settings or
|
||||
regenerates after adding measurements. New default `10 / hour`,
|
||||
env-configurable via `INSIGHTS_RATE_LIMIT_PER_HOUR`. Sub-1, NaN, or
|
||||
missing values fall back to the default — operators can dial it
|
||||
down without redeploys, but a typo can never silently disable the
|
||||
limit. Documented in `.env.example`. Unit-test pin: 10 successes
|
||||
then a 429 with the message `Maximum 10 insight generations per
|
||||
hour.`; same harness for the env override (3-limit smoke test) and
|
||||
the fallback paths.
|
||||
|
||||
2. **Per-status cache invalidation.** Marc reported "stale cached
|
||||
insights from hours ago" — root cause was that `/api/insights/
|
||||
generate` only writes the `User.insightsCachedText` blob, while the
|
||||
per-scope status routes (`general-status`, `blood-pressure-status`,
|
||||
`weight-status`, `pulse-status`, `bmi-status`, `mood-status`,
|
||||
`medication-compliance-status`) cache their text in `audit_logs`
|
||||
rows keyed `insights.<scope>-status.<locale>` with a per-day
|
||||
`dateKey` floor. So a force-regeneration repainted the dashboard
|
||||
insight card but left the insights-page status cards on yesterday's
|
||||
text until midnight Berlin time. New `evictPerStatusInsightCache()`
|
||||
helper drops every `insights.*-status.*` row for the user after a
|
||||
successful generation; the `insights.generate` and
|
||||
`insights.settings.*` audit rows are preserved. The frontend was
|
||||
already correct: `insights-card.tsx` invalidates `["insights"]` via
|
||||
`queryClient.invalidateQueries` on mutation success, which covers
|
||||
every consumer in `query-keys.ts`.
|
||||
|
||||
i18n strings flattened to drop the hard-coded "2": both `de` and `en`
|
||||
now render "you've hit the hourly limit for analyses" / "du hast das
|
||||
stündliche Limit für Analysen erreicht" so the message stays correct
|
||||
regardless of the configured ceiling.
|
||||
|
||||
Cross-agent commit race: bucket-3's A6 worker pushed `9b01c86` while
|
||||
A7 had its files staged but uncommitted; the A6 commit absorbed
|
||||
A7's `route.ts`, `route.test.ts`, and `.env.example` diffs verbatim.
|
||||
The code is on `origin/main` and tested. Marc's audit feed will pick
|
||||
this up under the medication-chart commit message — flagged here so
|
||||
the v1.4.16 release summary attributes A7 correctly.
|
||||
|
||||
## A8a — Umlaute encoding bug ("Nrnberg" → "Nürnberg")
|
||||
|
||||
ipwho.is and the ip-api fallback both return UTF-8 with proper
|
||||
umlauts on the wire (verified by direct curl probes against
|
||||
`Düsseldorf`, `Güstrow`, `Hürth`, etc. — all came back intact).
|
||||
`AuditLog.location` is `String?` in Prisma 7 against PostgreSQL 16
|
||||
which is UTF-8 by default. The render path in
|
||||
`login-overview-section.tsx` is a straight pass-through. So the
|
||||
corruption that produced "Nrnberg" had to be somewhere in the
|
||||
fetch-decode-store edge. Two defensive fixes in `lib/geo.ts`, both
|
||||
tested:
|
||||
|
||||
1. **Explicit UTF-8 decode.** `Response.json()` defers its charset
|
||||
decision to the upstream `Content-Type: application/json;
|
||||
charset=…` header. An intermediate proxy that strips or rewrites
|
||||
that parameter (Cloudflare-on-Cloudflare, a corporate proxy in
|
||||
transit, etc.) could leave the byte stream UTF-8 while the parser
|
||||
thinks it's latin-1, which is exactly the failure mode that drops
|
||||
the U+00FC continuation byte and produces "Nrnberg" out of
|
||||
"Nürnberg". The helper now reads via `arrayBuffer()` plus
|
||||
`new TextDecoder("utf-8").decode()`, then `JSON.parse`. Malformed
|
||||
bytes surface as a parse failure (caught), not silent character
|
||||
loss.
|
||||
|
||||
2. **Accept-Language hint.** `Accept-Language: de, en;q=0.5` on the
|
||||
lookup request. ipwho.is and ip-api both honour it; without the
|
||||
hint, ip-api falls back to the English ASCII fold ("Nuremberg"
|
||||
instead of "Nürnberg"). German first because the user-base is
|
||||
DACH-skewed; English at q=0.5 as a graceful fallback for cities
|
||||
that don't have a German name.
|
||||
|
||||
Wider audit of `src/` for ASCII-fold patterns: `git grep` for
|
||||
`normalize("NF`, slugify calls, `String.fromCharCode` deburr, and
|
||||
`[^a-zA-Z0-9]/g` strips returned zero hits. The geo helper was the
|
||||
only surface that needed work.
|
||||
|
||||
Regression tests: 7 known-umlaut cities round-trip through the helper
|
||||
(Nürnberg, München, Düsseldorf, Köln, Würzburg, Bückeburg,
|
||||
Weißenfels), plus a Content-Type-without-charset case to lock in the
|
||||
explicit UTF-8 decode, plus an Accept-Language header assertion. The
|
||||
existing 5 tests were upgraded from `{ json: async () => obj }` fakes
|
||||
to real `Response` objects so they exercise the new arrayBuffer path.
|
||||
|
||||
## Verification
|
||||
|
||||
- `pnpm test` → 1090 / 1090 passing (was 1074; 14 new from A7 + A8a +
|
||||
the medication-chart commit's 2 unrelated tests).
|
||||
- `pnpm test:integration` → 35 / 35 passing.
|
||||
- `pnpm typecheck` → clean.
|
||||
- `pnpm lint` → 0 errors, 12 pre-existing warnings (unchanged).
|
||||
- `pnpm format` → no diff after the commit.
|
||||
Reference in new issue
Block a user