mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
placeholderData keeps the previous range painted while a health-chart range switch fetches, paired with its truth condition: the container dims and goes inert while isPlaceholderData is true, so the old range cannot masquerade as the new one. The mood chart needs no pairing — its range is a client-side slice over one static-key query and never drops to a skeleton. /checkups and /mood gain the dashboard-template RSC prefetch: the primary list read moves into a shared lib read (route + wrapper consume one query, so filter/order/wire shape cannot drift), the wrapper honours the DASHBOARD_SSR_PREFETCH kill-switch, resolves identity through getUnswitchedSession, JSON-round-trips the dehydrated state, and fails soft to the bare client on every error path. Page-prefetch unit tests pin the seeded keys and the fail-soft arms; the SSR hydration spec gains an assertion per page. The achievement-unlock poll drops to five minutes (in-app writes already invalidate the cell; the poll only covers out-of-band ingest), and the plan-proposal poll gains the advisor-style bounded attempt ceiling so an open thread stops polling after its budget.
113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
/**
|
|
* The one mood-entry list read, shared by `GET /api/mood-entries` and the
|
|
* `/mood` RSC prefetch wrapper so the two cannot drift in filter, order, or
|
|
* wire shape — the SSR-seeded cell must be byte-compatible with what the
|
|
* client `queryFn` gets from the wire. Ciphertext never leaves this module:
|
|
* the note is decrypted onto `note` and the context rides `contextForWire`.
|
|
*/
|
|
import { prisma } from "@/lib/db";
|
|
import { shapeMoodNote } from "@/lib/crypto/note-cipher";
|
|
import { shapeLevelA } from "@/lib/mood/level-a";
|
|
import { contextForWire } from "@/lib/mood/context";
|
|
|
|
export interface MoodEntryListParams {
|
|
mood?: string;
|
|
source?: string;
|
|
from?: string;
|
|
to?: string;
|
|
limit: number;
|
|
offset: number;
|
|
sortBy: string;
|
|
sortDir: "asc" | "desc";
|
|
}
|
|
|
|
function parseTags(tags: string | null): string[] {
|
|
if (!tags) return [];
|
|
try {
|
|
return JSON.parse(tags) as string[];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function listMoodEntriesPage(
|
|
userId: string,
|
|
params: MoodEntryListParams,
|
|
) {
|
|
const { mood, source, from, to, limit, offset, sortBy, sortDir } = params;
|
|
|
|
const where = {
|
|
userId,
|
|
// v1.7.0 sync — hide soft-deleted (tombstoned) rows from the list.
|
|
deletedAt: null,
|
|
...(mood && { mood }),
|
|
// v1.15.13 — management-list source filter.
|
|
...(source && { source }),
|
|
...(from || to
|
|
? {
|
|
date: {
|
|
...(from && { gte: from }),
|
|
...(to && { lte: to }),
|
|
},
|
|
}
|
|
: {}),
|
|
};
|
|
|
|
const [entries, total] = await Promise.all([
|
|
prisma.moodEntry.findMany({
|
|
where,
|
|
orderBy: { [sortBy]: sortDir },
|
|
take: limit,
|
|
skip: offset,
|
|
// v1.8.5 — include the structured-tag link keys so the edit form
|
|
// can pre-populate the taxonomy picker.
|
|
// v1.12.0 — also carry the per-link `rating` + the tag `kind` so
|
|
// the client can split binary tags from rated factors on hydrate.
|
|
include: {
|
|
tagLinks: {
|
|
select: {
|
|
rating: true,
|
|
moodTag: { select: { key: true, kind: true } },
|
|
},
|
|
},
|
|
// v1.38 — the day context, so the edit dialog can pre-populate the
|
|
// sections without a second round trip per row. Sparse by design: most
|
|
// rows carry none and the join costs nothing on those.
|
|
context: true,
|
|
},
|
|
}),
|
|
prisma.moodEntry.count({ where }),
|
|
]);
|
|
|
|
const entriesWithParsedTags = entries.map(({ tagLinks, context, ...e }) => ({
|
|
// v1.23 — decrypt `noteEncrypted` onto `note`, strip the ciphertext.
|
|
// v1.37 — the five level-A values under the keys the create path takes,
|
|
// so the edit form reads back what it wrote instead of mapping column
|
|
// names to wire names by hand. The column spelling is stripped, not
|
|
// joined: one number under two names is a choice nobody made.
|
|
...shapeLevelA(shapeMoodNote(e)),
|
|
tags: parseTags(e.tags),
|
|
// v1.8.5 — flat list of binary structured-tag keys attached to the
|
|
// entry (rated factors are surfaced separately below).
|
|
tagKeys: tagLinks
|
|
.filter((link) => link.moodTag.kind !== "RATED")
|
|
.map((link) => link.moodTag.key),
|
|
// v1.12.0 — rated factors with their per-entry score, so the edit
|
|
// form re-renders the sliders without a refetch.
|
|
ratedFactors: tagLinks
|
|
.filter((link) => link.moodTag.kind === "RATED" && link.rating !== null)
|
|
.map((link) => ({
|
|
key: link.moodTag.key,
|
|
rating: link.rating as number,
|
|
})),
|
|
// v1.38 — the day context, or null. The ciphertext never leaves the
|
|
// server: `contextForWire` reads the note and drops the column.
|
|
context: context ? contextForWire(context) : null,
|
|
}));
|
|
|
|
return {
|
|
entries: entriesWithParsedTags,
|
|
meta: { total, limit, offset },
|
|
};
|
|
}
|