diff --git a/.env.production.example b/.env.production.example
index ae0c3eef3..c5c87c0e6 100644
--- a/.env.production.example
+++ b/.env.production.example
@@ -383,12 +383,6 @@ API_TOKEN_HMAC_KEY=""
# CODEX_MODEL=""
# CODEX_MODEL_FALLBACK_CHAIN=""
-# Surfaces reviewed n-of-1 experiment outcomes in the Coach snapshot. Off by
-# default: the read-back is the most overclaim-prone Coach surface, so it is a
-# deliberate operator opt-in. Set to "1" to enable; anything else keeps it
-# dormant while the review worker still records outcomes.
-# COACH_EXPERIMENT_VERDICT="1"
-
# -----------------------------------------------------------------------------
# Off-host backups -- all-or-none
diff --git a/docker-compose.yml b/docker-compose.yml
index c272075a9..40decdb45 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -360,10 +360,6 @@ x-healthlog-env: &healthlog_env
# outright; the account's list is then not read.
CODEX_MODEL: "${CODEX_MODEL:-}"
CODEX_MODEL_FALLBACK_CHAIN: "${CODEX_MODEL_FALLBACK_CHAIN:-}"
- # Surfaces reviewed n-of-1 experiment outcomes in the Coach snapshot.
- # Off by default — the read-back is the most overclaim-prone Coach
- # surface, so it is an explicit operator opt-in. Set to "1" to enable.
- COACH_EXPERIMENT_VERDICT: "${COACH_EXPERIMENT_VERDICT:-}"
# Daily-briefing output-token ceiling (optional; default 2500, clamped
# 500-8000). Raise when a verbose model's briefing JSON is cut off.
# Listed here so an operator value in .env reaches the container (the
diff --git a/e2e/onboarding-ai-offer.spec.ts b/e2e/onboarding-ai-offer.spec.ts
deleted file mode 100644
index 7f09c78e8..000000000
--- a/e2e/onboarding-ai-offer.spec.ts
+++ /dev/null
@@ -1,220 +0,0 @@
-/**
- * v1.38.19 — the AI offer at the end of the setup flow.
- *
- * The screen used to promise that insights "work for you right now" the
- * moment the operator had configured a key. It never knew whether the key
- * answered, and it never mentioned the consent receipt the egress actually
- * needs — so the person who believed it met a `consent.ai.required` refusal
- * on their first briefing. On 2026-09-11 the operator's own instance was
- * answering HTTP 500 from its OAuth proxy while the screen kept promising.
- *
- * Three runs, one account, and the only difference between them is a row in
- * `provider_health`:
- *
- * a success inside the freshness window → the offer, and one tap grants
- * a dead credential inside its cooldown → "not answering", and no button
- * no row at all → neither: the panel says nothing
- *
- * What is deliberately absent: any call to a provider. The flow's rule holds
- * — nothing in it talks to one — and a probe here would bill the operator for
- * every registration on the instance.
- *
- * Serial, and it restores the instance state it seeded: the tri-state folds
- * across accounts by design, so its inputs are instance-wide.
- */
-import {
- E2E_SETUP_AI_OFFER,
- SETUP_AI_OFFER_STORAGE_STATE_PATH,
-} from "./setup/global-setup";
-import {
- clearAiConsent,
- clearOperatorProvider,
- clearSharedProviderResult,
- seedOperatorProvider,
- seedSharedProviderResult,
-} from "./setup/ai-offer-fixture";
-import { resetSetupFlow } from "./setup/setup-flow-fixture";
-import { expect, test } from "./setup/test";
-import {
- answer,
- answerEverything,
- complete,
- expectScreen,
-} from "./setup-flow-helpers";
-
-const OFFER = '[data-slot="onboarding-ai-offer"]';
-const GRANT = '[data-slot="onboarding-ai-offer-grant"]';
-const UNAVAILABLE = '[data-slot="onboarding-ai-unavailable"]';
-const SHARED_KEY = '[data-slot="onboarding-ai-shared-key"]';
-/** The panel once it knows which variant it is; absent while the status is in
- * flight, so an assertion of absence cannot pass on an unanswered question. */
-const SETTLED = '[data-slot="onboarding-ai-panel"][data-ai-state]';
-
-/** Walk the account to the done screen through the API, then open it. */
-async function openDoneScreen(page: import("@playwright/test").Page) {
- await page.goto("/onboarding");
- await answerEverything(page);
- // Q6 applies because the answers name glucose and weight; the flow does not
- // complete while a screen it owes is unanswered.
- await answer(page, {
- step: "units",
- units: { glucoseUnit: "mmol/L", unitPreference: "metric" },
- });
- await complete(page);
- await answer(page, { step: "first-result", status: "skipped" });
- await page.goto("/onboarding/done");
- await expectScreen(page, "done");
- // The panel itself is unconditional; the variant inside it is not.
- await expect(page.locator('[data-slot="onboarding-ai-panel"]')).toBeVisible();
-}
-
-// One account, one instance-wide ledger. Two workers driving this at once
-// would each read the other's seed.
-test.describe.configure({ mode: "serial" });
-
-test.describe("the setup flow's AI offer follows the shared provider's health", () => {
- test.use({ storageState: SETUP_AI_OFFER_STORAGE_STATE_PATH });
-
- test.beforeEach(async () => {
- await resetSetupFlow(E2E_SETUP_AI_OFFER.username);
- await clearAiConsent(E2E_SETUP_AI_OFFER.username);
- await seedOperatorProvider();
- });
-
- test.afterAll(async () => {
- await clearOperatorProvider();
- await clearAiConsent(E2E_SETUP_AI_OFFER.username);
- });
-
- test("offers one tap when the shared provider worked recently, and the tap leaves a receipt", async ({
- page,
- }) => {
- test.setTimeout(90_000);
- await seedSharedProviderResult(E2E_SETUP_AI_OFFER.username, {
- result: "ok",
- okMinutesAgo: 30,
- });
-
- // No receipt before the tap — otherwise the assertion after it proves
- // nothing about the button.
- const before = await page.request.get(
- "/api/consent/ai/latest?kind=ai_full",
- );
- expect(before.status()).toBe(200);
- expect(
- ((await before.json()) as { data: { receipt: unknown } }).data.receipt,
- ).toBeNull();
-
- await openDoneScreen(page);
-
- await expect(page.locator(SETTLED)).toHaveAttribute(
- "data-ai-state",
- "offer",
- { timeout: 15_000 },
- );
- await expect(page.locator(OFFER)).toBeVisible();
- await expect(page.locator(UNAVAILABLE)).toHaveCount(0);
-
- // No provider was called to decide this. The flow reads a projection.
- const probes: string[] = [];
- page.on("request", (req) => {
- const path = new URL(req.url()).pathname;
- if (path === "/api/ai/test" || path.startsWith("/api/insights/"))
- probes.push(path);
- });
-
- await page.locator(GRANT).click();
-
- // The line the person reads swaps to the granted one.
- await expect(page.locator(SHARED_KEY)).toBeVisible({ timeout: 15_000 });
- await expect(page.locator(GRANT)).toHaveCount(0);
- expect(probes, "the offer must not trigger a provider call").toEqual([]);
-
- const after = await page.request.get("/api/consent/ai/latest?kind=ai_full");
- const receipt = (
- (await after.json()) as {
- data: { receipt: { kind: string; revokedAt: string | null } | null };
- }
- ).data.receipt;
- expect(receipt).not.toBeNull();
- expect(receipt!.kind).toBe("ai_full");
- expect(receipt!.revokedAt).toBeNull();
- });
-
- test("says the provider is not answering while its credential is benched, and offers no button", async ({
- page,
- }) => {
- test.setTimeout(90_000);
- await seedSharedProviderResult(E2E_SETUP_AI_OFFER.username, {
- result: "auth_failed",
- });
-
- await openDoneScreen(page);
-
- await expect(page.locator(SETTLED)).toHaveAttribute(
- "data-ai-state",
- "unavailable",
- { timeout: 15_000 },
- );
- await expect(page.locator(UNAVAILABLE)).toBeVisible();
- await expect(page.locator(GRANT)).toHaveCount(0);
- await expect(page.locator(OFFER)).toHaveCount(0);
- // The unproven promise is not shown to somebody who has not consented.
- await expect(page.locator(SHARED_KEY)).toHaveCount(0);
-
- // The rest of the panel is untouched: the flow stays complete without it.
- await expect(
- page.locator('[data-slot="onboarding-ai-keyless"]'),
- ).toBeVisible();
- await expect(
- page.locator('[data-slot="onboarding-open-dashboard"]'),
- ).toBeVisible();
-
- // The grant route itself stays open — it is the user's own consent act
- // and is reachable from Settings at any time. What this case pins is the
- // decision the screen was handed, so read it back from the source.
- const status = await page.request.get("/api/user/ai-provider");
- const data = (
- (await status.json()) as {
- data: { serverProviderHealth: string; serverProviderOffer: boolean };
- }
- ).data;
- expect(data.serverProviderHealth).toBe("unhealthy");
- expect(data.serverProviderOffer).toBe(false);
- });
-
- test("claims nothing on an instance whose provider nobody has used yet", async ({
- page,
- }) => {
- test.setTimeout(90_000);
- // The operator has configured a key and nothing has been asked of it —
- // the state of every deployment until its first AI call, and so the most
- // common way through this screen. The panel may not offer (nothing is
- // known to work) and may not report an outage (nothing failed).
- await clearSharedProviderResult();
-
- await openDoneScreen(page);
-
- await expect(page.locator(SETTLED)).toHaveAttribute(
- "data-ai-state",
- "neutral",
- { timeout: 15_000 },
- );
- await expect(
- page.locator('[data-slot="onboarding-ai-keyless"]'),
- ).toBeVisible();
- await expect(page.locator(UNAVAILABLE)).toHaveCount(0);
- await expect(page.locator(OFFER)).toHaveCount(0);
- await expect(page.locator(GRANT)).toHaveCount(0);
- await expect(page.locator(SHARED_KEY)).toHaveCount(0);
-
- const status = await page.request.get("/api/user/ai-provider");
- const data = (
- (await status.json()) as {
- data: { serverProviderHealth: string; serverProviderOffer: boolean };
- }
- ).data;
- expect(data.serverProviderHealth).toBe("unknown");
- expect(data.serverProviderOffer).toBe(false);
- });
-});
diff --git a/e2e/setup/ai-offer-fixture.ts b/e2e/setup/ai-offer-fixture.ts
deleted file mode 100644
index 5f427fa94..000000000
--- a/e2e/setup/ai-offer-fixture.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-/**
- * v1.38.19 — the instance state the shared-provider offer reads.
- *
- * Two rows decide what the setup flow's last screen may say, and neither has
- * a product endpoint that writes them the way this journey needs:
- *
- * `app_settings.admin_ai_key_encrypted` — presence alone makes the operator
- * the origin that would serve a user with no provider of their own
- * (`managedBy: "server"`). Nothing decrypts it on this path, so the value
- * below is a placeholder ciphertext and not a credential of any kind.
- *
- * `provider_health` — the ledger the tri-state folds. It is written only as
- * a side effect of a real completion, which this suite must never make, so
- * the journey states the outcome it wants to reason about instead.
- *
- * Both are INSTANCE-wide by nature: the projection deliberately folds across
- * accounts, because a fresh account's own ledger is empty by construction.
- * That is why the spec runs serially and puts both back afterwards.
- *
- * Seeded through `pg`, the same transport `global-setup.ts` and the other
- * fixtures use.
- */
-import pg from "pg";
-
-/** Not a key. Presence is the only thing read on this path. */
-const PLACEHOLDER_ADMIN_KEY = "e2e-admin-key-presence-only";
-
-function connect(): pg.Pool {
- const url = process.env.DATABASE_URL;
- if (!url) {
- throw new Error("[ai-offer-fixture] DATABASE_URL is not set");
- }
- return new pg.Pool({ connectionString: url });
-}
-
-async function userId(pool: pg.Pool, username: string): Promise {
- const { rows } = await pool.query<{ id: string }>(
- `SELECT id FROM users WHERE username = $1`,
- [username],
- );
- const id = rows[0]?.id;
- if (!id) throw new Error(`[ai-offer-fixture] no account named ${username}`);
- return id;
-}
-
-/** The operator has configured a shared provider on this instance. */
-export async function seedOperatorProvider(): Promise {
- const pool = connect();
- try {
- await pool.query(
- `INSERT INTO app_settings (id, admin_ai_key_encrypted)
- VALUES ('singleton', $1)
- ON CONFLICT (id) DO UPDATE SET admin_ai_key_encrypted = EXCLUDED.admin_ai_key_encrypted`,
- [PLACEHOLDER_ADMIN_KEY],
- );
- } finally {
- await pool.end();
- }
-}
-
-/** Put the instance back: no operator key, no ledger rows for either tag. */
-export async function clearOperatorProvider(): Promise {
- const pool = connect();
- try {
- await pool.query(
- `UPDATE app_settings SET admin_ai_key_encrypted = NULL WHERE id = 'singleton'`,
- );
- await pool.query(
- `DELETE FROM provider_health
- WHERE provider_type IN ('admin-openai', 'admin-codex')`,
- );
- } finally {
- await pool.end();
- }
-}
-
-/**
- * State one outcome for the operator's OpenAI provider.
- *
- * `ok` — it served somebody `okMinutesAgo` minutes ago.
- * `auth_failed` — its credential is dead and benched for another hour,
- * which is the shape the operator's own instance was in on
- * 2026-09-11 (a 500 from the OAuth proxy, then a 401).
- */
-export async function seedSharedProviderResult(
- username: string,
- outcome: { result: "ok"; okMinutesAgo: number } | { result: "auth_failed" },
-): Promise {
- const pool = connect();
- try {
- const id = await userId(pool, username);
- await pool.query(
- `DELETE FROM provider_health
- WHERE provider_type IN ('admin-openai', 'admin-codex')`,
- );
- if (outcome.result === "ok") {
- await pool.query(
- `INSERT INTO provider_health
- (id, user_id, provider_type, last_result, consecutive_failures,
- last_ok_at, updated_at)
- VALUES ($1, $2, 'admin-openai', 'ok', 0,
- NOW() - ($3 || ' minutes')::interval, NOW())`,
- [`e2e-ph-${id}`, id, String(outcome.okMinutesAgo)],
- );
- } else {
- await pool.query(
- `INSERT INTO provider_health
- (id, user_id, provider_type, last_result, last_status,
- consecutive_failures, last_failure_at, next_retry_at, updated_at)
- VALUES ($1, $2, 'admin-openai', 'auth_failed', 401, 1,
- NOW(), NOW() + interval '1 hour', NOW())`,
- [`e2e-ph-${id}`, id],
- );
- }
- } finally {
- await pool.end();
- }
-}
-
-/**
- * The ledger holds nothing for either shared tag, with the operator's key
- * still in place: a brand-new instance before its first AI call, which is
- * what every deployment looks like until somebody makes one.
- */
-export async function clearSharedProviderResult(): Promise {
- const pool = connect();
- try {
- await pool.query(
- `DELETE FROM provider_health
- WHERE provider_type IN ('admin-openai', 'admin-codex')`,
- );
- } finally {
- await pool.end();
- }
-}
-
-/** No receipt on file — the state a fresh account reaches the screen in. */
-export async function clearAiConsent(username: string): Promise {
- const pool = connect();
- try {
- const id = await userId(pool, username);
- await pool.query(`DELETE FROM consent_receipts WHERE user_id = $1`, [id]);
- await pool.query(`DELETE FROM rate_limits WHERE key = 'consent:' || $1`, [
- id,
- ]);
- } finally {
- await pool.end();
- }
-}
diff --git a/messages/de.json b/messages/de.json
index 7e7762cc3..c3db429d5 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -3012,8 +3012,6 @@
},
"correlationRow": {
"title": "Zusammenhänge",
- "experimentCta": "7-Tage-Experiment starten",
- "experimentTooltip": "Bald verfügbar",
"emptyTitle": "Mehr Daten nötig, um diesen Zusammenhang zu zeigen",
"sourceChip": "basierend auf {n} gepaarten Messungen ({window})",
"sourceWindowLast30": "letzte 30 Tage",
@@ -5174,7 +5172,9 @@
"connectCta": "Gerät verbinden",
"logCta": "Ersten Wert eintragen",
"importCta": "Bestehende Daten importieren",
- "modulesCta": "Module auswählen"
+ "modulesCta": "Module auswählen",
+ "aiOptional": "KI ist optional. Alles hier funktioniert auch ohne.",
+ "aiSetupCta": "KI einrichten"
},
"welcomeBack": {
"title": "Willkommen zurück",
@@ -5195,37 +5195,6 @@
"allergiesPlaceholder": "z. B. Penicillin, Laktose",
"privacyNote": "Verschlüsselt gespeichert und nur für besseren Kontext genutzt. Medikamente fragen wir hier nicht ab, die kannst du jederzeit auf der Medikamenten-Seite ergänzen."
},
- "ai": {
- "panelTitle": "So sehen Insights aus",
- "panelIntro": "Coach, das tägliche Briefing und die Einschätzungen je Messwert übersetzen deine eigenen Zahlen in Klartext. So sieht das aus, bevor du irgendetwas verbindest.",
- "showSample": "Beispiel-Briefing ansehen",
- "sampleTitle": "Tägliches Briefing",
- "sampleTag": "Beispiel",
- "sampleAriaLabel": "Beispielhaftes tägliches Briefing",
- "sampleBody": "Gewicht in zwei Wochen um 1,2 kg gesunken. Ruhepuls stabil bei 58 bpm. Blutdruck an 6 von 7 Tagen im Zielbereich; der einzige hohe Wert folgte auf ein spätes, salziges Abendessen.",
- "sampleCaption": "Nur ein Beispiel, nicht deine eigenen Daten. So liest sich ein Briefing, sobald Insights aktiv sind.",
- "ladderTitle": "Drei Wege, es zu aktivieren",
- "ladderLocalTitle": "Lokales Modell: KI-Anfragen bleiben in deinem Netzwerk",
- "ladderLocalBody": "Richte HealthLog auf Ollama oder LM Studio auf deinem eigenen Rechner aus. Kostenlos und vollständig privat, der ruhige Standard.",
- "ladderByokTitle": "Eigenen Schlüssel verwenden",
- "ladderByokBody": "Nutze deinen eigenen OpenAI- oder Anthropic-Schlüssel. Er läuft über dein Konto und deine Abrechnung, und die API-Stufe hält deine Daten standardmäßig aus dem Training heraus.",
- "ladderOauthTitle": "Mit einem Abo anmelden",
- "ladderOauthBody": "Verbinde ein ChatGPT-Konto: keine Kosten pro Token, aber die Bedingungen der Verbraucherstufe können Inhalte fürs Training nutzen. Deshalb eine bewusste Wahl, nie der Standard für deine Gesundheitsdaten.",
- "keylessLine": "HealthLog ist auch ohne KI vollständig nutzbar. Deine Kacheln, Diagramme, Trends und Exporte funktionieren alle. KI ergänzt nur den Klartext obendrauf.",
- "setupCta": "Einrichten (etwa eine Minute)",
- "sharedKeyNote": "Diese Instanz stellt bereits einen gemeinsamen KI-Anbieter bereit, deshalb funktionieren Insights für dich sofort, keine Einrichtung nötig.",
- "offer": {
- "title": "Die Zusammenfassung dieser Instanz ist bereit.",
- "body": "Der Betreiber stellt einen Anbieter bereit, der zuletzt innerhalb der letzten 24 Stunden funktioniert hat. Mit einem Tipp schaltest du den Klartext ein; deine Daten gehen dann an diesen einen Anbieter, an niemanden sonst.",
- "cta": "Klartext einschalten",
- "granted": "Eingeschaltet. Die erste Zusammenfassung erscheint auf dem Dashboard.",
- "error": "Das Einschalten hat nicht geklappt. Versuch es gleich noch einmal."
- },
- "unavailable": {
- "title": "Der Anbieter dieser Instanz antwortet gerade nicht.",
- "body": "HealthLog funktioniert ohne Klartext vollständig. Du kannst später in den Einstellungen einen eigenen Anbieter hinterlegen."
- }
- },
"steps": {
"who": "Für wen diese Akte ist",
"areas": "Worauf du achten willst",
@@ -11175,7 +11144,6 @@
"title": "Pläne",
"pageDescription": "Wenn-dann-Pläne aus deinen Coach-Gesprächen. Bestätige einen Vorschlag, damit der Coach ihn sich merkt.",
"readOnlyNote": "Der Coach ist gerade nicht verfügbar, deine Pläne kannst du aber weiter lesen und löschen.",
- "proposalTitle": "Plan-Vorschlag",
"ifThen": "Wenn {cue}, dann {action}.",
"targetPrefix": "Ziel: {target}",
"reviewPrefix": "Check-in: {when}",
diff --git a/messages/en.json b/messages/en.json
index 385bed6dc..d897c04e6 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -3012,8 +3012,6 @@
},
"correlationRow": {
"title": "Relationships",
- "experimentCta": "Try a 7-day experiment",
- "experimentTooltip": "Coming soon",
"emptyTitle": "Need more data to see this relationship",
"sourceChip": "based on {n} paired readings ({window})",
"sourceWindowLast30": "last 30 days",
@@ -5174,7 +5172,9 @@
"connectCta": "Connect a device",
"logCta": "Log your first measurement",
"importCta": "Import existing history",
- "modulesCta": "Choose your modules"
+ "modulesCta": "Choose your modules",
+ "aiOptional": "AI is optional. Everything here works without it.",
+ "aiSetupCta": "Set up AI"
},
"welcomeBack": {
"title": "Welcome back",
@@ -5195,37 +5195,6 @@
"allergiesPlaceholder": "e.g. penicillin, lactose",
"privacyNote": "Stored encrypted and used only to give you better context. We don't ask for your medications here. Add those anytime from the Medications page."
},
- "ai": {
- "panelTitle": "See what insights look like",
- "panelIntro": "Coach, the daily briefing and the per-metric reads turn your own numbers into plain language. Here's the shape of it, before you connect anything.",
- "showSample": "See a sample briefing",
- "sampleTitle": "Daily Briefing",
- "sampleTag": "Example",
- "sampleAriaLabel": "Example daily briefing",
- "sampleBody": "Weight down 1.2 kg over two weeks. Resting pulse steady at 58 bpm. Blood pressure in range on 6 of 7 days; the one high reading followed a late, salty dinner.",
- "sampleCaption": "Example only, not your own data. This is what a briefing reads like once insights are on.",
- "ladderTitle": "Three ways to turn it on",
- "ladderLocalTitle": "Local model: AI requests stay on your network",
- "ladderLocalBody": "Point HealthLog at Ollama or LM Studio on your own machine. Free and fully private, the calm default.",
- "ladderByokTitle": "Bring your own key",
- "ladderByokBody": "Use your own OpenAI or Anthropic key. It runs under your account and billing, and the API tier keeps your data out of training by default.",
- "ladderOauthTitle": "Sign in with a subscription",
- "ladderOauthBody": "Connect a ChatGPT account. No per-token cost, but the consumer tier's terms may use content for training, so it's a deliberate choice, never the default for your health data.",
- "keylessLine": "HealthLog is fully useful without AI. Your tiles, charts, trends and exports all work. AI only adds the plain-language read on top.",
- "setupCta": "Set it up (about a minute)",
- "sharedKeyNote": "This instance already provides a shared AI provider, so insights work for you right now, no setup needed.",
- "offer": {
- "title": "This instance can write your summary in plain language.",
- "body": "The operator provides a provider that last worked within the past 24 hours. One tap turns the plain-language read on; your data then goes to that one provider and to nobody else.",
- "cta": "Turn on plain language",
- "granted": "Turned on. The first summary appears on your dashboard.",
- "error": "Turning it on did not work. Give it another try in a moment."
- },
- "unavailable": {
- "title": "This instance’s provider is not answering right now.",
- "body": "HealthLog is fully useful without the plain-language read. You can add a provider of your own later under Settings."
- }
- },
"steps": {
"who": "Who this record is for",
"areas": "What to keep an eye on",
@@ -11175,7 +11144,6 @@
"title": "Plans",
"pageDescription": "If-then plans agreed in your Coach conversations. Confirm a proposal to make it part of the Coach's memory.",
"readOnlyNote": "The Coach is not available right now, but you can still read and delete your plans.",
- "proposalTitle": "Plan proposal",
"ifThen": "If {cue}, then {action}.",
"targetPrefix": "Target: {target}",
"reviewPrefix": "Check-in: {when}",
diff --git a/messages/es.json b/messages/es.json
index 6b9bbcf62..c6c9875ea 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -3012,8 +3012,6 @@
},
"correlationRow": {
"title": "Correlaciones",
- "experimentCta": "Iniciar experimento de 7 días",
- "experimentTooltip": "Próximamente",
"emptyTitle": "Necesitamos más datos para mostrar esta correlación",
"sourceChip": "basado en {n} mediciones pareadas ({window})",
"sourceWindowLast30": "Últimos 30 días",
@@ -5174,7 +5172,9 @@
"connectCta": "Conectar un dispositivo",
"logCta": "Registra tu primera medición",
"importCta": "Importar historial existente",
- "modulesCta": "Elige tus módulos"
+ "modulesCta": "Elige tus módulos",
+ "aiOptional": "La IA es opcional. Todo funciona también sin ella.",
+ "aiSetupCta": "Configurar IA"
},
"welcomeBack": {
"title": "Bienvenido de nuevo",
@@ -5195,37 +5195,6 @@
"allergiesPlaceholder": "p. ej. penicilina, lactosa",
"privacyNote": "Se almacena cifrado y solo se usa para darte mejor contexto. Aquí no te pedimos tu medicación: puedes añadirla cuando quieras desde la página de Medicación."
},
- "ai": {
- "panelTitle": "Mira cómo son los insights",
- "panelIntro": "Coach, el resumen diario y las lecturas por métrica convierten tus propios números en lenguaje claro. Así se ve, antes de conectar nada.",
- "showSample": "Ver un resumen de ejemplo",
- "sampleTitle": "Resumen diario",
- "sampleTag": "Ejemplo",
- "sampleAriaLabel": "Resumen diario de ejemplo",
- "sampleBody": "Peso 1,2 kg menos en dos semanas. Pulso en reposo estable en 58 lpm. Tensión dentro del rango 6 de 7 días; la única lectura alta llegó tras una cena tardía y salada.",
- "sampleCaption": "Solo un ejemplo, no son tus datos. Así se lee un resumen cuando los insights están activos.",
- "ladderTitle": "Tres formas de activarlo",
- "ladderLocalTitle": "Modelo local: las solicitudes de IA no salen de tu red",
- "ladderLocalBody": "Apunta HealthLog a Ollama o LM Studio en tu propia máquina. Gratis y totalmente privado: la opción tranquila por defecto.",
- "ladderByokTitle": "Usa tu propia clave",
- "ladderByokBody": "Usa tu propia clave de OpenAI o Anthropic. Funciona con tu cuenta y facturación, y el nivel de API mantiene tus datos fuera del entrenamiento por defecto.",
- "ladderOauthTitle": "Inicia sesión con una suscripción",
- "ladderOauthBody": "Conecta una cuenta de ChatGPT. Sin coste por token, pero los términos del nivel de consumo pueden usar el contenido para entrenamiento, así que es una elección deliberada, nunca la opción por defecto para tus datos de salud.",
- "keylessLine": "HealthLog es plenamente útil sin IA. Tus paneles, gráficos, tendencias y exportaciones funcionan; la IA solo añade la lectura en lenguaje claro por encima.",
- "setupCta": "Configúralo (un minuto aprox.)",
- "sharedKeyNote": "Esta instancia ya ofrece un proveedor de IA compartido, así que los insights funcionan para ti ahora mismo, sin configuración.",
- "offer": {
- "title": "Esta instancia ya puede escribir tu resumen en lenguaje claro.",
- "body": "El operador ofrece un proveedor que funcionó por última vez en las últimas 24 horas. Con un toque activas el lenguaje claro; tus datos irán entonces a ese único proveedor y a nadie más.",
- "cta": "Activar el lenguaje claro",
- "granted": "Activado. El primer resumen aparecerá en tu panel.",
- "error": "No se pudo activar. Vuelve a intentarlo en un momento."
- },
- "unavailable": {
- "title": "El proveedor de esta instancia no responde en este momento.",
- "body": "HealthLog es plenamente útil sin el lenguaje claro. Más adelante puedes añadir tu propio proveedor en los ajustes."
- }
- },
"steps": {
"who": "Para quién es este registro",
"areas": "Qué quieres vigilar",
@@ -11175,7 +11144,6 @@
"title": "Planes",
"pageDescription": "Planes de tipo «si… entonces» acordados en tus conversaciones con el Coach. Confirma una propuesta para que el Coach la recuerde.",
"readOnlyNote": "El Coach no está disponible ahora mismo, pero aún puedes leer y eliminar tus planes.",
- "proposalTitle": "Propuesta de plan",
"ifThen": "Si {cue}, entonces {action}.",
"targetPrefix": "Objetivo: {target}",
"reviewPrefix": "Revisión: {when}",
diff --git a/messages/fr.json b/messages/fr.json
index cafde4a52..95779eaa4 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -3012,8 +3012,6 @@
},
"correlationRow": {
"title": "Corrélations",
- "experimentCta": "Lancer une expérience de 7 jours",
- "experimentTooltip": "Bientôt disponible",
"emptyTitle": "Il faut plus de données pour afficher cette corrélation",
"sourceChip": "sur la base de {n} mesures appariées ({window})",
"sourceWindowLast30": "30 derniers jours",
@@ -5174,7 +5172,9 @@
"connectCta": "Connecter un appareil",
"logCta": "Enregistrez votre première mesure",
"importCta": "Importer un historique existant",
- "modulesCta": "Choisir vos modules"
+ "modulesCta": "Choisir vos modules",
+ "aiOptional": "L'IA est facultative. Tout fonctionne aussi sans elle.",
+ "aiSetupCta": "Configurer l'IA"
},
"welcomeBack": {
"title": "Bon retour",
@@ -5195,37 +5195,6 @@
"allergiesPlaceholder": "p. ex. pénicilline, lactose",
"privacyNote": "Stocké chiffré et utilisé uniquement pour un meilleur contexte. Nous ne demandons pas vos médicaments ici : ajoutez-les quand vous voulez depuis la page Médicaments."
},
- "ai": {
- "panelTitle": "Voyez à quoi ressemblent les insights",
- "panelIntro": "Coach, le briefing quotidien et les lectures par mesure traduisent vos propres chiffres en langage clair. Voici l'idée, avant de connecter quoi que ce soit.",
- "showSample": "Voir un exemple de briefing",
- "sampleTitle": "Briefing quotidien",
- "sampleTag": "Exemple",
- "sampleAriaLabel": "Exemple de briefing quotidien",
- "sampleBody": "Poids en baisse de 1,2 kg en deux semaines. Pouls au repos stable à 58 bpm. Tension dans la plage 6 jours sur 7 ; la seule valeur élevée a suivi un dîner tardif et salé.",
- "sampleCaption": "Exemple uniquement, pas vos données. Voilà à quoi ressemble un briefing une fois les insights activés.",
- "ladderTitle": "Trois façons de l'activer",
- "ladderLocalTitle": "Modèle local : les requêtes d’IA restent sur votre réseau",
- "ladderLocalBody": "Pointez HealthLog vers Ollama ou LM Studio sur votre propre machine. Gratuit et totalement privé, le choix calme par défaut.",
- "ladderByokTitle": "Apportez votre clé",
- "ladderByokBody": "Utilisez votre propre clé OpenAI ou Anthropic. Elle fonctionne avec votre compte et votre facturation, et le niveau API exclut vos données de l'entraînement par défaut.",
- "ladderOauthTitle": "Connectez-vous avec un abonnement",
- "ladderOauthBody": "Reliez un compte ChatGPT, sans coût par jeton, mais les conditions du niveau grand public peuvent utiliser le contenu pour l'entraînement : c'est donc un choix délibéré, jamais l'option par défaut pour vos données de santé.",
- "keylessLine": "HealthLog est pleinement utile sans IA. Vos tuiles, graphiques, tendances et exports fonctionnent tous ; l'IA n'ajoute que la lecture en langage clair par-dessus.",
- "setupCta": "Configurez-le (environ une minute)",
- "sharedKeyNote": "Cette instance fournit déjà un fournisseur d'IA partagé : les insights fonctionnent pour vous dès maintenant, aucune configuration nécessaire.",
- "offer": {
- "title": "Cette instance peut rédiger votre résumé en langage clair.",
- "body": "L’exploitant met à disposition un fournisseur qui a fonctionné au cours des dernières 24 heures. Une seule touche active le langage clair ; vos données partent alors vers ce seul fournisseur et vers personne d’autre.",
- "cta": "Activer le langage clair",
- "granted": "Activé. Le premier résumé apparaîtra sur votre tableau de bord.",
- "error": "L’activation n’a pas abouti. Réessayez dans un instant."
- },
- "unavailable": {
- "title": "Le fournisseur de cette instance ne répond pas pour le moment.",
- "body": "HealthLog reste pleinement utile sans le langage clair. Vous pourrez ajouter votre propre fournisseur plus tard dans les paramètres."
- }
- },
"steps": {
"who": "À qui appartient ce dossier",
"areas": "Ce que vous voulez suivre",
@@ -11175,7 +11144,6 @@
"title": "Plans",
"pageDescription": "Plans « si… alors » convenus dans vos conversations avec le Coach. Confirmez une proposition pour que le Coach s'en souvienne.",
"readOnlyNote": "Le Coach n'est pas disponible pour le moment, mais vous pouvez toujours lire et supprimer vos plans.",
- "proposalTitle": "Proposition de plan",
"ifThen": "Si {cue}, alors {action}.",
"targetPrefix": "Objectif : {target}",
"reviewPrefix": "Point d'étape : {when}",
diff --git a/messages/it.json b/messages/it.json
index a2db0d7ea..1bc25ec4c 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -3012,8 +3012,6 @@
},
"correlationRow": {
"title": "Correlazioni",
- "experimentCta": "Avvia esperimento di 7 giorni",
- "experimentTooltip": "In arrivo",
"emptyTitle": "Servono più dati per mostrare questa correlazione",
"sourceChip": "su {n} misurazioni appaiate ({window})",
"sourceWindowLast30": "Ultimi 30 giorni",
@@ -5174,7 +5172,9 @@
"connectCta": "Collega un dispositivo",
"logCta": "Registra la tua prima misurazione",
"importCta": "Importa la cronologia esistente",
- "modulesCta": "Scegli i tuoi moduli"
+ "modulesCta": "Scegli i tuoi moduli",
+ "aiOptional": "L'IA è facoltativa. Tutto funziona anche senza.",
+ "aiSetupCta": "Configura IA"
},
"welcomeBack": {
"title": "Bentornato",
@@ -5195,37 +5195,6 @@
"allergiesPlaceholder": "es. penicillina, lattosio",
"privacyNote": "Memorizzato cifrato e usato solo per darti un contesto migliore. Qui non chiediamo i tuoi farmaci: aggiungili quando vuoi dalla pagina Farmaci."
},
- "ai": {
- "panelTitle": "Guarda come sono gli insight",
- "panelIntro": "Coach, il briefing quotidiano e le letture per metrica traducono i tuoi numeri in linguaggio chiaro. Ecco com'è, prima di collegare qualsiasi cosa.",
- "showSample": "Vedi un briefing di esempio",
- "sampleTitle": "Briefing quotidiano",
- "sampleTag": "Esempio",
- "sampleAriaLabel": "Briefing quotidiano di esempio",
- "sampleBody": "Peso in calo di 1,2 kg in due settimane. Battito a riposo stabile a 58 bpm. Pressione nel range in 6 giorni su 7: l'unico valore alto è arrivato dopo una cena tardiva e salata.",
- "sampleCaption": "Solo un esempio, non i tuoi dati. Ecco come si legge un briefing quando gli insight sono attivi.",
- "ladderTitle": "Tre modi per attivarlo",
- "ladderLocalTitle": "Modello locale: le richieste all’IA restano nella tua rete",
- "ladderLocalBody": "Punta HealthLog su Ollama o LM Studio sulla tua macchina. Gratis e del tutto privato: la scelta tranquilla di default.",
- "ladderByokTitle": "Usa la tua chiave",
- "ladderByokBody": "Usa la tua chiave OpenAI o Anthropic. Gira sul tuo account e sulla tua fatturazione, e il piano API tiene i tuoi dati fuori dall'addestramento in modo predefinito.",
- "ladderOauthTitle": "Accedi con un abbonamento",
- "ladderOauthBody": "Collega un account ChatGPT. Nessun costo per token, ma i termini del piano consumer possono usare i contenuti per l'addestramento, quindi è una scelta deliberata, mai l'impostazione predefinita per i tuoi dati sanitari.",
- "keylessLine": "HealthLog è pienamente utile anche senza IA. Le tue schede, i grafici, i trend e le esportazioni funzionano tutti; l'IA aggiunge solo la lettura in linguaggio chiaro.",
- "setupCta": "Configuralo (circa un minuto)",
- "sharedKeyNote": "Questa istanza fornisce già un provider IA condiviso, quindi gli insight funzionano per te fin da subito, senza alcuna configurazione.",
- "offer": {
- "title": "Questa istanza può già scrivere il tuo riepilogo in parole semplici.",
- "body": "Il gestore mette a disposizione un fornitore che ha funzionato nelle ultime 24 ore. Con un tocco attivi le parole semplici; i tuoi dati andranno allora a quell’unico fornitore e a nessun altro.",
- "cta": "Attiva le parole semplici",
- "granted": "Attivato. Il primo riepilogo comparirà sulla tua dashboard.",
- "error": "L’attivazione non è riuscita. Riprova tra un momento."
- },
- "unavailable": {
- "title": "Il fornitore di questa istanza al momento non risponde.",
- "body": "HealthLog resta pienamente utile senza le parole semplici. Potrai aggiungere un fornitore tuo più avanti nelle impostazioni."
- }
- },
"steps": {
"who": "A chi appartiene questa cartella",
"areas": "Cosa vuoi tenere d'occhio",
@@ -11175,7 +11144,6 @@
"title": "Piani",
"pageDescription": "Piani «se… allora» concordati nelle conversazioni con il Coach. Conferma una proposta perché il Coach la tenga a mente.",
"readOnlyNote": "Il Coach non è disponibile al momento, ma puoi ancora leggere ed eliminare i tuoi piani.",
- "proposalTitle": "Proposta di piano",
"ifThen": "Se {cue}, allora {action}.",
"targetPrefix": "Obiettivo: {target}",
"reviewPrefix": "Verifica: {when}",
diff --git a/messages/ko.json b/messages/ko.json
index ac7b3cd10..5aeef7e5d 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -3012,8 +3012,6 @@
},
"correlationRow": {
"title": "관계",
- "experimentCta": "7일 실험 해보기",
- "experimentTooltip": "곧 제공돼요",
"emptyTitle": "이 관계를 보려면 데이터가 더 필요해요",
"sourceChip": "짝지은 측정값 {n}개 기준 ({window})",
"sourceWindowLast30": "최근 30일",
@@ -5174,7 +5172,9 @@
"connectCta": "기기 연결",
"logCta": "첫 측정값 기록하기",
"importCta": "기존 기록 불러오기",
- "modulesCta": "모듈 고르기"
+ "modulesCta": "모듈 고르기",
+ "aiOptional": "AI는 선택 사항이에요. AI 없이도 모든 기능이 작동해요.",
+ "aiSetupCta": "AI 설정"
},
"welcomeBack": {
"title": "다시 오셨네요",
@@ -5195,37 +5195,6 @@
"allergiesPlaceholder": "예: 페니실린, 유당",
"privacyNote": "암호화해 저장하고, 더 나은 맥락을 주는 데만 써요. 여기서 약은 묻지 않아요. 복약 페이지에서 언제든 추가하면 돼요."
},
- "ai": {
- "panelTitle": "인사이트가 어떤 모습인지 보기",
- "panelIntro": "코치, 데일리 브리핑, 지표별 해설이 내 숫자를 쉬운 말로 풀어 줘요. 무언가 연결하기 전에, 대략 이런 모습이에요.",
- "showSample": "샘플 브리핑 보기",
- "sampleTitle": "데일리 브리핑",
- "sampleTag": "예시",
- "sampleAriaLabel": "데일리 브리핑 예시",
- "sampleBody": "2주 동안 체중 1.2kg 감소. 안정 시 맥박은 58bpm으로 꾸준해요. 혈압은 7일 중 6일 범위 안, 높았던 하루는 늦은 시간의 짠 저녁 식사 다음이었어요.",
- "sampleCaption": "예시일 뿐 내 데이터가 아니에요. 인사이트를 켜면 브리핑이 이런 식으로 읽혀요.",
- "ladderTitle": "켜는 방법 세 가지",
- "ladderLocalTitle": "로컬 모델: AI 요청이 내 네트워크 안에 머물러요",
- "ladderLocalBody": "내 컴퓨터의 Ollama나 LM Studio를 HealthLog에 연결해요. 무료이고 완전히 비공개라서 가장 마음 편한 기본값이에요.",
- "ladderByokTitle": "내 API 키 사용",
- "ladderByokBody": "본인의 OpenAI나 Anthropic 키를 쓰면 돼요. 내 계정과 결제로 돌아가고, API 등급은 기본적으로 데이터를 학습에 쓰지 않아요.",
- "ladderOauthTitle": "구독 계정으로 로그인",
- "ladderOauthBody": "ChatGPT 계정을 연결해요. 토큰당 비용은 없지만 소비자 등급 약관은 내용을 학습에 쓸 수 있어서, 건강 데이터에는 기본값이 아니라 신중히 고를 선택이에요.",
- "keylessLine": "AI 없이도 HealthLog는 충분히 쓸모 있어요. 타일, 차트, 추이, 내보내기 모두 그대로 작동하고, AI는 그 위에 쉬운 말 해설을 더할 뿐이에요.",
- "setupCta": "설정하기 (1분 정도)",
- "sharedKeyNote": "이 인스턴스에는 이미 공용 AI 제공자가 있어서 지금 바로 인사이트를 쓸 수 있어요. 따로 설정할 게 없어요.",
- "offer": {
- "title": "이 인스턴스가 요약을 쉬운 말로 써 줄 수 있습니다.",
- "body": "운영자가 제공하는 공급자가 지난 24시간 안에 마지막으로 정상 동작했습니다. 한 번만 누르면 쉬운 말 요약이 켜지며, 그러면 데이터는 그 하나의 공급자에게만 전달되고 다른 어느 곳에도 가지 않습니다.",
- "cta": "쉬운 말 요약 켜기",
- "granted": "켜졌습니다. 첫 요약은 대시보드에 표시됩니다.",
- "error": "켜지지 않았습니다. 잠시 후에 다시 시도해 주세요."
- },
- "unavailable": {
- "title": "이 인스턴스의 공급자가 지금은 응답하지 않습니다.",
- "body": "쉬운 말 요약 없이도 HealthLog는 완전히 쓸모가 있습니다. 나중에 설정에서 직접 공급자를 등록할 수 있습니다."
- }
- },
"steps": {
"who": "이 기록의 대상",
"areas": "관심 있게 볼 항목",
@@ -11175,7 +11144,6 @@
"title": "계획",
"pageDescription": "코치와의 대화에서 합의한 조건-실행 계획이에요. 제안을 확정하면 코치가 기억해요.",
"readOnlyNote": "지금은 코치를 사용할 수 없지만 계획은 계속 읽고 삭제할 수 있습니다.",
- "proposalTitle": "계획 제안",
"ifThen": "만약 {cue}, 그러면 {action}.",
"targetPrefix": "목표: {target}",
"reviewPrefix": "점검: {when}",
diff --git a/messages/nl.json b/messages/nl.json
index 1a5fd2353..e0a3f8535 100644
--- a/messages/nl.json
+++ b/messages/nl.json
@@ -3012,8 +3012,6 @@
},
"correlationRow": {
"title": "Verbanden",
- "experimentCta": "Probeer een experiment van 7 dagen",
- "experimentTooltip": "Binnenkort beschikbaar",
"emptyTitle": "Meer gegevens nodig om dit verband te zien",
"sourceChip": "gebaseerd op {n} gekoppelde metingen ({window})",
"sourceWindowLast30": "afgelopen 30 dagen",
@@ -5174,7 +5172,9 @@
"connectCta": "Een apparaat koppelen",
"logCta": "Je eerste meting vastleggen",
"importCta": "Bestaande geschiedenis importeren",
- "modulesCta": "Je modules kiezen"
+ "modulesCta": "Je modules kiezen",
+ "aiOptional": "AI is optioneel. Alles werkt ook zonder.",
+ "aiSetupCta": "AI instellen"
},
"welcomeBack": {
"title": "Welkom terug",
@@ -5195,37 +5195,6 @@
"allergiesPlaceholder": "bijv. penicilline, lactose",
"privacyNote": "Versleuteld opgeslagen en alleen gebruikt om je betere context te geven. We vragen hier niet naar je medicatie. Die kun je altijd toevoegen via de pagina Medicatie."
},
- "ai": {
- "panelTitle": "Bekijk hoe inzichten eruitzien",
- "panelIntro": "Coach, de dagelijkse briefing en de toelichting per meetwaarde zetten je eigen cijfers om in begrijpelijke taal. Zo ziet dat eruit, nog voordat je iets koppelt.",
- "showSample": "Voorbeeldbriefing bekijken",
- "sampleTitle": "Dagelijkse briefing",
- "sampleTag": "Voorbeeld",
- "sampleAriaLabel": "Voorbeeld van een dagelijkse briefing",
- "sampleBody": "Gewicht in twee weken 1,2 kg gedaald. Rusthartslag stabiel op 58 bpm. Bloeddruk binnen het bereik op 6 van de 7 dagen; de ene hoge meting volgde op een laat, zout avondeten.",
- "sampleCaption": "Alleen een voorbeeld, niet je eigen gegevens. Zo leest een briefing zodra inzichten aanstaan.",
- "ladderTitle": "Drie manieren om het aan te zetten",
- "ladderLocalTitle": "Lokaal model: AI-verzoeken blijven op je eigen netwerk",
- "ladderLocalBody": "Verwijs HealthLog naar Ollama of LM Studio op je eigen computer. Gratis en volledig privé, de rustige standaardkeuze.",
- "ladderByokTitle": "Je eigen sleutel gebruiken",
- "ladderByokBody": "Gebruik je eigen OpenAI- of Anthropic-sleutel. Die draait onder je eigen account en facturering, en via de API worden je gegevens standaard niet gebruikt voor training.",
- "ladderOauthTitle": "Inloggen met een abonnement",
- "ladderOauthBody": "Koppel een ChatGPT-account. Geen kosten per token, maar volgens de voorwaarden voor consumenten mag inhoud voor training worden gebruikt. Het is dus een bewuste keuze, nooit de standaard voor je gezondheidsgegevens.",
- "keylessLine": "HealthLog is ook zonder AI volledig bruikbaar. Je tegels, grafieken, trends en exports werken allemaal. AI voegt alleen de toelichting in begrijpelijke taal toe.",
- "setupCta": "Instellen (ongeveer een minuut)",
- "sharedKeyNote": "Deze instantie biedt al een gedeelde AI-provider, dus inzichten werken nu al voor je, zonder instellen.",
- "offer": {
- "title": "Deze instantie kan je samenvatting in begrijpelijke taal schrijven.",
- "body": "De beheerder stelt een provider beschikbaar die in de afgelopen 24 uur nog werkte. Met één tik zet je de toelichting in begrijpelijke taal aan; je gegevens gaan dan naar die ene provider en naar niemand anders.",
- "cta": "Begrijpelijke taal aanzetten",
- "granted": "Aangezet. De eerste samenvatting verschijnt op je dashboard.",
- "error": "Aanzetten is niet gelukt. Probeer het zo meteen nog eens."
- },
- "unavailable": {
- "title": "De provider van deze instantie reageert op dit moment niet.",
- "body": "HealthLog is ook zonder de toelichting in begrijpelijke taal volledig bruikbaar. Je kunt later onder Instellingen een eigen provider toevoegen."
- }
- },
"steps": {
"who": "Voor wie dit dossier is",
"areas": "Wat je in de gaten wilt houden",
@@ -11175,7 +11144,6 @@
"title": "Plannen",
"pageDescription": "Als-dan-plannen die je in je gesprekken met de Coach hebt afgesproken. Bevestig een voorstel om het onderdeel te maken van het geheugen van de Coach.",
"readOnlyNote": "De Coach is nu niet beschikbaar, maar je kunt je plannen nog wel lezen en verwijderen.",
- "proposalTitle": "Planvoorstel",
"ifThen": "Als {cue}, dan {action}.",
"targetPrefix": "Doel: {target}",
"reviewPrefix": "Evaluatie: {when}",
diff --git a/messages/pl.json b/messages/pl.json
index bf2a68591..d35c7b385 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -3012,8 +3012,6 @@
},
"correlationRow": {
"title": "Korelacje",
- "experimentCta": "Uruchom 7-dniowy eksperyment",
- "experimentTooltip": "Wkrótce",
"emptyTitle": "Potrzeba więcej danych, aby pokazać tę korelację",
"sourceChip": "na podstawie {n} sparowanych pomiarów ({window})",
"sourceWindowLast30": "Ostatnie 30 dni",
@@ -5174,7 +5172,9 @@
"connectCta": "Połącz urządzenie",
"logCta": "Zapisz pierwszy pomiar",
"importCta": "Importuj istniejącą historię",
- "modulesCta": "Wybierz swoje moduły"
+ "modulesCta": "Wybierz swoje moduły",
+ "aiOptional": "AI jest opcjonalne. Wszystko działa także bez niego.",
+ "aiSetupCta": "Skonfiguruj AI"
},
"welcomeBack": {
"title": "Witaj ponownie",
@@ -5195,37 +5195,6 @@
"allergiesPlaceholder": "np. penicylina, laktoza",
"privacyNote": "Przechowywane w postaci zaszyfrowanej i używane tylko po to, by dać Ci lepszy kontekst. Nie pytamy tu o leki, dodasz je w dowolnej chwili na stronie Leki."
},
- "ai": {
- "panelTitle": "Zobacz, jak wyglądają insighty",
- "panelIntro": "Coach, codzienny briefing i oceny dla poszczególnych metryk zamieniają Twoje własne liczby w prosty język. Oto jak to wygląda, zanim cokolwiek podłączysz.",
- "showSample": "Zobacz przykładowy briefing",
- "sampleTitle": "Codzienny briefing",
- "sampleTag": "Przykład",
- "sampleAriaLabel": "Przykładowy codzienny briefing",
- "sampleBody": "Waga niższa o 1,2 kg w ciągu dwóch tygodni. Tętno spoczynkowe stabilne na 58 bpm. Ciśnienie w normie przez 6 z 7 dni; jedyny wysoki odczyt nastąpił po późnej, słonej kolacji.",
- "sampleCaption": "Tylko przykład, to nie są Twoje dane. Tak czyta się briefing, gdy insighty są włączone.",
- "ladderTitle": "Trzy sposoby, by to włączyć",
- "ladderLocalTitle": "Model lokalny: zapytania do AI zostają w Twojej sieci",
- "ladderLocalBody": "Skieruj HealthLog na Ollama lub LM Studio na własnej maszynie. Za darmo i w pełni prywatnie: spokojny wybór domyślny.",
- "ladderByokTitle": "Użyj własnego klucza",
- "ladderByokBody": "Użyj własnego klucza OpenAI lub Anthropic. Działa na Twoim koncie i rozliczeniu, a poziom API domyślnie wyklucza Twoje dane z treningu.",
- "ladderOauthTitle": "Zaloguj się przez subskrypcję",
- "ladderOauthBody": "Podłącz konto ChatGPT. Bez kosztu za token, ale warunki poziomu konsumenckiego mogą wykorzystywać treści do treningu, więc to świadomy wybór, nigdy domyślny dla danych o zdrowiu.",
- "keylessLine": "HealthLog jest w pełni użyteczny bez AI. Twoje kafelki, wykresy, trendy i eksporty działają; AI dokłada tylko odczyt prostym językiem.",
- "setupCta": "Skonfiguruj (około minuty)",
- "sharedKeyNote": "Ta instancja udostępnia już wspólnego dostawcę AI, więc insighty działają dla Ciebie od razu, bez konfiguracji.",
- "offer": {
- "title": "Ta instancja potrafi już napisać twoje podsumowanie zwykłym językiem.",
- "body": "Operator udostępnia dostawcę, który działał po raz ostatni w ciągu ostatnich 24 godzin. Jedno dotknięcie włącza zwykły język; twoje dane trafią wtedy do tego jednego dostawcy i do nikogo więcej.",
- "cta": "Włącz zwykły język",
- "granted": "Włączone. Pierwsze podsumowanie pojawi się na pulpicie.",
- "error": "Włączenie się nie powiodło. Spróbuj ponownie za chwilę."
- },
- "unavailable": {
- "title": "Dostawca tej instancji w tej chwili nie odpowiada.",
- "body": "HealthLog jest w pełni użyteczny bez zwykłego języka. Później możesz dodać w ustawieniach własnego dostawcę."
- }
- },
"steps": {
"who": "Dla kogo jest ta kartoteka",
"areas": "Co chcesz obserwować",
@@ -11175,7 +11144,6 @@
"title": "Plany",
"pageDescription": "Plany typu „jeśli… to” uzgodnione w rozmowach z Coachem. Potwierdź propozycję, aby Coach ją zapamiętał.",
"readOnlyNote": "Coach jest teraz niedostępny, ale nadal możesz czytać i usuwać swoje plany.",
- "proposalTitle": "Propozycja planu",
"ifThen": "Jeśli {cue}, to {action}.",
"targetPrefix": "Cel: {target}",
"reviewPrefix": "Przegląd: {when}",
diff --git a/messages/pt-BR.json b/messages/pt-BR.json
index f49440a12..05e875766 100644
--- a/messages/pt-BR.json
+++ b/messages/pt-BR.json
@@ -3012,8 +3012,6 @@
},
"correlationRow": {
"title": "Relações",
- "experimentCta": "Tente um experimento de 7 dias",
- "experimentTooltip": "Em breve",
"emptyTitle": "Precisa de mais dados para ver esta relação",
"sourceChip": "com base em {n} leituras pareadas ({window})",
"sourceWindowLast30": "últimos 30 dias",
@@ -5174,7 +5172,9 @@
"connectCta": "Conectar um dispositivo",
"logCta": "Registrar sua primeira medição",
"importCta": "Importar histórico existente",
- "modulesCta": "Escolher seus módulos"
+ "modulesCta": "Escolher seus módulos",
+ "aiOptional": "A IA é opcional. Tudo funciona também sem ela.",
+ "aiSetupCta": "Configurar IA"
},
"welcomeBack": {
"title": "Bem-vindo de volta",
@@ -5195,37 +5195,6 @@
"allergiesPlaceholder": "ex.: penicilina, lactose",
"privacyNote": "Armazenado criptografado e usado apenas para dar melhor contexto. Não pedimos seus medicamentos aqui. Adicione-os a qualquer momento na página Medicamentos."
},
- "ai": {
- "panelTitle": "Veja como os insights ficam",
- "panelIntro": "Coach, o briefing diário e as leituras por métrica transformam seus próprios números em linguagem simples. Aqui está a forma disso, antes de conectar qualquer coisa.",
- "showSample": "Ver um briefing de exemplo",
- "sampleTitle": "Briefing Diário",
- "sampleTag": "Exemplo",
- "sampleAriaLabel": "Exemplo de briefing diário",
- "sampleBody": "Peso caiu 1,2 kg em duas semanas. Pulso em repouso estável em 58 bpm. Pressão arterial na faixa em 6 de 7 dias; a única leitura alta seguiu um jantar tardio e salgado.",
- "sampleCaption": "Apenas exemplo, não seus próprios dados. É assim que um briefing lê quando os insights estão ligados.",
- "ladderTitle": "Três formas de ligar",
- "ladderLocalTitle": "Modelo local: solicitações de IA permanecem na sua rede",
- "ladderLocalBody": "Aponte o HealthLog para Ollama ou LM Studio na sua própria máquina. Gratuito e totalmente privado, o padrão tranquilo.",
- "ladderByokTitle": "Traga sua própria chave",
- "ladderByokBody": "Use sua própria chave OpenAI ou Anthropic. Roda sob sua conta e cobrança, e o nível de API mantém seus dados fora de treinamento por padrão.",
- "ladderOauthTitle": "Entre com uma assinatura",
- "ladderOauthBody": "Conecte uma conta ChatGPT. Sem custo por token, mas os termos do nível consumidor podem usar conteúdo para treinamento, então é uma escolha deliberada, nunca o padrão para seus dados de saúde.",
- "keylessLine": "O HealthLog é totalmente útil sem IA. Seus blocos, gráficos, tendências e exportações funcionam. A IA só adiciona a leitura em linguagem simples por cima.",
- "setupCta": "Configurar (cerca de um minuto)",
- "sharedKeyNote": "Esta instância já fornece um provedor de IA compartilhado, então os insights funcionam para você agora, sem configuração.",
- "offer": {
- "title": "Esta instância pode escrever seu resumo em linguagem simples.",
- "body": "O operador fornece um provedor que funcionou pela última vez nas últimas 24 horas. Um toque liga a leitura em linguagem simples; seus dados vão para esse único provedor e para ninguém mais.",
- "cta": "Ligar linguagem simples",
- "granted": "Ligado. O primeiro resumo aparece no seu painel.",
- "error": "Ligar não funcionou. Tente novamente em instantes."
- },
- "unavailable": {
- "title": "O provedor desta instância não está respondendo agora.",
- "body": "O HealthLog é totalmente útil sem a leitura em linguagem simples. Você pode adicionar um provedor próprio mais tarde nas Configurações."
- }
- },
"steps": {
"who": "Para quem é este registro",
"areas": "Em que prestar atenção",
@@ -11175,7 +11144,6 @@
"title": "Planos",
"pageDescription": "Planos do tipo “se… então…” acordados nas suas conversas com o Coach. Confirme uma proposta para torná-la parte da memória do Coach.",
"readOnlyNote": "O Coach não está disponível agora, mas você ainda pode ler e excluir seus planos.",
- "proposalTitle": "Proposta de plano",
"ifThen": "Se {cue}, então {action}.",
"targetPrefix": "Alvo: {target}",
"reviewPrefix": "Revisão: {when}",
diff --git a/messages/ru.json b/messages/ru.json
index 413260700..c96b8a0b2 100644
--- a/messages/ru.json
+++ b/messages/ru.json
@@ -3012,8 +3012,6 @@
},
"correlationRow": {
"title": "Связи",
- "experimentCta": "Попробовать 7-дневный эксперимент",
- "experimentTooltip": "Скоро",
"emptyTitle": "Нужно больше данных, чтобы увидеть эту связь",
"sourceChip": "на основе {n} парных измерений ({window})",
"sourceWindowLast30": "последние 30 дней",
@@ -5174,7 +5172,9 @@
"connectCta": "Подключить устройство",
"logCta": "Записать первое измерение",
"importCta": "Импортировать существующую историю",
- "modulesCta": "Выбрать модули"
+ "modulesCta": "Выбрать модули",
+ "aiOptional": "ИИ необязателен. Всё работает и без него.",
+ "aiSetupCta": "Настроить ИИ"
},
"welcomeBack": {
"title": "С возвращением",
@@ -5195,37 +5195,6 @@
"allergiesPlaceholder": "например, пенициллин, лактоза",
"privacyNote": "Хранится в зашифрованном виде и используется только для лучшего контекста. Здесь мы не спрашиваем о ваших лекарствах. Добавляйте их в любое время на странице «Лекарства»."
},
- "ai": {
- "panelTitle": "Посмотрите, как выглядит аналитика",
- "panelIntro": "Коуч, ежедневная сводка и заметки по каждому показателю превращают ваши числа в понятный язык. Вот как это выглядит, прежде чем вы что-либо подключите.",
- "showSample": "Посмотреть пример сводки",
- "sampleTitle": "Ежедневная сводка",
- "sampleTag": "Пример",
- "sampleAriaLabel": "Пример ежедневной сводки",
- "sampleBody": "Вес снизился на 1,2 кг за две недели. Пульс в покое стабилен на 58 уд/мин. АД в диапазоне 6 из 7 дней; единственное высокое измерение последовало за поздним солёным ужином.",
- "sampleCaption": "Только пример, не ваши данные. Так выглядит сводка, когда аналитика включена.",
- "ladderTitle": "Три способа включить",
- "ladderLocalTitle": "Локальная модель: запросы ИИ остаются в вашей сети",
- "ladderLocalBody": "Направьте HealthLog на Ollama или LM Studio на вашей машине. Бесплатно и полностью приватно — спокойный выбор по умолчанию.",
- "ladderByokTitle": "Свой ключ",
- "ladderByokBody": "Используйте свой ключ OpenAI или Anthropic. Он работает под вашим аккаунтом и оплатой, а API-тариф по умолчанию не использует ваши данные для обучения.",
- "ladderOauthTitle": "Вход по подписке",
- "ladderOauthBody": "Подключите аккаунт ChatGPT. Без оплаты за токены, но условия потребительского тарифа могут использовать контент для обучения, поэтому это осознанный выбор, а не значение по умолчанию для ваших данных о здоровье.",
- "keylessLine": "HealthLog полностью полезен и без ИИ. Ваши плитки, графики, динамика и экспорт работают. ИИ лишь добавляет понятное описание сверху.",
- "setupCta": "Настроить (около минуты)",
- "sharedKeyNote": "Этот экземпляр уже предоставляет общий провайдер ИИ, поэтому аналитика работает у вас прямо сейчас, без настройки.",
- "offer": {
- "title": "Этот экземпляр может написать вашу сводку понятным языком.",
- "body": "Оператор предоставляет провайдера, который последний раз работал в течение последних 24 часов. Одно нажатие включает понятное описание; ваши данные затем уходят только этому провайдеру и никому больше.",
- "cta": "Включить понятный язык",
- "granted": "Включено. Первая сводка появится на вашей сводке.",
- "error": "Включить не удалось. Попробуйте ещё раз через минуту."
- },
- "unavailable": {
- "title": "Провайдер этого экземпляра сейчас не отвечает.",
- "body": "HealthLog полностью полезен и без понятного описания. Вы можете добавить своего провайдера позже в настройках."
- }
- },
"steps": {
"who": "Для кого эта запись",
"areas": "За чем следить",
@@ -11175,7 +11144,6 @@
"title": "Планы",
"pageDescription": "Планы «если — то», согласованные в ваших беседах с Коучем. Подтвердите предложение, чтобы оно стало частью памяти Коуча.",
"readOnlyNote": "Коуч сейчас недоступен, но вы всё ещё можете читать и удалять свои планы.",
- "proposalTitle": "Предложение плана",
"ifThen": "Если {cue}, то {action}.",
"targetPrefix": "Цель: {target}",
"reviewPrefix": "Проверка: {when}",
diff --git a/src/__tests__/baselines/source-file-lines.json b/src/__tests__/baselines/source-file-lines.json
index 7705268f3..7579b4981 100644
--- a/src/__tests__/baselines/source-file-lines.json
+++ b/src/__tests__/baselines/source-file-lines.json
@@ -1,7 +1,7 @@
{
"$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 84,
- "total": 94047,
+ "total": 93880,
"entries": {
"app/medications/page-client.tsx": 839,
"components/charts/mood-chart.tsx": 1166,
@@ -30,7 +30,7 @@
"lib/ai/coach/types.ts": 1008,
"lib/ai/codex-client.ts": 907,
"lib/ai/prompts/insight-generator.ts": 1115,
- "lib/ai/provider.ts": 1628,
+ "lib/ai/provider.ts": 1497,
"lib/analytics/score/reader.ts": 998,
"lib/analytics/sleep-night.ts": 1345,
"lib/analytics/summaries-slice.ts": 1178,
@@ -56,7 +56,7 @@
"lib/import/health-connect/import.ts": 1689,
"lib/import/unzip-export-xml.ts": 825,
"lib/insights/comprehensive-aggregator.ts": 868,
- "lib/insights/comprehensive-generate.ts": 1674,
+ "lib/insights/comprehensive-generate.ts": 1670,
"lib/insights/correlation-channel-series.ts": 1339,
"lib/insights/correlation-discovery.ts": 1240,
"lib/insights/correlations.ts": 1026,
diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json
index 1ab04100f..a6c30e3f9 100644
--- a/src/__tests__/baselines/source-function-lines.json
+++ b/src/__tests__/baselines/source-function-lines.json
@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
- "count": 343,
- "total": 111318,
+ "count": 339,
+ "total": 110080,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275,
@@ -17,7 +17,7 @@
"app/api/devices/route.ts :: apiHandler(arg)": 226,
"app/api/encounters/[id]/route.ts :: apiHandler(arg) #2": 269,
"app/api/environment/route.ts :: apiHandler(arg)": 201,
- "app/api/export/health-record/route.ts :: apiHandler(arg)": 241,
+ "app/api/export/health-record/route.ts :: apiHandler(arg)": 240,
"app/api/ingest/medication/route.ts :: apiHandler(arg)": 259,
"app/api/insights/cards/route.ts :: apiHandler(arg)": 211,
"app/api/integrations/status/route.ts :: apiHandler(arg)": 476,
@@ -73,18 +73,18 @@
"components/charts/trend-card.tsx :: TrendCard": 365,
"components/clinician/clinician-view.tsx :: ClinicianView": 223,
"components/command-palette/command-palette.tsx :: PaletteBody": 333,
- "components/custom-metrics/custom-metric-chart.tsx :: CustomMetricChart": 231,
+ "components/custom-metrics/custom-metric-chart.tsx :: CustomMetricChart": 228,
"components/custom-metrics/custom-metric-detail.tsx :: CustomMetricDetail": 277,
"components/custom-metrics/custom-metric-form.tsx :: CustomMetricForm": 214,
- "components/custom-metrics/custom-metric-history-list.tsx :: CustomMetricHistoryList": 302,
+ "components/custom-metrics/custom-metric-history-list.tsx :: CustomMetricHistoryList": 298,
"components/cycle/bbt-chart.tsx :: BbtChart": 244,
"components/cycle/cycle-calendar.tsx :: CycleCalendar": 292,
"components/cycle/cycle-settings.tsx :: CycleSettings": 228,
- "components/cycle/cycle-view.tsx :: CycleView": 407,
+ "components/cycle/cycle-view.tsx :: CycleView": 405,
"components/cycle/log-day-sheet.tsx :: LogDaySheet": 753,
"components/daily/today-hero.tsx :: TodayHero": 488,
"components/dashboard/dashboard-header.tsx :: DashboardHeader": 215,
- "components/dashboard/medication-intake-quick-add.tsx :: MedicationIntakeQuickAdd": 384,
+ "components/dashboard/medication-intake-quick-add.tsx :: MedicationIntakeQuickAdd": 383,
"components/day/day-layer.tsx :: DayLayer": 358,
"components/day/day-view.tsx :: DayView": 309,
"components/documents/document-ai-section.tsx :: DocumentAiSection": 262,
@@ -129,13 +129,13 @@
"components/insights/sub-page-shell.tsx :: SubPageShell": 266,
"components/insights/trends-row.tsx :: TrendsRow": 215,
"components/labs/biomarker-form.tsx :: BiomarkerForm": 229,
- "components/labs/biomarker-manager.tsx :: BiomarkerManager": 242,
- "components/labs/biomarker-merge-sheet.tsx :: BiomarkerMergeSheet": 239,
+ "components/labs/biomarker-manager.tsx :: BiomarkerManager": 240,
+ "components/labs/biomarker-merge-sheet.tsx :: BiomarkerMergeSheet": 238,
"components/labs/lab-biomarker-chart.tsx :: LabBiomarkerChart": 370,
- "components/labs/lab-biomarker-detail.tsx :: LabBiomarkerDetail": 481,
+ "components/labs/lab-biomarker-detail.tsx :: LabBiomarkerDetail": 476,
"components/labs/lab-form.tsx :: LabForm": 474,
- "components/labs/lab-history-list.tsx :: LabHistoryList": 534,
- "components/labs/ocr-review-dialog.tsx :: OcrReviewDialog": 359,
+ "components/labs/lab-history-list.tsx :: LabHistoryList": 527,
+ "components/labs/ocr-review-dialog.tsx :: OcrReviewDialog": 356,
"components/labs/ocr-row-editor.tsx :: OcrRowEditor": 378,
"components/layout/auth-shell.tsx :: AuthShell": 518,
"components/layout/bottom-nav.tsx :: BottomNav": 262,
@@ -145,7 +145,7 @@
"components/links/entity-link-picker.tsx :: EntityLinkPicker": 392,
"components/measurement-reminders/vorsorge-dashboard-card.tsx :: VorsorgeDashboardCard": 215,
"components/measurement-reminders/vorsorge-section.tsx :: VorsorgeSection": 352,
- "components/measurements/measurement-form.tsx :: MeasurementForm": 500,
+ "components/measurements/measurement-form.tsx :: MeasurementForm": 496,
"components/medications/detail/efficacy/efficacy-chart.tsx :: EfficacyChart": 361,
"components/medications/detail/medication-detail-tabs.tsx :: MedicationDetailTabs": 767,
"components/medications/dose-history-add-dialog.tsx :: LedgerAddDialog": 223,
@@ -159,7 +159,7 @@
"components/medications/medication-table.tsx :: MedicationTableRowItem": 377,
"components/medications/scheduling/natural-language-extractor.tsx :: NaturalLanguageExtractor": 214,
"components/medications/scheduling/schedule-history-timeline.tsx :: EraDialog": 219,
- "components/medications/scheduling/schedule-history-timeline.tsx :: ScheduleHistoryTimeline": 256,
+ "components/medications/scheduling/schedule-history-timeline.tsx :: ScheduleHistoryTimeline": 254,
"components/medications/sections/api-tokens-row.tsx :: ApiTokensRow": 296,
"components/medications/sections/composition-section.tsx :: CompositionEditor": 362,
"components/medications/sections/inventory-dialogs.tsx :: AddInventoryDialog": 238,
@@ -168,19 +168,18 @@
"components/medications/wizard/medication-wizard-dialog.tsx :: WizardDialogShell": 516,
"components/medications/wizard/steps/step3-dose.tsx :: Step3Dose": 222,
"components/medications/wizard/steps/step8-summary.tsx :: Step8Summary": 230,
- "components/mental-health/mental-wellbeing.tsx :: MentalWellbeing": 202,
+ "components/mental-health/mental-wellbeing.tsx :: MentalWellbeing": 201,
"components/mood/manage/tag-groups-card.tsx :: TagGroupsCard": 256,
"components/mood/manage/tag-manager-card.tsx :: TagManagerCard": 270,
"components/mood/mood-form.tsx :: MoodForm": 514,
"components/mood/mood-list.tsx :: MoodList": 1109,
"components/mood/mood-tag-picker.tsx :: MoodTagPicker": 257,
"components/onboarding/confirm-screen.tsx :: ConfirmScreen": 255,
- "components/onboarding/done-screen.tsx :: DoneScreen": 408,
"components/onboarding/getting-started-checklist.tsx :: GettingStartedChecklist": 423,
"components/onboarding/tour.tsx :: OnboardingTour": 544,
- "components/practitioners/practitioner-list.tsx :: PractitionerList": 230,
+ "components/practitioners/practitioner-list.tsx :: PractitionerList": 225,
"components/provenance/source-provenance-view.tsx :: SourceProvenanceView": 363,
- "components/records/allergy-form.tsx :: AllergyForm": 225,
+ "components/records/allergy-form.tsx :: AllergyForm": 222,
"components/records/audit-c-panel.tsx :: AuditCPanel": 285,
"components/records/life-circumstances-manager.tsx :: SectionBody": 472,
"components/settings/about-section.tsx :: AboutSection": 236,
@@ -229,18 +228,18 @@
"components/timeline/life-event-form.tsx :: LifeEventForm": 281,
"components/timeline/timeline-chart.tsx :: ChartBody": 236,
"components/timeline/timeline-chronicle.tsx :: TimelineChronicle": 246,
- "components/timeline/timeline-view.tsx :: TimelineView": 486,
+ "components/timeline/timeline-view.tsx :: TimelineView": 478,
"components/ui/responsive-sheet.tsx :: ResponsiveSheet": 239,
"components/vaccinations/catalog-picker.tsx :: CatalogPicker": 221,
"components/vaccinations/custom-vaccine-sheet.tsx :: CustomVaccineSheet": 246,
"components/vaccinations/vaccination-form.tsx :: VaccinationForm": 218,
- "components/workouts/manual-workout-form.tsx :: ManualWorkoutForm": 323,
+ "components/workouts/manual-workout-form.tsx :: ManualWorkoutForm": 321,
"hooks/use-sheet-drag.ts :: createSheetDrag": 312,
"lib/activity/calendar-math.ts :: buildActivityCalendar": 204,
"lib/ai/anthropic-client.ts :: generateCompletion": 294,
"lib/ai/coach/cycle-snapshot.ts :: buildCycleSnapshotBlock": 202,
"lib/ai/coach/glp1-snapshot.ts :: buildGlp1SnapshotBlock": 218,
- "lib/ai/coach/snapshot.ts :: buildCoachSnapshotImpl": 880,
+ "lib/ai/coach/snapshot.ts :: buildCoachSnapshotImpl": 867,
"lib/ai/coach/tools/loop.ts :: runCoachToolLoop": 438,
"lib/ai/coach/turn/model.ts :: runTurnModel": 534,
"lib/ai/coach/turn/pipeline.ts :: runTurn": 274,
@@ -266,7 +265,7 @@
"lib/doctor-report-pdf/clinical-records-notes-section.ts :: buildClinicalRecordsNotesSection": 648,
"lib/doctor-report-pdf/measurements-charts-section.ts :: buildMeasurementsChartsSection": 384,
"lib/doctor-report-pdf/medication-mood-wellness-section.ts :: buildMedicationMoodWellnessSection": 509,
- "lib/doctor-report/collect.ts :: collectDoctorReportData": 667,
+ "lib/doctor-report/collect.ts :: collectDoctorReportData": 665,
"lib/documents/fenced-chat.ts :: streamFencedReply": 309,
"lib/documents/ingest.ts :: ingestDocument": 272,
"lib/export/backup-json-scan.ts :: scanBackupJson": 225,
@@ -281,11 +280,11 @@
"lib/google-health/data-points.ts :: forEachDataPointPage > walk": 223,
"lib/google-health/sync-core.ts :: upsertGoogleHealthMeasurements": 452,
"lib/illness/correlation.ts :: computeIllnessCorrelation": 233,
- "lib/import/labs-csv.ts :: importLabsCsv": 307,
- "lib/import/labs-csv.ts :: importLabsCsv > flush": 229,
+ "lib/import/labs-csv.ts :: importLabsCsv": 305,
+ "lib/import/labs-csv.ts :: importLabsCsv > flush": 227,
"lib/insights/comprehensive-aggregator.ts :: buildFromRollups": 217,
- "lib/insights/comprehensive-generate.ts :: generateComprehensiveInsight": 876,
- "lib/insights/features.ts :: extractFeatures": 796,
+ "lib/insights/comprehensive-generate.ts :: generateComprehensiveInsight": 872,
+ "lib/insights/features.ts :: extractFeatures": 794,
"lib/insights/mood-aggregates.ts :: fetchMoodAggregates": 246,
"lib/insights/status-provider.ts :: runStatusCompletion": 215,
"lib/integrations/connection-health-load.ts :: loadConnectionFacts": 229,
@@ -304,7 +303,7 @@
"lib/jobs/measurement-reminder.ts :: runMeasurementReminderTick": 286,
"lib/jobs/medication-intake-import.ts :: processNextChunk": 220,
"lib/jobs/medication-intake-import.ts :: processNextChunk > client.$transaction(arg)": 212,
- "lib/jobs/medication-low-stock.ts :: runMedicationLowStockTick": 286,
+ "lib/jobs/medication-low-stock.ts :: runMedicationLowStockTick": 283,
"lib/jobs/oura-backfill.ts :: runOuraBackfillForUser": 236,
"lib/jobs/reaction-line.ts :: runReactionLine": 337,
"lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck": 592,
@@ -342,6 +341,6 @@
"lib/withings/sync-ecg.ts :: syncUserEcg": 202,
"lib/withings/sync-sleep.ts :: syncUserSleep": 414,
"lib/withings/sync.ts :: syncUserMeasurements": 337,
- "proxy.ts :: proxy": 471
+ "proxy.ts :: proxy": 470
}
}
diff --git a/src/__tests__/baselines/test-as-never.json b/src/__tests__/baselines/test-as-never.json
index 20b560254..ef945bea3 100644
--- a/src/__tests__/baselines/test-as-never.json
+++ b/src/__tests__/baselines/test-as-never.json
@@ -1,7 +1,7 @@
{
"$comment": "as never casts in tests, per test file under src/. Maintained by src/__tests__/test-realism-ratchet.test.ts.",
"count": 523,
- "total": 5475,
+ "total": 5459,
"entries": {
"__tests__/api/auth/login/mfa-branch.test.ts": 10,
"__tests__/api/auth/mfa/verify.test.ts": 9,
@@ -196,7 +196,7 @@
"app/api/medications/[id]/intake/import/__tests__/route.test.ts": 9,
"app/api/medications/[id]/schedule-revisions/__tests__/route.test.ts": 24,
"app/api/medications/compliance/__tests__/route.test.ts": 10,
- "app/api/medications/extract/__tests__/route.test.ts": 38,
+ "app/api/medications/extract/__tests__/route.test.ts": 37,
"app/api/medications/intake/__tests__/route.test.ts": 59,
"app/api/medications/intake/bulk/__tests__/dose-arrival-scope.test.ts": 9,
"app/api/medications/intake/bulk/__tests__/route.test.ts": 68,
@@ -300,7 +300,7 @@
"lib/ai/__tests__/provider-reasoning-effort.test.ts": 5,
"lib/ai/__tests__/provider-response-timeout.test.ts": 2,
"lib/ai/__tests__/provider.test.ts": 35,
- "lib/ai/__tests__/resolve-chain.test.ts": 12,
+ "lib/ai/__tests__/resolve-chain.test.ts": 2,
"lib/ai/__tests__/server-provider-health.test.ts": 1,
"lib/ai/capabilities/__tests__/egress.test.ts": 1,
"lib/ai/capabilities/__tests__/gate.test.ts": 1,
@@ -385,7 +385,7 @@
"lib/import/__tests__/record-import-route.test.ts": 1,
"lib/insights/__tests__/background-reasoning-wire.test.ts": 2,
"lib/insights/__tests__/biomarker-status.test.ts": 23,
- "lib/insights/__tests__/blood-pressure-status.test.ts": 32,
+ "lib/insights/__tests__/blood-pressure-status.test.ts": 27,
"lib/insights/__tests__/bmi-status.test.ts": 21,
"lib/insights/__tests__/briefing-provider-budget.test.ts": 2,
"lib/insights/__tests__/comprehensive-briefing-reroll.test.ts": 1,
diff --git a/src/__tests__/baselines/version-stamp-comments.json b/src/__tests__/baselines/version-stamp-comments.json
index 0804205bb..642303b6b 100644
--- a/src/__tests__/baselines/version-stamp-comments.json
+++ b/src/__tests__/baselines/version-stamp-comments.json
@@ -1,7 +1,7 @@
{
"$comment": "Comment lines that open with a release version, per file under src/. Maintained by src/__tests__/version-stamp-ratchet.test.ts.",
- "count": 2892,
- "total": 9213,
+ "count": 2884,
+ "total": 9191,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 2,
"__tests__/acting-account-boundary-guard.test.ts": 7,
@@ -291,7 +291,7 @@
"app/api/insights/bmi-status/route.ts": 2,
"app/api/insights/breathing-screening/route.ts": 2,
"app/api/insights/cards/__tests__/route.test.ts": 2,
- "app/api/insights/cards/route.ts": 9,
+ "app/api/insights/cards/route.ts": 8,
"app/api/insights/chat/__tests__/dialog-harness.ts": 1,
"app/api/insights/chat/__tests__/golden-transcripts.test.ts": 1,
"app/api/insights/chat/__tests__/route-activity.test.ts": 1,
@@ -331,7 +331,7 @@
"app/api/insights/ecg/[id]/route.ts": 2,
"app/api/insights/ecg/route.ts": 1,
"app/api/insights/feedback/route.ts": 2,
- "app/api/insights/generate/__tests__/route.test.ts": 12,
+ "app/api/insights/generate/__tests__/route.test.ts": 11,
"app/api/insights/glp1-plateau/route.ts": 2,
"app/api/insights/glp1-timeline/route.ts": 4,
"app/api/insights/health-status/route.ts": 3,
@@ -1252,7 +1252,6 @@
"components/onboarding/__tests__/anamnesis-card.test.tsx": 1,
"components/onboarding/__tests__/confirm-failure-recovery.test.ts": 1,
"components/onboarding/__tests__/confirm-screen.test.tsx": 1,
- "components/onboarding/__tests__/done-screen.test.tsx": 1,
"components/onboarding/__tests__/first-result-screen.test.tsx": 2,
"components/onboarding/__tests__/getting-started-checklist.test.tsx": 1,
"components/onboarding/__tests__/question-screen.test.tsx": 1,
@@ -1263,7 +1262,7 @@
"components/onboarding/baseline-fields.tsx": 1,
"components/onboarding/baseline-form.tsx": 3,
"components/onboarding/confirm-screen.tsx": 5,
- "components/onboarding/done-screen.tsx": 2,
+ "components/onboarding/done-screen.tsx": 1,
"components/onboarding/first-result-screen.tsx": 3,
"components/onboarding/getting-started-checklist.tsx": 13,
"components/onboarding/onboarding-shell.tsx": 1,
@@ -1629,7 +1628,6 @@
"lib/ai/coach/eval/red-team.ts": 3,
"lib/ai/coach/eval/run-case.ts": 1,
"lib/ai/coach/eval/scenarios.ts": 10,
- "lib/ai/coach/experiment-flag.ts": 1,
"lib/ai/coach/facts.ts": 6,
"lib/ai/coach/follow-ups/__tests__/continue.test.ts": 1,
"lib/ai/coach/follow-ups/__tests__/derive.test.ts": 1,
@@ -1658,7 +1656,7 @@
"lib/ai/coach/memory/trail-read.ts": 1,
"lib/ai/coach/memory/turn-writes.ts": 1,
"lib/ai/coach/persistence.ts": 37,
- "lib/ai/coach/plans.ts": 5,
+ "lib/ai/coach/plans.ts": 4,
"lib/ai/coach/prompt-cache-key.ts": 1,
"lib/ai/coach/reference-grounding.ts": 1,
"lib/ai/coach/reminders.ts": 2,
@@ -2200,7 +2198,7 @@
"lib/insights/exclude-filter.ts": 1,
"lib/insights/feature-blocks.ts": 5,
"lib/insights/feature-cache.ts": 2,
- "lib/insights/features.ts": 34,
+ "lib/insights/features.ts": 33,
"lib/insights/general-status.ts": 5,
"lib/insights/glp1-plateau.ts": 2,
"lib/insights/graded-series.ts": 3,
@@ -2291,7 +2289,6 @@
"lib/jobs/coach-message-cleanup.ts": 1,
"lib/jobs/coach-nudge-ai.ts": 3,
"lib/jobs/coach-nudge.ts": 17,
- "lib/jobs/coach-plan-review.ts": 1,
"lib/jobs/coach-reminder-sweep.ts": 2,
"lib/jobs/cycle-reminder.ts": 2,
"lib/jobs/data-arrival.ts": 2,
diff --git a/src/__tests__/consent-grant-explicit-guard.test.ts b/src/__tests__/consent-grant-explicit-guard.test.ts
index 110e9c2ad..f741a8264 100644
--- a/src/__tests__/consent-grant-explicit-guard.test.ts
+++ b/src/__tests__/consent-grant-explicit-guard.test.ts
@@ -154,9 +154,10 @@ describe("AI consent is granted only by the person's own tap", () => {
}
}
}
- // Non-zero floors: a matcher that stops matching is red, not green.
- expect(callers).toBeGreaterThanOrEqual(3);
- expect(sites).toBeGreaterThanOrEqual(3);
+ // Non-zero floors: a matcher that stops matching is red, not green. Two
+ // callers today: the consent request and the document-reading prompt.
+ expect(callers).toBeGreaterThanOrEqual(2);
+ expect(sites).toBeGreaterThanOrEqual(2);
});
it("keeps both grant routes refusing a request without the confirmation", () => {
diff --git a/src/app/api/insights/cards/route.ts b/src/app/api/insights/cards/route.ts
index 04779c3ba..87cd8ee77 100644
--- a/src/app/api/insights/cards/route.ts
+++ b/src/app/api/insights/cards/route.ts
@@ -19,7 +19,7 @@ import { prisma } from "@/lib/db";
import { summarize, type DataPoint } from "@/lib/analytics/trends";
import { probeRollupCoverage } from "@/lib/rollups/measurement-coverage";
import { readDayMeanSeries } from "@/lib/insights/derived/baseline";
-import { getBpTargets } from "@/lib/analytics/bp-targets";
+import { resolveEffectiveBpTargets } from "@/lib/analytics/effective-range";
import { isBpReadingInTarget } from "@/lib/analytics/bp-in-target";
import {
generateAlerts,
@@ -72,6 +72,17 @@ interface InsightCard {
export const dynamic = "force-dynamic";
+/**
+ * The profile fields BMI and the BP band read: the person's own BP target
+ * where one is set, the age default otherwise.
+ */
+const PROFILE_SELECT = {
+ heightCm: true,
+ dateOfBirth: true,
+ gender: true,
+ thresholdsJson: true,
+} as const;
+
export const GET = apiHandler(async () => {
// v1.37.0 — MANAGE-level read: computed over the whole record, with no
// provider anywhere on the path.
@@ -85,7 +96,7 @@ export const GET = apiHandler(async () => {
await Promise.all([
prisma.user.findUnique({
where: { id: user.id },
- select: { heightCm: true, dateOfBirth: true },
+ select: PROFILE_SELECT,
}),
// WEIGHT + BP are manual entries (low volume, and BP needs exact-
// timestamp pairing below for `bpPctInTarget`), so a bounded raw read
@@ -168,14 +179,14 @@ export const GET = apiHandler(async () => {
}
let bpPctInTarget: number | null = null;
- const bpTargets = getBpTargets(dbUser?.dateOfBirth ?? null);
+ const bpTargets =
+ dbUser && resolveEffectiveBpTargets(dbUser, dbUser.thresholdsJson);
if (bpTargets) {
const sysData = byType("BLOOD_PRESSURE_SYS");
const diaData = byType("BLOOD_PRESSURE_DIA");
const pairs = pairByTimestamp(sysData, diaData, 5 * 60_000);
if (pairs.length > 0) {
- // v1.4.16 — one-sided ceiling semantics with hypotension
- // floor. See lib/analytics/bp-in-target.ts.
+ // One-sided ceiling with a hypotension floor (`bp-in-target.ts`).
const inTarget = pairs.filter((p) =>
isBpReadingInTarget(p.a, p.b, bpTargets),
).length;
diff --git a/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap b/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap
index 0ba896537..67543a14b 100644
--- a/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap
+++ b/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap
@@ -4715,7 +4715,7 @@ exports[`coach chat golden transcripts > 15 an unknown learn link is scrubbed, a
}
`;
-exports[`coach chat golden transcripts > 16a coach.provider.none via an empty chain and no legacy provider 1`] = `
+exports[`coach chat golden transcripts > 16a coach.provider.none via an empty chain 1`] = `
{
"body": null,
"calls": [
@@ -4757,342 +4757,6 @@ exports[`coach chat golden transcripts > 16a coach.provider.none via an empty ch
}
`;
-exports[`coach chat golden transcripts > 16b an empty chain served by the legacy provider 1`] = `
-{
- "body": null,
- "calls": [
- {
- "call": "createConversation",
- },
- {
- "call": "appendMessage",
- "content": "How is my blood pressure?",
- "metricSource": null,
- "model": null,
- "providerType": null,
- "role": "user",
- "tokensUsed": null,
- },
- {
- "call": "storeDeterministicFacts",
- },
- {
- "call": "enqueueCoachMemoryRefresh",
- },
- {
- "call": "assertConsentForChain",
- "chain": [
- "admin-openai",
- ],
- },
- {
- "amount": 600,
- "call": "reserveBudget",
- "cap": 2000000,
- "dateKey": "2026-09-26",
- "owner": "user",
- "surface": "coach",
- },
- {
- "call": "runStreamingRawCompletionWithFallback",
- "maxTokens": 600,
- "messages": [
- "user:70927bca288b:747",
- ],
- "providers": [
- "admin-openai",
- ],
- "signal": true,
- "surface": "coach",
- "system": "af01480eff5e:1404",
- "temperature": 0.4,
- "timeoutMs": 180000,
- },
- {
- "call": "annotate",
- "metaKeys": [
- "deltas",
- ],
- "name": "coach.stream.deltas",
- },
- {
- "actual": 42,
- "attribution": {
- "reservedOwner": "user",
- "servedBy": "local",
- },
- "cached": 5,
- "call": "reconcileSpend",
- "dateKey": "2026-09-26",
- "reserved": 600,
- },
- {
- "call": "appendMessage",
- "content": "Your systolic averaged 128 mmHg over the window.",
- "metricSource": {
- "activity": [
- {
- "count": 1,
- "durationMs": 0,
- "id": "a1",
- "label": "Reading your record summary",
- "labelKey": "coach.step.snapshot",
- "phase": "fetch",
- "round": 1,
- "status": "done",
- "stepRef": "s1",
- },
- {
- "durationMs": 0,
- "id": "a2",
- "label": "Thinking…",
- "labelKey": "insights.coach.activity.thinking",
- "phase": "thinking",
- "round": 1,
- "status": "done",
- },
- {
- "id": "a3",
- "label": "Writing the answer…",
- "labelKey": "insights.coach.activity.answer",
- "phase": "answer",
- "round": 1,
- "status": "done",
- },
- ],
- "groundedFigures": [
- 128,
- 82,
- 46,
- 105,
- 35.9375,
- 56.09756097560976,
- ],
- "metrics": [
- "bp",
- ],
- "steps": [
- {
- "count": 1,
- "domain": "snapshot",
- "id": "s1",
- "label": "Reading your record summary",
- "labelKey": "coach.step.snapshot",
- "status": "done",
- "tool": "snapshot",
- },
- ],
- "windows": [
- "last30days",
- ],
- },
- "model": "m-local",
- "providerType": "local",
- "role": "assistant",
- "tokensUsed": 42,
- },
- {
- "call": "annotate",
- "metaKeys": [
- "activityEntries",
- "clusterCount",
- "conversationId",
- "historyTurns",
- "promptChars",
- "promptVersion",
- "provider",
- "snapshotSent",
- "stop",
- "tokens",
- "toolMode",
- "toolsCalled",
- "trailStored",
- ],
- "name": "insights.coach.replied",
- },
- ],
- "contentType": "text/event-stream; charset=utf-8",
- "frames": [
- {
- "step": {
- "count": 1,
- "domain": "snapshot",
- "id": "s1",
- "label": "Reading your record summary",
- "labelKey": "coach.step.snapshot",
- "status": "done",
- "tool": "snapshot",
- },
- "type": "step",
- },
- {
- "activity": {
- "count": 1,
- "id": "a1",
- "label": "Reading your record summary",
- "labelKey": "coach.step.snapshot",
- "phase": "fetch",
- "round": 1,
- "status": "running",
- "stepRef": "s1",
- },
- "type": "activity",
- },
- {
- "activity": {
- "count": 1,
- "durationMs": 0,
- "id": "a1",
- "label": "Reading your record summary",
- "labelKey": "coach.step.snapshot",
- "phase": "fetch",
- "round": 1,
- "status": "done",
- "stepRef": "s1",
- },
- "type": "activity",
- },
- {
- "activity": {
- "id": "a2",
- "label": "Thinking…",
- "labelKey": "insights.coach.activity.thinking",
- "phase": "thinking",
- "round": 1,
- "status": "running",
- },
- "type": "activity",
- },
- {
- "activity": {
- "durationMs": 0,
- "id": "a2",
- "label": "Thinking…",
- "labelKey": "insights.coach.activity.thinking",
- "phase": "thinking",
- "round": 1,
- "status": "done",
- },
- "type": "activity",
- },
- {
- "activity": {
- "id": "a3",
- "label": "Writing the answer…",
- "labelKey": "insights.coach.activity.answer",
- "phase": "answer",
- "round": 1,
- "status": "running",
- },
- "type": "activity",
- },
- {
- "token": "Your ",
- "type": "token",
- },
- {
- "token": "systolic ",
- "type": "token",
- },
- {
- "token": "averaged ",
- "type": "token",
- },
- {
- "token": "128 ",
- "type": "token",
- },
- {
- "token": "mmHg ",
- "type": "token",
- },
- {
- "token": "over ",
- "type": "token",
- },
- {
- "token": "the ",
- "type": "token",
- },
- {
- "token": "window.",
- "type": "token",
- },
- {
- "metricSource": {
- "activity": [
- {
- "count": 1,
- "durationMs": 0,
- "id": "a1",
- "label": "Reading your record summary",
- "labelKey": "coach.step.snapshot",
- "phase": "fetch",
- "round": 1,
- "status": "done",
- "stepRef": "s1",
- },
- {
- "durationMs": 0,
- "id": "a2",
- "label": "Thinking…",
- "labelKey": "insights.coach.activity.thinking",
- "phase": "thinking",
- "round": 1,
- "status": "done",
- },
- {
- "id": "a3",
- "label": "Writing the answer…",
- "labelKey": "insights.coach.activity.answer",
- "phase": "answer",
- "round": 1,
- "status": "done",
- },
- ],
- "groundedFigures": [
- 128,
- 82,
- 46,
- 105,
- 35.9375,
- 56.09756097560976,
- ],
- "metrics": [
- "bp",
- ],
- "steps": [
- {
- "count": 1,
- "domain": "snapshot",
- "id": "s1",
- "label": "Reading your record summary",
- "labelKey": "coach.step.snapshot",
- "status": "done",
- "tool": "snapshot",
- },
- ],
- "windows": [
- "last30days",
- ],
- },
- "type": "provenance",
- },
- {
- "conversationId": "c-new",
- "messageId": "m2",
- "type": "done",
- "usage": {
- "model": "m-local",
- "totalTokens": 42,
- },
- },
- ],
- "status": 200,
- "thrown": null,
-}
-`;
-
exports[`coach chat golden transcripts > 16c coach.provider.none from the capability gate 1`] = `
{
"body": null,
diff --git a/src/app/api/insights/chat/__tests__/golden-harness.ts b/src/app/api/insights/chat/__tests__/golden-harness.ts
index e08e7c9aa..d34fb3c1f 100644
--- a/src/app/api/insights/chat/__tests__/golden-harness.ts
+++ b/src/app/api/insights/chat/__tests__/golden-harness.ts
@@ -119,7 +119,6 @@ export const m = {
resolveServerLocale: vi.fn(),
runStreamingRawCompletionWithFallback: vi.fn(),
resolveProviderChain: vi.fn(),
- resolveProvider: vi.fn(),
assertConsentForChain: vi.fn(),
appendMessage: vi.fn(),
createConversation: vi.fn(),
@@ -201,7 +200,6 @@ export function resetGolden(): void {
m.resolveProviderChain.mockResolvedValue([
{ providerType: "anthropic", instance: {} },
]);
- m.resolveProvider.mockResolvedValue({ type: "none" });
m.assertConsentForChain.mockImplementation(
async (args: { chain: Array<{ providerType: string }> }) => {
record("assertConsentForChain", {
diff --git a/src/app/api/insights/chat/__tests__/golden-transcripts.test.ts b/src/app/api/insights/chat/__tests__/golden-transcripts.test.ts
index 563defbfa..47b953db5 100644
--- a/src/app/api/insights/chat/__tests__/golden-transcripts.test.ts
+++ b/src/app/api/insights/chat/__tests__/golden-transcripts.test.ts
@@ -81,7 +81,6 @@ vi.mock("@/lib/ai/provider-runner", () => ({
}));
vi.mock("@/lib/ai/provider", () => ({
resolveProviderChain: h.m.resolveProviderChain,
- resolveProvider: h.m.resolveProvider,
}));
vi.mock("@/lib/ai/consent-guard", () => ({
assertConsentForChain: h.m.assertConsentForChain,
@@ -416,21 +415,8 @@ describe("coach chat golden transcripts", () => {
});
// 16 ── no provider ──────────────────────────────────────────────────────
- it("16a coach.provider.none via an empty chain and no legacy provider", async () => {
+ it("16a coach.provider.none via an empty chain", async () => {
m.resolveProviderChain.mockResolvedValue([]);
- m.resolveProvider.mockResolvedValue({ type: "none" });
- await golden({ message: "How is my blood pressure?" });
- });
-
- it("16b an empty chain served by the legacy provider", async () => {
- m.resolveProviderChain.mockResolvedValue([]);
- m.resolveProvider.mockResolvedValue({
- type: "openai",
- supportsTools: false,
- });
- m.runStreamingRawCompletionWithFallback.mockImplementation(
- streamingResult("Your systolic averaged 128 mmHg over the window."),
- );
await golden({ message: "How is my blood pressure?" });
});
diff --git a/src/app/api/insights/generate/__tests__/route.test.ts b/src/app/api/insights/generate/__tests__/route.test.ts
index 22eb6abba..cef1e81b7 100644
--- a/src/app/api/insights/generate/__tests__/route.test.ts
+++ b/src/app/api/insights/generate/__tests__/route.test.ts
@@ -93,11 +93,7 @@ vi.mock("@/lib/ai/provider", async () => {
);
return {
...actual,
- resolveProvider: vi.fn(),
- // v1.4.16: the route consults the multi-provider chain
- // first; the existing single-provider mocks return an empty chain
- // so the route falls through to `resolveProvider()` and the legacy
- // test fixtures keep behaving exactly as before.
+ // Empty unless a test hands the route a provider (`serveFrom`).
resolveProviderChain: vi.fn(async () => []),
};
});
@@ -167,7 +163,7 @@ import { NextRequest } from "next/server";
import { GET, POST, resolveInsightsRateLimit } from "../route";
import { enqueueStatusRefillForUser } from "@/lib/insights/comprehensive-generate";
-import { resolveProvider } from "@/lib/ai/provider";
+import { resolveProviderChain } from "@/lib/ai/provider";
import { checkRateLimit } from "@/lib/rate-limit";
import { prisma } from "@/lib/db";
import { hasUsableStatusProvider } from "@/lib/insights/status-provider";
@@ -214,8 +210,17 @@ beforeEach(() => {
});
});
+type ChainEntries = Awaited>;
+
+/** The chain the route resolves: one operator-key entry serving `instance`. */
+function serveFrom(instance: unknown) {
+ vi.mocked(resolveProviderChain).mockResolvedValue([
+ { providerType: "admin-openai", instance },
+ ] as unknown as ChainEntries);
+}
+
function makeWorkingProvider() {
- vi.mocked(resolveProvider).mockResolvedValue({
+ serveFrom({
type: "openai",
generateCompletion: vi.fn(async () => ({
content: JSON.stringify({
@@ -230,7 +235,7 @@ function makeWorkingProvider() {
providerType: "openai",
model: "gpt-4",
})),
- } as unknown as Awaited>);
+ });
}
interface ApiErrorEnvelope {
@@ -253,13 +258,12 @@ function jsonRequest(body: unknown = {}): Request {
function makeProviderThatThrows(
err: Error & { httpStatus?: number; bodyExcerpt?: string },
) {
- vi.mocked(resolveProvider).mockResolvedValue({
+ serveFrom({
type: "openai",
generateCompletion: vi.fn(async () => {
throw err;
}),
- // The route only calls generateCompletion; pad the type for TS.
- } as unknown as Awaited>);
+ });
}
describe("POST /api/insights/generate — daily token ceiling", () => {
@@ -314,7 +318,8 @@ describe("POST /api/insights/generate — a provider the health ledger benched",
httpStatus: 401,
});
makeProviderThatThrows(err);
- const provider = await vi.mocked(resolveProvider)("u-1");
+ const [{ instance: provider }] =
+ await vi.mocked(resolveProviderChain)("u-1");
const body = JSON.stringify({ force: true });
const request = new NextRequest("http://localhost/api/insights/generate", {
method: "POST",
@@ -894,7 +899,7 @@ describe("GET /api/insights/generate — read-only advisor read", () => {
expect(body.data.cached).toBe(true);
expect(body.data.insights).toEqual(cached);
// No completion is ever run on the read path.
- expect(resolveProvider).not.toHaveBeenCalled();
+ expect(resolveProviderChain).not.toHaveBeenCalled();
// A fresh cache (just now) does not trigger a warm — and the payload
// says so, so the client never starts a convergence poll.
expect(enqueueForceWarm).not.toHaveBeenCalled();
@@ -919,7 +924,7 @@ describe("GET /api/insights/generate — read-only advisor read", () => {
userId: "u-1",
locale: "en",
});
- expect(resolveProvider).not.toHaveBeenCalled();
+ expect(resolveProviderChain).not.toHaveBeenCalled();
// The stale serve is honest: `revalidating: true` rides on the
// payload so the client polls (bounded) until the warm lands.
const body = (await res.json()) as { data: { revalidating: boolean } };
@@ -953,7 +958,7 @@ describe("GET /api/insights/generate — read-only advisor read", () => {
userId: "u-1",
locale: "en",
});
- expect(resolveProvider).not.toHaveBeenCalled();
+ expect(resolveProviderChain).not.toHaveBeenCalled();
});
it("serves a fresh cache whose tag matches the reader's language", async () => {
diff --git a/src/app/api/medications/extract/__tests__/route.test.ts b/src/app/api/medications/extract/__tests__/route.test.ts
index 81d06c85b..0a9b558f1 100644
--- a/src/app/api/medications/extract/__tests__/route.test.ts
+++ b/src/app/api/medications/extract/__tests__/route.test.ts
@@ -51,7 +51,6 @@ vi.mock("@/lib/ai/coach/budget", () => ({
}));
vi.mock("@/lib/ai/provider", () => ({
- resolveProvider: vi.fn(async () => ({ type: "none" })),
resolveProviderChain: vi.fn(async () => []),
}));
@@ -82,7 +81,7 @@ import {
reconcileSpend,
resolveDailyCap,
} from "@/lib/ai/coach/budget";
-import { resolveProvider, resolveProviderChain } from "@/lib/ai/provider";
+import { resolveProviderChain } from "@/lib/ai/provider";
import {
runRawCompletionWithFallback,
AllProvidersFailedError,
@@ -382,9 +381,6 @@ describe("POST /api/medications/extract — validation + provider errors", () =>
it("returns 503 when no provider is configured for the user", async () => {
vi.mocked(requireAuth).mockResolvedValue(AUTH_OK as never);
vi.mocked(resolveProviderChain).mockResolvedValueOnce([]);
- vi.mocked(resolveProvider).mockResolvedValueOnce({
- type: "none",
- } as never);
// The status stays 503 (a shipped client may branch on it) and gains the
// code that says what it means.
diff --git a/src/app/api/medications/extract/route.ts b/src/app/api/medications/extract/route.ts
index b0086519f..a4a190050 100644
--- a/src/app/api/medications/extract/route.ts
+++ b/src/app/api/medications/extract/route.ts
@@ -72,7 +72,7 @@ import {
runRawCompletionWithFallback,
} from "@/lib/ai/provider-runner";
import { spentOnFailure } from "@/lib/ai/reasoning/answerless";
-import { resolveProvider, resolveProviderChain } from "@/lib/ai/provider";
+import { resolveProviderChain } from "@/lib/ai/provider";
import { singleUserTurn } from "@/lib/ai/types";
import {
@@ -198,17 +198,13 @@ async function handleExtract(request: NextRequest): Promise {
// surface a structured 503 — the wizard hides the overlay.
const chain = await resolveProviderChain(userId);
if (chain.length === 0) {
- const legacy = await resolveProvider(userId);
- if (legacy.type === "none") {
- annotate({ action: { name: "medications.extract.no-provider" } });
- throw new AiUnavailableError(
- "medicationExtract",
- "no_provider",
- null,
- MEDICATION_EXTRACT_NO_PROVIDER,
- );
- }
- chain.push({ providerType: "admin-openai", instance: legacy });
+ annotate({ action: { name: "medications.extract.no-provider" } });
+ throw new AiUnavailableError(
+ "medicationExtract",
+ "no_provider",
+ null,
+ MEDICATION_EXTRACT_NO_PROVIDER,
+ );
}
// The wire. The runner may cascade through every entry, so each one is
diff --git a/src/app/coach/plans/__tests__/page-read-only.test.tsx b/src/app/coach/plans/__tests__/page-read-only.test.tsx
index 246ca36f4..f087bf22f 100644
--- a/src/app/coach/plans/__tests__/page-read-only.test.tsx
+++ b/src/app/coach/plans/__tests__/page-read-only.test.tsx
@@ -18,9 +18,9 @@ vi.mock("@/hooks/use-ai-capability", () => ({
useAiCapabilityAnswer: () => capability.answer,
}));
-const plan = (id: string, status: string) => ({
+const plan = (id: string, status: string, metric = "WEIGHT") => ({
id,
- metric: "WEIGHT",
+ metric,
ifCue: `cue ${id}`,
thenAction: `action ${id}`,
target: null,
@@ -35,7 +35,7 @@ vi.mock("@/hooks/use-coach-plans", () => ({
data: [
plan("p-proposed", "proposed"),
plan("p-active", "active"),
- plan("p-met", "met"),
+ plan("p-met", "met", "mood vibes"),
],
isError: false,
isLoading: false,
@@ -48,12 +48,16 @@ vi.mock("@/hooks/use-coach-plans", () => ({
}));
import { I18nProvider } from "@/lib/i18n/context";
+import { primeMessages } from "@/lib/i18n/load-locale";
+import deMessages from "../../../../../messages/de.json";
import CoachPlansPage from "../page";
-function render(): string {
+primeMessages("de", deMessages);
+
+function render(locale: "en" | "de" = "en"): string {
return renderToStaticMarkup(
-
+ ,
);
@@ -90,4 +94,12 @@ describe("/coach/plans", () => {
expect(count(html, "coach-plan-delete")).toBe(1);
expect(html).not.toContain("you can still read and delete your plans");
});
+
+ it("names the metric in the reader's language, and an unknown key as written", () => {
+ capability.answer = { available: true };
+ const html = render("de");
+ expect(html.split("Gewicht").length - 1).toBe(2);
+ expect(html).toContain("mood vibes");
+ expect(html).not.toContain("WEIGHT");
+ });
});
diff --git a/src/app/coach/plans/page.tsx b/src/app/coach/plans/page.tsx
index cd646d1e5..ab2846195 100644
--- a/src/app/coach/plans/page.tsx
+++ b/src/app/coach/plans/page.tsx
@@ -23,6 +23,7 @@ import {
type CoachPlanDTO,
} from "@/hooks/use-coach-plans";
import { useAiCapabilityAnswer } from "@/hooks/use-ai-capability";
+import { planMetricLabel } from "@/lib/ai/coach/plan-metric";
import { useTranslations } from "@/lib/i18n/context";
import { formatDateOrRelative } from "@/lib/format";
import { cn } from "@/lib/utils";
@@ -109,7 +110,7 @@ function CoachPlansBody({ readOnly }: { readOnly: boolean }) {
) : null}
- {plan.metric}
+ {planMetricLabel(plan.metric, t)}{t(`coach.plans.status.${plan.status}`)}{formatDateOrRelative(plan.updatedAt, t)}
{plan.reviewDate ? (
diff --git a/src/components/insights/__tests__/ai-setup-hint-target.test.tsx b/src/components/insights/__tests__/ai-setup-hint-target.test.tsx
index 6e06357f6..c25d84aba 100644
--- a/src/components/insights/__tests__/ai-setup-hint-target.test.tsx
+++ b/src/components/insights/__tests__/ai-setup-hint-target.test.tsx
@@ -3,12 +3,21 @@
* phone it is the only control on the tile a thumb has to hit, so it carries
* the 44 px floor every other mobile action does (`size-11`), stepping down
* from `sm` like the rest of the app.
+ *
+ * The hint is also an invitation, so it stays away from a person who switched
+ * both the Coach and AI analysis off.
*/
import { renderToStaticMarkup } from "react-dom/server";
-import { describe, expect, it, vi } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
+const auth = vi.hoisted(() => ({
+ modules: {} as Record,
+}));
vi.mock("@/hooks/use-auth", () => ({
- useAuth: () => ({ user: { id: "u1" }, isAuthenticated: true }),
+ useAuth: () => ({
+ user: { id: "u1", modules: auth.modules },
+ isAuthenticated: true,
+ }),
}));
vi.mock("@/hooks/use-mounted", () => ({ useMounted: () => true }));
vi.mock("@/hooks/use-ai-capability", () => ({
@@ -22,16 +31,21 @@ vi.mock("@/hooks/use-ai-capability", () => ({
import { I18nProvider } from "@/lib/i18n/context";
import { AiSetupHint } from "../ai-setup-hint";
-function dismissButton(): string {
+function render(surface: "insights" | "settings" = "insights"): string {
vi.stubGlobal("window", {
localStorage: { getItem: () => null, setItem: () => undefined },
});
const html = renderToStaticMarkup(
-
+ ,
);
vi.unstubAllGlobals();
+ return html;
+}
+
+function dismissButton(): string {
+ const html = render();
const match = html.match(
/