diff --git a/services/agent-system/webui/index.html b/services/agent-system/webui/index.html index d20843a..1f22457 100644 --- a/services/agent-system/webui/index.html +++ b/services/agent-system/webui/index.html @@ -277,8 +277,9 @@ -
+
+
@@ -691,6 +692,73 @@ } } + async function copyForAI() { + const btn = document.getElementById('copy-ai-btn'); + const original = btn.textContent; + btn.textContent = 'Copying...'; + btn.disabled = true; + + try { + const snap = await fetchData('/snapshot'); + if (!snap) throw new Error('snapshot fetch failed'); + + const now = new Date(snap.timestamp); + const dateStr = now.toISOString().slice(0, 16).replace('T', ' '); + const lines = []; + + lines.push(`=== HOMELAB SNAPSHOT ${dateStr} ===`); + + if (snap.nodes && snap.nodes.length > 0) { + lines.push('NODES: ' + snap.nodes.map(n => + `${(n.hostname || n.id || '?').toUpperCase()} ${(n.health || 'unknown').toUpperCase()}` + ).join(', ')); + } else { + lines.push('NODES: none'); + } + + if (snap.non_nominal_services && snap.non_nominal_services.length > 0) { + lines.push('ERRORS: ' + snap.non_nominal_services.map(s => + `${s.name} (${s.node}) - ${s.health}` + ).join(', ')); + } else { + lines.push(`ERRORS: none (${snap.nominal_service_count} nominal)`); + } + + const activeIncidents = (snap.incidents || []).filter(i => !['resolved', 'closed'].includes(i.status)); + if (activeIncidents.length > 0) { + lines.push('INCIDENTS: ' + activeIncidents.map(i => + `[${i.severity}] ${i.message} (${i.node})` + ).join('; ')); + } else { + lines.push('INCIDENTS: none'); + } + + if (snap.events && snap.events.length > 0) { + lines.push(`EVENTS (last ${snap.events.length}):`); + snap.events.forEach(ev => { + const ts = ev.timestamp + ? new Date(ev.timestamp * 1000).toISOString().slice(11, 19) + : '?'; + const svc = ev.service ? '/' + ev.service : ''; + lines.push(` ${ts} [${ev.severity || ev.level || '?'}] ${ev.type} - ${ev.message || ''} (${ev.node || ''}${svc})`); + }); + } else { + lines.push('EVENTS (last 10): none'); + } + + const s = snap.summary || {}; + lines.push(`SUMMARY: status=${s.status || '?'} nodes=${s.node_count ?? '?'} services=${s.service_count ?? '?'} incidents=${s.incident_count ?? '?'}`); + + await navigator.clipboard.writeText(lines.join('\n')); + btn.textContent = 'Copied!'; + setTimeout(() => { btn.textContent = original; btn.disabled = false; }, 2000); + } catch (e) { + console.error('copyForAI error:', e); + btn.textContent = 'Error'; + setTimeout(() => { btn.textContent = original; btn.disabled = false; }, 2000); + } + } + // Initial load refreshData(); // Poll for updates diff --git a/services/agent-system/webui/web.py b/services/agent-system/webui/web.py index 49fd021..d6a7fae 100644 --- a/services/agent-system/webui/web.py +++ b/services/agent-system/webui/web.py @@ -1,6 +1,7 @@ import json import os import time +from datetime import datetime, timezone from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path @@ -141,6 +142,28 @@ def mutate_action(action_id, target_status): return False, str(e) +def get_snapshot(): + nodes = current_nodes() + services = current_services() + incidents = current_incidents() + events = current_events() + summary = current_summary() + + non_nominal = [s for s in services if s.get("health") != "nominal"] + nominal_count = len(services) - len(non_nominal) + + return { + "timestamp": datetime.now(timezone.utc).isoformat(), + "summary": summary, + "nodes": nodes, + "non_nominal_services": non_nominal, + "nominal_service_count": nominal_count, + "total_service_count": len(services), + "incidents": incidents, + "events": events[:10], + } + + def send_json(status, payload, handler): body = (json.dumps(payload) + "\n").encode("utf-8") handler.send_response(status) @@ -188,6 +211,10 @@ class Handler(BaseHTTPRequestHandler): send_json(200, current_actions(), self) return + if self.path == "/snapshot": + send_json(200, get_snapshot(), self) + return + if self.path in ("/", "/index.html"): body = (STATIC_DIR / "index.html").read_bytes() self.send_response(200)