refactor(dashboard): retire the legacy opt-in hero

The digest-driven Today hero is the single promoted hero at the top of
the dashboard, so the old opt-in hero band is redundant — a user who had
opted in and has Insights on saw two heroes. Remove the legacy hero and
its heroVisible layout flag.

- Drop the DashboardHero / DashboardHeroSkeleton mount from the
  dashboard page; the header now renders the greeting unconditionally
  (its showGreeting gate is gone with the hero that owned it).
- Retire the heroVisible field from the dashboard layout (type,
  defaults, resolver, serializer) and the widgets PUT schema. The
  resolver is a whitelist constructor, so a stored heroVisible is
  dropped on read and never re-persisted — no stored layout breaks.
- Delete the now-orphaned hero components (band, skeleton, ring
  carousel, briefing spotlight) and their tests; drop the retired
  dashboard.hero copy across all locales.
- Remove the hero-visibility toggle from Settings → Dashboard. The
  score-ring picker no longer hangs off that flag and stays available,
  since the rings it configures still ride the server-computed
  dashboard snapshot.
- Repoint the phone-width density guard at the Today hero and mock the
  daily digest so it renders.
- Repoint the preserve-when-absent doc analogy off the removed field.
This commit is contained in:
Marc-André Bombeck committed 2026-07-16 21:34:06 +02:00
1 parent d6ad76b1e1
commit 1d592bef78
29 files changed
+183 -2719

No files matched your search

