agent-system/webui/index.html

561 lines
16 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Agent System</title>
<style>
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #101418;
color: #e7edf3;
}
main {
max-width: 1100px;
margin: 0 auto;
padding: 24px;
}
h1 {
margin: 0 0 16px;
font-size: 24px;
font-weight: 650;
}
#logs {
height: 70vh;
overflow-y: auto;
border: 1px solid #2a3540;
background: #0b0f13;
padding: 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 13px;
line-height: 1.45;
white-space: pre-wrap;
}
#services {
border: 1px solid #2a3540;
background: #0b0f13;
padding: 12px;
margin-bottom: 12px;
font-size: 13px;
}
.panel-title {
font-weight: 700;
margin-bottom: 10px;
}
.service {
border-top: 1px solid #202a33;
padding: 10px 0;
}
.service:first-of-type {
border-top: 0;
padding-top: 0;
}
.service-header {
display: flex;
gap: 8px;
align-items: baseline;
justify-content: space-between;
margin-bottom: 6px;
}
.service-name {
font-weight: 700;
}
.service-history {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 6px;
}
.history-entry {
border: 1px solid #2a3540;
padding: 3px 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.run {
border: 1px solid #333;
margin: 10px;
padding: 10px;
}
.run.stopped {
opacity: 0.45;
}
.header {
display: flex;
align-items: center;
gap: 8px;
color: #ffffff;
font-weight: 700;
margin-bottom: 8px;
}
.run-title {
flex: 1;
min-width: 0;
}
.proposal {
border: 1px solid #4c5b2b;
background: #171d12;
color: #edf5d4;
margin: 8px 0;
padding: 10px;
}
.proposal-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.proposal-divider {
border-top: 1px solid #3c4930;
margin: 10px 0;
}
.row {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 8px;
margin-top: 12px;
}
.config-panel {
border: 1px solid #2a3540;
background: #111820;
margin-top: 12px;
padding: 12px;
}
.config-title {
font-weight: 700;
margin-bottom: 10px;
}
.config-grid {
display: grid;
grid-template-columns: auto minmax(96px, 140px) auto;
align-items: center;
gap: 8px;
}
.toggle {
display: flex;
align-items: center;
gap: 6px;
border: 1px solid #354453;
background: #151c23;
color: #e7edf3;
padding: 10px 12px;
white-space: nowrap;
}
.toggle input {
padding: 0;
}
input, button {
font: inherit;
border: 1px solid #354453;
background: #151c23;
color: #e7edf3;
padding: 10px 12px;
}
button {
cursor: pointer;
min-width: 88px;
}
button:hover {
background: #1d2730;
}
.error { color: #ff6b6b; }
.warning { color: #f4a261; }
.info { color: #5dade2; }
.action { color: #58d68d; }
.auto_action { color: #a7f3d0; }
.learning { color: #c4b5fd; }
.log, .result { color: #ccd6df; }
.status.running { color: yellow; }
.status.done { color: green; }
.status.error { color: red; }
.status.stopped { color: gray; }
.health-ok { color: #58d68d; }
.health-warning { color: #f4d03f; }
.health-error { color: #ff6b6b; }
.health-unknown { color: #95a5a6; }
</style>
</head>
<body>
<main>
<h1>Agent System</h1>
<section id="services">
<div class="panel-title">Services</div>
<div id="services-list"></div>
</section>
<div id="logs"></div>
<div class="row">
<input id="cmd" autocomplete="off" placeholder="Command">
<button onclick="send()">Send</button>
<button id="stop">STOP</button>
</div>
<section class="config-panel">
<div class="config-title">Auto Mode Config</div>
<div class="config-grid">
<label class="toggle">
<input id="config-auto-mode" type="checkbox">
auto_mode
</label>
<span></span>
<button id="save-config">Save</button>
<label for="restart-threshold">restart_ha threshold</label>
<input id="restart-threshold" type="number" min="0" max="1" step="0.01">
<span></span>
<label for="network-threshold">check_network threshold</label>
<input id="network-threshold" type="number" min="0" max="1" step="0.01">
<span></span>
</div>
</section>
</main>
<script>
const logs = document.getElementById("logs");
const servicesList = document.getElementById("services-list");
let polling = true;
let currentConfig = {
auto_mode: true,
action_thresholds: {
restart_ha: 0.8,
check_network: 0.9
},
default_threshold: 0.9,
allowed_auto_actions: ["restart_ha"]
};
function eventClass(event) {
const message = event.message || "";
if (event.type === "action") return "action";
if (message.startsWith("[error]")) return "error";
if (message.startsWith("[warning]")) return "warning";
if (message.startsWith("[info]")) return "info";
return event.type || "log";
}
function confidenceLabel(confidence) {
if (typeof confidence !== "number") return "";
return `${Math.round(confidence * 100)}%`;
}
function healthClass(status) {
const normalized = String(status || "unknown").toLowerCase();
if (["ok", "healthy", "up"].includes(normalized)) return "health-ok";
if (["warning", "degraded"].includes(normalized)) return "health-warning";
if (["error", "down", "failed"].includes(normalized)) return "health-error";
return "health-unknown";
}
function formatTime(timestamp) {
if (!timestamp) return "unknown";
return new Date(timestamp * 1000).toLocaleString();
}
async function fetchServices() {
const response = await fetch("/services", {cache: "no-store"});
const services = await response.json();
servicesList.innerHTML = "";
if (!Array.isArray(services) || services.length === 0) {
servicesList.textContent = "No service health data yet.";
return;
}
services.forEach((service) => {
const item = document.createElement("div");
item.className = "service";
const header = document.createElement("div");
header.className = "service-header";
const name = document.createElement("span");
name.className = "service-name";
name.textContent = service.name || "unknown";
header.appendChild(name);
const status = document.createElement("span");
status.className = healthClass(service.status);
status.textContent = String(service.status || "unknown").toUpperCase();
header.appendChild(status);
item.appendChild(header);
const lastCheck = document.createElement("div");
lastCheck.textContent = `Last check: ${formatTime(service.last_check)}`;
item.appendChild(lastCheck);
const history = document.createElement("div");
history.className = "service-history";
(service.history || []).slice(-5).forEach((entry) => {
const node = document.createElement("span");
node.className = `history-entry ${healthClass(entry.status)}`;
node.textContent = `${entry.status || "unknown"} ${formatTime(entry.timestamp)}`;
history.appendChild(node);
});
item.appendChild(history);
servicesList.appendChild(item);
});
}
async function fetchLogs() {
if (!polling) return;
const response = await fetch("/logs", {cache: "no-store"});
const text = await response.text();
logs.innerHTML = "";
const runs = {};
text.trim().split("\n").filter(Boolean).forEach((line) => {
let event;
try {
event = JSON.parse(line);
} catch {
event = {
type: "log",
message: line,
run_id: "legacy",
timestamp: Date.now() / 1000
};
}
const runId = event.run_id || "no-run";
if (!runs[runId]) {
runs[runId] = {
events: [],
status: "running",
received: 0,
expected: 0
};
}
const run = runs[runId];
run.events.push(event);
if (event.type === "run_status") {
run.status = event.status || run.status;
} else if (event.type === "run_progress") {
run.received = Number(event.received) || 0;
run.expected = Number(event.expected) || 0;
}
});
Object.entries(runs).forEach(([runId, runState]) => {
const run = document.createElement("div");
run.className = "run";
const status = runState.status;
if (status === "stopped") {
run.classList.add("stopped");
}
const header = document.createElement("div");
header.className = "header";
const title = document.createElement("div");
title.className = "run-title";
title.appendChild(document.createTextNode(`RUN ${runId} [`));
const statusNode = document.createElement("span");
statusNode.className = `status ${status}`;
statusNode.textContent = status.toUpperCase();
title.appendChild(statusNode);
title.appendChild(
document.createTextNode(`] (${runState.received}/${runState.expected})`)
);
header.appendChild(title);
if (runId !== "no-run" && status === "running") {
const stopButton = document.createElement("button");
stopButton.textContent = "STOP";
stopButton.setAttribute("onclick", `stop(${JSON.stringify(runId)})`);
header.appendChild(stopButton);
}
run.appendChild(header);
const eventList = document.createElement("div");
eventList.className = "events";
runState.events.forEach((event) => {
if (event.type === "proposal") {
const proposal = document.createElement("div");
proposal.className = "proposal";
const message = document.createElement("div");
message.textContent = event.message || "";
proposal.appendChild(message);
const confidence = document.createElement("div");
confidence.textContent = `Confidence: ${confidenceLabel(event.confidence)}`;
proposal.appendChild(confidence);
const actions = document.createElement("div");
actions.className = "proposal-actions";
const allOptions = event.options || event.actions || [];
const options = allOptions.filter((option) => !option.is_ignore).sort(
(a, b) => (b.confidence || 0) - (a.confidence || 0)
);
const ignoreOptions = allOptions.filter((option) => option.is_ignore);
options.forEach((option) => {
const button = document.createElement("button");
const optionConfidence = confidenceLabel(option.confidence);
button.textContent = optionConfidence
? `${option.label || option.command} (${optionConfidence})`
: option.label || option.command;
button.setAttribute(
"onclick",
`apply(${JSON.stringify(runId)}, ${JSON.stringify(option.command)})`
);
actions.appendChild(button);
});
proposal.appendChild(actions);
if (ignoreOptions.length) {
const divider = document.createElement("div");
divider.className = "proposal-divider";
proposal.appendChild(divider);
const ignoreActions = document.createElement("div");
ignoreActions.className = "proposal-actions";
ignoreOptions.forEach((option) => {
const button = document.createElement("button");
button.textContent = option.label || option.command;
button.setAttribute(
"onclick",
`apply(${JSON.stringify(runId)}, ${JSON.stringify(option.command)})`
);
ignoreActions.appendChild(button);
});
proposal.appendChild(ignoreActions);
}
eventList.appendChild(proposal);
return;
}
const div = document.createElement("div");
div.className = eventClass(event);
div.textContent = `${new Date(event.timestamp * 1000).toLocaleTimeString()} ${event.message}`;
eventList.appendChild(div);
});
run.appendChild(eventList);
logs.appendChild(run);
});
logs.scrollTop = logs.scrollHeight;
}
async function send() {
const cmd = document.getElementById("cmd");
const value = cmd.value.trim();
if (!value) return;
await fetch("/command", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({command: value})
});
cmd.value = "";
}
async function stop(run_id) {
await fetch("/stop", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({run_id})
});
}
async function apply(run_id, command) {
await fetch("/action", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({run_id, command})
});
}
function populateConfig(config) {
currentConfig = config;
document.getElementById("config-auto-mode").checked = Boolean(config.auto_mode);
document.getElementById("restart-threshold").value =
config.action_thresholds?.restart_ha ?? "";
document.getElementById("network-threshold").value =
config.action_thresholds?.check_network ?? "";
}
async function loadConfig() {
const response = await fetch("/config", {cache: "no-store"});
populateConfig(await response.json());
}
async function saveConfig() {
const restartThreshold = Number(document.getElementById("restart-threshold").value);
const networkThreshold = Number(document.getElementById("network-threshold").value);
if (!Number.isFinite(restartThreshold) || !Number.isFinite(networkThreshold)) {
return;
}
const config = {
auto_mode: document.getElementById("config-auto-mode").checked,
action_thresholds: {
...(currentConfig.action_thresholds || {}),
restart_ha: restartThreshold,
check_network: networkThreshold
},
default_threshold: currentConfig.default_threshold ?? 0.9,
allowed_auto_actions: currentConfig.allowed_auto_actions || ["restart_ha"]
};
await fetch("/config", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(config)
});
populateConfig(config);
}
document.getElementById("stop").addEventListener("click", async () => {
await fetch("/command", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({command: "stop"})
});
});
document.getElementById("save-config").addEventListener("click", saveConfig);
document.getElementById("cmd").addEventListener("keydown", (event) => {
if (event.key === "Enter") send();
});
loadConfig();
fetchServices();
fetchLogs();
setInterval(fetchLogs, 1000);
setInterval(fetchServices, 10000);
</script>
</body>
</html>