Files
claudetools/.claude/scripts/watchdog_alerts_page.py
2026-05-23 10:56:40 -07:00

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")