mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(dashboard): keep the derived score rings with AI analysis off
The hero rings read their owner from the surface map instead of their own map, and the readiness, recovery and sleep score rings no longer wait on the AI analysis switch: they are computed scores, not model text. The dose ring still follows the medications module.
This commit is contained in:
1 parent
2594045bef
commit
347e63d653
5 files changed
+52
-45
No files matched your search
@@ -4,7 +4,6 @@
|
||||
* Single source of truth for which widgets show on /, what order, and
|
||||
* what the default layout is for new users. Null / missing = default.
|
||||
*/
|
||||
import type { ModuleKey } from "@/lib/modules/registry";
|
||||
import {
|
||||
PRIORITY_ITEM_KINDS,
|
||||
isPriorityItemKind,
|
||||
@@ -513,21 +512,6 @@ export function resolveHeroRingOrder(
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ring id → owning toggleable module. Mirrors the derived routes'
|
||||
* `DERIVED_MODULE` map for the three derived rings (READINESS rides the
|
||||
* recovery module like the recovery/strain/stress trio); MED_COMPLIANCE
|
||||
* belongs to the medications module. Client-safe (type-only ModuleKey
|
||||
* import) so the Settings picker and the server snapshot resolver gate
|
||||
* on the same map — the `WIDGET_MODULE_BY_ID` pattern.
|
||||
*/
|
||||
export const SCORE_RING_MODULE: Record<ScoreRingId, ModuleKey> = {
|
||||
READINESS: "recovery",
|
||||
RECOVERY_SCORE: "recovery",
|
||||
SLEEP_SCORE: "sleep",
|
||||
MED_COMPLIANCE: "medications",
|
||||
};
|
||||
|
||||
function isScoreRingId(value: unknown): value is ScoreRingId {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
*
|
||||
* The resolver is tested in isolation with the derived dispatcher
|
||||
* mocked, pinning:
|
||||
* - module gating (owning module + the insights gate on derived rings);
|
||||
* - module gating (the owning module, from the surface map; the insights
|
||||
* key, AI analysis, gates no ring);
|
||||
* - the pass-through contract for derived scores (same resolvers the
|
||||
* batch route calls, no recomputation, non-`ok` → no ring);
|
||||
* - the dose ring: today's taken/scheduled progress off the shared
|
||||
@@ -117,7 +118,11 @@ describe("buildScoreRingsBlock() — selection + module gating", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("the insights gate drops derived rings but keeps the dose ring", async () => {
|
||||
it("keeps derived rings with AI analysis off: the scores are data", async () => {
|
||||
computeDerivedMetric.mockResolvedValue({
|
||||
status: "ok",
|
||||
value: { score: 70, band: "yellow" },
|
||||
});
|
||||
const rings = await buildScoreRingsBlock(
|
||||
fakePrisma,
|
||||
"user-1",
|
||||
@@ -126,16 +131,19 @@ describe("buildScoreRingsBlock() — selection + module gating", () => {
|
||||
NOW,
|
||||
medsToday(2, 2),
|
||||
);
|
||||
expect(computeDerivedMetric).not.toHaveBeenCalled();
|
||||
expect(loadBaselineProfile).not.toHaveBeenCalled();
|
||||
expect(rings).toEqual([
|
||||
{
|
||||
id: "MED_COMPLIANCE",
|
||||
score: 100,
|
||||
band: "green",
|
||||
doses: { taken: 2, scheduled: 2 },
|
||||
},
|
||||
]);
|
||||
expect(rings.map((r) => r.id)).toEqual(["READINESS", "MED_COMPLIANCE"]);
|
||||
});
|
||||
|
||||
it("drops the dose ring with the medications module off", async () => {
|
||||
const rings = await buildScoreRingsBlock(
|
||||
fakePrisma,
|
||||
"user-1",
|
||||
["MED_COMPLIANCE"],
|
||||
moduleMap({ medications: false }),
|
||||
NOW,
|
||||
medsToday(2, 2),
|
||||
);
|
||||
expect(rings).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -25,22 +25,18 @@
|
||||
* taken, `yellow` while doses remain — never `red`: pending doses in
|
||||
* the morning are not an alert state.
|
||||
*
|
||||
* Module gating rides the client-safe `SCORE_RING_MODULE` map in
|
||||
* `@/lib/dashboard-layout` (mirroring the derived routes'
|
||||
* `DERIVED_MODULE`: readiness/recovery → recovery module, sleep score →
|
||||
* sleep module, MED_COMPLIANCE → medications) plus the `insights` gate
|
||||
* every derived read sits behind.
|
||||
* Module gating reads the surface map (`score-ring:<id>` in
|
||||
* `@/lib/modules/surface`): readiness/recovery → recovery, sleep score →
|
||||
* sleep, MED_COMPLIANCE → medications. The derived scores are data, so the
|
||||
* `insights` key (AI analysis) gates no ring.
|
||||
*
|
||||
* Every ring resolves fail-soft: a throwing engine drops its ring, never
|
||||
* the snapshot.
|
||||
*/
|
||||
import type { PrismaClient } from "@/generated/prisma/client";
|
||||
import type { ModuleKey } from "@/lib/modules/gate";
|
||||
import {
|
||||
SCORE_RING_IDS,
|
||||
SCORE_RING_MODULE,
|
||||
type ScoreRingId,
|
||||
} from "@/lib/dashboard-layout";
|
||||
import { SCORE_RING_IDS, type ScoreRingId } from "@/lib/dashboard-layout";
|
||||
import { isSurfaceVisible } from "@/lib/modules/surface";
|
||||
import {
|
||||
computeDerivedMetric,
|
||||
loadBaselineProfile,
|
||||
@@ -122,12 +118,7 @@ export async function buildScoreRingsBlock(
|
||||
): Promise<DashboardScoreRing[]> {
|
||||
const eligible = selected.filter((id) => {
|
||||
if (!(SCORE_RING_IDS as readonly string[]).includes(id)) return false;
|
||||
if (modules[SCORE_RING_MODULE[id]] === false) return false;
|
||||
// The derived scores are an insights-layer read everywhere else
|
||||
// (`/api/insights/derived*` sits behind the insights module) — the
|
||||
// snapshot honours the same gate.
|
||||
if (DERIVED_RING_METRIC[id] && modules.insights === false) return false;
|
||||
return true;
|
||||
return isSurfaceVisible(`score-ring:${id}`, modules);
|
||||
});
|
||||
if (eligible.length === 0) return [];
|
||||
|
||||
|
||||
@@ -48,7 +48,10 @@ import { NAV_DESTINATIONS } from "@/components/layout/nav-model";
|
||||
import { CAPTURE_KIND_ORDER } from "@/components/layout/capture-picker";
|
||||
import { SETTINGS_SECTIONS } from "@/components/settings/settings-shell";
|
||||
import { LAYOUT_GROUP_IDS } from "@/components/settings/layout-groups";
|
||||
import { DASHBOARD_WIDGET_CATALOGUE_IDS } from "@/lib/dashboard-layout";
|
||||
import {
|
||||
DASHBOARD_WIDGET_CATALOGUE_IDS,
|
||||
SCORE_RING_IDS,
|
||||
} from "@/lib/dashboard-layout";
|
||||
import { SUMMARY_TYPE_MODULE } from "@/lib/dashboard/widget-modules";
|
||||
import { ENVIRONMENT_FIELDS } from "@/lib/environment/fields";
|
||||
import {
|
||||
@@ -128,6 +131,17 @@ describe("every surface id names a surface that exists", () => {
|
||||
for (const id of localIds("derived")) expect(ids.has(id), id).toBe(true);
|
||||
});
|
||||
|
||||
it("score-ring: a hero score ring id, the derived ones owned like their score", () => {
|
||||
const ids = new Set<string>(SCORE_RING_IDS);
|
||||
for (const id of localIds("score-ring")) {
|
||||
expect(ids.has(id), id).toBe(true);
|
||||
const derivedOwner = surfaceModule(`derived:${id}`);
|
||||
if (derivedOwner !== undefined) {
|
||||
expect(surfaceModule(`score-ring:${id}`), id).toBe(derivedOwner);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("settings: a Settings section slug", () => {
|
||||
const slugs = new Set<string>(SETTINGS_SECTIONS.map((s) => s.slug));
|
||||
for (const id of localIds("settings")) expect(slugs.has(id), id).toBe(true);
|
||||
|
||||
@@ -47,6 +47,7 @@ import type { ModuleKey } from "@/lib/modules/registry";
|
||||
* - `settings`: a Settings section, by its slug.
|
||||
* - `settings-layout`: a Settings, Layout group, by its id.
|
||||
* - `correlation`: a correlation-discovery channel, by its channel key.
|
||||
* - `score-ring`: a dashboard hero score ring, by its `ScoreRingId`.
|
||||
*/
|
||||
export const SURFACE_KINDS = [
|
||||
"nav",
|
||||
@@ -60,6 +61,7 @@ export const SURFACE_KINDS = [
|
||||
"settings",
|
||||
"settings-layout",
|
||||
"correlation",
|
||||
"score-ring",
|
||||
] as const;
|
||||
|
||||
export type SurfaceKind = (typeof SURFACE_KINDS)[number];
|
||||
@@ -163,6 +165,14 @@ const STATIC_SURFACE_MODULE = {
|
||||
"derived:STRESS_SCORE": "recovery",
|
||||
"derived:READINESS": "recovery",
|
||||
|
||||
// ── Dashboard hero score rings ──
|
||||
// The three derived rings follow their derived score; the dose ring is
|
||||
// medications'.
|
||||
"score-ring:READINESS": "recovery",
|
||||
"score-ring:RECOVERY_SCORE": "recovery",
|
||||
"score-ring:SLEEP_SCORE": "sleep",
|
||||
"score-ring:MED_COMPLIANCE": "medications",
|
||||
|
||||
// ── Settings sections ──
|
||||
"settings:environment": "environment",
|
||||
"settings:coach": "coach",
|
||||
|
||||
Reference in new issue
Block a user