Topology: power map, what each outlet feeds #40

Open
jared wants to merge 1 commits from power-map into main
Owner

This is part 2 of the topology page (part 1 was #39). It shows what each PDU, UPS and power strip outlet feeds, with live watts, and lets admins correct it.

What's on the page

  • Power cards: the RPDU, UPS and strip cards list each outlet that feeds something, draws power, or is off. Each row shows the outlet number, what it feeds (or unassigned) and its live watts. Off outlets show in red.
  • Badges: every device that's fed gets a ⚡ badge on its card showing what it draws. The badge reads "off" if its outlet is switched off.
  • Feed lines on hover, not all the time: hover a power card, or a device, to draw its feeds as dotted amber lines with watts. Both ends stay lit. When you're not hovering, nothing extra is drawn, so the diagram stays as clean as in part 1.
  • Problems: an outlet that is off but feeds something goes into the "to look at" list. It's red if the device it feeds is down.

How an outlet is matched, in order

  1. Saved assignment: set on this page. An empty target means "feeds nothing" and overrides a guess.
  2. The outlet's UniFi name names a device: this uses the same word-run matching as the port-name check (case, spaces and dashes ignored). Examples are "storage-01" and "Dream Machine Pro". Any name containing "modem", such as "AT&T Modem", maps to the modem.
  3. A MAC in the name: it is matched against host NICs (the monitor's host_macs, added in part 1) and UniFi device MACs.

Generic names like "Outlet 7" and "USB Outlet 1" match nothing. Gandalf never renames anything in UniFi.

On today's prod data, this matches six RPDU outlets automatically:

  • 6 → Dream Machine Pro
  • 13 → modem
  • 14 → storage-01
  • 15 → compute-storage-gpu-01
  • 16 → compute-storage-01, from the MAC in its name

Four outlets draw power without a name (7, 8, 19 and 20), and UPS outlet 1 draws 551 W unnamed (presumably the RPDU). These show as unassigned, ready to be set.

Editing (admins)

  • Click an outlet row, or focus it and press Enter. A small editor lets you choose Automatic, Nothing, or any device.
  • It sends PUT /api/topology/power/<device>/<outlet> with {"target": "<device>" | "" | null}.
  • The route requires admin rights and a JSON body. It checks that the power device and outlet exist and that the target is a known device. It returns 400 or 404 otherwise, and logs who changed what.

Code

  • db.py: a new power_feeds table, created on first use with the cutsheet tables, so no migration step is needed. Also get_power_feeds() and set_power_feed(), where None deletes the row and goes back to automatic.
  • topology.py:
    • build_power().
    • _named_device(), now shared with name_mismatch.
    • build_graph returns power and each node's power_in.
  • app.py: PUT /api/topology/power/.... /api/topology now passes saved feeds, and the page knows whether the viewer can edit.
  • static/topology.js: outlet rows, badges, hover feeds, the editor, and the off-outlet problem. Power gear is shown by default again.
  • README: a Power section.

Tests

  • TestPower (6 tests) covers:
    • name, alias and MAC matching
    • generic names
    • a saved assignment winning, and an empty target meaning nothing
    • power_in
    • no self-feed
    • listing every power device
  • TestPowerFeedApi (6 tests) covers:
    • assigning an outlet
    • nothing, and automatic
    • admin only
    • unknown target, source or outlet
    • body validation
    • JSON only
  • TestPowerFeeds in test_db.py (3 tests).
  • I also ran the SQL against a scratch MariaDB: insert, update, "nothing", and back to automatic.
  • Results: 471 passed; flake8 and bandit are clean; eslint is unchanged at 21 pre-existing warnings; no em or en dashes.

Screenshots

These come from prod-shaped data, with anonymised addresses. The problem view has simulated assignments and outlet 19 switched off.

Power rail Hovering the RPDU draws its feeds
Outlet editor Whole page, with badges
Light, with the off outlet in the problem list

🤖 Generated with Claude Code

This is part 2 of the topology page (part 1 was #39). It shows what each PDU, UPS and power strip outlet feeds, with live watts, and lets admins correct it. ## What's on the page - **Power cards**: the RPDU, UPS and strip cards list each outlet that feeds something, draws power, or is off. Each row shows the outlet number, what it feeds (or *unassigned*) and its live watts. Off outlets show in red. - **Badges**: every device that's fed gets a ⚡ badge on its card showing what it draws. The badge reads "off" if its outlet is switched off. - **Feed lines on hover, not all the time**: hover a power card, or a device, to draw its feeds as dotted amber lines with watts. Both ends stay lit. When you're not hovering, nothing extra is drawn, so the diagram stays as clean as in part 1. - **Problems**: an outlet that is off but feeds something goes into the "to look at" list. It's red if the device it feeds is down. ## How an outlet is matched, in order 1. **Saved assignment**: set on this page. An empty target means "feeds nothing" and overrides a guess. 2. **The outlet's UniFi name names a device**: this uses the same word-run matching as the port-name check (case, spaces and dashes ignored). Examples are "storage-01" and "Dream Machine Pro". Any name containing "modem", such as "AT&T Modem", maps to the modem. 3. **A MAC in the name**: it is matched against host NICs (the monitor's `host_macs`, added in part 1) and UniFi device MACs. Generic names like "Outlet 7" and "USB Outlet 1" match nothing. **Gandalf never renames anything in UniFi.** On today's prod data, this matches six RPDU outlets automatically: - 6 → Dream Machine Pro - 13 → modem - 14 → storage-01 - 15 → compute-storage-gpu-01 - 16 → compute-storage-01, from the MAC in its name Four outlets draw power without a name (7, 8, 19 and 20), and UPS outlet 1 draws 551 W unnamed (presumably the RPDU). These show as unassigned, ready to be set. ## Editing (admins) - Click an outlet row, or focus it and press Enter. A small editor lets you choose Automatic, Nothing, or any device. - It sends `PUT /api/topology/power/<device>/<outlet>` with `{"target": "<device>" | "" | null}`. - The route requires admin rights and a JSON body. It checks that the power device and outlet exist and that the target is a known device. It returns 400 or 404 otherwise, and logs who changed what. ## Code - `db.py`: a new `power_feeds` table, created on first use with the cutsheet tables, so no migration step is needed. Also `get_power_feeds()` and `set_power_feed()`, where `None` deletes the row and goes back to automatic. - `topology.py`: - `build_power()`. - `_named_device()`, now shared with `name_mismatch`. - `build_graph` returns `power` and each node's `power_in`. - `app.py`: `PUT /api/topology/power/...`. `/api/topology` now passes saved feeds, and the page knows whether the viewer can edit. - `static/topology.js`: outlet rows, badges, hover feeds, the editor, and the off-outlet problem. Power gear is shown by default again. - README: a Power section. ## Tests - `TestPower` (6 tests) covers: - name, alias and MAC matching - generic names - a saved assignment winning, and an empty target meaning nothing - `power_in` - no self-feed - listing every power device - `TestPowerFeedApi` (6 tests) covers: - assigning an outlet - nothing, and automatic - admin only - unknown target, source or outlet - body validation - JSON only - `TestPowerFeeds` in `test_db.py` (3 tests). - I also ran the SQL against a scratch MariaDB: insert, update, "nothing", and back to automatic. - **Results**: 471 passed; flake8 and bandit are clean; eslint is unchanged at 21 pre-existing warnings; no em or en dashes. ## Screenshots These come from prod-shaped data, with anonymised addresses. The problem view has simulated assignments and outlet 19 switched off. | | | |---|---| | Power rail ![](https://code.lotusguild.org/LotusGuild/gandalf/raw/branch/pr-40-screenshots/13-dark-power-rail.png) | Hovering the RPDU draws its feeds ![](https://code.lotusguild.org/LotusGuild/gandalf/raw/branch/pr-40-screenshots/14-dark-power-feeds.png) | | Outlet editor ![](https://code.lotusguild.org/LotusGuild/gandalf/raw/branch/pr-40-screenshots/15-dark-outlet-editor.png) | Whole page, with badges ![](https://code.lotusguild.org/LotusGuild/gandalf/raw/branch/pr-40-screenshots/01-dark-fit.png) | | Light, with the off outlet in the problem list ![](https://code.lotusguild.org/LotusGuild/gandalf/raw/branch/pr-40-screenshots/08-light-fit.png) | | 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jared added 1 commit 2026-11-14 20:28:37 -05:00
Topology: power map, what each outlet feeds
Lint / JS (eslint) (push) Successful in 21s
Lint / Python (flake8) (push) Successful in 2m36s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Test / Python Tests (pytest) (push) Successful in 2m46s
Lint / JS (eslint) (pull_request) Successful in 15s
Lint / Python (flake8) (pull_request) Successful in 2m14s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (push) Successful in 1m33s
Security / Python Security (bandit) (pull_request) Successful in 54s
Test / Python Tests (pytest) (pull_request) Successful in 1m3s
415d2222e7
- Each PDU, UPS and power strip card lists its outlets that feed
  something, draw power or are off, with live watts. Each device fed
  gets a power badge with what it draws. Hovering a power card or a
  device draws its feeds as dotted lines and keeps both ends lit.
- What an outlet feeds: a saved assignment, else the outlet's UniFi name
  when it names a device (or the modem), else a MAC in its name that
  belongs to a host NIC (the monitor's host_macs) or UniFi device.
- Admins click an outlet to set what it feeds, nothing, or automatic.
  Saved in a new power_feeds table (created with the cutsheet tables);
  PUT /api/topology/power/<device>/<outlet>, admin and JSON only, with
  the device, outlet and target checked. UniFi is never changed.
- An outlet that is off but feeds something is listed under "to look at".
- Power gear is shown by default again, now that it has something to say.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All checks were successful
Lint / JS (eslint) (push) Successful in 21s
Required
Details
Lint / Python (flake8) (push) Successful in 2m36s
Required
Details
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Test / Python Tests (pytest) (push) Successful in 2m46s
Lint / JS (eslint) (pull_request) Successful in 15s
Lint / Python (flake8) (pull_request) Successful in 2m14s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (push) Successful in 1m33s
Security / Python Security (bandit) (pull_request) Successful in 54s
Test / Python Tests (pytest) (pull_request) Successful in 1m3s
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin power-map:power-map
git checkout power-map
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/gandalf#40