import { useCallback, useEffect, useState } from "react"; import { Link } from "wouter"; import { ArrowLeft, Pause, Play, RefreshCw, Trash2 } from "lucide-react"; import { api } from "@/api"; import { ChartCard, MetricChart, chartColor, type ChartRow } from "@/components/chart"; import type { Heartbeat, MonitorView } from "@/types"; import { MonitorDialog } from "@/components/MonitorDialog"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { t } from "@/i18n"; export function MonitorDetailPage({ id }: { id: string }) { const [m, setM] = useState(null); const [hbs, setHbs] = useState([]); const [hours, setHours] = useState(24); const [dlgOpen, setDlgOpen] = useState(false); const load = useCallback(async () => { const [{ data: mv }, { data: hb }] = await Promise.all([ api.GET("/api/monitors/{id}", { params: { path: { id } } }), api.GET("/api/monitors/{id}/heartbeats", { params: { path: { id }, query: { hours } } }), ]); if (mv) setM(mv); if (hb) setHbs(hb); }, [id, hours]); useEffect(() => { void load(); const t = setInterval(() => void load(), 30_000); return () => clearInterval(t); }, [load]); if (!m) return
{t("common.loading")}
; const rows: ChartRow[] = hbs.map((h) => ({ t: new Date(h.checkedAt ?? "").getTime(), ping: h.pingMs, })); return (
{t("nav.monitors")}

{m.name}

{m.status}

{m.type} · {m.url || m.hostname}{m.port ? `:${m.port}` : ""} · {t("monitors.everyInterval", { interval: m.intervalS ?? 0 })}

{[ [t("monitors.uptime24h"), `${m.stats?.uptime24h?.toFixed(2)}%`], [t("monitors.uptime30d"), `${m.stats?.uptime30d?.toFixed(2)}%`], [t("monitors.avgLatency"), m.stats?.avgPing24h ? `${m.stats.avgPing24h} ms` : "—"], [t("monitors.checks"), String((m.stats?.up ?? 0) + (m.stats?.down ?? 0))], ].map(([k, v]) => (
{k}
{v}
))}

{t("monitors.uptime")}

{[24, 168, 720].map((h) => ( ))}
} > `${Math.round(v)} ms`} spanMs={hours * 3600_000} />

{t("monitors.recentChecks")}

{[...hbs].reverse().slice(0, 40).map((h) => ( ))} {hbs.length === 0 && ( )}
{new Date(h.checkedAt ?? "").toLocaleString()} {h.status} {h.pingMs ? `${h.pingMs}ms` : "—"} {h.msg}
{t("monitors.noChecks")}
); } /** UptimeBar renders the check history as a strip of status-colored bars — * the window is split into fixed buckets; a bucket is "down" if any check in * it failed, so outages stay visible at long ranges. */ function UptimeBar({ hbs, hours }: { hbs: Heartbeat[]; hours: number }) { const BUCKETS = 90; const now = Date.now(); const span = hours * 3600_000; const start = now - span; const buckets: { status: "up" | "down" | "empty"; from: number; to: number; n: number }[] = []; for (let i = 0; i < BUCKETS; i++) { buckets.push({ status: "empty", from: start + (i * span) / BUCKETS, to: start + ((i + 1) * span) / BUCKETS, n: 0 }); } for (const h of hbs) { const t = new Date(h.checkedAt ?? "").getTime(); if (Number.isNaN(t) || t < start) continue; const i = Math.min(BUCKETS - 1, Math.floor(((t - start) / span) * BUCKETS)); const b = buckets[i]; if (!b) continue; b.n++; if (h.status === "down") b.status = "down"; else if (b.status === "empty") b.status = "up"; } const fmt = (t: number) => new Date(t).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); return (
{buckets.map((b, i) => (
))}
); }