fix(api,lib): consent before reuse, tighter inputs and caches that tell on themselves

Per-status texts check AI consent before the unchanged-data refresh can
re-stamp them, the comprehensive fingerprint folds in the comparison
baseline and generation language so toggling either regenerates once,
and profile, threshold and source-priority writes evict the analytics,
targets and derived caches they feed. CSV export neutralises spreadsheet
formula prefixes on free-text cells while numeric columns stay typed,
the derived batch validates its type token against the measurement enum
and carries a per-user rate limit, and a dose synced from the phone app
counts as the interactive write it is. Background cache refresh
failures log a structured event instead of vanishing, the warm queue
runs visit-triggered jobs alongside the nightly walk, the formatter
memo evicts one entry instead of flushing wholesale with its option
objects frozen, the targets cold build bounds its fan-out, and time in
bed unions every writer's envelope while stages stay with the richest
recorder.
This commit is contained in:
Marc-André Bombeck committed 2026-06-11 23:20:38 +02:00
1 parent 95f21bfa4d
commit f790e14edd
40 files changed
+1141 -287

No files matched your search

+11 -1
View File
@@ -13,13 +13,17 @@
*
* Bearer-auth + cookie-auth both work via the shared `requireAuth()`
* helper. The analytics aggregator reads this row on every call and
* the read path is cheap (one column) — no caching layer to flush.
* the read path is cheap (one column). The PUT flushes the per-user
* profile-derived caches (`invalidateUserProfile`) because the ladder
* decides which source's rows the cached targets / derived / analytics
* payloads were built from.
*/
import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler";
import { apiSuccess, getClientIp } from "@/lib/api-response";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { invalidateUserProfile } from "@/lib/cache/invalidate";
import {
parseSourcePriority,
sourcePrioritySchema,
@@ -77,6 +81,12 @@ export const PUT = apiHandler(async (req: Request) => {
data: { sourcePriorityJson: parsed.data },
});
// The priority ladder drives every aggregator's source pick (the
// analytics rank SQL, the rollup read-swap, the targets grid's sleep
// dedup) — drop the cached payloads so the reorder reflects on the
// next read instead of after the TTL.
invalidateUserProfile(user.id);
await auditLog("user.source-priority.update", {
userId: user.id,
ipAddress: getClientIp(req),