This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { api, Monitor, MonitorStatus, Rollup } from "@/lib/api";
|
||||
import { Badge, Button, Card, CardHeader, CardTitle, Table, Thead, Tbody, Tr, Th, Td } from "@/components/ui";
|
||||
|
||||
function statusVariant(status: MonitorStatus) {
|
||||
switch (status) {
|
||||
case "up":
|
||||
return "success";
|
||||
case "down":
|
||||
return "danger";
|
||||
default:
|
||||
return "warning";
|
||||
}
|
||||
}
|
||||
|
||||
function uptimePct(rollups: Rollup[]): number {
|
||||
const checks = rollups.reduce((a, r) => a + r.checks, 0);
|
||||
const up = rollups.reduce((a, r) => a + r.up_count, 0);
|
||||
return checks > 0 ? (up / checks) * 100 : 0;
|
||||
}
|
||||
|
||||
function Heartbeat({ rollups }: { rollups: Rollup[] }) {
|
||||
const recent = rollups.slice(-48);
|
||||
return (
|
||||
<div className="flex items-end gap-0.5">
|
||||
{recent.map((r) => {
|
||||
const pct = r.checks > 0 ? (r.up_count / r.checks) * 100 : 0;
|
||||
const color = r.checks === 0 ? "bg-surface-2" : pct >= 99 ? "bg-success" : pct >= 80 ? "bg-warning" : "bg-danger";
|
||||
return (
|
||||
<div
|
||||
key={r.period_start}
|
||||
className={`h-8 w-1.5 rounded-sm ${color}`}
|
||||
title={`${new Date(r.period_start).toLocaleString()} — ${pct.toFixed(0)}% up`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{recent.length === 0 && <span className="text-xs text-text-secondary">No history yet.</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MonitorDetailPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const monitorId = params.id as string;
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
|
||||
const { data: monitor, isLoading } = useQuery({
|
||||
queryKey: ["monitors", monitorId],
|
||||
queryFn: () => api.getMonitor(monitorId),
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
|
||||
const { data: rollups } = useQuery({
|
||||
queryKey: ["monitors", monitorId, "uptime"],
|
||||
queryFn: () => api.getMonitorUptime(monitorId),
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
|
||||
const { data: incidents } = useQuery({
|
||||
queryKey: ["monitors", monitorId, "incidents"],
|
||||
queryFn: () => api.getMonitorIncidents(monitorId),
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
|
||||
const { mutate: deleteMonitor, isPending: isDeleting } = useMutation({
|
||||
mutationFn: () => api.deleteMonitor(monitorId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["monitors"] });
|
||||
router.push("/monitors");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: toggleEnabled } = useMutation({
|
||||
mutationFn: (enabled: boolean) => api.updateMonitor(monitorId, { enabled }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["monitors", monitorId] }),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-accent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!monitor) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="rounded-lg border border-danger/30 bg-danger/10 p-4 text-danger">Monitor not found.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const all = rollups ?? [];
|
||||
const last24 = all.slice(-24);
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="mb-6 flex items-start justify-between">
|
||||
<div>
|
||||
<Link href="/monitors" className="text-sm text-text-secondary hover:text-text-primary">
|
||||
← Monitors
|
||||
</Link>
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold text-text-primary">{monitor.name}</h1>
|
||||
<Badge variant={statusVariant(monitor.state.status)}>{monitor.state.status}</Badge>
|
||||
<Badge variant="neutral">{monitor.type}</Badge>
|
||||
{!monitor.enabled && <Badge variant="warning">disabled</Badge>}
|
||||
</div>
|
||||
{monitor.state.message && <p className="mt-1 text-sm text-text-secondary">{monitor.state.message}</p>}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="secondary" onClick={() => toggleEnabled(!monitor.enabled)}>
|
||||
{monitor.enabled ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
{!confirmDelete ? (
|
||||
<Button variant="danger" onClick={() => setConfirmDelete(true)}>
|
||||
Delete
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-danger">Are you sure?</span>
|
||||
<Button variant="danger" loading={isDeleting} onClick={() => deleteMonitor()}>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => setConfirmDelete(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<Card>
|
||||
<p className="text-xs text-text-secondary">Uptime (24h)</p>
|
||||
<p className="mt-1 text-2xl font-bold text-text-primary">{uptimePct(last24).toFixed(1)}%</p>
|
||||
</Card>
|
||||
<Card>
|
||||
<p className="text-xs text-text-secondary">Uptime (30d)</p>
|
||||
<p className="mt-1 text-2xl font-bold text-text-primary">{uptimePct(all).toFixed(1)}%</p>
|
||||
</Card>
|
||||
<Card>
|
||||
<p className="text-xs text-text-secondary">Latency</p>
|
||||
<p className="mt-1 text-2xl font-bold text-text-primary">{monitor.state.latency_ms}ms</p>
|
||||
</Card>
|
||||
<Card>
|
||||
<p className="text-xs text-text-secondary">Cert expiry</p>
|
||||
<p className="mt-1 text-sm font-medium text-text-primary">
|
||||
{monitor.state.cert_expiry_at ? new Date(monitor.state.cert_expiry_at).toLocaleDateString() : "—"}
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Heartbeat (last 48h)</CardTitle>
|
||||
</CardHeader>
|
||||
<Heartbeat rollups={all} />
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<Card padding={false}>
|
||||
<div className="border-b border-border px-6 py-4">
|
||||
<h2 className="text-lg font-semibold text-text-primary">Incidents</h2>
|
||||
</div>
|
||||
{!incidents || incidents.length === 0 ? (
|
||||
<div className="py-12 text-center text-sm text-text-secondary">No incidents recorded.</div>
|
||||
) : (
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Started</Th>
|
||||
<Th>Resolved</Th>
|
||||
<Th>Cause</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{incidents.map((inc) => (
|
||||
<Tr key={inc.incident_id}>
|
||||
<Td>
|
||||
<span className="text-xs text-text-secondary">{new Date(inc.started_at).toLocaleString()}</span>
|
||||
</Td>
|
||||
<Td>
|
||||
{inc.resolved_at ? (
|
||||
<span className="text-xs text-text-secondary">{new Date(inc.resolved_at).toLocaleString()}</span>
|
||||
) : (
|
||||
<Badge variant="danger">ongoing</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-xs text-text-primary">{inc.cause || "—"}</span>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<dl className="space-y-3 text-sm">
|
||||
<div>
|
||||
<dt className="text-text-secondary">Runner</dt>
|
||||
<dd className="mt-0.5 font-mono text-text-primary">{monitor.runner}</dd>
|
||||
</div>
|
||||
<div className="border-t border-border pt-3">
|
||||
<dt className="text-text-secondary">Interval</dt>
|
||||
<dd className="mt-0.5 text-text-primary">{monitor.interval_sec}s</dd>
|
||||
</div>
|
||||
<div className="border-t border-border pt-3">
|
||||
<dt className="text-text-secondary">Retries before down</dt>
|
||||
<dd className="mt-0.5 text-text-primary">{monitor.retries}</dd>
|
||||
</div>
|
||||
<div className="border-t border-border pt-3">
|
||||
<dt className="text-text-secondary">Target</dt>
|
||||
<dd className="mt-0.5 font-mono text-xs text-text-primary break-all">
|
||||
{monitor.target.url || `${monitor.target.host ?? ""}${monitor.target.port ? `:${monitor.target.port}` : ""}`}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { api, MonitorInput, MonitorType } from "@/lib/api";
|
||||
import { Button, Card } from "@/components/ui";
|
||||
|
||||
const inputClass =
|
||||
"w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30";
|
||||
|
||||
const labelClass = "mb-1.5 block text-sm font-medium text-text-secondary";
|
||||
|
||||
export default function NewMonitorPage() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [type, setType] = useState<MonitorType>("http");
|
||||
const [url, setUrl] = useState("");
|
||||
const [host, setHost] = useState("");
|
||||
const [port, setPort] = useState<number>(443);
|
||||
const [method, setMethod] = useState("GET");
|
||||
const [expectedStatus, setExpectedStatus] = useState<number>(200);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [tlsWarnDays, setTlsWarnDays] = useState<number>(14);
|
||||
const [intervalSec, setIntervalSec] = useState<number>(60);
|
||||
const [retries, setRetries] = useState<number>(1);
|
||||
const [runner, setRunner] = useState("server");
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
|
||||
const { data: servers } = useQuery({ queryKey: ["servers"], queryFn: () => api.listServers() });
|
||||
|
||||
const { mutate: create, isPending, error } = useMutation({
|
||||
mutationFn: (input: MonitorInput) => api.createMonitor(input),
|
||||
onSuccess: (m) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["monitors"] });
|
||||
router.push(`/monitors/${m.monitor_id}`);
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const target: MonitorInput["target"] = {};
|
||||
if (type === "http") {
|
||||
target.url = url;
|
||||
target.method = method;
|
||||
target.expected_status = expectedStatus;
|
||||
if (keyword) target.keyword = keyword;
|
||||
} else if (type === "tls") {
|
||||
target.host = host;
|
||||
target.port = port || 443;
|
||||
target.tls_warn_days = tlsWarnDays;
|
||||
} else if (type === "icmp") {
|
||||
target.host = host;
|
||||
} else {
|
||||
target.host = host;
|
||||
target.port = port;
|
||||
}
|
||||
create({ name, type, target, interval_sec: intervalSec, retries, runner, enabled });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<Link href="/monitors" className="text-sm text-text-secondary hover:text-text-primary">
|
||||
← Monitors
|
||||
</Link>
|
||||
<h1 className="mb-6 mt-2 text-2xl font-bold text-text-primary">New Monitor</h1>
|
||||
|
||||
<Card className="max-w-2xl">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className={labelClass}>Name</label>
|
||||
<input className={inputClass} value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. API health" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Type</label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{(["http", "tcp", "icmp", "tls"] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
onClick={() => setType(t)}
|
||||
className={`rounded-lg border px-3 py-2 text-sm font-medium transition-colors ${
|
||||
type === t ? "border-accent bg-accent/10 text-accent" : "border-border bg-surface-2 text-text-secondary hover:border-accent/40 hover:text-text-primary"
|
||||
}`}
|
||||
>
|
||||
{t.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{type === "http" && (
|
||||
<>
|
||||
<div>
|
||||
<label className={labelClass}>URL</label>
|
||||
<input className={inputClass} value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://example.com/health" required />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Method</label>
|
||||
<select className={inputClass} value={method} onChange={(e) => setMethod(e.target.value)}>
|
||||
<option>GET</option>
|
||||
<option>HEAD</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Expected status</label>
|
||||
<input type="number" className={inputClass} value={expectedStatus} onChange={(e) => setExpectedStatus(Number(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Keyword (optional, body must contain)</label>
|
||||
<input className={inputClass} value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="e.g. ok" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(type === "tcp" || type === "tls" || type === "icmp") && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Host</label>
|
||||
<input className={inputClass} value={host} onChange={(e) => setHost(e.target.value)} placeholder="example.com" required />
|
||||
</div>
|
||||
{type !== "icmp" && (
|
||||
<div>
|
||||
<label className={labelClass}>Port</label>
|
||||
<input type="number" className={inputClass} value={port} onChange={(e) => setPort(Number(e.target.value))} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === "tls" && (
|
||||
<div>
|
||||
<label className={labelClass}>Warn days before expiry</label>
|
||||
<input type="number" className={inputClass} value={tlsWarnDays} onChange={(e) => setTlsWarnDays(Number(e.target.value))} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Interval (seconds)</label>
|
||||
<input type="number" className={inputClass} value={intervalSec} onChange={(e) => setIntervalSec(Number(e.target.value))} min={10} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Retries before down</label>
|
||||
<input type="number" className={inputClass} value={retries} onChange={(e) => setRetries(Number(e.target.value))} min={1} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Runner</label>
|
||||
<select className={inputClass} value={runner} onChange={(e) => setRunner(e.target.value)}>
|
||||
<option value="server">Server (central)</option>
|
||||
{servers?.map((s) => (
|
||||
<option key={s.server_id} value={s.server_id}>
|
||||
Agent · {s.hostname}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-text-tertiary">Agent-run monitors require the agent monitor scheduler (P2).</p>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm text-text-secondary">
|
||||
<input type="checkbox" checked={enabled} onChange={(e) => setEnabled(e.target.checked)} />
|
||||
Enabled
|
||||
</label>
|
||||
|
||||
{error && <p className="text-sm text-danger">{(error as Error).message}</p>}
|
||||
|
||||
<div className="flex gap-3 pt-1">
|
||||
<Button type="submit" variant="primary" loading={isPending}>
|
||||
Create Monitor
|
||||
</Button>
|
||||
<Link href="/monitors">
|
||||
<Button type="button" variant="ghost">
|
||||
Cancel
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import { api, Monitor, MonitorStatus } from "@/lib/api";
|
||||
import { Badge, Button, Card, Table, Thead, Tbody, Tr, Th, Td } from "@/components/ui";
|
||||
|
||||
function statusVariant(status: MonitorStatus) {
|
||||
switch (status) {
|
||||
case "up":
|
||||
return "success";
|
||||
case "down":
|
||||
return "danger";
|
||||
default:
|
||||
return "warning";
|
||||
}
|
||||
}
|
||||
|
||||
function targetSummary(m: Monitor): string {
|
||||
if (m.type === "http") return m.target.url ?? "";
|
||||
if (m.type === "tls") return `${m.target.host ?? ""}:${m.target.port || 443}`;
|
||||
if (m.type === "icmp") return m.target.host ?? "";
|
||||
return `${m.target.host ?? ""}:${m.target.port ?? ""}`;
|
||||
}
|
||||
|
||||
export default function MonitorsPage() {
|
||||
const { data: monitors, isLoading } = useQuery({
|
||||
queryKey: ["monitors"],
|
||||
queryFn: () => api.listMonitors(),
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-text-primary">Monitors</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">Service uptime and latency checks.</p>
|
||||
</div>
|
||||
<Link href="/monitors/new">
|
||||
<Button variant="primary">New Monitor</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card padding={false}>
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-16">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-accent" />
|
||||
</div>
|
||||
) : !monitors || monitors.length === 0 ? (
|
||||
<div className="py-16 text-center">
|
||||
<p className="text-sm text-text-secondary">No monitors yet.</p>
|
||||
<Link href="/monitors/new">
|
||||
<Button variant="secondary" size="sm" className="mt-3">
|
||||
Create your first monitor
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Target</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Latency</Th>
|
||||
<Th>Last check</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{monitors.map((m) => (
|
||||
<Tr key={m.monitor_id}>
|
||||
<Td>
|
||||
<Link href={`/monitors/${m.monitor_id}`} className="font-medium text-text-primary hover:text-accent">
|
||||
{m.name}
|
||||
</Link>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge variant="neutral">{m.type}</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="font-mono text-xs text-text-secondary">{targetSummary(m)}</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge variant={statusVariant(m.state.status)}>{m.state.status}</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-text-secondary">{m.state.latency_ms}ms</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-xs text-text-secondary">
|
||||
{m.state.last_check_at ? new Date(m.state.last_check_at).toLocaleTimeString() : "—"}
|
||||
</span>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -60,6 +60,14 @@ function SettingsIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
function MonitorIcon() {
|
||||
return (
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 12h4l2 6 4-14 2 8h6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function StepsIcon() {
|
||||
return (
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
@@ -70,6 +78,7 @@ function StepsIcon() {
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ href: "/servers", label: "Servers", icon: <ServerIcon /> },
|
||||
{ href: "/monitors", label: "Monitors", icon: <MonitorIcon /> },
|
||||
{ href: "/keys", label: "SSH Keys", icon: <KeyIcon /> },
|
||||
{ href: "/secrets", label: "Secrets", icon: <SecretIcon /> },
|
||||
{ href: "/workflows", label: "Workflows", icon: <WorkflowIcon /> },
|
||||
|
||||
@@ -34,6 +34,69 @@ export interface Server {
|
||||
inventory?: Inventory;
|
||||
}
|
||||
|
||||
export type MonitorType = "http" | "tcp" | "icmp" | "tls";
|
||||
export type MonitorStatus = "up" | "down" | "pending";
|
||||
|
||||
export interface MonitorTarget {
|
||||
url?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
method?: string;
|
||||
expected_status?: number;
|
||||
keyword?: string;
|
||||
tls_warn_days?: number;
|
||||
}
|
||||
|
||||
export interface MonitorState {
|
||||
status: MonitorStatus;
|
||||
last_check_at?: string;
|
||||
latency_ms: number;
|
||||
message?: string;
|
||||
cert_expiry_at?: string;
|
||||
fails: number;
|
||||
}
|
||||
|
||||
export interface Monitor {
|
||||
monitor_id: string;
|
||||
name: string;
|
||||
type: MonitorType;
|
||||
target: MonitorTarget;
|
||||
interval_sec: number;
|
||||
runner: string; // "server" or a server_id
|
||||
retries: number;
|
||||
enabled: boolean;
|
||||
channel_ids?: string[];
|
||||
state: MonitorState;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface MonitorInput {
|
||||
name: string;
|
||||
type: MonitorType;
|
||||
target: MonitorTarget;
|
||||
interval_sec: number;
|
||||
runner: string;
|
||||
retries: number;
|
||||
enabled: boolean;
|
||||
channel_ids?: string[];
|
||||
}
|
||||
|
||||
export interface Incident {
|
||||
incident_id: string;
|
||||
monitor_id: string;
|
||||
started_at: string;
|
||||
resolved_at?: string;
|
||||
cause?: string;
|
||||
}
|
||||
|
||||
export interface Rollup {
|
||||
monitor_id: string;
|
||||
period_start: string;
|
||||
checks: number;
|
||||
up_count: number;
|
||||
sum_latency: number;
|
||||
}
|
||||
|
||||
export interface ConsoleConnectRequest {
|
||||
server_id: string;
|
||||
protocol: string;
|
||||
@@ -278,6 +341,35 @@ export const api = {
|
||||
return `curl -fsSL "${window.location.origin}/update" | bash`;
|
||||
},
|
||||
|
||||
// Monitors
|
||||
listMonitors(): Promise<Monitor[]> {
|
||||
return request<Monitor[]>("/monitors");
|
||||
},
|
||||
|
||||
getMonitor(monitorId: string): Promise<Monitor> {
|
||||
return request<Monitor>(`/monitors/${monitorId}`);
|
||||
},
|
||||
|
||||
createMonitor(input: MonitorInput): Promise<Monitor> {
|
||||
return request<Monitor>("/monitors", { method: "POST", body: JSON.stringify(input) });
|
||||
},
|
||||
|
||||
updateMonitor(monitorId: string, input: Partial<MonitorInput>): Promise<void> {
|
||||
return request<void>(`/monitors/${monitorId}`, { method: "PUT", body: JSON.stringify(input) });
|
||||
},
|
||||
|
||||
deleteMonitor(monitorId: string): Promise<void> {
|
||||
return request<void>(`/monitors/${monitorId}`, { method: "DELETE" });
|
||||
},
|
||||
|
||||
getMonitorIncidents(monitorId: string): Promise<Incident[]> {
|
||||
return request<Incident[]>(`/monitors/${monitorId}/incidents`);
|
||||
},
|
||||
|
||||
getMonitorUptime(monitorId: string): Promise<Rollup[]> {
|
||||
return request<Rollup[]>(`/monitors/${monitorId}/uptime`);
|
||||
},
|
||||
|
||||
getLatestAgentVersion(): Promise<{ version: string }> {
|
||||
return request<{ version: string }>("/agent/latest-version");
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user