fix(backup): don't count a stale selection, and say when archive links are dropped (#2656)

Two smaller restore-path fixes from the same review.

The modal's footer counted `selected` raw while the checkboxes rendered
`selected && isAvailable`. Switching commits keeps `selected` on purpose (it is
only pruned once the new preview lands), so for as long as the new commit's
preview was in flight — with the category list replaced by its spinner — the
footer still read "2 selected" over an enabled Restore button, and clicking it
restored the newly-picked commit with the previous commit's categories, none of
which the user had seen an item count for. The count and the POST body now come
from one `selectedCategories` memo gated on availability, exactly as the
checkboxes are, so both go empty until the preview lands.

Restoring Spool inventory without Print archives leaves archive_id_map empty,
so every usage -> archive link is nulled even where the archive exists locally.
It can't be resolved here (the archives payload isn't fetched for a category
that wasn't selected) and a later archives-only restore won't repair it either,
since the usage dedupe key doesn't include archive_id and those rows read as
already-present. So it gets a note naming the remedy while the user can still
redo the run with both categories ticked.

Carries the rebuilt bundle (index-C2LOlVCR.js -> index-C16HJNOV.js).
This commit is contained in:
jmoore-skild
2026-08-04 08:22:58 -04:00
parent e7495dd41b
commit 07244b6a43
4 changed files with 115 additions and 11 deletions
+45
View File
@@ -439,6 +439,51 @@ class TestRestoreSpools:
rows = (await db_session.execute(select(SpoolUsageHistory))).scalars().all()
assert len(rows) == 1
@pytest.mark.asyncio
async def test_dropped_archive_link_is_explained(self, db_session):
"""Spools without archives nulls every usage -> archive link, silently."""
tally = _CategoryTally()
inventory = {"spools": [self._spool_entry(id=41)]}
usage = {
"usage_history": [
{"spool_id": 41, "archive_id": 7, "weight_used": 1.0, "created_at": "2026-02-01 09:00:00"},
{"spool_id": 41, "archive_id": 8, "weight_used": 2.0, "created_at": "2026-02-01 10:00:00"},
{"spool_id": 41, "weight_used": 3.0, "created_at": "2026-02-01 11:00:00"},
]
}
# Empty archive_id_map: the archives category wasn't selected, so its
# payload was never fetched and there is nothing to match against.
await _service()._restore_spools(db_session, inventory, usage, False, tally, {})
await db_session.commit()
rows = (await db_session.execute(select(SpoolUsageHistory))).scalars().all()
assert len(rows) == 3
assert all(row.archive_id is None for row in rows)
# Only the two that had a link to lose are counted.
assert any("2 usage record(s) restored without their print-history link" in n for n in tally.notes)
assert any("select Print archives alongside" in n for n in tally.notes)
@pytest.mark.asyncio
async def test_no_note_when_every_archive_link_resolves(self, db_session):
tally = _CategoryTally()
inventory = {"spools": [self._spool_entry(id=41)]}
usage = {
"usage_history": [
{"spool_id": 41, "archive_id": 7, "weight_used": 1.0, "created_at": "2026-02-01 09:00:00"}
]
}
archive = PrintArchive(filename="linked.3mf", file_path="", file_size=1)
db_session.add(archive)
await db_session.flush()
await _service()._restore_spools(db_session, inventory, usage, False, tally, {7: archive.id})
await db_session.commit()
row = (await db_session.execute(select(SpoolUsageHistory))).scalar_one()
assert row.archive_id == archive.id
assert not any("print-history link" in note for note in tally.notes)
@pytest.mark.asyncio
async def test_dangling_printer_id_is_cleared(self, db_session):
tally = _CategoryTally()