- auto devices/light groups appearing after install get a server-side 'new'
flag (settings.new_device_ids) and a red dot top-right of the icon; opening
the device's editor clears it for every client
- known_devices baseline seeded silently on first run — upgrades never flood
the plan with dots; hand-made markers never flagged
- pure diffNewDevices + unit tests; backend schema; smoke_new_device.mjs;
TESTING.md row
- device icons: pointer cursor in View (grab only in Devices mode); click
behavior untouched
- openings in View: pure drawings always — no cursor/hover/hit/click
- lock badge: the one exception — visible and clickable in View (info card),
inert in Plan; removed a duplicate pointer-events in .oplock
- smoke covers all cursor/badge cases
- op-hit: pointer-events none + default cursor by default; interactive (grab,
hover outline, drag) only inside Plan; lock badge is a pure indicator
- opening click edits in Plan with any tool; no interaction whatsoever in View
- smoke_inert_openings.mjs; TESTING.md updated
- area-less rooms appear in the marker room list ('no area, manual'); markers
store room_id and land at the room centre; dialog reopen restores the choice
- pure parseRoomRef (space#area / space#@roomId) + unit tests; backend schema
- ROADMAP phase 11 closed; TESTING.md row; smoke_subarea.mjs
- lightColorOf(): on+rgb_color tints icon/glow/ripple (explicit ripple color wins);
brightness ignored by design; off/white/unavailable unchanged
- isAlarmState(): leak/smoke/gas/CO/safety/tamper/problem sensors and sirens in
'on' pulse a red ring over any display mode; outages never alarm;
prefers-reduced-motion honoured
- +2 unit tests, smoke_rgb_alarm.mjs, TESTING.md rows
Research only, no product changes. Confirms and structures the owner's mandate
(view mode must be display-only) and issue #3 feedback; lists deprecations and
approved follow-up features with implementation iterations.
- General settings dialog: fill colors grouped by mode (light on/off, temp
cold/ok/hot, lqi weak/strong), each with its own opacity; lqi fill lerps
between the endpoints; stored in settings.fill_colors (defaults omitted);
space-card uses the same palette
- per-space show_lqi toggle (badges + room tooltip line), inherits the card's
show_signal when unset
- fillColorsOf/lerpColor/roomFillStyle helpers (+4 tests), backend schemas,
smoke_general_settings; TESTING.md updated in the same commit
- Hover: an accent outline hugs the opening's wall strip + grab cursor.
- Drag (view mode): re-snaps continuously to the nearest derived wall; snapToWall now
normalizes the angle to [-90,90) — neighbouring rooms yield the same wall in opposite
directions, which used to flip the hinge while dragging across segment boundaries.
setPointerCapture wrapped in try/catch (an inactive pointerId must not kill the drag).
- Click still opens the status card (via a 250ms timer); double click opens the
properties dialog from view mode. Hit zone is now a slightly thicker strip along the
wall instead of the whole swing square (fewer accidental hovers). +1 test (77).
Verified live: drag moved a test door exactly along a vertical wall (angle 90 -> -90),
click/dblclick/markup-tool paths all работают; the user's 12 real openings untouched.
New markup tool 'Opening': click a wall — the opening snaps onto the nearest DERIVED
room wall (v1.19.0 model: openings keep absolute coords, so room edits never break
them) and takes its angle. Dialog: type, length in real cm (door 90 / window 120 —
per-space scale makes it honest), contact sensor (invertible), lock (doors).
Rendering after easy-floorplan (MIT): hinged leaf + swing arc drawing on via
stroke-dashoffset; window = two casements. No sensor → static plan defaults (door
open, window closed); unavailable freezes the default (pure openingAmount, tested).
Lock: padlock badge (green locked / orange unlocked / grey unknown); the lock is
never toggled from the plan — clicking shows an info card with both states.
snapToWall + openingAmount in logic.ts; space.openings validated server-side.
+2 tests (76). Verified live on the dacha (real TTLock + TRV window sensor).
Visuals borrowed from easy-floorplan (github.com/nicosandller/easy-floorplan), model kept ours.
isActiveState is independent of the live_states toggle; unavailable/unknown count as idle.
Honours prefers-reduced-motion. Badges scale with --dev-size. +1 test (74).
- scale (cm/cell) input compact again (generic width:100% dialog rule stretched it)
- space with no background image gets a white paper canvas (.stage.noplan), so
hand-drawn rooms read like a plan on paper
- TESTING.md updated in the same commit
- fix: Split snaps the click to the room's nearest wall instead of the grid, so
it works on non-grid-aligned rooms (imported/legacy polygons); the pull is
capped (~6 cells) so accidental mid-room clicks stay a miss; splitRoom() still
rejects a bad cut. closestPointOnBoundary() + unit test.
- docs: README (en+ru) documents Merge/Split/ruler/per-space scale (were undocumented);
TESTING.md merge/split rows + fresh self-run; new smokes smoke_merge_split,
smoke_split_nonsnap (incl. the capped-pull case).
- 73 frontend + 12 backend tests green; module eval + both cards register.
- Merge: click a room, then a neighbour. Adjacency is decided by the RESULT, not a
heuristic: mergeRooms unions the outlines and accepts only when they collapse into one
hole-free outline (corner touch / apart / hole => refused). A dialog picks the surviving
name+area; the kept room keeps its id so its label and devices stay put.
- Split: click the room, then two wall points; the chord cuts it, live ruler on the cut.
The bigger part stays the room (name/area/devices), the smaller opens the new-room
dialog. The cut is applied only on confirm — Cancel leaves the room whole.
- Boolean geometry via polyclip-ts (proper ESM + native types; polygon-clipping ships
named types but a default-only ESM build, breaking either tsc or the runtime).
Verified on the real plan, where neighbouring walls overlap collinearly rather than
match exactly — the case a hand-rolled union gets wrong. Bundle 151->202 KB.
+5 tests (72). Verified live: merge of Сауна+с/у -> 4-vertex outline, non-adjacent
refused, split preserves area (26667 -> 13333+13333), cancel leaves the room whole.
- A click strictly inside an existing room is refused while drawing; a click ON a wall
stays legal (neighbours share walls, and real walls overlap collinearly rather than
match exactly, so vertices land on existing outlines mid-span constantly).
- Closing an outline that overlaps an existing room is refused — vertex checks alone are
not enough, an outline drawn AROUND a room has every vertex outside it. Nesting counts
as overlap. The outline stays open so it can be corrected.
- New pure geometry: roomPoly, pointOnBoundary, pointStrictlyInside, segmentsProperlyCross
(touching/collinear deliberately not a crossing), roomsOverlap. +4 tests (67).
Verified live: all 15 existing neighbour pairs stay clean (no false positives).
A wall can only exist as an edge of a closed room:
- roomEdges(rooms) derives walls from room outlines, deduping shared ones, so deleting a
room keeps the borders its neighbours still contribute and drops the rest — the rule
falls out of the model instead of needing bookkeeping.
- Nothing is persisted while drawing: an outline you never close leaves no lines behind
(previously every click pair was written to space.segments immediately).
- The 'Erase line' tool is gone; space.segments is stripped on every save (validation
still tolerates it on read so a stale tab cannot fail a save).
- Dead code out: _addSegment/_removeSegmentByKey/_distToSeg/_pathSegs/_segKey.
Docs (ARCHITECTURE/TESTING/CHANGELOG) updated in the same commit. +2 tests (63).
The v1.18.0 measure label was rendered inside .devlayer, which markup mode hides
entirely (display:none) so icons don't obstruct drawing. Moved it to its own
.measurelayer. Verified visually on a real segment (3.60 m).
- Draw mode shows the current segment length near the cursor (metres or feet+inches
per the HA unit system). New pure helpers segmentCm/formatLength (unit-tested).
- New per-space 'Scale (grid cell size)' field (space.cell_cm, default 5 cm) so each
plan has its own real-world dimensions. i18n en/ru, .measurelabel style. +3 tests (61).
- comfort bounds: parseFloat+isFinite guard (empty input collapsed a bound to 0
and the auto-swap made 'comfort from 25' behave as 0-25 → green at 24°)
- avg room temperature in the room tooltip (what the temp fill actually uses)
- hover darkens the current fill instead of recoloring blue; grey when unfilled
- fill mode as a radio group with short labels; compact inline bounds inputs
- space dialog widened to 500px; TESTING.md updated in the same commit