Skip to Content

← All archived runs

Run: notifications-placement-parity

run.md

Run: notifications-placement-parity

  • branch: claude/notifications-placement-parity-vmddrw
  • pr: #712

01_define/output/spec.md

Spec: Notifications placement parity

  • slug: notifications-placement-parity
  • personas: All (admin, CSM, SDM, expert, vendor, customer)
  • touches: apps/web/components/notifications, apps/web/components/sidebar/app-sidebar.tsx, apps/web/app/(app)/notifications, apps/demo/components/notifications-panel.tsx, apps/demo/lib/mock/notifications.ts
  • complexity: standard

Problem

Notifications 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.

Proposed change

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).

Acceptance criteria

  • Web renders a visible, sidebar-anchored notification entry point (bottom-left) that opens the notification panel — replacing the current unrendered NotificationCenter mount.
  • The web panel's placement and visual design (unread dot, "N new" badge, mark-read / mark-all-read affordances, spacing/typography) match demo's NotificationsPanel pattern.
  • Web retains real-time delivery (Ably), persisted read state, and mark-read / mark-all-read writing through to the backend (not in-memory).
  • Web still links from the panel to the paginated "View all" full page, which remains functional.
  • Demo's sidebar notification panel remains its mock-only pattern (no behavioural change), and the two apps present notifications in the same placement/design.
  • Per-persona notification content is verified to exist in 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).

Out of scope

  • Building new notification types in packages/services — if the per-persona verification finds a missing type, it is recorded as follow-up work, not built this run.
  • Giving demo real-time delivery or persistence — demo stays mock-only by design.
  • Changing web's notification data model, services, actions, or the full-page pagination logic beyond what the placement/design alignment requires.

Open questions

  • none. Placement is decided (sidebar panel, bottom-left). The web 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.md

Build notes: notifications-placement-parity

  • commits: feat: notifications-placement-parity — sidebar-anchored notification panel in web

What changed

  • apps/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.
  • Item rendering still uses the existing NotificationInboxList (persisted mark-read, per-type accent colour, link navigation), so the panel body is the real inbox, not a mock.

Acceptance criteria status

  • Web renders a visible, sidebar-anchored notification entry point (bottom-left) that opens the panel — replaces the previously unrendered NotificationCenter mount.
  • The web panel's placement and visual design (unread dot/count, "N new" badge, mark-read / mark-all-read, spacing) match demo's NotificationsPanel pattern.
  • Web retains real-time delivery (Ably via useInboxSubscription), persisted read state, and mark-read / mark-all-read writing through the server actions (not in-memory).
  • The panel links to the paginated "View all" full page (/notifications), which is unchanged.
  • Demo's sidebar panel is untouched (mock-only), and both apps now present notifications in the same sidebar-anchored placement/design.
  • Per-persona notification type coverage verified against packages/services/src/notifications (see coverage note below). Gaps recorded, not built (per Out of scope).

Per-persona type coverage (verification)

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.

Verify result

  • mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. Release reads them back from the PR's check runs. No check is expected to fail.

Notes for review

  • Placement offset uses 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.
  • The bell → sidebar move means notifications are no longer in a top header (web never actually rendered one); the entry point now lives in the sidebar footer for both apps.

03_release/output/changelog.md

Changelog entry (run copy)

Live 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]

Your notifications now live in the sidebar

You can now open your notifications from a panel anchored to the sidebar, wherever you are in the app:

  • A notifications button in the sidebar shows how many unread items are waiting.
  • Opening it reveals your latest updates, with new ones arriving in real time.
  • You can mark a single item read, mark everything read, or jump to the full list to see the rest.

03_release/output/investor-update.md

Notifications now surface consistently across the product

Who 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.md

Release: notifications-placement-parity

  • pr: #712 (https://github.com/sustentus/sustentus/pull/712) · merged: pending (squash on ticked "Ready to merge")
  • CI: green — the only PR check ("Vercel Preview Comments") passed; web Vercel preview built to Ready. No failing checks.
  • technical docs: no technical docs impact — the change is a UI placement/design of the web notification entry point; no new app, package, route, env var, or architecture. apps/docs/app/technical describes the services notifications/ dir and delivery, which are unchanged.
  • business docs: no business docs impact — business/roles and platform-overview describe notification delivery semantics and templates (unchanged), not the entry-point placement. Behaviour semantics are unchanged.
  • release notes: both — changelog entry (apps/help/app/changelog/2026-07-27-notifications-placement-parity/page.mdx) + investor draft in this PR.
  • sent: pending — investor update sent immediately after merge via send-investor-update.sh.

Review summary

  • Fixed on branch: Tailwind arbitrary value 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.
  • Diff reviewed against 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).
  • Data layer untouched: listNotifications, useInboxSubscription, markNotificationRead/markAllNotificationsRead reused verbatim — presentation-only change.

Acceptance check (vs spec)

  • Web renders a visible, sidebar-anchored notification entry point — NotificationCenter mounted in the sidebar footer (was imported-but-unrendered).
  • Placement/design matches demo's NotificationsPanel — bottom-left fixed panel, unread count, "N new" badge, mark-read/all-read.
  • Real-time (Ably), persisted read state, server-action mark-read retained — data layer unchanged.
  • Links to the paginated "View all" full page (/notifications) — unchanged.
  • Demo panel untouched (mock-only); both apps share the placement/design.
  • Per-persona type coverage verified against 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).