+
-
-
+
-
-
+
+
📺 Views & Rotation
+
+
+
+
+ | View |
+ Method |
+ Viewport |
+ Current URL |
+ Idx |
+ Remaining |
+ Cycle |
+ Cache TTL |
+
+
+
+
+
+
-
+ async function pollDebug() {
+ if (!debugVisible) return;
+ try {
+ const res = await fetch(origin + "/api/debug");
+ lastDebugData = await res.json();
+ renderDebug(lastDebugData);
+ } catch {
+ /* silent */
+ }
+ }
-
+ let debugTickInterval = null;
+
+ function renderDebug(data) {
+ const s = data.server;
+ document.getElementById("stat-uptime").textContent = s.uptime;
+ document.getElementById("stat-views").textContent = data.views.length;
+ document.getElementById("stat-renders").textContent = s.activeRenders;
+ document.getElementById("stat-queue").textContent = s.queueLength;
+ document.getElementById("stat-cache").textContent = s.cacheEntries;
+ document.getElementById("stat-devices").textContent = data.devices.length;
+
+ // ── Views table ──
+ document.getElementById("debug-views-tbody").innerHTML = data.views
+ .map((v) => {
+ const methodBadge =
+ v.method === "mjpeg"
+ ? '
MJPEG'
+ : '
SSR';
+ return `
+
+ | ${esc(v.name)} |
+ ${methodBadge} |
+ ${v.viewportWidth}×${v.viewportHeight} |
+ ${esc(v.currentUrl)} |
+ ${v.currentUrlIndex}/${v.urls.length} |
+ ${v.remainingSec}s |
+ ${v.totalCycleSec}s |
+ ${v.cacheTtlSec}s |
+
`;
+ })
+ .join("");
+
+ // ── Cache table ──
+ document.getElementById("debug-cache-tbody").innerHTML = data.views
+ .flatMap((v) =>
+ v.cacheEntries.map(
+ (c) => `
+
+ | ${esc(v.name)} |
+ ${c.urlIndex}/${v.urls.length} |
+ ${esc(c.url)} |
+ ${c.ttlRemainingSec}s |
+
`,
+ ),
+ )
+ .join("");
+
+ // ── Devices table ──
+ document.getElementById("debug-devices-tbody").innerHTML = data.devices
+ .map((d) => {
+ const ago = Math.round((Date.now() - d.lastSeen) / 1000);
+ return `
+
+ | ${esc(d.ip)} |
+ ${esc(d.viewName)} |
+ ${d.requestCount} |
+ ${ago}s ago |
+ ${esc(d.userAgent.slice(0, 60))} |
+
`;
+ })
+ .join("");
+
+ if (!debugTickInterval) {
+ debugTickInterval = setInterval(tickDebugValues, 500);
+ }
+ }
+
+ function tickDebugValues() {
+ document.querySelectorAll("[data-remaining]").forEach((el) => {
+ let val = parseFloat(el.getAttribute("data-remaining"));
+ val = Math.max(0, val - 0.5);
+ el.setAttribute("data-remaining", val);
+ el.textContent = val.toFixed(1) + "s";
+ if (val <= 3) el.style.color = "var(--danger)";
+ else if (val <= 10) el.style.color = "var(--orange)";
+ else el.style.color = "var(--yellow)";
+ });
+
+ document.querySelectorAll("[data-ttl]").forEach((el) => {
+ let val = parseFloat(el.getAttribute("data-ttl"));
+ val = Math.max(0, val - 0.5);
+ el.setAttribute("data-ttl", val);
+ el.textContent = val.toFixed(1) + "s";
+ if (val <= 0) {
+ el.classList.remove("fresh");
+ el.classList.add("expired");
+ }
+ });
+ }
+
+ // ═══════════════════════════════════════════════
+ // KEYBOARD
+ // ═══════════════════════════════════════════════
+
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") closeModal();
+ });
+
+ // ═══════════════════════════════════════════════
+ // INIT
+ // ═══════════════════════════════════════════════
+
+ loadViews();
+
+