Commit Graph
247 Commits
Author SHA1 Message Date
jared 390748173c Merge pull request 'Docs: micro1 is now monitor-01' (#38) from rename/micro1-to-monitor-01 into main
Lint / Python (flake8) (push) Successful in 1m27s
Lint / JS (eslint) (push) Successful in 18s
Lint / Notify on failure (push) Skipped
Test / Python Tests (pytest) (push) Successful in 1m27s
Security / Python Security (bandit) (push) Successful in 1m7s
Lint / Deploy (push) Successful in 5s
deploy-2026.11.14-591
2026-11-14 17:55:49 -05:00
jaredandClaude Opus 5.5 64eba37f81 Docs: micro1 is now monitor-01
Lint / Python (flake8) (push) Successful in 2m1s
Security / Python Security (bandit) (push) Successful in 1m20s
Test / Python Tests (pytest) (push) Successful in 1m32s
Lint / JS (eslint) (pull_request) Successful in 21s
Lint / Python (flake8) (pull_request) Successful in 2m26s
Lint / Notify on failure (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 2m19s
Lint / Deploy (pull_request) Skipped
Lint / JS (eslint) (push) Successful in 7s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Test / Python Tests (pytest) (pull_request) Successful in 1m28s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 17:46:56 -05:00
jared 0631de74c7 Merge pull request 'Dashboard: flag stale alerts and tickets left open' (#37) from stale-alerts into main
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 49s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 58s
Lint / Deploy (push) Successful in 17s
Test / Python Tests (pytest) (push) Successful in 1m42s
deploy-2026.11.14-582
2026-11-14 16:16:29 -05:00
jaredandClaude Opus 5.5 0921e82c54 Dashboard: flag stale alerts and tickets left open
Lint / JS (eslint) (push) Successful in 19s
Lint / Python (flake8) (push) Successful in 1m7s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 52s
Test / Python Tests (pytest) (push) Successful in 44s
Lint / JS (eslint) (pull_request) Successful in 7s
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 52s
Test / Python Tests (pytest) (pull_request) Successful in 1m35s
Gandalf only comments on tickets, so a ticket can stay open long after its
alert cleared (pbs's ticket sat open for a month), and an alert can stay
active for days once nobody is looking.

- The monitor reads Tinker's open ticket list (tickets_api.php, read scope)
  every 15 minutes and saves it. A failed read keeps the previous list.
- Active alerts get a stale badge after monitor.stale_alert_days (3), and a
  "ticket closed" badge when their ticket was closed while still active.
- A Needs Follow-up panel lists open tickets whose every alert cleared at
  least monitor.stale_ticket_hours (24) ago.
- A "TO FOLLOW UP" status chip counts both. If the open ticket list is over
  2 hours old, the ticket flags are left off rather than guessed.
- Replaced an &mdash; in the alert count notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 15:57:58 -05:00
jared 7b80633088 Merge pull request 'WAN tickets: one per incident, notice UniFi updates, maintenance button' (#36) from wan-incidents into main
Lint / JS (eslint) (push) Successful in 14s
Lint / Python (flake8) (push) Successful in 1m37s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Successful in 16s
Security / Python Security (bandit) (push) Successful in 1m47s
Test / Python Tests (pytest) (push) Successful in 44s
deploy-2026.11.12-573
2026-11-12 13:46:31 -05:00
jaredandClaude Opus 5.5 379e3be365 WAN tickets: one per incident, notice UniFi updates, maintenance button
Lint / JS (eslint) (push) Successful in 42s
Lint / Python (flake8) (push) Successful in 1m35s
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 1m5s
Lint / Python (flake8) (pull_request) Successful in 58s
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 1m1s
Test / Python Tests (pytest) (pull_request) Successful in 1m10s
A UniFi Network update on 2026-11-12 dropped the UDM's WAN, Smart Power
power-cycled the modem, and Gandalf opened two tickets for the one event.

- Modem and WAN reports from one check cycle open a single ticket, led by
  the most urgent, with the rest listed under "Also seen in this incident".
  A report while the modem has an open alert, or one that ended under 30
  minutes ago, becomes a comment on that ticket instead.
- UniFi update log entries (NETWORK_UPDATED, FIRMWARE_UPDATED_V2 and the
  like) add a note to modem/WAN tickets from 20 minutes before the update
  to 15 after, and hold new modem/WAN tickets for 15 minutes.
- A Maintenance button for admins starts a global suppression for 30 min
  to 4 hours, with a banner on every page and End now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 12:53:22 -05:00
jared 6b03fe7b44 Merge pull request 'Mark host interfaces as unused' (#35) from unused-interfaces into main
Lint / JS (eslint) (push) Successful in 8s
Lint / Python (flake8) (push) Successful in 54s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 52s
Lint / Deploy (push) Successful in 4s
Test / Python Tests (pytest) (push) Successful in 1m15s
deploy-2026.11.11-564
2026-11-11 17:50:14 -05:00
jaredandClaude Opus 5.5 3834546f38 Mark host interfaces as unused
Lint / JS (eslint) (push) Successful in 44s
Lint / Python (flake8) (push) Successful in 1m40s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m18s
Lint / JS (eslint) (pull_request) Successful in 10s
Security / Python Security (bandit) (pull_request) Successful in 1m13s
Test / Python Tests (pytest) (pull_request) Successful in 1m11s
Lint / Python (flake8) (pull_request) Successful in 50s
Test / Python Tests (pytest) (push) Successful in 1m1s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Ports that are never meant to be up (compute-storage-01 nic1-3, large1's
spare NIC and USB adapter) kept their hosts "degraded" and were the source
of tonight's false alerts. An admin can now mark an interface unused from
the device page, with an optional reason. Unused interfaces raise no
link-down alerts, don't count toward the cluster-wide alert or the host's
status, and show greyed as "unused" on the dashboard, device page and rack.
Marking one closes any open link-down alert on it without a recovery note.

New table unused_interfaces, created with the cutsheet tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:19:45 -05:00
jared aeb7a89e81 Merge pull request 'Rack: Flex Mini on the NVR, coloured Sliger fronts, UniFi brush panel' (#32) from loose-gear-on-top into main
Lint / Python (flake8) (push) Successful in 47s
Lint / JS (eslint) (push) Successful in 7s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 2m26s
Test / Python Tests (pytest) (push) Successful in 2m44s
Lint / Deploy (push) Successful in 7s
deploy-2026.11.11-555
2026-11-11 02:09:38 -05:00
jared 9bdf5d9eed Merge pull request 'Tickets: don't adopt a Closed ticket Tinker couldn't reopen' (#34) from tinker-create-action into main
Lint / Python (flake8) (push) Successful in 47s
Lint / JS (eslint) (push) Successful in 9s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m39s
Test / Python Tests (pytest) (push) Successful in 1m49s
Lint / Deploy (push) Successful in 24s
deploy-2026.11.11-552
2026-11-11 02:01:10 -05:00
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 d234e36e3f Tickets: don't adopt a Closed ticket Tinker couldn't reopen
Lint / JS (eslint) (push) Successful in 14s
Lint / Python (flake8) (push) Successful in 57s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 55s
Lint / Python (flake8) (pull_request) Successful in 1m31s
Test / Python Tests (pytest) (push) Successful in 1m51s
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 1m22s
Test / Python Tests (pytest) (pull_request) Successful in 1m46s
Tinker's create response says what it did (action). For recurrence_noted it
matched a Closed ticket it could not reopen unattended: that ticket isn't
tracking the alert (this is what hid micro1 enp3s0 behind an old reboot
ticket), so Gandalf now logs it and keeps no ticket for the alert. Each such
create adds a "recurred" comment to the closed ticket, so the same title is
retried once a day rather than every poll. Other actions are logged.

See tinker_tickets#116 and #115.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 01:52:53 -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
jared 64ef4222d0 Merge remote-tracking branch 'origin/main' into loose-gear-on-top 2026-11-11 01:48:37 -05:00
jared fde1d01f27 Merge pull request 'Revert #31: revived glitch alerts opened tickets' (#33) from revert-31 into main
Lint / Python (flake8) (push) Successful in 57s
Lint / JS (eslint) (push) Successful in 7s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m50s
Test / Python Tests (pytest) (push) Successful in 1m59s
Lint / Deploy (push) Successful in 7s
deploy-2026.11.11-519
2026-11-11 01:17:47 -05:00
jaredandClaude Opus 5.5 8edaa78ad7 Revert #31: open link-down alerts from a monitor glitch were revived and ticketed
Lint / JS (eslint) (push) Successful in 12s
Lint / Python (flake8) (push) Successful in 1m47s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m15s
Lint / JS (eslint) (pull_request) Successful in 7s
Lint / Python (flake8) (pull_request) Successful in 48s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Test / Python Tests (pytest) (push) Successful in 1m41s
Security / Python Security (bandit) (pull_request) Successful in 1m32s
Test / Python Tests (pytest) (pull_request) Successful in 1m6s
#31 treated any open interface_down alert as previously up. Production had
six such alerts all created in the same second on 2026-03-02 (a monitor
glitch, on ports that were never in use: large1 enp36s0 and a USB adapter,
compute-storage-01 nic1-3, micro1 enp3s0). Reviving them opened per-host
tickets and a P1 cluster-wide ticket. Back to the previous behaviour while a
safer fix is worked out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 01:11:00 -05:00
jaredandClaude Opus 5.5 3bc8df6dc7 Tests: wrap a long line
Lint / JS (eslint) (push) Successful in 25s
Lint / Python (flake8) (push) Successful in 1m53s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m28s
Lint / Python (flake8) (pull_request) Successful in 58s
Test / Python Tests (pytest) (push) Successful in 1m9s
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 1m1s
Test / Python Tests (pytest) (pull_request) Successful in 1m6s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 01:08:52 -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 a0e062493d Merge pull request 'Monitor: keep checking interfaces whose link-down alert is still open' (#31) from stale-interface-alerts into main
Lint / JS (eslint) (push) Successful in 21s
Lint / Python (flake8) (push) Successful in 1m44s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m25s
Lint / Deploy (push) Successful in 5s
Test / Python Tests (pytest) (push) Successful in 1m2s
deploy-2026.11.11-501
2026-11-11 01:06:37 -05:00
jaredandClaude Opus 5.5 d07787ee38 Monitor: keep checking interfaces whose link-down alert is still open
Lint / JS (eslint) (push) Successful in 8s
Lint / Python (flake8) (push) Successful in 49s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 53s
Test / Python Tests (pytest) (push) Successful in 1m3s
Lint / Python (flake8) (pull_request) Successful in 55s
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 1m22s
Test / Python Tests (pytest) (pull_request) Successful in 1m27s
After the interface baseline is reset, a port that is already down reads as
"first seen down", which neither alerts nor resolves. An alert opened before
the reset was then stuck open without updates: micro1's enp3s0 had sat at
one failed check since March with no ticket. An interface with an open
interface_down alert is now treated as previously up, so the alert keeps
counting, tickets and resolves as normal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 01:02:53 -05:00
jared 7e75ba8b3c Merge pull request 'Rack: posts vs facing, three views, ZimaBoard 832 shelves to scale' (#30) from zimaboard-and-facing into main
Lint / JS (eslint) (push) Successful in 8s
Lint / Python (flake8) (push) Successful in 1m31s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m47s
Lint / Deploy (push) Successful in 8s
Test / Python Tests (pytest) (push) Successful in 49s
deploy-2026.11.11-492
2026-11-11 00:53:41 -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
jared 90e051dc22 Merge pull request 'Faceplates: USW Pro Max 24 PoE and 48 PoE' (#29) from pro-max-faceplates into main
Lint / JS (eslint) (push) Successful in 17s
Lint / Python (flake8) (push) Successful in 1m28s
Security / Python Security (bandit) (push) Successful in 1m5s
Lint / Notify on failure (push) Skipped
Test / Python Tests (pytest) (push) Successful in 1m8s
Lint / Deploy (push) Successful in 5s
deploy-2026.11.11-483
2026-11-11 00:45:49 -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
jared 6530433d9c Merge pull request 'Rack: clicking a port on an unlinked faceplate opens the cutsheet' (#28) from rack-unlinked-port-clicks into main
Lint / JS (eslint) (push) Successful in 8s
Lint / Python (flake8) (push) Successful in 49s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 55s
Lint / Deploy (push) Successful in 6s
Test / Python Tests (pytest) (push) Successful in 1m20s
deploy-2026.11.11-462
2026-11-10 23:54:51 -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
jared 3e2918526f Merge pull request 'Rack: drawn fronts (NVR Pro, UACC panel, server cases), faceplate labels, no em/en dashes' (#27) from rack-drawn-fronts into main
Lint / Python (flake8) (push) Successful in 50s
Lint / JS (eslint) (push) Successful in 10s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 2m0s
Test / Python Tests (pytest) (push) Successful in 2m4s
Lint / Deploy (push) Successful in 8s
deploy-2026.11.11-453
2026-11-10 23:45:36 -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
jared b416239438 Merge pull request 'Rack: put a patch panel's name above its ports' (#26) from rack-panel-label into main
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 55s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m10s
Lint / Deploy (push) Successful in 7s
Test / Python Tests (pytest) (push) Successful in 51s
deploy-2026.11.11-420
2026-11-10 23:20:22 -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
jared bee260f1cd Merge pull request 'Rack: front and rear mounting, rack settings on the page' (#25) from rack-rear-and-settings into main
Lint / JS (eslint) (push) Successful in 8s
Lint / Python (flake8) (push) Successful in 54s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 52s
Lint / Deploy (push) Successful in 4s
Test / Python Tests (pytest) (push) Successful in 1m44s
deploy-2026.11.11-411
2026-11-10 22:52:15 -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
jared cd3d2f05ee Merge pull request 'Rack: device links, shelves, multiple racks, export; layout and light-mode fixes' (#24) from racks-shelves-export into main
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 54s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 55s
Lint / Deploy (push) Successful in 4s
Test / Python Tests (pytest) (push) Successful in 1m5s
deploy-2026.11.11-402
2026-11-10 22:29:38 -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
jared 4f353888f8 Merge pull request 'Rack drawing (/rack)' (#23) from rack-view into main
Lint / JS (eslint) (push) Successful in 6s
Lint / Python (flake8) (push) Successful in 1m14s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m41s
Lint / Deploy (push) Successful in 6s
Test / Python Tests (pytest) (push) Successful in 47s
deploy-2026.11.11-393
2026-11-10 21:33:29 -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
jared ee2708dccb Merge pull request 'Inventory links, cutsheet marks on faceplates, single page titles' (#22) from inventory-and-faceplate-marks into main
Lint / JS (eslint) (push) Successful in 13s
Lint / Python (flake8) (push) Successful in 1m45s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m32s
Lint / Deploy (push) Successful in 4s
Test / Python Tests (pytest) (push) Successful in 46s
deploy-2026.11.11-384
2026-11-10 20:50:26 -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
jared 29119c2235 Merge pull request 'Alert history page and a tighter monitor health check' (#21) from events-history into main
Lint / JS (eslint) (push) Successful in 9s
Lint / Python (flake8) (push) Successful in 1m19s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 55s
Lint / Deploy (push) Successful in 4s
Test / Python Tests (pytest) (push) Successful in 48s
deploy-2026.11.11-375
2026-11-10 20:37:45 -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
jared 9e2a2b987c Merge pull request 'Recovery notes and fuller context on tickets' (#20) from ticket-recovery-and-context into main
Lint / JS (eslint) (push) Successful in 30s
Lint / Python (flake8) (push) Successful in 1m17s
Lint / Notify on failure (push) Skipped
Security / Python Security (bandit) (push) Successful in 58s
Lint / Deploy (push) Successful in 4s
Test / Python Tests (pytest) (push) Successful in 1m18s
deploy-2026.11.11-366
2026-11-10 20:09:03 -05:00
jaredandClaude Opus 5.5 437f7a415f Add recovery notes and fuller context to tickets
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 1m26s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m30s
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 1m28s
Test / Python Tests (pytest) (pull_request) Successful in 2m0s
When an alert that opened a ticket clears, the monitor now comments on the
ticket (when it cleared, how long it lasted, failed checks, a link back).
It never closes or changes a ticket's status. Failed notes retry each
cycle for 24 hours; a 401/403 from Tinker stops them until restart.

Tickets are now Markdown with a "What to check" list and, where known:
the switch port the device plugs into (kept from the last time it was
seen, so a dead host still shows it), cutsheet cables and their path,
how often the alert fired in the last 7/30 days, other open alerts on the
device, and links into Gandalf (gandalf.public_url). Titles are unchanged
so Tinker's duplicate detection still works.

network_events gains recovery_notified, added at monitor startup; rows
that cleared before the upgrade count as already notified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 19:44:50 -05:00