214 lines
10 KiB
Go
214 lines
10 KiB
Go
package webui
|
|
|
|
import "html"
|
|
|
|
// renderNetworkInline returns the network UI without a wrapping card (for embedding in Tools).
|
|
func renderNetworkInline() string {
|
|
return `<div id="net-pending" style="display:none" class="alert alert-warn">
|
|
<strong>⚠ Network change applied.</strong> Reverting in <span id="net-countdown">60</span>s unless confirmed.
|
|
<button class="btn btn-primary btn-sm" style="margin-left:8px" onclick="confirmNetChange()">Confirm</button>
|
|
<button class="btn btn-secondary btn-sm" style="margin-left:4px" onclick="rollbackNetChange()">Rollback</button>
|
|
</div>
|
|
<div id="iface-table"><p style="color:var(--muted);font-size:13px">Loading...</p></div>
|
|
<div class="grid2" style="margin-top:16px">
|
|
<div><div style="font-weight:700;font-size:13px;margin-bottom:8px">DHCP</div>
|
|
<div class="form-row"><label>Interface (leave empty for all)</label><input type="text" id="dhcp-iface" placeholder="eth0"></div>
|
|
<button class="btn btn-primary" onclick="runDHCP()">▶ Run DHCP</button>
|
|
<div id="dhcp-out" style="margin-top:10px;font-size:12px;color:var(--ok-fg)"></div>
|
|
</div>
|
|
<div><div style="font-weight:700;font-size:13px;margin-bottom:8px">Static IPv4</div>
|
|
<div class="form-row"><label>Interface</label><input type="text" id="st-iface" placeholder="eth0"></div>
|
|
<div class="form-row"><label>Address</label><input type="text" id="st-addr" placeholder="192.168.1.100"></div>
|
|
<div class="form-row"><label>Prefix length</label><input type="text" id="st-prefix" placeholder="24"></div>
|
|
<div class="form-row"><label>Gateway</label><input type="text" id="st-gw" placeholder="192.168.1.1"></div>
|
|
<div class="form-row"><label>DNS (comma-separated)</label><input type="text" id="st-dns" placeholder="8.8.8.8,8.8.4.4"></div>
|
|
<button class="btn btn-primary" onclick="setStatic()">Apply Static IP</button>
|
|
<div id="static-out" style="margin-top:10px;font-size:12px;color:var(--ok-fg)"></div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
var _netCountdownTimer = null;
|
|
var _netRefreshTimer = null;
|
|
const NET_ROLLBACK_SECS = 60;
|
|
function loadNetwork() {
|
|
fetch('/api/network').then(r=>r.json()).then(d => {
|
|
const rows = (d.interfaces||[]).map(i =>
|
|
'<tr><td style="cursor:pointer" onclick="selectIface(\''+i.Name+'\')" title="Use this interface in the forms below"><span style="text-decoration:underline">'+i.Name+'</span></td>' +
|
|
'<td style="cursor:pointer" onclick="toggleIface(\''+i.Name+'\',\''+i.State+'\')" title="Click to toggle"><span class="badge '+(i.State==='up'?'badge-ok':'badge-warn')+'">'+i.State+'</span></td>' +
|
|
'<td>'+(i.IPv4||[]).join(', ')+'</td></tr>'
|
|
).join('');
|
|
document.getElementById('iface-table').innerHTML =
|
|
'<table><tr><th>Interface</th><th>State (click to toggle)</th><th>Addresses</th></tr>'+rows+'</table>' +
|
|
(d.default_route ? '<p style="font-size:12px;color:var(--muted);margin-top:8px">Default route: '+d.default_route+'</p>' : '');
|
|
if (d.pending_change) showNetPending(d.rollback_in || NET_ROLLBACK_SECS);
|
|
else hideNetPending();
|
|
}).catch(function() {});
|
|
}
|
|
function selectIface(iface) {
|
|
document.getElementById('dhcp-iface').value = iface;
|
|
document.getElementById('st-iface').value = iface;
|
|
}
|
|
function toggleIface(iface, currentState) {
|
|
showNetPending(NET_ROLLBACK_SECS);
|
|
fetch('/api/network/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({iface:iface})})
|
|
.then(r=>r.json()).then(d => {
|
|
if (d.error) { hideNetPending(); alert('Error: '+d.error); return; }
|
|
loadNetwork();
|
|
showNetPending(d.rollback_in || NET_ROLLBACK_SECS);
|
|
}).catch(function() {
|
|
setTimeout(loadNetwork, 1500);
|
|
});
|
|
}
|
|
function hideNetPending() {
|
|
const el = document.getElementById('net-pending');
|
|
if (_netCountdownTimer) clearInterval(_netCountdownTimer);
|
|
_netCountdownTimer = null;
|
|
el.style.display = 'none';
|
|
}
|
|
function showNetPending(secs) {
|
|
if (!secs || secs < 1) { hideNetPending(); return; }
|
|
const el = document.getElementById('net-pending');
|
|
el.style.display = 'block';
|
|
if (_netCountdownTimer) clearInterval(_netCountdownTimer);
|
|
let remaining = secs;
|
|
document.getElementById('net-countdown').textContent = remaining;
|
|
_netCountdownTimer = setInterval(function() {
|
|
remaining--;
|
|
document.getElementById('net-countdown').textContent = remaining;
|
|
if (remaining <= 0) { hideNetPending(); loadNetwork(); }
|
|
}, 1000);
|
|
}
|
|
function confirmNetChange() {
|
|
hideNetPending();
|
|
fetch('/api/network/confirm',{method:'POST'}).then(()=>loadNetwork()).catch(()=>{});
|
|
}
|
|
function rollbackNetChange() {
|
|
hideNetPending();
|
|
fetch('/api/network/rollback',{method:'POST'}).then(()=>loadNetwork()).catch(()=>{});
|
|
}
|
|
function runDHCP() {
|
|
const iface = document.getElementById('dhcp-iface').value.trim();
|
|
showNetPending(NET_ROLLBACK_SECS);
|
|
fetch('/api/network/dhcp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({interface:iface||'all'})})
|
|
.then(r=>r.json()).then(d => {
|
|
document.getElementById('dhcp-out').textContent = d.output || d.error || 'Done.';
|
|
if (d.error) { hideNetPending(); return; }
|
|
showNetPending(d.rollback_in || NET_ROLLBACK_SECS);
|
|
loadNetwork();
|
|
}).catch(function() {
|
|
setTimeout(loadNetwork, 1500);
|
|
});
|
|
}
|
|
function setStatic() {
|
|
const dns = document.getElementById('st-dns').value.split(',').map(s=>s.trim()).filter(Boolean);
|
|
showNetPending(NET_ROLLBACK_SECS);
|
|
fetch('/api/network/static',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
|
interface: document.getElementById('st-iface').value,
|
|
address: document.getElementById('st-addr').value,
|
|
prefix: document.getElementById('st-prefix').value,
|
|
gateway: document.getElementById('st-gw').value,
|
|
dns: dns,
|
|
})}).then(r=>r.json()).then(d => {
|
|
document.getElementById('static-out').textContent = d.output || d.error || 'Done.';
|
|
if (d.error) { hideNetPending(); return; }
|
|
showNetPending(d.rollback_in || NET_ROLLBACK_SECS);
|
|
loadNetwork();
|
|
}).catch(function() {
|
|
setTimeout(loadNetwork, 1500);
|
|
});
|
|
}
|
|
loadNetwork();
|
|
if (_netRefreshTimer) clearInterval(_netRefreshTimer);
|
|
_netRefreshTimer = setInterval(loadNetwork, 5000);
|
|
</script>`
|
|
}
|
|
|
|
func renderNetwork() string {
|
|
return `<div class="card"><div class="card-head">Network Interfaces</div><div class="card-body">` +
|
|
renderNetworkInline() +
|
|
`</div></div>`
|
|
}
|
|
|
|
func renderServicesInline() string {
|
|
return `<p style="font-size:13px;color:var(--muted);margin-bottom:10px">` + html.EscapeString(`bee-selfheal.timer is expected to be active; the oneshot bee-selfheal.service itself is not shown as a long-running service.`) + `</p>
|
|
<div style="display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;margin-bottom:8px"><button class="btn btn-sm btn-secondary" onclick="loadServices()">↻ Refresh</button></div>
|
|
<div id="svc-table"><p style="color:var(--muted);font-size:13px">Loading...</p></div>
|
|
<div id="svc-out" style="display:none;margin-top:12px">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
|
<span id="svc-out-label" style="font-size:12px;font-weight:600;color:var(--muted)">Output</span>
|
|
<span id="svc-out-status" style="font-size:12px"></span>
|
|
</div>
|
|
<div id="svc-terminal" class="terminal" style="max-height:220px;width:100%;box-sizing:border-box"></div>
|
|
</div>
|
|
<script>
|
|
function loadServices() {
|
|
fetch('/api/services').then(r=>r.json()).then(svcs => {
|
|
const rows = svcs.map(s => {
|
|
const st = s.state||'unknown';
|
|
const badge = st==='active' ? 'badge-ok' : st==='failed' ? 'badge-err' : 'badge-warn';
|
|
const id = 'svc-body-'+s.name.replace(/[^a-z0-9]/g,'-');
|
|
const body = (s.body||'').replace(/</g,'<').replace(/>/g,'>');
|
|
return '<tr>' +
|
|
'<td style="white-space:nowrap">'+s.name+'</td>' +
|
|
'<td style="white-space:nowrap"><span class="badge '+badge+'" style="cursor:pointer" onclick="toggleBody(\''+id+'\')">'+st+' ▾</span>' +
|
|
'<div id="'+id+'" style="display:none;margin-top:6px"><pre style="font-size:11px;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;background:#1b1c1d;padding:8px;border-radius:4px;color:#b5cea8">'+body+'</pre></div>' +
|
|
'</td>' +
|
|
'<td style="white-space:nowrap">' +
|
|
'<button class="btn btn-sm btn-secondary" id="btn-'+s.name+'-start" onclick="svcAction(this,\''+s.name+'\',\'start\')">Start</button> ' +
|
|
'<button class="btn btn-sm btn-secondary" id="btn-'+s.name+'-stop" onclick="svcAction(this,\''+s.name+'\',\'stop\')">Stop</button> ' +
|
|
'<button class="btn btn-sm btn-secondary" id="btn-'+s.name+'-restart" onclick="svcAction(this,\''+s.name+'\',\'restart\')">Restart</button>' +
|
|
'</td></tr>';
|
|
}).join('');
|
|
document.getElementById('svc-table').innerHTML =
|
|
'<table><tr><th>Unit</th><th>Status</th><th>Actions</th></tr>'+rows+'</table>';
|
|
});
|
|
}
|
|
function toggleBody(id) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.style.display = el.style.display==='none' ? 'block' : 'none';
|
|
}
|
|
function svcAction(btn, name, action) {
|
|
var label = btn.textContent;
|
|
btn.disabled = true;
|
|
btn.textContent = '...';
|
|
var out = document.getElementById('svc-out');
|
|
var term = document.getElementById('svc-terminal');
|
|
var statusEl = document.getElementById('svc-out-status');
|
|
var labelEl = document.getElementById('svc-out-label');
|
|
out.style.display = 'block';
|
|
labelEl.textContent = action + ' ' + name;
|
|
term.textContent = 'Running...';
|
|
statusEl.textContent = '';
|
|
statusEl.style.color = '';
|
|
fetch('/api/services/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,action})})
|
|
.then(r=>r.json()).then(d => {
|
|
term.textContent = d.output || d.error || '(no output)';
|
|
term.scrollTop = term.scrollHeight;
|
|
if (d.status === 'ok') {
|
|
statusEl.textContent = '✓ done';
|
|
statusEl.style.color = 'var(--ok-fg, #2c662d)';
|
|
} else {
|
|
statusEl.textContent = '✗ failed';
|
|
statusEl.style.color = 'var(--crit-fg, #9f3a38)';
|
|
}
|
|
btn.textContent = label;
|
|
btn.disabled = false;
|
|
setTimeout(loadServices, 800);
|
|
}).catch(e => {
|
|
term.textContent = 'Request failed: ' + e;
|
|
statusEl.textContent = '✗ error';
|
|
statusEl.style.color = 'var(--crit-fg, #9f3a38)';
|
|
btn.textContent = label;
|
|
btn.disabled = false;
|
|
});
|
|
}
|
|
loadServices();
|
|
</script>`
|
|
}
|
|
|
|
func renderServices() string {
|
|
return `<div class="card"><div class="card-head">Bee Services</div><div class="card-body">` +
|
|
renderServicesInline() +
|
|
`</div></div>`
|
|
}
|