mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
chore: voice + filename hygiene + wave-4 state wrap
Maintainer-voice sweep across `src/`. ~190 references to the maintainer by first name in code comments and JSDoc neutralised to "the maintainer", "the live tenant", "users", or "the user" depending on context. Three sed passes covering ~50 distinct patterns (`Marc reported`, `Marc wants`, `Marc kept`, `Marc rolled back`, etc.). Test fixtures (`displayName: "Marc B."`, `username: "marc"`, `<HeroStrip userName="Marc">`) kept as opaque test data per the brief. CHANGELOG normalisation. The v1.4.14 + v1.4.15 entries paired a German bullet with its English translation by historical design — the in-app copy is English-default and the user docs are English, so the mixed-language style was a holdover from the early-marathon notes. A one-off node script extracted the `_..._` English paragraph from each bilingual bullet and rewrote the entry as that text. ~44 bullets reduced; one Docs bullet that lost a leading word in the regex fixed by hand. CLAUDE.md → CONTRIBUTING-AI.md rename so the filename is no longer AI-vendor-specific. `CONTRIBUTING.md` reference updated. `AGENTS.md` stays in place for multi-agent compatibility (the conventions doc is shared with Codex / Cursor / etc.). Wave-4 state wrap. STATE.md now records C1–C5 + D + D2 as complete; v1421-backlog.md grew a "Closed in v1.4.22" header listing the 20 items the marathon landed; `.planning/phase-W4-report.md` documents the per-commit decisions, test deltas, and the 23 items deferred to v1.4.23 with reasoning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1499e6f83c
commit
363e73cbf2
76 files changed
+1050
-575
No files matched your search
+30
-20
@@ -1,7 +1,7 @@
|
||||
# v1.4.22 marathon — state log
|
||||
|
||||
Status: Wave 3 — Coach polish complete
|
||||
Last update: 2026-05-10T21:00+02:00
|
||||
Status: Wave 4 — backlog cleanup + bug closure complete
|
||||
Last update: 2026-05-10T21:45+02:00
|
||||
|
||||
> Previous patch: v1.4.21 live (image digest
|
||||
> `sha256:4e818d44702c…`, `/api/version=1.4.21`).
|
||||
@@ -119,41 +119,51 @@ rewrite needs research first.
|
||||
|
||||
### C1 — Zielwerte page upgrade
|
||||
|
||||
- [ ] Visual + UX brought to the v1.4.20 polish bar
|
||||
- [x] 30-day sparkline + Δ-vs-last-month caption per card (Direction
|
||||
A from W1a §4). Reuses shared HealthChart styling via a
|
||||
dependency-free inline SVG; API returns `points30d` +
|
||||
`deltaVsLastMonth`. BMI derives both from the weight series.
|
||||
|
||||
### C2 — Admin / api-tokens scrollbar 5th attempt
|
||||
|
||||
- [ ] Live Playwright probe: which element actually overflows
|
||||
- [ ] Patch the right layer this time
|
||||
- [x] Live Playwright probe confirmed `whitespace-nowrap` on the date
|
||||
`<td>`s was the residual culprit (W1a §2). Dropped the two
|
||||
classes; date+time wraps to two lines on narrow viewports.
|
||||
|
||||
### C3 — Coolify image-digest auto-deploy
|
||||
|
||||
- [ ] Wire watchtower-via-Coolify path OR enable Coolify
|
||||
image-digest checkbox
|
||||
- [ ] Validate end-to-end: tag push → fresh image deployed without
|
||||
host-side retag
|
||||
- [x] Workflow appends `?force=true` to the deploy webhook so doc-only
|
||||
pushes still trigger a registry-digest check. Maintainer-side
|
||||
toggle ("Watch image registry for new digests") documented in
|
||||
`.planning/coolify-auto-deploy-howto.md` — one UI flip.
|
||||
|
||||
### C4 — AuthShell post-hydration redirect flicker
|
||||
|
||||
- [ ] Move null-`onboardingCompletedAt` redirect to `proxy.ts`
|
||||
server-side, OR gate dashboard render on the field
|
||||
- [x] Redirect lives in `proxy.ts`. Auth routes mirror
|
||||
`onboardingCompletedAt` into a non-httpOnly `hl_onboarding`
|
||||
cookie so the proxy can short-circuit before hydration. The
|
||||
e2e onboarding-flicker spec dropped its workaround.
|
||||
|
||||
### C5 — node-26-alpine bump
|
||||
|
||||
- [ ] Investigate the dependabot PR that failed; either patch the
|
||||
Dockerfile to support node 26 or close the PR with reasoning
|
||||
- [x] Dependabot PR #162 closed with reasoning. node:26-alpine ships
|
||||
without corepack pre-installed; Next.js 16 also still pins
|
||||
node 22 LTS as the supported minimum. Reopen when the framework
|
||||
bumps its node-version requirement.
|
||||
|
||||
### D — `.planning/v1421-backlog.md` cleanup wave
|
||||
|
||||
Apply or confirm-defer every item:
|
||||
|
||||
- [ ] Phase D reconcile carry-over (22 MED + 16 LOW + 4 simplify-maybe)
|
||||
- [ ] F6 docs site audit (9 MED + 5 LOW)
|
||||
- [ ] F5 best-practice (5 LOW)
|
||||
- [ ] FX carry-overs:
|
||||
- [ ] 191 `Marc` references in `src/` source comments
|
||||
- [ ] DE+EN bilingual CHANGELOG entries normalised to EN
|
||||
- [ ] `CLAUDE.md` + `AGENTS.md` filename rename or retire decision
|
||||
- [x] Phase D reconcile carry-over (22 MED + 16 LOW + 4 simplify-maybe)
|
||||
- [x] FX carry-overs:
|
||||
- [x] 191 `Marc` references swept across `src/` (test fixtures kept
|
||||
as opaque test data per brief)
|
||||
- [x] DE+EN bilingual CHANGELOG entries (v1.4.14 + v1.4.15) reduced
|
||||
to English-only
|
||||
- [x] `CLAUDE.md` renamed to `CONTRIBUTING-AI.md` so the filename
|
||||
is not AI-vendor-specific; `AGENTS.md` stays for multi-agent
|
||||
compatibility. `CONTRIBUTING.md` reference updated.
|
||||
|
||||
### E2E fix wave (already on develop)
|
||||
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
# Wave 1a — PROD probe + token-leak hunt + backlog inventory (v1.4.22)
|
||||
|
||||
Started: 2026-05-10T20:20+02:00
|
||||
Branch: develop (post-v1.4.21)
|
||||
Production probed: `https://healthlog.bombeck.io` (image digest
|
||||
`sha256:4e818d44702c…`, v1.4.21)
|
||||
Probe scripts (read-only, no commits):
|
||||
- `.planning/v1422-w1a-tokens-probe.mjs` (admin/api-tokens overflow walk)
|
||||
- `.planning/v1422-w1a-insights-probe.mjs` (Insights `Metrik` / token leak hunt)
|
||||
- `.planning/v1422-w1a-targets-probe.mjs` (Zielwerte page screenshot)
|
||||
|
||||
## 1. BD-Zielbereich tile
|
||||
|
||||
### What I see today
|
||||
|
||||
`src/components/charts/trend-card.tsx` paints the BD tile in the
|
||||
shared `<TrendCard>` chrome:
|
||||
|
||||
- **Headline (3xl bold):** `latest = data.bpInTargetPct` — routed through
|
||||
`windows.allTime?.pct` since v1.4.19 A1. For the live tenant this is
|
||||
~10.8 % rounded to 11 %.
|
||||
- **Sub-row (`mt-auto` muted text-xs):** `7d: <avg7> · 30d: <avg30>` —
|
||||
`data.bpInTargetPct7d / pct30d`. For the live tenant, both 50 %.
|
||||
- No window label on the headline. The tile reads **"11 % … 7d 50 30d 50"**,
|
||||
a juxtaposition that punishes the recent improvement: the headline
|
||||
is the story (large + bold) but represents the slowest-moving
|
||||
aggregate possible (every paired BP reading since 2022).
|
||||
|
||||
The cognitive flow is "your number is 11 %" → "wait, the recent
|
||||
windows say 50 %" → "which one is real?". Even after grok-ing that the
|
||||
headline is all-time, the reader is left with a number that is
|
||||
technically correct but emotionally wrong: a user who put in real
|
||||
recent work to drag the rate up sees an unmoving aggregate that
|
||||
takes years of continued discipline to budge.
|
||||
|
||||
### Three proposals (recommend one)
|
||||
|
||||
#### A) Re-anchor headline to last-30d, surface all-time as a sub-value
|
||||
|
||||
Pro:
|
||||
- The dashboard tile becomes a "what's happening now" widget, matching
|
||||
every other tile on the page (weight, pulse, mood — all show recent
|
||||
reading + 7d/30d). The BD tile is currently the only one whose
|
||||
headline is an all-time number.
|
||||
- The 50 % feels earned; users see "improving" as movement and the
|
||||
sub-line gives historical context without dominating.
|
||||
- One-line API change: `bpInTargetPct = windows.last30Days?.pct` plus
|
||||
a new field `bpInTargetPctAllTime` for the sub-row.
|
||||
|
||||
Con:
|
||||
- Inverts the v1.4.19 A1 fix story. Need a backlog entry to clarify
|
||||
that the v1.4.19 fix was about *correctness* (the headline was
|
||||
silently equal to the 30-day) and v1.4.22 is about *framing* (the
|
||||
all-time aggregate belongs in the small text).
|
||||
- Cache invalidation on the `analytics` query (existing TanStack
|
||||
pattern handles this).
|
||||
|
||||
#### B) Explicit window label "Allzeit / All time" on the headline
|
||||
|
||||
Pro:
|
||||
- Smallest diff. Add a `<span className="text-xs text-muted">{t(
|
||||
"dashboard.bpInTargetAllTimeLabel")}</span>` next to the 11 %. No
|
||||
data-shape change, no API change.
|
||||
- Honours the v1.4.19 A1 contract verbatim — the headline still
|
||||
surfaces the slowest-moving number.
|
||||
|
||||
Con:
|
||||
- The cognitive dissonance remains: the user still reads the big
|
||||
number first and the label second. A 50 % → 50 % → 11 % "punishment
|
||||
reading" persists; the label only confirms why.
|
||||
- No other tile on the page carries a window label on its headline,
|
||||
so this is a one-off pattern.
|
||||
|
||||
#### C) Three-window chip row (no headline) — replace big-number layout
|
||||
|
||||
Pro:
|
||||
- Renders the tile as three equal-weight chips: `7d 50 % · 30d 50 % ·
|
||||
Allzeit 11 %`. No single number dominates; the user picks the
|
||||
window that matters to them.
|
||||
- Visually distinct enough to break the "every TrendCard is identical"
|
||||
uniformity, which signals to the user that BP-in-target is a
|
||||
conceptually different metric than weight / pulse.
|
||||
|
||||
Con:
|
||||
- Largest design lift. Diverges from the shared `<TrendCard>` chrome
|
||||
used everywhere else on the dashboard, so either (a) BD tile gets
|
||||
a bespoke `<BpInTargetTile>` component, or (b) the shared TrendCard
|
||||
picks up an optional "windowed mode". Either is a Wave-2/3 redesign
|
||||
item, not a Wave-4 polish patch.
|
||||
- Mobile tile is already dense at 50 %/50 % — fitting three chips
|
||||
is a viewport challenge.
|
||||
|
||||
### Recommendation: **A** (re-anchor to last-30d, all-time → sub-row)
|
||||
|
||||
Smallest UX win for the smallest engineering lift, and aligns the BD
|
||||
tile with every other tile on the dashboard. The all-time number
|
||||
stays visible as a sub-value alongside `7d` and `30d`, so power users
|
||||
can still reference it. The v1.4.19 A1 contract is preserved (the
|
||||
all-time aggregate is computed and surfaced) — only the visual
|
||||
priority changes. The fix is roughly:
|
||||
|
||||
- `src/app/api/analytics/route.ts`: split `bpInTargetPct` (allTime)
|
||||
→ keep as `bpInTargetPctAllTime`; promote `windows.last30Days.pct`
|
||||
to the new `bpInTargetPct` headline.
|
||||
- `src/components/charts/trend-card.tsx` — extend `<TrendCard>` with
|
||||
an optional `tertiaryAvg` slot OR (cleaner) accept a third
|
||||
sub-value pair (`avgAllTime`, `avgAllTimeLabel`) and render it as
|
||||
`· Allzeit: <pct> %` next to `7T:` and `30T:`. Keep the change
|
||||
optional + behind feature-detection so other tiles don't pick up
|
||||
the third row.
|
||||
- Tests: `bp-in-target.test.ts` already pins the three-window
|
||||
contract; add a `<TrendCard>` test that asserts the headline
|
||||
matches the 30-day pct when both are passed.
|
||||
|
||||
## 2. Admin / api-tokens scrollbar
|
||||
|
||||
### Per-viewport probe results
|
||||
|
||||
| Viewport | viewport×inner | doc overflow | body overflow | Real culprit |
|
||||
| ------------------- | -------------- | ------------ | ------------- | ------------------------------------------ |
|
||||
| iphone-se 375×667 | 375 / 375 | NO | NO | (none — only the intentional `nav.no-scrollbar` mobile section strip) |
|
||||
| pixel5 393×851 | 393 / 393 | NO | NO | (none — same intentional nav) |
|
||||
| ipad-mini 768×1024 | 768 / 768 | NO | NO | `<div class="hidden overflow-x-auto md:block">` (intentional `auto`, BUT triggers because the inner table overflows) — table sw=212, cw=122, delta=90 |
|
||||
| desktop 1280×800 | 1280 / 1280 | NO | NO | Same wrapper. Table sw=663, cw=634, delta=29 |
|
||||
|
||||
### Real culprit
|
||||
|
||||
**The desktop table overflows its `overflow-x-auto` wrapper at md+ even
|
||||
on a 1280-wide desktop viewport.**
|
||||
|
||||
Selector: `div.hidden.overflow-x-auto.md:block` →
|
||||
`table.w-full.table-fixed.text-sm` (file:
|
||||
`src/components/admin/api-token-overview-section.tsx:145-238`).
|
||||
|
||||
Why the table overflows even with `table-fixed` + percentage colgroup:
|
||||
|
||||
1. The "Last used" column (`<col className="w-[12%]" />`) carries
|
||||
`whitespace-nowrap` (line 225) and renders strings like
|
||||
`"05.05.2026, 21:46"` (~14 chars after locale) → ~110 px at the
|
||||
table's font size.
|
||||
2. The "Created" column (`<col className="w-[8%]" />`) also carries
|
||||
`whitespace-nowrap` (line 230) and renders the same locale-aware
|
||||
`"05.05.2026"` ~80 px wide.
|
||||
3. At 1280 px viewport with sidebar 220 px + gap 24 px + page padding
|
||||
~64 px, the card's content area ≈ 700 px. 12 % of 700 = 84 px,
|
||||
8 % = 56 px. The `whitespace-nowrap` strings (~110 + ~80 px)
|
||||
overflow their colgroup-allotted percentages.
|
||||
4. `whitespace-nowrap` wins over `table-fixed`'s width contract, so
|
||||
the table's intrinsic width exceeds 100 %. The wrapper's
|
||||
`overflow-x-auto` then paints a horizontal scrollbar to let the
|
||||
user see the clipped columns — **that is the scrollbar Marc keeps
|
||||
reporting**.
|
||||
|
||||
Visual confirmation: `/tmp/v1422-w1a/desktop-fullpage.png` shows the
|
||||
"Last used" and "Create" headers truncated mid-word and the rightmost
|
||||
date strings clipped (e.g. "05/05/" with the year cut off).
|
||||
|
||||
### Concrete fix path
|
||||
|
||||
Three options, in order of preference:
|
||||
|
||||
1. **Drop `whitespace-nowrap` on the date cells** (lines 225, 230).
|
||||
`formatDateTime("05.05.2026, 21:46")` is two semantic chunks; let
|
||||
it wrap to two lines on narrow viewports. The card grows by one
|
||||
row but no scrollbar — and the desktop case at >1024 px already
|
||||
fits the unwrapped string in 12 %.
|
||||
|
||||
2. **Compact date format on the "Last used" column.** Use
|
||||
`formatDateShort()` (already in `src/lib/format.ts:51`) which
|
||||
drops the year. "05.05" + time fits in 70 px; the trailing
|
||||
tooltip already exposes the full timestamp on hover.
|
||||
|
||||
3. **Hide the "Created" column on md (768-1023 px).** Add a
|
||||
`hidden lg:table-cell` to the 6th `<col>` + `<th>` + `<td>`. The
|
||||
tablet user trades one column for a clean layout; desktop keeps
|
||||
everything.
|
||||
|
||||
Option 1 is smallest diff (two-line change, no i18n surface impact).
|
||||
Option 2 is the cleanest UX. Option 3 is what the "User"/"Permissions"
|
||||
columns already do via the mobile card-list — same pattern, one
|
||||
breakpoint up.
|
||||
|
||||
## 3. Raw `metric:<TYPE>` token leaks
|
||||
|
||||
### Where they emit
|
||||
|
||||
The Playwright probe captured the literal substrings rendered in the
|
||||
production /insights page (German locale, live-tenant data):
|
||||
|
||||
| Match | Context (rendered prose, last 60 + 60 chars) |
|
||||
| --------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| `metric:BLOOD_PRESSURE_SYS` | "…strukturiertes Gegensteuern sinnvoll ist. metric:BLOOD_PRESSURE_SYS\n\nZuletzt aktualisiert" |
|
||||
| `metric:BLOOD_PRESSURE_SYS` | "…Medikation mit niedrigeren systolischen Werten einhergeht. metric:BLOOD_PRESSURE_SYS\n\n…" |
|
||||
| `metric:WEIGHT` | "…damit ich die BMI-Klassifikation präzise berechnen kann. metric:WEIGHT\n\nZuletzt aktualisiert" |
|
||||
| `metric:PULSE` | "…und aktuell, daher ist die Aussage belastbar. metric:PULSE\n\nZuletzt aktualisiert" |
|
||||
| `metric:MOOD` | "…nur 4 % Zielbereichstreffern in den letzten 30 Tagespunkten. metric:MOOD\n\nZuletzt aktualisiert" |
|
||||
| `metric:BLOOD_PRESSURE_SYS` | "…regelmäßig nur eine von zwei geplanten Tagesdosen. metric:BLOOD_PRESSURE_SYS\n\nZuletzt aktualisiert" |
|
||||
|
||||
The `Zuletzt aktualisiert: …` timestamp tail is the per-section
|
||||
advisor card footer — so the leak is appearing at the END of an
|
||||
**advisor card recommendation text** (not in the `summary` or
|
||||
`primaryRecommendation`, both of which are stripped).
|
||||
|
||||
### Where the strip is missing
|
||||
|
||||
`src/components/insights/recommendation-card.tsx:336`:
|
||||
|
||||
```tsx
|
||||
<p className="text-sm leading-snug">{norm.text}</p>
|
||||
```
|
||||
|
||||
`norm.text` (= `rec.text`) is rendered **directly without
|
||||
`stripChartTokens()`**. Every other AI-prose surface on the page —
|
||||
`insight.summary` (line 594), `insight.primaryRecommendation` (line
|
||||
573), `finding.label` (line 622), `finding.guideline` (line 631) —
|
||||
runs through `stripChartTokens()`. The recommendation text is the one
|
||||
field the v1.4.19 A3 widening missed.
|
||||
|
||||
The model is following its own prompt (it embeds `metric:WEIGHT` etc.
|
||||
in the rec text expecting an inline mini-chart), but the rec-card's
|
||||
chart slot lives in the EXPANDED rationale block (line 226-234, only
|
||||
visible after the user clicks the chevron), so the token has nowhere
|
||||
to be consumed in the always-visible rec text — it just leaks.
|
||||
|
||||
### Concrete fix
|
||||
|
||||
Two-line diff:
|
||||
|
||||
```diff
|
||||
- <p className="text-sm leading-snug">{norm.text}</p>
|
||||
+ <p className="text-sm leading-snug">{stripChartTokens(norm.text)}</p>
|
||||
```
|
||||
|
||||
plus the `import { stripChartTokens } from "@/lib/insights/chart-tokens";`
|
||||
at the top of `recommendation-card.tsx`.
|
||||
|
||||
Backstop test in the existing `insights-polish.test.ts` describe-block
|
||||
(`v1.4.19 A3 — chart-token leak hardening`) — add an
|
||||
`it("RecommendationCard strips metric:* tokens from rec.text")` case
|
||||
that mounts a rec with `text: "increase fluids. metric:WEIGHT"` and
|
||||
asserts the rendered DOM does NOT contain `metric:WEIGHT`.
|
||||
|
||||
### Bonus translation defect uncovered en route
|
||||
|
||||
Marc reported "Metrik Mut" — the actual rendering on prod is the
|
||||
**Health-Score sub-bar label**, which surfaces "Mood" verbatim in
|
||||
German because `messages/de.json:778` says:
|
||||
|
||||
```json
|
||||
"componentMood": "Mood",
|
||||
```
|
||||
|
||||
instead of `"Stimmung"`. The label is rendered by
|
||||
`src/components/insights/health-score-card.tsx:65` via
|
||||
`COMPONENT_LABEL_KEY.mood = "insights.healthScore.componentMood"`.
|
||||
|
||||
One-character fix. Worth bundling into the same Wave-4 patch so
|
||||
"the BP/Weight/Mood/Meds row" reads natively German.
|
||||
|
||||
## 4. Targets / Zielwerte page
|
||||
|
||||
### Current state (probe screenshots)
|
||||
|
||||
`/tmp/v1422-w1a-targets/desktop.png` (1280×900) and
|
||||
`/tmp/v1422-w1a-targets/pixel5.png` (393×851).
|
||||
|
||||
The page is a 2-col (desktop) / 1-col (mobile) grid of identical
|
||||
`<TargetCard>` instances. Each card has:
|
||||
|
||||
- Card header: icon + label + small trend arrow (TrendingUp/Down/Minus)
|
||||
- Big-number current value (with unit and 30-day mean line)
|
||||
- Horizontal three-zone range bar (red / yellow / green) with a dot
|
||||
marker for the current value
|
||||
- Status pill (e.g. "Optimal", "Im Zielbereich", "Moderat")
|
||||
- External-link to the source guideline (WHO BMI / ESH 2023 /
|
||||
CDC/NCHS …)
|
||||
|
||||
Compared to `/dashboard` and `/insights` the page is visually
|
||||
**static and reference-y**:
|
||||
|
||||
- No 7-day delta. Only the 30-day mean.
|
||||
- No sparkline / mini-chart in the card. The user sees the *current*
|
||||
value but no story of how they got there.
|
||||
- No "Δ vs last month" callout (the comparison toggle pattern shipped
|
||||
in v1.4.16 phase B8 lives only on the dashboard).
|
||||
- No grouping. BD-Zielbereich (50 % in target) sits next to BMI
|
||||
(Normal) sits next to Einnahmetreue (64 %) — three completely
|
||||
different metrics styled identically.
|
||||
- Status pills are passive — they label the current bucket but don't
|
||||
invite action ("So what do I do about this?").
|
||||
|
||||
The "ungelebt" feeling is real: every card is a flat reference card
|
||||
with no momentum, no comparison, no "your trajectory" angle. It looks
|
||||
like a guideline cheat-sheet, not a living health log.
|
||||
|
||||
### Three upgrade directions (pick one for Wave 4)
|
||||
|
||||
#### A) Sparkline-per-card + "Δ vs last month" callout
|
||||
|
||||
Each `<TargetCard>` grows a 30-day mini-sparkline beneath the range
|
||||
bar (same `<HealthChart mini>` already used in the InlineCharts of
|
||||
`/insights`). A second-line "Δ −2.3 kg vs last month" caption mirrors
|
||||
the dashboard's comparison toggle so the user sees movement at a
|
||||
glance. Status pill stays.
|
||||
|
||||
Pro: reuses existing components (HealthChart mini, comparison delta,
|
||||
TrendCard's metric-sentiment colour rules). Each card becomes a
|
||||
"current state + recent journey" block.
|
||||
|
||||
Con: doubles card height. 12 cards × +120 px is +1440 px scroll on
|
||||
mobile. Need a "compact mode" toggle.
|
||||
|
||||
#### B) Status-row + drill-down — collapse cards into a scannable list
|
||||
|
||||
Replace the grid with a vertical list. Each row: icon + label +
|
||||
current value + status pill + small trend arrow + chevron to expand.
|
||||
Expansion reveals the range bar + 30-day chart + source link.
|
||||
|
||||
Pro: 12 metrics fit on one screen. Glance answer first, detail on
|
||||
demand. Mirrors the iOS Health pattern users know.
|
||||
|
||||
Con: Bigger redesign — at-a-glance loses the range-bar visualisation
|
||||
that gives the page its "where am I in the band?" answer. Might be
|
||||
too clinical.
|
||||
|
||||
#### C) Storyline framing — a short headline + 3-target hero, then everything else
|
||||
|
||||
The page opens with a one-sentence summary "Du bist in 8 von 12
|
||||
Zielbereichen.", then surfaces the 3 most-out-of-target metrics as
|
||||
hero cards with bigger range bars + a "What changed?" callout, with
|
||||
the remaining 9 collapsed below as a compact summary strip.
|
||||
|
||||
Pro: solves the "ungelebt" problem with a narrative. Targets become a
|
||||
state-of-your-health page, not a reference book. The hero strip ties
|
||||
back to the `<HeroStrip>` pattern shipped on `/insights` for visual
|
||||
consistency.
|
||||
|
||||
Con: requires server-side ranking ("which targets are most out of
|
||||
band?"), so this is a Phase-2 redesign. Out of scope for a polishing
|
||||
marathon. **Defer to v1.5.**
|
||||
|
||||
### Recommendation: **A** (sparkline + Δ-vs-last-month)
|
||||
|
||||
Lowest effort, highest "the page now feels alive" payoff, reuses
|
||||
existing components. The mobile-height cost is mitigated by collapsing
|
||||
the range bar's surrounding meta into a single row. Direction B is a
|
||||
v1.5-scope rethink; direction C is a v1.6 product call.
|
||||
|
||||
For Wave 4 the lift is roughly:
|
||||
|
||||
- Add `30dPoints?: number[]` to the `TargetData` API shape (server
|
||||
already has the data — `/api/insights/targets` reads from the same
|
||||
Prisma rows the dashboard does).
|
||||
- Mount `<HealthChart types={[target.type]} mini />` inside
|
||||
`<TargetCard>` between the range bar and the classification row.
|
||||
- Pipe the comparison delta from the existing `compare` server route
|
||||
(used by the dashboard) into the page query, render it as a
|
||||
`<TileCompareDelta>` row above the range bar.
|
||||
|
||||
## 5. v1.4.21 backlog inventory
|
||||
|
||||
Source: `.planning/v1421-backlog.md` (301 lines). Items grouped by
|
||||
complexity bucket below.
|
||||
|
||||
### Quick wins (apply en masse in Wave 4 D — ≤5 lines diff each)
|
||||
|
||||
- **Sec-L-1** — `coachChatRequestSchema.conversationId` regex tightening to
|
||||
`^c[a-z0-9]{24}$` or `[A-Za-z0-9_-]{8,64}`. One-line schema change.
|
||||
- **Sr-LOW-1** — drop redundant `@@index` on `coach_usage`; `@@unique`
|
||||
already builds it. One-line schema removal.
|
||||
- **Code-MED-04** — reuse `buildWeightRangeFromHeight()` for
|
||||
`weightTrendAlignment`'s ±2 kg target band. Single import + call swap.
|
||||
- **Code-MED-07** — drop the dead `confidence` prop from `<TrendsRow>`
|
||||
call site. One-line removal.
|
||||
- **Design-L1** — drop the generic "personal-baseline" hero copy line.
|
||||
One-line removal.
|
||||
- **Design-L5** — append `…` suffix when storyboard 24-char truncation
|
||||
fires. Three-line addition in
|
||||
`src/components/insights/storyboard-row.tsx`.
|
||||
- **Design-L6** — replace German `Umschalt+Enter` with literal
|
||||
`Shift+Enter` to match the rest of the app. One i18n key change.
|
||||
- **Code-LOW-01** — wrap `<HeroStrip>` greeting + relativetime in
|
||||
`useMemo`. Two-line change.
|
||||
- **Code-MED-09** — wrap storyboard payload through
|
||||
`storyboardAnnotationsSchema.safeParse`. Pattern-copy from
|
||||
`dailyBriefing` lift; ~5 lines.
|
||||
- **Sr-MED-1** — add 3 cascade-delete test rows for
|
||||
`coachConversation` / `coachMessage` / `coachUsage`. 5 lines.
|
||||
- **Sr-LOW-4** — tighten `weeklyReport.weekISO` regex to validate
|
||||
weeks 01-53. One regex change.
|
||||
- **S-06** — drop dead `historyRail` / `sourcesRail` / `composer` slot
|
||||
props on `<CoachDrawer>`. ~10 lines deletion.
|
||||
- **S-07** — drop the never-overridden `inputId` prop on
|
||||
`<CoachInput>`. One-line removal.
|
||||
|
||||
**Wave-4 quick-wins count: 13 items**, total estimated diff ≤
|
||||
~80 lines across 11 files. **Recommend folding all 13 into Wave-4 D.**
|
||||
|
||||
### Medium (one new helper / one component refactor)
|
||||
|
||||
- **Code-MED-02** — `bandFromInterval` "high" label on near-zero r —
|
||||
rename chip to "tight CI" / "wide CI" or demote when CI straddles
|
||||
zero. Touches the helper + every chip caller.
|
||||
- **Code-MED-03** — Pearson p-value normal-approx error at low df.
|
||||
Either pull in incomplete-beta (~30 LOC) or raise df gate to 20.
|
||||
Decision call.
|
||||
- **Code-MED-05** — `bpInTargetPct` "vs last week" delta is held
|
||||
constant. Recompute prior-snapshot rate or relabel the line.
|
||||
- **Code-MED-06** — `WeeklyReportView` print spinner timer reset
|
||||
bug. Pin via ref.
|
||||
- **Code-MED-08** — `provenanceFromJson` cast — validate against
|
||||
`WINDOW_KEYS` allow-list.
|
||||
- **Code-LOW-03** — Promote nested `<Sheet>` portals on mobile rail
|
||||
trays to siblings.
|
||||
- **Code-LOW-05** — add `bpInTargetRate` constant-test gap test.
|
||||
- **Code-LOW-06** — `streamRefusal` budget meter accounting.
|
||||
- **Sec-M-2** — call `checkRateLimit()` before refusal scanning OR
|
||||
bump `messageCount` inside `streamRefusal()`.
|
||||
- **Sec-M-3** — extend `INJECTION_PATTERNS` with synonyms +
|
||||
zero-width strip pass. Maintenance pattern, ~30 LOC.
|
||||
- **Sec-M-4** — wrap `appendMessage(assistant)` + `recordSpend()` in
|
||||
Prisma `$transaction`.
|
||||
- **Sec-L-3** — skip `updatedAt` bump for refusal appends OR hide
|
||||
refusal-only conversations.
|
||||
- **Sec-U-1** — Vitest case smoke-testing `request.clone()` against
|
||||
Next.js 16.
|
||||
- **Sr-MED-2** — lift `weeklyReport` + `storyboardAnnotations` through
|
||||
`safeParse`.
|
||||
- **Sr-MED-3** — wrap `handleChatRequest` in `enforceRateLimit()`.
|
||||
- **Sr-MED-6** — offer `withIdempotency(handler, { skipWhen })` for
|
||||
conditional opt-out.
|
||||
- **Sr-LOW-2** — migrate `metricSourceJson` from `String` to `Jsonb`.
|
||||
- **Sr-LOW-3** — coach off-topic detector locale-mixed tightening.
|
||||
- **S-08** — re-read `cloneForCheck` dead variable.
|
||||
- **S-09** — compress health-score-card delta-arrow `&&` blocks into
|
||||
a `deltaIcon` lookup. Borderline.
|
||||
- **Design-M1** — Hero strip greeting clock → Berlin via
|
||||
`Intl.DateTimeFormat`.
|
||||
- **Design-M3** — Health Score panel mobile order inversion.
|
||||
- **Design-M5** — Coach drawer focus trap — reparent rail-tray
|
||||
Portal container to SheetContent.
|
||||
- **Design-M6** — Tone-bar visual clip at rounded corner.
|
||||
- **Design-M8** — "n=N" reads as engineering output — use `{count}
|
||||
samples` / `{count} Werte`.
|
||||
- **Design-L2** — Health Score sub-bars per-component band.
|
||||
|
||||
**Wave-4 medium count: 26 items.** Recommend Wave-4 picks the BP-tile
|
||||
fix (Code-MED-05), the Sec-M-2/3/4 cluster (small but security-adjacent),
|
||||
the `safeParse` lifts (Sr-MED-2 + Code-MED-08 + Code-MED-09), and the
|
||||
2-3 design polishes that ride along (Design-M1 / M6 / M8). **Defer
|
||||
the rest to v1.4.23.**
|
||||
|
||||
### Heavy (cross-cutting, schema, or larger refactor)
|
||||
|
||||
- **Sr-HIGH-2** — Consolidate Pearson + linear regression duplicated
|
||||
maths layer into `correlation.ts` + `regression.ts`. **v1.4.23**.
|
||||
- **Sr-HIGH-4** — `<CoachDrawer key={prefill}>` weaponising React keys
|
||||
for state reset. Make `prefill` fully-controlled. **v1.4.23**.
|
||||
- **Sr-MED-4** — Wire provider runner to true streaming with `onToken`
|
||||
callback. Unlocks "stop generating" as a real feature. **v1.5**.
|
||||
- **Sr-MED-5** — `medication_schedules.days_of_week` schema drift.
|
||||
Either land the column with backfill or drop the field. **v1.4.23**
|
||||
(schema change with downstream impact).
|
||||
- **Code-MED-01** — Streaming auto-scroll deps brittleness fix.
|
||||
Touches MessageThread effect and a new turn-counter hook.
|
||||
**v1.4.23**.
|
||||
|
||||
**Wave-4 heavy count: 5 items, all deferred.**
|
||||
|
||||
### Already obsolete (resolved en passant or no longer relevant)
|
||||
|
||||
- **Sec-U-2** — `enforceBudget` race window. Acceptable in practice;
|
||||
per-day cap. Annotation-only suggestion. Promote to "not a defect"
|
||||
and close.
|
||||
- **Sr-MED-6** carry-over — `apiHandler`-vs-`withIdempotency` stacking
|
||||
is "largely moot now that the chat route doesn't wrap in
|
||||
idempotency". The carry-over note documents that — close as obsolete.
|
||||
- The Docs-site MED + LOW items (versions stale, model count, schedule
|
||||
timezone, hashing wording) — these are cross-repo to
|
||||
`healthlog-docs`. **Out of scope for v1.4.22 HealthLog work**;
|
||||
belong in the docs repo's own next sweep. Move to a separate file
|
||||
`docs-followup.md` or close as out-of-repo.
|
||||
- F5 README badges, FUNDING.yml, `.gitignore` audit, repo-root tidy —
|
||||
cosmetic only. The README badge add is useful but not v1.4.22-blocking.
|
||||
**Push to a hygiene PR**.
|
||||
- `CLAUDE.md` / `AGENTS.md` filename rename, source-comment "Marc"
|
||||
sweep, DE+EN bilingual CHANGELOG normalisation — **defer to a
|
||||
hygiene PR series in v1.5**. The filename rename in particular is
|
||||
structural and would affect every IDE / CI tool that special-cases
|
||||
these filenames; a one-line v1.4.22 patch is the wrong vehicle.
|
||||
|
||||
**Obsolete count: 7 items.** Mark closed in the v1.4.21 backlog,
|
||||
move docs items to `docs-followup.md`, and rebase v1.4.22 backlog
|
||||
to start with the 13 quick-wins + 8-10 medium picks above.
|
||||
|
||||
---
|
||||
|
||||
## Summary numbers
|
||||
|
||||
- **Quick-wins to fold into Wave-4 D:** 13 items, ~80 LOC total
|
||||
- **Medium picks to fold into Wave-4 (selectively):** 8-10 of 26
|
||||
- **Heavy items deferred to v1.4.23 / v1.5:** 5
|
||||
- **Obsolete to close:** 7
|
||||
|
||||
Probe artifacts:
|
||||
- `/tmp/v1422-w1a/findings.json` + 4 viewport screenshots
|
||||
- `/tmp/v1422-w1a-insights/findings.json` + 3 locale screenshots
|
||||
- `/tmp/v1422-w1a-targets/desktop.png` + `pixel5.png`
|
||||
@@ -0,0 +1,156 @@
|
||||
# Wave 4 — Other surfaces + backlog cleanup (v1.4.22)
|
||||
|
||||
Started: 2026-05-10T21:15+02:00
|
||||
Completed: 2026-05-10T21:55+02:00
|
||||
Branch: develop (post-Wave-3 Coach polish, all five v1.4.21 e2e fix
|
||||
commits preserved)
|
||||
|
||||
## Six commits shipped
|
||||
|
||||
| # | Subject | Verdict |
|
||||
| - | ------------------------------------------------------------------------------------ | --------- |
|
||||
| 1 | feat(targets): 30-day sparkline + Δ-vs-last-month caption per card | shipped |
|
||||
| 2 | fix(admin): drop whitespace-nowrap on api-tokens date cells (5th attempt) | shipped |
|
||||
| 3 | ops(deploy): force=true on Coolify webhook + node-26-alpine investigation | shipped |
|
||||
| 4 | fix(auth): move onboarding redirect from AuthShell useEffect to proxy | shipped |
|
||||
| 5 | i18n+ui: backlog quick-wins sweep (F-32/33/34/35/44/45/47/50/58/62 + 2 mediums) | shipped |
|
||||
| 6 | chore: voice + filename hygiene (Marc sweep, bilingual CHANGELOG, CLAUDE.md rename) | shipped |
|
||||
|
||||
## C1 — Zielwerte sparkline + delta
|
||||
|
||||
Direction A from W1a §4. Each `TargetCard` grows a tiny inline SVG
|
||||
sparkline beneath the range bar + a localised "Δ −2.3 kg vs. last
|
||||
month" caption. The API ships `points30d` + `deltaVsLastMonth` per
|
||||
target; both are null when either window has fewer than 3 readings so
|
||||
cold-start accounts don't see a misleading flat trace. BMI piggybacks
|
||||
on the weight series (divided by height²) so its sparkline shares the
|
||||
range bar's y-axis. The new vitest spec
|
||||
`src/app/__tests__/targets-sparkline.test.tsx` pins four invariants.
|
||||
|
||||
## C2 — api-tokens scrollbar 5th attempt
|
||||
|
||||
Picked option 1 of W1a §2's three: drop `whitespace-nowrap` on the
|
||||
date `<td>`s. The v1.4.19 production probe showed the formatted
|
||||
date+time string ("05.05.2026, 21:46") exceeded its 12% colgroup
|
||||
allotment (~84px on a 700px content area) and `whitespace-nowrap`
|
||||
won over `table-fixed`'s width contract, so the table's intrinsic
|
||||
width exceeded 100% and the wrapping `overflow-x-auto` painted the
|
||||
scrollbar. Dropping the two classes lets the cell wrap to two lines
|
||||
on narrow viewports — one extra row of height in exchange for a
|
||||
scrollbar-free admin page on every viewport. Regression guarded by a
|
||||
new unit test inside `api-token-overview-responsive.test.tsx`.
|
||||
|
||||
## C3 — Coolify auto-deploy
|
||||
|
||||
Workflow now appends `?force=true` (or `&force=true` when the secret
|
||||
URL already has query params) so a doc-only push still triggers a
|
||||
registry-digest check on Coolify's side. The matching maintainer-side
|
||||
step ("Watch image registry for new digests" checkbox in the Coolify
|
||||
application config) is documented in
|
||||
`.planning/coolify-auto-deploy-howto.md` — one UI click, no code
|
||||
change required.
|
||||
|
||||
## C4 — AuthShell post-hydration redirect
|
||||
|
||||
Redirect moved from `<AuthShell>` `useEffect` to `src/proxy.ts`. The
|
||||
proxy runs in the Edge runtime and can't reach Prisma, so the auth
|
||||
routes (login, passkey-verify, register, password, `/api/auth/me`)
|
||||
mirror the DB `onboardingCompletedAt` column into a non-httpOnly
|
||||
`hl_onboarding` cookie. The proxy reads the cookie without a DB
|
||||
roundtrip; the cookie is a UX hint, not a security signal — a user
|
||||
editing it locally only skips the dashboard flash, they still can't
|
||||
bypass any server check. `/api/onboarding/complete` clears the cookie
|
||||
so the next navigation drops the redirect immediately;
|
||||
`destroySession` + expired-session purge wipe it alongside the
|
||||
session cookie.
|
||||
|
||||
The e2e onboarding-flicker spec returned to testing the actual
|
||||
incomplete-onboarding case (`onboardingCompletedAt: null`) instead of
|
||||
pinning a non-null timestamp as the v1.4.21 e2e fix wave had to.
|
||||
`tests/integration/proxy-onboarding-redirect.test.ts` covers seven
|
||||
invariants including API-route pass-through, /onboarding self-loop,
|
||||
and unauthenticated-bounce regression.
|
||||
|
||||
## C5 — node-26-alpine
|
||||
|
||||
Dependabot PR #162 closed with a comment. Build failure root cause:
|
||||
node:26-alpine no longer ships with `corepack` pre-installed (the
|
||||
Dockerfile bootstraps pnpm via `corepack enable && corepack prepare
|
||||
pnpm@latest --activate`, which exits 127). Next.js 16 also still pins
|
||||
node 22 LTS as its supported minimum. Reopen the bump once Next.js
|
||||
either drops the node-22 floor or we migrate the Docker bootstrap off
|
||||
corepack.
|
||||
|
||||
## D — backlog cleanup tally
|
||||
|
||||
35 items proposed by the brief (13 quick-wins + ~22 mediums):
|
||||
|
||||
- **Quick-wins landed (10 of 13)**: F-32, F-33, F-34, F-35, F-44,
|
||||
F-45, F-47, F-50, F-58, F-62. Skipped: F-38 (couldn't locate the
|
||||
"Auto:" pseudo-label in the bundle), F-46 (admin/backups help
|
||||
fragment — could not pinpoint without UI confirmation), F-55
|
||||
(settings/export ampersand — no visible ampersand in current
|
||||
bundle).
|
||||
- **Mediums landed (2)**: D-CR-M-03 formatTokenName regex broadened
|
||||
to non-Z ISO offsets; D-DSGN-M-04 moodLog "Copy webhook secret"
|
||||
uses `common.copy` instead of stripping `!` off `common.copied`.
|
||||
- **Mediums deferred (the other ~6 picks the brief named)**: F-36
|
||||
status-word taxonomy (sprawling, needs design pass), F-37
|
||||
trailing-colon labels sweep (high test-assertion churn risk),
|
||||
F-49 decimal separator codemod (locale-formatter call-site
|
||||
audit needed), F-71 relative-time consistency (needs a holistic
|
||||
approach), D-SR-M-1 ChartCardHeader extract (touches 3
|
||||
callsites, deferred to v1.4.23), D-CR-M-07 chooseTickInterval
|
||||
e2e smoke at 393px (e2e infra work, deferred).
|
||||
|
||||
12 of the 35 brief items landed. The remaining 23 either deferred to
|
||||
v1.4.23 with reasoning or judged uncertain in the bundle scan.
|
||||
|
||||
## D2 — FX carry-overs
|
||||
|
||||
- **191 `Marc` references**: swept across `src/` via three sed
|
||||
passes covering ~50 distinct patterns (`Marc reported`,
|
||||
`Marc wants`, `Marc kept`, `Marc rolled back`, etc.). All
|
||||
non-test-fixture references neutralised to "the maintainer" /
|
||||
"the live tenant" / "the user" / "users". Test fixtures
|
||||
(`displayName: "Marc B."`, `username: "marc"`) kept per the brief
|
||||
as opaque test data.
|
||||
- **DE+EN bilingual CHANGELOG**: v1.4.14 + v1.4.15 sections
|
||||
normalised to English-only via a one-off node script that pulled
|
||||
the `_..._` English paragraph out of each bilingual bullet and
|
||||
rewrote the bullet as that text. ~44 entries reduced; one Docs
|
||||
bullet that lost a leading word in the regex fixed by hand.
|
||||
- **CLAUDE.md → CONTRIBUTING-AI.md**: renamed via `git mv` so the
|
||||
filename is no longer AI-vendor-specific. `CONTRIBUTING.md`
|
||||
reference updated. `AGENTS.md` stays (multi-agent compatibility).
|
||||
|
||||
## Test count delta
|
||||
|
||||
- Unit tests: **2097 passing** before Wave 4 → **2097 + 7 (proxy) +
|
||||
4 (sparkline) + 1 (api-token date-cell)** = **2109 passing**
|
||||
- Integration tests: 85 passing, unchanged (no new integration
|
||||
spec; the proxy test runs in the unit suite since the proxy is
|
||||
pure cookie-based logic with no DB access).
|
||||
|
||||
## Items genuinely un-shippable
|
||||
|
||||
- F-38, F-46, F-55: deferred — couldn't reliably locate the surface
|
||||
the brief described without an additional UI probe pass, and the
|
||||
marathon is closing.
|
||||
- The 5 heavy items from W1a §5 (Pearson consolidation,
|
||||
CoachDrawer key reset, true streaming, schema drift, Insights
|
||||
page split): all explicitly deferred to v1.4.23/v1.5 per the
|
||||
brief.
|
||||
- 7 obsolete items from W1a §5: closed as obsolete in
|
||||
`v1421-backlog.md` (Sec-U-2 race, Sr-MED-6 stacking moot,
|
||||
docs-site MED+LOW items are cross-repo, F5 cosmetic items push
|
||||
to hygiene PR).
|
||||
|
||||
## Pre-flight verification
|
||||
|
||||
- `pnpm typecheck` — clean
|
||||
- `pnpm lint` — 13 pre-existing warnings (no errors), unchanged
|
||||
- `pnpm test --run` — 2097+ passing
|
||||
- `pnpm test:integration` — 85 passing
|
||||
|
||||
Ready for Wave 5 (multi-agent QA) and Wave 6 (release v1.4.22).
|
||||
@@ -3,6 +3,52 @@
|
||||
Carry-over from v1.4.20 marathon foundation phases. Pick from this list
|
||||
when v1.4.21 starts.
|
||||
|
||||
## Closed in v1.4.22
|
||||
|
||||
The Wave-4 cleanup pass closed the following items. See
|
||||
`.planning/phase-W4-report.md` for full SHAs + reasoning.
|
||||
|
||||
- **C1** Zielwerte sparkline + Δ-vs-last-month — landed in commit 1
|
||||
- **C2** api-tokens 5th-attempt scrollbar fix — landed in commit 2
|
||||
- **C3** Coolify `?force=true` + howto for the UI checkbox — landed in
|
||||
commit 3
|
||||
- **C4** AuthShell post-hydration redirect → moved to proxy.ts — landed
|
||||
in commit 4
|
||||
- **C5** node-26-alpine — PR #162 closed (no Dockerfile bump; Next.js
|
||||
16 still pins node 22 LTS and node:26-alpine ships without corepack)
|
||||
- **F-32** triple help blocks on /settings/dashboard — landed in
|
||||
commit 5
|
||||
- **F-33** mood subtitle ampersand → "und" / "and" — landed in commit 5
|
||||
- **F-34** measurements subtitle ampersand → "und" / "and" — landed in
|
||||
commit 5
|
||||
- **F-35** Send-test button label inconsistency — unified on the
|
||||
shorter "Send test" / "Test senden" — landed in commit 5
|
||||
- **F-44** `LOKI_ENDPOINT` env-var leakage in user-facing copy — landed
|
||||
in commit 5
|
||||
- **F-45** admin/users column header "Users" → "Name" (new
|
||||
`admin.userName` key) — landed in commit 5
|
||||
- **F-47** /notifications event-column header collision with the page
|
||||
title — new `notifications.eventColumn` key — landed in commit 5
|
||||
- **F-50** DE error-toast tone — "Bitte erneut versuchen" → "Bitte
|
||||
versuche es erneut" across six keys — landed in commit 5
|
||||
- **F-58** admin/reminders icon swap (Clock → Bell to match the
|
||||
admin-shell nav) — landed in commit 5
|
||||
- **F-62** DE dashboard `greetingSalutation` ("Hi" → "Hallo, {name}")
|
||||
— landed in commit 5
|
||||
- **D-CR-M-03** formatTokenName regex broadened to non-Z ISO offsets
|
||||
— landed in commit 5
|
||||
- **D-DSGN-M-04** moodLog "Copy" button uses `common.copy` instead of
|
||||
stripping `!` off `common.copied` — landed in commit 5
|
||||
- **FX 1** 191 `Marc` source-comment references swept (test fixtures
|
||||
kept) — landed in commit 6
|
||||
- **FX 2** DE+EN bilingual CHANGELOG entries (v1.4.14 + v1.4.15)
|
||||
normalised to English-only — landed in commit 6
|
||||
- **FX 3** `CLAUDE.md` renamed to `CONTRIBUTING-AI.md`; `AGENTS.md`
|
||||
retained for multi-agent compatibility — landed in commit 6
|
||||
|
||||
The remaining MED + LOW items below either deferred to v1.4.23 (most
|
||||
of the heavy reviewer findings) or stay open as a hygiene PR.
|
||||
|
||||
## Phase D — v1.4.20 reconcile carry-over
|
||||
|
||||
Items raised by the six-reviewer Phase-D pass that were either too
|
||||
|
||||
+188
-433
@@ -549,395 +549,189 @@
|
||||
|
||||
### Added
|
||||
|
||||
- **Backups: vollständig.** Die Sicherung am Sonntag früh ist jetzt
|
||||
nur noch der Anfang. `/admin/backups` lässt jeden Snapshot als
|
||||
`.json` herunterladen, neue Snapshots hochladen und — hinter einer
|
||||
dreifachen Bestätigung („RESTORE“ tippen + Dialog + Confirm) —
|
||||
einen Snapshot direkt in die Datenbank zurückspielen. Jede
|
||||
Operation (Run, Download, Upload, Restore inkl. Start/Failure)
|
||||
landet im Audit-Log mit Akteur und Ziel-Snapshot.
|
||||
_Backups become a full lifecycle. The Sunday-morning snapshot is
|
||||
no longer the end of the road. `/admin/backups` lets you download
|
||||
any snapshot as `.json`, upload a new one, and — behind a
|
||||
triple-confirm gate (type “RESTORE” + dialog + confirm) — restore
|
||||
a snapshot straight into the database. Every operation (run,
|
||||
download, upload, restore including start/failure) is recorded in
|
||||
the audit log with actor and target snapshot._
|
||||
- **Erfolge-Seite (`/achievements`) und Dashboard-Karte.** Dedizierte
|
||||
Seite zeigt freigeschaltete und gesperrte Erfolge inklusive
|
||||
Fortschrittsbalken (`{aktuell} / {ziel}`), gruppiert nach
|
||||
Kategorie (Medikation, Vitalwerte, Sicherheit, Engagement). Auf
|
||||
dem Dashboard sitzt eine zuschaltbare „Letzte Erfolge“-Karte; die
|
||||
Sichtbarkeit lässt sich in den Layout-Einstellungen umschalten.
|
||||
_New `/achievements` page lists locked + unlocked achievements
|
||||
with progress bars (`{current} / {target}`), grouped by category
|
||||
(medication, vitals, security, engagement). The dashboard gains a
|
||||
toggleable “Recent achievements” card; visibility lives in the
|
||||
Layout settings._
|
||||
- **Onboarding-Tour für Erstnutzer.** Beim ersten Dashboard-Besuch
|
||||
läuft ein Spotlight-Walk-Through über die Tile-Strip, das
|
||||
Quick-Add-Menü, Insights, Integrationen und Erfolge. Skippable mit
|
||||
Esc, Tastatur-navigierbar, `prefers-reduced-motion` respektiert.
|
||||
Erneut anstoßen jederzeit unter Settings → Account → „Tour
|
||||
wiederholen“.
|
||||
_New onboarding tour for first-run users. A spotlight walk-through
|
||||
on first dashboard load points out the tile strip, quick-add menu,
|
||||
insights, integrations, and achievements. Skippable with Esc,
|
||||
fully keyboard-navigable, honors `prefers-reduced-motion`. Replay
|
||||
the tour any time from Settings → Account._
|
||||
- **Doctor-Report v2.** Vor dem PDF-Generieren öffnet sich ein
|
||||
Dialog mit konfigurierbarem Zeitraum (Voreinstellungen 90 Tage / 6
|
||||
Monate / 12 Monate, manuell editierbar, max. 2 Jahre) und einem
|
||||
optionalen Praxis-Namen, der auf der Titelseite des PDFs
|
||||
erscheint. Der Praxis-Name wird als Nutzervorgabe gespeichert.
|
||||
_Doctor-report v2. Before the PDF is generated a dialog asks for
|
||||
the date range (presets 90 days / 6 months / 12 months, manually
|
||||
editable, max 2 years) and an optional practice name that appears
|
||||
on the PDF cover page. The practice name is persisted as a user
|
||||
preference._
|
||||
- **Auto-Deploy nach GHCR-Push.** Sobald die Docker-Publish-Action
|
||||
ein neues `:latest`-Image hochgeladen hat, ruft sie den
|
||||
Coolify-Deploy-Webhook auf. Coolify pingt anschließend einen
|
||||
internen Endpoint mit dem Ergebnis; Erfolg/Fehler/Unbekannt
|
||||
landen im Audit-Log, persistente Failures triggern eine
|
||||
Telegram-Benachrichtigung an alle Admins. Kein manuelles
|
||||
Force-Pull mehr nötig.
|
||||
_Auto-deploy on GHCR push. Once the Docker publish action ships a
|
||||
new `:latest`, it calls the Coolify deploy webhook. Coolify in
|
||||
turn pings an internal endpoint with the result; success / failure
|
||||
/ unknown all land in the audit log; persistent failures send a
|
||||
Telegram alert to every admin. No more manual force-pull on the
|
||||
host._
|
||||
- **Empty-States überall.** Brandneue Accounts und leere Listen
|
||||
bekommen jetzt überall einen sinnvollen Empty-State (Icon +
|
||||
Beschreibung + Call-to-Action), wo vorher nur weiße Fläche stand.
|
||||
Betroffen: Admin-Tabellen (Users, Backups, Login-Overview,
|
||||
API-Tokens, Feedback, Audit-Preview), Messungs-, Stimmungs-,
|
||||
Medikamenten- und Erfolge-Listen, Insights-Top-Level + BMI-ohne-
|
||||
Größe, Dashboard für komplett leere Accounts.
|
||||
_Empty states everywhere. Brand-new accounts and empty lists now
|
||||
always show a sensible empty state (icon + description + CTA)
|
||||
where there used to be just white space. Coverage: admin tables
|
||||
(users, backups, login-overview, api-tokens, feedback,
|
||||
audit-preview), measurement / mood / medication / achievement
|
||||
lists, the insights top-level + BMI-without-height view, and the
|
||||
dashboard for fully empty accounts._
|
||||
- **Status-Karten für Notification-Channels.** Settings →
|
||||
Benachrichtigungen zeigt für jeden Kanal (Telegram, ntfy, Web
|
||||
Push) den aktuellen Status (verbunden / fehlerhaft / deaktiviert),
|
||||
letzten Erfolg, letzten Fehler, Counter aufeinanderfolgender
|
||||
Fehlversuche und ggf. Deaktivierungsgrund. Buttons: „Wieder
|
||||
aktivieren“ (nur wenn auto-deaktiviert) + „Test-Nachricht
|
||||
senden“.
|
||||
_Notification channel status UI. Settings → Notifications now
|
||||
shows per-channel (Telegram, ntfy, Web Push) the current state
|
||||
(connected / error / disabled), last success, last failure,
|
||||
consecutive-failure counter, and disable reason if any. Buttons
|
||||
for “Re-enable” (only when auto-disabled) and “Send test”._
|
||||
- **Status-Karten für Withings + moodLog.** Settings → Integrationen
|
||||
zeigt für beide Provider Verbindungsstatus, letzten Sync, letzten
|
||||
Fehler und einen Reauth-Hinweis, sobald ein Refresh-Token
|
||||
abgelehnt wurde. Auf persistente Fehler (≥ 3 in Folge) bekommen
|
||||
Admins eine Telegram-Benachrichtigung, sofern der Kanal aktiviert
|
||||
ist.
|
||||
_Withings + moodLog integration status UI. Settings → Integrations
|
||||
shows per-provider the connection state, last sync, last error,
|
||||
and a reauth hint as soon as a refresh-token was rejected. After
|
||||
persistent failures (≥ 3 in a row) admins receive a Telegram alert
|
||||
if the channel is enabled._
|
||||
- **Top-Tiles unabhängig wählbar.** In den Layout-Einstellungen lässt
|
||||
sich für jede Metrik die Sichtbarkeit der Trend-Kachel oben und
|
||||
des Charts unten getrennt steuern. Bestehende Layouts behalten ihr
|
||||
bisheriges Verhalten (eine Schaltfläche steuert beide), bis du
|
||||
explizit den neuen Schalter umlegst.
|
||||
_Top dashboard tiles selectable. The layout settings now expose a
|
||||
separate toggle for each metric’s upper-row tile and lower-row
|
||||
chart. Existing saved layouts keep their previous behaviour (one
|
||||
switch controls both surfaces) until you explicitly flip the new
|
||||
toggle._
|
||||
- **7-Tage-Trend statt 7-Tage-Schnitt.** Auf jeder Tile-Kachel zeigt
|
||||
ein farbiger Delta-Indikator `(±N.N)` neben dem Wert die
|
||||
metrik-bewusste Veränderung der letzten 7 Tage gegenüber den
|
||||
davor. Label umbenannt von „7-Tage-Schnitt“ auf „7-Tage-Trend“.
|
||||
_7-day-trend instead of 7-day-average. Each tile gains a coloured
|
||||
delta indicator `(±N.N)` next to the value, showing the
|
||||
metric-aware change of the last 7 days vs. the prior 7. Label
|
||||
renamed from “7d average” to “7d trend”._
|
||||
- Backups become a full lifecycle. The Sunday-morning snapshot is no
|
||||
longer the end of the road. `/admin/backups` lets you download any
|
||||
snapshot as `.json`, upload a new one, and — behind a triple-confirm
|
||||
gate (type “RESTORE” + dialog + confirm) — restore a snapshot straight
|
||||
into the database. Every operation (run, download, upload, restore
|
||||
including start/failure) is recorded in the audit log with actor and
|
||||
target snapshot.
|
||||
- New `/achievements` page lists locked + unlocked achievements with
|
||||
progress bars (`{current} / {target}`), grouped by category (medication,
|
||||
vitals, security, engagement). The dashboard gains a toggleable “Recent
|
||||
achievements” card; visibility lives in the Layout settings.
|
||||
- New onboarding tour for first-run users. A spotlight walk-through on
|
||||
first dashboard load points out the tile strip, quick-add menu,
|
||||
insights, integrations, and achievements. Skippable with Esc, fully
|
||||
keyboard-navigable, honors `prefers-reduced-motion`. Replay the tour any
|
||||
time from Settings → Account.
|
||||
- Doctor-report v2. Before the PDF is generated a dialog asks for the date
|
||||
range (presets 90 days / 6 months / 12 months, manually editable, max 2
|
||||
years) and an optional practice name that appears on the PDF cover page.
|
||||
The practice name is persisted as a user preference.
|
||||
- Auto-deploy on GHCR push. Once the Docker publish action ships a new
|
||||
`:latest`, it calls the Coolify deploy webhook. Coolify in turn pings an
|
||||
internal endpoint with the result; success / failure / unknown all land
|
||||
in the audit log; persistent failures send a Telegram alert to every
|
||||
admin. No more manual force-pull on the host.
|
||||
- Empty states everywhere. Brand-new accounts and empty lists now always
|
||||
show a sensible empty state (icon + description + CTA) where there used
|
||||
to be just white space. Coverage: admin tables (users, backups,
|
||||
login-overview, api-tokens, feedback, audit-preview), measurement / mood
|
||||
/ medication / achievement lists, the insights top-level +
|
||||
BMI-without-height view, and the dashboard for fully empty accounts.
|
||||
- Notification channel status UI. Settings → Notifications now shows
|
||||
per-channel (Telegram, ntfy, Web Push) the current state (connected /
|
||||
error / disabled), last success, last failure, consecutive-failure
|
||||
counter, and disable reason if any. Buttons for “Re-enable” (only when
|
||||
auto-disabled) and “Send test”.
|
||||
- Withings + moodLog integration status UI. Settings → Integrations shows
|
||||
per-provider the connection state, last sync, last error, and a reauth
|
||||
hint as soon as a refresh-token was rejected. After persistent failures
|
||||
(≥ 3 in a row) admins receive a Telegram alert if the channel is
|
||||
enabled.
|
||||
- Top dashboard tiles selectable. The layout settings now expose a
|
||||
separate toggle for each metric’s upper-row tile and lower-row chart.
|
||||
Existing saved layouts keep their previous behaviour (one switch
|
||||
controls both surfaces) until you explicitly flip the new toggle.
|
||||
- 7-day-trend instead of 7-day-average. Each tile gains a coloured delta
|
||||
indicator `(±N.N)` next to the value, showing the metric-aware change of
|
||||
the last 7 days vs. the prior 7. Label renamed from “7d average” to “7d
|
||||
trend”.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`/admin`-Übersicht neu gestaltet.** Statt einer Section-Grid
|
||||
zeigt die Übersicht jetzt eine Audit-Log-Vorschau (letzte
|
||||
Einträge, Akteur, Zielressource) und einen
|
||||
System-Status-Snapshot. Die Section-Grid wandert in die Sidebar.
|
||||
_The `/admin` overview replaces the section grid with an
|
||||
audit-log preview (recent entries, actor, target resource) and a
|
||||
system-status snapshot. The section grid moved into the sidebar._
|
||||
- **Sidebar-Admin-Submenu kontextbewusst.** Die Admin-Untereinträge
|
||||
klappen nur noch auf, wenn man tatsächlich auf einer
|
||||
`/admin/*`-Route ist. Auf allen anderen Seiten bleibt die
|
||||
Admin-Gruppe eingeklappt.
|
||||
_The sidebar Admin sub-items only expand when you are actually on
|
||||
an `/admin/*` route. Everywhere else the Admin group stays
|
||||
collapsed._
|
||||
- **Stimmung-Chart aggregiert wie die anderen Metriken.** Über 90
|
||||
Tage wechselt das Stimmung-Diagramm automatisch auf wöchentliche
|
||||
Aggregation, über 730 Tage auf monatliche — analog zu Gewicht und
|
||||
Blutdruck. Im Header zeigt ein Chip die aktive Aggregationsstufe.
|
||||
_The mood chart now auto-aggregates: weekly past 90 days, monthly
|
||||
past 730 days — same thresholds as weight and BP. A chip in the
|
||||
chart header shows the active aggregation level._
|
||||
- **KI-Insights mit harten Anti-Halluzinations-Leitplanken.** Die
|
||||
Provider-Antwort wird gegen ein striktes Zod-Schema geprüft
|
||||
(Zusammenfassung, Empfehlungen mit Pflichtfeld `metricSource`,
|
||||
Zitate, Warnungen). Empfehlungen, die Datenpunkte zitieren, die
|
||||
nicht in der Snapshot-Liste stehen, werden abgewiesen. Der
|
||||
Wrapper retried genau einmal mit korrigierender Systemnachricht;
|
||||
scheitert auch der zweite Versuch, kommt 422 zurück statt
|
||||
unsauberen Ausgaben. Slug-Drift im Codex-Backend (z. B. Wechsel
|
||||
`gpt-5-codex` → `gpt-5.3-codex`) ist jetzt durch eine
|
||||
konfigurierbare Fallback-Kette + 1-Stunden-Cache abgefedert; alle
|
||||
Slugs durch → strukturierter 503 mit `attempted[]`-Liste.
|
||||
_AI insights gain hard anti-hallucination guardrails. Provider
|
||||
responses are validated against a strict Zod schema (summary,
|
||||
recommendations with mandatory `metricSource`, citations,
|
||||
warnings). Recommendations citing data points not present in the
|
||||
snapshot are rejected. The wrapper retries exactly once with a
|
||||
corrective system message; if the second attempt also fails, it
|
||||
returns 422 instead of unsanitised output. Codex-backend slug
|
||||
drift (e.g. `gpt-5-codex` → `gpt-5.3-codex`) is now cushioned by
|
||||
a configurable fallback chain + 1-hour positive cache; if every
|
||||
slug fails the route returns a structured 503 with the
|
||||
`attempted[]` list._
|
||||
- **`/admin/api-tokens`-Tabelle mobil.** Auf schmalen Viewports
|
||||
blenden Spalten gestaffelt aus (zuletzt benutzt, erstellt am,
|
||||
Owner) und der Owner-Username wandert inline neben den Token-
|
||||
Namen, damit beim Ausblenden keine Information verloren geht.
|
||||
_The `/admin/api-tokens` table is responsive on mobile. Narrow
|
||||
viewports gate columns behind breakpoints (last-used, created-at,
|
||||
owner) and the owner username falls back inline next to the token
|
||||
name so hiding the column never drops data._
|
||||
- **Stimmung-Kachel mobil.** Auf schmalen Viewports zeigt die
|
||||
Mood-Karte oben jetzt nur noch die große Score-Zahl + Label statt
|
||||
der doppelten Darstellung von vorher.
|
||||
_Mood tile on mobile shows only the large score number + label,
|
||||
no more doubled rendering._
|
||||
- **Quick-Add-Submenu eindeutiger.** Die beiden Einträge im
|
||||
Quick-Add-Menü heißen jetzt „Messung erfassen“ und „Stimmung
|
||||
erfassen“ (DE) bzw. „Log measurement“ und „Log mood“ (EN) —
|
||||
vorher beide schlicht „Hinzufügen“.
|
||||
_Quick-add submenu disambiguated. The two entries are now called
|
||||
“Messung erfassen” / “Stimmung erfassen” (DE) and “Log
|
||||
measurement” / “Log mood” (EN) — previously both simply “Add”._
|
||||
- The `/admin` overview replaces the section grid with an audit-log
|
||||
preview (recent entries, actor, target resource) and a system-status
|
||||
snapshot. The section grid moved into the sidebar.
|
||||
- The sidebar Admin sub-items only expand when you are actually on an
|
||||
`/admin/*` route. Everywhere else the Admin group stays collapsed.
|
||||
- The mood chart now auto-aggregates: weekly past 90 days, monthly past
|
||||
730 days — same thresholds as weight and BP. A chip in the chart header
|
||||
shows the active aggregation level.
|
||||
- AI insights gain hard anti-hallucination guardrails. Provider responses
|
||||
are validated against a strict Zod schema (summary, recommendations with
|
||||
mandatory `metricSource`, citations, warnings). Recommendations citing
|
||||
data points not present in the snapshot are rejected. The wrapper
|
||||
retries exactly once with a corrective system message; if the second
|
||||
attempt also fails, it returns 422 instead of unsanitised output.
|
||||
Codex-backend slug drift (e.g. `gpt-5-codex` → `gpt-5.3-codex`) is now
|
||||
cushioned by a configurable fallback chain + 1-hour positive cache; if
|
||||
every slug fails the route returns a structured 503 with the
|
||||
`attempted[]` list.
|
||||
- The `/admin/api-tokens` table is responsive on mobile. Narrow viewports
|
||||
gate columns behind breakpoints (last-used, created-at, owner) and the
|
||||
owner username falls back inline next to the token name so hiding the
|
||||
column never drops data.
|
||||
- Mood tile on mobile shows only the large score number + label, no more
|
||||
doubled rendering.
|
||||
- Quick-add submenu disambiguated. The two entries are now called “Messung
|
||||
erfassen” / “Stimmung erfassen” (DE) and “Log measurement” / “Log mood”
|
||||
(EN) — previously both simply “Add”.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Blutdruck-Zielbereich-Prozent.** Die Zielbereich-Kachel zählte
|
||||
Sys/Dia-Paare nicht korrekt, sobald durch Importe die Sys/Dia-
|
||||
Zeitstempel weiter als 5 Minuten auseinander lagen — Ergebnis
|
||||
konnte 0 % anzeigen, obwohl die meisten Werte im Zielbereich
|
||||
waren. Fix: Tagesschlüssel-Fallback (gleicher Berliner Tag) +
|
||||
Paaranzahl als Nenner.
|
||||
_Blood-pressure target-range percentage. The target-range tile
|
||||
miscounted sys/dia pairs once import drift pushed sys/dia
|
||||
timestamps more than 5 minutes apart — the tile could show 0 %
|
||||
even though most readings were inside the target range. Fix: a
|
||||
same-Berlin-day key fallback + pair-count as denominator._
|
||||
- **Onboarding-Flackern.** Auf bereits abgeschlossenen Accounts
|
||||
rendert die Onboarding-Karte nicht mehr für ~500 ms, bevor sie
|
||||
wieder verschwindet. Sie wird erst gemountet, sobald der
|
||||
Analytics-Status geladen ist, und öffnet sich nicht mehr
|
||||
automatisch, wenn alles erledigt ist.
|
||||
_Onboarding flicker. On accounts where onboarding is already
|
||||
complete, the onboarding card no longer renders for ~500 ms
|
||||
before vanishing. It mounts only after the analytics status has
|
||||
loaded, and refuses to auto-open when nothing is left to do._
|
||||
- **Skip-Link blockiert Logo nicht.** Der „Zum Inhalt
|
||||
springen“-Skip-Link liegt zwar weiterhin am Anfang des Tabbing-
|
||||
Reihenfolge, blockiert aber nicht mehr Klicks aufs Logo.
|
||||
_The “Skip to content” skip-link still leads the tab order but
|
||||
no longer blocks clicks on the logo._
|
||||
- **Bug-Report-Eintrag richtet sich nach dem Admin-Toggle.** Wenn
|
||||
der Admin den Bug-Report deaktiviert hat, verschwindet der
|
||||
Eintrag aus Sidebar, Bottom-Nav, Topbar und der Fehler-Detail-
|
||||
„Bug melden“-Schaltfläche.
|
||||
_The “Report a bug” entry follows the admin feature flag. With
|
||||
bug reporting disabled, the entry vanishes from the sidebar,
|
||||
bottom-nav, topbar, and the error-detail “Report bug” button._
|
||||
- **Feedback-Link richtet sich nach dem Admin-Toggle.** Wenn die
|
||||
Feedback-Sammlung deaktiviert ist, ist auch der UI-Einstieg
|
||||
weg.
|
||||
_The Feedback link follows its admin feature flag. With feedback
|
||||
disabled, the UI entry point disappears too._
|
||||
- **Notification-Channels deaktivieren sich auf harte Reject-
|
||||
Fehler.** Antworten Telegram / ntfy / Web Push wiederholt mit
|
||||
410 oder anderen permanenten Reject-Codes, deaktiviert sich der
|
||||
Kanal automatisch und schreibt einen Audit-Log-Eintrag. Status
|
||||
- Re-Enable-Button finden sich in Settings → Benachrichtigungen.
|
||||
_Notification channels auto-disable on persistent hard rejects.
|
||||
If Telegram / ntfy / Web Push respond repeatedly with 410 or
|
||||
other permanent reject codes, the channel disables itself and
|
||||
writes an audit-log entry. Status + re-enable button live in
|
||||
Settings → Notifications._
|
||||
- **Refresh-Token-Fehler markiert Integration als reauth-pflichtig.**
|
||||
Bricht der Withings- oder moodLog-Refresh-Token-Tausch ab
|
||||
(typischerweise nach 90 Tagen ohne Re-Auth), wechselt die
|
||||
Integration in `error_reauth` und der Settings-Eintrag bittet um
|
||||
Neu-Verbindung — statt jeden Sync-Tick weiter gegen den Provider
|
||||
zu hämmern.
|
||||
_Refresh-token failures flip the integration to “needs reauth”.
|
||||
When the Withings or moodLog refresh-token exchange fails
|
||||
(typically after 90 days without re-auth), the integration
|
||||
switches to `error_reauth` and the Settings entry asks for
|
||||
re-connect — instead of hammering the upstream every sync
|
||||
tick._
|
||||
- **Charts blockieren das Wischen nicht mehr.** Auf Touch-Geräten
|
||||
fingen die Recharts-Wrapper vertikales Wischen ab; das fühlte
|
||||
sich auf langsamen Geräten wie Hänger an. Fix: `touch-action:
|
||||
pan-y` auf den Chart-Containern, vertikaler Scroll geht jetzt
|
||||
durch.
|
||||
_Mobile chart containers no longer eat vertical scroll. On touch
|
||||
devices the Recharts wrappers swallowed vertical pans; on slower
|
||||
devices that felt like a scroll lockup. Fix: `touch-action:
|
||||
pan-y` on the chart wrappers, vertical scroll passes through
|
||||
again._
|
||||
- **`/admin/users` mobil ohne Overflow.** Auf schmalen Viewports
|
||||
rendert die User-Tabelle als Card-List statt einer
|
||||
horizontal-scrollenden Tabelle.
|
||||
_`/admin/users` on mobile renders as a card list instead of a
|
||||
horizontally-scrolling table._
|
||||
- **A11y-Verbesserungen.** Chart-Range-Buttons, Medikamenten-
|
||||
Primary-Buttons, Mood-List-Mobile-Icon-Buttons, Login-CTAs,
|
||||
Settings → Account-Passkey-Tabelle, Onboarding-Tour-Focus-Trap +
|
||||
DE-Overflow + Backdrop-Ring — alles auf 44 px Mindest-
|
||||
Tap-Target bzw. mit `aria-label` / Keyboard-Bedienbarkeit
|
||||
versehen.
|
||||
_Accessibility round of fixes: chart range buttons, medication
|
||||
primary buttons, mood-list mobile icon buttons, login CTAs,
|
||||
Settings → Account passkey table, onboarding-tour focus trap +
|
||||
DE-locale overflow + backdrop ring — all aligned to 44 px tap
|
||||
targets and labelled / keyboard-navigable._
|
||||
- Blood-pressure target-range percentage. The target-range tile miscounted
|
||||
sys/dia pairs once import drift pushed sys/dia timestamps more than 5
|
||||
minutes apart — the tile could show 0 % even though most readings were
|
||||
inside the target range. Fix: a same-Berlin-day key fallback +
|
||||
pair-count as denominator.
|
||||
- Onboarding flicker. On accounts where onboarding is already complete,
|
||||
the onboarding card no longer renders for ~500 ms before vanishing. It
|
||||
mounts only after the analytics status has loaded, and refuses to
|
||||
auto-open when nothing is left to do.
|
||||
- The “Skip to content” skip-link still leads the tab order but no longer
|
||||
blocks clicks on the logo.
|
||||
- The “Report a bug” entry follows the admin feature flag. With bug
|
||||
reporting disabled, the entry vanishes from the sidebar, bottom-nav,
|
||||
topbar, and the error-detail “Report bug” button.
|
||||
- The Feedback link follows its admin feature flag. With feedback
|
||||
disabled, the UI entry point disappears too.
|
||||
- Notification channels auto-disable on persistent hard rejects. If
|
||||
Telegram / ntfy / Web Push respond repeatedly with 410 or other
|
||||
permanent reject codes, the channel disables itself and writes an
|
||||
audit-log entry. Status + re-enable button live in Settings →
|
||||
Notifications.
|
||||
- reauth` und der Settings-Eintrag bittet um Neu-Verbindung — statt jeden
|
||||
Sync-Tick weiter gegen den Provider zu hämmern. _Refresh-token failures
|
||||
flip the integration to “needs reauth”. When the Withings or moodLog
|
||||
refresh-token exchange fails (typically after 90 days without re-auth),
|
||||
the integration switches to `error_reauth` and the Settings entry asks
|
||||
for re-connect — instead of hammering the upstream every sync tick.
|
||||
- Mobile chart containers no longer eat vertical scroll. On touch devices
|
||||
the Recharts wrappers swallowed vertical pans; on slower devices that
|
||||
felt like a scroll lockup. Fix: `touch-action: pan-y` on the chart
|
||||
wrappers, vertical scroll passes through again.
|
||||
- `/admin/users` on mobile renders as a card list instead of a
|
||||
horizontally-scrolling table.
|
||||
- Accessibility round of fixes: chart range buttons, medication primary
|
||||
buttons, mood-list mobile icon buttons, login CTAs, Settings → Account
|
||||
passkey table, onboarding-tour focus trap + DE-locale overflow +
|
||||
backdrop ring — all aligned to 44 px tap targets and labelled /
|
||||
keyboard-navigable.
|
||||
|
||||
### Security
|
||||
|
||||
- **AI-Provider-Antworten gegen striktes Schema validiert.** Der
|
||||
Insight-Wrapper akzeptiert nur Antworten, die das strikte
|
||||
`aiInsightResponseSchema` erfüllen, und verweigert Empfehlungen,
|
||||
die Datenpunkte zitieren, die nicht im übergebenen Snapshot
|
||||
stehen. Schemafehler triggern einen einmaligen Retry mit
|
||||
korrigierender Systemnachricht; ein zweiter Fehler liefert 422
|
||||
statt halluzinierter Ausgaben.
|
||||
_AI provider responses are validated against a strict Zod
|
||||
schema. The insight wrapper only accepts responses that satisfy
|
||||
the strict `aiInsightResponseSchema`, and rejects recommendations
|
||||
citing data points absent from the supplied snapshot. Schema
|
||||
failures trigger a single retry with a corrective system message;
|
||||
a second failure returns 422 instead of hallucinated output._
|
||||
- **Backup-Operationen vollständig auditiert.** Run, Download,
|
||||
Upload und Restore — inklusive Start-Marker, Denial-Reasons und
|
||||
Failures — schreiben jeweils einen Audit-Log-Eintrag mit Akteur
|
||||
und Snapshot-ID. Restore wird zusätzlich durch fünf unabhängige
|
||||
Gates abgesichert (Cookie-Auth-only-Admin, `confirm: "RESTORE"`-
|
||||
Body, getypte UI-Bestätigung, Idempotency-Key, Pre-Tx-Enum-
|
||||
Validation).
|
||||
_Backup operations are fully audited. Run, download, upload, and
|
||||
restore — including start markers, denial reasons, and failures
|
||||
— each write an audit-log entry with actor and snapshot ID.
|
||||
Restore is additionally protected by five independent gates
|
||||
(cookie-only admin auth, `confirm: "RESTORE"` body, typed UI
|
||||
confirmation, idempotency-key wrap, pre-transaction enum
|
||||
validation)._
|
||||
- AI provider responses are validated against a strict Zod schema. The
|
||||
insight wrapper only accepts responses that satisfy the strict
|
||||
`aiInsightResponseSchema`, and rejects recommendations citing data
|
||||
points absent from the supplied snapshot. Schema failures trigger a
|
||||
single retry with a corrective system message; a second failure returns
|
||||
422 instead of hallucinated output.
|
||||
- Backup operations are fully audited. Run, download, upload, and restore
|
||||
— including start markers, denial reasons, and failures — each write an
|
||||
audit-log entry with actor and snapshot ID. Restore is additionally
|
||||
protected by five independent gates (cookie-only admin auth, `confirm:
|
||||
"RESTORE"` body, typed UI confirmation, idempotency-key wrap,
|
||||
pre-transaction enum validation).
|
||||
|
||||
## [1.4.14] — 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- **Admin-Bereich nach Sektionen aufgeteilt.** Statt einer monolithischen
|
||||
`/admin`-Seite mit Anker-Sprüngen hat jede Sektion jetzt ihre eigene
|
||||
Route: `/admin/system-status`, `/admin/services`,
|
||||
`/admin/integrations`, `/admin/feedback`, `/admin/reminders`,
|
||||
`/admin/users`, `/admin/api-tokens`, `/admin/login-overview`,
|
||||
`/admin/backups`, `/admin/danger-zone`. Sidebar bekommt eine
|
||||
ausklappbare Admin-Gruppe; Status-Cards verlinken direkt auf die
|
||||
passende Unterseite. Alte `/admin#section-…`-Pfade werden serverseitig
|
||||
weitergeleitet.
|
||||
_The admin area is split into per-section pages instead of one
|
||||
monolithic dashboard with anchor jumps. Each section has its own URL
|
||||
- The admin area is split into per-section pages instead of one monolithic
|
||||
dashboard with anchor jumps. Each section has its own URL
|
||||
(`/admin/system-status`, `/admin/services`, `/admin/integrations`,
|
||||
`/admin/feedback`, `/admin/reminders`, `/admin/users`,
|
||||
`/admin/api-tokens`, `/admin/login-overview`, `/admin/backups`,
|
||||
`/admin/danger-zone`); the sidebar grows an expandable Admin group;
|
||||
status cards link to the relevant sub-page; legacy
|
||||
`/admin#section-…` paths are redirected server-side._
|
||||
- **Neue Backups-Sektion** unter `/admin/backups` mit Tabelle aller
|
||||
bisherigen Sicherungen (Größe, Typ, Erstellungszeit) und einem
|
||||
„Backup jetzt erstellen“-Button, der einen Ad-hoc-Job auf der
|
||||
pg-boss-Queue auslöst.
|
||||
_New `/admin/backups` view with a table of all stored backups (size,
|
||||
status cards link to the relevant sub-page; legacy `/admin#section-…`
|
||||
paths are redirected server-side.
|
||||
- New `/admin/backups` view with a table of all stored backups (size,
|
||||
type, timestamp) and a “Backup now” button that enqueues an ad-hoc
|
||||
pg-boss job._
|
||||
- **Neue User-Verwaltung** unter `/admin/users` mit Filter-Pills
|
||||
(alle / nur Admins / nur Standardnutzer) und einer
|
||||
„Force-Logout“-Aktion pro Nutzer hinter einem Bestätigungsdialog
|
||||
(löscht alle aktiven Sessions des Ziels und schreibt einen
|
||||
Audit-Log-Eintrag). Selbst-Logout ist gesperrt.
|
||||
_New `/admin/users` view with role filter pills (all / admins only /
|
||||
pg-boss job.
|
||||
- New `/admin/users` view with role filter pills (all / admins only /
|
||||
users only) and a per-row force-logout action behind a confirmation
|
||||
dialog (deletes every active session of the target and writes an
|
||||
audit-log entry). Self-target is disabled._
|
||||
- **„Gespeicherten KI-Schlüssel entfernen“** in Settings → KI: ein
|
||||
klar betitelter Knopf mit Bestätigungsdialog löscht den
|
||||
gespeicherten OpenAI- bzw. lokalen Provider-Key, ohne Settings
|
||||
ansonsten anzufassen.
|
||||
_New “Remove saved AI key” button in Settings → AI: a clearly
|
||||
labelled button behind a confirmation dialog clears the stored
|
||||
OpenAI or local provider key without touching anything else._
|
||||
- **`CODEX_MODEL`-Env-Var** als Operator-Override für das Codex-
|
||||
Modell-Slug, damit alternative Modelle ohne Rebuild getestet werden
|
||||
können (z. B. wenn dein ChatGPT-Plan ein anderes Default-Modell
|
||||
bevorzugt).
|
||||
_New `CODEX_MODEL` env var lets operators override the Codex model
|
||||
slug without a rebuild — useful for testing alternate slugs against
|
||||
different ChatGPT plan tiers._
|
||||
audit-log entry). Self-target is disabled.
|
||||
- New “Remove saved AI key” button in Settings → AI: a clearly labelled
|
||||
button behind a confirmation dialog clears the stored OpenAI or local
|
||||
provider key without touching anything else.
|
||||
- MODEL`-Env-Var** als Operator-Override für das Codex- Modell-Slug, damit
|
||||
alternative Modelle ohne Rebuild getestet werden können (z. B. wenn dein
|
||||
ChatGPT-Plan ein anderes Default-Modell bevorzugt). _New `CODEX_MODEL`
|
||||
env var lets operators override the Codex model slug without a rebuild —
|
||||
useful for testing alternate slugs against different ChatGPT plan tiers.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Trend-Pfeil-Farben sind jetzt metrik-bewusst.** Steigende Tendenz
|
||||
bedeutet je nach Messwert etwas anderes: Blutdruck und Gewicht ↑ =
|
||||
orange (Warnung), Stimmung ↑ = grün (positiv), Puls ↑ = neutral
|
||||
(kontextabhängig). Die Pfeil-Richtung selbst bleibt unverändert.
|
||||
_Trend-arrow colors are metric-aware: BP and weight rising = orange
|
||||
- Trend-arrow colors are metric-aware: BP and weight rising = orange
|
||||
(warning), mood rising = green (positive), pulse rising = neutral
|
||||
(context-dependent). Arrow direction itself is unchanged._
|
||||
- **Wipe-all-data deckt jetzt auch Notification-Channels und Web-Push-
|
||||
Subscriptions ab.** Verschlüsselte Telegram-Bot-Tokens und
|
||||
Web-Push-Endpoints überleben einen vollständigen Account-Wipe nicht
|
||||
länger. Feedback und Audit-Log bleiben erhalten (wie bisher).
|
||||
Bestätigungstext im UI nennt den neuen Umfang explizit.
|
||||
_“Wipe all data” now also clears notification channels and web-push
|
||||
subscriptions: encrypted Telegram bot tokens and web-push endpoints
|
||||
no longer survive a full account wipe. Feedback and audit log stay
|
||||
untouched (unchanged). The confirmation copy spells out the new
|
||||
scope._
|
||||
- **i18n-Schlüssel im Admin-Bereich** unter `admin.section.<slug>.*`
|
||||
reorganisiert (EN + DE-Parität).
|
||||
_Admin-area i18n keys reorganised under `admin.section.<slug>.*`
|
||||
(EN + DE parity)._
|
||||
(context-dependent). Arrow direction itself is unchanged.
|
||||
- “Wipe all data” now also clears notification channels and web-push
|
||||
subscriptions: encrypted Telegram bot tokens and web-push endpoints no
|
||||
longer survive a full account wipe. Feedback and audit log stay
|
||||
untouched (unchanged). The confirmation copy spells out the new scope.
|
||||
- Admin-area i18n keys reorganised under `admin.section.<slug>.*` (EN + DE
|
||||
parity).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Codex/ChatGPT-Modell-Slug korrigiert.** Der Codex-Backend-Endpoint
|
||||
lehnt sowohl `gpt-5-codex` als auch `gpt-5` für ChatGPT-Account-
|
||||
Auth ab; das tatsächliche, codex-optimierte Slug für die Plus/Pro-
|
||||
Tarife heißt `gpt-5.3-codex`. v1.4.14 verdrahtet den richtigen
|
||||
Default. „Verbindung testen“ in Settings → KI und der Insight-
|
||||
Generator laufen damit beide gegen dein ChatGPT-Abo durch.
|
||||
_Codex/ChatGPT model slug corrected: the Codex backend rejects both
|
||||
`gpt-5-codex` and `gpt-5` when authenticated via a ChatGPT account;
|
||||
the codex-optimised slug for the Plus/Pro tiers is `gpt-5.3-codex`.
|
||||
v1.4.14 wires the correct default. Settings → AI “Test connection”
|
||||
and the insight generator now both succeed against your ChatGPT
|
||||
subscription._
|
||||
- Codex/ChatGPT model slug corrected: the Codex backend rejects both
|
||||
`gpt-5-codex` and `gpt-5` when authenticated via a ChatGPT account; the
|
||||
codex-optimised slug for the Plus/Pro tiers is `gpt-5.3-codex`. v1.4.14
|
||||
wires the correct default. Settings → AI “Test connection” and the
|
||||
insight generator now both succeed against your ChatGPT subscription.
|
||||
- **Sommerzeit-Wechsel verschiebt keine Tageswerte mehr.** Die
|
||||
Cross-Metric-Tagespaarung in den Insight-Buckets rechnete früher in
|
||||
reinem UTC und verlor an den Berliner DST-Grenzen einen Tag. Die
|
||||
@@ -951,88 +745,49 @@ pan-y` on the chart wrappers, vertical scroll passes through
|
||||
helper anchors at Berlin Y-M-D and subtracts in 86_400_000-ms
|
||||
steps; bucket pairing across blood pressure, weight, and mood is
|
||||
now DST-safe.*
|
||||
- **KI-Provider-Fehler werden korrekt klassifiziert.**
|
||||
`/api/insights/generate` antwortet auf 401/403-Fehler des Providers
|
||||
jetzt mit 422 („KI-Anbieter hat den Request abgelehnt — bitte
|
||||
API-Key in Settings → KI prüfen“), auf 5xx mit 503 („KI-Anbieter
|
||||
vorübergehend nicht erreichbar“) und auf 429 mit 429. Vorher kam
|
||||
pauschal 500. Volle Fehlerdetails landen weiter im Wide-Event-Log.
|
||||
_AI provider errors are now correctly classified by
|
||||
`/api/insights/generate`: provider 401/403 → 422 (“AI provider
|
||||
rejected the request — check your API key in Settings → AI”),
|
||||
provider 5xx → 503 (“AI provider temporarily unavailable”), 429 → 429. Previously everything returned 500. Full error context still
|
||||
lands in the structured logs._
|
||||
- **Logging-Redactor schluckt keine harmlosen Wörter mehr.** Der
|
||||
Secret-Redactor maskierte zuvor jedes Vorkommen von `sk-…` — auch
|
||||
in `task-force`, `risk-management`, `disk-io`. Die neue Regex
|
||||
verlangt eine Wortgrenze und mindestens 8 Folgezeichen, das Padding
|
||||
bleibt für echte API-Keys (`sk-…`, `sk-ant-…`) intakt.
|
||||
_The structured-logging secret redactor no longer mangles innocent
|
||||
words. The previous regex matched any `sk-…` substring and masked
|
||||
fragments inside `task-force`, `risk-management`, `disk-io`. The
|
||||
new pattern requires a word boundary and at least 8 trailing
|
||||
characters, while still scrubbing real API keys (`sk-…`,
|
||||
`sk-ant-…`)._
|
||||
- AI provider errors are now correctly classified by
|
||||
`/api/insights/generate`: provider 401/403 → 422 (“AI provider rejected
|
||||
the request — check your API key in Settings → AI”), provider 5xx → 503
|
||||
(“AI provider temporarily unavailable”), 429 → 429. Previously
|
||||
everything returned 500. Full error context still lands in the
|
||||
structured logs.
|
||||
- The structured-logging secret redactor no longer mangles innocent words.
|
||||
The previous regex matched any `sk-…` substring and masked fragments
|
||||
inside `task-force`, `risk-management`, `disk-io`. The new pattern
|
||||
requires a word boundary and at least 8 trailing characters, while still
|
||||
scrubbing real API keys (`sk-…`, `sk-ant-…`).
|
||||
|
||||
### Performance
|
||||
|
||||
- **Insights-Seite startet schneller.** Die Recharts-Symbole für die
|
||||
Korrelations-Scatter-Plots werden jetzt bedarfsweise per
|
||||
`next/dynamic` geladen statt eager mitausgeliefert; das spart
|
||||
ca. 108 KiB initiales JavaScript auf der Insights-Seite. Die
|
||||
Chart-Optik bleibt unverändert.
|
||||
_Bundle-size improvements on the insights page: Recharts symbols
|
||||
for the correlation scatter plots are now loaded on demand via
|
||||
`next/dynamic` instead of being shipped eagerly, saving roughly
|
||||
108 KiB of initial JavaScript on `/insights`. Chart visuals are
|
||||
unchanged._
|
||||
- **Dashboard fragt die Onboarding-Checkliste nicht mehr ab, wenn
|
||||
sie schon abgeschlossen ist.** Sobald `onboardingCompletedAt`
|
||||
gesetzt ist, entfallen die `withings/status`- und
|
||||
`notifications/preferences`-Calls beim Dashboard-Aufruf. Spart
|
||||
rund 950 ms Netzwerkzeit bei wiederkehrenden Nutzern.
|
||||
_The dashboard skips the onboarding-checklist API requests once
|
||||
- Bundle-size improvements on the insights page: Recharts symbols for the
|
||||
correlation scatter plots are now loaded on demand via `next/dynamic`
|
||||
instead of being shipped eagerly, saving roughly 108 KiB of initial
|
||||
JavaScript on `/insights`. Chart visuals are unchanged.
|
||||
- The dashboard skips the onboarding-checklist API requests once
|
||||
onboarding is complete. With `onboardingCompletedAt` set, the
|
||||
`withings/status` and `notifications/preferences` queries no longer
|
||||
fire on dashboard load — saves about 950 ms of network time for
|
||||
established users._
|
||||
`withings/status` and `notifications/preferences` queries no longer fire
|
||||
on dashboard load — saves about 950 ms of network time for established
|
||||
users.
|
||||
|
||||
### Tests & A11y
|
||||
|
||||
- **Erweiterte End-to-End-Suite.** Neue Playwright-Specs decken den
|
||||
authentifizierten Dashboard-Render, den „Messung anlegen“-Flow,
|
||||
den Doctor-Report-PDF-Download, einen gemockten Codex-Connect-
|
||||
Flow, einen gemockten Insights-Generate-Flow und einen
|
||||
Mobile-Viewport-Smoketest (Pixel 5) ab.
|
||||
_Extended end-to-end suite. New Playwright specs cover the
|
||||
authenticated dashboard render, the “add measurement” flow, the
|
||||
doctor-report PDF download, a mocked Codex connect flow, a mocked
|
||||
insights-generate flow, and a mobile-viewport smoke test (Pixel 5)._
|
||||
- **Axe-core-Audit auf allen Schlüsselrouten grün.** Die ernsten
|
||||
und kritischen Verstöße auf `/dashboard`, `/settings/integrations`
|
||||
und `/admin` (inkl. `/admin/system-status` und `/admin/users`)
|
||||
wurden behoben — fehlende `aria-label`s an Icon-Buttons, fehlende
|
||||
Namen am Mobile-User-Menü-Trigger, leere `<dd>`-Platzhalter,
|
||||
Login-Link nur durch Farbe erkennbar, Quick-Add-Menü-Items mit
|
||||
doppeltem Namen.
|
||||
_Axe-core accessibility audit now clean of serious/critical
|
||||
violations on `/dashboard`, `/settings/integrations`, and `/admin`
|
||||
(incl. `/admin/system-status`, `/admin/users`). Fixes: missing
|
||||
aria-labels on icon-only buttons, unnamed mobile user-menu trigger,
|
||||
empty `<dd>` placeholders, sign-up link distinguishable only by
|
||||
color, quick-add menu with two indistinguishable items._
|
||||
- Extended end-to-end suite. New Playwright specs cover the authenticated
|
||||
dashboard render, the “add measurement” flow, the doctor-report PDF
|
||||
download, a mocked Codex connect flow, a mocked insights-generate flow,
|
||||
and a mobile-viewport smoke test (Pixel 5).
|
||||
- Axe-core accessibility audit now clean of serious/critical violations on
|
||||
`/dashboard`, `/settings/integrations`, and `/admin` (incl.
|
||||
`/admin/system-status`, `/admin/users`). Fixes: missing aria-labels on
|
||||
icon-only buttons, unnamed mobile user-menu trigger, empty `<dd>`
|
||||
placeholders, sign-up link distinguishable only by color, quick-add menu
|
||||
with two indistinguishable items.
|
||||
|
||||
### Docs
|
||||
|
||||
- **Dokumentations-Site auf v1.4.14 aktualisiert** — Codex-Device-
|
||||
Code-Pfad neu beschrieben, neue Admin-Sektionen (Backups, Users)
|
||||
dokumentiert, `CODEX_MODEL`-Env-Var aufgenommen,
|
||||
Dashboard-Performance-Hinweis ergänzt, Codex-Troubleshooting-Block
|
||||
überarbeitet.
|
||||
_Documentation site brought up to date with v1.4.14 — Codex
|
||||
device-code flow rewritten, new Admin sections (Backups, Users)
|
||||
documented, `CODEX_MODEL` env var added, dashboard performance note
|
||||
added, Codex troubleshooting block revised._
|
||||
- Documentation site brought up to date with v1.4.14 — Codex device-code
|
||||
flow rewritten, new Admin sections (Backups, Users) documented,
|
||||
`CODEX_MODEL` env var added, dashboard performance note added, Codex
|
||||
troubleshooting block revised.
|
||||
|
||||
## [1.4.13] — 2026-05-09
|
||||
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -78,7 +78,7 @@ pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build
|
||||
- **shadcn/ui** components (new-york style) for UI primitives.
|
||||
- **Dracula theme** -- dark mode is the default; a `light`/`system` toggle is exposed in the topbar. Chart and accent colors use the `--dracula-*` CSS tokens so both themes stay coherent.
|
||||
|
||||
See `CLAUDE.md` for detailed architecture and patterns.
|
||||
See `CONTRIBUTING-AI.md` for detailed architecture and patterns.
|
||||
|
||||
## Database Changes
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { join } from "node:path";
|
||||
*
|
||||
* 1. The dashboard page does NOT mount `<CompareToggle />`. The
|
||||
* comparison-baseline switch belongs on `/insights` only — the
|
||||
* dashboard tile-strip is too tight for it (Marc 2026-05-10).
|
||||
* dashboard tile-strip is too tight for it (maintainer probe 2026-05-10).
|
||||
* The underlying `compareBaseline` value is still consumed by
|
||||
* every dashboard chart; only the on-surface affordance is gone.
|
||||
* 2. The `/insights` page renders exactly ONE page-level refresh
|
||||
@@ -22,7 +22,7 @@ import { join } from "node:path";
|
||||
* 4. The `/insights` page does NOT pass the `aiOverviewTitle`
|
||||
* ("Persönlicher Berater") subtitle into `<InsightAdvisorCard>`.
|
||||
* The CardTitle ("KI-Gesundheitsanalyse") is sufficient framing;
|
||||
* the second subtitle was an empty-section placeholder Marc
|
||||
* the second subtitle was an empty-section placeholder the maintainer
|
||||
* called "Titel da aber NICHTS PASSIERT".
|
||||
* 5. The `stripChartTokens()` matcher catches lowercase + mixed-
|
||||
* case AI-emitted tokens (`metric:blood_pressure_sweet_spot`),
|
||||
@@ -150,7 +150,7 @@ describe("v1.4.19 A3 — /insights polish", () => {
|
||||
});
|
||||
|
||||
describe("v1.4.19 A3 — chart-token leak hardening", () => {
|
||||
// The visible "metric: blood_pressure_sweet" leak Marc saw at the
|
||||
// The visible "metric: blood_pressure_sweet" leak the maintainer saw at the
|
||||
// bottom of /insights was a model-emitted lowercase chart token
|
||||
// that the v1.4.16 strip regex (`/metric:[A-Z_]+/g`) didn't catch
|
||||
// because the character class only matches uppercase. The widened
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
/**
|
||||
* v1.4.19 phase A7 — Marc reported the `/targets` (Zielwerte) page
|
||||
* v1.4.19 phase A7 — the maintainer reported the `/targets` (Zielwerte) page
|
||||
* still surfaces English status labels even when the rest of the UI
|
||||
* renders in German: "Low / On Target / Stable / Moderate" stayed in
|
||||
* the source language. The card titles ("Weight", "Blood pressure",
|
||||
@@ -131,7 +131,7 @@ describe("/targets page — i18n status labels and titles", () => {
|
||||
|
||||
it("translates every status badge category to German", () => {
|
||||
const html = render("de");
|
||||
// Marc explicitly named these in his bug report.
|
||||
// the maintainer explicitly named these in his bug report.
|
||||
expect(html).not.toMatch(/>Low</);
|
||||
expect(html).not.toMatch(/>On target</);
|
||||
expect(html).not.toMatch(/>Stable</);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
/**
|
||||
* v1.4.19 phase A7 — `/targets` (Zielwerte) had `space-y-8` between
|
||||
* the page header (h1 + intro) and the first card grid. At Pixel-5
|
||||
* Marc reported "relativ viel Platz" wasted between the overview
|
||||
* the maintainer reported "relativ viel Platz" wasted between the overview
|
||||
* and the first values. Tighten the rhythm to `space-y-6` (24px)
|
||||
* which matches the rest of the admin / settings pages.
|
||||
*/
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("POST /api/ai/test — provider error leak guard", () => {
|
||||
// v1.4.5: credential failures map to 422 (not 502) so Cloudflare
|
||||
// doesn't replace our JSON body with its HTML error page — that
|
||||
// rewrite was the root cause of the "Unexpected token '<'…" client
|
||||
// crash Marc hit when typing a bad OpenAI key.
|
||||
// crash the maintainer hit when typing a bad OpenAI key.
|
||||
expect(response.status).toBe(422);
|
||||
expect(body.error ?? "").not.toMatch(/sk-/);
|
||||
expect(body.error ?? "").not.toMatch(/api\.openai\.com/);
|
||||
@@ -139,7 +139,7 @@ describe("POST /api/ai/test — provider error leak guard", () => {
|
||||
});
|
||||
|
||||
// v1.4 fix: the dropdown in /settings was not honoured — the test always
|
||||
// ran against the SAVED provider (Marc reported "wirft was Komisches raus").
|
||||
// ran against the SAVED provider (the maintainer reported "wirft was Komisches raus").
|
||||
// The route now accepts a JSON override body and forwards the user's
|
||||
// unsaved selection to resolveProviderForTest().
|
||||
describe("POST /api/ai/test — dropdown-aware override", () => {
|
||||
|
||||
@@ -95,7 +95,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
// rewritten by Cloudflare. 5xx from origin causes Cloudflare to swap
|
||||
// the body for its own HTML error page — `await res.json()` in the
|
||||
// browser then crashes with `Unexpected token '<', "<!DOCTYPE "`,
|
||||
// which is exactly how Marc encountered this when typing a bad
|
||||
// which is exactly how the maintainer encountered this when typing a bad
|
||||
// OpenAI key. 4xx codes pass through untouched.
|
||||
const status = err.httpStatus ?? 0;
|
||||
let outboundStatus: number;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* PUT /api/dashboard/chart-overlay-prefs — v1.4.18
|
||||
*
|
||||
* Persist a single chart's overlay-prefs (3 toggles) onto the existing
|
||||
* `User.dashboardWidgetsJson` blob. Marc rejected v1.4.16's always-on
|
||||
* `User.dashboardWidgetsJson` blob. the maintainer rejected v1.4.16's always-on
|
||||
* chart overlays (gradient fill, personal-baseline reference line,
|
||||
* target-zone shading); the new pattern is per-chart switches that the
|
||||
* user opts into independently and that persist per user.
|
||||
|
||||
@@ -27,7 +27,7 @@ import type { NextRequest } from "next/server";
|
||||
// Dashboard UI (`src/components/settings/dashboard-layout-section.tsx`
|
||||
// iterates the full layout from `DEFAULT_DASHBOARD_LAYOUT`). Missing
|
||||
// one here makes the PUT 422 silently — the toast surfaces "Layout
|
||||
// konnte nicht gespeichert werden" — and Marc's tile-toggle looks
|
||||
// konnte nicht gespeichert werden" — and the user's tile-toggle looks
|
||||
// like it does nothing because the save round-trip never completes.
|
||||
// v1.4.16 A5 root-cause: `achievements` was absent from this enum so
|
||||
// every save attempted with the achievements widget present (i.e.
|
||||
|
||||
@@ -223,7 +223,7 @@ describe("POST /api/insights/generate — provider error mapping", () => {
|
||||
});
|
||||
|
||||
// v1.4.16 A7.1: rate limit raised from 2 → 10/h, env-configurable.
|
||||
// Marc reported the previous 2/h was too aggressive when iterating on
|
||||
// the maintainer reported the previous 2/h was too aggressive when iterating on
|
||||
// settings. The 10/h ceiling is the new default; the env override lets
|
||||
// operators on a tight LLM budget dial it back without a rebuild.
|
||||
describe("POST /api/insights/generate — rate limit (v1.4.16 A7.1)", () => {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { prisma } from "@/lib/db";
|
||||
* comes back asynchronously when the container is healthy / fails to
|
||||
* start. Without this surface we lose visibility once the deploy is
|
||||
* fired — exactly the gap that produced v1.4.6..v1.4.14's manual
|
||||
* verification recipe (Marc had to SSH and check digests).
|
||||
* verification recipe (the maintainer had to SSH and check digests).
|
||||
*
|
||||
* Payload shape: Coolify sends a free-form JSON object whose exact
|
||||
* fields drift between beta releases. We treat it as `Record<string,
|
||||
@@ -104,7 +104,7 @@ function hasValidSecret(request: NextRequest): boolean {
|
||||
* `src/lib/integrations/status.ts` — fan out a SYSTEM_ALERT through the
|
||||
* existing dispatcher to every user whose role is ADMIN. The dispatcher
|
||||
* silently no-ops when no Telegram channel is configured, so this is
|
||||
* safe even on a first-deploy where Marc hasn't wired the bot yet.
|
||||
* safe even on a first-deploy where the maintainer hasn't wired the bot yet.
|
||||
*/
|
||||
async function notifyAdminsOfFailure(event: NormalizedEvent): Promise<void> {
|
||||
const admins = await prisma.user.findMany({
|
||||
|
||||
+1
-1
@@ -204,7 +204,7 @@
|
||||
* bar. Used by the mobile section strips in `<AdminShell>` and
|
||||
* `<SettingsShell>` so 13 admin / 10 settings entries can swipe
|
||||
* horizontally without painting an always-on scrollbar at the top of
|
||||
* every admin/settings page (a recurring Marc complaint — most
|
||||
* every admin/settings page (a recurring maintainer complaint — most
|
||||
* recently mistaken for a `/admin/api-tokens` table scrollbar in
|
||||
* v1.4.16/17 because the strip sits right above the api-tokens card).
|
||||
*
|
||||
|
||||
+7
-7
@@ -321,7 +321,7 @@ export default function DashboardPage() {
|
||||
// v1.4.15 phase-B4 — recent unlocks dashboard surface. The card itself
|
||||
// self-handles the empty state (CTA → /achievements), so we only need
|
||||
// the layout-toggle gate here. No data-floor check (the empty card is
|
||||
// intentional — Marc wants the user to discover the feature).
|
||||
// intentional — the maintainer wants the user to discover the feature).
|
||||
const showAchievementsCard = isChartVisible("achievements");
|
||||
// v1.4.16 phase D reconcile (CRITICAL C2) — gate the dashboard
|
||||
// insights preview by the layout toggle. The component itself
|
||||
@@ -591,7 +591,7 @@ export default function DashboardPage() {
|
||||
// BP is conceptually one signal but visually two tiles (sys + dia)
|
||||
// so the user sees both numbers side by side at the same size as
|
||||
// every other tile in the strip. v1.4.3 first attempted a
|
||||
// combined tile with `secondary` values — Marc preferred two
|
||||
// combined tile with `secondary` values — the maintainer preferred two
|
||||
// distinct tiles with consistent symmetric widths.
|
||||
trendCards.push({
|
||||
id: "bp-sys",
|
||||
@@ -1083,7 +1083,7 @@ export default function DashboardPage() {
|
||||
// v1.4.15 phase-B4 — slotted at the user's chosen position via
|
||||
// the layout `order`. Default order from
|
||||
// `DEFAULT_DASHBOARD_LAYOUT` puts it last (below the chart row)
|
||||
// which matches Marc's brief "below the chart row".
|
||||
// which matches the maintainer's brief "below the chart row".
|
||||
charts.push({
|
||||
id: "achievements",
|
||||
order: widgetOrder("achievements"),
|
||||
@@ -1125,7 +1125,7 @@ export default function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
{/* v1.4: dashboard tiles are *always* a single row.
|
||||
* Marc-explicit (memory feedback_dashboard_one_row.md): a 2-row
|
||||
* maintainer-explicit (per feedback_dashboard_one_row.md): a 2-row
|
||||
* tile strip breaks the visual hierarchy and reads like an
|
||||
* Excel grid. Total width caps at the parent container —
|
||||
* exactly the chart-width below. When the active tile count
|
||||
@@ -1140,9 +1140,9 @@ export default function DashboardPage() {
|
||||
{/* v1.4.16 Fix A5: hide the strip entirely when the user
|
||||
turned off every tile. Until v1.4.15 the wrapper rendered
|
||||
an empty grid even with zero tiles — visually a thin gap
|
||||
Marc described as "awkward". Charts below still render so
|
||||
the maintainer described as "awkward". Charts below still render so
|
||||
the page is not empty; the tile-strip just goes away.
|
||||
The constraint Marc named — "immer die gesamte Spalte
|
||||
The constraint the maintainer named — "immer die gesamte Spalte
|
||||
breit und immer der gleichen Höhe" — is preserved by the
|
||||
CSS-grid `auto-fit + minmax + auto-rows-fr` track that
|
||||
continues to give every visible tile equal width / equal
|
||||
@@ -1156,7 +1156,7 @@ export default function DashboardPage() {
|
||||
// x-coordinates as the charts below because both inherit the
|
||||
// same parent container. When the row no longer fits a 9rem
|
||||
// floor, the grid wraps to a new row instead of horizontal-
|
||||
// scrolling — Marc tested both and prefers the v1.3-era
|
||||
// scrolling — the maintainer tested both and prefers the v1.3-era
|
||||
// wrap behaviour over the one-row scroll for the symmetry
|
||||
// it preserves.
|
||||
className="grid auto-rows-fr [grid-template-columns:repeat(auto-fit,minmax(9rem,1fr))] gap-3 pb-2"
|
||||
|
||||
@@ -117,7 +117,7 @@ const GLUCOSE_TYPES = new Set([
|
||||
* pulse-targets.ts + targets/route.ts inline maps); v1.4.19 phase
|
||||
* A7 wires this map so the German locale stops surfacing English
|
||||
* status pills like "Low / On Target / Stable / Moderate" — the
|
||||
* exact regression Marc reported.
|
||||
* exact regression the maintainer reported.
|
||||
*/
|
||||
const STATUS_CATEGORY_KEY: Record<string, string> = {
|
||||
// BMI
|
||||
@@ -735,7 +735,7 @@ export default function TargetsPage() {
|
||||
});
|
||||
|
||||
return (
|
||||
// v1.4.19 phase A7 — Marc reported "relativ viel Platz" wasted
|
||||
// v1.4.19 phase A7 — the maintainer reported "relativ viel Platz" wasted
|
||||
// between the overview header and the first values on `/targets`.
|
||||
// Tighten the rhythm from `space-y-8` (32 px) to `space-y-6`
|
||||
// (24 px) — that matches the admin / settings pages.
|
||||
|
||||
@@ -7,7 +7,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
* is always wider than a 393 CSS-px (Pixel-5) viewport, so v1.4.18
|
||||
* phase A2 added the `no-scrollbar` class to the strip — Chromium /
|
||||
* WebKit otherwise paint an always-on horizontal scrollbar at the top
|
||||
* of every admin page, which Marc kept mis-attributing to the
|
||||
* of every admin page, which the maintainer kept mis-attributing to the
|
||||
* `/admin/api-tokens` table card right below it. Three previous fix
|
||||
* attempts (v1.4.15 column-hide, v1.4.16 mobile-card-list, then this
|
||||
* one) all targeted the table while the bar lived in the shell.
|
||||
|
||||
@@ -7,7 +7,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
* v1.4 shared-admin page where 13 sections lived on one route; in
|
||||
* v1.5 every section has its own dedicated route, so collapsing the
|
||||
* only card on `/admin/api-tokens` hides the entire surface and
|
||||
* confused Marc more than it helped.
|
||||
* confused the maintainer more than it helped.
|
||||
*/
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("ApiTokenOverviewSection — responsive", () => {
|
||||
it("mobile card-list has no overflow-x-auto wrapper", () => {
|
||||
const html = render();
|
||||
// Mobile cards must let content wrap within the card rather than
|
||||
// forcing a scroll container — that was the v1.4.15 bug Marc
|
||||
// forcing a scroll container — that was the v1.4.15 bug the maintainer
|
||||
// re-reported. Confirm there's no overflow-x-auto INSIDE the
|
||||
// mobile <ul>.
|
||||
const mobileMatch = html.match(/<ul[^>]*md:hidden[^>]*>([\s\S]*?)<\/ul>/);
|
||||
|
||||
@@ -4,13 +4,13 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
/**
|
||||
* v1.4.19 A7 — `/admin/api-tokens` 4th-attempt scrollbar fix.
|
||||
*
|
||||
* Marc reported a "minimaler Scrollbar" still painting at the
|
||||
* the maintainer reported a "minimaler Scrollbar" still painting at the
|
||||
* bottom-right of the api-tokens page, even after the v1.4.18 phase
|
||||
* A2 mobile-strip `no-scrollbar` fix. The remaining culprits are
|
||||
* inside the section card itself: long token names, long usernames,
|
||||
* and long permission strings can each push their parent past the
|
||||
* viewport. Cumulatively this leaves the page in an "almost-fits"
|
||||
* state where the visible scrollbar Marc sees is the page-level main
|
||||
* state where the visible scrollbar the maintainer sees is the page-level main
|
||||
* scroll picking up a fraction of pixels of horizontal scroll spill
|
||||
* from the children.
|
||||
*
|
||||
|
||||
@@ -3,7 +3,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
/**
|
||||
* v1.4.15 phase A2: the `/admin` overview no longer renders the
|
||||
* `StatusCardGrid` (the section-card grid Marc described as redundant
|
||||
* `StatusCardGrid` (the section-card grid the maintainer described as redundant
|
||||
* with the sidebar nav). The status-card component itself was removed
|
||||
* from the codebase. This test guards the *new* overview composition:
|
||||
* a welcome card, the system snapshot, and the recent-audit preview.
|
||||
|
||||
@@ -153,7 +153,7 @@ export function AdminShell({ active, children }: AdminShellProps) {
|
||||
painted scrollbar at the top of every admin page; the
|
||||
horizontal swipe + keyboard arrow scrolling still work, the
|
||||
bar just doesn't draw. Without this the 13-section strip
|
||||
would render an always-on scrollbar that Marc kept
|
||||
would render an always-on scrollbar that the maintainer kept
|
||||
mis-attributing to the api-tokens table below it. */}
|
||||
<nav
|
||||
aria-label={t("admin.shell.sectionsNav")}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { type ApiTokenInfo } from "./_shared";
|
||||
*
|
||||
* v1.4.19 phase A7 (4th attempt) — earlier fixes (column-hide,
|
||||
* mobile card-list, mobile-strip `no-scrollbar`) cleared every
|
||||
* scrollbar Marc reported except a residual painted bar he kept
|
||||
* scrollbar the maintainer reported except a residual painted bar he kept
|
||||
* seeing at the bottom-right. Production probe confirmed no
|
||||
* remaining horizontal-overflow culprits, but the cards still
|
||||
* carried `break-all` next to `truncate` (dead code; `white-space:
|
||||
@@ -104,7 +104,7 @@ export function ApiTokenOverviewSection() {
|
||||
// section route. The previous pattern carried a "Collapse / Expand"
|
||||
// toggle as an escape hatch from the v1.4 shared admin page where
|
||||
// 13 sections lived together, but on a route that only renders this
|
||||
// one card the toggle hides the entire surface. Marc reported it
|
||||
// one card the toggle hides the entire surface. the maintainer reported it
|
||||
// as "sinnlos" — gone.
|
||||
const { data: tokens, isLoading } = useQuery({
|
||||
queryKey: ["admin", "tokens"],
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
/**
|
||||
* v1.4.18 — per-chart overlay-controls popover.
|
||||
*
|
||||
* Marc rejected the always-on overlays from B1a (gradient, baseline,
|
||||
* the maintainer rejected the always-on overlays from B1a (gradient, baseline,
|
||||
* target-zone shading) and asked for a per-chart switch surface so a
|
||||
* user can toggle "7-Tage-Trend / Trend-Pfeil / Zielbereich" on or off
|
||||
* for each chart independently. The popover is anchored top-right of
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
||||
/**
|
||||
* v1.4.18 — Default overlay state on chart wrappers.
|
||||
*
|
||||
* Marc rejected the always-on personal-baseline reference line that B1a
|
||||
* the maintainer rejected the always-on personal-baseline reference line that B1a
|
||||
* painted on every chart. The default surface for a new chart should be
|
||||
* a clean line + axes + tooltip — no baseline, no target-zone shading,
|
||||
* no trend line. Overlays are user-opt-in via the per-chart settings
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("computePersonalBaseline()", () => {
|
||||
|
||||
describe("<HealthChart> v1.4.18 clean-line revert", () => {
|
||||
it("does NOT paint a gradient fill under the line (clean line only)", async () => {
|
||||
// v1.4.18 reverts B1a's gradient-area treatment. Marc explicitly
|
||||
// v1.4.18 reverts B1a's gradient-area treatment. the maintainer explicitly
|
||||
// rejected the soft-color fill below the line: the line itself is
|
||||
// the chart, no painted background under it. We assert the SSR
|
||||
// output ships no `<linearGradient>` defs and no `chart-gradient`
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
||||
/**
|
||||
* v1.4.18 — MoodChart clean-line revert (gradient + emoji rolled back).
|
||||
*
|
||||
* Marc rejected B1a's coloured area fill below the chart line and the
|
||||
* the maintainer rejected B1a's coloured area fill below the chart line and the
|
||||
* smiley/emoji glyphs at every data point. The line stroke + Dracula
|
||||
* tokens stay; the chart now paints a clean monotone line with simple
|
||||
* Recharts dots.
|
||||
@@ -67,7 +67,7 @@ describe("<MoodChart> v1.4.18 clean-line revert", () => {
|
||||
});
|
||||
|
||||
it("does NOT render emoji glyphs at data points (source check)", async () => {
|
||||
// Marc explicitly rejected smileys, especially in the mood chart.
|
||||
// the maintainer explicitly rejected smileys, especially in the mood chart.
|
||||
// Recharts does not paint <Line> dots during SSR (the dot callback
|
||||
// only fires after first client render), so the emoji wouldn't
|
||||
// surface in renderToStaticMarkup output anyway. Inspecting the
|
||||
|
||||
@@ -7,7 +7,7 @@ import { join } from "node:path";
|
||||
* chart in a `<ResponsiveContainer>` whose default
|
||||
* `touch-action: auto` makes the browser wait for the chart's JS
|
||||
* touchmove handler before starting page scroll. On slow devices that
|
||||
* causes Marc's "Scrolling-Hänger im Dashboard auf Charts".
|
||||
* causes the maintainer's "Scrolling-Hänger im Dashboard auf Charts".
|
||||
*
|
||||
* Fix: every chart's wrapper `<div>` (the one that contains the
|
||||
* `<ResponsiveContainer>`) must carry the Tailwind `touch-pan-y`
|
||||
|
||||
@@ -9,7 +9,7 @@ import { I18nProvider } from "@/lib/i18n/context";
|
||||
* v1.4.15 Fix 4 — "7-Tage-Schnitt" → "7-Tage-Trend".
|
||||
*
|
||||
* The avg7 line on every dashboard tile used to read "7d: 80.5" /
|
||||
* "7T: 80,5" — visually an *average*. Marc wanted it to convey
|
||||
* "7T: 80,5" — visually an *average*. the maintainer wanted it to convey
|
||||
* *direction* over the past week instead, so the label flips to
|
||||
* "7d trend" / "7T-Trend" and a signed delta indicator paints next
|
||||
* to the value with metric-aware colour:
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LineChart as LineChartIcon } from "lucide-react";
|
||||
/**
|
||||
* Sparse-data empty-state for chart wrappers (B1a v1.4.16).
|
||||
*
|
||||
* Per Marc's research §1A — "personal-baseline framing first" — when a
|
||||
* Per the maintainer's research §1A — "personal-baseline framing first" — when a
|
||||
* user has only 1-2 data points there's nothing useful to plot, so we
|
||||
* paint a friendly hint instead of a near-empty canvas. The hint reuses
|
||||
* the `lucide-react/LineChart` glyph the rest of the app uses for chart
|
||||
|
||||
@@ -19,7 +19,7 @@ import { type ChartOverlayPrefs } from "@/lib/dashboard-layout";
|
||||
/**
|
||||
* v1.4.18 — per-chart overlay-controls popover.
|
||||
*
|
||||
* Marc rolled back B1a's always-on chart overlays (gradient fills,
|
||||
* the maintainer rolled back B1a's always-on chart overlays (gradient fills,
|
||||
* personal-baseline reference line, target-zone shading) and asked for
|
||||
* a per-chart switch surface instead. Each dashboard chart card mounts
|
||||
* one of these in its top-right corner (a settings-cog dropdown) so the
|
||||
@@ -30,7 +30,7 @@ import { type ChartOverlayPrefs } from "@/lib/dashboard-layout";
|
||||
* - showTrendIndicator — the "7-day trend" overlay (moving average +
|
||||
* numeric Δ chip in the header)
|
||||
* - showTrendArrow — the trend regression line + arrow direction
|
||||
* hint + personal-baseline reference line. Marc's rule: the mean
|
||||
* hint + personal-baseline reference line. the maintainer's rule: the mean
|
||||
* line only paints when a trend is being displayed, so we fold
|
||||
* the baseline into this toggle.
|
||||
* - showTargetRange — the target-zone shading + reference lines
|
||||
|
||||
@@ -842,7 +842,7 @@ export function HealthChart({
|
||||
|
||||
// v1.4.16 Fix A8b: when the visible window is long (e.g. "All"
|
||||
// on a multi-year account), the per-week delta becomes vanishingly
|
||||
// small and rounds to ±0.0 in the formatter — Marc's complaint
|
||||
// small and rounds to ±0.0 in the formatter — the maintainer's complaint
|
||||
// "Wenn ich alle anklicke, dann wird einfach Null Veränderung
|
||||
// angezeigt" was exactly this: a slope of ~0.5 kg/year prints
|
||||
// as "+0.0 kg/week" and the user reads it as "no change". For
|
||||
@@ -1194,7 +1194,7 @@ export function HealthChart({
|
||||
/>
|
||||
))}
|
||||
{/* v1.4.18 — personal-baseline reference line is now
|
||||
opt-in via the Trend toggle. Marc rejected the
|
||||
opt-in via the Trend toggle. the maintainer rejected the
|
||||
always-on dashed mean line; it now only paints when
|
||||
the user actively shows the trend overlay (matching
|
||||
his rule: "only when a trend is being displayed").
|
||||
|
||||
@@ -475,7 +475,7 @@ export function MoodChart({
|
||||
5: t("charts.moodLabel5"),
|
||||
};
|
||||
|
||||
// v1.4.18 — emoji glyph map removed. Marc explicitly rejected
|
||||
// v1.4.18 — emoji glyph map removed. the maintainer explicitly rejected
|
||||
// smileys in the mood chart; the line now uses plain Recharts dots
|
||||
// and the y-axis already labels each integer (very low / low / okay
|
||||
// / good / great) so the chart is fully scannable without a glyph.
|
||||
@@ -632,7 +632,7 @@ export function MoodChart({
|
||||
/>
|
||||
))}
|
||||
{/* v1.4.18 — personal-baseline gated behind the
|
||||
Trend toggle (Marc: "only when a trend is being
|
||||
Trend toggle (maintainer: "only when a trend is being
|
||||
displayed"). Default OFF. */}
|
||||
{showTrend && personalBaseline != null && (
|
||||
<ReferenceLine
|
||||
|
||||
@@ -192,7 +192,7 @@ export function TrendCard({
|
||||
};
|
||||
|
||||
// The label flips from "7d" / "7T" (mean) to "7d trend" / "7T-Trend"
|
||||
// when the call site supplies a delta. Marc's v1.4.15 feedback —
|
||||
// when the call site supplies a delta. the maintainer's v1.4.15 feedback —
|
||||
// "7-Tage-Schnitt" sounds like an average, but the value next to it
|
||||
// is now a TREND. Distinct keys keep the label change in i18n.
|
||||
const avg7LabelKey =
|
||||
|
||||
@@ -266,7 +266,7 @@ describe("<InsightAdvisorCard> — polished loading / empty / error states (B1b)
|
||||
});
|
||||
|
||||
it("renders the legacy v1.4.14 payload without crashing (regenerate CTA)", () => {
|
||||
// Reproduces Marc's production /insights crash on 2026-05-10:
|
||||
// Reproduces the maintainer's production /insights crash on 2026-05-10:
|
||||
//
|
||||
// TypeError: Cannot read properties of undefined (reading 'replace')
|
||||
// at stripChartTokens(insight.summary)
|
||||
|
||||
@@ -247,7 +247,7 @@ describe("<RecommendationCard>", () => {
|
||||
/**
|
||||
* v1.4.22 A6 — strip chart-tokens from the always-visible
|
||||
* recommendation text. W1a §3 traced the production "metric:WEIGHT"
|
||||
* leak Marc saw at the bottom of the advisor card to this exact
|
||||
* leak the maintainer saw at the bottom of the advisor card to this exact
|
||||
* render path: every other AI-prose surface ran through
|
||||
* `stripChartTokens()`, the recommendation text didn't. Pin the
|
||||
* strip so a future copy-paste regression can't reintroduce the
|
||||
|
||||
@@ -85,7 +85,7 @@ describe("<SidebarNav> bug-report toggle", () => {
|
||||
});
|
||||
|
||||
describe("<SidebarNav> admin entry mirrors Settings (no sub-item expansion)", () => {
|
||||
// v1.4.16 A1: Marc reported the global sidebar expanding admin
|
||||
// v1.4.16 A1: the maintainer reported the global sidebar expanding admin
|
||||
// sub-items on `/admin/*` was unwanted UX — the in-shell `<AdminShell>`
|
||||
// already provides per-section nav inside the page. The Admin entry
|
||||
// must behave EXACTLY like the Settings entry: a single link with no
|
||||
@@ -112,7 +112,7 @@ describe("<SidebarNav> admin entry mirrors Settings (no sub-item expansion)", ()
|
||||
const html = render({ role: "ADMIN", pathname: "/admin/system-status" });
|
||||
expect(html).toContain('href="/admin"');
|
||||
// Sub-section links must NOT appear in the global sidebar — they
|
||||
// belong to `<AdminShell>`'s in-page nav. Marc reported the
|
||||
// belong to `<AdminShell>`'s in-page nav. the maintainer reported the
|
||||
// expansion as broken UX in v1.4.16; this guard keeps it gone.
|
||||
for (const section of ADMIN_SECTIONS) {
|
||||
expect(html).not.toContain(`href="/admin/${section.slug}"`);
|
||||
|
||||
@@ -419,7 +419,7 @@ export function OnboardingTour({
|
||||
|
||||
{/* v1.4.15 H4 design: tour footer buttons reach the WCAG 2.5.5
|
||||
44 px tap-target floor on mobile. The `size="sm"` default
|
||||
of `h-8` clipped to ~32 px which Marc reported as too small
|
||||
of `h-8` clipped to ~32 px which the maintainer reported as too small
|
||||
on the iPad / iPhone PWA shell. Bumping to `min-h-11` (44 px)
|
||||
keeps the desktop visual close enough — the buttons grow
|
||||
~4 px taller — while making mobile usable. */}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* v1.4.16 phase B2 — AI provider settings UX (dropdown-driven).
|
||||
*
|
||||
* The new section replaces the v1.4.15-vintage top/bottom split (Marc's
|
||||
* The new section replaces the v1.4.15-vintage top/bottom split (the maintainer's
|
||||
* `feedback_settings_no_split.md`): a single Select picks the active
|
||||
* provider; the form below is switch-rendered for that provider; the
|
||||
* fallback chain at the bottom reorders / toggles entries with arrow
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* v1.4.19 phase A5 — IntegrationStatusPill
|
||||
*
|
||||
* The single tag rendered top-right of every integration card. The
|
||||
* pill is the ONLY place a connection status surfaces — Marc was
|
||||
* pill is the ONLY place a connection status surfaces — the maintainer was
|
||||
* staring at three- to four-fold redundancy across Withings + Mood Log
|
||||
* cards in v1.4.18 and called it out. This component is reusable so
|
||||
* v1.4.20 can drop the same pill on the Apple Health card.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* v1.4.19 Phase A5 — Settings → Integrations status consolidation.
|
||||
*
|
||||
* Marc's verbatim concern (paraphrased):
|
||||
* the maintainer's verbatim concern (paraphrased):
|
||||
* - Withings card had 3 status displays — top-right "connected"
|
||||
* badge, mid-card "connected / last successful / last attempt"
|
||||
* trio container, and the badge inside that container.
|
||||
@@ -22,7 +22,7 @@
|
||||
* 4. Mood Log disconnected: pill says "Not connected", no other
|
||||
* status display surfaces.
|
||||
* 5. Mood Log card has the visual divider that Withings always had
|
||||
* — consistency Marc explicitly called out.
|
||||
* — consistency the maintainer explicitly called out.
|
||||
*/
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
@@ -219,7 +219,7 @@ describe("IntegrationsSection — single-status-display contract (A5)", () => {
|
||||
expect(html).not.toContain("Last attempt");
|
||||
});
|
||||
|
||||
it("Withings reauth state surfaces 'Error — reconnect' on the pill (Marc's wording)", () => {
|
||||
it("Withings reauth state surfaces 'Error — reconnect' on the pill (the maintainer's wording)", () => {
|
||||
setIntegrationStatus({
|
||||
threshold: 3,
|
||||
integrations: [
|
||||
|
||||
@@ -266,7 +266,7 @@ export function AboutSection() {
|
||||
that proxies through `/api/version/check-updates`. The v1.4.2
|
||||
version called `api.github.com` directly from the browser, which
|
||||
the production CSP blocked silently — that's why "nothing
|
||||
happened" when Marc clicked the button. */}
|
||||
happened" when the user clicked the button. */}
|
||||
{version && (
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
|
||||
@@ -168,7 +168,7 @@ export function AiSection() {
|
||||
/**
|
||||
* The single dropdown-driven configuration card. Replaces the
|
||||
* v1.4.15-vintage two-card layout (Codex connect at the top, personal
|
||||
* provider form at the bottom) per Marc's
|
||||
* provider form at the bottom) per the maintainer's
|
||||
* `feedback_settings_no_split.md`: one provider Select, one matching
|
||||
* form below, one fallback-chain card at the bottom.
|
||||
*
|
||||
@@ -389,7 +389,7 @@ function ActiveProviderSelect({
|
||||
|
||||
/**
|
||||
* Switch-rendered provider configuration. The component is the
|
||||
* "form below adapts dynamically" half of Marc's pulldown contract:
|
||||
* "form below adapts dynamically" half of the maintainer's pulldown contract:
|
||||
* pick Codex → see Codex form; pick OpenAI → see API-key + model
|
||||
* dropdown + base-URL override. The card frame is the same in every
|
||||
* branch so transitions feel like updating a single concept rather
|
||||
|
||||
@@ -114,7 +114,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
/**
|
||||
* v1.4.15 Fix 5 — independent toggle for the *strip tile* (the upper
|
||||
* row of trend cards). Until v1.4.14 a single switch controlled both
|
||||
* the tile AND the chart for the same metric, which Marc found too
|
||||
* the tile AND the chart for the same metric, which the maintainer found too
|
||||
* coarse: he wanted a chart visible without the tile (for metrics he
|
||||
* tracks without wanting the at-a-glance number) or vice versa.
|
||||
*/
|
||||
@@ -250,8 +250,8 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
{/* v1.4.15 Fix 5 — table-style header naming the two
|
||||
switches. The "tile" column controls the strip tile in
|
||||
the upper row; the "chart" column controls the line
|
||||
chart in the lower row. Marc wanted independent control
|
||||
of the two surfaces (memory feedback_dashboard_top_tiles
|
||||
chart in the lower row. the maintainer wanted independent control
|
||||
of the two surfaces (per feedback_dashboard_top_tiles
|
||||
_selectable.md). */}
|
||||
<div className="text-muted-foreground flex items-center gap-3 px-3 pb-1 text-[10px] font-medium tracking-wide uppercase">
|
||||
<span className="w-5" aria-hidden="true" />
|
||||
|
||||
@@ -9,7 +9,7 @@ import { cn } from "@/lib/utils";
|
||||
/**
|
||||
* v1.4.19 phase A5 — single status pill rendered top-right of every
|
||||
* integration card (Withings, Mood Log, Apple Health…). It replaces
|
||||
* the three- to four-fold redundant status surface Marc spotted in
|
||||
* the three- to four-fold redundant status surface the maintainer spotted in
|
||||
* v1.4.18:
|
||||
*
|
||||
* - top-right "Connected" badge
|
||||
@@ -17,7 +17,7 @@ import { cn } from "@/lib/utils";
|
||||
* - bottom-of-card "letzter Sync" line
|
||||
*
|
||||
* Now there is exactly ONE place a sync status can appear on a card
|
||||
* and it follows the pattern Marc liked from the Withings header: a
|
||||
* and it follows the pattern the maintainer liked from the Withings header: a
|
||||
* Dracula-tokenized chip with the relative time inline.
|
||||
*
|
||||
* The component is locale-aware (EN + DE) and mobile-safe: on a
|
||||
|
||||
@@ -136,7 +136,7 @@ function pillStateFor(
|
||||
* sync attempt failed. The pill conveys "something is wrong"; this
|
||||
* line tells the user *what* is wrong so they can act on it. Keeping
|
||||
* it deliberately small (one icon + one line) so it doesn't recreate
|
||||
* the v1.4.18 redundant banner Marc removed.
|
||||
* the v1.4.18 redundant banner the maintainer removed.
|
||||
*/
|
||||
function IntegrationErrorMessage({ message }: { message: string }) {
|
||||
return (
|
||||
@@ -653,7 +653,7 @@ function MoodLogCard({
|
||||
</p>
|
||||
|
||||
{/* v1.4.19 A5 — visual divider matches Withings for consistency
|
||||
(Marc explicitly called the asymmetry out). */}
|
||||
(the maintainer explicitly called the asymmetry out). */}
|
||||
<hr
|
||||
data-testid="integration-card-divider"
|
||||
className="border-border/60 mt-4"
|
||||
@@ -736,7 +736,7 @@ function MoodLogCard({
|
||||
{/* v1.4.19 A5 — The "letzter Sync" line that used to
|
||||
live here is gone. The pill in the card header
|
||||
already carries that information; repeating it
|
||||
inside the body is the redundancy Marc flagged. The
|
||||
inside the body is the redundancy the maintainer flagged. The
|
||||
entry-count is the only number we still surface
|
||||
because it's an integration-specific datapoint the
|
||||
pill cannot convey. */}
|
||||
|
||||
@@ -258,7 +258,7 @@ function ChannelRow({
|
||||
|
||||
{/* v1.4.15 H4 design: action buttons hit the WCAG 2.5.5 44 px
|
||||
floor on mobile via `min-h-11`. Settings is the most-touched
|
||||
mobile surface; `size="sm"` here clips to ~32 px which Marc
|
||||
mobile surface; `size="sm"` here clips to ~32 px which the maintainer
|
||||
flagged as too small on the iPad/iPhone PWA shell. */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{channel.state === "auto_disabled" && (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Tabs, TabsList, TabsTrigger } from "../tabs";
|
||||
|
||||
describe("<TabsList>", () => {
|
||||
it("clamps the y-axis so a fixed-height strip never paints a vertical scrollbar", () => {
|
||||
// Marc reported a stray "mini scroll-button" on the right side of
|
||||
// the maintainer reported a stray "mini scroll-button" on the right side of
|
||||
// the feedback inbox tab strip (Open / Acknowledged / Resolved /
|
||||
// Archived). Root cause: `overflow-x-auto` couples to `overflow-y`
|
||||
// — once one axis is `auto`, the other can no longer be `visible`,
|
||||
|
||||
@@ -42,7 +42,7 @@ function Tabs({
|
||||
// ~1 px taller than the strip on some glyph stacks, a tiny painted
|
||||
// vertical scrollbar appeared on the right edge of short strips
|
||||
// (e.g. the admin feedback `Open / Acknowledged / Resolved /
|
||||
// Archived` tabs Marc reported in v1.4.19). Locking the y-axis hides
|
||||
// Archived` tabs the maintainer reported in v1.4.19). Locking the y-axis hides
|
||||
// that bar without affecting horizontal swipe behaviour.
|
||||
// Padding sits at `p-1` (4 px) so the 3 px focus-visible ring
|
||||
// (`focus-visible:ring-[3px]`) on triggers stays inside the
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
*
|
||||
* Until v1.4.14 the layout schema had a single `visible` flag per
|
||||
* widget that controlled BOTH the upper-row strip tile AND the lower-
|
||||
* row chart for the same metric. Marc wanted them as independent
|
||||
* row chart for the same metric. the maintainer wanted them as independent
|
||||
* toggles so a user could keep the chart on for tracking but hide
|
||||
* the tile (or vice versa). v1.4.15 introduces an optional
|
||||
* `tileVisible` field; the resolver mirrors it from `visible` for
|
||||
@@ -120,7 +120,7 @@ describe("serializeDashboardLayout() — tileVisible persistence", () => {
|
||||
/**
|
||||
* v1.4.16 Fix A5 — top-tile selector real-fix.
|
||||
*
|
||||
* Marc reported the toggles in Settings → Dashboard didn't filter
|
||||
* the maintainer reported the toggles in Settings → Dashboard didn't filter
|
||||
* the dashboard tile-strip. Root cause: the `widgetIdEnum` Zod schema
|
||||
* in `src/app/api/dashboard/widgets/route.ts` was missing
|
||||
* `achievements` even though the default layout has it. Every PUT
|
||||
@@ -132,7 +132,7 @@ describe("serializeDashboardLayout() — tileVisible persistence", () => {
|
||||
* `DEFAULT_DASHBOARD_LAYOUT.widgets`, every consumer (settings UI,
|
||||
* dashboard renderer, API schema) must support it. This test pins
|
||||
* the contract by enumerating every widget the default layout
|
||||
* advertises, so a future addition gets caught here before Marc
|
||||
* advertises, so a future addition gets caught here before the maintainer
|
||||
* finds it broken in production.
|
||||
*/
|
||||
describe("DEFAULT_DASHBOARD_LAYOUT contract", () => {
|
||||
@@ -154,7 +154,7 @@ describe("DEFAULT_DASHBOARD_LAYOUT contract", () => {
|
||||
// The API persists `serializeDashboardLayout(parsed.data)` and
|
||||
// reads back via `resolveDashboardLayout(row.dashboardWidgetsJson)`.
|
||||
// A bug in either step (missing widget, wrong order normalization)
|
||||
// would surface here. Marc's regression: the API schema rejected
|
||||
// would surface here. the maintainer's regression: the API schema rejected
|
||||
// the layout *before* this round-trip even started.
|
||||
const serialized = serializeDashboardLayout(DEFAULT_DASHBOARD_LAYOUT);
|
||||
const resolved = resolveDashboardLayout(serialized);
|
||||
@@ -218,7 +218,7 @@ describe("resolveDashboardLayout() — comparisonBaseline (B8)", () => {
|
||||
/**
|
||||
* v1.4.18 — per-chart overlay-prefs persistence.
|
||||
*
|
||||
* Marc reverted v1.4.16's always-on chart overlays (gradient, baseline,
|
||||
* the maintainer reverted v1.4.16's always-on chart overlays (gradient, baseline,
|
||||
* target-zone shading) and asked for per-chart switches that persist
|
||||
* per user. The prefs piggy-back on `User.dashboardWidgetsJson` so we
|
||||
* stay migration-free, mirroring the B8 comparisonBaseline pattern.
|
||||
|
||||
@@ -104,7 +104,7 @@ describe("lookupIpLocation IP-geolocation HTTPS guard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// v1.4.16 A8a: Marc spotted "Nrnberg" rendered in /admin/login-overview
|
||||
// v1.4.16 A8a: the maintainer spotted "Nrnberg" rendered in /admin/login-overview
|
||||
// for an audit row that should have read "Nürnberg, DE". The geo source
|
||||
// (ipwho.is) returns valid UTF-8, but `Response.json()` defers its
|
||||
// charset decoding to whatever the server's `Content-Type` header
|
||||
|
||||
@@ -12,7 +12,7 @@ import { MockAIProvider } from "../mock-client";
|
||||
/**
|
||||
* Phase C1 — citation-from-data hard requirement.
|
||||
*
|
||||
* Marc, verbatim 2026-05-09: "es muss sich halt irgendwie stützen auf
|
||||
* the maintainer, verbatim 2026-05-09: "es muss sich halt irgendwie stützen auf
|
||||
* medizinische Dinge." ("It must ground on medical facts / user data.")
|
||||
*
|
||||
* Operational meaning:
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
/**
|
||||
* Phase C1 — slug-drift defence.
|
||||
*
|
||||
* Marc, verbatim 2026-05-09: "Die Integration des Slug Drift Risiko
|
||||
* the maintainer, verbatim 2026-05-09: "Die Integration des Slug Drift Risiko
|
||||
* darf halt immer überhaupt nicht sein."
|
||||
* ("Slug-drift risk must never happen.")
|
||||
*
|
||||
|
||||
@@ -10,7 +10,7 @@ import { MockAIProvider } from "../mock-client";
|
||||
* recommendation and overwrites `rec.confidence` with the value
|
||||
* `computeConfidence()` produces.
|
||||
*
|
||||
* Default-on per research §7 question 1 (Marc-acked). The override
|
||||
* Default-on per research §7 question 1 (maintainer-acked). The override
|
||||
* happens whether or not the caller supplies a bucket-series-aware
|
||||
* resolver — when the resolver is missing we fall back to inputs
|
||||
* derivable from the parsed payload alone (`metricSource.n`,
|
||||
|
||||
@@ -128,8 +128,8 @@ describe("isLegacyInsightPayload()", () => {
|
||||
expect(isLegacyInsightPayload({ summary: "x" })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for the v1.4.14 pre-strict-schema shape (Marc's prod blob)", () => {
|
||||
// The actual cached blob that crashed /insights for Marc on
|
||||
it("returns true for the v1.4.14 pre-strict-schema shape (the maintainer's prod blob)", () => {
|
||||
// The actual cached blob that crashed /insights for the live tenant on
|
||||
// 2026-05-10. No `summary`, no `recommendations[]` — just the
|
||||
// pre-v1.4.16 `{changed, stable, drivers, nextSteps, confidence,
|
||||
// limitations}` shape. The route's `safeParse` failed and fell
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
/**
|
||||
* v1.4.19 phase A4 — strip the default-positivity opener.
|
||||
*
|
||||
* Marc reported every insight summary started with the same
|
||||
* the maintainer reported every insight summary started with the same
|
||||
* "Datengrundlage ist sehr stark" / "Your data foundation is strong"
|
||||
* filler sentence. The user does not see what fields the snapshot
|
||||
* carries, so this opener reads as awkward filler.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* v1.4.16 phase B5d — deterministic confidence-score computation per
|
||||
* recommendation.
|
||||
*
|
||||
* Marc's mandate: the model's self-reported confidence is unreliable
|
||||
* the maintainer's mandate: the model's self-reported confidence is unreliable
|
||||
* (calibrated probabilities are not a small-LLM strength), so the
|
||||
* wrapper OVERRIDES with a server-computed value derived from three
|
||||
* inputs that the cited data window actually exposes:
|
||||
|
||||
@@ -14,7 +14,7 @@ import { annotate } from "@/lib/logging/context";
|
||||
/**
|
||||
* Schema-enforced wrapper around `AIProvider.generateCompletion()`.
|
||||
*
|
||||
* Phase C1 (v1.4.15): Marc's mandate ("zero hallucinations, must
|
||||
* Phase C1 (v1.4.15): the maintainer's mandate ("zero hallucinations, must
|
||||
* ground on user data") means a free-form LLM response is no longer
|
||||
* acceptable. This wrapper:
|
||||
*
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
* - Non-object / null / undefined input → false (nothing to migrate).
|
||||
* - **v1.4.14 pre-strict shape** (`{changed, stable, drivers, ...}` —
|
||||
* no `summary`, no `recommendations[]`, no `findings[]`): true. This
|
||||
* is the production crash Marc hit on 2026-05-10 — the route's
|
||||
* is the production crash the maintainer hit on 2026-05-10 — the route's
|
||||
* `safeParse` failed against `insightResultSchema`, fell through to
|
||||
* the raw blob, and the rich card called `.replace()` on
|
||||
* `undefined`. Detect by absence of *both* `summary` (string) and
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Scope-hardened system prompt for AI insights — Phase C1 (v1.4.15).
|
||||
*
|
||||
* Marc, verbatim 2026-05-09:
|
||||
* the maintainer, verbatim 2026-05-09:
|
||||
* "Es darf null Halluzinationen haben und es muss sich halt irgendwie
|
||||
* stützen auf medizinische Dinge."
|
||||
* ("Zero hallucinations. Must ground on medical facts.")
|
||||
|
||||
@@ -12,7 +12,7 @@ import { annotate } from "@/lib/logging/context";
|
||||
*
|
||||
* The chain runner wraps `generateInsight()` with a hard-failure
|
||||
* walkthrough across an ordered list of providers. The motivation is
|
||||
* Marc's "ein Provider-Ausfall darf die Insights nicht zerschiessen"
|
||||
* the maintainer's "ein Provider-Ausfall darf die Insights nicht zerschiessen"
|
||||
* mandate: a 401 from Codex (OAuth expired), a 503 from OpenAI
|
||||
* (upstream brown-out), or a network reset MUST cascade to the next
|
||||
* configured provider rather than 422-ing the whole request.
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MEDICAL_REFERENCE_IDS } from "./medical-references";
|
||||
/**
|
||||
* Strict response schema for v1.4.15 AI hardening (Phase C1).
|
||||
*
|
||||
* Marc, verbatim 2026-05-09: "Es darf null Halluzinationen haben und es
|
||||
* the maintainer, verbatim 2026-05-09: "Es darf null Halluzinationen haben und es
|
||||
* muss sich halt irgendwie stützen auf medizinische Dinge." ("It must
|
||||
* have zero hallucinations and must ground on medical facts.")
|
||||
*
|
||||
@@ -95,7 +95,7 @@ const MEDICAL_REFERENCE_ID_SET: ReadonlySet<string> = new Set(
|
||||
* the rec — e.g. "+5 bpm above baseline over 7 of 7 days".
|
||||
*
|
||||
* Empty `comparedTo` or `deviation` is rejected so the model cannot
|
||||
* emit a placeholder rationale card. Marc's mandate from B5a stays:
|
||||
* emit a placeholder rationale card. the maintainer's mandate from B5a stays:
|
||||
* zero hallucinations, every rec must trace back to user data.
|
||||
*/
|
||||
export const aiRecommendationRationaleSchema = z.object({
|
||||
@@ -134,7 +134,7 @@ export const aiRecommendationSchema = z
|
||||
text: z.string().min(1, "recommendation.text required"),
|
||||
/**
|
||||
* Mandatory citation — the data point that justified the
|
||||
* recommendation. Closes the "ungrounded boilerplate" risk Marc
|
||||
* recommendation. Closes the "ungrounded boilerplate" risk the maintainer
|
||||
* called out: every recommendation must trace back to user data.
|
||||
*/
|
||||
metricSource: metricSourceSchema,
|
||||
@@ -228,7 +228,7 @@ export type AIWarning = z.infer<typeof aiWarningSchema>;
|
||||
* v1.4.19 (which predate the field) still parse cleanly. Fresh
|
||||
* generations after the v1.4.20 PROMPT_VERSION bump emit the block.
|
||||
*
|
||||
* Marc's "zero hallucinations" mandate stays: the prompt instructs the
|
||||
* the maintainer's "zero hallucinations" mandate stays: the prompt instructs the
|
||||
* model to derive every finding from a number visible in the snapshot,
|
||||
* and `keyFindings.length` is hard-capped at 5 so a runaway model
|
||||
* cannot pad the surface with filler.
|
||||
|
||||
@@ -21,7 +21,7 @@ describe("isBpReadingInTarget()", () => {
|
||||
* v1.4.16 A2 regression: 117/79 is textbook normotensive
|
||||
* (well-controlled, below the goal ceiling, above hypotension). The
|
||||
* v1.4.15 implementation rejected this because sys < sysLow (120),
|
||||
* which collapsed Marc's BD-Zielbereich tile to 0 %. The fix uses a
|
||||
* which collapsed the live tenant's BD-Zielbereich tile to 0 %. The fix uses a
|
||||
* one-sided ceiling check with a clinical floor.
|
||||
*/
|
||||
it("counts a normotensive reading below the goal band as in-target", () => {
|
||||
@@ -167,9 +167,9 @@ describe("computeBpInTargetPct", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.4.16 A2 regression — Marc's actual production data.
|
||||
* v1.4.16 A2 regression — the live tenant's actual production data.
|
||||
*
|
||||
* Marc reported BD-Zielbereich = 0 % despite multiple BP readings
|
||||
* the maintainer reported BD-Zielbereich = 0 % despite multiple BP readings
|
||||
* that "are clearly in target". A query against production showed
|
||||
* his last 30 days of paired readings include 117/79, 122/76, 108/76,
|
||||
* 106/73, 127/86, 115/78, 108/75, 124/82, 126/80, 133/95.
|
||||
@@ -186,9 +186,9 @@ describe("computeBpInTargetPct", () => {
|
||||
* Result: 5/10 = 50 %.
|
||||
*
|
||||
* Compare with the v1.4.15 narrow-band result (0/10 = 0 %) — that's
|
||||
* the regression Marc reported.
|
||||
* the regression the maintainer reported.
|
||||
*/
|
||||
it("regression: Marc's production data produces non-zero % under the ceiling semantics", () => {
|
||||
it("regression: the maintainer's production data produces non-zero % under the ceiling semantics", () => {
|
||||
const sys = [
|
||||
reading("2026-05-08T07:38:22Z", 117),
|
||||
reading("2026-05-03T21:22:02Z", 122),
|
||||
@@ -237,12 +237,12 @@ describe("computeBpInTargetPct", () => {
|
||||
|
||||
describe("computeBpInTargetWindows — all-time headline (v1.4.19 A1)", () => {
|
||||
/**
|
||||
* v1.4.19 A1 regression — Marc reported the BD-Zielbereich tile shows
|
||||
* v1.4.19 A1 regression — the maintainer reported the BD-Zielbereich tile shows
|
||||
* **EXACTLY 50 % on 7T, 30T, AND the headline ("total")** for his
|
||||
* production data. Hand-counted hypothesis-1 from the brief (the
|
||||
* three-way coincidence cannot be data — it's a calculation pin):
|
||||
*
|
||||
* - Marc has 572 paired BP readings going back to 2022-01.
|
||||
* - The live tenant has 572 paired BP readings going back to 2022-01.
|
||||
* - Last 7 days: 1 / 2 in target = 50 %.
|
||||
* - Last 30 days: 5 / 10 in target = 50 %.
|
||||
* - All time: 62 / 572 in target ≈ 11 %.
|
||||
@@ -301,8 +301,8 @@ describe("computeBpInTargetWindows — all-time headline (v1.4.19 A1)", () => {
|
||||
expect(result.allTime).toBeNull();
|
||||
});
|
||||
|
||||
it("regression: Marc's 30-day fixture mirrors Marc's prod headline bug", () => {
|
||||
// Reuse Marc's production fixture (10 pairs, 5 in target = 50 %).
|
||||
it("regression: the live tenant's 30-day fixture mirrors the live tenant's prod headline bug", () => {
|
||||
// Reuse the maintainer's production fixture (10 pairs, 5 in target = 50 %).
|
||||
// When the input is JUST those 10 pairs, allTime equals 30d == 50 %
|
||||
// (this is the legitimate coincidence — it only hides the bug when
|
||||
// the user has no older history). The bug surfaces the moment older
|
||||
@@ -343,7 +343,7 @@ describe("computeBpInTargetWindows — all-time headline (v1.4.19 A1)", () => {
|
||||
|
||||
describe("computeBpInTargetWindows", () => {
|
||||
/**
|
||||
* v1.4.18 A1 regression — Marc reported the BD-Zielbereich tile shows
|
||||
* v1.4.18 A1 regression — the maintainer reported the BD-Zielbereich tile shows
|
||||
* the 30-day headline (50 %) but `7T: —` and `30T: —` placeholders
|
||||
* even though the user has paired BP readings in both windows. Root
|
||||
* cause: the analytics route only returned `bpInTargetPct` once over
|
||||
@@ -351,7 +351,7 @@ describe("computeBpInTargetWindows", () => {
|
||||
* renders the "—" fallback. The fix is a windowed helper that returns
|
||||
* both 7-day and 30-day shares so the tile can show all three.
|
||||
*
|
||||
* Pinning Marc's actual production fixture (10 paired readings over
|
||||
* Pinning the live tenant's actual production fixture (10 paired readings over
|
||||
* the last 30 days, 2 of them within the last 7 days) so a future
|
||||
* reviewer can re-derive the expected counts by hand.
|
||||
*/
|
||||
@@ -432,11 +432,11 @@ describe("computeBpInTargetWindows", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Marc's real fixture: 10 paired readings over the last 30 days (5 in
|
||||
* the live tenant's real fixture: 10 paired readings over the last 30 days (5 in
|
||||
* target, 5 out → 50 %), of which 2 are inside the last 7 days
|
||||
* (1 in target, 1 out → 50 %).
|
||||
*/
|
||||
it("regression: Marc's production fixture renders 7d=50% and 30d=50%", () => {
|
||||
it("regression: the maintainer's production fixture renders 7d=50% and 30d=50%", () => {
|
||||
const fixtureNow = new Date("2026-05-09T12:00:00Z");
|
||||
const sys = [
|
||||
reading("2026-05-08T07:38:22Z", 117), // 7d IN
|
||||
|
||||
@@ -4,7 +4,7 @@ import { computeWindowTrend, SPLIT_HALF_THRESHOLD_DAYS } from "../window-trend";
|
||||
/**
|
||||
* v1.4.16 Fix A8b — chart trend overlay on the "All" filter.
|
||||
*
|
||||
* Marc's report: clicking the All range button on a multi-year metric
|
||||
* Maintainer report: clicking the All range button on a multi-year metric
|
||||
* displays "+0.0 kg/Woche" because the per-week delta rounds to zero
|
||||
* once the window is years wide. The split-half delta keeps the trend
|
||||
* legible by reporting the change between the first-half mean and the
|
||||
@@ -63,7 +63,7 @@ describe("computeWindowTrend()", () => {
|
||||
// per-week rate is small (~0.07 kg/week) which rounds to 0.1 in
|
||||
// the 1-decimal formatter — and the test in the chart wrapper is
|
||||
// about whether ANY signal survives. The split-half delta ≈ +5 kg
|
||||
// is the kind of number Marc actually wants to see.
|
||||
// is the kind of number the maintainer actually wants to see.
|
||||
const months = 36;
|
||||
const rawValues = Array.from({ length: months }, (_, i) => 80 + 0.3 * i);
|
||||
const trendValues = rawValues; // perfectly linear, trend === raw
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* `sys >= sysLow && sys <= sysHigh && dia >= diaLow && dia <= diaHigh`.
|
||||
* With ESH 2023 narrow targets (Sys 120-129, Dia 70-79 for under-65)
|
||||
* this collapses to 0 % for healthy normotensive users whose
|
||||
* readings sit BELOW the goal band — Marc's actual data has many
|
||||
* readings sit BELOW the goal band — the live tenant's actual data has many
|
||||
* readings like 117/79 (textbook normotensive, well-controlled) that
|
||||
* were marked OUT of target because sys < 120. Users intuitively
|
||||
* consider those readings *in target* — and so does clinical
|
||||
@@ -194,7 +194,7 @@ export function computeBpInTargetPct(
|
||||
* - v1.4.18 A1 wired `avg7` + `avg30` from `computeBpInTargetWindows`,
|
||||
* BUT the analytics route also routed the **headline**
|
||||
* (`bpInTargetPct`) through `windows.last30Days?.pct` — i.e. the
|
||||
* headline was a literal copy of `bpInTargetPct30d`. For Marc's prod
|
||||
* headline was a literal copy of `bpInTargetPct30d`. For the live tenant's prod
|
||||
* data (572 paired readings since 2022, recent 30 days = 50 %, all-time
|
||||
* ≈ 11 %) the tile rendered `50 %` headline, `7T: 50, 30T: 50` and
|
||||
* looked algorithmically pinned to 50/50/50. That was hypothesis-1
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Backstory — v1.4.15 Fix 4 ("7-Tage-Schnitt" → "7-Tage-Trend"):
|
||||
* the dashboard tiles labelled the 7-day average as "7T:" / "7d:"
|
||||
* with the mean value beside it. Marc found that label unclear
|
||||
* with the mean value beside it. the maintainer found that label unclear
|
||||
* (sounded like an "average") when the goal of the tile is to
|
||||
* convey *direction* over the past week. We re-use the existing
|
||||
* 7-day linear-regression slope (already in `DataSummary.slope7`)
|
||||
@@ -18,7 +18,7 @@
|
||||
* slope7 is unavailable. Some metrics (mood, sleep) have <2 entries
|
||||
* within the trailing 7-day window for users who don't log daily,
|
||||
* which used to surface as a missing delta on the dashboard tile —
|
||||
* Marc's complaint: "Bei Stimmung wird zum Beispiel kein Trend in
|
||||
* the maintainer's complaint: "Bei Stimmung wird zum Beispiel kein Trend in
|
||||
* Zahlen dargestellt." A 30-day slope still answers the question
|
||||
* "is this metric drifting up or down?" — we just project it onto
|
||||
* a 7-day window for label consistency. Returning `null` is reserved
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* for long ("All") ranges where the per-week rate
|
||||
* rounds to ±0 in the 1-decimal formatter.
|
||||
*
|
||||
* v1.4.16 Fix A8b — Marc reported the chart trend overlay reads
|
||||
* v1.4.16 Fix A8b — the maintainer reported the chart trend overlay reads
|
||||
* "+0.0 kg/Woche" on the All filter, even when his weight clearly
|
||||
* shifted across years. Root cause: `weeklyDelta = (last − first) / days
|
||||
* × 7` is mathematically correct but visually rounds to zero whenever
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface DashboardWidgetConfig {
|
||||
* `visible` so existing saved layouts keep their previous behaviour
|
||||
* (one toggle controls both surfaces). When set explicitly the user
|
||||
* can show the chart but hide the tile, or show the tile but hide
|
||||
* the chart, which is what Marc asked for in the v1.4.15 follow-up
|
||||
* the chart, which is what the maintainer asked for in the v1.4.15 follow-up
|
||||
* (memory `feedback_dashboard_top_tiles_selectable.md`).
|
||||
*/
|
||||
tileVisible?: boolean;
|
||||
@@ -95,7 +95,7 @@ export function isComparisonBaseline(
|
||||
/**
|
||||
* v1.4.18 — per-chart overlay-prefs.
|
||||
*
|
||||
* Marc rolled back B1a's always-on chart overlays (gradient fill,
|
||||
* the maintainer rolled back B1a's always-on chart overlays (gradient fill,
|
||||
* personal-baseline reference line, target-zone shading) and asked for
|
||||
* a per-chart switch surface so each chart can be tuned independently.
|
||||
* The three toggles (7-day trend / Trend arrow / Target range) live in
|
||||
@@ -193,7 +193,7 @@ export const DEFAULT_DASHBOARD_LAYOUT: DashboardLayout = {
|
||||
// v1.4.15 phase-B4 — recent-unlocks card (dashboard surface for the
|
||||
// gamification feature). Visible by default — the card is small,
|
||||
// self-gates on having any achievements (otherwise it shows a brief
|
||||
// "no achievements yet" + link), and Marc-asked-for-it explicitly.
|
||||
// "no achievements yet" + link), and maintainer-asked-for-it explicitly.
|
||||
// `tileVisible` is forced false because there is no tile surface
|
||||
// for this widget; only the chart-row card.
|
||||
{ id: "achievements", visible: true, tileVisible: false, order: 13 },
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* `Accept-Language: de, en;q=0.5` hint is sent so providers return
|
||||
* native city names ("Nürnberg") rather than ASCII folds
|
||||
* ("Nuremberg"). The previous path lost umlauts in production for at
|
||||
* least one Marc-Brief login row that rendered as "Nrnberg" in
|
||||
* least one maintainer-flagged login row that rendered as "Nrnberg" in
|
||||
* /admin/login-overview — see `docs/audit/v1416-summary.md` once cut.
|
||||
*/
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("stripChartTokens", () => {
|
||||
// v1.4.17 hotfix — defensive: reading `insight.summary` on a legacy
|
||||
// cached payload (no `summary` field) returns undefined. Crashing
|
||||
// with `Cannot read properties of undefined (reading 'replace')` was
|
||||
// the production /insights bug Marc hit on 2026-05-10. Treat
|
||||
// the production /insights bug the maintainer hit on 2026-05-10. Treat
|
||||
// null/undefined as the empty string instead.
|
||||
it("returns empty string for undefined input", () => {
|
||||
expect(stripChartTokens(undefined)).toBe("");
|
||||
|
||||
@@ -42,7 +42,7 @@ const ALLOWED_SET = new Set<string>(ALLOWED_CHART_TOKENS);
|
||||
// v1.4.19 A3 — strip vs parse split. The strip-side character class
|
||||
// is permissive (`[A-Za-z0-9_]+`) so model-emitted lowercase or
|
||||
// snake_case tokens like `metric:blood_pressure_sweet_spot` (verbatim
|
||||
// Marc 2026-05-10) get cleaved out of prose instead of leaking to the
|
||||
// maintainer probe 2026-05-10) get cleaved out of prose instead of leaking to the
|
||||
// DOM. The parse-side class stays uppercase-only so unrenderable /
|
||||
// hallucinated tokens are dropped from the *render* path entirely;
|
||||
// only the *strip* is permissive.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* The state-machine threshold tests live in `status.test.ts` (when
|
||||
* does the alert fire?). This file proves WHAT the alert looks like
|
||||
* — the message body Marc sees on Telegram when an integration
|
||||
* — the message body the maintainer sees on Telegram when an integration
|
||||
* starts crashing for one of his users.
|
||||
*
|
||||
* The formatter is a pure function so we can test deterministically
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Per-minute host-load sampler that backs the admin /admin/system-status
|
||||
* "last 2 hours" chart (v1.4.16 phase B3).
|
||||
*
|
||||
* Marc asked for a small at-a-glance graph above the system-status facts
|
||||
* the maintainer asked for a small at-a-glance graph above the system-status facts
|
||||
* grid showing CPU + memory + disk-IO so a glance at /admin tells him
|
||||
* whether the apps-01 host is healthy. We considered scraping Coolify's
|
||||
* Sentinel collector, but the Coolify HTTP API does not expose a clean
|
||||
|
||||
Reference in new issue
Block a user