mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +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
@@ -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