mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +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
@@ -911,6 +911,8 @@ model User {
|
||||
// per-day timeline mirroring the CycleDayLog shape.
|
||||
illnessEpisodes IllnessEpisode[]
|
||||
illnessDayLogs IllnessDayLog[]
|
||||
symptomDefinitions SymptomDefinition[]
|
||||
symptomEvents SymptomEvent[]
|
||||
// v1.25 (W-RECORDS) — structured health-record entries. Allergies map to
|
||||
// FHIR AllergyIntolerance; family-history entries map to FHIR
|
||||
// FamilyMemberHistory. Reference records (not time-series signals); the
|
||||
@@ -7726,6 +7728,8 @@ model IllnessEpisode {
|
||||
dayLogs IllnessDayLog[]
|
||||
documentLinks DocumentConditionLink[]
|
||||
encounterLinks EncounterConditionLink[]
|
||||
/// Symptom events the person filed against this episode while it was open.
|
||||
symptomEvents SymptomEvent[]
|
||||
|
||||
@@index([userId, deletedAt])
|
||||
@@index([userId, onsetAt])
|
||||
@@ -7814,6 +7818,68 @@ model IllnessSymptomLink {
|
||||
@@map("illness_symptom_links")
|
||||
}
|
||||
|
||||
/// A symptom the person defined for themselves ("aura", "headache"), v1.40.
|
||||
///
|
||||
/// Separate from the seeded `IllnessSymptom` catalogue on the illness day log:
|
||||
/// that one is a fixed list of cold symptoms rated 1-3 per episode day, this one
|
||||
/// is the person's own vocabulary, logged as timestamped events with a 0-10
|
||||
/// intensity whether or not an episode is open. Rides the `illness` module.
|
||||
/// At most eight active definitions per account
|
||||
/// (`MAX_ACTIVE_SYMPTOM_DEFINITIONS`), and each active one is its own
|
||||
/// correlation channel `SYMPTOM:<id>`, so the cap also bounds the matrix.
|
||||
model SymptomDefinition {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
/// The person's own name for the symptom, AES-256-GCM encrypted at rest. A
|
||||
/// symptom name is health data in the person's own words.
|
||||
labelEncrypted Bytes @map("label_encrypted")
|
||||
/// Lucide icon name from `SYMPTOM_ICON_ALLOWLIST` (the cycle custom-symptom
|
||||
/// list, which the iOS client already maps). NULL = the default glyph.
|
||||
icon String?
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
/// FALSE = hidden: not offered in the quick entry, not a correlation
|
||||
/// channel, history kept. Does not count toward the cap.
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
events SymptomEvent[]
|
||||
|
||||
@@index([userId, sortOrder])
|
||||
@@map("symptom_definitions")
|
||||
}
|
||||
|
||||
/// One occurrence of a person-defined symptom: when, how strong (0-10, the
|
||||
/// same numeric rating scale the pain score uses), an optional note, and an
|
||||
/// optional link to the illness episode it happened during. v1.40.
|
||||
model SymptomEvent {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
definitionId String @map("definition_id")
|
||||
/// The instant it happened (UTC). Day grouping is the reader's concern.
|
||||
occurredAt DateTime @map("occurred_at")
|
||||
/// 0-10, CHECK-constrained in the migration.
|
||||
intensity Int
|
||||
/// Optional free-text note, AES-256-GCM encrypted at rest.
|
||||
noteEncrypted Bytes? @map("note_encrypted")
|
||||
/// The episode the person said this happened during. Only an open episode
|
||||
/// of the same account can be linked (the route checks); deleting the
|
||||
/// episode row keeps the event and drops the link.
|
||||
episodeId String? @map("episode_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
definition SymptomDefinition @relation(fields: [definitionId], references: [id], onDelete: Cascade)
|
||||
episode IllnessEpisode? @relation(fields: [episodeId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([userId, occurredAt])
|
||||
@@index([definitionId, occurredAt])
|
||||
@@index([episodeId])
|
||||
@@map("symptom_events")
|
||||
}
|
||||
|
||||
// ─── Structured health records (v1.25 W-RECORDS) ──────────────────
|
||||
//
|
||||
// Allergies + family history kept as STRUCTURED records (not time-series
|
||||
|
||||
Reference in new issue
Block a user