---
path: /notifications
surface: tenant-admin
domain: operations
auth: signed-in + tenant:admin (getTenantServerSession)
source: apps/oshun/tenant-admin/src/app/notifications/page.tsx
status: drafted
last_walked: '—'
---

# Notifications

## Purpose

Read-only inspection of how tenant communications are evaluated before sending:
per-channel delivery decisions against user preferences, localized template
rendering, message-center inbox state, and digest batching. Backs the
`notifications` workspace (`configuration` group) in
`libs/oshun/navigation/src/tenant-ia.ts`.

## Entry points

- Shell sidebar (`TenantSidebar`) — **"Messages"** (`shortLabel`) under the
  **Configuration** group, link to `/notifications`
  (`data-workspace-id="notifications"`)
- Workspace grid on `/` (home) — list item `data-workspace-id="notifications"`
- Direct URL `/notifications` (bookmarkable; middleware-gated)

## Layout regions

Inside `TenantShell`:

- **Header**: `TenantHeader` shows `tenant · <tenantId>` › **"Notifications"**
- **Main**:
  `<section data-testid="tenant-notifications-panel" data-tenant-id={tenantId}>`
  - `<h1>Notifications</h1>` + subtitle paragraph
  - `.summaryGrid` — three `.summaryItem` tiles: **Deliveries** (count of
    decisions with `verdict === 'deliver'`), **Inbox unread** (`inbox.length`),
    **Digest items** (`digest?.totalItems ?? 0`)
  - `.table` — per-channel decision table: columns **Channel**, **Preference**
    (`minSeverity` or "override"), **Decision** (badge: `deliver` → `badgeGood`,
    else `badgeNeutral`)

## States

- [ ] **Loading** — N/A; server component renders fixtures in one pass
- [ ] **Empty** — N/A; fixture `prefs.perChannel` always yields rows (email,
      push, in-app). "Digest items" can show `0` when no digest is built
- [ ] **Populated (short)** — three channel rows render; summary tiles reflect
      `decideDelivery` verdicts, `inboxFor` unread count, `buildDigest` items
- [ ] **Populated (long)** — N/A; fixture fixed at 3 channels
- [ ] **Error (recoverable)** — N/A; no runtime query
- [ ] **Error (unrecoverable)** — no session →
      `redirect('/unauthorized?reason=missing-session&returnTo=/notifications')`
- [ ] **Offline** — N/A for data (SSR-rendered)
- [ ] **Gated** — `getTenantServerSession()` null → redirect to `/unauthorized`;
      sidebar **Locked** when `canEnterTenantWorkspace` is false
- [ ] **Standalone PWA** — verify; no manifest/SW confirmed in source

## Interactions

The `<main>` content is a static report — **no buttons, inputs, toggles, or
template editor**. All interactivity is shell chrome:

- [ ] **Skip to main content** (link, `TenantShell`) → anchors `#tenant-main`
- [ ] **Sidebar workspace links** (links, `TenantSidebar`) → navigate; current
      `aria-current="page"`; **Locked** when gated
- [ ] **Channel decision rows** — non-interactive; each row shows channel,
      resolved preference `minSeverity`, and a verdict badge

## Data & contracts

- **Reads**: no BFF/network read. The page composes pure functions from
  `@oshun/tenant-console` on in-page fixtures:
  - `decideDelivery({ request, prefs: UserNotificationPreferences })` →
    per-channel `verdict`
  - `renderTemplate({ template: NotificationTemplate, variables, locale, fallbackLocale })`
    → `rendered.subject` / `rendered.body`
  - `appendEntry(...)` then
    `inboxFor({ entries, userId, onlyUnread, minSeverity, limit })` → `inbox`
  - `enqueueForDigest(...)` then
    `buildDigest({ userId, tenantId, cadence, nowUnixSeconds, queue })` →
    `digest`
  - `NOW = Date.UTC(2026, 4, 11, 15)` (seconds); user fixture `op-jane`
- **Writes**: _None._
- **Realtime**: _None._
- **Caching**: SSR (server component); deterministic per `tenantId`
- **Auth/role check**: `getTenantServerSession()` in page; middleware gate

Contracts: `NotificationTemplate`, `UserNotificationPreferences` from
`@oshun/tenant-console` (`libs/oshun/tenant-console`). Inbox/digest entries
carry `deepLinkPath` values `/policy` and `/members` pointing at sibling routes.

## Cross-references

- Workspace definition: `libs/oshun/navigation/src/tenant-ia.ts`
  (`notifications` — group `configuration`,
  `requiredScopes: ['tenant:admin:*']`, `iconToken: 'bell'`,
  `shortLabel: 'Messages'`)
- Shell: `TenantShell.tsx`, `TenantSidebar.tsx`, `TenantHeader.tsx`
- Deep-link targets: [policy.md](policy.md), [members.md](members.md)
- Sibling routes: [integrations.md](integrations.md), [roles.md](roles.md),
  [status.md](status.md)
- Conventions:
  [../WALKTHROUGH/00-conventions.md](../WALKTHROUGH/00-conventions.md)

## Open questions / known gaps

- [ ] No live notifications BFF route is wired — page renders a static fixture
      of one delivery request, one template, one inbox entry, one digest item.
- [ ] No template editor, send action, or preference toggles exist on this route
      (read-only). Verify where tenant template management is intended to live.
- [ ] "Preference" column shows `override` when no per-channel `minSeverity` is
      found; confirm this is the intended fallback label.
