Procházet zdrojové kódy

Add developer LAN mode detection and warning banner

Parse the MQTT "fun" field bit 0x20000000 to detect whether connected
printers have Developer LAN Mode enabled. Show a persistent orange
warning banner when any printer lacks it, since newer firmware silently
rejects MQTT write commands without developer mode.

- Parse fun field into developer_mode on PrinterState
- Add /printers/developer-mode-warnings lightweight polling endpoint
- Include developer_mode in printer status API and support bundle
- Orange banner with affected printer names and wiki link
- Translations for all 6 locales (en, de, fr, it, ja, pt-BR)
- 7 backend + 4 frontend tests
maziggy před 3 měsíci
rodič
revize
0e87c377e6

+ 2 - 0
CHANGELOG.md

@@ -26,6 +26,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Filament Cost Tracking** ([#454](https://github.com/maziggy/bambuddy/pull/454), [#452](https://github.com/maziggy/bambuddy/issues/452)) — Track per-spool filament costs and see cost breakdowns for every print. Each spool can have a `cost_per_kg` value; when a print completes, the usage tracker calculates the cost from actual filament consumption and stores it in the usage history. Archive costs are automatically aggregated from spool usage records. A global `default_filament_cost` setting (Settings → Filament) provides a fallback when spools don't have individual costs set. The print modal shows a real-time cost preview based on loaded filaments. Archive cards display the total cost. The inventory table includes a sortable cost/kg column. The recalculate-costs endpoint can retroactively update all archive costs when filament prices change. Contributed by @Keybored02.
 - **Filament Cost Tracking** ([#454](https://github.com/maziggy/bambuddy/pull/454), [#452](https://github.com/maziggy/bambuddy/issues/452)) — Track per-spool filament costs and see cost breakdowns for every print. Each spool can have a `cost_per_kg` value; when a print completes, the usage tracker calculates the cost from actual filament consumption and stores it in the usage history. Archive costs are automatically aggregated from spool usage records. A global `default_filament_cost` setting (Settings → Filament) provides a fallback when spools don't have individual costs set. The print modal shows a real-time cost preview based on loaded filaments. Archive cards display the total cost. The inventory table includes a sortable cost/kg column. The recalculate-costs endpoint can retroactively update all archive costs when filament prices change. Contributed by @Keybored02.
 - **Background Print Dispatch** ([#408](https://github.com/maziggy/bambuddy/pull/408), [#112](https://github.com/maziggy/bambuddy/issues/112)) — Printing from archives and the file manager now runs in the background via an async dispatch service. FTP uploads and print-start commands are decoupled from API request latency, so the UI responds immediately. Real-time progress is streamed to all clients via WebSocket, rendered as a persistent toast with per-job upload progress bars, status badges (dispatched/processing/completed/failed/cancelled), and a cancel button. The dispatcher supports concurrent uploads to different printers with per-printer queuing to prevent conflicts. Cancellation is cooperative — uploads abort at the next chunk boundary and clean up partial files on the printer. Batch progress tracking shows overall completion across multi-printer dispatches. Translations added for all 6 locales (en, de, fr, it, ja, pt-BR).
 - **Background Print Dispatch** ([#408](https://github.com/maziggy/bambuddy/pull/408), [#112](https://github.com/maziggy/bambuddy/issues/112)) — Printing from archives and the file manager now runs in the background via an async dispatch service. FTP uploads and print-start commands are decoupled from API request latency, so the UI responds immediately. Real-time progress is streamed to all clients via WebSocket, rendered as a persistent toast with per-job upload progress bars, status badges (dispatched/processing/completed/failed/cancelled), and a cancel button. The dispatcher supports concurrent uploads to different printers with per-printer queuing to prevent conflicts. Cancellation is cooperative — uploads abort at the next chunk boundary and clean up partial files on the printer. Batch progress tracking shows overall completion across multi-printer dispatches. Translations added for all 6 locales (en, de, fr, it, ja, pt-BR).
 - **Include Beta Updates Setting** — New toggle in Settings → Updates to opt in to beta/prerelease update notifications. Default: off (stable only). The update checker now fetches `/releases` instead of `/releases/latest` and filters by `parse_version()` prerelease detection (not GitHub's `prerelease` flag, which may not be set correctly). Users on the Docker `latest` tag will no longer see notifications for beta releases they can't install.
 - **Include Beta Updates Setting** — New toggle in Settings → Updates to opt in to beta/prerelease update notifications. Default: off (stable only). The update checker now fetches `/releases` instead of `/releases/latest` and filters by `parse_version()` prerelease detection (not GitHub's `prerelease` flag, which may not be set correctly). Users on the Docker `latest` tag will no longer see notifications for beta releases they can't install.
+- **Developer LAN Mode Detection & Warning Banner** — Automatically detects whether connected printers have Developer LAN Mode enabled by parsing the MQTT `fun` field (bit `0x20000000`). When any connected printer lacks developer mode, a persistent orange warning banner appears at the top of the UI with the affected printer name(s) and a link to Bambu Lab's documentation on how to enable it. Without developer mode, MQTT write operations (start/stop/pause prints, AMS control, light/speed/gcode commands) are silently rejected by newer firmware. The `developer_mode` state is included in the support bundle for diagnostics. New `/printers/developer-mode-warnings` endpoint provides a lightweight polling summary. Translations added for all 6 locales (en, de, fr, it, ja, pt-BR).
 
 
 ### Improved
 ### Improved
 - **P2S Dual-AMS tray_now Test Coverage** — Added 14 integration tests for multi-AMS tray_now disambiguation on single-nozzle printers (resolving AMS-B slots via mapping field, AMS-A passthrough, multi-color mapping, ambiguous/missing mapping fallbacks, last_loaded_tray tracking). Added 9 unit tests for `_resolve_local_slot_from_mapping` (snow decoding, unmapped entry filtering, ambiguity detection, AMS-HT slot matching). All 66 tray_now-related tests pass.
 - **P2S Dual-AMS tray_now Test Coverage** — Added 14 integration tests for multi-AMS tray_now disambiguation on single-nozzle printers (resolving AMS-B slots via mapping field, AMS-A passthrough, multi-color mapping, ambiguous/missing mapping fallbacks, last_loaded_tray tracking). Added 9 unit tests for `_resolve_local_slot_from_mapping` (snow decoding, unmapped entry filtering, ambiguity detection, AMS-HT slot matching). All 66 tray_now-related tests pass.
@@ -34,6 +35,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Spool Assignment Snapshot Test Coverage** — Added 7 backend unit tests covering spool assignment snapshotting at print start, snapshot-preferred spool lookup in both 3MF and AMS delta paths, fallback to live query for pre-upgrade sessions, and the core mid-print unlink scenario from #459.
 - **Spool Assignment Snapshot Test Coverage** — Added 7 backend unit tests covering spool assignment snapshotting at print start, snapshot-preferred spool lookup in both 3MF and AMS delta paths, fallback to live query for pre-upgrade sessions, and the core mid-print unlink scenario from #459.
 - **Background Dispatch Test Coverage** — Added 5 backend unit tests for dispatch cancel races (single-lock TOCTOU fix), batch counter reset re-check, and job lifecycle. Added 2 FTP regression tests for voidresp error handling (upload-loop prevention) and A1 model voidresp skip. Added 1 frontend test for reprint toast suppression.
 - **Background Dispatch Test Coverage** — Added 5 backend unit tests for dispatch cancel races (single-lock TOCTOU fix), batch counter reset re-check, and job lifecycle. Added 2 FTP regression tests for voidresp error handling (upload-loop prevention) and A1 model voidresp skip. Added 1 frontend test for reprint toast suppression.
 - **Tray Change Split Test Coverage** — Added 8 MQTT unit tests for `tray_change_log` lifecycle (default empty, seed on print start, clear on new print, record during RUNNING/PAUSE, ignore during IDLE, deduplicate, multi-change history). Added 6 usage tracker unit tests for weight splitting (per-layer gcode split, linear fallback, no-change normal path, empty log recovery, missing spool skip, triple segment split).
 - **Tray Change Split Test Coverage** — Added 8 MQTT unit tests for `tray_change_log` lifecycle (default empty, seed on print start, clear on new print, record during RUNNING/PAUSE, ignore during IDLE, deduplicate, multi-change history). Added 6 usage tracker unit tests for weight splitting (per-layer gcode split, linear fallback, no-change normal path, empty log recovery, missing spool skip, triple segment split).
+- **Developer Mode Detection Test Coverage** — Added 7 backend unit tests for MQTT `fun` field parsing (bit clear/set detection, exact bit check, invalid hex handling, state persistence across messages). Added 4 frontend tests for the warning banner (single/multiple printer names, hidden when empty, "How to enable" link).
 - **Frontend Pre-Commit Hooks** ([#458](https://github.com/maziggy/bambuddy/issues/458)) — Added `frontend-typecheck` (`tsc --noEmit`) and `frontend-lint` (`eslint .`) hooks to the pre-commit config. Both hooks only trigger when `frontend/src/**/*.{ts,tsx}` files are staged.
 - **Frontend Pre-Commit Hooks** ([#458](https://github.com/maziggy/bambuddy/issues/458)) — Added `frontend-typecheck` (`tsc --noEmit`) and `frontend-lint` (`eslint .`) hooks to the pre-commit config. Both hooks only trigger when `frontend/src/**/*.{ts,tsx}` files are staged.
 
 
 ## [0.2.1b] - 2026-02-19
 ## [0.2.1b] - 2026-02-19

+ 24 - 0
backend/app/api/routes/printers.py

@@ -91,6 +91,29 @@ async def list_usb_cameras(
     return {"cameras": cameras}
     return {"cameras": cameras}
 
 
 
 
+@router.get("/developer-mode-warnings")
+async def get_developer_mode_warnings(
+    _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_READ),
+    db: AsyncSession = Depends(get_db),
+):
+    """Check if any connected printer lacks developer LAN mode."""
+    result = await db.execute(select(Printer).where(Printer.is_active == True))  # noqa: E712
+    printers = result.scalars().all()
+    statuses = printer_manager.get_all_statuses()
+
+    warnings = []
+    for printer in printers:
+        state = statuses.get(printer.id)
+        if state and state.connected and state.developer_mode is False:
+            warnings.append(
+                {
+                    "printer_id": printer.id,
+                    "name": printer.name,
+                }
+            )
+    return warnings
+
+
 @router.get("/{printer_id}", response_model=PrinterResponse)
 @router.get("/{printer_id}", response_model=PrinterResponse)
 async def get_printer(
 async def get_printer(
     printer_id: int,
     printer_id: int,
@@ -467,6 +490,7 @@ async def get_printer_status(
         big_fan2_speed=state.big_fan2_speed,
         big_fan2_speed=state.big_fan2_speed,
         heatbreak_fan_speed=state.heatbreak_fan_speed,
         heatbreak_fan_speed=state.heatbreak_fan_speed,
         firmware_version=state.firmware_version,
         firmware_version=state.firmware_version,
+        developer_mode=state.developer_mode if state else None,
         plate_cleared=printer_manager.is_plate_cleared(printer_id),
         plate_cleared=printer_manager.is_plate_cleared(printer_id),
     )
     )
 
 

+ 1 - 0
backend/app/api/routes/support.py

@@ -495,6 +495,7 @@ async def _collect_support_info() -> dict:
                     "external_camera_configured": bool(printer.external_camera_url),
                     "external_camera_configured": bool(printer.external_camera_url),
                     "plate_detection_enabled": printer.plate_detection_enabled,
                     "plate_detection_enabled": printer.plate_detection_enabled,
                     "hms_error_count": len(state.hms_errors) if state else 0,
                     "hms_error_count": len(state.hms_errors) if state else 0,
+                    "developer_mode": state.developer_mode if state else None,
                     "nozzle_rack_count": len(state.nozzle_rack) if state else 0,
                     "nozzle_rack_count": len(state.nozzle_rack) if state else 0,
                 }
                 }
             )
             )

+ 2 - 0
backend/app/schemas/printer.py

@@ -245,5 +245,7 @@ class PrinterStatus(BaseModel):
     heatbreak_fan_speed: int | None = None  # Hotend heatbreak fan
     heatbreak_fan_speed: int | None = None  # Hotend heatbreak fan
     # Firmware version (from info.module[name="ota"].sw_ver)
     # Firmware version (from info.module[name="ota"].sw_ver)
     firmware_version: str | None = None
     firmware_version: str | None = None
+    # Developer LAN mode: True = enabled, False = disabled (MQTT encryption), None = unknown
+    developer_mode: bool | None = None
     # Queue: user has acknowledged plate is cleared for next queued print
     # Queue: user has acknowledged plate is cleared for next queued print
     plate_cleared: bool = False
     plate_cleared: bool = False

+ 11 - 0
backend/app/services/bambu_mqtt.py

@@ -168,6 +168,9 @@ class PrinterState:
     tray_change_log: list = field(default_factory=list)
     tray_change_log: list = field(default_factory=list)
     # Firmware version info (from info.module[name="ota"].sw_ver)
     # Firmware version info (from info.module[name="ota"].sw_ver)
     firmware_version: str | None = None
     firmware_version: str | None = None
+    # Developer LAN mode: parsed from MQTT "fun" field bit 0x20000000
+    # True = dev mode ON (no encryption), False = dev mode OFF (encryption required), None = unknown
+    developer_mode: bool | None = None
 
 
 
 
 # Stage name mapping from BambuStudio DeviceManager.cpp
 # Stage name mapping from BambuStudio DeviceManager.cpp
@@ -2051,6 +2054,14 @@ class BambuMQTTClient:
         ams_extruder_map_data = self.state.raw_data.get("ams_extruder_map")
         ams_extruder_map_data = self.state.raw_data.get("ams_extruder_map")
         mapping_data = self.state.raw_data.get("mapping")
         mapping_data = self.state.raw_data.get("mapping")
         self.state.raw_data = data
         self.state.raw_data = data
+
+        # Parse developer LAN mode from "fun" field
+        if "fun" in data:
+            try:
+                fun_int = int(data["fun"], 16)
+                self.state.developer_mode = (fun_int & 0x20000000) == 0
+            except (ValueError, TypeError):
+                pass
         if ams_data is not None:
         if ams_data is not None:
             self.state.raw_data["ams"] = ams_data
             self.state.raw_data["ams"] = ams_data
         if vt_tray_data is not None:
         if vt_tray_data is not None:

+ 113 - 0
backend/tests/unit/services/test_bambu_mqtt.py

@@ -2190,3 +2190,116 @@ class TestTrayChangeLog:
             mqtt_client.state.last_loaded_tray = tn
             mqtt_client.state.last_loaded_tray = tn
 
 
         assert mqtt_client.state.tray_change_log == [(0, 0), (1, 50), (3, 120), (0, 200)]
         assert mqtt_client.state.tray_change_log == [(0, 0), (1, 50), (3, 120), (0, 200)]
+
+
+class TestDeveloperModeDetection:
+    """Tests for developer LAN mode detection from MQTT 'fun' field."""
+
+    @pytest.fixture
+    def mqtt_client(self):
+        """Create a BambuMQTTClient instance for testing."""
+        from backend.app.services.bambu_mqtt import BambuMQTTClient
+
+        client = BambuMQTTClient(
+            ip_address="192.168.1.100",
+            serial_number="TEST123",
+            access_code="12345678",
+        )
+        return client
+
+    def test_developer_mode_initially_none(self, mqtt_client):
+        """Verify developer_mode starts as None (unknown)."""
+        assert mqtt_client.state.developer_mode is None
+
+    def test_developer_mode_on_when_bit_clear(self, mqtt_client):
+        """Verify developer_mode is True when bit 0x20000000 is clear."""
+        # Bit 29 clear in lower 32 bits = developer mode ON
+        payload = {
+            "print": {
+                "gcode_state": "IDLE",
+                "fun": "1C8187FF9CFF",
+            }
+        }
+        mqtt_client._process_message(payload)
+        assert mqtt_client.state.developer_mode is True
+
+    def test_developer_mode_off_when_bit_set(self, mqtt_client):
+        """Verify developer_mode is False when bit 0x20000000 is set."""
+        # Bit 29 set in lower 32 bits = developer mode OFF (encryption required)
+        payload = {
+            "print": {
+                "gcode_state": "IDLE",
+                "fun": "1C81A7FF9CFF",
+            }
+        }
+        mqtt_client._process_message(payload)
+        assert mqtt_client.state.developer_mode is False
+
+    def test_developer_mode_exact_bit_check(self, mqtt_client):
+        """Verify only bit 0x20000000 matters, not other bits."""
+        # 0x20000000 in hex = bit 29. Set ONLY that bit.
+        payload = {
+            "print": {
+                "gcode_state": "IDLE",
+                "fun": "000020000000",
+            }
+        }
+        mqtt_client._process_message(payload)
+        assert mqtt_client.state.developer_mode is False
+
+        # All zeros = all bits clear = developer mode ON
+        payload["print"]["fun"] = "000000000000"
+        mqtt_client._process_message(payload)
+        assert mqtt_client.state.developer_mode is True
+
+    def test_developer_mode_invalid_fun_ignored(self, mqtt_client):
+        """Verify invalid fun values don't crash or change state."""
+        mqtt_client.state.developer_mode = True
+
+        payload = {
+            "print": {
+                "gcode_state": "IDLE",
+                "fun": "not_a_hex_value",
+            }
+        }
+        mqtt_client._process_message(payload)
+        # Should remain unchanged
+        assert mqtt_client.state.developer_mode is True
+
+    def test_developer_mode_missing_fun_preserves_state(self, mqtt_client):
+        """Verify messages without fun field don't reset developer_mode."""
+        mqtt_client.state.developer_mode = False
+
+        payload = {
+            "print": {
+                "gcode_state": "RUNNING",
+                "mc_percent": 50,
+            }
+        }
+        mqtt_client._process_message(payload)
+        assert mqtt_client.state.developer_mode is False
+
+    def test_developer_mode_persists_across_messages(self, mqtt_client):
+        """Verify developer_mode set by fun persists across messages without fun."""
+        # First message sets developer_mode
+        mqtt_client._process_message(
+            {
+                "print": {
+                    "gcode_state": "IDLE",
+                    "fun": "3EC1AFFF9CFF",
+                }
+            }
+        )
+        assert mqtt_client.state.developer_mode is False
+
+        # Subsequent messages without fun don't change it
+        for _ in range(3):
+            mqtt_client._process_message(
+                {
+                    "print": {
+                        "gcode_state": "RUNNING",
+                        "mc_percent": 50,
+                    }
+                }
+            )
+        assert mqtt_client.state.developer_mode is False

+ 71 - 0
frontend/src/__tests__/components/Layout.test.tsx

@@ -53,6 +53,9 @@ describe('Layout', () => {
       }),
       }),
       http.get('/api/v1/auth/status', () => {
       http.get('/api/v1/auth/status', () => {
         return HttpResponse.json({ auth_enabled: false, requires_setup: false });
         return HttpResponse.json({ auth_enabled: false, requires_setup: false });
+      }),
+      http.get('/api/v1/printers/developer-mode-warnings', () => {
+        return HttpResponse.json([]);
       })
       })
     );
     );
   });
   });
@@ -184,4 +187,72 @@ describe('Layout', () => {
       });
       });
     });
     });
   });
   });
