Commit Graph

741 Commits

Author SHA1 Message Date
Pouzor a364a95cb5 fix(search): clean up node/property search matches
- add tests for notes, property key/value, hidden-property exclusion, matched-value display
- fix flatMap indentation and stray semicolon
- use stable match key instead of display_value (avoids dup-key collisions)
- keep label searchable but not repeated in matched-values column
2026-07-18 01:17:36 +02:00
Peter Becker 9000f82213 add search for nodes and properties 2026-07-17 23:00:20 +02:00
Pouzor - Rémy Jardient 93e9b68821 Merge pull request #289 from nicolabottini/feat/autosave-canvas
feat: autosave canvas after configurable inactivity delay
2026-07-17 20:41:12 +02:00
Pouzor d63c9e6451 fix(canvas): drive autosave debounce off an edit counter
useAutosave reset its debounce whenever the nodes/edges array reference
changed. Live status polling (setNodeStatus) returns a fresh nodes array
on every message without dirtying the canvas, so during active
monitoring the status churn kept re-arming the timer faster than the
delay and the silent save could be starved indefinitely — the user
thinks autosave ran, but it never fired. Select-only changes reset it
the same way.

Add a monotonic editSeq counter that bumps only on real user edits. The
store's set() is wrapped to increment editSeq whenever an update flips
hasUnsavedChanges to true, so it stays centralised instead of touching
every mutating action. onNodesChange/onEdgesChange now set the dirty
flag only when a genuine edit occurred (not on select/measure), so those
no longer bump the counter. App keys the autosave changeSignals on
[editSeq] instead of [nodes, edges].

Add a regression test asserting editSeq bumps on add/update but not on
setNodeStatus, select, or initial dimensions measure.

ha-relevant: maybe
2026-07-17 20:29:48 +02:00
Pouzor 71ab3bf391 fix(canvas): stop non-edits from marking the canvas unsaved
Autosave saves whenever hasUnsavedChanges is set, so any source that
dirties the canvas without a user edit turns into a spurious silent
save. Two such sources:

1. Live status updates. useStatusPolling routed backend status pushes
   through updateNode, which always sets hasUnsavedChanges. With the
   60s status cycle, an idle opted-in tab saved itself every cycle and
   could clobber edits made elsewhere with its stale in-memory canvas.
   Add setNodeStatus, a live-overlay action that updates a node's
   status/response_time/last_seen without dirtying (mirrors
   setServiceStatuses), and route polling through it.

2. Initial dimensions measure. onNodesChange treated React Flow's
   first-measure `dimensions` change as an edit, marking a freshly
   loaded canvas dirty before any interaction — autosave then saved on
   every load/switch. Only count a `dimensions` change as an edit when
   it is a real resize (resizing === true).

Add regression tests: setNodeStatus updates status without dirtying;
onNodesChange ignores initial-measure dimensions and select-only
changes but still dirties on a user resize.

ha-relevant: maybe
2026-07-17 13:02:14 +02:00
Pouzor 9c0ed32485 Merge remote-tracking branch 'origin/main' into feat/autosave-canvas
# Conflicts:
#	frontend/src/App.tsx
2026-07-17 11:58:56 +02:00
Pouzor 8d752e0bd8 fix(canvas): gate autosave on canvas provenance, not selection
The previous guard compared the pinned design against the live
activeDesignId (the selection). On a design switch the selection flips
synchronously while the new canvas loads asynchronously, so the
on-screen nodes briefly still belong to the previous design — arming the
timer with the newly-selected id would save those stale nodes under the
wrong design. Track the design the canvas was actually loaded as
(loadedDesignId provenance) and gate autosave on that instead: pin it
when the timer is armed and re-check the live provenance at fire time,
skipping the save if a different canvas has since loaded.

Also validate the persisted `enabled` flag by type (mirror of `delay`),
and add the matching regression tests (provenance switch skip, enabled
type validation).

ha-relevant: maybe
2026-07-17 11:43:42 +02:00
Pouzor 310b9cb3fd fix(canvas): guard autosave against design-switch clobber, add tests
Extract the debounced autosave into a useAutosave hook. Pin the active
design id when the timer is armed and re-check it at fire time: if the
user switched designs while the timer was pending, the in-memory canvas
belongs to a different design, so skip the save instead of writing it
under the wrong design id. Also skip when there is no active design.

