Skip to Content

← All archived runs

Run: console-dashboard-activity-polish

run.md

Run: console-dashboard-activity-polish

  • branch: claude/console-dashboard-activity-polish-2b2fi0
  • pr: #907

02_define/output/spec.md

Spec: Console dashboard, activity and notifications polish

  • slug: console-dashboard-activity-polish
  • personas: Admin (Sustentus staff), Partner
  • touches: apps/console/app/(console)/page.tsx, apps/console/app/(console)/activity/, apps/console/app/(console)/notifications/, apps/console/components/
  • complexity: standard

Problem

The landing dashboard, the activity trail and the notification surfaces are what both console audiences see first and most — a partner lands on the dashboard the moment they accept an invitation, and staff live on these pages all day. They also carry the batch's confirmed irritations hardest: every zero state is a bare muted sentence where the design wants a designed one, every page pops in with no loading state because each is a server component that awaits its reads before anything renders, and the stat cards, feed rows and notification rows read as default shadcn rather than Sustentus. console-shell-sidebar moved the frame to a sidebar shell, set the brand baseline in the chrome and freed the width; these pages have not been restyled to sit inside it. Partners judge Sustentus by exactly this surface, so the polish advances Q2-2026 Objective 1 — Establish Product-Market Fit with Vendor Partners — under the Scale the Bridge initiative.

Proposed change

