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

+24
View File
@@ -45,6 +45,8 @@ import {
import { DashboardHeader } from "@/components/dashboard/dashboard-header";
import { DashboardHero } from "@/components/dashboard/hero/dashboard-hero";
import { DashboardHeroSkeleton } from "@/components/dashboard/hero/dashboard-hero-skeleton";
import { TodayHero } from "@/components/daily/today-hero";
import { TodayHeroSkeleton } from "@/components/daily/today-hero-skeleton";
import {
QuickEntrySheets,
type QuickEntryDialog,
@@ -123,6 +125,8 @@ import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { useAnalyticsQuery } from "@/lib/queries/use-analytics-query";
import { useDashboardSnapshot } from "@/lib/queries/use-dashboard-snapshot";
import { useDailyDigest } from "@/lib/queries/use-daily-digest";
import { useModuleEnabled } from "@/hooks/use-module-enabled";
import { isDashboardSnapshotEnabled } from "@/lib/dashboard/snapshot-flag";
import type { DataSummary } from "@/lib/analytics/trends";
import { mergeSlimAndThickAnalytics } from "@/lib/analytics/merge-slim-thick";
@@ -223,6 +227,13 @@ export default function DashboardPageClient() {
const snapshotEnabled = isDashboardSnapshotEnabled();
const snapshotQuery = useDashboardSnapshot(snapshotEnabled);
// S2 — the Today hero reads the unified daily digest (the S1 DTO) from
// its own cached route. Gated on the `insights` module (the digest is
// the AI-narrative daily layer; the route 403s when insights is off)
// and the auth state so a logged-out / gated account never fires it.
const insightsEnabled = useModuleEnabled("insights");
const digestQuery = useDailyDigest(isAuthenticated && insightsEnabled);
const analyticsSlimQuery = useAnalyticsQuery({
slice: "summaries",
enabled: !snapshotEnabled && isAuthenticated,
@@ -775,6 +786,19 @@ export default function DashboardPageClient() {
showGreeting={!heroVisible}
/>
{/* S2 — the Today hero, promoted above the tile strip (MARC sign-off
decision 1). Renders the S1 daily digest: the day's read, the
honest score/freshness face, and the worth-a-look rail. Shimmer
skeleton while the cached digest loads; a genuinely empty account
degrades to nothing (the hero itself returns null). The dense
tile grid + charts below are untouched. */}
{insightsEnabled &&
(digestQuery.isLoading ? (
<TodayHeroSkeleton />
) : digestQuery.data ? (
<TodayHero digest={digestQuery.data} />
) : null)}
{heroVisible &&
(primaryLoading || !heroSnapshot ? (
<DashboardHeroSkeleton />