"use client"; import { usePoll } from "@/lib/use-poll"; import type { EmailStats, AutomationTimelineEntry } from "@/lib/types"; import { StatCard } from "@/components/stat-card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { StatusBadge } from "@/components/status-badge"; import { CardSkeleton } from "@/components/skeleton"; import { EmptyState } from "@/components/empty-state"; interface BackupResult { host: string; status: string; last_run: string; size?: string; } interface DriftResult { stack: string; drifted: boolean; details?: string; } interface StackRestart { stack: string; status: string; timestamp: string; } const categoryColors: Record = { receipts: "bg-amber-500/20 border-amber-500/30 text-amber-400", newsletters: "bg-blue-500/20 border-blue-500/30 text-blue-400", accounts: "bg-violet-500/20 border-violet-500/30 text-violet-400", spam: "bg-red-500/20 border-red-500/30 text-red-400", personal: "bg-green-500/20 border-green-500/30 text-green-400", finance: "bg-emerald-500/20 border-emerald-500/30 text-emerald-400", work: "bg-cyan-500/20 border-cyan-500/30 text-cyan-400", promotions: "bg-amber-500/15 border-amber-500/25 text-amber-300", social: "bg-purple-500/20 border-purple-500/30 text-purple-400", travel: "bg-cyan-500/15 border-cyan-500/25 text-cyan-300", orders: "bg-orange-500/20 border-orange-500/30 text-orange-400", updates: "bg-teal-500/20 border-teal-500/30 text-teal-400", }; function getAccountColor(name: string): string { const lower = name.toLowerCase(); if (lower.includes("gmail") || lower.includes("lzbellina")) return "text-blue-400"; if (lower.includes("dvish")) return "text-amber-400"; if (lower.includes("proton") || lower.includes("admin")) return "text-violet-400"; return "text-foreground"; } function getAccountGradient(name: string): string { const lower = name.toLowerCase(); if (lower.includes("gmail") || lower.includes("lzbellina")) return "bg-gradient-to-b from-blue-300 to-blue-500 bg-clip-text text-transparent"; if (lower.includes("dvish")) return "bg-gradient-to-b from-amber-300 to-amber-500 bg-clip-text text-transparent"; if (lower.includes("proton") || lower.includes("admin")) return "bg-gradient-to-b from-violet-300 to-violet-500 bg-clip-text text-transparent"; return "text-foreground"; } function getCategoryClass(cat: string): string { const lower = cat.toLowerCase(); for (const [key, cls] of Object.entries(categoryColors)) { if (lower.includes(key)) return cls; } return "bg-white/[0.06] border-white/[0.08] text-muted-foreground"; } // Order matters — more specific keys must come BEFORE generic ones // ("digest" before "email" so "Email Digest" matches digest, not email) const expectedIntervals: [string, number][] = [ ["digest", 36 * 60 * 60 * 1000], // 36 hours (daily) ["changelog", 8 * 24 * 60 * 60 * 1000], // 8 days (weekly) ["predictor", 8 * 24 * 60 * 60 * 1000], // 8 days (weekly) ["validator", 36 * 60 * 60 * 1000], // 36 hours (daily) ["receipt", 36 * 60 * 60 * 1000], // 36 hours (daily) ["drift", 36 * 60 * 60 * 1000], // 36 hours (daily) ["backup", 36 * 60 * 60 * 1000], // 36 hours (daily) ["restart", 30 * 60 * 1000], // 30 min ["stack", 30 * 60 * 1000], // 30 min ["email", 2 * 60 * 60 * 1000], // 2 hours ["organizer", 2 * 60 * 60 * 1000], // 2 hours ]; function getExpectedInterval(name: string): number { const lower = name.toLowerCase(); for (const [key, interval] of expectedIntervals) { if (lower.includes(key)) return interval; } return 60 * 60 * 1000; // default 1 hour } function isOnSchedule(entry: AutomationTimelineEntry): boolean { if (!entry.last_run || !entry.exists) return false; const lastRun = new Date(entry.last_run).getTime(); const now = Date.now(); const expected = getExpectedInterval(entry.name); // Allow 2x the expected interval as grace return (now - lastRun) < expected * 2; } function formatRelativeTime(dateStr: string): string { try { const d = new Date(dateStr); const now = Date.now(); const diff = now - d.getTime(); if (diff < 60000) return "just now"; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return `${Math.floor(diff / 86400000)}d ago`; } catch { return dateStr; } } function formatTimeOnly(dateStr: string): string { try { return new Date(dateStr).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", }); } catch { return dateStr; } } export default function AutomationsPage() { const { data: emails } = usePoll("/api/automations/email", 60000); const { data: backups } = usePoll>("/api/automations/backup", 120000); const { data: drift } = usePoll>("/api/automations/drift", 120000); const { data: restartsData } = usePoll<{ entries: StackRestart[] }>("/api/automations/restarts", 60000); const { data: timeline } = usePoll("/api/automation-timeline", 60000); const restarts = restartsData?.entries ?? []; // Compute stats for top row const totalEmailsToday = emails?.accounts ? emails.accounts.reduce((sum, acct) => { const today = Number((acct as Record).today ?? (acct as Record).today_total ?? 0); return sum + today; }, 0) : 0; const backupOk = String(backups?.status ?? "unknown") === "ok"; const driftStatus = String(drift?.status ?? "unknown"); const driftClean = driftStatus === "clean" || driftStatus === "no_log"; const restartCount = restarts.length; // Sort timeline by most recent first const sortedTimeline = timeline ? [...timeline].sort((a, b) => { if (!a.last_run) return 1; if (!b.last_run) return -1; return new Date(b.last_run).getTime() - new Date(a.last_run).getTime(); }) : []; return (

Automations

{/* Top: Big stats row */}
{/* Color Legend */}
Legend: On schedule Overdue No log Email Accounts Receipts Spam
{/* Automation Timeline */} Automation Timeline {!timeline ? ( ) : sortedTimeline.length === 0 ? (

No automation data

) : (
{sortedTimeline.map((entry, i) => { const onSchedule = isOnSchedule(entry); return (
{entry.name}
{entry.last_run ? ( <> {formatTimeOnly(entry.last_run)} {formatRelativeTime(entry.last_run)} ) : ( never )}
); })}
)}
{/* Middle: Email Organizers */} Email Organizers {!emails ? ( ) : (
{emails.accounts.map((acct: Record) => { const name = String(acct.account ?? acct.name ?? "?"); const today = Number(acct.today ?? acct.today_total ?? 0); const cats = (acct.categories ?? acct.today_categories ?? {}) as Record; return (

{name}

today

{today}

{Object.entries(cats).map(([cat, count]) => ( {cat}: {count} ))}
); })}
)}
{/* Bottom: System Health -- 2 columns */}
{/* Backup Details */} Backup Details {backups && ( )} {!backups ? ( ) : (
{backups.has_errors ? (

Errors detected in backup

) : null}
Log entries today {String(backups.entries ?? 0)}
{backups.last_run ? (
Last run {String(backups.last_run)}
) : null} {backups.email_count != null ? (
Emails backed up {String(backups.email_count)}
) : null}
)}
{/* Config Drift + Restarts */} Config Drift & Restarts {/* Drift */}

Config Drift

{drift && ( )}
{drift && (

{String(drift.last_result ?? "No scan results yet")}

)}
{/* Restarts */}

Recent Restarts

{restarts.length === 0 ? ( ) : (
{restarts.map((r, i) => (
{r.stack}
{new Date(r.timestamp).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", })}
))}
)}
); }