+
+  describe('developer mode warning banner', () => {
+    it('shows warning banner when printers lack developer mode', async () => {
+      server.use(
+        http.get('/api/v1/printers/developer-mode-warnings', () => {
+          return HttpResponse.json([
+            { printer_id: 1, name: 'X1 Carbon' },
+          ]);
+        })
+      );
+
+      render(<Layout />);
+
+      await waitFor(() => {
+        expect(document.body.textContent).toContain('Developer LAN mode is not enabled on');
+        expect(document.body.textContent).toContain('X1 Carbon');
+      });
+    });
+
+    it('shows multiple printer names in warning banner', async () => {
+      server.use(
+        http.get('/api/v1/printers/developer-mode-warnings', () => {
+          return HttpResponse.json([
+            { printer_id: 1, name: 'X1 Carbon' },
+            { printer_id: 2, name: 'P1S' },
+          ]);
+        })
+      );
+
+      render(<Layout />);
+
+      await waitFor(() => {
+        expect(document.body.textContent).toContain('X1 Carbon');
+        expect(document.body.textContent).toContain('P1S');
+      });
+    });
+
+    it('hides warning banner when no printers lack developer mode', async () => {
+      // Default handler returns empty array
+      render(<Layout />);
+
+      await waitFor(() => {
+        const sidebar = document.querySelector('aside');
+        expect(sidebar).toBeInTheDocument();
+      });
+
+      // Banner should not be present
+      expect(document.body.textContent).not.toContain('Developer LAN mode is not enabled on');
+    });
+
+    it('shows how to enable link in warning banner', async () => {
+      server.use(
+        http.get('/api/v1/printers/developer-mode-warnings', () => {
+          return HttpResponse.json([
+            { printer_id: 1, name: 'X1 Carbon' },
+          ]);
+        })
+      );
+
+      render(<Layout />);
+
+      await waitFor(() => {
+        expect(document.body.textContent).toContain('How to enable');
+        const link = document.querySelector('a[href*="enable-developer-mode"]');
+        expect(link).toBeInTheDocument();
+      });
+    });
+  });
 });
 });

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

