fix v1.44.2: external review CR-1..CR-3

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
This commit is contained in:
Matysh
2026-07-27 12:58:27 +03:00
parent 45c863138a
commit ae9168f6ec
18 changed files with 289 additions and 48 deletions
+15 -2
View File
@@ -1070,12 +1070,25 @@ export function roomFillModeOf(
* server moves /files/<oldId>/ to /files/<newId>/, the urls must follow.
*/
export function migratePdfUrls<T extends { url: string }>(
pdfs: T[], oldId: string, newId: string,
pdfs: T[], oldId: string, newId: string, mapping?: Record<string, string>,
): T[] {
if (!oldId || !newId || oldId === newId) return pdfs;
const from = '/files/' + oldId + '/';
const to = '/files/' + newId + '/';
return pdfs.map((p) => (p.url.includes(from) ? { ...p, url: p.url.split(from).join(to) } : p));
return pdfs.map((p) => {
if (!p.url.includes(from)) return p;
const tail = p.url.split(from)[1] || '';
const [name, query] = [tail.split('?')[0], tail.includes('?') ? '?' + tail.split('?')[1] : ''];
if (mapping) {
// review CR-3: rewrite ONLY files the server confirmed it copied, and use
// the name it actually wrote (collisions get a unique name). A url that
// was not copied keeps pointing at the still-existing old folder.
const dst = mapping[decodeURIComponent(name)] ?? mapping[name];
if (!dst) return p;
return { ...p, url: p.url.split(from + name)[0] + to + encodeURIComponent(dst) + query };
}
return { ...p, url: p.url.split(from).join(to) };
});
}
// ---------------- kiosk gestures ----------------