Name update
This commit is contained in:
+47
-47
@@ -1,7 +1,7 @@
|
||||
// PXEForge web UI — vanilla JS, no build step, no framework, no network
|
||||
// OpenPXE web UI — vanilla JS, no build step, no framework, no network
|
||||
// dependencies. Uses fetch() + EventSource only.
|
||||
//
|
||||
// Tabs (Phase 4): Dashboard / Network / Forge Gate / Storage / Terminal /
|
||||
// Tabs (Phase 4): Dashboard / Network / Queue / Storage / Terminal /
|
||||
// About. The shell swaps a single view into #view-root.
|
||||
//
|
||||
// Keep this readable — nobody wants to debug a clever vanilla-JS
|
||||
@@ -75,19 +75,19 @@
|
||||
return fetch(url, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
|
||||
}
|
||||
|
||||
// Animated "anvil + progress bar" widget. Built once here and inlined
|
||||
// wherever a card wants to convey "an image is being forged onto the
|
||||
// bench right now." Used on the dashboard and on the Forge Gate.
|
||||
function forgeProgressWidget(imaging, gateTotal) {
|
||||
const total = Math.max(gateTotal, imaging, 1);
|
||||
// Animated brand-mark + progress bar widget. Built once here and inlined
|
||||
// wherever a card wants to convey "an image is being deployed onto a
|
||||
// queued client right now." Used on the Dashboard and the Queue tab.
|
||||
function queueProgressWidget(imaging, queueTotal) {
|
||||
const total = Math.max(queueTotal, imaging, 1);
|
||||
const pct = imaging > 0 ? Math.round((imaging / total) * 100) : 0;
|
||||
const root = el('div', {class: 'forge-progress' + (imaging === 0 ? ' idle' : '')}, [
|
||||
el('div', {class: 'anvil', 'aria-hidden': 'true'}),
|
||||
const root = el('div', {class: 'queue-progress' + (imaging === 0 ? ' idle' : '')}, [
|
||||
el('div', {class: 'mark', 'aria-hidden': 'true'}),
|
||||
el('div', {class: 'info'}, [
|
||||
el('div', {class: 'label'},
|
||||
imaging === 0
|
||||
? 'No active imaging'
|
||||
: (imaging + ' of ' + total + ' device' + (total === 1 ? '' : 's') + ' being forged')),
|
||||
: (imaging + ' of ' + total + ' device' + (total === 1 ? '' : 's') + ' deploying')),
|
||||
el('div', {class: 'bar-track'},
|
||||
el('div', {class: 'bar-fill', style: 'width:' + pct + '%'})),
|
||||
]),
|
||||
@@ -120,7 +120,7 @@
|
||||
const status = await getJSON('/api/status');
|
||||
const isos = await getJSON('/api/isos');
|
||||
const clients = (await getJSON('/api/clients')).clients || [];
|
||||
const gates = (await getJSON('/api/gate')).gates || [];
|
||||
const entries = (await getJSON('/api/queue')).entries || [];
|
||||
|
||||
const ipxeOk = (status.ipxe_assets || []).length > 0;
|
||||
const stats = el('div', {class: 'statstrip'}, [
|
||||
@@ -136,9 +136,9 @@
|
||||
el('div', {class: 'stat', style: 'padding-bottom:0'}, [
|
||||
el('div', {class: 'label'}, 'Imaging now'),
|
||||
el('div', {class: 'value'}, String(status.imaging_count || 0)),
|
||||
el('div', {class: 'trend'}, (status.waiting_count || 0) + ' waiting at gate'),
|
||||
el('div', {class: 'trend'}, (status.waiting_count || 0) + ' waiting in queue'),
|
||||
]),
|
||||
forgeProgressWidget(status.imaging_count || 0, status.gate_count || 0),
|
||||
queueProgressWidget(status.imaging_count || 0, status.queue_count || 0),
|
||||
]),
|
||||
el('div', {class: 'card'}, el('div', {class: 'stat'}, [
|
||||
el('div', {class: 'label'}, 'Images available'),
|
||||
@@ -151,7 +151,7 @@
|
||||
el('div', {class: 'card'}, el('div', {class: 'stat'}, [
|
||||
el('div', {class: 'label'}, 'Uptime'),
|
||||
el('div', {class: 'value', style: 'font-size:22px'}, fmtUptime(status.uptime_secs)),
|
||||
el('div', {class: 'trend'}, 'PXEForge ' + status.version),
|
||||
el('div', {class: 'trend'}, 'OpenPXE ' + status.version),
|
||||
])),
|
||||
]);
|
||||
|
||||
@@ -159,10 +159,10 @@
|
||||
// boot" log. Use last_seen desc (already sorted by API).
|
||||
const recent = clients.slice(0, 8);
|
||||
const recentRows = recent.map(c => {
|
||||
const g = gates.find(g => g.mac === c.mac);
|
||||
const g = entries.find(g => g.mac === c.mac);
|
||||
let status = el('span', {class: 'tag ok'}, 'active');
|
||||
if (g && g.assigned_target) status = el('span', {class:'tag ok'}, 'assigned: ' + g.assigned_target);
|
||||
else if (g) status = el('span', {class:'tag accent'}, '#' + g.position + ' at gate');
|
||||
else if (g) status = el('span', {class:'tag accent'}, '#' + g.position + ' in queue');
|
||||
return el('tr', {}, [
|
||||
el('td', {class: 'mono'}, c.mac),
|
||||
el('td', {}, c.last_ip ? String(c.last_ip) : '-'),
|
||||
@@ -229,13 +229,13 @@
|
||||
]),
|
||||
el('p', {class:'msg'},
|
||||
'Server IP, NIC, mask, and gateway are auto-detected at startup. ' +
|
||||
'To change them, set PXEFORGE_PUBLIC_IP and restart — editing them ' +
|
||||
'To change them, set OPENPXE_PUBLIC_IP and restart — editing them ' +
|
||||
'from a hot UI would silently break PXE for every client mid-boot.'),
|
||||
el('label', {class:'field', style:'margin-top:18px'}, [
|
||||
el('span', {class:'name'}, 'DNS server (optional, informational)'),
|
||||
dns,
|
||||
el('span', {class:'hint'},
|
||||
'PXEForge does not run a DNS server itself; this field records ' +
|
||||
'OpenPXE does not run a DNS server itself; this field records ' +
|
||||
'what your upstream DNS is so you don\'t have to dig it out at ' +
|
||||
'3 AM during a deployment.'),
|
||||
]),
|
||||
@@ -247,8 +247,8 @@
|
||||
},
|
||||
|
||||
gate: async () => {
|
||||
const [{ gates = [] }, isos] = await Promise.all([
|
||||
getJSON('/api/gate'), getJSON('/api/isos'),
|
||||
const [{ entries = [] }, isos] = await Promise.all([
|
||||
getJSON('/api/queue'), getJSON('/api/isos'),
|
||||
]);
|
||||
const targets = isos.flatMap(i => i.boot_entries.map(e => ({
|
||||
id: e.id, title: e.title + ' — ' + familyLabel(i.introspection.family)
|
||||
@@ -261,7 +261,7 @@
|
||||
const msg = el('div', {class:'msg'});
|
||||
launch.onclick = async () => {
|
||||
if (!pick.value) { msg.textContent = 'Pick an image first.'; msg.className='msg err'; return; }
|
||||
const r = await postJSON('/api/gate/assign', { target: pick.value, gate_ids: [] });
|
||||
const r = await postJSON('/api/queue/assign', { target: pick.value, entry_ids: [] });
|
||||
if (!r.ok) { msg.textContent = 'Assign failed: ' + r.status; msg.className='msg err'; return; }
|
||||
const j = await r.json();
|
||||
if (!j.ok) { msg.textContent = 'Assign failed: ' + (j.error || 'unknown'); msg.className='msg err'; return; }
|
||||
@@ -270,9 +270,9 @@
|
||||
render('gate');
|
||||
};
|
||||
|
||||
const track = gates.length
|
||||
? el('div', {class:'gate-track'},
|
||||
gates.map(g => el('div', {class:'gate-row' + (g.assigned_target ? ' assigned' : '')}, [
|
||||
const track = entries.length
|
||||
? el('div', {class:'queue-track'},
|
||||
entries.map(g => el('div', {class:'queue-row' + (g.assigned_target ? ' assigned' : '')}, [
|
||||
el('div', {class:'pos'}, '#' + g.position),
|
||||
el('div', {}, [
|
||||
el('div', {class:'mac'}, g.mac),
|
||||
@@ -283,20 +283,20 @@
|
||||
? el('span', {class:'tag ok'}, '→ ' + g.assigned_target)
|
||||
: el('span', {class:'tag accent'}, 'waiting')),
|
||||
el('button', {class:'ghost', onclick: async () => {
|
||||
await fetch('/api/gate/' + encodeURIComponent(g.id), {method:'DELETE'});
|
||||
await fetch('/api/queue/' + encodeURIComponent(g.id), {method:'DELETE'});
|
||||
render('gate');
|
||||
}}, 'Release'),
|
||||
]))
|
||||
)
|
||||
: el('div', {class:'empty'},
|
||||
'No clients at the gate. Boot a client and choose "Gated Deployment" in the PXE menu.');
|
||||
'No clients at the gate. Boot a client and choose "Queued Deployment" in the PXE menu.');
|
||||
|
||||
const imaging = gates.filter(g => g.assigned_target).length;
|
||||
const imaging = entries.filter(g => g.assigned_target).length;
|
||||
|
||||
return el('div', {class:'grid'}, [
|
||||
el('div', {class:'card'}, [
|
||||
el('header', {}, el('h2', {}, 'Forge')),
|
||||
forgeProgressWidget(imaging, gates.length),
|
||||
queueProgressWidget(imaging, entries.length),
|
||||
]),
|
||||
el('div', {class:'card'}, [
|
||||
el('header', {}, el('h2', {}, 'Launch an image across the gate')),
|
||||
@@ -313,7 +313,7 @@
|
||||
el('div', {class:'card'}, [
|
||||
el('header', {}, [
|
||||
el('h2', {}, 'Gate positions'),
|
||||
el('span', {class:'sub'}, gates.length + ' waiting'),
|
||||
el('span', {class:'sub'}, entries.length + ' waiting'),
|
||||
]),
|
||||
el('div', {class:'body'}, track),
|
||||
]),
|
||||
@@ -516,7 +516,7 @@
|
||||
// Reserved menu shortcuts that the operator might want to bind.
|
||||
const reserved = [
|
||||
{id: '_local', title: '↳ Boot from Local HDD (built-in)'},
|
||||
{id: '_gate', title: '↳ Gated Deployment (built-in)'},
|
||||
{id: '_queue', title: '↳ Queued Deployment (built-in)'},
|
||||
{id: '_tools_menu', title: '↳ Tools menu (built-in)'},
|
||||
];
|
||||
|
||||
@@ -583,7 +583,7 @@
|
||||
]),
|
||||
upsertBtn, msg,
|
||||
el('p', {class:'msg', style:'margin-top:14px'},
|
||||
'When a client with a bound MAC requests boot.ipxe, PXEForge ' +
|
||||
'When a client with a bound MAC requests boot.ipxe, OpenPXE ' +
|
||||
'short-circuits past the interactive menu and chains directly. ' +
|
||||
'Inspired by Tinkerbell smee\'s MAC-prepended URL pattern.'),
|
||||
]),
|
||||
@@ -629,7 +629,7 @@
|
||||
const ts = (line.timestamp || new Date().toISOString()).replace(/\.\d+/, '').replace('T', ' ').replace('Z', '');
|
||||
const span = el('span', {class: 'lvl-' + lvl}, [
|
||||
el('span', {class:'ts'}, ts + ' '),
|
||||
el('span', {class:'tg'}, '[' + (line.target || 'pxeforge') + '] '),
|
||||
el('span', {class:'tg'}, '[' + (line.target || 'openpxe') + '] '),
|
||||
line.message,
|
||||
'\n',
|
||||
]);
|
||||
@@ -647,7 +647,7 @@
|
||||
const r = await getJSON('/api/log/recent');
|
||||
for (const l of (r.lines || [])) append(l);
|
||||
} catch (e) {
|
||||
append({timestamp: new Date().toISOString(), level:'warn', target:'pxeforge::ui',
|
||||
append({timestamp: new Date().toISOString(), level:'warn', target:'openpxe::ui',
|
||||
message: 'failed to load recent logs: ' + e.message});
|
||||
}
|
||||
|
||||
@@ -655,12 +655,12 @@
|
||||
const es = new EventSource('/api/log/stream');
|
||||
es.onmessage = (ev) => {
|
||||
try { append(JSON.parse(ev.data)); }
|
||||
catch { append({timestamp: new Date().toISOString(), level:'debug', target:'pxeforge::ui', message: ev.data}); }
|
||||
catch { append({timestamp: new Date().toISOString(), level:'debug', target:'openpxe::ui', message: ev.data}); }
|
||||
};
|
||||
es.addEventListener('lagged', (ev) => {
|
||||
const j = JSON.parse(ev.data || '{}');
|
||||
append({timestamp: new Date().toISOString(), level:'warn',
|
||||
target:'pxeforge::ui',
|
||||
target:'openpxe::ui',
|
||||
message: 'log stream lagged: ' + (j.skipped || '?') + ' lines skipped'});
|
||||
});
|
||||
es.onerror = () => {
|
||||
@@ -668,7 +668,7 @@
|
||||
// We only append once, on transition from connected → erroring.
|
||||
if (!term._notedErr) {
|
||||
term._notedErr = true;
|
||||
append({timestamp: new Date().toISOString(), level:'warn', target:'pxeforge::ui',
|
||||
append({timestamp: new Date().toISOString(), level:'warn', target:'openpxe::ui',
|
||||
message: 'log stream connection lost — auto-reconnecting'});
|
||||
setTimeout(() => { term._notedErr = false; }, 5000);
|
||||
}
|
||||
@@ -701,7 +701,7 @@
|
||||
target: 'terminal-output', message: out});
|
||||
} catch (err) {
|
||||
append({timestamp: new Date().toISOString(), level:'error',
|
||||
target:'pxeforge::ui', message: 'command failed: ' + err.message});
|
||||
target:'openpxe::ui', message: 'command failed: ' + err.message});
|
||||
}
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
if (history.length === 0) return;
|
||||
@@ -716,7 +716,7 @@
|
||||
});
|
||||
|
||||
// Welcome banner.
|
||||
append({timestamp: new Date().toISOString(), level:'info', target:'pxeforge::terminal',
|
||||
append({timestamp: new Date().toISOString(), level:'info', target:'openpxe::terminal',
|
||||
message: 'Connected. Type "help" for available commands.'});
|
||||
|
||||
// Focus the input on next tick (after view swap completes).
|
||||
@@ -729,7 +729,7 @@
|
||||
const status = await getJSON('/api/status');
|
||||
return el('div', {class:'card'}, [
|
||||
el('div', {class:'about-hero'}, [
|
||||
el('h2', {}, 'PXEForge'),
|
||||
el('h2', {}, 'OpenPXE'),
|
||||
el('p', {class:'lead'},
|
||||
'Air-gapped network PXE boot, container-native, that anyone can run. ' +
|
||||
'No CDN calls, no telemetry, no surprise external dependencies — ship ' +
|
||||
@@ -758,7 +758,7 @@
|
||||
const viewTitles = {
|
||||
dashboard: 'Dashboard',
|
||||
network: 'Network',
|
||||
gate: 'Forge Gate',
|
||||
gate: 'Queue',
|
||||
storage: 'Storage',
|
||||
hosts: 'Hosts',
|
||||
terminal: 'Terminal',
|
||||
@@ -769,7 +769,7 @@
|
||||
// script in index.html before paint; we just flip it here and persist.
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
try { localStorage.setItem('pxeforge-theme', theme); } catch {}
|
||||
try { localStorage.setItem('openpxe-theme', theme); } catch {}
|
||||
}
|
||||
function currentTheme() {
|
||||
return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
||||
@@ -801,12 +801,12 @@
|
||||
try { currentBody._cleanup(); } catch (e) { /* ignore */ }
|
||||
}
|
||||
root.innerHTML = '';
|
||||
// Animated anvil loader. Replaces the old text "Loading…" so the
|
||||
// user gets a sense of "the forge is heating up" instead of a flat
|
||||
// spinner. The SVG itself drives all animation via SMIL, no JS.
|
||||
// Animated rainbow-mark loader replacing the old plain-text
|
||||
// \"Loading…\". The SVG drives all animation via SMIL — no JS, no
|
||||
// CSS keyframes.
|
||||
root.appendChild(el('div', {class:'loader'}, [
|
||||
el('div', {class:'anvil'}),
|
||||
el('div', {}, 'Heating the forge…'),
|
||||
el('div', {class:'mark'}),
|
||||
el('div', {}, 'Loading…'),
|
||||
]));
|
||||
try {
|
||||
const body = await views[view]();
|
||||
@@ -826,7 +826,7 @@
|
||||
$$('[data-bind=version]').forEach(n => n.textContent = s.version);
|
||||
$$('[data-bind=iso_count],[data-bind=iso_count2]').forEach(n => n.textContent = String(s.iso_count));
|
||||
$$('[data-bind=client_count],[data-bind=client_count2]').forEach(n => n.textContent = String(s.client_count));
|
||||
$$('[data-bind=gate_count],[data-bind=gate_count2]').forEach(n => n.textContent = String(s.gate_count));
|
||||
$$('[data-bind=queue_count],[data-bind=queue_count2]').forEach(n => n.textContent = String(s.queue_count));
|
||||
$$('[data-bind=host_count]').forEach(n => n.textContent = String(s.host_bindings || 0));
|
||||
const chip = $('[data-bind=ready_chip]');
|
||||
if (chip) {
|
||||
|
||||
Reference in New Issue
Block a user