ソースを参照

fix(settings): printer temperature history retention gets a setting; no retention can wipe history; bug reports keep no email

- Settings > Filament > History Retention sets how long nozzle, bed and
  chamber history is kept (15 locales). Both retention settings take at
  least one day; a stored 0 or less is read as one day.
- Bug reports no longer store the reporter's email locally (it still goes
  to the relay); emails stored before are cleared at startup.
maziggy 4 日 前
親
コミット
346b58ed8f
93 ファイル変更、234 行追加、13 行削除
  1. 2 0
      CHANGELOG.md
  2. 14 0
      backend/app/core/database.py
  3. 16 2
      backend/app/main.py
  4. 3 2
      backend/app/schemas/settings.py
  5. 3 4
      backend/app/services/bug_report.py
  6. 88 0
      backend/tests/unit/test_bug_report_no_email_stored.py
  7. 33 0
      backend/tests/unit/test_history_retention_minimum.py
  8. 37 0
      frontend/src/__tests__/pages/SettingsPageUnsavedFields.test.tsx
  9. 1 0
      frontend/src/api/client.ts
  10. 1 0
      frontend/src/i18n/locales/de.ts
  11. 1 0
      frontend/src/i18n/locales/en.ts
  12. 1 0
      frontend/src/i18n/locales/es.ts
  13. 1 0
      frontend/src/i18n/locales/fr.ts
  14. 1 0
      frontend/src/i18n/locales/it.ts
  15. 1 0
      frontend/src/i18n/locales/ja.ts
  16. 1 0
      frontend/src/i18n/locales/ko.ts
  17. 1 0
      frontend/src/i18n/locales/nl.ts
  18. 1 0
      frontend/src/i18n/locales/pt-BR.ts
  19. 1 0
      frontend/src/i18n/locales/ru.ts
  20. 1 0
      frontend/src/i18n/locales/sv.ts
  21. 1 0
      frontend/src/i18n/locales/tr.ts
  22. 1 0
      frontend/src/i18n/locales/uk.ts
  23. 1 0
      frontend/src/i18n/locales/zh-CN.ts
  24. 1 0
      frontend/src/i18n/locales/zh-TW.ts
  25. 19 0
      frontend/src/pages/SettingsPage.tsx
  26. 0 0
      static/assets/AdditionalSection-BS4jKQZ_.js
  27. 0 0
      static/assets/AmsUnitCard-BA1kNAAc.js
  28. 0 0
      static/assets/ArchivesPage-CPCRoH0-.js
  29. 0 0
      static/assets/AssignToAmsModal-BufltGAu.js
  30. 0 0
      static/assets/CamWallPage-BRz1-F-K.js
  31. 0 0
      static/assets/CameraDiagnoseModal-BzXGlNW1.js
  32. 0 0
      static/assets/CameraPage-Bi4wQvZz.js
  33. 0 0
      static/assets/CameraWall-C6RI--gR.js
  34. 0 0
      static/assets/ColumnConfigModal-DI2G-taU.js
  35. 0 0
      static/assets/ConfigureAmsSlotModal-Dkl5UliL.js
  36. 0 0
      static/assets/ConnectAuthorizePage-B_iuY599.js
  37. 0 0
      static/assets/ContextMenu-C_0dzPUp.js
  38. 0 0
      static/assets/ExternalLinkPage-BzAiv8CY.js
  39. 0 1
      static/assets/FileManagerPage-B_y28KHv.js
  40. 0 0
      static/assets/FileUploadModal-nOLPracX.js
  41. 0 0
      static/assets/FinancePage-BhfhZoHB.js
  42. 0 0
      static/assets/GCodeViewerPage-CDYjG1w2.js
  43. 0 0
      static/assets/GroupEditPage-UtibD2rq.js
  44. 0 0
      static/assets/ImagePreviewModal-D1gpeZEa.js
  45. 0 0
      static/assets/InventoryPage-LT8XtYDw.js
  46. 0 0
      static/assets/LibraryTrashPage-Cbz3Eysg.js
  47. 0 0
      static/assets/LoginPage-DizBlgZY.js
  48. 0 0
      static/assets/MaintenancePage-CWA0N8WH.js
  49. 0 0
      static/assets/ModelSourcesPage-DU_tggJh.js
  50. 0 0
      static/assets/NotificationsPage-B-drvuIN.js
  51. 0 1
      static/assets/PdfPreviewModal-D38Ah0kq.js
  52. 0 0
      static/assets/PhotoGalleryModal-kgV1rufo.js
  53. 0 0
      static/assets/PrinterLocationsPage-Du5RuMNV.js
  54. 0 0
      static/assets/PrintersPage-D76QxNOb.js
  55. 0 0
      static/assets/ProfilesPage-DnyMVNzl.js
  56. 0 0
      static/assets/ProjectDetailPage-FDYSniIh.js
  57. 0 0
      static/assets/ProjectsPage-SxzLZKzq.js
  58. 0 0
      static/assets/QueuePage-D7Lll6J-.js
  59. 0 0
      static/assets/RichTextEditor-BPDZ-mJz.js
  60. 0 0
      static/assets/SettingsPage-BWlkSarL.js
  61. 0 0
      static/assets/SetupPage-CI7mS45J.js
  62. 0 0
      static/assets/SliceModal-BJhgW19H.js
  63. 0 0
      static/assets/SpoolBuddyAmsPage-C4m8DGiz.js
  64. 0 0
      static/assets/SpoolBuddyDashboard-CBYSUEwN.js
  65. 0 0
      static/assets/SpoolBuddyInventoryPage-OVqV0875.js
  66. 1 1
      static/assets/SpoolBuddySettingsPage-BP0O2PoP.js
  67. 0 0
      static/assets/SpoolBuddyWriteTagPage-CIP7qyLR.js
  68. 0 0
      static/assets/SpreadsheetPreviewModal-DUDDFNQs.js
  69. 0 0
      static/assets/StatsPage-4fcPnjxn.js
  70. 0 0
      static/assets/StreamOverlayPage-CL0PzPJE.js
  71. 0 0
      static/assets/SystemInfoPage-D5hFex3s.js
  72. 0 0
      static/assets/de-wX9Qe7U5.js
  73. 0 0
      static/assets/es-BXtl7hUY.js
  74. 0 0
      static/assets/fr-DJPWIeay.js
  75. 0 0
      static/assets/index-BiT5tvve.js
  76. 1 1
      static/assets/inventorySearch-DGFWhWni.js
  77. 0 0
      static/assets/it-BW8W-RWX.js
  78. 0 0
      static/assets/it-LoVD1KO1.js
  79. 0 0
      static/assets/ja-DP-9GOiz.js
  80. 0 0
      static/assets/ko-C5ATgkng.js
  81. 0 0
      static/assets/locationSensorDefaults-C_81TWcC.js
  82. 0 0
      static/assets/nl--08XDJKe.js
  83. 0 0
      static/assets/pdf-fCMRqnDd.js
  84. 0 0
      static/assets/pt-BR-CAZD7NQy.js
  85. 0 0
      static/assets/ru-CT8bhx1P.js
  86. 0 0
      static/assets/sv-Bpnxldxg.js
  87. 0 0
      static/assets/sv-CPcXdzfs.js
  88. 0 0
      static/assets/tr-DS_ZSv_-.js
  89. 0 0
      static/assets/uk-CJ_ixWXS.js
  90. 0 0
      static/assets/utils-CQTRxJLO.js
  91. 0 0
      static/assets/zh-CN-sqExXzdg.js
  92. 0 0
      static/assets/zh-TW-C87-vdOB.js
  93. 1 1
      static/index.html

