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
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
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
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
- 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).
- 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.
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
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
Approve (single + bulk) created the canvas Node under the first design
instead of the design the user is viewing, so approved devices were
invisible on the active canvas and got wiped on the next save — and a
re-approve returned "0 approved" because the rows were already approved.
- bulk-approve now accepts design_id; the UI sends the active design.
- single approve already honoured design_id; the UI now sends it too.
- generalize the wireless branch (status=online, mesh props, no ICMP
check) to Z-Wave as well as Zigbee, using build_zwave_properties.
ha-relevant: yes
The modal is kept mounted by its parent (only `open` toggles), so Radix
onOpenChange never fires for a parent-driven open and the draft-reset
branch was dead code — abandoned edits leaked into the next open. Reset
on the `open` prop edge via effect instead. Also pass radix 10 to
parseInt for the size inputs. Adds a reopen-after-cancel regression test.
ha-relevant: yes
The Custom Style modal listed node types as a flat list. It now groups
them under category headers (Hardware, Virtualization, IoT, Zigbee,
Z-Wave, Personal, Generic) like the Add/Edit Node modal, and exposes the
Z-Wave node types for styling.
ha-relevant: yes
Scan History now recognises kind=zwave: dedicated Z-Wave filter chip,
badge (RadioTower, orange) and import-done toast, instead of falling
back to the generic IP type.
ha-relevant: yes
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
Reworks the "Pending Devices" panel into a "Device Inventory": scanned
devices already placed on a canvas are no longer suppressed — they stay
listed and badged with how many canvases they appear on.
- scanner: stop deleting/skipping on-canvas IPs (hidden still suppressed)
- scan API: /pending returns all non-hidden devices; compute canvas_count
by correlating ip/ieee_address against nodes grouped by design
- frontend: rename to "Device Inventory", top-right canvas-count corner,
toggle to show/hide on-canvas devices (default show)
ha-relevant: maybe
Collapsible Deep Scan section in ScanConfigModal exposes extra port ranges,
HTTP probe and TLS-verify switches. Pre-filled from saved defaults; edits are
passed to trigger() as a per-scan override and do not change the persisted
defaults (those live in the Options/scan config). scanApi.trigger now accepts
an optional deep-scan body.
ha-relevant: yes
The toggle label renders before settingsApi.get() resolves, so reading
.checked immediately raced the async state update and failed on slower CI.
Wait for the resolved checked state instead.
ha-relevant: no
Replace the inline sidebar 'history' view with a dedicated ScanHistoryModal.
Same data and actions (refresh, auto-refresh while running, stop scan,
transition toasts) plus run duration, finished timestamp, and kind/status
filters. Scan/Zigbee start now surface a toast instead of force-switching
the sidebar view.
ha-relevant: yes
Replaces the detail-panel container selector with the existing "Parent
Container" dropdown in the edit modal (double-click). The selector now
shows for any node that is nested in — or can nest into — a container_mode
node, not just lxc/vm/docker_container, so a dragged-in node can be
re-targeted or detached ("None") from the modal.
- NodeModal: container_mode candidates count as valid parents; selector
shown for nested nodes; submit/type-change keep valid container parents
- App: pass container_mode in parentCandidates
- revert detail-panel selector + setNodeParent store action (updateNode
already handles parent attach/detach)
ha-relevant: yes
Dropping a top-level node over any container_mode node (proxmox,
docker_host, ...) now pops a confirm modal that nests it as a child
(sets parentId), mirroring the existing drop-onto-group flow.
- store: addToContainer(containerId, childId)
- CanvasContainer: detect container_mode intersection on drag stop
(group still wins if both intersect)
- generalize ConfirmAddToGroupModal with a container variant
ha-relevant: yes
Make Node Groups mutable instead of fixed-at-creation:
- removeFromGroup / addToGroup store actions (inverse pair, history-aware)
- right panel: per-member remove button + editable group description
(reuses data.notes, no backend/serializer change)
- drag a node over a group → confirm modal to add it (getIntersectingNodes)
Tests: +21 (store, panel, canvas detection, modal).
ha-relevant: yes
Connection labels now accept line breaks. The label field in EdgeModal
is a textarea (Enter inserts a newline) and the rendered edge label uses
whitespace-pre-line so newlines display as multiple lines.
Closes#183
ha-relevant: yes
Adds optional live status checking per service (not just per node),
requested as a follow-up to issue #196.
Backend:
- New check_service / check_services: HTTP(S) GET for web services, TCP
connect otherwise; UDP and port-less non-web services stay 'unknown'.
- New scheduler job 'service_checks', independent interval (default 300s),
added/removed live via set_service_checks_enabled.
- Settings gain service_check_enabled + service_check_interval (>=30s),
persisted to scan_config.json. New WS message type 'service_status'.
Frontend:
- Live per-service status overlay in canvasStore (not persisted, so it
never round-trips through canvas save), fed by the WS message.
- DetailPanel + canvas node service rows: offline service turns red
(#f85149), otherwise keeps its category colour.
- SettingsModal: toggle + interval input (default 300s / 5 min).
Off by default — no behaviour change until enabled.
ha-relevant: yes
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
Clicking Settings now opens a dialog with the status-check interval and
canvas snap options plus Save/Cancel, replacing the inline left-panel view.
Removes the 'settings' Sidebar view and SettingsPanel component.
ha-relevant: maybe
In the Hidden view the only bulk action is Restore, but the Enter
shortcut in select mode always called handleBulkApprove — approving
hidden devices. Dispatch on statusFilter: restore when hidden, approve
when pending. Add regression tests for both views.
ha-relevant: maybe
The close X used a raw <button onClick={onClose}>, bypassing Base UI's
dismissal. Outside-click worked (it goes through onOpenChange) but the
synthetic click on the X was dropped on Firefox/Windows. Route the X
through Base UI's DialogClose, the same path as outside-click and the
library's default close button. Add a regression test.
ha-relevant: maybe
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
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
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
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
- Center webkit slider thumb on 4px track (margin-top: -4.5px) so the
opacity bullet no longer sits below the bar.
- Drop redundant inline accentColor; CSS module owns track color.
- Remove dead ::-ms-* prefix rules.
- Tests: assert aria-labels on grid/select triggers, cover getFontLabel
happy path + unknown-value fallback via the Font selector DOM.
- Approval (single + bulk) of zigbee pending devices now writes IEEE, Vendor,
Model, LQI into Node.properties so they show in the right panel.
- Zigbee re-import refreshes properties on existing canvas Nodes and skips
creating a pending row when the device was already approved — keeps
approved devices out of pending/hidden modals.
- Coordinator Node also receives the same properties on first creation and
on re-import.
- Remove the Parent Container selector from the add/edit node modal.
Zigbee nodes have no IP-based check — hide Check Method and Check Target
fields in NodeModal for all three zigbee types. Default and force
check_method='none' so the scheduler marks them always online.
Backend approve and zigbee import routes also set status='online' and
check_method='none' for zigbee node types.
Replaces sidebar pending/hidden panels with a wide modal showing
devices as cards. Adds search, segmented source/status filters,
type filter, select mode for bulk approve/hide/restore, and
keyboard shortcuts. Hidden cards click-to-restore (no approval
detour); approval no longer pops the edit modal.
Backend: new restore + bulk-restore endpoints (hidden -> pending).
Replaces the 4-option dropdown with a 1..48 slider. Existing handle IDs
('bottom', 'bottom-2'..'bottom-4') are preserved and the hand-tuned pixel
positions for counts 1..4 are locked in (regression-tested) so saved
canvases render identically. Counts >=5 use uniform spacing; node card
auto-grows in width so handles stay clickable. Existing edge-remap on
shrink scales unchanged thanks to its index-based loop.
ha-relevant: yes
Bare if without braces meant onClose() ran unconditionally after the
window.confirm — clicking Cancel still closed the modal and dropped
unsaved edits. Wrap the confirm body so onClose only fires on accept.
- Add docker_container node type (Package icon, sky-blue accent) as child of docker_host
- Parent selector for docker_container filters to docker_host only via nodeType field
- Virtual edge drag-connect syncs parent_id for docker_container <-> docker_host
- Fix setProxmoxContainerMode: remove proxmox-only guard so width/height are properly
set/cleared for all container-capable types (docker_host, vm, lxc)
- Fix handleAddNode: only give group size when container_mode=true, making create
and reload behavior consistent (was giving size unconditionally for CONTAINER_MODE_TYPES)
- Add regression tests for docker_host container mode toggle and docker_container nesting
The native <input type="color"> only supports 6-digit hex, stripping alpha
and forcing background/border/text colors to be fully opaque on edit.
Each color field now shows an opacity slider (0–100%) below the swatch.
Values are stored as 8-digit hex (#rrggbbaa). Existing zones with 6-digit
colors are handled transparently (alpha defaults to 100%).
- colorUtils.ts: hexToRgba / rgbaToHex8 helpers
- GroupRectModal: opacity sliders for all three color fields
- 26 new tests across colorUtils and GroupRectModal