The device editor has offered display='value' since v1.26.0; MARKER_SCHEMA
accepted only badge/ripple/icon_ripple. Picking it produced
not a valid value for dictionary value @ data['config']['markers'][n]['display']
and since one rejected marker fails the whole config write, the user could not
save the plan at all until the setting was undone. Reported by @RemyRoux with
the exact error text, 2026-07-27 — a year and a half after the feature shipped.
The schema now accepts it, and the class of bug is closed rather than the
instance: DISPLAY_MODES, TAP_ACTIONS, SPACE_FILL_MODES and ROOM_FILL_MODES are
exported from src/logic.ts, the editors render their options from them, and a
backend test parses those lists out of the TypeScript source and asserts the
schema accepts every one (and rejects a bogus value). Reverting the one-word
schema fix fails that test, which is the check that was missing.
Plus an HA-harness test saving a config that contains a value-display marker —
the exact call the user's card was making.
Docs: CHANGELOG.md + CHANGELOG.ru.md + TESTING.md + STATUS.md.
R4-1: collecting superseded plan files runs after the configuration is already
durable, but an error listing the directory propagated out of config/set. The
client saw a failure for a revision the server had committed, and its retry
came back as a conflict. collect_plans now reports 0 instead of raising, and
config/set logs and proceeds — the event fires, the revision is returned.
R4-2: the pending set was cleared when a batch went out, not when it came back,
so every render during an in-flight content/sign queued another request: six
calls where one was needed, and unbounded on a socket that is slow rather than
busy. Queued and in-flight are separate states now; a failure backs off (2 s
doubling to 60 s) instead of retrying on the next frame; an in-flight entry
expires after 15 s so a promise that never settles cannot wedge retries; a late
answer after dispose() no longer renders.
Tests: test/signing.test.mjs — eight cases with hand-settled promises, verified
against a v1.45.1 checkout where four of them fail (2 sign calls instead of 1,
no backoff, a late answer rendering after teardown). Backend: a broken
collector still yields a successful save whose revision the next CAS accepts.
Pure collector: a disappearing directory returns 0.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
R3-1 (high): v1.45.0 made the upload safe but left deletion to the client —
after a successful save the card asked the backend to remove everything but the
file it had just committed. Two open editors cannot be ordered: a delayed
request from one deleted the plan the other had just saved, leaving the
accepted configuration pointing at nothing, the exact damage copy-on-write was
introduced to prevent.
houseplan/plan/cleanup is removed. config/set collects inside its own write
lock from the two configurations that bracket the commit (plans.collect_plans):
a file the old revision referenced and the new one does not is superseded and
goes; any other unreferenced upload waits out PLAN_ORPHAN_TTL_S, because a
fresh one may belong to a transaction that has not committed yet. The collector
lives in a pure module so it can be reasoned about and unit-tested without the
HA harness.
R3-2: houseplan-space-card signed its plan url and threw the result away —
getCardSize() mutated a throwaway model while render() rebuilt its own from the
config, so the <image> requested the protected path and got 401 on every
render. Both cards now share ContentSigner (src/signing.ts), which also gives
the static card batching, expiry handling and periodic re-signing. is
released in finally: one failed request no longer wedges a url for the life of
the page.
Tests: five backend interleaving cases from the report, six unit tests for the
pure collector, smoke_space_card_bg (verified to fail against a v1.45.0 build:
the raw url reaches the DOM and no retry happens). 57 smokes, 124 unit, 22
backend-pure.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
The HA harness reuses one config directory inside a module, so the s1 upload
left by test_plan_set_validates counted as a third file and the cleanup
assertion read 3 instead of 2. Own space id plus a defensive sweep.
R2-1 (high): plan replacement committed filesystem state before the config CAS.
The upload wrote the final name and unlinked the other extension, so a rejected
config write left the live plan already replaced — or the stored config
pointing at a deleted file. Uploads now go to <space>.<token>.<ext> and delete
nothing; houseplan/plan/cleanup runs only after the config write is accepted.
The '.' separator is load-bearing: a space id cannot contain one, so cleaning
'f1' can never reach the files of 'f1-attic'.
R2-2: the backend signs at most MAX_SIGN_PATHS (200) per request and ignores
the rest silently, while the card sent its whole cache in one call and trusted
any cached entry forever — past 200 attachments the later ones stopped being
refreshed and expired for good. Requests are chunked to the shared constant,
entries carry their issue time (aging urls keep rendering while a replacement
is fetched, expired ones are dropped), and the cache is pruned to urls the live
config still references.
R2-3: areaClimate() rescanned the whole registry per room and per measurement.
areaClimateMap() classifies once and returns Map<area,{temp,hum}>, memoized on
hass identity so fresh states are always observed. Smoke measurement: 133
registry scans per update with 44 rooms before, 2 after, flat in room count.
Also: smoke_ux_fixes wrote its screenshot to a hard-coded /tmp path and could
not run on Windows.
Tests: smoke_plan_upload_reject, smoke_sign_cap, smoke_climate_once (all fail
on v1.44.8), three backend tests for versioned plan names and cleanup scoping,
unit tests for chunk/referencedContentUrls and areaClimateMap.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
CI checks `cmp dist == custom_components/houseplan/frontend`; the three
previous commits shipped source and docs without the rebuilt bundle, so
validate.yml failed on all of them. Same folder that HA serves statically —
the one that must never be skipped.
Found on the owner's install: the image lands in /config/houseplan/plans, the
space keeps plan_url=null, the plan never shows and re-saving does not help.
_saveSpaceDialog held a reference to the space object across the await that
uploads the file. _reloadConfigOnly() — which runs on every
houseplan_config_updated event — REPLACES _serverCfg, so that reference became
an orphan: plan_url, aspect, title and every display setting were written into a
detached object while the save shipped the untouched config. In 'create' mode
the whole new space was lost the same way.
- upload first, then touch the config; no reference is held across an await.
- _saveConfigNow() sets _cfgWriting like the debounced writer, so a revision
arriving mid-save defers its reload instead of replacing the config (audit L2
extended to this path).
- demo/smoke_plan_upload_race.mjs: on v1.44.7 the sent config still carries the
OLD plan_url and the created space is missing; passes here. The demo's
config/get now returns a fresh object, as a real server does — returning the
same reference is what hid this class of bug from the smoke layer.
- DEVELOPMENT.md: the deploy target is custom_components/houseplan/frontend/,
and deploy verification must go over HTTP. A copy placed next to __init__.py
is served by nobody — that cost two deployments today.
- docs: CHANGELOG.md + CHANGELOG.ru.md + TESTING.md + STATUS.md.
The card signs content urls because a browser cannot authenticate an <image
href>. But _display() was called inside _buildModel(), and the space model is
memoized on the config fingerprint — so the UNSIGNED url froze in the cache and
the signature, which did arrive, never reached the element. The plan never
loaded, and the browser kept hitting the unsigned path: 401, which Home
Assistant reports as a failed login attempt from the viewer's own IP (that is
how the owner spotted it). PDF links were unaffected: they already resolved at
render time.
- _buildModel() keeps the raw plan_url; the render pass calls _display().
- _display() returns '' for an unsigned content url instead of the plain path,
and the <image> is not emitted at all until the signature lands — no 401, no
spurious login-attempt warning.
- _resign() replaces 'drop everything and re-request': the previous urls are
kept until the new ones arrive, so a wall tablet never blanks.
- demo/smoke_plan_signed.mjs: reproduces on v1.44.6 (href stays ?v=..., never
?authSig=), passes here. TESTING.md row added.
- docs: CHANGELOG.md + CHANGELOG.ru.md + STATUS.md.
After v1.44.5 read the area registry instead of visible icons, every hidden
temperature entity in the area became a candidate, including ones measuring
something other than room air. Verified against a live 60-area install: a NAS
processor temperature, kettle water, a 90 C sauna heater and a virtual
better_thermostat all leaked into room averages.
- areaClimate(): skip entity_category (diagnostic/config), skip EXCLUDED_DOMAINS
platforms, skip entity ids naming a non-air medium (water/coolant/flow_temp/
return_temp/target/setpoint/chip/cpu/processor/board/device_temp/batter/
freezer/fridge/oven/kettle/boiler).
- rules.ts: kettle/thermopot -> mdi:kettle, sauna/harvia -> mdi:hot-tub, so they
no longer fall through to the generic thermometer rule.
- test: all four real false positives asserted out, one real sensor left.
- docs: CHANGELOG.md + CHANGELOG.ru.md + STATUS.md snapshot.
- areaClimate() walks the HA registry for the area instead of the list
of VISIBLE icons: a thermometer hidden by curation or by the user was
silently dropped from the room card, tooltip and temperature fill
(field report). Curation still filters fridges/TRVs; the auto icon is
used on purpose so a custom marker icon cannot change what a device
measures; an explicit per-room source still wins
- room tooltip no longer says 'open the area' — room clicks were removed
in v1.40.1 (the link icon does it)
- +1 unit test (120); both changelogs updated
B2: the HTTP upload view failed OPEN when the config entry was
unavailable while the WS path failed closed — both now share one
may_write() policy helper (new auth.py) that denies non-admins when the
policy cannot be read.
B5: _finite now guards room rects, polygon vertices, view_box and
opening coordinates, not just layout positions; the declared
MAX_OPENINGS cap is finally enforced.
L4 (sub-item): every drag pipeline captures the pointer through the
tolerant helper (an inactive pointerId used to kill device/label/resize
drags); decor shapes gained a bounds clamp so they cannot be dragged far
outside the plan and persisted there.
+2 backend tests (16); both changelogs updated in this commit
The v1.43.0 auth fix closed the hole but left the DISPLAY path
unauthenticated: HA authenticates by a Bearer header or an authSig
signed path, and an <image href> / <a href> sends neither, so plan
backgrounds and manual links returned 401. Reproduced live before the
fix (fetch 401, Image onerror).
- new WS houseplan/content/sign mints async_sign_path urls (24 h,
bound to the connection's refresh token, only for our own endpoint)
- the card resolves display urls through _display(): signed when known,
requests a batched signature otherwise, re-renders when it lands, and
drops all signatures every 12 h so long-lived wall tablets stay valid
- houseplan-space-card signs its background too
- backend test asserts the unsigned url is refused and the signed one
returns the bytes WITHOUT an Authorization header
- 10 most recent releases translated; older entries stay English-only
- policy updated in STATUS.md and CONTRIBUTING: user-visible changes go
into BOTH changelogs in the same commit (the user base is largely
Russian-speaking — see the Telegram chat)
- cross-links between the two files and from both READMEs
CR-1: the lock invariant is restated precisely (never by an accidental
tap; the door card's labeled button is the ONE sanctioned surface),
unlocking now confirms, and smoke_lock_invariant exercises all five
actuation paths (icon tap, controls[], card entities, _cardToggle,
opening card).
CR-2: attachment migration is transactional — the server COPIES files,
the config is committed with its revision check, and only then the old
folder is removed via the new houseplan/files/cleanup. A rejected save
no longer leaves the stored urls pointing at an emptied folder.
CR-3: migrate returns an exact {source: written} mapping; only confirmed
copies are rewritten, destination name collisions get a unique name
instead of silently linking a pre-existing file, and a failed migration
raises a toast instead of being swallowed.
+1 unit test (119), +1 backend test, +1 smoke (51 total); docs
same-commit
- badges + header line in README.md / README.ru.md
- 'Getting help & sharing your plan' section in both READMEs, asking for
the version number when reporting (console banner / integration page)
- .github/ISSUE_TEMPLATE/config.yml contact links (chat + discussions)
- CONTRIBUTING 'Where to ask'; STATUS (community row) and SCOPE (field
feedback source)
- device card opens with controllable entities: toggles inline (finger
targets), cover/lock/climate hand off to HA more-info; metadata and
manuals moved below; config/diagnostic entities filtered; locks still
never toggle from a card
- marker.is_light: a smart switch driving dumb fixtures glows in the
light-sources fill (its own entity or the bound controls) — no
light-group helper needed
- backend schema; smoke_card_controls.mjs, smoke_glow extended; docs
same-commit
- the room gear became a fixed-size pill button (was 0.9em/60% opacity
inside the label — invisible in practice, field report); shown on
unnamed rooms too, which is where you name them
- metrics line 0.62em -> 0.75em (unreadable on tablets)
- tooltips: latch on the first touch/pen pointer event instead of
trusting (hover: none) alone; any touch drops an open tip
- smoke_feedback_v2.mjs; docs same-commit
- upload/plan_set tests asserted the old public /houseplan_files/... paths
and only run in CI, so the B1 change surfaced there
- +test for the fail-closed admin check (audit B2/T4: the authorization
boundary had zero coverage)
T1: demo/serve.mjs exports check/checkAll/finish — all 48 smokes now
assert named facts and exit non-zero on a mismatch or an uncaught
in-card exception (verified by breaking the kiosk guard on purpose).
Informational values were frozen from a v1.43.1 run and cross-read
against the source; timings assert budgets, not exact numbers.
T2: new CI job 'smoke' gated on 'frontend', builds a FRESH bundle
before running (the committed demo/srv/assets copy is a snapshot) and
uploads per-file logs on failure.
T3: [auto] now means 'a named failing check exists' and each line names
it (43 lines); 72 aspirational markers honestly downgraded to [manual].
Fixed the 'ZERO edit buttons' contradiction (wrong since v1.30.1) and
the opening-click line (true again since v1.43.1).
Three smokes carried pre-v1.39.0/v1.25 expectations and were testing
old behaviour: tap defaults for lights, card-wide tap action, label drag
requiring plan mode.
DEVELOPMENT.md documents the harness contract.
L1: memoized space model + open pairs (structural fingerprint key, epoch
bumped synchronously at mutation time, not inside the debounce); hoisted
per-room geometry out of the render loop; smoke asserts zero recomputation
across state pushes.
L4: openings get the 3 px drag threshold used by every other pipeline and
only write when the geometry actually changed — taps open the dialog again.
G2: interiorPoint() replaces the vertex mean, so island rooms inside
concave (U/L) parents are accepted and their evenodd holes render; traced
duplicates still are not containment.
G3: segKey rounds before ordering — one shared wall, one key.
B2: _check_write fails closed when the entry is unavailable.
B3: layout/set honours expected_rev and returns the new rev.
B4: config/set without expected_rev over a non-empty store logs a warning.
B5: coordinates reject NaN/Infinity; spaces/rooms/markers/decor/layout capped.
+2 unit tests (118), +2 backend tests (14), smoke_render_perf; docs
same-commit
L2 (silent data loss): debounce gains flush()/pending(); _reloadConfigOnly
flushes a pending write and defers while one is in flight; conflict path
forces; failed reload now toasts instead of an empty catch; teardown flushes.
G1 (split corruption): same-edge cuts carve the niche properly instead of
walking the outline twice; partition invariant (parts sum to the original)
rejects anything else; +1 unit test covering 5 niche shapes and both legacy
cut shapes.
B1 (unauthenticated content): plans and marker files move to
HouseplanContentView (/api/houseplan/content/..., requires_auth); only the
card bundle stays static; contentUrl() rewrites legacy URLs on read (no
storage migration); repairs.py accepts both prefixes; +1 unit test.
L3 (dialog zombies): all four save catch-blocks guard against a closed
dialog; the card no longer blanks when a save fails after Esc.
smokes: smoke_save_race, smoke_dialog_zombie; docs (TESTING/CHANGELOG/
ARCHITECTURE incl. the optimistic-UI note) same-commit
- taps on tablets synthesized mousemove and popped the tooltip over the
finger (user feedback item 5); _showTip gated by (hover: none)
- smoke_touch_tips.mjs (matchMedia shim); TESTING/CHANGELOG same-commit
- space.settings.card_font_scale (tier 2 base) + room.settings
name_scale/label_scale (tier 3), 50-300%, multiplied together and
stacking with resize-k and kiosk per-screen multipliers
- live sample card in both the space and room dialogs
- backend schema; smoke_font_scales.mjs (9 checks); docs same-commit
- four-tier principle fixed in ARCHITECTURE: global > space > room >
device, specific overrides general, unset inherits
- room.settings { fill_mode, temp_source, hum_source } with backend
schema; pure roomFillModeOf + sourceValue (+2 test suites, 114)
- gear on room cards in the Plan editor opens Room settings: rename,
re-area (current area included in the list), fill override (may opt
out of glow darkness), explicit temp/hum source with a searchable
device+entity dropdown; the same section in the creation dialog
- source feeds the room card, tooltip and temp fill; works for rooms
without an HA area (user-feedback case #1)
- smoke_room_settings.mjs (10 checks); TESTING/CHANGELOG/ARCHITECTURE
same-commit
- new WS houseplan/files/migrate moves /files/<oldId>/ to the new id
(admin-only, sanitized ids, merge-safe, removes the empty old dir)
- _saveMarker calls it and rewrites pdf urls (migratePdfUrls pure
helper, +1 unit test, 112) when rebinding changes the id
- field incident: the sauna heater manuals pointed at an orphaned
old-id folder that got cleaned up; data restored by hand on the
home instance (official Harvia PDFs re-downloaded)
- TESTING/CHANGELOG same-commit
- rooms inert in View (default cursor); mdi:open-in-new after the room
name (View, rooms with an area) navigates to the HA area
- smoke_room_link.mjs (7 checks); TESTING/CHANGELOG/UX-MODES same-commit
- alignGuides/segmentAngle/is45 pure helpers (+2 unit tests, 108);
per-context candidates (room vertices + path pts / other icons /
decor endpoints+corners / other room cards), nearest per axis, max
two guides, dashed accent lines with a source dot
- cursor badge shows length + angle, green on 45deg multiples
- indication only, no magnetism (owner's decision); guides live in
plan/devices/decor, never in View
- smoke_align_guides.mjs (9 checks); TESTING/CHANGELOG same-commit
- the markup layer's .seg lines ran solid through open stretches;
cutSegments extracted (outlineWithout now reuses it) and applied to
_segments in _renderMarkupLayer
- smoke_openwall: planSegCut check (15 total); docs same-commit
- LS_NAV stores {space, mode}; restored in setConfig from the cached
config, with a retry after the live config when the cache was stale;
deep-link hash wins; edit modes restored for admins only
- UX-MODES updated (owner reversed the 'always start in View' rule)
- smoke_nav_persist.mjs; TESTING/CHANGELOG same-commit
- Virtual / Pick-from-HA radios; Show-entities checkbox with tooltip
gates device entities (groups/helpers always listed); searchable
dropdown only in HA mode, auto-opens when empty, closes on pick;
Save guarded until a binding is chosen; logic untouched
- smoke_binding_ui.mjs (16 checks); marker_stay/controls smokes green;
TESTING/CHANGELOG same-commit
- outlineWithout trims the rooms' solid strokes under open stretches
(.room.noedge kills the polygon stroke incl. hover; trimmed
.room-outline path draws the remaining walls)
- dash color follows the space stroke color; openwalls layer moved
after the glow layer
- +1 unit test (105); smoke_openwall extended; docs same-commit
- _openWallHit shared by click and hover; amber dashed preview of the
stretch that would open, red solid when the click would close it;
stage.wallhot drives the pointer cursor
- smoke_openwall_hover.mjs (9 checks); TESTING/CHANGELOG same-commit
- room.open_to symmetric links; 'Open boundary' plan tool toggles the
shared wall pair under the click (sharedBoundary collinear-overlap
math + distToSegment pull, same as Split); dashed rendering with an
amber hot state while the tool is active; Esc -> Draw
- glow: the clip becomes the transitive open zone (openZoneOf BFS,
either-direction links) + door sectors from the zone's outer walls
- model rooms carry open_to; backend ROOM_SCHEMA open_to: [str]
- +3 unit tests (104), smoke_openwall.mjs (8 checks); docs same-commit
- room outline + sectors as separate clipPath children (always union)
instead of subpaths of one nonzero path where opposite windings
cancelled the overlap; smoke asserts one contour per path
- TESTING/CHANGELOG same-commit
- marker.glow_radius_cm overrides the global default per device (field
in the marker dialog, HA units, placeholder = global default); pools
and door sectors use the per-source radius
- backend schema; smoke_glow extended (11 checks); docs same-commit
- primaryEntity: tiered selection, domain priority beats hidden flag
(hidden light > visible config switch); visible same-domain still wins
- live-debugged on the real install: individual lamps hidden in the
registry (light group setup) got switch.*_do_not_disturb / identify
buttons as primary — tap toggle 'did nothing'
- +1 unit test (101); TESTING/CHANGELOG same-commit
- marker.controls[]: bound light.*/switch.* entities; explicit per-marker
tap_action=toggle flips them with HA-group semantics in one call
(controlsAction + isControllable pure helpers, +2 unit tests, 100)
- icon state and RGB tint mirror the targets (stateless remotes and
virtual dumb-switch markers finally show something); info card lists
targets with states; locks filtered everywhere
- chips+search UI in the marker dialog; backend schema; smoke_controls
(9 checks); TESTING/CHANGELOG same-commit
- fill_mode 'glow': uniform darkness over every room; lit lamps render
radial gradient pools (rgb_color -> color temp via kelvinToRgb ->
configurable default; brightness scales alpha)
- pools clipped by the source's room + doorway sectors (doorSector rays
to door edges; hasRoomBehind blocks entrance doors); windows and
islands don't participate (no shadow casting, documented)
- glow radius in HA units (m/ft) in general settings, stored in cm;
palette group glow_base/glow_light; backend schema updated
- +4 unit tests (98 total); smoke_glow.mjs (10 checks); docs same-commit
- roomsOverlap: full nesting is legal (islands), edge crossings and
partial overlaps still rejected; polyContainsPoly + islandsOf pure
helpers (+2 unit tests, 94 total)
- draw tool: per-click point-in-room rejection removed, validation at
contour closing; parent rooms with islands render as evenodd paths
(ring fills correctly, island stays clickable through the hole)
- smoke_island_rooms.mjs; smoke_merge_split stale room-count asserts
fixed (rg pushed a 5th room; cancelWhole silently false for ages)
- TESTING/CHANGELOG same-commit
- rebinding migrates the layout position to the new marker id instead of
recentering; room change within the same space keeps the position
(owner's decision); new icons and cross-space moves still center
- smoke_marker_stay.mjs; TESTING/CHANGELOG same-commit
- marker dialog stores autoIcon (DevItem.icon); ha-icon-picker gets it
as placeholder, fallback input too; 'Auto: mdi:...' hint with preview
under the picker, hidden once an explicit icon is chosen
- smoke_icon_placeholder.mjs; TESTING/CHANGELOG same-commit
- third mode tab: draw lines/rects/ovals/text with grid snap, drag
preview; select+move (Delete key), erase, color/width/fill controls;
Esc ladder; text dialog with S/M/L sizes and dblclick re-edit
- decor renders under rooms, visible in every mode, click-transparent
outside its editor; stored in space.decor with backend schema
(line/rect/ellipse/text variants), rev/optimistic locking as usual
- smoke_decor.mjs (19 checks); smoke_editor_tabs updated to 3 tabs;
TESTING/CHANGELOG/UX-MODES same-commit
- _openingPreview getter mirrors the click's snap (same eps, same
existing-opening hit test); dashed 90cm ghost + center dot on the wall
- smoke_opening_preview.mjs; TESTING/CHANGELOG same-commit
- splitRoomPath in logic.ts: wall-to-wall polyline cut with interior
vertices; validates wall crossings, self-intersection, degenerate
parts; splitRoom delegates (2-point path); +3 unit tests (93 total)
- split UI: interior clicks add snapped intermediate points, live
polyline + vertices + preview; new/updated toasts
- cursors: pointer for merge/delroom and split room-pick stage,
crosshair while cutting (stage tool-* classes)
- Esc: split — drop last point, then room pick, then back to draw;
merge — clear selection, then back to draw
- smoke_split_polyline.mjs (14 checks); TESTING/CHANGELOG same-commit
- .room.picked moved after .room.outlined: equal specificity, source
order silently killed the amber highlight in markup (gotcha x4,
documented inline)
- smoke_merge_highlight.mjs (waits out the 0.12s transition before
reading computed colors); TESTING/CHANGELOG same-commit
- _markupClick ignores clicks originating from .roomlabel/.rlhandle
(composedPath) and anything during an active drag/resize
- smoke_card_tool_conflict.mjs (draw/delroom/handle/during-resize +
normal stage click still works); TESTING/CHANGELOG same-commit
- room label becomes a card: name on top, optional metrics below
(temperature / humidity / avg zigbee / lights), four checkboxes in
space settings, all off by default; lights render On/Off or '1 of 3'
- areaHum + areaLightStats pure helpers (+3 unit tests, 90 total)
- resize via corner handles in the Plan editor (hover), uniform 0.5-3x,
scale stored as layout k next to the position; drag preserves it
- fix latent v1.25 regression: draggable HTML labels were never rendered
in the Plan editor (static SVG only) — real name-only cards now render
there, draggable and resizable
- repair merge/split smokes still calling removed _toggleMarkup
- validation.py: 4 label_* bools; smoke_room_cards.mjs; docs same-commit
- two tabs only: 'Plan editor' / 'Device editor'; View is the implicit
default state, no tab
- Device editor gets its own bottom toolbar (add/show-all/reset/rules
moved from the header), mirroring the Plan toolbar
- X button on both toolbars and inside the active tab returns to View;
re-click on active tab is a no-op; Plan<->Devices switches directly
- smoke_editor_tabs.mjs; TESTING/CHANGELOG/UX-MODES same-commit
- cog next to the space name shows in View/Plan/Devices (canEdit only);
'+' add-space tab stays Plan-only
- tabs flex-centered so the cog aligns with the tab text
- smoke_gear_tabs.mjs; TESTING.md + CHANGELOG same-commit
- View-mode door/window info card gets an Unlock (red) / Lock button when a
lock entity is bound and available; disabled while locking/unlocking,
hidden when unavailable
- plan-icon taps still never toggle locks (hard block untouched);
smoke_lock_action.mjs covers the full matrix incl. the block
- TESTING.md, CHANGELOG, UX-MODES same-commit
- 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