Skip to content

Shared switch ports (KAN-199) and overrides generate (KAN-120) - #30

Merged
gitkodak merged 5 commits into
mainfrom
shared-ports-and-overrides-generate
Aug 17, 2026
Merged

Shared switch ports (KAN-199) and overrides generate (KAN-120)#30
gitkodak merged 5 commits into
mainfrom
shared-ports-and-overrides-generate

Conversation

@gitkodak

Copy link
Copy Markdown
Owner

What this changes

Five commits, building on each other:

  • Flag switch ports shared by several wired clients (KAN-199). Several wired clients reporting the same switch and port usually means an unmanaged switch or a virtualisation host the controller cannot see, and needs no cooperation from the hidden device to detect, unlike LLDP. Surfaced four ways from one Topology.shared_ports(): a marker on the diagram, a named SHARED SWITCH PORTS section in --report, a console warning (obfuscation-aware), and never as a synthetic node — which of the two causes it is can't be told apart from here, so nothing is invented either way.
  • Fix the marker being invisible in the default render. The * port-label marker rode on Style.show_port_labels, which --layout unifi (the default) turns off entirely. Gave the edge its own layout-independent channel (arrowhead=diamond, unconditional, the same approach TOPOLOGY_GRAPH's arrowhead=odot already used) so the signal survives the render most people actually produce.
  • Add unifi-map overrides generate (KAN-120). Prints a commented overrides.toml skeleton to stdout, seeded from the same three signals --report names: unplaced clients, shared switch ports, and ambiguous artwork matches. Every block is commented out, so the file is a no-op until edited; the one thing it never fills in is a client's real uplink.
  • Documentation sweep for both features: CHANGELOG.md Unreleased entries, docs/usage.md's diagram-reading table and --report walkthrough, a new "Generating a starting point" section in docs/overrides.md, and TODO.md/CLAUDE.md reconciled against what actually shipped.
  • Escape controller-supplied text in the generated TOML. Found by external review: a label containing " produced invalid TOML the moment a block was uncommented, and a label containing a newline would have broken out of its # comment entirely — a real break of "inert until edited", not a cosmetic one. Fixed with two small encoders, applied to every label and id reaching the output.

Checklist

  • make check passes (ruff format --check, ruff check, pytest), and I
    checked its exit code rather than eyeballing piped output
  • Tests added or updated, and none of them touch the network
  • Any new fixture data is non-identifying: no real hostnames, subnets, SSIDs
    or device addresses
  • No Ubiquiti artwork vendored into the repository
  • No cache/ or out/ contents committed
  • If this changes what gets drawn, the legend and the docs still describe
    it accurately (docs/usage.md for layouts and reading the diagram,
    docs/output.md for formats)

Anything you are unsure about

KAN-199's other signal — reading the controller's own has_unknown_switch boolean from the v2 topology payload — is still unbuilt and still unverified (per-site behaviour, whether it clears, whether an all-UniFi network ever sets it). Documented as open in TODO.md/CLAUDE.md rather than attempted here, since it would be guessing.

Every guard in this PR was mutation-tested: reverted, confirmed the new/changed tests fail red, restored.

sakodak and others added 5 commits August 16, 2026 20:13
Several wired clients reporting the same switch and port usually means an
unmanaged switch or a virtualisation host the controller cannot see, and
needs no cooperation from the hidden device to detect, unlike LLDP.

Topology.shared_ports() is the one source of truth, restricted to direct
CLIENT_UPLINK reports and computed against the final topology so a
[[hosted]] override that reparents a client already resolves the flag.
Surfaced four ways from that one method: a `*` on the port label plus a
legend note in DOT/SVG/PDF/PNG, the label marker alone in draw.io (no
legend there), a named SHARED SWITCH PORTS section in --report, and a
console warning from cmd_render that is obfuscation-aware like
_report_displacements. Never drawn as a synthetic node: which of the two
causes it is can't be told apart from here, so no node is invented either
way.

The has_unknown_switch controller flag stays unread; it's a separate,
unverified signal and TODO.md/CLAUDE.md now say so explicitly rather than
letting it read as done alongside this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The `*` port-label marker from the KAN-199 shared-port change rode on
Style.show_port_labels, which --layout unifi (the default) turns off
entirely because ortho routing can't place edge labels without them
drifting onto unrelated nodes. So on the render most people actually
produce, the marker and its legend row were both gone: detection,
--report and the console warning all still worked, but the diagram
itself showed nothing.

Give the edge its own layout-independent channel instead, the same way
TOPOLOGY_GRAPH's arrowhead=odot already is: arrowhead=diamond, applied
unconditionally rather than gated on show_port_labels. The two markers
never collide, since shared_ports() only ever counts direct
CLIENT_UPLINK edges. The legend row stays gated on show_legend,
matching every other marker's treatment in --layout unifi.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-120)

Prints a candidate overrides.toml to stdout, seeded from the same three
signals --report already names: clients with no reported uplink
([[link]], with `to` always left blank), switch ports shared by several
wired clients ([[device]] + [[hosted]], KAN-199), and artwork matches
refused as ambiguous ([[node]]). Every block is commented out, so the
file is a no-op until a human edits and uncommments it; the one thing
it never fills in is a client's real uplink.

A new `generate` action on the existing `overrides` subcommand rather
than a flag on render: this produces a config skeleton, not a diagram,
and being its own subcommand makes it opt-in for free rather than
something a render flag could leave on by habit. Artwork ambiguity is
resolved offline and best-effort, the same way `unifi-map shape`
already does, so it never touches the network.

--report now points at `overrides generate` from each of the three
sections it can help with.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CHANGELOG.md had no Unreleased entry for either. docs/usage.md's
"Reading the diagram" table was missing the new diamond-arrowhead row
-- the same gap the code itself had until the previous commit -- and
its --report summary sentence didn't mention the new SHARED SWITCH
PORTS section. Neither usage.md's command list nor overrides.md
mentioned `overrides generate` at all; overrides.md gets its own
"Generating a starting point" section, sibling to the existing one for
`check`. TODO.md's KAN-199 entry still described the label-only marker
from before the arrowhead fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
overrides.generate_candidates() interpolated node/edge labels and ids
straight into the skeleton TOML and into the `#` comment lines around
them. A switch named Core "A" produced name = "Unmanaged switch (port
7 on Core "A")" -- invalid TOML the moment the block was uncommented.
Worse, a raw newline in a label would have ended the `#` line it sat
on and let whatever followed be read as real TOML, unreviewed: a
genuine break of "inert until edited", the one guarantee this command
makes, not just a cosmetic one.

Two helpers fix both hazards: _comment_safe() collapses whitespace so
nothing can end the line it sits on (same technique already used by
render_mermaid._flatten()), and _toml_value() additionally escapes `\`
and `"` for values placed inside quotes. Applied everywhere a label or
an id reaches the output, not just the field the repro happened to
hit -- _norm_mac() never validates a MAC's shape, and this project's
own threat model treats all of it as attacker-controlled.

Six tests pin it, including one that parses the entire generated file
with tomllib and asserts it comes back empty, which is what a newline
actually escaping its comment would have broken. Mutation-tested:
reverted, confirmed all six fail red, restored.

Found by external review of cfcd2fe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@gitkodak
gitkodak merged commit 518d533 into main Aug 17, 2026
9 checks passed
@gitkodak
gitkodak deleted the shared-ports-and-overrides-generate branch August 17, 2026 02:02
@gitkodak gitkodak mentioned this pull request Aug 17, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants