Phase 24: Design System v2 — analytics-style UI (0.30.0)

- New /api/dashboard/funnel (5-stage stack health, 30s TTL cache) and
  /api/dashboard/summary (containers, daily uptime jsonl, ops activity)
- Token system (tokens.css + Tailwind sp-* aliases); legacy bg/card/accent
  remapped onto the tokens; Schibsted Grotesk bundled via fontsource
- TopNav pill navigation + AppShell replace the sidebar layout (off-canvas
  drawer below 1024px); central display-weight page titles
- Dashboard redesign: FunnelChart (gradient/hatch SVG waterfall), container
  count card with per-host bars + Insights chip, UptimeChart, OpsGrid,
  AiPromptBar; 30/7-day range selector; host sections retained below
- Stacks page honours ?q= / ?filter= deep links + new status-filter select

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-10 10:54:06 +00:00
co-authored by Claude Fable 5
parent 6464e0677c
commit 34cb215266
37 changed files with 1551 additions and 222 deletions
+32 -3
View File
@@ -1,5 +1,5 @@
import { useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { Link, useSearchParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { Plus, Search, HardDrive } from "lucide-react";
import { Button, Input } from "@/components/ui";
@@ -13,11 +13,23 @@ import { useAuthStore } from "@/store/auth";
import { useStackActions } from "@/hooks/useStackActions";
type SortKey = "name" | "status" | "updated";
type StatusFilter = "all" | "running" | "stopped" | "attention";
const STATUS_FILTERS: Record<string, StatusFilter> = {
running: "running",
stopped: "stopped",
attention: "attention",
};
export function Stacks() {
const isAdmin = useAuthStore((s) => s.user?.role === "admin");
const { busyId, start, stop, restart } = useStackActions();
const [q, setQ] = useState("");
// The dashboard explore bar deep-links here with ?q= / ?filter=.
const [params] = useSearchParams();
const [q, setQ] = useState(params.get("q") ?? "");
const [statusFilter, setStatusFilter] = useState<StatusFilter>(
STATUS_FILTERS[params.get("filter") ?? ""] ?? "all"
);
const [sort, setSort] = useState<SortKey>("name");
const { data, isLoading } = useQuery({
@@ -50,13 +62,20 @@ export function Stacks() {
s.name.toLowerCase().includes(q.toLowerCase()) ||
s.id.toLowerCase().includes(q.toLowerCase())
);
if (statusFilter !== "all") {
list = list.filter((s) =>
statusFilter === "attention"
? s.status === "error" || s.status === "partial"
: s.status === statusFilter
);
}
list = [...list].sort((a, b) => {
if (sort === "name") return a.name.localeCompare(b.name);
if (sort === "status") return a.status.localeCompare(b.status);
return b.updated_at.localeCompare(a.updated_at);
});
return list;
}, [data, q, sort]);
}, [data, q, statusFilter, sort]);
return (
<div className="space-y-4">
@@ -70,6 +89,16 @@ export function Stacks() {
onChange={(e) => setQ(e.target.value)}
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as StatusFilter)}
className="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-800"
>
<option value="all">All statuses</option>
<option value="running">Running</option>
<option value="stopped">Stopped</option>
<option value="attention">Needs attention</option>
</select>
<select
value={sort}
onChange={(e) => setSort(e.target.value as SortKey)}