feat(body-sites): record a body site on conditions and add a body-site view

A condition (illness episode) can carry where on the body it sits, in the
person's own words and encrypted at rest, plus an optional side (LEFT,
RIGHT, BOTH). Same enum, codec and reasoning as the procedure's site.

- migration 0354: two nullable columns on illness_episodes, reusing the
  laterality type from 0349
- create and edit accept bodySite and laterality; on PATCH an absent key
  keeps the stored value, so an edit from the shipped iPhone app, which
  never sends either field, leaves them untouched
- GET /api/body-sites: the sites the record holds across visits and
  conditions, folded after the decrypt and bounded per kind; with site
  (and laterality) the visits and conditions there, one hop to their
  documents, lab results and visits. Read in the profile section;
  conditions only under an illness grant with the module on, and links
  into ungranted sections come back as unlabelled placeholders
- the link service reads document and visit links from the condition's
  end of the two existing tables
- /checkups gains a Body sites tab; the visit and condition forms share
  one body-site field pair that suggests sites already used; a
  condition's page links its site into the view
- the doctor report, the clinician page and the MCP doctor-visit summary
  name the site on conditions; the Coach illness block carries it
  sanitised, and MCP get_illness_recovery returns it fenced
- backup and restore carry both fields in both file kinds; the site is
  registered as ciphertext and rotates with the key
- i18n in all seven locales, OpenAPI regenerated

Refs #1025
This commit is contained in:
Marc-André Bombeck committed 2026-09-25 21:55:14 +02:00
1 parent af65f01fec
commit 14f0fc4d4f
57 files changed
+3501 -133

No files matched your search

+9 -1
View File
@@ -947,6 +947,10 @@ const DELEGABLE_ROUTES: Record<string, DelegableEntry> = {
domain: "profile",
why: "The record's procedure and surgery history: the visits of kind PROCEDURE that happened, searched by body site. The same rows the visit list serves, read one way, so it sits under the same domain and level; the search runs over the resolved user's own rows after the decrypt and cannot address anyone else's.",
},
"app/api/body-sites/route.ts": {
domain: "profile",
why: "The body sites the record holds and, for a picked one, the procedures and conditions filed there. It starts from the visits, so it sits in their section and level. Conditions join only when the grant also covers illness and the record has the module on; otherwise the condition table is not read, and a procedure's links into a section the grant does not cover come back as unlabelled placeholders. Matching runs over the resolved user's own rows after the decrypt.",
},
"app/api/encounters/suggest/route.ts": {
domain: "profile",
why: "Which of the record's visits a document or lab panel dated around a given day belongs to. A read over the same rows the visit list serves, reduced to a verdict — the caller learns nothing about the record it could not learn from the list itself, and the anchor it passes is a date rather than an id, so it cannot address a row.",
@@ -1952,8 +1956,12 @@ const ACTOR_ROUTES: Record<string, string> = {
*
* v1.39.1 -- the procedure history adds one: a read on the record list at the
* level and domain of the visit list it is a view of. 230 -> 231.
*
* 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.
*/
const FROZEN_ENTRY_COUNT = 231;
const FROZEN_ENTRY_COUNT = 232;
/**
* The two surfaces that authenticate a Bearer token outside `requireAuth` —