mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
Twice-daily BP protocol (RRULE BYHOUR=7,19) now fires at every clock hour: byHourTimesOfDay expands BYHOUR into the engine's timesOfDay so the label, RRULE, and engine output agree instead of collapsing to one nudge at notifyHour. Vorsorge surface: - move create/edit into a ResponsiveSheet with a pinned Cancel/Save footer, matching Labs/Illness/Medications - wire the unused PATCH support: Edit affordance opens the form pre-filled; an enable/disable Switch drives the enabled flag - translate COACH-origin labels (the row stores an i18n key, not prose) and render a neutral Coach provenance badge + an until-<date> line - surface origin + endsOn on the hook type - pull-to-refresh + always-on rationale documented on the nav entry - NativeSelect-vs-Radix choice documented as deliberate Coach: - expired COACH course-window reminders self-clean: the cron tick soft-deletes finished finite cadences instead of leaving dead rows - accept-dedup gains a partial unique index (user, metric) where origin=COACH and not deleted, with a P2002 catch in the route - per-user rate-limit bucket on the suggestion-action endpoint - SUGGEST-REMINDER parser keeps prose written after the block - document POST /api/coach/reminder-suggestions in OpenAPI Coach chrome: - HistoryRail hideHeading prop drops the duplicate Conversations heading on the page surface - align the rail-tray strip inset to px-3 sm:px-4 so the dividers line up; reword the FAB inset comment satisfyReminder closes the forward-only TOCTOU via a conditional updateMany (count===0 no-op). Migration headers corrected to v1.18.1.
204 lines
8.3 KiB
SQL
204 lines
8.3 KiB
SQL
-- v1.18.1 — illness / condition journal.
|
||
--
|
||
-- A CONDITION journal (not a flu log): any illness/condition kept for
|
||
-- oneself, with a retrospective lens. Retrospective + correlated, NEVER a
|
||
-- predictor/diagnoser. Born-gated by the module system — the gate is
|
||
-- app-layer, so there is no enable flag in the schema.
|
||
--
|
||
-- Tables (the daily timeline mirrors the Cycle three-table shape):
|
||
--
|
||
-- * `illness_episodes` — one episode. `illness_type` widens beyond an
|
||
-- infection log (infection / allergy / injury / mental-health /
|
||
-- autoimmune / chronic / other). `illness_lifecycle`
|
||
-- (ACUTE / CHRONIC_ONGOING / RECURRING / FLARE) distinguishes a bout from
|
||
-- an ongoing chronic condition; CHRONIC_ONGOING carries no resolved_at
|
||
-- and is excluded from the recovery-gap math. `parent_condition_id` is a
|
||
-- self-FK (ON DELETE SET NULL) so a flare/recurrence threads under one
|
||
-- parent condition. `note_encrypted` is a Bytes column (the AES-256-GCM
|
||
-- `*Encrypted` convention). Soft-deleted.
|
||
--
|
||
-- * `illness_day_logs` — one row per day of an episode (mirrors
|
||
-- cycle_day_logs): a coarse 0–3 `functional_impact` slider, an optional
|
||
-- `fever_c` reading (queryable plaintext, the basal_body_temp_c
|
||
-- precedent), an encrypted day note, the day's symptom links. Unique per
|
||
-- (episode_id, date). Soft-deleted.
|
||
--
|
||
-- * `illness_symptoms` — a seeded, icon-bearing, labelKey-i18n catalog
|
||
-- (Jackson / WURSS-derived; the cycle_symptoms precedent). Eight
|
||
-- high-signal cold/illness symptoms, scored 0–3 on the link.
|
||
--
|
||
-- * `illness_symptom_links` — the per-day-log join, composite PK
|
||
-- (day_log_id, symptom_id), 0–3 severity, cascade on both sides.
|
||
--
|
||
-- Additive; no existing row touched. The catalog seed uses deterministic
|
||
-- ids + ON CONFLICT ("key") DO NOTHING so re-running is a no-op (the
|
||
-- cycle/mood-tag seed precedent). Keep in sync with the app-side taxonomy
|
||
-- module if either changes.
|
||
--
|
||
-- Idempotent guards (`IF NOT EXISTS` / `DO $$`) so reruns are safe on prod.
|
||
--
|
||
-- Reversibility (down):
|
||
-- DROP TABLE IF EXISTS "illness_symptom_links";
|
||
-- DROP TABLE IF EXISTS "illness_symptoms";
|
||
-- DROP TABLE IF EXISTS "illness_day_logs";
|
||
-- DROP TABLE IF EXISTS "illness_episodes";
|
||
-- DROP TYPE IF EXISTS "illness_lifecycle";
|
||
-- DROP TYPE IF EXISTS "illness_type";
|
||
-- A roll-back drops the journal wholesale — no other domain depends on it.
|
||
|
||
-- CreateEnum
|
||
DO $$ BEGIN
|
||
CREATE TYPE "illness_type" AS ENUM ('INFECTION', 'ALLERGY', 'INJURY', 'MENTAL_HEALTH', 'AUTOIMMUNE', 'CHRONIC', 'OTHER');
|
||
EXCEPTION
|
||
WHEN duplicate_object THEN null;
|
||
END $$;
|
||
|
||
-- CreateEnum
|
||
DO $$ BEGIN
|
||
CREATE TYPE "illness_lifecycle" AS ENUM ('ACUTE', 'CHRONIC_ONGOING', 'RECURRING', 'FLARE');
|
||
EXCEPTION
|
||
WHEN duplicate_object THEN null;
|
||
END $$;
|
||
|
||
-- CreateTable
|
||
CREATE TABLE IF NOT EXISTS "illness_episodes" (
|
||
"id" TEXT NOT NULL,
|
||
"user_id" TEXT NOT NULL,
|
||
"label" TEXT NOT NULL,
|
||
"type" "illness_type" NOT NULL,
|
||
"lifecycle" "illness_lifecycle" NOT NULL DEFAULT 'ACUTE',
|
||
"onset_at" TIMESTAMP(3) NOT NULL,
|
||
"resolved_at" TIMESTAMP(3),
|
||
"parent_condition_id" TEXT,
|
||
"note_encrypted" BYTEA,
|
||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||
"deleted_at" TIMESTAMP(3),
|
||
|
||
CONSTRAINT "illness_episodes_pkey" PRIMARY KEY ("id")
|
||
);
|
||
|
||
-- CreateTable
|
||
CREATE TABLE IF NOT EXISTS "illness_day_logs" (
|
||
"id" TEXT NOT NULL,
|
||
"user_id" TEXT NOT NULL,
|
||
"episode_id" TEXT NOT NULL,
|
||
"date" TEXT NOT NULL,
|
||
"functional_impact" INTEGER,
|
||
"fever_c" DOUBLE PRECISION,
|
||
"note_encrypted" BYTEA,
|
||
"tz" TEXT,
|
||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||
"deleted_at" TIMESTAMP(3),
|
||
|
||
CONSTRAINT "illness_day_logs_pkey" PRIMARY KEY ("id")
|
||
);
|
||
|
||
-- CreateTable
|
||
CREATE TABLE IF NOT EXISTS "illness_symptoms" (
|
||
"id" TEXT NOT NULL,
|
||
"key" TEXT NOT NULL,
|
||
"label_key" TEXT NOT NULL,
|
||
"icon" TEXT,
|
||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||
|
||
CONSTRAINT "illness_symptoms_pkey" PRIMARY KEY ("id")
|
||
);
|
||
|
||
-- CreateTable
|
||
CREATE TABLE IF NOT EXISTS "illness_symptom_links" (
|
||
"day_log_id" TEXT NOT NULL,
|
||
"symptom_id" TEXT NOT NULL,
|
||
"severity" INTEGER,
|
||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
|
||
CONSTRAINT "illness_symptom_links_pkey" PRIMARY KEY ("day_log_id","symptom_id")
|
||
);
|
||
|
||
-- CreateIndex
|
||
CREATE INDEX IF NOT EXISTS "illness_episodes_user_id_deleted_at_idx" ON "illness_episodes"("user_id", "deleted_at");
|
||
|
||
-- CreateIndex
|
||
CREATE INDEX IF NOT EXISTS "illness_episodes_user_id_onset_at_idx" ON "illness_episodes"("user_id", "onset_at");
|
||
|
||
-- CreateIndex
|
||
CREATE INDEX IF NOT EXISTS "illness_episodes_parent_condition_id_idx" ON "illness_episodes"("parent_condition_id");
|
||
|
||
-- CreateIndex
|
||
CREATE UNIQUE INDEX IF NOT EXISTS "illness_day_logs_episode_id_date_key" ON "illness_day_logs"("episode_id", "date");
|
||
|
||
-- CreateIndex
|
||
CREATE INDEX IF NOT EXISTS "illness_day_logs_user_id_date_idx" ON "illness_day_logs"("user_id", "date");
|
||
|
||
-- CreateIndex
|
||
CREATE INDEX IF NOT EXISTS "illness_day_logs_episode_id_idx" ON "illness_day_logs"("episode_id");
|
||
|
||
-- CreateIndex
|
||
CREATE UNIQUE INDEX IF NOT EXISTS "illness_symptoms_key_key" ON "illness_symptoms"("key");
|
||
|
||
-- CreateIndex
|
||
CREATE INDEX IF NOT EXISTS "illness_symptoms_sort_order_idx" ON "illness_symptoms"("sort_order");
|
||
|
||
-- CreateIndex
|
||
CREATE INDEX IF NOT EXISTS "illness_symptom_links_symptom_id_idx" ON "illness_symptom_links"("symptom_id");
|
||
|
||
-- AddForeignKey
|
||
DO $$ BEGIN
|
||
ALTER TABLE "illness_episodes" ADD CONSTRAINT "illness_episodes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||
EXCEPTION
|
||
WHEN duplicate_object THEN null;
|
||
END $$;
|
||
|
||
-- AddForeignKey
|
||
DO $$ BEGIN
|
||
ALTER TABLE "illness_episodes" ADD CONSTRAINT "illness_episodes_parent_condition_id_fkey" FOREIGN KEY ("parent_condition_id") REFERENCES "illness_episodes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||
EXCEPTION
|
||
WHEN duplicate_object THEN null;
|
||
END $$;
|
||
|
||
-- AddForeignKey
|
||
DO $$ BEGIN
|
||
ALTER TABLE "illness_day_logs" ADD CONSTRAINT "illness_day_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||
EXCEPTION
|
||
WHEN duplicate_object THEN null;
|
||
END $$;
|
||
|
||
-- AddForeignKey
|
||
DO $$ BEGIN
|
||
ALTER TABLE "illness_day_logs" ADD CONSTRAINT "illness_day_logs_episode_id_fkey" FOREIGN KEY ("episode_id") REFERENCES "illness_episodes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||
EXCEPTION
|
||
WHEN duplicate_object THEN null;
|
||
END $$;
|
||
|
||
-- AddForeignKey
|
||
DO $$ BEGIN
|
||
ALTER TABLE "illness_symptom_links" ADD CONSTRAINT "illness_symptom_links_day_log_id_fkey" FOREIGN KEY ("day_log_id") REFERENCES "illness_day_logs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||
EXCEPTION
|
||
WHEN duplicate_object THEN null;
|
||
END $$;
|
||
|
||
-- AddForeignKey
|
||
DO $$ BEGIN
|
||
ALTER TABLE "illness_symptom_links" ADD CONSTRAINT "illness_symptom_links_symptom_id_fkey" FOREIGN KEY ("symptom_id") REFERENCES "illness_symptoms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||
EXCEPTION
|
||
WHEN duplicate_object THEN null;
|
||
END $$;
|
||
|
||
-- ─── Seed the default illness-symptom catalog ────────────────────────
|
||
-- Jackson/WURSS-derived high-signal cold/illness symptoms, scored 0–3 on
|
||
-- the link. Idempotent: deterministic ids + ON CONFLICT ("key") DO NOTHING
|
||
-- (the cycle_symptoms seed precedent 0129). Keep in sync with the app-side
|
||
-- illness symptom taxonomy if either changes.
|
||
INSERT INTO "illness_symptoms" ("id", "key", "label_key", "icon", "sort_order", "is_active") VALUES
|
||
('is_runny_nose', 'runny_nose', 'illness.symptom.runnyNose', 'Droplets', 0, true),
|
||
('is_stuffy_nose', 'stuffy_nose', 'illness.symptom.stuffyNose', 'Wind', 1, true),
|
||
('is_sneezing', 'sneezing', 'illness.symptom.sneezing', 'Waves', 2, true),
|
||
('is_sore_throat', 'sore_throat', 'illness.symptom.soreThroat', 'Flame', 3, true),
|
||
('is_cough', 'cough', 'illness.symptom.cough', 'Megaphone', 4, true),
|
||
('is_headache', 'headache', 'illness.symptom.headache', 'Brain', 5, true),
|
||
('is_body_aches', 'body_aches', 'illness.symptom.bodyAches', 'PersonStanding', 6, true),
|
||
('is_fatigue', 'fatigue', 'illness.symptom.fatigue', 'BatteryLow', 7, true)
|
||
ON CONFLICT ("key") DO NOTHING;
|