export function show(panel) { document.getElementById('panelStatus').style.display = panel === 'status' ? 'block' : 'none'; document.getElementById('panelConfigs').style.display = panel === 'configs' ? 'block' : 'none'; document.getElementById('panelLogs').style.display = panel === 'logs' ? 'block' : 'none'; document.getElementById('panelCommands').style.display = panel === 'commands' ? 'block' : 'none'; if (panel === 'status') loadStatus(); if (panel === 'configs') refreshConfigs(); if (panel === 'logs') loadLogs(); } export async function api(path, opts) { const res = await fetch(path, opts); if (!res.ok) { const text = await res.text(); throw new Error(res.status + ' ' + text); } return res; } export async function loadStatus() { try { const res = await api('/api/status'); const data = await res.json(); renderIfaces(data.Interfaces || []); } catch (e) { document.getElementById('ifaces').innerHTML = '
Error loading status: ' + e.message + '
'; } } export function renderIfaces(ifaces) { const out = []; if (ifaces.length === 0) { out.push('
No interfaces
'); } for (const ifc of ifaces) { const name = ifc.Name || 'n/a'; const typ = ifc.Type || ''; const state = ifc.OperationalState || ifc.AdministrativeState || ''; const mac = ifc.HardwareAddress ? arrayToMac(ifc.HardwareAddress) : ''; const addrs = (ifc.Addresses || []).map(a => ipFromArray(a)).join('
'); const routes = (ifc.Routes || []).map(r => routeToString(r)).join('
'); const dns = (ifc.DNS || []).map(d => ipFromArray(d.Address || d)).join('
'); out.push(`

${name} ${typ}

State
${state}
MAC
${mac}
Addresses
${addrs}
DNS
${dns}
Routes
${routes}
`); } document.getElementById('ifaces').innerHTML = out.join(''); } export function arrayToMac(a) { if (!Array.isArray(a)) return ''; return a.map(x => ('0' + x.toString(16)).slice(-2)).join(':'); } export function ipFromArray(obj) { // obj can be { Family: number, Address: [..] } or {Address: [...]} let arr = null; if (Array.isArray(obj)) arr = obj; else if (obj && Array.isArray(obj.Address)) arr = obj.Address; else return ''; // detect IPv4 vs IPv6 if (arr.length === 4) return arr.join('.'); if (arr.length === 16) { // IPv6 - format groups of 2 bytes const parts = []; for (let i = 0; i < 16; i += 2) { parts.push(((arr[i] << 8) | arr[i + 1]).toString(16)); } return parts.join(':').replace(/(:0+)+/, '::'); } return JSON.stringify(arr); } export function routeToString(r) { if (!r) return ''; const dest = r.Destination ? ipFromArray(r.Destination) : ''; const pref = r.Gateway ? ipFromArray(r.Gateway) : ''; return dest + (pref ? ' → ' + pref : ''); } /* Configs editor */ export async function refreshConfigs() { try { const res = await api('/api/configs'); const data = await res.json(); const sel = document.getElementById('configSelect'); sel.innerHTML = ''; data.files.forEach(f => { const opt = document.createElement('option'); opt.value = f; opt.textContent = f; sel.appendChild(opt); }); if (data.files.length > 0) { loadConfig(); } else { document.getElementById('cfgEditor').value = ''; } } catch (e) { alert('Failed to list configs: ' + e.message); } } export async function loadConfig() { const name = document.getElementById('configSelect').value; if (!name) return; try { const res = await api('/api/config/' + encodeURIComponent(name)); const txt = await res.text(); document.getElementById('cfgEditor').value = txt; document.getElementById('validateResult').textContent = ''; } catch (e) { alert('Failed to load: ' + e.message); } } export async function validateConfig() { const name = document.getElementById('configSelect').value; const content = document.getElementById('cfgEditor').value; document.getElementById('validateResult').textContent = 'Validating...'; try { const res = await api('/api/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, content }) }); const r = await res.json(); if (r.ok) { document.getElementById('validateResult').textContent = 'OK'; } else { document.getElementById('validateResult').textContent = 'Error: ' + (r.output || 'validation failed'); } } catch (e) { document.getElementById('validateResult').textContent = 'Error: ' + e.message; } } export async function saveConfig() { const name = document.getElementById('configSelect').value; const content = document.getElementById('cfgEditor').value; const restart = document.getElementById('restartAfterSave').checked; if (!confirm('Save file ' + name + '? This will create a backup and (optionally) restart networkd.')) return; try { const res = await api('/api/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, content, restart }) }); const r = await res.json(); alert('Saved: ' + (r.status || 'ok')); } catch (e) { alert('Save failed: ' + e.message); } } /* Logs & commands */ export async function loadLogs() { try { const res = await api('/api/logs'); const txt = await res.text(); document.getElementById('logsArea').textContent = txt; } catch (e) { document.getElementById('logsArea').textContent = 'Error: ' + e.message; } } export async function restartNetworkd() { if (!confirm('Restart systemd-networkd? Active connections may be reset.')) return; try { const res = await api('/api/reload', { method: 'POST' }); const j = await res.json(); document.getElementById('cmdResult').textContent = JSON.stringify(j); } catch (e) { document.getElementById('cmdResult').textContent = 'Error: ' + e.message; } } export async function rebootDevice() { if (!confirm('Reboot device now?')) return; try { const res = await api('/api/reboot', { method: 'POST' }); const j = await res.json(); document.getElementById('cmdResult').textContent = JSON.stringify(j); } catch (e) { document.getElementById('cmdResult').textContent = 'Error: ' + e.message; } }