Dashboard: rebuild into an operator cockpit (0.38.0)

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>
This commit is contained in:
menzelj
2026-06-24 11:04:57 +00:00
co-authored by Claude Opus 4.8
parent 5c46e40866
commit c830d28b65
15 changed files with 771 additions and 1067 deletions
@@ -0,0 +1,96 @@
import { Link } from "react-router-dom";
import { Server, Boxes, Container, HeartPulse, ArrowUpCircle, Archive } from "lucide-react";
import type { FleetKpis } from "@/api/dashboard";
import { cn } from "@/lib/utils";
type Tone = "default" | "warn" | "error";
function Kpi({
icon,
label,
value,
sub,
tone = "default",
to,
}: {
icon: React.ReactNode;
label: string;
value: React.ReactNode;
sub?: string;
tone?: Tone;
to?: string;
}) {
const toneText =
tone === "error"
? "text-red-600 dark:text-red-400"
: tone === "warn"
? "text-sp-amber"
: "text-sp-text-1";
const inner = (
<div className="sp-card flex h-full flex-col gap-1 p-4">
<div className="flex items-center gap-1.5 text-sp-text-3">
<span className="[&>svg]:h-3.5 [&>svg]:w-3.5">{icon}</span>
<span className="sp-label">{label}</span>
</div>
<p className={cn("sp-display text-3xl leading-none", toneText)}>{value}</p>
{sub && <p className="text-xs text-sp-text-3">{sub}</p>}
</div>
);
return to ? (
<Link to={to} className="block transition-transform hover:-translate-y-0.5">
{inner}
</Link>
) : (
inner
);
}
export function FleetKpiRow({ kpis }: { kpis: FleetKpis }) {
return (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
<Kpi
icon={<Server />}
label="Hosts"
value={`${kpis.hosts_online}/${kpis.hosts_total}`}
sub="online"
tone={kpis.hosts_online < kpis.hosts_total ? "error" : "default"}
/>
<Kpi
icon={<Boxes />}
label="Stacks"
value={`${kpis.stacks_running}/${kpis.stacks_total}`}
sub={kpis.stacks_partial > 0 ? `${kpis.stacks_partial} partial` : "running"}
tone={kpis.stacks_partial > 0 ? "warn" : "default"}
/>
<Kpi
icon={<Container />}
label="Containers"
value={`${kpis.containers_running}/${kpis.containers_total}`}
sub="running"
/>
<Kpi
icon={<HeartPulse />}
label="Unhealthy"
value={kpis.unhealthy}
sub={kpis.unhealthy > 0 ? "need a look" : "all healthy"}
tone={kpis.unhealthy > 0 ? "error" : "default"}
/>
<Kpi
icon={<ArrowUpCircle />}
label="Updates"
value={kpis.updates_available}
sub={kpis.updates_available > 0 ? "available" : "up to date"}
tone={kpis.updates_available > 0 ? "warn" : "default"}
to={kpis.updates_available > 0 ? "/images" : undefined}
/>
<Kpi
icon={<Archive />}
label="Backups"
value={kpis.backups_failing > 0 ? kpis.backups_failing : "OK"}
sub={kpis.backups_failing > 0 ? "failing/overdue" : "on schedule"}
tone={kpis.backups_failing > 0 ? "error" : "default"}
to={kpis.backups_failing > 0 ? "/settings" : undefined}
/>
</div>
);
}