tiny update

This commit is contained in:
KingKDot
2026-08-24 09:35:03 -05:00
parent fcc6cc53be
commit 0921e39333
131 changed files with 9948 additions and 1076 deletions
+254 -54
View File
@@ -302,7 +302,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
state === "idle" ? "Stopped" :
"Connecting");
statusEl.innerHTML = `${icons[state] || icons.idle} <span>${label}</span>`;
statusEl.innerHTML = icons[state] || icons.idle;
const labelEl = document.createElement("span");
labelEl.textContent = String(label || "");
statusEl.appendChild(labelEl);
const base = "inline-flex items-center gap-2 px-3 py-2 rounded-full border text-sm";
const styles = {
streaming: "bg-violet-900/40 text-violet-100 border-violet-700/70",
@@ -915,7 +918,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function sendCmd(type, payload) {
@@ -2038,7 +2041,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
const lookupBtn = document.getElementById("backstageLookupBtn");
if (!resultsEl) return;
if (msg.done) {
if (msg?.done) {
if (statusEl) {
statusEl.innerHTML = '<i class="fa-solid fa-check mr-1 text-emerald-400"></i>Search complete';
setTimeout(() => statusEl.classList.add("hidden"), 3000);
@@ -2053,23 +2056,31 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
return;
}
if (msg.path) {
const lookupPath = safeClientString(msg?.path, "", 32768);
if (lookupPath) {
// Remove placeholder if present
const placeholder = resultsEl.querySelector(":scope > div:not([data-lookup-path])");
if (placeholder) placeholder.remove();
const item = document.createElement("button");
item.type = "button";
item.dataset.lookupPath = msg.path;
item.dataset.lookupPath = lookupPath;
item.className = "w-full text-left px-2 py-1.5 rounded text-xs text-slate-200 hover:bg-violet-600/30 hover:text-violet-100 transition-colors font-mono truncate block";
item.textContent = msg.path;
item.title = "Click to start in backstage: " + msg.path;
item.textContent = lookupPath;
item.title = "Click to start in backstage: " + lookupPath;
item.addEventListener("click", () => {
const killCheckbox = document.getElementById("backstageLookupKill");
const killExe = killCheckbox?.checked ? msg.exe : "";
sendCmd("backstage_start_process", { path: '"' + msg.path + '"', kill_exe: killExe });
const killExe = killCheckbox?.checked
? safeClientString(msg?.exe, "", 1024)
: "";
sendCmd("backstage_start_process", { path: '"' + lookupPath + '"', kill_exe: killExe });
item.classList.add("text-emerald-400");
item.innerHTML = '<i class="fa-solid fa-check mr-1"></i>' + (killExe ? '(killed) ' : '') + msg.path;
const icon = document.createElement("i");
icon.className = "fa-solid fa-check mr-1";
item.replaceChildren(
icon,
document.createTextNode((killExe ? "(killed) " : "") + lookupPath),
);
});
resultsEl.appendChild(item);
item.scrollIntoView({ block: "nearest" });
@@ -2096,12 +2107,144 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
"#ec4899", "#06b6d4", "#84cc16", "#f97316", "#6366f1",
];
const WINDOW_MAP_MAX_MONITORS = 64;
const WINDOW_MAP_MAX_WINDOWS = 2000;
const WINDOW_MAP_MAX_COORDINATE = 1000000;
const WINDOW_MAP_MAX_DIMENSION = 100000;
function safeClientString(value, fallback = "", maxLength = 1024) {
let text;
try {
text = String(value ?? fallback);
} catch {
text = String(fallback);
}
return text.slice(0, maxLength);
}
function boundedClientNumber(value, min, max, fallback = 0) {
let number;
try {
number = Number(value);
} catch {
return fallback;
}
if (!Number.isFinite(number)) return fallback;
return Math.min(max, Math.max(min, number));
}
function boundedClientInteger(value, min, max, fallback = 0) {
return Math.trunc(boundedClientNumber(value, min, max, fallback));
}
function normalizeWindowMapPayload(msg) {
const monitorValues = Array.isArray(msg?.monitors)
? msg.monitors.slice(0, WINDOW_MAP_MAX_MONITORS)
: [];
const windowValues = Array.isArray(msg?.windows)
? msg.windows.slice(0, WINDOW_MAP_MAX_WINDOWS)
: [];
const monitors = monitorValues.map((value, position) => {
const monitor = value && typeof value === "object" ? value : {};
return {
index: boundedClientInteger(
monitor.index,
-WINDOW_MAP_MAX_COORDINATE,
WINDOW_MAP_MAX_COORDINATE,
position,
),
name: safeClientString(monitor.name, "", 1024),
x: boundedClientNumber(
monitor.x,
-WINDOW_MAP_MAX_COORDINATE,
WINDOW_MAP_MAX_COORDINATE,
),
y: boundedClientNumber(
monitor.y,
-WINDOW_MAP_MAX_COORDINATE,
WINDOW_MAP_MAX_COORDINATE,
),
width: boundedClientNumber(
monitor.width,
0,
WINDOW_MAP_MAX_DIMENSION,
),
height: boundedClientNumber(
monitor.height,
0,
WINDOW_MAP_MAX_DIMENSION,
),
primary: monitor.primary === true,
};
});
const windows = windowValues.map((value) => {
const windowValue = value && typeof value === "object" ? value : {};
return {
title: safeClientString(windowValue.title, "", 2048),
processName: safeClientString(windowValue.processName, "", 1024),
pid: boundedClientInteger(windowValue.pid, 0, 0xffffffff, 0),
x: boundedClientNumber(
windowValue.x,
-WINDOW_MAP_MAX_COORDINATE,
WINDOW_MAP_MAX_COORDINATE,
),
y: boundedClientNumber(
windowValue.y,
-WINDOW_MAP_MAX_COORDINATE,
WINDOW_MAP_MAX_COORDINATE,
),
width: boundedClientNumber(
windowValue.width,
0,
WINDOW_MAP_MAX_DIMENSION,
),
height: boundedClientNumber(
windowValue.height,
0,
WINDOW_MAP_MAX_DIMENSION,
),
monitor: boundedClientInteger(
windowValue.monitor,
-1,
WINDOW_MAP_MAX_MONITORS - 1,
-1,
),
};
});
return { monitors, windows };
}
function setWindowMapPlaceholder(element, text, { canvas = false, spinner = false } = {}) {
if (!element) return;
const wrapper = document.createElement("div");
wrapper.className = canvas
? "flex items-center justify-center h-40 text-slate-500 text-sm"
: "text-slate-500 text-xs text-center py-2";
if (spinner) {
const icon = document.createElement("i");
icon.className = "fa-solid fa-spinner fa-spin mr-2";
wrapper.appendChild(icon);
}
wrapper.appendChild(document.createTextNode(text));
element.replaceChildren(wrapper);
}
function createWindowMapSpan(className, text = "") {
const span = document.createElement("span");
span.className = className;
span.textContent = safeClientString(text, "", 4096);
return span;
}
function openWindowMap() {
if (!windowMapModal) return;
windowMapModal.style.display = "flex";
sendCmd("backstage_window_list", {});
if (windowMapCanvas) windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Loading…</div>';
if (windowMapList) windowMapList.innerHTML = "";
setWindowMapPlaceholder(windowMapCanvas, "Loading…", { canvas: true, spinner: true });
windowMapList?.replaceChildren();
}
function closeWindowMap() {
@@ -2112,22 +2255,21 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
if (windowMapCloseBtn) windowMapCloseBtn.addEventListener("click", closeWindowMap);
if (windowMapRefreshBtn) windowMapRefreshBtn.addEventListener("click", () => {
sendCmd("backstage_window_list", {});
if (windowMapCanvas) windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Refreshing…</div>';
if (windowMapList) windowMapList.innerHTML = "";
setWindowMapPlaceholder(windowMapCanvas, "Refreshing…", { canvas: true, spinner: true });
windowMapList?.replaceChildren();
});
if (windowMapModal) windowMapModal.addEventListener("click", (e) => {
if (e.target === windowMapModal) closeWindowMap();
});
function handleWindowListResult(msg) {
const monitors = msg.monitors || [];
const windows = msg.windows || [];
const { monitors, windows } = normalizeWindowMapPayload(msg);
if (!windowMapCanvas || !windowMapList) return;
if (monitors.length === 0 && windows.length === 0) {
windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm">No windows or monitors found</div>';
windowMapList.innerHTML = '<div class="text-slate-500 text-xs text-center py-2">No visible windows on the hidden desktop</div>';
setWindowMapPlaceholder(windowMapCanvas, "No windows or monitors found", { canvas: true });
setWindowMapPlaceholder(windowMapList, "No visible windows on the hidden desktop");
return;
}
@@ -2159,58 +2301,116 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
function ty(y) { return padY + (y - minY) * scale; }
function tw(w) { return w * scale; }
let html = "";
windowMapCanvas.style.height = mapH + "px";
const canvasFragment = document.createDocumentFragment();
for (let i = 0; i < monitors.length; i++) {
const m = monitors[i];
const x = tx(m.x), y = ty(m.y), w = tw(m.width), h = tw(m.height);
html += `<div class="absolute border-2 border-dashed border-slate-600 rounded" style="left:${x}px;top:${y}px;width:${w}px;height:${h}px;" title="Monitor ${m.index}: ${m.name} (${m.width}x${m.height})">
<span class="absolute top-1 left-1.5 text-[10px] font-mono text-slate-500">${m.primary ? "★ " : ""}${m.name || "Monitor " + m.index}</span>
<span class="absolute bottom-1 right-1.5 text-[10px] font-mono text-slate-600">${m.width}×${m.height}</span>
</div>`;
const monitorBox = document.createElement("div");
monitorBox.className = "absolute border-2 border-dashed border-slate-600 rounded";
monitorBox.style.left = `${x}px`;
monitorBox.style.top = `${y}px`;
monitorBox.style.width = `${w}px`;
monitorBox.style.height = `${h}px`;
monitorBox.title = `Monitor ${m.index}: ${m.name} (${m.width}x${m.height})`;
monitorBox.append(
createWindowMapSpan(
"absolute top-1 left-1.5 text-[10px] font-mono text-slate-500",
`${m.primary ? "★ " : ""}${m.name || "Monitor " + m.index}`,
),
createWindowMapSpan(
"absolute bottom-1 right-1.5 text-[10px] font-mono text-slate-600",
`${m.width}×${m.height}`,
),
);
canvasFragment.appendChild(monitorBox);
}
for (let i = 0; i < windows.length; i++) {
const w = windows[i];
const mappedWindow = windows[i];
const color = WINDOW_COLORS[i % WINDOW_COLORS.length];
const x = tx(w.x), y = ty(w.y), ww = Math.max(tw(w.width), 8), wh = Math.max(tw(w.height), 8);
const shortTitle = w.title.length > 30 ? w.title.slice(0, 28) + "…" : w.title;
html += `<div class="absolute rounded border overflow-hidden cursor-default" style="left:${x}px;top:${y}px;width:${ww}px;height:${wh}px;border-color:${color};background:${color}18;" title="${escHtml(w.title)}\n${w.processName} (PID ${w.pid})\nPosition: ${w.x},${w.y} Size: ${w.width}×${w.height}\nMonitor: ${w.monitor >= 0 ? w.monitor : "none"}">
${wh > 20 && ww > 50 ? `<div class="px-1 py-0.5 text-[9px] font-mono text-slate-200 truncate" style="background:${color}50;">${escHtml(shortTitle)}</div>` : ""}
</div>`;
const x = tx(mappedWindow.x), y = ty(mappedWindow.y);
const width = Math.max(tw(mappedWindow.width), 8);
const height = Math.max(tw(mappedWindow.height), 8);
const shortTitle = mappedWindow.title.length > 30
? mappedWindow.title.slice(0, 28) + "…"
: mappedWindow.title;
const windowBox = document.createElement("div");
windowBox.className = "absolute rounded border overflow-hidden cursor-default";
windowBox.style.left = `${x}px`;
windowBox.style.top = `${y}px`;
windowBox.style.width = `${width}px`;
windowBox.style.height = `${height}px`;
windowBox.style.borderColor = color;
windowBox.style.background = `${color}18`;
windowBox.title = [
mappedWindow.title,
`${mappedWindow.processName} (PID ${mappedWindow.pid})`,
`Position: ${mappedWindow.x},${mappedWindow.y} Size: ${mappedWindow.width}×${mappedWindow.height}`,
`Monitor: ${mappedWindow.monitor >= 0 ? mappedWindow.monitor : "none"}`,
].join("\n");
if (height > 20 && width > 50) {
const title = document.createElement("div");
title.className = "px-1 py-0.5 text-[9px] font-mono text-slate-200 truncate";
title.style.background = `${color}50`;
title.textContent = shortTitle;
windowBox.appendChild(title);
}
canvasFragment.appendChild(windowBox);
}
windowMapCanvas.innerHTML = html;
let listHtml = `<div class="grid gap-1">
<div class="grid grid-cols-[auto_1fr_auto_auto_auto] gap-x-3 px-2 py-1 text-[11px] uppercase tracking-wide text-slate-500 border-b border-slate-800">
<span></span><span>Title</span><span>Process</span><span>Position</span><span>Monitor</span>
</div>`;
for (let i = 0; i < windows.length; i++) {
const w = windows[i];
const color = WINDOW_COLORS[i % WINDOW_COLORS.length];
const monLabel = w.monitor >= 0 ? (monitors[w.monitor] ? (monitors[w.monitor].name || "#" + w.monitor) : "#" + w.monitor) : "—";
listHtml += `<div class="grid grid-cols-[auto_1fr_auto_auto_auto] gap-x-3 items-center px-2 py-1.5 rounded hover:bg-slate-800/50 text-xs text-slate-300">
<span class="w-2.5 h-2.5 rounded-sm" style="background:${color};"></span>
<span class="truncate font-medium" title="${escHtml(w.title)}">${escHtml(w.title)}</span>
<span class="font-mono text-slate-400">${escHtml(w.processName)} <span class="text-slate-600">(${w.pid})</span></span>
<span class="font-mono text-slate-500">${w.x},${w.y} ${w.width}×${w.height}</span>
<span class="font-mono ${w.monitor >= 0 ? "text-slate-300" : "text-rose-400"}">${monLabel}</span>
</div>`;
}
listHtml += "</div>";
windowMapCanvas.replaceChildren(canvasFragment);
if (windows.length === 0) {
listHtml = '<div class="text-slate-500 text-xs text-center py-2">No visible windows on the hidden desktop</div>';
setWindowMapPlaceholder(windowMapList, "No visible windows on the hidden desktop");
return;
}
windowMapList.innerHTML = listHtml;
}
const list = document.createElement("div");
list.className = "grid gap-1";
const header = document.createElement("div");
header.className = "grid grid-cols-[auto_1fr_auto_auto_auto] gap-x-3 px-2 py-1 text-[11px] uppercase tracking-wide text-slate-500 border-b border-slate-800";
header.append(
createWindowMapSpan("", ""),
createWindowMapSpan("", "Title"),
createWindowMapSpan("", "Process"),
createWindowMapSpan("", "Position"),
createWindowMapSpan("", "Monitor"),
);
list.appendChild(header);
function escHtml(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
for (let i = 0; i < windows.length; i++) {
const mappedWindow = windows[i];
const color = WINDOW_COLORS[i % WINDOW_COLORS.length];
const monitorLabel = mappedWindow.monitor >= 0
? (monitors[mappedWindow.monitor]?.name || `#${mappedWindow.monitor}`)
: "—";
const row = document.createElement("div");
row.className = "grid grid-cols-[auto_1fr_auto_auto_auto] gap-x-3 items-center px-2 py-1.5 rounded hover:bg-slate-800/50 text-xs text-slate-300";
const colorMarker = createWindowMapSpan("w-2.5 h-2.5 rounded-sm", "");
colorMarker.style.background = color;
const title = createWindowMapSpan("truncate font-medium", mappedWindow.title);
title.title = mappedWindow.title;
const process = createWindowMapSpan("font-mono text-slate-400", mappedWindow.processName + " ");
process.appendChild(createWindowMapSpan("text-slate-600", `(${mappedWindow.pid})`));
row.append(
colorMarker,
title,
process,
createWindowMapSpan(
"font-mono text-slate-500",
`${mappedWindow.x},${mappedWindow.y} ${mappedWindow.width}×${mappedWindow.height}`,
),
createWindowMapSpan(
`font-mono ${mappedWindow.monitor >= 0 ? "text-slate-300" : "text-rose-400"}`,
monitorLabel,
),
);
list.appendChild(row);
}
windowMapList.replaceChildren(list);
}
connectWs();
+15 -9
View File
@@ -9,6 +9,7 @@ import {
staggerChildren,
} from "./motion.js";
import { isLocalServerAddress } from "./build-server-url.js";
import { escapeHtml } from "./format.js";
const form = document.getElementById("build-form");
const buildBtn = document.getElementById("build-btn");
@@ -2912,25 +2913,29 @@ if (buildUpdateAllBtn) {
if (!res.ok) {
const data = await res.json().catch(() => ({}));
updateAllModalBody.innerHTML = `<p class="text-red-400"><i class="fa-solid fa-circle-xmark mr-2"></i>${data.error || "Failed to check eligible clients"}</p>`;
updateAllModalBody.innerHTML = `<p class="text-red-400"><i class="fa-solid fa-circle-xmark mr-2"></i>${escapeHtml(data.error || "Failed to check eligible clients")}</p>`;
return;
}
const data = await res.json();
const eligible = Math.max(0, Math.trunc(Number(data.eligible) || 0));
const totalOnline = Math.max(0, Math.trunc(Number(data.totalOnline) || 0));
const skippedInMemory = Math.max(0, Math.trunc(Number(data.skippedInMemory) || 0));
const skippedNoMatch = Math.max(0, Math.trunc(Number(data.skippedNoMatch) || 0));
let html = "";
html += `<p class="text-white"><i class="fa-solid fa-users mr-2 text-amber-400"></i><strong>${data.eligible}</strong> client(s) will receive the update after build.</p>`;
html += `<p class="text-white"><i class="fa-solid fa-users mr-2 text-amber-400"></i><strong>${eligible}</strong> client(s) will receive the update after build.</p>`;
html += '<div class="mt-2 text-xs text-slate-400 space-y-1">';
html += `<p><i class="fa-solid fa-globe mr-1 text-blue-400"></i> ${data.totalOnline} total online client(s)</p>`;
if (data.skippedInMemory > 0) {
html += `<p><i class="fa-solid fa-memory mr-1 text-red-400"></i> ${data.skippedInMemory} client(s) will be skipped (running in-memory)</p>`;
html += `<p><i class="fa-solid fa-globe mr-1 text-blue-400"></i> ${totalOnline} total online client(s)</p>`;
if (skippedInMemory > 0) {
html += `<p><i class="fa-solid fa-memory mr-1 text-red-400"></i> ${skippedInMemory} client(s) will be skipped (running in-memory)</p>`;
}
if (data.skippedNoMatch > 0) {
html += `<p><i class="fa-solid fa-ban mr-1 text-slate-500"></i> ${data.skippedNoMatch} client(s) will be skipped (no matching platform)</p>`;
if (skippedNoMatch > 0) {
html += `<p><i class="fa-solid fa-ban mr-1 text-slate-500"></i> ${skippedNoMatch} client(s) will be skipped (no matching platform)</p>`;
}
html += "</div>";
if (data.eligible === 0 && data.totalOnline === 0) {
if (eligible === 0 && totalOnline === 0) {
html += '<p class="mt-3 text-xs text-slate-500">No clients are currently online. The build will still run but no updates will be sent.</p>';
}
@@ -2939,7 +2944,8 @@ if (buildUpdateAllBtn) {
updateAllModalBody.innerHTML = html;
} catch (err) {
updateAllModalBody.innerHTML = `<p class="text-red-400"><i class="fa-solid fa-circle-xmark mr-2"></i>Error: ${err.message}</p>`;
const message = err instanceof Error ? err.message : String(err || "Unknown error");
updateAllModalBody.innerHTML = `<p class="text-red-400"><i class="fa-solid fa-circle-xmark mr-2"></i>Error: ${escapeHtml(message)}</p>`;
}
});
}
+1 -1
View File
@@ -23,7 +23,7 @@ const ROLE_COLORS = {
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function formatTime(ts) {
@@ -71,7 +71,8 @@ function escapeHtml(value) {
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function setSummary(summary) {
@@ -121,9 +121,26 @@ function pushRecent(href) {
localStorage.setItem(RECENT_KEY, JSON.stringify(list.slice(0, RECENT_MAX)));
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function safeClassTokens(value, fallback = "") {
const tokens = String(value || "")
.split(/\s+/)
.filter((token) => /^[A-Za-z0-9_-]+$/.test(token));
return tokens.length ? tokens.join(" ") : fallback;
}
function flagFor(country) {
if (!country || country.length !== 2) return "";
return `<span class="fi fi-${country.toLowerCase()}" style="margin-right:6px;border-radius:2px;"></span>`;
const code = String(country || "").trim().toUpperCase();
if (!/^[A-Z]{2}$/.test(code)) return "";
return `<span class="fi fi-${code.toLowerCase()}" style="margin-right:6px;border-radius:2px;"></span>`;
}
function clientLabel(c) {
@@ -163,15 +180,17 @@ function buildResults(query, pages, clients) {
}
function makeIcon(faClass, colorClass) {
return `<i class="fa-solid ${faClass} ${colorClass || "text-slate-300"}" style="width:18px;text-align:center;"></i>`;
const icon = safeClassTokens(faClass, "fa-circle-question");
const color = safeClassTokens(colorClass, "text-slate-300");
return `<i class="fa-solid ${icon} ${color}" style="width:18px;text-align:center;"></i>`;
}
function renderRow(r, active) {
const activeCls = active ? "cmdp-row cmdp-row-active" : "cmdp-row";
if (r.kind === "page") {
return `<div class="${activeCls}" data-href="${r.href}">
return `<div class="${activeCls}" data-href="${escapeHtml(r.href)}">
${makeIcon(r.icon, r.color)}
<span class="cmdp-label">${r.label}</span>
<span class="cmdp-label">${escapeHtml(r.label)}</span>
<span class="cmdp-kind">page</span>
</div>`;
}
@@ -179,26 +198,29 @@ function renderRow(r, active) {
const c = r.client;
const dot = c.online ? "background:#10b981" : "background:#475569";
const os = osBadge(c.os || "");
return `<div class="${activeCls}" data-client="${c.id}">
<i class="fa ${os.icon} cv-tone-${os.tone}" style="width:18px;text-align:center;"></i>
const osIcon = safeClassTokens(os.icon, "fa-solid fa-circle-question");
const osTone = safeClassTokens(os.tone, "pill-unknown");
return `<div class="${activeCls}" data-client="${escapeHtml(c.id)}">
<i class="fa ${osIcon} cv-tone-${osTone}" style="width:18px;text-align:center;"></i>
<span class="cmdp-status-dot" style="${dot}"></span>
${flagFor(c.country)}
<span class="cmdp-label">${r.label}</span>
<span class="cmdp-label">${escapeHtml(r.label)}</span>
<span class="cmdp-kind">${c.online ? "online" : "offline"}</span>
</div>`;
}
if (r.kind === "action") {
return `<div class="${activeCls}" data-action="${r.action.key}">
return `<div class="${activeCls}" data-action="${escapeHtml(r.action.key)}">
${makeIcon(r.action.icon, r.action.color)}
<span class="cmdp-label">${r.action.label}</span>
<span class="cmdp-label">${escapeHtml(r.action.label)}</span>
<span class="cmdp-kind">needs client</span>
</div>`;
}
if (r.kind === "client-action") {
const c = r.client;
return `<div class="${activeCls}" data-href="${r.action.href(c.id)}">
const encodedId = encodeURIComponent(String(c.id || ""));
return `<div class="${activeCls}" data-href="${escapeHtml(r.action.href(encodedId))}">
${makeIcon(r.action.icon, r.action.color)}
<span class="cmdp-label">${r.action.label} · ${clientLabel(c)}</span>
<span class="cmdp-label">${escapeHtml(`${r.action.label} · ${clientLabel(c)}`)}</span>
<span class="cmdp-kind">action</span>
</div>`;
}
@@ -320,7 +342,7 @@ function renderList() {
}
let html = "";
if (state.mode === "client-actions" && state.pendingAction) {
html += `<div class="cmdp-section">Pick a client for: ${state.pendingAction.label}</div>`;
html += `<div class="cmdp-section">Pick a client for: ${escapeHtml(state.pendingAction.label)}</div>`;
}
html += state.results.map((r, i) => renderRow(r, i === state.active)).join("");
list.innerHTML = html;
@@ -336,7 +358,12 @@ function activate(idx, newTab) {
const r = state.results[idx];
if (!r) return;
if (r.kind === "page" || r.kind === "client-action") {
navigate(r.kind === "page" ? r.href : r.action.href(r.client.id), newTab);
navigate(
r.kind === "page"
? r.href
: r.action.href(encodeURIComponent(String(r.client.id || ""))),
newTab,
);
return;
}
if (r.kind === "client") {
+2 -1
View File
@@ -106,7 +106,8 @@ function tryFit() {
function setStatus(label, tone = "pill-offline") {
if (!statusPill) return;
statusPill.className = `pill ${tone}`;
statusPill.innerHTML = `<i class="fa-solid fa-circle"></i> ${label}`;
statusPill.innerHTML = '<i class="fa-solid fa-circle"></i>';
statusPill.appendChild(document.createTextNode(` ${String(label || "")}`));
}
function writeSystem(msg) {
@@ -80,5 +80,5 @@ function showAccessDenied(reasons) {
function escapeHtml(text) {
const el = document.createElement("span");
el.textContent = text;
return el.innerHTML;
return el.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
@@ -131,5 +131,5 @@ export function formatBytes(bytes) {
export function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
+1 -1
View File
@@ -1,7 +1,7 @@
export function escapeHtml(value) {
const div = document.createElement("div");
div.textContent = value == null ? "" : String(value);
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
export function formatBytes(bytes, fractionDigits = 2) {
+4 -4
View File
@@ -342,14 +342,14 @@ function displayFileList(files) {
` : ""}
<button
class="view-log-btn px-3 py-2 rounded-lg border border-blue-700 bg-blue-900/50 hover:bg-blue-800/70 text-blue-100 flex items-center gap-2"
data-filename="${file.name}"
data-filename="${escapeHtml(file.name)}"
>
<i class="fa-solid fa-eye"></i>
<span>View</span>
</button>
<button
class="download-log-btn px-3 py-2 rounded-lg border border-emerald-700 bg-emerald-900/40 hover:bg-emerald-800/70 text-emerald-100 flex items-center gap-2"
data-filename="${file.name}"
data-filename="${escapeHtml(file.name)}"
>
<i class="fa-solid fa-download"></i>
<span>Download</span>
@@ -357,7 +357,7 @@ function displayFileList(files) {
${archiveMode ? "" : `
<button
class="delete-log-btn px-3 py-2 rounded-lg border border-red-700 bg-red-900/40 hover:bg-red-800/70 text-red-100 flex items-center gap-2"
data-filename="${file.name}"
data-filename="${escapeHtml(file.name)}"
>
<i class="fa-solid fa-trash"></i>
<span>Delete</span>
@@ -428,7 +428,7 @@ function formatDate(dateStr) {
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text || "";
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function resetSearch() {
+1 -1
View File
@@ -47,7 +47,7 @@ function formatTimestamp(ts) {
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text || "";
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function parseDetails(details) {
+1 -1
View File
@@ -1801,7 +1801,7 @@ async function openGroupPicker(clientId) {
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function renderClientLogs(logs) {
+1 -1
View File
@@ -533,7 +533,7 @@ function formatBytes(bytes) {
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text == null ? "" : String(text);
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function formatDuration(ms) {
@@ -10,7 +10,7 @@ export async function loadPluginNavItems(activeMap) {
if (!plugins || !Array.isArray(plugins)) return;
const navPlugins = plugins.filter(
(p) => p.navbar && p.enabled
(p) => p && typeof p === "object" && p.navbar && p.enabled
);
if (navPlugins.length === 0) return;
@@ -28,11 +28,17 @@ export async function loadPluginNavItems(activeMap) {
if (!childContainer) return;
const itemsHtml = navPlugins.map((p) => {
const linkId = `plugin-nav-${p.id}`;
activeMap[`/plugins/${p.id}`] = linkId;
const pluginId = String(p.id || "").trim();
if (
!/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,127}$/.test(pluginId) ||
pluginId.includes("..") ||
pluginId.endsWith(".")
) return "";
const linkId = `plugin-nav-${pluginId}`;
activeMap[`/plugins/${pluginId}`] = linkId;
const childObj = {
href: `/plugins/${p.id}`,
href: `/plugins/${pluginId}`,
label: p.navbar.label || p.name,
icon: p.navbar.icon || "fa-cube",
iconColor: "text-slate-300",
+39 -8
View File
@@ -81,16 +81,45 @@ export const NAV_GROUPS = [
TOPBAR — dropdown menus
────────────────────────────────────────────── */
function escapeNavHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function safeNavHref(value) {
const href = String(value ?? "").trim();
if (!href.startsWith("/") || href.startsWith("//") || /[\u0000-\u001f\u007f]/.test(href)) {
return "#";
}
return escapeNavHtml(href);
}
function safeNavId(value) {
return String(value ?? "").replace(/[^A-Za-z0-9_.:-]/g, "-").slice(0, 192);
}
function safeNavClassList(value) {
return String(value ?? "")
.split(/\s+/)
.filter((token) => /^[A-Za-z0-9_-]+$/.test(token))
.join(" ");
}
export function dropdownItem(child) {
const hiddenCls = child.hidden ? " hidden" : "";
const prefetchAttr = child.turboPrefetch === false ? ' data-turbo-prefetch="false"' : "";
const badgeHtml = child.hasBadge
? `<span id="enrollment-badge" class="nav-dd-badge hidden"></span>`
: "";
return `<a href="${child.href}" id="${child.linkId}"${prefetchAttr}
class="nav-dd-item${hiddenCls}" data-link-id="${child.linkId}">
<i class="fa-solid ${child.icon} ${child.iconColor} nav-dd-item-icon"></i>
<span>${child.label}</span>
const linkId = safeNavId(child.linkId);
return `<a href="${safeNavHref(child.href)}" id="${linkId}"${prefetchAttr}
class="nav-dd-item${hiddenCls}" data-link-id="${linkId}">
<i class="fa-solid ${safeNavClassList(child.icon)} ${safeNavClassList(child.iconColor)} nav-dd-item-icon"></i>
<span>${escapeNavHtml(child.label)}</span>
${badgeHtml}
</a>`;
}
@@ -241,10 +270,12 @@ export function sidebarChild(child) {
const badgeHtml = child.hasBadge
? `<span id="enrollment-badge" class="sb-badge hidden"></span>`
: "";
return `<a href="${child.href}" id="${child.linkId}"
class="sb-link sb-link-child${hiddenCls}" data-link-id="${child.linkId}" title="${child.label}">
<i class="fa-solid ${child.icon} ${child.iconColor} sb-icon"></i>
<span class="sb-text">${child.label}</span>
const linkId = safeNavId(child.linkId);
const label = escapeNavHtml(child.label);
return `<a href="${safeNavHref(child.href)}" id="${linkId}"
class="sb-link sb-link-child${hiddenCls}" data-link-id="${linkId}" title="${label}">
<i class="fa-solid ${safeNavClassList(child.icon)} ${safeNavClassList(child.iconColor)} sb-icon"></i>
<span class="sb-text">${label}</span>
${badgeHtml}
</a>`;
}
+42 -74
View File
@@ -79,6 +79,8 @@ const previewModalImg = document.getElementById("notification-preview-image");
const previewModalClose = document.getElementById("notification-preview-close");
const MAX_ROWS = 200;
const MAX_CACHED_PREVIEW_OBJECT_URLS = 8;
const previewObjectUrls = new Map();
// Defaults loaded from /api/notifications/my-settings
let defaultWebhookTemplate = "";
@@ -225,7 +227,7 @@ function formatTime(ts) {
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text == null ? "" : String(text);
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function wireToggle(inputEl, btnEl) {
@@ -459,26 +461,50 @@ function previewFormatter(cell) {
return wrapper;
}
mountPreviewLoader(wrapper, item, row.isLive);
return wrapper;
}
function rememberPreviewObjectUrl(notificationId, objectUrl) {
const existing = previewObjectUrls.get(notificationId);
if (existing && existing !== objectUrl) URL.revokeObjectURL(existing);
previewObjectUrls.delete(notificationId);
previewObjectUrls.set(notificationId, objectUrl);
while (previewObjectUrls.size > MAX_CACHED_PREVIEW_OBJECT_URLS) {
const oldestId = previewObjectUrls.keys().next().value;
const oldestUrl = previewObjectUrls.get(oldestId);
previewObjectUrls.delete(oldestId);
if (oldestUrl) URL.revokeObjectURL(oldestUrl);
}
}
function mountPreviewLoader(wrapper, item, isLive) {
const notificationId = item?.id || "";
const skipFetch = !notificationId || (!row.isLive && !item?.screenshotId);
const skipFetch = !notificationId || (!isLive && !item?.screenshotId);
if (skipFetch) {
wrapper.textContent = "-";
wrapper.className = "text-slate-500";
return wrapper;
return;
}
if (item._previewObjectUrl) {
const img = createPreviewImage(item._previewObjectUrl);
wrapper.appendChild(img);
return wrapper;
const cachedUrl = previewObjectUrls.get(notificationId);
if (cachedUrl) {
wrapper.appendChild(createPreviewImage(cachedUrl));
return;
}
wrapper.textContent = "Loading...";
wrapper.className = "text-slate-500";
const loadButton = document.createElement("button");
loadButton.type = "button";
loadButton.className = "text-xs px-2 py-1 rounded border border-slate-700 bg-slate-900/80 hover:bg-slate-800 text-slate-300";
loadButton.textContent = "View image";
wrapper.appendChild(loadButton);
let attempts = 0;
const maxAttempts = 5;
const fetchPreview = async () => {
if (!wrapper.isConnected) return;
attempts += 1;
loadButton.disabled = true;
loadButton.textContent = attempts > 1 ? `Retrying (${attempts}/${maxAttempts})...` : "Loading...";
try {
const url = `/api/notifications/${encodeURIComponent(notificationId)}/screenshot?ts=${Date.now()}`;
const res = await fetch(url, { cache: "no-store" });
@@ -493,7 +519,7 @@ function previewFormatter(cell) {
}
const blob = await res.blob();
const objectUrl = URL.createObjectURL(blob);
item._previewObjectUrl = objectUrl;
rememberPreviewObjectUrl(notificationId, objectUrl);
wrapper.innerHTML = "";
wrapper.className = "";
wrapper.appendChild(createPreviewImage(objectUrl));
@@ -506,8 +532,7 @@ function previewFormatter(cell) {
wrapper.className = "text-slate-500";
}
};
fetchPreview();
return wrapper;
loadButton.addEventListener("click", fetchPreview, { once: true });
}
function createPreviewImage(objectUrl) {
@@ -524,7 +549,6 @@ function createPreviewImage(objectUrl) {
previewModal.classList.remove("hidden");
previewModal.classList.add("flex");
});
if (img.decode) img.decode().catch(() => {});
return img;
}
@@ -549,67 +573,7 @@ function createNotificationRowElement(item, isLive) {
const preview = row.querySelector(".preview-slot");
if (preview) {
const notificationId = item?.id || "";
const skipFetch = !notificationId || (!isLive && !item?.screenshotId);
if (skipFetch) {
preview.textContent = "-";
preview.className = "text-slate-500";
} else {
preview.textContent = "Loading...";
preview.className = "text-slate-500";
const img = document.createElement("img");
img.className = "max-h-32 w-auto rounded border border-slate-800/80 cursor-zoom-in";
img.loading = "lazy";
img.alt = "Notification screenshot";
let attempts = 0;
const maxAttempts = 5;
const fetchPreview = async () => {
attempts += 1;
try {
const url = `/api/notifications/${encodeURIComponent(notificationId)}/screenshot?ts=${Date.now()}`;
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) {
if ((res.status === 202 || res.status === 404) && attempts < maxAttempts) {
setTimeout(fetchPreview, 1000 * attempts);
return;
}
preview.textContent = "-";
preview.className = "text-slate-500";
return;
}
const blob = await res.blob();
const objectUrl = URL.createObjectURL(blob);
img.classList.add("opacity-0");
preview.innerHTML = "";
preview.className = "";
preview.appendChild(img);
img.addEventListener("load", () => img.classList.remove("opacity-0"));
img.addEventListener("error", () => {
preview.textContent = "-";
preview.className = "text-slate-500";
URL.revokeObjectURL(objectUrl);
});
img.src = objectUrl;
img.dataset.previewUrl = objectUrl;
if (img.decode) img.decode().catch(() => {});
img.addEventListener("click", () => {
if (!previewModal || !previewModalImg) return;
previewModalImg.src = img.dataset.previewUrl || objectUrl;
previewModal.classList.remove("hidden");
previewModal.classList.add("flex");
});
} catch {
if (attempts < maxAttempts) {
setTimeout(fetchPreview, 1000 * attempts);
return;
}
preview.textContent = "-";
preview.className = "text-slate-500";
}
};
fetchPreview();
}
mountPreviewLoader(preview, item, isLive);
}
return row;
@@ -967,6 +931,10 @@ const clearIfActive = () => {
};
document.addEventListener("visibilitychange", clearIfActive);
window.addEventListener("focus", clearIfActive);
window.addEventListener("beforeunload", () => {
for (const objectUrl of previewObjectUrls.values()) URL.revokeObjectURL(objectUrl);
previewObjectUrls.clear();
});
clearIfActive();
function updateDesktopPermissionUi() {
+4 -3
View File
@@ -362,7 +362,8 @@ function escapeHtml(value) {
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
async function setPluginEnabled(plugin, enabled, confirmed = false) {
@@ -441,8 +442,8 @@ function showPluginEnableConfirmModal(plugin, sigInfo) {
<h3 class="text-lg font-semibold text-slate-100">Enable Unverified Plugin</h3>
</div>
<p class="text-sm text-slate-300 mb-2">${statusText}</p>
<p class="text-sm text-slate-400 mb-1">Plugin: <strong class="text-slate-200">${plugin.name || plugin.id}</strong></p>
${sig.fingerprint ? `<p class="text-xs text-slate-500 font-mono mb-3">Signer: ${sig.fingerprint}</p>` : '<p class="text-xs text-slate-500 mb-3">No signature present.</p>'}
<p class="text-sm text-slate-400 mb-1">Plugin: <strong class="text-slate-200">${escapeHtml(plugin.name || plugin.id)}</strong></p>
${sig.fingerprint ? `<p class="text-xs text-slate-500 font-mono mb-3">Signer: ${escapeHtml(sig.fingerprint)}</p>` : '<p class="text-xs text-slate-500 mb-3">No signature present.</p>'}
<p class="text-sm text-slate-400" style="margin-bottom: 16px;">Type <strong class="text-emerald-300">confirm</strong> below to enable this plugin:</p>
<input type="text" id="plugin-enable-confirm-input" class="w-full px-3 py-2 rounded-lg bg-slate-800 border border-slate-600 text-slate-100 text-sm focus:outline-none focus:border-emerald-500" style="margin-bottom: 16px;" placeholder="Type confirm" autocomplete="off" spellcheck="false" />
<div class="plugin-modal-actions flex justify-end gap-2">
+3 -2
View File
@@ -75,7 +75,8 @@ function updateStatus(state, text) {
disconnected: '<i class="fa-solid fa-circle text-slate-500"></i>',
};
statusEl.innerHTML = `${icons[state] || icons.disconnected} ${text}`;
statusEl.innerHTML = icons[state] || icons.disconnected;
statusEl.appendChild(document.createTextNode(` ${String(text || "")}`));
const stateClasses = {
connected: "bg-emerald-500/10 text-emerald-300 border-emerald-500/30",
error: "bg-red-500/10 text-red-300 border-red-500/30",
@@ -491,7 +492,7 @@ function formatBytes(bytes) {
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
// Context menu
@@ -310,9 +310,12 @@ function actionButtons(c) {
}
function esc(s) {
const d = document.createElement("div");
d.textContent = String(s ?? "");
return d.innerHTML;
return String(s ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function dedupeGpu(raw) {
@@ -852,7 +852,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
error: '<i class="fa-solid fa-circle-exclamation text-rose-400"></i>',
};
const label = streamStatusLabel;
statusEl.innerHTML = `${icons[state] || icons.idle} <span>${label}</span>`;
statusEl.innerHTML = icons[state] || icons.idle;
const labelEl = document.createElement("span");
labelEl.textContent = String(label || "");
statusEl.appendChild(labelEl);
const base = "inline-flex items-center gap-2 px-3 py-2 rounded-full border text-sm";
const styles = {
streaming: "bg-emerald-900/40 text-emerald-100 border-emerald-700/70",
@@ -1606,12 +1609,12 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
accessibility: "Accessibility",
fullDiskAccess: "Full Disk Access",
};
const list = missing.map(k => labels[k] || k).join(", ");
const list = missing.map((key) => labels[String(key)] || String(key)).join(", ");
banner.innerHTML = `
<div class="flex items-center gap-2">
<i class="fa-solid fa-triangle-exclamation text-amber-400"></i>
<span><strong>macOS permissions missing:</strong> ${list}</span>
<span><strong>macOS permissions missing:</strong> <span class="rd-elevate-missing"></span></span>
</div>
<div class="text-xs text-amber-300/80">
The client needs elevated privileges to grant these permissions. Enter the user's password to elevate.
@@ -1626,6 +1629,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
</div>
<div id="rdElevateStatus" class="text-xs text-slate-400 hidden"></div>
`;
const missingEl = banner.querySelector(".rd-elevate-missing");
if (missingEl) missingEl.textContent = list;
// Insert banner above the canvas area
if (canvasContainer && canvasContainer.parentNode) {
+7 -5
View File
@@ -44,7 +44,7 @@ if (typeof document !== "undefined" && typeof MutationObserver !== "undefined")
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
const ROW_SELECTOR = "[data-client-row]";
@@ -699,12 +699,14 @@ export function createRenderer({
return dashboardBadges(client)
.sort((a, b) => (Number(b.priority) || 0) - (Number(a.priority) || 0))
.map((badge) => {
const label = String(badge.label || badge.title || badge.pluginId || "Plugin");
const title = String(badge.title || label);
const label = String(badge.label || badge.title || badge.pluginId || "Plugin").slice(0, 256);
const title = String(badge.title || label).slice(0, 512);
const tone = String(badge.tone || "info").replace(/[^a-z0-9_-]/gi, "").toLowerCase() || "info";
const icon = String(badge.icon || "").replace(/[^a-z0-9_\- ]/gi, "").trim();
const imageUrl = String(badge.imageUrl || "").trim();
const href = String(badge.href || "").trim();
const rawImageUrl = String(badge.imageUrl || "").trim().slice(0, 2_048);
const rawHref = String(badge.href || "").trim().slice(0, 2_048);
const imageUrl = /^(?:https?:\/\/|\/(?!\/))/i.test(rawImageUrl) ? rawImageUrl : "";
const href = /^(?:https?:\/\/|\/(?!\/)|#)/i.test(rawHref) ? rawHref : "";
const visual = imageUrl
? `<img src="${escapeHtml(imageUrl)}" alt="">`
: `<i class="${escapeHtml(icon || "fa-solid fa-puzzle-piece")}"></i>`;
+1 -1
View File
@@ -238,7 +238,7 @@ function updateSelectedCount() {
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
let cachedSavedScripts = [];
+32 -2
View File
@@ -794,6 +794,36 @@ async function loadMfaStatus() {
}
}
function renderMfaQrCode(svgMarkup) {
if (!mfaQrCode) return;
mfaQrCode.replaceChildren();
const svg = typeof svgMarkup === "string" ? svgMarkup.trim() : "";
if (!svg || svg.length > 512 * 1024 || !/^<svg[\s>]/i.test(svg) || !/<\/svg>$/i.test(svg)) {
return;
}
// Keep API-provided SVG out of the document DOM. SVG loaded through an
// image element is an inert image context, so scripts and event handlers in
// a compromised response cannot run in the settings page.
const image = document.createElement("img");
image.alt = "MFA setup QR code";
image.className = "block h-full w-full";
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
mfaQrCode.appendChild(image);
}
function safeMfaOtpauthUrl(value) {
const candidate = typeof value === "string" ? value.trim() : "";
if (!candidate || candidate.length > 4096) return "#";
try {
const parsed = new URL(candidate);
return parsed.protocol === "otpauth:" && parsed.hostname === "totp" ? parsed.href : "#";
} catch {
return "#";
}
}
async function startMfaSetup() {
try {
const res = await fetch("/api/mfa/setup", { method: "POST", credentials: "include" });
@@ -802,9 +832,9 @@ async function startMfaSetup() {
showMfaMessage(data.error || "Failed to start MFA setup.", "error");
return;
}
if (mfaQrCode) mfaQrCode.innerHTML = data.qrSvg || "";
renderMfaQrCode(data.qrSvg);
if (mfaSecretText) mfaSecretText.textContent = data.secret || "";
if (mfaOtpauthLink) mfaOtpauthLink.href = data.otpauthUrl || "#";
if (mfaOtpauthLink) mfaOtpauthLink.setAttribute("href", safeMfaOtpauthUrl(data.otpauthUrl));
revealElement(mfaSetupPanel, { duration: 240, offset: 8 });
if (mfaEnableCodeInput) mfaEnableCodeInput.focus();
} catch {
@@ -112,7 +112,7 @@ function renderTable(proxies) {
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
// ── Actions ───────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -117,7 +117,7 @@ function renderRpcEndpointManager(records) {
function escapeHtml(value) {
const element = document.createElement("span");
element.textContent = String(value);
return element.innerHTML;
return element.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
testAllRpcBtn.addEventListener("click", async () => {
@@ -50,7 +50,7 @@ function showMessage(text, type = "ok") {
function escapeHtml(value) {
const div = document.createElement("div");
div.textContent = value == null ? "" : String(value);
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function getRule(clientId) {
@@ -115,7 +115,7 @@ let dragGhost = null;
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function findBlockDef(blockId) {
+8 -2
View File
@@ -68,7 +68,10 @@ function setStatus(text, cls) {
if (!statusPill) return;
statusPill.className =
"inline-flex items-center gap-2 px-3 py-1.5 rounded-full border " + cls;
statusPill.innerHTML = `<i class="fa-solid fa-circle"></i><span>${text}</span>`;
statusPill.innerHTML = '<i class="fa-solid fa-circle"></i>';
const label = document.createElement("span");
label.textContent = String(text || "");
statusPill.appendChild(label);
}
function downsampleTo16k(float32Buffer, sampleRate) {
@@ -537,7 +540,10 @@ function daSetStatus(text, cls) {
if (!daStatusPill) return;
daStatusPill.className =
"inline-flex items-center gap-2 px-3 py-1.5 rounded-full border " + cls;
daStatusPill.innerHTML = `<i class="fa-solid fa-circle"></i><span>${text}</span>`;
daStatusPill.innerHTML = '<i class="fa-solid fa-circle"></i>';
const label = document.createElement("span");
label.textContent = String(text || "");
daStatusPill.appendChild(label);
}
function daClearPlayback() {
+4 -1
View File
@@ -523,7 +523,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/s
state === "error" ? "Error" :
"Stopped");
statusEl.innerHTML = `${icons[state] || icons.idle} <span>${label}</span>`;
statusEl.innerHTML = icons[state] || icons.idle;
const labelEl = document.createElement("span");
labelEl.textContent = String(label || "");
statusEl.appendChild(labelEl);
const wsOpen = ws && ws.readyState === WebSocket.OPEN;
const streamLocked = streamState === "streaming" || streamState === "starting" || streamState === "stopping";
+1 -1
View File
@@ -211,7 +211,7 @@ function formatBytes(bytes) {
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
return div.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
clientSearch.addEventListener("input", filterAndRenderClients);