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
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
- 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
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