115 lines
4.0 KiB
TypeScript
115 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { usePoll } from "@/lib/use-poll";
|
|
import type { OverviewStats } from "@/lib/types";
|
|
import { StatCard } from "@/components/stat-card";
|
|
import { ActivityFeed } from "@/components/activity-feed";
|
|
import { JellyfinCard } from "@/components/jellyfin-card";
|
|
import { OllamaCard } from "@/components/ollama-card";
|
|
import { HostCard } from "@/components/host-card";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
function SectionHeading({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<div className="flex items-center gap-3">
|
|
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground/70">
|
|
{children}
|
|
</h2>
|
|
<div className="flex-1 h-px bg-gradient-to-r from-white/[0.06] to-transparent" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function DashboardPage() {
|
|
const { data } = usePoll<OverviewStats>("/api/stats/overview", 60000);
|
|
|
|
// Handle both API field name variants
|
|
const endpoints = data?.containers?.endpoints || data?.containers?.by_endpoint || {};
|
|
const rawEmail = data?.emails_today ?? data?.email_today ?? 0;
|
|
const emailCount = typeof rawEmail === "object" && rawEmail !== null ? (rawEmail as Record<string, number>).total ?? 0 : rawEmail;
|
|
const alertCount = data?.alerts ?? data?.unhealthy_count ?? 0;
|
|
const running = data?.containers?.running ?? Object.values(endpoints).reduce((s, e) => s + (e.running || 0), 0);
|
|
const hostsOnline = data?.hosts_online ?? Object.values(endpoints).filter(e => !e.error).length;
|
|
const gpuPct = data?.gpu?.utilization_pct;
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Row 1: Stat Cards */}
|
|
<SectionHeading>Overview</SectionHeading>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-5">
|
|
<StatCard
|
|
label="Containers"
|
|
value={data ? `${running}/${data.containers.total}` : "\u2014"}
|
|
sub={data ? "running / total" : undefined}
|
|
/>
|
|
<StatCard
|
|
label="Hosts Online"
|
|
value={data ? hostsOnline : "\u2014"}
|
|
sub="endpoints"
|
|
/>
|
|
<StatCard
|
|
label="GPU"
|
|
value={
|
|
data?.gpu?.available
|
|
? `${gpuPct ?? 0}%`
|
|
: "\u2014"
|
|
}
|
|
sub={data?.gpu?.name ?? "unavailable"}
|
|
pct={data?.gpu?.available ? gpuPct : undefined}
|
|
/>
|
|
<StatCard
|
|
label="Emails Today"
|
|
value={data ? emailCount : "\u2014"}
|
|
sub="processed"
|
|
/>
|
|
<StatCard
|
|
label="Alerts"
|
|
value={data ? alertCount : "\u2014"}
|
|
sub="active"
|
|
/>
|
|
</div>
|
|
|
|
{/* Row 2: Activity + Jellyfin + Ollama */}
|
|
<SectionHeading>Live</SectionHeading>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-5">
|
|
<ActivityFeed />
|
|
<JellyfinCard />
|
|
<OllamaCard />
|
|
</div>
|
|
|
|
{/* Row 3: Hosts */}
|
|
<SectionHeading>Infrastructure</SectionHeading>
|
|
<Card className="overflow-hidden relative">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">Hosts</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
|
|
{data
|
|
? Object.entries(endpoints).map(
|
|
([name, info]) => (
|
|
<HostCard
|
|
key={name}
|
|
name={name}
|
|
running={info.running}
|
|
total={info.total}
|
|
error={info.error}
|
|
/>
|
|
)
|
|
)
|
|
: Array.from({ length: 5 }).map((_, i) => (
|
|
<Card key={i}>
|
|
<CardContent className="pt-3 pb-3 px-4">
|
|
<p className="text-xs text-muted-foreground">
|
|
Loading...
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|