Explorar o código

fix(settings): saved settings take effect, unused ones are removed

- Max copies per run and the Docker compose directory are saved; they
  were never sent.
- The default printer is in the UI flags (null outside the caller's
  printer scope) and preselected once per print dialog opening, not
  where the printer list is hidden or for a file sliced for another
  model.
- The global auto-archive switch is the master over each printer's
  own for prints started outside Bambuddy; printer calibration runs
  are skipped before it, so they never send a print-start
  notification.
- The library archive mode setting is removed; it was never read. A
  Git restore drops it from older backups without listing it.
maziggy hai 6 días
pai
achega
c9b551df0b
Modificáronse 100 ficheiros con 406 adicións e 194 borrados
  1. 5 0
      CHANGELOG.md
  2. 6 0
      backend/app/api/routes/settings.py
  3. 28 19
      backend/app/main.py
  4. 0 5
      backend/app/schemas/settings.py
  5. 6 0
      backend/app/services/github_restore.py
  6. 86 0
      backend/tests/integration/test_global_auto_archive_switch.py
  7. 2 0
      backend/tests/integration/test_settings_ui_flags_3023.py
  8. 39 0
      backend/tests/integration/test_ui_flags_default_printer.py
  9. 8 0
      backend/tests/unit/test_github_backup_settings_secrets.py
  10. 26 0
      backend/tests/unit/test_internal_printer_jobs.py
  11. 48 0
      frontend/src/__tests__/components/PrintModal.test.tsx
  12. 16 0
      frontend/src/__tests__/components/PrintModalCrossModel.test.tsx
  13. 0 1
      frontend/src/__tests__/pages/InventoryPageArchivedConsumed.test.tsx
  14. 0 1
      frontend/src/__tests__/pages/InventoryPageCopyButton.test.tsx
  15. 0 1
      frontend/src/__tests__/pages/InventoryPageDeepLink.test.tsx
  16. 0 1
      frontend/src/__tests__/pages/InventoryPageGroupRestore.test.tsx
  17. 0 1
      frontend/src/__tests__/pages/InventoryPageLowStock.test.tsx
  18. 0 1
      frontend/src/__tests__/pages/InventoryPagePrintLabels.test.tsx
  19. 0 1
      frontend/src/__tests__/pages/InventoryPageSpoolmanLocation.test.tsx
  20. 88 0
      frontend/src/__tests__/pages/SettingsPageUnsavedFields.test.tsx
  21. 0 1
      frontend/src/api/client.ts
  22. 27 5
      frontend/src/components/PrintModal/index.tsx
  23. 1 9
      frontend/src/i18n/locales/de.ts
  24. 1 9
      frontend/src/i18n/locales/en.ts
  25. 1 9
      frontend/src/i18n/locales/es.ts
  26. 1 9
      frontend/src/i18n/locales/fr.ts
  27. 1 9
      frontend/src/i18n/locales/it.ts
  28. 1 9
      frontend/src/i18n/locales/ja.ts
  29. 1 8
      frontend/src/i18n/locales/ko.ts
  30. 1 9
      frontend/src/i18n/locales/nl.ts
  31. 1 9
      frontend/src/i18n/locales/pt-BR.ts
  32. 1 8
      frontend/src/i18n/locales/ru.ts
  33. 1 9
      frontend/src/i18n/locales/sv.ts
  34. 1 8
      frontend/src/i18n/locales/tr.ts
  35. 1 9
      frontend/src/i18n/locales/uk.ts
  36. 1 9
      frontend/src/i18n/locales/zh-CN.ts
  37. 1 9
      frontend/src/i18n/locales/zh-TW.ts
  38. 4 21
      frontend/src/pages/SettingsPage.tsx
  39. 0 0
      static/assets/AdditionalSection-BCcQRTKQ.js
  40. 0 0
      static/assets/AmsUnitCard-CUx9p2v6.js
  41. 0 0
      static/assets/ArchivesPage-JilnP9pX.js
  42. 0 0
      static/assets/AssignToAmsModal-BuslsYWw.js
  43. 0 0
      static/assets/CamWallPage-CO3ZUqb2.js
  44. 0 0
      static/assets/CameraDiagnoseModal-jUf5wCOp.js
  45. 0 0
      static/assets/CameraPage-DD-_PRay.js
  46. 0 0
      static/assets/CameraWall-CIFG_rbT.js
  47. 0 0
      static/assets/ColumnConfigModal-CRFZx03j.js
  48. 0 0
      static/assets/ConfigureAmsSlotModal-B3rNLxCc.js
  49. 0 0
      static/assets/ConnectAuthorizePage-BAwHtJ_h.js
  50. 0 0
      static/assets/ContextMenu-iQaDMRMq.js
  51. 0 0
      static/assets/ExternalLinkPage-DVqaak4R.js
  52. 0 1
      static/assets/FileManagerPage-Ce1ZSbck.js
  53. 0 0
      static/assets/FileUploadModal-xH-xBAwl.js
  54. 0 0
      static/assets/FinancePage-BTmCNBE2.js
  55. 0 0
      static/assets/GCodeViewerPage-SjE-zlyZ.js
  56. 0 0
      static/assets/GroupEditPage-axw65b8h.js
  57. 0 0
      static/assets/ImagePreviewModal-BCYgEvOk.js
  58. 0 0
      static/assets/InventoryPage-CPymhcaz.js
  59. 0 0
      static/assets/LibraryTrashPage-DdeKmJHy.js
  60. 0 0
      static/assets/LoginPage-BUrz_-2s.js
  61. 0 0
      static/assets/MaintenancePage-CUGCBK_D.js
  62. 0 0
      static/assets/ModelSourcesPage-DyfDKRUf.js
  63. 0 0
      static/assets/NotificationsPage-BbRbmhQL.js
  64. 0 1
      static/assets/PdfPreviewModal-D_laxCuF.js
  65. 0 0
      static/assets/PhotoGalleryModal-oEeo5CR8.js
  66. 0 0
      static/assets/PrinterLocationsPage-BMz8OK0N.js
  67. 0 0
      static/assets/PrintersPage-wKRUwJUL.js
  68. 0 0
      static/assets/ProfilesPage-EEBkpJSv.js
  69. 0 0
      static/assets/ProjectDetailPage-C_9Sxv4G.js
  70. 0 0
      static/assets/ProjectsPage-BRaLy0mi.js
  71. 0 0
      static/assets/QueuePage-CIGN-riR.js
  72. 0 0
      static/assets/RichTextEditor-DHWuQypn.js
  73. 0 0
      static/assets/SettingsPage-DAd2Rbkn.js
  74. 0 0
      static/assets/SetupPage-BgnTYgfd.js
  75. 0 0
      static/assets/SliceModal-CXRyOQkN.js
  76. 0 0
      static/assets/SpoolBuddyAmsPage-C7VK2rbC.js
  77. 0 0
      static/assets/SpoolBuddyCalibrationPage-ACVrbLeC.js
  78. 0 0
      static/assets/SpoolBuddyDashboard-welJGIru.js
  79. 0 0
      static/assets/SpoolBuddyInventoryPage-DHQ2LH3P.js
  80. 1 1
      static/assets/SpoolBuddySettingsPage-CNHpwgIw.js
  81. 0 0
      static/assets/SpoolBuddyWriteTagPage-CQdfdrri.js
  82. 0 0
      static/assets/SpreadsheetPreviewModal-RcpOjOu2.js
  83. 0 0
      static/assets/StatsPage-B6LXwkWQ.js
  84. 0 0
      static/assets/StreamOverlayPage-BKX0YUXG.js
  85. 0 0
      static/assets/SystemInfoPage-BCFVOOlb.js
  86. 0 0
      static/assets/de-D0GtQVkj.js
  87. 0 0
      static/assets/de-mgb0RyZq.js
  88. 0 0
      static/assets/es-ByQJdCqZ.js
  89. 0 0
      static/assets/es-rm5cH5pl.js
  90. 0 0
      static/assets/fr-CNvpzB4j.js
  91. 0 0
      static/assets/fr-Et2eyvIH.js
  92. 0 0
      static/assets/index-BlsvkMcT.js
  93. 1 1
      static/assets/inventorySearch-3g_FrCfp.js
  94. 0 0
      static/assets/it-gnCEsI8h.js
  95. 0 0
      static/assets/it-jJlvgvcv.js
  96. 0 0
      static/assets/ja-D0wu3paX.js
  97. 0 0
      static/assets/ja-D8He1KGK.js
  98. 0 0
      static/assets/ko-C4ZprGjR.js
  99. 0 0
      static/assets/ko-DftDwEdH.js
  100. 0 0
      static/assets/locationSensorDefaults-BLgEpBMI.js

+ 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
+- **Settings that were not saved or not used** —
+  - **Max copies per run** (Workflow) and the Docker compose directory used in the update instructions are saved. They changed on screen and were never sent, so a 100-copy pipeline run stayed refused at the old limit of 50.
+  - **Default printer** (General) is now selected in the print dialog when it opens, unless the file was sliced for another model or the job goes to a model rather than a printer. The setting was saved and never used.
+  - **Auto-archive prints** (General) now switches archiving off for every printer, for prints started outside Bambuddy. While it is on, each printer's own switch decides. Before, only the printer's switch was read. A printer's own calibration runs no longer send a "print started" notification while archiving is off.
+  - **Create Archive Entry When Printing** (File Manager) is removed. It was saved and never used: printing from the File Manager always creates an archive entry.
 - **A spool Spoolman creates from the AMS is the size of the roll** — a 250 g refill matched to an existing 1000 g filament was created as a 1000 g spool with 250 g left, showing as 75% used.
 - **An unregistered SpoolBuddy that is still running registers again on its next heartbeat**, as the documentation says. It stayed missing until the daemon or the Pi restarted, and the refused heartbeats filled its send buffer, which then held back everything queued behind them.
 - **SpoolBuddy installer, full mode, installed under /home** — the Bambuddy service it writes now starts. The service hid /home from itself and failed with 203/EXEC, as the main installer's did before #1685.

+ 6 - 0
backend/app/api/routes/settings.py

@@ -670,6 +670,9 @@ _UI_FLAG_FIELDS: tuple[str, ...] = (
     # forecast's default lead time.
     "spoolman_enabled",
     "forecast_global_lead_time_days",
+    # Preselected in the print dialog; withheld below from a caller who may
+    # not use that printer.
+    "default_printer_id",
 )
 
 
@@ -677,6 +680,7 @@ _UI_FLAG_FIELDS: tuple[str, ...] = (
 async def get_ui_flags(
     db: AsyncSession = Depends(get_db),
     _: User | None = Depends(require_auth_if_enabled),
+    printer_scope: PrinterScope = RequestPrinterScope,
 ):
     """Install configuration the app shell needs, for any signed-in user.
 
@@ -694,6 +698,8 @@ async def get_ui_flags(
     full = await _build_settings_response(db, is_api_key=False)
     dumped = full.model_dump()
     flags = {key: dumped[key] for key in _UI_FLAG_FIELDS if key in dumped}
+    if flags.get("default_printer_id") is not None and not printer_scope.allows(flags["default_printer_id"]):
+        flags["default_printer_id"] = None
     # Whether G-code snippets are set up, not the snippets themselves.
     flags["has_gcode_snippets"] = bool(dumped.get("gcode_snippets"))
     return flags

+ 28 - 19
backend/app/main.py

@@ -4517,7 +4517,33 @@ async def on_print_start(printer_id: int, data: dict):
                 await _send_print_start_notification(printer_id, data, logger=logger)
             return
 
-        if not printer.auto_archive:
+        # Skip the printer's own jobs — a calibration run is not a user's print.
+        # See is_internal_printer_job for what counts and why both fields are
+        # tested; the pressure-advance line reports as a subtask name with no
+        # /usr/ path, which the old prefix-only test here missed entirely.
+        #
+        # No notification either. The event describes the printer calibrating
+        # itself, so "Print started" is as wrong as the archive was, and the
+        # matching completion is suppressed in on_print_complete for the same
+        # reason. Checked before the auto-archive switches, whose "off" path
+        # still notifies.
+        if is_internal_printer_job(data.get("filename", ""), data.get("subtask_name", "")):
+            logger.info(
+                "[CALLBACK] Skipping archive — internal printer job detected: filename=%s, subtask=%s",
+                data.get("filename", ""),
+                data.get("subtask_name", ""),
+            )
+            return
+
+        # The global switch is the master: off, no printer archives prints
+        # started outside Bambuddy; on, each printer's own switch decides.
+        from backend.app.api.routes.settings import get_setting
+
+        _global_auto_archive = await get_setting(db, "auto_archive")
+        archives_prints = printer.auto_archive and (
+            _global_auto_archive is None or _global_auto_archive.lower() == "true"
+        )
+        if not archives_prints:
             # auto-archive disabled — check if there's an expected print (dispatched
             # by BamBuddy via queue/reprint) that already has an archive to promote.
             # If so, fall through to the expected-print handling below so the archive
@@ -4577,23 +4603,6 @@ async def on_print_start(printer_id: int, data: dict):
 
         logger.info("[CALLBACK] Print start detected - filename: %s, subtask: %s", filename, subtask_name)
 
-        # Skip the printer's own jobs — a calibration run is not a user's print.
-        # See is_internal_printer_job for what counts and why both fields are
-        # tested; the pressure-advance line reports as a subtask name with no
-        # /usr/ path, which the old prefix-only test here missed entirely.
-        #
-        # No notification either. The event describes the printer calibrating
-        # itself, so "Print started" is as wrong as the archive was, and the
-        # matching completion is suppressed in on_print_complete for the same
-        # reason.
-        if is_internal_printer_job(filename, subtask_name):
-            logger.info(
-                "[CALLBACK] Skipping archive — internal printer job detected: filename=%s, subtask=%s",
-                filename,
-                subtask_name,
-            )
-            return
-
         if not filename and not subtask_name:
             # Send notification without archive data (no filename)
             logger.info("[CALLBACK] Skipping archive - no filename or subtask_name")
@@ -4903,7 +4912,7 @@ async def on_print_start(printer_id: int, data: dict):
                 return  # Skip creating a new archive
 
             # The archive was deleted after the job was queued.
-            if not printer.auto_archive:
+            if not archives_prints:
                 # Only the expected print brought a printer that does not
                 # archive this far; with it gone, nothing is archived.
                 return

+ 0 - 5
backend/app/schemas/settings.py

@@ -297,10 +297,6 @@ class AppSettings(BaseModel):
     )
 
     # File Manager / Library settings
-    library_archive_mode: str = Field(
-        default="ask",
-        description="When printing from File Manager, create archive entry: 'always', 'never', or 'ask'",
-    )
     library_disk_warning_gb: float = Field(
         default=5.0,
         description="Show warning when free disk space falls below this threshold (GB)",
@@ -802,7 +798,6 @@ class AppSettingsUpdate(BaseModel):
     ha_enabled: bool | None = None
     ha_url: str | None = None
     ha_token: str | None = None
-    library_archive_mode: str | None = None
     library_disk_warning_gb: float | None = None
     camera_view_mode: str | None = None
     camera_light_mode: Literal["off", "all", "selected"] | None = None

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

@@ -258,6 +258,10 @@ _MANYFOLD_CLIENT_ID_KEY = "manyfold_client_id"
 # null. Anywhere else it is refused, as the editor refuses a null.
 _READS_A_SAVED_NULL = frozenset({"ams_temp_alarm", "default_printer_id", "open_in_slicer"})
 
+# Settings that no longer exist. Older backups still hold them; a restore drops
+# them without listing them as skipped.
+_RETIRED_SETTING_KEYS = frozenset({"library_archive_mode"})
+
 
 def is_restorable_setting_key(key: str) -> bool:
     """Whether a restore writes this settings key at all (with a value its
@@ -660,6 +664,8 @@ class GitHubRestoreService:
             if key.startswith("_"):
                 internal.append(key)
                 continue
+            if key in _RETIRED_SETTING_KEYS:
+                continue
             # A null is skipped by the restore itself, not refused.
             if value is not None:
                 valid, form = _checked_setting(key, value)

+ 86 - 0
backend/tests/integration/test_global_auto_archive_switch.py

@@ -0,0 +1,86 @@
+"""The global auto-archive switch is the master over the printers' own.
+
+Off, no printer archives a print started outside Bambuddy; on, each
+printer's own switch decides. The global switch used to be saved and never
+read, so turning it off changed nothing.
+"""
+
+import pytest
+from sqlalchemy import select
+
+from backend.app.models.archive import PrintArchive
+from backend.app.models.settings import Settings
+from backend.tests.integration.test_external_print_confirmation_1898 import (  # noqa: F401 (autouse fixture)
+    SUBTASK,
+    _clear_print_state,
+    _drive_print_start,
+)
+
+
+async def _archives(db_session) -> list:
+    db_session.expire_all()
+    return list((await db_session.execute(select(PrintArchive))).scalars().all())
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_global_off_archives_nothing(test_engine, db_session, printer_factory):
+    printer = await printer_factory()
+    db_session.add(Settings(key="auto_archive", value="false"))
+    await db_session.commit()
+
+    await _drive_print_start(test_engine, printer, download_ok=False)
+
+    assert await _archives(db_session) == []
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_global_on_lets_the_printer_decide(test_engine, db_session, printer_factory):
+    printer = await printer_factory()
+    db_session.add(Settings(key="auto_archive", value="true"))
+    await db_session.commit()
+
+    await _drive_print_start(test_engine, printer, download_ok=False)
+
+    assert len(await _archives(db_session)) == 1
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_a_printer_switched_off_stays_off_under_global_on(test_engine, db_session, printer_factory):
+    printer = await printer_factory(auto_archive=False)
+
+    await _drive_print_start(test_engine, printer, download_ok=False)
+
+    assert await _archives(db_session) == []
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_unset_global_lets_the_printer_decide(test_engine, db_session, printer_factory):
+    """Installs that never saved the setting have no row; it reads as on."""
+    printer = await printer_factory()
+
+    await _drive_print_start(test_engine, printer, download_ok=False)
+
+    assert len(await _archives(db_session)) == 1
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_global_off_still_tracks_a_print_bambuddy_sent(test_engine, db_session, printer_factory, archive_factory):
+    from backend.app.main import _active_prints, register_expected_print
+
+    printer = await printer_factory()
+    archive = await archive_factory(printer.id, filename=f"{SUBTASK}.gcode.3mf", status="pending", with_run=False)
+    db_session.add(Settings(key="auto_archive", value="false"))
+    await db_session.commit()
+    archive_id = archive.id
+    register_expected_print(printer.id, f"{SUBTASK}.gcode.3mf", archive_id)
+
+    await _drive_print_start(test_engine, printer, download_ok=False)
+
+    assert [a.id for a in await _archives(db_session)] == [archive_id]
+    assert (await db_session.get(PrintArchive, archive_id)).status == "printing"
+    assert archive_id in _active_prints.values()

+ 2 - 0
backend/tests/integration/test_settings_ui_flags_3023.py

@@ -178,6 +178,8 @@ class TestWhatItExposes:
             "light_accent",
             "spoolman_enabled",
             "forecast_global_lead_time_days",
+            # Withheld (None) from a caller outside that printer's scope.
+            "default_printer_id",
             "has_gcode_snippets",
         }
 

+ 39 - 0
backend/tests/integration/test_ui_flags_default_printer.py

@@ -0,0 +1,39 @@
+"""The print dialog preselects the default printer, for users who cannot read
+all settings too: it comes with the UI flags, and only when the caller may use
+that printer."""
+
+import pytest
+from httpx import AsyncClient
+
+from backend.app.models.settings import Settings
+from backend.tests.integration.test_printer_scope_1727 import _admin_token, _auth, _group, _user
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_the_default_printer_comes_with_the_ui_flags(async_client: AsyncClient, db_session, printer_factory):
+    printer = await printer_factory()
+    db_session.add(Settings(key="default_printer_id", value=str(printer.id)))
+    await db_session.commit()
+
+    flags = (await async_client.get("/api/v1/settings/ui-flags")).json()
+
+    assert flags["default_printer_id"] == printer.id
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_a_default_printer_outside_the_callers_printers_is_not_offered(
+    async_client: AsyncClient, db_session, printer_factory
+):
+    mine = await printer_factory()
+    theirs = await printer_factory()
+    db_session.add(Settings(key="default_printer_id", value=str(theirs.id)))
+    await db_session.commit()
+    admin = await _admin_token(async_client)
+    team = await _group(async_client, admin, "team", permissions=["printers:read"], printer_ids=[mine.id])
+    jwt, _ = await _user(async_client, admin, "member", [team])
+
+    flags = (await async_client.get("/api/v1/settings/ui-flags", headers=_auth(jwt))).json()
+
+    assert flags["default_printer_id"] is None

+ 8 - 0
backend/tests/unit/test_github_backup_settings_secrets.py

@@ -135,3 +135,11 @@ async def test_everything_backed_up_is_something_a_restore_knows(db_session):
     plan = await GitHubRestoreService()._plan_settings(db_session, collected)
 
     assert plan.unknown == () and plan.internal == ()
+
+
+@pytest.mark.asyncio
+async def test_a_retired_setting_in_an_old_backup_is_dropped_quietly(db_session):
+    plan = await GitHubRestoreService()._plan_settings(db_session, {"library_archive_mode": "ask", "currency": "EUR"})
+
+    assert plan.unknown == ()
+    assert "library_archive_mode" not in plan.stored

+ 26 - 0
backend/tests/unit/test_internal_printer_jobs.py

@@ -170,6 +170,32 @@ class TestPrintStartSkipsTheCalibration:
         skipped = [r for r in capture_logs.records if "internal printer job" in str(r.message)]
         assert skipped, "Should log that the calibration run was skipped"
 
+    @pytest.mark.asyncio
+    async def test_archiving_off_does_not_notify_it_either(self):
+        """The auto-archive "off" path sends a print-start notification of its
+        own, so the calibration check has to come first."""
+        sess, notif, plug, ws, pm, relay = _mocked_print_start()
+        with sess as mock_session_maker, notif as mock_notif, plug as mock_plug, ws as mock_ws, pm as mock_pm, relay:
+            mock_notif.on_print_start = AsyncMock()
+            mock_plug.on_print_start = AsyncMock()
+            mock_ws.send_print_start = AsyncMock()
+            mock_pm.get_printer = MagicMock(return_value=MagicMock(name="Test", serial_number="TEST123"))
+            mock_printer = MagicMock(auto_archive=False, id=1)
+            mock_session = AsyncMock()
+            mock_session.__aenter__ = AsyncMock(return_value=mock_session)
+            mock_session.__aexit__ = AsyncMock()
+            mock_session.execute = AsyncMock(
+                return_value=MagicMock(scalar_one_or_none=MagicMock(return_value=mock_printer))
+            )
+            mock_session_maker.return_value = mock_session
+
+            with patch("backend.app.main._send_print_start_notification", new_callable=AsyncMock) as mock_notify:
+                from backend.app.main import on_print_start
+
+                await on_print_start(1, {"filename": "", "subtask_name": "auto_pa_line_calib_mode"})
+
+                mock_notify.assert_not_called()
+
 
 class TestPrintCompleteStaysQuiet:
     @pytest.mark.asyncio

+ 48 - 0
frontend/src/__tests__/components/PrintModal.test.tsx

@@ -2958,3 +2958,51 @@ describe('PrintModal edit: moving a job to another printer', () => {
     expect(patched[0].nozzle_rack_choice).toBeNull();
   });
 });
+
+describe('PrintModal default printer', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    server.use(
+      http.get('/api/v1/printers/', () => HttpResponse.json(mockPrinters)),
+      http.get('/api/v1/settings/', () => HttpResponse.json({ default_printer_id: 2, currency: 'USD' })),
+      http.get('/api/v1/archives/:id/plates', () => HttpResponse.json({ is_multi_plate: false, plates: [] })),
+      http.get('/api/v1/archives/:id/filament-requirements', () => HttpResponse.json({ filaments: [] })),
+      http.get('/api/v1/printers/:id/status', () =>
+        HttpResponse.json({ connected: true, state: 'IDLE', ams: [], vt_tray: [] }),
+      ),
+    );
+  });
+
+  it('preselects the default printer from Settings', async () => {
+    render(<PrintModal mode="create" archiveId={1} archiveName="Test Print" onClose={vi.fn()} />);
+
+    expect(await screen.findByText('1 printer selected')).toBeInTheDocument();
+  });
+
+  it('does not bring the default back once it was deselected', async () => {
+    const user = userEvent.setup();
+    render(<PrintModal mode="create" archiveId={1} archiveName="Test Print" onClose={vi.fn()} />);
+    await screen.findByText('1 printer selected');
+
+    await user.click(screen.getByText('P1S'));
+    await new Promise((resolve) => setTimeout(resolve, 100));
+
+    expect(screen.queryByText('1 printer selected')).not.toBeInTheDocument();
+  });
+  it('does not select a default printer of another model than the file was sliced for', async () => {
+    server.use(
+      http.get('/api/v1/archives/:id', async () => {
+        // Later than printers and settings: the model must be waited for.
+        await new Promise((resolve) => setTimeout(resolve, 50));
+        return HttpResponse.json({ id: 1, filename: 'test.3mf', sliced_for_model: 'X1C' });
+      }),
+    );
+    const user = userEvent.setup();
+    render(<PrintModal mode="create" archiveId={1} archiveName="Test Print" onClose={vi.fn()} />);
+    // The P1S is hidden behind "show all" as another model than X1C.
+    await user.click(await screen.findByText('show all'));
+    await new Promise((resolve) => setTimeout(resolve, 200));
+
+    expect(screen.getByText('P1S').closest('button')).not.toHaveClass('border-bambu-green');
+  });
+});

+ 16 - 0
frontend/src/__tests__/components/PrintModalCrossModel.test.tsx

