Files
agent/public/app.js
T
2026-07-23 09:12:00 +09:00

108 lines
3.4 KiB
JavaScript

const form = document.querySelector("[data-config-form]");
const statusGrid = document.querySelector("[data-status-grid]");
const message = document.querySelector("[data-message]");
const refreshButton = document.querySelector("[data-refresh]");
function getPath(object, path) {
return path.split(".").reduce((current, key) => current?.[key], object);
}
function setPath(object, path, value) {
const parts = path.split(".");
let cursor = object;
for (const part of parts.slice(0, -1)) {
cursor[part] ??= {};
cursor = cursor[part];
}
cursor[parts.at(-1)] = value;
}
function coerceInputValue(input) {
if (input.type === "checkbox") return input.checked;
if (input.type === "number") return Number(input.value);
return input.value.trim();
}
function renderStatus(config, bridge, kasm, camera) {
const items = [
["Agent", config?.server?.publicBaseUrl ?? "online"],
["Workspace", config?.workspace?.root ?? "-"],
["ROS", `${config?.ros?.stack ?? "-"} · ${bridge?.stack?.stack ?? bridge?.health?.stack ?? "offline"}`],
["KasmVNC", kasm?.running ? `running · ${kasm.url}` : kasm?.installed ? "stopped" : "not installed"],
["Camera", camera?.enabled ? `${camera.videoDevice} · enabled` : "disabled"],
];
statusGrid.innerHTML = items.map(([label, value]) => `
<article>
<span>${label}</span>
<strong>${value}</strong>
</article>
`).join("");
}
function fillForm(config) {
for (const input of form.elements) {
if (!input.name) continue;
const value = getPath(config, input.name);
if (input.type === "checkbox") {
input.checked = Boolean(value);
} else if (value !== undefined) {
input.value = String(value);
}
}
}
function readFormPatch() {
const patch = {};
for (const input of form.elements) {
if (!input.name) continue;
setPath(patch, input.name, coerceInputValue(input));
}
return patch;
}
async function json(path, init) {
const response = await fetch(path, init);
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.message || data.detail || `HTTP ${response.status}`);
return data;
}
async function refresh() {
message.textContent = "Loading...";
const [config, bridge, kasm, camera] = await Promise.all([
json("/api/config"),
json("/api/bridge/status").catch((error) => ({ ok: false, health: { error: error.message } })),
json("/api/kasmvnc/status").catch((error) => ({ ok: false, message: error.message })),
json("/api/camera/status").catch((error) => ({ enabled: false, error: error.message })),
]);
fillForm(config);
renderStatus(config, bridge, kasm, camera);
message.textContent = "";
}
form?.addEventListener("submit", async (event) => {
event.preventDefault();
message.textContent = "Saving...";
try {
const config = await json("/api/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(readFormPatch()),
});
fillForm(config);
await refresh();
message.textContent = "Saved.";
} catch (error) {
message.textContent = error instanceof Error ? error.message : "Save failed.";
}
});
refreshButton?.addEventListener("click", () => refresh().catch((error) => {
message.textContent = error instanceof Error ? error.message : "Refresh failed.";
}));
refresh().catch((error) => {
message.textContent = error instanceof Error ? error.message : "Load failed.";
});