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`.
This commit is contained in:
maziggy
2026-04-07 12:32:44 +02:00
parent ed61e756a6
commit 4c052e04e1
5 changed files with 14 additions and 2 deletions
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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');
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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>