Explorar o código

fix(backup): a Git backup includes each spool's "Total Consumed" reset point and a restore brings it back (an older backup keeps the live one)

maziggy hai 3 días
pai
achega
19bd258672

+ 1 - 0
CHANGELOG.md

@@ -164,6 +164,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **A Git backup restore keeps "Reset Total Consumed"** — The backup did not include the point each spool's "Total Consumed" counter was reset at, so a restore brought every reset counter back to its full lifetime figure. It is now backed up and restored; a backup made before this keeps the reset points the spools already have.
 - **A finished print's filament is charged once** — Two cases in the built-in inventory:
   - **An AMS update right as a print finished** could raise the spool's used weight from the AMS remain% while the print's exact usage was still being worked out, and that usage was then added on top: the print was charged twice. The AMS sync now waits until the print is charged, as it does while the print runs.
   - **A print the printer reported finished without any file name** (one that failed while preparing, right after Bambuddy started) kept the AMS weight sync off until the next print started. It now turns the sync back on.

+ 2 - 0
backend/app/services/github_backup.py

@@ -793,6 +793,8 @@ class GitHubBackupService:
                 "label_weight": s.label_weight,
                 "core_weight": s.core_weight,
                 "weight_used": s.weight_used,
+                # The "Reset Total Consumed" point (WU-7).
+                "weight_used_baseline": s.weight_used_baseline,
                 "weight_locked": s.weight_locked,
                 "slicer_filament": s.slicer_filament,
                 "slicer_filament_name": s.slicer_filament_name,

+ 5 - 0
backend/app/services/github_restore.py

@@ -1684,6 +1684,11 @@ class GitHubRestoreService:
             for late_field in ("material_number", "category", "low_stock_threshold_pct", "storage_location"):
                 if late_field in entry:
                     fields[late_field] = entry[late_field]
+            # The "Reset Total Consumed" point (WU-7), same late-field rule; a
+            # value that is no amount of filament keeps what the row holds.
+            baseline = _restored_number(entry.get("weight_used_baseline"))
+            if baseline is not None:
+                fields["weight_used_baseline"] = baseline
             # Last drying (#2863), same late-field rule.
             if "last_dried_at" in entry:
                 fields["last_dried_at"] = _parse_dt(entry.get("last_dried_at"))

+ 12 - 0
backend/tests/unit/test_github_backup_spool_fields.py

@@ -64,3 +64,15 @@ async def test_collects_the_drying_record(db_session):
     assert entry["last_dried_at"] == "2026-10-06 12:30:00"
     assert entry["last_dried_temp"] == 55
     assert entry["last_dried_hours"] == 7.5
+
+
+@pytest.mark.asyncio
+async def test_collects_the_total_consumed_reset_point(db_session):
+    """WU-7: without it, a restore lost every "Reset Total Consumed"."""
+    db_session.add(Spool(material="PLA", weight_used=500, weight_used_baseline=300))
+    await db_session.commit()
+
+    files: dict = {}
+    await GitHubBackupService()._collect_spools(db_session, files)
+
+    assert files["spools/inventory.json"]["spools"][0]["weight_used_baseline"] == 300

+ 39 - 0
backend/tests/unit/test_github_restore.py

@@ -964,6 +964,45 @@ class TestRestoreSpools:
         assert len(spools) == 1
         assert spools[0].created_at == datetime(2026, 1, 5, 12, 0, 0)
 
+    @pytest.mark.asyncio
+    async def test_restores_the_total_consumed_reset_point(self, db_session):
+        """WU-7: a restore lost every "Reset Total Consumed"."""
+        await _service()._restore_spools(
+            db_session,
+            {"spools": [self._spool_entry(weight_used=500, weight_used_baseline=300)]},
+            None,
+            False,
+            _CategoryTally(),
+            {},
+        )
+        await db_session.commit()
+
+        assert (await db_session.execute(select(Spool))).scalar_one().weight_used_baseline == 300
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize(
+        ("backed_up", "expected"),
+        [({"weight_used_baseline": 300}, 300), ({}, 650), ({"weight_used_baseline": "300"}, 650)]
+        + [({"weight_used_baseline": bad}, 650) for bad in (-5, float("inf"), None, True)],
+    )
+    async def test_overwriting_a_spool_takes_the_backups_reset_point(self, db_session, backed_up, expected):
+        """Only a real amount: an older backup without one, or a damaged
+        value, keeps the live point."""
+        db_session.add(Spool(material="PLA", tag_uid="AABBCCDD", weight_used=700, weight_used_baseline=650))
+        await db_session.commit()
+
+        await _service()._restore_spools(
+            db_session,
+            {"spools": [self._spool_entry(weight_used=500, **backed_up)]},
+            None,
+            True,
+            _CategoryTally(),
+            {},
+        )
+        await db_session.commit()
+
+        assert (await db_session.execute(select(Spool))).scalar_one().weight_used_baseline == expected
+
     @pytest.mark.asyncio
     async def test_restores_the_owners_own_bookkeeping_fields(self, db_session):
         """#2870 + #729: purchasing number, category, threshold, storage."""