demo a thing WEB UI IS BACK

This commit is contained in:
lda
2026-04-12 02:00:01 +07:00 Unverified
parent e23617bd90
commit ea611d5135
7 changed files with 336 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
const $ = (id) => document.getElementById(id);
function setStatus(kind, text) {
const pill = $("status-pill");
pill.className = `pill ${kind}`;
pill.textContent = text;
}
async function api(path, init) {
const res = await fetch(path, {
headers: { "content-type": "application/json" },
...init,
});
if (!res.ok) {
let body = "";
try { body = JSON.stringify(await res.json(), null, 2); } catch (_) {}
throw new Error(`${res.status} ${res.statusText}\n${body}`);
}
return res.json();
}
function renderAgents(agents) {
const root = $("agents");
root.innerHTML = "";
if (!agents.length) {
root.innerHTML = '<div class="item">No agents enrolled yet</div>';
return;
}
for (const a of agents) {
const row = document.createElement("div");
row.className = "item";
row.innerHTML = `<span>${a.agent_id}</span><span>${a.connected ? "connected" : "offline"}</span>`;
row.onclick = () => { $("agent-id").value = a.agent_id; };
root.appendChild(row);
}
}
function renderAlerts(alerts) {
const root = $("alerts");
root.innerHTML = "";
if (!alerts.length) {
root.innerHTML = '<div class="item">No active alerts</div>';
return;
}
for (const a of alerts) {
const row = document.createElement("div");
row.className = "item";
row.innerHTML = `<span>${a.kind}${a.agent_id ? ` (${a.agent_id})` : ""}</span><span>${a.severity}</span>`;
root.appendChild(row);
}
}
async function loadAgents() {
const agents = await api("/api/v1/control/agents");
renderAgents(agents);
return agents;
}
async function loadAlerts() {
const alerts = await api("/api/v1/control/alerts");
renderAlerts(alerts);
return alerts;
}
async function loadAudit() {
const events = await api("/api/v1/control/audit/events?limit=25");
$("audit").textContent = JSON.stringify(events, null, 2);
}
function buildCommandPayload(kind, query) {
if (kind === "devs") {
return { command: { kind: "devs", dev: null, up_only: false } };
}
if (kind === "status") {
return { command: { kind: "status", query: query || null, name: null, ips: [], devs: [], nuds: [], macs: [] } };
}
if (kind === "leases") {
return { command: { kind: "leases", include_state: true } };
}
if (kind === "inventory") {
return { command: { kind: "inventory", query: query || null, name: null, ips: [], devs: [], nuds: [], macs: [] } };
}
return { command: { kind: "wake", query: query || null, mac: null, ip: null } };
}
async function runCommand(evt) {
evt.preventDefault();
const agentId = $("agent-id").value.trim();
const kind = $("command-kind").value;
const query = $("command-query").value.trim();
if (!agentId) return;
const payload = buildCommandPayload(kind, query);
const out = $("command-result");
out.textContent = "Running...";
try {
const result = await api(`/api/v1/control/agents/${encodeURIComponent(agentId)}/command`, {
method: "POST",
body: JSON.stringify(payload),
});
out.textContent = JSON.stringify(result, null, 2);
setStatus("ok", "Command OK");
await loadAudit();
} catch (err) {
out.textContent = String(err);
setStatus("bad", "Command Failed");
}
}
async function bootstrap() {
try {
setStatus("warn", "Loading");
await Promise.all([loadAgents(), loadAlerts(), loadAudit()]);
setStatus("ok", "Ready");
} catch (err) {
setStatus("bad", "API Error");
$("audit").textContent = String(err);
}
}
$("refresh-agents").onclick = () => loadAgents().catch((e) => { setStatus("bad", "Agent Error"); console.error(e); });
$("refresh-alerts").onclick = () => loadAlerts().catch((e) => { setStatus("bad", "Alerts Error"); console.error(e); });
$("refresh-audit").onclick = () => loadAudit().catch((e) => { setStatus("bad", "Audit Error"); console.error(e); });
$("command-form").onsubmit = runCommand;
bootstrap();
+63
View File
@@ -0,0 +1,63 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wakey Control UI</title>
<link rel="stylesheet" href="/ui/styles.css" />
</head>
<body>
<header class="topbar">
<h1>Wakey Control UI</h1>
<div id="status-pill" class="pill">Loading</div>
</header>
<main class="layout">
<section class="panel">
<h2>Agents</h2>
<button id="refresh-agents" class="btn">Refresh</button>
<div id="agents" class="list"></div>
</section>
<section class="panel">
<h2>Command Runner</h2>
<form id="command-form" class="stack">
<label>
Agent ID
<input id="agent-id" name="agent_id" type="text" required />
</label>
<label>
Command
<select id="command-kind" name="kind">
<option value="devs">devs</option>
<option value="status">status</option>
<option value="leases">leases</option>
<option value="inventory">inventory</option>
<option value="wake">wake</option>
</select>
</label>
<label>
Query (status/inventory/wake)
<input id="command-query" name="query" type="text" />
</label>
<button class="btn" type="submit">Run</button>
</form>
<pre id="command-result" class="code"></pre>
</section>
<section class="panel">
<h2>Active Alerts</h2>
<button id="refresh-alerts" class="btn">Refresh</button>
<div id="alerts" class="list"></div>
</section>
<section class="panel wide">
<h2>Recent Audit Events</h2>
<button id="refresh-audit" class="btn">Refresh</button>
<pre id="audit" class="code"></pre>
</section>
</main>
<script src="/ui/app.js"></script>
</body>
</html>
+100
View File
@@ -0,0 +1,100 @@
:root {
--bg: #0f172a;
--card: #111827;
--line: #334155;
--text: #e5e7eb;
--muted: #94a3b8;
--ok: #16a34a;
--warn: #d97706;
--bad: #dc2626;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
background: radial-gradient(circle at 15% 15%, #1f2937 0%, var(--bg) 45%);
color: var(--text);
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
backdrop-filter: blur(6px);
background: rgba(15, 23, 42, 0.85);
}
.layout {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1rem;
padding: 1rem;
}
.panel {
background: linear-gradient(180deg, rgba(17,24,39,0.95), rgba(17,24,39,0.7));
border: 1px solid var(--line);
border-radius: 10px;
padding: 0.9rem;
}
.panel.wide { grid-column: 1 / -1; }
.btn {
border: 1px solid var(--line);
background: #1e293b;
color: var(--text);
padding: 0.45rem 0.7rem;
border-radius: 8px;
cursor: pointer;
}
.btn:hover { border-color: #64748b; }
.stack { display: grid; gap: 0.7rem; }
label { display: grid; gap: 0.35rem; font-size: 0.9rem; color: var(--muted); }
input, select {
width: 100%;
padding: 0.45rem 0.55rem;
color: var(--text);
background: #0b1220;
border: 1px solid var(--line);
border-radius: 8px;
}
.list { display: grid; gap: 0.4rem; margin-top: 0.7rem; }
.item {
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.45rem 0.6rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
}
.code {
margin-top: 0.7rem;
background: #020617;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.7rem;
min-height: 120px;
overflow: auto;
}
.pill {
border-radius: 999px;
padding: 0.2rem 0.6rem;
font-size: 0.82rem;
border: 1px solid var(--line);
color: var(--muted);
}
.pill.ok { color: #86efac; border-color: #14532d; }
.pill.warn { color: #fcd34d; border-color: #78350f; }
.pill.bad { color: #fca5a5; border-color: #7f1d1d; }