feat(devices): пакетное «Скрыть/Показать выбранные» в каталоге устройств (#618)

На вкладках «На плане» и «Скрытые» — флажки строк, «Выбрать все (N)» по
всему отфильтрованному набору (включая строки за «Показать ещё»), панель
«Выбрано: K · Скрыть/Показать выбранные (K) · Снять выбор» в отдельном
контейнере .device-inbox-batch вне .device-inbox-filters.

- src/device-inbox.ts: чистые inboxVisibilityAllowed (один источник для
  canHide/canShow и выбора), selectableInboxRows, effectiveInboxSelection,
  applyInboxVisibility — свёртка одиночных действий; одиночное = пакет из
  одной строки.
- src/device-inbox-batch.ts (новый, lazy editor): выбор, панель, строка-
  флажок и writeInboxVisibility — одна запись houseplan/config/set с
  expected_rev на пакет, busy='__batch__' (inert), откат маркеров при отказе,
  выбор сохраняется и сужается. Одиночный _setInboxHidden рантайма — тонкая
  обёртка над той же записью; рантайм стал короче базы на 14 строк.
- сброс выбора при смене вкладки (клик и стрелка), поиска и «Только новые».
- i18n en/ru/de/fr: 9 ключей по ТЗ §8.
- смок demo/smoke_device_inbox_batch.mjs (AC1–AC8, AC10), юниты
  test/device-inbox.test.mjs (AC2, AC4), 4 мутанта в реестре.
- docs: USER-GUIDE ru/en, FILTERING.md, оба changelog.
- monolith-baseline: hostRefs 4948→4959, bundleBytes 2500387→2505569 —
  новая фича (состояние выбора и запись идут через порт хоста).
- bundle-budget: LAZY_EDITOR_GZIP_CEILING 245 400 → 246 600 (замер 245 610,
  +1 190 Б gzip от базы 244 420); initial View 289 449 → 289 765 (+316 Б,
  строки en-словаря), в пределах потолка.

Golden device-inbox-* изменятся ожидаемо — пересъёмка предрелизным гейтом
на Linux CI (§11.4).

Issue: #618
User-Visible: yes
This commit is contained in:
Claude
2026-09-24 22:50:30 +03:00
parent 2b3959e55b
commit d17bcc05fb
17 changed files with 873 additions and 39 deletions
+49
View File
@@ -2064,6 +2064,55 @@ const MUTANT_DEFINITIONS = [
replace: ' if (isRemovedPlanEntity(h, eid, removed) && !removedBindings.has(value)) continue;',
}],
},
// #618: batch Hide/Show in the Devices catalog.
{
id: 'device-inbox-batch-eligibility-active',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="issue 618" test/device-inbox.test.mjs',
because: 'an HA-disabled, orphaned or unverified binding must be neither selectable nor counted in '
+ '"Select all (N)"; otherwise a batch writes hidden flags the single-row action refuses (#618 B2)',
patches: [{
file: 'src/device-inbox.ts',
find: " return isInboxBatchTab(category) && status.kind === 'active';",
replace: ' return isInboxBatchTab(category) && !!status.kind;',
}],
},
{
id: 'device-inbox-show-keeps-stub',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="issue 618" test/device-inbox.test.mjs',
because: 'Show must keep the marker with hidden:false; dropping an automatic stub lets the seeder '
+ 'hide the device again on the next rebuild (docs/FILTERING.md, #618 B5)',
patches: [{
file: 'src/device-inbox.ts',
find: ' && (marker.binding !== row.binding || marker.removed === true)),\n updated,\n ];',
replace: ' && (marker.binding !== row.binding || marker.removed === true)),\n'
+ ' ...(hidden ? [updated] : []),\n ];',
}],
},
{
id: 'device-inbox-batch-single-write',
guard: 'node demo/smoke_device_inbox_batch.mjs',
because: 'a batch of K rows must be exactly one houseplan/config/set with expected_rev; K writes '
+ 'mean K revisions and K inert waits, the problem #618 exists to remove',
patches: [{
file: 'src/device-inbox-batch.ts',
find: ' await deps.saveConfigNow(); // #618 B5: one write per batch',
replace: ' for (let write = 0; write < rows.length; write += 1) await deps.saveConfigNow(); '
+ '// #618 B5: one write per batch',
}],
},
{
id: 'device-inbox-batch-rollback',
guard: 'node demo/smoke_device_inbox_batch.mjs',
because: 'a rejected batch write must leave every marker unchanged; without the rollback the '
+ 'catalog shows K rows moved that the server never accepted (#618 B8)',
patches: [{
file: 'src/device-inbox-batch.ts',
find: ' if (host._serverCfg === cfg) cfg.markers = previousMarkers; // #618 B8 rollback',
replace: ' // rollback removed by mutant',
}],
},
{
id: 'live-child-still-suppressed-by-parent-tombstone',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '