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
This commit is contained in:
Marc-André Bombeck committed 2026-08-08 08:10:15 +02:00
1 parent 8f263e4767
commit 7b1a44e799
6 files changed
+56 -7

No files matched your search

+34
View File
@@ -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
+6 -1
View File
@@ -887,6 +887,10 @@ const DELEGABLE_ROUTES: Record<string, DelegableEntry> = {
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<string, string> = {
* 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` —
+1 -6
View File
@@ -447,12 +447,7 @@ async function postBulk(request: NextRequest): Promise<Response> {
// 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) ||
+4
View File
@@ -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",
+2
View File
@@ -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",
@@ -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<void> {
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,
},
},
},
});