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>
</select>
</div>
<div class="header-actions">
<div class="header-actions" style="display:flex; gap:8px; align-items:center">
<button onclick="refreshData()">Refresh</button>
<button id="copy-ai-btn" onclick="copyForAI()">Copy for AI</button>
</div>
</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
refreshData();
// Poll for updates

View file

@ -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)