notifications-placement-parityrun.md01_define/output/spec.mdNotifications are placed and styled differently across the two apps, so the same product feels
inconsistent between the demo and the real platform. Demo shows a fixed panel anchored to the sidebar
(bottom-left) with per-persona mock feeds, an unread dot, an "N new" badge, and mark-read/all-read
(in-memory only). Web has the richer implementation — real-time delivery (Ably), a persisted count
badge, persisted mark-read, and a paginated "View all" full page — but its entry point is the
NotificationCenter component, which is currently imported into app-sidebar.tsx but never
rendered (no <NotificationCenter /> JSX exists anywhere in apps/web), so web has no visible,
consistent notification surface today. This divergence undercuts the demo-web-parity epic under
Refine the Bridge (Q2 2026 O1 — vendor engagement, notifications & escalations): the demo we show
and the product we ship should present notifications the same way.
Standardize both apps on demo's sidebar-anchored panel placement/design (owner decision, this run): a notification entry point anchored to the sidebar bottom-left, opening a fixed panel with an unread dot, an "N new" badge, and mark-read / mark-all-read. Web adopts this placement and visual language while keeping all of its existing richer behaviour — real-time delivery via Ably, persisted read state, and the paginated "View all" full page (the panel still links out to it). This is primarily a placement + design-alignment change, not a rebuild of the notification system: web's services, actions, real-time hook, and full page are reused as-is; only the presentation/entry-point layer changes to match demo. Demo stays mock-only (no real-time/persistence).
NotificationCenter mount.NotificationsPanel pattern.packages/services for the types demo
illustrates (SLA at risk, invoice due / awaiting approval, blocker aging, quote undecided, CSAT
returned, integration warning, invite pending, lead qualified out). Any missing type is recorded
(not built — see Out of scope).packages/services — if the per-persona verification finds a
missing type, it is recorded as follow-up work, not built this run.NotificationCenter being
imported-but-unrendered is a build-time reconciliation detail, not a scope question: Build wires the
agreed sidebar panel as the live entry point.02_build/output/notes.mdfeat: notifications-placement-parity — sidebar-anchored notification panel in webapps/web/components/notifications/notification-center.tsx: rebuilt from a header-style
Popover bell into a sidebar-anchored panel matching demo's placement/design. The trigger is
now a SidebarMenuButton (Bell icon, "Notifications" label, unread count pill using
bg-sidebar-primary), and the feed opens as a fixed panel anchored bottom-left
(md:left-[calc(var(--sidebar-width)+0.5rem)], md:bottom-4, md:w-80) — the same shape as the
demo's NotificationsPanel, but styled with @sustentus/ui tokens (bg-popover, border) rather
than demo's raw palette. Header carries a "N new" badge, "Mark all read", and a close button; the
footer keeps the "View all" link to /notifications. All of web's data behaviour is preserved:
listNotifications, useInboxSubscription (Ably real-time), persisted markNotificationRead /
markAllNotificationsRead, and the optimistic/refresh logic are unchanged — only the presentation
and entry point moved.apps/web/components/sidebar/app-sidebar.tsx: mounted <NotificationCenter /> as the first item
in the SidebarFooter menu (gated on !isOnboarding, like the other footer entries). Previously
the component was imported but never rendered, so web had no visible notification surface — it does
now.NotificationInboxList (persisted mark-read, per-type accent
colour, link navigation), so the panel body is the real inbox, not a mock.NotificationCenter mount.NotificationsPanel pattern.useInboxSubscription), persisted read state, and
mark-read / mark-all-read writing through the server actions (not in-memory)./notifications), which is unchanged.packages/services/src/notifications
(see coverage note below). Gaps recorded, not built (per Out of scope).Demo-illustrated signals mapped to the real notification registry:
| Demo signal (persona) | Real type / wrapper | Status |
|---|---|---|
| SLA at risk (admin) | notifySlaBreach / SLA_BREACH |
✅ exists |
| Invoice due (customer) | notifyInvoiceRaised / InvoiceNotificationType |
✅ exists |
| Invoice awaiting approval (csm) | notifyInvoiceRaised / notifyInvoiceApproved |
✅ exists |
| Blocker aging / unresolved (csm/expert) | notifyServiceLeadConcernRaised / notifyServiceLeadEscalated |
✅ exists (closest) |
| Invite pending (admin) | notifyUserInvited / UserNotificationType |
✅ exists |
| Lead qualified out (admin) | notifyServiceLeadStatusChanged (status transition) |
✅ exists |
| Quote undecided (csm) | quotes have ACCEPTED/REJECTED only — no "pending/undecided" type |
⚠️ derived signal, no discrete type |
| CSAT returned (vendor) | only notifyServiceLeadCsatRequested (request) — no "returned/completed" type |
⚠️ derived signal, no discrete type |
| Integration warning (admin) | no dedicated type — would be SYSTEM/ops |
⚠️ no discrete type |
The three ⚠️ rows are derived/aging or system-level signals, not missing first-class notification
types. Adding new packages/services types is explicitly out of scope for this run; recorded here for
a follow-up if the product wants them as real notifications.
var(--sidebar-width) so the panel tracks the shared sidebar width. In the
sidebar's collapsed icon mode the panel keeps the expanded offset (the demo doesn't handle collapse
either) — acceptable for placement parity; flag if the collapsed-mode anchor matters.03_release/output/changelog.mdLive entry: apps/help/app/changelog/2026-07-27-notifications-placement-parity/page.mdx
title: "Your notifications now live in the sidebar" personas: [admin, csm, sdm, expert, vendor, customer]
You can now open your notifications from a panel anchored to the sidebar, wherever you are in the app:
03_release/output/investor-update.mdWho it's for: All six personas (admin, CSM, SDM, expert, vendor, customer) What shipped: A sidebar notification panel now appears across the platform, matching the demo — real-time updates and read tracking preserved. Why it matters: Refine the Bridge, Objective 1 (vendor product-market fit) — consistent notification placement keeps users on top of what needs action.
The web app previously rendered no visible notification surface at all.
Dig deeper: <merged-PR URL> · <changelog entry URL>
03_release/output/release.mdapps/docs/app/technical describes the services notifications/ dir and delivery, which are unchanged.business/roles and platform-overview describe notification delivery semantics and templates (unchanged), not the entry-point placement. Behaviour semantics are unchanged.apps/help/app/changelog/2026-07-27-notifications-placement-parity/page.mdx) + investor draft in this PR.md:left-[calc(var(--sidebar-width)+0.5rem)] produced invalid CSS (calc requires spaces around +; Tailwind needs them as underscores). Corrected to md:left-[calc(var(--sidebar-width)_+_0.5rem)] so the panel anchors correctly beside the sidebar. Without this the left offset silently fell back and the panel mis-positioned.CONVENTIONS.md (standard complexity → medium): arrow functions, named @sustentus/ui imports, sentence-case copy, braces on all control flow — all conform. No dead code or leftover scaffolding. The header-bell popover was fully replaced (no orphaned Popover imports).listNotifications, useInboxSubscription, markNotificationRead/markAllNotificationsRead reused verbatim — presentation-only change.NotificationCenter mounted in the sidebar footer (was imported-but-unrendered).NotificationsPanel — bottom-left fixed panel, unread count, "N new" badge, mark-read/all-read./notifications) — unchanged.packages/services/src/notifications — 6 signals map to real wrappers, 3 (quote undecided, CSAT returned, integration warning) recorded as derived/system signals with no discrete type (not built — out of scope).