cockpit: show both AP bands with per-band restart
vanrouter.js hardcoded the 5GHz iface; generalize to an APS list (iface + hostapd unit + band label). The Access Points card now renders a status line, client table, and Restart button per band, and the WAN table excludes both AP ifaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e652b0f7bd
commit
db16964d5a
@@ -16,10 +16,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<h3>Access Point</h3>
|
||||
<button id="ap-restart" class="btn">Restart AP</button>
|
||||
</div>
|
||||
<h3>Access Points</h3>
|
||||
<div id="ap"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ th { color: #6a6e73; font-weight: 600; }
|
||||
.bar-fill.warn { background: #e08a00; }
|
||||
.bar-fill.crit { background: #c9190b; }
|
||||
.muted { color: #6a6e73; font-size: 12px; }
|
||||
.ap-band + .ap-band { border-top: 1px solid #ededed; margin-top: 10px; padding-top: 4px; }
|
||||
.active-wan { font-weight: 700; color: #0066cc; }
|
||||
.btn { margin-left: 6px; padding: 3px 10px; cursor: pointer; }
|
||||
.btn[disabled] { cursor: default; opacity: 0.5; }
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
// Read-only status via cockpit.spawn (logged-in user); mutating actions use
|
||||
// { superuser: "require" } which triggers Cockpit's admin (polkit) escalation.
|
||||
|
||||
const AP = "wlxc83a35a4ee55"; // Realtek RTL8852BU AP interface (MAC-derived, stable)
|
||||
// AP radios (MAC-derived iface names, stable) — each runs its own hostapd unit.
|
||||
const APS = [
|
||||
{ iface: "wlxc83a35a4ee55", unit: "hostapd", band: "5GHz" }, // RTL8852BU
|
||||
{ iface: "wlxd8ec5e2faa8c", unit: "hostapd-2g", band: "2.4GHz" }, // RTL8822BU
|
||||
];
|
||||
const PREFER_FILE = "/run/van-failover/prefer"; // van-failover reads this to pick the preferred WAN
|
||||
|
||||
function run(args, opts) {
|
||||
@@ -24,19 +28,19 @@ function esc_sh(s) {
|
||||
|
||||
/* ---------- Access Point ---------- */
|
||||
|
||||
async function readAP() {
|
||||
async function readAP(a) {
|
||||
let active = "inactive";
|
||||
try { active = (await sh("systemctl is-active hostapd || true")).trim(); } catch (e) { /* ignore */ }
|
||||
try { active = (await sh(`systemctl is-active ${a.unit} || true`)).trim(); } catch (e) { /* ignore */ }
|
||||
|
||||
let info = "";
|
||||
try { info = await run(["iw", "dev", AP, "info"]); } catch (e) { info = ""; }
|
||||
try { info = await run(["iw", "dev", a.iface, "info"]); } catch (e) { info = ""; }
|
||||
const ssid = (info.match(/\bssid (.+)/) || [])[1];
|
||||
const chan = (info.match(/\bchannel \d+[^\n]*/) || [])[0];
|
||||
const width = (info.match(/\bwidth: ([^\n,]+)/) || [])[1];
|
||||
|
||||
let stations = [];
|
||||
try {
|
||||
const dump = await run(["iw", "dev", AP, "station", "dump"]);
|
||||
const dump = await run(["iw", "dev", a.iface, "station", "dump"]);
|
||||
stations = dump.split(/Station /).slice(1).map(b => ({
|
||||
mac: b.split(" ")[0],
|
||||
sig: (b.match(/signal:\s*([\-\d]+)/) || [])[1],
|
||||
@@ -44,26 +48,36 @@ async function readAP() {
|
||||
}));
|
||||
} catch (e) { stations = []; }
|
||||
|
||||
return { active, ssid, chan, width, stations };
|
||||
return { band: a.band, unit: a.unit, active, ssid, chan, width, stations };
|
||||
}
|
||||
|
||||
function renderAP(ap) {
|
||||
const beaconing = !!ap.ssid;
|
||||
let html = `<p>hostapd: <span class="pill ${ap.active === "active" ? "ok" : "bad"}">${esc(ap.active)}</span>`;
|
||||
if (beaconing)
|
||||
html += ` SSID <b>${esc(ap.ssid)}</b> ${esc(ap.chan || "")} ${esc(ap.width || "")}`;
|
||||
else
|
||||
html += ` <span class="pill bad">not beaconing</span>`;
|
||||
html += `</p><p>Clients: <b>${ap.stations.length}</b></p>`;
|
||||
function renderAPs(aps) {
|
||||
const el = document.getElementById("ap");
|
||||
el.innerHTML = "";
|
||||
aps.forEach(ap => {
|
||||
const beaconing = !!ap.ssid;
|
||||
const div = document.createElement("div");
|
||||
div.className = "ap-band";
|
||||
let html = `<p><b>${esc(ap.band)}</b> ${esc(ap.unit)}: ` +
|
||||
`<span class="pill ${ap.active === "active" ? "ok" : "bad"}">${esc(ap.active)}</span>`;
|
||||
if (beaconing)
|
||||
html += ` SSID <b>${esc(ap.ssid)}</b> ${esc(ap.chan || "")} ${esc(ap.width || "")}`;
|
||||
else
|
||||
html += ` <span class="pill bad">not beaconing</span>`;
|
||||
html += ` Clients: <b>${ap.stations.length}</b>` +
|
||||
` <button class="btn ap-restart">Restart</button></p>`;
|
||||
|
||||
if (ap.stations.length) {
|
||||
html += `<table><thead><tr><th>MAC</th><th>Signal</th><th>TX rate</th></tr></thead><tbody>`;
|
||||
ap.stations.forEach(s => {
|
||||
html += `<tr><td>${esc(s.mac)}</td><td>${esc(s.sig || "?")} dBm</td><td>${esc(s.tx || "?")}</td></tr>`;
|
||||
});
|
||||
html += `</tbody></table>`;
|
||||
}
|
||||
document.getElementById("ap").innerHTML = html;
|
||||
if (ap.stations.length) {
|
||||
html += `<table><thead><tr><th>MAC</th><th>Signal</th><th>TX rate</th></tr></thead><tbody>`;
|
||||
ap.stations.forEach(s => {
|
||||
html += `<tr><td>${esc(s.mac)}</td><td>${esc(s.sig || "?")} dBm</td><td>${esc(s.tx || "?")}</td></tr>`;
|
||||
});
|
||||
html += `</tbody></table>`;
|
||||
}
|
||||
div.innerHTML = html;
|
||||
div.querySelector(".ap-restart").onclick = () => restartAP(ap.unit);
|
||||
el.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Temperatures (van-thermal daemon state) ---------- */
|
||||
@@ -175,7 +189,8 @@ async function readWAN() {
|
||||
const devs = devOut.trim().split("\n").map(line => {
|
||||
const [device, type, state, ...rest] = line.split(":");
|
||||
return { device, type, state, connection: rest.join(":") };
|
||||
}).filter(d => (d.type === "ethernet" || d.type === "wifi") && d.device !== AP);
|
||||
}).filter(d => (d.type === "ethernet" || d.type === "wifi") &&
|
||||
!APS.some(a => a.iface === d.device));
|
||||
|
||||
let routes = [];
|
||||
try { routes = JSON.parse(await run(["ip", "-j", "route", "show", "default"])); } catch (e) { routes = []; }
|
||||
@@ -254,8 +269,8 @@ async function toggleWAN(d, isUp) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
async function restartAP() {
|
||||
try { await run(["systemctl", "restart", "hostapd"], { superuser: "require" }); }
|
||||
async function restartAP(unit) {
|
||||
try { await run(["systemctl", "restart", unit], { superuser: "require" }); }
|
||||
catch (e) { window.alert("Restart failed: " + e.message); }
|
||||
setTimeout(refresh, 2500);
|
||||
}
|
||||
@@ -264,8 +279,9 @@ async function restartAP() {
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const [ap, th, bat, fo, wan] = await Promise.all([readAP(), readThermal(), readBattery(), readFailover(), readWAN()]);
|
||||
renderAP(ap);
|
||||
const [aps, th, bat, fo, wan] = await Promise.all([
|
||||
Promise.all(APS.map(readAP)), readThermal(), readBattery(), readFailover(), readWAN()]);
|
||||
renderAPs(aps);
|
||||
renderThermal(th);
|
||||
renderBattery(bat);
|
||||
renderFailover(fo);
|
||||
@@ -276,6 +292,5 @@ async function refresh() {
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("ap-restart").onclick = restartAP;
|
||||
refresh();
|
||||
setInterval(refresh, 5000);
|
||||
|
||||
Reference in New Issue
Block a user