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:
Marc-André Bombeck committed 2026-06-27 01:25:35 +02:00
1 parent ee704cacab
commit dcbac63fdf
20 files changed
+2162 -14

No files matched your search

+10
View File
@@ -318,6 +318,16 @@ async function main() {
await rotateBytesColumn("CoachFact", "factEncrypted", prisma.coachFact),
);
// ───── CoachPlan (Bytes columns) ─────
// "ifCueEncrypted" "thenActionEncrypted" "targetEncrypted"
for (const field of [
"ifCueEncrypted",
"thenActionEncrypted",
"targetEncrypted",
]) {
results.push(await rotateBytesColumn("CoachPlan", field, prisma.coachPlan));
}
// ───── UserHealthProfile (Bytes columns) ─────
// "aboutMeEncrypted" "conditionsEncrypted" "allergiesEncrypted"
// "coachFocusEncrypted" "pendingQuestionsEncrypted"