"use client"; import React from "react"; import { useSSE } from "@/lib/use-sse"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import type { ActivityEvent } from "@/lib/types"; const green = { bg: "#22c55e", shadow: "0 0 8px rgba(34, 197, 94, 0.4)" }; const blue = { bg: "#3b82f6", shadow: "0 0 8px rgba(59, 130, 246, 0.4)" }; const amber = { bg: "#f59e0b", shadow: "0 0 8px rgba(245, 158, 11, 0.4)" }; const red = { bg: "#ef4444", shadow: "0 0 8px rgba(239, 68, 68, 0.4)" }; const purple = { bg: "#a855f7", shadow: "0 0 8px rgba(168, 85, 247, 0.4)" }; const typeColors: Record = { stack_healthy: green, backup_result: green, drift_clean: green, cron_complete: green, disk_scan_complete: green, email_classified: blue, email_classifying: blue, email_cached: blue, start: blue, receipt_extracted: amber, container_restarted: amber, restart_analysis: amber, container_unhealthy: red, drift_found: red, disk_warning: red, error: red, changelog_generated: purple, changelog_commits: purple, pr_reviewed: purple, }; const defaultDot = { bg: "#6b7280", shadow: "none" }; function formatTime(ts: string) { try { return new Date(ts).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", }); } catch { return ts; } } const feedCategoryColors: Record = { receipts: "text-amber-400", newsletters: "text-blue-400", accounts: "text-violet-400", spam: "text-red-400", personal: "text-green-400", finance: "text-emerald-400", work: "text-cyan-400", promotions: "text-amber-400", social: "text-purple-400", }; function getCategoryColor(cat: string): string { const lower = cat.toLowerCase(); for (const [key, cls] of Object.entries(feedCategoryColors)) { if (lower.includes(key)) return cls; } return "text-foreground"; } function EventMessage({ event }: { event: ActivityEvent }): React.ReactElement { switch (event.type) { case "email_classified": { const cat = String(event.category ?? "?"); return ( Classified as {cat} {event.label ? ` - ${String(event.label)}` : ""} ); } case "email_classifying": return [{String(event.progress ?? "?")}] Classifying: {String(event.subject ?? "?")}; case "email_cached": { const cat = String(event.category ?? "?"); return ( Cached: {String(event.subject ?? "?")} → {cat} ); } case "receipt_extracted": return ( Receipt: {String(event.vendor ?? "?")} ${String(event.amount ?? "?")} ); case "container_restarted": return ( Restarted {String(event.container)} on {String(event.endpoint)} ); case "container_unhealthy": return event.container ? ( Unhealthy: {String(event.container)} on {String(event.endpoint)} ) : ( {String(event.raw ?? "Unhealthy container detected")} ); case "backup_result": return Backup: {String(event.status ?? "?")}; case "drift_clean": return Config drift check: all clean; case "drift_found": return Config drift: {String(event.drifts ?? "?")} drifts in {String(event.services ?? "?")} services; case "cron_complete": return Automation run completed; case "stack_healthy": return All containers healthy; case "disk_warning": return Disk warning: {String(event.days)} days remaining; case "disk_scan_complete": return Disk scan: {String(event.count)} filesystems checked; case "pr_reviewed": return AI reviewed PR #{String(event.pr)}; case "changelog_generated": return Changelog: {String(event.commits)} commits; case "changelog_commits": return {String(event.count)} new commits found; case "restart_analysis": return ( LLM says {String(event.decision)} for {String(event.container)} ); default: { if (typeof event.raw === "string") { const cleaned = event.raw.replace(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},?\d*\s+\S+\s+/, ""); return {cleaned}; } return {event.type} from {event.source}; } } } export function ActivityFeed() { const events = useSSE("/api/activity"); return ( Activity Feed LIVE {events.length === 0 && (

Waiting for events...

)}
{events.map((event, i) => (

{formatTime(event.timestamp)} · {event.source}

))}
); }