A presentation pass over the dashboard, /activity, /notifications, the staff-only /notifications/delivery page and the components those surfaces share, inside the shell the first stub established. Visual/UX only (Jamie, 2026-08-27): no read changes, no new figure, no new filter, no change to scoping or gating.

  • Designed empty states. A single console-local empty-state component (an icon, a short headline, a line of explanation, and an action where one genuinely exists) replaces the bare muted paragraph everywhere these surfaces render zero data: no activity on the dashboard preview and on /activity, no notifications in the centre and in the bell popover, and no failures on /notifications/delivery. It lives in apps/console/components/ so stubs 3 and 4 can adopt it; packages/ui is not touched. Today's audience-aware wording is preserved — a partner's zero state still says "on the tenants assigned to you", staff's still says "in the console" — and the past-the-end-of-the-list messages on both paginated pages stay distinct from true-zero, keeping their "back to the first page" way out.
  • Loading skeletons. Navigating to the dashboard, /activity, /notifications or /notifications/delivery shows a skeleton that mirrors the page it is loading — stat cards as cards, feed and notification rows as rows — built on the Skeleton primitive @sustentus/ui already exports, instead of the current blank pause while the server read resolves. Whether that is a route-level loading.tsx or a Suspense boundary per region is Build's to pick.
  • Email delivery on the dashboard: a designed all-clear. Jamie's ruling this run (2026-08-28) replaces the "renders nothing at zero" behaviour: the dashboard shows an email-delivery block in both states — the existing error alert when sends have failed, and a designed all-clear when none have. It stays staff-only exactly as today: the failureSummary() read is still skipped for a partner, so a partner's dashboard shows no email block at all and pays for no cross-tenant count.
  • Brand pass. Stat cards, the welcome header block, feed entries, notification rows (including the unread emphasis and the bell's popover) and the delivery page's failure rows follow the brand typography scale, spacing and palette per the brand-guidelines skill, consistent with the shell chrome, and correct in both light and dark themes.
  • Small screens. These pages stay readable at tablet and half-screen widths inside the new shell — the dashboard's card grid, the feed's row layout and the pagination controls reflow rather than overflowing horizontally.

Data, scoping and reads are untouched: staff see the estate, a partner sees the tenants carrying their id, and the "counts + activity is the whole of it" ruling from the console batch still stands.

Context budget: read the five target files and their components in lieu of technical/architecture/repository-structure to pin exact behaviour, plus the shipped console-shell-sidebar spec for the baseline this builds on. No scope.md exists for this batch — the breakdown records it as Jamie's intake interrogation (2026-08-27); rulings trace there and to this run's two answers.

Acceptance criteria

  • Every true-zero state on these surfaces renders the designed empty state rather than a bare line of text: the dashboard's recent-activity card, /activity, /notifications, the bell popover, and /notifications/delivery — each keeping today's audience-specific wording.
  • On /activity and /notifications, a page number past the end still reads as past-the-end (not as "you have nothing") and still offers the way back to the first page.
  • Navigating to the dashboard, /activity, /notifications and /notifications/delivery shows a skeleton shaped like the page being loaded, not a blank pause or an abrupt pop-in.
  • A staff viewer's dashboard shows an email-delivery block in both states — the error alert when sends have failed in the window, a designed all-clear when none have, linking to /notifications/delivery.
  • A partner viewer's dashboard shows no email-delivery block in either state, and the failureSummary() read is still skipped for them.
  • Stat cards, the welcome block, feed entries, notification rows and the delivery page's rows follow the brand typography scale and palette, and render correctly in both light and dark themes.
  • At tablet and half-screen widths none of these four pages shows a horizontal scrollbar of its own; cards, rows and pagination controls reflow.
  • A partner viewer sees exactly the data they see today — the same counts, the same trail entries, the same notifications, nothing added and nothing widened.

Out of scope

  • New dashboard figures (platform health, uptime), new filters, new pages, or any new or widened read. Counts + activity remains the whole of it.
  • The notification read model, the mark-read and mark-all-read actions, and the layout-freshness caveat — the bell still shows what the shell last rendered.
  • Access control of any kind: proxy.ts, requireViewer/requireStaff, isConsoleStaff, the console-access marker. Which viewer sees which surface is unchanged.
  • The email-delivery model, its 24-hour alert window, its 30-day TTL and its result cap — the delivery page is restyled, not re-read.
  • Changes to packages/ui; the empty-state component is console-local this run.
  • The tenant pages (stub 3), and the (auth) / no-access surfaces (stub 4).

Open questions

  • none — (non-blocking note) the mechanism for the skeletons (route-level loading.tsx vs a Suspense boundary per region) and the exact composition of the empty-state component are Build's to pick; the criteria above are the requirement.

03_build/output/notes.md

Build notes: console-dashboard-activity-polish

  • commits: feat: console-dashboard-activity-polish — empty states, skeletons and brand pass
  • ci: GREEN on 9ab0339 — Quality Project, Project run labels, the CONVENTIONS review check and the tenant-management (apps/console) + web previews; the other six Vercel projects were skipped for this diff

What changed

  • components/empty-state.tsx (new): the console's one zero state — icon, title, description, optional action, plus a compact mode for the bell popover. Console-local so stubs 3 and 4 can adopt it; packages/ui untouched, as the spec requires. Wording stays the caller's, because what is empty depends on who is looking.
  • components/page-header.tsx (new): the heading block all four pages open with, replacing four separately-drifting copies of the same h1 + muted line.
  • components/console-skeletons.tsx (new): loading shapes that mirror what they stand in for — header, two stat cards, feed rows, the email block, and a list card with its pager row.
  • activity/loading.tsx, notifications/loading.tsx, notifications/delivery/loading.tsx (new): route-level loading for the three leaf surfaces. Deliberately not a (console)/loading.tsx: a segment-root loading file is inherited by /tenants/**, which would put a dashboard-shaped skeleton on stub 3's pages.
  • (console)/page.tsx: the dashboard's three reads each moved behind their own Suspense boundary instead of one Promise.all before first paint — heading and layout arrive immediately, each region fills as its read lands, and the reads still run concurrently. The email-delivery region renders only for staff, so a partner's dashboard still skips the failureSummary() read entirely.
  • components/email-delivery-alert.tsx: now renders in both states. The all-clear (success variant, MailCheck) replaces the previous render-nothing-at-zero behaviour per Jamie's ruling (2026-08-28); the failure alert is unchanged, including the misconfigured-key wording.
  • components/stat-card.tsx: brand pass — the figure carries text-primary at 3xl with tabular-nums, the label above it drops back to small and muted.
  • components/activity-feed.tsx, components/notification-list.tsx: emptyMessage: string became empty: ReactNode, so callers hand in a designed state instead of a bare line. The notification row gained a primary unread dot, aria-hidden and paired with the existing semibold label rather than carrying the unread state alone.
  • The four pages + the layout's bell popover: adopted PageHeader and EmptyState, keeping every audience-specific string and the past-the-end-of-pagination distinction.

Acceptance criteria status

  • Designed empty state on every true-zero surface — dashboard recent-activity card, /activity, /notifications, the bell popover (compact), /notifications/delivery; each keeps its staff/partner wording.
  • Past-the-end still reads as past-the-end — a distinct SearchX state naming the real total, with the existing "back to the first page" button untouched below it.
  • Skeletons shaped like the page — loading.tsx on the three leaf routes, per-region Suspense fallbacks on the dashboard.
  • Staff dashboard shows the email block in both states, both linking to /notifications/delivery.
  • Partner dashboard shows no email block and still skips the read — the component that performs it is not rendered for a partner.
  • Brand typography and palette across stat cards, headers, feed and notification rows and the delivery page's rows; semantic tokens throughout (text-primary, bg-muted, text-muted-foreground, the success/error Alert variants), so both themes follow.
  • Small screens — the empty state is centred and width-capped, the header line is max-w-2xl text-balance, and the existing flex-wrap row and pager layouts are unchanged.
  • A partner sees exactly today's data — no read, scope, gate or query changed; the diff is presentation only.

Notes for Verify

  • No tests added, deliberately. The diff is presentational and touches no pure logic; only the unit tier exists (node environment, no DOM), so the criteria here are not unit-assertable, and page markup is on the "what not to test" list. The four existing console unit test files are untouched.
  • Worth a look on the preview: the dashboard's three Suspense regions settling in sequence, and the staff all-clear, which is the one genuinely new piece of UI in the run.
  • apps/console/app/(console)/tenants/page.tsx has a local emptyMessage helper of its own. It is stub 3's surface and was left alone; the name is coincidental, not a missed call site.

04_verify/output/verify.md

Verify: console-dashboard-activity-polish

  • ci: GREEN on 76b4954 — the head carrying every code line of this run, including the two Verify fixes below; settled via ci-status.sh after that push. The doc-only commit adding this file is verified green immediately after it is pushed, and its SHA is reported at the gate.
  • previews smoked: none by the agent. tenant-management (apps/console — the only app this run touches) did build for 76b4954, and that is the deployment the operator's smoke will reach via the branch alias (tenant-management-git-claude-console-dashboard-2f982c-sustentus.vercel.app). The agent still cannot reach it: the preview sits behind Vercel deployment protection and answers 302 to vercel.com/sso-api. See Finding 1.
  • production-readiness: not required — the diff touches no database access, no auth mechanics, no payments and no env vars. Every service call in it (countForConsole, readRecentActivity, readActivityPage, readCentrePage, failureSummary, recentFailures) is the pre-existing call with the pre-existing arguments; turbo.jsonglobalEnv is untouched because no process.env read was added.
  • code-review: medium (spec complexity: standard), run by the agent — the CI Review diff against CONVENTIONS.md job was skipped on the commit carrying the code and ran only on the markdown-only head, so there were no CI review comments to triage (Finding 3). 2 findings, both fixed on branch.
  • security-review: run — no HIGH or MEDIUM findings. Detail below.
  • playwright: TODO — manual DoD smoke performed instead (and see Finding 1: the manual half is entirely the operator's on this run).

Security review

Scope: the branch diff against origin/main. The one authorisation-relevant change is that the staff-gated email-delivery read moved from an inline ternary in the dashboard's Promise.all into a child server component rendered only when staff is true. Verified:

  • No gating changed. requireViewer() still runs at the top of the page (live Clerk read, not a session claim), isConsoleStaff(viewer.access) still computes staff from it, scopeOf(viewer) still bounds the tenant count, and proxy.ts / requireStaff() are untouched.
  • The partner still never pays for the read. EmailDelivery is the component that calls failureSummary(), and it is not rendered for a partner — so the read is skipped, not merely hidden, exactly as before.
  • No new client boundary. Every file added or changed here is a server component (no "use client" in the diff), so viewer is never serialised toward the browser. The only client component in the area, NotificationBell, is unchanged and still receives server-rendered children.
  • No unsafe rendering. No dangerouslySetInnerHTML, no eval, no raw HTML; all new text is JSX-escaped literals. No new file, path, query or secret handling anywhere in the diff.

DoD smoke (on the preview — each line says who verified it)

Agent half — code-path tracing only. No criterion below is agent-demonstrated on a preview; the agent could not reach one (see Findings 1 and 3).

  • Preview deploys and stays gated — the console branch preview answers 302 to Vercel SSO on / and /activity; nothing is publicly reachable (agent)
  • Traced in the diff: every true-zero path now renders EmptyState — dashboard feed, /activity, /notifications, bell popover (compact), /notifications/delivery (agent)
  • Traced in the diff: past-the-end keeps its own SearchX state and the "back to the first page" control below it, on both paginated pages (agent)
  • Traced in the diff: loading.tsx exists for /activity, /notifications and /notifications/delivery; the dashboard uses three per-region Suspense boundaries (agent)
  • Traced in the diff: the partner branch renders no email block and performs no failureSummary() read (agent; see Security review)

Operator half — not yet performed. Every line below is outstanding.

  • Each of the 8 acceptance criteria demonstrated signed-in on the console preview (operator)
  • auth: a staff viewer and a partner viewer both sign in and reach the dashboard (operator)
  • payments: not touched by this diff — nothing to test (operator, n/a)
  • notifications: none expected — this run fires no notification and changes no send path; the bell's existing preview should still populate (operator)
  • walkthrough clip posted to the feature thread in #build (operator)

Findings & cleanup

  1. The agent cannot reach the console preview, so the whole on-preview DoD half is the operator's. The preview is behind Vercel deployment protection: / and /activity both answer 302 to vercel.com/sso-api, before Clerk is even involved, so there is no surface the agent can demonstrate anything on. Not a defect in the change — recorded so the gate is not read as though the agent smoked it. A related bookkeeping note on which commit carries the preview: Vercel skips tenant-management on commits that touch only .icm/**, so the doc-only heads of this run (af25bb8, and the commit adding this file) have no console preview of their own, while the code heads (9ab0339, 76b4954) do. The branch alias always serves the newest successful build — 76b4954 — which is the code under review. A skipped project is recorded as skipped throughout this file, never quoted as a green preview, and no cosmetic commit was pushed to re-trigger a build: that is the "kick CI" move the pipeline forbids.
  2. The all-clear asserted delivery it could not knowemail-delivery-alert.tsx. The zero-failure branch read "Notification email is getting through", inferred from total === 0 alone. Both counts in failureSummary() come from failure rows, and successful sends are never recorded, so an environment with no provider key and no administrative actions in the window produces the same zero as a healthy one: staff would have seen a green all-clear while console email was entirely dead. Its 24-hour window also contradicted the 30-day list it links to. Fixed on branch: the state now reports the absence of failures ("No notification email has failed in the last 24 hours"), says outright that this is not proof of delivery, and names both windows. The component comment records why the wording must not be strengthened.
  3. The CI CONVENTIONS review never saw this run's code. On 9ab0339 — the commit carrying every line of the change — Review diff against CONVENTIONS.md is skipped; it ran and passed only on af25bb8, which contains nothing but markdown. So the check being green on the PR head says nothing about the code. The agent ran /code-review at medium in its place, which is what found Finding 2. Not fixed on branch — this is a repo-wide CI trigger issue, not this run's to change; raising it for a chore lane rather than widening this PR.
  4. Unread state was visual-onlynotification-list.tsx. The new dot was aria-hidden and the comment justified it as "paired with the semibold label", but font-semibold carries no accessible semantics, and the bell popover renders no Mark read control — so nothing in the accessibility tree distinguished read from unread there. Fixed on branch: an sr-only "Unread." label now sits beside the decorative dot.

Context budget: read packages/services/src/db/services/console-email-delivery-log/index.ts beyond the Inputs table, to establish what failureSummary() can and cannot know for Finding 2.

05_ship/output/changelog.md


title: A refreshed dashboard, activity trail and notifications in the console date: 2026-08-28T00:00:00Z personas: [admin] slug: console-dashboard-activity-polish pr: https://github.com/sustentus/sustentus/pull/907

A refreshed dashboard, activity trail and notifications in the console

The sidebar arrived last week; the pages inside it are being refreshed one surface at a time. This is the first of them — the dashboard you land on, the activity trail, and everywhere notifications appear. Nothing new to do here, and nothing new on show: the same numbers, the same entries, the same people who could see them before.

Nothing is no longer a blank space. A quiet morning, a newly assigned partner, a page past the end of the trail — each now says what it means and what would fill it, instead of a lone grey sentence. Being told "nothing has happened yet" is different from wondering whether the page loaded, and the two no longer look alike.

Pages arrive in shape. Opening the dashboard, the activity trail, notifications or email delivery now shows the outline of what is coming — the cards, the rows — which fills in as it loads. Nothing jumps into place after a pause.

It looks like Sustentus. Tenant counts, feed entries and notification rows follow the same type and colour as the rest of the product, in both light and dark. Unread notifications are now marked as unread for screen readers too, not by weight alone.

Email delivery says so when it is fine. For the Sustentus team, the dashboard used to mention notification email only when something had failed, so silence was ambiguous. It now confirms when nothing has failed in the last 24 hours — and says plainly that this is the absence of failures rather than proof of delivery, since successful sends are not recorded.

The tenant pages and the sign-in screens are next.

05_ship/output/investor-update.md

The console's busiest pages now look like Sustentus

Who it's for: Sustentus staff and invited partners What shipped: The dashboard, activity trail and notification surfaces gained designed empty states, loading skeletons and brand styling in both themes. Why it matters: Partners judge us by this surface — Scale the Bridge, Objective 1: Establish Product-Market Fit with Vendor Partners.

Same data, same access — presentation only.

Dig deeper: https://github.com/sustentus/sustentus/pull/907 · https://help.sustentus.com/changelog/2026-08-28-console-dashboard-activity-polish

05_ship/output/release.md

Ship: console-dashboard-activity-polish

  • pr: #907 · merge: authorised — Ready to merge ticked by Jamie; this commit rides the squash
  • CI: GREEN on the head this file is pushed with, settled via ci-status.sh after the last push (recorded at the merge; the run's earlier heads were green too — 76b4954 carried the code and built the console preview)
  • technical docs: apps/docs/app/technical/applications/console/page.mdx — the Home dashboard's email-delivery bullet and the delivery-log passage above it said the block appears "only when the trailing 24 hours contain a failed send". It now renders in both states, so both passages were corrected, including why the all-clear reports the absence of failures rather than proof of delivery.
  • business docs: no business docs impact — the console is the internal tenant-management surface, never tenant-facing. No service-journey step and no feature-role-matrix entity is touched, and no persona gained or lost a capability.
  • release notes: both
  • sent: ship note queued to #product-update by ship-note.yaml on the merge
  • close-out: archive .icm/runs/console-dashboard-activity-polish/ to apps/docs/archive/pipeline-runs/. The console-ui-polish epic is not finished — two stubs remain in flight (console-tenants-polish, console-auth-access-polish), so the intake folder stays put.

Acceptance check (vs spec)

  • Designed empty state on every true-zero surface — dashboard recent-activity card, /activity, /notifications, the bell popover (compact), /notifications/delivery; each keeps its staff/partner wording. Traced in the diff at Verify.
  • Past-the-end still reads as past-the-end on both paginated pages, with the way back to the first page intact — a distinct state naming the real total. Traced in the diff at Verify.
  • Skeletons shaped like the page — loading.tsx on the three leaf routes, per-region Suspense on the dashboard. Traced in the diff at Verify.
  • Staff dashboard shows the email-delivery block in both states, both linking to /notifications/delivery. Corrected during Verify so the all-clear no longer asserts delivery it cannot know.
  • Partner dashboard shows no email-delivery block and still skips the failureSummary() read — the component performing it is not rendered for a partner. Confirmed by the security review.
  • Brand typography and palette across stat cards, headers, feed and notification rows and the delivery page's rows, correct in both themes — semantic tokens throughout.
  • Small screens — the empty state is centred and width-capped, the header line wraps, and the existing flex-wrap row and pager layouts are unchanged.
  • A partner sees exactly today's data — no read, scope, gate or query changed; the diff is presentation only.

How these were verified. All eight were established by code-path tracing in the diff plus the code and security reviews, recorded in 04_verify/output/verify.md. The agent demonstrated none of them on the preview: the console preview sits behind Vercel deployment protection and answers 302 to Vercel SSO, so the whole on-preview half of the Definition of Done was the operator's. Ticking Ready to merge is Jamie's confirmation that it was done.