+ 2 - 0
CHANGELOG.md

@@ -164,6 +164,8 @@ 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
+- **Printer temperature history retention can be set, and no retention setting can wipe history** — How long the nozzle, bed and chamber temperature history is kept had no setting in the interface; it now sits next to the AMS history under **Settings → Filament → History Retention**. Both settings take at least one day: 0 or less was accepted through the API and made the daily cleanup delete all history, and a value stored that way is now read as one day.
+- **Bug reports no longer keep the reporter's email** — Every report sent from Bambuddy was also stored locally with the email it was sent with, and nothing ever read or removed it. The email still goes with the report; the local record keeps only the outcome, and emails stored before are cleared at startup.
 - **Unused API pieces removed and the rest documented** — The firmware upload no longer broadcasts a WebSocket progress message nothing listened to (the page polls the upload status, as before); the old route that deleted every plate-detection calibration of a printer at once is gone (references are deleted one at a time from the plate-detection dialog); `/spoolbuddy/calibration` now opens the SpoolBuddy settings page, where calibration lives. The wiki now documents `POST /archives/upload-source` for slicer post-processing scripts and `PUT /local-presets/{id}`.
 - **Load and Unload work on an AMS-HT** — Clicking **Load** on an AMS-HT slot was refused with "tray_id must be 0..15 …": the printer card numbered the slot like a regular AMS (unit × 4 + slot, 512 for the first AMS-HT), and the printer's commands would have turned the AMS-HT into a unit it does not have. **Unload** while the AMS-HT was the loaded tray was addressed to that same non-existent unit. Both now address the AMS-HT by its own unit number, as Bambu Studio does, and **Unload** from an AMS-HT slot's menu is aimed at that slot. Loading from the A2L's AMS-Lite now sends the target Bambu Studio sends as well.
 - **A second Bambuddy on the same machine no longer cuts the camera every minute** — Bambuddy's once-a-minute cleanup kills camera ffmpeg processes left behind by a crash or restart, and recognised them by their command alone. Another Bambuddy on the same host (a second install, a test instance) runs the very same command, so each one's cleanup killed the other's live camera once a minute and the view reconnected. Only an ffmpeg started by this Bambuddy, or left orphaned by an earlier run, is cleaned up now.

+ 14 - 0
backend/app/core/database.py

