509 lines
23 KiB
Python
509 lines
23 KiB
Python
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{\" \"}Ack’d:{\" \"}\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())}")
|