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.
Extract getVisibleNodeIds/filterVisibleEdges from CanvasContainer into
src/utils/collapseFilter.ts. Replace inline O(n^2) BFS (nested array
.find per node) with O(n) traversal backed by parentId->children and
id->node Maps, and wrap consumer calls in useMemo so visibility is
recomputed only when nodes/edges change rather than on every render.
Add 12 unit tests covering the filter logic that the original PR left
untested: single-level collapse, multi-level subtree hiding via
collapsed ancestor, sibling isolation when an inner zone is collapsed,
empty zones, missing custom_colors, insertion-order independence, and
edge filtering for hidden source/target.
Personal computing devices (laptops, phones, tablets) currently collapse
into the generic icon because the type vocabulary has no entries for
them. This adds two new NodeTypes with Lucide icons:
- laptop -> Laptop icon (reuses the computer accent color per theme)
- mobile -> Smartphone icon (new pink accent #ec4899 across themes)
Touches:
- types/index.ts NodeType union + NODE_TYPE_LABELS
- utils/nodeIcons.ts Lucide import + ICON_REGISTRY +
NODE_TYPE_DEFAULT_ICONS
- utils/themes.ts nodeAccents in all 6 themes
- canvas/nodes/index.tsx LaptopNode + MobileNode wrappers
- canvas/nodes/nodeTypes.ts register in react-flow nodeTypes
- modals/NodeModal.tsx new "Personal" type group
- modals/CustomStyleModal.tsx expose new types in style editor
- types/__tests__/types.test.ts enumerate new types
- utils/__tests__/themes.test.ts enumerate new types
Backwards-compatible: existing nodes typed as 'generic', 'server', etc.
keep rendering exactly as before. No data migration required.
- Use nullish coalescing operator to provide empty array default
- Prevents 'Cannot read properties of undefined' error when nodes is undefined
- Fixes failing GroupRectNode tests that don't provide mock nodes
Co-authored-by: CyberClaw <noreply@openclaw.ai>
- Replace component render tests with unit tests on types and state logic
- Tests now verify: type definitions, optional properties, toggle logic, nesting support
- Removes complex mocking and React component testing that triggers act() warnings
- Full integration testing is covered by CanvasContainer tests
- Reduces test file from 142 lines to focused unit tests
Co-authored-by: CyberClaw <noreply@openclaw.ai>
- Add collapsed state to NodeData.custom_colors (type=groupRect only)
- Implement toggleNodeCollapsed action in canvasStore (Zustand)
- Extend GroupRectNode UI with smooth chevron toggle button
- Rotating chevron icon (↓ → when collapsed)
- Shows '+N' badge when zone is hidden
- Reduces zone opacity to 0.6 when collapsed
- All transitions target 60 FPS (ease-out 200ms)
- Filter child nodes/edges in CanvasContainer based on parent collapse state
- Breadth-first traversal handles multi-level nesting
- Connecting edges to hidden nodes are automatically hidden
- Add comprehensive test coverage
- Store: toggleNodeCollapsed state mutation, unsaved flag
- Component: chevron rendering, click handlers, opacity transitions
- Persist collapsed state via YAML serialization (part of custom_colors)
Benefits:
- Declutter large Zigbee meshes, multi-building networks
- Preserve layout structure without deleting nodes
- Smooth 60 FPS transitions for UX polish
CONTRIBUTING.md compliance:
- Strict TypeScript types, no 'any'
- Zustand store pattern, no prop drilling
- Tests for store logic and component behavior
- Frontend linting requirements met
Co-authored-by: CyberClaw <noreply@openclaw.ai>
- Live view rebuilt parent map from proxmox/group only, so docker_container
(and any vm/lxc with container_mode=true) lost parentId and rendered
outside its host. Match App.tsx logic: include any node with
container_mode=true.
- Live view ignored viewport.theme_id and custom_style on load, so the
shared canvas always rendered in default theme. Apply both like the
authenticated canvas does.
- Backend /liveview response now includes custom_style from CanvasState.
Adds regression tests on both ends.
- App.tsx: filter descendants of edited node from parentCandidates to
prevent picking a child as parent (would create a cycle).
- NodeModal.tsx: clear parent_id when switching to a type with no valid
parents, and revalidate parent_id on submit against the type's allowlist.
- Tests: cover getValidParentTypes for lxc/vm/docker_container and types
that disallow parents.
ha-relevant: yes
Adds back the Parent Container selector in NodeModal, shown when:
- child type is lxc or vm (parents: proxmox/vm/lxc/docker_host)
- child type is docker_container (parents: docker_host/lxc/vm/proxmox)
Selector hidden when no valid candidate node exists on the canvas.
Centralizes the parent-type allowlist in getValidParentTypes
(shared with the virtual-edge auto-parent helper).
ha-relevant: yes
Group nodes can now be source/target of edges via snap points on
each side (top/right/bottom/left). Each side exposes a source handle
plus an invisible target overlay, matching the pattern used by
BaseNode.
Extend splitIps to accept comma/semicolon/newline delimiters with dedupe,
and reuse it in DetailPanel for both the IP list and the service-URL host.
Fixes ServiceBadge links pointing at concatenated string when IPs were
entered with ';' or newline separators. Adds tests for new delimiters.
- Selected edge shows interactive dots at both endpoints; drag onto any
handle (different snap point or different node) to reconnect. Edge
follows the cursor live during drag; drop on empty space leaves edge
unchanged.
- Endpoint dots render in EdgeLabelRenderer above the node layer and walk
the elementsFromPoint stack so the underlying handle wins over the
dragged dot itself.
- ProxmoxGroupNode in container mode now renders bottom_handles snap
points (was always single 'bottom' handle), matching BaseNode.
- New canvasStore.reconnectEdge action: swaps source/target/handles via
normalizeHandle, snapshots history, marks dirty.
- Center webkit slider thumb on 4px track (margin-top: -4.5px) so the
opacity bullet no longer sits below the bar.
- Drop redundant inline accentColor; CSS module owns track color.
- Remove dead ::-ms-* prefix rules.
- Tests: assert aria-labels on grid/select triggers, cover getFontLabel
happy path + unknown-value fallback via the Font selector DOM.
Backend already writes the properties on approve, but the frontend's local
addNode() call rebuilt NodeData without them — so newly approved zigbee
nodes showed an empty right panel until a full canvas reload. Build the
property list on the client from the PendingDevice fields so the canvas
state matches the DB row immediately. Single + bulk approve flows updated.
- Approval (single + bulk) of zigbee pending devices now writes IEEE, Vendor,
Model, LQI into Node.properties so they show in the right panel.
- Zigbee re-import refreshes properties on existing canvas Nodes and skips
creating a pending row when the device was already approved — keeps
approved devices out of pending/hidden modals.
- Coordinator Node also receives the same properties on first creation and
on re-import.
- Remove the Parent Container selector from the add/edit node modal.
Zigbee nodes have no IP-based check — hide Check Method and Check Target
fields in NodeModal for all three zigbee types. Default and force
check_method='none' so the scheduler marks them always online.
Backend approve and zigbee import routes also set status='online' and
check_method='none' for zigbee node types.
Add a second tab in the node Icon picker to choose from the ~2250 brand
icons hosted by homarr-labs/dashboard-icons (Plex, Sonarr, Home
Assistant, etc.) served via jsDelivr CDN. The Generic tab keeps the
existing lucide picker unchanged.
Storage uses a 'brand:<slug>' prefix on custom_icon, so existing nodes
referencing lucide keys keep working with zero migration. A new
resolveCustomIcon helper returns a discriminated union (lucide | brand)
and a NodeIcon component centralizes rendering for both kinds.
Includes a manifest fetch script (scripts/fetch-dashboard-icons.mjs)
and a checked-in dashboardIcons.json snapshot.
Modal grew taller than viewport when icon picker expanded, hiding
header and footer buttons. Constrain DialogContent to 90vh and add
overflow-y-auto so all controls stay reachable.
Add zigbee_coordinator, zigbee_router, zigbee_enddevice to the
CustomStyleModal editable list with Radio/Zap/Lightbulb icons so users
can tweak color, opacity, and size like any other node type.
Draw.io / Figma style: while dragging a node, show dashed cyan guide
lines when its edges (left / center / right / top / middle / bottom)
align with another node's, and snap the position to the matched line
within a configurable threshold.
- utils/alignment.ts: pure snap math, returns delta + guide segments.
Same-size boxes show all aligned guides simultaneously.
- canvas/AlignmentGuides.tsx: SVG overlay locked to the React Flow
viewport (panned/zoomed correctly).
- hooks/useAlignmentGuides.ts: wires onNodeDrag/onNodeDragStop, applies
snap via setNodes, listens for Alt to temporarily disable.
- utils/alignmentSettings.ts: localStorage-backed prefs (enabled,
threshold 2-16px) with a tiny CustomEvent pub-sub so the SettingsPanel
and the drag hook stay in sync without a global store.
- Sidebar settings panel: toggle + threshold slider.
Multi-selection drag uses the union bounding box. Children with
parentId are skipped for v1 to avoid mixing absolute and parent-relative
coordinates. Hold Alt to escape snap.
Backend sends ISO timestamps with +00:00 offset. Code blindly appended
'Z' when the string didn't end with 'Z', producing '...+00:00Z' which
parses to Invalid Date.
Detect any timezone marker (Z, z, or numeric offset like +00:00) and
only append 'Z' for naive datetimes.
Co-authored-by:
Trash button in PendingDevicesModal now removes only filtered devices
instead of wiping the whole pending list. Falls back to clearPending
when no filters are active.
Mirrors IP scan flow: POST /zigbee/import-pending now creates a
ScanRun(kind=zigbee, status=running) and returns immediately.
Networkmap fetch + pending upsert run in the background, status
transitions to done/error when finished.
Frontend: import modal closes on submit, scan history shows the
run with a ZIG/IP kind chip and toasts on completion. Pending
modal auto-refreshes when run finishes.
scan_runs.kind column added (default 'ip', idempotent migration).
Existing zigbee tests refactored to exercise _persist_pending_import
directly (background tasks don't see the test session); route test
verifies the run is created with kind=zigbee.
Replaces sidebar pending/hidden panels with a wide modal showing
devices as cards. Adds search, segmented source/status filters,
type filter, select mode for bulk approve/hide/restore, and
keyboard shortcuts. Hidden cards click-to-restore (no approval
detour); approval no longer pops the edit modal.
Backend: new restore + bulk-restore endpoints (hidden -> pending).
- SearchModal.tsx: same null-IP guard as SearchBar; also search by
friendly_name and ieee_address. Fixes Docker CI typecheck.
- python-multipart 0.0.26 -> 0.0.27 (CVE-2026-42561). Fixes
Security pip-audit.
Coordinator auto-approves to a canvas Node; routers/end devices land in
pending_devices keyed by IEEE. Discovered parent->child edges are stored
in pending_device_links so that approving a pending device later
auto-creates the Edge once both endpoints exist as canvas Nodes.
- new POST /api/v1/zigbee/import-pending (default mode in modal)
- new pending_device_links table; ieee_address on nodes + pending_devices
- pending_devices.ip migrated to nullable (table rebuild on existing DBs)
- approve / bulk-approve return auto-created edges; sidebar pushes them
into the canvas store with bottom -> top-t handles
- ZigbeeImportModal: radio toggle pending vs canvas; reset on close
- PendingDeviceModal: zigbee badge, IEEE/LQI/vendor/model rows, services
hidden for zigbee
- Sidebar pending row: ZIG source badge, LQI badge, friendly_name fallback
- SearchBar: null-safe IP, also searches friendly_name and ieee_address
- Tooltip trigger uses asChild to avoid nested-button hydration error
- Modal: small italic note under Test/Fetch buttons warning users that
large meshes can take several minutes
- Service: _NETWORKMAP_TIMEOUT 180s -> 300s (5 min) for very large meshes
- Schema: mqtt_tls + mqtt_tls_insecure flags on import + test-connection
requests; model_validator enforces insecure requires tls
- Service: _build_tls_context() using ssl.create_default_context();
logger.warning when verification disabled; tls_context plumbed into
aiomqtt.Client for both fetch_networkmap and test_mqtt_connection
- Route: passes tls flags through to service
- Frontend: TLS checkbox auto-toggles port 1883<->8883 unless user
edited; insecure checkbox disabled until TLS on, red-tinted; password
field marked autocomplete=new-password
- Tests: 7 new backend tests (TLS context build, client kwargs assertion,
router happy path, insecure-without-tls 422)