import { Rocket, RefreshCw, CircleStop, AlertTriangle, Clock, Activity, ShieldCheck } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; import { formatTimeAgo, formatAgeShort } from '@/lib/relativeTime'; import { VERB_LABELS, type ActionVerb } from '@/context/DeployFeedbackContext'; import type { SidebarActivitySummary } from './useSidebarActivitySummary'; export type SidebarActivityAction = | { kind: 'open-stack-notification'; summary: Extract } | { kind: 'open-auto-updates' } | { kind: 'open-activity' } | { kind: 'noop' }; interface SidebarActivityTickerProps { summary: SidebarActivitySummary; onAction: (action: SidebarActivityAction) => void; } interface RenderConfig { dotClass: string; pulse: boolean; Icon: LucideIcon | null; iconClass: string; primary: React.ReactNode; kicker: string; action: SidebarActivityAction; } const VERB_ICON: Record = { deploy: Rocket, install: Rocket, update: RefreshCw, restart: RefreshCw, down: CircleStop, stop: CircleStop, scan: ShieldCheck, }; function formatClockHHMM(unixSecs: number): string { const d = new Date(unixSecs * 1000); return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; } function buildConfig(summary: SidebarActivitySummary): RenderConfig { switch (summary.kind) { case 'active-op': { const elapsed = formatAgeShort(Date.now() - summary.startedAt); return { dotClass: 'bg-brand shadow-[0_0_6px_var(--brand)]', pulse: true, Icon: VERB_ICON[summary.action], iconClass: 'text-brand', primary: ( {VERB_LABELS[summary.action].present} {summary.stackName} · {elapsed} ), kicker: 'LIVE · STREAMING', action: { kind: 'noop' }, }; } case 'failure': { const stack = summary.notif.stack_name ?? 'unknown'; return { dotClass: 'bg-destructive shadow-[0_0_6px_var(--destructive)]', pulse: false, Icon: AlertTriangle, iconClass: 'text-destructive', primary: ( Failed · {stack} · {formatTimeAgo(summary.notif.timestamp * 1000)} ), kicker: 'ALERT · VIEW LOGS →', action: { kind: 'open-stack-notification', summary }, }; } case 'automation': { const nextLabel = formatClockHHMM(summary.nextRunAt); return { dotClass: 'bg-warning shadow-[0_0_6px_var(--warning)]', pulse: false, Icon: Clock, iconClass: 'text-warning', primary: ( Auto-update · next run {nextLabel} ), kicker: 'AUTOMATION · OPEN SCHEDULE →', action: { kind: 'open-auto-updates' }, }; } case 'recent-event': { return { dotClass: 'bg-brand shadow-[0_0_6px_var(--brand)]', pulse: false, Icon: Activity, iconClass: 'text-brand', primary: ( {summary.notif.stack_name} · {summary.notif.message} · {formatTimeAgo(summary.notif.timestamp * 1000)} ), kicker: 'LIVE · VIEW STACK →', action: { kind: 'open-stack-notification', summary }, }; } case 'disconnected': { return { dotClass: 'bg-warning', pulse: false, Icon: null, iconClass: '', primary: Notifications reconnecting, kicker: 'LIVE · NOTIFICATIONS PAUSED', action: { kind: 'noop' }, }; } case 'quiet-live': { return { dotClass: 'bg-success shadow-[0_0_6px_var(--success)]', pulse: false, Icon: null, iconClass: '', primary: Live · no stack changes in 1h, kicker: 'LIVE · OPEN ACTIVITY →', action: { kind: 'open-activity' }, }; } } // Exhaustiveness guard: any new SidebarActivitySummary variant added later // without a matching case will fail to assign to `never` and TS will // surface the gap at build time. const _exhaustive: never = summary; throw new Error(`Unhandled summary kind: ${JSON.stringify(_exhaustive)}`); } export function SidebarActivityTicker({ summary, onAction }: SidebarActivityTickerProps) { const config = buildConfig(summary); const { Icon } = config; const isClickable = config.action.kind !== 'noop'; return ( ); }