347 lines
13 KiB
Python
347 lines
13 KiB
Python
"""
|
|
Write replacement WatchdogAlerts.tsx that matches the new WatchdogAlert interface.
|
|
Run on the server: python3 /tmp/watchdog_alerts_page.py
|
|
"""
|
|
|
|
content = '''import { useState } from "react";
|
|
import { Link } from "react-router-dom";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
ShieldAlert,
|
|
RefreshCw,
|
|
CheckCircle2,
|
|
Clock,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
} from "lucide-react";
|
|
import { watchdogAlertsApi, WatchdogAlert } from "../api/client";
|
|
import { Card, CardHeader, CardTitle, CardContent } from "../components/Card";
|
|
import { Button } from "../components/Button";
|
|
import { NativeSelect } from "../components/Select";
|
|
import { useToast } from "../hooks/useToast";
|
|
import { cn } from "../lib/utils";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Status derivation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type WatchdogStatus = "active" | "acknowledged" | "resolved";
|
|
|
|
function watchdogStatus(alert: WatchdogAlert): WatchdogStatus {
|
|
if (alert.resolved_at) return "resolved";
|
|
if (alert.acknowledged_at) return "acknowledged";
|
|
return "active";
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Status badge
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function WatchdogStatusBadge({ status }: { status: WatchdogStatus }) {
|
|
const classes: Record<WatchdogStatus, string> = {
|
|
active: "bg-red-500/15 text-red-600 dark:text-red-400",
|
|
acknowledged: "bg-amber-500/15 text-amber-600 dark:text-amber-400",
|
|
resolved: "bg-green-500/15 text-green-600 dark:text-green-400",
|
|
};
|
|
const label: Record<WatchdogStatus, string> = {
|
|
active: "Active",
|
|
acknowledged: "Acknowledged",
|
|
resolved: "Resolved",
|
|
};
|
|
return (
|
|
<span
|
|
className={cn(
|
|
"inline-block px-2 py-0.5 rounded text-[10px] font-semibold uppercase tracking-wider",
|
|
classes[status]
|
|
)}
|
|
>
|
|
{label[status]}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Filter type
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type StatusFilter = "" | "active" | "acknowledged" | "resolved";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// WatchdogAlerts page
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function WatchdogAlerts() {
|
|
const { toast } = useToast();
|
|
const queryClient = useQueryClient();
|
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>("active");
|
|
const [expandedLogId, setExpandedLogId] = useState<string | null>(null);
|
|
|
|
const {
|
|
data: allAlerts = [],
|
|
isLoading,
|
|
isError,
|
|
refetch,
|
|
} = useQuery({
|
|
queryKey: ["watchdog-alerts-page"],
|
|
queryFn: () => watchdogAlertsApi.list().then((r) => r.data),
|
|
refetchInterval: 30000,
|
|
});
|
|
|
|
// Client-side filter by derived status
|
|
const alerts = allAlerts.filter(
|
|
(a) => !statusFilter || watchdogStatus(a) === statusFilter
|
|
);
|
|
|
|
const acknowledgeMutation = useMutation({
|
|
mutationFn: (id: string) => watchdogAlertsApi.acknowledge(id),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["watchdog-alerts-page"] });
|
|
toast({ type: "success", title: "Alert acknowledged" });
|
|
},
|
|
onError: (err: Error) => {
|
|
toast({
|
|
type: "error",
|
|
title: "Could not acknowledge alert",
|
|
message: err.message,
|
|
});
|
|
},
|
|
});
|
|
|
|
const resolveMutation = useMutation({
|
|
mutationFn: (id: string) => watchdogAlertsApi.resolve(id),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["watchdog-alerts-page"] });
|
|
toast({ type: "success", title: "Alert resolved" });
|
|
},
|
|
onError: (err: Error) => {
|
|
toast({
|
|
type: "error",
|
|
title: "Could not resolve alert",
|
|
message: err.message,
|
|
});
|
|
},
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: string) => watchdogAlertsApi.delete(id),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["watchdog-alerts-page"] });
|
|
toast({ type: "success", title: "Alert deleted" });
|
|
},
|
|
onError: (err: Error) => {
|
|
toast({ type: "error", title: "Could not delete alert", message: err.message });
|
|
},
|
|
});
|
|
|
|
const isMutating =
|
|
acknowledgeMutation.isPending ||
|
|
resolveMutation.isPending ||
|
|
deleteMutation.isPending;
|
|
|
|
const activeCount = allAlerts.filter((a) => watchdogStatus(a) === "active").length;
|
|
const acknowledgedCount = allAlerts.filter(
|
|
(a) => watchdogStatus(a) === "acknowledged"
|
|
).length;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Watchdog Alerts</h1>
|
|
<p className="text-[hsl(var(--muted-foreground))]">
|
|
Agent crash and restart exhaustion events
|
|
</p>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Summary chips */}
|
|
<div className="flex flex-wrap gap-3">
|
|
<div className="flex items-center gap-3 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-2 min-w-[140px]">
|
|
<div className="text-2xl font-bold tabular-nums text-red-600 dark:text-red-400">
|
|
{activeCount}
|
|
</div>
|
|
<div className="text-xs uppercase tracking-wider text-red-600/80 dark:text-red-400/80">
|
|
Active
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3 rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-2 min-w-[140px]">
|
|
<div className="text-2xl font-bold tabular-nums text-amber-600 dark:text-amber-400">
|
|
{acknowledgedCount}
|
|
</div>
|
|
<div className="text-xs uppercase tracking-wider text-amber-600/80 dark:text-amber-400/80">
|
|
Acknowledged
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{isError && (
|
|
<div className="rounded-md bg-destructive/10 border border-destructive/20 px-4 py-3 text-sm text-destructive">
|
|
Failed to load watchdog alerts. Check your connection and try
|
|
refreshing.
|
|
</div>
|
|
)}
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<CardTitle>Alert Stream</CardTitle>
|
|
<NativeSelect
|
|
value={statusFilter}
|
|
onChange={(e) =>
|
|
setStatusFilter(e.target.value as StatusFilter)
|
|
}
|
|
className="w-auto min-w-[11rem]"
|
|
aria-label="Filter by status"
|
|
>
|
|
<option value="active">Active Only</option>
|
|
<option value="acknowledged">Acknowledged</option>
|
|
<option value="resolved">Resolved</option>
|
|
<option value="">All Statuses</option>
|
|
</NativeSelect>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoading ? (
|
|
<p className="text-[hsl(var(--muted-foreground))]">
|
|
Loading watchdog alerts...
|
|
</p>
|
|
) : alerts.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<ShieldAlert className="h-10 w-10 text-[hsl(var(--muted-foreground))] mb-3" />
|
|
<p className="text-sm font-medium">No watchdog alerts</p>
|
|
<p className="text-xs text-[hsl(var(--muted-foreground))]">
|
|
No events match the current filter.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{alerts.map((alert: WatchdogAlert) => {
|
|
const status = watchdogStatus(alert);
|
|
const logExpanded = expandedLogId === alert.id;
|
|
|
|
return (
|
|
<div
|
|
key={alert.id}
|
|
className="rounded-lg border border-[hsl(var(--border))] p-4 space-y-2"
|
|
>
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="space-y-0.5 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<WatchdogStatusBadge status={status} />
|
|
<span className="text-xs text-[hsl(var(--muted-foreground))]">
|
|
{alert.restart_attempts} restart attempt
|
|
{alert.restart_attempts !== 1 ? "s" : ""}
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-[hsl(var(--muted-foreground))]">
|
|
<Link
|
|
to={`/agents/${alert.agent_id}`}
|
|
className="text-[hsl(var(--primary))] hover:underline font-mono"
|
|
>
|
|
{alert.agent_id.slice(0, 8)}…
|
|
</Link>
|
|
{" \xb7 "}
|
|
Triggered:{" "}
|
|
{new Date(alert.triggered_at).toLocaleString()}
|
|
{alert.acknowledged_at && (
|
|
<>
|
|
{" \xb7 "}Ack:{" "}
|
|
{new Date(alert.acknowledged_at).toLocaleString()}
|
|
{alert.acknowledged_by && ` by ${alert.acknowledged_by}`}
|
|
</>
|
|
)}
|
|
{alert.resolved_at && (
|
|
<>
|
|
{" \xb7 "}Resolved:{" "}
|
|
{new Date(alert.resolved_at).toLocaleString()}
|
|
</>
|
|
)}
|
|
</div>
|
|
{alert.last_error && (
|
|
<p className="text-sm text-[hsl(var(--foreground))] mt-1">
|
|
<span className="font-medium">Error:</span>{" "}
|
|
{alert.last_error}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex shrink-0 items-center gap-1.5">
|
|
{status === "active" && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={isMutating}
|
|
onClick={() =>
|
|
acknowledgeMutation.mutate(alert.id)
|
|
}
|
|
title="Acknowledge"
|
|
>
|
|
<Clock className="h-3.5 w-3.5 mr-1" />
|
|
Ack
|
|
</Button>
|
|
)}
|
|
{status !== "resolved" && (
|
|
<Button
|
|
size="sm"
|
|
disabled={isMutating}
|
|
onClick={() => resolveMutation.mutate(alert.id)}
|
|
title="Resolve"
|
|
>
|
|
<CheckCircle2 className="h-3.5 w-3.5 mr-1" />
|
|
Resolve
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
disabled={isMutating}
|
|
onClick={() => deleteMutation.mutate(alert.id)}
|
|
>
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{alert.log_tail && (
|
|
<div>
|
|
<button
|
|
className="flex items-center gap-1 text-xs text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))]"
|
|
onClick={() =>
|
|
setExpandedLogId(logExpanded ? null : alert.id)
|
|
}
|
|
>
|
|
{logExpanded ? (
|
|
<ChevronDown className="h-3 w-3" />
|
|
) : (
|
|
<ChevronRight className="h-3 w-3" />
|
|
)}
|
|
Agent log tail
|
|
</button>
|
|
{logExpanded && (
|
|
<pre className="mt-1 max-h-64 overflow-auto rounded bg-[hsl(var(--muted))]/50 p-2 text-xs font-mono text-[hsl(var(--foreground))]">
|
|
{alert.log_tail}
|
|
</pre>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
'''
|
|
|
|
path = "/home/guru/gururmm/dashboard/src/pages/WatchdogAlerts.tsx"
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
f.write(content)
|
|
print(f"Written WatchdogAlerts.tsx: {len(content.splitlines())} lines")
|