/* ============================================================ 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 = `
📦

No builds found.

`; 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 = `
🖥️ VM ${vmid}
Loading…
Status
Node
CPU
Memory
`; 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, }; })();