feat(dashboard): snapshot carries the day's dose state, score and verdict inputs

The snapshot's fast phase gains a medsToday block — active count,
today's scheduled/taken/skipped tally after the idempotent projection,
and the earliest server-computed next due across active medications —
and the warm phase derives a compact health score from the coverage
map and graded blood-pressure windows the extras build already holds.
A pure verdict resolver ranks the day's single highlighted item over
nine rungs from an overdue dose down to a quiet-day fallback, with
fixed crisis floors for the blood-pressure rung and timezone-correct
day checks. The layout grows a heroVisible flag riding the stored
widgets JSON, the iOS summary route adopts the shared dose-state
builder with an unchanged wire shape, and the new sub-blocks report
their build time alongside the existing snapshot timings.
This commit is contained in:
Marc-André Bombeck committed 2026-06-12 09:02:58 +02:00
1 parent 2718930e8d
commit ad33fa46ed
15 files changed
+2103 -113

No files matched your search

+29
View File
@@ -606,6 +606,35 @@ const dashboardSnapshotResponse = z
glucoseByContext: dataSummaryRecord,
})
.nullable(),
// Dashboard hero — today's medication block (fast phase, always
// present). Projection-backed tally + earliest next-due across
// active medications.
medsToday: z
.object({
activeCount: z.number().int(),
scheduledToday: z.number().int(),
takenToday: z.number().int(),
skippedToday: z.number().int(),
nextDueAt: z.string().nullable(),
nextDueOverdue: z.boolean(),
nextDueMedicationName: z.string().nullable(),
})
.describe(
"Today's medication block: active-medication count, today-window tally (scheduled / taken / skipped), and the earliest next-due slot across active medications. `nextDueOverdue: true` marks an OPEN overdue slot (anchor passed, still inside its catch-up band). Staleness contract: the snapshot is cache-served, so a `nextDueAt` in the past with `nextDueOverdue: false` means the anchor passed after the snapshot was built — render the plain day summary, never an overdue state.",
),
// Dashboard hero — health score (warm phase, nullable on a
// rollup-coverage miss). Score + band + delta only; the per-pillar
// component breakdown stays on the analytics route.
healthScore: z
.object({
score: z.number().int(),
band: z.enum(["green", "yellow", "red"]),
delta: z.number().nullable(),
})
.nullable()
.describe(
"Personal health score summary (0..100 score, traffic-light band, week-over-week delta). Null on a rollup-coverage miss (it rides the thick phase alongside `extras`) and when no pillar is computable. Component breakdown is deliberately not serialised here.",
),
briefing: z.record(z.string(), z.unknown()).nullable(),
briefingState: z.enum(["ready", "preparing", "disabled", "no-provider"]),
briefingUpdatedAt: z.string().nullable(),