You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Move "Recent activity" out of the dashboards and into one panel reachable from anywhere in the app.
Today ActivityFeed is a SectionCard rendered on all three dashboards (admin-dashboard.tsx, manager-dashboard.tsx, user-dashboard.tsx), fed by getMyRecentActivity and getRecentApplications. It's only visible while standing on the dashboard.
Scope decision: no unread state. The feed stays derived from existing data — no schema change, no lastSeenNotificationsAt, no badge, no mark-as-read. This ticket relocates and improves access to information the app already computes. Per-item read state can come later if it's ever wanted.
Where the trigger goes
There is no shared persistent header — app-shell.tsx is sidebar (desktop, md:flex) + MobileNav (mobile, md:hidden) + main. So the trigger needs a home in both. They're structurally parallel, which makes this clean:
Desktop — sidebar.tsx, the h-14 header bar. It currently holds only the logo link and has free space to its right. Add the trigger there with ml-auto.
Mobile — mobile-nav.tsx, the existing ml-auto cluster. That cluster already holds the hamburger SheetTrigger; the activity trigger goes immediately to its left, so the cluster reads [activity] [menu].
Both headers are h-14 with the same bg-sidebar border-sidebar-border treatment and the same logo block, so the control lands top-right on both breakpoints — one position to learn.
Rejected alternatives, and why:
Sidebar footer beside UserMenu — puts it bottom-left on desktop and top-right on mobile. Two different places for one control.
A NavList entry — nav items navigate to routes. A control that opens a drawer shouldn't sit among them pretending to be a destination.
The panel itself
Sheet with side="right".components/ui/sheet.tsx already exists, docs/DESIGN.md establishes Sheet as this app's drawer pattern, and one component works at every breakpoint. side="right" matters — MobileNav's menu sheet is side="left", so opposite sides keep the two drawers from reading as the same surface.
Use CONCEPT_ICONS.activity, not a bell. A bell promises unread counts this ticket deliberately doesn't have. The activity icon is already the one on the current SectionCard, so the vocabulary stays consistent — see Standardize Lucide Iconography Across The App #572.
Contents: the existing ActivityFeedList markup, largely as-is — status dot, text, relative time via LocalTime — with the same empty state. Grouping by day is nice if it falls out cheaply; it isn't the point.
Only render the trigger when signed in.identity is null for anonymous visitors and there's nothing to show.
Scope
1. Add the trigger in both sidebar.tsx and mobile-nav.tsx as described.
2. Build the sheet around the existing feed markup, extracted from activity-feed.tsx so the list renders identically in its new home.
3. Preserve the per-persona split. The feed already differs by role — getMyRecentActivity for applicants, getRecentApplications for reviewers — and reviewer content must stay inside buildApplicationScopeWhere, so a manager sees only their managed positions' activity. Do not collapse the two into one unscoped query.
4. Remove "Recent activity" from all three dashboards. Move it, don't duplicate it — two copies of one list is worse than today, and removing the card reclaims real dashboard space.
5. Server-rendered, opened client-side. Fetch on the server as the dashboards already do and pass items down through the shell; the sheet is a client leaf owning only open/close. No useEffect fetching.
Relationship to other tickets
Manager Daily Digest #549 (manager daily digest) — when scoping the email epic, an in-app "new since you were last here" surface was the alternative to a cron-driven digest email. This panel is the beginning of that surface, but without unread state it does not replace a digest: a manager still has no way to learn that something changed without opening the panel and reading it. Revisit Manager Daily Digest #549 once this lands — the gap it fills is "tell me", which this ticket deliberately does not.
Goal
Move "Recent activity" out of the dashboards and into one panel reachable from anywhere in the app.
Today
ActivityFeedis aSectionCardrendered on all three dashboards (admin-dashboard.tsx,manager-dashboard.tsx,user-dashboard.tsx), fed bygetMyRecentActivityandgetRecentApplications. It's only visible while standing on the dashboard.Scope decision: no unread state. The feed stays derived from existing data — no schema change, no
lastSeenNotificationsAt, no badge, no mark-as-read. This ticket relocates and improves access to information the app already computes. Per-item read state can come later if it's ever wanted.Where the trigger goes
There is no shared persistent header —
app-shell.tsxis sidebar (desktop,md:flex) +MobileNav(mobile,md:hidden) + main. So the trigger needs a home in both. They're structurally parallel, which makes this clean:sidebar.tsx, theh-14header bar. It currently holds only the logo link and has free space to its right. Add the trigger there withml-auto.mobile-nav.tsx, the existingml-autocluster. That cluster already holds the hamburgerSheetTrigger; the activity trigger goes immediately to its left, so the cluster reads[activity] [menu].Both headers are
h-14with the samebg-sidebar border-sidebar-bordertreatment and the same logo block, so the control lands top-right on both breakpoints — one position to learn.Rejected alternatives, and why:
UserMenu— puts it bottom-left on desktop and top-right on mobile. Two different places for one control.NavListentry — nav items navigate to routes. A control that opens a drawer shouldn't sit among them pretending to be a destination.The panel itself
Sheetwithside="right".components/ui/sheet.tsxalready exists,docs/DESIGN.mdestablishes Sheet as this app's drawer pattern, and one component works at every breakpoint.side="right"matters —MobileNav's menu sheet isside="left", so opposite sides keep the two drawers from reading as the same surface.CONCEPT_ICONS.activity, not a bell. A bell promises unread counts this ticket deliberately doesn't have. The activity icon is already the one on the currentSectionCard, so the vocabulary stays consistent — see Standardize Lucide Iconography Across The App #572.ActivityFeedListmarkup, largely as-is — status dot, text, relative time viaLocalTime— with the same empty state. Grouping by day is nice if it falls out cheaply; it isn't the point.identityis null for anonymous visitors and there's nothing to show.Scope
1. Add the trigger in both
sidebar.tsxandmobile-nav.tsxas described.2. Build the sheet around the existing feed markup, extracted from
activity-feed.tsxso the list renders identically in its new home.3. Preserve the per-persona split. The feed already differs by role —
getMyRecentActivityfor applicants,getRecentApplicationsfor reviewers — and reviewer content must stay insidebuildApplicationScopeWhere, so a manager sees only their managed positions' activity. Do not collapse the two into one unscoped query.4. Remove "Recent activity" from all three dashboards. Move it, don't duplicate it — two copies of one list is worse than today, and removing the card reclaims real dashboard space.
5. Server-rendered, opened client-side. Fetch on the server as the dashboards already do and pass items down through the shell; the sheet is a client leaf owning only open/close. No
useEffectfetching.Relationship to other tickets
CONCEPT_ICONSvocabulary.Non-goals
Notificationmodel and no schema change of any kind./notificationspage.Acceptance criteria
SectionCardEmptytreatment.docs/WORKFLOWS.mdupdated — the dashboard entries (AP-1, PM-1 and the admin equivalent) each describe the Recent activity card.npm run prettier:check,eslint:check,tsc:check,testall pass.Tests
tests/db/— the reviewer feed respects position scope; the applicant feed returns only the caller's own activity.