Add unit tests for autosaveSettings (persistence, validation, pub/sub)
and useAutosave (debounce, enable/unsaved/design guards, switch skip).

ha-relevant: maybe
2026-07-17 11:16:56 +02:00
Pouzor - Rémy Jardient fceefe6d4d Merge pull request #290 from Pouzor/fix/undo-auto-layout-and-yaml-import
fix: allow undo after Auto Layout and YAML import
2026-07-16 19:39:40 +02:00
Pouzor ba4f8b0be4 fix(canvas): allow undo after Auto Layout and YAML import
Auto Layout and YAML import routed through loadCanvas, which wipes
past/future history and clears the unsaved flag (design-switch
semantics). Undo was therefore impossible after either action (#280).

Add an in-place applyLayout store action that snapshots history, marks
the canvas unsaved and re-fits the view without discarding undo history,
and wire both Auto Layout and YAML import to it.

ha-relevant: yes
2026-07-16 19:29:41 +02:00
Nicola Bottini a7b7327ce5 feat: autosave canvas after configurable inactivity delay
Adds an opt-in autosave feature (disabled by default) that silently
saves the canvas after a configurable period of inactivity.

- frontend/src/utils/autosaveSettings.ts (new) — localStorage-backed
  setting {enabled, delay} with pub/sub CustomEvent pattern (mirrors
  alignmentSettings.ts). Key: homelable.autosave.
- frontend/src/App.tsx:
  * handleSave accepts optional { silent?: boolean } — skips the success
    toast when called from autosave to avoid noise on every periodic save
  * autosave state (useState + subscribeAutosaveSettings)
  * debounce useEffect: resets on nodes/edges change; when quiet for
    autosave.delay seconds and hasUnsavedChanges is true, calls
    handleSaveRef.current silently
- frontend/src/components/modals/SettingsModal.tsx:
  * Canvas section: Autosave canvas toggle + Save after delay selector
    (3/5/10/30/60 s). Changes persist immediately to localStorage via
    writeAutosaveSettings + propagate cross-tab via CustomEvent.

Default: disabled — no behaviour change for existing users.
Manual Ctrl+S and the Save button continue to work normally.
Errors always show a toast regardless of the silent flag.
2026-07-16 12:22:18 -04:00
Pouzor - Rémy Jardient 6ae8b91934 Merge pull request #288 from Pouzor/fix/edge-waypoints-follow-dragged-node
fix(canvas): move edge waypoints when the connected node or its container is dragged
2026-07-16 17:36:24 +02:00
Pouzor 3cab115a80 fix(canvas): move edge waypoints when the connected node or its container is dragged
Waypoints are stored as absolute canvas coords, so they stayed put when a
connected node was dragged, kinking a previously clean route. A dragged
container moved its children on screen too (their stored parent-relative
position is unchanged), so their edges were missed entirely.

onNodesChange now translates the waypoints of every edge touching a moved
node by that node's delta, propagating a container's delta to all
descendants and translating once when both endpoints ride along.

ha-relevant: yes

Fixes #279
2026-07-16 17:24:52 +02:00
Pouzor - Rémy Jardient 9e088ca7ac Merge pull request #281 from nicolabottini/feat/mcp-approve-delete-design
feat(mcp): design_id on approve_device + delete_design tool
2026-07-16 17:04:25 +02:00
Pouzor - Rémy Jardient 6cc8399dca Merge pull request #287 from Pouzor/fix/container-host-edit-height-reset
fix(canvas): keep container host height when editing an unrelated field
2026-07-16 17:02:18 +02:00
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 - Rémy Jardient d5cbdce870 Merge pull request #286 from nicolabottini/fix/container-host-edit-child-positions
fix(canvas): keep container children in place when editing a host node
2026-07-16 16:11:46 +02:00
Nicola Bottini 15b7ce4481 fix(canvas): keep container children in place when editing a host node
Editing any field on a container-mode host node (Proxmox/VM/LXC/docker)
collapsed all of its nested children into the top-left corner.

Root cause: the node modal always includes container_mode in its update
payload, and handleUpdateNode gated setProxmoxContainerMode on the field's
presence rather than an actual change. So every edit (e.g. changing the
icon) re-fired the container ON transition, which re-applied the
absolute->relative position conversion to children that were already
parent-relative -- piling them up in a corner.

- App.tsx: only call setProxmoxContainerMode when container_mode actually
  changes (compare against the pre-update value).
- canvasStore: make setProxmoxContainerMode per-child idempotent so a
  redundant ON/OFF call can never re-convert an already-nested child's
  position.
- add regression test for the idempotent ON transition.
2026-07-14 19:32:29 -04:00
Nicola Bottini 8df7f682e0 feat(mcp): add delete_design tool
Wires DELETE /api/v1/designs/{id} into the MCP layer.
The backend already implements the endpoint and prevents
deleting the last remaining design.

- mcp/app/tools.py: Tool schema + _dispatch case
- mcp/tests/test_tools.py: 2 new tests (dispatch + schema)
30/30 tests passing.
2026-07-13 15:15:10 -04:00
Nicola Bottini 30619caf0f feat(mcp): add design_id to approve_device tool
The backend approve endpoint already accepts design_id via NodeCreate,
but the MCP schema didn't expose the field so callers couldn't target
a specific canvas when approving a discovered device. Add _DESIGN_ID_FIELD
to approve_device so AI clients (and the MCP bridge) can pass it through.
2026-07-13 15:15:10 -04:00
Pouzor - Rémy Jardient 47e144ac1f Merge pull request #275 from Pouzor/fix/mcp-readme-transport-http
docs: fix MCP client transport to http (was sse)
2026-07-12 00:07:49 +02:00
Pouzor 5fada99e25 docs: fix MCP client transport to http (was sse)
Server uses Streamable HTTP (StreamableHTTPSessionManager), but README
told users to register the client with --transport sse / "type": "sse".
An SSE client waits for an endpoint event the server never emits, so
Claude Code reports "Failed to connect" despite a healthy container.

Switch all client config snippets to http transport and point at the
trailing-slash /mcp/ URL to avoid the 307 redirect that can drop the
auth header.

Fixes #274

ha-relevant: no
2026-07-12 00:04:59 +02:00
Pouzor - Rémy Jardient 2e4b353fcd Merge pull request #273 from MikeSviblov/fix/mcp-list-pending-status-filter
fix: list_pending_devices MCP tool leaked approved/hidden inventory rows
2026-07-11 23:54:46 +02:00
Pouzor d0e5c105ec feat: add inventory, hidden-device and restore MCP tools
The status filter on list_pending_devices closed the leak but left the
MCP surface unable to see anything beyond pending: approved inventory
and hidden devices were no longer reachable, and a wrongly hidden device
could not be recovered through the MCP at all.

Add three tools mirroring existing backend endpoints:
- list_inventory      GET  /scan/pending  (pending + approved, optional status filter)
- list_hidden_devices GET  /scan/hidden
- restore_device      POST /scan/pending/{id}/restore  (undo hide_device)

Bulk approve/hide/restore and scan-config endpoints are intentionally
left out: bulk mutation is the same blast radius that caused the mass-hide
incident this PR addresses.

Tests: 5 new (47 passed total).
2026-07-11 23:40:03 +02:00
Mike Sviblov 9a03097f80 fix: list_pending_devices MCP tool leaked approved/hidden inventory rows
The backend GET /scan/pending endpoint intentionally returns the whole
inventory — approved devices stay listed so the frontend can show the
canvas-presence badge. The MCP tool proxied that response verbatim while
its description promises devices "not yet approved or hidden", so MCP
clients saw every approved device as if it were still awaiting triage
(59 "pending" entries on a fully-triaged canvas, 58 of them approved).

Filter the tool response to status == "pending", keeping legacy rows
that predate the status field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udrvtwi3cqcxUNHZtBHkBg
2026-07-11 15:17:45 +03:00
Pouzor - Rémy Jardient 228f2a67e4 Merge pull request #271 from Pouzor/release/3.0.0
chore: bump version to 3.0.0
v3.0.0
2026-07-10 17:37:43 +02:00
Pouzor 9331bc43cb chore: bump version to 3.0.0
Release 3.0.0. See CHANGELOG.md for full details.
2026-07-10 17:06:20 +02:00
Pouzor - Rémy Jardient f98a55dca0 Merge pull request #270 from Pouzor/feat/zigbee-zwave-autosync
feat: scheduled auto-sync for Zigbee & Z-Wave imports
2026-07-10 16:20:02 +02:00
Pouzor 5139daef90 fix: satisfy mypy in _run_mesh_sync branch typing
CI mypy flagged the shared payload/background locals in _run_mesh_sync
as incompatibly typed across the zigbee/zwave branches. Annotate them
with the union / generic Callable and alias the per-branch
env_import_request imports so the two bindings don't clash.
2026-07-10 15:58:47 +02:00
Pouzor d3728f4108 fix: widen Settings modal to two columns; env cleanup
- SettingsModal: widen to sm:max-w-3xl with max-h-[90vh] scroll and a
  two-column grid so it no longer overflows the viewport. Left column =
  status/service checks + canvas prefs; right column groups all auto-sync
  config (Zigbee, Z-Wave, Proxmox).
- .env.example: drop ZIGBEE_/ZWAVE_ SYNC_ENABLED + SYNC_INTERVAL — the
  auto-sync activation is configured in the Settings modal (persisted to
  scan_config.json), same as Proxmox; connection config stays env-only.

ha-relevant: no
2026-07-10 15:45:46 +02:00
Pouzor b99450db2f feat: scheduled auto-sync for Zigbee & Z-Wave imports
Mirror the Proxmox auto-sync pattern for the Zigbee2MQTT and Z-Wave JS
UI mesh imports. Connection config + MQTT credentials live in .env only,
are never persisted to scan_config.json, and are never returned by any
API or shown in the UI (single source of truth).

- config: ZIGBEE_* / ZWAVE_* env settings; only sync_enabled+interval
  are persisted, connection/credentials stay env-only
- routes: GET/POST /config, POST /sync-now; auto-sync reuses the exact
  manual _background_*_import + _persist_pending_import path (fresh
  import when empty, update-in-place when nodes exist, ScanRun trace)
- scheduler: zigbee_sync / zwave_sync jobs with live enable + reschedule
- frontend: reusable MeshAutoSync section in Settings (Zigbee, Z-Wave)
- .env.example: documented both blocks
- tests: scheduler jobs, router config/sync-now/auth, credential-never-
  persisted, SettingsModal sections

Manual Zigbee/Z-Wave import behaviour is unchanged.

ha-relevant: no
2026-07-10 14:51:01 +02:00
Pouzor - Rémy Jardient a90ca2f039 Merge pull request #269 from Pouzor/fix/proxmox-autosync-scan-history
fix: record ScanRun for scheduled Proxmox auto-sync
2026-07-10 13:42:16 +02:00
Pouzor ad4aa4aba4 fix: record ScanRun for scheduled Proxmox auto-sync
The scheduled auto-sync job called _persist_pending_import directly and
never created a ScanRun, so auto-imports left no trace in Scan history
(unlike manual /sync-now and /import-pending, which both record a run).

Create a ScanRun(kind=proxmox) and delegate to the shared
_background_proxmox_import flow — fetch, persist, mark the run
done/error, and broadcast the inventory-reload signal.

ha-relevant: maybe
2026-07-10 12:47:16 +02:00
Pouzor - Rémy Jardient c4cb44709d Merge pull request #268 from Pouzor/refactor/frontend-test-consolidation
test: consolidate and restructure frontend tests
2026-07-10 10:26:24 +02:00
Pouzor 24e755a097 docs: add frontend test infra README (factories, mocks, split conventions)
ha-relevant: yes
2026-07-10 03:04:47 +02:00
Pouzor 075f7f6d78 test: dedup sonner mocks and close Toolbar seam-mock gaps
- Replace 10 inline sonner toast mocks with the shared mockSonner() builder.
- Toolbar: add tests for the store-driven undo/redo disabled state and the
  unsaved-changes dot — behaviour the full-store mock previously left
  unasserted.

ha-relevant: yes
2026-07-10 02:59:38 +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 64c48de1d1 test: add shared frontend test infra (factories, mocks, render)
Foundation for the frontend test consolidation (mirrors backend #267):
- src/test/factories.ts: canonical makeNode/makeEdge/makeNodeData/makeDesign
- src/test/mocks/: reusable mockSonner/mockReactFlow/makeUseCanvasStore builders
  consumed via async vi.mock dynamic-import to survive hoisting
- src/test/render.tsx: renderWithProviders wrapping Tooltip + ReactFlow providers

Migrate ScanConfigModal sonner mock as canary. No behavior change.

ha-relevant: yes
2026-07-10 02:15:49 +02:00
Pouzor - Rémy Jardient 012ba9cb97 Update README.md 2026-07-10 01:41:51 +02:00
Pouzor - Rémy Jardient 6644a208bb Update README.md 2026-07-10 01:39:42 +02:00
Pouzor - Rémy Jardient 54647413c4 Update README.md 2026-07-10 00:53:16 +02:00
Pouzor - Rémy Jardient 93a50cf144 Merge pull request #266 from nicolabottini/feat/mcp-design-targeting
feat: MCP design/canvas targeting + auto-position + auto-edge-handles
2026-07-10 00:46:53 +02:00
Pouzor b7985306cd test: cover auto-position and auto-edge-handle; fix negative-cell clamp
Add backend API tests for the two issue #265 features that shipped
untested: auto-positioning root nodes into a free grid slot (first at
origin, collision avoidance, child origin default, explicit coords and
explicit zero preserved) and auto-assigning edge handles from absolute
canvas Y (source above/below/equal target, partial handle fill, child
abs-Y resolved through parent).

Drop the max(0, round()) clamp in _find_free_position: it folded
negative-positioned nodes onto cell (0,0), falsely blocking or freeing
the origin slot. Negative nodes now keep their true cells and never
intersect the positive search space.
2026-07-10 00:16:32 +02:00
Nicola Bottini 27e18f1c96 feat: auto-position nodes and auto-assign edge handles on create
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 00:02:11 +02:00
Nicola Bottini 2d00be71bb feat: add create_design tool so the MCP can create canvases
Wrap POST /api/v1/designs so AI clients can create a new design (canvas) and get its id back for use as design_id. Completes the create-and-target canvas workflow alongside list_designs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 23:58:28 +02:00
Nicola Bottini 6e3d45fddc feat: let the MCP target a specific design/canvas
Add an optional design_id argument to create_node, create_edge and get_canvas so AI clients can read and populate a canvas other than the first design. Add a list_designs tool wrapping GET /api/v1/designs so those IDs are discoverable.

Backward compatible: omitting design_id preserves the existing first-design fallback in the backend (nodes.py / edges.py / canvas.py).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 23:58:28 +02:00
Pouzor - Rémy Jardient fd3a3f1da6 Merge pull request #267 from Pouzor/refactor/unify-test-auth-fixture
test: refactor backend test suite for quality and consolidation
2026-07-09 23:39:27 +02:00
Pouzor 9205f9d36e test: close coverage gaps hidden by seam mocking
Audit of the heavy-mock suites (scanner/status_checker/scheduler)
confirmed the mocking is sound layered seam-mocking, not brittle
internal patching. It did hide real gaps where a primitive was always
mocked and never exercised:

- status_checker._http_get: add status-code interpretation tests
  (2xx/4xx online, 5xx offline) + service-check exception -> offline.
  Module coverage 95% -> 100%.
- scheduler.reschedule_status_checks / reschedule_service_checks were
  wholly untested; add validation + guard tests. Add _run_proxmox_sync
  happy-path and error-swallow tests. Module coverage 76% -> 91%.

639 passed.

ha-relevant: no
2026-07-09 22:14:36 +02:00
Pouzor 991169ba5a test: consolidate migration tests into a migrations/ subpackage
Group the three upgrade-path suites (legacy->designs, hardware->
properties, pre-migration DB backup) under tests/migrations/ with
clearer names. Pure relocation via git mv; each keeps its own
self-contained fixtures. No behavior change; 627 passed.

ha-relevant: no
2026-07-09 21:51:48 +02:00
Pouzor b513fa6f4e test: split monolithic test_scan into a scan/ subpackage
Break the 1897-line test_scan.py into topic modules under tests/scan/
(routes, approve, properties, run) sharing fixtures via a package
conftest.py and pure builders via helpers.py. Same 80 tests, no
behavior change; full suite 627 passed.

ha-relevant: no
2026-07-09 21:41:49 +02:00