mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
Add virtual printer Queue mode and sidebar badge indicators
Virtual Printer Changes: - Add new "Queue" mode that archives files and adds them to print queue - Rename modes: "Immediate" → "Archive", "Queue for Review" → "Review" - Three modes now: Archive (immediate), Review (pending), Queue (print queue) - Queue mode creates unassigned queue items (printer_id=null) Print Queue Changes: - Make printer_id nullable in PrintQueueItem model/schema - Add support for unassigned queue items (no printer assigned) - Queue page shows "Unassigned" filter option - Edit modal allows assigning printer to unassigned items - Unassigned items highlighted in orange Sidebar Badge Indicators: - Queue icon shows yellow badge with pending queue item count - Archive icon shows blue badge with pending upload count - Badges auto-update every 5 seconds and on window focus Other: - Update backup/restore to handle nullable printer_id and ams_mapping - Add database migration for nullable printer_id column - Update VirtualPrinterSettings tests for new modes - Update virtual printer API integration tests - Remove speed controls documentation from wiki (not implemented)
This commit is contained in:
@@ -64,7 +64,7 @@ def _enrich_response(item: PrintQueueItem) -> PrintQueueItemResponse:
|
||||
|
||||
@router.get("/", response_model=list[PrintQueueItemResponse])
|
||||
async def list_queue(
|
||||
printer_id: int | None = Query(None, description="Filter by printer"),
|
||||
printer_id: int | None = Query(None, description="Filter by printer (-1 for unassigned)"),
|
||||
status: str | None = Query(None, description="Filter by status"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -72,11 +72,15 @@ async def list_queue(
|
||||
query = (
|
||||
select(PrintQueueItem)
|
||||
.options(selectinload(PrintQueueItem.archive), selectinload(PrintQueueItem.printer))
|
||||
.order_by(PrintQueueItem.printer_id, PrintQueueItem.position)
|
||||
.order_by(PrintQueueItem.printer_id.nulls_first(), PrintQueueItem.position)
|
||||
)
|
||||
|
||||
if printer_id is not None:
|
||||
query = query.where(PrintQueueItem.printer_id == printer_id)
|
||||
if printer_id == -1:
|
||||
# Special value: filter for unassigned items
|
||||
query = query.where(PrintQueueItem.printer_id.is_(None))
|
||||
else:
|
||||
query = query.where(PrintQueueItem.printer_id == printer_id)
|
||||
if status:
|
||||
query = query.where(PrintQueueItem.status == status)
|
||||
|
||||
@@ -91,22 +95,31 @@ async def add_to_queue(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Add an item to the print queue."""
|
||||
# Validate printer exists
|
||||
result = await db.execute(select(Printer).where(Printer.id == data.printer_id))
|
||||
if not result.scalar_one_or_none():
|
||||
raise HTTPException(400, "Printer not found")
|
||||
# Validate printer exists (if assigned)
|
||||
if data.printer_id is not None:
|
||||
result = await db.execute(select(Printer).where(Printer.id == data.printer_id))
|
||||
if not result.scalar_one_or_none():
|
||||
raise HTTPException(400, "Printer not found")
|
||||
|
||||
# Validate archive exists
|
||||
result = await db.execute(select(PrintArchive).where(PrintArchive.id == data.archive_id))
|
||||
if not result.scalar_one_or_none():
|
||||
raise HTTPException(400, "Archive not found")
|
||||
|
||||
# Get next position for this printer
|
||||
result = await db.execute(
|
||||
select(func.max(PrintQueueItem.position))
|
||||
.where(PrintQueueItem.printer_id == data.printer_id)
|
||||
.where(PrintQueueItem.status == "pending")
|
||||
)
|
||||
# Get next position for this printer (or for unassigned items)
|
||||
if data.printer_id is not None:
|
||||
result = await db.execute(
|
||||
select(func.max(PrintQueueItem.position))
|
||||
.where(PrintQueueItem.printer_id == data.printer_id)
|
||||
.where(PrintQueueItem.status == "pending")
|
||||
)
|
||||
else:
|
||||
# For unassigned items, get max position across all unassigned
|
||||
result = await db.execute(
|
||||
select(func.max(PrintQueueItem.position))
|
||||
.where(PrintQueueItem.printer_id.is_(None))
|
||||
.where(PrintQueueItem.status == "pending")
|
||||
)
|
||||
max_pos = result.scalar() or 0
|
||||
|
||||
item = PrintQueueItem(
|
||||
@@ -127,7 +140,7 @@ async def add_to_queue(
|
||||
# Load relationships for response
|
||||
await db.refresh(item, ["archive", "printer"])
|
||||
|
||||
logger.info(f"Added archive {data.archive_id} to queue for printer {data.printer_id}")
|
||||
logger.info(f"Added archive {data.archive_id} to queue for printer {data.printer_id or 'unassigned'}")
|
||||
|
||||
# MQTT relay - publish queue job added
|
||||
try:
|
||||
@@ -176,8 +189,8 @@ async def update_queue_item(
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
|
||||
# Validate new printer_id if being changed
|
||||
if "printer_id" in update_data:
|
||||
# Validate new printer_id if being changed (and not None)
|
||||
if "printer_id" in update_data and update_data["printer_id"] is not None:
|
||||
result = await db.execute(select(Printer).where(Printer.id == update_data["printer_id"]))
|
||||
if not result.scalar_one_or_none():
|
||||
raise HTTPException(400, "Printer not found")
|
||||
|
||||
@@ -513,7 +513,7 @@ async def export_backup(
|
||||
for qi in queue_items:
|
||||
backup["print_queue"].append(
|
||||
{
|
||||
"printer_serial": printer_id_to_serial.get(qi.printer_id),
|
||||
"printer_serial": printer_id_to_serial.get(qi.printer_id) if qi.printer_id else None,
|
||||
"archive_hash": archive_id_to_hash.get(qi.archive_id),
|
||||
"project_name": project_id_to_name.get(qi.project_id) if qi.project_id else None,
|
||||
"position": qi.position,
|
||||
@@ -521,6 +521,7 @@ async def export_backup(
|
||||
"require_previous_success": qi.require_previous_success,
|
||||
"auto_off_after": qi.auto_off_after,
|
||||
"manual_start": qi.manual_start,
|
||||
"ams_mapping": qi.ams_mapping,
|
||||
"status": qi.status,
|
||||
"started_at": qi.started_at.isoformat() if qi.started_at else None,
|
||||
"completed_at": qi.completed_at.isoformat() if qi.completed_at else None,
|
||||
@@ -1519,32 +1520,44 @@ async def import_backup(
|
||||
skipped_details["print_queue"] = []
|
||||
|
||||
for qi_data in backup["print_queue"]:
|
||||
printer_serial = qi_data.get("printer_serial")
|
||||
printer_serial = qi_data.get("printer_serial") # Can be None for unassigned items
|
||||
archive_hash = qi_data.get("archive_hash")
|
||||
|
||||
if not printer_serial or not archive_hash:
|
||||
# Archive is required, but printer can be None (unassigned)
|
||||
if not archive_hash:
|
||||
skipped["print_queue"] += 1
|
||||
continue
|
||||
|
||||
printer_id = printer_serial_to_id.get(printer_serial)
|
||||
# Look up printer_id (None if unassigned or printer not found)
|
||||
printer_id = printer_serial_to_id.get(printer_serial) if printer_serial else None
|
||||
archive_id = archive_hash_to_id.get(archive_hash)
|
||||
|
||||
if not printer_id or not archive_id:
|
||||
# Archive must exist, but printer is optional (unassigned items)
|
||||
if not archive_id:
|
||||
skipped["print_queue"] += 1
|
||||
skipped_details["print_queue"].append(f"{printer_serial}/{archive_hash[:8] if archive_hash else 'N/A'}")
|
||||
skipped_details["print_queue"].append(
|
||||
f"{printer_serial or 'unassigned'}/{archive_hash[:8] if archive_hash else 'N/A'}"
|
||||
)
|
||||
continue
|
||||
|
||||
# If printer_serial was specified but printer not found, skip
|
||||
if printer_serial and not printer_id:
|
||||
skipped["print_queue"] += 1
|
||||
skipped_details["print_queue"].append(f"{printer_serial}/{archive_hash[:8]}")
|
||||
continue
|
||||
|
||||
project_name = qi_data.get("project_name")
|
||||
project_id = project_name_to_id.get(project_name) if project_name else None
|
||||
|
||||
qi = PrintQueueItem(
|
||||
printer_id=printer_id,
|
||||
printer_id=printer_id, # Can be None for unassigned items
|
||||
archive_id=archive_id,
|
||||
project_id=project_id,
|
||||
position=qi_data.get("position", 0),
|
||||
require_previous_success=qi_data.get("require_previous_success", False),
|
||||
auto_off_after=qi_data.get("auto_off_after", False),
|
||||
manual_start=qi_data.get("manual_start", False),
|
||||
ams_mapping=qi_data.get("ams_mapping"),
|
||||
status=qi_data.get("status", "pending"),
|
||||
error_message=qi_data.get("error_message"),
|
||||
)
|
||||
@@ -1887,11 +1900,16 @@ async def update_virtual_printer_settings(
|
||||
new_model = model if model is not None else current_model
|
||||
|
||||
# Validate mode
|
||||
if new_mode not in ("immediate", "queue"):
|
||||
# "review" is the new name for "queue" (pending review before archiving)
|
||||
# "print_queue" archives and adds to print queue (unassigned)
|
||||
if new_mode not in ("immediate", "queue", "review", "print_queue"):
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"detail": "Mode must be 'immediate' or 'queue'"},
|
||||
content={"detail": "Mode must be 'immediate', 'review', or 'print_queue'"},
|
||||
)
|
||||
# Normalize legacy "queue" to "review" for storage
|
||||
if new_mode == "queue":
|
||||
new_mode = "review"
|
||||
|
||||
# Validate model
|
||||
if model is not None and model not in VIRTUAL_PRINTER_MODELS:
|
||||
|
||||
Reference in New Issue
Block a user