@@ -250,6 +250,8 @@ export interface PrinterStatus {
   big_fan2_speed: number | null;     // Chamber/exhaust fan
   big_fan2_speed: number | null;     // Chamber/exhaust fan
   heatbreak_fan_speed: number | null; // Hotend heatbreak fan
   heatbreak_fan_speed: number | null; // Hotend heatbreak fan
   firmware_version: string | null;   // Firmware version from MQTT
   firmware_version: string | null;   // Firmware version from MQTT
+  // Developer LAN mode: true = enabled, false = disabled, null = unknown
+  developer_mode: boolean | null;
   // Queue: user has acknowledged plate is cleared for next queued print
   // Queue: user has acknowledged plate is cleared for next queued print
   plate_cleared: boolean;
   plate_cleared: boolean;
 }
 }
@@ -2292,6 +2294,8 @@ export const api = {
       `/printers/${id}?delete_archives=${deleteArchives}`,
       `/printers/${id}?delete_archives=${deleteArchives}`,
       { method: 'DELETE' }
       { method: 'DELETE' }
     ),
     ),
+  getDeveloperModeWarnings: () =>
+    request<{ printer_id: number; name: string }[]>('/printers/developer-mode-warnings'),
   getPrinterStatus: (id: number) =>
   getPrinterStatus: (id: number) =>
     request<PrinterStatus>(`/printers/${id}/status`),
     request<PrinterStatus>(`/printers/${id}/status`),
   refreshPrinterStatus: (id: number) =>
   refreshPrinterStatus: (id: number) =>

+ 28 - 1
frontend/src/components/Layout.tsx

@@ -1,6 +1,6 @@
 import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
 import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
 import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom';
 import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom';
-import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, FolderKanban, FolderOpen, X, Menu, Info, Plug, Bug, LogOut, Key, Loader2, Disc3, type LucideIcon } from 'lucide-react';
+import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, FolderKanban, FolderOpen, X, Menu, Info, Plug, Bug, LogOut, Key, Loader2, Disc3, ShieldAlert, type LucideIcon } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { useTheme } from '../contexts/ThemeContext';
 import { useTheme } from '../contexts/ThemeContext';
 import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
 import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
@@ -149,6 +149,15 @@ export function Layout() {
     refetchInterval: 60 * 1000, // Refresh every minute
     refetchInterval: 60 * 1000, // Refresh every minute
   });
   });
 
 
+  // Check developer LAN mode warnings
+  const { data: devModeWarnings } = useQuery({
+    queryKey: ['developer-mode-warnings'],
+    queryFn: api.getDeveloperModeWarnings,
+    staleTime: 10 * 1000,
+    refetchInterval: 30 * 1000,
+    refetchOnWindowFocus: true,
+  });
+
   // Fetch pending queue items count for badge
   // Fetch pending queue items count for badge
   const { data: queueItems } = useQuery({
   const { data: queueItems } = useQuery({
     queryKey: ['queue', 'pending'],
     queryKey: ['queue', 'pending'],
@@ -807,6 +816,24 @@ export function Layout() {
             </div>
             </div>
           </div>
           </div>
         )}
         )}
+        {devModeWarnings && devModeWarnings.length > 0 && (
+          <div className="bg-orange-500/20 border-b border-orange-500/30 px-4 py-2 flex items-center justify-between">
+            <div className="flex items-center gap-2 text-sm">
+              <ShieldAlert className="w-4 h-4 text-orange-500" />
+              <span className="text-orange-200">
+                {t('printers.developerModeWarning', {
+                  names: devModeWarnings.map(w => w.name).join(', '),
+                  defaultValue: `Developer LAN mode is not enabled on: ${devModeWarnings.map(w => w.name).join(', ')}. Some features may not work.`
+                })}
+              </span>
+              <a href="https://wiki.bambulab.com/en/knowledge-sharing/enable-developer-mode"
+                 target="_blank" rel="noopener noreferrer"
+                 className="text-orange-400 hover:text-orange-300 font-medium underline ml-2">
+                {t('printers.howToEnable', { defaultValue: 'How to enable' })}
+              </a>
+            </div>
+          </div>
+        )}
         {/* Persistent update banner */}
         {/* Persistent update banner */}
         {showUpdateBanner && (
         {showUpdateBanner && (
           <div className="bg-bambu-green/20 border-b border-bambu-green/30 px-4 py-2 flex items-center justify-between">
           <div className="bg-bambu-green/20 border-b border-bambu-green/30 px-4 py-2 flex items-center justify-between">

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

@@ -459,6 +459,8 @@ export default {
       height: 'Höhe',
       height: 'Höhe',
       instruction: 'Passen Sie den Erkennungsbereich an, um sich auf die Druckplatte zu konzentrieren. Der grüne Rahmen in der Vorschau zeigt den aktuellen Bereich.',
       instruction: 'Passen Sie den Erkennungsbereich an, um sich auf die Druckplatte zu konzentrieren. Der grüne Rahmen in der Vorschau zeigt den aktuellen Bereich.',
     },
     },
+    developerModeWarning: 'Der Entwickler-LAN-Modus ist nicht aktiviert auf: {{names}}. Einige Funktionen funktionieren möglicherweise nicht.',
+    howToEnable: 'Aktivieren',
   },
   },
 
 
   // Archives page
   // Archives page

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

@@ -459,6 +459,8 @@ export default {
       height: 'Height',
       height: 'Height',
       instruction: 'Adjust the detection area to focus on the build plate. The green box in the preview shows the current area.',
       instruction: 'Adjust the detection area to focus on the build plate. The green box in the preview shows the current area.',
     },
     },
+    developerModeWarning: 'Developer LAN mode is not enabled on: {{names}}. Some features may not work.',
+    howToEnable: 'How to enable',
   },
   },
 
 
   // Archives page
   // Archives page

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

