From 336e37d0180af0de9fcb9b72980577027c9b24ec Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sun, 15 Feb 2026 05:09:33 -0500 Subject: [PATCH] feat: add activity notifications panel with session feed - Bell icon shows dot indicator for recent activity - Dropdown panel shows recent sessions with status icons - Links to session detail and sessions list page Co-Authored-By: Claude Opus 4.6 --- .../components/layout/NotificationsPanel.tsx | 105 ++++++++++++++++++ frontend/src/components/layout/TopBar.tsx | 7 +- 2 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/layout/NotificationsPanel.tsx diff --git a/frontend/src/components/layout/NotificationsPanel.tsx b/frontend/src/components/layout/NotificationsPanel.tsx new file mode 100644 index 00000000..df6c352a --- /dev/null +++ b/frontend/src/components/layout/NotificationsPanel.tsx @@ -0,0 +1,105 @@ +import { useState, useEffect, useRef } from 'react' +import { Link } from 'react-router-dom' +import { Bell, CheckCircle, Clock } from 'lucide-react' +import { sessionsApi } from '@/api/sessions' +import type { Session } from '@/types/session' + +function timeAgo(dateStr: string): string { + const diff = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000) + if (diff < 60) return 'just now' + if (diff < 3600) return `${Math.floor(diff / 60)}m ago` + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago` + return `${Math.floor(diff / 86400)}d ago` +} + +export function NotificationsPanel() { + const [open, setOpen] = useState(false) + const [sessions, setSessions] = useState([]) + const [hasNew, setHasNew] = useState(false) + const ref = useRef(null) + + useEffect(() => { + sessionsApi.list({ size: 8 }) + .then(data => { + setSessions(data) + // Mark as "new" if any session was updated in the last hour + const oneHourAgo = Date.now() - 3600000 + setHasNew(data.some(s => new Date(s.started_at).getTime() > oneHourAgo)) + }) + .catch(() => {}) + }, []) + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) + } + if (open) document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }, [open]) + + return ( +
+ + + {open && ( +
+
+

Activity

+ setOpen(false)} + className="text-[0.6875rem] text-muted-foreground hover:text-foreground" + > + View All + +
+ + {sessions.length === 0 ? ( +
+ No recent activity +
+ ) : ( +
+ {sessions.map(session => ( + setOpen(false)} + className="flex items-start gap-3 px-4 py-3 hover:bg-accent/50 transition-colors" + > +
+ {session.completed_at ? ( + + ) : ( + + )} +
+
+

+ {session.tree_snapshot?.name || 'Session'} +

+

+ {session.completed_at + ? `Completed ${timeAgo(session.completed_at)}` + : `Started ${timeAgo(session.started_at)}`} + {session.client_name && ` ยท ${session.client_name}`} +

+
+ + ))} +
+ )} +
+ )} +
+ ) +} diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx index 0cec8634..51411258 100644 --- a/frontend/src/components/layout/TopBar.tsx +++ b/frontend/src/components/layout/TopBar.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect, useCallback } from 'react' import { Link, useNavigate } from 'react-router-dom' -import { Search, Zap, Bell, LogOut, User, Shield, Settings } from 'lucide-react' +import { Search, Zap, LogOut, User, Shield, Settings } from 'lucide-react' import { useAuthStore } from '@/store/authStore' import { usePermissions } from '@/hooks/usePermissions' import { useWorkspaceStore } from '@/store/workspaceStore' @@ -8,6 +8,7 @@ import { getWorkspaceLabels } from '@/constants/workspaceLabels' import { BrandLogo } from '@/components/common/BrandLogo' import { CommandPalette } from './CommandPalette' import { QuickLaunch } from './QuickLaunch' +import { NotificationsPanel } from './NotificationsPanel' import { cn } from '@/lib/utils' export function TopBar() { @@ -103,9 +104,7 @@ export function TopBar() { > - + {/* User avatar & menu */}