A new Cutsheet page records where every cable is meant to go and compares
it with what Gandalf discovers.
- Inventory for what discovery can't see: patch panels, wall jacks,
shelves and rack positions. Panels and jacks pass through front to rear.
- Cables with two endpoints, ID, type, length, color, status (planned,
installed, dark, retired) and notes; the far end may be unterminated.
- Each cable's full run is traced through panels and jacks, and runs
ending on a switch port are checked against its discovered neighbor
(LLDP, client table, or a DAC serial seen at both ends): matches,
wrong neighbor, no link, missing port, port conflict, unverified, idle.
- Undocumented links: live switch ports no cable mentions, one click to
add with what Gandalf already knows. Port panels show the planned cable.
- Labels to make: cable IDs and panel-port labels not yet applied, with a
print view, for a handheld label maker.
- CSV export, and all-or-nothing import that upserts by cable ID.
- Cables, panels and jacks appear in the dashboard search.
Editing needs the admin group and a JSON request body. Every change is
written to cutsheet_history in the same transaction. Tables are created
on first use.
Refs #12
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ports were keyed by their UniFi name, so when two ports shared a name the
later one replaced the earlier. The 24-port switch lost port 13 this way
(ports 13 and 14 are both "Office TV"). A repeated name is now kept as
"<name> (Port N)".
Traffic rates are now matched to the previous poll by port number rather
than name. Names are re-read from UniFi every poll, so a port renamed
there keeps its rates instead of losing them for a cycle.
Collect what UniFi reports about each SFP cage: whether a module is
present, its type (DAC or optic), part, serial, fault flags, and DOM
readings when the module has them. The faceplate draws empty cages,
optics, DACs and unidentified modules differently and outlines a faulted
module in red. The port panel gets an SFP Module section. Both ends of a
DAC report the same cable serial, so each end links to the other.
Closes#7Closes#8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remote-User/Name/Email/Groups were read from every request, so a LAN host
connecting to the container directly could claim any user and group. Add
auth.trusted_proxies and ignore those headers unless the request came from
one of them: the X-Real-IP the local nginx sets, or the peer address when
gunicorn is reached directly. With nothing configured, nobody is trusted.
Rejected attempts are logged, and /api/avatar now uses the same check.
The container's nginx site blanks the same headers for non-NPM sources
(pve-infra 4e3ab7c).
Closes#3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host's node_exporter stops as soon as a shutdown begins and a normal
reboot takes about 3-5 minutes, so the 2-check failure_threshold cut a
ticket for planned reboots. Add monitor.host_failure_threshold, default 5,
for hosts with no metrics and ping-only hosts. Interface link-down alerts
keep the shorter threshold, and the dashboard still shows the host as down
straight away.
Closes#14
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the port blocks on device pages with SVG faceplates traced from
reference images: Dream Machine Pro, USW Pro 24 PoE, USW Aggregation,
USW Flex, USW Flex Mini, USP PDU Pro and UPS 2U Pro. Port LEDs use each
model's documented colors, and ports show a cable or module when up.
Remove SWITCH_LAYOUTS, which had wrong data: the aggregation switch was
numbered across the rows instead of odd over even, and the USW Flex had
an SFP cage it doesn't have. Models without a faceplate use plain blocks.
Show PDU and UPS outlets with on/off state and auto power-cycle, and let
the UPS page show its panel even though it reports no ports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>