summaryrefslogtreecommitdiffstats
path: root/index.html
diff options
context:
space:
mode:
Diffstat (limited to 'index.html')
-rw-r--r--index.html377
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>