Found during the #3358 v16.0 verification sweep. This is the §7 ⚠️ item "Notifications actually clear" — and it does not, on the standard os dev / os serve (hono) server.
Summary
On os dev / os serve (the standalone hono server), marking a notification read has no working endpoint, so the unread badge never clears — the exact regression #3354 set out to fix. Both paths the Console can use fail:
POST /api/v1/notifications/read → 404 Not found
POST /api/v1/notifications/read/all → 404 Not found
GET /api/v1/notifications → 404 Not found
- Data-API fallback
PATCH /api/v1/data/sys_notification_receipt/:id → 405 API operation 'update' is not allowed … allowed: ["get","list"] (ADR-0103 engine-owned)
Repro
objectstack dev --ui --seed-admin on the showcase; sign in as admin.
- Open the message center (bell) — several unread notifications (the
showcase_scheduled_digest flow generates one/min); badge shows 未读 20 / 9+.
- Click a notification or 全部标记为已读 (Mark all read). The badge does not clear.
- Confirm the failing calls:
curl -b <cookies> -X POST http://localhost:<port>/api/v1/notifications/read/all # → 404
curl -b <cookies> -X POST http://localhost:<port>/api/v1/notifications/read # → 404
curl -b <cookies> http://localhost:<port>/api/v1/notifications # → 404
Why it's user-visible
The Console marks read via those dedicated routes — packages/app-shell/src/layout/AppHeader.tsx:483:
await fetch(`${serverUrl}/api/v1/notifications/${subPath}`, { method: 'POST', ... }) // subPath = 'read' | 'read/all'
With the routes 404, the receipt is never upserted; the direct sys_notification_receipt write is separately rejected by the ADR-0103 engine-owned gate (405, get/list only). No path works → unread state is permanent.
Root cause (same class as the Server-Timing gap)
The #3354 fix mounts these routes in the runtime dispatcher plugin — packages/runtime/src/dispatcher-plugin.ts:704-729, whose own comment states the intent:
"the standalone / os dev server mounts ONLY the explicit routes here, so every /api/v1/notifications* request 404'd … Mount them explicitly so read-state actually persists in every deployment."
But on os serve the hono server (@objectstack/plugin-hono-server) owns HTTP and only advertises the prefix in discovery (hono-plugin.ts:688) without mounting the /notifications* handlers; the dispatcher-plugin's server.get/post(...) registrations for /notifications* are not reachable on the hono listener (mirrors F1 / the Server-Timing per-request path, where allowPerfDisclosure() also lives only on the runtime-dispatcher path). Discovery therefore advertises a 404ing route (declared !== enforced).
Note: /api/v1/mcp — registered a few lines up the same block (dispatcher-plugin.ts:660) — is also not reachable as expected (returns 501 from a hono-native handler), consistent with the dispatcher-plugin registrations not landing on the hono app.
Suggested fix
Mount the /notifications GET + read + read/all routes on the hono server too (e.g. in @objectstack/plugin-hono-server, delegating into the same dispatcher, exactly as /data is), or ensure the dispatcher-plugin's explicit route registrations bind to the hono app under os serve. Add an e2e test that boots the hono app, POSTs /notifications/read/all as a user, and asserts the receipt state flips to read (and the unread count drops).
Severity
P2 (borderline P1): a core, always-present notification UX is non-functional on the flagship OSS dev/standalone server; not data-loss or security. Distinct from #3361 (Server-Timing) but same underlying seam.
Responsible: #3354 (didn't take effect on the hono server). Refs release notes content/docs/releases/v16.mdx ("i18n — collaboration notifications … the notifications REST routes are wired (#3354)").
Found during the #3358 v16.0 verification sweep. This is the §7⚠️ item "Notifications actually clear" — and it does not, on the standard
os dev/os serve(hono) server.Summary
On
os dev/os serve(the standalone hono server), marking a notification read has no working endpoint, so the unread badge never clears — the exact regression #3354 set out to fix. Both paths the Console can use fail:POST /api/v1/notifications/read→ 404 Not foundPOST /api/v1/notifications/read/all→ 404 Not foundGET /api/v1/notifications→ 404 Not foundPATCH /api/v1/data/sys_notification_receipt/:id→ 405API operation 'update' is not allowed … allowed: ["get","list"](ADR-0103 engine-owned)Repro
objectstack dev --ui --seed-adminon the showcase; sign in as admin.showcase_scheduled_digestflow generates one/min); badge shows未读 20/9+.Why it's user-visible
The Console marks read via those dedicated routes —
packages/app-shell/src/layout/AppHeader.tsx:483:With the routes 404, the receipt is never upserted; the direct
sys_notification_receiptwrite is separately rejected by the ADR-0103 engine-owned gate (405, get/list only). No path works → unread state is permanent.Root cause (same class as the Server-Timing gap)
The #3354 fix mounts these routes in the runtime dispatcher plugin —
packages/runtime/src/dispatcher-plugin.ts:704-729, whose own comment states the intent:But on
os servethe hono server (@objectstack/plugin-hono-server) owns HTTP and only advertises the prefix in discovery (hono-plugin.ts:688) without mounting the/notifications*handlers; the dispatcher-plugin'sserver.get/post(...)registrations for/notifications*are not reachable on the hono listener (mirrors F1 / the Server-Timing per-request path, whereallowPerfDisclosure()also lives only on the runtime-dispatcher path). Discovery therefore advertises a 404ing route (declared !== enforced).Note:
/api/v1/mcp— registered a few lines up the same block (dispatcher-plugin.ts:660) — is also not reachable as expected (returns 501 from a hono-native handler), consistent with the dispatcher-plugin registrations not landing on the hono app.Suggested fix
Mount the
/notificationsGET +read+read/allroutes on the hono server too (e.g. in@objectstack/plugin-hono-server, delegating into the same dispatcher, exactly as/datais), or ensure the dispatcher-plugin's explicit route registrations bind to the hono app underos serve. Add an e2e test that boots the hono app, POSTs/notifications/read/allas a user, and asserts the receipt state flips to read (and the unread count drops).Severity
P2 (borderline P1): a core, always-present notification UX is non-functional on the flagship OSS dev/standalone server; not data-loss or security. Distinct from #3361 (Server-Timing) but same underlying seam.
Responsible: #3354 (didn't take effect on the hono server). Refs release notes
content/docs/releases/v16.mdx("i18n — collaboration notifications … the notifications REST routes are wired (#3354)").