Replace the all-switches Inspector page with a per-device front panel,
port table and detail panel. /inspector now redirects to the dashboard,
and /device/<name>#port-<n> links directly to a port.
Restore neighbor names after UniFi's LLDP change: read local_port_idx
and resolve neighbors by MAC using Prometheus NIC data, UniFi devices
and the wired client table. This also brings back Run Diagnostics on
server ports, which depends on a named neighbor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Modem and WAN:
- Poll the BGW320 status pages read-only for light levels, PON and WAN
state, with a WAN / Fiber panel, 7-day Rx trend, and alerts on WAN down,
optical limits, and Rx drift
- Read WAN outages and Smart Power restarts from the UniFi system log,
since outages can be over between 5 minute polls
Device pages:
- Add device search to the dashboard and a page per host, switch, router,
AP, PDU, or modem
- Move all Link Debug data into expandable rows there; /links redirects
- Collect UDM ports and UniFi device details (firmware, load, WAN, outlets)
- Reorder the dashboard
Monitoring fixes:
- Alert when a configured host stops reporting: its series disappear
rather than reporting down, so a dead host never raised an alert
- Alert on Prometheus outages instead of blanking the dashboard
- Treat a failed Pulse ping as unknown rather than host unreachable
- Stop new switch ports reporting lifetime bytes as one interval's rate
- Run database sessions in UTC: other server time zones shifted displayed
ages and timed suppression expiries
Also log every avatar 404 reason, add a favicon, and correct the WAN link
labels (1G on Port 9, not 10G SFP+).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The switch port's LLDP entry describes the server, while the server's lldpctl
describes the switch, so the two were never comparable and every link with lldpd
running was flagged as cross-cabled. Pass the switch name in the port data and
compare it with the server's neighbour instead; skip the check without it.
On exception the monitor slept 30s inside the except block then fell
through to time.sleep(poll_interval), giving a 150s recovery gap instead
of 30s. Adding continue after the error sleep fixes this.
Also adds a regression test asserting dmesg filtering uses grep -F --
so a future refactor cannot silently reintroduce the regex wildcard bug.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.py: split 'with open(sentinel): pass' onto two lines (flake8 E701)
- tests/test_diagnose.py: rename test and assert StrictHostKeyChecking=accept-new (not =no which was fixed earlier)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 33 tests for DiagnosticsRunner static methods (build_ssh_command,
parse_output, parse_sysfs_stats, parse_ethtool and variants)
- Add test.yml CI workflow running pytest on every push/PR
- Add security.yml CI workflow running bandit on every push/PR (weekly)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>