Sanitized mirror from private repository - 2026-04-05 10:36:59 UTC
This commit is contained in:
104
dashboard/ui/app/page.tsx
Normal file
104
dashboard/ui/app/page.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
"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 { CalendarCard } from "@/components/calendar-card";
|
||||
import { HostRow } from "@/components/host-card";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
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;
|
||||
const totalHosts = Object.keys(endpoints).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Row 1: Stats */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
<StatCard
|
||||
label="Containers"
|
||||
value={data ? `${running}/${data.containers.total}` : "\u2014"}
|
||||
color="blue"
|
||||
sub={data ? "running / total" : undefined}
|
||||
/>
|
||||
<StatCard
|
||||
label="Hosts Online"
|
||||
value={data ? hostsOnline : "\u2014"}
|
||||
color="green"
|
||||
sub="endpoints"
|
||||
/>
|
||||
<StatCard
|
||||
label="GPU — RTX 5090"
|
||||
value={
|
||||
data?.gpu?.available
|
||||
? `${gpuPct ?? 0}%`
|
||||
: "\u2014"
|
||||
}
|
||||
color="violet"
|
||||
gauge={data?.gpu?.available ? gpuPct : undefined}
|
||||
sub={data?.gpu?.available ? `${data.gpu.temp_c ?? "—"}\u00b0C \u00b7 ${data.gpu.power_w ?? data.gpu.power_draw_w ?? "—"}W` : "unavailable"}
|
||||
/>
|
||||
<StatCard
|
||||
label="Emails Today"
|
||||
value={data ? emailCount : "\u2014"}
|
||||
color="amber"
|
||||
sub="processed"
|
||||
/>
|
||||
<StatCard
|
||||
label="Alerts"
|
||||
value={data ? alertCount : "\u2014"}
|
||||
color="emerald"
|
||||
sub="active"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Calendar + Activity Feed */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<CalendarCard />
|
||||
<ActivityFeed />
|
||||
</div>
|
||||
|
||||
{/* Row 3: Jellyfin + GPU + Hosts */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<JellyfinCard />
|
||||
<OllamaCard />
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Hosts {data ? `(${hostsOnline}/${totalHosts} online)` : ""}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data ? (
|
||||
<div className="divide-y divide-white/[0.06]">
|
||||
{Object.entries(endpoints).map(([name, info]) => (
|
||||
<HostRow
|
||||
key={name}
|
||||
name={name}
|
||||
running={info.running}
|
||||
total={info.total}
|
||||
error={info.error}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">Loading...</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user