feat(journal): add per-user mood journal module (task 86)

Ship journal entries with mood tracking, insights, and Recharts charts.

Adds v1 API endpoints per ADR 0005 and migration 0020_journal_entries.
This commit is contained in:
ginnoir
2026-07-04 19:41:15 -05:00
parent 8e2ddd6b72
commit 04ae809e07
34 changed files with 1889 additions and 6 deletions
+56
View File
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
computeDayStreak,
entryMoodScore,
pillsCorrelation,
topMoodId,
} from "../../src/modules/journal/server/analytics";
import { toDayKey } from "../../src/modules/journal/day-key";
describe("journal analytics", () => {
it("scores multi-select moods as an average", () => {
const score = entryMoodScore({ moods: ["happy", "stressed"] });
assert.equal(score, 3);
});
it("computes consecutive day streak", () => {
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const streak = computeDayStreak([toDayKey(today), toDayKey(yesterday)]);
assert.equal(streak, 2);
});
it("finds the most common mood id", () => {
const top = topMoodId([
{ moods: ["happy"], recordedAt: "", stress: null, pillsTaken: null, id: "1" },
{ moods: ["happy", "calm"], recordedAt: "", stress: null, pillsTaken: null, id: "2" },
{ moods: ["sad"], recordedAt: "", stress: null, pillsTaken: null, id: "3" },
]);
assert.equal(top, "happy");
});
it("compares mood averages with and without pills", () => {
const result = pillsCorrelation([
{
id: "1",
recordedAt: "2026-01-01T10:00:00.000Z",
moods: ["happy"],
stress: null,
pillsTaken: true,
},
{
id: "2",
recordedAt: "2026-01-02T10:00:00.000Z",
moods: ["sad"],
stress: null,
pillsTaken: false,
},
]);
assert.equal(result.withPillsCount, 1);
assert.equal(result.withoutPillsCount, 1);
assert.equal(result.avgMoodWithPills, 5);
assert.equal(result.avgMoodWithoutPills, 1);
});
});