Преглед изворни кода

Fix thumbnails returning 401 after backend restart

  Stream tokens are stored in-memory and lost on restart. The frontend
  cached the old token for up to 50 minutes (staleTime) and never
  refetched, so all thumbnail/stream requests failed with 401 until
  a manual page reload.

  Added a global error listener in useStreamTokenSync that detects
  when img/video elements fail to load with the current stream token
  and automatically invalidates the camera-stream-token query to
  fetch a fresh one. Debounced with a 5s cooldown to avoid spamming
  when many images fail at once.
maziggy пре 1 месец
родитељ
комит
0ece6725bd
4 измењених фајлова са 43 додато и 4 уклоњено
  1. 1 0
      CHANGELOG.md
  2. 41 3
      frontend/src/hooks/useCameraStreamToken.ts
  3. 0 0
      static/assets/index-CVTD1FD_.js
  4. 1 1
      static/index.html

+ 1 - 0
CHANGELOG.md

@@ -16,6 +16,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **REST Smart Plug: Separate Power/Energy URLs and Unit Multipliers** ([#472](https://github.com/maziggy/bambuddy/issues/472)) — REST/Webhook smart plugs can now use individual URLs for power and energy data instead of requiring all values in a single status response. Each value falls back to the shared Status URL when no separate URL is configured, so existing setups work without changes. Added power and energy multipliers for unit conversion (e.g., set energy multiplier to `0.001` to convert Wh to kWh). Useful for platforms like ioBroker that expose each data point as a separate API endpoint.
 
 ### Fixed
+- **Thumbnails Broken After Backend Restart** — Archive and library thumbnails returned 401 Unauthorized after a backend restart because stream tokens are stored in memory and lost on restart. The frontend now detects failed token-protected image loads and automatically refreshes the stream token, so thumbnails recover without a page reload.
 - **SpoolBuddy Kiosk Screen Blanks on Boot** — The touchscreen display would blank immediately after the RPi booted, requiring a touch to wake. Added `consoleblank=0` to the kernel cmdline to disable Linux console blanking during the Plymouth-to-labwc transition, and changed the `wlr-randr` anti-blank loop to fire immediately instead of sleeping 60 seconds first.
 - **Queue Widget Ignores Plate-Clear Setting** ([#752](https://github.com/maziggy/bambuddy/issues/752)) — The "Clear Plate & Start Next" button on printer cards appeared even when "Require plate-clear confirmation" was disabled in Settings → Queue. The backend correctly auto-dispatched without waiting, but the frontend widget always showed the prompt. The widget now respects the setting and shows a passive queue link instead when plate-clear confirmation is disabled.
 - **WebSocket Crash on Printers Without `fun` Field** ([#873](https://github.com/maziggy/bambuddy/issues/873)) — Connecting to printers that don't send the MQTT `fun` field (A1, P1 series, X1Plus firmware) caused a repeating `'str' object has no attribute 'get'` crash in the WebSocket handler, showing the printer as offline with missing AMS and SD card info. The developer mode probe introduced in 0.2.3b1 published an MQTT message inside `_update_state()` between overwriting `raw_data` with the full MQTT dict (where `vt_tray` is a raw dict) and restoring the previously normalized list — the `publish()` call released the GIL, letting the event loop read the un-normalized dict and iterate over string keys instead of spool dicts. Fixed by normalizing `vt_tray` dict→list in the MQTT data before assignment, and moving preserved field restoration before the probe. Added defensive normalization in `printer_state_to_dict` as a belt-and-suspenders guard.

+ 41 - 3
frontend/src/hooks/useCameraStreamToken.ts

@@ -1,6 +1,6 @@
-import { useEffect } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { api, setStreamToken, withStreamToken } from '../api/client';
+import { useEffect, useRef } from 'react';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { api, setStreamToken, getStreamToken, withStreamToken } from '../api/client';
 import { useAuth } from '../contexts/AuthContext';
 
 /**
@@ -8,11 +8,17 @@ import { useAuth } from '../contexts/AuthContext';
  * Stores the token globally via setStreamToken() so URL generators
  * in client.ts can use withStreamToken() automatically.
  *
+ * Also listens for global image load errors on token-protected URLs
+ * and automatically refreshes the token (e.g., after backend restart
+ * invalidates in-memory tokens).
+ *
  * Mount this hook once near the app root (e.g., in App.tsx or a layout component).
  * Components that need token-protected URLs can import withStreamToken directly.
  */
 export function useStreamTokenSync() {
   const { authEnabled } = useAuth();
+  const queryClient = useQueryClient();
+  const refreshingRef = useRef(false);
 
   const { data } = useQuery({
     queryKey: ['camera-stream-token'],
@@ -26,6 +32,38 @@ export function useStreamTokenSync() {
     setStreamToken(data?.token ?? null);
     return () => setStreamToken(null);
   }, [data?.token]);
+
+  // Listen for image/video load errors on token-protected URLs.
+  // When the backend restarts, in-memory stream tokens are lost and all
+  // thumbnail/stream requests return 401. This handler detects that and
+  // forces a token refresh so images recover without a page reload.
+  useEffect(() => {
+    if (!authEnabled) return;
+
+    const handleError = (event: Event) => {
+      const el = event.target;
+      if (!(el instanceof HTMLImageElement || el instanceof HTMLVideoElement)) return;
+
+      const src = el.src || '';
+      const token = getStreamToken();
+      if (!token || !src.includes(`token=${encodeURIComponent(token)}`)) return;
+
+      // This image/video used our stream token and failed — token likely invalid
+      if (refreshingRef.current) return;
+      refreshingRef.current = true;
+
+      queryClient.invalidateQueries({ queryKey: ['camera-stream-token'] });
+
+      // Reset after a delay so future errors can trigger another refresh
+      setTimeout(() => {
+        refreshingRef.current = false;
+      }, 5000);
+    };
+
+    // Use capture phase to catch errors before they're swallowed
+    document.addEventListener('error', handleError, true);
+    return () => document.removeEventListener('error', handleError, true);
+  }, [authEnabled, queryClient]);
 }
 
 /**

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/index-CVTD1FD_.js


+ 1 - 1
static/index.html

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

Неке датотеке нису приказане због велике количине промена