1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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;
}
}
|