demo a thing WEB UI IS BACK
This commit is contained in:
@@ -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();
|
||||
@@ -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
@@ -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; }
|
||||
Reference in New Issue
Block a user