- Add animated toggle per edge in EdgeModal (cyan switch, "Flow Animation")
- SVG-native <animate> element for reliable cross-browser dot animation
- Persist animated field: backend model, schemas (EdgeBase/EdgeUpdate/EdgeSave), DB migration
- Include animated in App.tsx edgesToSave serialization so it survives save/reload
- Add animated: bool to EdgeData TypeScript type
- Add hideIp / toggleHideIp to canvasStore (UI-only, not persisted)
- Add maskIp util: 192.168.1.115 → 192.168.XX.XX (non-IPv4 passthrough)
- BaseNode reads hideIp from store and masks last two octets when active
- Sidebar shows Eye/EyeOff toggle below Scan Network, highlights when active
- Exclude peer-to-peer edges from Dagre to keep same-type nodes at same rank
- Snap peer group Y to average, re-space X preserving user's original order
- Reverse upward edges (sourceHandle=top) in Dagre so child nodes always render below parent
- Add layout.test.ts covering peer alignment, X ordering, and upward edge cases
- Add custom SVG favicon and Logo component (house + network nodes motif)
- Update page title to Homelable with meta description
- Show Logo in sidebar header and toolbar
- Add theme store and ThemeModal for canvas style switching
- Refactor node colors and edge styles for theme support
- Add groupRect entry to NODE_DEFAULT_COLORS (required by Record<NodeType, NodeColors>)
- Explicitly type Select onValueChange callbacks as (v: string) to satisfy
stricter type inference in tsc -b build mode
- Any TCP service (except port 22 and known non-HTTP protocols) is now
clickable — fixes Sonarr, Radarr, Jellyfin, Proxmox web UI, etc.
- HTTPS auto-detected for port 443/8443 or names containing ssl/tls/https
- Non-HTTP blocklist: FTP, SMTP, DNS, LDAP, SMB, MySQL, Postgres,
Redis, MongoDB, Kafka, Memcached, etc.
- Inline "Add service" form in detail panel (name, port, tcp/udp)
- Remove button (×) on hover for each service row
- Group rect nodes excluded from detail panel
- getServiceUrl extracted to utils/serviceUrl.ts
- 13 new tests (79 total, all pass)
- New GroupRectNode: rounded rect with NodeResizer (8 handles), no
connection handles, always behind network nodes via negative zIndex
- GroupRectModal: label, font preset (Inter/Mono/Serif), 3×3 text
position grid, text/border/background color pickers, z-order 1–9
- Sidebar: "Add Rectangle" button (below Add Node), group rects
excluded from node count stats
- Save/load: size persisted in custom_colors.width/height, no backend
schema changes required
- elevateNodesOnSelect=false on ReactFlow so selected rects never
pop above network nodes
- Tests: 3 new store tests + 9 GroupRectModal tests (66 total, all pass)
Backend:
- Add MAC OUI lookup table (QEMU 52:54:00, Proxmox bc:24:11, VMware, VBox, Hyper-V)
- suggest_node_type() now accepts mac param and uses OUI as a low-priority hint
Frontend:
- Detect VM type from MAC prefix on pending device cards
- Show colored badge (QEMU / PVE / VMware / VBox / Hyper-V) in orange with tooltip
- Show recognized service name (Home Assistant, Synology, Proxmox, etc.) as
card title instead of IP when detected via service signatures
- Keep IP displayed smaller below when title differs from IP
- Add SSH / HTTP / HTTPS badges as colored pills
- Show +N badge for additional services beyond the three common ones
- nodeColors.ts: add missing camera/printer/computer/cpl entries to Record<NodeType, NodeColors>
- CanvasContainer.tsx: use ConnectionMode enum instead of bare string literal
- export.ts: cast style object to Partial<CSSStyleDeclaration> for CSS custom property
- Sidebar.tsx: cast suggested_type to NodeType to satisfy NodeData type
C2 - JWT token was stored in localStorage (XSS-accessible):
- Switch Zustand persist storage from localStorage to sessionStorage
- Token is now scoped to the current tab and cleared on browser close
H5 - docker-compose had unsafe SECRET_KEY fallback:
- Replace ${SECRET_KEY:-change_me_in_production} with :? syntax
- Docker Compose now aborts with a clear error if SECRET_KEY is unset
M1 - Login endpoint had timing leak allowing username enumeration:
- Always call verify_password() regardless of username match
- Use hmac.compare_digest() for constant-time username comparison
- Both checks run every time; attacker cannot distinguish wrong
username from wrong password via response timing
C3 - config.yml contains credentials, remove from git tracking:
- Add backend/config.yml to .gitignore
- git rm --cached to untrack it
- Add backend/config.yml.example with instructions
C1 - SECRET_KEY must come from .env, no unsafe default:
- Remove hardcoded "change_me_in_production" default from config.py
- App now fails to start if SECRET_KEY is not set (pydantic required field)
- Generate real random key in backend/.env (gitignored)
- Add backend/.env.example for new contributors
H1 - WebSocket /ws/status was unauthenticated:
- Backend: require ?token= query param, validate via decode_token(),
close with code 1008 (Policy Violation) if missing or invalid
- Frontend: append ?token=<jwt> to WebSocket URL
- Normalize stub handle IDs (top-t/bottom-t → top/bottom) in canvasStore.onConnect
so React Flow can locate the handle and render edges without save+reload
- Also normalize on save in App.tsx as a safety net for persisted handle IDs
- Auto-create virtual edge (LXC top → Proxmox bottom) when parent_id is set
via the node edit modal, and remove it when parent_id is cleared/changed
- Auto-set LXC/VM parent_id when a virtual edge is drawn to/from a Proxmox node
source handles: visible, can initiate drag from top or bottom
target handles: invisible (opacity:0, 12x12px), overlap the source at same
position — React Flow detects them for snapping and shows visual feedback
without the user seeing a second dot. Ensures bottom→top connections work.
connectionMode=loose alone does not reliably make source handles act as
drop targets. Each position now has both a source handle (to initiate
drag) and a target handle (to receive drops). They overlap visually as
one dot but support full bidirectional connections.
React Flow only allows initiating a drag from type=source handles.
Top handles were type=target so users could never start a connection
from the top of any node. With connectionMode=loose, source-to-source
connections are already allowed, so all handles can now be source.
React Flow blocks connections involving a parentId parent/child pair by
default to prevent hierarchy cycles. Override with isValidConnection
that only rejects self-loops (source === target), allowing explicit
edges between Proxmox container nodes and their nested VMs/LXCs.
The visibleEdges filter was silently dropping any edge between a
container-mode Proxmox and its children, even ones the user explicitly
drew. Since no edges are auto-generated, the filter only blocked
intentional connections. Removed it entirely.
- NodeSave schema was missing custom_icon field — icon reset to default on reload
- handleSave was not including custom_icon in the payload
- CameraNode now uses Cctv icon (clearer than Camera)
- Added 'cctv' entry to ICON_REGISTRY for the icon picker
- New 'camera' node type with Camera icon (frontend + types)
- Scanner fingerprint: camera ports (554, 8554, 37777, 34567, 2020) now suggest 'camera' instead of 'iot'
- service_signatures.json: all camera/NVR entries updated to suggested_node_type=camera
- 'camera' added to priority list in suggest_node_type (above iot)
- canvasStore.onConnect was hardcoding type=ethernet and ignoring edgeData fields (type, label, color, etc.)
- EdgeModal had no key, so useState was not reset between connections — initial prop ignored
- Added key based on source/target/handles so modal re-mounts for each new connection