/* * USMT Dash - terminal panel + demo sandbox * * Loaded before the dashboard's own script. It does two things: * * 1. Terminal panel (Live and Demo mode). Streams the real console output of * USMT-Server.ps1 and USMT-Worker.ps1 from the server's /get-logs route. * * 2. Demo sandbox (Demo mode only). The public demo's API is read-only, so every request * that would change something (tracking tags, docs, images, force refresh) is handled * here instead, in the visitor's own browser (IndexedDB). Reads are answered from the * real server merged with the visitor's changes, and the terminals show what the real * services would have printed. Nothing a visitor does is ever sent to the server or * seen by anyone else. * * In Live mode window.fetch is passed straight through untouched. */ (function () { 'use strict'; const realFetch = window.fetch.bind(window); const configReady = realFetch('/get-config', { cache: 'no-store' }) .then(r => (r.ok ? r.json() : null)) .catch(() => null) .then(c => c || { Mode: 'Live', Demo: false, Terminals: false }); // ===================================================================================== // Helpers // ===================================================================================== const sleep = ms => new Promise(r => setTimeout(r, ms)); const clone = o => JSON.parse(JSON.stringify(o)); const jsonResponse = (obj, status = 200) => new Response(JSON.stringify(obj), { status, headers: { 'Content-Type': 'application/json' } }); const textResponse = (txt, status = 200, headers = {}) => new Response(txt, { status, headers }); const pad2 = n => String(n).padStart(2, '0'); function psTime(d = new Date()) { // 03:01:43 PM let h = d.getHours(); const ampm = h >= 12 ? 'PM' : 'AM'; h = h % 12 || 12; return `${pad2(h)}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())} ${ampm}`; } function psStamp(d = new Date()) { // 10/06/26 03:01 PM let h = d.getHours(); const ampm = h >= 12 ? 'PM' : 'AM'; h = h % 12 || 12; return `${pad2(d.getMonth() + 1)}/${pad2(d.getDate())}/${String(d.getFullYear()).slice(-2)} ${pad2(h)}:${pad2(d.getMinutes())} ${ampm}`; } // Small deterministic hash (FNV-1a + murmur finalizer) for visitor-added devices function hash(str) { let h = 0x811c9dc5; for (const ch of String(str).toUpperCase()) { h ^= ch.charCodeAt(0); h = Math.imul(h, 0x01000193); } h ^= h >>> 16; h = Math.imul(h, 0x85ebca6b); h ^= h >>> 13; h = Math.imul(h, 0xc2b2ae35); h ^= h >>> 16; return h >>> 0; } const rand = (name, salt) => hash(name + '|' + salt) / 4294967296; const ALPHA = '0123456789ABCDEFGHJKLMNPQRSTUVWXYZ'; const serial = seed => Array.from({ length: 8 }, (_, i) => ALPHA[Math.floor(rand(seed, 's' + i) * ALPHA.length)]).join(''); const hex = (seed, n) => Array.from({ length: n }, (_, i) => Math.floor(rand(seed, 'h' + i) * 256).toString(16).toUpperCase().padStart(2, '0')); // ===================================================================================== // Terminal panel // ===================================================================================== const TERMINALS = [ { id: 'server', title: 'USMT-Server.ps1' }, { id: 'worker', title: 'USMT-Worker.ps1' } ]; const MAX_LINES = 2500; const Term = { el: null, open: false, active: 'worker', demo: false, state: {}, // id -> { lastId, body, hold, held[], unread } timer: null, init(demo) { this.demo = demo; TERMINALS.forEach(t => { this.state[t.id] = { lastId: 0, body: null, hold: false, held: [], loaded: false }; }); this.build(); try { if (localStorage.getItem('usmt_term_open') === '1') this.toggle(true); } catch (e) {} try { const tab = localStorage.getItem('usmt_term_tab'); if (tab && this.state[tab]) this.select(tab); } catch (e) {} this.loop(); }, build() { const dock = document.createElement('div'); dock.id = 'usmt-term'; dock.className = 'usmt-term collapsed'; dock.innerHTML = `
LIVE ${this.demo ? '● your actions (simulated)' : ''}
`; document.body.appendChild(dock); this.el = dock; const tabs = dock.querySelector('.usmt-term-tabs'); const screens = dock.querySelector('.usmt-term-screens'); TERMINALS.forEach(t => { const b = document.createElement('button'); b.type = 'button'; b.className = 'usmt-term-tab'; b.dataset.id = t.id; b.textContent = t.title; b.onclick = () => { this.select(t.id); if (!this.open) this.toggle(true); }; tabs.appendChild(b); const body = document.createElement('div'); body.className = 'usmt-term-body'; body.dataset.id = t.id; screens.appendChild(body); this.state[t.id].body = body; }); dock.querySelector('.usmt-term-toggle').onclick = () => this.toggle(); const follow = dock.querySelector('.usmt-term-follow'); follow.onclick = () => { follow.classList.toggle('active'); this.scroll(true); }; this.select(this.active); // drag-to-resize const handle = dock.querySelector('.usmt-term-resize'); handle.addEventListener('mousedown', e => { e.preventDefault(); const startY = e.clientY, startH = dock.querySelector('.usmt-term-window').offsetHeight; const move = ev => { const h = Math.max(140, Math.min(window.innerHeight - 140, startH + (startY - ev.clientY))); dock.style.setProperty('--usmt-term-h', h + 'px'); }; const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); }; document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); }); }, toggle(force) { this.open = force === undefined ? !this.open : force; this.el.classList.toggle('collapsed', !this.open); document.body.classList.toggle('usmt-term-open', this.open); try { localStorage.setItem('usmt_term_open', this.open ? '1' : '0'); } catch (e) {} if (this.open) { this.poll(this.active); this.scroll(true); } }, select(id) { this.active = id; this.el.querySelectorAll('.usmt-term-tab').forEach(b => b.classList.toggle('active', b.dataset.id === id)); this.el.querySelectorAll('.usmt-term-body').forEach(b => b.classList.toggle('active', b.dataset.id === id)); const t = TERMINALS.find(x => x.id === id); this.el.querySelector('.usmt-term-wtitle').textContent = (this.demo ? 'Administrator: PowerShell (demo container) - ' : 'Administrator: PowerShell - ') + t.title; this.el.querySelector(`.usmt-term-tab[data-id="${id}"]`).classList.remove('unread'); try { localStorage.setItem('usmt_term_tab', id); } catch (e) {} this.poll(id); this.scroll(true); }, following() { return this.el.querySelector('.usmt-term-follow').classList.contains('active'); }, scroll(force) { const body = this.state[this.active].body; if (force || this.following()) body.scrollTop = body.scrollHeight; }, // segs: [[text, ConsoleColor], ...] render(id, lines, simulated) { const st = this.state[id]; const frag = document.createDocumentFragment(); for (const ln of lines) { const row = document.createElement('div'); row.className = 'usmt-term-line' + (simulated ? ' sim' : ''); if (ln.t) row.title = ln.t; const segs = ln.s || []; if (segs.length === 0) row.innerHTML = ' '; for (const [text, color] of segs) { const span = document.createElement('span'); span.className = 'c-' + (color || 'Default'); span.textContent = text; row.appendChild(span); } frag.appendChild(row); } st.body.appendChild(frag); while (st.body.childNodes.length > MAX_LINES) st.body.removeChild(st.body.firstChild); if (id === this.active && this.open) this.scroll(); else if (lines.length) { const tab = this.el.querySelector(`.usmt-term-tab[data-id="${id}"]`); if (tab) tab.classList.add('unread'); } }, // Simulated output from the visitor's own actions (demo mode) write(id, segs) { if (!this.el) return; this.render(id, [{ s: segs, t: new Date().toLocaleTimeString() }], true); }, // While a simulated worker cycle is printing, real worker lines are held back so the // two don't interleave mid-cycle; they're flushed when the simulated cycle ends. hold(id, on) { const st = this.state[id]; if (!st) return; st.hold = on; if (!on && st.held.length) { this.render(id, st.held, false); st.held = []; } }, async poll(id) { const st = this.state[id]; if (st.busy) return; st.busy = true; try { const r = await realFetch(`/get-logs?source=${id}&after=${st.lastId}&t=${Date.now()}`, { cache: 'no-store' }); if (!r.ok) return; const data = await r.json(); const lines = (data.lines || []).filter(l => l.i > st.lastId); if (!lines.length) return; st.lastId = lines[lines.length - 1].i; if (!st.loaded) { st.loaded = true; if (lines[0].s && lines[0].s[0] && lines[0].s[0][1] !== 'Prompt') { lines.unshift({ s: [['... (earlier output trimmed) ...', 'DarkGray']] }); } } if (st.hold) st.held.push(...lines); else this.render(id, lines, false); } catch (e) { } finally { st.busy = false; } }, async loop() { let tick = 0; for (;;) { if (this.open) { await this.poll(this.active); if (tick % 3 === 0) for (const t of TERMINALS) if (t.id !== this.active) await this.poll(t.id); } tick++; await sleep(this.open ? 1500 : 4000); } } }; // ===================================================================================== // Demo sandbox: per-visitor state in IndexedDB // ===================================================================================== const Store = { db: null, state: null, empty() { return { version: 1, tracked: null, // visitor's copy of Tracked-Devices.json (null = untouched) appliedTracked: null, // roster the (simulated) worker last swept rev: 0, // bumps whenever simulated data changes docs: {}, // title -> { content, modified } deletedDocs: [], // server docs the visitor archived images: {}, // name -> { dataUrl, date } (visitor uploads) renamedImages: {}, // new name -> original server name deletedImages: [] // server image names the visitor deleted }; }, loading: null, load() { if (!this.loading) this.loading = this.open(); return this.loading; }, async open() { this.state = this.empty(); try { this.db = await new Promise((res, rej) => { const req = indexedDB.open('usmt-dash-demo', 1); req.onupgradeneeded = () => req.result.createObjectStore('kv'); req.onsuccess = () => res(req.result); req.onerror = () => rej(req.error); }); const saved = await new Promise(res => { const tx = this.db.transaction('kv', 'readonly').objectStore('kv').get('state'); tx.onsuccess = () => res(tx.result); tx.onerror = () => res(null); }); if (saved && saved.version === 1) this.state = Object.assign(this.empty(), saved); } catch (e) { this.db = null; } // private browsing etc. - keep changes in memory only // The demo container reseeds its fleet on restart; a roster copied from an older // seed would reference tags that no longer exist, so drop it (docs/images are kept). if (this.state.tracked) { const base = await serverGet('/Tracked-Devices.json', true); if (base && hash(base) !== this.state.baseHash) { this.state.tracked = null; this.state.appliedTracked = null; this.save(); } } return this.state; }, save() { if (!this.db) return; try { this.db.transaction('kv', 'readwrite').objectStore('kv').put(this.state, 'state'); } catch (e) {} }, async reset() { this.state = this.empty(); if (this.db) await new Promise(res => { const tx = this.db.transaction('kv', 'readwrite'); tx.objectStore('kv').delete('state'); tx.oncomplete = res; tx.onerror = res; }); }, hasChanges() { const s = this.state; return !!(s && (s.tracked || Object.keys(s.docs).length || s.deletedDocs.length || Object.keys(s.images).length || Object.keys(s.renamedImages).length || s.deletedImages.length)); } }; // Short-lived memo of server reads so one dashboard poll doesn't fetch the same file twice const serverMemo = {}; async function serverGet(path, asText) { const key = path + (asText ? '#t' : ''); const hit = serverMemo[key]; if (hit && Date.now() - hit.at < 2500) return hit.promise; const promise = realFetch(path + (path.includes('?') ? '&' : '?') + 't=' + Date.now(), { cache: 'no-store' }) .then(r => (r.ok ? (asText ? r.text() : r.json()) : null)) .catch(() => null); serverMemo[key] = { at: Date.now(), promise }; return promise; } // ===================================================================================== // Demo sandbox: simulated data for the visitor's roster // ===================================================================================== const SCHEME = /^(PRV|PUB)(DT|LT)-[0-9A-Z]{8}$/; function parseRoster(tracked) { const tagMap = {}, manualPairs = {}, skipTags = new Set(); for (const [tag, entry] of Object.entries(tracked || {})) { const list = Array.isArray(entry) ? entry : (entry.devices || []); if (!Array.isArray(entry) && entry.skipUSMT) skipTags.add(tag); for (const raw of list) { if (!raw || !String(raw).trim()) continue; const d = String(raw).trim().toUpperCase(); const m = d.match(/^([^,]+)\s*,\s*([^,]+)$/); const add = n => { (tagMap[n] = tagMap[n] || []).includes(tag) || tagMap[n].push(tag); }; if (m) { const s = m[1].trim(), t = m[2].trim(); manualPairs[s] = t; add(s); add(t); } else add(d); } } return { tagMap, manualPairs, skipTags }; } function blankRow(src, dst, tags) { return { "Source Device": src, "Destination Device": dst, "Tags": tags, "Source Total Space": "N/A", "Source Free Space": "N/A", "USMT Status": "N/A", "Backup Status": "N/A", "Source Online": "N/A", "Destination Online": "N/A", "Source Scanstate Line": "N/A", "Destination Loadstate Line": "N/A", "Source Scanstate File": "N/A", "Destination Loadstate File": "N/A", "Source Scanstate Elapsed": "N/A", "Destination Loadstate Elapsed": "N/A" }; } // A device the visitor typed that the demo fleet doesn't know: borrow the live status of // a fleet device with the same prefix, so it keeps moving through the stages like the rest. function pickTemplate(name, rows) { const prefix = name.split('-')[0]; const pool = rows.filter(r => r["Source Device"].startsWith(prefix + '-') && r["Destination Device"] !== "N/A" && !["Skipped", "N/A"].includes(r["Backup Status"]) && r["USMT Status"] !== "N/A") .sort((a, b) => a["Source Device"].localeCompare(b["Source Device"])); return pool.length ? pool[hash(name) % pool.length] : null; } const synthDest = name => name.split('-')[0] + '-' + serial(name + '|dest'); function renameIn(value, from, to) { return typeof value === 'string' ? value.split(from).join(to) : value; } function synthAdv(name, templateName, adv) { const base = adv[templateName]; if (!base) return null; const a = clone(base); a.MAC = hex(name + 'mac', 6).join(':'); if (a.IP && a.IP !== 'N/A') { const p = a.IP.split('.'); a.IP = `${p[0]}.${p[1]}.${10 + Math.floor(rand(name, 'ip3') * 240)}.${10 + Math.floor(rand(name, 'ip4') * 240)}`; } if (a.Disk) a.Disk = a.Disk.replace(/S\/N: [^<]*$/, 'S/N: ' + hex(name + 'sn', 8).join('').replace(/(.{4})/g, '$1_').replace(/_$/, '.')); for (const k of ['LastUser', 'SCCMPrimaryUsers']) if (typeof a[k] === 'string') a[k] = renameIn(a[k], templateName, name).replace(templateName.toLowerCase(), name.toLowerCase()); return a; } // Builds the Data.json rows the worker would produce for a roster, plus synthetic // advanced/software entries for devices the fleet doesn't know. function buildRows(tracked, serverRows, serverAdv) { const { tagMap, manualPairs, skipTags } = parseRoster(tracked); const bySrc = {}; for (const r of serverRows) bySrc[r["Source Device"].toUpperCase()] = r; const manualDests = new Set(Object.values(manualPairs)); const out = [], adv = {}, software = {}; // software: name -> template name to copy for (const dev of Object.keys(tagMap).sort()) { const tags = tagMap[dev].join(', '); const srow = bySrc[dev]; if (tagMap[dev].some(t => skipTags.has(t))) { const r = blankRow(dev, "N/A", tags); Object.assign(r, { "USMT Status": "Skipped", "Backup Status": "Skipped", "Source Online": "Skipped", "Destination Online": "-" }); out.push(r); if (!serverAdv[dev]) { const t = pickTemplate(dev, serverRows); if (t && SCHEME.test(dev)) { adv[dev] = synthAdv(dev, t["Source Device"], serverAdv); software[dev] = t["Source Device"]; } } continue; } if (manualPairs[dev]) { const dst = manualPairs[dev]; if (srow && srow["Destination Device"] === dst && srow["USMT Status"] === "N/A") { out.push(Object.assign(clone(srow), { Tags: tags })); continue; } const r = blankRow(dev, dst, tags); for (const [n, col] of [[dev, "Source Online"], [dst, "Destination Online"]]) { const t = SCHEME.test(n) ? pickTemplate(n, serverRows) : null; r[col] = t ? t["Source Online"] : "Not Found"; if (t && !serverAdv[n]) { adv[n] = synthAdv(n, t["Source Device"], serverAdv); software[n] = t["Source Device"]; } else if (!t) software[n] = null; } out.push(r); continue; } if (manualDests.has(dev)) continue; if (srow && !["Skipped", "N/A"].includes(srow["Backup Status"])) { out.push(Object.assign(clone(srow), { Tags: tags })); continue; } const t = SCHEME.test(dev) ? pickTemplate(dev, serverRows) : null; if (!t) { // Not in the fleet and not a valid name: what the worker reports for a typo const r = blankRow(dev, "N/A", tags); Object.assign(r, { "USMT Status": "Not Associated", "Backup Status": "Not Associated", "Source Online": "Not Found", "Destination Online": "-", "Source Scanstate Line": "Unreachable (Not Found)", "Source Scanstate File": "Unreachable" }); out.push(r); software[dev] = null; // SCCM has no inventory for it: an empty list, same as Get-Software.ps1 continue; } const tSrc = t["Source Device"], tDst = t["Destination Device"], dst = synthDest(dev); const r = clone(t); for (const k of Object.keys(r)) r[k] = renameIn(renameIn(r[k], tSrc, dev), tDst, dst); r.Tags = tags; out.push(r); if (!serverAdv[dev]) adv[dev] = synthAdv(dev, tSrc, serverAdv); if (!serverAdv[dst]) adv[dst] = synthAdv(dst, tDst, serverAdv); software[dev] = tSrc; software[dst] = tDst; } return { rows: out, adv, software }; } async function serverSnapshot() { const [data, adv] = await Promise.all([serverGet('/Data.json'), serverGet('/Advanced-Cache.json')]); const rows = data && Array.isArray(data.Devices) ? data.Devices : (Array.isArray(data) ? data : []); return { data: data || {}, rows, adv: adv || {} }; } async function visitorTracked() { const s = Store.state; if (s.tracked) return s.tracked; return (await serverGet('/Tracked-Devices.json')) || {}; } async function ensureOwnRoster() { const s = Store.state; if (!s.tracked) { const text = (await serverGet('/Tracked-Devices.json', true)) || '{}'; const base = JSON.parse(text); s.baseHash = hash(text); s.tracked = clone(base); s.appliedTracked = clone(base); } return s.tracked; } // ===================================================================================== // Demo sandbox: simulated worker cycle (prints like USMT-Worker.ps1 -v) // ===================================================================================== const Sim = { running: false, again: null, worker: null, timer: null }; function scheduleCycle(kind, delay) { clearTimeout(Sim.timer); Sim.timer = setTimeout(() => runCycle(kind), delay); } function pingLine(name, status) { const color = { "Online": "Green", "Timed Out": "DarkYellow" }[status] || "Red"; const text = { "Online": "Online", "Timed Out": "Timed Out.", "DNS Issue": "DNS Issue (reverse lookup mismatch).", "Not Found": "Not Found (No IP)." }[status] || "Not Found (No IP)."; return [[` > Fast Ping: ${name}... `, 'DarkGray'], [text, color]]; } function logLines(name, logName, file) { const out = [[[` > Scraping logs for '${logName}'...`, 'DarkGray']]]; const folders = ['Windows\\CCM\\Logs\\SMSTSLog', 'Windows\\CCM\\Logs']; for (const f of folders) { const path = `\\\\${name}\\C$\\${f}`; const hit = typeof file === 'string' && file.startsWith(path + '\\'); out.push([[` Checking ${path}... `, 'DarkGray'], hit ? ['Found!', 'Green'] : ['Empty/Not there.', 'DarkGray']]); if (hit) break; } return out; } function deviceLines(r, isNew, full) { const W = []; const add = (...segs) => W.push(segs); const src = r["Source Device"], dst = r["Destination Device"]; if (r["Backup Status"] === "Skipped") { add([' -> [SKIP USMT] Tag marked Skip USMT. Caching Advanced/Software only.', 'DarkMagenta']); if (isNew || full) add([` -> [CACHE MISS / UPDATE] Running AD & SQL lookup for ${src}...`, 'DarkYellow']); return W; } if (r["USMT Status"] === "N/A" && dst !== "N/A") { add([' -> [MANUAL MATCH] Hardware exception detected. Bypassing SCCM...', 'DarkYellow']); W.push(pingLine(src, r["Source Online"]), pingLine(dst, r["Destination Online"])); return W; } add([' -> Querying SCCM for associations...', 'DarkGray']); if (r["Source Online"] === "Cached") { add([' -> SCCM DB Status: Completed (Raw: 2)', 'DarkGray']); add([' -> [CACHE HIT] Fast-tracking row and bypassing network sweeps.', 'Green']); return W; } if (r["USMT Status"] === "Not Associated") { add([' -> No SCCM association found. Sweeping source device anyway...', 'DarkGray']); W.push(pingLine(src, r["Source Online"])); if (r["Source Online"] === "Online") { add([' > Checking C: Drive Storage (SMB Method)... ', 'DarkGray'], [`${(r["Source Free Space"] || '').replace(' GB', 'GB')} Free out of ${(r["Source Total Space"] || '').replace(' GB', 'GB')}`, 'Green']); W.push(...logLines(src, 'scanstateprogress', r["Source Scanstate File"])); } if (isNew || full) add([` -> [CACHE MISS / UPDATE] Running AD & SQL lookup for ${src}...`, 'DarkYellow']); add([` - Status: ${r["Backup Status"]}`, 'Cyan']); return W; } const raw = { "Not Started": 0, "In Progress": 1, "Completed": 2 }[r["USMT Status"]]; if (isNew) add([` -> [CACHE MISS] Fetching software for Dest Device ${dst}...`, 'DarkCyan']); add([` -> SCCM DB Status: ${r["USMT Status"]} (Raw: ${raw === undefined ? '?' : raw})`, 'DarkGray']); add([` -> [SWEEP SOURCE] ${src}`, 'DarkCyan']); W.push(pingLine(src, r["Source Online"])); if (r["Source Online"] === "Online") { add([' > Checking C: Drive Storage (SMB Method)... ', 'DarkGray'], [`${(r["Source Free Space"] || '').replace(' GB', 'GB')} Free out of ${(r["Source Total Space"] || '').replace(' GB', 'GB')}`, 'Green']); W.push(...logLines(src, 'scanstateprogress', r["Source Scanstate File"])); } add([` -> [SWEEP DESTINATION] ${dst}`, 'DarkCyan']); W.push(pingLine(dst, r["Destination Online"])); if (r["Destination Online"] === "Online") W.push(...logLines(dst, 'loadstateprogress', r["Destination Loadstate File"])); add([' -> Calculating Master Status...', 'DarkGray']); add([' -> Fetching Advanced SQL/AD Data...', 'DarkGray']); if (isNew || full) { add([` -> [CACHE MISS / UPDATE] Running AD & SQL lookup for ${src}...`, 'DarkYellow']); add([` -> [CACHE MISS / UPDATE] Running AD & SQL lookup for ${dst}...`, 'DarkYellow']); } add([` - Status: ${r["Backup Status"]}`, 'Cyan']); return W; } async function runCycle(kind) { if (Sim.running) { Sim.again = kind; return; } Sim.running = true; const full = kind === 'full'; const w = segs => Term.write('worker', segs); Term.hold('worker', true); try { const s = Store.state; const tracked = await visitorTracked(); const applied = s.appliedTracked || tracked; const snap = await serverSnapshot(); const before = new Set(Object.keys(parseRoster(applied).tagMap)); const { rows } = buildRows(tracked, snap.rows, snap.adv); const { tagMap } = parseRoster(tracked); const total = Object.keys(tagMap).length; const started = new Date(); const state = (status, cur) => { Sim.worker = { Status: status, Current: cur, Total: total }; }; if (kind === 'force') w([['[!] Force Update triggered by Dashboard!', 'Cyan']]); if (full) w([['[!] Force FULL Refresh triggered by Dashboard!', 'Cyan']]); w([]); w([]); w([[`--- Cycle Start: ${psTime(started)} ---`, 'White']]); state('Preparing...', 0); await sleep(400); w([]); w([['[Checking Software Cache Engine...]', 'Cyan']]); state('Software Check...', 0); if (full) w([['Dashboard Force FULL Refresh. Triggering global software/advanced update.', 'Magenta']]); for (const dev of Object.keys(tagMap).sort()) { if (full || !before.has(dev)) { w([[`Fetching background software for ${dev}...`, 'DarkCyan']]); await sleep(full ? 8 : 60); } } w([['[Software Cache Synchronized]', 'Green']]); w([]); w([[`--- Commencing Data Sweep (${total} Devices) ---`, 'White']]); const delay = Math.max(15, Math.min(90, 9000 / Math.max(1, rows.length))); let i = 0; for (const r of rows) { i++; state('Updating Devices...', i); const src = r["Source Device"]; w([[`[${i}/${total}] Processing: ${src}`, 'Yellow']]); w([[` -> Tags: [${r.Tags}]`, 'DarkCyan']]); for (const segs of deviceLines(r, !before.has(src), full)) w(segs); await sleep(delay); } w([[' -> Pruning and Compressing Software Cache...', 'DarkGray']]); w([[' -> Pruning and Saving Advanced Info Cache...', 'DarkGray']]); w([[' -> Writing payload to Data.json...', 'DarkGray']]); const secs = (new Date() - started) / 1000; w([]); w([[`Cycle Complete in ${secs.toFixed(1)}s.`, 'Gray']]); const next = new Date(Date.now() + 45000); w([[`Next Cycle Scheduled For: ${psStamp(next)}`, 'DarkGray']]); w([]); w([]); if (!s.tracked) await ensureOwnRoster(); s.appliedTracked = clone(tracked); s.rev++; Store.save(); } finally { Sim.worker = null; Sim.running = false; Term.hold('worker', false); } if (Sim.again) { const k = Sim.again; Sim.again = null; scheduleCycle(k, 1500); } } // ===================================================================================== // Demo sandbox: route handlers // ===================================================================================== const srv = (text, color) => Term.write('server', [[text, color]]); const DOC_NAME = /[<>:"/\\|?*]/g; const IMG_TYPES = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml', bmp: 'image/bmp' }; async function readBody(init) { try { return JSON.parse((init && init.body) || '{}'); } catch (e) { return {}; } } async function serverDocNames() { const list = await serverGet('/get-docs-list'); return Array.isArray(list) ? list : (list ? [list] : []); } async function visibleDocNames() { const s = Store.state; const names = new Set((await serverDocNames()).filter(d => !s.deletedDocs.includes(d))); Object.keys(s.docs).forEach(d => names.add(d)); return [...names].sort((a, b) => a.localeCompare(b)); } async function docContent(title) { const s = Store.state; if (s.docs[title]) return s.docs[title].content; if (s.deletedDocs.includes(title)) return null; const r = await realFetch(`/Documentation/${encodeURIComponent(title)}.md?t=${Date.now()}`); return r.ok ? r.text() : null; } async function serverImages() { const list = await serverGet('/list-images'); return Array.isArray(list) ? list : (list ? [list] : []); } // name the visitor sees -> what backs it ({ dataUrl } or { server: originalName }) async function imageMap() { const s = Store.state; const map = {}; const renamedFrom = new Set(Object.values(s.renamedImages)); for (const img of await serverImages()) { if (s.deletedImages.includes(img.Name) || renamedFrom.has(img.Name)) continue; map[img.Name] = { server: img.Name, Date: img.Date }; } for (const [name, orig] of Object.entries(s.renamedImages)) map[name] = { server: orig, Date: new Date().toISOString() }; for (const [name, img] of Object.entries(s.images)) map[name] = { dataUrl: img.dataUrl, Date: img.date }; return map; } const routes = { // ---- tracking roster ---- 'GET Tracked-Devices.json': async () => { const s = Store.state; const text = s.tracked ? JSON.stringify(s.tracked, null, 2) : ((await serverGet('/Tracked-Devices.json', true)) || '{}'); // Trailing whitespace makes the text differ after a simulated cycle, which is how // the dashboard notices it should reload Data.json. return textResponse(text + ' '.repeat(s.rev), 200, { 'Content-Type': 'application/json' }); }, 'GET get-all-tags': async () => jsonResponse(Object.keys(await visitorTracked())), 'GET get-tracked': async url => { const entry = (await visitorTracked())[url.searchParams.get('tech')]; if (!entry) return jsonResponse([]); return jsonResponse(Array.isArray(entry) ? entry : (entry.devices || [])); }, 'GET get-tag-meta': async url => { const entry = (await visitorTracked())[url.searchParams.get('tech')]; return jsonResponse({ skipUSMT: !!(entry && !Array.isArray(entry) && entry.skipUSMT) }); }, 'POST update-tracked': async (url, init) => { const body = await readBody(init); const tracked = await ensureOwnRoster(); const tech = body.tech; const skip = Object.prototype.hasOwnProperty.call(body, 'skipUSMT') ? !!body.skipUSMT : false; if (!body.devices || body.devices.length === 0) delete tracked[tech]; else tracked[tech] = { devices: body.devices, skipUSMT: skip }; Store.save(); srv(`Updated tracking roster for Tech/Tag: ${tech}`, 'Green'); scheduleCycle('roster', 2500); // the worker picks the change up on its next sweep return textResponse('', 200); }, // ---- worker data ---- 'GET Worker-State.json': async () => { const state = (await serverGet('/Worker-State.json')) || {}; if (Sim.worker) return jsonResponse(Object.assign({}, state, Sim.worker)); return jsonResponse(state); }, 'GET Data.json': async () => { const s = Store.state; const snap = await serverSnapshot(); if (!s.appliedTracked) return jsonResponse(snap.data); const { rows } = buildRows(s.appliedTracked, snap.rows, snap.adv); return jsonResponse({ LastUpdated: snap.data.LastUpdated || psStamp(), Devices: rows }); }, 'GET Advanced-Cache.json': async () => { const s = Store.state; const snap = await serverSnapshot(); if (!s.appliedTracked) return jsonResponse(snap.adv); const { adv } = buildRows(s.appliedTracked, snap.rows, snap.adv); for (const [k, val] of Object.entries(adv)) if (val) snap.adv[k] = val; return jsonResponse(snap.adv); }, 'GET Software-Cache.json': async () => { const s = Store.state; const cache = (await serverGet('/Software-Cache.json')) || {}; if (s.appliedTracked) { const snap = await serverSnapshot(); const { software } = buildRows(s.appliedTracked, snap.rows, snap.adv); for (const [name, template] of Object.entries(software)) { if (cache[name]) continue; cache[name] = (template && cache[template]) || { Timestamp: new Date().toISOString(), Data: [] }; } } return jsonResponse(cache); }, 'GET get-software': async url => { const name = (url.searchParams.get('name') || '').toUpperCase(); const cache = await (await routes['GET Software-Cache.json']()).json(); srv(`Fetching Software for: ${name}`, 'Yellow'); const entry = cache[name]; return jsonResponse(entry ? (Array.isArray(entry) ? entry : [].concat(entry.Data || [])) : []); }, 'POST force-update': async () => { srv('Received Force Update Command from Dashboard', 'Cyan'); scheduleCycle('force', 800); return textResponse('', 200); }, 'POST force-full': async () => { srv('Received Force FULL Refresh Command from Dashboard', 'Cyan'); scheduleCycle('full', 800); return textResponse('', 200); }, // ---- knowledge base ---- 'GET get-docs-list': async () => jsonResponse(await visibleDocNames()), 'GET Documentation': async url => { const rel = decodeURIComponent(url.pathname.replace(/^\/Documentation\//, '')); if (rel.startsWith('images/')) return null; // passthrough (handled by the rewriter) if (!rel.endsWith('.md') || rel.includes('/')) return null; const s = Store.state; const title = rel.slice(0, -3); if (s.docs[title]) return textResponse(s.docs[title].content, 200, { 'Content-Type': 'text/markdown', 'Last-Modified': new Date(s.docs[title].modified).toUTCString() }); if (s.deletedDocs.includes(title)) return textResponse('', 404); return null; }, 'POST save-doc': async (url, init) => { const body = await readBody(init); const s = Store.state; const title = String(body.title || '').replace(DOC_NAME, ''); const original = String(body.originalTitle || '').replace(DOC_NAME, ''); if (!title.trim()) return textResponse('', 400); if (original && original !== title) { delete s.docs[original]; if ((await serverDocNames()).includes(original) && !s.deletedDocs.includes(original)) s.deletedDocs.push(original); } s.docs[title] = { content: String(body.content || ''), modified: Date.now() }; s.deletedDocs = s.deletedDocs.filter(d => d !== title); Store.save(); srv(`Saved documentation: ${title}.md`, 'Green'); return textResponse('', 200); }, 'POST delete-doc': async (url, init) => { const body = await readBody(init); const s = Store.state; const title = String(body.title || '').replace(DOC_NAME, ''); if (!title.trim()) return textResponse('', 400); const onServer = (await serverDocNames()).includes(title) && !s.deletedDocs.includes(title); if (!s.docs[title] && !onServer) return textResponse('', 404); delete s.docs[title]; if (onServer) s.deletedDocs.push(title); Store.save(); srv(`Archived documentation: ${title}.md`, 'Yellow'); return textResponse('', 200); }, // ---- images ---- 'GET list-images': async () => { const map = await imageMap(); return jsonResponse(Object.entries(map).map(([Name, v]) => ({ Name, Date: v.Date })).sort((a, b) => a.Name.localeCompare(b.Name))); }, 'POST upload-image': async (url, init) => { const body = await readBody(init); const name = String(body.filename || '').replace(DOC_NAME, ''); if (!name || !body.data) return textResponse('', 500); const ext = (name.split('.').pop() || '').toLowerCase(); Store.state.images[name] = { dataUrl: `data:${IMG_TYPES[ext] || 'application/octet-stream'};base64,${body.data}`, date: new Date().toISOString() }; delete Store.state.renamedImages[name]; Store.save(); ImageRewriter.refresh(); srv(`Uploaded Image: ${name}`, 'Cyan'); return textResponse('', 200); }, 'POST rename-image': async (url, init) => { const body = await readBody(init); const s = Store.state; const oldRaw = String(body.oldName || ''), oldName = oldRaw.replace(DOC_NAME, ''); const newName = String(body.newName || '').replace(DOC_NAME, ''); const map = await imageMap(); if (!map[oldName] || !newName) return textResponse('', 404); if (s.images[oldName]) { s.images[newName] = s.images[oldName]; delete s.images[oldName]; } else { const orig = map[oldName].server; delete s.renamedImages[oldName]; s.renamedImages[newName] = orig; } // Same markdown crawler as the server: fix links in every article const encOld = encodeURIComponent(oldRaw), encNew = encodeURIComponent(newName); for (const title of await visibleDocNames()) { const content = await docContent(title); if (content == null) continue; let next = content.split('images/' + oldRaw).join('images/' + newName); next = next.split('images/' + encOld).join('images/' + encNew); if (next !== content) { s.docs[title] = { content: next, modified: Date.now() }; srv(`Auto-fixed image link in ${title}.md`, 'Magenta'); } } Store.save(); ImageRewriter.refresh(); return textResponse('', 200); }, 'POST delete-image': async (url, init) => { const body = await readBody(init); const s = Store.state; const name = String(body.filename || '').replace(DOC_NAME, ''); const map = await imageMap(); if (!map[name]) return textResponse('', 404); if (s.images[name]) delete s.images[name]; else if (s.renamedImages[name]) { s.deletedImages.push(s.renamedImages[name]); delete s.renamedImages[name]; } else s.deletedImages.push(name); Store.save(); ImageRewriter.refresh(); srv(`Deleted Image: ${body.filename}`, 'Yellow'); return textResponse('', 200); } }; function findRoute(url, method) { const path = decodeURIComponent(url.pathname.replace(/^\//, '')); if (routes[`${method} ${path}`]) return routes[`${method} ${path}`]; if (method === 'GET' && path.startsWith('Documentation/')) return routes['GET Documentation']; return null; } // requests can't be intercepted with fetch, so images the visitor uploaded or // renamed are swapped in as their elements appear. const ImageRewriter = { map: {}, async refresh() { this.map = await imageMap(); document.querySelectorAll('img').forEach(img => this.fix(img)); }, fix(img) { const src = img.getAttribute('src') || ''; if (img.dataset.usmtFor === src) return; // already rewritten by us const m = src.match(/^\/?Documentation\/images\/([^?#]+)/); if (!m) return; let name; try { name = decodeURIComponent(m[1]); } catch (e) { return; } const entry = this.map[name]; let want; if (!entry) want = 'data:,'; // deleted in this sandbox else if (entry.dataUrl) want = entry.dataUrl; // uploaded by this visitor else if (entry.server !== name) want = '/Documentation/images/' + encodeURIComponent(entry.server); if (want) { img.dataset.usmtFor = want; img.setAttribute('src', want); } }, start() { this.refresh(); new MutationObserver(muts => { for (const m of muts) { if (m.type === 'attributes') this.fix(m.target); else m.addedNodes.forEach(n => { if (n.nodeType !== 1) return; if (n.tagName === 'IMG') this.fix(n); else n.querySelectorAll && n.querySelectorAll('img').forEach(i => this.fix(i)); }); } }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); } }; // ===================================================================================== // Demo banner + about dialog // ===================================================================================== const Demo = { build() { const controls = document.querySelector('.nav-right-controls'); if (controls) { const pill = document.createElement('button'); pill.type = 'button'; pill.className = 'usmt-demo-pill'; pill.textContent = 'DEMO'; pill.title = 'About this demo'; pill.onclick = () => this.about(true); controls.insertBefore(pill, controls.firstChild); } const modal = document.createElement('div'); modal.className = 'usmt-demo-modal'; modal.innerHTML = ` `; document.body.appendChild(modal); modal.addEventListener('click', e => { if (e.target === modal) this.about(false); }); modal.querySelector('.usmt-demo-close').onclick = () => this.about(false); modal.querySelector('.usmt-demo-terms').onclick = () => { this.about(false); Term.toggle(true); }; modal.querySelector('.usmt-demo-reset').onclick = async () => { await Store.reset(); try { ['usmt_workbench_stacks', 'usmt_custom_columns', 'usmt_custom_filters', 'usmt_custom_widths', 'usmt_custom_widths_locked', 'usmt_tech_id'].forEach(k => localStorage.removeItem(k)); } catch (e) {} location.reload(); }; this.modal = modal; let seen = false; try { seen = localStorage.getItem('usmt_demo_seen') === '1'; } catch (e) {} if (!seen) this.about(true); }, about(show) { this.modal.classList.toggle('show', show); if (!show) { try { localStorage.setItem('usmt_demo_seen', '1'); } catch (e) {} } } }; // ===================================================================================== // Wire-up // ===================================================================================== window.fetch = async function (input, init) { const cfg = await configReady; if (!cfg.Demo) return realFetch(input, init); let url; try { url = new URL(typeof input === 'string' ? input : input.url, location.href); } catch (e) { return realFetch(input, init); } if (url.origin !== location.origin) return realFetch(input, init); await Store.load(); const method = ((init && init.method) || (input && input.method) || 'GET').toUpperCase(); const handler = findRoute(url, method); if (handler) { try { const res = await handler(url, init); if (res) return res; } catch (e) { console.error('[demo sandbox]', e); return textResponse('', 500); } } return realFetch(input, init); }; function onReady(fn) { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn(); } onReady(async () => { const cfg = await configReady; if (cfg.Terminals) Term.init(!!cfg.Demo); if (cfg.Demo) { document.documentElement.classList.add('usmt-demo'); await Store.load(); Demo.build(); ImageRewriter.start(); } }); })();