The Proxmox/Zigbee auto-sync Save tests clicked Save after only
waiting for an unrelated field, so under CI load getConfig could
resolve after the click and Save fired with the default
sync_enabled=false. Wait for the toggle to reflect the loaded
state first.
YAML export wrote per-side handle counts but never the
show_port_numbers toggle, so the option was lost on re-import
while connection points survived.
ha-relevant: maybe
Fixes#272
Setting --error-bg alone had no effect: sonner only honors per-type
surfaces when richColors is enabled, so error toasts kept using
--normal-bg and looked identical to a normal toast. Enable richColors
and pin success/info/warning back to the neutral popover surface so only
errors turn red.
Error toasts previously used the same neutral popover background as
success toasts, so a failure (e.g. a save while the backend is down)
read like a normal 'Canvas saved'. Set sonner's per-type --error-bg/
--error-text/--error-border so errors stand out in red.
Neutralize the app modal backdrop's dim/blur while the tour runs so a
ringed sidebar/toolbar control behind an open modal (steps 2, 10) stays
readable — the overlay's SVG spotlight is now the single source of
dimming. Add a walkthrough-running body class toggled by the overlay and
a CSS rule stripping [data-slot=dialog-overlay] background/backdrop-filter.
Also refine coach-card placement: dialogCardPosition drops the card into
the empty bottom-right corner for large modals (inventory, scan history)
and beside small modals, so it never covers modal content (steps 3, 4).
Bespoke spotlight walkthrough (no new deps) that guides new users through the
main features. A version-stamped invite bubble offers the tour to new users and
re-offers it once to existing users after an upgrade; restartable from Settings.
- Multi-hole SVG-mask overlay keeps a ringed control and any open modal lit
- Steps: scan, scan history, inventory, nodes, edit, text/zone, grouping,
style, imports, and a closing recap with a GitHub link
- Scan/history/inventory/import steps inject demo data (no backend calls) and
are filtered out in standalone, which ends on a full-mode recap
- Persisted invite state via localStorage (utils/walkthrough.ts), live tour
state via zustand (stores/walkthroughStore.ts)
ha-relevant: maybe
Cleared canvas re-showed the demo, and backend errors silently fell back to
demo — hiding real outages and forcing users to wipe demo nodes before bulk
edits. Now:
- backend load reports `initialized` (CanvasState row exists = ever saved)
- decideCanvasLoad() picks real | empty | demo; empty is kept empty
- backend down/error shows an error banner + toast, never the demo
- data-new-user flag reserved as the Getting Started walkthrough hook
ha-relevant: yes
Add native HTML5 drag-and-drop reorder to the property list in the
detail panel. A grip handle appears when more than one property exists;
dragging updates property order (persisted with canvas save) and is
undoable via history snapshot.
Closes#292
ha-relevant: yes
- 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
nmap -sV sends plaintext probes to TLS ports (e.g. Proxmox 8006). They
stall until the host crosses --host-timeout, at which point nmap skips
the whole host and discards ports it already found — a reachable host is
returned with 0 open ports (issue #277).
Split Phase 2 into two passes: Pass A (-sS/-sT --open) discovers ports
fast with no version detection; its ports are authoritative. Pass B
(-sV --host-timeout, scoped to the found ports) enriches banners
best-effort — on timeout/failure the discovered ports are kept with
empty banners instead of losing the host.
Healthy hosts still get full-intensity -sV banners (no quality drop).
Version host-timeout is now configurable via SCANNER_VERSION_HOST_TIMEOUT
(default 60s).
ha-relevant: yes
The add/edit zone modal grew tall enough to overflow the viewport.
Apply the same layout as NodeModal: widen to sm:max-w-3xl and split
fields into a Content column (label, font, text/label position, text
size) and a Style column (colors, border style/width, z-order).
ha-relevant: yes
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
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
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
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
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
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.
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
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
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.
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.
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.
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
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).
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