mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
feat(coach): goal / if-then plan memory + locale-keyed dashboard snapshot
Add a CoachPlan model: durable, user-scoped if-then implementation plans the Coach proposes and the user confirms. The background memory-refresh pass extracts plans the user agreed to and writes them as "proposed"; only the user-facing PATCH on /api/coach/plans/[id] activates one. The free-text cue / action / target are encrypted at rest with the shared Bytes codec; metric, status and dates stay plain so the injection picker ranks without a per-row decrypt. Active plans fold into the Coach snapshot's memory block and survive the prompt-budget drop alongside facts. List, confirm and soft-delete routes are Coach-gated and narrow the owner from the session. Key the dashboard-snapshot query cell by locale so a locale switch reads freshly localised tile copy on its own cell instead of serving the prior locale's prose until the slot goes stale. The zero-arg key stays byte-identical, so every prefix-based invalidation keeps reaching the locale-keyed cells; the offline-banner and preloader match on the prefix. Migration 0194 (additive CREATE TABLE). Encryption-rotation registry and script cover the three new Bytes columns.
This commit is contained in:
1 parent
ee704cacab
commit
dcbac63fdf
20 files changed
+2162
-14
No files matched your search
@@ -0,0 +1,34 @@
|
||||
-- v1.21.3 (B1) — Coach goal / if-then implementation-plan memory.
|
||||
--
|
||||
-- Durable, user-scoped plans the Coach PROPOSES and the user CONFIRMS. The
|
||||
-- extractor writes a row as status 'proposed'; only the user-facing PATCH
|
||||
-- flips it to 'active'. Free-text fields (the if-cue, then-action, optional
|
||||
-- target) are AES-256-GCM ciphertext stored as `bytea` via the shared Bytes
|
||||
-- codec — the same shape as coach_facts.fact_encrypted. metric / status /
|
||||
-- dates / source_conversation_id stay plain so the injection picker can rank
|
||||
-- and filter without a per-row decrypt. status is an app-side closed enum
|
||||
-- (proposed | active | met | abandoned), deliberately NOT a DB enum so this
|
||||
-- migration stays purely additive. Soft-delete via deleted_at.
|
||||
CREATE TABLE "coach_plans" (
|
||||
"id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"metric" TEXT NOT NULL,
|
||||
"if_cue_encrypted" BYTEA NOT NULL,
|
||||
"then_action_encrypted" BYTEA NOT NULL,
|
||||
"target_encrypted" BYTEA,
|
||||
"status" TEXT NOT NULL DEFAULT 'proposed',
|
||||
"review_date" TIMESTAMP(3),
|
||||
"source_conversation_id" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"deleted_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "coach_plans_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- Injection picker scans a user's active plans newest-first; the status +
|
||||
-- deleted_at columns are part of the predicate, so the composite index lets
|
||||
-- the planner serve the read index-backed.
|
||||
CREATE INDEX "coach_plans_user_id_status_deleted_at_idx" ON "coach_plans"("user_id", "status", "deleted_at");
|
||||
|
||||
ALTER TABLE "coach_plans" ADD CONSTRAINT "coach_plans_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -570,6 +570,7 @@ model User {
|
||||
integrationStatuses IntegrationStatus[]
|
||||
coachConversations CoachConversation[]
|
||||
coachFacts CoachFact[]
|
||||
coachPlans CoachPlan[]
|
||||
coachUsage CoachUsage[]
|
||||
// v1.4.25 W8d — workout passthrough (Apple HKWorkout / Withings
|
||||
// getworkouts / future Strava). Schema lands now so the v1.5 iOS
|
||||
@@ -3891,6 +3892,48 @@ model CoachFact {
|
||||
@@map("coach_facts")
|
||||
}
|
||||
|
||||
// v1.21.3 (B1) — durable, user-scoped goal / if-then implementation plans the
|
||||
// Coach proposes and the user CONFIRMS. The Coach extractor writes a plan as
|
||||
// status "proposed"; only the user-facing PATCH activates it (status "active")
|
||||
// — there is no silent self-edit of the user's plan. Free-text fields (the
|
||||
// if-cue, the then-action, and the optional target) are encrypted at rest
|
||||
// (AES-256-GCM, the same Bytes codec as CoachFact). `metric`, `status`, the
|
||||
// dates and the source-conversation id stay plain so the injection picker can
|
||||
// rank / filter without paying a per-row decrypt. Soft-delete via `deletedAt`
|
||||
// so an abandoned plan is hidden from injection but kept for audit.
|
||||
//
|
||||
// `status` is an APP-SIDE closed enum (NOT a DB enum, to keep the migration
|
||||
// purely additive): proposed | active | met | abandoned.
|
||||
model CoachPlan {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
// The metric this plan moves (e.g. WEIGHT, SLEEP, BLOOD_PRESSURE). Plain so
|
||||
// the picker can filter / group without a decrypt.
|
||||
metric String
|
||||
// Free-text "if <cue>" and "then <action>" — implementation-intention prose,
|
||||
// encrypted at rest. Never a number / measurement (those live in the data).
|
||||
ifCueEncrypted Bytes @map("if_cue_encrypted")
|
||||
thenActionEncrypted Bytes @map("then_action_encrypted")
|
||||
// Optional free-text target ("70 kg by August", "lights out by 23:00"). Free
|
||||
// prose like the cue/action, so encrypted at rest as well.
|
||||
targetEncrypted Bytes? @map("target_encrypted")
|
||||
// App-side closed enum: proposed | active | met | abandoned.
|
||||
status String @default("proposed")
|
||||
// Optional review checkpoint the Coach can recall ("how did this go by …").
|
||||
reviewDate DateTime? @map("review_date")
|
||||
// Provenance — nullable so a future manual-entry surface can leave it null.
|
||||
sourceConversationId String? @map("source_conversation_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
// Soft-delete: hides the plan from injection but keeps the row for audit.
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId, status, deletedAt])
|
||||
@@map("coach_plans")
|
||||
}
|
||||
|
||||
// v1.15.20 — user-authored "about me" self-description the AI surfaces
|
||||
// (Coach system prompt + daily-briefing user prompt) inject as a clearly
|
||||
// delimited, user-provided context block. 1:1 with User and lazily
|
||||
|
||||
Reference in new issue
Block a user