Commit Graph
86 Commits
Author SHA1 Message Date
jaredandClaude Opus 5.5 cbdae54d70 Rack: brush panel opening runs nearly the full width between the ears
Lint / Python (flake8) (push) Successful in 58s
Lint / JS (eslint) (push) Successful in 7s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m6s
Test / Python Tests (pytest) (push) Successful in 1m7s
Lint / JS (eslint) (pull_request) Successful in 9s
Lint / Python (flake8) (pull_request) Successful in 1m51s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m45s
Test / Python Tests (pytest) (pull_request) Successful in 1m1s
As in Ubiquiti's product image and the rack photo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 01:57:19 -05:00
jaredandClaude Opus 5.5 36cbdd061d Rack: brush panel holds up at rack size
Lint / Python (flake8) (push) Successful in 1m2s
Lint / JS (eslint) (push) Successful in 10s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Lint / JS (eslint) (pull_request) Successful in 7s
Lint / Python (flake8) (pull_request) Successful in 1m26s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (push) Successful in 1m55s
Test / Python Tests (pytest) (push) Successful in 1m24s
Security / Python Security (bandit) (pull_request) Successful in 49s
Test / Python Tests (pytest) (pull_request) Successful in 1m10s
The bristles were hundreds of hard strands, which aliased into flat
stripes at normal rack size. Each row is now shaded root to tip with a
faint strand texture over it, so the panel looks the same at any size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 01:56:24 -05:00
jaredandClaude Opus 5.5 6e6150f9b9 Rack: rack handles on the Sliger ears
Lint / JS (eslint) (push) Successful in 13s
Lint / Python (flake8) (push) Successful in 1m50s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m17s
Test / Python Tests (pytest) (push) Successful in 1m39s
Lint / Python (flake8) (pull_request) Successful in 1m28s
Lint / JS (eslint) (pull_request) Successful in 17s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m26s
Test / Python Tests (pytest) (pull_request) Successful in 1m6s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 01:55:12 -05:00
jaredandClaude Opus 5.5 7408b1feca Rack: Sliger fronts in red, white and blue, and the UniFi brush panel
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 53s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m0s
Lint / Python (flake8) (pull_request) Successful in 52s
Test / Python Tests (pytest) (push) Successful in 1m7s
Lint / JS (eslint) (pull_request) Successful in 10s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m52s
Test / Python Tests (pytest) (pull_request) Successful in 1m51s
From the rack photo on the wiki, the three Sligers have their coloured
perforated fronts fitted, not the faceplate-off view: Front drawings
"Sliger CX4712 4U, red/white/blue front" draw the hex-perforated panel
(about 20 rows of holes, the drive cages faint behind it), black rack ears,
the power button with Gandalf's status ring, and the USB recess.

UniFi UACC-Rack-Panel-Brush-1U is drawn from Ubiquiti's product images:
silver face, ear plates with rounded slots, a recessed chamfered frame and
two rows of bristles meeting at a seam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 01:51:29 -05:00
jaredandClaude Opus 5.5 d8e827c88b Rack: gear resting on other gear, and live desk switches on shelves
Lint / Python (flake8) (push) Failing after 47s
Lint / JS (eslint) (push) Successful in 17s
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m26s
Lint / Notify on failure (push) Successful in 7s
Test / Python Tests (pytest) (push) Successful in 2m2s
A USW Flex Mini lies on top of the NVR. A shelf item with Front drawing
"No shelf: gear resting on what is below" draws no tray or ears, so its
contents sit directly on the item under it.

UniFi desk switches on a shelf (Flex Mini, Flex) are drawn with their live
faceplate, scaled to their real width from Ubiquiti's tech specs (107 mm and
122.5 mm), with working ports. faceplateSvg() returns a faceplate's SVG on
its own so it can be placed inside the shelf drawing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 01:08:42 -05:00
jared 0243b23732 Merge remote-tracking branch 'origin/main' into zimaboard-and-facing
Lint / Python (flake8) (push) Successful in 58s
Lint / JS (eslint) (push) Successful in 8s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Lint / JS (eslint) (pull_request) Successful in 13s
Security / Python Security (bandit) (push) Successful in 1m11s
Test / Python Tests (pytest) (push) Successful in 1m14s
Security / Python Security (bandit) (pull_request) Successful in 1m44s
Test / Python Tests (pytest) (pull_request) Successful in 1m40s
Lint / Python (flake8) (pull_request) Successful in 48s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
# Conflicts:
#	static/style.css
#	tests/test_cutsheet.py
2026-11-11 00:46:13 -05:00
jaredandClaude Opus 5.5 486a93c5a0 Rack: ConnectX-3 SFP+ cards in the ZimaBoards' PCIe slots; correct NIC mapping
Lint / JS (eslint) (push) Successful in 10s
Lint / Python (flake8) (push) Successful in 51s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Lint / JS (eslint) (pull_request) Successful in 8s
Lint / Python (flake8) (pull_request) Successful in 50s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m7s
Security / Python Security (bandit) (push) Successful in 1m8s
Test / Python Tests (pytest) (push) Successful in 1m38s
Test / Python Tests (pytest) (pull_request) Successful in 1m38s
From the rack photo, each ZimaBoard has a single-port ConnectX-3 lying flat
in its side PCIe slot with a DAC attached. Front drawing "ZimaBoard 832 +
ConnectX-3" draws the card beside the board at scale: PCB at board height,
heatsink, SFP+ cage with the DAC and pull tab, and a Gandalf link mark for
the card's interface.

