浏览代码

fix(archives): re-sliced archives read as not printed with a cost; restore checks its numbers; prints are bound under one set of names

- A re-sliced archive is "archived" (Not printed) and priced like any
  archive with grams (estimate_filament_cost, shared with archive_print).
- A reprint deletes a timelapse with no plate on record when the 3MF has
  exactly one plate.
- on_print_start binds every print through _bind_active_print, so a
  resumed print keeps its #3240 plate-restore binding.
- A model job's own nozzle_mapping is cleared on assignment to a nozzle-rack
  printer (not on H2D, not for variants).
- GitHub restore: grams, costs, energy, print times and usage weights must be
  finite numbers >= 0 (grams <= 100000, ints <= 2^31-1), else left empty
  with a note; quantity 0..10000 is kept.
maziggy 4 天之前
父节点
当前提交
3c2ab99635
共有 93 个文件被更改,包括 601 次插入 和 83 次删除
  1. 0 24
      .basedpyright/baseline.json
  2. 5 0
      CHANGELOG.md
  3. 8 4
      backend/app/api/routes/library.py
  4. 40 21
      backend/app/main.py
  5. 19 19
      backend/app/services/archive.py
  6. 65 10
      backend/app/services/github_restore.py
  7. 14 0
      backend/app/services/print_scheduler.py
  8. 65 0
      backend/tests/integration/test_active_print_binding_names.py
  9. 54 0
      backend/tests/integration/test_reslice_archive_status_cost.py
  10. 132 0
      backend/tests/unit/test_github_restore_numbers.py
  11. 70 0
      backend/tests/unit/test_model_assign_drops_nozzle_mapping.py
  12. 96 0
      backend/tests/unit/test_reprint_unplated_timelapse.py
  13. 2 0
      frontend/src/i18n/locales/de.ts
  14. 2 0
      frontend/src/i18n/locales/en.ts
  15. 2 0
      frontend/src/i18n/locales/es.ts
  16. 2 0
      frontend/src/i18n/locales/fr.ts
  17. 2 0
      frontend/src/i18n/locales/it.ts
  18. 2 0
      frontend/src/i18n/locales/ja.ts
  19. 2 0
      frontend/src/i18n/locales/ko.ts
  20. 2 0
      frontend/src/i18n/locales/nl.ts
  21. 2 0
      frontend/src/i18n/locales/pt-BR.ts
  22. 2 0
      frontend/src/i18n/locales/ru.ts
  23. 2 0
      frontend/src/i18n/locales/sv.ts
  24. 2 0
      frontend/src/i18n/locales/tr.ts
  25. 2 0
      frontend/src/i18n/locales/uk.ts
  26. 2 0
      frontend/src/i18n/locales/zh-CN.ts
  27. 2 0
      frontend/src/i18n/locales/zh-TW.ts
  28. 0 0
      static/assets/AdditionalSection-D36_5Wqb.js
  29. 0 0
      static/assets/AmsUnitCard-zU4y9Dkm.js
  30. 0 0
      static/assets/ArchivesPage-D41CBI2o.js
  31. 0 0
      static/assets/AssignToAmsModal-ChbH5t2b.js
  32. 0 0
      static/assets/CamWallPage-quiP1adY.js
  33. 0 0
      static/assets/CameraDiagnoseModal-guerwlfl.js
  34. 0 0
      static/assets/CameraPage-B5dWHCvC.js
  35. 0 0
      static/assets/CameraWall-DAn7RdZA.js
  36. 0 0
      static/assets/ColumnConfigModal-s0DUmgu_.js
  37. 0 0
      static/assets/ConfigureAmsSlotModal-DAIt_D7l.js
  38. 0 0
      static/assets/ConnectAuthorizePage-B8kUHvte.js
  39. 0 0
      static/assets/ContextMenu-gbb1JkEx.js
  40. 0 0
      static/assets/ExternalLinkPage-CG5aeuqW.js
  41. 0 1
      static/assets/FileManagerPage-DOcE644q.js
  42. 0 0
      static/assets/FileUploadModal-CLVxvtlH.js
  43. 0 0
      static/assets/FinancePage-CfnLp5UB.js
  44. 0 0
      static/assets/GCodeViewerPage-Cnt8CPel.js
  45. 0 0
      static/assets/GroupEditPage-BgASgPdI.js
  46. 0 0
      static/assets/ImagePreviewModal-_HHmqc50.js
  47. 0 0
      static/assets/InventoryPage-BPXktYnD.js
  48. 0 0
      static/assets/LibraryTrashPage-C1E28_0e.js
  49. 0 0
      static/assets/LoginPage-BIaPHbg8.js
  50. 0 0
      static/assets/MaintenancePage-D6wHCn4h.js
  51. 0 0
      static/assets/ModelSourcesPage-wd_J2spd.js
  52. 0 0
      static/assets/NotificationsPage-IyXDFotr.js
  53. 0 1
      static/assets/PdfPreviewModal-OLlIMbw4.js
  54. 0 0
      static/assets/PhotoGalleryModal-tY5NL9XU.js
  55. 0 0
      static/assets/PrinterLocationsPage-Co-ZM3jL.js
  56. 0 0
      static/assets/PrintersPage-CNGIs9q9.js
  57. 0 0
      static/assets/ProfilesPage-rS9vPHEf.js
  58. 0 0
      static/assets/ProjectDetailPage-BQAncbK2.js
  59. 0 0
      static/assets/ProjectsPage-CkdU9XuP.js
  60. 0 0
      static/assets/QueuePage-CE6X1Aol.js
  61. 0 0
      static/assets/RichTextEditor-Dl-XiCDc.js
  62. 0 0
      static/assets/SettingsPage-9ZVoPdaL.js
  63. 0 0
      static/assets/SetupPage-DwsOcMtK.js
  64. 0 0
      static/assets/SliceModal-KXGH4ldx.js
  65. 0 0
      static/assets/SpoolBuddyAmsPage-DWQqSkGV.js
  66. 0 0
      static/assets/SpoolBuddyDashboard-DXb_3CWd.js
  67. 0 0
      static/assets/SpoolBuddyInventoryPage-BESkeCyu.js
  68. 1 1
      static/assets/SpoolBuddySettingsPage-BjufIaGU.js
  69. 0 0
      static/assets/SpoolBuddyWriteTagPage-C5_j6clD.js
  70. 0 0
      static/assets/SpreadsheetPreviewModal-B-JsZzg0.js
  71. 0 0
      static/assets/StatsPage-CK-2tPw6.js
  72. 0 0
      static/assets/StreamOverlayPage-CeKlJQH7.js
  73. 0 0
      static/assets/SystemInfoPage-doBQwYF4.js
  74. 0 0
      static/assets/de-4ioempp1.js
  75. 0 0
      static/assets/es-DpJKx-EW.js
  76. 0 0
      static/assets/fr-xkfLC38W.js
  77. 0 0
      static/assets/index-BrsvYaVv.js
  78. 1 1
      static/assets/inventorySearch-CP5BRBza.js
  79. 0 0
      static/assets/it-C1149LDg.js
  80. 0 0
      static/assets/ja-orcfMFZM.js
  81. 0 0
      static/assets/ko-DdEswUHB.js
  82. 0 0
      static/assets/locationSensorDefaults-CWgd1fgy.js
  83. 0 0
      static/assets/nl-Bwum3z1V.js
  84. 0 0
      static/assets/pdf-CfGojSXf.js
  85. 0 0
      static/assets/pt-BR-LpQsAf78.js
  86. 0 0
      static/assets/ru-DjCmUPSL.js
  87. 0 0
      static/assets/sv-DcnNQ9gK.js
  88. 0 0
      static/assets/tr-CKS-erzp.js
  89. 0 0
      static/assets/uk-ZjAfoH0T.js
  90. 0 0
      static/assets/utils-2f0Jw5iC.js
  91. 0 0
      static/assets/zh-CN-B7cfiE3N.js
  92. 0 0
      static/assets/zh-TW-O3qdj-Xb.js
  93. 1 1
      static/index.html

+ 0 - 24
.basedpyright/baseline.json

@@ -1819,30 +1819,6 @@
                     "lineCount": 1
                 }
             },
