(function () {
'use strict';
const $ = (selector, scope = document) => scope.querySelector(selector);
const $$ = (selector, scope = document) => [...scope.querySelectorAll(selector)];
const renderCache = new WeakMap();
const pages = {
overview: { label: 'Overview', title: 'Management', subtitle: 'Overview', icon: 'grid' },
vms: { label: 'Machines', title: 'Machine', subtitle: 'Collection', icon: 'server' },
network: { label: 'Network & ports', title: 'Private', subtitle: 'Connections', icon: 'network' },
system: { label: 'Host health', title: 'Host', subtitle: 'Wellbeing', icon: 'heart' },
create: { label: 'New machine', title: 'Something', subtitle: 'New', icon: 'plus' },
'vm-detail': { label: 'Machine details', title: 'Machine', subtitle: 'Details', icon: 'server' },
};
const state = {
token: '',
authenticated: false,
session: 0,
vms: [],
loaded: false,
health: null,
checks: null,
vmError: '',
healthError: '',
checksError: '',
page: 'overview',
detailId: null,
detail: null,
portEditing: false,
selected: new Set(),
filter: 'all',
query: '',
sort: 'recent',
busy: new Set(),
controllers: new Set(),
requests: { vms: 0, health: 0, detail: 0, logs: 0 },
timer: null,
refreshPromise: null,
checksPromise: null,
lastUpdate: 0,
lastChecks: 0,
lastChecksAttempt: 0,
searchIndex: -1,
menuTrigger: null,
};
function esc(value) {
return String(value ?? '').replace(/[&<>"']/g, (character) => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
})[character]);
}
function icon(name, className = '') {
return ``;
}
function text(selector, value) {
const element = $(selector);
const content = String(value ?? '—');
if (element.textContent !== content) {
element.textContent = content;
renderCache.delete(element);
}
}
// Unchanged polling responses must not replace focused controls or selections.
function html(selector, markup) {
const element = typeof selector === 'string' ? $(selector) : selector;
if (renderCache.get(element) === markup) return;
element.innerHTML = markup;
renderCache.set(element, markup);
}
function label(value) {
const valueText = String(value || 'unknown').replace(/[-_]/g, ' ');
return valueText.charAt(0).toUpperCase() + valueText.slice(1);
}
function number(value) {
return new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value);
}
function formatRam(value) {
const mib = Number(value);
if (!Number.isFinite(mib)) return '—';
return mib >= 1024 ? `${number(mib / 1024)} GiB` : `${number(mib)} MiB`;
}
function timestamp(value) {
if (value == null || value === '') return NaN;
if (typeof value === 'number' || /^\d+(\.\d+)?$/.test(value)) {
const numeric = Number(value);
return numeric < 1e12 ? numeric * 1000 : numeric;
}
return Date.parse(value);
}
function formatDate(value) {
const time = timestamp(value);
if (!Number.isFinite(time)) return '—';
return new Intl.DateTimeFormat(undefined, {
month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit',
}).format(new Date(time));
}
function runtimeStatus(vm) {
return String(vm.observed_status || vm.status || 'unknown').toLowerCase();
}
function tone(status) {
if (status === 'running') return 'running';
if (status === 'stopped' || status === 'inactive') return 'stopped';
if (['ok', 'enforced', 'ready'].includes(status)) return 'good';
if (['failed', 'dead', 'error', 'unhealthy', 'missing', 'drift', 'quarantined'].includes(status)) return 'attention';
return 'pending';
}
function badge(status) {
return `${esc(label(status))}`;
}
function vcpu(vm) {
const cpu = Number(vm.cpu);
return Number.isFinite(cpu) && cpu > 0 ? Math.ceil(cpu) : 0;
}
function percent(part, total) {
return total > 0 ? Math.min(100, Math.max(0, part / total * 100)) : 0;
}
function vmPath(identifier, suffix = '') {
return `/vms/${encodeURIComponent(identifier)}${suffix}`;
}
function vmHref(identifier) {
return `#vm/${encodeURIComponent(identifier)}`;
}
function machineName(vm) {
return vm.name || vm.id;
}
function isAborted(error) {
return error.name === 'AbortError';
}
function apiError(data, status) {
if (data?.error?.message) return data.error.message;
if (typeof data?.detail === 'string') return data.detail;
if (Array.isArray(data?.detail)) {
return data.detail.map((item) => {
const field = (item.loc || []).filter((part) => part !== 'body').join('.');
return `${field ? `${field}: ` : ''}${item.msg || item.message || 'Invalid value'}`;
}).join('; ');
}
return `The server returned HTTP ${status}.`;
}
async function api(path, { method = 'GET', body } = {}) {
const session = state.session;
const controller = new AbortController();
state.controllers.add(controller);
let timedOut = false;
// Installation and lifecycle calls may legitimately take longer than reads.
const timer = method === 'GET' ? window.setTimeout(() => {
timedOut = true;
controller.abort();
}, 20000) : null;
try {
const response = await fetch(path, {
method,
headers: {
Accept: 'application/json',
'X-UVM-Token': state.token,
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
},
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
cache: 'no-store',
signal: controller.signal,
});
const raw = await response.text();
if (session !== state.session) throw new DOMException('Session changed', 'AbortError');
let data = null;
try { data = raw ? JSON.parse(raw) : null; } catch {
if (response.ok) throw new Error('The server returned an unreadable response.');
}
if (!response.ok) {
const error = new Error(apiError(data, response.status));
error.status = response.status;
if (response.status === 401 && state.authenticated) {
logout('Your token was not accepted. Connect again to continue.');
}
throw error;
}
return data;
} catch (error) {
if (timedOut) throw new Error('The request timed out. Try again in a moment.');
if (error instanceof TypeError) throw new Error('Unable to reach the control plane. Check your connection.');
throw error;
} finally {
window.clearTimeout(timer);
state.controllers.delete(controller);
}
}
function requireList(value) {
if (!Array.isArray(value)) throw new Error('The server returned an unexpected response.');
return value;
}
function toast(message, error = false) {
const item = document.createElement('div');
item.className = `toast${error ? ' toast-error' : ''}`;
item.innerHTML = icon(error ? 'info' : 'check');
const copy = document.createElement('span');
copy.textContent = message;
item.append(copy);
const container = $('#toast-container');
while (container.children.length >= 3) container.firstElementChild.remove();
container.append(item);
window.setTimeout(() => item.remove(), error ? 6500 : 3800);
}
function lockForm(form) {
const controls = $$('input, button, select', form).map((element) => [element, element.disabled]);
controls.forEach(([element]) => { element.disabled = true; });
return () => controls.forEach(([element, disabled]) => { element.disabled = disabled; });
}
async function withButton(button, pendingLabel, operation) {
if (button.disabled) return;
const previous = button.innerHTML;
button.disabled = true;
if (pendingLabel) button.textContent = pendingLabel;
try { return await operation(); } finally {
button.disabled = false;
button.innerHTML = previous;
}
}
function fleetStats() {
const running = state.vms.filter((vm) => runtimeStatus(vm) === 'running');
const stopped = state.vms.filter((vm) => runtimeStatus(vm) === 'stopped');
const sum = (vms, value) => vms.reduce((total, vm) => total + value(vm), 0);
return {
total: state.vms.length,
running: running.length,
stopped: stopped.length,
other: state.vms.length - running.length - stopped.length,
cpu: sum(state.vms, vcpu),
activeCpu: sum(running, vcpu),
ram: sum(state.vms, (vm) => Number(vm.ram_mib) || 0),
activeRam: sum(running, (vm) => Number(vm.ram_mib) || 0),
gateways: [...new Set(state.vms.map((vm) => vm.gateway).filter(Boolean))],
managed: state.vms.filter((vm) => Array.isArray(vm.ports)).length,
enforced: state.vms.filter((vm) => vm.policy_status === 'enforced').length,
ports: sum(state.vms, (vm) => (vm.ports || []).length),
};
}
function compactMetric(symbol, value, caption, word = false) {
return `
${esc(description)}
${control}| Machine | Runtime state | Guest address | Allocation | Port policy | Details |
|---|---|---|---|---|---|
${icon('server')}${esc(machineName(vm))} | ${badge(runtimeStatus(vm))} | ${esc(vm.guest_ip || '—')} | ${vcpu(vm)} vCPU·${esc(formatRam(vm.ram_mib))} | ${badge(vm.policy_status || 'unknown')} | ${icon('chevron')} |
${esc(check.message)}
The host returned no diagnostic checks.
' : '${vm.status === 'stopped' ? 'This legacy machine is ready for a managed policy. Set its first rules to continue.' : 'This machine has a legacy policy. Stop it before defining its first managed rules.'}
` : portsMarkup(vm.ports)); $('#port-input').value = (vm.ports || []).map((rule) => `${rule.port}:${rule.protocol}`).join(', '); } $('#detail-ports').hidden = state.portEditing; $('#port-editor').hidden = !state.portEditing; $('#edit-ports-btn').hidden = state.portEditing || (vm.ports === null && vm.status !== 'stopped'); $('#sync-ports-btn').hidden = vm.ports === null; $('#sync-ports-btn').disabled = busy; $('#open-ports-btn').hidden = vm.ports === null; $('#close-ports-btn').hidden = vm.ports === null; } async function loadDetail(identifier) { const request = ++state.requests.detail; const session = state.session; try { const vm = await api(vmPath(identifier)); if (request !== state.requests.detail || session !== state.session || state.page !== 'vm-detail') return; if (!vm?.id) throw new Error('The machine response was not recognized.'); state.detail = vm; state.detailId = vm.id; renderDetail(); await loadLogs(); } catch (error) { if (isAborted(error) || request !== state.requests.detail || session !== state.session || state.page !== 'vm-detail') return; if (state.detail && error.status !== 404) { text('#detail-error', error.message); $('#detail-error').hidden = false; } else { state.detail = null; $('#detail-content').hidden = true; $('#detail-loading').hidden = false; html('#detail-loading', emptyState('This machine is out of view.', error.message, 'Back to machines', '#vms')); } } } async function loadLogs() { if (!state.detail) return; const id = state.detail.id; const request = ++state.requests.logs; const session = state.session; const tail = Number($('#log-tail').value); $('#log-refresh-btn').disabled = true; text('#log-meta', 'Bringing the latest entries into view…'); try { const logs = await api(vmPath(id, `/logs?tail=${tail}`)); if (request !== state.requests.logs || session !== state.session || state.page !== 'vm-detail' || state.detailId !== id) return; text('#log-output', logs?.content || 'A quiet log. No entries yet.'); text('#log-meta', logs?.truncated ? 'The available tail is shown. A size limit or oversized line truncated the log.' : `Up to ${number(tail)} recent lines. Refresh to see new entries.`); } catch (error) { if (isAborted(error) || request !== state.requests.logs || session !== state.session || state.page !== 'vm-detail' || state.detailId !== id) return; text('#log-output', 'The runtime log is unavailable.'); text('#log-meta', error.message); } finally { if (request === state.requests.logs && session === state.session) $('#log-refresh-btn').disabled = false; } } function confirmAction(title, description, actionLabel, destructive = false) { const dialog = $('#confirm-dialog'); if (dialog.open) return Promise.resolve(false); text('#confirm-title', title); text('#confirm-body', description); text('#confirm-ok', actionLabel); $('#confirm-ok').className = `button ${destructive ? 'button-danger' : 'button-primary'}`; dialog.returnValue = 'cancel'; return new Promise((resolve) => { dialog.addEventListener('close', () => resolve(dialog.returnValue === 'confirm'), { once: true }); dialog.showModal(); }); } async function refreshAfterMutation(id) { await loadVms(); if (state.page === 'vm-detail' && state.detailId === id) await loadDetail(id); } async function machineAction(action, id) { if (!state.authenticated || state.busy.has(id)) return; const session = state.session; const vm = state.vms.find((item) => item.id === id) || (state.detail?.id === id ? state.detail : null); const name = vm ? machineName(vm) : id; const confirmations = { stop: [`Let ${name} take a breather?`, 'The runtime will stop. Its private disk, guest credentials, and network identity are kept for the next start.', 'Stop machine'], restart: [`A fresh start for ${name}?`, 'The runtime will stop and boot again from its existing private disk. Changes on the disk are retained.', 'Restart machine'], delete: [`Say goodbye to ${name}?`, 'This permanently removes the machine and its private disk, and releases its network resources.', 'Delete machine'], }; if (confirmations[action] && !await confirmAction(...confirmations[action], action === 'delete')) return; if (session !== state.session || !state.authenticated) return; state.busy.add(id); renderMachines(); renderDetail(); try { const suffix = action === 'ports-sync' ? '/ports/sync' : action === 'delete' ? '' : `/${action}`; await api(vmPath(id, suffix), { method: action === 'delete' ? 'DELETE' : 'POST' }); if (session !== state.session) return; if (action === 'delete' && state.detailId === id) { state.detail = null; state.detailId = null; location.hash = '#vms'; } const messages = { start: 'Machine started.', stop: 'Machine stopped. Its disk is tucked away.', restart: 'Machine restarted.', delete: 'Machine deleted.', 'ports-sync': 'Port policy synced.' }; toast(messages[action]); await refreshAfterMutation(id); } catch (error) { if (!isAborted(error) && session === state.session) toast(error.message, true); } finally { if (session === state.session) { state.busy.delete(id); renderMachines(); renderDetail(); } } } function parsePorts(value) { if (!value.trim()) return []; const rules = new Map(); value.split(',').forEach((part) => { const match = part.trim().match(/^(\d+)(?:\s*:\s*(TCP|HTTP|HTTPS))?$/i); const port = match ? Number(match[1]) : 0; if (!match || !Number.isInteger(port) || port < 1 || port > 65535) { throw new Error(`“${part.trim()}” is not a valid port rule. Use a port from 1 to 65535, followed by TCP, HTTP, or HTTPS.`); } const protocol = (match[2] || 'TCP').toUpperCase(); if (rules.has(port) && rules.get(port).protocol !== protocol) throw new Error(`Port ${port} has conflicting protocol labels.`); rules.set(port, { port, protocol }); }); return [...rules.values()].sort((a, b) => a.port - b.port); } function parseRam(value) { const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*([BKMG]i?B?)?$/i); if (!match) throw new Error('Use a memory amount such as 512M, 1G, or 2G.'); const factors = { b: 1 / (1024 * 1024), k: 1 / 1024, kb: 1 / 1024, ki: 1 / 1024, kib: 1 / 1024, m: 1, mb: 1, mi: 1, mib: 1, g: 1024, gb: 1024, gi: 1024, gib: 1024 }; const mib = Math.floor(Number(match[1]) * factors[(match[2] || 'm').toLowerCase()]); if (!Number.isFinite(mib) || mib < 128) throw new Error('Give this machine at least 128 MiB of memory.'); return mib; } async function submitPorts(event) { event.preventDefault(); if (!state.detail || state.busy.has(state.detail.id)) return; const id = state.detail.id; const session = state.session; const mode = event.submitter?.value || 'set'; let ports; try { ports = parsePorts($('#port-input').value); } catch (error) { text('#port-error', error.message); return; } text('#port-error', ''); const unlock = lockForm($('#port-editor')); state.busy.add(id); try { await api(vmPath(id, '/ports'), { method: 'POST', body: { mode, ports } }); if (session !== state.session) return; state.portEditing = false; toast({ set: 'Port policy updated.', open: 'Port rules opened.', close: 'Port rules closed.' }[mode]); await refreshAfterMutation(id); } catch (error) { if (!isAborted(error) && session === state.session) text('#port-error', error.message); } finally { unlock(); if (session === state.session) { state.busy.delete(id); renderDetail(); } } } async function copyText(value) { try { try { if (!navigator.clipboard) throw new Error('Clipboard unavailable'); await navigator.clipboard.writeText(value); } catch { const previous = document.activeElement; const field = document.createElement('textarea'); field.value = value; field.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0'; document.body.append(field); field.select(); const copied = document.execCommand('copy'); field.remove(); previous?.focus({ preventScroll: true }); if (!copied) throw new Error('Copying is unavailable. Select the command and copy it manually.'); } toast('SSH command copied.'); } catch (error) { toast(error.message, true); } } function updateCreateSummary() { let memory = '—'; let ports = 'Check port rules'; try { memory = formatRam(parseRam($('#create-ram').value)); } catch { /* The form reports validation on submit. */ } try { const rules = parsePorts($('#create-ports').value); if ($('#create-default-ssh').checked && !rules.some((rule) => rule.port === 22)) rules.unshift({ port: 22, protocol: 'TCP' }); ports = rules.length ? rules.map((rule) => `${rule.port} / ${rule.protocol}`).join(', ') : 'No inbound TCP ports'; } catch { /* Keep the summary useful while a rule is being typed. */ } const cpu = Number($('#create-cpu').value); text('#create-summary-name', $('#create-name').value.trim() || 'Room for an idea.'); html('#create-summary-details', detailRows([ ['Compute', Number.isFinite(cpu) && cpu > 0 ? `${Math.ceil(cpu)} vCPU` : '—'], ['Memory', memory], ['Guest IP', $('#create-guest-ip').value.trim() || 'Automatic'], ['Guest user', $('#create-username').value.trim() || '—'], ['Allowed ports', ports], ])); const presets = { small: [1, 512], balanced: [2, 1024], roomy: [4, 2048] }; let mib = null; try { mib = parseRam($('#create-ram').value); } catch { /* No preset matches an incomplete value. */ } $$('[data-preset]').forEach((button) => { const [presetCpu, presetRam] = presets[button.dataset.preset]; const active = cpu === presetCpu && mib === presetRam; button.classList.toggle('active', active); button.setAttribute('aria-pressed', String(active)); }); } function resetPasswordVisibility() { $('#create-password').type = 'password'; const toggle = $('[data-action="toggle-password"]'); toggle.setAttribute('aria-pressed', 'false'); toggle.setAttribute('aria-label', 'Show password'); } async function submitCreate(event) { event.preventDefault(); if (state.busy.has('create')) return; const session = state.session; let payload; try { parseRam($('#create-ram').value); const cpu = Number($('#create-cpu').value); if (!Number.isFinite(cpu) || cpu <= 0) throw new Error('CPU capacity must be a positive number.'); payload = { name: $('#create-name').value.trim() || null, cpu, ram: $('#create-ram').value.trim(), guest_ip: $('#create-guest-ip').value.trim() || null, username: $('#create-username').value.trim(), password: $('#create-password').value, ports: parsePorts($('#create-ports').value), no_default_ports: !$('#create-default-ssh').checked, }; if (!payload.password) throw new Error('Add a password for the guest account.'); } catch (error) { text('#create-error', error.message); return; } text('#create-error', ''); const button = $('#create-submit'); const previous = button.innerHTML; const unlock = lockForm($('#create-form')); state.busy.add('create'); button.textContent = 'Creating your machine…'; try { const vm = await api('/vms', { method: 'POST', body: payload }); if (session !== state.session) return; if (!vm?.id) throw new Error('The machine response was not recognized. Refresh the registry to check its state.'); $('#create-form').reset(); resetPasswordVisibility(); updateCreateSummary(); toast('A new little machine, ready for you.'); location.hash = vmHref(vm.id); await loadVms(); } catch (error) { if (!isAborted(error) && session === state.session) text('#create-error', error.message); } finally { unlock(); button.innerHTML = previous; if (session === state.session) state.busy.delete('create'); } } async function runBulkAction() { if (!state.selected.size || state.busy.has('bulk')) return; const ids = [...state.selected]; const action = $('#bulk-action').value; const session = state.session; const labels = { start: 'Start', stop: 'Stop', restart: 'Restart', delete: 'Delete', 'ports-sync': 'Sync policies for' }; const title = `${labels[action]} ${ids.length} ${ids.length === 1 ? 'machine' : 'machines'}?`; const description = action === 'delete' ? 'Every selected machine and its private disk will be permanently removed. Network resources will be released.' : `This applies to the ${ids.length} selected ${ids.length === 1 ? 'machine' : 'machines'}. Each result will be reported separately.`; if (!await confirmAction(title, description, action === 'ports-sync' ? 'Sync policies' : `${labels[action]} selected`, action === 'delete')) return; if (session !== state.session) return; state.busy.add('bulk'); ids.forEach((id) => state.busy.add(id)); await withButton($('#bulk-run-btn'), 'Applying…', async () => { try { const results = requireList(await api('/vms/actions', { method: 'POST', body: { action, identifiers: ids } })); if (session !== state.session) return; const failures = results.filter((result) => result.outcome !== 'succeeded'); state.selected = new Set(failures.map((result) => result.id)); $('#bulk-results').hidden = !failures.length; if (failures.length) { html('#bulk-results', `No machines or pages found. Try another name.
'); $('#search-results').hidden = false; $('#global-search').setAttribute('aria-expanded', 'true'); if (state.searchIndex >= 0) $('#global-search').setAttribute('aria-activedescendant', `search-option-${state.searchIndex}`); else $('#global-search').removeAttribute('aria-activedescendant'); } function focusSearch() { setMobileNav(false); $('#global-search').focus(); $('#global-search').select(); renderSearch(); } function setMobileNav(open, restoreFocus = false) { const mobile = window.matchMedia('(max-width: 700px)').matches; const shown = open && mobile; $('#main-app').classList.toggle('nav-open', shown); $('#nav-toggle').setAttribute('aria-expanded', String(shown)); $('#nav-toggle').setAttribute('aria-label', shown ? 'Close navigation' : 'Open navigation'); $('#nav-scrim').hidden = !shown; $('.workspace').inert = shown; if (shown) { $('#workspace-nav').setAttribute('role', 'dialog'); $('#workspace-nav').setAttribute('aria-modal', 'true'); $('.brand', $('#workspace-nav')).focus(); } else { $('#workspace-nav').removeAttribute('role'); $('#workspace-nav').removeAttribute('aria-modal'); if (restoreFocus && mobile) $('#nav-toggle').focus(); } } function route(initial = false) { if (!state.authenticated) return; let value = location.hash.slice(1) || 'overview'; let identifier = null; if (value.startsWith('vm/')) { try { identifier = decodeURIComponent(value.slice(3)); } catch { identifier = null; } value = identifier ? 'vm-detail' : 'overview'; } if (!Object.prototype.hasOwnProperty.call(pages, value)) value = 'overview'; const previous = state.page; state.page = value; state.requests.detail += 1; state.requests.logs += 1; state.detailId = identifier; state.detail = null; state.portEditing = false; closeMenu(); closeSearch(); setMobileNav(false); $$('.page').forEach((page) => { page.hidden = page.id !== `page-${value}`; }); $$('.nav-item[data-nav]').forEach((item) => { const active = item.dataset.nav === (value === 'vm-detail' ? 'vms' : value); item.classList.toggle('active', active); if (active) item.setAttribute('aria-current', 'page'); else item.removeAttribute('aria-current'); }); text('#sidebar-title', pages[value].title); text('#sidebar-subtitle', pages[value].subtitle); text('#breadcrumb', pages[value].label); document.title = `${pages[value].label} · UVM Control`; $('#main-content').scrollTop = 0; if (!initial && previous !== value && window.matchMedia('(max-width: 700px)').matches) window.scrollTo({ top: 0, behavior: 'instant' }); if (value === 'vm-detail') { $('#detail-content').hidden = true; $('#detail-loading').hidden = false; html('#detail-loading', 'Getting to know this machine…'); text('#log-output', 'Bringing the runtime log into view…'); loadDetail(identifier); } if (value === 'system') loadDiagnostics(); if (value === 'create') updateCreateSummary(); if (!initial && value !== 'vm-detail') $(`#page-${value} h1`)?.focus({ preventScroll: true }); } function storeToken(token) { try { if (token) sessionStorage.setItem('uvm_token', token); else sessionStorage.removeItem('uvm_token'); } catch { /* A restricted browser can still use an in-memory session. */ } } async function connect(token) { if (!token || !/^[!-~]+$/.test(token)) { text('#auth-error', 'Enter a management token without spaces or non-ASCII characters.'); return; } const session = ++state.session; state.token = token; text('#auth-error', ''); const button = $('#auth-btn'); const previous = button.innerHTML; button.disabled = true; button.textContent = 'Opening your workspace…'; $('#token-input').disabled = true; try { const vms = requireList(await api('/vms')); if (session !== state.session) return; state.authenticated = true; state.vms = vms; state.loaded = true; state.lastUpdate = Date.now(); storeToken(token); $('#token-input').value = ''; $('#auth-screen').hidden = true; $('#main-app').hidden = false; renderFleet(); renderHost(); route(true); Promise.all([loadHealth(), loadDiagnostics()]); window.clearInterval(state.timer); state.timer = window.setInterval(() => { if (!document.hidden) refreshData(); }, 5000); } catch (error) { if (session !== state.session || isAborted(error)) return; state.token = ''; storeToken(''); text('#auth-error', error.status === 401 ? 'That token wasn’t accepted. Give it another look.' : error.message); $('#token-input').focus(); } finally { if (session === state.session) { button.disabled = false; button.innerHTML = previous; $('#token-input').disabled = false; if (!state.authenticated) $('#token-input').focus(); } } } function logout(message = '') { state.session += 1; state.controllers.forEach((controller) => controller.abort()); state.controllers.clear(); window.clearInterval(state.timer); state.timer = null; state.token = ''; state.authenticated = false; state.vms = []; state.loaded = false; state.health = null; state.checks = null; state.vmError = ''; state.healthError = ''; state.checksError = ''; state.lastUpdate = 0; state.lastChecks = 0; state.lastChecksAttempt = 0; state.refreshPromise = null; state.checksPromise = null; state.selected.clear(); state.busy.clear(); state.detail = null; state.detailId = null; state.portEditing = false; state.query = ''; state.filter = 'all'; state.sort = 'recent'; storeToken(''); closeMenu(); closeSearch(); setMobileNav(false); if ($('#confirm-dialog').open) $('#confirm-dialog').close('cancel'); $('#create-form').reset(); resetPasswordVisibility(); $('#install-force').checked = false; $('#install-result').hidden = true; $('#bulk-results').hidden = true; $('#vm-search').value = ''; $('#vm-sort').value = 'recent'; $('#global-search').value = ''; $('#token-input').value = ''; $('#token-input').disabled = false; text('#create-error', ''); text('#port-error', ''); text('#auth-error', message); $('#main-app').hidden = true; $('#auth-screen').hidden = false; $('#refresh-btn').classList.remove('is-loading'); $('#toast-container').replaceChildren(); document.title = 'UVM Control'; $('#token-input').focus(); } function handleAction(target) { const action = target.dataset.action; const id = target.dataset.id; if (action === 'search') focusSearch(); if (action === 'refresh') manualRefresh(); if (action === 'diagnostics') loadDiagnostics(); if (action === 'machine-menu') openMachineMenu(target); if (action === 'inspect') { closeMenu(); location.hash = vmHref(id); } if (action === 'vm-action') { closeMenu(true); machineAction(target.dataset.operation, id); } if (action === 'clear-selection') { state.selected.clear(); renderMachines(); } if (action === 'clear-filters') { state.query = ''; state.filter = 'all'; $('#vm-search').value = ''; renderMachines(); } if (action === 'dismiss-results') $('#bulk-results').hidden = true; if (action === 'install') installAssets(); if (action === 'refresh-logs') loadLogs(); if (action === 'copy-ssh') copyText($('#ssh-command').textContent); if (action === 'delete-current' && state.detail) machineAction('delete', state.detail.id); if (action === 'sync-ports' && state.detail) machineAction('ports-sync', state.detail.id); if (action === 'edit-ports') { state.portEditing = true; text('#port-error', ''); renderDetail(); $('#port-input').focus(); } if (action === 'cancel-ports') { state.portEditing = false; text('#port-error', ''); renderDetail(); } if (action === 'toggle-password') { const visible = $('#create-password').type === 'password'; $('#create-password').type = visible ? 'text' : 'password'; target.setAttribute('aria-pressed', String(visible)); target.setAttribute('aria-label', visible ? 'Hide password' : 'Show password'); } } function bindEvents() { $('#auth-form').addEventListener('submit', (event) => { event.preventDefault(); connect($('#token-input').value.trim()); }); $('#logout-btn').addEventListener('click', () => logout()); $('#refresh-btn').addEventListener('click', manualRefresh); $('#create-form').addEventListener('submit', submitCreate); $('#create-form').addEventListener('input', updateCreateSummary); $('#port-editor').addEventListener('submit', submitPorts); $('#bulk-run-btn').addEventListener('click', runBulkAction); $('#log-tail').addEventListener('change', loadLogs); $('#vm-search').addEventListener('input', (event) => { state.query = event.target.value; renderMachines(); }); $('#vm-sort').addEventListener('change', (event) => { state.sort = event.target.value; renderMachines(); }); $('#vm-select-all').addEventListener('change', (event) => { visibleVms().forEach((vm) => { if (event.target.checked) state.selected.add(vm.id); else state.selected.delete(vm.id); }); renderMachines(); }); $('#machine-rows').addEventListener('change', (event) => { const checkbox = event.target.closest('[data-select]'); if (!checkbox) return; if (checkbox.checked) state.selected.add(checkbox.dataset.select); else state.selected.delete(checkbox.dataset.select); const id = checkbox.dataset.select; renderMachines(); $$('[data-select]').find((item) => item.dataset.select === id)?.focus({ preventScroll: true }); }); $('#global-search').addEventListener('input', () => { state.searchIndex = -1; renderSearch(); }); $('#global-search').addEventListener('focus', renderSearch); $('#global-search').addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeSearch(); event.stopPropagation(); return; } if (!['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)) return; const results = $$('.search-result'); if (!results.length) return; event.preventDefault(); if (event.key === 'Enter') { const selected = results[Math.max(0, state.searchIndex)]; location.hash = selected.getAttribute('href'); $('#global-search').value = ''; closeSearch(); $('#global-search').blur(); } else { state.searchIndex = (state.searchIndex + (event.key === 'ArrowDown' ? 1 : -1) + results.length) % results.length; renderSearch(); $(`#search-option-${state.searchIndex}`)?.scrollIntoView({ block: 'nearest' }); } }); $('#nav-toggle').addEventListener('click', () => setMobileNav(!$('#main-app').classList.contains('nav-open'))); $('#nav-scrim').addEventListener('click', () => setMobileNav(false, true)); $('#confirm-dialog').addEventListener('click', (event) => { if (event.target !== $('#confirm-dialog')) return; const rect = event.target.getBoundingClientRect(); if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) event.target.close('cancel'); }); document.addEventListener('click', (event) => { const target = event.target.closest('[data-action]'); if (!event.target.closest('.global-search')) closeSearch(); if (!event.target.closest('#action-menu, [data-action="machine-menu"]')) closeMenu(); if (event.target.closest('.search-result')) { $('#global-search').value = ''; closeSearch(); } if (event.target.closest('a[href^="#"]')) setMobileNav(false); const filter = event.target.closest('[data-filter]'); if (filter) { state.filter = filter.dataset.filter; renderMachines(); } const preset = event.target.closest('[data-preset]'); if (preset) { const sizes = { small: [1, '512M'], balanced: [2, '1G'], roomy: [4, '2G'] }; const [cpu, ram] = sizes[preset.dataset.preset]; $('#create-cpu').value = cpu; $('#create-ram').value = ram; updateCreateSummary(); } if (target && !target.disabled) handleAction(target); }); document.addEventListener('keydown', (event) => { const editing = event.target.closest('input, textarea, select, [contenteditable="true"]'); if (state.authenticated && ((event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) || (event.key === '/' && !editing))) { if ($('#confirm-dialog').open) return; event.preventDefault(); focusSearch(); } if (event.key === 'Escape') { closeSearch(); closeMenu(true); if ($('#main-app').classList.contains('nav-open')) setMobileNav(false, true); } if (!$('#action-menu').hidden && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) { event.preventDefault(); const items = $$('button:not(:disabled)', $('#action-menu')); let index = items.indexOf(document.activeElement); index = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : (index + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length; items[index]?.focus(); } if (event.key === 'Tab' && $('#main-app').classList.contains('nav-open')) { const items = $$('a, button:not(:disabled)', $('#workspace-nav')); if (event.shiftKey && document.activeElement === items[0]) { event.preventDefault(); items.at(-1).focus(); } if (!event.shiftKey && document.activeElement === items.at(-1)) { event.preventDefault(); items[0].focus(); } } }); document.addEventListener('focusin', (event) => { if (!event.target.closest('.global-search')) closeSearch(); if (!$('#action-menu').hidden && !event.target.closest('#action-menu, [data-action="machine-menu"]')) closeMenu(); }); $('#main-content').addEventListener('scroll', () => { closeMenu(); closeSearch(); }, { passive: true }); window.addEventListener('resize', () => { closeMenu(); if (!window.matchMedia('(max-width: 700px)').matches) setMobileNav(false); }); window.addEventListener('hashchange', () => route()); document.addEventListener('visibilitychange', () => { if (!document.hidden && state.authenticated) refreshData(); }); } function boot() { bindEvents(); text('#today-label', new Intl.DateTimeFormat(undefined, { weekday: 'short', month: 'short', day: 'numeric' }).format(new Date())); updateCreateSummary(); let stored = ''; try { stored = sessionStorage.getItem('uvm_token') || ''; } catch { /* Session storage is optional. */ } if (stored) connect(stored); } boot(); })();