Commit Graph

109 Commits

Author SHA1 Message Date
Pouzor 4956e8ab90 fix(canvas): keep container host height when editing an unrelated field
Editing any field on a container-mode vm/lxc/docker_host node (e.g. just
renaming it or adding a property) reset its manually-set height to
undefined, snapping the container back to auto-fit content size and
scrambling/overflowing its nested children.

Root cause: updateNode clears height on every 'properties' change and the
node modal always includes properties in its payload. Only proxmox was
excluded from the reset; vm/lxc/docker_host in container mode were not.

- export CONTAINER_MODE_TYPES from virtualEdgeParent (reuse, no re-declare)
- updateNode: skip the height reset for any container-mode host type;
  proxmox stays always-excluded (legacy), plain leaf nodes still auto-fit
- regression tests: manual height preserved for container-mode
  vm/lxc/docker_host and proxmox; still reset for a non-container vm

Fixes #278

ha-relevant: yes
2026-07-16 16:53:07 +02:00
Pouzor 4124d6c5a4 test: split canvasStore monolith and relocate misnamed util test
- Split the 1430-line canvasStore.test.ts into a canvasStore/ subpackage by
  concern (nodes, containers, sizing, edges, selection, grouping, history,
  clipboard, customStyle, floorMap); each file <350 lines. Test bodies are
  unchanged; local makeNode/makeEdge replaced by the shared factories.
- Move panels/DetailPanel.test.ts -> utils/serviceUrl.test.ts: it tested
  getServiceUrl, not DetailPanel.

Same 1396 tests, all green.

ha-relevant: yes
2026-07-10 02:48:29 +02:00
Pouzor 1aecb0a4d9 feat: sync active design to URL for refresh/share
Switching designs now reflects the active design id in the URL
(?design=<id>). A page refresh or shared link reopens that design;
an absent or unknown id falls back to the default design.

ha-relevant: no
2026-07-09 16:29:04 +02:00
Pouzor 0b4bd5680d feat: create a new canvas by copying an existing one
Add a 'Copy from existing' option to the New Canvas modal. It lists every
canvas with node/group/text counts; picking one deep-copies its nodes, edges,
parent/child links and canvas state (viewport, custom style, floor plan) into
a fresh design.

Backend: POST /designs/{source_id}/copy remaps node ids, re-points edges and
parent links, and clones canvas state; GET /designs now returns per-design
counts for the picker. Standalone mode clones the localStorage canvas.

Closes #216

ha-relevant: maybe
2026-07-07 15:53:42 +02:00
Pouzor 7bc3e5d8a0 fix: preserve edge connection points in YAML export/import (#208)
YAML round-trip dropped every edge's handles: export never wrote them
and import hardcoded sourceHandle='bottom'/targetHandle='top-t'. After
import all connections collapsed onto slot 0 ("converge at a single
point"). Per-side handle counts were dropped too, so the extra bottom
slots did not even exist to attach to.

- yaml types: add sourceHandle/targetHandle to connections and
  top/bottom/left/rightHandles to nodes
- export: write each edge's real handles + per-side counts (only above
  the side default); orient parent-edge handles parent->child
- import: restore handle counts onto node data and use the stored
  handles, falling back to the legacy defaults for pre-existing YAML

Positions remain dagre-managed (unchanged); this restores connection
points only.

ha-relevant: yes
2026-07-07 11:16:29 +02:00
Pouzor adf82f8f01 feat: merge IP-scanned and Proxmox-imported devices by MAC
Reconcile the same physical device discovered by both the nmap IP scan and
the Proxmox importer into a single inventory row, keyed on MAC. Previously
each path only deduped by IP, and the importer captured no MAC (and no IP for
stopped guests), so most guests double-listed.

Backend:
- mac_utils.normalize_mac: canonical MAC (lowercase, ':'-separated), the
  cross-source join key. Normalized on write and on compare.
- proxmox_service: capture the guest NIC MAC agent-free from the net0 config
  (qemu virtio=<MAC>, lxc hwaddr=<MAC>); works for stopped guests. Resolver
  now returns (ip, mac).
- proxmox persist: match existing Node/PendingDevice by ieee OR ip OR MAC;
  fill mac, keep the vm/lxc type, union sources.
- scanner persist: match PendingDevice by ip OR MAC; fill the IP a Proxmox
  import lacked, keep a pve row's type, union the scan source. Stamp query
  matches raw + normalized MAC (legacy-safe).
- Multi-source tags: new PendingDevice.discovery_sources JSON column so a
  merged device shows under both the IP and Proxmox filters. Idempotent
  migration backfills from discovery_source (legacy NULL-scalar rows with an
  IP become ["arp"]). _sources_after_merge preserves a scanned row's IP origin
  through the merge without tagging a pure Proxmox guest.
- Import now broadcasts a scan update on completion so an open inventory
  reloads without a manual refresh.

Frontend:
- pendingSources: sourceBuckets/orderedSources map discovery_sources to filter
  buckets; a device with ["arp","proxmox"] matches both filters and renders
  both badges. PendingDevicesModal filter + badges use them.

Tests: MAC normalization, config MAC capture, cross-source merge both
directions, legacy-row IP-tag preservation, no-false-IP-tag guard, refresh
broadcast, and the frontend bucket mapping.

ha-relevant: maybe
2026-07-06 20:11:03 +02:00
Pouzor 05ef746f22 fix: cluster edges render on left/right handles from approve flow
Cluster edges created via the pending -> approve path rendered on the top
handle instead of left/right, because the edge and its endpoints lost their
handle information on the way to the canvas.

- Approve resolver (scan.py) now returns each edge's type + source/target
  handle. Handle IDs are the bare slot-0 side names ('right'/'left'), the
  canonical stored form React Flow resolves to the correct side; a '-t' target
  id fails to resolve and falls back to the top handle.
- Frontend injectAutoEdges no longer hardcodes iot/bottom/top-t. It injects
  each edge with its real type + handles and bumps the referenced nodes'
  left/right handle counts (which default to 0) so the cluster endpoints exist.
  Logic extracted to a pure, tested util (applyAutoEdges).
- clusterEdges direct-import path uses the bare 'left' target to match.

Tests: new autoEdges unit tests; updated backend handle assertions.

ha-relevant: maybe
2026-07-06 11:13:32 +02:00
Pouzor 485d2f2b04 feat: configurable edge line style + width per type and per edge
Edge render (solid/dashed/dotted) and stroke width were hardcoded per
edge type. Expose both as user settings.

- Custom Style modal (Edges): line-style buttons, 1-4x width slider,
  live preview; left-list swatch renders the actual line.
- Per-edge EdgeModal: same controls; line style follows the type preset
  live until overridden.
- Renderer applies line_style/width_mult over BASE_STYLES (width scales
  markers + animation overlays); unset keeps the type default look.
- Persist line_style/width_mult through serializer, canvas save, and the
  edges API (new nullable columns, idempotent migration).

ha-relevant: yes
2026-07-05 14:11:01 +02:00
Pouzor c95d104245 feat: selectable marker shapes per edge endpoint
Replace the on/off arrowhead toggle with a per-end shape picker. Each end
(start/end) independently selects: none, arrow, arrow-open, circle, diamond,
or square. Markers still recolor live from the resolved stroke color.

Frontend:
- MarkerShape type + edgeMarkers util (normalizeMarker, MARKER_GEOMETRY);
  legacy boolean coerces to 'arrow' on read.
- Per-shape <marker> inner geometry; symmetric shapes use fixed orient.
- MarkerShapePicker reused in EdgeModal and CustomStyleModal.
- Serializer normalizes to shape strings.

Backend:
- Edge marker columns Boolean -> String (default 'none'); TEXT migration.
- normalize_marker() + validators coerce legacy bool / unknown values.

ha-relevant: yes
2026-07-05 11:30:49 +02:00
Pouzor 1cf525844b feat: arrowhead endpoints for edges + fix parallel edges not rendering
Add optional filled-triangle arrowheads at either end of an edge,
independently toggleable per edge (EdgeModal) and as per-edge-type
defaults (CustomStyleModal). Arrowheads are custom inline <marker> defs
filled with the live stroke colour so they recolour reactively with
custom_color / vlan / selected state. Persisted frontend (serializer)
and backend (edge columns + schemas + runtime migration).

Also fix two dedupe layers that silently dropped legitimate parallel
links between the same two devices:
- store: React Flow addEdge() connectionExists dropped a second edge
  with matching source+target when handles were null/equal. Build the
  edge with a unique id and append directly.
- render: rewireEdgesForCollapse deduped ALL edges by src->tgt key even
  when nothing was collapsed, filtering real parallel edges out of the
  visible set. Restrict the anti-mesh dedupe to rewired collapse stubs.

Tests: marker render, per-edge/per-type UI, store apply, serializer
round-trip, backend edge/canvas persistence, parallel-edge regressions.

ha-relevant: yes
2026-07-05 01:16:26 +02:00
Pouzor 1479c777d0 fix: replace Proxmox cluster handles with configurable side points
The Proxmox node had two always-on cluster-left/cluster-right handles that
rendered independently of the new per-side connection-point config — so a
configured side stacked on top of the forced handle (e.g. 0 config still showed
1; 2 config left the forced one in the middle).

- ProxmoxGroupNode: remove the always-on cluster handles in both container and
  non-container modes; cluster links now use the normal per-side points
- add migrateClusterHandles(nodes, edges): on load, remap any edge still bound
  to cluster-left/right onto the left/right slot-0 point and set that node's
  side count to at least 1, so existing links survive (edge 'cluster' type/style
  kept). Applied on API load, standalone load, LiveView, and YAML import
- App: new cluster links are made by choosing the Cluster edge type in the edge
  modal (drop the cluster-handle auto-default)

ha-relevant: yes
2026-07-04 01:35:28 +02:00
Pouzor b708a08fd0 feat: customizable connection points per side (issue #243)
Generalize the bottom-only handle machinery to all four sides. Top/Bottom
keep their single-handle default; Left/Right are opt-in (default 0) so
existing diagrams render unchanged. Handle IDs stay keyed off the bare side
name at slot 0, keeping saved edges valid.

- handleUtils: side-generic API (handleId, handlePositions, clampHandles,
  sideDefault, removedHandleIds, sideHandleCount); legacy bottom fns kept as
  delegating aliases
- BaseNode + ProxmoxGroupNode render all sides via new shared SideHandles
- updateNode remaps edges on any side's shrink (fallback to slot-0 / bottom)
- serializer round-trips top/left/right_handles
- NodeModal: spatial cross of -/N/+ steppers around a node preview, replacing
  the four stacked sliders; seeds per-type defaults
- CustomStyleModal: per-type default connection points per side

ha-relevant: yes
2026-07-03 21:25:14 +02:00
Pouzor 6160090919 revert: drop zigbee LQI edge coloring and path highlighting
Split out of this PR — will land separately with reworked zigbee edge
capture/storage. Keeps only the floor plan + generic media upload work.

- Remove getLqiColor + LQI-derived iot edge stroke (edges/index.tsx)
- Remove zigbee path highlight effect (DetailPanel.tsx)
- Remove highlightedPath/setHighlightedPath store state (canvasStore.ts)
- Delete utils/zigbeePathfinding.ts
- Revert associated test scaffolding and package-lock churn
2026-07-02 17:16:51 +02:00
Pouzor 1ed013bde2 feat: floor plan viewport rendering, per-canvas config, server media upload
- Render floor plan inside React Flow ViewportPortal so it pans/zooms with
  nodes (was screen-fixed, desynced on pan/zoom); zoom-stable resize handles.
- Move floor plan config from the left panel into the canvas (design) edit
  modal; attach per-design and fix cross-design bleed on load.
- Store images via a new generic backend media endpoint (POST/GET/DELETE
  /api/v1/media) on disk under <data_dir>/uploads, not base64 in the canvas.
- Disable floor plans in standalone mode (no backend to upload/serve); drop
  base64 localStorage persistence. See ADR-001 in CLAUDE.md.
- Tests: backend media route, DesignModal floor plan + upload, store floorMap.

ha-relevant: maybe
2026-07-02 16:36:25 +02:00
Pranjal Joshi 8821b05b7b feat: floor plan map, LQI edge coloring, zigbee path highlighting, eslint/test fixes, vite audit fix 2026-07-02 02:00:04 +05:30
Pouzor 1f2d298419 fix: type standalone canvas theme_id as ThemeId
The Docker CI build (tsc -b, stricter than the local tsc --noEmit) rejected
passing the storage theme_id (string) to setTheme/setCustomStyle which expect
ThemeId. Type StandaloneCanvas.theme_id as ThemeId so the standalone load path
matches the theme store API.

ha-relevant: no
2026-06-30 00:58:42 +02:00
Pouzor ca171089c2 feat: support multi-canvas (designs) in frontend-only standalone mode
Standalone mode (VITE_STANDALONE=true) skipped the designs system entirely:
the design list stayed empty (switcher hidden) and all canvases collapsed onto
a single localStorage key. Add a localStorage-backed design layer mirroring the
backend designs API.

- standaloneStorage util: list/create/update/delete designs + per-design canvas
  storage (homelable_designs + homelable_canvas:<id>). ensureSeed migrates a
  legacy single-canvas install into a default design so existing data survives.
- App.tsx: load/seed designs, save + switch canvases per design id in standalone.
- Sidebar.tsx: create/update/delete dispatch to standaloneStorage when standalone.
- Tests for the new storage util.

ha-relevant: no
2026-06-29 23:13:56 +02:00
Pouzor f0af367c34 fix: force export background on the live react-flow element
The html-to-image style-option override did not reliably beat the
react-flow root's colorMode background, so white exports stayed black.
Set the background colour directly on the live element for the duration
of the capture (then restore it), which html-to-image reads when it
clones computed styles.

Refs #165
2026-06-29 01:13:55 +02:00
Pouzor 46185d187f fix: actually apply white export background over react-flow root
The .react-flow root paints its own opaque background via colorMode,
so html-to-image's backgroundColor option (which only fills transparent
areas) stayed black. Override the root background with an inline style
so the chosen export background is visible.

Refs #165
2026-06-29 01:02:00 +02:00
Pouzor fd2c50c1aa fix: white background option for export + Firefox download
Fixes two issues reported in #165:

- Export always produced a black background, making prints with a
  white page wasteful/unreadable. Adds a Dark/White background choice
  in the export modal, threaded through both PNG and SVG export.
- Firefox refused the download because the programmatic anchor was not
  attached to the document. The anchor is now appended before click and
  removed after, which Firefox requires.

Closes #165

ha-relevant: yes
2026-06-29 00:47:39 +02:00
Pouzor 313feb8ceb feat: add SVG export option to canvas export modal
Adds an SVG format choice below the Ultra quality option in the
export modal. PNG quality options still drive raster export; the
new SVG button exports a scalable vector via html-to-image toSvg.

Closes #211

ha-relevant: yes
2026-06-28 23:52:28 +02:00
Pouzor 7fdce6af37 fix: order auto-layout children by parent port number
Dagre orders sibling nodes by node-insertion order and ignores handle
ids, so after auto-layout a host's children were frequently laid out in
the reverse of the ports they plug into. Add a post-pass that keeps
Dagre's X slots but reassigns which child sits in each, sorted by the
parent's bottom-port index, shifting each child's whole subtree by the
same delta so nested nodes follow their parent. Peer-group members are
skipped so the existing peer layout is untouched.

ha-relevant: yes
2026-06-28 01:03:38 +02:00
Pouzor 2a8c9d618b feat: drop new nodes at centre of visible canvas
New nodes previously landed at a fixed canvas origin, often off-screen,
forcing the user to drag them into view. Add a projector (registered by
CanvasContainer inside ReactFlowProvider) that maps the visible-canvas
centre into flow coordinates, and use it for add-node, group rect, text,
Zigbee/Z-Wave imports, and pending-device approval (single + bulk).

ha-relevant: yes
2026-06-28 00:27:18 +02:00
Pouzor 612280e924 feat: show inventory timestamps on Device Inventory tiles
Surface the same lifecycle timestamps on the Device Inventory cards as on the
detail panel. Tiles for devices placed on a canvas show their linked node's
created / last scan / last modified / last seen (correlated by ip or
ieee_address, aggregated across matches: created = oldest, others = newest).
Devices not yet on any canvas fall back to their discovered_at.

Rendered as compact relative times ("2d ago") with the full date on hover, in
a tight two-column footer so the tile keeps its original footprint.

Backend: PendingDeviceResponse gains node_created_at / node_last_scan /
node_last_modified / node_last_seen; the canvas correlation now also pulls node
timestamps in the same single query. Frontend: shared timeFormat util
(absolute + relative), reused by the detail panel.

ha-relevant: maybe
2026-06-27 13:24:29 +02:00
Pouzor c8ed63712d refactor: extract copyToClipboard util with HTTP fallback + tests 2026-06-26 23:51:23 +02:00
Pouzor 5b08d57124 fix: add zwave node types to canvas theme maps
The 6 per-theme NodeType color maps in themes.ts were missing the three
zwave_* keys, breaking the production build (tsc -b) even though the
dev typecheck passed.

ha-relevant: yes
2026-06-26 13:58:30 +02:00
Pouzor 8faf5c1c79 feat: add Z-Wave network scan via MQTT gateway
Import a Z-Wave JS UI (zwavejs2mqtt) network over the MQTT gateway API,
mirroring the existing Zigbee pipeline:

- New Z-Wave Import modal + sidebar entry (broker, prefix, gateway name)
- coordinator/router/end-device typing with mesh tree from node neighbors
- import to Pending section or straight to canvas
- Pending Devices gains a Z-Wave source filter
- shared mqtt_common helpers extracted from the zigbee service

ha-relevant: yes
2026-06-26 11:19:37 +02:00
Pouzor 2a4d109ee6 fix: persist manual node sizes and add width/height inputs
Nested vm/lxc/docker leaf nodes lost their size on reload: the
deserialize restore branch excluded those types entirely, so a
resized child snapped back to content-fit. Gate the restore on
container_mode instead of node type.

Also prefer explicit width/height over the DOM-measured value when
serializing, so a manual resize persists its exact target rather than
drifting to the fractional content-fit size.

Add a Size section to the detail panel with W/H number inputs
(setNodeSize store action, clamped to the resizer minimums). Inputs
resync live when the node is resized by corner drag, without
clobbering active keystrokes.

ha-relevant: yes
2026-06-17 11:55:55 +02:00
Pouzor 7c2417f5a9 fix(security): resolve code scanning alerts
Add least-privilege 'permissions: contents: read' to quality, security
and docker-ci workflows (actions/missing-workflow-permissions).

Harden markdown table cell escaping in exportMarkdown: escape backslash
before pipe and collapse newlines so untrusted values can't break the
table (js/incomplete-sanitization). Add regression tests.

ha-relevant: maybe
2026-06-09 17:15:26 +02:00
Pouzor b52bbc6d9f feat(settings): move Hide IP toggle into Settings modal, persist it
Hide-IP was a sidebar button held only in memory, so it reset on reload.
Moved it into the Settings modal Canvas section and persist it to
localStorage (new ipDisplay util); the canvas store now seeds hideIp from
storage and writes through on toggleHideIp/setHideIp. Settings is now also
reachable in standalone (no-backend) builds, with the backend-only status
interval guarded so the modal still works there.

ha-relevant: yes
2026-06-05 11:26:28 +02:00
Pouzor cc2cc72482 Merge remote-tracking branch 'origin/main' into feat/electrical 2026-06-02 16:10:46 +02:00
Pouzor 12d527aad6 feat: manage canvases with custom name and icon
Make designs (canvases) fully user-manageable: create with a chosen name
and icon, rename, change icon, and delete. Replaces the hardcoded
"New Electrical Design" button with a generic "New Canvas" flow.

- Add Design.icon column + migration that backfills legacy rows
  (electrical -> zap, others -> dashboard)
- DesignModal: name input + curated lucide icon picker (create + edit)
- Sidebar switcher gains per-canvas edit/delete; delete guards the last
  canvas and confirms
- designStore: addDesign/updateDesign/removeDesign with active reassignment
- Fix data loss on design switch: abort load when the save fails and keep
  unsaved edits; skip the save-old step when the previous canvas was deleted
- designsApi create/update carry icon; design_type kept for back-compat

Tests: backend design CRUD (icon + cascade + last-canvas guard), designStore
actions, designIcons resolver, DesignModal create/edit/validation.

ha-relevant: yes
2026-06-02 15:47:57 +02:00
Pouzor bf90d6312b fix(canvas): include MAC property in approved node sent to canvas (#168)
The backend stored the MAC, but the frontend built the approved canvas
node locally with properties=[] (non-zigbee) and no mac field. On save,
canvas/save upserts every node field from the in-memory canvas, so the
empty properties overwrote the DB MAC — the address showed after a
no-save reload but vanished once saved.

Now both approve paths (single + bulk) attach a MAC property row and
the mac field to the node added to the canvas, mirroring the backend
build_mac_property. Hidden by default, toggleable from the right panel.

- new buildMacProperty util + unit tests
- bulk-approve modal test asserts MAC propagation (IP device) and
  absence for a zigbee device

ha-relevant: yes
2026-05-31 16:15:14 +02:00
Pranjal Joshi d5e35a885d ci: fix lint/type errors - missing electrical icons, unused vars, mypy issues 2026-05-31 14:53:38 +05:30
Pranjal Joshi 46435605eb feat: multi-design canvas system with electrical nodes/edges
Backend: New Design model + designs table; design_id FK on nodes, edges, canvas_state; migration seeds default 'Network Topology' design; full CRUD API for designs; canvas load/save accept design_id.

Frontend: designStore (Zustand), design switcher in Sidebar, design-aware canvas load/save, auto-save on design switch.

Electrical node types (14): grid, ups, battery, generator, solar_panel, inverter, circuit_breaker, contactor, electrical_switch, socket, light, meter, transformer, load — icons, registrations, accent colors in all 6 themes.

Electrical edge type: registered in edgeTypes, BASE_STYLES, edgeColors, all theme edgeColors, EDGE_DEFAULT_COLORS.

Bug fixes: data corruption on design switch (stale closure), race condition on save-then-load, missing Zap import, missing Electrical group in NodeModal, missing electrical entries in custom theme edgeColors, inline imports hoisted.
2026-05-31 14:35:22 +05:30
Pouzor 1431f5b19e feat(canvas): raise switch port cap to 64, add port numbers and fibre edge type
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
2026-05-29 17:56:51 +02:00
Rémy dc70805673 Merge pull request #166 from CodeWarrior4Life/pr-laptop-mobile-types
Add laptop and mobile node types
2026-05-29 10:25:35 +02:00
Pouzor 05c24d622b style(themes): align laptop/mobile keys + theme-coherent mobile accents
- Pad laptop:/mobile: to match each theme's existing key-column width
  (dark, light, custom were off by 1-4 spaces).
- Replace hardcoded #ec4899 mobile pink with palette-coherent accents:
  light  -> #db2777 (contrast on light bg)
  neon   -> #ff3399 (neon family)
  matrix -> #00cc66 (green palette)
  default/dark/custom keep #ec4899.
2026-05-29 10:12:04 +02:00
Pouzor 9823be9d78 fix(liveview): apply collapse filter in read-only canvas
LiveView.tsx passed raw nodes/edges from the store straight to
ReactFlow, bypassing the collapse pipeline that CanvasContainer applies
in the editor. Result: a group or zone marked collapsed in edit mode
still showed all its contents on /view?key=... and in standalone live
view. The flag was being persisted and read correctly — only the
read-only canvas ignored it.

Reuse the same memoized computeCollapseInfo + rewireEdgesForCollapse
pipeline. Add two regression tests that load a /liveview payload with
a collapsed group and assert the children never reach ReactFlow.
2026-05-29 09:27:45 +02:00
Pouzor b71c96897a fix(canvas): make collapse reachable + persist for every node type
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.
2026-05-29 08:58:36 +02:00
Pouzor 20e1820a4e refactor(canvas): promote collapsed to first-class NodeData field + edge rewire
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.
2026-05-29 01:34:21 +02:00
Pouzor 517486ff79 perf(canvas): memoize collapse visibility filter + add tests
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.
2026-05-29 01:09:46 +02:00
Cyril Grosse III b5b1056ae6 Add laptop and mobile node types
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.
2026-05-28 15:55:46 -04:00
Pouzor 8b97f578c6 feat(node-modal): sanitize parent_id on type change and exclude descendants
- 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
2026-05-16 17:41:58 +02:00
Pouzor 3a4df578ec feat(node-modal): restore Parent Container selector for child types
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
2026-05-16 17:08:06 +02:00
Pouzor 43426e96ac feat(docker): allow vm and proxmox as parent for docker_container
Extend docker_container parent rule to accept vm and proxmox nodes
in addition to docker_host and lxc, covering nested virtualization
topologies (Docker on a VM, Docker directly on Proxmox host).

ha-relevant: yes
2026-05-16 16:47:14 +02:00
Pouzor 280d5a2ae1 feat(docker): allow LXC as parent for docker_container
Previously docker_container could only attach to a docker_host via
virtual edge. Extend the rule so an LXC node can also act as parent,
enabling docker_container nesting under LXC without requiring an
intermediate docker_host.

Extracts the virtual-edge parent rule into a pure helper
(resolveVirtualEdgeParent) with unit tests covering all type pairs.

ha-relevant: yes
2026-05-15 22:54:51 +02:00
Pouzor ea451885af refactor(ip-parsing): centralize multi-delim splitting in splitIps
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.
2026-05-15 13:36:15 +02:00
Pouzor 5f87c64dcf fix(zigbee): default IEEE/Vendor/Model/LQI props to hidden, preserve user visibility
- New zigbee props (approve + first-time re-import) ship with visible=false
  so the canvas card stays clean. User opts in from the right panel.
- On re-import of an already-approved node, merge instead of overwrite:
  keys already present keep their visible flag (and any user-edited value
  is replaced with the freshly imported one), brand-new keys are appended
  hidden. Non-zigbee custom properties are preserved untouched.
2026-05-14 01:10:45 +02:00
Pouzor 5822a1483a fix(zigbee): inject IEEE/Vendor/Model/LQI props into approved nodes client-side
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.
2026-05-14 00:45:58 +02:00