mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
The worth-a-look rail mixed actionable items (an overdue dose, a broken sync) with observational ones (a milestone, a new ECG pointer, a tension window) that have nothing to act on beyond "seen it" — there was no way to clear them short of them expiring on their own. Add server-side dismissal scoped to the three observational kinds only: a new DismissedPriorityItem table (userId + itemKey, composite-unique), a POST /api/daily/digest/dismiss route, and a deterministic itemKey the digest builder stamps per kind (metric + reach day for a milestone, the recording's own timestamp for an ECG, the local day + part of day for a tension window) so a dismissal never bleeds into a later day's instance. The actionable kinds never carry an itemKey, so the dismiss affordance and the endpoint are both structurally unreachable for them — an itemKey not namespaced under a dismissible kind 422s before any lookup.
42 lines
1.8 KiB
TypeScript
42 lines
1.8 KiB
TypeScript
/**
|
|
* Deterministic identity keys for the dismissible (observational) Today rail
|
|
* items — `milestone`, `ecg_new_recording`, `tension_window`. Pure string
|
|
* formatting, no DB, no clock: both the digest composer (`./digest.ts`, which
|
|
* stamps the key onto the emitted `PriorityItem`) and the IO seam
|
|
* (`./load-digest.ts`, which needs the SAME keys to look up which of today's
|
|
* candidates are already dismissed) import from here so the format is one
|
|
* source of truth.
|
|
*
|
|
* Each key is namespaced `<kind>:...` (see `isDismissibleItemKey` in
|
|
* `./priority-item.ts`) and folds in enough of the underlying candidate's own
|
|
* identity that a fresh instance the NEXT day never collides with today's
|
|
* dismissed one — a milestone by its reach day, an ECG recording by its own
|
|
* timestamp, a tension window by the local day it was detected for.
|
|
*/
|
|
import type { Milestone } from "@/lib/daily/milestones";
|
|
|
|
/** `milestone:<kind>:<metricType>:<sinceDate>` — one key per durable state reached. */
|
|
export function milestoneItemKey(
|
|
milestone: Pick<Milestone, "kind" | "metricType" | "sinceDate">,
|
|
): string {
|
|
return `milestone:${milestone.kind}:${milestone.metricType}:${milestone.sinceDate}`;
|
|
}
|
|
|
|
/** `ecg_new_recording:<recordedAt ISO>` — the recording's own timestamp is unique. */
|
|
export function ecgItemKey(recordedAt: Date): string {
|
|
return `ecg_new_recording:${recordedAt.toISOString()}`;
|
|
}
|
|
|
|
/**
|
|
* `tension_window:<localDayKey>:<partOfDay>` — at most one per user per day.
|
|
* `partOfDay` mirrors `DailyDigestTensionWindow["partOfDay"]` (`./digest.ts`)
|
|
* by value rather than by import, so this module stays a leaf the digest
|
|
* composer can depend on without a cycle.
|
|
*/
|
|
export function tensionWindowItemKey(
|
|
localDayKey: string,
|
|
partOfDay: "morning" | "afternoon" | "evening" | "night",
|
|
): string {
|
|
return `tension_window:${localDayKey}:${partOfDay}`;
|
|
}
|