fix(activity): word the refusals that now reach a delegate's feed

The custom-metric create and update refusals and the medication layout
refusal are filed through auditLog now, so a delegate's refused request
reaches the owner's activity feed, where it read as the generic change.
Each gets its own sentence in every bundle; the refusal sentences live in
their own function. The audit write guard states its floor, and the
schedule-reader guard names the medication create and edit, which answer
through medicationDetail.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 17:19:21 +02:00
1 parent fb9cd9b965
commit a0dce10c1a
14 files changed
+106 -54

No files matched your search

@@ -39,6 +39,8 @@ describe("audit rows go through auditLog()", () => {
.map(([rel]) => rel);
it("finds the writers it polices", () => {
expect(files.length).toBeGreaterThan(3000);
expect(writers.length).toBeGreaterThanOrEqual(2);
expect(writers).toContain("lib/auth/audit.ts");
});
+3 -13
View File
@@ -1,16 +1,11 @@
{
"$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 101,
"total": 116897,
"count": 91,
"total": 101342,
"entries": {
"app/medications/page-client.tsx": 839,
"app/page-client.tsx": 2324,
"components/admin/backups-section.tsx": 1462,
"components/charts/health-chart.tsx": 2517,
"components/charts/mood-chart.tsx": 1166,
"components/cycle/log-day-sheet.tsx": 1639,
"components/documents/document-detail-sheet.tsx": 1304,
"components/documents/documents-view.tsx": 1223,
"components/insights/coach-panel/chat-bubble.tsx": 888,
"components/insights/coach-panel/coach-conversation.tsx": 1286,
"components/insights/coach-panel/use-coach.ts": 1486,
@@ -18,17 +13,12 @@
"components/insights/health-score-card.tsx": 1023,
"components/insights/insights-tab-strip.tsx": 1086,
"components/layout/sidebar-nav.tsx": 855,
"components/measurement-reminders/vorsorge-section.tsx": 1536,
"components/measurements/measurement-form.tsx": 815,
"components/measurements/measurement-list.tsx": 2019,
"components/medications/detail/medication-detail-tabs.tsx": 1160,
"components/medications/dose-history-ledger.tsx": 1052,
"components/medications/medication-table.tsx": 853,
"components/medications/wizard/wizard-payload.ts": 1414,
"components/mood/mood-list.tsx": 1408,
"components/records/life-circumstances-manager.tsx": 1080,
"components/settings/dashboard-layout-section.tsx": 1033,
"components/settings/managed-record-settings-section.tsx": 1071,
"components/timeline/timeline-chart.tsx": 1174,
"components/timeline/timeline-geometry.ts": 1395,
"lib/ai/anthropic-client.ts": 804,
@@ -92,7 +82,7 @@
"lib/measurements/import-apple-health-export.ts": 1722,
"lib/medications/scheduling/cadence.ts": 978,
"lib/medications/scheduling/recurrence.ts": 1108,
"lib/medications/scheduling/slot-upsert.ts": 926,
"lib/medications/scheduling/slot-upsert.ts": 912,
"lib/notifications/senders/apns.ts": 1100,
"lib/oura/sync.ts": 900,
"lib/rollups/measurement-rollups.ts": 1496,
@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 355,
"total": 114858,
"total": 114773,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 314,
@@ -15,16 +15,16 @@
"app/api/cycle/calendar/route.ts :: apiHandler(arg)": 230,
"app/api/cycle/day-logs/bulk/route.ts :: postBulk": 213,
"app/api/cycle/insights/route.ts :: apiHandler(arg)": 226,
"app/api/devices/route.ts :: apiHandler(arg)": 231,
"app/api/devices/route.ts :: apiHandler(arg)": 226,
"app/api/documents/inbound/bulk/route.ts :: apiHandler(arg)": 207,
"app/api/encounters/[id]/route.ts :: apiHandler(arg) #2": 269,
"app/api/environment/route.ts :: apiHandler(arg)": 201,
"app/api/export/health-record/route.ts :: apiHandler(arg)": 241,
"app/api/ingest/medication/route.ts :: apiHandler(arg)": 267,
"app/api/ingest/medication/route.ts :: apiHandler(arg)": 262,
"app/api/insights/cards/route.ts :: apiHandler(arg)": 211,
"app/api/integrations/status/route.ts :: apiHandler(arg)": 451,
"app/api/labs/[id]/route.ts :: apiHandler(arg) #2": 239,
"app/api/labs/ocr/commit/route.ts :: commitOcrRows": 285,
"app/api/labs/ocr/commit/route.ts :: commitOcrRows": 280,
"app/api/labs/route.ts :: postLabResult": 242,
"app/api/mcp/oauth/token/route.ts :: POST": 250,
"app/api/mcp/oauth/token/route.ts :: POST > withBackgroundEvent(arg)": 248,
@@ -346,7 +346,7 @@
"lib/notifications/senders/web-push.ts :: sendViaWebPush": 268,
"lib/personal-records/pr-detection-worker.ts :: detectPersonalRecordsForUser": 239,
"lib/personal-records/pr-direction.ts :: getPRDirection": 202,
"lib/record-activity/activity-verb.ts :: recordActivityVerbLine": 395,
"lib/record-activity/activity-verb.ts :: recordActivityVerbLine": 325,
"lib/strava/sync.ts :: runStravaSync": 219,
"lib/support/report.ts :: collectSupportReportFacts": 332,
"lib/targets/build-response.ts :: buildTargetsResponse": 330,
@@ -115,10 +115,17 @@ const MAY_READ_EVERY_ROW: Record<string, string> = {
"units consumed by a recorded dose",
"lib/medications/intake-slot-dedup.ts":
"folds duplicate rows of doses already recorded",
// The medication create and edit write the schedule rows and answer
// through `medicationDetail` (read-medication.ts), which shapes them with
// scheduleWireFields and derives the due state the switch allows.
"lib/medications/create-medication.ts":
"writes the schedules; answers through medicationDetail",
"lib/medications/update-medication/update-medication.ts":
"writes the schedules; answers through medicationDetail",
// The medication edit: the schedule count and the primary schedule's id
// for the reminder-window bridge, and the replaced rows archived as an era
// whose payload is empty for a stretch with tracking off. Nothing due comes
// out of either; the edit answers through scheduleWireFields.
// out of either; the edit answers through medicationDetail.
"lib/medications/update-medication/preflight.ts":
"schedule count and the primary schedule's id, derives nothing due",
"lib/medications/update-medication/write.ts":
+61 -35
View File
@@ -61,6 +61,8 @@ export function recordActivityVerbLine(
action: string,
name: string,
): string {
const refused = refusedAttemptVerb(t, action, name);
if (refused !== null) return refused;
switch (action) {
case "measurement.create":
return t("recordSharing.activityVerb.measurementCreate", { name });
@@ -342,6 +344,54 @@ export function recordActivityVerbLine(
case "health-record.export":
return t("recordSharing.activityVerb.healthRecordExport", { name });
/* ---------------------------------------------------------------- */
/* Acts a delegable module performs outside its own route file. */
/* */
/* The two below are filed by library modules the routes import — a */
/* background worker's completion row and a lifecycle reconciliation */
/* — and a guard that walked only `route.ts` could not see either. */
/* Both carry the actor through, so both reach the owner's feed. */
/* ---------------------------------------------------------------- */
case "medication.intake.import":
return t("recordSharing.activityVerb.medicationIntakeImportFinished", {
name,
});
case "medication.oneShot.reconciled":
return t("recordSharing.activityVerb.medicationOneShotReconciled", {
name,
});
case "anamnesis.fact.create":
return t("recordSharing.activityVerb.anamnesisFactCreate", { name });
case "customMetric.create":
return t("recordSharing.activityVerb.customMetricCreate", { name });
case "customMetric.update":
return t("recordSharing.activityVerb.customMetricUpdate", { name });
case "customMetric.delete":
return t("recordSharing.activityVerb.customMetricDelete", { name });
case "documents.inbound.store":
return t("recordSharing.activityVerb.documentStore", { name });
case "documents.inbound.update":
return t("recordSharing.activityVerb.documentUpdate", { name });
case "documents.inbound.discard":
return t("recordSharing.activityVerb.documentDiscard", { name });
default:
return t("recordSharing.activity.acted", { name });
}
}
/**
* The sentence for an attempt that was refused, or null when `action` is no
* refusal. Asked first by `recordActivityVerbLine`.
*/
function refusedAttemptVerb(
t: Translator,
action: string,
name: string,
): string | null {
switch (action) {
/* ---------------------------------------------------------------- */
/* Attempts that were REFUSED, and things that happened around them. */
/* */
@@ -407,47 +457,23 @@ export function recordActivityVerbLine(
return t("recordSharing.activityVerb.rejectedDoseImport", { name });
case "custom-metric.entry.create.validation-failed":
return t("recordSharing.activityVerb.rejectedTrackedValue", { name });
case "custom-metric.metric.create.validation-failed":
return t("recordSharing.activityVerb.rejectedTrackedValueTypeCreate", {
name,
});
case "custom-metric.metric.update.validation-failed":
return t("recordSharing.activityVerb.rejectedTrackedValueTypeUpdate", {
name,
});
case "medication.layout.validation-failed":
return t("recordSharing.activityVerb.rejectedMedicationLayout", { name });
// Not a validation failure: the request was well-formed and admitted,
// and the queue would not take it. The owner sees an import that never
// ran rather than one that was refused, because those need different
// next steps.
case "medication.intake.import.kickoff.denied":
return t("recordSharing.activityVerb.deniedDoseImport", { name });
/* ---------------------------------------------------------------- */
/* Acts a delegable module performs outside its own route file. */
/* */
/* The two below are filed by library modules the routes import — a */
/* background worker's completion row and a lifecycle reconciliation */
/* — and a guard that walked only `route.ts` could not see either. */
/* Both carry the actor through, so both reach the owner's feed. */
/* ---------------------------------------------------------------- */
case "medication.intake.import":
return t("recordSharing.activityVerb.medicationIntakeImportFinished", {
name,
});
case "medication.oneShot.reconciled":
return t("recordSharing.activityVerb.medicationOneShotReconciled", {
name,
});
case "anamnesis.fact.create":
return t("recordSharing.activityVerb.anamnesisFactCreate", { name });
case "customMetric.create":
return t("recordSharing.activityVerb.customMetricCreate", { name });
case "customMetric.update":
return t("recordSharing.activityVerb.customMetricUpdate", { name });
case "customMetric.delete":
return t("recordSharing.activityVerb.customMetricDelete", { name });
case "documents.inbound.store":
return t("recordSharing.activityVerb.documentStore", { name });
case "documents.inbound.update":
return t("recordSharing.activityVerb.documentUpdate", { name });
case "documents.inbound.discard":
return t("recordSharing.activityVerb.documentDiscard", { name });
default:
return t("recordSharing.activity.acted", { name });
return null;
}
}