feat: add Copy for AI snapshot button to webui

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
oskar 2026-05-21 12:05:37 +02:00
parent f21be4f4d4
commit ae7446a04b
2 changed files with 96 additions and 1 deletions

View file

@ -277,8 +277,9 @@
<option value="maintenance">MAINTENANCE</option> <option value="maintenance">MAINTENANCE</option>
</select> </select>
</div> </div>
<div class="header-actions"> <div class="header-actions" style="display:flex; gap:8px; align-items:center">
<button onclick="refreshData()">Refresh</button> <button onclick="refreshData()">Refresh</button>
<button id="copy-ai-btn" onclick="copyForAI()">Copy for AI</button>
</div> </div>
</header> </header>
@ -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 // Initial load
refreshData(); refreshData();
// Poll for updates // Poll for updates

View file

@ -1,6 +1,7 @@
import json import json
import os import os
import time import time
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
@ -141,6 +142,28 @@ def mutate_action(action_id, target_status):
return False, str(e) 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): def send_json(status, payload, handler):
body = (json.dumps(payload) + "\n").encode("utf-8") body = (json.dumps(payload) + "\n").encode("utf-8")
handler.send_response(status) handler.send_response(status)
@ -188,6 +211,10 @@ class Handler(BaseHTTPRequestHandler):
send_json(200, current_actions(), self) send_json(200, current_actions(), self)
return return
if self.path == "/snapshot":
send_json(200, get_snapshot(), self)
return
if self.path in ("/", "/index.html"): if self.path in ("/", "/index.html"):
body = (STATIC_DIR / "index.html").read_bytes() body = (STATIC_DIR / "index.html").read_bytes()
self.send_response(200) self.send_response(200)