-            {
-                "code": "reportOptionalMemberAccess",
-                "range": {
-                    "startColumn": 76,
-                    "endColumn": 78,
-                    "lineCount": 1
-                }
-            },
-            {
-                "code": "reportOptionalMemberAccess",
-                "range": {
-                    "startColumn": 69,
-                    "endColumn": 71,
-                    "lineCount": 1
-                }
-            },
-            {
-                "code": "reportOptionalMemberAccess",
-                "range": {
-                    "startColumn": 82,
-                    "endColumn": 84,
-                    "lineCount": 1
-                }
-            },
             {
                 "code": "reportOptionalMemberAccess",
                 "range": {

+ 5 - 0
CHANGELOG.md

@@ -164,6 +164,11 @@ 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 re-sliced archive reads as not printed and shows a cost** — Re-slicing an archive created a new archive marked "completed", so it appeared under **Printed** and was counted as a finished print although nothing had printed. It is now "archived" (shown as **Not printed**), like any file archived without printing, and it shows an estimated filament cost like other archives with grams.
+- **A reprint of a single-plate archive no longer leaves the old timelapse behind** — A timelapse attached by hand, or before Bambuddy recorded plates, was always kept as an unlinked file on a reprint, since on a multi-plate archive it can be another plate's only copy. On an archive with one plate there is no other plate, and it is now deleted with the run it belonged to.
+- **A GitHub backup restore checks the numbers it restores** — Grams, costs, energy, print times and spool-usage weights were copied from the backup as they were, so a damaged or hand-edited backup could put text, negative or absurd values into the totals. Anything that is not a valid number (grams at most 100000, as the archive editor allows) is left empty, and the restore summary says so. A quantity of 0 (a ruined plate) is kept instead of becoming 1.
+- **A print resumed after a restart keeps its plate height** — Each way Bambuddy picks up a running print (new archive, expected print, restart resume) remembered it under a different set of names. When the printer later reported a name that path had left out, the finished print was still found but lost its binding, and with it the height the plate restore uses after a restart. Every path now remembers the same names.
+- **An "Any model" job gets a fresh nozzle mapping on a nozzle-rack printer** — A nozzle mapping stored on a model-based job (through the API or an order's plate spec) was sent unchanged to whichever H2C picked it up, instead of the rack positions of that printer. It is now worked out for the printer that takes the job. On printers without a rack, such as the H2D, where the mapping means left and right nozzle, it is kept.
 - **A used weight entered for a new spool is kept** — Adding a spool (one, several with **Add copies**, or from a CSV) left its weight unlocked even when a used weight was entered, so the automatic AMS sync could overwrite a part-used spool's weight on its first pass. A used weight entered at creation now locks the spool, as editing the weight already did. A negative used weight is refused; more than the label weight is still accepted, since a mis-labelled spool really can give more.
 - **Spoolman's "last used" means a print used the spool** — Every AMS sync stamped *last used* on every loaded Spoolman spool, as did clearing a removed spool's location and weighing on the SpoolBuddy scale. Only print usage sets it now (through Spoolman's own usage endpoint), as in built-in inventory.
 - **The AMS slot K-profile picker accounts for the nozzle's flow** — On printers that report a high-flow or standard nozzle (H2-series), the picker offered and pre-selected K-profiles measured on the other kind, which are not applied when the slot is configured. They now sit under **Other**, and every profile shows its flow ([HF] / [S]), as in the spool form.

+ 8 - 4
backend/app/api/routes/library.py

@@ -76,7 +76,7 @@ from backend.app.schemas.library import (
     ZipExtractResult,
 )
 from backend.app.schemas.slicer import SliceRequest, SliceResponse
-from backend.app.services.archive import ThreeMFParser
+from backend.app.services.archive import ThreeMFParser, estimate_filament_cost
 from backend.app.services.design_settings import (
     DesignOverride,
     apply_design_overrides,
@@ -5167,9 +5167,13 @@ async def slice_and_persist_as_archive(
         bed_type=parsed_metadata.get("bed_type") or source_archive.bed_type,
         makerworld_url=source_archive.makerworld_url,
         designer=source_archive.designer,
-        # Sliced-but-not-printed: keep status default ("completed") so it
-        # surfaces in the normal archives list, but do not stamp
-        # started/completed_at — the user hasn't actually printed it yet.
+        # Sliced, not printed: "archived", as archive_print files an unprinted
+        # 3MF. It is listed under "Not printed" and is not counted as a
+        # completed print; no started/completed_at either.
+        status="archived",
+        # Priced like any archive with grams, for the card's estimate.
+        # Statistics read the print log, so nothing is counted that did not print.
+        cost=await estimate_filament_cost(db, filament_g or None, new_filament_type),
         extra_data=metadata,
         created_by_id=current_user_id,
     )

+ 40 - 21
backend/app/main.py

@@ -875,6 +875,26 @@ def _is_bambuddy_authorized_print_in_memory(printer_id: int, state: PrinterState
     )
 
 
+def _bind_active_print(
+    printer_id: int,
+    archive_id: int,
+    archive_filename: str | None,
+    filename: str | None = None,
+    subtask_name: str | None = None,
+) -> None:
+    """Track a running print under every name its completion may report.
+
+    One set for every path that binds a print (new archive, expected print,
+    restart resume, fallback): the archive's file name, the name the printer
+    reported, and the subtask with and without ``.3mf``. Paths that registered
+    fewer names lost the bound archive at completion, and with it the plate
+    restore's height (#3240), whenever MQTT reported a name they had left out.
+    """
+    for name in {archive_filename, filename, f"{subtask_name}.3mf" if subtask_name else None, subtask_name}:
+        if name:
+            _active_prints[(printer_id, name)] = archive_id
+
+
 def _mark_owned_if_dispatched(archive) -> None:
     """Remember a running print's archive as Bambuddy's when it carries a
     dispatch marker Bambuddy writes itself: ``billing_run_id`` (minted per
@@ -4766,9 +4786,10 @@ async def on_print_start(printer_id: int, data: dict):
                 # was deleted once it attached (#3275). That one is left in the
                 # archive directory, which goes with the archive. A video whose
                 # plate is not on record (attached by hand, or before the
-                # column existed) is kept too: nothing tells it apart from
-                # another plate's, and keeping costs one file where deleting
-                # can cost the only copy.
+                # column existed) is kept too where the 3MF has other plates,
+                # or cannot be read: nothing tells it apart from another plate's,
+                # and keeping costs one file where deleting can cost the only
+                # copy. On an archive with exactly one plate it is that plate's.
                 archive.timelapse_baseline = None
                 stale_timelapse_relpath = archive.timelapse_path
                 stale_timelapse_plate_id = archive.timelapse_plate_id
@@ -4776,7 +4797,16 @@ async def on_print_start(printer_id: int, data: dict):
                 if stale_timelapse_relpath:
                     archive.timelapse_path = None
                     archive.timelapse_plate_id = None
-                if stale_timelapse_relpath and stale_timelapse_plate_id != starting_plate_id:
+                stale_is_this_plates = stale_timelapse_plate_id == starting_plate_id
+                if stale_timelapse_relpath and stale_timelapse_plate_id is None and archive.file_path:
+                    from backend.app.services.archive import plate_indexes_in_3mf
+
+                    plates = await asyncio.to_thread(
+                        plate_indexes_in_3mf,
+                        app_settings.base_dir / archive.file_path,  # SEC-PATH-OK: stored relative archive path
+                    )
+                    stale_is_this_plates = len(plates) == 1
+                if stale_timelapse_relpath and not stale_is_this_plates:
                     logger.info(
                         "Kept timelapse %s of plate %s on archive %s: plate %s is starting",
                         stale_timelapse_relpath,
@@ -4785,7 +4815,7 @@ async def on_print_start(printer_id: int, data: dict):
                         starting_plate_id,
                     )
                 # The video is deleted once the adoption stands (below).
-                delete_stale_video = bool(stale_timelapse_relpath) and stale_timelapse_plate_id == starting_plate_id
+                delete_stale_video = bool(stale_timelapse_relpath) and stale_is_this_plates
                 # Persist a restart-stable id so a later restart resumes this
                 # archive by subtask_id instead of name-matching + duplicating
                 # it (#1485). The printer often hasn't echoed subtask_id back
@@ -4879,9 +4909,7 @@ async def on_print_start(printer_id: int, data: dict):
                         )
 
                 # Track as active print
-                _active_prints[(printer_id, archive.filename)] = archive.id
-                if subtask_name:
-                    _active_prints[(printer_id, f"{subtask_name}.3mf")] = archive.id
+                _bind_active_print(printer_id, archive.id, archive.filename, filename, subtask_name)
 
                 # Start timelapse session if external camera is enabled (#1353).
                 # Queue / VP-dispatched prints land here in the expected-archive
@@ -5042,7 +5070,7 @@ async def on_print_start(printer_id: int, data: dict):
 
             if subtask_match:
                 logger.info("Resuming archive %s on subtask_id match (%s)", existing_archive.id, subtask_id)
-                _active_prints[(printer_id, existing_archive.filename)] = existing_archive.id
+                _bind_active_print(printer_id, existing_archive.id, existing_archive.filename, filename, subtask_name)
                 _mark_owned_if_dispatched(existing_archive)
                 if existing_archive.energy_start_kwh is None:
                     await _record_energy_start(existing_archive, printer_id, db, context="subtask-resume")
@@ -5126,7 +5154,7 @@ async def on_print_start(printer_id: int, data: dict):
                     f"Skipping duplicate - already have printing archive {existing_archive.id} for {check_name}"
                 )
                 # Track this as the active print
-                _active_prints[(printer_id, existing_archive.filename)] = existing_archive.id
+                _bind_active_print(printer_id, existing_archive.id, existing_archive.filename, filename, subtask_name)
                 _mark_owned_if_dispatched(existing_archive)
                 # Attach subtask_id retroactively so future restarts can resume.
                 # Compare for inequality (not "is empty") to also pick up reprint
@@ -5717,12 +5745,7 @@ async def on_print_start(printer_id: int, data: dict):
                 _maybe_start_layer_timelapse(printer, printer_id, fallback_archive.id)
 
                 # Track as active print
-                _active_prints[(printer_id, fallback_archive.filename)] = fallback_archive.id
-                if filename:
-                    _active_prints[(printer_id, filename)] = fallback_archive.id
-                if subtask_name:
-                    _active_prints[(printer_id, f"{subtask_name}.3mf")] = fallback_archive.id
-                    _active_prints[(printer_id, subtask_name)] = fallback_archive.id
+                _bind_active_print(printer_id, fallback_archive.id, fallback_archive.filename, filename, subtask_name)
 
                 # Record starting energy if smart plug available (#941: persisted column)
                 await _record_energy_start(fallback_archive, printer_id, db, context="fallback")
@@ -5867,11 +5890,7 @@ async def on_print_start(printer_id: int, data: dict):
                     return
 
                 # Track this active print (use both original filename and downloaded filename)
-                _active_prints[(printer_id, downloaded_filename)] = archive.id
-                if filename and filename != downloaded_filename:
-                    _active_prints[(printer_id, filename)] = archive.id
-                if subtask_name:
-                    _active_prints[(printer_id, f"{subtask_name}.3mf")] = archive.id
+                _bind_active_print(printer_id, archive_id, downloaded_filename, filename, subtask_name)
 
                 logger.info("Created archive %s for %s", archive.id, downloaded_filename)
 

+ 19 - 19
backend/app/services/archive.py

@@ -210,6 +210,24 @@ def read_3dmodel_without_meshes(zf: zipfile.ZipFile, model_path: str = "3D/3dmod
 _GCODE_SCAN_BYTES = 64 * 1024
 
 
+async def estimate_filament_cost(db: AsyncSession, grams: float | None, filament_type: str | None) -> float | None:
+    """An archive's filament cost from its grams and type: the first type's
+    cost per kg on record, else the default filament cost (25 when unset).
+    ``None`` without grams or a type."""
+    if not grams or not filament_type:
+        return None
+    # For multi-material prints, the first filament type prices the whole print
+    primary_type = filament_type.split(",")[0].strip()
+    filament = (await db.execute(select(Filament).where(Filament.type == primary_type).limit(1))).scalar_one_or_none()
+    if filament:
+        return round((grams / 1000) * filament.cost_per_kg, 2)
+    from backend.app.api.routes.settings import get_setting
+
+    default_cost_setting = await get_setting(db, "default_filament_cost")
+    default_cost_per_kg = float(default_cost_setting) if default_cost_setting else 25.0
+    return round((grams / 1000) * default_cost_per_kg, 2)
+
+
 class ThreeMFParser:
     """Parser for Bambu Lab 3MF files."""
 
@@ -1448,25 +1466,7 @@ class ArchiveService:
         started_at = datetime.now(timezone.utc) if status == "printing" else None
         completed_at = datetime.now(timezone.utc) if status in ("completed", "failed", "archived") else None
 
-        # Calculate cost based on filament usage and type
-        cost = None
-        filament_grams = metadata.get("filament_used_grams")
-        filament_type = metadata.get("filament_type")
-        if filament_grams and filament_type:
-            # For multi-material prints, use the first filament type for cost calculation
-            primary_type = filament_type.split(",")[0].strip()
-            # Look up filament cost_per_kg from database
-            filament_result = await self.db.execute(select(Filament).where(Filament.type == primary_type).limit(1))
-            filament = filament_result.scalar_one_or_none()
-            if filament:
-                cost = round((filament_grams / 1000) * filament.cost_per_kg, 2)
-            else:
-                # Use default filament cost from settings
-                from backend.app.api.routes.settings import get_setting
-
-                default_cost_setting = await get_setting(self.db, "default_filament_cost")
-                default_cost_per_kg = float(default_cost_setting) if default_cost_setting else 25.0
-                cost = round((filament_grams / 1000) * default_cost_per_kg, 2)
+        cost = await estimate_filament_cost(self.db, metadata.get("filament_used_grams"), metadata.get("filament_type"))
 
         # Calculate quantity from printable objects count
         # printable_objects is a dict of {identify_id: name} for non-skipped objects

+ 65 - 10
backend/app/services/github_restore.py

@@ -34,6 +34,7 @@ Design notes worth knowing before editing:
 import asyncio
 import json
 import logging
+import math
 import os
 import re
 from contextvars import ContextVar
@@ -499,6 +500,31 @@ class _Detail:
     params: dict[str, str | int] = dataclasses_field(default_factory=dict)
 
 
+# The archive editor's own bound on grams (ArchiveUpdate).
+_MAX_RESTORED_GRAMS = 100_000
+# What a 32-bit integer column takes (PostgreSQL rejects more).
+_MAX_RESTORED_INT = 2**31 - 1
+
+
+def _restored_number(value, *, maximum: float | None = None, integer: bool = False):
+    """A number from a backup as the database should hold it, or None.
+
+    Only a finite real number of at least 0 (and at most ``maximum``) passes:
+    a damaged or hand-edited backup must not put text, negative or absurd
+    values into the filament and cost totals.
+    """
+    if isinstance(value, bool) or not isinstance(value, (int, float)):
+        return None
+    if integer and maximum is None:
+        maximum = _MAX_RESTORED_INT
+    try:
+        if not math.isfinite(value) or value < 0 or (maximum is not None and value > maximum):
+            return None
+        return int(value) if integer else float(value)
+    except OverflowError:  # an integer too large for a float
+        return None
+
+
 class _CategoryTally:
     """Mutable accumulator matching ``GitHubRestoreCategoryResult``."""
 
@@ -1369,10 +1395,28 @@ class GitHubRestoreService:
             started_at = _parse_dt(entry.get("started_at"))
             existing = await self._find_archive(db, entry, started_at)
 
+            numbers = {
+                "print_time_seconds": _restored_number(entry.get("print_time_seconds"), integer=True),
+                "filament_used_grams": _restored_number(entry.get("filament_used_grams"), maximum=_MAX_RESTORED_GRAMS),
+                "cost": _restored_number(entry.get("cost")),
+                "energy_kwh": _restored_number(entry.get("energy_kwh")),
+                "energy_cost": _restored_number(entry.get("energy_cost")),
+                "wear_cost": _restored_number(entry.get("wear_cost")),
+            }
+            if any(entry.get(key) is not None and value is None for key, value in numbers.items()):
+                tally.note(
+                    "archivesNumbersDropped",
+                    "Some archives had grams, costs, energy or print times that are not valid numbers - left empty",
+                )
+            # 0 is a real quantity (a ruined plate, #3051); the archive editor
+            # takes 0..10000. Anything else is the one print it stood for.
+            quantity = _restored_number(entry.get("quantity"), maximum=10_000, integer=True)
+            if quantity is None:
+                quantity = 1
             fields = {
                 "print_name": entry.get("print_name"),
-                "print_time_seconds": entry.get("print_time_seconds"),
-                "filament_used_grams": entry.get("filament_used_grams"),
+                "print_time_seconds": numbers["print_time_seconds"],
+                "filament_used_grams": numbers["filament_used_grams"],
                 "filament_type": entry.get("filament_type"),
                 "filament_color": entry.get("filament_color"),
                 "layer_height": entry.get("layer_height"),
@@ -1390,12 +1434,12 @@ class GitHubRestoreService:
                 "is_favorite": bool(entry.get("is_favorite")),
                 "tags": entry.get("tags"),
                 "notes": entry.get("notes"),
-                "cost": entry.get("cost"),
+                "cost": numbers["cost"],
                 "failure_reason": entry.get("failure_reason"),
-                "quantity": entry.get("quantity") or 1,
-                "energy_kwh": entry.get("energy_kwh"),
-                "energy_cost": entry.get("energy_cost"),
-                "wear_cost": entry.get("wear_cost"),
+                "quantity": quantity,
+                "energy_kwh": numbers["energy_kwh"],
+                "energy_cost": numbers["energy_cost"],
+                "wear_cost": numbers["wear_cost"],
             }
 
             printer_id = entry.get("printer_id")
@@ -1845,15 +1889,26 @@ class GitHubRestoreService:
                 # they can still redo the run with both categories ticked.
                 unlinked_archives += 1
 
+            weight_used = _restored_number(entry.get("weight_used"))
+            percent_used = _restored_number(entry.get("percent_used"), integer=True)
+            cost = _restored_number(entry.get("cost"))
+            if any(
+                entry.get(key) is not None and value is None
+                for key, value in (("weight_used", weight_used), ("percent_used", percent_used), ("cost", cost))
+            ):
+                tally.note(
+                    "usageNumbersDropped",
+                    "Some usage entries had weights or costs that are not valid numbers - left empty",
+                )
             row = SpoolUsageHistory(
                 spool_id=spool_id,
                 printer_id=printer_id,
                 print_name=entry.get("print_name"),
                 archive_id=archive_id,
-                weight_used=entry.get("weight_used") or 0,
-                percent_used=entry.get("percent_used") or 0,
+                weight_used=weight_used or 0,
+                percent_used=percent_used or 0,
                 status=entry.get("status") or "completed",
-                cost=entry.get("cost"),
+                cost=cost,
             )
             if created_at is not None:
                 row.created_at = created_at

+ 14 - 0
backend/app/services/print_scheduler.py

@@ -2424,6 +2424,20 @@ class PrintScheduler:
                                 printer_id,
                             )
                             item.ams_mapping = None
+                        # On a nozzle-rack printer its own nozzle mapping likewise
+                        # (set through the API or an order's plate spec): it names
+                        # rack positions, and the dispatch sends a stored one as-is
+                        # instead of resolving this printer's rack. Elsewhere it
+                        # holds extruder indices, the same on every printer of the
+                        # model (an H2D VP keeps the slicer's for that reason), and
+                        # one that came with the chosen variant belongs to that
+                        # variant's file: both stay.
+                        if (
+                            (chosen is None or chosen.variant is None)
+                            and printer is not None
+                            and is_nozzle_rack_model(printer.model)
+                        ):
+                            item.nozzle_mapping = None
 
                         # Resolve the AMS mapping for the assigned printer. It is
                         # always missing here, so this computes it, and it also

+ 65 - 0
backend/tests/integration/test_active_print_binding_names.py

@@ -0,0 +1,65 @@
+"""Every path that binds a running print registers the same names (AP-3).
+
+A restart resume registered only the archive's file name; the new-archive,
+expected and fallback paths each registered a different mix. When MQTT
+reported a name the binding path had left out, completion still found the
+archive by a name search but lost the binding, and with it the plate restore's
+height (#3240).
+"""
+
+from types import SimpleNamespace
+
+import pytest
+
+from backend.app import main as main_module
+from backend.app.models.archive import PrintArchive
+from backend.tests.integration.test_external_print_confirmation_1898 import (  # noqa: F401
+    DISPATCH,
+    SUBTASK,
+    _clear_print_state,
+    _drive_print_start,
+)
+
+pytestmark = pytest.mark.integration
+
+
+@pytest.mark.asyncio
+async def test_a_print_resumed_after_a_restart_is_bound_under_the_printers_names(
+    test_engine, db_session, printer_factory
+):
+    printer_id = (await printer_factory()).id
+    archive = PrintArchive(
+        printer_id=printer_id,
+        filename="My Part.gcode.3mf",
+        file_path="archives/test/My Part.gcode.3mf",
+        file_size=1,
+        print_name=SUBTASK,
+        status="printing",
+    )
+    db_session.add(archive)
+    await db_session.commit()
+
+    await _drive_print_start(
+        test_engine, SimpleNamespace(id=printer_id), download_ok=False, state_fields={"progress": 40}
+    )
+
+    bound = {
+        name
+        for (pid, name), archive_id in main_module._active_prints.items()
+        if pid == printer_id and archive_id == archive.id
+    }
+    assert {"My Part.gcode.3mf", SUBTASK, f"{SUBTASK}.3mf", DISPATCH} <= bound
+
+
+def test_the_binding_names_are_one_set():
+    main_module._active_prints.clear()
+
+    main_module._bind_active_print(3, 9, "a.gcode.3mf", "/data/Metadata/plate_1.gcode", "a")
+
+    assert {name for (_pid, name) in main_module._active_prints} == {
+        "a.gcode.3mf",
+        "/data/Metadata/plate_1.gcode",
+        "a.3mf",
+        "a",
+    }
+    main_module._active_prints.clear()

+ 54 - 0
backend/tests/integration/test_reslice_archive_status_cost.py

@@ -0,0 +1,54 @@
+"""A re-sliced, never-printed archive reads as not printed and shows a cost.
+
+It was stored with the default status "completed": listed under "Printed" and
+counted where completed archives are counted, although nothing was printed.
+Unprinted files are "archived" elsewhere (shown as "Not printed"). It also got
+grams but no cost, unlike every other archive with grams; the cost is now
+priced the same way print start prices it.
+"""
+
+import pytest
+from sqlalchemy import select
+
+from backend.app.models.archive import PrintArchive
+from backend.tests.integration.test_slice_archive_output_path_2832 import _slice, archive_root  # noqa: F401
+
+pytestmark = [pytest.mark.integration, pytest.mark.asyncio]
+
+
+async def _resliced(db_session, response):
+    return await db_session.scalar(select(PrintArchive).where(PrintArchive.id == response.archive_id))
+
+
+async def test_it_is_not_printed(db_session, archive_factory, printer_factory, archive_root):  # noqa: F811
+    printer = await printer_factory()
+    source = await archive_factory(printer.id, print_name="Bracket", filename="Bracket.3mf")
+
+    archive = await _resliced(db_session, await _slice(db_session, source, "Bracket.3mf"))
+
+    assert archive.status == "archived"
+    assert (archive.started_at, archive.completed_at) == (None, None)
+
+
+async def test_it_is_priced_like_a_printed_archive(db_session, archive_factory, printer_factory, archive_root):  # noqa: F811
+    """12.5 g at the default 25 per kg; no filament of that type is on record."""
+    printer = await printer_factory()
+    source = await archive_factory(printer.id, print_name="Bracket", filename="Bracket.3mf", filament_type="PLA")
+
+    archive = await _resliced(db_session, await _slice(db_session, source, "Bracket.3mf"))
+
+    assert archive.filament_used_grams == 12.5
+    assert archive.cost == pytest.approx(0.31)
+
+
+async def test_the_filament_on_record_sets_the_price(db_session, archive_factory, printer_factory, archive_root):  # noqa: F811
+    from backend.app.models.filament import Filament
+
+    db_session.add(Filament(name="House PLA", type="PLA", cost_per_kg=40.0))
+    await db_session.commit()
+    printer = await printer_factory()
+    source = await archive_factory(printer.id, print_name="Bracket", filename="Bracket.3mf", filament_type="PLA")
+
+    archive = await _resliced(db_session, await _slice(db_session, source, "Bracket.3mf"))
+
+    assert archive.cost == pytest.approx(0.5)

+ 132 - 0
backend/tests/unit/test_github_restore_numbers.py

@@ -0,0 +1,132 @@
+"""Numbers restored from a GitHub backup are checked like numbers typed in.
+
+Archive and spool-usage restore copied grams, costs, energy and print time
+straight from the backup: a hand-edited or damaged backup could put negative or
+absurd values (or text) into the filament and cost totals, which the archive
+editor itself refuses. Each such field now has to be a finite number of at
+least 0 (grams at most 100000, as the editor allows); anything else is left
+empty, said once per category.
+"""
+
+import math
+
+import pytest
+from sqlalchemy import select
+
+from backend.app.models.archive import PrintArchive
+from backend.app.models.spool_usage_history import SpoolUsageHistory
+from backend.app.services.github_restore import _CategoryTally
+from backend.tests.unit.test_github_restore import TestRestoreArchives, _codes, _service
+
+pytestmark = pytest.mark.asyncio
+
+
+def _entry(**numbers):
+    return TestRestoreArchives()._archive_entry(**numbers)
+
+
+async def _restore_archive(db_session, entry):
+    tally = _CategoryTally()
+    await _service()._restore_archives(db_session, {"archives": [entry]}, False, tally, {})
+    await db_session.commit()
+    return (await db_session.execute(select(PrintArchive))).scalar_one(), tally
+
+
+async def test_good_numbers_come_through(db_session):
+    row, tally = await _restore_archive(
+        db_session,
+        _entry(
+            filament_used_grams=45.2,
+            cost=1.13,
+            energy_kwh=0.4,
+            energy_cost=0.12,
+            wear_cost=0.05,
+            print_time_seconds=3600,
+        ),
+    )
+
+    assert (
+        row.filament_used_grams,
+        row.cost,
+        row.energy_kwh,
+        row.energy_cost,
+        row.wear_cost,
+        row.print_time_seconds,
+    ) == (
+        45.2,
+        1.13,
+        0.4,
+        0.12,
+        0.05,
+        3600,
+    )
+    assert "archivesNumbersDropped" not in _codes(tally)
+
+
+@pytest.mark.parametrize(
+    "bad",
+    [
+        {"filament_used_grams": -5},
+        {"filament_used_grams": 1e9},
+        {"cost": "12"},
+        {"energy_kwh": math.inf},
+        {"energy_cost": float("nan")},
+        {"wear_cost": True},
+        {"print_time_seconds": -1},
+    ],
+    ids=["negative-grams", "absurd-grams", "text-cost", "infinite-energy", "nan-cost", "bool-wear", "negative-time"],
+)
+async def test_a_bad_number_is_left_empty_and_said_once(db_session, bad):
+    row, tally = await _restore_archive(db_session, _entry(**bad))
+
+    (field,) = bad
+    assert getattr(row, field) is None
+    assert _codes(tally).count("archivesNumbersDropped") == 1
+
+
+async def test_a_bad_quantity_becomes_one(db_session):
+    row, _ = await _restore_archive(db_session, _entry(quantity="many"))
+
+    assert row.quantity == 1
+
+
+async def test_usage_numbers_are_checked_too(db_session):
+    tally = _CategoryTally()
+    usage = {
+        "usage_history": [
+            {
+                "spool_id": None,
+                "weight_used": -3,
+                "percent_used": 500,
+                "cost": "abc",
+                "created_at": "2026-02-01 09:00:00",
+            }
+        ]
+    }
+
+    await _service()._restore_spools(db_session, {"spools": []}, usage, False, tally, {})
+    await db_session.commit()
+
+    row = (await db_session.execute(select(SpoolUsageHistory))).scalar_one()
+    assert (row.weight_used, row.cost) == (0, None)
+    assert "usageNumbersDropped" in _codes(tally)
+
+
+async def test_a_quantity_of_zero_is_kept(db_session):
+    """A ruined plate (#3051): counting it as a print again would be wrong."""
+    row, _ = await _restore_archive(db_session, _entry(quantity=0))
+
+    assert row.quantity == 0
+
+
+async def test_a_huge_integer_is_left_empty_not_a_crash(db_session):
+    row, tally = await _restore_archive(db_session, _entry(print_time_seconds=10**400, cost=10**400))
+
+    assert (row.print_time_seconds, row.cost) == (None, None)
+    assert "archivesNumbersDropped" in _codes(tally)
+
+
+async def test_an_integer_too_large_for_the_column_is_left_empty(db_session):
+    row, _ = await _restore_archive(db_session, _entry(print_time_seconds=2**31))
+
+    assert row.print_time_seconds is None

+ 70 - 0
backend/tests/unit/test_model_assign_drops_nozzle_mapping.py

@@ -0,0 +1,70 @@
+"""An "Any <model>" job does not carry a nozzle mapping onto the printer it gets.
+
+Every move from one printer to another resets the printer-specific settings
+(drop_printer_resolved), but the model-based assignment dropped only the AMS
+mapping. A nozzle mapping on a model job (set through the API, a variant or an
+order's plate spec) was then sent as-is and skipped the rack resolve for the
+printer actually picked.
+"""
+
+import pytest
+
+from backend.app.models.library import LibraryFile
+from backend.app.models.print_queue import PrintQueueItem
+from backend.app.services.print_scheduler import PrintScheduler
+from backend.tests.unit.test_scheduler_pinned_waiting_reason_3074 import (
+    _add_item,
+    _item,
+    _run,
+    ctx,  # noqa: F401 - fixture
+)
+
+
+@pytest.mark.asyncio
+async def test_the_picked_printer_works_its_own_mapping_out(ctx):
+    """On a nozzle-rack printer (H2C) the mapping names rack positions."""
+    from backend.app.models.printer import Printer
+
+    async with ctx.session_maker() as db:
+        printer = await db.get(Printer, 1)
+        printer.model = "H2C"
+        await db.commit()
+    item_id = await _add_item(ctx, printer_id=None, target_model="H2C")
+    async with ctx.session_maker() as db:
+        item = await db.get(PrintQueueItem, item_id)
+        item.nozzle_mapping = "[16, 17]"
+        library_file = await db.get(LibraryFile, item.library_file_id)
+        library_file.file_metadata = {"sliced_for_model": "H2C"}  # else the cross-model gate holds it
+        await db.commit()
+
+    launched = await _run(ctx, PrintScheduler(), idle=True)
+
+    launched.assert_called_once()
+    item = await _item(ctx, item_id)
+    assert item.printer_id == 1
+    assert item.nozzle_mapping is None
+
+
+@pytest.mark.asyncio
+async def test_on_a_dual_nozzle_printer_without_a_rack_the_mapping_stays(ctx):
+    """An H2D's nozzle mapping holds extruder indices, the same on every H2D:
+    the slicer's pick from an "any printer" VP must survive assignment."""
+    from backend.app.models.printer import Printer
+
+    async with ctx.session_maker() as db:
+        printer = await db.get(Printer, 1)
+        printer.model = "H2D"
+        await db.commit()
+    item_id = await _add_item(ctx, printer_id=None, target_model="H2D")
+    async with ctx.session_maker() as db:
+        item = await db.get(PrintQueueItem, item_id)
+        item.nozzle_mapping = "[0, 1]"
+        library_file = await db.get(LibraryFile, item.library_file_id)
+        library_file.file_metadata = {"sliced_for_model": "H2D"}
+        await db.commit()
+
+    launched = await _run(ctx, PrintScheduler(), idle=True)
+
+    launched.assert_called_once()
+
+    assert (await _item(ctx, item_id)).nozzle_mapping == "[0, 1]"

+ 96 - 0
backend/tests/unit/test_reprint_unplated_timelapse.py

@@ -0,0 +1,96 @@
+"""A reprint of a single-plate archive does not leave the old video behind.
+
+A video attached by hand, or before the plate column existed, has no plate on
+record. On a reprint it was always kept, because on a Send All archive it can
+be another plate's only copy; on an archive with one plate there is no other
+plate, so it was just an orphan file per reprint. It is now deleted there, and
+still kept when the archive's 3MF has several plates or cannot be read.
+"""
+
+import zipfile
+from contextlib import ExitStack
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+
+from backend.app.core.config import settings as app_settings
+from backend.app.main import register_expected_print
+from backend.tests.unit.test_reprint_clears_stale_timelapse import (  # noqa: F401
+    _build_mocks,
+    _clear_dicts,
+    _patches,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+def _three_mf(path, plates: int | None):
+    path.parent.mkdir(parents=True, exist_ok=True)
+    if plates is None:
+        path.write_bytes(b"not a zip")
+        return
+    body = "".join(f'<plate><metadata key="index" value="{n}"/></plate>' for n in range(1, plates + 1))
+    with zipfile.ZipFile(path, "w") as zf:
+        zf.writestr("Metadata/slice_info.config", f"<config>{body}</config>")
+
+
+async def _reprint(tmp_path, plates):
+    relpath = "archives/42/timelapse/by-hand.mp4"
+    video = tmp_path / relpath
+    video.parent.mkdir(parents=True, exist_ok=True)
+    video.write_bytes(b"old video")
+    _three_mf(tmp_path / "archives/42/MyModel.3mf", plates)
+
+    printer = MagicMock(id=1, auto_archive=True, external_camera_enabled=False, external_camera_url=None)
+    archive = MagicMock(
+        id=42,
+        filename="MyModel.3mf",
+        subtask_id=None,
+        print_time_seconds=None,
+        created_by_id=None,
+        printer_id=1,
+        print_name="MyModel",
+        status="archived",
+        file_path="archives/42/MyModel.3mf",
+        energy_start_kwh=None,
+        timelapse_path=relpath,
+        timelapse_plate_id=None,  # not on record
+    )
+    register_expected_print(1, "MyModel.3mf", archive_id=42, ams_mapping=None, plate_id=1)
+    session = _build_mocks(printer, archive)
+    patches = _patches()
+    with ExitStack() as stack:
+        maker, notif, plug, ws, pm, relay = (stack.enter_context(p) for p in patches[:6])
+        for extra in patches[6:]:
+            stack.enter_context(extra)
+        stack.enter_context(patch.object(app_settings, "base_dir", tmp_path))
+        # The mock printer has no camera to reach.
+        stack.enter_context(
+            patch("backend.app.services.camera.capture_camera_frame_bytes", AsyncMock(return_value=None))
+        )
+        maker.return_value = session
+        notif.on_print_start = AsyncMock()
+        plug.on_print_start = AsyncMock()
+        ws.send_print_start = AsyncMock()
+        ws.send_archive_updated = AsyncMock()
+        relay.on_print_start = AsyncMock()
+        pm.get_printer = MagicMock(return_value=MagicMock(name="Test", serial_number="TEST123"))
+        from backend.app.main import on_print_start
+
+        await on_print_start(1, {"filename": "MyModel.3mf", "subtask_name": "MyModel"})
+    return video, archive
+
+
+async def test_on_a_single_plate_archive_it_is_deleted(tmp_path):
+    video, archive = await _reprint(tmp_path, plates=1)
+
+    assert archive.timelapse_path is None
+    assert not video.exists()
+
+
+@pytest.mark.parametrize("plates", [2, None], ids=["several-plates", "unreadable-3mf"])
+async def test_where_it_could_be_another_plates_it_is_kept(tmp_path, plates):
+    video, archive = await _reprint(tmp_path, plates=plates)
+
+    assert archive.timelapse_path is None
+    assert video.exists()

+ 2 - 0
frontend/src/i18n/locales/de.ts

@@ -5700,6 +5700,8 @@ export default {
         noData: 'Keine Daten dieser Art in diesem Backup',
         archivesPrinterMissing: 'Einige Archive verwiesen auf nicht mehr vorhandene Drucker - Verknüpfung entfernt',
         archivesProjectMissing: 'Einige Archive verwiesen auf nicht mehr vorhandene Projekte - Verknüpfung entfernt',
+        archivesNumbersDropped: 'Einige Archive enthielten Gramm-, Kosten-, Energie- oder Druckzeitwerte, die keine gültigen Zahlen sind - leer gelassen',
+        usageNumbersDropped: 'Einige Verbrauchseinträge enthielten Gewichte oder Kosten, die keine gültigen Zahlen sind - leer gelassen',
         archivesOwnerCleared: 'Einige Archive verwiesen auf nicht mehr vorhandene Benutzer - Eigentümer entfernt. Sie sind daher nur für Benutzer mit der Berechtigung archives:read_all sichtbar, bis ein Administrator sie neu zuweist',
         archivesOwnerUnmatched: 'Einige Archive nennen einen Eigentümer, den es auf dieser Instanz nicht gibt - der Eigentümer wurde entfernt statt aus der Benutzer-ID der Sicherung geraten. Sie sind daher nur für Benutzer mit der Berechtigung archives:read_all sichtbar, bis ein Administrator sie neu zuweist',
         archivesOwnerUnknown: 'Einige Archive wurden ohne Eigentümer wiederhergestellt - diese Sicherung enthält keinen, daher sind sie nur für Benutzer mit der Berechtigung archives:read_all sichtbar, bis ein Administrator sie neu zuweist',

+ 2 - 0
frontend/src/i18n/locales/en.ts

@@ -5752,6 +5752,8 @@ export default {
         noData: 'No data of this kind in this backup',
         archivesPrinterMissing: 'Some archives referenced printers that no longer exist - link cleared',
         archivesProjectMissing: 'Some archives referenced projects that no longer exist - link cleared',
+        archivesNumbersDropped: 'Some archives had grams, costs, energy or print times that are not valid numbers - left empty',
+        usageNumbersDropped: 'Some usage entries had weights or costs that are not valid numbers - left empty',
         archivesOwnerCleared: 'Some archives referenced users that no longer exist - owner cleared, so they are visible only to users with the archives:read_all permission until an admin reassigns them',
         archivesOwnerUnmatched: 'Some archives name an owner this instance does not have - owner cleared rather than guessed from the backup\'s user id, so they are visible only to users with the archives:read_all permission until an admin reassigns them',
         archivesOwnerUnknown: 'Some archives were restored without an owner - this backup does not record one, so they are visible only to users with the archives:read_all permission until an admin reassigns them',

+ 2 - 0
frontend/src/i18n/locales/es.ts

@@ -5707,6 +5707,8 @@ export default {
         noData: 'No hay datos de este tipo en esta copia de seguridad',
         archivesPrinterMissing: 'Algunos archivos hacían referencia a impresoras que ya no existen - enlace eliminado',
         archivesProjectMissing: 'Algunos archivos hacían referencia a proyectos que ya no existen - enlace eliminado',
+        archivesNumbersDropped: 'Algunos archivos tenían gramos, costes, energía o tiempos de impresión que no son números válidos - se dejaron vacíos',
+        usageNumbersDropped: 'Algunas entradas de uso tenían pesos o costes que no son números válidos - se dejaron vacíos',
         archivesOwnerCleared: 'Algunos archivos hacían referencia a usuarios que ya no existen - se ha borrado el propietario, por lo que solo son visibles para usuarios con el permiso archives:read_all hasta que un administrador los reasigne',
         archivesOwnerUnmatched: 'Algunos archivos indican un propietario que no existe en esta instancia - se ha borrado el propietario en lugar de deducirlo del id de usuario de la copia de seguridad, por lo que solo son visibles para usuarios con el permiso archives:read_all hasta que un administrador los reasigne',
         archivesOwnerUnknown: 'Algunos archivos se restauraron sin propietario - esta copia de seguridad no registra ninguno, por lo que solo son visibles para usuarios con el permiso archives:read_all hasta que un administrador los reasigne',

+ 2 - 0
frontend/src/i18n/locales/fr.ts

@@ -5687,6 +5687,8 @@ export default {
         noData: 'Aucune donnée de ce type dans cette sauvegarde',
         archivesPrinterMissing: 'Certaines archives référençaient des imprimantes qui n\'existent plus - lien effacé',
         archivesProjectMissing: 'Certaines archives référençaient des projets qui n\'existent plus - lien effacé',
+        archivesNumbersDropped: 'Certaines archives contenaient des grammes, coûts, énergie ou durées d’impression qui ne sont pas des nombres valides - laissés vides',
+        usageNumbersDropped: 'Certaines entrées d’utilisation contenaient des poids ou coûts qui ne sont pas des nombres valides - laissés vides',
         archivesOwnerCleared: 'Certaines archives référençaient des utilisateurs qui n\'existent plus - propriétaire effacé, elles ne sont donc visibles que par les utilisateurs disposant de la permission archives:read_all jusqu\'à ce qu\'un administrateur les réattribue',
         archivesOwnerUnmatched: 'Certaines archives désignent un propriétaire absent de cette instance - le propriétaire a été effacé plutôt que déduit de l\'identifiant utilisateur de la sauvegarde, elles ne sont donc visibles que par les utilisateurs disposant de la permission archives:read_all jusqu\'à ce qu\'un administrateur les réattribue',
         archivesOwnerUnknown: 'Certaines archives ont été restaurées sans propriétaire - cette sauvegarde n\'en enregistre aucun, elles ne sont donc visibles que par les utilisateurs disposant de la permission archives:read_all jusqu\'à ce qu\'un administrateur les réattribue',

+ 2 - 0
frontend/src/i18n/locales/it.ts

@@ -5686,6 +5686,8 @@ export default {
         noData: 'Nessun dato di questo tipo in questo backup',
         archivesPrinterMissing: 'Alcuni archivi facevano riferimento a stampanti non più esistenti - collegamento rimosso',
         archivesProjectMissing: 'Alcuni archivi facevano riferimento a progetti non più esistenti - collegamento rimosso',
+        archivesNumbersDropped: 'Alcuni archivi avevano grammi, costi, energia o tempi di stampa non validi come numeri - lasciati vuoti',
+        usageNumbersDropped: 'Alcune voci di utilizzo avevano pesi o costi non validi come numeri - lasciati vuoti',
         archivesOwnerCleared: 'Alcuni archivi facevano riferimento a utenti non più esistenti - proprietario rimosso, quindi sono visibili solo agli utenti con il permesso archives:read_all finché un amministratore non li riassegna',
         archivesOwnerUnmatched: 'Alcuni archivi indicano un proprietario che questa istanza non ha - il proprietario è stato rimosso anziché dedotto dall\'id utente del backup, quindi sono visibili solo agli utenti con il permesso archives:read_all finché un amministratore non li riassegna',
         archivesOwnerUnknown: 'Alcuni archivi sono stati ripristinati senza proprietario - questo backup non ne registra alcuno, quindi sono visibili solo agli utenti con il permesso archives:read_all finché un amministratore non li riassegna',

+ 2 - 0
frontend/src/i18n/locales/ja.ts

@@ -5700,6 +5700,8 @@ export default {
         noData: 'この種類のデータはこのバックアップに含まれていません',
         archivesPrinterMissing: '一部のアーカイブが存在しないプリンターを参照していました - リンクを解除しました',
         archivesProjectMissing: '一部のアーカイブが存在しないプロジェクトを参照していました - リンクを解除しました',
+        archivesNumbersDropped: '一部のアーカイブのグラム・コスト・電力・印刷時間が有効な数値ではありません - 空欄にしました',
+        usageNumbersDropped: '一部の使用履歴の重量またはコストが有効な数値ではありません - 空欄にしました',
         archivesOwnerCleared: '一部のアーカイブが存在しないユーザーを参照していました - 所有者を解除したため、管理者が割り当て直すまで archives:read_all 権限を持つユーザーにしか表示されません',
         archivesOwnerUnmatched: '一部のアーカイブはこのインスタンスに存在しない所有者を指しています - バックアップのユーザー ID から推測せずに所有者を解除したため、管理者が割り当て直すまで archives:read_all 権限を持つユーザーにしか表示されません',
         archivesOwnerUnknown: '一部のアーカイブは所有者なしで復元されました - このバックアップに所有者が記録されていないため、管理者が割り当てるまで archives:read_all 権限を持つユーザーにしか表示されません',

+ 2 - 0
frontend/src/i18n/locales/ko.ts

@@ -5463,6 +5463,8 @@ export default {
         noData: '이 백업에는 이런 종류의 데이터가 없습니다',
         archivesPrinterMissing: '일부 아카이브가 더 이상 존재하지 않는 프린터를 참조했습니다 - 연결을 해제했습니다',
         archivesProjectMissing: '일부 아카이브가 더 이상 존재하지 않는 프로젝트를 참조했습니다 - 연결을 해제했습니다',
+        archivesNumbersDropped: '일부 아카이브의 그램, 비용, 에너지 또는 출력 시간이 유효한 숫자가 아닙니다 - 비워 두었습니다',
+        usageNumbersDropped: '일부 사용 기록의 무게 또는 비용이 유효한 숫자가 아닙니다 - 비워 두었습니다',
         archivesOwnerCleared: '일부 아카이브가 더 이상 존재하지 않는 사용자를 참조했습니다 - 소유자를 비웠으므로 관리자가 다시 지정하기 전까지 archives:read_all 권한이 있는 사용자에게만 보입니다',
         archivesOwnerUnmatched: '일부 아카이브가 이 인스턴스에 없는 소유자를 가리킵니다 - 백업의 사용자 ID로 추측하지 않고 소유자를 비웠으므로 관리자가 다시 지정하기 전까지 archives:read_all 권한이 있는 사용자에게만 보입니다',
         archivesOwnerUnknown: '일부 아카이브가 소유자 없이 복원되었습니다 - 이 백업에 소유자가 기록되어 있지 않으므로 관리자가 지정하기 전까지 archives:read_all 권한이 있는 사용자에게만 보입니다',

+ 2 - 0
frontend/src/i18n/locales/nl.ts

@@ -5750,6 +5750,8 @@ export default {
         noData: 'Geen gegevens van dit type in deze back-up',
         archivesPrinterMissing: 'Sommige archieven verwezen naar printers die niet meer bestaan - koppeling gewist',
         archivesProjectMissing: 'Sommige archieven verwezen naar projecten die niet meer bestaan - koppeling gewist',
+        archivesNumbersDropped: 'Sommige archieven hadden grammen, kosten, energie of printtijden die geen geldige getallen zijn - leeg gelaten',
+        usageNumbersDropped: 'Sommige verbruiksregels hadden gewichten of kosten die geen geldige getallen zijn - leeg gelaten',
         archivesOwnerCleared: 'Sommige archieven verwezen naar gebruikers die niet meer bestaan - eigenaar gewist, waardoor ze alleen zichtbaar zijn voor gebruikers met de machtiging archives:read_all totdat een beheerder ze opnieuw toewijst',
         archivesOwnerUnmatched: 'Sommige archieven noemen een eigenaar die deze instantie niet heeft - eigenaar gewist in plaats van te gokken op basis van het gebruikers-ID uit de back-up, waardoor ze alleen zichtbaar zijn voor gebruikers met de machtiging archives:read_all totdat een beheerder ze opnieuw toewijst',
         archivesOwnerUnknown: 'Sommige archieven zijn zonder eigenaar hersteld - deze back-up registreert er geen, waardoor ze alleen zichtbaar zijn voor gebruikers met de machtiging archives:read_all totdat een beheerder ze opnieuw toewijst',

+ 2 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -5686,6 +5686,8 @@ export default {
         noData: 'Não há dados desse tipo neste backup',
         archivesPrinterMissing: 'Alguns arquivos referenciavam impressoras que não existem mais - vínculo removido',
         archivesProjectMissing: 'Alguns arquivos referenciavam projetos que não existem mais - vínculo removido',
+        archivesNumbersDropped: 'Alguns arquivos tinham gramas, custos, energia ou tempos de impressão que não são números válidos - deixados vazios',
+        usageNumbersDropped: 'Algumas entradas de uso tinham pesos ou custos que não são números válidos - deixados vazios',
         archivesOwnerCleared: 'Alguns arquivos referenciavam usuários que não existem mais - o proprietário foi limpo, então eles só ficam visíveis para usuários com a permissão archives:read_all até que um administrador os reatribua',
         archivesOwnerUnmatched: 'Alguns arquivos apontam para um proprietário que esta instância não tem - o proprietário foi limpo em vez de deduzido do id de usuário da cópia, então eles só ficam visíveis para usuários com a permissão archives:read_all até que um administrador os reatribua',
         archivesOwnerUnknown: 'Alguns arquivos foram restaurados sem proprietário - este backup não registra nenhum, então eles só ficam visíveis para usuários com a permissão archives:read_all até que um administrador os atribua',

+ 2 - 0
frontend/src/i18n/locales/ru.ts

@@ -5455,6 +5455,8 @@ export default {
         noData: 'В этой резервной копии нет данных такого типа',
         archivesPrinterMissing: 'Некоторые архивы ссылались на несуществующие принтеры - связь очищена',
         archivesProjectMissing: 'Некоторые архивы ссылались на несуществующие проекты - связь очищена',
+        archivesNumbersDropped: 'В некоторых архивах граммы, стоимость, энергия или время печати не являются корректными числами - оставлены пустыми',
+        usageNumbersDropped: 'В некоторых записях расхода вес или стоимость не являются корректными числами - оставлены пустыми',
         archivesOwnerCleared: 'Некоторые архивы ссылались на несуществующих пользователей - владелец очищен, поэтому они видны только пользователям с правом archives:read_all, пока администратор не назначит владельца заново',
         archivesOwnerUnmatched: 'Некоторые архивы указывают владельца, которого нет в этом экземпляре - владелец очищен, а не угадан по идентификатору пользователя из резервной копии, поэтому они видны только пользователям с правом archives:read_all, пока администратор не назначит владельца заново',
         archivesOwnerUnknown: 'Некоторые архивы восстановлены без владельца - в этой резервной копии он не записан, поэтому они видны только пользователям с правом archives:read_all, пока администратор не назначит владельца',

+ 2 - 0
frontend/src/i18n/locales/sv.ts

@@ -5748,6 +5748,8 @@ errors: {
         noData: 'Ingen data av denna typ i denna säkerhetskopia',
         archivesPrinterMissing: 'Vissa arkiv refererade till skrivare som inte längre finns — länk rensad',
         archivesProjectMissing: 'Vissa arkiv refererade till projekt som inte längre finns — länk rensad',
+        archivesNumbersDropped: 'Vissa arkiv hade gram, kostnader, energi eller utskriftstider som inte är giltiga tal - lämnades tomma',
+        usageNumbersDropped: 'Vissa förbrukningsposter hade vikter eller kostnader som inte är giltiga tal - lämnades tomma',
         archivesOwnerCleared: 'Vissa arkiv refererade till användare som inte längre finns — ägare rensad, så de är synliga endast för användare med behörigheten archives:read_all tills en admin tilldelar dem på nytt',
         archivesOwnerUnmatched: 'Vissa arkiv namnger en ägare som denna instans inte har — ägare rensad snarare än gissad från säkerhetskopians användar-ID, så de är synliga endast för användare med behörigheten archives:read_all tills en admin tilldelar dem på nytt',
         archivesOwnerUnknown: 'Vissa arkiv återställdes utan en ägare — denna säkerhetskopia registrerar ingen, så de är synliga endast för användare med behörigheten archives:read_all tills en admin tilldelar dem på nytt',

+ 2 - 0
frontend/src/i18n/locales/tr.ts

@@ -5676,6 +5676,8 @@ export default {
         noData: 'Bu yedekte bu türde veri yok',
         archivesPrinterMissing: 'Bazı arşivler artık var olmayan yazıcılara işaret ediyordu - bağlantı temizlendi',
         archivesProjectMissing: 'Bazı arşivler artık var olmayan projelere işaret ediyordu - bağlantı temizlendi',
+        archivesNumbersDropped: 'Bazı arşivlerde gram, maliyet, enerji veya baskı süresi geçerli bir sayı değil - boş bırakıldı',
+        usageNumbersDropped: 'Bazı kullanım kayıtlarında ağırlık veya maliyet geçerli bir sayı değil - boş bırakıldı',
         archivesOwnerCleared: 'Bazı arşivler artık var olmayan kullanıcılara işaret ediyordu - sahip temizlendi, bu yüzden bir yönetici yeniden atayana kadar yalnızca archives:read_all iznine sahip kullanıcılara görünürler',
         archivesOwnerUnmatched: 'Bazı arşivler bu örnekte bulunmayan bir sahibi belirtiyor - sahip, yedekteki kullanıcı kimliğinden tahmin edilmek yerine temizlendi, bu yüzden bir yönetici yeniden atayana kadar yalnızca archives:read_all iznine sahip kullanıcılara görünürler',
         archivesOwnerUnknown: 'Bazı arşivler sahipsiz geri yüklendi - bu yedek sahip bilgisi içermiyor, bu yüzden bir yönetici atama yapana kadar yalnızca archives:read_all iznine sahip kullanıcılara görünürler',

+ 2 - 0
frontend/src/i18n/locales/uk.ts

@@ -5740,6 +5740,8 @@ export default {
         noData: "У цій резервній копії немає даних такого типу",
         archivesPrinterMissing: "Деякі архіви посилалися на принтери, яких більше немає - зв'язок очищено",
         archivesProjectMissing: "Деякі архіви посилалися на проєкти, яких більше немає - зв'язок очищено",
+        archivesNumbersDropped: 'У деяких архівах грами, вартість, енергія або час друку не є коректними числами - залишено порожніми',
+        usageNumbersDropped: 'У деяких записах витрат вага або вартість не є коректними числами - залишено порожніми',
         archivesOwnerCleared: "Деякі архіви посилалися на користувачів, яких більше немає - власника очищено, тому вони видимі лише користувачам із дозволом archives:read_all, доки адміністратор не призначить власника знову",
         archivesOwnerUnmatched: "Деякі архіви вказують власника, якого немає в цьому екземплярі - власника очищено, а не вгадано за ідентифікатором користувача з резервної копії, тому вони видимі лише користувачам із дозволом archives:read_all, доки адміністратор не призначить власника знову",
         archivesOwnerUnknown: "Деякі архіви відновлено без власника - у цій резервній копії його не записано, тому вони видимі лише користувачам із дозволом archives:read_all, доки адміністратор не призначить власника",

+ 2 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -5686,6 +5686,8 @@ export default {
         noData: '此备份中没有这类数据',
         archivesPrinterMissing: '部分归档引用了已不存在的打印机 - 已清除关联',
         archivesProjectMissing: '部分归档引用了已不存在的项目 - 已清除关联',
+        archivesNumbersDropped: '部分归档的克数、成本、能耗或打印时间不是有效数字 - 已留空',
+        usageNumbersDropped: '部分用量记录的重量或成本不是有效数字 - 已留空',
         archivesOwnerCleared: '部分归档引用了已不存在的用户 - 已清除归属,因此在管理员重新指派之前,只有拥有 archives:read_all 权限的用户才能看到它们',
         archivesOwnerUnmatched: '部分归档指向本实例没有的用户 - 已清除归属,而不是根据备份中的用户 ID 猜测,因此在管理员重新指派之前,只有拥有 archives:read_all 权限的用户才能看到它们',
         archivesOwnerUnknown: '部分归档在恢复时没有归属 - 此备份未记录归属,因此在管理员指派之前,只有拥有 archives:read_all 权限的用户才能看到它们',

+ 2 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -5686,6 +5686,8 @@ export default {
         noData: '此備份中沒有這類資料',
         archivesPrinterMissing: '部分封存參照了已不存在的印表機 - 已清除連結',
         archivesProjectMissing: '部分封存參照了已不存在的專案 - 已清除連結',
+        archivesNumbersDropped: '部分封存的克數、成本、能耗或列印時間不是有效數字 - 已留空',
+        usageNumbersDropped: '部分用量紀錄的重量或成本不是有效數字 - 已留空',
         archivesOwnerCleared: '部分封存參照了已不存在的使用者 - 已清除擁有者,因此在管理員重新指派之前,只有具備 archives:read_all 權限的使用者才看得到',
         archivesOwnerUnmatched: '部分封存指向本執行個體沒有的使用者 - 已清除擁有者,而非依備份中的使用者 ID 推測,因此在管理員重新指派之前,只有具備 archives:read_all 權限的使用者才看得到',
         archivesOwnerUnknown: '部分封存還原時沒有擁有者 - 此備份未記錄擁有者,因此在管理員指派之前,只有具備 archives:read_all 權限的使用者才看得到',

文件差异内容过多而无法显示
+ 0 - 0
static/assets/AdditionalSection-D36_5Wqb.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/AmsUnitCard-zU4y9Dkm.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ArchivesPage-D41CBI2o.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/AssignToAmsModal-ChbH5t2b.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/CamWallPage-quiP1adY.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/CameraDiagnoseModal-guerwlfl.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/CameraPage-B5dWHCvC.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/CameraWall-DAn7RdZA.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ColumnConfigModal-s0DUmgu_.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ConfigureAmsSlotModal-DAIt_D7l.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ConnectAuthorizePage-B8kUHvte.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ContextMenu-gbb1JkEx.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ExternalLinkPage-CG5aeuqW.js


文件差异内容过多而无法显示
+ 0 - 1
static/assets/FileManagerPage-DOcE644q.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/FileUploadModal-CLVxvtlH.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/FinancePage-CfnLp5UB.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/GCodeViewerPage-Cnt8CPel.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/GroupEditPage-BgASgPdI.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ImagePreviewModal-_HHmqc50.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/InventoryPage-BPXktYnD.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/LibraryTrashPage-C1E28_0e.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/LoginPage-BIaPHbg8.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/MaintenancePage-D6wHCn4h.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ModelSourcesPage-wd_J2spd.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/NotificationsPage-IyXDFotr.js


文件差异内容过多而无法显示
+ 0 - 1
static/assets/PdfPreviewModal-OLlIMbw4.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/PhotoGalleryModal-tY5NL9XU.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/PrinterLocationsPage-Co-ZM3jL.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/PrintersPage-CNGIs9q9.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ProfilesPage-rS9vPHEf.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ProjectDetailPage-BQAncbK2.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ProjectsPage-CkdU9XuP.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/QueuePage-CE6X1Aol.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/RichTextEditor-Dl-XiCDc.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SettingsPage-9ZVoPdaL.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SetupPage-DwsOcMtK.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SliceModal-KXGH4ldx.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SpoolBuddyAmsPage-DWQqSkGV.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SpoolBuddyDashboard-DXb_3CWd.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-BESkeCyu.js


文件差异内容过多而无法显示
+ 1 - 1
static/assets/SpoolBuddySettingsPage-BjufIaGU.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SpoolBuddyWriteTagPage-C5_j6clD.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SpreadsheetPreviewModal-B-JsZzg0.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/StatsPage-CK-2tPw6.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/StreamOverlayPage-CeKlJQH7.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SystemInfoPage-doBQwYF4.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/de-4ioempp1.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/es-DpJKx-EW.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/fr-xkfLC38W.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/index-BrsvYaVv.js


+ 1 - 1
static/assets/inventorySearch-DTH3xXZS.js → static/assets/inventorySearch-CP5BRBza.js

@@ -1 +1 @@
-import{mt as e}from"./index-DAlcmDFx.js";function t(t,n){let r=n.trim().toLowerCase();if(!r)return!0;let i=r.match(/^#(\d+)$/);return i?t.id===Number(i[1]):String(t.id).includes(r)||t.material.toLowerCase().includes(r)||(t.brand?.toLowerCase().includes(r)??!1)||(t.color_name?.toLowerCase().includes(r)??!1)||(e(t.color_name,t.rgba,t.color_name_is_synthesized)?.toLowerCase().includes(r)??!1)||(t.subtype?.toLowerCase().includes(r)??!1)||(t.note?.toLowerCase().includes(r)??!1)||(t.slicer_filament_name?.toLowerCase().includes(r)??!1)||(t.storage_location?.toLowerCase().includes(r)??!1)||(t.material_number?.toLowerCase().includes(r)??!1)||(t.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(r)||(e.supplier_article_number?.toLowerCase().includes(r)??!1))??!1)}function n(e,n){return n?e.filter(e=>t(e,n)):e}export{n as t};
+import{mt as e}from"./index-BrsvYaVv.js";function t(t,n){let r=n.trim().toLowerCase();if(!r)return!0;let i=r.match(/^#(\d+)$/);return i?t.id===Number(i[1]):String(t.id).includes(r)||t.material.toLowerCase().includes(r)||(t.brand?.toLowerCase().includes(r)??!1)||(t.color_name?.toLowerCase().includes(r)??!1)||(e(t.color_name,t.rgba,t.color_name_is_synthesized)?.toLowerCase().includes(r)??!1)||(t.subtype?.toLowerCase().includes(r)??!1)||(t.note?.toLowerCase().includes(r)??!1)||(t.slicer_filament_name?.toLowerCase().includes(r)??!1)||(t.storage_location?.toLowerCase().includes(r)??!1)||(t.material_number?.toLowerCase().includes(r)??!1)||(t.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(r)||(e.supplier_article_number?.toLowerCase().includes(r)??!1))??!1)}function n(e,n){return n?e.filter(e=>t(e,n)):e}export{n as t};

文件差异内容过多而无法显示
+ 0 - 0
static/assets/it-C1149LDg.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ja-orcfMFZM.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ko-DdEswUHB.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/locationSensorDefaults-CWgd1fgy.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/nl-Bwum3z1V.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/pdf-CfGojSXf.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/pt-BR-LpQsAf78.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ru-DjCmUPSL.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/sv-DcnNQ9gK.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/tr-CKS-erzp.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/uk-ZjAfoH0T.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/utils-2f0Jw5iC.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/zh-CN-B7cfiE3N.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/zh-TW-O3qdj-Xb.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DAlcmDFx.js"></script>
+    <script type="module" crossorigin src="/assets/index-BrsvYaVv.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/chunk-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-p9vNUtaN.js">
     <link rel="modulepreload" crossorigin href="/assets/QueryClientProvider-B5zKGR0V.js">

部分文件因为文件数量过多而无法显示