import { Activity, AlertTriangle, BadgeCheck, Bot, Cpu, Database, Film, Flag, HardDrive, MemoryStick, Radio, Smile, Sticker, Users, UsersRound } from "lucide-react"; import { type ReactNode, useEffect, useState } from "react"; import { api } from "../api"; import { Alert } from "../components/ui"; import type { Navigate } from "../routing"; import { formatBytes, formatQuantity } from "../lib/format"; import type { DashboardResponse } from "../types"; export function Dashboard({ navigate }: { navigate: Navigate }) { const [data, setData] = useState(null); const [error, setError] = useState(""); useEffect(() => { let cancelled = false; async function load() { try { const res = await api.dashboard(); if (!cancelled) setData(res); } catch (err) { if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load dashboard"); } } void load(); // Refreshed periodically rather than once: counts and host load are the // kind of numbers an operator leaves this page open to watch. const timer = window.setInterval(() => void load(), 15000); return () => { cancelled = true; window.clearInterval(timer); }; }, []); const counts = data?.counts; const storage = data?.storage; const host = data?.host; return (
{error && {error}}
} label="Pending reports" value={counts ? formatQuantity(String(counts.PendingReports)) : "…"} tone={counts && counts.PendingReports > 0 ? "warn" : "good"} href="/moderation" navigate={navigate} /> } label="Verification requests" value={counts ? formatQuantity(String(counts.PendingVerifications)) : "…"} tone={counts && counts.PendingVerifications > 0 ? "warn" : "good"} href="/verification" navigate={navigate} />
} label="Users" value={counts ? formatQuantity(String(counts.Users)) : "…"} href="/accounts" navigate={navigate} /> } label="Online now" value={counts ? formatQuantity(String(counts.OnlineUsers)) : "…"} sub="last 5 min" href="/accounts" navigate={navigate} /> } label="Bots" value={counts ? formatQuantity(String(counts.Bots)) : "…"} href="/bots" navigate={navigate} /> } label="Channels" value={counts ? formatQuantity(String(counts.BroadcastChannels)) : "…"} href="/channels" navigate={navigate} /> } label="Supergroups" value={counts ? formatQuantity(String(counts.Supergroups)) : "…"} href="/channels" navigate={navigate} />
} label="Sticker packs" value={counts ? formatQuantity(String(counts.StickerSets)) : "…"} href="/stickers" navigate={navigate} /> } label="Emoji packs" value={counts ? formatQuantity(String(counts.EmojiSets)) : "…"} href="/emoji" navigate={navigate} /> } label="GIFs" value={counts ? formatQuantity(String(counts.Gifs)) : "…"} sub="saved by users" href="/gif-catalog" navigate={navigate} /> } label="Media storage used" value={storage ? formatBytes(storage.PhysicalBytes) : "…"} sub={storage ? `${storage.BackendKind} backend` : undefined} href="/storage" navigate={navigate} />
} label="CPU load" percent={host?.Ready ? host.CPUPercent : undefined} valueText={host?.Ready ? `${host.CPUPercent.toFixed(0)}%` : "…"} /> } label="RAM used" percent={host?.Ready && host.MemTotalBytes > 0 ? (host.MemUsedBytes / host.MemTotalBytes) * 100 : undefined} valueText={host?.Ready ? formatBytes(String(host.MemUsedBytes)) : "…"} sub={host?.Ready ? `of ${formatBytes(String(host.MemTotalBytes))}` : undefined} /> } label="Disk free" percent={ host?.Ready && host.DiskReady && host.DiskTotalBytes > 0 ? ((host.DiskTotalBytes - host.DiskFreeBytes) / host.DiskTotalBytes) * 100 : undefined } valueText={host?.Ready && host.DiskReady ? formatBytes(String(host.DiskFreeBytes)) : "…"} sub={host?.Ready && host.DiskReady ? `of ${formatBytes(String(host.DiskTotalBytes))}` : "no reading yet"} warnAbove={85} />
); } function Section({ title, hint, children }: { title: string; hint?: string; children: ReactNode }) { return (
{title} {hint && {hint}}
{children}
); } type Tone = "neutral" | "good" | "warn" | "danger"; function StatTile({ icon, label, value, sub, tone = "neutral", href, navigate }: { icon: ReactNode; label: string; value: string; sub?: string; tone?: Tone; href?: string; navigate?: Navigate; }) { const toneClass = tone === "neutral" ? "" : ` ${tone}`; const body = ( <>
{icon} {tone === "warn" && }
{value}
{label}
{sub &&
{sub}
} ); if (href && navigate) { return ( { event.preventDefault(); navigate(href); }} > {body} ); } return
{body}
; } // UsageTile renders a host metric with a fill bar instead of a click target -- // there's no page a CPU/RAM/disk reading opens to, unlike every entity/queue // tile above. function UsageTile({ icon, label, percent, valueText, sub, warnAbove = 90 }: { icon: ReactNode; label: string; percent?: number; valueText: string; sub?: string; warnAbove?: number; }) { const clamped = percent === undefined ? 0 : Math.max(0, Math.min(100, percent)); const tone: Tone = percent === undefined ? "neutral" : percent >= warnAbove ? "danger" : percent >= warnAbove - 15 ? "warn" : "neutral"; const toneClass = tone === "neutral" ? "" : ` ${tone}`; return (
{icon}
{valueText}
{label}
{sub &&
{sub}
}
); }