mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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.
709 lines
14 KiB
TypeScript
709 lines
14 KiB
TypeScript
export const SHARING_DOMAINS = [
|
|
"measurements",
|
|
"medications",
|
|
"labs",
|
|
"profile",
|
|
"illness",
|
|
"mind",
|
|
"cycle",
|
|
"documents",
|
|
] as const;
|
|
|
|
export type SharingDomain = (typeof SHARING_DOMAINS)[number];
|
|
export type SharingScope = SharingDomain | "record";
|
|
export type DelegatedLevel = "write" | "manage";
|
|
export type MutatingAction = "POST" | "PUT" | "PATCH" | "DELETE";
|
|
export type RecordKind = "shared" | "managed";
|
|
export type MutationEffect =
|
|
"create" | "update" | "delete" | "restore" | "upsert" | "export";
|
|
|
|
export interface MutatingHandlerContract {
|
|
readonly handlerModule: `app/api/${string}/route.ts`;
|
|
readonly route: `/api/${string}`;
|
|
readonly action: MutatingAction;
|
|
readonly recordKind: RecordKind;
|
|
readonly scope: SharingScope;
|
|
readonly level: DelegatedLevel;
|
|
readonly ownRecord: {
|
|
readonly actor: "record-owner";
|
|
readonly expected: "allow";
|
|
};
|
|
readonly deniedLevel: {
|
|
readonly actor: "delegate";
|
|
readonly level: "read" | "write";
|
|
readonly expected: "deny";
|
|
};
|
|
readonly admittedLevel: {
|
|
readonly actor: "delegate";
|
|
readonly expected: "allow";
|
|
};
|
|
readonly actorAttribution: {
|
|
readonly field: "actorUserId";
|
|
readonly expected: "acting-delegate";
|
|
};
|
|
readonly resultingEffect: {
|
|
readonly expected: "applied-once";
|
|
readonly mutation: MutationEffect;
|
|
};
|
|
readonly sideEffect: {
|
|
readonly recordTarget: "resolved-record";
|
|
readonly delegatedProviderEgress: "deny";
|
|
};
|
|
readonly encryptedFieldCarveOut: "never-assert-decrypted-field-content";
|
|
}
|
|
|
|
function handler(
|
|
handlerModule: `app/api/${string}/route.ts`,
|
|
route: `/api/${string}`,
|
|
action: MutatingAction,
|
|
scope: SharingScope,
|
|
level: DelegatedLevel,
|
|
mutation: MutationEffect,
|
|
): MutatingHandlerContract {
|
|
return {
|
|
handlerModule,
|
|
route,
|
|
action,
|
|
recordKind: "shared",
|
|
scope,
|
|
level,
|
|
ownRecord: { actor: "record-owner", expected: "allow" },
|
|
deniedLevel: {
|
|
actor: "delegate",
|
|
level: level === "write" ? "read" : "write",
|
|
expected: "deny",
|
|
},
|
|
admittedLevel: { actor: "delegate", expected: "allow" },
|
|
actorAttribution: {
|
|
field: "actorUserId",
|
|
expected: "acting-delegate",
|
|
},
|
|
resultingEffect: { expected: "applied-once", mutation },
|
|
sideEffect: {
|
|
recordTarget: "resolved-record",
|
|
delegatedProviderEgress: "deny",
|
|
},
|
|
encryptedFieldCarveOut: "never-assert-decrypted-field-content",
|
|
};
|
|
}
|
|
|
|
export const ADMITTED_MUTATING_HANDLERS = [
|
|
handler(
|
|
"app/api/measurements/route.ts",
|
|
"/api/measurements",
|
|
"POST",
|
|
"measurements",
|
|
"write",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/labs/route.ts",
|
|
"/api/labs",
|
|
"POST",
|
|
"labs",
|
|
"write",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/biomarkers/route.ts",
|
|
"/api/biomarkers",
|
|
"POST",
|
|
"labs",
|
|
"write",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/illness/episodes/route.ts",
|
|
"/api/illness/episodes",
|
|
"POST",
|
|
"illness",
|
|
"write",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/encounters/route.ts",
|
|
"/api/encounters",
|
|
"POST",
|
|
"profile",
|
|
"write",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/practitioners/route.ts",
|
|
"/api/practitioners",
|
|
"POST",
|
|
"profile",
|
|
"write",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/side-effects/route.ts",
|
|
"/api/medications/[id]/side-effects",
|
|
"POST",
|
|
"medications",
|
|
"write",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/medications/route.ts",
|
|
"/api/medications",
|
|
"POST",
|
|
"medications",
|
|
"write",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/medications/intake/route.ts",
|
|
"/api/medications/intake",
|
|
"POST",
|
|
"medications",
|
|
"write",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/intake/route.ts",
|
|
"/api/medications/[id]/intake",
|
|
"POST",
|
|
"medications",
|
|
"write",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/measurements/[id]/route.ts",
|
|
"/api/measurements/[id]",
|
|
"PUT",
|
|
"measurements",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/measurements/[id]/route.ts",
|
|
"/api/measurements/[id]",
|
|
"DELETE",
|
|
"measurements",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/measurements/bulk-delete/route.ts",
|
|
"/api/measurements/bulk-delete",
|
|
"POST",
|
|
"measurements",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/measurements/restore/route.ts",
|
|
"/api/measurements/restore",
|
|
"POST",
|
|
"measurements",
|
|
"manage",
|
|
"restore",
|
|
),
|
|
handler(
|
|
"app/api/measurement-reminders/route.ts",
|
|
"/api/measurement-reminders",
|
|
"POST",
|
|
"measurements",
|
|
"manage",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/measurement-reminders/[id]/route.ts",
|
|
"/api/measurement-reminders/[id]",
|
|
"PATCH",
|
|
"measurements",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/measurement-reminders/[id]/route.ts",
|
|
"/api/measurement-reminders/[id]",
|
|
"DELETE",
|
|
"measurements",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/measurement-reminders/[id]/complete/route.ts",
|
|
"/api/measurement-reminders/[id]/complete",
|
|
"POST",
|
|
"measurements",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/measurement-reminders/[id]/satisfy/route.ts",
|
|
"/api/measurement-reminders/[id]/satisfy",
|
|
"POST",
|
|
"measurements",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/measurement-reminders/[id]/skip/route.ts",
|
|
"/api/measurement-reminders/[id]/skip",
|
|
"POST",
|
|
"measurements",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/measurement-reminders/[id]/snooze/route.ts",
|
|
"/api/measurement-reminders/[id]/snooze",
|
|
"POST",
|
|
"measurements",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/labs/[id]/route.ts",
|
|
"/api/labs/[id]",
|
|
"PUT",
|
|
"labs",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/labs/[id]/route.ts",
|
|
"/api/labs/[id]",
|
|
"DELETE",
|
|
"labs",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/labs/restore/route.ts",
|
|
"/api/labs/restore",
|
|
"POST",
|
|
"labs",
|
|
"manage",
|
|
"restore",
|
|
),
|
|
handler(
|
|
"app/api/biomarkers/[id]/route.ts",
|
|
"/api/biomarkers/[id]",
|
|
"PUT",
|
|
"labs",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/allergies/route.ts",
|
|
"/api/allergies",
|
|
"POST",
|
|
"profile",
|
|
"manage",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/allergies/[id]/route.ts",
|
|
"/api/allergies/[id]",
|
|
"PATCH",
|
|
"profile",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/allergies/[id]/route.ts",
|
|
"/api/allergies/[id]",
|
|
"DELETE",
|
|
"profile",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/family-history/route.ts",
|
|
"/api/family-history",
|
|
"POST",
|
|
"profile",
|
|
"manage",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/family-history/[id]/route.ts",
|
|
"/api/family-history/[id]",
|
|
"PATCH",
|
|
"profile",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/family-history/[id]/route.ts",
|
|
"/api/family-history/[id]",
|
|
"DELETE",
|
|
"profile",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/illness/episodes/[id]/route.ts",
|
|
"/api/illness/episodes/[id]",
|
|
"PATCH",
|
|
"illness",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/illness/episodes/[id]/route.ts",
|
|
"/api/illness/episodes/[id]",
|
|
"DELETE",
|
|
"illness",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/illness/episodes/[id]/resolve/route.ts",
|
|
"/api/illness/episodes/[id]/resolve",
|
|
"PATCH",
|
|
"illness",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/illness/episodes/[id]/restore/route.ts",
|
|
"/api/illness/episodes/[id]/restore",
|
|
"POST",
|
|
"illness",
|
|
"manage",
|
|
"restore",
|
|
),
|
|
handler(
|
|
"app/api/encounters/[id]/route.ts",
|
|
"/api/encounters/[id]",
|
|
"PATCH",
|
|
"profile",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/encounters/[id]/route.ts",
|
|
"/api/encounters/[id]",
|
|
"DELETE",
|
|
"profile",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/encounters/[id]/restore/route.ts",
|
|
"/api/encounters/[id]/restore",
|
|
"POST",
|
|
"profile",
|
|
"manage",
|
|
"restore",
|
|
),
|
|
handler(
|
|
"app/api/practitioners/[id]/route.ts",
|
|
"/api/practitioners/[id]",
|
|
"PATCH",
|
|
"profile",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/practitioners/[id]/route.ts",
|
|
"/api/practitioners/[id]",
|
|
"DELETE",
|
|
"profile",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/practitioners/[id]/restore/route.ts",
|
|
"/api/practitioners/[id]/restore",
|
|
"POST",
|
|
"profile",
|
|
"manage",
|
|
"restore",
|
|
),
|
|
// v1.38.0 — the immunization log.
|
|
handler(
|
|
"app/api/vaccinations/route.ts",
|
|
"/api/vaccinations",
|
|
"POST",
|
|
"profile",
|
|
"write",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/vaccinations/[id]/route.ts",
|
|
"/api/vaccinations/[id]",
|
|
"PATCH",
|
|
"profile",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/vaccinations/[id]/route.ts",
|
|
"/api/vaccinations/[id]",
|
|
"DELETE",
|
|
"profile",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/vaccinations/[id]/restore/route.ts",
|
|
"/api/vaccinations/[id]/restore",
|
|
"POST",
|
|
"profile",
|
|
"manage",
|
|
"restore",
|
|
),
|
|
handler(
|
|
"app/api/vaccinations/[id]/booster/route.ts",
|
|
"/api/vaccinations/[id]/booster",
|
|
"POST",
|
|
"profile",
|
|
"write",
|
|
"upsert",
|
|
),
|
|
handler(
|
|
"app/api/illness/episodes/[id]/day-logs/route.ts",
|
|
"/api/illness/episodes/[id]/day-logs",
|
|
"POST",
|
|
"illness",
|
|
"manage",
|
|
"upsert",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/route.ts",
|
|
"/api/medications/[id]",
|
|
"PUT",
|
|
"medications",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/schedule-revisions/route.ts",
|
|
"/api/medications/[id]/schedule-revisions",
|
|
"POST",
|
|
"medications",
|
|
"manage",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts",
|
|
"/api/medications/[id]/schedule-revisions/[revisionId]",
|
|
"PATCH",
|
|
"medications",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts",
|
|
"/api/medications/[id]/schedule-revisions/[revisionId]",
|
|
"DELETE",
|
|
"medications",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/inventory/route.ts",
|
|
"/api/medications/[id]/inventory",
|
|
"POST",
|
|
"medications",
|
|
"manage",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/inventory/[itemId]/route.ts",
|
|
"/api/medications/[id]/inventory/[itemId]",
|
|
"PATCH",
|
|
"medications",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/inventory/[itemId]/route.ts",
|
|
"/api/medications/[id]/inventory/[itemId]",
|
|
"DELETE",
|
|
"medications",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/side-effects/[logId]/route.ts",
|
|
"/api/medications/[id]/side-effects/[logId]",
|
|
"DELETE",
|
|
"medications",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/glp1/route.ts",
|
|
"/api/medications/[id]/glp1",
|
|
"POST",
|
|
"medications",
|
|
"manage",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/intake/[eventId]/route.ts",
|
|
"/api/medications/[id]/intake/[eventId]",
|
|
"PUT",
|
|
"medications",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/intake/[eventId]/route.ts",
|
|
"/api/medications/[id]/intake/[eventId]",
|
|
"DELETE",
|
|
"medications",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/intake/bulk-delete/route.ts",
|
|
"/api/medications/[id]/intake/bulk-delete",
|
|
"POST",
|
|
"medications",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/intake/purge/route.ts",
|
|
"/api/medications/[id]/intake/purge",
|
|
"DELETE",
|
|
"medications",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/medications/[id]/intake/import/route.ts",
|
|
"/api/medications/[id]/intake/import",
|
|
"POST",
|
|
"medications",
|
|
"manage",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/medications/intake/dose-history-import/route.ts",
|
|
"/api/medications/intake/dose-history-import",
|
|
"POST",
|
|
"medications",
|
|
"manage",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/mood-entries/route.ts",
|
|
"/api/mood-entries",
|
|
"POST",
|
|
"mind",
|
|
"manage",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/mood-entries/[id]/route.ts",
|
|
"/api/mood-entries/[id]",
|
|
"PUT",
|
|
"mind",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/mood-entries/[id]/route.ts",
|
|
"/api/mood-entries/[id]",
|
|
"DELETE",
|
|
"mind",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/mood-entries/bulk-delete/route.ts",
|
|
"/api/mood-entries/bulk-delete",
|
|
"POST",
|
|
"mind",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/mood-entries/restore/route.ts",
|
|
"/api/mood-entries/restore",
|
|
"POST",
|
|
"mind",
|
|
"manage",
|
|
"restore",
|
|
),
|
|
handler(
|
|
"app/api/mental-health/assessments/route.ts",
|
|
"/api/mental-health/assessments",
|
|
"POST",
|
|
"mind",
|
|
"manage",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/cycle/cycles/[id]/route.ts",
|
|
"/api/cycle/cycles/[id]",
|
|
"DELETE",
|
|
"cycle",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/cycle/day-logs/route.ts",
|
|
"/api/cycle/day-logs",
|
|
"POST",
|
|
"cycle",
|
|
"manage",
|
|
"upsert",
|
|
),
|
|
handler(
|
|
"app/api/cycle/day-logs/[id]/route.ts",
|
|
"/api/cycle/day-logs/[id]",
|
|
"PATCH",
|
|
"cycle",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/cycle/day-logs/[id]/route.ts",
|
|
"/api/cycle/day-logs/[id]",
|
|
"DELETE",
|
|
"cycle",
|
|
"manage",
|
|
"delete",
|
|
),
|
|
handler(
|
|
"app/api/cycle/period/route.ts",
|
|
"/api/cycle/period",
|
|
"POST",
|
|
"cycle",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/cycle/symptoms/custom/route.ts",
|
|
"/api/cycle/symptoms/custom",
|
|
"POST",
|
|
"cycle",
|
|
"manage",
|
|
"create",
|
|
),
|
|
handler(
|
|
"app/api/nutrients/water/route.ts",
|
|
"/api/nutrients/water",
|
|
"POST",
|
|
"measurements",
|
|
"manage",
|
|
"upsert",
|
|
),
|
|
handler(
|
|
"app/api/insights/patterns/[id]/route.ts",
|
|
"/api/insights/patterns/[id]",
|
|
"PATCH",
|
|
"record",
|
|
"manage",
|
|
"update",
|
|
),
|
|
handler(
|
|
"app/api/export/health-record/route.ts",
|
|
"/api/export/health-record",
|
|
"POST",
|
|
"record",
|
|
"manage",
|
|
"export",
|
|
),
|
|
] as const satisfies readonly MutatingHandlerContract[];
|