mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(symptoms): add person-defined symptoms and timestamped occurrences
A symptom of one's own ("aura", "headache") is a definition with an
encrypted name and an icon from the cycle custom-symptom allowlist; each
occurrence carries a time, a 0-10 intensity (the pain score's scale), an
optional encrypted note and an optional link to one of the record's open
illness episodes. Both ride the illness module: every route answers
illness.disabled when it is off.
- Migration 0369 (idempotent): symptom_definitions and symptom_events,
intensity CHECK 0-10, episode link ON DELETE SET NULL.
- /api/symptoms/definitions{,/[id]} and /api/symptoms/events{,/[id]}:
eight active definitions per record (a hidden one frees its slot,
showing it again re-checks), hide or purge, events create/list/edit/
delete, a new link must name an open episode of the same record
(422 symptoms.episode.notOpen). Error group symptoms.*.
- Sharing: reads at READ, logging an occurrence at WRITE, defining and
editing at MANAGE; C1/C3/C4 audit details; frozen lists, the handler
matrix and the activity verbs updated.
- Both encrypted columns registered and rotated; both models in the wipe
plan; both carried at both backup ends with occurrences nested in their
definition, plaintext in a portable file and ciphertext in a recovery
file, and a link to an episode the file lacks dropped and named
(symptomEpisodeReference).
- OpenAPI regenerated.
This commit is contained in:
1 parent
7cd323499e
commit
d6571c4e9c
30 files changed
+3091
-16
No files matched your search
@@ -991,6 +991,22 @@ const DELEGABLE_ROUTES: Record<string, DelegableEntry> = {
|
||||
domain: "illness",
|
||||
why: "The day logs of one episode. The episode-scoped queries are safe because `loadOwnedEpisode(id, user.id)` runs first and now resolves against the record; the upsert arm keeps `requireAuth()`.",
|
||||
},
|
||||
"app/api/symptoms/definitions/route.ts": {
|
||||
domain: "illness",
|
||||
why: "The record's own symptom list. Module-gated on the record on both arms and scoped `userId: user.id`; the create arm is MANAGE (see the manage literal), because the vocabulary is the owner's and a delegate who may log an occurrence picks from it.",
|
||||
},
|
||||
"app/api/symptoms/definitions/[id]/route.ts": {
|
||||
domain: "illness",
|
||||
why: "One symptom definition of the record, fetch-then-guard against the resolved user. Reached only through this module's MANAGE arm; the argument for admitting it is the manage literal's reason line.",
|
||||
},
|
||||
"app/api/symptoms/events/route.ts": {
|
||||
domain: "illness",
|
||||
why: "The record's symptom occurrences. Module-gated on the record; the definition and the optional episode a create names are both re-narrowed to the resolved user before anything is written, so a delegate cannot file one record's occurrence under another's episode. The create arm is a delegable write.",
|
||||
},
|
||||
"app/api/symptoms/events/[id]/route.ts": {
|
||||
domain: "illness",
|
||||
why: "One symptom occurrence of the record, fetch-then-guard against the resolved user. Reached only through this module's MANAGE arm.",
|
||||
},
|
||||
"app/api/workouts/route.ts": {
|
||||
domain: "measurements",
|
||||
why: "The record's workout list. Verified to carry no AI-written paragraph: the single-workout route beside it does, and stays refused for exactly that reason.",
|
||||
@@ -1427,6 +1443,8 @@ const DELEGABLE_WRITE_ROUTES: Record<string, string> = {
|
||||
"Adding an analyte to the record's catalogue. A name the record already tracks is the ordinary 409 from the same `(userId, name)` uniqueness the owner would hit themselves.",
|
||||
"app/api/illness/episodes/route.ts":
|
||||
"Opening an illness episode. The module gate runs against the RECORD before the write, so a delegate cannot create an episode inside a record whose owner switched the module off.",
|
||||
"app/api/symptoms/events/route.ts":
|
||||
"Logging a symptom occurrence. The definition and the episode the body names are looked up under the RECORD, and an episode must still be open, so a delegate cannot attach an occurrence to an episode the owner closed or to another record's.",
|
||||
"app/api/encounters/route.ts":
|
||||
"Filing a visit, or booking one. Every id the body may carry — the practitioner, the checkup it closes, the three link arrays — is re-narrowed to the resolved record before anything is written, so a delegate cannot attach one record's document to another's visit. Booking mints the appointment reminder under the RECORD, which is correct: it is the owner's appointment and the owner's channels that should be nudged about it.",
|
||||
"app/api/practitioners/route.ts":
|
||||
@@ -1596,6 +1614,21 @@ const DELEGABLE_MANAGE_ROUTES: Record<string, ManageEntry> = {
|
||||
conditions: ["C3"],
|
||||
why: "Writing a day of an episode. The upsert revives a tombstoned day and replaces that day's symptom links, so C3 names the date, the previous symptom set, and the revival.",
|
||||
},
|
||||
"app/api/symptoms/definitions/route.ts": {
|
||||
domain: "illness",
|
||||
conditions: ["C1"],
|
||||
why: "Defining a symptom on the record. Capped at eight active ones; C1 so a manager burns their own bucket rather than the owner's.",
|
||||
},
|
||||
"app/api/symptoms/definitions/[id]/route.ts": {
|
||||
domain: "illness",
|
||||
conditions: ["C1", "C3", "C4"],
|
||||
why: "Renaming, hiding and deleting a symptom. Hiding keeps the history; the purge is hard and takes the occurrences with it, so C3 names the definition and the count. C4 on the edit, over the plaintext fields only; a rename is named, never quoted. C1 on the bucket.",
|
||||
},
|
||||
"app/api/symptoms/events/[id]/route.ts": {
|
||||
domain: "illness",
|
||||
conditions: ["C3", "C4"],
|
||||
why: "Correcting and removing one occurrence. The delete is hard, so C3 carries its time and intensity; C4 on the edit, the encrypted note named and never quoted.",
|
||||
},
|
||||
"app/api/mood-entries/route.ts": {
|
||||
domain: "mind",
|
||||
conditions: ["C6"],
|
||||
@@ -1960,8 +1993,13 @@ const ACTOR_ROUTES: Record<string, string> = {
|
||||
* v1.39.2 -- the body-site view adds one: a read on the record list in the
|
||||
* visits' domain, with conditions joined only under an illness grant.
|
||||
* 231 -> 232.
|
||||
*
|
||||
* v1.40 -- person-defined symptoms add eight: four modules on the record
|
||||
* list, the occurrence create on the write literal, and the definition create,
|
||||
* the definition edit/delete and the occurrence edit/delete on the manage
|
||||
* literal. 232 -> 240.
|
||||
*/
|
||||
const FROZEN_ENTRY_COUNT = 232;
|
||||
const FROZEN_ENTRY_COUNT = 240;
|
||||
|
||||
/**
|
||||
* The two surfaces that authenticate a Bearer token outside `requireAuth` —
|
||||
@@ -2722,7 +2760,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(77);
|
||||
expect(ADMITTED_MUTATING_HANDLERS.length).toBe(83);
|
||||
|
||||
const expected = ADMITTED_MUTATING_HANDLERS.map(
|
||||
({ handlerModule, action, level }) =>
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("sharing and handler inventories", () => {
|
||||
});
|
||||
|
||||
it("keeps every admitted mutation uniquely addressable and fully controlled", () => {
|
||||
expect(ADMITTED_MUTATING_HANDLERS).toHaveLength(77);
|
||||
expect(ADMITTED_MUTATING_HANDLERS).toHaveLength(83);
|
||||
expect(
|
||||
new Set(
|
||||
ADMITTED_MUTATING_HANDLERS.map(
|
||||
|
||||
Reference in new issue
Block a user