diff options
Diffstat (limited to 'index.html')
| -rw-r--r-- | index.html | 377 |
1 files changed, 0 insertions, 377 deletions
diff --git a/index.html b/index.html deleted file mode 100644 index f516f7c..0000000 --- a/index.html +++ /dev/null @@ -1,377 +0,0 @@ -<!-- index.html --> -<!doctype html> -<html lang="en"> - -<head> - <meta charset="utf-8" /> - <title>Network UI — Control Panel</title> - <meta name="viewport" content="width=device-width,initial-scale=1" /> - <script type="module" src="https://unpkg.com/@fluentui/web-components@2.6.1/dist/web-components.min.js"></script> - <style> - :root { - --panel-gap: 18px; - } - - body { - font-family: Arial, sans-serif; - margin: 0; - } - - header { - background: #f3f3f3; - padding: 12px 20px; - border-bottom: 1px solid #ddd; - } - - main { - display: flex; - height: calc(100vh - 58px); - } - - nav { - width: 220px; - border-right: 1px solid #eee; - padding: 16px; - box-sizing: border-box; - } - - section { - flex: 1; - padding: 16px; - overflow: auto; - box-sizing: border-box; - } - - .panel { - background: #fff; - border: 1px solid #eee; - padding: 12px; - border-radius: 8px; - margin-bottom: 12px; - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02); - } - - .iface { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; - } - - .small { - font-size: 0.9em; - color: #444; - } - - pre { - background: #fafafa; - padding: 8px; - border-radius: 6px; - overflow: auto; - } - - textarea { - width: 100%; - height: 360px; - font-family: monospace; - font-size: 13px; - } - - label { - display: block; - margin-bottom: 6px; - font-weight: 600; - } - - .controls { - display: flex; - gap: 10px; - align-items: center; - } - </style> -</head> - -<body> - <header> - <h2 style="margin:0">Systemd-networkd — Admin Panel</h2> - <div class="small">Single page admin + teaching panel. Auth: HTTP Basic.</div> - </header> - - <main> - <nav> - <fluent-button appearance="secondary" id="btnStatus" style="width:100%; margin-bottom:8px" - onclick="show('status')">Network Status</fluent-button> - <fluent-button appearance="secondary" id="btnConfigs" style="width:100%; margin-bottom:8px" - onclick="show('configs')">Configs</fluent-button> - <fluent-button appearance="secondary" id="btnLogs" style="width:100%; margin-bottom:8px" - onclick="show('logs')">Logs</fluent-button> - <fluent-button appearance="accent" id="btnCommands" style="width:100%; margin-top:14px" - onclick="show('commands')">Commands</fluent-button> - <div style="margin-top:18px; font-size:0.9em; color:#555"> - <strong>Tips</strong> - <ul> - <li>Validate before saving (systemd-analyze verify).</li> - <li>Backups are created automatically on save.</li> - </ul> - </div> - </nav> - - <section id="panelStatus" style="display:block;"> - <div class="panel"> - <div style="display:flex; justify-content:space-between; align-items:center;"> - <div> - <h3 style="margin:0">Network Status</h3> - <div class="small">Parsed output from <code>networkctl status --json=short</code></div> - </div> - <div><fluent-button appearance="accent" onclick="loadStatus()">Refresh</fluent-button></div> - </div> - </div> - <div id="ifaces"></div> - </section> - - <section id="panelConfigs" style="display:none;"> - <div class="panel"> - <div style="display:flex; justify-content:space-between; align-items:center;"> - <div> - <h3 style="margin:0">Configurations</h3> - <div class="small">Edit <code>/etc/systemd/network/*.network</code> files. Validate -> Save -> - (optional) Restart</div> - </div> - <div class="controls"> - <fluent-button onclick="refreshConfigs()">Refresh list</fluent-button> - <fluent-button appearance="accent" onclick="saveConfig()">Save</fluent-button> - </div> - </div> - </div> - - <div class="panel"> - <label for="configSelect">Config file</label> - <select id="configSelect" style="width:100%; padding:8px; margin-bottom:8px" - onchange="loadConfig()"></select> - <label for="cfgEditor">Contents</label> - <textarea id="cfgEditor" spellcheck="false"></textarea> - <div style="margin-top:8px;"> - <input type="checkbox" id="restartAfterSave" /> <label for="restartAfterSave" - style="display:inline; font-weight:normal">Restart systemd-networkd after save</label> - <fluent-button style="margin-left:8px" onclick="validateConfig()">Validate</fluent-button> - <span id="validateResult" class="small" style="margin-left:12px"></span> - </div> - </div> - </section> - - <section id="panelLogs" style="display:none;"> - <div class="panel"> - <div style="display:flex; justify-content:space-between;"> - <div> - <h3 style="margin:0">systemd-networkd Logs</h3> - <div class="small">journalctl -u systemd-networkd.service</div> - </div> - <fluent-button appearance="accent" onclick="loadLogs()">Refresh</fluent-button> - </div> - </div> - <div class="panel"> - <pre id="logsArea">[no logs]</pre> - </div> - </section> - - <section id="panelCommands" style="display:none;"> - <div class="panel"> - <h3 style="margin-top:0">Commands</h3> - <div class="small" style="margin-bottom:8px">Restart networkd or reboot device.</div> - <div class="controls"> - <fluent-button appearance="accent" onclick="restartNetworkd()">Restart networkd</fluent-button> - <fluent-button appearance="warning" onclick="rebootDevice()">Reboot device</fluent-button> - </div> - <div id="cmdResult" style="margin-top:12px"></div> - </div> - </section> - </main> - - <script> - 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(); - } - - 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; - } - - 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>'; - } - } - - 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(''); - } - - function arrayToMac(a) { - if (!Array.isArray(a)) return ''; - return a.map(x => ('0' + x.toString(16)).slice(-2)).join(':'); - } - 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); - } - 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 */ - 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); - } - } - - 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); - } - } - - 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; - } - } - - 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 */ - 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; - } - } - - 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; - } - } - - 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; - } - } - - /* initial */ - loadStatus(); - </script> -</body> - -</html> |