@@ -5439,6 +5439,7 @@ async def _run_migrations(conn):
     # Migration: Disambiguate the four ``user_print_*`` notification template
     # names by appending " Email" (#1792). See ``_migrate_rename_user_print_template_names``.
     await _migrate_rename_user_print_template_names(conn)
+    await _migrate_clear_bug_report_emails(conn)
 
     # Migration: per-file print progress inside a project (#1897).
     # - print_archives.library_file_id: which library file a queued run was
@@ -7190,6 +7191,19 @@ async def _migrate_rename_user_print_template_names(conn) -> None:
             )
 
 
+async def _migrate_clear_bug_report_emails(conn) -> None:
+    """Clear the reporter emails stored with past bug reports.
+
+    Every report kept the email it was sent with, and nothing ever read or
+    purged the table; the email only needs to reach the relay. Repeatable:
+    nothing writes one any more.
+    """
+    from sqlalchemy import text
+
+    async with conn.begin_nested():
+        await conn.execute(text("UPDATE bug_reports SET reporter_email = NULL WHERE reporter_email IS NOT NULL"))
+
+
 async def seed_notification_templates():
     """Seed default notification templates if they don't exist."""
     from sqlalchemy import select

+ 16 - 2
backend/app/main.py

@@ -9295,6 +9295,18 @@ async def on_print_complete(printer_id: int, data: dict):
 _ams_history_task: asyncio.Task | None = None
 AMS_HISTORY_INTERVAL = 300  # Record every 5 minutes
 AMS_HISTORY_RETENTION_DAYS = 30  # Keep data for 30 days
+
+
+def _retention_days(stored: str | None, default: int) -> int:
+    """A history retention setting as days to keep: at least 1, the default
+    when unset or unreadable. A stored 0 or less (the API took one before it
+    had a minimum) would put the cutoff at now and delete all history."""
+    try:
+        return max(1, int(stored)) if stored is not None else default
+    except (TypeError, ValueError):
+        return default
+
+
 _ams_cleanup_counter = 0  # Track recordings to trigger periodic cleanup
 # Track alarm cooldowns (printer_id:ams_id:type -> last_alarm_time)
 _ams_alarm_cooldown: dict[str, datetime] = {}
@@ -9733,7 +9745,7 @@ async def record_ams_history():
 
                     result = await db.execute(select(Settings).where(Settings.key == "ams_history_retention_days"))
                     setting = result.scalar_one_or_none()
-                    retention_days = int(setting.value) if setting else AMS_HISTORY_RETENTION_DAYS
+                    retention_days = _retention_days(setting.value if setting else None, AMS_HISTORY_RETENTION_DAYS)
 
                     cutoff = utcnow_naive() - timedelta(days=retention_days)
                     result = await db.execute(delete(AMSSensorHistory).where(AMSSensorHistory.recorded_at < cutoff))
@@ -9859,7 +9871,9 @@ async def record_printer_sensor_history():
                         select(Settings).where(Settings.key == "printer_sensor_history_retention_days")
                     )
                     setting = result.scalar_one_or_none()
