Волна 1 эпика #674 — мёртвое без риска, каждое имя проверено git grep по текущему dev. Удалено: шесть demo/shot_*.mjs без читателей (shot_furniture ещё и не работает с #159), demo/capture_wall_strip_backup.mjs, demo/benchmark_coordinate_write_barrier.mjs (нигде не запускался, но входил в манифест smoke через demo/benchmark_*.mjs — теперь лист покрытия чист), scripts/dev/styles-split.mjs (падает на текущем src/styles.ts), docs/README.ru.md (индекс четырёх документов из 38 — роль у README.ru.md), demo/README.md (одна строка в карте пакета AGENTS.md вместо него) и из legacy/ — снимок аудита v1.58.0, черновик 089, завершённые планы, продуктовые снимки и две разовые диагностики; история git хранит. legacy/docs/SUN-CONTRAST.md остаётся: на него ссылаются src/sun.ts и SUN.md. Перенесено в legacy/docs/: superpowers/specs (11 дизайн-документов до процесса), QUALITY-560.md, ROADMAP.md, STATUS-FEATURES.md. Ссылки поправлены там, где они были: FILTERING.md, ТЗ 006/007/058, testing-notes, комментарии src/open-spans.ts и validation.py (только путь документа), SCOPE.md, STATUS.md, smoke_household_journeys.mjs, опись legacy/README.md; ложный маркер benchmark_coordinate_write_barrier снят с чек-листа. Issue: #678 User-Visible: no Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
24 KiB
Устройства и маркеры
Приложение к
docs/TESTING.md: перенесено оттуда дословно (#634). Индекс всех приложений —README.md.
Device icon design package (#179)
- Pure presentation tests cover lock/unlock, exact marker-only LQI bands
0/40/41/179/180, unchanged room gradient, package pulse defaults, semantic colors and reduced motion [unit:device-presentation.test.mjs,device-pulse.test.mjs]. - Shared face tests cover shell/core DOM, four Double positions, a third
legacy section, deterministic font fitting, full text and safe CSS color
variables [unit:
device-face.test.mjs]. - The browser renders the exact shell ratio and shadow color, Light/Dark
cores without backdrop blur, state/LQI colors, 3.6 s presence pulse,
unavailable no-hover, full Text/Double values, 44×44 target, View and
Device-editor keyboard paths, and no Plan tab stop
[auto:
smoke_device_icon_design.mjs]. - Full plan, preview and static card preserve the same face after the DOM
redesign; static mode, state/value and disabled-device contracts stay
green [auto:
smoke_device_preview_parity.mjs,smoke_static_icon.mjs,smoke_state_value.mjs,smoke_disabled_device.mjs]. - Restoring unavailable hover, shifting the LQI boundary, restoring value
ellipsis or bypassing
_clickDevice()makes its guard red [mutation:device-unavailable-hover-restored,device-marker-lqi-low-boundary-shifted,device-long-value-ellipsis-restored,device-keyboard-bypasses-click-path]. - Pre-beta golden reviews desktop/mobile Light/Dark states, combo states, Text, four Double positions, long and legacy values, LQI, reduced motion, sizes 32/56/96 and colored backgrounds. Full smoke/golden/performance remains a Linux release gate.
Dense device-marker hit ownership (#564)
- Pure geometry covers visible capsule priority, invisible 44 px-floor
overlap, nearest-core selection, stable exact ties, stadium corners and
spatial-index locality [unit:
device-hit-owner.test.mjs]. - Source contracts keep painted shells globally above transparent floors,
route hover/click/pointer lifecycle through one semantic owner and forbid
layout reads from the pointer-move path
[unit:
device-hit-owner-contract.test.mjs]. - Household J7 checks the five-marker dense column at tablet and phone
widths: every visible centre has the same native and semantic owner, its
click opens that marker, and a pointer sequence remains latched through
its terminal click [auto:
smoke_household_journeys.mjs]. - Real browser geometry covers Icon, Text, Double and untouched legacy
faces at all four cardinal sides/directions. In every case a painted
point overlaps only the neighbour's invisible 44 px floor and still owns
the native hit, semantic owner and click
[auto:
smoke_device_hit_capsules.mjs]. - Replacing nearest-screen-core selection with first-input/DOM order makes
both the pure geometry guard and the real rendered-face browser guard red
[mutations:
dense-device-hit-falls-back-to-input-order,dense-device-hit-browser-skips-painted-priority].
Device marker polish and pointer modality (#212)
- Shared icon geometry applies one 0.9 visual factor after card/per-marker
sizing, keeps the saved centre and 44×44 hit floor unchanged, and gives
wide Text cores a radius equal to half their height
[unit:
device-marker-polish-contract.test.mjs; auto:smoke_device_icon_design.mjs]. - Only a genuinely dispatched toggle/run action produces one
1 → .95 → 1feedback cycle lasting 200 ms. Info, editor, confirmation before acceptance, unavailable/secure/no-target and cancelled gestures do not; reduced motion has no scale tween [auto:smoke_device_icon_design.mjs]. - Pointer authority is isolated per card. Touch/pen and compatibility mouse
input clear JS/CSS hover, while a later real mouse restores it only on
fine/hover hardware; mode/space/visibility/disconnect cleanup remains
bounded [unit:
pointer-modality.test.mjs; auto:smoke_feedback_v2.mjs]. - Pre-beta visual review covers Light/Dark desktop and touch matrices for ordinary, Text, Double, unavailable and vacuum markers; full golden and performance gates remain release work.
Device value badge (#90)
- An untouched legacy thermometer/humidity marker remains pixel-identical;
saving another field does not materialize
value_badge. [auto: device-presentation] - Explicit on/off overrides the legacy temperature gate; zero, false and
off remain visible, while missing/unknown/unavailable render a stable
—. [auto: device-presentation] - State, every allowlisted attribute, derived LQI and
marker:<id>light state resolve identically on the full plan, static space card and preview. [auto: smoke_device_preview_parity] - Opening the editor explicitly selects the persisted source and position, even when they are not the first dynamic options, without touching config. [auto: smoke_device_preview_parity]
- Right, bottom, left and top update live in the editor; bottom stacks above system LQI and derived LQI suppresses the duplicate system row. [auto: device-presentation]
- Browser bounding boxes stay inside
.previewstagewith a safe gap for all positions, a long value, scale ×3 and the maximum activity ring. [auto: smoke_device_preview_parity] - Rebind resets the source, delete leaves a missing diagnostic reference, and space import remaps internal refs or disables/counts external refs. [auto: test_ha_import_export]
static_iconsuppresses but preserves the setting; live-state and room label toggles do not suppress an explicit badge. [auto: device-presentation]
Policy: this checklist is updated in the same commit as any functional change (like CHANGELOG.md). For a pre-release, build the production bundle and run the smallest unit/smoke subset that covers its changed surfaces. Run the complete local frontend, backend and smoke gates only before a stable release. The exact-SHA GitHub Validate remains mandatory for publication. Items marked
[manual]are covered by unit tests or the headless smokes indemo/— they still deserve an occasional eyeball. File every failure as a GitHub issue before fixing.
What
[manual]means (since 2026-07-27). A named check exists that FAILS when the behaviour breaks — innpm testor in the smoke suite, both of which run in CI on every push. Where the check lives is written next to the marker ([auto: smoke_modes]). Before this date the marker described an intention: the smoke suite printed values and always exited 0, so 96 markers guarded nothing (external audit T1/T3). If you add a checklist line marked[manual], add the failing check in the same commit.
⚠ Rule: a new scrollable list inside a dialog is tested by GEOMETRY, never by the DOM. Any new scrolling box or
overflowcontainer added to a dialog MUST get a smoke that measures the container's own height and the visible position of its first item (getBoundingClientRect, and the item's rect against the box's rect) — counting rendered rows, or asserting that the nodes exist, proves nothing. The failure mode is always the same and always invisible to a DOM check: a scrolling box is a flex item whose automatic minimum size is zero (min-height: auto→ 0 for anoverflowchild), and a dialog body is a flex column with a height cap, so the box is the one child that can be squeezed to a sliver while every row inside it renders happily. It has bitten us twice already: the target search results in the tap action dialog (v1.53.1 — 26 matching automations rendered into a 1 px stripe; the smoke counted rows and passed) and the «Already uploaded» plan picker (dev, unreleased — rows present, box 14 px tall, same story). Both smokes measure heights now; write the third one that way from the start.
HA-disabled binding gate
The source-of-truth matrix is
legacy/docs/superpowers/specs/2026-08-08-ha-disabled-devices-design.md §17.
test/ha-binding-status.test.mjs covers full/limited registry decisions and
the active-only state projection. The standalone demo exposes complete
disabled_by rows through both registry list WS commands plus
window.__setRegistryDisabled(kind, id, disabledBy) and
window.__setRegistryAccess(mode) for browser scenarios.
- A saved device/entity marker disappears from View, room data, Glow, controls, live text, openings and vacuum overlays after its registry row becomes disabled; config/layout remain byte-for-byte unchanged.
- Device editor → Hidden and disabled shows a labelled grey ghost. Show is refused, metadata/Delete/Open in HA remain available, and the ghost is not draggable.
- Reactivating the same ID restores its metadata/layout without a false new-device event; an explicitly user-hidden marker stays hidden.
- A never-seen auto device disabled before discovery appears as new only after its first activation.
- All disabled child entities make an otherwise active device disabled; one disabled auxiliary entity never suppresses active functional rows.
- If full registry WS access is denied, positive live evidence stays
active, an unknown binding is
unverified, and no false disabled/orphaned ghost or service call is produced. - Two full cards plus a static space card share one registry fetch and one subscription pair per HA connection; registry events invalidate all of them without a reload.
houseplanDiagnostics()reports only redacted registry access/age/error and binding-status counts; it contains no names, states or marker data.
HA Area marker relocation (#126)
test/device-area-relocation.test.mjscovers direct-binding authority, same/cross-space transitions, conservative legacy backfill, explicit and composite exclusions, rebind, malformed metadata and delete-first provenance.test/space-geometry.test.mjsproves both marker-position paths can suppress one stale saved point without changing the stored layout input.demo/smoke_area_relocation.mjschanges an authoritative registry Area against the production bundle, checks one serialized layout delete plus config/attention persistence, and proves the read-only hosted card moves immediately without writes.- Area-provenance cleanup ignores the filtered display roster, preserves empty Device/Entity Registry namespaces, accepts exact live entity states as existence evidence, requests only one confirmation refresh and removes an orphan only after two distinct non-empty authoritative revisions.
- A rejected confirmed-cleanup config write remains retryable on the next rebuild; the same revision, state ticks and repeated empty frames never become extra confirmation or a registry reload loop.
- Backend validation and import/export tests cover the 20,000-entry bound, exact entry schema, same-source preservation and cross-source removal.
Device display preview and face parity
The behaviour matrix is defined in
legacy/docs/superpowers/specs/2026-08-08-device-display-preview-design.md §22.
Pure source/value/presentation rules live in test/device-presentation.test.mjs.
demo/smoke_device_preview_parity.mjs compares the same live fixture across
the interactive plan, hp-device-preview and houseplan-space-card, including
semantic classes, icon/value/badges, scale variables, provider text and the
public binding-status hook.
- Every binding/display/icon/size/angle/control/temperature draft change updates the preview before Save; Cancel writes neither config nor layout.
- Working, open, cover, presence, short event, transition, alarm, static,
unavailable, media-neutral, composite-Power and
live_states: falseexplanations match the actual face. - The local short-activity demo lasts 3.3 seconds and the continuous demo runs until stopped. Neither sends a service call; reduced motion uses a compact dot, and both reset immediately on binding change, real activity or alarm.
- Provider metadata is cached between dialog openings and refreshed after registry/config-entry changes; source integrations remain separate from the binding provider.
- Long provider/source/state text wraps without horizontal scroll; maximum marker/ripple size fits the stage and reports its preview scale.
- Derived temperature/humidity values keep the compact plan form (
22.4°,48%), while a direct entity value continues to use HA localization and units.
Device icon package parity (#211)
The independent reference subset under
demo/srv/reference/device-icons/ comes directly from designer package 1.1.1;
it is not generated from production CSS. The package archive hash and the
owner's #219 red/green Lock/Unlock paint override are recorded in that
directory's README.
node demo/smoke_device_icon_design.mjsreads the SVG colors and stroke widths, then compares them with fresh computed styles. It also measures circular core/shell geometry, the realmdi:lightbulb-spotpainted path, value-pill radius and 44×44 hit area at 32/56/96 px.node demo/capture_device_icon_reference.mjswrites a two-column Reference SVG / Runtime matrix for both themes toartifacts/device-icon-reference/. Code review must inspect this artifact visually; a green historical golden is not proof of package parity.- Preview/static parity and unavailable keyboard/tap behavior remain
covered by
smoke_device_preview_parity,smoke_static_iconandsmoke_disabled_deviceafter a fresh production build.
Device marker geometry and input polish (#213)
node demo/smoke_device_icon_pixel_alignment.mjscovers core bases 24…112 CSS px in quarter-pixel steps at DPR 1/1.25/1.5/2. DOM centres, isolated painted centroids/support and a deliberate 1 CSS px mutant must distinguish browser raster parity from a persistent offset.node demo/smoke_device_icon_design.mjskeeps the effective 32/56/96 geometry, uses the direct 0.55 MDI/core ratio and proves hover plus the configured action from the far value-capsule end at right/bottom/left/top.- Opening binding/registry-less/lock-action smokes preserve the secure no-toggle-on-plan invariant while checking compact Light/Dark locked/unlocked/unknown shell/core presentation.
node demo/smoke_opening_entity_search.mjschecks the real opening dialog: contact and lock search by friendly name/entity ID, preserved contact priority, visible IDs, persistent none option and unchangedopening.contact/opening.lockstorage.- Unit presentation coverage compares marker LQI colour with the shared
continuous
lqiColor()across former 40/41 and 179/180 boundaries; bands remain semantic metadata only.
Text marker shell shape (#217)
node demo/smoke_device_icon_design.mjschecks the external Text frame, not only its core: a long value keeps a saturating capsule radius at 24/32/56/96/112 px, while Icon-only remains circular and Double remains a capsule. The runtime mutation toborder-radius: 50%must be rejected.device-text-shell-long-lightanddevice-text-shell-long-darkisolate a large498 ppmText marker. Golden review must visibly confirm straight upper/lower middle sections rather than an ellipse.node demo/capture_device_icon_reference.mjsincludes an additional 96 px Text row beside the normative Light/DarkText Default.svg.
Device lock and orange foreground palette (#219)
- Closed/
lockedis green#66D17A; open/unlockedis red#F0410C. The same core/stroke palette is used by ordinary lock markers and compact door/gate lock badges; glyph shape remains closed/open/question. - Every device glyph on an orange core (
on/working and physicalopen) is white in Light and#252525in Dark.device-icon-state-table-lightanddevice-icon-state-table-darkshowonandopentogether, plus both lock states [unit: device-marker-polish-contract; auto: smoke_device_icon_design; golden: device-icon-state-table-*]. - Alarm, hover, focus, selected, unavailable, virtual, press feedback, pulse, hit-area and lock actions retain their existing priority and behaviour [unit: device presentation/polish/pointer; visual source review].
Unified device status and pulse activity (#98)
- The Display list contains exactly Icon + state, Icon + state and activity,
Value + state, Always static icon, in that order. Legacy
display: ripplereads and saves back asicon_ripple - Icon + dynamic plate shows state plate/morph but no ordinary activity effect; Icon + activity adds the semantic effect; Value keeps the state-coloured plate and hides ordinary activity; Always static icon keeps one neutral base icon and suppresses all state-driven visuals
- Motion/vibration/sound/contact rising edges render exactly three waves for about 3.3 s; initial load and recovery from unknown/unavailable do not fake an event; a rapid retrigger restarts it
- Occupancy/presence is one calm continuous pulse for the whole active state
- Cover/lock/valve movement continuously pulses until the travelling state ends;
direct terminal
closed ↔ open/locked ↔ unlockedwithout an intermediate state breathes for about 3.3 s - Actual work (light/switch/fan/humidifier on, active climate action,
vacuum cleaning, script running) is yellow and slowly
breathes in Icon + activity;
automation = onis merely enabled and remains neutral - Every
media_playeris neutral and has no running activity foron,idle,playing,pausedand other transport states; explicitoffuses the same faded treatment asunknown/unavailable. Several resolved media entities fade only when none is available and powered - Controls aggregate their targets: any working target drives both the yellow plate and the running effect
- Open contact/open valve are orange; unlocked lock is red and locked lock is green; an open cover stays neutral because its icon morph carries that state
- Unavailable suppresses ordinary activity. Alarm outranks all dynamic
presentation, including when ordinary live states are off;
static_icondeliberately hides alarm paint without suppressing service-call errors static_iconhides temperature/humidity/LQI, RGB, value, icon morph, activity and live vacuum puck/trails/room highlight on the full and static cards; preview still names the real HA state/source and explains the static result- Switching a static vacuum back to a dynamic display restores applicable live/server trails; choosing static never deletes stored trail history
value_static_icon(#588) shows the same value asvalue— including on the plan itself and the space card, where sources are resolved lazily — while state, alarm, unavailability, RGB and activity never paint it; no pulse, no °/%/LQI, value badge suppressed with its setting kept, and the live vacuum puck, trail and route warning stay hidden [auto:smoke_static_icon.mjs, goldendevice-icon-state-table-*]- Activity colour and size (×2..×8) apply per device; alarm ignores them
- Icon size ×0.5..×3 and rotation 0..355° apply per device; the temp/humidity badges scale with the icon
- With OS "reduce motion" enabled, ordinary activity becomes a compact solid dot; alarm keeps the red plate and accessible alarm description, without an animated or static ring
Climate temperature opt-in (dev)
- «Use the device's temperature sensor» (marker dialog, climate devices
only, default OFF): current_temperature shows as the standard
.tvalbadge and joins the room average like a thermometer; unavailable / missing attribute = no badge, no vote; hidden devices keep voting (registry-wide climate, like hidden thermometers); the tick survives dialog recreation [auto: smoke_climate_temp; units: test/devices.test.mjs; backend: tests_backend/test_validation.py (use_climate_temp)]
Styling hooks and HA-formatted values (docs/STYLING-HOOKS.md, dev, unreleased)
-
The hooks are there and they are the config's ids: open the plan's DOM (devtools → the card's shadow root) and check that a device marker carries
data-hp="device",data-id,data-entityanddata-area; a roomdata-hp="room"+data-id+data-area; a door/window/gatedata-hp="opening"+data-kind; a decor shapedata-hp="decor"+data-kind; a visible room carddata-hp="room-label"; a floor tabdata-hp="space-tab". The ids are the ones in your config, not DOM positions — they survive a reload [auto: smoke_styling_hooks] -
Absent is absent: a virtual marker has NO
data-entityat all, and a sub-area room (no HA area) has NOdata-area— never the string «undefined» [auto: smoke_styling_hooks] -
A card-mod rule actually applies: with card-mod installed, add
ha-card [data-hp="device"] .lqi { display: none; }to the card — the signal badges disappear and nothing else moves. Then target one marker by[data-entity="…"]and confirm it is the only one affected [manual] -
The static card carries the same hooks: a
houseplan-space-cardhasdata-hpon its rooms, visible room labels and markers (it draws no openings and no decor, so those are simply absent), and it needs its OWN card-mod block — it is a different card with its own shadow root [auto: smoke_styling_hooks] -
ha-iconinternals stay out of reach: a rule may style the icon HOST (colour, transform) but cannot reach the<svg>inside it. That is a browser rule, and it is why the hooks sit on our wrappers [auto: smoke_styling_hooks] -
A value badge is formatted by HA: set a numeric sensor's display precision in HA (Settings → the entity → Display precision) to 1 and put it on the plan as «value instead of icon». The badge shows the rounded number with YOUR decimal separator and the entity's unit — once, not twice — and matches what more-info shows [auto: smoke_value_format]
-
A live decor label is formatted by HA: the same sensor in a text shape reads identically; a switch shows «Включено», not
on; an attribute goes through the attribute formatter (a climate'scurrent_temperatureis a number, not the climate's state) [auto: smoke_value_format + unit logic.test] -
A literal suffix stays literal: write an attribute token followed by
проц.— the suffix is part of the text, with no hidden unit override and no duplicate appended by the label renderer [auto: smoke_live_text] -
An older Home Assistant is unchanged: on an HA without
formatEntityStatethe badge and the label print the raw state with the entity's unit appended, exactly as before — nothing is blank and nothing throws [auto: smoke_value_format + unit logic.test] -
The °/% plates are untouched: the small temperature/humidity badges next to an icon (and the same numbers in a room card and the tooltip) still read «21.5°» / «48%». They are a derived reading, not an entity state — deliberately ours [auto: smoke_value_format]
-
The text block's handles are small but still catchable: select a label in the Background editor. The four corner circles and the rotate handle are a quarter of their old size — beads, not buttons — and the dashed frame no longer hides the text. Now grab one on a TABLET with a finger, aiming roughly at it rather than exactly: it is caught, because the invisible hit circle is still the old finger-sized one. Same at any zoom [auto: smoke_decor_text]