@@ -459,6 +459,8 @@ export default {
       height: 'Hauteur',
       height: 'Hauteur',
       instruction: 'Ajustez le cadre vert pour cibler le plateau.',
       instruction: 'Ajustez le cadre vert pour cibler le plateau.',
     },
     },
+    developerModeWarning: 'Le mode développeur LAN n\'est pas activé sur : {{names}}. Certaines fonctionnalités peuvent ne pas fonctionner.',
+    howToEnable: 'Comment activer',
   },
   },
 
 
   // Archives page
   // Archives page

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

@@ -450,6 +450,8 @@ export default {
       height: 'Altezza',
       height: 'Altezza',
       instruction: 'Regola l\'area di rilevamento per focalizzare il piatto. Il riquadro verde mostra l\'area corrente.',
       instruction: 'Regola l\'area di rilevamento per focalizzare il piatto. Il riquadro verde mostra l\'area corrente.',
     },
     },
+    developerModeWarning: 'La modalità sviluppatore LAN non è attivata su: {{names}}. Alcune funzionalità potrebbero non funzionare.',
+    howToEnable: 'Come attivare',
   },
   },
 
 
   // Archives page
   // Archives page

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

@@ -466,6 +466,8 @@ export default {
     openCameraWindow: 'カメラを新しいウィンドウで開く',
     openCameraWindow: 'カメラを新しいウィンドウで開く',
     firmwareUpdateButton: 'アップデート',
     firmwareUpdateButton: 'アップデート',
     clickToViewHmsErrors: 'クリックしてHMSエラーを表示',
     clickToViewHmsErrors: 'クリックしてHMSエラーを表示',
+    developerModeWarning: '開発者LANモードが有効になっていません: {{names}}。一部の機能が動作しない可能性があります。',
+    howToEnable: '有効化方法',
   },
   },
   archives: {
   archives: {
     title: '印刷アーカイブ',
     title: '印刷アーカイブ',

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

@@ -459,6 +459,8 @@ export default {
       height: 'Altura',
       height: 'Altura',
       instruction: 'Ajuste a área de detecção para focar na placa de construção. A caixa verde na pré-visualização mostra a área atual.',
       instruction: 'Ajuste a área de detecção para focar na placa de construção. A caixa verde na pré-visualização mostra a área atual.',
     },
     },
+    developerModeWarning: 'O modo desenvolvedor LAN não está ativado em: {{names}}. Alguns recursos podem não funcionar.',
+    howToEnable: 'Como ativar',
   },
   },
 
 
   // Archives page
   // Archives page

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/index-BG7oML9S.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-CKMY9yxc.js"></script>
+    <script type="module" crossorigin src="/assets/index-BG7oML9S.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DJax8qcY.css">
     <link rel="stylesheet" crossorigin href="/assets/index-DJax8qcY.css">
   </head>
   </head>
   <body>
   <body>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů