fix: close four small gaps around the record surface

The privacy dashboard re-implemented the audit-retention resolver with its own
default and its own floor. Two implementations of one disclosure, quoted to the
same person on two pages and by the purge job; it uses the shared resolver now.

The record-content branch in the settings gate matched a classification rather
than a page, so a second manage-writable destination would have rendered the
allergy and family-history managers under its own heading without anybody
choosing that. It resolves through a slug map, and the contract test fails if
the map and the classification disagree in either direction — a destination
cannot join the kind without somebody deciding what it renders.

The managed-profile nav filter assumed a guardian grant is always manage. It is
today, which is why the check was easy to leave out; an entry below that level
would have listed destinations the gate refuses.

Interpolation used a replacement string, and String.replace reads $&, $`, $',
$1 and $$ out of one. A display name containing $' rewrote the sentence around
it — and display names land in exactly the sentences that say who did what in
somebody's health record. A replacer function has no $ grammar at all, which
removes the class rather than escaping the members of it somebody thought of.
This commit is contained in:
Marc-André Bombeck committed 2026-08-07 01:00:02 +02:00
1 parent 5de488c35c
commit 6ae0cc993f
9 files changed
+272 -14

No files matched your search

+14 -6
View File
@@ -8,17 +8,25 @@
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiSuccess } from "@/lib/api-response";
import { getAuditLogRetentionDays } from "@/lib/jobs/audit-log-cleanup";
import { annotate } from "@/lib/logging/context";
import { ENCRYPTED_COLUMNS } from "@/lib/crypto/encrypted-columns";
export const dynamic = "force-dynamic";
/**
* Retention windows. These mirror the defaults + env names the cleanup jobs
* read (`coach-message-cleanup.ts`, `audit-log-cleanup.ts`, the reminder
* cleanup handlers); read here directly so the dashboard never imports a
* pg-boss job module. Off-host backup retention is operator infrastructure
* (not an app setting), so it is disclosed as prose, not a number.
* Retention windows, as the privacy dashboard states them.
*
* The audit window comes from `getAuditLogRetentionDays()` rather than from a
* second parse of the same variable. It used to be re-implemented here with
* its own default and its own floor, which is two implementations of one
* disclosure — and this surface, the sharing panel and the purge job all quote
* the number to the same person. A drift between them is a page telling
* somebody their data is kept for a length of time it is not.
*
* The rest still parse locally so the dashboard never imports a pg-boss job
* module. Off-host backup retention is operator infrastructure (not an app
* setting), so it is disclosed as prose, not a number.
*/
function resolveRetention() {
const intEnv = (name: string, fallback: number, min: number): number => {
@@ -28,7 +36,7 @@ function resolveRetention() {
};
return {
coachMessagesDays: intEnv("COACH_MESSAGE_RETENTION_DAYS", 365, 30),
auditLogDays: intEnv("AUDIT_LOG_RETENTION_DAYS", 365, 7),
auditLogDays: getAuditLogRetentionDays(),
// The push-attempt + mood-dispatch ledgers are a fixed 90-day window.
deliveryLogDays: 90,
};