Profiling #694 found three costs on every View pass, paid even with the
summary panel hidden.
The summary panel read the safe-area probe's computed style in layout(),
which the card reaches up to five times per render (renderControls,
menuItems, renderPanel twice, the clock check), and its updated()
measured the stage, probe and kiosk buttons after every DOM commit. The
insets now live in the measured state: measureLayout is the only method
that reads style or layout, and updated() calls it only when an input of
the measurement changed (probe, kiosk buttons or stage element, title,
language, mode, kiosk, kiosk scale, narrow, HA theme), after connect()
or an identity change, on visibility, once after document.fonts.ready,
and from resized() as before. A floor switch or an HA tick no longer
measures.
The _model getter rebuilt the config fingerprint (a walk over every
space and room with JSON.stringify of room settings) on each of its
dozens of reads per render. ConfigFingerprintPass remembers the whole
cache key (epoch and fingerprint) from the start of willUpdate() to the
end of render() while the epoch, the config object and its spaces array
are unchanged. Remembering only the fingerprint and concatenating the key
on every read was tried first: in 2.5D on the large house the switch cycle
measured slower than without any memo, and CPU profiles showed several
times more garbage collection on load and on the first visit of a floor;
one remembered key per pass has neither. Outside the pass (handlers, updated(),
timers) every read still builds the key, so an in-place edit without an
epoch bump stays visible (HP-1454-04). No write to the fingerprinted
fields is reachable from willUpdate() or render().
_isoScene read the stage box during render only to feed an aspect into
the overlay fit, whose frame has not depended on the aspect since #713.
It now uses the frame's own aspect and passes stageSize: null.
render-layout-read.mjs now also judges _isoScene and the whole summary
runtime except measureLayout, forbids layout property reads
(clientWidth, offsetTop, ...) besides the two calls, and reports every
violation. Two registered mutants restore the old reads.
No visible change: panel caps, side, offsets and kiosk clearance are
computed from the same values; the 2.5D frame is the same.
Issue: #725
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
На окне не шире 480 px шапка карточки и страницы бокового меню занимает одну
строку ≤ 56 px: вкладки пространств (без шестерёнок и «+», прокрутка вбок,
текущая вкладка докручивается в видимую область), масштаб, одна шестерёнка
«Действия и настройки» и у админа слот крестика #647. Заголовок скрыт.
Меню шестерёнки (src/header-menu.ts): редакторы, настройки и добавление
пространства, общие настройки, PDF, помощь, сводная панель и объёмный вид
(alpha) — в фиксированном порядке и с теми же условиями и действиями, что у
прежних кнопок. Пункт закрывает меню; Escape возвращает фокус на шестерёнку;
касание вне меню поглощается прозрачной подложкой. На > 480 px шапка прежняя,
в киоске меню нет.
- три кнопки шапки вынесены в renderHeaderActions — шапка карточки не растёт;
- сводная панель отдаёт свои пункты методом menuItems();
- i18n: title.header_menu (en/ru/de/fr);
- потолок initial View 291 000 → 292 600 (замер 291 635), база bundleBytes;
- смок smoke_mobile_view_header, юнит header-menu, 10 мутантов; смоки
support_feedback, gear_tabs, modes, toolbar_stable_width переведены на
меню на ≤ 480 px; подпись вкладки в span.tabtitle без pointer-events;
- USER-GUIDE.ru, UX-MODES, TOUCH-SUPPORT, STYLING-HOOKS, data-hp-contract,
оба changelog.
Issue: #616
User-Visible: yes
Карточка и редакторский рантайм держали ≈380 неиспользуемых импортов, 56
мёртвых объявлений и дублей типов (warm-boot, LS_*, GLOW_*, debounce,
navigate, lruRead — копии карточки в рантайме) и 112 приватных членов
карточки, которых не читал никто — делегаты `_editorRuntimeOrThrow()._x()`,
оставшиеся от выноса #425, и аксессоры glow-состояния. Всё это снято; в 9
других файлах — по одиночной ошибке. Делегаты и поля, которых касаются
браузерные смоки (`card._x(...)`), оставлены и посчитаны отдельно.
Гейт `npm run lint:unused` (scripts/unused-locals-gate.mjs, в gate:small и
Validate после сборки): `tsc --noUnusedLocals` чист, кроме приватных членов
карточки из порта HouseplanEditorHostPort / `host.` (portPrivates) и членов,
которых зовёт харнесс (harnessPrivates); храповик по шести числам
scripts/monolith-metrics.mjs против scripts/monolith-baseline.json —
delegates 260→159, portMembers 350, hostRefs 4948, portPrivates 96,
harnessPrivates 107, bundleBytes 2 510 141→2 500 387. `npm run inventory`
печатает те же числа. Заморозка 54 тестов, читающих монолит как текст
(test/monolith-text-anchors.test.mjs); PROCESS.md §2.7 — правило.
Логический исходник для контрактных тестов (test/houseplan-source.mjs)
дописывает члены рантайма без делегата в карточке — контракт продукта не
зависит от наличия заглушки. Потолки ядер и initial gzip опущены на выигрыш
(292 000 → 290 400). Бандл пересобран, три копии синхронны.
Issue: #624
User-Visible: no
Сводная панель теперь использует канонический порядок карточек Home Assistant и не переносит локальные настройки между визуальными колонками после remount.
Issue: #561
User-Visible: yes
`src/config-adoption.ts` owns config/layout with revision and fingerprint;
the host keeps `_serverCfg`/`_cfgRev`/`_layout`/`_layoutRev` as delegates.
All seven authoritative adoptions go through `adoptAuthoritativeGated`
(backdrop readiness → continuity → adopt → profile tail); the post-write
paths (space/delete ×2, optimize_undo, import/apply) gain the gate and take
revisions from the re-read bodies. `rollbackOptimistic` moves to the owner;
plan-optimize, space copy and the vacuum writers stop assigning identity.
Issue: #500
User-Visible: no
Moving the aggregate out of render fixed the first frame, but the work
itself was still one uninterrupted ~1.5 s task on the large-house
fixture — the interface stayed frozen, just a moment later, which is the
same symptom the issue reports. cleanFloorAreaSteps yields after every
room; the runtime advances it with an 8 ms budget per frame and
reschedules until it finishes, so no slice outlives a frame and the
skeletons stay until the number is ready.
The smoke now watches longtask entries for the whole show, not only the
first frame: a single long task while the values are computed fails it.
Issue: #509
User-Visible: no
Two halves of the same first paint. The panel showed «Source unavailable»
in every row until the lazy metrics chunk arrived, because value() could
not tell "not loaded yet" from "source is dead"; and metrics() ran inside
render, walking the HA registry and unioning the clean floor of every
space synchronously — 11 s on the large-house fixture.
- totalCleanFloorAreaM2 computes the space's masonry and junction
topology once per SPACE and hands them to innerContourForRoom, which
otherwise unions the whole space again for every room: 11 045 → 1 488 ms
on that fixture, same 306.3 m². The card has always done this through
its own _innerContour cache; the panel now does the same.
- Aggregates leave the render path: the first frame paints skeletons and
the work starts right after the frame is shown (timeout → rAF →
timeout, never requestIdleCallback, which under load would leave the
skeleton up for seconds). Stale memo keeps the previous number on
screen instead of flashing back to a skeleton.
- valueState() separates pending from unavailable; a pending row keeps
the same plate, grid and height and carries a pulsing rectangle the
height of the line, replaced by the value with a short fade. Reduced
motion keeps the rectangle and drops the pulse.
- Panel enter/exit animation (#505, 190 ms) is now actually visible —
the main thread is free — and the smoke witnesses it.
Mutants: summary-first-paint-shows-unavailable, summary-metrics-block-first-frame,
summary-area-recomputes-walls-per-room, summary-stale-metric-falls-back-to-skeleton.
Issue: #509
User-Visible: yes