Merge the day picker, Coach new-chat button, day panel on /coach and text cleanup into release/v1.42.0

This commit is contained in:
Marc-André Bombeck committed 2026-10-09 15:11:24 +02:00
commit 1d080ee166
69 files changed
+2293 -2241

No files matched your search

@@ -757,7 +757,7 @@ describe("doctor-report surgical history section", () => {
);
const text = await extractText(bytes);
expect(text).toContain("Operationen und Eingriffe");
expect(text).toMatch(/Knee · Links/);
expect(text).toMatch(/Knee \(Links\)/);
});
it("omits the section when there is no procedure", async () => {
@@ -819,7 +819,7 @@ describe("doctor-report condition body site", () => {
it("prints the site with its side in the report's language", async () => {
const text = await render(CONDITIONS, "de");
expect(text).toContain("Körperstelle");
expect(text).toMatch(/Knee · Links/);
expect(text).toMatch(/Knee \(Links\)/);
});
it("keeps the old table when no condition names a site", async () => {
@@ -100,7 +100,7 @@ describe("readCoachCorrelations", () => {
pValue: 0.001,
qValue: 0.02,
interpretation:
"Higher time in daylight tends to go with higher next-day sleep duration in your data — a pattern worth watching, not a cause.",
"Higher time in daylight tends to go with higher next-day sleep duration in your data. It is a pattern to watch, not a cause.",
lagDays: 1,
},
],
@@ -345,7 +345,7 @@ describe("executeCoachTool", () => {
lagDays: 1,
n: 42,
r: 0.31,
note: "Higher time in daylight tends to go with higher next-day sleep duration in your data — a pattern worth watching, not a cause.",
note: "Higher time in daylight tends to go with higher next-day sleep duration in your data. It is a pattern to watch, not a cause.",
},
],
coincident: {
@@ -118,7 +118,7 @@ describe("formatRelationsBlock — cross-metric correlations, descriptive only",
const rels: RelevantCorrelation[] = [
{
interpretation:
"Higher time in daylight tends to go with higher next-day sleep duration in your data — a pattern worth watching, not a cause.",
"Higher time in daylight tends to go with higher next-day sleep duration in your data. It is a pattern to watch, not a cause.",
n: 42,
r: 0.51,
},
@@ -457,7 +457,7 @@ describe("generateMetricStatus — generation path", () => {
vi.mocked(getRelevantCorrelationsForMetric).mockResolvedValueOnce([
{
interpretation:
"Higher time in daylight tends to go with lower next-day resting heart rate in your data — a pattern worth watching, not a cause.",
"Higher time in daylight tends to go with lower next-day resting heart rate in your data. It is a pattern to watch, not a cause.",
n: 35,
r: -0.46,
},
@@ -157,8 +157,8 @@ describe("buildCoachReadStrip — the pattern line speaks the reader's language"
// A sentence that is different but wrong is still a broken page.
expect(note).toBe(
"Wenn die Symptomstärke höher liegt, scheint die Schlafdauer am " +
"Folgetag nach bisheriger Datenlage eher niedriger auszufallen — ein " +
"Muster, das Beachtung verdient, keine Ursache.",
"Folgetag nach bisheriger Datenlage eher niedriger auszufallen. Das ist " +
"ein Muster, das Beachtung verdient, keine Ursache.",
);
});
@@ -170,8 +170,8 @@ describe("buildCoachReadStrip — the pattern line speaks the reader's language"
expect(strip.driver).not.toBeNull();
expect(strip.driver!.note).toBe(
"Higher symptom severity looks like it goes with lower next-day sleep " +
"duration in your data, on the evidence so far — a pattern worth " +
"watching, not a cause.",
"duration in your data, on the evidence so far. It is a pattern " +
"to watch, not a cause.",
);
});
-23
View File
@@ -60,7 +60,6 @@ import {
insightsGeneratePostResponse,
coachReadQuery,
coachReadStripResponse,
coachSeededQuestionResponse,
narrativeQuery,
narrativeResponse,
glp1TimelineQuery,
@@ -399,28 +398,6 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
},
"/api/insights/coach/seeded-question": {
get: {
tags: ["Insights"],
summary: "Today's suggested Coach opener",
description:
"Resolves today's single most notable derived wellness signal into a tappable opener for the Coach's blank-chat hero, using the same confidence-gated detector the daily briefing uses. The selection happens SERVER-side and the client renders it rather than recomputing. `signal: null` is the ordinary answer and means the hero keeps its neutral greeting — never a fabricated opener. Two different causes produce that null and are indistinguishable on the wire: nothing crossed the notability gate, or the account has turned proactive suggestions off, in which case the detector does not run at all. The opener exists only to open the Coach, so it is served only while the `coach` AI capability is available; otherwise the answer is the same neutral `signal: null` and `ai` says why. Never refused for an AI reason. Shared analytics-read budget. Cookie or Bearer auth; not delegable.",
responses: {
"200": {
description: "The opener, or the neutral null.",
content: {
"application/json": {
schema: dataEnvelope(
coachSeededQuestionResponse,
"CoachSeededQuestionEnvelope",
),
},
},
},
...stdResponses,
},
},
},
"/api/insights/narrative": {
get: {
tags: ["Insights"],
@@ -2747,38 +2747,6 @@ export const coachReadStripResponse = z
"The two server-authoritative lines a metric sub-page renders above its chart. Pure compute over the baseline and correlation engines — no provider call, no cache table — so web and native decode the same resolved DTO.",
});
// ── Coach seeded opener (`/api/insights/coach/seeded-question`) ───────
export const coachSeededQuestionResponse = z
.object({
signal: z
.object({
sourceMetric: z
.string()
.describe(
"Sentinel id the client keys its localised copy on (`readiness` / `recovery`).",
),
score: z.number().describe("The latest 0..100 score."),
band: z
.string()
.describe(
"`yellow` or `red`. Green never surfaces — a good day is not notable.",
),
})
.nullable()
.describe(
"Null whenever nothing crossed the detector's confidence and notability gate, AND whenever the account has turned proactive suggestions off, AND whenever the `coach` capability is unavailable (then `ai` says why) — all mean the hero keeps its neutral greeting rather than showing a fabricated opener.",
),
ai: aiCapabilityState.describe(
"The `coach` capability the opener exists for.",
),
})
.meta({
id: "CoachSeededQuestion",
description:
"Today's single most notable derived wellness signal, resolved server-side into a tappable opener for the Coach's blank-chat hero. The client renders the resolved DTO and never recomputes the selection.",
});
// ── Period narrative (`/api/insights/narrative`) ──────────────────────
export const narrativeQuery = z
-6
View File
@@ -76,12 +76,6 @@ export const coachKeys = {
messageId,
"trail",
] as const,
/**
* v1.21.2 (A3) — today's most notable derived signal, resolved into the
* Coach hero's pre-seeded relevance opener
* (`GET /api/insights/coach/seeded-question`).
*/
coachSeededQuestion: () => ["coach-seeded-question"] as const,
/**
* v1.22 (B2/B6) — the user's Coach episodic reminders (`GET /api/coach/
* reminders`). The optional `status` arg keys the in-app tile read
@@ -109,7 +109,6 @@ describe("parseNotificationPrefs", () => {
nudgeVitals: true,
nudgeRoutine: true,
nudgeFrequency: "weekly",
ambientSuggestions: true,
nudgeAiComposed: false,
},
measurementReminder: {
@@ -134,7 +133,6 @@ describe("parseNotificationPrefs", () => {
nudgeVitals: true,
nudgeRoutine: true,
nudgeFrequency: "weekly",
ambientSuggestions: true,
nudgeAiComposed: false,
},
measurementReminder: {
@@ -161,7 +159,6 @@ describe("parseNotificationPrefs", () => {
nudgeVitals: true,
nudgeRoutine: true,
nudgeFrequency: "weekly",
ambientSuggestions: true,
nudgeAiComposed: false,
},
measurementReminder: {
@@ -204,7 +201,6 @@ describe("resolveNotificationPrefs (deep-merge)", () => {
nudgeVitals: true,
nudgeRoutine: true,
nudgeFrequency: "weekly",
ambientSuggestions: true,
nudgeAiComposed: false,
},
measurementReminder: {
@@ -235,7 +231,6 @@ describe("resolveNotificationPrefs (deep-merge)", () => {
nudgeVitals: true,
nudgeRoutine: true,
nudgeFrequency: "weekly",
ambientSuggestions: true,
nudgeAiComposed: false,
},
measurementReminder: {
@@ -263,7 +258,6 @@ describe("resolveNotificationPrefs (deep-merge)", () => {
nudgeVitals: true,
nudgeRoutine: true,
nudgeFrequency: "weekly",
ambientSuggestions: true,
nudgeAiComposed: false,
},
measurementReminder: {
@@ -291,7 +285,6 @@ describe("resolveNotificationPrefs (deep-merge)", () => {
nudgeVitals: true,
nudgeRoutine: true,
nudgeFrequency: "weekly",
ambientSuggestions: true,
nudgeAiComposed: false,
},
measurementReminder: {
@@ -320,7 +313,6 @@ describe("resolveNotificationPrefs (deep-merge)", () => {
nudgeVitals: true,
nudgeRoutine: true,
nudgeFrequency: "weekly",
ambientSuggestions: true,
nudgeAiComposed: false,
},
measurementReminder: {
-24
View File
@@ -125,14 +125,6 @@ const coachPrefsSchema = z
nudgeRoutine: z.boolean(),
/** v1.16.5 — frequency cap: one nudge per 7 or per 14 days. */
nudgeFrequency: z.enum(["weekly", "biweekly"]),
/**
* v1.25.0 — opt-out for the proactive ambient SUGGESTIONS surfaced in the
* UI (the daily seeded example opener on the Coach hero + the "try asking"
* prompt chips). Default ON. Separate from `nudgesEnabled` (the push-nudge
* master switch) so a user can keep the nudges but silence the example
* prompts, or vice versa.
*/
ambientSuggestions: z.boolean(),
/**
* v1.25.0 — opt-IN for AI-composed nudge copy. Default OFF: the proactive
* nudge ships a deterministic warm localized template, and that template is
@@ -219,11 +211,6 @@ export interface NotificationPrefs {
nudgeRoutine: boolean;
/** v1.16.5 — cap interval: "weekly" (7 d) or "biweekly" (14 d). */
nudgeFrequency: "weekly" | "biweekly";
/**
* v1.25.0 — proactive ambient example suggestions in the UI (seeded
* opener + suggested-prompt chips). Default `true`.
*/
ambientSuggestions: boolean;
/**
* v1.25.0 — opt-in for AI-composed nudge copy. Default `false`; the
* deterministic template stays the default + fail-closed fallback.
@@ -285,7 +272,6 @@ export const DEFAULT_NOTIFICATION_PREFS: NotificationPrefs = {
nudgeVitals: true,
nudgeRoutine: true,
nudgeFrequency: "weekly",
ambientSuggestions: true,
nudgeAiComposed: false,
},
measurementReminder: {
@@ -483,16 +469,6 @@ export interface CoachNudgePrefs {
aiComposed: boolean;
}
/**
* v1.25.0 — the single source every proactive ambient SUGGESTION surface
* consults (the daily seeded example opener on the Coach hero + the
* suggested-prompt chips). Default `true`; a null / drifted row resolves to
* the documented default so the suggestions work out of the box.
*/
export function resolveCoachAmbientSuggestionsEnabled(raw: unknown): boolean {
return parseNotificationPrefs(raw).coach.ambientSuggestions;
}
export function resolveCoachNudgePrefs(raw: unknown): CoachNudgePrefs {
const coach = parseNotificationPrefs(raw).coach;
return {