diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-09-28 12:09:33 +0300 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-09-28 12:09:33 +0300 |
| commit | 2bf20fe39cd70b25f89ccd3b40d06895f25a0833 (patch) | |
| tree | 509173d3e3ab980214046df156429f6de8236081 /static/functions.js | |
| parent | 47529804bef15ed84730ff3409f0d426fcef2112 (diff) | |
| download | network-2bf20fe39cd70b25f89ccd3b40d06895f25a0833.tar.zst | |
Iteration
Diffstat (limited to 'static/functions.js')
| -rw-r--r-- | static/functions.js | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/static/functions.js b/static/functions.js new file mode 100644 index 0000000..ce909e2 --- /dev/null +++ b/static/functions.js @@ -0,0 +1,184 @@ + +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 = '<div class="panel">Error loading status: ' + e.message + '</div>'; + } +} + +export function renderIfaces(ifaces) { + const out = []; + if (ifaces.length === 0) { + out.push('<div class="panel">No interfaces</div>'); + } + 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('<br>'); + const routes = (ifc.Routes || []).map(r => routeToString(r)).join('<br>'); + const dns = (ifc.DNS || []).map(d => ipFromArray(d.Address || d)).join('<br>'); + out.push(`<div class="panel"><h4 style="margin:0">${name} <small class="small">${typ}</small></h4> + <div class="iface"> + <div><strong>State</strong><div class="small">${state}</div></div> + <div><strong>MAC</strong><div class="small">${mac}</div></div> + <div><strong>Addresses</strong><div class="small">${addrs}</div></div> + <div><strong>DNS</strong><div class="small">${dns}</div></div> + <div style="grid-column:1/-1"><strong>Routes</strong><div class="small">${routes}</div></div> + </div></div>`); + } + 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; + } +} |