The jacks were lit in NIC name order, which put the ConnectX-3 (enp1s0, PCI
bus 1) on the left RJ45. The onboard ports are enp2s0 (left) and enp3s0
(right); the PCIe card is enp1s0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 00:35:25 -05:00
jaredandClaude Opus 5.5 a9de175d90 Rack: ports-facing separate from posts, three views, ZimaBoard 832 shelves to scale
Lint / JS (eslint) (push) Successful in 8s
Lint / Python (flake8) (push) Successful in 1m0s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m9s
Lint / Python (flake8) (pull_request) Successful in 57s
Test / Python Tests (pytest) (push) Successful in 1m14s
Lint / JS (eslint) (pull_request) Successful in 8s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 56s
Test / Python Tests (pytest) (pull_request) Successful in 1m5s
Gear on the rear posts usually still faces the front (the StarTech's RPDU
and ZimaBoard shelf), so "which posts" and "which way the ports face" are
now separate fields (mount, facing). The page has three views: Front (front
posts), Rear posts (rear-post gear seen from the front, i.e. what is behind
the front gear) and Rear (from behind). An item seen from the side it
doesn't face shows its back. Facing defaults to front, so existing items
keep their meaning.

Shelves are drawn at true scale with their contents ordered by a new
"Position on shelf". The IceWhale ZimaBoard 832 is traced from IceWhale's
labelled photo of its port end (ZimaDocs): Mini-DP, two RJ45-over-USB 3.0
stacks with link/activity LEDs, DC jack, gunmetal case, gold PCB edge and
black base. Its RJ45 link lights follow the host's physical NICs and the
status glow follows the host.

New cutsheet_items columns facing and slot, added on first use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 00:12:07 -05:00
jaredandClaude Opus 5.5 ce12dede5f Faceplates: USW Pro Max 24 PoE and 48 PoE
Lint / JS (eslint) (push) Successful in 12s
Lint / Python (flake8) (push) Successful in 1m10s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m11s
Test / Python Tests (pytest) (push) Successful in 1m42s
Lint / Python (flake8) (pull_request) Successful in 2m28s
Lint / JS (eslint) (pull_request) Successful in 14s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m10s
Test / Python Tests (pytest) (pull_request) Successful in 1m16s
Both laid out from the CC0 NetBox devicetype-library images, with jack
centres measured from them: the 24 has one row of 24 RJ45 in groups of 8
(17-24 at 2.5G) and 2 SFP+; the 48 has two rows, odd ports on top, in
groups of 16 (33-48 at 2.5G) and 4 SFP+ (49-52). The "2.5G" and "10G" bars
under the faster ports are drawn too (fpSpeedBar).

