(() => { const errorEl = document.getElementById("error"); const healthEl = document.getElementById("health"); const statusFields = document.getElementById("status-fields"); const pathInput = document.getElementById("file-path"); const fileMeta = document.getElementById("file-meta"); const fileRows = document.getElementById("file-rows"); const shotGallery = document.getElementById("shot-gallery"); const execOut = document.getElementById("exec-out"); const execMeta = document.getElementById("exec-meta"); const startupState = document.getElementById("startup-state"); const startupAdd = document.getElementById("startup-add"); const startupRemove = document.getElementById("startup-remove"); let objectUrls = []; function showError(message) { errorEl.textContent = message || ""; errorEl.classList.toggle("show", Boolean(message)); } async function readError(res) { try { const data = await res.json(); return data.error || res.statusText; } catch { return res.statusText || "request failed"; } } async function api(url, options) { showError(""); const res = await fetch(url, options); if (!res.ok) { throw new Error(await readError(res)); } return res; } function formatBytes(n) { if (n < 1024) return `${n} B`; const units = ["KB", "MB", "GB", "TB"]; let value = n / 1024; let i = 0; while (value >= 1024 && i < units.length - 1) { value /= 1024; i += 1; } return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[i]}`; } function joinPath(base, name) { if (!base) return name; if (/[\\/]$/.test(base)) return base + name; return `${base}\\${name}`; } function parentPath(path) { const trimmed = String(path || "").replace(/[\\/]+$/, ""); const idx = Math.max(trimmed.lastIndexOf("\\"), trimmed.lastIndexOf("/")); if (idx <= 2) return trimmed.slice(0, 3); return trimmed.slice(0, idx); } function revokeShots() { for (const url of objectUrls) URL.revokeObjectURL(url); objectUrls = []; } async function loadHealth() { try { const res = await api("/health"); const data = await res.json(); healthEl.textContent = data.status === "ok" ? "online" : data.status; healthEl.className = "badge ok"; } catch (err) { healthEl.textContent = "offline"; healthEl.className = "badge bad"; showError(err.message); } } async function loadStatus() { const res = await api("/api/v1/status"); const data = await res.json(); const fields = [ ["Hostname", data.hostname], ["User", data.user], ["OS", data.os], ["Architecture", data.architecture], ["Version", data.agent_version], ["Listen", data.listen_address], ["Uptime", `${data.uptime_seconds}s`], ["Local IPs", (data.local_ips || []).join(", ") || "—"], ["Startup", data.startup_enabled ? "enabled" : "disabled"], ]; setStartup(Boolean(data.startup_enabled)); statusFields.replaceChildren( ...fields.flatMap(([label, value]) => { const dt = document.createElement("dt"); dt.textContent = label; const dd = document.createElement("dd"); dd.textContent = value || "—"; return [dt, dd]; }) ); } async function listFiles(path) { const query = new URLSearchParams(); if (path) query.set("path", path); query.set("depth", "0"); const res = await api(`/api/v1/files?${query}`); const data = await res.json(); pathInput.value = data.path || ""; const entries = data.entries || []; fileMeta.textContent = `${entries.length} entries`; fileRows.replaceChildren(); for (const entry of entries) { const tr = document.createElement("tr"); const name = document.createElement("td"); name.className = entry.type === "dir" ? "name" : "name file"; name.textContent = entry.name; name.addEventListener("click", () => { const full = joinPath(data.path, entry.name); if (entry.type === "dir") { listFiles(full).catch((err) => showError(err.message)); } else { const link = document.createElement("a"); link.href = `/api/v1/download?path=${encodeURIComponent(full)}`; link.download = entry.name; document.body.append(link); link.click(); link.remove(); } }); const type = document.createElement("td"); type.textContent = entry.type; const size = document.createElement("td"); size.textContent = entry.type === "dir" ? "—" : formatBytes(entry.size || 0); const modified = document.createElement("td"); modified.textContent = entry.modified_time ? new Date(entry.modified_time).toLocaleString() : ""; tr.append(name, type, size, modified); fileRows.append(tr); } } async function captureScreen() { revokeShots(); shotGallery.replaceChildren(); const format = document.getElementById("shot-format").value; const quality = document.getElementById("shot-quality").value; const res = await api(`/api/v1/screenshot?format=${encodeURIComponent(format)}&quality=${encodeURIComponent(quality)}`); const contentType = res.headers.get("content-type") || ""; if (contentType.includes("application/json")) { const data = await res.json(); for (const image of data.images || []) { addShot(base64Blob(image.data_base64, image.content_type), `Monitor ${image.monitor}`); } return; } addShot(await res.blob(), "Display"); } function base64Blob(data, contentType) { const binary = atob(data); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i += 1) { bytes[i] = binary.charCodeAt(i); } return new Blob([bytes], { type: contentType || "application/octet-stream" }); } function addShot(blob, caption) { const url = URL.createObjectURL(blob); objectUrls.push(url); const figure = document.createElement("figure"); const img = document.createElement("img"); img.src = url; img.alt = caption; const figcaption = document.createElement("figcaption"); figcaption.className = "meta"; figcaption.textContent = caption; figure.append(img, figcaption); shotGallery.append(figure); } function setStartup(enabled) { startupState.textContent = enabled ? "Startup: enabled" : "Startup: disabled"; startupAdd.disabled = enabled; startupRemove.disabled = !enabled; } async function setStartupEnabled(enabled) { const res = await api("/api/v1/startup", { method: enabled ? "POST" : "DELETE" }); const data = await res.json(); setStartup(Boolean(data.startup_enabled)); loadStatus().catch((err) => showError(err.message)); } async function runCommand() { const command = document.getElementById("exec-command").value.trim(); const timeout = Number(document.getElementById("exec-timeout").value); const res = await api("/api/v1/exec", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command, timeout_sec: timeout }), }); const data = await res.json(); execMeta.textContent = `exit code ${data.exit_code}`; const stdout = data.stdout || ""; const stderr = data.stderr || ""; execOut.textContent = [stdout, stderr && `STDERR:\n${stderr}`].filter(Boolean).join("\n\n"); } document.querySelectorAll("nav button").forEach((button) => { button.addEventListener("click", () => { document.querySelectorAll("nav button").forEach((item) => item.classList.remove("active")); document.querySelectorAll(".panel").forEach((panel) => panel.classList.remove("active")); button.classList.add("active"); document.getElementById(button.dataset.tab).classList.add("active"); }); }); document.getElementById("refresh-status").addEventListener("click", () => { Promise.all([loadHealth(), loadStatus()]).catch((err) => showError(err.message)); }); startupAdd.addEventListener("click", () => { setStartupEnabled(true).catch((err) => showError(err.message)); }); startupRemove.addEventListener("click", () => { setStartupEnabled(false).catch((err) => showError(err.message)); }); document.getElementById("file-list").addEventListener("click", () => { listFiles(pathInput.value.trim()).catch((err) => showError(err.message)); }); document.getElementById("file-up").addEventListener("click", () => { listFiles(parentPath(pathInput.value.trim())).catch((err) => showError(err.message)); }); document.getElementById("shot-capture").addEventListener("click", () => { captureScreen().catch((err) => showError(err.message)); }); document.getElementById("exec-run").addEventListener("click", () => { runCommand().catch((err) => showError(err.message)); }); Promise.all([loadHealth(), loadStatus(), listFiles("")]).catch((err) => showError(err.message)); })();