mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
Let a checkup be skipped or pushed back, and remember every outcome
Refs #223. A due Vorsorge item could only be done, deleted or ignored; Telegram's postpone moved the date with no trace. Both decisions are first-class now, and both are honest: - POST /{id}/skip (no body): the interval restarts from the skip instant through the recurrence engine, lastSkippedAt is stamped, skipCount increments, any snooze clears. lastSatisfiedAt is NEVER touched — done and skipped stay distinguishable forever, enforced in the primitive and pinned by tests. - POST /{id}/snooze ({until: YYYY-MM-DD}): the server resolves the day to the notifyHour in the profile timezone and moves snoozedUntil and nextDueAt to the same instant, so every due-state consumer follows without knowing snooze exists. At least tomorrow, at most five years. - GET /{id}/history: the new append-only completion ledger (MeasurementReminderEvent), one row per satisfy from every engine path and per skip, onTime derived at write time against the due instant that was current when the event landed. Two anchor fixes close the backdated-event gaps: the auto-resolve floor is now the later of last satisfy and last skip (an old reading synced late no longer resolves the cycle the user just skipped), and a backdated real result re-anchors the next due at the later of its own instant and the skip decision. Screenings stay skippable and snoozable — the satisfy-side 409 guards claimed fulfilment; a skip claims the opposite. Appointment reminders 404 on all three routes, in the lookup, like every by-id sibling. /checkups gains the postpone sheet (quick chips, date field, the skip zone with its own confirm) in both view branches, with muted meta lines and no card colour change. The MCP preventive-care tool carries the resolved fields; the OpenAPI contract publishes all three paths and the widened DTO.
This commit is contained in:
1 parent
cd12b3886e
commit
5619783648
35 files changed
+3715
-115
No files matched your search
@@ -97,6 +97,9 @@ function reminderRow(
|
||||
location: null,
|
||||
nextDueAt: at,
|
||||
lastSatisfiedAt: null,
|
||||
snoozedUntil: null,
|
||||
lastSkippedAt: null,
|
||||
skipCount: 0,
|
||||
enabled: true,
|
||||
vaccinationAntigen: null,
|
||||
createdAt: at,
|
||||
@@ -152,6 +155,27 @@ const BY_ID_SITES: ReadonlyArray<{
|
||||
sites: 1,
|
||||
surface: "the manual satisfy action",
|
||||
},
|
||||
{
|
||||
// v1.37.20 (#223) — skipping an appointment would move a date the visit
|
||||
// record still believes it owns; snoozing one likewise. Both refuse in
|
||||
// the lookup, before any write. This entry went in RED (the routes did
|
||||
// not exist yet) and turned green when they landed with the exclusion.
|
||||
file: "src/app/api/measurement-reminders/[id]/skip/route.ts",
|
||||
sites: 1,
|
||||
surface: "the honest skip action",
|
||||
},
|
||||
{
|
||||
file: "src/app/api/measurement-reminders/[id]/snooze/route.ts",
|
||||
sites: 1,
|
||||
surface: "the snooze action",
|
||||
},
|
||||
{
|
||||
// v1.37.20 (iOS #68) — the ledger read answers 404 for an appointment
|
||||
// row like every by-id sibling.
|
||||
file: "src/app/api/measurement-reminders/[id]/history/route.ts",
|
||||
sites: 1,
|
||||
surface: "the completion-ledger history read",
|
||||
},
|
||||
{
|
||||
file: "src/lib/telegram-webhook-handlers.ts",
|
||||
sites: 2,
|
||||
@@ -161,9 +185,9 @@ const BY_ID_SITES: ReadonlyArray<{
|
||||
|
||||
describe("an appointment reminder is not addressable by id", () => {
|
||||
it("names a by-id set that is not empty (no vacuous pass)", () => {
|
||||
expect(BY_ID_SITES.length).toBe(4);
|
||||
expect(BY_ID_SITES.length).toBe(7);
|
||||
expect(BY_ID_SITES.reduce((total, entry) => total + entry.sites, 0)).toBe(
|
||||
7,
|
||||
10,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1140,6 +1140,18 @@ const DELEGABLE_ROUTES: Record<string, DelegableEntry> = {
|
||||
domain: "measurements",
|
||||
why: "The same primitive from the other caller. Reached only through this module's MANAGE arm; the argument for admitting it is the manage literal's reason line, and the entry here is what leg (a) freezes and leg (f) checks the section against.",
|
||||
},
|
||||
"app/api/measurement-reminders/[id]/skip/route.ts": {
|
||||
domain: "measurements",
|
||||
why: "Honestly skipping the current due cycle (v1.37.20, #223) — the counterpart of complete/satisfy, restarting the interval without claiming fulfilment. Reached only through this module's MANAGE arm; the argument for admitting it is the manage literal's reason line, and the entry here is what leg (a) freezes and leg (f) checks the section against.",
|
||||
},
|
||||
"app/api/measurement-reminders/[id]/snooze/route.ts": {
|
||||
domain: "measurements",
|
||||
why: "Pushing one due date back to a calendar day (v1.37.20, #223), the cadence untouched. Reached only through this module's MANAGE arm; the argument for admitting it is the manage literal's reason line, and the entry here is what leg (a) freezes and leg (f) checks the section against.",
|
||||
},
|
||||
"app/api/measurement-reminders/[id]/history/route.ts": {
|
||||
domain: "measurements",
|
||||
why: "The completion ledger behind one reminder (v1.37.20, iOS #68), read-only and newest-first. The lookup binds the resolved user id and refuses an appointment row like every by-id sibling.",
|
||||
},
|
||||
"app/api/labs/restore/route.ts": {
|
||||
domain: "labs",
|
||||
why: "Restoring tombstoned results, scoped so a foreign or live id is a no-op. Reached only through this module's MANAGE arm; the argument for admitting it is the manage literal's reason line, and the entry here is what leg (a) freezes and leg (f) checks the section against.",
|
||||
@@ -1477,6 +1489,16 @@ const DELEGABLE_MANAGE_ROUTES: Record<string, ManageEntry> = {
|
||||
conditions: [],
|
||||
why: "The same primitive from the other caller.",
|
||||
},
|
||||
"app/api/measurement-reminders/[id]/skip/route.ts": {
|
||||
domain: "measurements",
|
||||
conditions: [],
|
||||
why: "Honestly letting one due cycle go (v1.37.20, #223). Additive management of a schedule the level admits — the interval restarts, lastSatisfiedAt is never touched.",
|
||||
},
|
||||
"app/api/measurement-reminders/[id]/snooze/route.ts": {
|
||||
domain: "measurements",
|
||||
conditions: [],
|
||||
why: "Pushing one due date back (v1.37.20, #223). Additive, self-expiring, the anchor untouched.",
|
||||
},
|
||||
"app/api/labs/[id]/route.ts": {
|
||||
domain: "labs",
|
||||
conditions: ["C4"],
|
||||
@@ -1911,8 +1933,12 @@ const ACTOR_ROUTES: Record<string, string> = {
|
||||
* v1.38.0 -- the immunization log adds six: two reads and one restore on the
|
||||
* record list, one create on the write literal, and the edit/delete pair plus
|
||||
* the restore on the manage literal. 215 -> 221.
|
||||
*
|
||||
* v1.37.20 (#223 / iOS #68) -- Vorsorge skip/snooze adds five: skip, snooze
|
||||
* and the completion-ledger history read on the record list, plus skip and
|
||||
* snooze on the manage literal. 224 -> 229.
|
||||
*/
|
||||
const FROZEN_ENTRY_COUNT = 224;
|
||||
const FROZEN_ENTRY_COUNT = 229;
|
||||
|
||||
/**
|
||||
* The two surfaces that authenticate a Bearer token outside `requireAuth` —
|
||||
@@ -2673,7 +2699,7 @@ describe("(g) the MANAGE route set is frozen", () => {
|
||||
|
||||
it("keeps the admitted mutation inventory complete and discoverable", () => {
|
||||
expect(ADMITTED_MUTATING_HANDLERS.length).toBeGreaterThan(0);
|
||||
expect(ADMITTED_MUTATING_HANDLERS.length).toBe(75);
|
||||
expect(ADMITTED_MUTATING_HANDLERS.length).toBe(77);
|
||||
|
||||
const expected = ADMITTED_MUTATING_HANDLERS.map(
|
||||
({ handlerModule, action, level }) =>
|
||||
|
||||
Reference in new issue
Block a user