Registered as USPM24P/USPM48P with USPM24/USPM48 aliases, and as Front
drawing options so the new switch draws on the rack before it is adopted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 00:01:12 -05:00
jaredandClaude Opus 5.5 5fdd317539 Rack: clicking a port on an unlinked faceplate opens the cutsheet
Lint / JS (eslint) (push) Successful in 8s
Lint / Python (flake8) (push) Successful in 50s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m8s
Lint / JS (eslint) (pull_request) Successful in 22s
Lint / Python (flake8) (pull_request) Successful in 1m49s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m12s
Test / Python Tests (pytest) (pull_request) Successful in 1m9s
Test / Python Tests (pytest) (push) Successful in 1m3s
The NVR Pro is drawn by Front drawing because UniFi doesn't report it, so
it has no device page; clicking one of its ports went to /device/UNVR-Pro
and showed "No device with this name". Ports on a faceplate whose item
isn't linked to a live device now open the port's cable when one is
documented, and otherwise the item on the cutsheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 23:53:18 -05:00
jaredandClaude Opus 5.5 50db56cdcb Rack: draw the UniFi UACC blank 24-port keystone patch panel
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 58s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Lint / JS (eslint) (pull_request) Successful in 11s
Lint / Python (flake8) (pull_request) Successful in 1m6s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (push) Successful in 1m0s
Test / Python Tests (pytest) (push) Successful in 1m59s
Security / Python Security (bandit) (pull_request) Successful in 1m42s
Test / Python Tests (pytest) (pull_request) Successful in 1m0s
Front drawing "UniFi Patch Panel Blank 24" draws the real panel, laid out
from the CC0 NetBox devicetype-library image: grey steel face, darker ears
with oval holes, 24 keystone openings with the cable-support bar behind
them, and a cable slot at each end. Ports with a documented cable get a
keystone jack; cutsheet marks, port labels and run highlighting work as on
the generic panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 23:39:27 -05:00
jaredandClaude Opus 5.5 619a2c913d Remove em and en dashes from the UI, code and docs
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 57s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 57s
Lint / JS (eslint) (pull_request) Successful in 10s
Lint / Python (flake8) (pull_request) Successful in 51s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m0s
Test / Python Tests (pytest) (push) Successful in 1m3s
Test / Python Tests (pytest) (pull_request) Successful in 52s
Page titles read "Rack | GANDALF", ranges use a hyphen (U26-42,
1-100 of 300), empty cells show "-", and dash-separated phrases use a
colon, comma or full stop. In the shared base.js only the on-screen strings
changed (boot line, placeholders, offline banner and toast).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 23:37:56 -05:00
jaredandClaude Opus 5.5 3c9e5fd593 Faceplates: model labels as printed on the units
Lint / JS (eslint) (push) Successful in 16s
Lint / Python (flake8) (push) Successful in 1m6s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Lint / Python (flake8) (pull_request) Successful in 52s
Lint / JS (eslint) (pull_request) Successful in 7s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 53s
Security / Python Security (bandit) (push) Successful in 54s
Test / Python Tests (pytest) (push) Successful in 2m11s
Test / Python Tests (pytest) (pull_request) Successful in 2m16s
The label under each UniFi screen was one weight, left-aligned, and on the
UDM read "Dream Machine Pro". Checked against the NetBox reference photos
(and the UDM against the real unit), the labels are now centred under the
screen with the first word bold and the rest light: "UDM Pro",
"USW Pro" with its PoE mark, "USW", "NVR Pro", "USP PDU Pro". One helper
(fpModel) draws them all, so the NVR no longer looks bolder than the
switches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 23:36:23 -05:00
jaredandClaude Opus 5.5 565fdd81de Rack: NVR Pro as a real faceplate; Sliger and Rosewill touch-ups
Lint / JS (eslint) (push) Successful in 18s
Security / Python Security (bandit) (push) Successful in 1m45s
Lint / Python (flake8) (push) Successful in 1m25s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Lint / Python (flake8) (pull_request) Successful in 52s
Lint / JS (eslint) (pull_request) Successful in 6s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Test / Python Tests (pytest) (push) Successful in 1m18s
Security / Python Security (bandit) (pull_request) Successful in 56s
Test / Python Tests (pytest) (pull_request) Successful in 53s
The hand-drawn NVR Pro didn't match the UniFi faceplates: flat colours,
low contrast and its own port shapes. It is now a FACEPLATES entry
(UNVRPRO) built from the same parts as the UDM Pro: metal body, RJ45 jack,
SFP+ cage, quad screen, vents and labels. Front drawing "UniFi NVR Pro"
uses it, and it would go live if UniFi ever reports the model.

