firmware&dashboard changes
This commit is contained in:
@@ -418,10 +418,9 @@ td.dim { color: var(--text-dim); }
|
||||
td.muted { color: var(--text-muted); }
|
||||
|
||||
/* ── Badges ── */
|
||||
.badge { display: inline-block; padding: 1px 6px; font-size: 10px; letter-spacing: 0.04em; }
|
||||
.badge.high { color: var(--green-live); border: 1px solid var(--green-dim); }
|
||||
.badge.normal { color: var(--text-dim); border: 1px solid var(--border); }
|
||||
.badge.low { color: var(--text-muted); border: 1px solid var(--text-muted); }
|
||||
.imp-high { color: var(--green-live); }
|
||||
.imp-normal { color: var(--text-dim); }
|
||||
.imp-low { color: var(--text-muted); }
|
||||
|
||||
/* ── RSSI colours ── */
|
||||
.rssi-hi { color: var(--green-hi); }
|
||||
@@ -436,6 +435,20 @@ thead th.sort-active::after { content: ' ' attr(data-arrow); }
|
||||
|
||||
/* ── Enc / node colours ── */
|
||||
.enc-open { color: var(--orange); }
|
||||
.enc-secure { color: var(--green-hi); }
|
||||
|
||||
tr.group-hdr td {
|
||||
background: var(--bg-panel);
|
||||
color: var(--green-mid);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 5px 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
th.group-active { color: var(--green-hi); cursor: pointer; }
|
||||
th.group-col { cursor: pointer; }
|
||||
th.group-col:hover { color: var(--text); }
|
||||
.nodes-multi { color: var(--green-hi); }
|
||||
|
||||
/* ── Cross-node cells ── */
|
||||
|
||||
@@ -8,6 +8,7 @@ let networksData = [];
|
||||
let netSort = { col: 'times_seen', dir: 'desc' };
|
||||
let crossNodeData = { node_ids: [], networks: [] };
|
||||
let clientsData = [];
|
||||
let clientsGroupBy = null; // null | 'vendor' | 'ssid'
|
||||
let presenceData = {};
|
||||
let alertsData = {};
|
||||
let chartBssid = null;
|
||||
@@ -28,7 +29,13 @@ function rssiClass(rssi) {
|
||||
|
||||
function impBadge(imp) {
|
||||
const cls = ['high', 'normal', 'low'].includes(imp) ? imp : 'normal';
|
||||
return `<span class="badge ${cls}">${fmt(imp)}</span>`;
|
||||
return `<span class="imp-${cls}">${fmt(imp)}</span>`;
|
||||
}
|
||||
|
||||
function encClass(enc) {
|
||||
if (!enc || enc === 'OPEN' || enc === 'None') return 'enc-open';
|
||||
if (enc === 'WPA3' || enc === 'WPA2/WPA3') return 'enc-secure';
|
||||
return 'muted';
|
||||
}
|
||||
|
||||
function shortTime(iso) {
|
||||
@@ -174,7 +181,7 @@ function renderNetworksTable() {
|
||||
|
||||
count.textContent = `${sorted.length} unique networks`;
|
||||
tbody.innerHTML = sorted.map(r => {
|
||||
const encCls = (!r.encryption || r.encryption === 'OPEN' || r.encryption === 'None') ? 'enc-open' : 'muted';
|
||||
const encCls = encClass(r.encryption);
|
||||
const nodeCls = r.node_count > 1 ? 'nodes-multi' : 'muted';
|
||||
return `<tr>
|
||||
<td>${fmt(r.ssid, '<hidden>')}</td>
|
||||
@@ -256,7 +263,7 @@ function renderCrossNodeTable() {
|
||||
tbody.innerHTML = networks.map(net => {
|
||||
const seenRssis = nodeIds.filter(id => net.nodes[id]).map(id => net.nodes[id].best_rssi);
|
||||
const globalBest = seenRssis.length ? Math.max(...seenRssis) : null;
|
||||
const encCls = (!net.encryption || net.encryption === 'OPEN' || net.encryption === 'None') ? 'enc-open' : 'muted';
|
||||
const encCls = encClass(net.encryption);
|
||||
const nodeCls = net.node_count > 1 ? 'nodes-multi' : 'muted';
|
||||
|
||||
const nodeCells = nodeIds.map(id => {
|
||||
@@ -285,10 +292,38 @@ async function fetchClients() {
|
||||
renderClientsTable();
|
||||
}
|
||||
|
||||
function clientsRow(r) {
|
||||
const probing = r.probed_ssids.length
|
||||
? r.probed_ssids.map(s => `<span style="color:var(--text-dim)">${s}</span>`).join(', ')
|
||||
: '<span style="color:var(--text-muted)">wildcard only</span>';
|
||||
const vendorCls = r.randomized ? 'muted' : 'dim';
|
||||
const nodeCls = r.node_count > 1 ? 'nodes-multi' : 'muted';
|
||||
return `<tr>
|
||||
<td>${fmt(r.src_mac)}</td>
|
||||
<td class="${vendorCls}">${r.vendor || '—'}</td>
|
||||
<td style="max-width:260px;white-space:normal;line-height:1.8">${probing}</td>
|
||||
<td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
|
||||
<td class="${rssiClass(r.avg_rssi)}">${fmt(r.avg_rssi)} dBm</td>
|
||||
<td class="${nodeCls}">${fmt(r.node_count)}</td>
|
||||
<td>${fmt(r.times_seen)}</td>
|
||||
<td class="dim">${shortTime(r.last_seen)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function toggleClientsGroup(by) {
|
||||
clientsGroupBy = (clientsGroupBy === by) ? null : by;
|
||||
renderClientsTable();
|
||||
}
|
||||
|
||||
function renderClientsTable() {
|
||||
const tbody = document.getElementById('clients-tbody');
|
||||
const empty = document.getElementById('clients-empty');
|
||||
const count = document.getElementById('clients-count');
|
||||
const thVendor = document.getElementById('clients-th-vendor');
|
||||
const thProbing = document.getElementById('clients-th-probing');
|
||||
|
||||
if (thVendor) thVendor.className = clientsGroupBy === 'vendor' ? 'group-active' : 'group-col';
|
||||
if (thProbing) thProbing.className = clientsGroupBy === 'ssid' ? 'group-active' : 'group-col';
|
||||
|
||||
if (!clientsData.length) {
|
||||
tbody.innerHTML = '';
|
||||
@@ -299,22 +334,29 @@ function renderClientsTable() {
|
||||
empty.style.display = 'none';
|
||||
count.textContent = `${clientsData.length} unique MACs`;
|
||||
|
||||
tbody.innerHTML = clientsData.map(r => {
|
||||
const probing = r.probed_ssids.length
|
||||
? r.probed_ssids.map(s => `<span style="color:var(--text-dim)">${s}</span>`).join(', ')
|
||||
: '<span style="color:var(--text-muted)">wildcard only</span>';
|
||||
const vendorCls = r.randomized ? 'muted' : 'dim';
|
||||
const nodeCls = r.node_count > 1 ? 'nodes-multi' : 'muted';
|
||||
return `<tr>
|
||||
<td>${fmt(r.src_mac)}</td>
|
||||
<td class="${vendorCls}">${r.vendor || '—'}</td>
|
||||
<td style="max-width:260px;white-space:normal;line-height:1.8">${probing}</td>
|
||||
<td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
|
||||
<td class="${rssiClass(r.avg_rssi)}">${fmt(r.avg_rssi)} dBm</td>
|
||||
<td class="${nodeCls}">${fmt(r.node_count)}</td>
|
||||
<td>${fmt(r.times_seen)}</td>
|
||||
<td class="dim">${shortTime(r.last_seen)}</td>
|
||||
</tr>`;
|
||||
if (!clientsGroupBy) {
|
||||
tbody.innerHTML = clientsData.map(clientsRow).join('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build groups
|
||||
const groups = {};
|
||||
clientsData.forEach(r => {
|
||||
let key;
|
||||
if (clientsGroupBy === 'vendor') {
|
||||
key = r.vendor || 'Unknown';
|
||||
} else {
|
||||
key = r.probed_ssids.length ? r.probed_ssids[0] : '(wildcard only)';
|
||||
}
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(r);
|
||||
});
|
||||
|
||||
const sorted = Object.keys(groups).sort((a, b) => a.localeCompare(b));
|
||||
tbody.innerHTML = sorted.map(key => {
|
||||
const rows = groups[key];
|
||||
const hdr = `<tr class="group-hdr"><td colspan="8">${key} — ${rows.length} device${rows.length > 1 ? 's' : ''}</td></tr>`;
|
||||
return hdr + rows.map(clientsRow).join('');
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@@ -382,7 +424,7 @@ function renderPresenceView() {
|
||||
} else {
|
||||
arrNetsEmpty.style.display = 'none';
|
||||
arrNetsTbody.innerHTML = newNets.map(r => {
|
||||
const encCls = (!r.encryption || r.encryption === 'OPEN' || r.encryption === 'None') ? 'enc-open' : 'muted';
|
||||
const encCls = encClass(r.encryption);
|
||||
return `<tr>
|
||||
<td>${fmt(r.ssid, '<hidden>')}</td>
|
||||
<td class="dim">${fmt(r.bssid)}</td>
|
||||
@@ -398,8 +440,8 @@ function renderPresenceView() {
|
||||
const regMacsTbody = document.getElementById('regulars-macs-tbody');
|
||||
const regMacsEmpty = document.getElementById('regulars-macs-empty');
|
||||
const regCount = document.getElementById('regulars-count');
|
||||
const regMacs = p.regular_macs || [];
|
||||
const regNets = p.regular_networks || [];
|
||||
const regMacs = p.reg_macs || [];
|
||||
const regNets = p.reg_networks || [];
|
||||
regCount.textContent = `${regMacs.length} devices · ${regNets.length} networks`;
|
||||
if (!regMacs.length) {
|
||||
regMacsTbody.innerHTML = '';
|
||||
@@ -425,7 +467,7 @@ function renderPresenceView() {
|
||||
} else {
|
||||
regNetsEmpty.style.display = 'none';
|
||||
regNetsTbody.innerHTML = regNets.map(r => {
|
||||
const encCls = (!r.encryption || r.encryption === 'OPEN' || r.encryption === 'None') ? 'enc-open' : 'muted';
|
||||
const encCls = encClass(r.encryption);
|
||||
return `<tr>
|
||||
<td>${fmt(r.ssid, '<hidden>')}</td>
|
||||
<td class="dim">${fmt(r.bssid)}</td>
|
||||
@@ -841,7 +883,7 @@ function renderSearchResults(data, q) {
|
||||
// As a network
|
||||
if (data.as_network) {
|
||||
const rows = data.as_network.map(r => {
|
||||
const encCls = (!r.encryption || r.encryption === 'OPEN') ? 'enc-open' : 'muted';
|
||||
const encCls = encClass(r.encryption);
|
||||
return `<tr>
|
||||
<td>${fmt(r.ssid, '<hidden>')}</td>
|
||||
<td class="dim">${fmt(r.bssid)}</td>
|
||||
@@ -936,7 +978,7 @@ function renderSearchResults(data, q) {
|
||||
if (data.vendor_networks) {
|
||||
const label = data.vendor_name || q;
|
||||
const rows = data.vendor_networks.map(r => {
|
||||
const encCls = (!r.encryption || r.encryption === 'OPEN') ? 'enc-open' : 'muted';
|
||||
const encCls = encClass(r.encryption);
|
||||
return `<tr>
|
||||
<td>${fmt(r.ssid, '<hidden>')}</td>
|
||||
<td class="dim">${fmt(r.bssid)}</td>
|
||||
|
||||
Reference in New Issue
Block a user