Browse Source

fix: queue stuck on "Busy" for "Any Model" jobs (#435)

When a print was queued with "Any [Model]", the queue widget only
queried items with printer_id=X after dispatch, missing the next
pending model-based item (printer_id IS NULL, target_model="P1S").
The "Clear Plate & Start Next" button never appeared, leaving the
scheduler stuck reporting "Busy".

Add optional target_model query parameter to GET /queue/. When
combined with printer_id, uses OR logic to also return unassigned
items whose target_model matches. The frontend now passes the
printer's model through PrinterQueueWidget to this query.
maziggy 3 months ago
parent
commit
5e6c04f303

+ 3 - 0
CHANGELOG.md

@@ -4,6 +4,9 @@ All notable changes to Bambuddy will be documented in this file.
 
 ## [0.2.1] - Unreleased
 
+### Fixed
+- **Queue Stuck on "Busy" for "Any Model" Jobs** ([#435](https://github.com/maziggy/bambuddy/issues/435)) — When a print was queued with "Any [Model]" (e.g., "Any P1S"), it was created with `printer_id=NULL` and `target_model="P1S"`. After the assigned printer finished, the queue widget queried only for items matching `printer_id=X`, missing the next pending model-based item (`printer_id IS NULL`). With no next item found, the "Clear Plate & Start Next" button never appeared, leaving the scheduler stuck reporting "Busy". The queue API now accepts an optional `target_model` parameter; when combined with `printer_id`, it uses OR logic to also return unassigned items whose `target_model` matches the printer's model. The frontend passes the printer's model through to this query.
+
 ## [0.2.1b] - 2026-02-19
 
 ### Fixed

+ 17 - 1
backend/app/api/routes/print_queue.py

@@ -8,7 +8,7 @@ from pathlib import Path
 
 import defusedxml.ElementTree as ET
 from fastapi import APIRouter, Depends, HTTPException, Query
-from sqlalchemy import func, select
+from sqlalchemy import and_, func, or_, select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
@@ -248,6 +248,9 @@ def _enrich_response(item: PrintQueueItem) -> PrintQueueItemResponse:
 async def list_queue(
     printer_id: int | None = Query(None, description="Filter by printer (-1 for unassigned)"),
     status: str | None = Query(None, description="Filter by status"),
+    target_model: str | None = Query(
+        None, description="Filter by target model (also includes model-based items when combined with printer_id)"
+    ),
     db: AsyncSession = Depends(get_db),
     _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_READ),
 ):
@@ -267,8 +270,21 @@ async def list_queue(
         if printer_id == -1:
             # Special value: filter for unassigned items
             query = query.where(PrintQueueItem.printer_id.is_(None))
+        elif target_model:
+            # Include both printer-specific items AND model-based (unassigned) items
+            query = query.where(
+                or_(
+                    PrintQueueItem.printer_id == printer_id,
+                    and_(
+                        PrintQueueItem.printer_id.is_(None),
+                        func.lower(PrintQueueItem.target_model) == target_model.lower(),
+                    ),
+                )
+            )
         else:
             query = query.where(PrintQueueItem.printer_id == printer_id)
+    elif target_model:
+        query = query.where(func.lower(PrintQueueItem.target_model) == target_model.lower())
     if status:
         query = query.where(PrintQueueItem.status == status)
 

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

@@ -3188,10 +3188,11 @@ export const api = {
     request<HASensorEntity[]>('/smart-plugs/ha/sensors'),
 
   // Print Queue
-  getQueue: (printerId?: number, status?: string) => {
+  getQueue: (printerId?: number, status?: string, targetModel?: string) => {
     const params = new URLSearchParams();
     if (printerId) params.set('printer_id', String(printerId));
     if (status) params.set('status', status);
+    if (targetModel) params.set('target_model', targetModel);
     return request<PrintQueueItem[]>(`/queue/?${params}`);
   },
   getQueueItem: (id: number) => request<PrintQueueItem>(`/queue/${id}`),

+ 4 - 3
frontend/src/components/PrinterQueueWidget.tsx

@@ -9,18 +9,19 @@ import { formatRelativeTime } from '../utils/date';
 
 interface PrinterQueueWidgetProps {
   printerId: number;
+  printerModel?: string | null;
   printerState?: string | null;
   plateCleared?: boolean;
 }
 
-export function PrinterQueueWidget({ printerId, printerState, plateCleared }: PrinterQueueWidgetProps) {
+export function PrinterQueueWidget({ printerId, printerModel, printerState, plateCleared }: PrinterQueueWidgetProps) {
   const { t } = useTranslation();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
   const { hasPermission } = useAuth();
   const { data: queue } = useQuery({
-    queryKey: ['queue', printerId, 'pending'],
-    queryFn: () => api.getQueue(printerId, 'pending'),
+    queryKey: ['queue', printerId, 'pending', printerModel],
+    queryFn: () => api.getQueue(printerId, 'pending', printerModel || undefined),
     refetchInterval: 30000,
   });
 

+ 1 - 1
frontend/src/pages/PrintersPage.tsx

@@ -2473,7 +2473,7 @@ function PrinterCard({
                 </div>
 
                 {/* Queue Widget - always visible when there are pending items */}
-                <PrinterQueueWidget printerId={printer.id} printerState={status.state} plateCleared={status.plate_cleared} />
+                <PrinterQueueWidget printerId={printer.id} printerModel={printer.model} printerState={status.state} plateCleared={status.plate_cleared} />
               </>
             )}
 

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-scckn0-v.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-jjif866q.js"></script>
+    <script type="module" crossorigin src="/assets/index-scckn0-v.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DPf6CLKV.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff