Skip to content

Commit 4057524

Browse files
baozhoutaoclaude
andcommitted
fix(runtime): mount the in-app notifications REST routes (ADR-0030)
handleNotification (GET /notifications, POST /notifications/read[/all]) and its discovery entry existed, but dispatcher-plugin never registered server.<verb>() mounts for them — so only the cloud hosts' hono catch-all reached the handler; the standalone / `os dev` server 404'd every /api/v1/notifications* request. That was the real reason unread notifications never cleared: the console's direct sys_notification_receipt write is rejected by ADR-0103's engine-owned gate, and this REST fallback — the intended path — was unreachable. Mounting the three routes makes mark-read persist in every deployment. Guarded by the dispatcher-plugin route-registration regression test. Verified end-to-end in the showcase example: POST /notifications/read/all → 200, receipts flip delivered→read server-side, and the bell badge stays cleared across the inbox poll (previously it flipped every row back to unread). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 54b71be commit 4057524

3 files changed

Lines changed: 70 additions & 3 deletions

File tree

.changeset/localize-collab-notifications-and-storage-objects.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
---
22
"@objectstack/plugin-audit": patch
33
"@objectstack/service-storage": patch
4+
"@objectstack/runtime": patch
45
---
56

6-
feat(i18n): localize collaboration notification titles and the storage objects
7+
feat(i18n): localize collaboration notification titles and the storage objects; wire the notifications REST routes
78

8-
Two gaps left the notification surface English-only on localized workspaces
9-
(observed as `sys_file "repro.png" assigned to you` over an all-Chinese UI):
9+
Three gaps behind one report (a `sys_file "repro.png" assigned to you`
10+
notification that was English on an all-Chinese workspace, opened an English
11+
detail page, and never cleared its unread state):
1012

1113
- **plugin-audit** — the assignment (`collab.assignment`) and @mention
1214
(`collab.mention`) bell titles were hardcoded English literals built from the
@@ -28,3 +30,14 @@ Two gaps left the notification surface English-only on localized workspaces
2830
`i18n.loadTranslations` on `kernel:ready`, matching service-messaging.
2931
(`sys_attachment` stays in platform-objects' bundles pending the
3032
storage-domain decomposition.)
33+
34+
- **runtime** — the in-app notifications REST surface (`GET
35+
/api/v1/notifications`, `POST /api/v1/notifications/read`, `POST
36+
/api/v1/notifications/read/all`; ADR-0030) had its `handleNotification`
37+
dispatch branch and discovery entry, but no `server.<verb>()` mount in
38+
`dispatcher-plugin`, so only the cloud hosts' hono catch-all reached it — the
39+
standalone / `os dev` server 404'd every request. That left mark-read with no
40+
working endpoint (the console's direct `sys_notification_receipt` write is
41+
rejected by ADR-0103's engine-owned gate), so unread notifications could never
42+
clear. The three routes are now mounted explicitly, guarded by the
43+
route-registration regression test.

packages/runtime/src/dispatcher-plugin.routes.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,23 @@ describe('createDispatcherPlugin — HTTP route registration', () => {
8989
expect(routes).toContain('POST /api/v1/packages');
9090
});
9191

92+
// Regression: the in-app notifications surface (ADR-0030) — inbox list +
93+
// receipt mark-read — had a `handleNotification` dispatch() branch and a
94+
// discovery entry, but NO server.<verb>() registration, so every
95+
// `/api/v1/notifications*` request 404'd on the standalone / `os dev` server
96+
// (only the cloud hosts' hono catch-all reached it). That left mark-read with
97+
// no working endpoint — the console's direct receipt write is rejected by
98+
// ADR-0103's engine-owned gate — so unread notifications could never clear.
99+
it('mounts /notifications (GET list, POST read, POST read/all) so mark-read reaches dispatch()', async () => {
100+
const { server, routes } = makeFakeServer();
101+
const plugin = createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false });
102+
await plugin.start?.(makeCtx(server));
103+
104+
expect(routes).toContain('GET /api/v1/notifications');
105+
expect(routes).toContain('POST /api/v1/notifications/read');
106+
expect(routes).toContain('POST /api/v1/notifications/read/all');
107+
});
108+
92109
it('also mounts a known existing route (sanity that start() ran)', async () => {
93110
const { server, routes } = makeFakeServer();
94111
const plugin = createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false });

packages/runtime/src/dispatcher-plugin.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,43 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
691691
}
692692
});
693693

694+
// ── In-app notifications (ADR-0030) ─────────────────────────
695+
// The inbox list + receipt mark-read surface backed by the
696+
// messaging service. `handleNotification` + its discovery entry
697+
// already existed, but only the cloud hosts' @objectstack/hono
698+
// catch-all reached it — the standalone / `os dev` server mounts
699+
// ONLY the explicit routes here, so every `/api/v1/notifications*`
700+
// request 404'd (mark-read had no working endpoint: the console's
701+
// direct receipt write is rejected by ADR-0103's engine-owned
702+
// gate, and this REST fallback was unreachable). Mount them
703+
// explicitly so read-state actually persists in every deployment.
704+
server.get(`${prefix}/notifications`, async (req: any, res: any) => {
705+
try {
706+
const result = await dispatcher.dispatch('GET', '/notifications', undefined, req.query, { request: req });
707+
sendResult(result, res);
708+
} catch (err: any) {
709+
errorResponse(err, res);
710+
}
711+
});
712+
713+
server.post(`${prefix}/notifications/read`, async (req: any, res: any) => {
714+
try {
715+
const result = await dispatcher.dispatch('POST', '/notifications/read', req.body, req.query, { request: req });
716+
sendResult(result, res);
717+
} catch (err: any) {
718+
errorResponse(err, res);
719+
}
720+
});
721+
722+
server.post(`${prefix}/notifications/read/all`, async (req: any, res: any) => {
723+
try {
724+
const result = await dispatcher.dispatch('POST', '/notifications/read/all', req.body, req.query, { request: req });
725+
sendResult(result, res);
726+
} catch (err: any) {
727+
errorResponse(err, res);
728+
}
729+
});
730+
694731
// ── Packages ────────────────────────────────────────────────
695732
// Single pipeline (ADR-0076 D11 / OQ#9): every package route flows
696733
// through dispatch() — like analytics / i18n / automation / AI —

0 commit comments

Comments
 (0)