Compare commits
No commits in common. "ae7446a04bc9c75b0f87e254f7acc78364315da3" and "8fb4d3d634302fb382b4beaaba3e41d30a59acc4" have entirely different histories.
ae7446a04b
...
8fb4d3d634
1
hosts/vps/services.txt
Normal file
1
hosts/vps/services.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
npm
|
||||||
|
|
@ -23,7 +23,7 @@ services:
|
||||||
local:
|
local:
|
||||||
- stability-agent
|
- stability-agent
|
||||||
external:
|
external:
|
||||||
- piha:redis
|
- piha:agent-system-redis
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
container_port: 18180
|
container_port: 18180
|
||||||
|
|
@ -32,12 +32,3 @@ services:
|
||||||
config_path: /opt/homelab/config/control-plane
|
config_path: /opt/homelab/config/control-plane
|
||||||
data_path: /opt/homelab/data/control-plane
|
data_path: /opt/homelab/data/control-plane
|
||||||
logs_path: /opt/homelab/logs/control-plane
|
logs_path: /opt/homelab/logs/control-plane
|
||||||
|
|
||||||
node_exporter:
|
|
||||||
role: metrics-exporter
|
|
||||||
deployment_model: docker-compose
|
|
||||||
exposure: local-only
|
|
||||||
offline_required: true
|
|
||||||
depends_on:
|
|
||||||
local: []
|
|
||||||
external: []
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ nodes:
|
||||||
roles:
|
roles:
|
||||||
- edge
|
- edge
|
||||||
- ingress
|
- ingress
|
||||||
- control-plane
|
|
||||||
|
|
||||||
chelsty-infra:
|
chelsty-infra:
|
||||||
site: chelsty
|
site: chelsty
|
||||||
|
|
|
||||||
|
|
@ -277,9 +277,8 @@
|
||||||
<option value="maintenance">MAINTENANCE</option>
|
<option value="maintenance">MAINTENANCE</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions" style="display:flex; gap:8px; align-items:center">
|
<div class="header-actions">
|
||||||
<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>
|
||||||
|
|
||||||
|
|
@ -692,73 +691,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -142,28 +141,6 @@ 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)
|
||||||
|
|
@ -211,10 +188,6 @@ 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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue