Commit Graph

686 Commits

Author SHA1 Message Date
Pouzor - Rémy Jardient 15b000d555 Merge pull request #262 from Pouzor/fix/258-match-device-by-ip-token-and-mac
fix: match scanned devices to canvas nodes by ip-token and mac (#258)
2026-07-09 15:12:13 +02:00
Pouzor 2d3b646e45 fix: match scanned devices to canvas nodes by ip-token and mac (#258)
Node.ip stores several comma-separated addresses once a user adds an
IPv6 (e.g. "fe80::1, 192.168.1.5"). All placement/inventory matching did
exact string equality on that field, so a device scanned as the plain
IPv4 looked absent from the canvas: canvas_count stayed 0, the "In N
canvas" badge and hide-on-canvas button vanished, and bulk-approve
re-placed a duplicate node.

Match per-address instead, and add MAC (a stable identifier immune to IP
edits) as a cumulative match key alongside ieee/ip:
- _canvas_correlation indexes ip per token + by_mac
- bulk_approve skip detection tokenizes ip + tracks mac
- find_duplicate_node narrows with Node.ip.contains then confirms per
  token in Python (guards the 10.0.0.4 / 10.0.0.40 substring false match)

ha-relevant: maybe
2026-07-09 14:41:53 +02:00
Pouzor - Rémy Jardient 2bebe8f42d Merge pull request #261 from Pouzor/feat/single-approve-duplicate-prompt
feat: prompt on duplicate device instead of silently blocking/merging approve
2026-07-09 13:54:56 +02:00
Pouzor 0792105b96 feat: prompt on duplicate device instead of silently blocking/merging approve
Single-device approve now guards duplicates per-design the same way bulk
approve does, and asks the user instead of failing or silently merging:

- create_node and approve_device reject a same-design duplicate (ieee, ip
  or mac) with 409 + the existing node; a force flag creates it anyway.
- Frontend shows a confirm dialog: go to existing node, add duplicate
  anyway, or cancel.
- approve_device no longer rejects a device already on another canvas
  (status is global, canvas membership is per-design) — it can be placed
  on a new design, matching bulk approve.
- IEEE (Zigbee/Z-Wave) devices now use the same prompt as ip/mac instead
  of auto-merging into the existing node.
- bulk approve reports which devices it skipped as duplicates.

Closes #260

ha-relevant: maybe
2026-07-09 13:05:19 +02:00
Pouzor - Rémy Jardient 8ebc716de8 Merge pull request #259 from Pouzor/feat/proxmox-resync-and-inventory-fix
feat: manual Proxmox re-sync + env-only connection config + inventory approve fix
2026-07-07 23:21:22 +02:00
Pouzor b09ebb5cd2 fix: keep approved devices in inventory after bulk approve
Bulk/single approve optimistically stripped approved rows from the local list,
but scanApi.pending() still returns them (on-canvas, with an 'In N canvas'
badge). The list went empty until the modal was reopened. Reload after approve
instead of stripping, so approved rows stay visible with a fresh canvas_count.

ha-relevant: yes
2026-07-07 21:23:28 +02:00
Pouzor 9437a74147 feat: manual Proxmox re-sync button + make connection config env-only
Add a 'Re-sync now' button to the Proxmox auto-sync settings section that
triggers an immediate inventory import (POST /proxmox/sync-now) using the
server env config — the manual counterpart to scheduled auto-sync.

Also fix a dual-source-of-truth bug: Proxmox connection config (host, port,
token, verify_tls) is now env-only and never persisted to scan_config.json.
Previously save_overrides() dumped host/port/verify alongside scan settings,
so saving an unrelated setting wrote an empty proxmox_host that load_overrides
then clobbered PROXMOX_HOST with on every boot. Only the auto-sync activation
(sync_enabled + sync_interval) stays user-editable and persisted.

ha-relevant: no
2026-07-07 21:20:13 +02:00
Pouzor - Rémy Jardient 4206d50c70 Merge pull request #257 from Pouzor/feat/canvas-copy-from-existing
feat: create a new canvas by copying an existing one
2026-07-07 16:23:51 +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 - Rémy Jardient 6b591cdd88 Merge pull request #256 from Pouzor/fix/media-path-injection
fix: harden media path handling against path injection
2026-07-07 14:55:27 +02:00
Pouzor 7db57bba3b fix: resolve media file by directory listing to kill path-injection taint
Match the validated filename against iterdir() entries with == instead of
building a path from the user string, so no tainted value ever reaches a
filesystem sink. Satisfies CodeQL py/path-injection.

ha-relevant: no
2026-07-07 14:35:44 +02:00
Pouzor b907c4b05e fix: use re.fullmatch for media filename allowlist so CodeQL recognizes barrier
ha-relevant: no
2026-07-07 14:24:19 +02:00
Pouzor 2d376c2bed fix: harden media path handling against path injection (CodeQL py/path-injection)
Resolve media filenames through a shared _resolve_media_path() barrier that
confirms the resolved path sits directly under the resolved media dir, so
CodeQL can trace the sanitization the regex already guaranteed.

ha-relevant: no
2026-07-07 11:50:12 +02:00
Pouzor - Rémy Jardient 738595be5a Merge pull request #255 from Pouzor/fix/yaml-export-connection-points
fix: preserve edge connection points in YAML export/import (#208)
2026-07-07 11:38:34 +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 - Rémy Jardient 65a1f49f93 Merge pull request #254 from Pouzor/fix/mesh-edges-second-canvas
fix: keep mesh/cluster links so edges resolve onto a second canvas
2026-07-07 10:45:21 +02:00
Pouzor 5b5eabf5db fix: keep mesh/cluster links so edges resolve onto a second canvas
Approving the same zigbee/zwave/proxmox devices onto a second design
placed the nodes but drew no edges. _resolve_pending_links_for_ieee
deleted each pending_device_link after materializing its edge, so the
first approve consumed the whole topology and later approves had nothing
to resolve.

Links are topology, not one-shot: every importer wipes+reinserts its
link set on each import, so they can safely persist across approvals.

- keep the link rows (drop both db.delete(link) calls)
- scope resolution to the target design (Node.ieee_address + design_id)
  so a re-approve links that canvas's nodes, not another's
- thread design_id through all three approve call sites

Existing links deleted by the old code do not come back on their own;
a re-import repopulates them.

ha-relevant: maybe
2026-07-07 10:32:47 +02:00
Pouzor - Rémy Jardient 601f731135 Merge pull request #253 from Pouzor/feat/proxmox-import
feat: import hosts/VMs/LXC from Proxmox VE with optional auto-sync
2026-07-07 09:48:05 +02:00
Pouzor 3d25fcaae2 update doc 2026-07-07 00:53:06 +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 9670d0a86a feat: proxmox import diagnostics, node style fix, and cluster edges
Scan History:
- Proxmox is now a first-class scan kind (badge, filter chip, Server icon,
  completion toast) instead of being mislabeled as an IP scan.
- A done run carrying a non-fatal advisory renders amber (info) with a
  warning toast, distinct from red failures.

Import diagnostics:
- test-connection probes /access/permissions and warns when the API token
  has no ACL (VMs/LXC would be invisible) — points at the PVEAuditor grant.
- import surfaces an advisory when hosts import but no guests are visible
  (privilege-separated token whose rights are the intersection with the user),
  rather than a silent "done".

Node style:
- Proxmox container mode is now opt-in (container_mode === true), matching the
  rest of the codebase (App.tsx nesting logic). Imported proxmox nodes leave
  the flag unset and render like a manually-created node instead of an empty
  group container.

Cluster edges:
- Hosts from one import are chained with 'cluster' edges via left/right handles,
  distinct from the vertical host->guest 'virtual' edges. Wired for both the
  direct "Add to Canvas" path and the pending -> approve path (host<->host
  proxmox_cluster links, resolved to cluster edges on approve; cluster hosts
  get left/right handles).

Tests added on both sides.

ha-relevant: maybe
2026-07-06 10:26:52 +02:00
Pouzor abefc42fdf fix: tolerate legacy NULL properties on pending devices
The pending_devices.properties column is added by an idempotent migration, so
existing rows have properties = NULL. PendingDeviceResponse typed it as a list,
so GET /scan/pending 500'd on any pre-existing device.

- Coerce NULL/non-list properties to [] in PendingDeviceResponse.
- Backfill existing NULL rows to '[]' in init_db migrations.
- Regression test: /scan/pending returns 200 with a legacy NULL-properties row.

ha-relevant: maybe
2026-07-06 00:01:27 +02:00
Pouzor ab36ba6f81 feat: import hosts/VMs/LXC from Proxmox VE with optional auto-sync
Add a Proxmox VE importer that reads the /api2/json REST API with a read-only
API token and drops hosts (proxmox), VMs (vm) and LXC containers (lxc) onto the
canvas as typed nodes with run state and hardware specs (vCPU/RAM/disk).

- Backend: proxmox_service (httpx) + proxmox routes (test-connection, import,
  import-pending, config). Two-tier dedupe — merge onto an existing scanned node
  by IP, else synthetic pve-{host}-{vmid} identity. Update-in-place, never
  deletes. Host->guest rendered as a 'virtual' edge via the pending-link flow.
- Security: token is env-only (PROXMOX_TOKEN_*), never written to disk by the
  app, never returned by any endpoint; errors are credential-sanitized.
- Auto-sync: optional scheduled re-import into pending (APScheduler job).
- PendingDevice.properties carries specs through approve (+ migration).
- Frontend: ProxmoxImportModal, sidebar entry, pending inventory source filter,
  Settings auto-sync section, proxmoxApi client.
- Docs: docs/proxmox-import.md, README + FEATURES sections, .env.example keys.
- Tests: backend service/router/scheduler, frontend modal/client/pending.

ha-relevant: maybe
2026-07-05 18:58:12 +02:00
Pouzor - Rémy Jardient 1d40d70150 Merge pull request #252 from Pouzor/feat/configurable-edge-line-style
feat: configurable edge line style + width per type and per edge
2026-07-05 15:19:54 +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 - Rémy Jardient 40ec26ab7e Merge pull request #250 from Pouzor/feat/edge-arrowheads
feat: edge endpoint marker shapes + fix parallel edges not rendering
2026-07-05 11:59:45 +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 - Rémy Jardient ae2d3e1eab Merge pull request #249 from Pouzor/feat/customizable-connection-points-per-side
feat: customizable connection points per side (#243)
2026-07-04 15:32:05 +02:00
Pouzor e8530ad3db feat: shortcut from node Appearance to canvas-wide type style
Adds a link under the colour swatches in the add/edit node modal that opens the
Custom Style editor with the node's type preselected, so a user can edit the
style for every node of that type without navigating Style -> Customize -> pick
type manually.

- CustomStyleModal: new optional `initialNodeType` prop; when set, the modal
  opens straight into that node type's editor instead of the empty placeholder.
- NodeModal: new optional `onEditTypeStyle(type)` prop renders the shortcut link
  (hidden for group/groupRect and when no handler is wired).
- App: `styleEditorType` state; both Add and Edit NodeModals forward the handler,
  and a standalone CustomStyleModal instance opens preselected (stacked over the
  node modal so in-progress node edits are preserved).

ha-relevant: yes
2026-07-04 15:14:33 +02:00
Pouzor e39ab7d530 fix: restore connection-handle magnet area (unclip node root)
The overflow-hidden added to the BaseNode root (to tame the oversized 16px
NodeResizer handles) also clipped the connection handles, which sit centred on
the node edge with half their box outside it. CSS overflow:hidden disables
pointer events in the clipped region, so the outer half of every handle — the
side an edge approaches from — became non-interactive, halving the magnet area.

- Remove overflow-hidden from the BaseNode root so handles stay fully grabbable.
- Shrink the NodeResizer handles 16px -> 8px (rounded): the reason the clip was
  added in the first place, so the resizer no longer looks oversized when zoomed
  out without relying on clipping.
- Enlarge the invisible target (magnet) hit area 12px -> 20px.
- Set connectionRadius={30} on ReactFlow for stronger distance-based snapping.

ha-relevant: yes
2026-07-04 12:59:08 +02:00
Pouzor 738eac3ebb fix: restore node overflow-hidden (oversized resize handles)
Removing overflow-hidden from the BaseNode root un-clipped the NodeResizer
handles, making them huge (especially when zoomed out). Clipping was never what
made left/right connection points fail — that was backend persistence (fixed
separately) — and top/bottom handles have always worked while clipped. Restore
overflow-hidden so the resizer looks correct; side handles still work.

ha-relevant: yes
2026-07-04 01:47:16 +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 6302c43e06 fix: persist top/left/right connection-point counts (issue #243)
The backend only stored bottom_handles, so top/left/right_handles were dropped
on canvas save and reset to defaults on reload — a left/right snappoint would
vanish after reload.

- models.py: add top_handles (default 1), left_handles (0), right_handles (0)
- database.py: ALTER TABLE migrations for the three columns
- schemas: add fields to NodeBase, NodeUpdate, and the canvas-save node schema
- tests: save+reload round-trip and default-fallback coverage

ha-relevant: no
2026-07-04 00:04:24 +02:00
Pouzor 63e664efdd feat: widen node modal, two-column layout, unclip side handles
- NodeModal: widen to max-w-3xl; split into Information (left) and Design
  (right) columns with headers; Notes moved to left as a resizable textarea
- Connection Points: spatial cross of -/N/+ steppers around a node preview
  (replaces the four stacked sliders)
- BaseNode: drop overflow-hidden from the node root so left/right handles are
  no longer clipped to an ungrabbable sliver (inner sections keep their own
  clipping; rounded corners preserved)

ha-relevant: yes
2026-07-04 00:03:09 +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 - Rémy Jardient c9a35b730d Merge pull request #248 from Pouzor/docs/features-md
docs: add user-facing FEATURES.md
2026-07-03 14:51:24 +02:00
Pouzor d573cffe0e docs: link README to FEATURES.md before install section 2026-07-03 14:44:37 +02:00
Pouzor b59476f2f9 docs: add user-facing FEATURES.md
Feature guide with table of contents: one line per feature plus how to
enable and use it. Covers zones, groups, multiple canvases, style,
floor plan, scanner, zigbee/zwave import, device inventory, live status,
export, live view, gethomepage, MCP, settings.

Untrack FEATURES.md (was ignored as internal meta) now that it ships as
public docs.

ha-relevant: no
2026-07-03 14:30:48 +02:00
Pouzor - Rémy Jardient 9b40ba0b7c Merge pull request #247 from Pouzor/fix/mesh-import-duplicate-nodes
fix: mesh (zigbee/zwave) import — duplicate-node crash + coordinator to pending
2026-07-03 12:16:47 +02:00
Pouzor 6f82c15c69 fix: backfill inventory row for on-canvas mesh devices missing one
A device already on a canvas (Node exists) but with no pending_devices row
never showed in the discovery inventory — the inventory lists pending_devices,
not nodes. This stranded legacy auto-placed coordinators: on a canvas yet
invisible in the inventory list.

Both mesh imports now, in the already-approved-node branch, ensure an
inventory row exists: create one as status="approved" when missing, or
refresh its metadata (preserving status) when present. New/unplaced devices
still land as status="pending"; hidden rows stay hidden.

Tests updated: approved-node path now backfills an approved inventory row
(zigbee + zwave), plus a regression that a hidden inventory row is not
revived.

ha-relevant: yes
2026-07-03 12:08:53 +02:00
Pouzor 1de6e91ba3 feat: colour device-inventory role badge by node-type accent
The role/type badge on each inventory tile was always flat grey
(bg-[#21262d] text-muted-foreground), which read as disabled and gave no
visual cue about the device kind. Colour it with the same per-type accent
the node uses on the canvas — resolved through the active theme / style
section via resolveNodeColors — so a zigbee_coordinator, zwave_router, etc.
gets its style-section colour (translucent background + solid text).

Applies to every source (IP, zigbee, zwave) since all node types carry an
accent in the theme.

Test: role badge renders with the node-type accent colour, not the grey
muted class.

ha-relevant: yes
2026-07-03 11:13:18 +02:00
Pouzor 3f6e9b00f7 fix: send zigbee/zwave coordinator to pending inventory, not auto-canvas
The coordinator was special-cased in both mesh imports to auto-create a
canvas Node, so it never appeared in the pending inventory and users could
not approve/hide/type it like every other device.

Remove the auto-placement: the coordinator now flows through the shared
pending path (upsert into pending_devices with suggested_type
zigbee_coordinator / zwave_coordinator). An already-approved coordinator
Node still gets its properties refreshed on re-import via the shared
approved-node path. The response's coordinator/coordinator_already_existed
fields are retained (now always unset) for backward-compatible shape; the
frontend already ignored them.

Tests updated: coordinator lands in pending (counts include it), no Node
auto-created, pending metadata carried, approved coordinator refresh + no
pending re-list.

ha-relevant: yes
2026-07-03 02:43:42 +02:00
Pouzor 60383bee64 fix: tolerate same device on multiple canvases in zigbee/zwave import
Zigbee and Z-Wave imports looked up canvas nodes by ieee_address with
scalar_one_or_none(), assuming one node per IEEE globally. A device placed
on two designs (one Node per canvas — a supported feature) made re-import
crash with MultipleResultsFound.

- Both mesh imports now refresh properties on every matching node instead
  of a single row (loop over .scalars().all()).
- approve_device guards against a true duplicate: same IEEE already on the
  SAME design reuses that node instead of inserting a second one.
- New node_dedupe service: loss-free repair keyed on (ieee, design_id).
  Collapses only genuine same-canvas duplicates (merges properties/services/
  missing fields, re-points edges + parent_id, drops self-loops/parallel
  edges). Cross-design placements preserved. Runs at start of both imports
  and bulk-approve. No-op on healthy DBs.
- IP correlation path already handled multiple nodes; left unchanged.

Tests: dedupe unit tests (collapse, cross-design preservation, edge/parent
re-point, idempotent), zigbee + zwave multi-canvas regression, approve
no-dupe guard.

ha-relevant: yes
2026-07-03 01:52:56 +02:00
Pouzor - Rémy Jardient b0cf0deab0 Merge pull request #207 from pranjal-joshi/pranjal/floorplan
feat: floor plan map, LQI edge coloring, zigbee path highlighting
2026-07-03 00:48:39 +02:00
Pouzor bdde03cdfa feat: floor plan selection handles, dbl-click edit, bottom z-index
- Resize handles only render when the plan is selected (click to select,
  click outside to deselect); previously always shown when unlocked.
- Double-clicking an unlocked plan opens the canvas edit modal (via a
  floorMapEditNonce signal the Sidebar watches).
- Floor plan always sits at the bottom of the canvas (z-index -1), behind
  nodes and edges, locked or not.
- Remount the edit modal on every open (key bump) so it re-seeds from the
  current floor plan; fixes Save clobbering a canvas-side resize/move with
  stale modal dimensions.
- Drop the no-op history snapshot on floor-plan edits (floorMap isn't part
  of undo history).
2026-07-03 00:22:53 +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 046c99e219 fix: null -> undefined for StandaloneCanvas.floorMap 2026-07-02 10:33:57 +05:30