feat(dashboard): promote a Today hero above the tile strip

Surface the day's read at the top of the dashboard: a Today hero that
renders the daily digest — the health score with an honest
provisional/final and sleep-pending face, the briefing lead with a
read-the-full-briefing affordance, and the worth-a-look rail of priority
cards. The dense tile grid and chart rows stay exactly where they are,
one scroll below, so muscle memory survives and no new nav destination
is added.

The hero reads one cached digest cell (GET /api/daily/digest) through the
centralised query-key factory and unwraps the envelope; it recomputes
nothing and never warms a provider on mount. It is gated on the insights
module and self-gates its empty state: a genuinely empty account renders
nothing rather than an alarming card, while an account with a score but
nothing notable gets a calm all-clear line in the rail's place. A shimmer
skeleton holds the footprint while the digest loads.

Priority-card actions wire to their existing destinations by construction
(log dose, reconnect a sync, view check-ups). New copy lands in all six
locales.
This commit is contained in:
Marc-André Bombeck committed 2026-07-16 19:31:13 +02:00
1 parent ba6c0990bf
commit 168c1c85d7
14 files changed
+581

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
"use client";
/**
* S2 — client cell for the unified daily digest (`GET /api/daily/digest`).
*
* The Today hero reads the ALREADY-CACHED `DailyDigest` DTO S1 assembled
* server-side. This hook is a plain GET of that cached route — it never
* warms a provider, never triggers a fresh AI call (the whole daily-value
* system's warm-on-mount ban extends here); the route composes from the
* nightly-cached briefing + dashboard snapshot ingredients.
*
* Gating: `enabled` is caller-supplied so the page can fold in the
* `insights` module flag (the digest is the AI-narrative daily layer;
* the route returns 403 `module.disabled` when insights is off) and the
* auth state. Freshness mirrors the dashboard snapshot cadence — a warm
* return-to-dashboard within the `staleTime` is a free cache hit, and a
* background sync surfaces on focus/poll — so the two above-the-fold
* cells refresh in lockstep without a second request storm.
*
* The read unwraps the `{ data, error }` envelope through `apiGet`
* (`(await res.json()).data`), and the queryKey is the centralised
* factory entry `queryKeys.dailyDigest()`.
*/
import { useQuery } from "@tanstack/react-query";
import { apiGet } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
import { DASHBOARD_REFETCH_INTERVAL_MS } from "@/lib/queries/refetch-interval";
import { retryOnceOnTransientError } from "@/lib/queries/retry-transient";
import type { DailyDigest } from "@/lib/daily/digest";
export function useDailyDigest(enabled = true) {
return useQuery({
queryKey: queryKeys.dailyDigest(),
queryFn: () => apiGet<DailyDigest>("/api/daily/digest"),
enabled,
// Match the dashboard snapshot's freshness so the hero and the tile
// strip below it share one refresh cadence: a warm remount inside the
// minute is free; a background sync surfaces on focus; an open tab
// polls in the foreground only.
staleTime: 60_000,
refetchOnMount: false,
refetchOnWindowFocus: true,
refetchInterval: DASHBOARD_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: false,
// One retry on transient network / 5xx (never 401/403) so a single
// blip doesn't collapse the hero to its empty degrade.
retry: retryOnceOnTransientError,
});
}
+14
View File
@@ -0,0 +1,14 @@
/**
* Query keys — the unified daily-value system (S2 Today surface).
* Part of the centralized factory; aggregated in `./index.ts`.
*
* The Today hero reads ONE cell against `GET /api/daily/digest` (the
* `DailyDigest` DTO S1 shipped). A single flat key: the digest is a
* per-user daily read with no discriminator, so `["daily","digest"]`
* is the whole surface. Kept in the factory so a future daily-data
* mutation can invalidate it by prefix without a bare-array literal
* (the `healthlog/queryKey-factory` rule bans those).
*/
export const dailyKeys = {
dailyDigest: () => ["daily", "digest"] as const,
};
+2
View File
@@ -21,6 +21,7 @@ import { coachKeys } from "./coach";
import { familyHistoryKeys } from "./family-history";
import { customMetrics } from "./custom-metrics";
import { cycleKeys } from "./cycle";
import { dailyKeys } from "./daily";
import { dashboardKeys } from "./dashboard";
import { environmentKeys } from "./environment";
import { documentKeys } from "./documents";
@@ -41,6 +42,7 @@ export const queryKeys = {
...authKeys,
...measurementKeys,
...moodKeys,
...dailyKeys,
...dashboardKeys,
...insightsKeys,
...medicationKeys,