Files
nest/uvm/dashboard/static/js/dashboard.js
T

1341 lines
66 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
})[character]);
}
function icon(name, className = '') {
return `<svg class="icon ${className}" aria-hidden="true"><use href="#i-${name}"></use></svg>`;
}
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 `<span class="badge badge-${tone(status)}">${esc(label(status))}</span>`;
}
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 `<article class="compact-metric"><span class="round-icon">${icon(symbol)}</span><div><strong${word ? ' class="metric-word"' : ''}>${esc(value)}</strong><span>${esc(caption)}</span></div></article>`;
}
function emptyState(title, description, buttonLabel, target, isAction = false) {
const control = isAction
? `<button class="button button-small" type="button" data-action="${target}">${esc(buttonLabel)}${icon('arrow')}</button>`
: `<a class="button button-primary button-small" href="${esc(target)}">${icon('plus')}${esc(buttonLabel)}</a>`;
return `<div class="empty-state"><span class="empty-icon">${icon('server')}</span><h3>${esc(title)}</h3><p>${esc(description)}</p>${control}</div>`;
}
function portsMarkup(ports, limit = Infinity) {
if (ports === null) return '<span class="muted">Legacy policy</span>';
if (!ports?.length) return '<span class="muted">No open ports</span>';
return ports.slice(0, limit).map((rule) => `<span class="port-chip">${esc(rule.port)}<small>${esc(rule.protocol)}</small></span>`).join('')
+ (ports.length > limit ? `<span class="port-chip">+${ports.length - limit}</span>` : '');
}
function renderConnection() {
const connected = state.loaded && !state.vmError;
$('#connection-dot').className = `dot ${connected ? 'dot-sage' : state.vmError ? 'dot-error' : ''}`;
$('#footer-dot').className = `dot ${connected ? 'dot-sage' : 'dot-error'}`;
text('#connection-label', connected ? 'Connected' : state.vmError ? 'Reconnecting' : 'Connecting');
$('#connection-banner').hidden = !state.vmError;
text('#connection-message', state.vmError ? `${state.vmError}${state.loaded ? ' Showing the last available snapshot.' : ''}` : '');
text('#last-updated', state.lastUpdate ? `Updated ${new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(state.lastUpdate)}` : 'Waiting for an update');
}
function renderChrome() {
const stats = fleetStats();
text('#nav-vm-count', state.loaded ? stats.total : '—');
text('#rail-machines', state.loaded ? `${stats.total} ${stats.total === 1 ? 'machine' : 'machines'} in your fleet` : 'Your private fleet');
text('#rail-network', state.loaded ? `${stats.ports} private port ${stats.ports === 1 ? 'rule' : 'rules'}` : 'Connections, considered');
const failed = (state.checks || []).filter((check) => !check.ok).length;
const checksKnown = state.checks !== null;
text('#rail-health', checksKnown ? `${state.checks.length - failed} of ${state.checks.length} checks ready` : state.checksError ? 'Checks unavailable' : 'Checking the essentials');
$('#nav-health-dot').style.background = checksKnown ? (failed ? 'var(--peach)' : 'var(--sage)') : 'transparent';
if (state.checksError) {
text('#insight-copy', 'Host insights are taking a little longer. Open host health to try again.');
} else if (checksKnown && failed) {
html('#insight-copy', `A little care goes a long way. <strong>${failed} host ${failed === 1 ? 'check needs' : 'checks need'} your attention.</strong>`);
} else if (checksKnown && state.checks.length) {
html('#insight-copy', `The essentials are in place. <strong>All ${state.checks.length} host checks are ready.</strong>`);
} else if (checksKnown) {
text('#insight-copy', 'Your host returned no readiness checks. Open host health for the details.');
} else {
text('#insight-copy', 'A fresh perspective on your local compute. Checking the little details.');
}
text('#insight-footer', checksKnown ? 'From your latest host checks' : 'A little clarity, on its way');
renderConnection();
}
function renderOverview() {
const stats = fleetStats();
text('#metric-total', state.loaded ? number(stats.total).padStart(2, '0') : '—');
text('#metric-running', state.loaded ? `${stats.running} running now` : 'Waiting for the fleet');
text('#metric-stopped', state.loaded ? `${stats.stopped} at rest` : '—');
text('#metric-cpu', state.loaded ? `${number(stats.cpu)} vCPU` : '— vCPU');
text('#metric-ram', state.loaded ? formatRam(stats.ram) : '— MiB');
text('#cpu-active-label', state.loaded ? `${number(stats.activeCpu)} active / ${number(stats.cpu)}` : 'Allocated');
text('#ram-active-label', state.loaded ? `${Math.round(percent(stats.activeRam, stats.ram))}% active` : 'Allocated');
$('#cpu-allocation-bar').style.width = `${percent(stats.activeCpu, stats.cpu)}%`;
$('#ram-allocation-bar').style.width = `${percent(stats.activeRam, stats.ram)}%`;
if (!state.loaded) {
html('#overview-machines', state.vmError
? emptyState('A little trouble connecting.', 'Your machines will appear as soon as the registry is reachable.', 'Try again', 'refresh', true)
: '<div class="loading-state">Bringing your machines into view…</div>');
} else if (!stats.total) {
html('#overview-machines', emptyState('Your first machine starts here.', 'A private little space for whatever you have in mind.', 'Create a machine', '#create'));
} else {
const recent = [...state.vms].sort((a, b) => (timestamp(b.created_at) || 0) - (timestamp(a.created_at) || 0)).slice(0, 5);
html('#overview-machines', `<div class="table-scroll"><table class="portfolio-table"><thead><tr><th>Machine</th><th>Runtime state</th><th>Guest address</th><th>Allocation</th><th>Port policy</th><th><span class="sr-only">Details</span></th></tr></thead><tbody>${recent.map((vm) => `
<tr><td><div class="portfolio-name">${icon('server')}<a href="${esc(vmHref(vm.id))}">${esc(machineName(vm))}</a></div></td><td>${badge(runtimeStatus(vm))}</td><td>${esc(vm.guest_ip || '—')}</td><td class="resource-inline">${vcpu(vm)} vCPU<span>·</span>${esc(formatRam(vm.ram_mib))}</td><td>${badge(vm.policy_status || 'unknown')}</td><td><a class="mini-link" href="${esc(vmHref(vm.id))}" aria-label="Inspect ${esc(machineName(vm))}">${icon('chevron')}</a></td></tr>`).join('')}</tbody></table></div>`);
}
const runningPercent = Math.round(percent(stats.running, stats.total));
html('#ring-percent', `${state.loaded ? runningPercent : '—'}<small>%</small>`);
text('#ring-total', state.loaded ? `${stats.running} of ${stats.total} machines` : 'Your fleet, in balance');
text('#legend-running', state.loaded ? stats.running : '—');
text('#legend-stopped', state.loaded ? stats.stopped : '—');
text('#legend-other', state.loaded ? stats.other : '—');
$('#fleet-ring').setAttribute('aria-label', state.loaded
? `${stats.running} running, ${stats.stopped} stopped, and ${stats.other} other machines. ${runningPercent}% running.`
: 'Waiting for machine activity');
let offset = 0;
[['running', stats.running], ['stopped', stats.stopped], ['other', stats.other]].forEach(([name, count]) => {
const share = percent(count, stats.total);
const arc = share === 100 ? 100 : Math.max(0, share - 1.5);
const element = $(`#ring-${name}`);
element.style.strokeDasharray = `${arc} ${100 - arc}`;
element.style.strokeDashoffset = String(-offset);
element.style.opacity = count && arc ? '1' : '0';
offset += share;
});
renderNotes(stats);
}
function renderNotes(stats = fleetStats()) {
const checks = state.checks;
const failed = (checks || []).filter((check) => !check.ok).length;
const rows = [
['system', 'heart', 'Host essentials', checks ? failed ? `${failed} ${failed === 1 ? 'check could' : 'checks could'} use a look` : checks.length ? 'Everything is in its place' : 'No checks returned' : state.checksError ? 'Host checks unavailable' : 'Checking the foundations', checks ? `${checks.length - failed}/${checks.length}` : '—'],
['network', 'network', 'Your private network', stats.gateways.length ? stats.gateways.join(' · ') : 'A gateway appears with your first machine', stats.gateways.length ? `${stats.gateways.length} ${stats.gateways.length === 1 ? 'gateway' : 'gateways'}` : '—'],
['network', 'shield', 'Considered connections', stats.managed ? `${stats.managed} managed ${stats.managed === 1 ? 'policy' : 'policies'} in your fleet` : 'A private policy for each new machine', state.loaded ? `${stats.ports} ${stats.ports === 1 ? 'rule' : 'rules'}` : '—'],
];
html('#workspace-notes', rows.map(([route, symbol, title, detail, value]) => `<a class="note-row" href="#${route}">${icon(symbol)}<div><strong>${esc(title)}</strong><small>${esc(detail)}</small></div><span>${esc(value)}</span>${icon('chevron')}</a>`).join(''));
}
function visibleVms() {
const query = state.query.trim().toLowerCase();
return state.vms.filter((vm) => {
const status = runtimeStatus(vm);
const matchesStatus = state.filter === 'all' || (state.filter === 'attention'
? !['running', 'stopped'].includes(status) || tone(vm.policy_status) === 'attention'
: status === state.filter);
const matchesQuery = !query || [vm.name, vm.id, vm.guest_ip].some((value) => String(value || '').toLowerCase().includes(query));
return matchesStatus && matchesQuery;
}).sort((a, b) => {
if (state.sort === 'name') return machineName(a).localeCompare(machineName(b));
if (state.sort === 'status') return runtimeStatus(a).localeCompare(runtimeStatus(b));
return (timestamp(b.created_at) || 0) - (timestamp(a.created_at) || 0);
});
}
function renderMachines() {
const stats = fleetStats();
html('#fleet-summary', compactMetric('server', state.loaded ? stats.total : '—', 'Machines in your collection')
+ compactMetric('play', state.loaded ? stats.running : '—', 'Running, right now')
+ compactMetric('stop', state.loaded ? stats.stopped : '—', 'At rest, disks retained'));
const machines = visibleVms();
html('#machine-rows', machines.map((vm) => {
const id = esc(vm.id);
const running = runtimeStatus(vm) === 'running';
const busy = state.busy.has(vm.id) || ['starting', 'stopping', 'terminating'].includes(vm.status);
return `<tr class="${state.selected.has(vm.id) ? 'selected' : ''}">
<td class="check-cell"><input type="checkbox" data-select="${id}" aria-label="Select ${esc(machineName(vm))}" ${state.selected.has(vm.id) ? 'checked' : ''} /></td>
<td><div class="machine-name"><span class="round-icon">${icon('server')}</span><div><a href="${esc(vmHref(vm.id))}" title="${esc(machineName(vm))}">${esc(machineName(vm))}</a><small>${esc(vm.guest_ip || vm.id)}</small></div></div></td>
<td>${badge(runtimeStatus(vm))}</td><td class="allocation-cell">${vcpu(vm)} vCPU<small>${esc(formatRam(vm.ram_mib))}</small></td>
<td><div class="table-ports">${portsMarkup(vm.ports, 2)}</div></td><td>${badge(vm.policy_status || 'unknown')}</td>
<td><div class="row-controls"><button class="icon-button" type="button" data-action="vm-action" data-operation="${running ? 'stop' : 'start'}" data-id="${id}" aria-label="${running ? 'Stop' : 'Start'} ${esc(machineName(vm))}" title="${running ? 'Stop' : 'Start'} machine" ${busy ? 'disabled' : ''}>${icon(running ? 'stop' : 'play')}</button><button class="icon-button" type="button" data-action="machine-menu" data-id="${id}" aria-label="Actions for ${esc(machineName(vm))}" aria-haspopup="menu" aria-expanded="false">${icon('more')}</button></div></td>
</tr>`;
}).join(''));
$('#machines-empty').hidden = machines.length > 0;
if (!machines.length) {
html('#machines-empty', !state.loaded
? state.vmError ? emptyState('The registry is out of reach.', state.vmError, 'Try again', 'refresh', true) : '<div class="loading-state">Bringing your machines into view…</div>'
: stats.total ? emptyState('Nothing here by that name.', 'Try another search or give your filters a fresh start.', 'Clear filters', 'clear-filters', true)
: emptyState('A little quiet. A lot of potential.', 'Create your first private machine and give your next idea a home.', 'New machine', '#create'));
}
text('#visible-count', state.loaded ? `${machines.length} of ${stats.total}` : 'Loading');
text('#fleet-list-caption', state.loaded ? `${stats.total} ${stats.total === 1 ? 'machine' : 'machines'} · ${stats.running} running · ${stats.stopped} stopped` : 'Your private machine registry.');
const selectedVisible = machines.filter((vm) => state.selected.has(vm.id)).length;
const selectAll = $('#vm-select-all');
selectAll.disabled = !machines.length;
selectAll.checked = machines.length > 0 && selectedVisible === machines.length;
selectAll.indeterminate = selectedVisible > 0 && selectedVisible < machines.length;
$('#bulk-controls').hidden = !state.selected.size;
text('#bulk-count', `${state.selected.size} selected`);
$$('.filter-tab').forEach((button) => {
const active = button.dataset.filter === state.filter;
button.classList.toggle('active', active);
button.setAttribute('aria-pressed', String(active));
});
}
function renderNetwork() {
const stats = fleetStats();
html('#network-summary', compactMetric('network', state.loaded ? stats.gateways.length : '—', 'Private gateways')
+ compactMetric('shield', state.loaded ? stats.managed : '—', 'Managed port policies')
+ compactMetric('grid', state.loaded ? stats.ports : '—', 'Configured TCP rules'));
text('#network-gateway', stats.gateways.join(' · ') || (state.loaded ? 'No gateway yet' : 'Loading'));
text('#network-machine-count', state.loaded ? `${stats.total} private ${stats.total === 1 ? 'machine' : 'machines'}` : 'Waiting for your fleet');
html('#network-policies', state.vms.length ? state.vms.map((vm) => `<a href="${esc(vmHref(vm.id))}" class="policy-row"><div class="policy-row-heading"><strong>${esc(machineName(vm))}</strong>${icon('chevron')}</div><div class="policy-row-meta"><span>${esc(vm.guest_ip || 'Address pending')}</span>${badge(vm.policy_status || 'unknown')}</div><div class="table-ports">${portsMarkup(vm.ports)}</div></a>`).join('')
: !state.loaded ? state.vmError ? emptyState('Connections are out of view.', state.vmError, 'Try again', 'refresh', true) : '<div class="loading-state">Looking at your connections…</div>'
: emptyState('Good connections take a first step.', 'Each new machine comes with a private address and an explicit port policy.', 'Create a machine', '#create'));
}
function renderHost() {
const checks = state.checks;
const ready = (checks || []).filter((check) => check.ok).length;
const failed = (checks || []).length - ready;
text('#checks-ready', checks ? ready : '—');
text('#checks-attention', checks ? failed : '—');
text('#checks-total', checks ? checks.length : '—');
$('#checks-ready-bar').style.width = `${percent(ready, checks?.length)}%`;
$('#checks-attention-bar').style.width = `${percent(failed, checks?.length)}%`;
text('#checks-summary', checks ? `${ready} / ${checks.length} ready` : state.checksError ? 'Unavailable' : 'Taking a look');
const healthy = state.health?.status === 'ok';
html('#health-summary', compactMetric('heart', state.health ? healthy ? 'Available' : label(state.health.status) : state.healthError ? 'Unavailable' : 'Checking', 'Management API', true)
+ compactMetric('check', checks ? `${ready} / ${checks.length}` : '—', 'Host checks ready')
+ compactMetric('info', checks ? failed : '—', 'Checks needing attention'));
text('#diagnostics-count', checks ? `${ready} / ${checks.length} ready` : state.checksError ? 'Unavailable' : 'Checking');
text('#info-api-url', location.origin);
text('#info-observed', state.lastChecks ? formatDate(state.lastChecks) : 'Not checked yet');
const names = {
root: 'Host permissions', kvm: 'KVM virtualization', Firecracker: 'Firecracker runtime',
'guest assets': 'Guest image assets', 'private firewall': 'Private port filtering',
ip: 'IP networking', iptables: 'Egress routing', nft: 'nftables', conntrack: 'Connection tracking',
debugfs: 'Guest disk tools', openssl: 'OpenSSL', 'ssh-keygen': 'SSH key tools', ssh: 'SSH client',
};
html('#diagnostics-list', state.checksError
? emptyState('A check-in is taking a little longer.', state.checksError, 'Try again', 'diagnostics', true)
: checks ? checks.length ? checks.map((check) => `<article class="diagnostic-item${check.ok ? '' : ' attention'}"><span class="diagnostic-icon">${icon(check.ok ? 'check' : 'info')}</span><div><h3>${esc(names[check.name] || label(check.name))}<span>${check.ok ? 'Ready' : 'Attention'}</span></h3><p>${esc(check.message)}</p></div></article>`).join('')
: '<p class="panel-footnote">The host returned no diagnostic checks.</p>'
: '<div class="loading-state">Checking in with your host…</div>');
$('#diagnostics-refresh-btn').disabled = Boolean(state.checksPromise);
renderChrome();
renderNotes();
}
function renderFleet() {
renderChrome();
renderOverview();
renderMachines();
renderNetwork();
if (!$('#search-results').hidden) renderSearch();
}
async function loadVms() {
const request = ++state.requests.vms;
const session = state.session;
try {
const vms = requireList(await api('/vms'));
if (request !== state.requests.vms || session !== state.session) return false;
state.vms = vms;
state.loaded = true;
state.vmError = '';
state.lastUpdate = Date.now();
const existing = new Set(vms.map((vm) => vm.id));
state.selected = new Set([...state.selected].filter((id) => existing.has(id)));
renderFleet();
if (state.page === 'vm-detail' && state.detail) {
const vm = vms.find((item) => item.id === state.detail.id);
if (vm) {
state.detail = vm;
renderDetail();
} else {
await loadDetail(state.detail.id);
}
}
return true;
} catch (error) {
if (isAborted(error) || request !== state.requests.vms || session !== state.session) return false;
state.vmError = error.message;
renderConnection();
if (!state.loaded) renderFleet();
return false;
}
}
async function loadHealth() {
const request = ++state.requests.health;
const session = state.session;
try {
const health = await api('/health');
if (request !== state.requests.health || session !== state.session) return false;
if (!health || typeof health.status !== 'string') throw new Error('The health response was not recognized.');
state.health = health;
state.healthError = '';
renderHost();
return true;
} catch (error) {
if (isAborted(error) || request !== state.requests.health || session !== state.session) return false;
state.health = null;
state.healthError = error.message;
renderHost();
return false;
}
}
function loadDiagnostics() {
if (state.checksPromise) return state.checksPromise;
const session = state.session;
state.lastChecksAttempt = Date.now();
const operation = (async () => {
try {
const checks = requireList(await api('/diagnostics'));
if (session !== state.session) return false;
state.checks = checks;
state.checksError = '';
state.lastChecks = Date.now();
return true;
} catch (error) {
if (isAborted(error) || session !== state.session) return false;
state.checks = null;
state.checksError = error.message;
return false;
} finally {
if (state.checksPromise === operation) state.checksPromise = null;
if (session === state.session) renderHost();
}
})();
state.checksPromise = operation;
$('#diagnostics-refresh-btn').disabled = true;
return operation;
}
function refreshData(forceChecks = false) {
if (!state.authenticated) return Promise.resolve(false);
if (state.refreshPromise) return state.refreshPromise;
const session = state.session;
$('#refresh-btn').classList.add('is-loading');
const loads = [loadVms(), loadHealth()];
if (forceChecks || Date.now() - state.lastChecksAttempt > 30000) loads.push(loadDiagnostics());
const operation = Promise.all(loads).then((results) => results.every(Boolean)).finally(() => {
if (state.refreshPromise === operation) state.refreshPromise = null;
if (session === state.session) $('#refresh-btn').classList.remove('is-loading');
});
state.refreshPromise = operation;
return operation;
}
async function manualRefresh() {
const session = state.session;
const success = await refreshData(true);
if (session === state.session && state.authenticated) toast(success ? 'A fresh look at your workspace.' : 'Some workspace data could not be refreshed.', !success);
}
function detailRows(rows) {
return rows.map(([key, value, mono]) => `<div><dt>${esc(key)}</dt><dd${mono ? ' class="mono"' : ''}>${esc(value ?? '—')}</dd></div>`).join('');
}
function renderDetail() {
const vm = state.detail;
if (!vm || state.page !== 'vm-detail') return;
$('#detail-loading').hidden = true;
$('#detail-content').hidden = false;
text('#detail-heading', machineName(vm));
text('#detail-id', vm.id);
html('#detail-status', badge(runtimeStatus(vm)));
document.title = `${machineName(vm)} · UVM Control`;
const busy = state.busy.has(vm.id);
const running = runtimeStatus(vm) === 'running';
const actionButton = (action, caption, symbol, primary = false) => `<button type="button" class="button${primary ? ' button-primary' : ''}" data-action="vm-action" data-operation="${action}" data-id="${esc(vm.id)}" ${busy ? 'disabled' : ''}>${icon(symbol)}${caption}</button>`;
html('#detail-actions', running
? actionButton('restart', 'Restart', 'refresh') + actionButton('stop', 'Stop machine', 'stop')
: actionButton('start', 'Start machine', 'play', true));
html('#detail-config', detailRows([
['Recorded state', label(vm.status)], ['Observed state', label(runtimeStatus(vm))],
['vCPUs', vcpu(vm)], ['Memory', formatRam(vm.ram_mib)], ['Guest user', vm.username],
['Created', formatDate(vm.created_at)], ['Last updated', formatDate(vm.updated_at)],
]));
html('#detail-network', detailRows([
['Guest address', vm.guest_ip, true], ['Gateway', vm.gateway, true], ['MAC address', vm.mac, true],
['Policy state', label(vm.policy_status)], ['Policy type', vm.ports === null ? 'Legacy / unmanaged' : 'Managed TCP'],
['Process ID', vm.pid ?? 'Not running'], ['Current operation', vm.operation ? label(vm.operation) : 'None'],
]));
text('#detail-error', vm.last_error || '');
$('#detail-error').hidden = !vm.last_error;
text('#ssh-command', `ssh ${vm.username || 'root'}@${vm.guest_ip || '—'}`);
text('#ssh-hint', vm.ports === null ? 'SSH reachability is not managed for this legacy policy.'
: !(vm.ports || []).some((rule) => Number(rule.port) === 22) ? 'TCP port 22 is not allowed by this policy.'
: !running ? 'Start this machine before connecting.' : 'Ready to copy into your local terminal.');
if (!state.portEditing) {
html('#detail-ports', vm.ports === null
? `<p>${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.'}</p>`
: 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', `<header><h2>${results.length - failures.length} succeeded · ${failures.length} need attention</h2><button class="icon-button" type="button" data-action="dismiss-results" aria-label="Dismiss action results">${icon('close')}</button></header><ul>${failures.map((result) => `<li><strong>${esc(result.id)}</strong> · ${esc(label(result.outcome))}${result.error ? `: ${esc(result.error)}` : ''}</li>`).join('')}</ul>`);
}
toast(failures.length ? `${results.length - failures.length} succeeded. See the details for the remaining machines.` : `${results.length} ${results.length === 1 ? 'machine' : 'machines'} updated.`, Boolean(failures.length));
await loadVms();
} catch (error) {
if (!isAborted(error) && session === state.session) toast(error.message, true);
} finally {
if (session === state.session) {
state.busy.delete('bulk');
ids.forEach((id) => state.busy.delete(id));
renderMachines();
}
}
});
}
async function installAssets() {
const session = state.session;
const force = $('#install-force').checked;
await withButton($('#install-btn'), 'Preparing your host…', async () => {
$('#install-force').disabled = true;
$('#install-result').hidden = false;
text('#install-result', 'Installing the host and guest assets. This can take a few minutes.');
try {
const result = await api('/install', { method: 'POST', body: { force } });
if (session !== state.session) return;
text('#install-result', `Installation complete.\n\nFirecracker\n${result.firecracker}\n\nGuest kernel\n${result.kernel}\n\nRoot filesystem\n${result.rootfs}`);
toast('A fresh foundation for your machines.');
await Promise.all([loadDiagnostics(), loadHealth()]);
} catch (error) {
if (!isAborted(error) && session === state.session) {
text('#install-result', error.message);
toast('Installation could not finish. See the host details.', true);
}
} finally { $('#install-force').disabled = false; }
});
}
function closeMenu(restoreFocus = false) {
$('#action-menu').hidden = true;
if (state.menuTrigger?.isConnected) {
state.menuTrigger.setAttribute('aria-expanded', 'false');
if (restoreFocus) state.menuTrigger.focus();
}
state.menuTrigger = null;
}
function openMachineMenu(trigger) {
if (state.menuTrigger === trigger && !$('#action-menu').hidden) return closeMenu(true);
closeMenu();
const id = trigger.dataset.id;
const vm = state.vms.find((item) => item.id === id);
if (!vm) return;
const running = runtimeStatus(vm) === 'running';
const items = [['inspect', 'eye', 'Inspect machine']];
if (running) items.push(['restart', 'refresh', 'Restart'], ['stop', 'stop', 'Stop machine']);
else items.push(['start', 'play', 'Start machine']);
if (vm.ports !== null) items.push(['ports-sync', 'shield', 'Sync port policy']);
items.push(['delete', 'trash', 'Delete machine']);
const menu = $('#action-menu');
html(menu, items.map(([action, symbol, caption]) => `<button type="button" role="menuitem" data-action="${action === 'inspect' ? 'inspect' : 'vm-action'}" data-operation="${action}" data-id="${esc(id)}" class="${action === 'delete' ? 'menu-danger' : ''}" ${state.busy.has(id) && action !== 'inspect' ? 'disabled' : ''}>${icon(symbol)}${caption}</button>`).join(''));
state.menuTrigger = trigger;
trigger.setAttribute('aria-expanded', 'true');
menu.hidden = false;
const rect = trigger.getBoundingClientRect();
const height = menu.getBoundingClientRect().height;
menu.style.left = `${Math.max(12, Math.min(rect.right - 181, window.innerWidth - 193))}px`;
menu.style.top = `${Math.max(12, rect.bottom + height + 8 > window.innerHeight ? rect.top - height - 6 : rect.bottom + 6)}px`;
$('button', menu)?.focus();
}
function closeSearch() {
$('#search-results').hidden = true;
$('#global-search').setAttribute('aria-expanded', 'false');
$('#global-search').removeAttribute('aria-activedescendant');
state.searchIndex = -1;
}
function renderSearch() {
if (!state.authenticated) return;
const query = $('#global-search').value.trim().toLowerCase();
const candidates = Object.entries(pages).filter(([key]) => key !== 'vm-detail').map(([key, page]) => ({
title: page.label, description: 'Workspace page', href: `#${key}`, icon: page.icon, search: page.label,
})).concat(state.vms.map((vm) => ({ title: machineName(vm), description: `${vm.guest_ip || vm.id} · ${label(runtimeStatus(vm))}`, href: vmHref(vm.id), icon: 'server', search: [vm.name, vm.id, vm.guest_ip].join(' ') })));
const results = candidates.filter((item) => !query || item.search.toLowerCase().includes(query)).slice(0, 8);
state.searchIndex = Math.min(state.searchIndex, results.length - 1);
html('#search-results', results.length ? results.map((item, index) => `<a id="search-option-${index}" class="search-result" href="${esc(item.href)}" role="option" aria-selected="${index === state.searchIndex}" tabindex="-1">${icon(item.icon)}<div><strong>${esc(item.title)}</strong><small>${esc(item.description)}</small></div>${icon('arrow')}</a>`).join('') : '<p class="search-empty">No machines or pages found. Try another name.</p>');
$('#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();
})();