@@ -79,6 +79,22 @@ function renderCrossModel() {
 describe('PrintModal cross-model mode', () => {
   beforeEach(() => mockBackend());
 
+  it('does not select the default printer the hidden picker would have shown', async () => {
+    // A selected printer behind the candidate list would bring up a filament
+    // mapping panel for a printer nobody picked.
+    server.use(
+      http.get('/api/v1/settings/', () => HttpResponse.json({ default_printer_id: 1, currency: 'USD' })),
+      http.get('/api/v1/printers/:id/status', () =>
+        HttpResponse.json({ connected: true, state: 'IDLE', ams: [], vt_tray: [] }),
+      ),
+    );
+    renderCrossModel();
+    await screen.findByText('h2d.gcode.3mf');
+    await new Promise((resolve) => setTimeout(resolve, 200));
+
+    expect(screen.queryByText(/Filament Mapping/)).not.toBeInTheDocument();
+  });
+
   it('replaces the printer picker with the candidate list', async () => {
     renderCrossModel();
     expect(await screen.findByText('x1c.gcode.3mf')).toBeInTheDocument();

+ 0 - 1
frontend/src/__tests__/pages/InventoryPageArchivedConsumed.test.tsx

@@ -78,7 +78,6 @@ const baseSettings = {
   ha_url_from_env: false,
   ha_token_from_env: false,
   ha_env_managed: false,
-  library_archive_mode: 'ask',
   library_disk_warning_gb: 5.0,
   camera_view_mode: 'window',
   preferred_slicer: 'bambu_studio',

+ 0 - 1
frontend/src/__tests__/pages/InventoryPageCopyButton.test.tsx

@@ -115,7 +115,6 @@ const MOCK_SETTINGS = {
   ha_url_from_env: false,
   ha_token_from_env: false,
   ha_env_managed: false,
-  library_archive_mode: 'ask',
   library_disk_warning_gb: 5.0,
   camera_view_mode: 'window',
   preferred_slicer: 'bambu_studio',

+ 0 - 1
frontend/src/__tests__/pages/InventoryPageDeepLink.test.tsx

@@ -103,7 +103,6 @@ const MOCK_SETTINGS = {
   ha_url_from_env: false,
   ha_token_from_env: false,
   ha_env_managed: false,
-  library_archive_mode: 'ask',
   library_disk_warning_gb: 5.0,
   camera_view_mode: 'window',
   preferred_slicer: 'bambu_studio',

+ 0 - 1
frontend/src/__tests__/pages/InventoryPageGroupRestore.test.tsx

@@ -64,7 +64,6 @@ const baseSettings = {
   ha_url_from_env: false,
   ha_token_from_env: false,
   ha_env_managed: false,
-  library_archive_mode: 'ask',
   library_disk_warning_gb: 5.0,
   camera_view_mode: 'window',
   preferred_slicer: 'bambu_studio',

+ 0 - 1
frontend/src/__tests__/pages/InventoryPageLowStock.test.tsx

@@ -71,7 +71,6 @@ const mockSettings = {
   ha_url_from_env: false,
   ha_token_from_env: false,
   ha_env_managed: false,
-  library_archive_mode: 'ask',
   library_disk_warning_gb: 5.0,
   camera_view_mode: 'window',
   preferred_slicer: 'bambu_studio',

+ 0 - 1
frontend/src/__tests__/pages/InventoryPagePrintLabels.test.tsx

@@ -106,7 +106,6 @@ const MOCK_SETTINGS = {
   ha_url_from_env: false,
   ha_token_from_env: false,
   ha_env_managed: false,
-  library_archive_mode: 'ask',
   library_disk_warning_gb: 5.0,
   camera_view_mode: 'window',
   preferred_slicer: 'bambu_studio',

+ 0 - 1
frontend/src/__tests__/pages/InventoryPageSpoolmanLocation.test.tsx

@@ -70,7 +70,6 @@ const mockSettings = {
   ha_url_from_env: false,
   ha_token_from_env: false,
   ha_env_managed: false,
-  library_archive_mode: 'ask',
   library_disk_warning_gb: 5.0,
   camera_view_mode: 'window',
   preferred_slicer: 'bambu_studio',

+ 88 - 0
frontend/src/__tests__/pages/SettingsPageUnsavedFields.test.tsx

@@ -0,0 +1,88 @@
+/**
+ * Two settings the page showed and let the user change but never saved:
+ * "Max copies per run" (a 100-copy pipeline run stayed refused at 50) and the
+ * Docker compose directory used in the update instructions.
+ */
+
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { fireEvent, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { http, HttpResponse } from 'msw';
+import { render } from '../utils';
+import { server } from '../mocks/server';
+import { SettingsPage } from '../../pages/SettingsPage';
+import { setAuthToken } from '../../api/client';
+
+const settings = {
+  auto_archive: true,
+  save_thumbnails: true,
+  capture_finish_photo: true,
+  default_filament_cost: 25.0,
+  currency: 'USD',
+  time_format: 'system',
+  date_format: 'system',
+  check_updates: true,
+  check_printer_firmware: false,
+  pipeline_max_copies: 50,
+  docker_compose_dir: '',
+};
+
+describe('SettingsPage fields that were never saved', () => {
+  let saves: Record<string, unknown>[];
+
+  beforeEach(() => {
+    window.history.replaceState({}, '', '/');
+    vi.mocked(localStorage.getItem).mockReset();
+    localStorage.clear();
+    setAuthToken(null);
+    saves = [];
+    server.use(
+      http.get('/api/v1/settings/', () => HttpResponse.json(settings)),
+      http.put('/api/v1/settings/', async ({ request }) => {
+        const body = (await request.json()) as Record<string, unknown>;
+        saves.push(body);
+        return HttpResponse.json({ ...settings, ...body });
+      }),
+      http.get('/api/v1/updates/check', () =>
+        HttpResponse.json({
+          update_available: true,
+          current_version: '1.0.0',
+          latest_version: '9.9.9',
+          is_docker: true,
+          compose_dir_detected: '',
+        }),
+      ),
+      http.get('/api/v1/auth/status', () => HttpResponse.json({ auth_enabled: false, requires_setup: false })),
+    );
+  });
+
+  async function open(tab?: string) {
+    const user = userEvent.setup();
+    render(<SettingsPage />);
+    await screen.findByText('Date Format');
+    if (tab) await user.click(screen.getByText(tab));
+    // The page suppresses auto-save right after the settings load.
+    await new Promise((resolve) => setTimeout(resolve, 200));
+    return user;
+  }
+
+  it('saves the maximum copies per pipeline run', async () => {
+    await open('Workflow');
+
+    const input = await screen.findByLabelText('Max copies per run');
+    fireEvent.change(input, { target: { value: '200' } });
+
+    await waitFor(() => expect(saves.at(-1)).toMatchObject({ pipeline_max_copies: 200 }), { timeout: 3000 });
+  });
+
+  it('saves the Docker compose directory', async () => {
+    const user = await open();
+
+    const field = await screen.findByPlaceholderText('/opt/bambuddy');
+    await user.type(field, '/srv/bambuddy');
+
+    await waitFor(() => expect(saves.at(-1)).toMatchObject({ docker_compose_dir: '/srv/bambuddy' }), {
+      timeout: 3000,
+    });
+  });
+});

+ 0 - 1
frontend/src/api/client.ts

@@ -1497,7 +1497,6 @@ export interface AppSettings {
   ha_token_from_env: boolean;
   ha_env_managed: boolean;
   // File Manager / Library settings
-  library_archive_mode: 'always' | 'never' | 'ask';
   library_disk_warning_gb: number;
   // Camera view settings
   camera_view_mode: 'window' | 'embedded';

+ 27 - 5
frontend/src/components/PrintModal/index.tsx

@@ -438,14 +438,14 @@ export function PrintModal({
   }, [selectedPrinters, inventoryRemainQueries]);
 
   // Fetch archive details to get sliced_for_model
-  const { data: archiveDetails } = useQuery({
+  const { data: archiveDetails, isError: archiveDetailsFailed } = useQuery({
     queryKey: ['archive', archiveId],
     queryFn: () => api.getArchive(archiveId!),
     enabled: !!archiveId && !isLibraryFile,
   });
 
   // Fetch library file details to get sliced_for_model
-  const { data: libraryFileDetails } = useQuery({
+  const { data: libraryFileDetails, isError: libraryFileDetailsFailed } = useQuery({
     queryKey: ['library-file', libraryFileId],
     queryFn: () => api.getLibraryFile(libraryFileId!),
     enabled: isLibraryFile && !!libraryFileId,
@@ -453,6 +453,10 @@ export function PrintModal({
 
   // Get sliced_for_model from archive or library file
   const slicedForModel = archiveDetails?.sliced_for_model || libraryFileDetails?.sliced_for_model || null;
+  // Until the file's details are in, the model it was sliced for is unknown.
+  const sourceDetailsPending =
+    (!!archiveId && !isLibraryFile && !archiveDetails && !archiveDetailsFailed) ||
+    (isLibraryFile && !libraryFileDetails && !libraryFileDetailsFailed);
 
   // The archive's own saved AMS-slot pick from the slicer (see the "Save AMS
   // mapping" virtual-printer setting) — undefined for library files or
@@ -752,15 +756,33 @@ export function PrintModal({
     setSelectedPlates((prev) => (prev.size === 1 && prev.has(chosen) ? prev : new Set([chosen])));
   }, [isCrossModel, libraryFileId, candidatePlates]);
 
-  // Auto-select first printer when only one available
+  // Auto-select the default printer (Settings), else the only one available.
+  // The default is offered once per opening: deselecting it to pick another
+  // printer must not bring it back, and neither may settings that arrive after
+  // a printer was already chosen. It is not offered where the printer list is
+  // hidden (several variants, or printing to a model), nor when the file was
+  // sliced for another model.
+  const defaultPrinterOffered = useRef(false);
   useEffect(() => {
     // Skip auto-select for edit mode (already initialized from queueItem)
     if (mode === 'edit-queue-item') return;
+    if (selectedPrinters.length > 0) {
+      defaultPrinterOffered.current = true;
+      return;
+    }
     const activePrinters = printers?.filter(p => p.is_active) || [];
-    if (activePrinters.length === 1 && selectedPrinters.length === 0) {
+    const defaultId = settings?.default_printer_id;
+    const preferred =
+      defaultId != null && !isCrossModel && assignmentMode === 'printer' && !sourceDetailsPending
+        ? activePrinters.find(p => p.id === defaultId && (!slicedForModel || p.model === slicedForModel))
+        : undefined;
+    if (preferred && !defaultPrinterOffered.current) {
+      defaultPrinterOffered.current = true;
+      setSelectedPrinters([preferred.id]);
+    } else if (activePrinters.length === 1) {
       setSelectedPrinters([activePrinters[0].id]);
     }
-  }, [mode, printers, selectedPrinters.length]);
+  }, [mode, printers, selectedPrinters.length, settings?.default_printer_id, isCrossModel, assignmentMode, slicedForModel, sourceDetailsPending]);
 
   // Clear manual mappings and per-printer configs when printer or plate changes.
   // The per-plate mappings go with them: a manual override holds a global tray id,

+ 1 - 9
frontend/src/i18n/locales/de.ts

@@ -2303,12 +2303,6 @@ export default {
     saveThumbnails: 'Vorschaubilder speichern',
     captureFinishPhoto: 'Abschlussfoto aufnehmen',
     noPrintersConfigured: 'Keine Drucker konfiguriert',
-    // Archive settings
-    archiveMode: {
-      always: 'Immer Archiveintrag erstellen',
-      never: 'Nie Archiveintrag erstellen',
-      ask: 'Jedes Mal fragen',
-    },
     // Updates
     checkForUpdatesLabel: 'Nach Updates suchen',
     checkPrinterFirmware: 'Drucker-Firmware prüfen',
@@ -2847,7 +2841,7 @@ export default {
     progressInTitleDescription: 'Zeigt den Prozentsatz des aktiven Drucks und einen Fortschrittsring im Browser-Tab an.',
     // Archive
     autoArchivePrints: 'Drucke automatisch archivieren',
-    autoArchiveDescription: '3MF-Dateien automatisch speichern, wenn Drucke abgeschlossen sind',
+    autoArchiveDescription: '3MF-Dateien automatisch speichern, wenn Drucke abgeschlossen sind. Aus: Kein Drucker archiviert Drucke, die außerhalb von Bambuddy gestartet wurden.',
     saveThumbnailsDescription: 'Vorschaubilder aus 3MF-Dateien extrahieren und speichern',
     captureFinishPhotoDescription: 'Foto von der Druckerkamera aufnehmen, wenn der Druck abgeschlossen ist. Bambuddy zeichnet während des Drucks einen kurzen Zeitraffer auf, damit das Foto aus dem Moment vor dem Absenken der Druckplatte stammen kann. Die Zeitraffer-Datei bleibt erhalten, wenn du den Zeitraffer für diesen Druck aktiviert hast, andernfalls wird sie nach Aufnahme des Fotos automatisch gelöscht.',
     finishPhotoRestorePlate: 'Druckplatte für Abschlussfoto anheben',
@@ -2906,8 +2900,6 @@ export default {
     energyModeTotalDescription: 'Dashboard zeigt Gesamtenergie der Smart Plugs',
     // File Manager
     fileManager: 'Dateimanager',
-    createArchiveEntry: 'Archiveintrag beim Drucken erstellen',
-    createArchiveEntryDescription: 'Beim Drucken aus dem Dateimanager optional einen Archiveintrag erstellen',
     lowDiskSpaceWarning: 'Warnung bei wenig Speicherplatz',
     lowDiskSpaceDescription: 'Warnung anzeigen, wenn freier Speicherplatz unter diesen Schwellenwert fällt',
     // Updates

+ 1 - 9
frontend/src/i18n/locales/en.ts

@@ -2325,12 +2325,6 @@ export default {
     saveThumbnails: 'Save thumbnails',
     captureFinishPhoto: 'Capture finish photo',
     noPrintersConfigured: 'No printers configured',
-    // Archive settings
-    archiveMode: {
-      always: 'Always create archive entry',
-      never: 'Never create archive entry',
-      ask: 'Ask each time',
-    },
     // Updates
     checkForUpdatesLabel: 'Check for updates',
     checkPrinterFirmware: 'Check printer firmware',
@@ -2869,7 +2863,7 @@ export default {
     progressInTitleDescription: 'Show the active print\'s percentage and a progress ring in the browser tab.',
     // Archive
     autoArchivePrints: 'Auto-archive prints',
-    autoArchiveDescription: 'Automatically save 3MF files when prints complete',
+    autoArchiveDescription: 'Automatically save 3MF files when prints complete. When off, no printer archives prints started outside Bambuddy.',
     saveThumbnailsDescription: 'Extract and save preview images from 3MF files',
     captureFinishPhotoDescription: 'Take a photo from printer camera when print completes. Bambuddy records a brief timelapse during the print so the photo can be sourced from the moment before the bed drops; the timelapse file is kept if you enabled timelapse for this print, otherwise it is deleted automatically after the photo is captured.',
     finishPhotoRestorePlate: 'Restore plate for finish photo',
@@ -2928,8 +2922,6 @@ export default {
     energyModeTotalDescription: 'Dashboard shows lifetime energy from smart plugs',
     // File Manager
     fileManager: 'File Manager',
-    createArchiveEntry: 'Create Archive Entry When Printing',
-    createArchiveEntryDescription: 'When printing from File Manager, optionally create an archive entry',
     lowDiskSpaceWarning: 'Low Disk Space Warning',
     lowDiskSpaceDescription: 'Show warning when free disk space falls below this threshold',
     // Updates

+ 1 - 9
frontend/src/i18n/locales/es.ts

@@ -2306,12 +2306,6 @@ export default {
     saveThumbnails: 'Guardar miniaturas',
     captureFinishPhoto: 'Capturar foto de finalización',
     noPrintersConfigured: 'No hay impresoras configuradas',
-    // Archive settings
-    archiveMode: {
-      always: 'Crear siempre una entrada de archivo',
-      never: 'No crear nunca una entrada de archivo',
-      ask: 'Preguntar cada vez',
-    },
     // Updates
     checkForUpdatesLabel: 'Buscar actualizaciones',
     checkPrinterFirmware: 'Comprobar el firmware de la impresora',
@@ -2849,7 +2843,7 @@ export default {
     progressInTitleDescription: 'Muestra el porcentaje de la impresión activa y un anillo de progreso en la pestaña del navegador.',
     // Archive
     autoArchivePrints: 'Archivar impresiones automáticamente',
-    autoArchiveDescription: 'Guardar automáticamente los archivos 3MF cuando se completan las impresiones',
+    autoArchiveDescription: 'Guardar automáticamente los archivos 3MF cuando se completan las impresiones. Desactivado: ninguna impresora archiva las impresiones iniciadas fuera de Bambuddy.',
     saveThumbnailsDescription: 'Extraer y guardar imágenes de vista previa de los archivos 3MF',
     captureFinishPhotoDescription: 'Tomar una foto desde la cámara de la impresora cuando se completa la impresión. Bambuddy graba un breve timelapse durante la impresión para que la foto pueda obtenerse del momento previo al descenso de la cama; el archivo del timelapse se conserva si activaste el timelapse para esta impresión, de lo contrario se elimina automáticamente tras capturar la foto.',
     finishPhotoRestorePlate: 'Elevar la cama para la foto final',
@@ -2908,8 +2902,6 @@ export default {
     energyModeTotalDescription: 'El panel muestra la energía total de los enchufes inteligentes',
     // File Manager
     fileManager: 'Gestor de archivos',
-    createArchiveEntry: 'Crear una entrada de archivo al imprimir',
-    createArchiveEntryDescription: 'Al imprimir desde el gestor de archivos, crear opcionalmente una entrada de archivo',
     lowDiskSpaceWarning: 'Advertencia de poco espacio en disco',
     lowDiskSpaceDescription: 'Mostrar una advertencia cuando el espacio libre en disco caiga por debajo de este umbral',
     // Updates

+ 1 - 9
frontend/src/i18n/locales/fr.ts

@@ -2258,12 +2258,6 @@ export default {
     saveThumbnails: 'Enregistrer les vignettes',
     captureFinishPhoto: 'Prendre une photo à la fin',
     noPrintersConfigured: 'Aucune imprimante configurée',
-    // Archive settings
-    archiveMode: {
-      always: 'Toujours créer une archive',
-      never: 'Ne jamais créer d\'archive',
-      ask: 'Demander à chaque fois',
-    },
     // Updates
     checkForUpdatesLabel: 'Vérifier les mises à jour',
     checkPrinterFirmware: 'Vérifier le firmware imprimante',
@@ -2800,7 +2794,7 @@ export default {
     progressInTitle: 'Progression dans l\'onglet',
     progressInTitleDescription: 'Affiche le pourcentage de l\'impression en cours et un anneau de progression dans l\'onglet du navigateur.',
     autoArchivePrints: 'Archiver automatiquement les impressions',
-    autoArchiveDescription: 'Sauvegarder automatiquement les fichiers 3MF à la fin des impressions',
+    autoArchiveDescription: 'Sauvegarder automatiquement les fichiers 3MF à la fin des impressions. Désactivé : aucune imprimante n\'archive les impressions lancées en dehors de Bambuddy.',
     saveThumbnailsDescription: 'Extraire et sauvegarder les images d\'aperçu des fichiers 3MF',
     captureFinishPhotoDescription: 'Prendre une photo avec la caméra de l\'imprimante à la fin de l\'impression. Bambuddy enregistre un court timelapse pendant l\'impression afin que la photo puisse provenir du moment précédant l\'abaissement du plateau ; le fichier du timelapse est conservé si vous avez activé le timelapse pour cette impression, sinon il est supprimé automatiquement après la capture de la photo.',
     finishPhotoRestorePlate: 'Remonter le plateau pour la photo finale',
@@ -2856,8 +2850,6 @@ export default {
     energyModePrintDescription: 'Le tableau de bord affiche la somme de l\'énergie utilisée pendant les impressions',
     energyModeTotalDescription: 'Le tableau de bord affiche l\'énergie totale des prises connectées',
     fileManager: 'Gestionnaire de fichiers',
-    createArchiveEntry: 'Créer une entrée d\'archive lors de l\'impression',
-    createArchiveEntryDescription: 'Lors de l\'impression depuis le gestionnaire de fichiers, créer optionnellement une entrée d\'archive',
     lowDiskSpaceWarning: 'Avertissement espace disque faible',
     lowDiskSpaceDescription: 'Afficher un avertissement lorsque l\'espace disque libre descend sous ce seuil',
     printerFirmware: 'Firmware imprimante',

+ 1 - 9
frontend/src/i18n/locales/it.ts

@@ -2258,12 +2258,6 @@ export default {
     saveThumbnails: 'Salva miniature',
     captureFinishPhoto: 'Acquisisci foto finale',
     noPrintersConfigured: 'Nessuna stampante configurata',
-    // Archive settings
-    archiveMode: {
-      always: 'Crea sempre voce archivio',
-      never: 'Non creare mai voce archivio',
-      ask: 'Chiedi ogni volta',
-    },
     // Updates
     checkForUpdatesLabel: 'Controlla aggiornamenti',
     checkPrinterFirmware: 'Controlla firmware stampante',
@@ -2799,7 +2793,7 @@ export default {
     progressInTitle: 'Avanzamento nella scheda',
     progressInTitleDescription: 'Mostra la percentuale della stampa attiva e un anello di avanzamento nella scheda del browser.',
     autoArchivePrints: 'Archiviazione automatica stampe',
-    autoArchiveDescription: 'Salva automaticamente i file 3MF al completamento delle stampe',
+    autoArchiveDescription: 'Salva automaticamente i file 3MF al completamento delle stampe. Disattivato: nessuna stampante archivia le stampe avviate al di fuori di Bambuddy.',
     saveThumbnailsDescription: 'Estrai e salva le immagini di anteprima dai file 3MF',
     captureFinishPhotoDescription: 'Scatta una foto dalla fotocamera della stampante al completamento della stampa. Bambuddy registra un breve timelapse durante la stampa in modo che la foto possa essere ricavata dal momento precedente all\'abbassamento del piatto; il file del timelapse viene mantenuto se hai abilitato il timelapse per questa stampa, altrimenti viene eliminato automaticamente dopo l\'acquisizione della foto.',
     finishPhotoRestorePlate: 'Solleva il piatto per la foto finale',
@@ -2855,8 +2849,6 @@ export default {
     energyModePrintDescription: 'La dashboard mostra la somma dell\'energia usata durante le stampe',
     energyModeTotalDescription: 'La dashboard mostra l\'energia totale dalle prese smart',
     fileManager: 'Gestore file',
-    createArchiveEntry: 'Crea voce archivio durante la stampa',
-    createArchiveEntryDescription: 'Quando si stampa dal gestore file, crea opzionalmente una voce di archivio',
     lowDiskSpaceWarning: 'Avviso spazio disco insufficiente',
     lowDiskSpaceDescription: 'Mostra avviso quando lo spazio disco scende sotto questa soglia',
     printerFirmware: 'Firmware stampante',

+ 1 - 9
frontend/src/i18n/locales/ja.ts

@@ -2302,12 +2302,6 @@ export default {
     saveThumbnails: 'サムネイルを保存',
     captureFinishPhoto: '完了写真を撮影',
     noPrintersConfigured: 'プリンターが設定されていません',
-    // Archive settings
-    archiveMode: {
-      always: '常にアーカイブを作成',
-      never: 'アーカイブを作成しない',
-      ask: '毎回確認',
-    },
     // Updates
     checkForUpdatesLabel: 'アップデートを確認',
     checkPrinterFirmware: 'プリンターファームウェアの確認',
@@ -2846,7 +2840,7 @@ export default {
     progressInTitleDescription: 'ブラウザのタブに進行中の印刷の進捗率と進捗リングを表示します。',
     // Archive
     autoArchivePrints: '印刷を自動アーカイブ',
-    autoArchiveDescription: '印刷完了時に3MFファイルを自動保存',
+    autoArchiveDescription: '印刷完了時に3MFファイルを自動保存。オフの場合、Bambuddy 以外から開始された印刷はどのプリンターでもアーカイブされません。',
     saveThumbnailsDescription: '3MFファイルからプレビュー画像を抽出して保存',
     captureFinishPhotoDescription: '印刷完了時にプリンターカメラから写真を撮影します。Bambuddy は印刷中に短いタイムラプスを記録し、ベッドが下がる前の瞬間から写真を取得できるようにします。この印刷でタイムラプスを有効にしていた場合はタイムラプスファイルが保存され、それ以外の場合は写真の取得後に自動的に削除されます。',
     finishPhotoRestorePlate: '完了写真のためにプレートを戻す',
@@ -2905,8 +2899,6 @@ export default {
     energyModeTotalDescription: 'ダッシュボードにスマートプラグの累計エネルギーを表示',
     // File Manager
     fileManager: 'ファイルマネージャー',
-    createArchiveEntry: '印刷時にアーカイブエントリを作成',
-    createArchiveEntryDescription: 'ファイルマネージャーから印刷時に、オプションでアーカイブエントリを作成',
     lowDiskSpaceWarning: 'ディスク容量不足の警告',
     lowDiskSpaceDescription: '空きディスク容量がこのしきい値を下回った場合に警告を表示',
     // Updates

+ 1 - 8
frontend/src/i18n/locales/ko.ts

@@ -2202,11 +2202,6 @@ export default {
     saveThumbnails: '썸네일 저장',
     captureFinishPhoto: '완료 사진 캡처',
     noPrintersConfigured: '설정된 프린터 없음',
-    archiveMode: {
-      always: '항상 아카이브 항목 생성',
-      never: '아카이브 항목 생성 안 함',
-      ask: '매번 묻기'
-    },
     checkForUpdatesLabel: '업데이트 확인',
     checkPrinterFirmware: '프린터 펌웨어 확인',
     includeBetaUpdates: '베타 버전 포함',
@@ -2710,7 +2705,7 @@ export default {
     progressInTitle: '탭에 인쇄 진행률 표시',
     progressInTitleDescription: '브라우저 탭에 진행 중인 인쇄의 백분율과 진행 링을 표시합니다.',
     autoArchivePrints: '인쇄 자동 아카이브',
-    autoArchiveDescription: '인쇄 완료 시 3MF 파일 자동 저장',
+    autoArchiveDescription: '인쇄 완료 시 3MF 파일 자동 저장. 끄면 Bambuddy 외부에서 시작된 인쇄는 어떤 프린터에서도 보관되지 않습니다.',
     saveThumbnailsDescription: '3MF 파일에서 미리보기 이미지 추출 및 저장',
     captureFinishPhotoDescription: '인쇄 완료 시 프린터 카메라로 사진 촬영. Bambuddy는 인쇄 중 짧은 타임랩스를 기록하여 베드가 내려가기 전 순간에서 사진을 가져올 수 있도록 합니다. 이 인쇄에 대해 타임랩스를 활성화한 경우 타임랩스 파일이 보관되며, 그렇지 않으면 사진 촬영 후 자동으로 삭제됩니다.',
     finishPhotoRestorePlate: '완료 사진을 위해 베드 올리기',
@@ -2766,8 +2761,6 @@ export default {
     energyModePrintDescription: '대시보드가 인쇄 중 사용된 에너지 합계를 표시',
     energyModeTotalDescription: '대시보드가 스마트 플러그의 전체 에너지를 표시',
     fileManager: '파일 관리자',
-    createArchiveEntry: '인쇄 시 아카이브 항목 생성',
-    createArchiveEntryDescription: '파일 관리자에서 인쇄할 때 선택적으로 아카이브 항목 생성',
     lowDiskSpaceWarning: '디스크 공간 부족 경고',
     lowDiskSpaceDescription: '여유 디스크 공간이 이 임계값 아래로 떨어지면 경고 표시',
     printerFirmware: '프린터 펌웨어',

+ 1 - 9
frontend/src/i18n/locales/nl.ts

@@ -2323,12 +2323,6 @@ export default {
     saveThumbnails: 'Miniaturen opslaan',
     captureFinishPhoto: 'Eindfoto vastleggen',
     noPrintersConfigured: 'Geen printers geconfigureerd',
-    // Archive settings
-    archiveMode: {
-      always: 'Altijd archiefitem maken',
-      never: 'Nooit archiefitem maken',
-      ask: 'Elke keer vragen',
-    },
     // Updates
     checkForUpdatesLabel: 'Controleren op updates',
     checkPrinterFirmware: 'Printerfirmware controleren',
@@ -2867,7 +2861,7 @@ export default {
     progressInTitleDescription: 'Toon het percentage van de actieve afdruk en een voortgangsring in het browsertabblad.',
     // Archive
     autoArchivePrints: 'Afdrukken automatisch archiveren',
-    autoArchiveDescription: '3MF-bestanden automatisch opslaan wanneer afdrukken zijn voltooid',
+    autoArchiveDescription: '3MF-bestanden automatisch opslaan wanneer afdrukken zijn voltooid. Uit: geen enkele printer archiveert afdrukken die buiten Bambuddy zijn gestart.',
     saveThumbnailsDescription: 'Voorbeeldafbeeldingen uit 3MF-bestanden extraheren en opslaan',
     captureFinishPhotoDescription: 'Maak een foto met de printercamera wanneer de afdruk is voltooid. Bambuddy neemt tijdens de afdruk een korte timelapse op zodat de foto kan worden genomen van het moment voordat het bed zakt; het timelapsebestand blijft behouden als je timelapse voor deze afdruk hebt ingeschakeld, anders wordt het automatisch verwijderd nadat de foto is vastgelegd.',
     finishPhotoRestorePlate: 'Plaat herstellen voor eindfoto',
@@ -2926,8 +2920,6 @@ export default {
     energyModeTotalDescription: 'Dashboard toont het totale energieverbruik van slimme stekkers over de hele levensduur',
     // File Manager
     fileManager: 'Bestandsbeheer',
-    createArchiveEntry: 'Archiefitem maken bij afdrukken',
-    createArchiveEntryDescription: 'Bij afdrukken vanuit Bestandsbeheer optioneel een archiefitem maken',
     lowDiskSpaceWarning: 'Waarschuwing weinig schijfruimte',
     lowDiskSpaceDescription: 'Waarschuwing tonen wanneer vrije schijfruimte onder deze drempel komt',
     // Updates

+ 1 - 9
frontend/src/i18n/locales/pt-BR.ts

@@ -2258,12 +2258,6 @@ export default {
     saveThumbnails: 'Salvar miniaturas',
     captureFinishPhoto: 'Capturar foto de conclusão',
     noPrintersConfigured: 'Nenhuma impressora configurada',
-    // Archive settings
-    archiveMode: {
-      always: 'Sempre criar entrada de arquivo',
-      never: 'Nunca criar entrada de arquivo',
-      ask: 'Perguntar a cada vez',
-    },
     // Updates
     checkForUpdatesLabel: 'Verificar atualizações',
     checkPrinterFirmware: 'Verificar firmware da impressora',
@@ -2799,7 +2793,7 @@ export default {
     progressInTitle: 'Progresso na aba',
     progressInTitleDescription: 'Mostra a porcentagem da impressão ativa e um anel de progresso na aba do navegador.',
     autoArchivePrints: 'Arquivar impressões automaticamente',
-    autoArchiveDescription: 'Salvar automaticamente arquivos 3MF quando impressões forem concluídas',
+    autoArchiveDescription: 'Salvar automaticamente arquivos 3MF quando impressões forem concluídas. Desativado: nenhuma impressora arquiva impressões iniciadas fora do Bambuddy.',
     saveThumbnailsDescription: 'Extrair e salvar imagens de pré-visualização dos arquivos 3MF',
     captureFinishPhotoDescription: 'Tirar foto da câmera da impressora quando a impressão for concluída. Bambuddy grava um timelapse curto durante a impressão para que a foto possa ser obtida do momento antes da mesa descer; o arquivo do timelapse é mantido se você habilitou o timelapse para esta impressão, caso contrário ele é excluído automaticamente após a captura da foto.',
     finishPhotoRestorePlate: 'Elevar a mesa para a foto final',
@@ -2855,8 +2849,6 @@ export default {
     energyModePrintDescription: 'O painel mostra a soma da energia usada durante as impressões',
     energyModeTotalDescription: 'O painel mostra a energia total dos plugues inteligentes',
     fileManager: 'Gerenciador de arquivos',
-    createArchiveEntry: 'Criar entrada de arquivo ao imprimir',
-    createArchiveEntryDescription: 'Ao imprimir pelo gerenciador de arquivos, criar opcionalmente uma entrada de arquivo',
     lowDiskSpaceWarning: 'Aviso de pouco espaço em disco',
     lowDiskSpaceDescription: 'Mostrar aviso quando o espaço livre em disco ficar abaixo deste limite',
     printerFirmware: 'Firmware da impressora',

+ 1 - 8
frontend/src/i18n/locales/ru.ts

@@ -2199,11 +2199,6 @@ export default {
     saveThumbnails: "Сохранять миниатюры",
     captureFinishPhoto: "Снимок после завершения",
     noPrintersConfigured: "Принтеры не настроены",
-    archiveMode: {
-      always: "Всегда создавать запись в архиве",
-      never: "Не создавать запись в архиве",
-      ask: "Спрашивать каждый раз",
-    },
     checkForUpdatesLabel: "Проверять обновления",
     checkPrinterFirmware: "Проверять прошивку принтера",
     includeBetaUpdates: "Включать бета-версии",
@@ -2714,7 +2709,7 @@ export default {
     progressInTitle: "Прогресс во вкладке",
     progressInTitleDescription: "Показывает процент текущей печати и кольцо прогресса во вкладке браузера.",
     autoArchivePrints: "Автоматически архивировать печать",
-    autoArchiveDescription: "Автоматически сохранять 3MF после завершения печати",
+    autoArchiveDescription: "Автоматически сохранять 3MF после завершения печати. Если выключено, ни один принтер не архивирует печать, запущенную не из Bambuddy.",
     saveThumbnailsDescription: "Извлекать и сохранять изображения предпросмотра из 3MF",
     captureFinishPhotoDescription: "Сделать снимок камерой принтера после завершения печати. Во время печати Bambuddy записывает короткий таймлапс, чтобы получить кадр до опускания стола. Если таймлапс был включён для задания, файл сохранится; иначе после получения снимка он будет автоматически удалён.",
     finishPhotoRestorePlate: "Поднимать стол для финального снимка",
@@ -2770,8 +2765,6 @@ export default {
     energyModePrintDescription: "На панели показывается сумма энергии, израсходованной во время печати",
     energyModeTotalDescription: "На панели показывается полный расход энергии по данным умных розеток",
     fileManager: "Файловый менеджер",
-    createArchiveEntry: "Создавать запись в архиве при печати",
-    createArchiveEntryDescription: "При запуске печати из файлового менеджера при необходимости создавать запись в архиве",
     lowDiskSpaceWarning: "Предупреждение о нехватке места",
     lowDiskSpaceDescription: "Показывать предупреждение, когда свободного места остаётся меньше указанного порога",
     printerFirmware: "Прошивка принтера",

+ 1 - 9
frontend/src/i18n/locales/sv.ts

@@ -2322,12 +2322,6 @@ export default {
     saveThumbnails: 'Spara minibilder',
     captureFinishPhoto: 'Ta färdig-foto',
     noPrintersConfigured: 'Inga skrivare konfigurerade',
-    // Archive settings
-    archiveMode: {
-      always: 'Skapa alltid arkivpost',
-      never: 'Skapa aldrig arkivpost',
-      ask: 'Fråga varje gång',
-    },
     // Updates
     checkForUpdatesLabel: 'Sök efter uppdateringar',
     checkPrinterFirmware: 'Kontrollera skrivarens firmware',
@@ -2866,7 +2860,7 @@ export default {
     progressInTitleDescription: 'Visa den aktiva utskriftens procentförlopp och en ring i webbläsarfliken.',
     // Archive
     autoArchivePrints: 'Auto-arkivera utskrifter',
-    autoArchiveDescription: 'Spara automatiskt 3MF-filer när utskrifter slutförs',
+    autoArchiveDescription: 'Spara automatiskt 3MF-filer när utskrifter slutförs. Av: ingen skrivare arkiverar utskrifter som startats utanför Bambuddy.',
     saveThumbnailsDescription: 'Extrahera och spara förhandsvisningsbilder från 3MF-filer',
     captureFinishPhotoDescription: 'Ta ett foto från skrivarkameran när utskriften slutförs. Bambuddy spelar in en kort timelapse under utskriften så att fotot kan hämtas från ögonblicket innan byggplattan sänks; timelapse-filen behålls om du aktiverat timelapse för denna utskrift, annars tas den automatiskt bort efter att fotot tagits.',
     finishPhotoRestorePlate: 'Återställ platta för foto',
@@ -2925,8 +2919,6 @@ export default {
     energyModeTotalDescription: 'Instrumentpanelen visar livstidsenergi från smarta kontakter',
     // File Manager
     fileManager: 'Filhanterare',
-    createArchiveEntry: 'Skapa arkivpost vid utskrift',
-    createArchiveEntryDescription: 'När du skriver ut från Filhanteraren, skapa en arkivpost',
     lowDiskSpaceWarning: 'Varning för lågt diskutrymme',
     lowDiskSpaceDescription: 'Visa en varning när tillgängligt diskutrymme är under detta tröskelvärde',
     // Updates

+ 1 - 8
frontend/src/i18n/locales/tr.ts

@@ -2307,11 +2307,6 @@ export default {
     captureFinishPhoto: 'Bitiş fotoğrafı çek',
     noPrintersConfigured: 'Yapılandırılmış yazıcı yok',
     // Arşiv ayarları
-    archiveMode: {
-      always: 'Her zaman arşiv girişi oluştur',
-      never: 'Asla arşiv girişi oluşturma',
-      ask: 'Her seferinde sor',
-    },
     // Güncellemeler
     checkForUpdatesLabel: 'Güncellemeleri kontrol et',
     checkPrinterFirmware: 'Yazıcı firmware\'ini kontrol et',
@@ -2849,7 +2844,7 @@ export default {
     progressInTitleDescription: 'Tarayıcı sekmesinde etkin baskının yüzdesini ve bir ilerleme halkası gösterir.',
     // Arşiv
     autoArchivePrints: 'Baskıları otomatik arşivle',
-    autoArchiveDescription: 'Baskılar tamamlandığında 3MF dosyalarını otomatik olarak kaydet',
+    autoArchiveDescription: 'Baskılar tamamlandığında 3MF dosyalarını otomatik olarak kaydet. Kapalıyken hiçbir yazıcı Bambuddy dışında başlatılan baskıları arşivlemez.',
     saveThumbnailsDescription: '3MF dosyalarından önizleme görüntülerini çıkar ve kaydet',
     captureFinishPhotoDescription: 'Baskı tamamlandığında yazıcı kamerasından bir fotoğraf çek. Bambuddy, baskı sırasında kısa bir zaman atlamalı kayıt yapar, böylece fotoğraf tabla inmeden önceki andan alınabilir. Bu baskı için zaman atlamalı kaydı etkinleştirdiyseniz dosya saklanır, aksi takdirde fotoğraf çekildikten sonra otomatik olarak silinir.',
     finishPhotoRestorePlate: 'Bitiş fotoğrafı için tablayı yükselt',
@@ -2908,8 +2903,6 @@ export default {
     energyModeTotalDescription: 'Gösterge paneli, akıllı prizlerden ömür boyu enerjiyi gösterir',
     // Dosya Yöneticisi
     fileManager: 'Dosya Yöneticisi',
-    createArchiveEntry: 'Yazdırırken Arşiv Girişi Oluştur',
-    createArchiveEntryDescription: 'Dosya Yöneticisinden yazdırırken isteğe bağlı olarak bir arşiv girişi oluştur',
     lowDiskSpaceWarning: 'Düşük Disk Alanı Uyarısı',
     lowDiskSpaceDescription: 'Boş disk alanı bu eşiğin altına düştüğünde uyarı göster',
     // Güncellemeler

+ 1 - 9
frontend/src/i18n/locales/uk.ts

@@ -2321,12 +2321,6 @@ export default {
     saveThumbnails: "Зберегти мініатюри",
     captureFinishPhoto: "Зробити фото після завершення",
     noPrintersConfigured: "Принтери не налаштовані",
-    // Archive settings
-    archiveMode: {
-      always: "Завжди створювати запис архіву",
-      never: "Ніколи не створювати запис архіву",
-      ask: "Запитувати щоразу",
-    },
     // Updates
     checkForUpdatesLabel: "Перевірити наявність оновлень",
     checkPrinterFirmware: "Перевірити прошивку принтера",
@@ -2864,7 +2858,7 @@ export default {
     progressInTitleDescription: "Показує відсоток активного друку та кільце прогресу на вкладці браузера.",
     // Archive
     autoArchivePrints: "Автоматично архівувати друки",
-    autoArchiveDescription: "Автоматично зберігати файли 3MF після завершення друку",
+    autoArchiveDescription: "Автоматично зберігати файли 3MF після завершення друку. Якщо вимкнено, жоден принтер не архівує друк, запущений не з Bambuddy.",
     saveThumbnailsDescription: "Витягніть і збережіть зображення попереднього перегляду з файлів 3MF.",
     captureFinishPhotoDescription: "Зробіть фотографію з камери принтера після завершення друку. Bambuddy записує короткий проміжок часу під час друку, щоб фотографію можна було отримати з моменту, коли стіл опускається; файл уповільненої зйомки зберігається, якщо ви ввімкнули уповільнену зйомку для цього друку, інакше він автоматично видаляється після зйомки фотографії.",
     finishPhotoRestorePlate: "Піднімати стіл для фінального знімка",
@@ -2923,8 +2917,6 @@ export default {
     energyModeTotalDescription: "На приладовій панелі показано енергію від розумних розеток протягом усього терміну служби",
     // File Manager
     fileManager: "Менеджер файлів",
-    createArchiveEntry: "Створити архівний запис під час друку",
-    createArchiveEntryDescription: "Під час друку з диспетчера файлів можна додатково створити запис архіву",
     lowDiskSpaceWarning: "Попередження про брак місця на диску",
     lowDiskSpaceDescription: "Показувати попередження, коли вільний простір на диску стає нижче цього порогу",
     // Updates

+ 1 - 9
frontend/src/i18n/locales/zh-CN.ts

@@ -2303,12 +2303,6 @@ export default {
     saveThumbnails: '保存缩略图',
     captureFinishPhoto: '拍摄完成照片',
     noPrintersConfigured: '未配置打印机',
-    // Archive settings
-    archiveMode: {
-      always: '始终创建归档条目',
-      never: '从不创建归档条目',
-      ask: '每次询问',
-    },
     // Updates
     checkForUpdatesLabel: '检查更新',
     checkPrinterFirmware: '检查打印机固件',
@@ -2844,7 +2838,7 @@ export default {
     progressInTitle: '在标签页显示打印进度',
     progressInTitleDescription: '在浏览器标签页中显示当前打印的百分比和进度环。',
     autoArchivePrints: '自动归档打印',
-    autoArchiveDescription: '打印完成时自动保存3MF文件',
+    autoArchiveDescription: '打印完成时自动保存3MF文件。关闭时,所有打印机都不会归档在 Bambuddy 之外启动的打印。',
     saveThumbnailsDescription: '从3MF文件中提取并保存预览图像',
     captureFinishPhotoDescription: '打印完成时从打印机摄像头拍照。Bambuddy 会在打印期间录制一段短延时摄影,以便从热床下降前的瞬间获取照片;如果您为本次打印启用了延时摄影,文件将保留,否则会在拍照完成后自动删除。',
     finishPhotoRestorePlate: '为完成照片抬升热床',
@@ -2900,8 +2894,6 @@ export default {
     energyModePrintDescription: '仪表板显示打印期间使用的能源总和',
     energyModeTotalDescription: '仪表板显示智能插座的累计能源',
     fileManager: '文件管理器',
-    createArchiveEntry: '打印时创建归档条目',
-    createArchiveEntryDescription: '从文件管理器打印时,可选择创建归档条目',
     lowDiskSpaceWarning: '磁盘空间不足警告',
     lowDiskSpaceDescription: '当可用磁盘空间低于此阈值时显示警告',
     printerFirmware: '打印机固件',

+ 1 - 9
frontend/src/i18n/locales/zh-TW.ts

@@ -2303,12 +2303,6 @@ export default {
     saveThumbnails: '儲存縮圖',
     captureFinishPhoto: '拍攝完成照片',
     noPrintersConfigured: '未設定印表機',
-    // Archive settings
-    archiveMode: {
-      always: '始終建立歸檔條目',
-      never: '從不建立歸檔條目',
-      ask: '每次詢問',
-    },
     // Updates
     checkForUpdatesLabel: '檢查更新',
     checkPrinterFirmware: '檢查印表機韌體',
@@ -2844,7 +2838,7 @@ export default {
     progressInTitle: '在分頁顯示列印進度',
     progressInTitleDescription: '在瀏覽器分頁中顯示目前列印的百分比和進度環。',
     autoArchivePrints: '自動歸檔列印',
-    autoArchiveDescription: '列印完成時自動儲存3MF檔案',
+    autoArchiveDescription: '列印完成時自動儲存3MF檔案。關閉時,所有印表機都不會歸檔在 Bambuddy 之外啟動的列印。',
     saveThumbnailsDescription: '從3MF檔案中提取並儲存預覽影像',
     captureFinishPhotoDescription: '列印完成時從印表機攝影機拍照。Bambuddy 會在列印期間錄製一段短縮時攝影,以便從熱床下降前的瞬間取得照片;如果您為本次列印啟用了縮時攝影,檔案將保留,否則會在拍照完成後自動刪除。',
     finishPhotoRestorePlate: '為完成照片抬升熱床',
@@ -2900,8 +2894,6 @@ export default {
     energyModePrintDescription: '儀表板顯示列印期間使用的能源總和',
     energyModeTotalDescription: '儀表板顯示智慧插座的累計能源',
     fileManager: '檔案管理器',
-    createArchiveEntry: '列印時建立歸檔條目',
-    createArchiveEntryDescription: '從檔案管理器列印時,可選擇建立歸檔條目',
     lowDiskSpaceWarning: '磁碟空間不足警告',
     lowDiskSpaceDescription: '當可用磁碟空間低於此閾值時顯示警告',
     printerFirmware: '印表機韌體',

+ 4 - 21
frontend/src/pages/SettingsPage.tsx

@@ -1284,6 +1284,8 @@ export function SettingsPage() {
       baseline.date_format !== localSettings.date_format ||
       baseline.time_format !== localSettings.time_format ||
       baseline.default_printer_id !== localSettings.default_printer_id ||
+      (baseline.pipeline_max_copies ?? 50) !== (localSettings.pipeline_max_copies ?? 50) ||
+      (baseline.docker_compose_dir ?? '') !== (localSettings.docker_compose_dir ?? '') ||
       baseline.ftp_retry_enabled !== localSettings.ftp_retry_enabled ||
       baseline.ftp_retry_count !== localSettings.ftp_retry_count ||
       baseline.ftp_retry_delay !== localSettings.ftp_retry_delay ||
@@ -1299,7 +1301,6 @@ export function SettingsPage() {
       baseline.ha_enabled !== localSettings.ha_enabled ||
       baseline.ha_url !== localSettings.ha_url ||
       baseline.ha_token !== localSettings.ha_token ||
-      (baseline.library_archive_mode ?? 'ask') !== (localSettings.library_archive_mode ?? 'ask') ||
       Number(baseline.library_disk_warning_gb ?? 5) !== Number(localSettings.library_disk_warning_gb ?? 5) ||
       (baseline.camera_light_mode ?? 'off') !== (localSettings.camera_light_mode ?? 'off') ||
       Number(baseline.camera_light_delay ?? 2) !== Number(localSettings.camera_light_delay ?? 2) ||
@@ -1412,6 +1413,8 @@ export function SettingsPage() {
         date_format: localSettings.date_format,
         time_format: localSettings.time_format,
         default_printer_id: localSettings.default_printer_id,
+        pipeline_max_copies: localSettings.pipeline_max_copies ?? 50,
+        docker_compose_dir: localSettings.docker_compose_dir ?? '',
         ftp_retry_enabled: localSettings.ftp_retry_enabled,
         ftp_retry_count: localSettings.ftp_retry_count,
         ftp_retry_delay: localSettings.ftp_retry_delay,
@@ -1427,7 +1430,6 @@ export function SettingsPage() {
         ha_enabled: localSettings.ha_enabled,
         ha_url: localSettings.ha_url,
         ha_token: localSettings.ha_token,
-        library_archive_mode: localSettings.library_archive_mode,
         library_disk_warning_gb: localSettings.library_disk_warning_gb,
         camera_light_mode: localSettings.camera_light_mode,
         camera_light_delay: localSettings.camera_light_delay,
@@ -2529,25 +2531,6 @@ export function SettingsPage() {
               </h2>
             </CardHeader>
             <CardContent className="space-y-3">
-              {/* Archive Mode */}
-              <div>
-                <label className="block text-sm text-bambu-gray mb-1">
-                  {t('settings.createArchiveEntry')}
-                </label>
-                <select
-                  value={localSettings.library_archive_mode ?? 'ask'}
-                  onChange={(e) => updateSetting('library_archive_mode', e.target.value as 'always' | 'never' | 'ask')}
-                  className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
-                >
-                  <option value="always">{t('settings.archiveMode.always')}</option>
-                  <option value="never">{t('settings.archiveMode.never')}</option>
-                  <option value="ask">{t('settings.archiveMode.ask')}</option>
-                </select>
-                <p className="text-xs text-bambu-gray mt-1">
-                  {t('settings.createArchiveEntryDescription')}
-                </p>
-              </div>
-
               {/* Disk Space Warning Threshold */}
               <div>
                 <label className="block text-sm text-bambu-gray mb-1">

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/AdditionalSection-BCcQRTKQ.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/AmsUnitCard-CUx9p2v6.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ArchivesPage-JilnP9pX.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/AssignToAmsModal-BuslsYWw.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/CamWallPage-CO3ZUqb2.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/CameraDiagnoseModal-jUf5wCOp.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/CameraPage-DD-_PRay.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/CameraWall-CIFG_rbT.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ColumnConfigModal-CRFZx03j.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ConfigureAmsSlotModal-B3rNLxCc.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ConnectAuthorizePage-BAwHtJ_h.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ContextMenu-iQaDMRMq.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ExternalLinkPage-DVqaak4R.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 1
static/assets/FileManagerPage-Ce1ZSbck.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/FileUploadModal-xH-xBAwl.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/FinancePage-BTmCNBE2.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/GCodeViewerPage-SjE-zlyZ.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/GroupEditPage-axw65b8h.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ImagePreviewModal-BCYgEvOk.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/InventoryPage-CPymhcaz.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/LibraryTrashPage-DdeKmJHy.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/LoginPage-BUrz_-2s.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/MaintenancePage-CUGCBK_D.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ModelSourcesPage-DyfDKRUf.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/NotificationsPage-BbRbmhQL.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 1
static/assets/PdfPreviewModal-D_laxCuF.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/PhotoGalleryModal-oEeo5CR8.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/PrinterLocationsPage-BMz8OK0N.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/PrintersPage-wKRUwJUL.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ProfilesPage-EEBkpJSv.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ProjectDetailPage-C_9Sxv4G.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ProjectsPage-BRaLy0mi.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/QueuePage-CIGN-riR.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/RichTextEditor-DHWuQypn.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/SettingsPage-DAd2Rbkn.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/SetupPage-BgnTYgfd.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/SliceModal-CXRyOQkN.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/SpoolBuddyAmsPage-C7VK2rbC.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/SpoolBuddyCalibrationPage-ACVrbLeC.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/SpoolBuddyDashboard-welJGIru.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-DHQ2LH3P.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 1
static/assets/SpoolBuddySettingsPage-CNHpwgIw.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/SpoolBuddyWriteTagPage-CQdfdrri.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/SpreadsheetPreviewModal-RcpOjOu2.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/StatsPage-B6LXwkWQ.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/StreamOverlayPage-BKX0YUXG.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/SystemInfoPage-BCFVOOlb.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/de-D0GtQVkj.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/de-mgb0RyZq.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/es-ByQJdCqZ.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/es-rm5cH5pl.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/fr-CNvpzB4j.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/fr-Et2eyvIH.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/index-BlsvkMcT.js


+ 1 - 1
static/assets/inventorySearch-C4akseUN.js → static/assets/inventorySearch-3g_FrCfp.js

@@ -1 +1 @@
-import{mt as e}from"./index-CFLW6HNX.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-BlsvkMcT.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};

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/it-gnCEsI8h.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/it-jJlvgvcv.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ja-D0wu3paX.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ja-D8He1KGK.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ko-C4ZprGjR.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/ko-DftDwEdH.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/locationSensorDefaults-BLgEpBMI.js


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio