Skip to content

Commit 13d48b3

Browse files
committed
fix(webapp): report message catalogs ride the registry, not an import side effect
The health catalog registered itself at import time, reachable only through a bare side-effect import — which the production SSR bundle tree-shakes away under "sideEffects": false, so GET /api/v1/reports/health threw 'no catalog registered for report "health"' in production while working in dev. Verified on the built server bundle: main's lacks the catalog, this branch's carries it. Catalogs are now values on the report registry entries and the resolver reads them from there; the mutable register-at-import registry is gone, so the class of bug is structurally impossible.
1 parent d9f4fea commit 13d48b3

5 files changed

Lines changed: 26 additions & 13 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Fix the health report failing with an internal error when requested through the API.

apps/webapp/app/presenters/v3/reports/health/health-messages.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* metrics / evidence — meaning lives here, numbers stay facts.
99
*/
1010

11-
import { registerReportMessages, type ReportMessages } from "../report-messages";
11+
import { type ReportMessages } from "../report-messages";
1212
import { type ReasonCode, type Severity } from "../report-view-model";
1313

1414
/** Metric id -> expanded display label. */
@@ -166,5 +166,3 @@ export const healthMessages: ReportMessages = {
166166
statementMessage,
167167
actionMessage: (code) => ACTIONS[code] ?? code,
168168
};
169-
170-
registerReportMessages("health", healthMessages);

apps/webapp/app/presenters/v3/reports/health/health.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { applyFlowPolicy, buildFlowRead, interpretFlow } from "./flow";
2626
import { interpretLiveness } from "./liveness";
2727
// Registers the "health" message catalog (side effect) so the renderer resolves this report's
2828
// codes. Kept here — the health report's entry module — so loading it always registers its prose.
29-
import "./health-messages";
3029

3130
// Re-exported so the data layer + tests keep a single import path (`./health`).
3231
export { HEALTH_THRESHOLDS, isPendingIncreasing, type HealthInput } from "./health-core";

apps/webapp/app/presenters/v3/reports/report-messages.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* No report vocabulary here — that would re-couple the renderer to a specific report.
66
*/
77

8+
import { REPORT_REGISTRY } from "./report-registry";
89
import { type ReasonCode, type Severity } from "./report-view-model";
910

1011
/**
@@ -22,16 +23,14 @@ export type ReportMessages = {
2223
actionMessage(code: ReasonCode): string;
2324
};
2425

25-
const catalogs = new Map<string, ReportMessages>();
26-
27-
/** Register a report's catalog under its title (e.g. "health"). Called for its side effect. */
28-
export function registerReportMessages(title: string, messages: ReportMessages): void {
29-
catalogs.set(title, messages);
30-
}
31-
32-
/** Look up a report's catalog by `vm.title`. Throws if the report never registered one. */
26+
/**
27+
* Look up a report's catalog by `vm.title`. Catalogs live as values on the
28+
* report registry entries — there is deliberately no register-at-import-time
29+
* step: a side-effect registration is exactly what the production bundle
30+
* tree-shakes away under `"sideEffects": false`.
31+
*/
3332
export function reportMessages(title: string): ReportMessages {
34-
const messages = catalogs.get(title);
33+
const messages = REPORT_REGISTRY[title]?.messages;
3534
if (!messages) {
3635
throw new Error(`report-messages: no catalog registered for report "${title}"`);
3736
}

apps/webapp/app/presenters/v3/reports/report-registry.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,21 @@
1010
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
1111
import { interpret as interpretHealth } from "./health/health";
1212
import { loadHealthInput } from "./health/health-data";
13+
import { healthMessages } from "./health/health-messages";
14+
import { type ReportMessages } from "./report-messages";
1315
import { type ReportViewModel } from "./report-view-model";
1416

1517
export type ReportLoader<TInput> = {
1618
load: (env: AuthenticatedEnvironment, period: string) => Promise<TInput>;
1719
interpret: (input: TInput) => ReportViewModel;
20+
/**
21+
* The report's message catalog, carried BY VALUE on the registry entry. It
22+
* used to be registered as a side effect of importing the catalog module —
23+
* which the production SSR bundle tree-shook away (`"sideEffects": false`),
24+
* leaving `GET /api/v1/reports/health` throwing "no catalog registered".
25+
* A value on the entry cannot be dropped.
26+
*/
27+
messages: ReportMessages;
1828
};
1929

2030
function defineReport<TInput>(loader: ReportLoader<TInput>): ReportLoader<unknown> {
@@ -25,6 +35,7 @@ export const REPORT_REGISTRY: Record<string, ReportLoader<unknown>> = {
2535
health: defineReport({
2636
load: (env, period) => loadHealthInput(env, period),
2737
interpret: interpretHealth,
38+
messages: healthMessages,
2839
}),
2940
};
3041

0 commit comments

Comments
 (0)