From 7b1a44e799c24f9063faa2b674d31b8be0b02aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 8 Aug 2026 08:10:15 +0200 Subject: [PATCH] chore(mood): account for MoodContext in the four whole-schema guards - the wipe plan names it before its parent, so the confirmation screen's count is the number of rows that existed rather than zero - the backup plan classifies it as carried and two-ended, and the round-trip registry seeds one, which is what makes that claim provable - the sharing surface admits the linked-day read with the reason it may answer - CHANGELOG for the release --- CHANGELOG.md | 34 +++++++++++++++++++++ src/__tests__/sharing-surface-guard.test.ts | 7 ++++- src/app/api/mood-entries/bulk/route.ts | 7 +---- src/lib/data-wipe/wipe-plan.ts | 4 +++ src/lib/export/backup-plan.ts | 2 ++ tests/integration/backup-round-trip.test.ts | 9 ++++++ 6 files changed, 56 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f81deb5d..953226978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ ## [Unreleased] +### Added + +- A mood entry can now carry the shape of the day around it. Four sections sit + below the sliders — work, contacts, leisure, and one notable event — and + every one of them is closed until you open it, so a quick check-in is still + a face and Save. What you fill in is counted on the closed section's header, + so nothing you wrote is out of sight without a hint that it is there. The + work section asks how the day went and how long it ran, the contacts section + asks who and how it felt rather than how many people, the leisure section + asks what the free time went into, and the event section holds one thing + that happened with a note beside it. That note is encrypted at rest like + every other free-text field. + +- Sleep, activity and vitals are not asked for a second time. They are read + from the modules that already hold them and shown read-only beside the + entry, with the source of each figure named and a way in to correct it + there. Correct a sleep session in the sleep module and the mood entry shows + the corrected figure the next time you open it, because nothing was copied. + A figure nobody recorded reads as not recorded rather than as zero, and a + module you have switched off leaves its block out entirely instead of + showing empty rows. Body symptoms stay in the illness journal, linked rather + than duplicated. + +- Where the app compares the day's context to the day's mood, it names how + many days each side of the comparison rests on, right beside the sentence, + and it describes a connection rather than a cause. The comparison runs the + same statistics the tag board already uses, including the correction that + keeps a wide sweep from turning one-in-twenty noise into a finding. + +- The iOS contract now carries both the five mood values and the day context. + Everything is optional and nothing an existing build sends has changed + meaning: an app that posts only a five-point label still writes a complete + entry, because the server derives the pleasantness value from that label. + ## [1.37.1] — 2026-08-08 ### Added diff --git a/src/__tests__/sharing-surface-guard.test.ts b/src/__tests__/sharing-surface-guard.test.ts index f51839ab9..b8859eadc 100644 --- a/src/__tests__/sharing-surface-guard.test.ts +++ b/src/__tests__/sharing-surface-guard.test.ts @@ -887,6 +887,10 @@ const DELEGABLE_ROUTES: Record = { domain: "mind", why: "The record's mood entries. The nested include is the tag vocabulary — a key and a kind, no identifiers. Note that `GET /api/mood/tags` is refused and stays refused: it pulls the owner's tag LAYOUT, which is a presentation preference and belongs to the person, not the record.", }, + "app/api/mood/linked-context/route.ts": { + domain: "mind", + why: "What the sleep, activity, vitals and illness modules already hold for one of the record's days, read-only and resolved per request. Every figure it returns is a figure a delegate can already read on the record's own surfaces; nothing here is a preference of the person, and nothing is written.", + }, "app/api/mood-entries/[id]/route.ts": { domain: "mind", why: "One mood entry of the record, fetch-then-guard against the resolved user.", @@ -1808,8 +1812,9 @@ const ACTOR_ROUTES: Record = { * managed-record configuration adds one more: 196 → 197. The bounded * read-only profile summary makes the next explicit admission: 197 → 198. * The two initial Cycle reads follow as separate, scoped admissions: 198 → 200. + * The mood day's linked figures, read-only, add one: 200 → 201. */ -const FROZEN_ENTRY_COUNT = 200; +const FROZEN_ENTRY_COUNT = 201; /** * The two surfaces that authenticate a Bearer token outside `requireAuth` — diff --git a/src/app/api/mood-entries/bulk/route.ts b/src/app/api/mood-entries/bulk/route.ts index 86e56ebca..bbc16cd8d 100644 --- a/src/app/api/mood-entries/bulk/route.ts +++ b/src/app/api/mood-entries/bulk/route.ts @@ -447,12 +447,7 @@ async function postBulk(request: NextRequest): Promise { // client was already being told. // v1.38 — the day context, inside the same transaction as the row, so // an entry reported as skipped really did write nothing. - await persistMoodContext( - tx, - upserted.id, - user.id, - parsedContext.data, - ); + await persistMoodContext(tx, upserted.id, user.id, parsedContext.data); if ( (entry.tagKeys && entry.tagKeys.length > 0) || diff --git a/src/lib/data-wipe/wipe-plan.ts b/src/lib/data-wipe/wipe-plan.ts index 5b4778f77..0d8441dac 100644 --- a/src/lib/data-wipe/wipe-plan.ts +++ b/src/lib/data-wipe/wipe-plan.ts @@ -71,6 +71,10 @@ export const WIPE_MODELS = [ "MedicationComplianceRollup", // ── Mood ──────────────────────────────────────────────────────────────── + // The day context, before the entry it hangs off: it cascades either way, + // but a parent deleted first leaves the child's own count reporting zero for + // rows that existed, and the confirmation screen quotes those counts. + "MoodContext", "MoodEntry", "MoodEntryRollup", "MoodReminderDispatch", diff --git a/src/lib/export/backup-plan.ts b/src/lib/export/backup-plan.ts index 3a58e8206..84bc08a66 100644 --- a/src/lib/export/backup-plan.ts +++ b/src/lib/export/backup-plan.ts @@ -80,6 +80,7 @@ export const BACKED_UP_MODELS = [ // ── Mood ────────────────────────────────────────────────────────────────── "MoodEntry", + "MoodContext", "MoodEntryTagLink", "MoodTag", "MoodTagCategory", @@ -212,6 +213,7 @@ export const TWO_ENDED_MODELS = [ "MedicationIntakeEvent", "MedicationSideEffect", "MoodEntry", + "MoodContext", "MoodEntryTagLink", "MoodTag", "LabResult", diff --git a/tests/integration/backup-round-trip.test.ts b/tests/integration/backup-round-trip.test.ts index 9a290d81b..6eabbb543 100644 --- a/tests/integration/backup-round-trip.test.ts +++ b/tests/integration/backup-round-trip.test.ts @@ -106,6 +106,7 @@ const COUNT_BACK: Record< MedicationSideEffect: (p, userId) => p.medicationSideEffect.count({ where: { userId } }), MoodEntry: (p, userId) => p.moodEntry.count({ where: { userId } }), + MoodContext: (p, userId) => p.moodContext.count({ where: { userId } }), MoodEntryTagLink: (p, userId) => p.moodEntryTagLink.count({ where: { moodEntry: { userId } } }), MoodTag: (p, userId) => p.moodTag.count({ where: { userId } }), @@ -233,6 +234,14 @@ async function seedEveryTwoEndedModel(prisma: PrismaClient): Promise { source: "MOODLOG", moodLoggedAt: AT("2026-07-01T20:00:00.000Z"), tagLinks: { create: { moodTagId: moodTag.id, rating: 4 } }, + context: { + create: { + userId: OWNER_ID, + workStatus: "regular", + contactCircles: JSON.stringify(["family"]), + leisureJoy: 6, + }, + }, }, });