cockpit: unified Clients card (Wi-Fi + wired via bridge FDB)
New "Clients" card lists everything on the van LAN in one table — hostname, IP, MAC, connection (5GHz / 2.4GHz / LAN eth0 / LAN USB), signal, TX rate. Wi-Fi rows come from the hostapd station dumps as before; wired rows from learned bridge-FDB entries on the LAN ports (the port's own MAC is "permanent", and FDB duplicates entries per vlan — both filtered), enriched with IP/hostname from dnsmasq leases + neighbor table. The per-band station tables move out of the Access Points card (which keeps its status line + client count), and the WAN/Uplinks table now hides unmanaged devices so the networkd-owned LAN ports don't show up as ghost WAN rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a36b64c7c4
commit
776f020af3
@@ -12,6 +12,8 @@ const APS = [
|
||||
{ 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
|
||||
// Wired LAN bridge ports: clients behind them are found via the bridge FDB.
|
||||
const LAN_PORTS = { "eth0": "LAN (eth0)", "enx00e04c331140": "LAN (USB)" };
|
||||
// Starlink dish: gRPC status API on the fixed management IP (reached via the /32
|
||||
// link route the netplan profile installs on the RTL8153 uplink).
|
||||
const STARLINK = { iface: "enxd8ec5eeb3512", dish: "192.168.100.1:9200" };
|
||||
@@ -44,6 +46,7 @@ const STATUS_SCRIPT = (() => {
|
||||
add(`stations${i}`, `iw dev ${a.iface} station dump`);
|
||||
});
|
||||
add("neigh", "ip -j neigh show dev br0");
|
||||
add("fdb", "bridge -j fdb show br br0");
|
||||
add("leases", "cat /var/lib/misc/dnsmasq.leases");
|
||||
add("thermal", "cat /run/van-thermal/state.json");
|
||||
// Sentinel words (ABSENT/NOGRPCURL/UNREACHABLE) let the renderer tell the
|
||||
@@ -113,7 +116,7 @@ function parseClientDir(secs) {
|
||||
return dir;
|
||||
}
|
||||
|
||||
function renderAPs(aps, dir) {
|
||||
function renderAPs(aps) {
|
||||
const el = document.getElementById("ap");
|
||||
el.innerHTML = "";
|
||||
aps.forEach(ap => {
|
||||
@@ -128,22 +131,57 @@ function renderAPs(aps, dir) {
|
||||
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>IP</th><th>Hostname</th><th>Signal</th><th>TX rate</th></tr></thead><tbody>`;
|
||||
ap.stations.forEach(s => {
|
||||
const c = dir[s.mac.toLowerCase()] || {};
|
||||
html += `<tr><td>${esc(s.mac)}</td><td>${esc(c.ip || "—")}</td><td>${esc(c.host || "—")}</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);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Clients (Wi-Fi stations + wired bridge-port FDB) ---------- */
|
||||
|
||||
function collectClients(aps, secs, dir) {
|
||||
// Wi-Fi clients come from the hostapd station dumps (with signal/rate);
|
||||
// wired ones from learned bridge-FDB entries on the LAN ports.
|
||||
const rows = [];
|
||||
const seen = new Set();
|
||||
aps.forEach(ap => ap.stations.forEach(s => {
|
||||
const c = dir[s.mac.toLowerCase()] || {};
|
||||
seen.add(s.mac.toLowerCase());
|
||||
rows.push({ conn: ap.band, mac: s.mac, ip: c.ip, host: c.host,
|
||||
sig: s.sig ? s.sig + " dBm" : "?", tx: s.tx || "?" });
|
||||
}));
|
||||
parseJSON(secs.fdb, []).forEach(e => {
|
||||
const conn = LAN_PORTS[e.ifname];
|
||||
const mac = (e.mac || "").toLowerCase();
|
||||
// learned entries only: "permanent" = the port's own MAC, and FDB
|
||||
// duplicates entries per vlan — hence the seen-dedupe.
|
||||
if (!conn || e.master !== "br0" || e.state === "permanent" || seen.has(mac))
|
||||
return;
|
||||
seen.add(mac);
|
||||
const c = dir[mac] || {};
|
||||
rows.push({ conn, mac: e.mac, ip: c.ip, host: c.host, sig: "—", tx: "—" });
|
||||
});
|
||||
rows.sort((a, b) => a.conn.localeCompare(b.conn) || (a.ip || "").localeCompare(b.ip || ""));
|
||||
return rows;
|
||||
}
|
||||
|
||||
function renderClients(rows) {
|
||||
const el = document.getElementById("clients");
|
||||
if (!rows.length) {
|
||||
el.innerHTML = `<p class="muted">No clients connected.</p>`;
|
||||
return;
|
||||
}
|
||||
let html = `<table><thead><tr><th>Hostname</th><th>IP</th><th>MAC</th>` +
|
||||
`<th>Connection</th><th>Signal</th><th>TX rate</th></tr></thead><tbody>`;
|
||||
rows.forEach(r => {
|
||||
html += `<tr><td>${esc(r.host || "—")}</td><td>${esc(r.ip || "—")}</td>` +
|
||||
`<td>${esc(r.mac)}</td><td>${esc(r.conn)}</td>` +
|
||||
`<td>${esc(r.sig)}</td><td>${esc(r.tx)}</td></tr>`;
|
||||
});
|
||||
html += `</tbody></table>`;
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
/* ---------- Temperatures (van-thermal daemon state) ---------- */
|
||||
|
||||
function renderThermal(th) {
|
||||
@@ -299,6 +337,7 @@ function parseWAN(secs) {
|
||||
const [device, type, state, ...rest] = line.split(":");
|
||||
return { device, type, state, connection: rest.join(":") };
|
||||
}).filter(d => (d.type === "ethernet" || d.type === "wifi") &&
|
||||
d.state !== "unmanaged" && // networkd-owned LAN ports (eth0, enx* dongle)
|
||||
!APS.some(a => a.iface === d.device));
|
||||
|
||||
const routes = parseJSON(secs.routes, []);
|
||||
@@ -386,7 +425,9 @@ async function restartAP(unit) {
|
||||
async function refresh() {
|
||||
try {
|
||||
const secs = parseSections(await sh(STATUS_SCRIPT));
|
||||
renderAPs(APS.map((a, i) => parseAP(a, i, secs)), parseClientDir(secs));
|
||||
const aps = APS.map((a, i) => parseAP(a, i, secs));
|
||||
renderAPs(aps);
|
||||
renderClients(collectClients(aps, secs, parseClientDir(secs)));
|
||||
renderThermal(parseJSON(secs.thermal, null));
|
||||
renderStarlink(secs.starlink);
|
||||
renderBattery(parseJSON(secs.battery, null));
|
||||
|
||||
Reference in New Issue
Block a user