cockpit: show IP + hostname for AP clients

Join station MACs against dnsmasq leases (IP + hostname), with the
kernel's br0 neighbor table as a fallback IP source for static-IP
clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Andreas Wrede
2026-07-06 14:08:03 -04:00
co-authored by Claude Fable 5
parent db16964d5a
commit 23448294a4
+29 -6
View File
@@ -51,7 +51,28 @@ async function readAP(a) {
return { band: a.band, unit: a.unit, active, ssid, chan, width, stations }; return { band: a.band, unit: a.unit, active, ssid, chan, width, stations };
} }
function renderAPs(aps) { async function readClientDir() {
// MAC -> { ip, host } for AP clients. Kernel neighbor table first (covers
// static-IP clients), then dnsmasq leases on top (authoritative + hostname).
const dir = {};
try {
JSON.parse(await run(["ip", "-j", "neigh", "show", "dev", "br0"])).forEach(n => {
if (n.lladdr && n.dst && !n.dst.includes(":")) // IPv4 only
dir[n.lladdr.toLowerCase()] = { ip: n.dst, host: null };
});
} catch (e) { /* ignore */ }
try {
// lease line: <expiry-epoch> <mac> <ip> <hostname|*> <client-id>
(await run(["cat", "/var/lib/misc/dnsmasq.leases"])).trim().split("\n").forEach(l => {
const f = l.split(" ");
if (f.length >= 4)
dir[f[1].toLowerCase()] = { ip: f[2], host: f[3] === "*" ? null : f[3] };
});
} catch (e) { /* ignore */ }
return dir;
}
function renderAPs(aps, dir) {
const el = document.getElementById("ap"); const el = document.getElementById("ap");
el.innerHTML = ""; el.innerHTML = "";
aps.forEach(ap => { aps.forEach(ap => {
@@ -68,9 +89,11 @@ function renderAPs(aps) {
` <button class="btn ap-restart">Restart</button></p>`; ` <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>IP</th><th>Hostname</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>`; 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>`; html += `</tbody></table>`;
} }
@@ -279,9 +302,9 @@ async function restartAP(unit) {
async function refresh() { async function refresh() {
try { try {
const [aps, th, bat, fo, wan] = await Promise.all([ const [aps, dir, th, bat, fo, wan] = await Promise.all([
Promise.all(APS.map(readAP)), readThermal(), readBattery(), readFailover(), readWAN()]); Promise.all(APS.map(readAP)), readClientDir(), readThermal(), readBattery(), readFailover(), readWAN()]);
renderAPs(aps); renderAPs(aps, dir);
renderThermal(th); renderThermal(th);
renderBattery(bat); renderBattery(bat);
renderFailover(fo); renderFailover(fo);