Replace the analytics-style dashboard (stack-health funnel, uptime %, operations/day grid, AI pill) with an attention-driven fleet cockpit: - New /api/dashboard/fleet endpoint: server-side fan-out across the local host and every agent into one payload — a prioritized "needs attention" list, headline KPIs, an honest stack-status breakdown and a per-host resource rollup. Each agent uses its own DB session so the fan-out is concurrency-safe; failures degrade to "offline" instead of stalling. - New frontend: AttentionStrip, FleetKpiRow, StackStatusBar and HostResourceTable; Dashboard.tsx rewritten around them. - Remove the funnel/summary endpoints, the uptime sampler loop and the ops-activity machinery; delete the now-unused chart components. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
3.0 KiB
TypeScript
102 lines
3.0 KiB
TypeScript
import { Link } from "react-router-dom";
|
|
import {
|
|
AlertTriangle,
|
|
AlertCircle,
|
|
CheckCircle2,
|
|
ServerOff,
|
|
HeartPulse,
|
|
ArrowUpCircle,
|
|
HardDrive,
|
|
MemoryStick,
|
|
Archive,
|
|
ChevronRight,
|
|
} from "lucide-react";
|
|
import type { AttentionItem } from "@/api/dashboard";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const KIND_ICON: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
agent_offline: ServerOff,
|
|
unhealthy: HeartPulse,
|
|
stack_error: AlertTriangle,
|
|
stack_partial: AlertCircle,
|
|
updates: ArrowUpCircle,
|
|
disk_pressure: HardDrive,
|
|
mem_pressure: MemoryStick,
|
|
backup_failed: Archive,
|
|
backup_overdue: Archive,
|
|
};
|
|
|
|
export function AttentionStrip({
|
|
items,
|
|
loading,
|
|
}: {
|
|
items?: AttentionItem[];
|
|
loading: boolean;
|
|
}) {
|
|
if (loading) {
|
|
return (
|
|
<div className="sp-card p-4">
|
|
<div className="sp-skeleton h-5 w-40" />
|
|
<div className="sp-skeleton mt-3 h-10" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!items || items.length === 0) {
|
|
return (
|
|
<div className="flex items-center gap-3 rounded-card border border-sp-green/40 bg-sp-green/10 px-4 py-3 text-sp-green">
|
|
<CheckCircle2 className="h-5 w-5 shrink-0" />
|
|
<p className="text-sm font-medium">All systems healthy — nothing needs your attention.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const errors = items.filter((i) => i.severity === "error").length;
|
|
|
|
return (
|
|
<div className="sp-card overflow-hidden p-0">
|
|
<div className="flex items-center justify-between border-b border-sp-border px-4 py-2.5">
|
|
<h2 className="sp-label">Needs attention</h2>
|
|
<span
|
|
className={cn(
|
|
"rounded-pill px-2 py-0.5 text-xs font-semibold",
|
|
errors > 0
|
|
? "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300"
|
|
: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300"
|
|
)}
|
|
>
|
|
{items.length}
|
|
</span>
|
|
</div>
|
|
<ul className="divide-y divide-sp-border">
|
|
{items.map((item, i) => {
|
|
const Icon = KIND_ICON[item.kind] ?? AlertCircle;
|
|
const isError = item.severity === "error";
|
|
return (
|
|
<li key={`${item.kind}-${item.host}-${i}`}>
|
|
<Link
|
|
to={item.link}
|
|
className="flex items-center gap-3 px-4 py-2.5 transition-colors hover:bg-sp-surface-2"
|
|
>
|
|
<Icon
|
|
className={cn(
|
|
"h-4 w-4 shrink-0",
|
|
isError ? "text-red-600 dark:text-red-400" : "text-sp-amber"
|
|
)}
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm font-medium text-sp-text-1">{item.title}</p>
|
|
{item.detail && (
|
|
<p className="truncate text-xs text-sp-text-3">{item.detail}</p>
|
|
)}
|
|
</div>
|
|
<ChevronRight className="h-4 w-4 shrink-0 text-sp-text-3" />
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|