Sliger bays are numbered 1-10 (driveAtlas's order); the Rosewill door is
perforated with a latch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 23:31:19 -05:00
jaredandClaude Opus 5.5 0f4e129c20 Rack: drawn fronts for the NVR and server cases
Lint / JS (eslint) (push) Successful in 15s
Security / Python Security (bandit) (push) Successful in 1m5s
Lint / Python (flake8) (push) Successful in 49s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Lint / Python (flake8) (pull_request) Successful in 53s
Test / Python Tests (pytest) (push) Successful in 1m0s
Lint / JS (eslint) (pull_request) Successful in 7s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 53s
Test / Python Tests (pytest) (pull_request) Successful in 57s
Items Gandalf has no live faceplate for can pick a Front drawing: the
UniFi NVR Pro (drawn from the CC0 NetBox devicetype-library image), the
Sliger CX4712 4U and Rosewill RSV-L4500U 4U cases (from driveAtlas's
layouts), and generic 1U/2U/4U servers. The power light shows the linked
device's live status. They export with the rest of the rack.

New cutsheet_items column chassis, added on first use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 23:25:02 -05:00
jaredandClaude Opus 5.5 97e6692ebf Rack: put a patch panel's name above its ports
Lint / JS (eslint) (push) Successful in 8s
Lint / Python (flake8) (push) Successful in 51s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 54s
Test / Python Tests (pytest) (push) Successful in 1m21s
Lint / Python (flake8) (pull_request) Successful in 1m7s
Lint / JS (eslint) (pull_request) Successful in 13s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m18s
Test / Python Tests (pytest) (pull_request) Successful in 1m5s
A long panel name (UACC-Rack-Panel-Patch-Blank-24) was drawn in the left
ear at full size and ran under the ports. It is now a small caption in
the panel's top-left corner, shortened past 48 characters with the full
name in its tooltip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 23:02:18 -05:00
jaredandClaude Opus 5.5 c50e1f8322 Rack: front and rear mounting, rack settings in the page
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 49s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 51s
Lint / Python (flake8) (pull_request) Successful in 55s
Test / Python Tests (pytest) (push) Successful in 1m4s
Lint / JS (eslint) (pull_request) Successful in 7s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 52s
Test / Python Tests (pytest) (pull_request) Successful in 55s
Items are mounted from the front, the rear, or full depth. On a 4-post
rack a front and a rear item can share a unit; a full-depth one takes
both sides, and the rear view shows its back. Overlaps are checked per
side. Shelf contents take the shelf's side. A Front / Rear switch flips
the drawing.

Racks are saved in Gandalf (new cutsheet_racks table) and edited on the
rack page by admins: name, height, notes, add and delete. Renaming moves
the rack's items; saving the first rack carries over the items of the
config default it replaces. Config racks remain the fallback.

Faceplates are stretched to the full width only when the drawing is
close to the slot's proportions, so the PDU is not squashed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 22:35:37 -05:00
jaredandClaude Opus 5.5 bdd68a397d Rack: link items to devices, shelves, several racks, export; fix layout and light mode
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 48s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 49s
Lint / Python (flake8) (pull_request) Successful in 51s
Test / Python Tests (pytest) (push) Successful in 1m2s
Lint / JS (eslint) (pull_request) Successful in 7s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 50s
Test / Python Tests (pytest) (pull_request) Successful in 53s
Cutsheet items gain a "Gandalf device" link, so an item named after the
product (UDM-Pro) shows the live device's faceplate, ports and status.
Cable checks, suggestions, tickets and the device page treat an item and
its linked device as one. Without this, product-named items drew as bare
name plates.

Items can sit on a shelf instead of a rack unit; the shelf draws what is
on it with live status. Items have a rack, the page shows a tab per rack
(config "racks"), and a rack can be downloaded as SVG or PNG.

The rack is centred, long runs of empty units fold into one band (with a
toggle), every device fills the 19" width, the UPS shows its front only,
and the rack keeps its own palette so it reads in light mode.

New cutsheet_items columns rack, shelf and device are added on first use.
Deleting a shelf, or making it something else, is refused while it holds
items; renaming it carries over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 21:44:59 -05:00
jaredandClaude Opus 5.5 f655836446 Add a rack drawing (/rack)
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 50s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 50s
Test / Python Tests (pytest) (push) Successful in 1m2s
Lint / Python (flake8) (pull_request) Successful in 52s
Lint / JS (eslint) (pull_request) Successful in 7s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m42s
Test / Python Tests (pytest) (pull_request) Successful in 2m2s
The rack front is drawn from the rack units on cutsheet items. Switches
with a faceplate use it, with live port state; patch panels get a generic
panel with port labels; everything else is a labelled plate. Each
documented port shows its cutsheet state, and hovering a port lights up
every port on the same cable run (through patch panels) and names the
cables. Clicking opens the switch port or the cable.

Overlapping items and items past the top of the rack are listed instead
of drawn over another; where two claim the same top unit, the first one
recorded wins. rack.units sets the height (default 42).

/api/rack builds on the cutsheet view; cutsheet.rack_layout and
cutsheet.port_marks hold the placement and per-port logic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 20:59:30 -05:00
jaredandClaude Opus 5.5 c262e5ecc8 Inventory links, cutsheet marks on faceplates, and single page titles
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 54s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m1s
Lint / JS (eslint) (pull_request) Successful in 19s
Test / Python Tests (pytest) (push) Successful in 2m6s
Security / Python Security (bandit) (pull_request) Successful in 1m18s
Lint / Python (flake8) (pull_request) Successful in 53s
Test / Python Tests (pytest) (pull_request) Successful in 1m1s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Device pages get an Inventory button: it opens the device's HomeBox record
once an admin pastes its URL (stored in the new device_links table; only
URLs on inventory.base_url are accepted), and searches the inventory for
the model until then. Tickets link the record too.

Faceplate ports with a documented cable get a corner dot: green matches,
red mismatch, grey documented but unconfirmed. The port's tooltip names
the cable, and a legend toggle (remembered per browser) hides them.

Page titles no longer repeat GANDALF ("Alert History - GANDALF - GANDALF").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 20:47:14 -05:00
jaredandClaude Opus 5.5 9d561db703 Add an alert history page and tie /health staleness to the poll interval
Lint / JS (eslint) (push) Successful in 6s
Lint / Python (flake8) (push) Successful in 1m56s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 2m3s
Test / Python Tests (pytest) (push) Successful in 1m0s
Lint / Python (flake8) (pull_request) Successful in 48s
Lint / JS (eslint) (pull_request) Successful in 6s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 2m7s
Test / Python Tests (pytest) (pull_request) Successful in 2m6s
/events lists every alert from the last 24 hours to 90 days, open or
cleared, with how long each lasted, failed checks and its ticket. Filters
(device, type, severity, state, range) live in the URL, so device pages
and tickets link straight to a device's history. A "Recurring" table
lists alerts that fired more than once on the same target. On phones the
rows become cards.

/api/events/history backs it (100 per page). The existing /api/events is
unchanged.

/health and the dashboard's monitor indicator now call the monitor
stalled after five missed polls (at least 10 minutes) instead of a fixed
20, and /health reports the monitor's age.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 20:29:09 -05:00
jaredandClaude Opus 5.5 86aa29b378 Cutsheet trace mode and mismatch alerts
Lint / Python (flake8) (push) Successful in 1m8s
Security / Python Security (bandit) (push) Successful in 56s
Test / Python Tests (pytest) (push) Successful in 43s
Lint / Python (flake8) (pull_request) Successful in 50s
Lint / JS (eslint) (pull_request) Successful in 30s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 55s
Test / Python Tests (pytest) (pull_request) Successful in 50s
Lint / JS (eslint) (push) Successful in 16m17s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Trace a cord: pick a panel port, press Start, and unplug the cord at the
panel. The page polls a new admin-only endpoint that reads every switch
port's state from UniFi (cached 2 s), shows the one port that dropped
with a button to record the cable, and confirms when it's plugged back
in. Several ports dropping at once, or none within 3 minutes, is
explained rather than guessed at.

Mismatch alerts: the monitor runs the cutsheet check every poll. Each
mismatched installed run (wrong neighbor, no link, missing port, port
conflict) keeps one cable_mismatch event on its switch, resolved when
fixed, and opens a P4 ticket after cutsheet.mismatch_ticket_hours
(default 24, 0 never). Suppressing the switch or port stops the ticket.

Also fixes buttons with the hidden attribute still showing, which left a
Delete button on the new-cable form.

Closes #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 16:54:57 -05:00
jaredandClaude Opus 5.5 52cfea5439 Cutsheet: show the next free cable ID as a hint, not a placeholder
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 48s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Lint / Python (flake8) (pull_request) Successful in 55s
Lint / JS (eslint) (pull_request) Successful in 14s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m45s
Security / Python Security (bandit) (push) Successful in 1m22s
Test / Python Tests (pytest) (pull_request) Successful in 1m6s
Test / Python Tests (pytest) (push) Successful in 1m0s
A placeholder of C01 in the empty ID field looked like a real value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 16:10:51 -05:00
jaredandClaude Opus 5.5 b9412ff6ba Add cutsheets: planned cabling checked against discovery
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 1m36s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m40s
Lint / Python (flake8) (pull_request) Successful in 52s
Test / Python Tests (pytest) (push) Successful in 1m4s
Lint / JS (eslint) (pull_request) Successful in 7s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m13s
Test / Python Tests (pytest) (pull_request) Successful in 1m15s
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>
2026-11-10 16:05:08 -05:00
jaredandClaude Opus 5.5 7e588e5b02 Keep every switch port and show SFP modules
Security / Python Security (bandit) (push) Successful in 1m50s
Test / Python Tests (pytest) (push) Successful in 1m52s
Lint / Python (flake8) (pull_request) Successful in 1m20s
Lint / Python (flake8) (push) Successful in 1m16s
Lint / JS (eslint) (pull_request) Successful in 10s
Lint / JS (eslint) (push) Successful in 15s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m5s
Test / Python Tests (pytest) (pull_request) Successful in 1m7s
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 #7
Closes #8

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:37:08 -05:00
jaredandClaude Opus 5.5 051e70b82b Draw realistic front panels for every device model
Lint / JS (eslint) (push) Successful in 10s
Lint / Python (flake8) (push) Successful in 57s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m7s
Lint / Python (flake8) (pull_request) Successful in 59s
Test / Python Tests (pytest) (push) Successful in 1m22s
Lint / JS (eslint) (pull_request) Successful in 11s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m7s
Test / Python Tests (pytest) (pull_request) Successful in 1m28s
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>
2026-11-05 11:11:48 -05:00
jaredandClaude Opus 5.5 bdcfb76c25 Move the port inspector onto device pages
Lint / JS (eslint) (push) Successful in 1m14s
Lint / Python (flake8) (push) Successful in 2m12s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m3s
Test / Python Tests (pytest) (push) Successful in 1m11s
Lint / Python (flake8) (pull_request) Successful in 1m1s
Lint / JS (eslint) (pull_request) Successful in 10s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 56s
Test / Python Tests (pytest) (pull_request) Successful in 1m5s
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>
2026-11-05 10:11:11 -05:00
jaredandClaude Opus 5.5 e32499b7f6 Add AT&T modem monitoring and device pages; replace Link Debug
Lint / Python (flake8) (pull_request) Successful in 2m6s
Lint / JS (eslint) (pull_request) Successful in 18s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 2m11s
Test / Python Tests (pytest) (pull_request) Successful in 2m20s
Lint / JS (eslint) (push) Successful in 10s
Lint / Python (flake8) (push) Successful in 1m58s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m58s
Lint / Deploy (push) Successful in 4s
Test / Python Tests (pytest) (push) Successful in 58s
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>
2026-11-04 23:40:17 -05:00
jaredandClaude Sonnet 4.6 33b214eac0 fix: escape ticket_id text content in dynamic events table
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
ticket_id was already escaped in the href attribute but the visible
text (#<id>) used the raw value in an innerHTML template literal.
Apply lt.escHtml() for defense-in-depth against a compromised ticket API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 23:02:09 -04:00
jaredandClaude Sonnet 4.6 b3495caac8 fix: aria-required sync, aria-label pills, deduplicate setDuration logic
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- updateSuppressForm() now sets required + aria-required on sup-name/sup-detail
  when target type changes; sup-reason gets static aria-required="true"
- onTypeChange() in suppressions page syncs aria-required on s-name
- s-name in suppressions.html gets initial required/aria-required (default type=host)
- Duration pills in both modal and suppressions page now have descriptive
  aria-label ("30 minutes", "1 hour", etc.) alongside the group aria-label
- setDuration() in app.js accepts optional {expiresId,pillSel,hintId} opts so
  logic lives in one place; suppressions.html setDur() delegates to it
- Post-create form reset uses setDur() instead of manually patching DOM

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 12:58:32 -04:00
jaredandClaude Sonnet 4.6 ac7f406175 fix: inspector stale data warning, remove dead supported_modes code
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- inspector.html: show orange '⚠ Stale: HH:MM' with tooltip when link_stats data is >15 min old (previously just showed the time with no visual warning)
- style.css: add .g-stale-warn helper class (orange, bold) for the stale indicator
- diagnose.py: remove supported_modes accumulation from parse_ethtool() — field was collected but never consumed by analyze() or displayed anywhere

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 12:05:08 -04:00
jaredandClaude Sonnet 4.6 68f69e1539 refactor: extract _annotate_suppressions helper, remove orphaned CSS
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- Extract identical suppression-annotation loop from index() and
  api_status() into _annotate_suppressions() helper to eliminate DRY
  violation
- Improve stuck-job error message: 'thread crash' → 'no activity for
  5 minutes' (less alarming, more accurate)
- Remove orphaned .events-filter-bar CSS class (never referenced in
  any template or JS file)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:39:52 -04:00
jaredandClaude Sonnet 4.6 872dbf3b48 a11y: aria-pressed for all pill groups, aria-label on search inputs and buttons
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- Add role="group" + aria-label to duration-pills and sev-pills containers
- Add aria-pressed to severity filter, duration, and refresh-interval pills
- Keep aria-pressed in sync with JS (setDuration, applyRefreshPillUI, modal reset)
- Add aria-label to events-search, host-search, links-search inputs
- Add aria-label to host and UniFi device suppress buttons in templates
- Replace dynamic style color strings in links.html stat cards with TDS
  utility classes (lt-text-red/green/amber) via downCls/errCls variables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:34:16 -04:00
jaredandClaude Sonnet 4.6 5ee7154c45 CSS: migrate links.html static inline styles to classes
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- lt-divider--unifi / lt-divider-label--unifi: replace hardcoded margin
  and cyan label color on the UniFi switch section divider
- lt-text-amber / lt-text-cyan on stat card icons and values (matches
  same migration done in index.html)
- lt-stats-grid--mb: margin-bottom:16px on the summary stats grid
- g-page-sub-aside: replaces margin-left:8px on the updated timestamp
  span in links and inspector page subtitle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:19:32 -04:00
jaredandClaude Sonnet 4.6 d63f73e90f CSS: extract notification panel inline styles to classes
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- lt-notif-empty: replaces all hardcoded padding/font/color/align on
  the empty-state and loading/error text in the notification bell panel
- lt-notif-view-all: replaces width/text-align/display/font-size inline
  style on the 'View dashboard' footer link
- lt-notif-dot: moves border-radius:50%;margin-top from inline style
  (only background color remains inline, which is dynamic per-severity)
- Initial 'Loading…' text in the panel HTML uses lt-notif-empty

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:18:33 -04:00
jaredandClaude Sonnet 4.6 bc71cc829b CSS: replace remaining inline color/size styles with TDS utilities
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- Stat card icons and values: style="color:var(--red)" etc replaced with
  lt-text-red, lt-text-amber, lt-text-cyan, lt-text-green (defined in
  base.css with both color and glow-shadow)
- Host search input: style="width:180px" extracted to .lt-search-input--sm
- base.html: suppress modal form groups use lt-form-group--last for last
  item (already committed); lt-divider--compact applied to settings divider

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:17:22 -04:00
jaredandClaude Sonnet 4.6 c3f0fcbefb CSS: remove remaining fixable inline styles across templates
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- Suppress modal form groups: strip margin-bottom:12px (lt-form-group
  already has margin-bottom in TDS); use lt-form-group--last for the
  final group where zero margin is needed
- Keyboard shortcuts table: remove width:100% (lt-table is already full-
  width in base.css)
- Settings divider: replace style=margin override with .lt-divider--compact
- Topology bus section: move max-width:860px into .topo-bus-section rule

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:15:15 -04:00
jaredandClaude Sonnet 4.6 d8a4474814 CSS: convert all topology inline styles to modifier classes
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
Replace style= attributes on topology wire/node elements with semantic
modifier classes:
- topo-vc-wire--wan, --10g, --mgmt (wire colour semantics in CSS)
- topo-v2-host--bus (bus-section node size constraint)
- topo-legend-item--offrack already done in prior commit

Zero inline styles remain in the topology section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:13:24 -04:00
jaredandClaude Sonnet 4.6 9e0f8555cc Feature: show suppression status on active alert rows
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
Active events now carry an is_suppressed boolean (added in api_status()
and the index() route via check_suppressed() against the pre-loaded
suppression list). The events table renders a muted '🔕 sup' badge next
to the severity and dims the entire row (.row-suppressed) so operators
can immediately see which firing alerts are silenced.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:11:15 -04:00
jaredandClaude Sonnet 4.6 f25a377968 Cleanup: strip redundant inline styles, add CSS classes
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- Remove style="margin-top:4px" from .g-page-sub in all three secondary
  pages (the value is already defined in .g-page-sub rule in style.css)
- suppressions.html: replace inline style="padding:12px 14px" with TDS
  lt-section-body class
- index.html topology legend: replace inline dashed-border style with
  .topo-legend-item--offrack modifier class in style.css

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:09:25 -04:00
jaredandClaude Sonnet 4.6 7abb76afaa Redesign: alerts above fold, fix nav, globalise suppress modal
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- Dashboard: reorder sections so Active Alerts appear immediately below
  stat cards rather than after 200+ lines of topology — primary content
  is now above the fold on all screen sizes
- Network Hosts section gains a collapse toggle (persisted to localStorage)
  so the topology diagram can be hidden when not needed
- Admin nav corrected: admin now gets a direct Suppressions link;
  non-admin no longer sees the page at all (it was always admin-only)
- Suppress modal moved from index.html into base.html so [S] keyboard
  shortcut works on every page, not just the dashboard; form listeners
  wired in app.js accordingly
- Settings modal KV grid: replaced lt-kv-row/lt-kv-label/lt-kv-value
  (light-mode only) with lt-kv-key/lt-kv-val (dark-mode defined)
- style.css: add lt-btn-secondary dark-mode definition, lt-cmd-hint-btn
  class for ⌘K button, topology collapse styles; remove dead .g-page-header,
  .g-page-title, .empty-state classes; strip redundant inline styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:07:53 -04:00
jaredandClaude Sonnet 4.6 6e3a579956 Cleanup: fix eslint warnings, button loading state, inspector footer hint
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- app.js: replace != with !== (eqeqeq rule) on resolved_24h null check
  and totalActive pagination check
- style.css: extend loading state to all .lt-btn.is-loading (not just
  refresh button), so suppressions form submit shows disabled feedback;
  remove dead .link-collapse-bar rule
- base.html: add inspector page to footer R=REFRESH hint; update
  keyboard shortcut table to include Inspector in refresh shortcut doc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 22:42:40 -04:00
jaredandClaude Sonnet 4.6 332f041e05 Dynamic resolved count, host search filter, lt-divider for UniFi section
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- db.py: add resolved_24h to get_status_summary() so each /api/status
  poll carries the fresh 24h resolved count
- app.js: wire stat-resolved-val to update from summary.resolved_24h
  so the Resolved 24h card stays accurate after auto-refresh
- index.html: add lt-toolbar/lt-search above host grid for quick
  client-side host filtering by name
- links.html: replace custom unifi-section-header div with lt-divider
- style.css: remove unused .unifi-section-header rules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 18:36:57 -04:00
jaredandClaude Sonnet 4.6 ddc258a453 TDS polish: lt-frame tables, links search toolbar, dead CSS cleanup
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- index.html: wrap UniFi devices table in lt-frame with section header
- links.html: add static lt-toolbar with lt-search filter and collapse
  controls above the dynamic container; remove collapse bar from
  renderLinks() since it's now static; add applyLinksSearch() to
  filter host/switch panels by name as user types
- suppressions.html: wrap Available Targets section in lt-frame
- style.css: remove unused .link-summary-panel and related rules
  (replaced by lt-stats-grid in previous commit)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 17:39:11 -04:00
jaredandClaude Sonnet 4.6 9d15d4b802 Add stat cards, lt-frame alert queue, and timeline for resolved alerts
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- Four lt-stat-card widgets (Critical, Warning, Hosts, Resolved 24h)
  below the status bar; Critical card pulses red when count > 0
- Clicking Critical or Warning card filters the events table by severity
- Events table wrapped in lt-frame with ASCII corner ornaments and
  lt-section-header; filter bar moved to lt-toolbar with lt-search icon
- Recently Resolved table replaced with lt-timeline component
- updateStatusBar() and updateHostGrid() keep stat card values live

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 22:19:50 -04:00
jaredandClaude Sonnet 4.6 6f9787b74a Add notification bell, settings modal, and context-sensitive footer
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- Notification bell in header polls /api/status and shows active alerts
  with severity-colored dots; badge counts unread items via localStorage
- Settings modal ([ * ] CFG) controls auto-refresh interval (15s/30s/1m/5m/off)
  persisted to localStorage and wired into lt.autoRefresh on all pages
- Context-sensitive footer hints: Dashboard shows REFRESH + SUPPRESS,
  Link Debug shows REFRESH, all pages show CFG + HELP
- Added S key (quick suppress) and * key (settings) shortcuts
- ⌘K affordance button added to header-right
- R key now uses lt.autoRefresh.now() so it works on any page
- refreshAll() pushes fresh events to notification bell on each poll

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 21:33:02 -04:00
jaredandClaude Sonnet 4.6 032d0db81e Add LDAP avatar photos, UX polish, and TDS component upgrades
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
- Add /api/avatar endpoint querying lldap for user jpegPhoto; disk cache
  with sentinel pattern avoids repeat LDAP hits for users without photos
- Add ldap3 dependency and ldap config block to config.json
- Wire lt-avatar img overlay in base.html with capture-phase error
  fallback (lt-avatar-img-err) to reveal initials when image is absent
- Fix lt-avatar CSS shim: position:relative + absolute inset on img
  (local base.css was missing these; added to style.css)
- Replace all empty-state paragraphs with proper lt-empty-state markup
  (icon + title + body) across index, suppressions, inspector, app.js
- Add lt-spinner--cyan next to refresh button; shows during refreshAll()
- Replace inspector panel-section-title with lt-divider throughout
- Add data-tooltip attributes to SFP DOM metrics, TX/RX/Carrier/Duplex/
  Auto-neg/Error labels in links.html and inspector panel
- Add tooltips to events table column headers (Sev, First Seen, Failures)
- Fix links.html host panel timestamp (was reading sample.updated which
  is always undefined; now uses data.updated)
- Fix UniFi status text casing (Online→ONLINE to match server render)
- Remove dead topo-status-* class manipulation from updateTopology()
- Always render alert-count-badge; toggle display:none when count is 0
- Fix double UniFi get_devices() call in monitor.py run loop
- Fix chip-critical animation (was using green pulse-glow; now red)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 21:09:56 -04:00
jaredandClaude Sonnet 4.6 5fdb77d664 Integrate test code improvements using web_template components
Lint / Python (flake8) (push) Skipped
Lint / JS (eslint) (push) Skipped
lt-alert:
- Replace custom .stale-banner with lt-alert lt-alert--warning in app.js
  and links.html; remove stale-banner CSS, reuse lt-alert margin rule

lt-progress:
- Replace custom .traffic-bar-track/.traffic-bar-fill in links.html with
  lt-progress from base.css; TX uses default (orange), RX uses --cyan,
  both flip to --red when utilisation >85% (trafficBarClass helper)
- Keep traffic layout classes (.traffic-section/.traffic-row etc.) for structure

Suppression type badges:
- Map target_type to distinct badge colors: host→badge-warning (orange),
  interface→badge-info (cyan), unifi_device→badge-purple (new alias using
  --accent-purple from base.css), all→badge-critical (red)
- Applied in both server-rendered table (Jinja2 dict lookup) and
  renderActiveRows() JS

Topology animated down-wire:
- Add data-host attribute to .topo-v2-wire-10g/.topo-v2-wire-1g elements
- updateTopology() toggles .wire-down class on the 10G drop-wire when
  host.status === 'down'
- .wire-down CSS: animated repeating-linear-gradient dashed red line
  via wire-dash-anim @keyframes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 23:37:47 -04:00
jaredandClaude Sonnet 4.6 3a9cc472e1 Audit quick wins: null guard, API error toasts, aria-labels on suppress buttons
- app.js: guard events array with || [] before .filter() to prevent crash on null
- app.js: show warning toast when /api/network or /api/status fail (was silent)
- app.js: add aria-label to all dynamically-generated suppress buttons
- index.html: add aria-label to server-rendered suppress buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 17:50:00 -04:00
jaredandClaude Sonnet 4.6 1767c12f1b Fix field name mismatches, add events filter, in-place suppression refresh
- links.html: fix all field name bugs (auto_negotiation→autoneg, full_duplex,
  tx/rx_errors/drops_per_sec→_rate, tx/rx_bytes_per_sec→_rate, poe_total_w/poe_max_w
  computed from ports, renderUnifiSwitches uses top-level updated timestamp)
- suppressions.html: in-place DOM refresh after create/remove (no page reload),
  datalist autocomplete for target names, form reset after submit
- inspector.html: ESC key closes detail panel via lt.keys.on
- index.html: events filter bar with search input + severity pills (All/Critical/Warning),
  MutationObserver re-applies filter after dynamic updates
- style.css: g-section-actions, events-filter-bar, sev-pills layout
- app.js/db.py/monitor.py: carry forward prior session fixes (Promise.allSettled,
  daemon_ok, stale connection handling, double Prometheus call, self.cfg fix)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 23:35:02 -04:00