Notifications Center — one page to read, route, and configure every notification (in-app + email + watchdogs)
Why¶
The notification machinery is real but scattered: the in-app inbox lives at /notifications yet is
linked from NOWHERE (reachable only by typing the URL); who receives system EMAILS is configured in a
Tools-page row (SystemEmailRoutingTool → aote-system/system/emailPolicy); watchdog health lives in
systemHealth/* docs with no UI; and there is no per-user notification preference of any kind —
users cannot choose which events reach them or how. Owner directive (2026-07-07, verbatim): "I'd like a
formal Notifications (Center) page … not only handles in-app notifications, but a centralized place to
handle and configure notifications."
Current state (mapped 2026-07-07)¶
- Inbox page
app/notifications/— full table, 8 topic groups over the 25-valueNotificationTypeunion, filters, mark-read/delete, live Firestore listener (useSharedNotifications, 100-doc cap). - Email routing
components/tools/SystemEmailRoutingTool.tsx+/api/system/email-policy+lib/notifications/emailPolicy.ts— per-CATEGORY (4 auto-email categories) enable + recipient override + greeting; admin-global, NOT per-user. Emails fire only at severity ≥ warning (notify.ts:202). - Producers — 16+
notifyXxxhelpers inlib/notifications/notify.ts; the AUTO paths are unwired (that's the T-037–T-041 cluster, still todo: auto-match, vendor-invoice upload, bank-sync, suggestions; plus dormant Resend webhook + overdue cron). - Watchdogs — reconciliation (NAS-pinged), scrape per-pipeline (Vercel-cron-pinged, T-147), markers
in
systemHealth/*; alerts land asdata_integritynotifications; no status UI. - Gaps — no per-user prefs (type/topic × in-app/email/off), no digest, no delivery log, no bulk actions/search in the inbox, no admin UI for page rules.
Scope (phased)¶
- P0 — SHIPPED with this task's opening commit: "Notifications" entry in the universal sidebar
(
NAVIGATION_RESOURCES+ALLOWED_MENU_KEYS+ AppShell tab order/icon + open page rule + pageKey gate). Visible on every page for every authenticated user; page title already "Notifications". - P1 — Gather the existing surfaces into the page (tabbed):
- Inbox (existing table) ·
- Email Routing — move/embed
SystemEmailRoutingTool(admin-gated tab), retire the Tools row · - System Health — watchdog panel: per-pipeline scrape markers, reconciliation freshness, last/next ping, recent watchdog alerts.
- P2 — Per-user preferences (the new engine):
users/{uid}(or subcollection) preference matrix — per topic/type: in-app / email / both / off (+ future digest). Enforce innotify.tsrecipient resolution (in-app create skip + email filter) with safe defaults (current behavior). Preferences UI as a matrix tab. System-critical alerts (watchdogs → super_admin) exempt from opt-out. - P3 — Delivery visibility: Resend webhook handler (bounce/complaint — the dormant T-041 extra)
- a sent/failed email log tab.
Notes¶
- The T-037–T-041 cluster (wiring producers on AUTO events) is complementary, not overlapping: they make events fire; this page governs who sees/receives them. Do P2 with those in mind.
- Owner may route this to the User Management agent (notification-center brief exists in that agent's onboarding scope); board entry deliberately self-contained for handoff.
Decision log¶
2026-07-07 — opened; P0 shipped¶
- ✅ Read AGENTS.md. Scope-checked the board: T-037–T-041 cover producer wiring only; no task covers a Notifications Center page/preferences — new scope. (README next-free pointer was stale at T-160 while T-160/T-161 were taken; took T-162, pointer → T-163.)
- P0 shipped in the same commit: sidebar entry via the universal nav (5 files:
lib/navigation/resources.ts,components/layout/AppShell.tsx[BellOutlined + tab order],lib/rbac/pageRules.ts[pre-existing opennotifications: {}rule kept — no rule change needed],app/notifications/page.tsx[pageKey gate],components/profile/ProfileApp.tsx[access-preview label]). tsc clean. - Sidebar-universality finding (owner's companion question): the sidebar is ALREADY one universal
object — every one of the 28 AppShell mounts passes the identical
NAVIGATION_RESOURCES+useAllowedMenuKeys()RBAC filter; visibility differences are per-USER rule evaluation (subsidiary/role/badge), not per-page configuration. No refactor warranted; concrete "item vanishes between pages" repros should be chased as rule bugs or the page-rules loading flash. - Source: EOP Local Assistance (fork) · session edb0abc7-6373-449b-8e31-379fd38d391b
2026-07-07 — routed to the User Management agent (P1–P3)¶
- ✅ Read AGENTS.md. The owner delegated the routing call (verbatim): "I'll let you decide to whether pass it on to the 'User Management' agent or if you wanna handle it yourself. I'm fine either way." Decision (EOP Local Assistance (fork)): hand P1–P3 to User Management. Rationale: (a) the notification center is in that agent's onboarding charter; (b) P2's per-user preference engine and the inbox's live Firestore listener overlap that agent's own T-025 (Admin-SDK → client-SDK migration) — single ownership avoids collisions; (c) load-balancing (this agent holds T-147 close-out, T-154, T-047).
- P0 remains shipped (
44dd3a34); this task is handoff-ready as written. User Management: start at the "Current state" + "Scope" sections above; coordinate P1's SystemEmailRoutingTool move with your T-025 file-touch plan. - Source: EOP Local Assistance (fork) · session edb0abc7-6373-449b-8e31-379fd38d391b
2026-07-09 — accepted by User Management; status → doing (planning, pre-build)¶
- ✅ Read AGENTS.md. Scope-checked the board: no existing task covers a Notifications Center page /
per-user preferences — T-037–T-041 wire producers (complementary), T-025 is the client-SDK
migration (overlaps only where noted below). Not a dup. Accepting P1–P3 from EOP Local Assistance
(fork); P0 already shipped (
44dd3a34). - Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
Current-state reconciliation (grounds the T-025 sequencing):
- The inbox listener useSharedNotifications is ALREADY on the client SDK (firebase/firestore
onSnapshot via useSharedFirestoreQuery, gated by useFirebaseAuth). So P1's Inbox tab reuses
an already-migrated read path — no self-collision with T-025 on reads.
- The genuine T-025 interaction is P2's preference write path. Decision: route user pref edits
through a self-scoped API route (mirroring the T-161 P3 /api/profile/name-preferences
pattern — resolves the caller's own record, no id param), not a client-SDK write. This stays
forward-compatible with T-025 without racing its not-yet-written client-SDK write rules.
notify.ts enforcement stays server-side (Admin SDK) — the privileged/derived layer T-025
explicitly reserves, so it's unaffected by the migration.
Planned sequence (build starts on the owner's go):
- P1 — tabbed page: Inbox (existing table) · Email Routing (move SystemEmailRoutingTool in,
admin-gated; retire the Tools row) · System Health (read systemHealth/* markers). Lowest risk;
I own SystemEmailRoutingTool from T-161 P4.
- P2 — per-user preference matrix (type/topic × in-app / email / both / off), enforced in
notify.ts recipient resolution with current behaviour as the default and watchdog→owner
alerts exempt from opt-out. Matrix shape designed so the T-037–T-041 producer types slot
in when wired. Granularity (per-type vs per-topic) + digest deferral → owner sign-off at P2
planning, quoted per AGENTS.md.
- P3 — Resend bounce/complaint webhook + delivery log tab.
House rules acknowledged: never persist what can be derived (the bell count already derives from the page query — preferences store only genuine user choices, not computed reach); no versioned structure labels; owner approvals quoted in this log.
Status = doing (owned + planning), pre-build: awaiting the owner's go to start P1, and a call on whether to deploy the stacked T-160/T-161 work first. No code yet.
2026-07-09 — UI-preview gate added (owner instruction); preview v1 published¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Owner instruction (verbatim, 2026-07-09): "Before you enter, I'd like you to make sure that there's a step where you'd integrate the UI. And we get to that point, please provide me a re-design preview so that I know how it can look like, and have things tweaked before really having the newly created features deployed."
- Gate recorded: every UI-integrating phase of this task ships preview → owner tweaks → approval → build (commit-only, PR flow) → deploy only on the owner's word. Same rhythm as the T-048 subsidiary-tab / contact-drawer redesigns.
- Preview v1 published (label
notifications-center-v1): https://claude.ai/code/artifact/22f87672-7b76-4ed2-afd6-76657bd579be — the full page mock on the app's design language: sidebar with the P0 Notifications entry + unread badge; tabs Inbox (real topic taxonomy, severity, filters, mark-read/delete) · Preferences (P2 matrix — all 25 types under the 8 topics, per-row In-app/Email/Both/Off, defaults = today's behaviour,data_integritywatchdog-exempt lock) · Email Routing (the Tools tool relocated, incl. the T-161 greeting selects) · System Health (4 real watchdog pipelines with thresholds/remedies + recent activity). Admin/Member toggle demonstrates tab gating; light/dark. - Awaiting owner feedback on v1 before any build.
2026-07-09 — owner feedback round 1 → preview v2 published¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Owner feedback on v1 (verbatim, 2026-07-09): "1. With the Preferences tab, instead of showing In-app, Email, both and off, show just in-app and email and allow the tabs to be turned on and off. If both are on, then it's 'both', while if both are off, then it's 'off' 2. I'm unsure whether 'System Health' should be in Notifications 3. Don't show push notification numbers on the sidebar tab"
- v2 changes (same artifact URL, label
toggles-no-badge-health-option-v2): - Preference rows = two independent toggles (In-app / Email); effective state derives (both on = both, both off = off/muted, shown red). Watchdog-exempt row: both locked on.
- System Health placement made comparable in-preview — a "Health tab: in Notifications / in Tools" control shows both options (option B removes the tab + notes it lives in Tools). PM recommendation logged: keep it in Notifications (the alerts it explains land in this inbox; Tools is simultaneously LOSING Email Routing to this page, so sending surfaces both directions is confusing) — but the owner decides; decision pending.
- Sidebar Notifications item shows no unread-count badge (count stays on the Inbox tab label inside the page).
- Awaiting owner verdict on v2 + the System Health placement call.
2026-07-09 — round 2: preferences approved · rename proposals (v3) · hide-n-seek notifications fixed¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Owner (verbatim): "Preferences looks good." — the two-switch (In-app / Email) matrix design is APPROVED for P2.
- Rename ask (verbatim): "maybe if we rename the Notifications tab to something more like
Notifications, Alert Control & Warner Preferences... Something similar to that or if not similar,
more appropriate than that." Preview v3 (same URL, label
rename-notification-center-v3) shows the recommended "Notification Center" (sidebar + H1); alternatives offered: "Notifications & Alerts", "Alerts & Preferences". Owner to pick; the real nav rename ships with the P1 build. - Hide-n-seek fix — SHIPPED (
3e11ce2a, PR #878). Owner (verbatim): "There're a few notifications that prompts user to click for more detail, but then when the user arrives the Notification page, the notification prompts the user to check system log, which turned it into a whole kind of hide-n-seek. Could you find those conversation and make it so that the entire notification could show up in on notifications page?" Inventory + fix: - Offenders: the reconciliation summaries (daily cron
pages/api/cron/reconciliation.ts+ manualpages/api/accounting/reconciliation/run.ts) said "… detected. Open the reconciliation report for details." → now itemized via sharedbuildReconciliationSummaryMessage(ids/amounts/reasons, 12-per-category cap, exact "…and N more" overflow; 3 unit tests). - Rendering half: the inbox clamped every message to ONE ellipsized row (crushing even good
multi-line messages like the DMARC coverage lists) → now 2-line collapsed, expandable in place,
newline-aware (
app/notifications/client.tsx). - Reviewed, already self-contained: scrape-heartbeat (remedy included), dmarc-heartbeat
(itemized), rotation-reminder (runbook included). Excluded:
ocbc/velocity/debug(dev tool pointing at a NAS folder — nothing to inline). - Still open for the owner: rename verdict · System Health placement (in Notifications vs Tools).
2026-07-09 — naming RESOLVED: "Notifications" stays; owner grants a one-off deploy¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Owner (verbatim): "Let's just stick to notifications and merge it, then deploy the web app (one off deploy approval)". After weighing "Notification Center", "Notifications & Alerts", "Alerts" (preview v4) and the escalation ladder (alerts/alarms/warnings/signals/watchtower), the page and sidebar keep the existing name "Notifications" — zero code change needed (P0 already ships that label). Naming discussion closed.
- One-off deploy approved and executed by User Management via the Vercel deploy hook
(main 🟡🟡 = Vercel, manual — this is the owner's explicit per-instance order the policy
requires). Ships everything stacked on
mainincl. the T-160/T-161 signing-name work, the contact-drawer redesign, and this task's hide-n-seek fix (3e11ce2a). - Still open (last design decision before the P1 build): System Health placement — in Notifications vs in Tools; the comparison toggle stays live in the preview.
2026-07-09 — owner's 7-point restructure directive → "Notification Hub"; preview v5¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Owner directive (verbatim, 2026-07-09): "1. Rename it Notification Hub 2. a) Create a sub-tab named Push Notifications in the sidebar, and for that page, b) shows only the inbox tab and the Preference tab 3. a) Create a sub-tab named System Emails. b) Migrate Email Templates from the Tools tab and c) without dividing each in it's own tab on the System Emails page, allow the template of 'Data-integrity & watchdog alerts', 'Invoice delivery failures', 'Invoice overdue alerts' & 'Invoice re-issue needed' to be previewed there while attached the Email routing into each email type as well 4. Move 'System Health' as a sub-tab under the Settings Tab in the sidebar 5. Create a sub-tab under Tools named 'Typesetting', and migrate 'Invoice Previewer', 'Payment Receipt Preview (MEL)', 'Payment Request Preview (MEL)', & 'Payment Confirmation Previewer' to it. 6. a) Create a 'Features Preview' tab under Settings, b) migrate 'IR56M — sign & seal test' to it, and c) create a 'WOPC — sign & seal test' for the WOPC referencing the features of the 'IR56M — sign & seal test' 7. Move 'DMARC Reports' as a tab on the 'System Health' page"
- This SUPERSEDES the earlier one-page/4-tab P1 sketch and the "Notifications stays" naming
verdict; System Health placement is now RESOLVED: Settings (item 4), with DMARC as a tab on
it (item 7). Read-back confirmed; all migrate-sources verified in code
(
components/tools/ToolsApp.tsxrows +/admin/dmarc). - UX review delivered (owner asked): endorsed the audience-split IA as better than the 4-tab sketch; strongest idea = template-preview-beside-routing (item 3c). Flags recorded: (a) the name "Push Notifications" technically implies OS/browser push the app doesn't do — alternatives "My Notifications"/"Inbox" offered, owner's word stands, a live name-compare toggle is in the preview; (b) watchdog alerts' View action must DEEP-LINK to Settings ▸ System Health to offset the extra navigation hop; (c) System Emails page lazy-renders/collapses previews (weight).
- Preview v5 published (same URL, label
hub-restructure-v5): nested sidebar (Notification Hub ▸ Push Notifications · System Emails / Tools ▸ Typesetting / Settings ▸ System Health · Features Preview), Push Notifications = Inbox + Preferences (approved designs carried over), System Emails = per-kind routing beside a LIVE template mock (greeting select re-renders the preview), System Health with Watchdogs + DMARC tabs, Typesetting + Features Preview card pages (WOPC sign-&-seal marked NEW — doubles as the T-160 verification rig). Admin/Member gating: member sees only Push Notifications. - Assumptions logged: sidebar parents expand with children (parent click → first child);
/notificationsand/admin/dmarcredirect to their new homes. Scope split proposed: items 1–4+7 stay T-162; items 5–6 (Tools Typesetting + Features Preview + the new WOPC harness) to a companion task — awaiting the owner's OK. - Awaiting owner tweaks/approval on v5 before the build.
2026-07-09 — sub-tab HOUSING comparison (owner ask) → preview v6¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Owner asked how else the sub-tabs could be housed in the UI. Survey delivered (sidebar children · always-open groups · in-page tabs · card landings · second rail) with the dividing rule: own audience/permission or deep-link target → sidebar child; same audience, different view → in-page tab. Recommendation: children under parents, never collapsed (open groups) — hierarchy + per-child gating with nothing hidden; card landing noted as the upgrade path for Tools if it keeps growing.
- Preview v6 (same URL, label
sidebar-housing-options-v6): a Sidebar control cycles the SAME content through the three candidate housings — open groups (recommended, as drawn in v5) → collapsible parents (▸/▾ toggle, children can hide) → card landings (sidebar shows only parents; Notification Hub / Tools / Settings become card doorway pages). Housing decision pending the owner.
2026-07-10 — housing + taxonomy verdicts; Rehearsals live-front spec; preview v7¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Housing RESOLVED — collapsible. Owner (verbatim): "Sidebar: collapsible is definitely what I envisioned." (v7 defaults to it; the compare toggle stays for reference.)
- Taxonomy review delivered (owner asked whether the parent↔child pairings are categorically appropriate). Full Tools inventory (12 rows) grounded it. Governing rule recorded: use it to do/check work → Tools · set or observe the system → Settings · the system talking to people → Notification Hub. Suggested deltas: (1) keep the invoice family whole — "Full Invoice Preview" + "Invoice Sections" join Typesetting (6 tools); (2) sign-&-seal harnesses are rehearsal rigs by FUNCTION → Tools ▸ Rehearsals (categorize by what a thing IS, not its maturity — status shelves force re-homing); hold "Features Preview" for real feature toggles; (3) "Deployments — build status" is system observation → propose third tab on Settings ▸ System Health; (4) design references (Sidebar Icon Reference, Component Showcase) stay flat in Tools for now.
- Owner verdicts (verbatim): "I agree with the invoice section and full invoice preview merging to Typesetting." → delta (1) RESOLVED. On IR56M: "we could make it so that there's a front that allows a live preview of user inputting information on inline fields and those info gets shown immediately on the form, then provide the '[Open the sign & seal test here] (https://eop.theestablishers.com/ir56m/sign-test)' at the bottom of the IR56M section or the web app also allows user to user demo pre-filled information to do the test, and we'll include the Send Demo email button as well."
- Rehearsals live-front SPEC (from the owner's words): each rehearsal (IR56M · WOPC) is a
live form front — type into inline fields placed ON a form facsimile and the values render
immediately; Use demo data pre-fills; Send demo email exercises the real email
pipeline; footer link opens the full sign-&-seal flow (
/ir56m/sign-test; WOPC gets a new/wopc/sign-test). WOPC front's closing block renders via the T-160 signing-name resolution — the harness doubles as that pipeline's verification rig. - Preview v7 (same URL, label
rehearsals-live-fronts-v7): collapsible default · Typesetting = 6 cards · Tools ▸ Rehearsals with BOTH live form fronts working (type-in fields, demo fill, demo-email button, sign-&-seal links) · Settings ▸ System Health gains a “Deployments (proposed)” third tab so delta (3) can be judged in place · Features Preview removed. - Still open: owner's word on Rehearsals-under-Tools naming/parent (v7 shows the recommendation) and on the Deployments tab (marked “proposed” in v7).
2026-07-10 — v7 feedback round → v8: expandable rows, collapsible mail kinds, "Pipeline Preview", Deployments own sub-tab¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Owner feedback on v7 (verbatim): "1. For Typesetting a) Show everything as expandable rows just like how they were in the Tools page, b) Aside making the following collapsible as well, group Invoice Previewer, Full Invoice Preview & Invoice Sections into one ALSO collapsible parent row c) Specify the name as 'Project Invoice Previewer' d) Don't show 'Open previewer →', show what's in the previewer in the collapsible row (as mentioned in 1a)) 2. Same with the cards in System Emails, make them collapsible 3. I don't like how it's called 'Rehearsals', change it to something else (Pipeline Preview/ Walkthrough...) 4. Deployment should have it's own sub tab under Settings"
- Resolved by this round: Typesetting = expandable rows Tools-style, expanding shows the previewer inline (no link-outs), with the three invoice tools under one collapsible parent "Project Invoice Previewer" · System Emails kinds are collapsible (first open by default) · the live-front page is named "Pipeline Preview" (owner's first candidate; contrasts with Typesetting: document vs process) — Tools parent stands · Deployments = its own Settings sub-tab (not a System Health tab); System Health keeps Watchdogs · DMARC.
- Preview v8 published (same URL, label
expandable-rows-pipeline-preview-v8) implementing all four, with inline previewer mocks (invoice controls + sheet, sections px table, WOPC closing block, MEL receipt/request) marked "…the existing tool embeds here unchanged." - Awaiting the owner's v8 verdict — if clean, the design is CLOSED and the build starts (T-162 items 1–4+7; companion task for the Tools estate: Typesetting · Pipeline Preview · Deployments move).
2026-07-10 — Typesetting row renames (owner) → preview v9¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Owner (verbatim): "Rename Payment Receipt Preview (MEL) to Coaching Payment Receipt Preview
and Payment Request Preview (MEL) to Coaching Payment Request (Invoice) Preview" — these become
the BUILD-TIME row labels for the migrated tools. Applied in preview v9 (same URL, label
coaching-preview-renames-v9), incl. the facsimile sheet titles.
2026-07-10 — owner: "Go" — BUILD STARTED. Wave A shipped (P2 preferences engine + tabbed Push Notifications)¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Design v9 approved by the owner's "Go". Build waves: A P2 engine + Push Notifications tabs (this entry) · B Hub sidebar nesting + System Emails page · C System Health + DMARC + redirects · D–F Tools/Settings estate → split to T-165 (created now, per the recorded proposal).
- Wave A shipped:
lib/notifications/topics.ts— taxonomy moved out of the inbox client and made EXHAUSTIVE over the full 28-valueNotificationTypeunion (TYPE_META: Record<NotificationType,…>= compile-checked; newprojectstopic; inbox topic filter now derives membership from it).lib/notifications/preferences.ts+prefs.server.ts— the P2 engine: sparse per-user overrides onusers/{uid}.notificationPrefs(only non-default entries persisted — derive rule), two independent channel switches per kind, watchdog exemption (data_integrity× admin-tier can't be muted), batch reads fail-open to deliver.- Default = {inApp:on, email:on} for EVERY kind — a deliberate deviation from the preview's
per-row default markers: email fan-out only exists where the system already emails
(severity ≥ warning via notifyRoles + routed categories), and e.g. the DMARC coverage-gap
alert rides type
systemat warning — a per-kind email-off default would have silently muted owner-critical mail. Behaviour-preserving beats preview-cosmetic; matrix explainer says so. /api/profile/notification-preferences(GET catalog+overrides / PUT validated, self-scoped — T-161 P3 posture; locked kinds force-dropped server-side).- Enforcement in
lib/notifications/notify.ts:notifyUsersgates the in-app channel (every broadcast helper funnels through it);notifyRoles' email block gates the email channel after the category policy; channels independent; pref-read failures deliver-all. - Push Notifications page:
/notificationsretitled, now Inbox | Preferences tabs; the Preferences matrix is the approved two-switch UI (topic collapses, both-off shown red "off", watchdog-exempt lock, per-click optimistic saves). - Heavy unfiltered tsc clean · eslint clean on new/changed code (pre-existing stub
anys untouched) · vitest 645 green (+21: taxonomy exhaustiveness, defaults, normalize/derive-drop, exemption, channel filter).
2026-07-10 — Wave B shipped: Notification Hub sidebar nesting + System Emails page¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Sidebar:
notificationsrelabelled "Notification Hub" and made a collapsible parent via the house pattern (resources +DEFAULT_TAB_ORDERparent+ per-child page rules): children Push Notifications (/notifications, open rule — parent+child share the route, the proven bank precedent) and System Emails (/notifications/system-emails, admin rule inheriting the Tools gate vocabulary). Icons wired; saved sidebar orders self-reconcile (existing AppShell reparenting logic). - System Emails page (owner spec 3c): ONE page, four collapsible kinds, each pairing routing
(enable / recipients / greeting — same
/api/system/email-policymodel + Save semantics as the Tools row) with a live template preview rendered by the sender's own markup: the pure template extracted tointegrityEmailTemplate.ts(sender rewired, zero behaviour change except one honest fix: multi-line alert messages now render as lines in the HTML email too —white-space:pre-line— closing the email-side twin of the inbox clamp), and/api/tools/email-previewextended with the four alert kinds + agreetingsample param. Changing a kind's greeting re-renders its preview via the T-161 naming lib (sample person). The full Email Templates storyboard embeds below (the migrated tool, unchanged). - Tools slimmed: the "Email Templates" and "System Email Routing" rows removed (pointer comment left in place).
- Heavy unfiltered tsc clean · eslint: only a pre-existing pageRules
any(identical at baseline) · vitest 648 green (+3 template tests: greeting escape/omission, pre-line, text variant).
2026-07-10 — Wave C shipped: Settings ▸ System Health (Watchdogs · DMARC) + Deployments sub-tab¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Settings children (collapsible, house pattern): System Health (
/settings/system-health, new) and Deployments (points at the existing/deploymentspage — its Tools link-card retired);hk-banksstays; the standalone dmarc sidebar entry is gone (its badge vocabulary moved INTO the system-health rule; thedmarcPageKey retired, one rule test updated). - System Health page: Watchdogs tab reads
/api/system/health— a new READ-ONLY admin endpoint assembling the SAME freshness facts the dead-man's-switch crons check (reconciliation latest report;systemHealth/*markers via the cron's ownlastIngestAtfield incl. the legacy fallback; workspace entries max updateTime) with thresholds/remedies mirrored and keep-in-sync comments pointing at the crons (deliberately no refactor of the watchdog owner's files). Recent watchdog activity derives client-side from the shared notifications listener (viewer's owndata_integritycopies — no new index). DMARC Reports tab embeds the former/admin/dmarccontent (DmarcReportsContentexported);?tab=dmarcdeep-link; tab switch updates the URL. - Redirect:
/admin/dmarc→/settings/system-health?tab=dmarc(bookmarks + stale actionUrls keep working). - Watchdog deep-links (the recorded mitigation): scrape-heartbeat + reconciliation-heartbeat
alerts now
actionUrl: /settings/system-health; dmarc-heartbeat ×2 →/settings/system-health?tab=dmarc. (The reconciliation CRON's daily summary keeps/admin/reconciliation— that's the report page, still the right target for report content.) - Heavy unfiltered tsc clean · vitest 648 green · the 53 eslint hits in the three touched legacy files are byte-identical at baseline (pre-existing).
- T-162 build COMPLETE for its estate (P0 + waves A/B/C = the fork's P1+P2 re-scoped through the owner's v9 design). Remaining in-task: P3 (Resend webhook + delivery log) — parked pending the owner's word; the Tools estate continues in T-165.
2026-07-10 — coordination note from Accounting (Diagnostics): new type match_suggestion (T-041)¶
- ✅ Read AGENTS.md. T-041 shipped the auto-matcher suggestion notifications with a NEW
NotificationType:match_suggestion(topicbooks, label "Match suggestion", info severity, producernotifyMatchSuggestion). Registered in TYPE_META + labels + topic map + bell/inbox icon maps, so the preferences matrix and inbox grouping pick it up automatically — no action needed unless you want a custom row treatment. Also note T-038 reusespayment_statusand T-040 reusesbank_sync(no new rows). - Source: Accounting (Diagnostics) · https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
2026-07-20 — owner: the Hub must be THE single source of truth; recipient model v2 (dynamic ∪ specific) → preview v10¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Trigger — owner, reviewing the deployed System Emails page, asked about the invoice alert kinds (verbatim): "For the Invoice related alerts, instead of allowing configuration for user to decide whom exactly to send the alerts to, I'm wondering a) shouldn't these emails sent to whomever created the invoice and/ or system administrator or IT Specialist etc… b) Do we currently record whoever that creates the invoice so that we could have the system contact the invoice creator for follow-up alerts/notificaitons?"
- Facts reported for (b) (verified in code, not changed yet):
- Creator IS recorded: invoice writes stamp
createdBywith the session identity (email ≫ name ≫ "unknown") —pages/api/projects/by-id/[projectId]/invoices/index.ts(create :204, amend :304). It's an EMAIL string, so routing to the creator needs an email→uid resolve at send time. - The Hub is NOT the single source of truth today: the three invoice email kinds route via a
hardcoded
INVOICE_NOTIFICATION_ROLES = ['admin','project_admin']inlib/notifications/notify.ts(:301), and only the 4 alert categories are governed by/api/system/email-policyat all — e.g. the DMARC coverage-gap alert rides kindsystemwith no Hub row. - Liveness: only
invoice_reissue_neededactually fires today (2 call sites);invoice_overduehas a helper but NO cron;email_bounceawaits the parked P3 Resend webhook. - Owner directive (verbatim): "Regarding (b), are you saying that the configuration provided in Notification Hub isn't THE single source of truth? If so, we need to make it so. As for (a), I think we'll have to integrate all other alert configuration to allow including personnel like the original creator while allowing adding specific people"
- Recipient model v2 (agreed direction, pending v10 preview sign-off):
- Hub = authoritative for EVERY email-capable kind. The policy store becomes keyed per
kind; the hidden code role lists (
INVOICE_NOTIFICATION_ROLES, and the accounting twin) are retired to SEED DEFAULTS only — first read materializes them as stored, visible, editable config;notify.ts's email path reads the Hub policy for every kind (incl.system). - Mixed Send-to entries per kind: dynamic personnel tokens (✦ Original creator, All
admins, Project admins, Finance team…) resolved at send time from producer-attached context
(creator =
createdByemail → uid), freely mixed with specific users. Per-user Preferences still win for the individual (their "Off" stays off). - Preview v10 published (same artifact URL, label
recipient-model-v10): mixed-chip Send-to rows (dashed ✦ chips vs solid people), invoice kinds defaulting to ✦ Original creator + ✦ All admins + ✦ Project admins, a 5th demonstration rowsystem_notice(DMARC coverage gap) proving all-kinds governance, and the notebar rewritten to state the single-source-of-truth rule. Build starts on the owner's v10 approval per the standing preview gate; open build questions flagged to the owner: fold in the missinginvoice_overduecron, and/or unpark P3 (bounce webhook) with creator routing built in.
2026-07-21 — owner: "Sure" → recipient model v2 BUILT & MERGED (628a7154 / #911)¶
- ✅ Read AGENTS.md. Source: User Management · https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Owner approved v10 with "Sure" — read per the stated default as build as shown; the overdue cron and P3 (bounce webhook) stay parked until the owner says otherwise.
- Shipped (SHA
628a7154, PR #911): emailPolicy.tsv2 —EMAIL_CAPABLE_KINDS(all 18 kinds any producer dispatches throughnotifyRoles, incl.systemandaccess_expiry_warningfrom the super-admin lane), mixedRecipientEntrymodel (✦ dynamic tokenscreator/admins/project_admins/finance/super_admins+ specific uids), compile-time-exhaustiveSEED_KIND_POLICIES(the retired hidden role lists as stored-visible defaults), send-time resolution mirroringnotifyRolesmatching,stored → migrated-v1-category → seedprecedence, v2 write validation (creator token only on kinds with producer context). Legacycategoriesfield read-only for in-place migration; never written by v2 (rollback-safe).notify.ts— email path reads the per-kind Hub policy for EVERY kind; unclassified kinds warn + fall back to the role audience (the sync tripwire for futurenotifyRolescall sites).INVOICE_NOTIFICATION_ROLES/ACCOUNTING_NOTIFICATION_ROLESdemoted to in-app audience + seed material. The 3 invoice alert producers takecreatorEmail.- Creator resolution —
resolveInvoiceCreatorIdentityServer(earliestupdateLogsentry'seditedBy= the creation write, mirroring the invoice timeline's derivation; legacy doc-levelcreatedByfallback; null on failure, backstop tokens still cover). Both LIVEinvoice_reissue_neededcall sites (invoice PATCH + /api/invoices/send) attach it. /api/system/email-policyv2 — per-kind GET (effective config +storedflag + role-token membership so the UI shows the people behind a ✦ chip) and validated PUT (merge:truepreserves the legacy field). First Save materializes all seeds as stored config.- System Emails UI — every governed kind a collapsible row (topic chips; "seed default — not
saved yet" tag), mixed Send-to picker with dashed ✦ chips + resolved-membership hint,
per-kind live preview via new
alert-generic&kind=sample (all alert emails share the one sender template, so the preview is honest), v10 single-source-of-truth notebar. - Removed the orphaned
SystemEmailRoutingTool(unreferenced since wave B; consumed the retired v1 API shape). - Owner-visible seed deltas (all editable in the Hub): the 3 invoice alert kinds seed
✦ creator + ✦ admins + ✦ project admins (the directive's (a));
systemseeds ✦ All admins per the approved v10 (widened from super_admin-only — super_admin is inside the tier);access_expiry_warningstays ✦ Super admins;payment_statusseeds the union of its two dispatch estates (admins + project admins + finance). - Unchanged by design: severity ≥ warning email gate, per-user preference gating (a person's "Off" still wins), greeting resolution (T-161), in-app delivery & audiences.
- Heavy unfiltered tsc clean · vitest 673 green (emailPolicy suite rewritten for v2: seeds exhaustive, resolver incl. case-insensitive creator matching, precedence/migration, T-161 greeting grammar preserved) · eslint on touched files byte-identical to stash baseline (25 pre-existing, 0 new).
- Blast radius: email routing only — every kind that emails now resolves through the Hub policy; producers/in-app untouched; Records' seams untouched. NOT yet deployed (merged ≠ deployed; deploy on the owner's word per house rules).