-                    retention_days = int(setting.value) if setting else PRINTER_SENSOR_HISTORY_RETENTION_DAYS
+                    retention_days = _retention_days(
+                        setting.value if setting else None, PRINTER_SENSOR_HISTORY_RETENTION_DAYS
+                    )
 
                     cutoff = utcnow_naive() - timedelta(days=retention_days)
                     cleanup = await db.execute(

+ 3 - 2
backend/app/schemas/settings.py

@@ -758,8 +758,9 @@ class AppSettingsUpdate(BaseModel):
     ams_temp_good: float | None = None
     ams_temp_fair: float | None = None
     ams_temp_alarm: float | None = None
-    ams_history_retention_days: int | None = None
-    printer_sensor_history_retention_days: int | None = None
+    # At least a day: 0 or less put the cleanup cutoff at now and deleted all history daily.
+    ams_history_retention_days: int | None = Field(default=None, ge=1)
+    printer_sensor_history_retention_days: int | None = Field(default=None, ge=1)
     queue_drying_enabled: bool | None = None
     queue_drying_block: bool | None = None
     ambient_drying_enabled: bool | None = None

+ 3 - 4
backend/app/services/bug_report.py

@@ -9,6 +9,9 @@ from backend.app.core.config import BUG_REPORT_RELAY_URL
 from backend.app.core.database import async_session
 from backend.app.models.bug_report import BugReport
 
+# The reporter's email goes to the relay with the report (it is how the report
+# gets answered) and is not kept in bug_reports: nothing reads that table.
+
 logger = logging.getLogger(__name__)
 
 # Rate limiting: max 5 reports per hour
@@ -68,7 +71,6 @@ async def submit_report(
                 async with async_session() as db:
                     report = BugReport(
                         description=description,
-                        reporter_email=reporter_email,
                         status="failed",
                         error_message=error_msg,
                     )
@@ -86,7 +88,6 @@ async def submit_report(
         async with async_session() as db:
             report = BugReport(
                 description=description,
-                reporter_email=reporter_email,
                 status="failed",
                 error_message="Failed to reach bug report relay",
             )
@@ -104,7 +105,6 @@ async def submit_report(
         async with async_session() as db:
             report = BugReport(
                 description=description,
-                reporter_email=reporter_email,
                 status="failed",
                 error_message=relay_data.get("message", "Relay returned failure"),
             )
@@ -125,7 +125,6 @@ async def submit_report(
     async with async_session() as db:
         report = BugReport(
             description=description,
-            reporter_email=reporter_email,
             github_issue_number=issue_number,
             github_issue_url=issue_url,
             status="submitted",

+ 88 - 0
backend/tests/unit/test_bug_report_no_email_stored.py

@@ -0,0 +1,88 @@
+"""A bug report's email goes to the relay and is not kept locally.
+
+Every submission, successful or not, was stored in ``bug_reports`` with the
+reporter's email, and nothing ever read or purged that table. The email still
+goes to the relay (it is how the report gets answered); the local row keeps
+the outcome only, and the emails already stored are cleared at startup.
+"""
+
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import httpx
+import pytest
+
+from backend.app.services.bug_report import submit_report
+
+EMAIL = "reporter@example.invalid"
+
+
+def _relay(response=None, error=None):
+    client = AsyncMock()
+    client.post = AsyncMock(side_effect=error) if error else AsyncMock(return_value=response)
+    client.__aenter__ = AsyncMock(return_value=client)
+    client.__aexit__ = AsyncMock(return_value=False)
+    return client
+
+
+def _response(status, body=None):
+    response = MagicMock(status_code=status)
+    response.json.return_value = body or {}
+    return response
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+    "relay",
+    [
+        _relay(_response(200, {"success": True, "issue_number": 7, "issue_url": "https://example.invalid/7"})),
+        _relay(_response(500)),
+        _relay(error=httpx.ConnectError("down")),
+        _relay(_response(200, {"success": False, "message": "refused"})),
+    ],
+    ids=["submitted", "relay-http-error", "relay-unreachable", "relay-refused"],
+)
+async def test_the_stored_report_has_no_email(relay):
+    db = AsyncMock()
+    db.add = MagicMock()
+    session = MagicMock()
+    session.return_value.__aenter__ = AsyncMock(return_value=db)
+    session.return_value.__aexit__ = AsyncMock(return_value=False)
+
+    with (
+        patch("backend.app.services.bug_report.httpx.AsyncClient", return_value=relay),
+        patch("backend.app.services.bug_report.async_session", session),
+        patch("backend.app.services.bug_report._rate_limit_timestamps", []),
+        patch("backend.app.services.bug_report.BUG_REPORT_RELAY_URL", "https://example.invalid/api/bug-report"),
+    ):
+        await submit_report(description="It broke", reporter_email=EMAIL, screenshot_base64=None, support_info=None)
+
+    stored = db.add.call_args.args[0]
+    assert stored.reporter_email is None
+    assert relay.post.await_args.kwargs["json"]["reporter_email"] == EMAIL
+
+
+@pytest.mark.asyncio
+async def test_emails_stored_before_are_cleared():
+    from sqlalchemy import text
+    from sqlalchemy.ext.asyncio import create_async_engine
+
+    import backend.app.models  # noqa: F401 - populate Base.metadata
+    from backend.app.core.database import Base, _migrate_clear_bug_report_emails
+
+    engine = create_async_engine("sqlite+aiosqlite:///:memory:")
+    try:
+        async with engine.begin() as conn:
+            await conn.run_sync(Base.metadata.create_all)
+            await conn.execute(
+                text(
+                    "INSERT INTO bug_reports (description, reporter_email, status, email_sent) VALUES ('x', :e, 'failed', 0)"
+                ),
+                {"e": EMAIL},
+            )
+            await _migrate_clear_bug_report_emails(conn)
+            await _migrate_clear_bug_report_emails(conn)  # again at the next start: harmless
+
+            emails = (await conn.execute(text("SELECT reporter_email FROM bug_reports"))).scalars().all()
+        assert emails == [None]
+    finally:
+        await engine.dispose()

+ 33 - 0
backend/tests/unit/test_history_retention_minimum.py

@@ -0,0 +1,33 @@
+"""History retention can no longer be set so that it deletes all history.
+
+``printer_sensor_history_retention_days`` had no UI and, like the AMS one, no
+minimum: 0 or less put the daily cleanup's cutoff at now and deleted every row.
+Both settings now take at least one day, and the cleanup reads a stored value
+the API accepted before as at least one day too.
+"""
+
+import pytest
+from pydantic import ValidationError
+
+from backend.app.main import _retention_days
+from backend.app.schemas.settings import AppSettingsUpdate
+
+
+@pytest.mark.parametrize("key", ["ams_history_retention_days", "printer_sensor_history_retention_days"])
+@pytest.mark.parametrize("value", [0, -5])
+def test_less_than_a_day_is_refused(key, value):
+    with pytest.raises(ValidationError):
+        AppSettingsUpdate(**{key: value})
+
+
+@pytest.mark.parametrize("key", ["ams_history_retention_days", "printer_sensor_history_retention_days"])
+def test_a_day_or_more_is_taken(key):
+    assert getattr(AppSettingsUpdate(**{key: 1}), key) == 1
+
+
+@pytest.mark.parametrize(
+    ("stored", "days"),
+    [(None, 30), ("90", 90), ("0", 1), ("-3", 1), ("not a number", 30)],
+)
+def test_the_cleanup_reads_a_stored_value_as_at_least_a_day(stored, days):
+    assert _retention_days(stored, 30) == days

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

@@ -86,3 +86,40 @@ describe('SettingsPage fields that were never saved', () => {
     });
   });
 });
+
+describe('SettingsPage printer temperature history retention', () => {
+  let saves: Record<string, unknown>[];
+
+  beforeEach(() => {
+    window.history.replaceState({}, '', '/');
+    vi.mocked(localStorage.getItem).mockReset();
+    localStorage.clear();
+    setAuthToken(null);
+    saves = [];
+    const withRetention = { ...settings, ams_history_retention_days: 30, printer_sensor_history_retention_days: 30 };
+    server.use(
+      http.get('/api/v1/settings/', () => HttpResponse.json(withRetention)),
+      http.put('/api/v1/settings/', async ({ request }) => {
+        const body = (await request.json()) as Record<string, unknown>;
+        saves.push(body);
+        return HttpResponse.json({ ...withRetention, ...body });
+      }),
+      http.get('/api/v1/auth/status', () => HttpResponse.json({ auth_enabled: false, requires_setup: false })),
+    );
+  });
+
+  it('can be set next to the AMS history and is saved', async () => {
+    const user = userEvent.setup();
+    render(<SettingsPage />);
+    await screen.findByText('Date Format');
+    await user.click(screen.getAllByText('Filament')[0]);
+    await new Promise((resolve) => setTimeout(resolve, 200));
+
+    const input = await screen.findByLabelText('Keep printer temperature history (nozzle, bed, chamber) for');
+    fireEvent.change(input, { target: { value: '90' } });
+
+    await waitFor(() => expect(saves.at(-1)).toMatchObject({ printer_sensor_history_retention_days: 90 }), {
+      timeout: 3000,
+    });
+  });
+});

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

@@ -1435,6 +1435,7 @@ export interface AppSettings {
   // resolves to ams_temp_fair so existing installs are unaffected.
   ams_temp_alarm: number | null;
   ams_history_retention_days: number;  // days to keep AMS sensor history
+  printer_sensor_history_retention_days?: number;  // days to keep printer heater history
   // Queue auto-drying settings
   queue_drying_enabled: boolean;  // Auto-dry AMS between queued prints
   queue_drying_block: boolean;  // Block queue until drying completes

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

@@ -2455,6 +2455,7 @@ export default {
     tempAlarmMustBePositive: 'Ein Schwellenwert von 0 oder weniger wird ignoriert — der Alarm nutzt dann den mittleren Schwellenwert.',
     historyRetention: 'Verlaufsaufbewahrung',
     keepSensorHistory: 'Sensorverlauf behalten für',
+    keepPrinterTemperatureHistory: 'Temperaturverlauf des Druckers (Düse, Bett, Kammer) behalten für',
     historyRetentionDescription: 'Ältere Feuchtigkeits- und Temperaturdaten werden automatisch gelöscht',
     defaultPrintOptions: 'Standard-Druckoptionen',
     defaultPrintOptionsDescription: 'Standardwerte für Druckoptionen bei neuen Drucken festlegen. Diese können im Druckdialog pro Druck überschrieben werden.',

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

@@ -2477,6 +2477,7 @@ export default {
     tempAlarmMustBePositive: 'A threshold of 0 or less is ignored — the alarm falls back to the fair threshold.',
     historyRetention: 'History Retention',
     keepSensorHistory: 'Keep sensor history for',
+    keepPrinterTemperatureHistory: 'Keep printer temperature history (nozzle, bed, chamber) for',
     historyRetentionDescription: 'Older humidity and temperature data will be automatically deleted',
     defaultPrintOptions: 'Default Print Options',
     defaultPrintOptionsDescription: 'Set default values for print options when starting new prints. These can be overridden per print in the print dialog.',

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

@@ -2458,6 +2458,7 @@ export default {
     tempAlarmMustBePositive: 'Un umbral de 0 o inferior se ignora: la alarma usa el umbral aceptable.',
     historyRetention: 'Retención del historial',
     keepSensorHistory: 'Conservar el historial del sensor durante',
+    keepPrinterTemperatureHistory: 'Conservar el historial de temperaturas de la impresora (boquilla, cama, cámara) durante',
     historyRetentionDescription: 'Los datos de humedad y temperatura más antiguos se eliminarán automáticamente',
     defaultPrintOptions: 'Opciones de impresión predeterminadas',
     defaultPrintOptionsDescription: 'Establezca valores predeterminados para las opciones de impresión al iniciar nuevas impresiones. Se pueden anular por impresión en el diálogo de impresión.',

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

@@ -2410,6 +2410,7 @@ export default {
     tempAlarmMustBePositive: 'Un seuil de 0 ou moins est ignoré — l\'alerte utilise alors le seuil correct.',
     historyRetention: 'Rétention d\'historique',
     keepSensorHistory: 'Garder l\'historique pendant',
+    keepPrinterTemperatureHistory: 'Garder l\'historique des températures de l\'imprimante (buse, plateau, caisson) pendant',
     historyRetentionDescription: 'Les anciennes données seront supprimées.',
     defaultPrintOptions: 'Options d\'impression par défaut',
     defaultPrintOptionsDescription: 'Définir les valeurs par défaut des options d\'impression. Modifiables dans la boîte de dialogue d\'impression.',

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

@@ -2410,6 +2410,7 @@ export default {
     tempAlarmMustBePositive: 'Una soglia pari o inferiore a 0 viene ignorata: l\'allarme usa la soglia discreta.',
     historyRetention: 'Conservazione cronologia',
     keepSensorHistory: 'Mantieni cronologia sensori per',
+    keepPrinterTemperatureHistory: 'Mantieni la cronologia delle temperature della stampante (ugello, piatto, camera) per',
     historyRetentionDescription: 'I dati più vecchi saranno eliminati automaticamente',
     defaultPrintOptions: 'Opzioni di stampa predefinite',
     defaultPrintOptionsDescription: 'Imposta i valori predefiniti per le opzioni di stampa. Possono essere modificati nella finestra di stampa.',

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

@@ -2454,6 +2454,7 @@ export default {
     tempAlarmMustBePositive: '0以下のしきい値は無視され、普通のしきい値で発報します。',
     historyRetention: '履歴の保持',
     keepSensorHistory: 'センサー履歴の保持期間',
+    keepPrinterTemperatureHistory: 'プリンター温度履歴(ノズル・ベッド・チャンバー)の保持期間',
     historyRetentionDescription: '古い湿度と温度データは自動的に削除されます',
     defaultPrintOptions: 'デフォルト印刷オプション',
     defaultPrintOptionsDescription: '新しい印刷のデフォルト値を設定します。印刷ダイアログで個別に変更できます。',

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

@@ -2332,6 +2332,7 @@ export default {
     tempAlarmMustBePositive: '0 이하의 기준값은 무시되며, 보통 기준값으로 알립니다.',
     historyRetention: '기록 보존',
     keepSensorHistory: '센서 기록 보존 기간',
+    keepPrinterTemperatureHistory: '프린터 온도 기록(노즐, 베드, 챔버) 보존 기간',
     historyRetentionDescription: '오래된 습도 및 온도 데이터가 자동으로 삭제됩니다',
     defaultPrintOptions: '기본 인쇄 옵션',
     defaultPrintOptionsDescription: '새 인쇄 시작 시 기본값을 설정하세요. 인쇄 대화 상자에서 인쇄별로 재정의할 수 있습니다.',

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

@@ -2475,6 +2475,7 @@ export default {
     tempAlarmMustBePositive: 'Een drempel van 0 of lager wordt genegeerd — het alarm valt terug op de drempel Redelijk.',
     historyRetention: 'Bewaartermijn geschiedenis',
     keepSensorHistory: 'Sensorgeschiedenis bewaren gedurende',
+    keepPrinterTemperatureHistory: 'Temperatuurgeschiedenis van de printer (nozzle, bed, kamer) bewaren gedurende',
     historyRetentionDescription: 'Oudere gegevens over luchtvochtigheid en temperatuur worden automatisch verwijderd',
     defaultPrintOptions: 'Standaard afdrukopties',
     defaultPrintOptionsDescription: 'Stel standaardwaarden in voor afdrukopties bij het starten van nieuwe afdrukken. Deze kunnen per afdruk in het afdrukdialoogvenster worden overschreven.',

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

@@ -2410,6 +2410,7 @@ export default {
     tempAlarmMustBePositive: 'Um limiar de 0 ou menos é ignorado — o alarme usa o limiar razoável.',
     historyRetention: 'Retenção de Histórico',
     keepSensorHistory: 'Manter histórico do sensor por',
+    keepPrinterTemperatureHistory: 'Manter o histórico de temperatura da impressora (bico, mesa, câmara) por',
     historyRetentionDescription: 'Dados antigos de umidade e temperatura serão automaticamente excluídos',
     defaultPrintOptions: 'Opções de impressão padrão',
     defaultPrintOptionsDescription: 'Defina valores padrão para opções de impressão. Podem ser alterados no diálogo de impressão.',

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

@@ -2335,6 +2335,7 @@ export default {
     tempAlarmMustBePositive: "Порог 0 или меньше игнорируется — тревога срабатывает по допустимому порогу.",
     historyRetention: "Хранение истории",
     keepSensorHistory: "Хранить историю датчиков",
+    keepPrinterTemperatureHistory: 'Хранить историю температур принтера (сопло, стол, камера)',
     historyRetentionDescription: "Более старые данные влажности и температуры будут удаляться автоматически",
     defaultPrintOptions: "Параметры печати по умолчанию",
     defaultPrintOptionsDescription: "Задайте значения по умолчанию для новых заданий. Их можно изменить отдельно в диалоге запуска печати.",

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

@@ -2474,6 +2474,7 @@ export default {
     tempAlarmMustBePositive: 'Ett tröskelvärde på 0 eller lägre ignoreras — larmet faller tillbaka till Godkänd-tröskelvärdet.',
     historyRetention: 'Bevarande av historik',
     keepSensorHistory: 'Bevara sensordata för',
+    keepPrinterTemperatureHistory: 'Bevara skrivarens temperaturhistorik (munstycke, bädd, kammare) i',
     historyRetentionDescription: 'Äldre luftfuktighets- och temperaturdata kommer att tas bort automatiskt',
     defaultPrintOptions: 'Standardutskriftsalternativ',
     defaultPrintOptionsDescription: 'Sätt standardvärden för utskriftsalternativ när nya utskrifter startas. Dessa kan åsidosättas per utskrift i utskriftsdialogrutan.',

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

@@ -2459,6 +2459,7 @@ export default {
     tempAlarmMustBePositive: 'Sıfır veya daha küçük bir eşik yok sayılır — alarm orta eşiği kullanır.',
     historyRetention: 'Geçmiş Saklama',
     keepSensorHistory: 'Sensör geçmişini sakla',
+    keepPrinterTemperatureHistory: 'Yazıcı sıcaklık geçmişini (nozül, tabla, hazne) sakla',
     historyRetentionDescription: 'Daha eski nem ve sıcaklık verileri otomatik olarak silinecek',
     defaultPrintOptions: 'Varsayılan Baskı Seçenekleri',
     defaultPrintOptionsDescription: 'Yeni baskılar başlatılırken baskı seçenekleri için varsayılan değerleri ayarlayın. Bunlar baskı iletişim kutusunda baskı başına geçersiz kılınabilir.',

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

@@ -2473,6 +2473,7 @@ export default {
     tempAlarmMustBePositive: "Поріг 0 або менший ігнорується — тривога спрацьовує на задовільному порозі.",
     historyRetention: "Збереження історії",
     keepSensorHistory: "Зберігати історію датчиків для",
+    keepPrinterTemperatureHistory: 'Зберігати історію температур принтера (сопло, стіл, камера) протягом',
     historyRetentionDescription: "Старіші дані про вологість і температуру буде автоматично видалено",
     defaultPrintOptions: "Параметри друку за умовчанням",
     defaultPrintOptionsDescription: "Установіть типові значення параметрів для нових завдань друку. Їх можна змінити для окремого завдання в діалоговому вікні друку.",

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

@@ -2455,6 +2455,7 @@ export default {
     tempAlarmMustBePositive: '小于或等于 0 的阈值将被忽略,告警改用一般阈值。',
     historyRetention: '历史保留',
     keepSensorHistory: '保留传感器历史',
+    keepPrinterTemperatureHistory: '保留打印机温度历史(喷嘴、热床、腔室)',
     historyRetentionDescription: '较旧的湿度和温度数据将被自动删除',
     defaultPrintOptions: '默认打印选项',
     defaultPrintOptionsDescription: '设置新打印的默认选项值。可在打印对话框中逐次覆盖。',

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

@@ -2455,6 +2455,7 @@ export default {
     tempAlarmMustBePositive: '小於或等於 0 的閾值將被忽略,警報改用一般閾值。',
     historyRetention: '歷史保留',
     keepSensorHistory: '保留感測器歷史',
+    keepPrinterTemperatureHistory: '保留印表機溫度歷史(噴嘴、熱床、腔室)',
     historyRetentionDescription: '較舊的濕度和溫度資料將被自動刪除',
     defaultPrintOptions: '預設列印選項',
     defaultPrintOptionsDescription: '設定新列印的預設選項值。可在列印對話方塊中逐次覆蓋。',

+ 19 - 0
frontend/src/pages/SettingsPage.tsx

@@ -1278,6 +1278,7 @@ export function SettingsPage() {
       baseline.ams_temp_fair !== localSettings.ams_temp_fair ||
       (baseline.ams_temp_alarm ?? null) !== (localSettings.ams_temp_alarm ?? null) ||
       baseline.ams_history_retention_days !== localSettings.ams_history_retention_days ||
+      baseline.printer_sensor_history_retention_days !== localSettings.printer_sensor_history_retention_days ||
       baseline.disable_filament_warnings !== localSettings.disable_filament_warnings ||
       baseline.prefer_lowest_filament !== localSettings.prefer_lowest_filament ||
       (baseline.queue_drying_enabled ?? false) !== (localSettings.queue_drying_enabled ?? false) ||
@@ -1407,6 +1408,7 @@ export function SettingsPage() {
         ams_temp_fair: localSettings.ams_temp_fair,
         ams_temp_alarm: localSettings.ams_temp_alarm ?? null,
         ams_history_retention_days: localSettings.ams_history_retention_days,
+        printer_sensor_history_retention_days: localSettings.printer_sensor_history_retention_days,
         disable_filament_warnings: localSettings.disable_filament_warnings,
         prefer_lowest_filament: localSettings.prefer_lowest_filament,
         queue_drying_enabled: localSettings.queue_drying_enabled,
@@ -6541,6 +6543,23 @@ export function SettingsPage() {
                       <span className="text-bambu-gray">{t('common.days')}</span>
                     </div>
                   </div>
+                  <div>
+                    <label htmlFor="printer-sensor-history-retention" className="block text-sm text-bambu-gray mb-1">
+                      {t('settings.keepPrinterTemperatureHistory')}
+                    </label>
+                    <div className="flex items-center gap-2">
+                      <NumberInput
+                        id="printer-sensor-history-retention"
+                        min={1}
+                        max={365}
+                        value={localSettings.printer_sensor_history_retention_days ?? 30}
+                        onChange={(v) => updateSetting('printer_sensor_history_retention_days', v)}
+                        fallback={30}
+                        className="w-24 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
+                      />
+                      <span className="text-bambu-gray">{t('common.days')}</span>
+                    </div>
+                  </div>
                   <p className="text-xs text-bambu-gray">
                     {t('settings.historyRetentionDescription')}
                   </p>

ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/AdditionalSection-BS4jKQZ_.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/AmsUnitCard-BA1kNAAc.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ArchivesPage-CPCRoH0-.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/AssignToAmsModal-BufltGAu.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/CamWallPage-BRz1-F-K.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/CameraDiagnoseModal-BzXGlNW1.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/CameraPage-Bi4wQvZz.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/CameraWall-C6RI--gR.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ColumnConfigModal-DI2G-taU.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ConfigureAmsSlotModal-Dkl5UliL.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ConnectAuthorizePage-B_iuY599.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ContextMenu-C_0dzPUp.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ExternalLinkPage-BzAiv8CY.js


ファイルの差分が大きいため隠しています
+ 0 - 1
static/assets/FileManagerPage-B_y28KHv.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/FileUploadModal-nOLPracX.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/FinancePage-BhfhZoHB.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/GCodeViewerPage-CDYjG1w2.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/GroupEditPage-UtibD2rq.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ImagePreviewModal-D1gpeZEa.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/InventoryPage-LT8XtYDw.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/LibraryTrashPage-Cbz3Eysg.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/LoginPage-DizBlgZY.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/MaintenancePage-CWA0N8WH.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ModelSourcesPage-DU_tggJh.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/NotificationsPage-B-drvuIN.js


ファイルの差分が大きいため隠しています
+ 0 - 1
static/assets/PdfPreviewModal-D38Ah0kq.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/PhotoGalleryModal-kgV1rufo.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/PrinterLocationsPage-Du5RuMNV.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/PrintersPage-D76QxNOb.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ProfilesPage-DnyMVNzl.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ProjectDetailPage-FDYSniIh.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ProjectsPage-SxzLZKzq.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/QueuePage-D7Lll6J-.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/RichTextEditor-BPDZ-mJz.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SettingsPage-BWlkSarL.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SetupPage-CI7mS45J.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SliceModal-BJhgW19H.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SpoolBuddyAmsPage-C4m8DGiz.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SpoolBuddyDashboard-CBYSUEwN.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-OVqV0875.js


ファイルの差分が大きいため隠しています
+ 1 - 1
static/assets/SpoolBuddySettingsPage-BP0O2PoP.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SpoolBuddyWriteTagPage-CIP7qyLR.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SpreadsheetPreviewModal-DUDDFNQs.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/StatsPage-4fcPnjxn.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/StreamOverlayPage-CL0PzPJE.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/SystemInfoPage-D5hFex3s.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/de-wX9Qe7U5.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/es-BXtl7hUY.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/fr-DJPWIeay.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/index-BiT5tvve.js


+ 1 - 1
static/assets/inventorySearch-C59qF_UX.js → static/assets/inventorySearch-DGFWhWni.js

@@ -1 +1 @@
-import{mt as e}from"./index-DDeeYGPC.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-BiT5tvve.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-BW8W-RWX.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/it-LoVD1KO1.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ja-DP-9GOiz.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ko-C5ATgkng.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/locationSensorDefaults-C_81TWcC.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/nl--08XDJKe.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/pdf-fCMRqnDd.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/pt-BR-CAZD7NQy.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/ru-CT8bhx1P.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/sv-Bpnxldxg.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/sv-CPcXdzfs.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/tr-DS_ZSv_-.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/uk-CJ_ixWXS.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/utils-CQTRxJLO.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/zh-CN-sqExXzdg.js


ファイルの差分が大きいため隠しています
+ 0 - 0
static/assets/zh-TW-C87-vdOB.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-DDeeYGPC.js"></script>
+    <script type="module" crossorigin src="/assets/index-BiT5tvve.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">

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません