/* ============================================================
Sovereign Orchestrator — Frontend Application
Pure vanilla JS — no frameworks, no libraries
============================================================ */
const App = (() => {
'use strict';
// ── State ─────────────────────────────────────────────────
const state = {
currentSection: 'dashboard',
systemStatus: null,
builds: [],
isos: [],
vms: {}, // vmid -> status data
pollTimer: null,
activeBuildId: null,
buildPollTimer: null,
};
// ── DOM Refs ──────────────────────────────────────────────
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
// ── API Helper ────────────────────────────────────────────
async function api(method, path, body = null) {
const opts = {
method,
headers: { 'Content-Type': 'application/json' },
};
if (body) opts.body = JSON.stringify(body);
const res = await fetch(path, opts);
if (!res.ok) {
let detail = `HTTP ${res.status}`;
try {
const j = await res.json();
detail = j.detail || j.message || detail;
} catch (_) { /* ignore */ }
throw new Error(detail);
}
return res.json();
}
// ── Toast System ──────────────────────────────────────────
const TOAST_ICONS = {
success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️',
};
function toast(type, message, duration = 4500) {
const container = $('#toast-container');
const el = document.createElement('div');
el.className = `toast ${type}`;
el.innerHTML = `
${TOAST_ICONS[type] || ''}
${escHtml(message)}
`;
container.appendChild(el);
setTimeout(() => {
el.classList.add('leaving');
el.addEventListener('animationend', () => el.remove());
}, duration);
}
function escHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// ── Navigation ────────────────────────────────────────────
function initNav() {
$$('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
navigateTo(link.dataset.section);
});
});
// Quick action cards
$$('.action-card[data-goto]').forEach(card => {
card.addEventListener('click', () => navigateTo(card.dataset.goto));
});
// Mobile toggle
const toggle = $('#mobile-toggle');
const sidebar = $('#sidebar');
toggle.addEventListener('click', () => sidebar.classList.toggle('open'));
// Close sidebar on mobile when nav clicked
$$('.nav-link').forEach(link => {
link.addEventListener('click', () => sidebar.classList.remove('open'));
});
// Handle hash on load
const hash = location.hash.replace('#', '');
if (hash && $(`[data-section="${hash}"]`)) {
navigateTo(hash);
}
}
function navigateTo(section) {
state.currentSection = section;
// Update nav
$$('.nav-link').forEach(l => l.classList.remove('active'));
const activeLink = $(`.nav-link[data-section="${section}"]`);
if (activeLink) activeLink.classList.add('active');
// Update sections
$$('.section').forEach(s => s.classList.remove('active'));
const activeSection = $(`#section-${section}`);
if (activeSection) {
activeSection.classList.remove('active');
// Force reflow for animation
void activeSection.offsetWidth;
activeSection.classList.add('active');
}
location.hash = section;
// Section-specific loads
if (section === 'deployments') {
loadBuilds();
loadISOs();
} else if (section === 'dashboard') {
refreshAll();
}
}
// ── System Status ─────────────────────────────────────────
async function fetchStatus() {
try {
const data = await api('GET', '/api/status');
state.systemStatus = data;
updateStatusUI(data);
} catch (err) {
updateStatusUI(null, err.message);
}
}
function updateStatusUI(data, error) {
const badge = $('#system-status-badge');
const dashStatus = $('#dash-system-status');
const indicator = $('#dash-status-indicator');
if (error) {
badge.className = 'status-badge error';
badge.querySelector('.status-text').textContent = 'Offline';
dashStatus.textContent = 'Offline';
if (indicator) indicator.innerHTML = '';
return;
}
badge.className = 'status-badge';
badge.querySelector('.status-text').textContent = 'System Online';
dashStatus.textContent = data.status || 'Online';
// Update dashboard counts if available
if (data.iso_count !== undefined) $('#dash-iso-count').textContent = data.iso_count;
if (data.build_count !== undefined) $('#dash-build-count').textContent = data.build_count;
if (data.vm_count !== undefined) $('#dash-vm-count').textContent = data.vm_count;
}
// ── Builds ────────────────────────────────────────────────
async function loadBuilds() {
try {
const data = await api('GET', '/api/builds');
state.builds = Array.isArray(data) ? data : (data.builds || []);
renderBuilds();
renderTimeline();
$('#dash-build-count').textContent = state.builds.length;
} catch (err) {
// Silently handle — builds endpoint may not exist yet
console.warn('Failed to load builds:', err.message);
}
}
function renderBuilds() {
const container = $('#builds-list');
if (!state.builds.length) {
container.innerHTML = `
`;
return;
}
container.innerHTML = state.builds.map(b => {
const statusClass = b.status === 'success' || b.status === 'completed' ? 'success'
: b.status === 'error' || b.status === 'failed' ? 'error'
: 'pending';
return `
${escHtml(b.fqdn || b.name || b.id || 'Build')}
${escHtml(b.id || b.build_id || '')}
${escHtml(b.status || 'unknown')}
`;
}).join('');
// Click to view details
$$('.build-item', container).forEach(item => {
item.addEventListener('click', () => viewBuild(item.dataset.buildId));
});
}
function renderTimeline() {
const container = $('#recent-builds-timeline');
const recent = state.builds.slice(0, 8);
if (!recent.length) {
container.innerHTML = `
📦
No builds yet. Generate your first ISO to get started.
`;
return;
}
container.innerHTML = recent.map(b => {
const statusClass = b.status === 'success' || b.status === 'completed' ? 'success'
: b.status === 'error' || b.status === 'failed' ? 'error'
: 'pending';
const timeStr = b.created_at || b.timestamp || '';
return `
${escHtml(b.fqdn || b.name || 'Build')}
${escHtml(b.id || b.build_id || '')}
${timeStr ? ` · ${escHtml(formatTime(timeStr))}` : ''}
`;
}).join('');
}
async function viewBuild(buildId) {
if (!buildId) return;
try {
const data = await api('GET', `/api/builds/${buildId}`);
const logCard = $('#build-log-card');
const logOutput = $('#build-log-output');
logCard.hidden = false;
logOutput.textContent = data.log || data.output || JSON.stringify(data, null, 2);
logOutput.scrollTop = logOutput.scrollHeight;
navigateTo('iso-builder');
} catch (err) {
toast('error', `Failed to load build: ${err.message}`);
}
}
// ── ISOs ──────────────────────────────────────────────────
async function loadISOs() {
try {
const data = await api('GET', '/api/isos');
state.isos = Array.isArray(data) ? data : (data.isos || []);
renderISOSelect();
$('#dash-iso-count').textContent = state.isos.length;
} catch (err) {
console.warn('Failed to load ISOs:', err.message);
}
}
function renderISOSelect() {
const select = $('#deploy-iso');
// Keep the first placeholder option
const placeholder = select.querySelector('option');
select.innerHTML = '';
select.appendChild(placeholder);
state.isos.forEach(iso => {
const opt = document.createElement('option');
const name = typeof iso === 'string' ? iso : (iso.name || iso.filename || iso.id || '');
opt.value = name;
opt.textContent = name;
select.appendChild(opt);
});
}
// ── ISO Builder Form ─────────────────────────────────────
function initISOForm() {
const form = $('#iso-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = $('#iso-submit-btn');
setButtonLoading(btn, true);
const formData = new FormData(form);
const payload = {};
for (const [key, value] of formData.entries()) {
if (key === 'ssh_keys') {
payload[key] = value.split('\n').map(k => k.trim()).filter(Boolean);
} else if (key === 'disk_list') {
payload[key] = value.split(',').map(d => d.trim()).filter(Boolean);
} else {
payload[key] = value;
}
}
try {
const data = await api('POST', '/api/generate-iso', payload);
toast('success', 'ISO generation started!');
// Show build log
const logCard = $('#build-log-card');
const logOutput = $('#build-log-output');
logCard.hidden = false;
logOutput.textContent = '▶ Build initiated…\n';
// If we got a build ID, start polling
const buildId = data.build_id || data.id;
if (buildId) {
state.activeBuildId = buildId;
pollBuildLog(buildId);
} else {
logOutput.textContent += JSON.stringify(data, null, 2);
}
} catch (err) {
toast('error', `ISO generation failed: ${err.message}`);
} finally {
setButtonLoading(btn, false);
}
});
}
async function pollBuildLog(buildId) {
// Clear previous timer
if (state.buildPollTimer) clearInterval(state.buildPollTimer);
const logOutput = $('#build-log-output');
const poll = async () => {
try {
const data = await api('GET', `/api/builds/${buildId}`);
const log = data.log || data.output || '';
if (log) {
logOutput.textContent = log;
logOutput.scrollTop = logOutput.scrollHeight;
}
const status = data.status || '';
if (status === 'success' || status === 'completed') {
toast('success', 'ISO build completed successfully!');
clearInterval(state.buildPollTimer);
state.buildPollTimer = null;
loadBuilds();
} else if (status === 'error' || status === 'failed') {
toast('error', 'ISO build failed.');
clearInterval(state.buildPollTimer);
state.buildPollTimer = null;
loadBuilds();
}
} catch (_) { /* ignore poll errors */ }
};
await poll();
state.buildPollTimer = setInterval(poll, 3000);
}
// ── Deploy Form ───────────────────────────────────────────
function initDeployForm() {
const form = $('#deploy-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = $('#deploy-submit-btn');
setButtonLoading(btn, true);
const formData = new FormData(form);
const payload = {};
for (const [key, value] of formData.entries()) {
if (['vmid', 'cores', 'memory', 'disk_size'].includes(key)) {
payload[key] = parseInt(value, 10);
} else {
payload[key] = value;
}
}
try {
const data = await api('POST', '/api/deploy', payload);
toast('success', `VM ${payload.vmid} deployment started!`);
// Add the VM to watched list
if (payload.vmid) {
addVMCard(payload.vmid);
}
} catch (err) {
toast('error', `Deployment failed: ${err.message}`);
} finally {
setButtonLoading(btn, false);
}
});
}
// ── VM Control ────────────────────────────────────────────
function initVMControl() {
const lookupBtn = $('#vm-lookup-btn');
const lookupInput = $('#vm-lookup-id');
lookupBtn.addEventListener('click', () => {
const vmid = parseInt(lookupInput.value, 10);
if (vmid && vmid >= 100) {
addVMCard(vmid);
lookupInput.value = '';
} else {
toast('warning', 'Enter a valid VM ID (≥ 100)');
}
});
lookupInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
lookupBtn.click();
}
});
}
async function addVMCard(vmid) {
const grid = $('#vm-cards-grid');
// Remove empty state
const empty = grid.querySelector('.empty-state');
if (empty) empty.remove();
// Don't duplicate
if ($(`#vm-card-${vmid}`, grid)) {
fetchVMStatus(vmid);
return;
}
const card = document.createElement('div');
card.className = 'glass-card vm-card';
card.id = `vm-card-${vmid}`;
card.innerHTML = `
`;
grid.appendChild(card);
card.style.animation = 'fadeSlideIn 0.4s ease both';
await fetchVMStatus(vmid);
}
async function fetchVMStatus(vmid) {
const card = $(`#vm-card-${vmid}`);
if (!card) return;
try {
const data = await api('GET', `/api/vm/${vmid}/status`);
state.vms[vmid] = data;
updateVMCard(vmid, data);
} catch (err) {
updateVMCard(vmid, { status: 'unknown', error: err.message });
}
}
function updateVMCard(vmid, data) {
const card = $(`#vm-card-${vmid}`);
if (!card) return;
const status = (data.status || 'unknown').toLowerCase();
const isRunning = status === 'running';
const isStopped = status === 'stopped';
// Status tag
const tag = card.querySelector('.vm-status-tag');
tag.className = `vm-status-tag ${isRunning ? 'running' : isStopped ? 'stopped' : 'unknown'}`;
tag.querySelector('.tag-label').textContent = capitalize(status);
// Info fields
const setField = (name, val) => {
const el = card.querySelector(`[data-field="${name}"]`);
if (el) el.textContent = val;
};
setField('status', capitalize(status));
setField('node', data.node || '—');
setField('cpu', data.cpus || data.cores || '—');
setField('mem', data.maxmem ? formatBytes(data.maxmem) : (data.memory ? `${data.memory} MB` : '—'));
}
async function vmAction(vmid, action) {
const actionPath = action === 'start' ? 'start' : 'stop';
try {
toast('info', `${capitalize(action)}ing VM ${vmid}…`);
await api('POST', `/api/vm/${vmid}/${actionPath}`);
toast('success', `VM ${vmid} ${action} command sent!`);
// Refresh after a short delay
setTimeout(() => fetchVMStatus(vmid), 2000);
} catch (err) {
toast('error', `Failed to ${action} VM ${vmid}: ${err.message}`);
}
}
// ── Polling ───────────────────────────────────────────────
function startPolling() {
// Initial fetch
fetchStatus();
loadBuilds();
loadISOs();
// Poll every 5 seconds
state.pollTimer = setInterval(() => {
fetchStatus();
// Refresh VM statuses
for (const vmid of Object.keys(state.vms)) {
fetchVMStatus(vmid);
}
}, 5000);
}
// ── Utility ───────────────────────────────────────────────
function setButtonLoading(btn, loading) {
const text = btn.querySelector('.btn-text');
const spinner = btn.querySelector('.btn-spinner');
if (loading) {
btn.disabled = true;
if (text) text.style.opacity = '0.6';
if (spinner) spinner.hidden = false;
} else {
btn.disabled = false;
if (text) text.style.opacity = '1';
if (spinner) spinner.hidden = true;
}
}
function capitalize(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function formatTime(ts) {
try {
const d = new Date(ts);
if (isNaN(d.getTime())) return ts;
const now = new Date();
const diff = now - d;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
} catch (_) {
return ts;
}
}
// ── Refresh All ───────────────────────────────────────────
async function refreshAll() {
await Promise.all([
fetchStatus(),
loadBuilds(),
loadISOs(),
]);
toast('info', 'Data refreshed');
}
// ── Init ──────────────────────────────────────────────────
function init() {
initNav();
initISOForm();
initDeployForm();
initVMControl();
startPolling();
}
// Boot
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// ── Public API ────────────────────────────────────────────
return {
refreshAll,
fetchVMStatus,
vmAction,
};
})();