sync: auto-sync from DESKTOP-0O8A1RL at 2026-05-23 11:05:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
346
.claude/scripts/watchdog_alerts_page.py
Normal file
346
.claude/scripts/watchdog_alerts_page.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
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")
|
||||
Reference in New Issue
Block a user