v1.46.5: audit of every automatic deletion

Owner's decision after the incident: a detached plan is never deleted, at any
age. v1.46.4 gave it a month; this makes it permanent and, more importantly,
writes the reasoning where the next change will trip over it — docs/SCOPE.md now
carries the standing rule. The component may delete a file only when a user
action says so. 'Nothing points at this any more' is not such an action, because
the two errors are not symmetrical: wasted disk is visible, cheap and
reversible; a deleted file is none of those.

Went through every other automatic deletion with the same question. One more
was wrong: houseplan/files/cleanup rmtree'd whatever folder the card named. A
partial migration leaves urls pointing into it — files/migrate deliberately does
not rewrite the ones it could not confirm — so those were live links to files
being deleted; and a wrong or stale id from any client destroyed a live device's
manuals. The server now reads the stored config under its lock and removes only
what nothing references, keeping the rest and saying so.

Also: a plan of a DELETED space now waits thirty days rather than an hour.
Deleting a space is deliberate; an hour is a short window to notice a misclick.

The rest came out clean: layout/delete and marker/room/space removal are all
confirm-guarded user actions, upload temporaries are never user-visible, and
dropping legacy 'segments' is a documented migration.
This commit is contained in:
Matysh
2026-07-28 20:36:17 +03:00
parent f953a3c286
commit 33e71ca96c
17 changed files with 213 additions and 50 deletions
+14 -10
View File
@@ -208,16 +208,20 @@ cancelled would wait for a write that may never come. A new icon has no id yet,
files go to a per-dialog staging folder and move to the real id once the config
write is accepted — the same copy → save → cleanup order as a rebind.
`config/set` collects what its commit superseded — that much a commit knows for
certain. *Unreferenced* is a far weaker signal, and how weak depends on the
case. A space with `plan_url = null` had its image **detached**, which is
reversible and which the editor promises leaves the file alone: those are never
collected. A space that does have a plan can only be holding its own rejected
uploads, so `PLAN_ORPHAN_TTL_S` (1 hour) still applies to them. For attachments,
a per-dialog staging folder holds nothing but an upload from a dialog that was
never saved — one hour again — while a marker's own folder waits
`SCHEDULED_GRACE_S` (30 days). This is not theoretical caution: applying the
one-hour rule to every unreferenced file destroyed two detached plans on
2026-07-28.
certain. *Unreferenced* is a far weaker signal, and the policy follows from one
asymmetry: **a few unnecessary megabytes can always be removed by hand; a file
we should not have removed cannot be brought back.** When the evidence is weak,
keep the file. Owner's decision, 2026-07-28, after the one-hour rule applied to
every unreferenced file destroyed two detached plans.
| Case | What it means | Rule |
|---|---|---|
| In the old revision, not in the new | a save replaced it | removed immediately |
| Space exists, has **no** plan | detached, one click to undo | **never removed** |
| Space exists, has a plan | its own rejected upload | `PLAN_ORPHAN_TTL_S` (1 h) |
| Space no longer exists | deliberate deletion, but people misclick | `SCHEDULED_GRACE_S` (30 d) |
| Attachment in `up_*` | a dialog that was never saved | `PLAN_ORPHAN_TTL_S` (1 h) |
| Attachment in a marker folder | unreferenced, but may come back | `SCHEDULED_GRACE_S` (30 d) |
**Config writes are serialized** (HP-1454-03). `_writeConfig()` chains onto a
single promise: one `config/set` in flight, each carrying the revision the
+20
View File
@@ -1,5 +1,25 @@
# Changelog
## v1.46.5 — 2026-07-28 (audit of every automatic deletion)
- **A detached plan is never deleted, at any age.** v1.46.4 gave it a month;
this makes it permanent and writes the reason down where the next change will
see it. The rule, now in docs/SCOPE.md: the component may delete a file only
when a user action says so — replacing a plan, removing an attachment,
deleting a device. "Nothing points at this any more" is not such an action.
The errors are not symmetrical: wasted disk is visible, cheap and reversible;
a deleted file is none of those.
- **`houseplan/files/cleanup` no longer takes a folder on the client's word.**
After a device is rebound its files are copied to the new id and the old
folder is dropped — with `rmtree`, on whatever id the card sent. Two ways that
ends badly: a partial copy leaves some urls still pointing into that folder
(the migration deliberately does not rewrite those, so they were live links to
files being deleted), and a wrong or stale id from any client would destroy a
live device's manuals. The server now checks the stored configuration itself,
under the config lock, and removes only files nothing references.
- **A plan of a space that was deleted waits thirty days instead of an hour.**
Deleting a space is deliberate, but an hour is a short window in which to
notice it was a misclick.
## v1.46.4 — 2026-07-28 (data loss: detached plans were collected as garbage)
- **A plan you detach is no longer deleted an hour later.** Switching a space to
"draw" clears the reference and, as the editor has always said, leaves the
+20
View File
@@ -6,6 +6,26 @@
> **Правило проекта:** оба файла пополняются в одном коммите с самим
> изменением — как и остальная документация (см. docs/STATUS.md).
## v1.46.5 — 2026-07-28 (ревизия всех автоматических удалений)
- **Отцеплённый план не удаляется никогда, ни в каком возрасте.** В v1.46.4 ему
давался месяц; теперь это навсегда, и причина записана там, где её увидит
следующая правка. Правило, оно же в docs/SCOPE.md: компонент вправе удалить
файл только тогда, когда об этом говорит действие пользователя — замена
плана, удаление вложения, удаление устройства. «На это больше никто не
ссылается» таким действием не является. Ошибки несимметричны: занятое зря
место видно, стоит копейки и обратимо; удалённый файл — ничего из этого.
- **`houseplan/files/cleanup` больше не сносит папку по слову клиента.** После
перепривязки устройства файлы копируются под новый id, а старая папка
удалялась — через `rmtree`, по тому id, который прислала карточка. Два плохих
исхода: при частичном копировании часть ссылок продолжает указывать внутрь
этой папки (миграция намеренно их не переписывает — то есть это были живые
ссылки на удаляемые файлы), а неверный или устаревший id от любого клиента
уничтожил бы инструкции существующего устройства. Теперь сервер сам сверяется
с сохранённой конфигурацией, под её блокировкой, и удаляет только то, на что
никто не ссылается.
- **План удалённого пространства ждёт тридцать дней вместо часа.** Удаление
пространства осознанно, но час — короткое окно, чтобы заметить промах.
## v1.46.4 — 2026-07-28 (потеря данных: отцеплённые планы убирались как мусор)
- **Отцеплённый план больше не удаляется через час.** Переключение пространства
в режим «нарисовать» снимает ссылку и, как редактор всегда и говорил,
+12
View File
@@ -66,6 +66,18 @@ refuse locks or be added to this paragraph.
- Plan-level "security glance": one badge for "all locked / N open" (J2).
- Threshold colouring for room-card metrics (J5).
## Standing rule: never delete a user's file on an inference
Fixed with the owner on 2026-07-28, after automatic collection removed two
detached floor plans. The component may delete a file only when the user's
action says so — replacing a plan, removing an attachment, deleting a device.
"Nothing points at this any more" is not such an action: detaching a plan is one
click and reversible, and the editor tells the user the file stays.
The asymmetry is the whole argument. Wasted disk is visible, cheap and
reversible; a deleted file is none of those. Where the evidence is weak, keep
the file — and if a future version wants to reclaim that space, it asks.
## Out of scope — never build, point users to the right tool
- Automations, scenes, scripts, notifications → HA core.
+2 -2
View File
@@ -15,12 +15,12 @@
| Item | State |
|---|---|
| Version | **v1.46.4** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance |
| Version | **v1.46.5** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance |
| Workflow | Since 2026-07-22: minor changes go to branch **`dev`** (build + smokes → deploy home → commit → push, NO release); releases are batched on the owner's command (merge dev→main, one tag, one release with a summary changelog, CI checked on dev beforehand) |
| GitHub | https://github.com/Matysh/houseplan-card — **`main` carries every published release, the latest tag is the current version above**; `dev` is where work lands and is merged into `main` at release time (so `dev` is normally equal to or ahead of `main`, never behind). Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
| CI | validate.yml (hacs + hassfest + frontend + backend) green; release.yml attaches the bundle on release publish |
| HACS | Custom repository works. **Inclusion PR: hacs/default#9004** — open, valid, labeled; ~864 older open PRs but merge rate ≈180/mo; realistic ETA 13 months (checked 2026-07-24) |
| Home instance | ha.jbstudio.pro (SSH port 323, key `ha_jb`), deployed **v1.46.4** via direct copy (HACS custom repo also installed) |
| Home instance | ha.jbstudio.pro (SSH port 323, key `ha_jb`), deployed **v1.46.5** via direct copy (HACS custom repo also installed) |
| Localization | UI en/ru (src/i18n/*.json), everything user-visible localized incl. kiosk popover |
| Tests | Four layers: frontend unit (`npm test`, node:test over `test-build/`), pure backend (`pytest tests_backend`, runs anywhere), HA-harness backend (same folder, CI only — needs py3.13 + pytest-homeassistant-custom-component), and browser smokes (`demo/smoke_*.mjs`, headless chromium). **Counts are not written down here** — they went stale within two releases while the version line beside them was kept current, which reads as less coverage than exists (review R5-2). Run `npm run inventory` for the current numbers, or read them off the last CI run |
| Community | **Telegram chat: https://t.me/ha_houseplan** (created 2026-07-27) — the primary user-facing support channel; GitHub issues stay for bugs/features. Link it from any new release notes and posts |
+9 -4
View File
@@ -239,10 +239,15 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
on the plan after a reload. Same for each tap action and each fill mode
[auto: backend test_every_display_mode_the_editor_offers_is_accepted and
neighbours, test_a_marker_showing_its_value_can_be_saved]
- [ ] Detaching a plan keeps the file (v1.46.4): switch a space to "draw",
restart, wait a day — the image is still in `config/houseplan/plans/` and
can be re-attached. Replacing a plan still removes the one it replaced,
immediately [auto: unit: test_scheduled_collection_never_takes_a_detached_plan]
- [ ] Detaching a plan keeps the file (v1.46.4/v1.46.5): switch a space to
"draw", restart, wait — the image stays in `config/houseplan/plans/`
indefinitely and can be re-attached. Replacing a plan still removes the one
it replaced, immediately
[auto: unit: test_scheduled_collection_never_takes_a_detached_plan]
- [ ] Rebinding a device does not eat its manuals (v1.46.5): attach two files to
a device, rebind it to another HA device — both are readable afterwards.
If a copy failed, the file it failed on is still there rather than deleted
with the folder [auto: backend test_files_cleanup_keeps_referenced_files]
- [ ] Nothing accumulates on an idle instance (v1.46.2/v1.46.3, HP-1461-01,
HP-1462-01): attach a file, cancel the dialog, and do not save anything
else — the file is gone after a restart AND after the daily pass, while