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

509 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sys
path = "/home/guru/gururmm/dashboard/src/pages/Alerts.tsx"
with open(path, "r", encoding="utf-8") as f:
content = f.read()
# ─── Insert WatchdogAlertsSection component before export function Alerts() ───
insert_before = "export function Alerts() {"
watchdog_section = (
"// ─── WatchdogAlertsSection ─────────────────────────────────────────────────\n"
"\n"
"type WdogStatus = \"active\" | \"acknowledged\" | \"resolved\";\n"
"\n"
"function wdogStatus(alert: WatchdogAlert): WdogStatus {\n"
" if (alert.resolved_at) return \"resolved\";\n"
" if (alert.acknowledged_at) return \"acknowledged\";\n"
" return \"active\";\n"
"}\n"
"\n"
"function WatchdogAlertsSection() {\n"
" const queryClient = useQueryClient();\n"
" const { toast } = useToast();\n"
" const [showAll, setShowAll] = useState(false);\n"
" const [expandedLogId, setExpandedLogId] = useState<string | null>(null);\n"
"\n"
" const { data: allAlerts = [], isLoading } = useQuery({\n"
" queryKey: [\"watchdog-alerts\"],\n"
" queryFn: () => watchdogAlertsApi.list().then((r) => r.data),\n"
" refetchInterval: 30000,\n"
" });\n"
"\n"
" const alerts = showAll\n"
" ? allAlerts\n"
" : allAlerts.filter((a) => !a.resolved_at);\n"
"\n"
" const acknowledgeMutation = useMutation({\n"
" mutationFn: (id: string) => watchdogAlertsApi.acknowledge(id),\n"
" onSuccess: () => {\n"
" queryClient.invalidateQueries({ queryKey: [\"watchdog-alerts\"] });\n"
" toast({ type: \"success\", title: \"Alert acknowledged\" });\n"
" },\n"
" onError: (err: Error) =>\n"
" toast({ type: \"error\", title: \"Could not acknowledge\", message: err.message }),\n"
" });\n"
"\n"
" const resolveMutation = useMutation({\n"
" mutationFn: (id: string) => watchdogAlertsApi.resolve(id),\n"
" onSuccess: () => {\n"
" queryClient.invalidateQueries({ queryKey: [\"watchdog-alerts\"] });\n"
" toast({ type: \"success\", title: \"Alert resolved\" });\n"
" },\n"
" onError: (err: Error) =>\n"
" toast({ type: \"error\", title: \"Could not resolve\", message: err.message }),\n"
" });\n"
"\n"
" const deleteMutation = useMutation({\n"
" mutationFn: (id: string) => watchdogAlertsApi.delete(id),\n"
" onSuccess: () => {\n"
" queryClient.invalidateQueries({ queryKey: [\"watchdog-alerts\"] });\n"
" toast({ type: \"success\", title: \"Alert deleted\" });\n"
" },\n"
" onError: (err: Error) =>\n"
" toast({ type: \"error\", title: \"Could not delete\", message: err.message }),\n"
" });\n"
"\n"
" const isMutating =\n"
" acknowledgeMutation.isPending ||\n"
" resolveMutation.isPending ||\n"
" deleteMutation.isPending;\n"
"\n"
" const activeCount = allAlerts.filter((a) => wdogStatus(a) === \"active\").length;\n"
"\n"
" return (\n"
" <Card>\n"
" <CardHeader>\n"
" <div className=\"flex items-center justify-between gap-3\">\n"
" <CardTitle className=\"flex items-center gap-2\">\n"
" <AlertTriangle className=\"h-4 w-4 text-amber-500\" />\n"
" Watchdog Alerts\n"
" {activeCount > 0 && (\n"
" <span className=\"ml-1 rounded-full bg-amber-500/15 px-2 py-0.5 text-xs font-medium text-amber-600 dark:text-amber-400\">\n"
" {activeCount} active\n"
" </span>\n"
" )}\n"
" </CardTitle>\n"
" <button\n"
" className=\"text-xs text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] underline-offset-2 hover:underline\"\n"
" onClick={() => setShowAll((v) => !v)}\n"
" >\n"
" {showAll ? \"Active only\" : \"Show all\"}\n"
" </button>\n"
" </div>\n"
" </CardHeader>\n"
" <CardContent>\n"
" {isLoading && (\n"
" <p className=\"py-6 text-center text-sm text-[hsl(var(--muted-foreground))]\">Loading...</p>\n"
" )}\n"
" {!isLoading && alerts.length === 0 && (\n"
" <p className=\"py-6 text-center text-sm text-[hsl(var(--muted-foreground))]\">\n"
" {showAll ? \"No watchdog alerts recorded.\" : \"No active watchdog alerts.\"}\n"
" </p>\n"
" )}\n"
" <div className=\"space-y-3\">\n"
" {alerts.map((alert) => {\n"
" const status = wdogStatus(alert);\n"
" const logExpanded = expandedLogId === alert.id;\n"
" const statusColor =\n"
" status === \"active\"\n"
" ? \"text-red-600 dark:text-red-400\"\n"
" : status === \"acknowledged\"\n"
" ? \"text-amber-600 dark:text-amber-400\"\n"
" : \"text-[hsl(var(--muted-foreground))]\";\n"
"\n"
" return (\n"
" <div\n"
" key={alert.id}\n"
" className=\"rounded-lg border border-[hsl(var(--border))] p-4 space-y-2\"\n"
" >\n"
" <div className=\"flex items-start justify-between gap-4\">\n"
" <div className=\"space-y-0.5\">\n"
" <div className=\"flex items-center gap-2\">\n"
" <span\n"
" className={`text-xs font-semibold uppercase tracking-wider ${statusColor}`}\n"
" >\n"
" {status}\n"
" </span>\n"
" <span className=\"text-xs text-[hsl(var(--muted-foreground))]\">\n"
" \xb7 {alert.restart_attempts} restart attempt\n"
" {alert.restart_attempts !== 1 ? \"s\" : \"\"}\n"
" </span>\n"
" </div>\n"
" <p className=\"text-xs text-[hsl(var(--muted-foreground))]\">\n"
" Agent:{\" \"}\n"
" <span\n"
" className=\"font-mono\"\n"
" title={alert.agent_id}\n"
" >\n"
" {alert.agent_id.slice(0, 8)}…\n"
" </span>\n"
" {\" \"}\xb7{\" \"}\n"
" Triggered: {formatRelative(alert.triggered_at)}\n"
" {alert.acknowledged_at && (\n"
" <>\n"
" {\" \"}\xb7{\" \"}Ackd:{\" \"}\n"
" {formatRelative(alert.acknowledged_at)}\n"
" </>\n"
" )}\n"
" {alert.resolved_at && (\n"
" <>\n"
" {\" \"}\xb7{\" \"}Resolved:{\" \"}\n"
" {formatRelative(alert.resolved_at)}\n"
" </>\n"
" )}\n"
" </p>\n"
" {alert.last_error && (\n"
" <p className=\"text-sm text-[hsl(var(--foreground))]\">\n"
" <span className=\"font-medium\">Error:</span> {alert.last_error}\n"
" </p>\n"
" )}\n"
" </div>\n"
"\n"
" <div className=\"flex shrink-0 items-center gap-1.5\">\n"
" {status === \"active\" && (\n"
" <Button\n"
" size=\"sm\"\n"
" variant=\"secondary\"\n"
" disabled={isMutating}\n"
" onClick={() => acknowledgeMutation.mutate(alert.id)}\n"
" >\n"
" Acknowledge\n"
" </Button>\n"
" )}\n"
" {status !== \"resolved\" && (\n"
" <Button\n"
" size=\"sm\"\n"
" variant=\"secondary\"\n"
" disabled={isMutating}\n"
" onClick={() => resolveMutation.mutate(alert.id)}\n"
" >\n"
" Resolve\n"
" </Button>\n"
" )}\n"
" <Button\n"
" size=\"sm\"\n"
" variant=\"ghost\"\n"
" disabled={isMutating}\n"
" onClick={() => deleteMutation.mutate(alert.id)}\n"
" >\n"
" Delete\n"
" </Button>\n"
" </div>\n"
" </div>\n"
"\n"
" {alert.log_tail && (\n"
" <div>\n"
" <button\n"
" className=\"flex items-center gap-1 text-xs text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))]\"\n"
" onClick={() =>\n"
" setExpandedLogId(logExpanded ? null : alert.id)\n"
" }\n"
" >\n"
" {logExpanded ? (\n"
" <ChevronDown className=\"h-3 w-3\" />\n"
" ) : (\n"
" <ChevronRight className=\"h-3 w-3\" />\n"
" )}\n"
" Agent log tail\n"
" </button>\n"
" {logExpanded && (\n"
" <pre className=\"mt-1 max-h-48 overflow-auto rounded bg-[hsl(var(--muted))]/50 p-2 text-xs font-mono text-[hsl(var(--foreground))]\">\n"
" {alert.log_tail}\n"
" </pre>\n"
" )}\n"
" </div>\n"
" )}\n"
" </div>\n"
" );\n"
" })}\n"
" </div>\n"
" </CardContent>\n"
" </Card>\n"
" );\n"
"}\n"
"\n"
)
if insert_before in content:
content = content.replace(insert_before, watchdog_section + insert_before, 1)
print("OK: WatchdogAlertsSection inserted")
else:
print("ERROR: could not find 'export function Alerts() {'")
sys.exit(1)
# ─── Add <WatchdogAlertsSection /> at the bottom of the Alerts return div ───
# The closing of the Alerts function return div
old_end = " </div>\n );\n}\n\n// Re-export types"
new_end = " <WatchdogAlertsSection />\n </div>\n );\n}\n\n// Re-export types"
if old_end in content:
content = content.replace(old_end, new_end, 1)
print("OK: <WatchdogAlertsSection /> rendered in Alerts return")
else:
print("ERROR: closing div anchor not found")
# Debug
idx = content.find("// Re-export types")
if idx >= 0:
print("Context:", repr(content[max(0,idx-200):idx+30]))
with open(path, "w", encoding="utf-8") as f:
f.write(content)
print(f"Done. Lines: {len(content.splitlines())}")
"\n",
'type WatchdogStatus = "active" | "acknowledged" | "resolved";\n',
"\n",
"function watchdogStatus(alert: WatchdogAlert): WatchdogStatus {\n",
' if (alert.resolved_at) return "resolved";\n',
' if (alert.acknowledged_at) return "acknowledged";\n',
' return "active";\n',
"}\n",
"\n",
"function WatchdogAlertsPanel({ agentId }: { agentId: string }) {\n",
" const queryClient = useQueryClient();\n",
" const { toast } = useToast();\n",
' const [statusFilter, setStatusFilter] = useState<"" | WatchdogStatus>("");\n',
" const [expandedLogId, setExpandedLogId] = useState<string | null>(null);\n",
"\n",
" const { data: alerts = [], isLoading } = useQuery({\n",
' queryKey: ["watchdog-alerts", agentId],\n',
" queryFn: () => watchdogAlertsApi.listForAgent(agentId).then((r) => r.data),\n",
" refetchInterval: 30000,\n",
" });\n",
"\n",
" const filtered = alerts.filter(\n",
" (a) => !statusFilter || watchdogStatus(a) === statusFilter\n",
" );\n",
"\n",
" const acknowledgeMutation = useMutation({\n",
" mutationFn: (id: string) => watchdogAlertsApi.acknowledge(id),\n",
" onSuccess: () => {\n",
' queryClient.invalidateQueries({ queryKey: ["watchdog-alerts", agentId] });\n',
' toast({ type: "success", title: "Alert acknowledged" });\n',
" },\n",
" onError: (err: Error) =>\n",
' toast({ type: "error", title: "Could not acknowledge", message: err.message }),\n',
" });\n",
"\n",
" const resolveMutation = useMutation({\n",
" mutationFn: (id: string) => watchdogAlertsApi.resolve(id),\n",
" onSuccess: () => {\n",
' queryClient.invalidateQueries({ queryKey: ["watchdog-alerts", agentId] });\n',
' toast({ type: "success", title: "Alert resolved" });\n',
" },\n",
" onError: (err: Error) =>\n",
' toast({ type: "error", title: "Could not resolve", message: err.message }),\n',
" });\n",
"\n",
" const deleteMutation = useMutation({\n",
" mutationFn: (id: string) => watchdogAlertsApi.delete(id),\n",
" onSuccess: () => {\n",
' queryClient.invalidateQueries({ queryKey: ["watchdog-alerts", agentId] });\n',
' toast({ type: "success", title: "Alert deleted" });\n',
" },\n",
" onError: (err: Error) =>\n",
' toast({ type: "error", title: "Could not delete", message: err.message }),\n',
" });\n",
"\n",
" const isMutating =\n",
" acknowledgeMutation.isPending ||\n",
" resolveMutation.isPending ||\n",
" deleteMutation.isPending;\n",
"\n",
' const activeCount = alerts.filter((a) => watchdogStatus(a) === "active").length;\n',
"\n",
" return (\n",
" <Card>\n",
" <CardHeader>\n",
' <CardTitle className="flex items-center gap-2">\n',
' <AlertTriangle className="h-4 w-4 text-amber-500" />\n',
" Watchdog Alerts\n",
" {activeCount > 0 && (\n",
' <span className="ml-1 rounded-full bg-amber-500/15 px-2 py-0.5 text-xs font-medium text-amber-600 dark:text-amber-400">\n',
" {activeCount} active\n",
" </span>\n",
" )}\n",
" </CardTitle>\n",
" </CardHeader>\n",
' <CardContent className="space-y-3">\n',
' <div className="flex items-center gap-2">\n',
" <NativeSelect\n",
" value={statusFilter}\n",
' onChange={(e) => setStatusFilter(e.target.value as "" | WatchdogStatus)}\n',
' className="w-auto min-w-[10rem]"\n',
' aria-label="Filter by status"\n',
" >\n",
' <option value="">All Statuses</option>\n',
' <option value="active">Active</option>\n',
' <option value="acknowledged">Acknowledged</option>\n',
' <option value="resolved">Resolved</option>\n',
" </NativeSelect>\n",
" </div>\n",
"\n",
" {isLoading && (\n",
' <p className="py-6 text-center text-sm text-[hsl(var(--muted-foreground))]">\n',
" Loading...\n",
" </p>\n",
" )}\n",
"\n",
" {!isLoading && filtered.length === 0 && (\n",
' <p className="py-6 text-center text-sm text-[hsl(var(--muted-foreground))]">\n',
" {statusFilter\n",
' ? "No alerts match the current filter."\n',
' : "No watchdog alerts recorded."}\n',
" </p>\n",
" )}\n",
"\n",
" {filtered.map((alert) => {\n",
" const status = watchdogStatus(alert);\n",
" const logExpanded = expandedLogId === alert.id;\n",
" const statusColor =\n",
' status === "active"\n',
' ? "text-red-600 dark:text-red-400"\n',
' : status === "acknowledged"\n',
' ? "text-amber-600 dark:text-amber-400"\n',
' : "text-[hsl(var(--muted-foreground))]";\n',
"\n",
" return (\n",
" <div\n",
" key={alert.id}\n",
' className="rounded-lg border border-[hsl(var(--border))] p-4 space-y-2"\n',
" >\n",
' <div className="flex items-start justify-between gap-4">\n',
' <div className="space-y-0.5">\n',
' <div className="flex items-center gap-2">\n',
" <span\n",
" className={`text-xs font-semibold uppercase tracking-wider ${statusColor}`}\n",
" >\n",
" {status}\n",
" </span>\n",
' <span className="text-xs text-[hsl(var(--muted-foreground))]">\n',
" \xb7 {alert.restart_attempts} restart attempt\n",
' {alert.restart_attempts !== 1 ? "s" : ""}\n',
" </span>\n",
" </div>\n",
' <p className="text-xs text-[hsl(var(--muted-foreground))]">\n',
" Triggered: {new Date(alert.triggered_at).toLocaleString()}\n",
" {alert.acknowledged_at && (\n",
" <>\n",
' {" "}\n',
" \xb7 Acknowledged:{\" \"}\n",
" {new Date(alert.acknowledged_at).toLocaleString()}\n",
" </>\n",
" )}\n",
" {alert.resolved_at && (\n",
" <>\n",
' {" "}\n',
" \xb7 Resolved:{\" \"}\n",
" {new Date(alert.resolved_at).toLocaleString()}\n",
" </>\n",
" )}\n",
" </p>\n",
" {alert.last_error && (\n",
' <p className="text-sm text-[hsl(var(--foreground))]">\n',
' <span className="font-medium">Error:</span> {alert.last_error}\n',
" </p>\n",
" )}\n",
" </div>\n",
"\n",
' <div className="flex shrink-0 items-center gap-1.5">\n',
' {status === "active" && (\n',
" <Button\n",
' size="sm"\n',
' variant="secondary"\n',
" disabled={isMutating}\n",
" onClick={() => acknowledgeMutation.mutate(alert.id)}\n",
" >\n",
" Acknowledge\n",
" </Button>\n",
" )}\n",
' {status !== "resolved" && (\n',
" <Button\n",
' size="sm"\n',
' variant="secondary"\n',
" disabled={isMutating}\n",
" onClick={() => resolveMutation.mutate(alert.id)}\n",
" >\n",
" Resolve\n",
" </Button>\n",
" )}\n",
" <Button\n",
' size="sm"\n',
' variant="ghost"\n',
" disabled={isMutating}\n",
" onClick={() => deleteMutation.mutate(alert.id)}\n",
" >\n",
" Delete\n",
" </Button>\n",
" </div>\n",
" </div>\n",
"\n",
" {alert.log_tail && (\n",
" <div>\n",
" <button\n",
' className="flex items-center gap-1 text-xs text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))]"\n',
" onClick={() =>\n",
" setExpandedLogId(logExpanded ? null : alert.id)\n",
" }\n",
" >\n",
" {logExpanded ? (\n",
' <ChevronDown className="h-3 w-3" />\n',
" ) : (\n",
' <ChevronRight className="h-3 w-3" />\n',
" )}\n",
" Agent log tail\n",
" </button>\n",
" {logExpanded && (\n",
' <pre className="mt-1 max-h-48 overflow-auto rounded bg-[hsl(var(--muted))]/50 p-2 text-xs font-mono text-[hsl(var(--foreground))]">\n',
" {alert.log_tail}\n",
" </pre>\n",
" )}\n",
" </div>\n",
" )}\n",
" </div>\n",
" );\n",
" })}\n",
" </CardContent>\n",
" </Card>\n",
" );\n",
"}\n",
"\n",
]
file_lines[anchor_idx:anchor_idx] = C
print(f"OK: inserted {len(C)} lines before AgentAlertsPanel anchor")
content2 = "".join(file_lines)
old_tab = (
' <TabPanel tabId="alerts" activeTab={activeTab}>\n'
" <AgentAlertsPanel agentId={agent.id} />\n"
" </TabPanel>"
)
new_tab = (
' <TabPanel tabId="alerts" activeTab={activeTab}>\n'
" <AgentAlertsPanel agentId={agent.id} />\n"
' <div className="mt-6">\n'
" <WatchdogAlertsPanel agentId={agent.id} />\n"
" </div>\n"
" </TabPanel>"
)
if old_tab in content2:
content2 = content2.replace(old_tab, new_tab, 1)
print("OK: TabPanel updated")
else:
print("ERROR: TabPanel anchor not found")
idx = content2.find('tabId="alerts"')
if idx >= 0:
print("Context:", repr(content2[idx:idx+200]))
with open(path, "w", encoding="utf-8") as f:
f.write(content2)
print(f"Done. Lines: {len(content2.splitlines())}")