feat(daily): let the Today rail dismiss milestone / ECG / tension cards

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.
This commit is contained in:
Marc-André Bombeck committed 2026-07-17 14:29:07 +02:00
1 parent 4f198ec25f
commit 270f33edb2
17 files changed
+903 -9

No files matched your search

+64
View File
@@ -10848,6 +10848,42 @@ paths:
"403": *a5
"422": *a3
"429": *a4
/api/daily/digest/dismiss:
post:
tags:
- Insights
summary: Dismiss a Today rail item
description: "\"Dismiss / mark seen\" for the Today rail's OBSERVATIONAL PriorityItem kinds only — milestone,
ecg_new_recording, tension_window. The ACTIONABLE kinds (dose_window, sync_issue, preventive_care,
coach_checkin) are structurally unreachable: itemKey must be namespaced under one of the three dismissible kinds
or the request 422s before any lookup runs. Persisted server-side so the dismissal survives reload / a second
device; an upsert, so a repeat dismiss of the same instance is a no-op. Requires the insights module. Cookie or
Bearer auth."
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
itemKey:
type: string
minLength: 1
maxLength: 200
required:
- itemKey
additionalProperties: false
responses:
"200":
description: The item is dismissed (idempotent).
content:
application/json:
schema:
$ref: "#/components/schemas/DismissPriorityItemEnvelope"
"401": *a1
"403": *a5
"422": *a3
"429": *a4
components:
schemas:
EncryptedExportRequest:
@@ -30150,6 +30186,10 @@ components:
- ecg_new_recording
- tension_window
description: Closed rail-item kind.
itemKey:
description: Stable dismiss identity, namespaced `<kind>:...`. Present only on the observational kinds (milestone /
ecg_new_recording / tension_window) — absent on the actionable kinds, which are never dismissible.
type: string
title:
type: string
description: Localised headline (resolved server-side).
@@ -30193,6 +30233,30 @@ components:
- actions
additionalProperties: false
description: One 'worth a look' rail item. The single model every daily-value consumer renders through PriorityCard.
DismissPriorityItemEnvelope:
type: object
properties:
data:
type: object
properties:
dismissed:
type: boolean
const: true
required:
- dismissed
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationScheduleOutput:
type: object
properties: