mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
Address review: label key, gate every feed, retire the dead switch
Five points from the review, in the order they were raised. 1. The RMSSD marker did not survive. The tile label paints truncated, uppercase and on one line, so an appended `· RMSSD` was the first thing dropped in the longer locales — the part a reader needs to tell a 40 ms RMSSD night from a collapsed SDNN one. `measurements.typeHrvRmssd` already exists in all seven bundles, so each series now carries its own key. `pickHrvSummary` returns that key beside the series it chose, which keeps one mapping from series to label rather than a conditional at the call site. 2. The module gate bit on one path only. `gateSummariesByModules` was private to the snapshot builder, and with the snapshot flag off the tiles are fed by `/api/analytics`, which filtered nothing — so recovery-off plus RMSSD-only still painted the tile, on the exact path the helper's docblock promised it could not. The filter moves next to the map it reads, and both analytics slices apply it. Gated after the cache read, so the cached cell stays module-independent and toggling a module takes effect on the next request instead of waiting out the TTL. 3. The same class one step on: recovery off plus RMSSD only left a switch that was on, a tile that could never paint, and no explanation. The repo already calls that a dead switch and hides the row; HRV is only dead sometimes, so the answer needs one fact the Settings screen does not have. The server resolves it — `unavailableWidgetIds` on the widgets payload — and Settings drops those rows. An account with SDNN keeps the toggle: its tile still works with recovery off, because SDNN is a vital no module owns. The SDNN probe is an index-only existence check that runs only when recovery is off, outside the layout cache so the row goes the moment the module does. 4. The brittle test sliced from the helper to the end of the file, so any function added below it would have gone red pointing at HRV. Bounded to the function's own body, with a guard on the bound itself. The three wiring assertions matched formatted source text — they would have passed just as well if the tile never rendered — and are replaced by a test that renders the tile through a real TrendCard and asserts the reading is on screen. One negative source check is kept, naming the expression the bug lived on rather than the formatting of its replacement. 5. Rebased onto v1.38.6; the changelog entry sits under [Unreleased]. Not verified here: `pnpm db:generate` cannot run in this container (the Prisma engine download is refused), so snapshot.test.ts, the widgets route test and the analytics route test could not load, and typecheck is incomplete. Lint, format and the HRV suite (17 tests) pass.
This commit is contained in:
11 files changed
+551
-235
No files matched your search
@@ -22,7 +22,8 @@ import {
|
||||
type BpInTargetEnvelope,
|
||||
} from "@/lib/analytics/bp-in-target-fast-path";
|
||||
import { computeAndRecordUserHealthScore } from "@/lib/analytics/score/record";
|
||||
import { isModuleEnabled } from "@/lib/modules/gate";
|
||||
import { isModuleEnabled, resolveModuleMap } from "@/lib/modules/gate";
|
||||
import { gateSummariesByModules } from "@/lib/dashboard/widget-modules";
|
||||
import { resolveRestMode } from "@/lib/illness/rest-mode";
|
||||
import { deriveBpWindow90 } from "@/lib/analytics/window-confidence";
|
||||
import { computeCorrelationHypothesesFastPath } from "@/lib/analytics/correlations-fast-path";
|
||||
@@ -109,10 +110,32 @@ export const GET = apiHandler(async (request?: Request) => {
|
||||
() => computeSummariesSlice(user.id),
|
||||
annotate,
|
||||
);
|
||||
// Drop the types whose module the user turned off, from the SAME map
|
||||
// the snapshot builder gates on (`SUMMARY_TYPE_MODULE`, via
|
||||
// `gateSummariesByModules`). With `NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false`
|
||||
// this slice — not the snapshot — feeds the dashboard tile strip, and
|
||||
// it used to hand back every type regardless: recovery off plus an
|
||||
// RMSSD-only account still painted the HRV tile, because nothing on
|
||||
// this path had ever filtered.
|
||||
//
|
||||
// Gated AFTER the cache read on purpose. The cached cell stays
|
||||
// module-independent, so toggling a module takes effect on the next
|
||||
// request instead of waiting out the TTL, and two users' module states
|
||||
// can never share a cached answer.
|
||||
const slimModules = await resolveModuleMap(user.id);
|
||||
const slimGated = gateSummariesByModules(
|
||||
slim.summaries,
|
||||
slim.lastSeenByType,
|
||||
slimModules,
|
||||
);
|
||||
// v1.4.34 IW-B — bfcache-friendly directive on the slim slice too
|
||||
// so a back-forward navigation that landed on the dashboard tile
|
||||
// strip can restore from memory instead of paying a full reload.
|
||||
const slimRes = apiSuccess(slim);
|
||||
const slimRes = apiSuccess({
|
||||
...slim,
|
||||
summaries: slimGated.summaries,
|
||||
lastSeenByType: slimGated.lastSeenByType,
|
||||
});
|
||||
slimRes.headers.set("Cache-Control", NO_STORE_BUT_BFCACHE);
|
||||
return slimRes;
|
||||
}
|
||||
@@ -269,8 +292,18 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
// pre-fix behaviour for tenants whose year-ago window happened to
|
||||
// fall outside the 425-day floor.
|
||||
const slim = await computeSummariesSlice(user.id);
|
||||
const results = slim.summaries;
|
||||
const lastSeenByType = slim.lastSeenByType;
|
||||
// Same module gate as the slim slice and the snapshot builder — the
|
||||
// default slice feeds the dashboard too (it is the thick half of the
|
||||
// legacy two-cell path), so it cannot be the one feed that leaks a
|
||||
// disabled module's types.
|
||||
const thickModules = await resolveModuleMap(user.id);
|
||||
const thickGated = gateSummariesByModules(
|
||||
slim.summaries,
|
||||
slim.lastSeenByType,
|
||||
thickModules,
|
||||
);
|
||||
const results = thickGated.summaries;
|
||||
const lastSeenByType = thickGated.lastSeenByType;
|
||||
|
||||
// `computeSummariesSlice` annotates `action: "analytics.get.slim"`
|
||||
// for telemetry on the slim route. Restore the default-slice action
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { resolveModuleMap } from "@/lib/modules/gate";
|
||||
import { unavailableWidgetIds } from "@/lib/dashboard/widget-modules";
|
||||
import { prisma, toJson } from "@/lib/db";
|
||||
import {
|
||||
takeBaseToken,
|
||||
@@ -218,7 +220,36 @@ export const GET = apiHandler(async () => {
|
||||
() => buildDashboardLayout(user.id),
|
||||
annotate,
|
||||
);
|
||||
return apiSuccess(layout);
|
||||
|
||||
// Which toggles cannot do anything for this account. Resolved here, on
|
||||
// the server, so the Settings screen never has to fetch dashboard data
|
||||
// to find out whether a switch it is about to draw is dead.
|
||||
//
|
||||
// Deliberately OUTSIDE the cached layout above: the layout cell is keyed
|
||||
// on the user and holds their arrangement, which does not change when a
|
||||
// module is toggled. Folding a module-dependent answer into it would
|
||||
// serve a stale one for the rest of the 5-minute TTL — the switch would
|
||||
// linger for minutes after Recovery went off, which is the confusion this
|
||||
// is removing.
|
||||
//
|
||||
// The SDNN probe is an index-only existence check
|
||||
// (`@@index([userId, type, measuredAt])`), not a summaries read; it runs
|
||||
// only when Recovery is off, so the common account pays nothing.
|
||||
const modules = await resolveModuleMap(user.id);
|
||||
let hasSdnn = true;
|
||||
if (modules.recovery === false) {
|
||||
const sdnnRow = await prisma.measurement.findFirst({
|
||||
where: { userId: user.id, type: "HEART_RATE_VARIABILITY" },
|
||||
select: { id: true },
|
||||
});
|
||||
hasSdnn = sdnnRow !== null;
|
||||
}
|
||||
const unavailable = unavailableWidgetIds(modules, { hasSdnn });
|
||||
return apiSuccess(
|
||||
unavailable.length > 0
|
||||
? { ...layout, unavailableWidgetIds: unavailable }
|
||||
: layout,
|
||||
);
|
||||
});
|
||||
|
||||
export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
Reference in new issue
Block a user