108 lines
3.4 KiB
JavaScript
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.";
|
|
});
|