Issue #20: bump MAX_BOTTOM_HANDLES 48 -> 64 (covers 48+4 SFP switches) and add
a per-node "Show Port Numbers" toggle that labels each bottom connection point.
Issue #21: add `fibre` as a first-class edge/connection type (bright cyan with a
subtle glow) alongside ethernet/wifi/iot/vlan/virtual/cluster - selectable in the
edge modal, themeable, registered in the React Flow edgeTypes registry, and
round-tripped through YAML import/export.
Backport of homelable-hacs PR #23.
ha-relevant: yes
Three connected bugs in PR #158's collapse feature:
1. Toggle wired to the wrong component
The chevron was on GroupRectNode and computed children via React Flow
parentId. But in this codebase parentId is set by createGroup() on
type=group nodes, not on groupRect zones — zones are decorative
rectangles. Result: childrenCount was always 0 on every zone and the
button never rendered, so the feature was unreachable from the UI.
Fix:
- Add the same chevron toggle to GroupNode (the actual container).
parentId children are already known there, so the existing BFS in
computeCollapseInfo hides them when data.collapsed flips.
- For GroupRectNode, switch childrenCount to spatial containment so
drawn zones also work: hit-test other nodes' bbox centres against
the zone bbox.
2. Visibility filter ignored spatial zones
Extend computeCollapseInfo with a second pass that hides every node
whose centre lies inside a collapsed groupRect, plus the parentId
subtrees of those nodes (so a Proxmox host inside a collapsed zone
takes its VMs/LXCs with it). Edge rewiring routes vanished endpoints
to the same visible zone via a unified hiddenBy map populated by both
passes.
3. Save dropped data.collapsed for every type except groupRect
The DevTools payload was the smoking gun: for a type=group node the
serializer wrote custom_colors: {show_border: true} with no collapsed
key, so the backend stored a stale false on every save. Only the
groupRect branch of serializeNode/deserializeApiNode stashed and
hoisted the flag. Move the stash + hoist to the general branch too
(backend's custom_colors is dict[str, Any] so no schema change).
Tests: 11 new cases for spatial containment + GroupNode toggle UI, and
4 round-trip cases for collapse on non-groupRect types.
Three follow-ups to PR #158 review:
1. Promote collapsed to NodeData.collapsed
The flag was previously stashed inside NodeData.custom_colors, which is
a colors/style object — semantically wrong. Move it to a first-class
boolean on NodeData. Persistence keeps the existing API shape: serialize
writes it into the custom_colors blob (alongside width/height/z_order,
matching how groupRect already stashes layout metadata), and deserialize
hoists it back. Legacy saves from the original PR shape load correctly.
2. Re-route cross-zone edges to the collapsed ancestor
Previously any edge touching a hidden node was dropped, so a Zigbee
coordinator outside a collapsed mesh lost all visible links to it.
rewireEdgesForCollapse now walks each endpoint up the parentId chain to
its nearest visible ancestor, surfaces a single stub edge on the
collapsed zone, de-dupes parallel rewires (a 20-device mesh becomes one
stub, not twenty), and drops edges that would self-loop on a zone or
reference an orphan.
3. Revert package-lock.json churn
The 63-line diff from the original PR was npm-version drift (libc
arrays stripped from optional deps), unrelated to the feature.
Tests:
- canvasStore.collapse: updated to assert on data.collapsed.
- collapseFilter: 8 cases for visibility + 7 for edge rewire, covering
cross-boundary, nested collapse, sibling self-loop, mesh dedup, and
orphan endpoints.
- canvasSerializer.collapse: round-trip + legacy-shape compat.
Replaces the 4-option dropdown with a 1..48 slider. Existing handle IDs
('bottom', 'bottom-2'..'bottom-4') are preserved and the hand-tuned pixel
positions for counts 1..4 are locked in (regression-tested) so saved
canvases render identically. Counts >=5 use uniform spacing; node card
auto-grows in width so handles stay clickable. Existing edge-remap on
shrink scales unchanged thanks to its index-based loop.
ha-relevant: yes
Proxmox nodes with container_mode=false fell through both width conditions
in deserializeApiNode and got no explicit width on reload, causing RF to
auto-size to content width and ignoring the user's manual resize.
- canvasSerializer: unified width restore logic — saved width applies to all
node types; proxmox container_mode defaults (300x200) only kick in when
no saved width exists
- BaseNode: add overflow-hidden + min-w-0 to properties row so truncate
actually clips long values instead of expanding the node
- Fix ApiEdge type: add 'basic' to animated union
- Add 5 standalone storage tests: properties, empty properties, waypoints,
basic animation, and all three animation types round-trip through localStorage
- Drag waypoints to reshape edges; double-click a waypoint to remove it
- + handles at segment midpoints to insert new waypoints
- Bezier style: catmull-rom smooth curves through waypoints
- Smooth style: rounded-corner polyline with soft 45° snap (snaps within 15px)
- First + handle biased to source axis for perpendicular node exit
- snap45both: ray-intersection solver ensures both adjacent segments snap to 45° simultaneously
- Clear path button in EdgeModal when waypoints exist
- Waypoints serialised/deserialised with canvas state
- Add lasso/box selection via selectionOnDrag (Space to pan, lasso by default)
- Add lasso/pan toggle button in canvas controls (bottom-left)
- Multi-select panel in right panel when 2+ nodes selected (including zones)
- Create named Group node from selected nodes with bounding box math
- Group node: resizable, inline rename, show/hide border toggle, status summary
- GroupDetailPanel: lists members, online/offline count, ungroup action
- Fix group persistence after save/reload (extend proxmox container map to include group nodes)
- Fix groupRect serialization to preserve parent_id
- Remove background color from group and proxmox container node wrappers
- Add tests for all new store actions, GroupNode, MultiSelectPanel, GroupDetailPanel
edges/index.tsx was never committed — both animation modes were
rendering as snake (truthy string check). Now uses animMode to
distinguish 'snake' (moving blob) from 'flow' (continuous flowing dashes).