mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
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:
1 parent
d6ad76b1e1
commit
1d592bef78
29 files changed
+183
-2719
No files matched your search
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user