Explorar o código

Fix SpoolBuddy inventory not updating on spool changes (#905)

  Spool CRUD endpoints (create, bulk create, update, delete, archive,
  restore) did not emit websocket events, so SpoolBuddy Dashboard and
  other tabs relying on event-driven cache invalidation never refreshed.

  All inventory mutation endpoints now broadcast an `inventory_changed`
  websocket event. Frontend handles it by invalidating `inventory-spools`.
maziggy hai 1 mes
pai
achega
4c052e04e1

+ 1 - 0
CHANGELOG.md

@@ -33,6 +33,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **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.
 - **Ghost Jobs From SQLite Lock on Print Completion** ([#897](https://github.com/maziggy/bambuddy/issues/897)) — When a print finished, the queue status update (`printing` → `completed`) could fail silently if the SQLite database was locked by another writer (e.g. the runtime tracker). The failed commit left the job permanently stuck in `printing` status — a "ghost job" that caused the UI to show false double-assignments when the next job started. The critical queue status commit now retries up to 3 times with backoff on SQLite lock errors (PostgreSQL is unaffected — it uses row-level locking). Additionally, the runtime tracker was holding a single long transaction across all printers; it now commits per-printer to minimize lock hold time.
 - **Multi-Plug Automation Only Works for First Plug** ([#903](https://github.com/maziggy/bambuddy/issues/903)) — When multiple smart plugs were assigned to the same printer (e.g. a TUYA printer plug and a particle filter plug via Home Assistant), only the first plug's automation worked. The auto-on at print start, auto-off at print completion, and queue auto-off all queried for a single plug instead of iterating all plugs linked to the printer. All automation paths now control every assigned plug. Also fixed the queue auto-off path which was hardcoded to Tasmota instead of using the correct service for the plug type (HA, MQTT, REST).
+- **SpoolBuddy Inventory Not Updating on Spool Changes** — Adding, editing, deleting, archiving, or restoring a spool in the internal inventory did not update SpoolBuddy's frontend views until the next manual refresh or 30-second poll. The spool CRUD endpoints did not emit websocket events, and the SpoolBuddy Dashboard had no polling fallback. All inventory mutation endpoints now broadcast an `inventory_changed` websocket event, and the frontend invalidates the spool cache on receipt — so SpoolBuddy (and all other tabs) reflect changes instantly.
 - **AMS Slot Changes Fail Until Reconnect** ([#887](https://github.com/maziggy/bambuddy/issues/887)) — After a keep-alive timeout, paho-mqtt auto-reconnects but the new session can be half-broken: the printer continues sending status updates but silently ignores commands. The developer mode probe detected this (no response, leaving `developer_mode` as `null`), but had no timeout or recovery — one unanswered probe permanently blocked retries. Added a 10-second probe timeout with one retry; after two consecutive unanswered probes, Bambuddy force-closes the socket to trigger a clean reconnect with a fresh session. Additionally, the developer mode probe was firing on every auto-reconnect, which destabilized some firmware MQTT brokers (A1/P1 series) — causing a reconnect → probe → disconnect feedback loop. The probe result is now cached across reconnects and only runs once on the first connection, with a 5-second delay after connect to let the session stabilize.
 - **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.
 

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

@@ -522,6 +522,7 @@ async def create_spool(
     await db.commit()
     await db.refresh(spool)
     result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool.id))
+    await ws_manager.broadcast({"type": "inventory_changed"})
     return result.scalar_one()
 
 
@@ -540,6 +541,7 @@ async def bulk_create_spools(
     await db.commit()
     ids = [s.id for s in spools]
     result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id.in_(ids)))
+    await ws_manager.broadcast({"type": "inventory_changed"})
     return list(result.scalars().all())
 
 
@@ -566,6 +568,7 @@ async def update_spool(
 
     await db.commit()
     result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool_id))
+    await ws_manager.broadcast({"type": "inventory_changed"})
     return result.scalar_one()
 
 
@@ -583,6 +586,7 @@ async def delete_spool(
 
     await db.delete(spool)
     await db.commit()
+    await ws_manager.broadcast({"type": "inventory_changed"})
     return {"status": "deleted"}
 
 
@@ -603,6 +607,7 @@ async def archive_spool(
     spool.archived_at = datetime.now(timezone.utc)
     await db.commit()
     result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool_id))
+    await ws_manager.broadcast({"type": "inventory_changed"})
     return result.scalar_one()
 
 
@@ -621,6 +626,7 @@ async def restore_spool(
     spool.archived_at = None
     await db.commit()
     result = await db.execute(select(Spool).options(selectinload(Spool.k_profiles)).where(Spool.id == spool_id))
+    await ws_manager.broadcast({"type": "inventory_changed"})
     return result.scalar_one()
 
 

+ 5 - 0
frontend/src/hooks/useWebSocket.ts

@@ -263,6 +263,11 @@ export function useWebSocket() {
         }));
         break;
 
+      case 'inventory_changed':
+        // Spool created/updated/deleted/archived/restored - refresh inventory across all tabs
+        debouncedInvalidate('inventory-spools');
+        break;
+
       case 'spool_assignment_changed':
         // Spool assigned/unassigned - refresh assignment data across all tabs
         debouncedInvalidate('spool-assignments');

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/index-CflptLB_.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-q3PKuEzO.js"></script>
+    <script type="module" crossorigin src="/assets/index-CflptLB_.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-Zgcm6AwS.css">
   </head>
   <body>

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