244 lines
9.7 KiB
TypeScript
244 lines
9.7 KiB
TypeScript
"use client";
|
|
|
|
import { usePoll } from "@/lib/use-poll";
|
|
import type { EmailStats } 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";
|
|
|
|
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<string, string> = {
|
|
newsletters: "bg-blue-500/15 border-blue-500/25 text-blue-300",
|
|
promotions: "bg-amber-500/15 border-amber-500/25 text-amber-300",
|
|
social: "bg-purple-500/15 border-purple-500/25 text-purple-300",
|
|
finance: "bg-green-500/15 border-green-500/25 text-green-300",
|
|
receipts: "bg-emerald-500/15 border-emerald-500/25 text-emerald-300",
|
|
travel: "bg-cyan-500/15 border-cyan-500/25 text-cyan-300",
|
|
orders: "bg-orange-500/15 border-orange-500/25 text-orange-300",
|
|
personal: "bg-pink-500/15 border-pink-500/25 text-pink-300",
|
|
work: "bg-indigo-500/15 border-indigo-500/25 text-indigo-300",
|
|
updates: "bg-teal-500/15 border-teal-500/25 text-teal-300",
|
|
spam: "bg-red-500/15 border-red-500/25 text-red-300",
|
|
};
|
|
|
|
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";
|
|
}
|
|
|
|
export default function AutomationsPage() {
|
|
const { data: emails } = usePoll<EmailStats>("/api/automations/email", 60000);
|
|
const { data: backups } = usePoll<Record<string, unknown>>("/api/automations/backup", 120000);
|
|
const { data: drift } = usePoll<Record<string, unknown>>("/api/automations/drift", 120000);
|
|
const { data: restartsData } = usePoll<{ entries: StackRestart[] }>("/api/automations/restarts", 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<string, unknown>).today ?? (acct as Record<string, unknown>).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;
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<h1 className="text-lg font-semibold">Automations</h1>
|
|
|
|
{/* Top: Big stats row */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<StatCard
|
|
label="Emails Today"
|
|
value={emails ? totalEmailsToday : "\u2014"}
|
|
color="blue"
|
|
sub="classified"
|
|
/>
|
|
<StatCard
|
|
label="Backup Status"
|
|
value={backups ? (backupOk ? "OK" : "FAIL") : "\u2014"}
|
|
color={backupOk || !backups ? "green" : "amber"}
|
|
sub={backups?.has_errors ? "errors detected" : "all good"}
|
|
/>
|
|
<StatCard
|
|
label="Config Drift"
|
|
value={drift ? (driftClean ? "0" : driftStatus) : "\u2014"}
|
|
color={driftClean || !drift ? "emerald" : "amber"}
|
|
sub={drift ? String(drift.last_result ?? "no scan yet") : "loading"}
|
|
/>
|
|
<StatCard
|
|
label="Restarts"
|
|
value={restartsData ? restartCount : "\u2014"}
|
|
color={restartCount === 0 || !restartsData ? "violet" : "amber"}
|
|
sub="container restarts"
|
|
/>
|
|
</div>
|
|
|
|
{/* Middle: Email Organizers */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-base font-semibold">Email Organizers</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{!emails ? (
|
|
<p className="text-sm text-muted-foreground">Loading...</p>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{emails.accounts.map((acct: Record<string, unknown>) => {
|
|
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<string, number>;
|
|
return (
|
|
<div key={name} className="flex items-center gap-6 rounded-xl px-4 py-3 bg-white/[0.02] border border-white/[0.04] hover:bg-white/[0.04] transition-colors">
|
|
<div className="shrink-0 min-w-[140px]">
|
|
<p className="text-base font-medium text-foreground">{name}</p>
|
|
<p className="text-xs text-muted-foreground/60 mt-0.5">today</p>
|
|
</div>
|
|
<div className="shrink-0">
|
|
<p className="text-3xl font-bold text-foreground">{today}</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2 flex-1">
|
|
{Object.entries(cats).map(([cat, count]) => (
|
|
<Badge
|
|
key={cat}
|
|
variant="secondary"
|
|
className={`text-xs px-2.5 py-0.5 border ${getCategoryClass(cat)}`}
|
|
>
|
|
{cat}: {count}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Bottom: System Health — 2 columns */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
{/* Backup Details */}
|
|
<Card>
|
|
<CardHeader className="pb-2 flex flex-row items-center justify-between">
|
|
<CardTitle className="text-base font-semibold">Backup Details</CardTitle>
|
|
{backups && (
|
|
<StatusBadge
|
|
color={backupOk ? "green" : "red"}
|
|
label={String(backups.status ?? "unknown")}
|
|
/>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent>
|
|
{!backups ? (
|
|
<p className="text-sm text-muted-foreground">Loading...</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{backups.has_errors ? (
|
|
<p className="text-sm text-red-400 font-medium">Errors detected in backup</p>
|
|
) : null}
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-muted-foreground/70">Log entries today</span>
|
|
<span className="text-foreground font-medium">{String(backups.entries ?? 0)}</span>
|
|
</div>
|
|
{backups.last_run ? (
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-muted-foreground/70">Last run</span>
|
|
<span className="text-foreground">{String(backups.last_run)}</span>
|
|
</div>
|
|
) : null}
|
|
{backups.email_count != null ? (
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-muted-foreground/70">Emails backed up</span>
|
|
<span className="text-foreground font-medium">{String(backups.email_count)}</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Config Drift + Restarts */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-base font-semibold">Config Drift & Restarts</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Drift */}
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<p className="text-xs uppercase tracking-wider text-muted-foreground/70 font-medium">Config Drift</p>
|
|
{drift && (
|
|
<StatusBadge
|
|
color={driftClean ? "green" : "amber"}
|
|
label={driftStatus}
|
|
/>
|
|
)}
|
|
</div>
|
|
{drift && (
|
|
<p className="text-sm text-muted-foreground">
|
|
{String(drift.last_result ?? "No scan results yet")}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Restarts */}
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wider text-muted-foreground/70 font-medium mb-2">Recent Restarts</p>
|
|
{restarts.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground/60">No recent restarts</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{restarts.map((r, i) => (
|
|
<div key={i} className="flex items-center justify-between text-sm">
|
|
<span className="text-foreground font-medium">{r.stack}</span>
|
|
<div className="flex items-center gap-2">
|
|
<StatusBadge
|
|
color={r.status === "success" ? "green" : "red"}
|
|
label={r.status}
|
|
/>
|
|
<span className="text-xs text-muted-foreground/70">
|
|
{new Date(r.timestamp).toLocaleTimeString("en-US", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
})}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|