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
@@ -197,7 +197,8 @@ Not touched by `deploy.sh` — the VM's LAN address/name come from `ap/van-ap-dn
|
|||||||
### Cockpit dashboard
|
### Cockpit dashboard
|
||||||
- `https://<wayback>:9090` → "Van Router". Reload the browser after deploying plugin changes (Cockpit caches packages per session).
|
- `https://<wayback>:9090` → "Van Router". Reload the browser after deploying plugin changes (Cockpit caches packages per session).
|
||||||
- Read-only status works as any user; Prefer/Up/Down/Restart need *Administrative access* (polkit).
|
- Read-only status works as any user; Prefer/Up/Down/Restart need *Administrative access* (polkit).
|
||||||
- **Prefer** sets the manual WAN preference (see *Manual preference* above); **Up/Down** connect/disconnect the NM device; **Restart** restarts hostapd.
|
- The Access Points card shows both bands (5GHz `hostapd`, 2.4GHz `hostapd-2g`) with per-band client lists and **Restart** buttons; the radio list lives in `vanrouter.js` (`const APS`).
|
||||||
|
- **Prefer** sets the manual WAN preference (see *Manual preference* above); **Up/Down** connect/disconnect the NM device.
|
||||||
|
|
||||||
### Home Assistant VM (`ha_van`)
|
### Home Assistant VM (`ha_van`)
|
||||||
- HAOS runs as a libvirt KVM VM whose NIC is **bridged into `br0`** — it is a first-class LAN
|
- HAOS runs as a libvirt KVM VM whose NIC is **bridged into `br0`** — it is a first-class LAN
|
||||||
|
|||||||
@@ -16,10 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-head">
|
<h3>Access Points</h3>
|
||||||
<h3>Access Point</h3>
|
|
||||||
<button id="ap-restart" class="btn">Restart AP</button>
|
|
||||||
</div>
|
|
||||||
<div id="ap"></div>
|
<div id="ap"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ th { color: #6a6e73; font-weight: 600; }
|
|||||||
.bar-fill.warn { background: #e08a00; }
|
.bar-fill.warn { background: #e08a00; }
|
||||||
.bar-fill.crit { background: #c9190b; }
|
.bar-fill.crit { background: #c9190b; }
|
||||||
.muted { color: #6a6e73; font-size: 12px; }
|
.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; }
|
.active-wan { font-weight: 700; color: #0066cc; }
|
||||||
.btn { margin-left: 6px; padding: 3px 10px; cursor: pointer; }
|
.btn { margin-left: 6px; padding: 3px 10px; cursor: pointer; }
|
||||||
.btn[disabled] { cursor: default; opacity: 0.5; }
|
.btn[disabled] { cursor: default; opacity: 0.5; }
|
||||||
|
|||||||
@@ -4,7 +4,11 @@
|
|||||||
// Read-only status via cockpit.spawn (logged-in user); mutating actions use
|
// Read-only status via cockpit.spawn (logged-in user); mutating actions use
|
||||||
// { superuser: "require" } which triggers Cockpit's admin (polkit) escalation.
|
// { 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
|
const PREFER_FILE = "/run/van-failover/prefer"; // van-failover reads this to pick the preferred WAN
|
||||||
|
|
||||||
function run(args, opts) {
|
function run(args, opts) {
|
||||||
@@ -24,19 +28,19 @@ function esc_sh(s) {
|
|||||||
|
|
||||||
/* ---------- Access Point ---------- */
|
/* ---------- Access Point ---------- */
|
||||||
|
|
||||||
async function readAP() {
|
async function readAP(a) {
|
||||||
let active = "inactive";
|
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 = "";
|
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 ssid = (info.match(/\bssid (.+)/) || [])[1];
|
||||||
const chan = (info.match(/\bchannel \d+[^\n]*/) || [])[0];
|
const chan = (info.match(/\bchannel \d+[^\n]*/) || [])[0];
|
||||||
const width = (info.match(/\bwidth: ([^\n,]+)/) || [])[1];
|
const width = (info.match(/\bwidth: ([^\n,]+)/) || [])[1];
|
||||||
|
|
||||||
let stations = [];
|
let stations = [];
|
||||||
try {
|
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 => ({
|
stations = dump.split(/Station /).slice(1).map(b => ({
|
||||||
mac: b.split(" ")[0],
|
mac: b.split(" ")[0],
|
||||||
sig: (b.match(/signal:\s*([\-\d]+)/) || [])[1],
|
sig: (b.match(/signal:\s*([\-\d]+)/) || [])[1],
|
||||||
@@ -44,26 +48,36 @@ async function readAP() {
|
|||||||
}));
|
}));
|
||||||
} catch (e) { stations = []; }
|
} catch (e) { stations = []; }
|
||||||
|
|
||||||
return { active, ssid, chan, width, stations };
|
return { band: a.band, unit: a.unit, active, ssid, chan, width, stations };
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAP(ap) {
|
function renderAPs(aps) {
|
||||||
const beaconing = !!ap.ssid;
|
const el = document.getElementById("ap");
|
||||||
let html = `<p>hostapd: <span class="pill ${ap.active === "active" ? "ok" : "bad"}">${esc(ap.active)}</span>`;
|
el.innerHTML = "";
|
||||||
if (beaconing)
|
aps.forEach(ap => {
|
||||||
html += ` SSID <b>${esc(ap.ssid)}</b> ${esc(ap.chan || "")} ${esc(ap.width || "")}`;
|
const beaconing = !!ap.ssid;
|
||||||
else
|
const div = document.createElement("div");
|
||||||
html += ` <span class="pill bad">not beaconing</span>`;
|
div.className = "ap-band";
|
||||||
html += `</p><p>Clients: <b>${ap.stations.length}</b></p>`;
|
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) {
|
if (ap.stations.length) {
|
||||||
html += `<table><thead><tr><th>MAC</th><th>Signal</th><th>TX rate</th></tr></thead><tbody>`;
|
html += `<table><thead><tr><th>MAC</th><th>Signal</th><th>TX rate</th></tr></thead><tbody>`;
|
||||||
ap.stations.forEach(s => {
|
ap.stations.forEach(s => {
|
||||||
html += `<tr><td>${esc(s.mac)}</td><td>${esc(s.sig || "?")} dBm</td><td>${esc(s.tx || "?")}</td></tr>`;
|
html += `<tr><td>${esc(s.mac)}</td><td>${esc(s.sig || "?")} dBm</td><td>${esc(s.tx || "?")}</td></tr>`;
|
||||||
});
|
});
|
||||||
html += `</tbody></table>`;
|
html += `</tbody></table>`;
|
||||||
}
|
}
|
||||||
document.getElementById("ap").innerHTML = html;
|
div.innerHTML = html;
|
||||||
|
div.querySelector(".ap-restart").onclick = () => restartAP(ap.unit);
|
||||||
|
el.appendChild(div);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Temperatures (van-thermal daemon state) ---------- */
|
/* ---------- Temperatures (van-thermal daemon state) ---------- */
|
||||||
@@ -175,7 +189,8 @@ async function readWAN() {
|
|||||||
const devs = devOut.trim().split("\n").map(line => {
|
const devs = devOut.trim().split("\n").map(line => {
|
||||||
const [device, type, state, ...rest] = line.split(":");
|
const [device, type, state, ...rest] = line.split(":");
|
||||||
return { device, type, state, connection: rest.join(":") };
|
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 = [];
|
let routes = [];
|
||||||
try { routes = JSON.parse(await run(["ip", "-j", "route", "show", "default"])); } catch (e) { 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();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restartAP() {
|
async function restartAP(unit) {
|
||||||
try { await run(["systemctl", "restart", "hostapd"], { superuser: "require" }); }
|
try { await run(["systemctl", "restart", unit], { superuser: "require" }); }
|
||||||
catch (e) { window.alert("Restart failed: " + e.message); }
|
catch (e) { window.alert("Restart failed: " + e.message); }
|
||||||
setTimeout(refresh, 2500);
|
setTimeout(refresh, 2500);
|
||||||
}
|
}
|
||||||
@@ -264,8 +279,9 @@ async function restartAP() {
|
|||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
const [ap, th, bat, fo, wan] = await Promise.all([readAP(), readThermal(), readBattery(), readFailover(), readWAN()]);
|
const [aps, th, bat, fo, wan] = await Promise.all([
|
||||||
renderAP(ap);
|
Promise.all(APS.map(readAP)), readThermal(), readBattery(), readFailover(), readWAN()]);
|
||||||
|
renderAPs(aps);
|
||||||
renderThermal(th);
|
renderThermal(th);
|
||||||
renderBattery(bat);
|
renderBattery(bat);
|
||||||
renderFailover(fo);
|
renderFailover(fo);
|
||||||
@@ -276,6 +292,5 @@ async function refresh() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("ap-restart").onclick = restartAP;
|
|
||||||
refresh();
|
refresh();
|
||||||
setInterval(refresh, 5000);
|
setInterval(refresh, 5000);
|
||||||
|
|||||||
Reference in New Issue
Block a user