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
- 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
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
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
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
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
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
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
- 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).
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
- 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
Z-Wave (controller/router/end device) types existed in the model, theme
accents, default icons and the custom-style editor, but were missing from
the node Add/Edit type selector, so they could not be placed manually like
Zigbee nodes. Add a Z-Wave group and default mesh-radio nodes to no status
check (not IP-reachable), matching Zigbee behaviour.
The `typecheck` script ran `tsc --noEmit` against the root tsconfig, which has
`files: []` and only project references — so without `-b` it checked nothing in
src and let real type errors (e.g. theme_id) reach Docker CI. Switch to `tsc -b`
(same as the build) so pre-commit and the Quality job catch them locally.
ha-relevant: no
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
The "View" link opens the read-only live view of the canvas. In frontend-only
standalone mode the editor already renders the only (localStorage) copy, so the
live view adds nothing — hide the link. Kept in full mode.
ha-relevant: no
The Total/Online/Offline counts reflect live status-check results, which
require a backend scanner. In frontend-only standalone mode there is nothing to
populate them, so hide the footer (kept in full mode and live view).
ha-relevant: no
Two gaps surfaced after adding per-design localStorage storage:
- Live view (/view) still read the legacy bare `homelable_canvas` key, which
no longer exists once canvases are keyed per design — the read-only tab
rendered empty. Now passes the active design id (?design=<id>) and reads that
design's canvas, falling back to the first design.
- The Settings modal was gated out entirely in standalone, leaving the Settings
button dead even though canvas prefs (snap, hide-IP) work without a backend.
Mount it in standalone; only the backend status-check section stays hidden.
ha-relevant: no
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
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
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
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
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
create_node/create_edge persisted rows with design_id=null when the
client omitted it (the MCP write tools), so they existed in the DB but
never rendered on the canvas until a container restart reconciled them.
Both routes now fall back to the first design, matching bulk-approve.
Also fix MCP resource reads (homelable://canvas, homelable://edges):
the framework passes a pydantic AnyUrl, not a str, which raised
"'AnyUrl' object has no attribute 'startswith'". Coerce to str.
EdgeResponse now exposes design_id for symmetry with NodeResponse.
ha-relevant: no
Stop button had no effect: run_scan only checked the cancel flag between
CIDR ranges and between hosts, never inside the per-range nmap call. For a
single /24 the whole scan is one blocking call, so cancel was ignored for
minutes and the run status stayed 'running'.
- thread run_id into _nmap_scan/_ping_sweep/_nmap_port_scan; check cancel
before each phase and skip queued hosts once cancelled
- flip ScanRun status to 'cancelled' eagerly in the stop endpoint so the UI
reacts immediately instead of waiting for a checkpoint
Fixes#218
ha-relevant: yes
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
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
Bulk-approve filtered status=='pending', so a device already approved onto
another canvas (status is global, canvas membership is per-design) — or whose
node was later deleted — was silently skipped. Selecting 64 devices on an
empty canvas produced only the ~28 still pending.
Approve now places a node on the target design for any selected, non-hidden
device that isn't already on that design (deduped by ip/ieee_address, including
within the batch). Returned device_ids/node_ids stay index-aligned so the
client places them all.
ha-relevant: yes
mypy flagged the per-request node_* timestamp attributes as undefined on the
model. Declare them as class-level defaults (like canvas_count) and set
__allow_unmapped__ so SQLAlchemy 2.0 doesn't try to map the Optional[datetime]
annotations as columns.