+21 -43
View File
@@ -395,62 +395,40 @@ describe("resolveDashboardLayout() — chartOverlayPrefs (v1.4.18)", () => {
});
/**
* Dashboard hero (daily verdict) visibility — `heroVisible` piggy-backs
* on the layout blob like the B8 comparison baseline. Resolver contract:
* anything that is not the literal `true` clamps back to `false`
* (the hero is opt-in; legacy blobs keep it off and a stale client
* cannot poison the field with a non-boolean). Serializer persists the
* resolved boolean explicitly so a re-read never has to guess the
* default.
* The legacy opt-in dashboard hero was retired; its `heroVisible` flag is
* gone from the layout type, defaults, resolver, and serializer. A stored
* blob that still carries the field must resolve without throwing and drop
* it silently (the resolver is a whitelist constructor, so an unknown key
* is ignored on read and never re-persisted).
*/
describe("resolveDashboardLayout() — heroVisible", () => {
it("defaults to false for legacy layouts (no field saved)", () => {
const legacy = {
version: 1,
widgets: [{ id: "weight", visible: true, order: 0 }],
};
expect(resolveDashboardLayout(legacy).heroVisible).toBe(false);
});
it("respects an explicit heroVisible: true", () => {
describe("resolveDashboardLayout() — retired heroVisible field", () => {
it("drops a stored heroVisible without throwing", () => {
const saved = {
version: 1,
widgets: [{ id: "weight", visible: true, order: 0 }],
heroVisible: true,
};
expect(resolveDashboardLayout(saved).heroVisible).toBe(true);
const resolved = resolveDashboardLayout(saved) as unknown as Record<
string,
unknown
>;
expect(resolved).not.toHaveProperty("heroVisible");
});
it("clamps non-boolean values back to false", () => {
for (const poisoned of ["true", 0, null, 1, {}]) {
const saved = {
version: 1,
widgets: [{ id: "weight", visible: true, order: 0 }],
heroVisible: poisoned,
};
expect(resolveDashboardLayout(saved).heroVisible).toBe(false);
}
it("does not carry heroVisible on DEFAULT_DASHBOARD_LAYOUT", () => {
expect(DEFAULT_DASHBOARD_LAYOUT).not.toHaveProperty("heroVisible");
});
it("defaults heroVisible to false in DEFAULT_DASHBOARD_LAYOUT", () => {
expect(DEFAULT_DASHBOARD_LAYOUT.heroVisible).toBe(false);
});
it("preserves heroVisible: true through serialize → resolve round-trip", () => {
const layout: DashboardLayout = {
...DEFAULT_DASHBOARD_LAYOUT,
heroVisible: true,
};
const resolved = resolveDashboardLayout(serializeDashboardLayout(layout));
expect(resolved.heroVisible).toBe(true);
});
it("serializer derives false when the field is missing on input", () => {
const layout: DashboardLayout = {
it("does not re-emit heroVisible on serialize", () => {
const saved = {
version: 1,
widgets: [{ id: "weight", visible: true, order: 0 }],
heroVisible: true,
};
expect(serializeDashboardLayout(layout).heroVisible).toBe(false);
const serialized = serializeDashboardLayout(
resolveDashboardLayout(saved),
) as unknown as Record<string, unknown>;
expect(serialized).not.toHaveProperty("heroVisible");
});
});
+2 -22
View File
@@ -357,8 +357,8 @@ function coerceChartOverlayPrefsMap(value: unknown): ChartOverlayPrefsMap {
* canonical compliance engine — the ring that absorbs the retired
* hero dose row's information role.
*
* The preference piggy-backs on the layout blob like `heroVisible` (a UI
* affordance, no Prisma migration). The resolver drops unknown ids,
* The preference piggy-backs on the layout blob like `chartOverlayPrefs`
* (a UI affordance, no Prisma migration). The resolver drops unknown ids,
* dedupes, and clamps to `MAX_SELECTED_SCORE_RINGS`; a missing /
* malformed field falls back to the default single MED_COMPLIANCE ring,
* while an explicitly-saved empty array stays empty (the user chose no
@@ -505,16 +505,6 @@ export interface DashboardLayout {
comparisonBaseline?: ComparisonBaseline;
/** v1.4.18 — per-chart overlay-prefs (3 toggles per chart card). */
chartOverlayPrefs?: ChartOverlayPrefsMap;
/**
* Dashboard hero (daily verdict) visibility. Piggy-backs on the
* layout blob like the B8 comparison baseline — a UI affordance, not
* an analytical attribute, so no Prisma migration. Opt-in: the
* resolver clamps anything that is not the literal `true` back to
* `false` (legacy blobs without the field keep the hero off; a
* stale client cannot poison it with a non-boolean), and the
* serializer persists the resolved boolean explicitly.
*/
heroVisible?: boolean;
/**
* v1.27.7 — hero score rings (max 3, closed `SCORE_RING_IDS` set)
* rendered next to the health-score ring. See the doc on
@@ -540,9 +530,6 @@ const DASHBOARD_LAYOUT_VERSION = 1;
*/
export const DEFAULT_DASHBOARD_LAYOUT: DashboardLayout = {
version: DASHBOARD_LAYOUT_VERSION,
// Hero (daily verdict) is off by default; users opt in via
// Settings → Dashboard.
heroVisible: false,
// One medication-adherence ring next to the health score by default —
// the successor of the hero dose row's information role.
selectedScoreRings: [...DEFAULT_SELECTED_SCORE_RINGS],
@@ -699,10 +686,6 @@ export function resolveDashboardLayout(raw: unknown): DashboardLayout {
// client cannot poison the dashboard with values the renderer
// doesn't know how to draw.
chartOverlayPrefs: coerceChartOverlayPrefsMap(candidate.chartOverlayPrefs),
// Hero visibility — anything that is not the literal `true`
// clamps to `false` (default-off for legacy blobs and malformed
// values alike; the hero is opt-in).
heroVisible: candidate.heroVisible === true,
// v1.27.7 — hero score rings: unknown ids drop, duplicates collapse,
// the list clamps to three. A legacy blob without the field gets the
// default single MED_COMPLIANCE ring.
@@ -744,9 +727,6 @@ export function serializeDashboardLayout(
// v1.4.18 — persist per-chart overlay prefs verbatim through the
// same coercion the resolver runs so the wire shape is stable.
chartOverlayPrefs: coerceChartOverlayPrefsMap(layout.chartOverlayPrefs),
// Persist hero visibility explicitly (same clamp as the resolver)
// so a re-read never has to guess the default.
heroVisible: layout.heroVisible === true,
// v1.27.7 — persist the score-ring selection through the same
// coercion the resolver runs so the wire shape is stable.
selectedScoreRings: resolvedSelectedScoreRings,
@@ -743,7 +743,7 @@ export const medicationListLayoutSchema = z
.enum(MEDICATION_LIST_VIEWS)
.optional()
.describe(
'Which presentation /medications renders in. Default "cards". Optional on PUT — when omitted the stored value is preserved (preserve-when-absent, like `heroVisible` on the dashboard layout). Always present on responses.',
'Which presentation /medications renders in. Default "cards". Optional on PUT — when omitted the stored value is preserved (preserve-when-absent, like `chartOverlayPrefs` on the dashboard layout). Always present on responses.',
),
order: z
.array(z.string().min(1).max(MEDICATION_ORDER_ID_MAX_LENGTH))