summaryrefslogtreecommitdiffstats
path: root/static/functions.js
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-09-28 12:09:33 +0300
committerPetri Hienonen <petri.hienonen@gmail.com>2025-09-28 12:09:33 +0300
commit2bf20fe39cd70b25f89ccd3b40d06895f25a0833 (patch)
tree509173d3e3ab980214046df156429f6de8236081 /static/functions.js
parent47529804bef15ed84730ff3409f0d426fcef2112 (diff)
downloadnetwork-2bf20fe39cd70b25f89ccd3b40d06895f25a0833.tar.zst
Iteration
Diffstat (limited to 'static/functions.js')
-rw-r--r--static/functions.js184
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;
+ }
+}