Преглед на файлове

fix(printers): equalize external tray height with regular AMS slots

    On dual-nozzle printers (H2C/H2D), the External card stacked a
    separate "Ext-L" / "Ext-R" caption below each tray to mark which
    extruder it fed. That caption appeared on the External card only,
    making the bottom row of the printer card's AMS panel visibly
    taller than the row above it.

    Fix: the L/R distinction now lives inside the slot's colour circle
    in place of the numeric index, and the bottom caption is removed.
    FilamentSlotCircle's slotNumber prop is widened to `number | string`
    to carry the letter. Single-nozzle externals (one tray, no L/R
    distinction) keep the numeric "1".

    The Ext-L / Ext-R strings still drive the slot's "location" label
    in the filament hover card, so detail context is preserved.
maziggy преди 2 месеца
родител
ревизия
00e4aed7af
променени са 76 файла, в които са добавени 12485 реда и са изтрити 4926 реда
  1. 0 0
      CHANGELOG.md
  2. 5 1
      CONTRIBUTING.md
  3. 2 2
      README.md
  4. 15 87
      backend/app/api/routes/archives.py
  5. 0 32
      backend/app/api/routes/background_dispatch.py
  6. 25 5
      backend/app/api/routes/camera.py
  7. 21 100
      backend/app/api/routes/library.py
  8. 122 16
      backend/app/api/routes/print_queue.py
  9. 28 1
      backend/app/api/routes/printers.py
  10. 2 4
      backend/app/api/routes/settings.py
  11. 0 9
      backend/app/api/routes/websocket.py
  12. 10 0
      backend/app/core/database.py
  13. 5 5
      backend/app/core/permissions.py
  14. 9009 0
      backend/app/data/hms_actions.json
  15. 0 7
      backend/app/main.py
  16. 4 0
      backend/app/models/print_queue.py
  17. 0 22
      backend/app/schemas/archive.py
  18. 0 27
      backend/app/schemas/library.py
  19. 31 1
      backend/app/schemas/print_queue.py
  20. 14 0
      backend/app/schemas/printer.py
  21. 0 1100
      backend/app/services/background_dispatch.py
  22. 228 1
      backend/app/services/bambu_mqtt.py
  23. 14 0
      backend/app/services/camera_fanout.py
  24. 75 0
      backend/app/services/hms_actions.py
  25. 60 13
      backend/app/services/print_scheduler.py
  26. 8 1
      backend/app/services/printer_manager.py
  27. 3 4
      backend/app/services/slice_dispatch.py
  28. 19 289
      backend/tests/integration/test_background_dispatch_api.py
  29. 33 0
      backend/tests/integration/test_camera_api.py
  30. 251 3
      backend/tests/integration/test_ownership_permissions.py
  31. 316 0
      backend/tests/integration/test_print_queue_api.py
  32. 97 0
      backend/tests/integration/test_printers_api.py
  33. 0 420
      backend/tests/unit/services/test_background_dispatch.py
  34. 0 721
      backend/tests/unit/services/test_background_dispatch_watchdog.py
  35. 3 4
      backend/tests/unit/services/test_bambu_mqtt.py
  36. 229 0
      backend/tests/unit/services/test_hms_actions.py
  37. 276 0
      backend/tests/unit/test_scheduler_cleanup_library.py
  38. 2 0
      frontend/scripts/check-i18n-parity.mjs
  39. 187 123
      frontend/src/__tests__/components/PrintModal.test.tsx
  40. 115 8
      frontend/src/__tests__/components/PrintModalDispatchToast.test.tsx
  41. 2 82
      frontend/src/__tests__/contexts/ToastContext.test.tsx
  42. 0 105
      frontend/src/__tests__/hooks/useDispatchedPrinterIds.test.ts
  43. 12 6
      frontend/src/__tests__/pages/FileManagerPage.test.tsx
  44. 6 6
      frontend/src/__tests__/pages/ProjectDetailPage.test.tsx
  45. 18 68
      frontend/src/api/client.ts
  46. 106 6
      frontend/src/components/CameraTile.tsx
  47. 62 9
      frontend/src/components/CameraWall.tsx
  48. 5 2
      frontend/src/components/FilamentSlotCircle.tsx
  49. 54 7
      frontend/src/components/HMSErrorModal.tsx
  50. 2 2
      frontend/src/components/PrintModal/PlateSelector.tsx
  51. 0 9
      frontend/src/components/PrintModal/PrinterSelector.tsx
  52. 43 22
      frontend/src/components/PrintModal/ScheduleOptions.tsx
  53. 122 227
      frontend/src/components/PrintModal/index.tsx
  54. 7 6
      frontend/src/components/PrintModal/types.ts
  55. 2 3
      frontend/src/components/spoolbuddy/SpoolBuddyLayout.tsx
  56. 24 532
      frontend/src/contexts/ToastContext.tsx
  57. 0 85
      frontend/src/hooks/useDispatchedPrinterIds.ts
  58. 0 8
      frontend/src/hooks/useWebSocket.ts
  59. 4 2
      frontend/src/i18n/index.ts
  60. 59 49
      frontend/src/i18n/locales/de.ts
  61. 59 49
      frontend/src/i18n/locales/en.ts
  62. 59 49
      frontend/src/i18n/locales/es.ts
  63. 59 49
      frontend/src/i18n/locales/fr.ts
  64. 59 49
      frontend/src/i18n/locales/it.ts
  65. 59 49
      frontend/src/i18n/locales/ja.ts
  66. 62 51
      frontend/src/i18n/locales/ko.ts
  67. 59 49
      frontend/src/i18n/locales/pt-BR.ts
  68. 59 48
      frontend/src/i18n/locales/tr.ts
  69. 59 49
      frontend/src/i18n/locales/zh-CN.ts
  70. 59 49
      frontend/src/i18n/locales/zh-TW.ts
  71. 25 59
      frontend/src/pages/ArchivesPage.tsx
  72. 13 94
      frontend/src/pages/FileManagerPage.tsx
  73. 27 7
      frontend/src/pages/PrintersPage.tsx
  74. 4 28
      frontend/src/pages/ProjectDetailPage.tsx
  75. 5 5
      frontend/src/pages/QueuePage.tsx
  76. 81 0
      scripts/update_hms_actions.py

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
CHANGELOG.md


+ 5 - 1
CONTRIBUTING.md

@@ -295,7 +295,11 @@ Permissions follow the `resource:action` pattern (e.g., `filaments:read`, `print
 | `update` | Modify existing resources |
 | `delete` | Remove resources |
 
-Some resources have additional actions (e.g., `printers:control` for start/stop, `printers:files` for file transfer).
+Some resources have additional actions. Examples: `printers:control` for live printer controls
+such as stop/pause/resume, `printers:files` for printer storage access, `queue:create` for
+creating queue items that may dispatch immediately when scheduled ASAP, `library:upload` for
+File Manager uploads/imports, and `archives:reprint_own` / `archives:reprint_all` for archive
+reprint eligibility. Archive reprint still needs `queue:create` before it can enqueue a job.
 
 ### Adding New Permissions
 

+ 2 - 2
README.md

@@ -168,7 +168,7 @@ Optional but recommended — drop the [`slicer-api/` Compose stack](slicer-api/R
 - Configurable drying presets per filament type (temperature & duration for AMS 2 Pro and AMS-HT)
 - **Per-filament humidity threshold** — Set a different humidity trigger per filament type (e.g. Nylon at 20%, PLA at 60%, ASA at 30%) instead of one global value. Mixed-material AMS units use the most-restrictive threshold across the loaded spools so a single PLA + Nylon unit triggers at Nylon's level. Drives both the auto-drying scheduler and the hourly humidity alarm so the two can never disagree on whether a unit is "too humid"
 - Dual external spool support for H2D (Ext-L / Ext-R)
-- HMS error monitoring with history and clear errors
+- **HMS error monitoring with one-click actions** — Live HMS error log with history and the same Resume / Stop / Continue / Retry / Check Assistant / Don't Remind Me action buttons BambuStudio shows. Click and the matching MQTT command goes back to the printer — no more walking to the device just to dismiss a paused-print dialog. Catalog covers every Bambu model (X1 / P1 / A1 / H2 series); buttons are translated in all 11 supported locales
 - **Heater history charts** — Bambuddy logs nozzle, bed, and chamber readings every minute and surfaces them via a tiny chart icon on each heater tile in the printer card. Click for a per-heater modal with current / average / min / max stats, target overlay, and a 6h / 24h / 48h / 7d time range — works on read-only chamber sensors (X1C / P2S) too. AMS humidity and temperature get the same treatment (already shipped).
 - Print success rates & trends
 - Filament usage tracking
@@ -178,7 +178,7 @@ Optional but recommended — drop the [`slicer-api/` Compose stack](slicer-api/R
 - CSV/Excel export
 
 ### ⏰ Scheduling & Automation
-- **Background print dispatch** — FTP uploads and print-start commands run in the background with real-time WebSocket progress toasts (per-job upload bars, status badges, cancel button)
+- **Unified dispatch through the queue** — Every print Bambuddy starts (File Manager, archive reprint, printer-card upload-and-print, scheduled queue items) flows through the same queue scheduler, so each print is visible on the queue page, attributable to the user that started it, deficit-checked, and cancellable from one place. FTP uploads and print-start commands run in the background with real-time WebSocket progress toasts (per-job upload bars, status badges, cancel button). Installations with custom groups or API keys: the immediate-print actions now require the `queue:create` permission alongside the existing `printers:control` — see [the permissions guide](https://wiki.bambuddy.cool/admin/permissions/) if you've granted control without queue-create
 - Print queue with three tabs (Queue / History / Timeline), multi-select drag-and-drop, batch grouping, and a Gantt-style timeline
 - Multi-printer selection (send to multiple printers at once)
 - Batch grouping — multi-plate prints auto-group into a collapsible row; any 2+ selected items can be grouped manually via "Group as batch", with ungroup on the batch parent

+ 15 - 87
backend/app/api/routes/archives.py

@@ -25,7 +25,7 @@ from backend.app.models.archive import PrintArchive
 from backend.app.models.filament import Filament
 from backend.app.models.spool_usage_history import SpoolUsageHistory
 from backend.app.models.user import User
-from backend.app.schemas.archive import ArchiveResponse, ArchiveSlim, ArchiveStats, ArchiveUpdate, ReprintRequest
+from backend.app.schemas.archive import ArchiveResponse, ArchiveSlim, ArchiveStats, ArchiveUpdate
 from backend.app.schemas.print_log import PrintLogResponse
 from backend.app.schemas.slicer import SliceRequest
 from backend.app.services.archive import ArchiveService
@@ -4025,96 +4025,24 @@ async def slice_archive(
 async def reprint_archive(
     archive_id: int,
     printer_id: int,
-    body: ReprintRequest | None = None,
-    db: AsyncSession = Depends(get_db),
-    auth_result: tuple[User | None, bool] = Depends(
-        require_ownership_permission(
-            Permission.ARCHIVES_REPRINT_ALL,
-            Permission.ARCHIVES_REPRINT_OWN,
-        )
-    ),
+    # SECURITY.md SEC-AUTH-1: every route either has an explicit auth dep or
+    # is in the route-auth-coverage allowlist. Gating the deprecation stub on
+    # QUEUE_CREATE matches the replacement route (POST /queue/) and means
+    # anonymous callers bounce at auth instead of seeing the deprecation
+    # message — leaking "this route exists" to unauthenticated callers is
+    # exactly the shape the backstop guards against.
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_CREATE),
 ):
-    """Dispatch an archived 3MF file for send/start on a printer."""
-    from backend.app.models.printer import Printer
-    from backend.app.services.background_dispatch import DispatchEnqueueRejected, background_dispatch
-    from backend.app.services.printer_manager import printer_manager
-
-    user, can_modify_all = auth_result
-
-    # Use defaults if no body provided
-    if body is None:
-        body = ReprintRequest()
-
-    # Get archive
-    service = ArchiveService(db)
-    archive = await service.get_archive(archive_id)
-    if not archive:
-        raise HTTPException(404, "Archive not found")
-
-    # Ownership check
-    if not can_modify_all:
-        if archive.created_by_id != user.id:
-            raise HTTPException(403, "You can only reprint your own archives")
-
-    # Get printer
-    result = await db.execute(select(Printer).where(Printer.id == printer_id))
-    printer = result.scalar_one_or_none()
-    if not printer:
-        raise HTTPException(404, "Printer not found")
-
-    # Check printer is connected
-    if not printer_manager.is_connected(printer_id):
-        raise HTTPException(400, "Printer is not connected")
-
-    if not archive.file_path:
-        raise HTTPException(
-            404,
-            "No 3MF file available for this archive. "
-            "The file could not be downloaded from the printer when the print was recorded.",
-        )
-
-    # Validate archive file exists
-    file_path = settings.base_dir / archive.file_path
-    if not file_path.is_file():
-        raise HTTPException(404, "Archive file not found")
-
-    plate_name = body.plate_name
-    if not plate_name and body.plate_id is not None:
-        plate_name = f"Plate {body.plate_id}"
-
-    dispatch_source_name = archive.filename
-    if plate_name:
-        dispatch_source_name = f"{archive.filename} • {plate_name}"
-
-    try:
-        dispatch_result = await background_dispatch.dispatch_reprint_archive(
-            archive_id=archive_id,
-            archive_name=dispatch_source_name,
-            printer_id=printer_id,
-            printer_name=printer.name,
-            options=body.model_dump(exclude_none=True),
-            requested_by_user_id=user.id if user else None,
-            requested_by_username=user.username if user else None,
-        )
-    except DispatchEnqueueRejected as e:
-        raise HTTPException(status_code=409, detail=str(e)) from e
-
-    logger.info(
-        "Dispatched reprint archive %s for printer %s (dispatch_job_id=%s, dispatch_position=%s)",
+    """Legacy direct reprint endpoint. Use POST /queue/ instead."""
+    logger.warning(
+        "Gone API used: POST /archives/%s/reprint?printer_id=%s; use POST /queue/ instead",
         archive_id,
         printer_id,
-        dispatch_result["dispatch_job_id"],
-        dispatch_result["dispatch_position"],
     )
-
-    return {
-        "status": "dispatched",
-        "printer_id": printer_id,
-        "archive_id": archive_id,
-        "filename": archive.filename,
-        "dispatch_job_id": dispatch_result["dispatch_job_id"],
-        "dispatch_position": dispatch_result["dispatch_position"],
-    }
+    raise HTTPException(
+        status_code=410,
+        detail="Direct archive reprint has been removed. Create a print queue item with POST /queue/.",
+    )
 
 
 # =============================================================================

+ 0 - 32
backend/app/api/routes/background_dispatch.py

@@ -1,32 +0,0 @@
-from fastapi import APIRouter, HTTPException
-
-from backend.app.core.auth import RequirePermissionIfAuthEnabled
-from backend.app.core.permissions import Permission
-from backend.app.models.user import User
-from backend.app.services.background_dispatch import background_dispatch
-
-router = APIRouter(prefix="/background-dispatch", tags=["background-dispatch"])
-
-
-@router.delete("/{job_id}")
-async def cancel_dispatch_job(
-    job_id: int,
-    _: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
-):
-    """Cancel a background-dispatch job.
-
-    Queued jobs are cancelled immediately. Active jobs are marked for
-    cooperative cancellation and will stop at the next cancellation checkpoint.
-    """
-    result = await background_dispatch.cancel_job(job_id)
-
-    if not result["cancelled"]:
-        raise HTTPException(status_code=404, detail="Dispatch job not found")
-
-    return {
-        "status": "cancelling" if result.get("pending") else "cancelled",
-        "job_id": result["job_id"],
-        "source_name": result["source_name"],
-        "printer_id": result["printer_id"],
-        "printer_name": result["printer_name"],
-    }

+ 25 - 5
backend/app/api/routes/camera.py

@@ -35,6 +35,7 @@ from backend.app.services.camera import (
 from backend.app.services.camera_fanout import (
     MjpegBroadcaster,
     get_or_create_broadcaster,
+    get_subscriber_count,
     iter_subscriber,
     shutdown_broadcaster,
 )
@@ -771,17 +772,36 @@ async def stop_camera_stream(
     printer_id: int,
     _: User | None = RequirePermissionIfAuthEnabled(Permission.CAMERA_VIEW),
 ):
-    """Stop all active camera streams for a printer.
-
-    This can be called by the frontend when the camera window is closed.
-    Accepts both GET and POST (POST for sendBeacon compatibility).
+    """Stop active camera streams for a printer.
+
+    Called by the frontend on viewer unmount (cam-wall tile, embedded viewer,
+    popup window). Accepts both GET and POST (POST for sendBeacon compatibility).
+
+    Reference-count guard: every viewer of a printer subscribes to the same
+    fan-out broadcaster, so a force-shutdown triggered by ONE leaving viewer
+    used to kill the others' streams (cam-wall tile froze when a user opened
+    then closed the embedded viewer). If any subscriber is still attached,
+    skip the force-teardown — the broadcaster's natural grace-shutdown (5 s
+    after subscribers drop to 0) handles cleanup when the leaving viewer's
+    HTTP connection actually closes.
     """
+    broadcaster_key = f"printer-{printer_id}"
+    remaining_subscribers = get_subscriber_count(broadcaster_key)
+    if remaining_subscribers >= 1:
+        logger.info(
+            "Skipping force-shutdown for printer %s: %d subscriber(s) still attached; "
+            "natural cleanup will tear down when last viewer disconnects",
+            printer_id,
+            remaining_subscribers,
+        )
+        return {"stopped": 0, "skipped": True}
+
     stopped = 0
 
     # Tear down the fan-out broadcaster first (#1089). This cleanly notifies
     # all subscribed viewers and asks the upstream generator to stop
     # reconnecting before we fall back to forcefully killing the process below.
-    if await shutdown_broadcaster(f"printer-{printer_id}"):
+    if await shutdown_broadcaster(broadcaster_key):
         logger.info("Shut down camera fan-out broadcaster for printer %s", printer_id)
 
     # Stop ffmpeg/RTSP streams

+ 21 - 100
backend/app/api/routes/library.py

@@ -49,7 +49,6 @@ from backend.app.schemas.library import (
     FileDuplicate,
     FileListResponse,
     FileMoveRequest,
-    FilePrintRequest,
     FileResponse as FileResponseSchema,
     FileUpdate,
     FileUploadResponse,
@@ -196,11 +195,11 @@ def validate_print_file_upload(filename: str, content: bytes) -> None:
     — raw ``.gcode`` and corrupt/non-zip ``.3mf`` uploads cascade into a
     confusing "Printing stopped because the printer was unable to parse the
     3mf file" rejection 30 seconds after the user clicks Print. The
-    background dispatcher (``background_dispatch.py``) appends ``.3mf`` to
-    a raw-gcode filename when constructing the FTP destination, which is
-    how the printer ends up with a file named ``.gcode.3mf`` whose body is
-    raw gcode — exactly the shape that triggers the firmware parse
-    failure. Catching both classes here gives an actionable error at the
+    the queue dispatch path appends ``.3mf`` to a raw-gcode filename when
+    constructing the FTP destination, which is how the printer ends up with a
+    file named ``.gcode.3mf`` whose body is raw gcode — exactly the shape that
+    triggers the firmware parse failure. Catching both classes here gives an
+    actionable error at the
     upload itself.
 
     Compares the filename suffix rather than ``os.path.splitext`` because
@@ -4117,101 +4116,23 @@ async def slice_library_file(
 async def print_library_file(
     file_id: int,
     printer_id: int,
-    body: FilePrintRequest | None = None,
-    db: AsyncSession = Depends(get_db),
-    current_user: User | None = Depends(require_permission_if_auth_enabled(Permission.PRINTERS_CONTROL)),
+    # SECURITY.md SEC-AUTH-1: every route either has an explicit auth dep or
+    # is in the route-auth-coverage allowlist. Gating the deprecation stub on
+    # QUEUE_CREATE matches the replacement route (POST /queue/) and means
+    # anonymous callers bounce at auth instead of seeing the deprecation
+    # message.
+    _: User | None = Depends(require_permission_if_auth_enabled(Permission.QUEUE_CREATE)),
 ):
-    """Dispatch a library file for send/start on a printer.
-
-    The actual send/start work is handled asynchronously by background
-    dispatch so the UI can continue immediately.
-
-    Only sliced files (.gcode or .gcode.3mf) can be printed.
-    """
-    from backend.app.models.printer import Printer
-    from backend.app.services.background_dispatch import DispatchEnqueueRejected, background_dispatch
-    from backend.app.services.printer_manager import printer_manager
-
-    # Use defaults if no body provided
-    if body is None:
-        body = FilePrintRequest()
-
-    # Get the library file
-    result = await db.execute(LibraryFile.active().where(LibraryFile.id == file_id))
-    lib_file = result.scalar_one_or_none()
-
-    if not lib_file:
-        raise HTTPException(status_code=404, detail="File not found")
-
-    # Validate file is sliced
-    if not is_sliced_file(lib_file.filename):
-        raise HTTPException(
-            status_code=400,
-            detail="Not a sliced file. Only .gcode or .gcode.3mf files can be printed.",
-        )
-
-    # Filenames containing FAT32/exFAT-illegal characters would 553 at
-    # FTP upload time (#1540). Older rows may pre-date the rename-time
-    # validation, so reject the print attempt with an actionable message
-    # rather than silently renaming user data.
-    try:
-        validate_print_filename(lib_file.filename)
-    except InvalidFilenameError as e:
-        raise HTTPException(status_code=400, detail=str(e)) from e
-
-    # Get the full file path
-    file_path = Path(app_settings.base_dir) / lib_file.file_path
-
-    if not file_path.exists():
-        raise HTTPException(status_code=404, detail="File not found on disk")
-
-    # Get printer
-    result = await db.execute(select(Printer).where(Printer.id == printer_id))
-    printer = result.scalar_one_or_none()
-    if not printer:
-        raise HTTPException(status_code=404, detail="Printer not found")
-
-    # Check printer is connected
-    if not printer_manager.is_connected(printer_id):
-        raise HTTPException(status_code=400, detail="Printer is not connected")
-
-    # Validate project exists before dispatching so a bogus ID yields 404, not a FK-constraint 500
-    if body.project_id is not None:
-        project_result = await db.execute(select(Project).where(Project.id == body.project_id))
-        if not project_result.scalar_one_or_none():
-            raise HTTPException(status_code=404, detail="Project not found")
-
-    plate_name = body.plate_name
-    if not plate_name and body.plate_id is not None:
-        plate_name = f"Plate {body.plate_id}"
-
-    dispatch_source_name = lib_file.filename
-    if plate_name:
-        dispatch_source_name = f"{lib_file.filename} • {plate_name}"
-
-    try:
-        dispatch_result = await background_dispatch.dispatch_print_library_file(
-            file_id=file_id,
-            filename=dispatch_source_name,
-            printer_id=printer_id,
-            printer_name=printer.name,
-            options=body.model_dump(exclude_none=True, exclude={"cleanup_library_after_dispatch"}),
-            project_id=body.project_id,
-            requested_by_user_id=current_user.id if current_user else None,
-            requested_by_username=current_user.username if current_user else None,
-            cleanup_library_after_dispatch=body.cleanup_library_after_dispatch,
-        )
-    except DispatchEnqueueRejected as e:
-        raise HTTPException(status_code=409, detail=str(e)) from e
-
-    return {
-        "status": "dispatched",
-        "printer_id": printer_id,
-        "archive_id": None,
-        "filename": lib_file.filename,
-        "dispatch_job_id": dispatch_result["dispatch_job_id"],
-        "dispatch_position": dispatch_result["dispatch_position"],
-    }
+    """Legacy direct library print endpoint. Use POST /queue/ instead."""
+    logger.warning(
+        "Gone API used: POST /library/files/%s/print?printer_id=%s; use POST /queue/ instead",
+        file_id,
+        printer_id,
+    )
+    raise HTTPException(
+        status_code=410,
+        detail="Direct library-file print has been removed. Create a print queue item with POST /queue/.",
+    )
 
 
 # ============ File Detail Endpoints ============

+ 122 - 16
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 and_, func, or_, select
+from sqlalchemy import and_, func, or_, select, update
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
@@ -153,6 +153,13 @@ def _enrich_response(item: PrintQueueItem) -> PrintQueueItemResponse:
         except json.JSONDecodeError:
             nozzle_mapping_parsed = None
 
+    nozzles_info_parsed = None
+    if item.nozzles_info:
+        try:
+            nozzles_info_parsed = json.loads(item.nozzles_info)
+        except json.JSONDecodeError:
+            nozzles_info_parsed = None
+
     # Create response with parsed ams_mapping
     item_dict = {
         "id": item.id,
@@ -197,6 +204,8 @@ def _enrich_response(item: PrintQueueItem) -> PrintQueueItemResponse:
         "gcode_injection": item.gcode_injection,
         # H2C rack-swap nozzle pick (#1780)
         "nozzle_mapping": nozzle_mapping_parsed,
+        "nozzles_info": nozzles_info_parsed,
+        "cleanup_library_after_dispatch": item.cleanup_library_after_dispatch,
     }
     response = PrintQueueItemResponse(**item_dict)
     if item.archive:
@@ -397,6 +406,23 @@ async def add_to_queue(
             and archive.created_by_id != current_user.id
         ):
             raise HTTPException(404, "Archive not found")
+        # Reprint perm gate (#1625): the legacy /archives/{id}/reprint endpoint
+        # required ARCHIVES_REPRINT_OWN/ALL; the unified queue route must keep
+        # that gate or an operator with QUEUE_CREATE could reprint via direct
+        # API call even if explicitly denied reprint perm. Mirrors the
+        # frontend `canModify('archives', 'reprint', ...)` helper:
+        # REPRINT_ALL allows any archive, REPRINT_OWN allows own only,
+        # ownerless archives require REPRINT_ALL (fail-closed).
+        if current_user is not None:
+            owns_archive = archive.created_by_id is not None and archive.created_by_id == current_user.id
+            has_reprint = current_user.has_permission(Permission.ARCHIVES_REPRINT_ALL.value) or (
+                owns_archive and current_user.has_permission(Permission.ARCHIVES_REPRINT_OWN.value)
+            )
+            if not has_reprint:
+                raise HTTPException(
+                    status_code=403,
+                    detail="Permission archives:reprint_own or archives:reprint_all required",
+                )
 
     # Validate library file exists (if provided) and get it for filament extraction
     library_file = None
@@ -502,21 +528,59 @@ async def add_to_queue(
         await db.flush()  # Get batch.id before creating items
         batch_id = batch.id
 
-    # Get next position for this printer (or for unassigned/model-based items)
+    # Get queue scope for this printer (or for unassigned/model-based 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")
+        queue_scope = (
+            PrintQueueItem.printer_id == data.printer_id,
+            PrintQueueItem.status == "pending",
         )
     else:
-        # For unassigned/model-based 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")
+        # For unassigned/model-based items, scope across all unassigned.
+        queue_scope = (
+            PrintQueueItem.printer_id.is_(None),
+            PrintQueueItem.status == "pending",
+        )
+
+    # Serialize concurrent queue inserts to the same scope (#1625-followup).
+    # The race: two concurrent ASAP inserts both compute MAX(position) before
+    # either commits; in an empty scope, both INSERT at position 1 (duplicate).
+    # In a non-empty scope, Postgres's row-level locks on the UPDATE shift
+    # serialize naturally, but the empty-scope path has no rows to lock.
+    # A transaction-scoped advisory lock keyed on the printer_id closes that
+    # window; the lock is released automatically at commit/rollback. Different
+    # printers don't contend. SQLite serializes writes implicitly so this is a
+    # no-op there.
+    #
+    # Dialect is checked against the actual session binding, NOT the
+    # `is_sqlite()` helper, because the test fixture overrides `get_db` with a
+    # SQLite engine while `settings.database_url` still points at Postgres
+    # (the helper reads settings). Inspecting the connection directly is the
+    # right shape for any code that mutates SQL based on the live dialect.
+    from sqlalchemy import text
+
+    bind = db.get_bind()
+    if bind.dialect.name == "postgresql":
+        scope_key = data.printer_id if data.printer_id is not None else 0
+        # 1625 namespaces the lock so it can't collide with other advisory
+        # locks elsewhere in the codebase.
+        await db.execute(text("SELECT pg_advisory_xact_lock(1625, :k)"), {"k": scope_key})
+
+    insert_position = max(1, data.insert_position or 1)
+    if data.insert_at_top or data.insert_position is not None:
+        result = await db.execute(select(func.max(PrintQueueItem.position)).where(*queue_scope))
+        max_pos = result.scalar() or 0
+        insert_position = min(insert_position, max_pos + 1)
+        await db.execute(
+            update(PrintQueueItem)
+            .where(*queue_scope)
+            .where(PrintQueueItem.position >= insert_position)
+            .values(position=PrintQueueItem.position + quantity)
         )
-    max_pos = result.scalar() or 0
+        start_position = insert_position
+    else:
+        result = await db.execute(select(func.max(PrintQueueItem.position)).where(*queue_scope))
+        max_pos = result.scalar() or 0
+        start_position = max_pos + 1
 
     # Resolve print_time_seconds for SJF scheduling (cache on item at creation)
     cached_print_time = None
@@ -571,8 +635,9 @@ async def add_to_queue(
             use_ams=data.use_ams,
             nozzle_offset_cali=data.nozzle_offset_cali,
             gcode_injection=data.gcode_injection,
+            cleanup_library_after_dispatch=data.cleanup_library_after_dispatch,
             project_id=data.project_id,
-            position=max_pos + 1 + i,
+            position=start_position + i,
             status="pending",
             created_by_id=current_user.id if current_user else None,
             batch_id=batch_id,
@@ -1188,19 +1253,39 @@ async def cancel_queue_item(
 async def stop_queue_item(
     item_id: int,
     db: AsyncSession = Depends(get_db),
-    _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_UPDATE_ALL),
+    auth_result: tuple[User | None, bool] = Depends(
+        require_ownership_permission(
+            Permission.QUEUE_UPDATE_ALL,
+            Permission.QUEUE_UPDATE_OWN,
+        )
+    ),
 ):
-    """Stop an actively printing queue item."""
+    """Stop an actively printing queue item.
+
+    Ownership-scoped (#1625-followup): callers with QUEUE_UPDATE_OWN can stop
+    their own items; callers with QUEUE_UPDATE_ALL can stop any item. Mirrors
+    the /cancel shape. Pre-fix this required QUEUE_UPDATE_ALL — Operators
+    holding only _OWN saw the Stop button in the queue UI but got 403 on click.
+    """
 
     from backend.app.models.smart_plug import SmartPlug
     from backend.app.services.printer_manager import printer_manager
     from backend.app.services.tasmota import tasmota_service
 
+    user, can_modify_all = auth_result
+
     result = await db.execute(select(PrintQueueItem).where(PrintQueueItem.id == item_id))
     item = result.scalar_one_or_none()
     if not item:
         raise HTTPException(404, "Queue item not found")
 
+    # Ownership check — mirrors /cancel. Ownerless items (created_by_id IS NULL)
+    # require _ALL: stop is destructive and an _OWN holder can't claim "they
+    # own it" the way /start does (#1670).
+    if not can_modify_all and user is not None:
+        if item.created_by_id is None or item.created_by_id != user.id:
+            raise HTTPException(403, "You can only stop your own queue items")
+
     if item.status != "printing":
         raise HTTPException(400, f"Can only stop items that are printing, current status: '{item.status}'")
 
@@ -1270,10 +1355,21 @@ async def start_queue_item(
     item_id: int,
     skip_filament_check: bool = Query(default=False),
     db: AsyncSession = Depends(get_db),
-    user: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_UPDATE_OWN),
+    auth_result: tuple[User | None, bool] = Depends(
+        require_ownership_permission(
+            Permission.QUEUE_UPDATE_ALL,
+            Permission.QUEUE_UPDATE_OWN,
+        )
+    ),
 ):
     """Manually start a staged (manual_start) queue item.
 
+    Ownership-scoped (#1625-followup): callers with QUEUE_UPDATE_OWN can
+    start their own items + claim ownership of NULL-owner items (VP-uploaded
+    items arrive unattributed per #1670). Callers with QUEUE_UPDATE_ALL can
+    start any item. Pre-fix this required QUEUE_UPDATE_OWN with no ownership
+    check, so _OWN holders could start anyone's queue items via direct API.
+
     Clears the manual_start flag so the scheduler picks it up. When
     ``skip_filament_check`` is false (the default) the live filament
     deficit (#1496) is checked first — if the assigned spool can't satisfy
@@ -1281,6 +1377,8 @@ async def start_queue_item(
     payload so the caller can show a confirm dialog and retry with
     ``skip_filament_check=true``.
     """
+    user, can_modify_all = auth_result
+
     result = await db.execute(
         select(PrintQueueItem)
         .options(
@@ -1295,6 +1393,14 @@ async def start_queue_item(
     if not item:
         raise HTTPException(404, "Queue item not found")
 
+    # Ownership check — softer than /cancel because /start is the entry point
+    # for #1670's VP-import flow: an unowned item is claimable by the first
+    # _OWN holder who clicks ▶, and the route below credits them as owner.
+    # An item with a DIFFERENT owner → 403.
+    if not can_modify_all and user is not None:
+        if item.created_by_id is not None and item.created_by_id != user.id:
+            raise HTTPException(403, "You can only start your own queue items")
+
     if item.status != "pending":
         raise HTTPException(400, f"Can only start pending items, current status: '{item.status}'")
 

+ 28 - 1
backend/app/api/routes/printers.py

@@ -27,6 +27,7 @@ from backend.app.schemas.printer import (
     AMSUnit,
     DiagnosticRequest,
     FilaSwitchResponse,
+    HmsActionBody,
     HMSErrorResponse,
     NozzleInfoResponse,
     NozzleRackSlot,
@@ -456,7 +457,9 @@ async def get_printer_status(
 
     # Convert HMS errors to response format
     hms_errors = [
-        HMSErrorResponse(code=e.code, attr=e.attr, module=e.module, severity=e.severity)
+        HMSErrorResponse(
+            code=e.code, attr=e.attr, module=e.module, severity=e.severity, actions=e.actions, job_id=e.job_id
+        )
         for e in (state.hms_errors or [])
     ]
 
@@ -3787,3 +3790,27 @@ async def get_runtime_debug(
         else None,
         "is_active": printer.is_active,
     }
+
+
+@router.post("/{printer_id}/hms/execute-action")
+async def execute_hms_action(
+    printer_id: int,
+    body: HmsActionBody,
+    _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
+    db: AsyncSession = Depends(get_db),
+):
+    """Execute an HMS action on the printer."""
+    result = await db.execute(select(Printer).where(Printer.id == printer_id))
+    printer = result.scalar_one_or_none()
+    if not printer:
+        raise HTTPException(404, "Printer not found")
+
+    client = printer_manager.get_client(printer_id)
+    if not client:
+        raise HTTPException(400, "Printer not connected")
+
+    success = client.execute_hms_action(body.print_error, body.action, body.job_id)
+    if not success:
+        raise HTTPException(400, "Failed to execute HMS action")
+
+    return {"success": True, "message": "HMS action executed"}

+ 2 - 4
backend/app/api/routes/settings.py

@@ -974,8 +974,8 @@ async def restore_backup(
             # 3b. Pause timer-based background services BEFORE the DB swap.
             # close_all_connections() below only disposes the engine's pool,
             # not the asyncio tasks that opened sessions from it. The print
-            # scheduler (30 s cadence), smart-plug snapshot loop (30 s),
-            # notification digest loop, and background dispatch worker all
+            # scheduler (30 s cadence), smart-plug snapshot loop (30 s), and
+            # notification digest loop all
             # wake up and call async_session(), which lazily re-creates a
             # pool connection holding RowExclusiveLock on print_queue /
             # smart_plug_energy_snapshots / etc. The DROP TABLE CASCADE
@@ -984,7 +984,6 @@ async def restore_backup(
             # full restore rollback. Successful restore already requires a
             # container restart, so we don't restart the services here.
             try:
-                from backend.app.services.background_dispatch import background_dispatch
                 from backend.app.services.notification_service import notification_service
                 from backend.app.services.print_scheduler import scheduler as print_scheduler
                 from backend.app.services.smart_plug_manager import smart_plug_manager
@@ -993,7 +992,6 @@ async def restore_backup(
                 print_scheduler.stop()
                 smart_plug_manager.stop_scheduler()
                 notification_service.stop_digest_scheduler()
-                await background_dispatch.stop()
                 # In-flight loop iterations need a moment to commit + release
                 # their DB sessions before we dispose() the engine pool.
                 await asyncio.sleep(1.0)

+ 0 - 9
backend/app/api/routes/websocket.py

@@ -23,7 +23,6 @@ from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect
 from backend.app.core.auth import is_auth_enabled, verify_websocket_token
 from backend.app.core.database import async_session
 from backend.app.core.websocket import ws_manager
-from backend.app.services.background_dispatch import background_dispatch
 from backend.app.services.printer_manager import printer_manager, printer_state_to_dict
 
 logger = logging.getLogger(__name__)
@@ -106,14 +105,6 @@ async def websocket_endpoint(websocket: WebSocket, token: str | None = Query(def
                 }
             )
 
-        dispatch_state = await background_dispatch.get_state()
-        if (dispatch_state.get("dispatched", 0) + dispatch_state.get("processing", 0)) > 0:
-            await websocket.send_json(
-                {
-                    "type": "background_dispatch",
-                    "data": dispatch_state,
-                }
-            )
         logger.info("Sent initial status for %s printers", len(statuses))
 
         # Keep connection alive and handle incoming messages.

+ 10 - 0
backend/app/core/database.py

@@ -948,6 +948,16 @@ async def run_migrations(conn):
     else:
         await _safe_execute(conn, "ALTER TABLE print_queue ADD COLUMN skip_filament_check BOOLEAN DEFAULT false")
 
+    # Migration: cleanup flag for transient printer-card uploads routed through
+    # the scheduler. The archive copy is durable; the library row/file can be
+    # deleted after dispatch.
+    if is_sqlite():
+        await _safe_execute(conn, "ALTER TABLE print_queue ADD COLUMN cleanup_library_after_dispatch BOOLEAN DEFAULT 0")
+    else:
+        await _safe_execute(
+            conn, "ALTER TABLE print_queue ADD COLUMN cleanup_library_after_dispatch BOOLEAN DEFAULT false"
+        )
+
     # Migration: Add queue_force_color_match column to virtual_printers (#1188).
     # Opt-in flag: when true, VP queue-mode uploads pin the per-slot type+color
     # from the 3MF onto the queue item's filament_overrides so the scheduler

+ 5 - 5
backend/app/core/permissions.py

@@ -19,7 +19,7 @@ class Permission(StrEnum):
     PRINTERS_CREATE = "printers:create"
     PRINTERS_UPDATE = "printers:update"
     PRINTERS_DELETE = "printers:delete"
-    PRINTERS_CONTROL = "printers:control"  # Start/stop/pause/resume prints
+    PRINTERS_CONTROL = "printers:control"  # Printer controls: stop/pause/resume, lights, motors, drying, etc.
     PRINTERS_FILES = "printers:files"  # Send files to printer
     PRINTERS_AMS_RFID = "printers:ams_rfid"  # Re-read AMS RFID tags
     PRINTERS_CLEAR_PLATE = "printers:clear_plate"  # Confirm plate cleared for next print
@@ -36,15 +36,15 @@ class Permission(StrEnum):
     ARCHIVES_UPDATE_ALL = "archives:update_all"
     ARCHIVES_DELETE_OWN = "archives:delete_own"
     ARCHIVES_DELETE_ALL = "archives:delete_all"
-    ARCHIVES_REPRINT_OWN = "archives:reprint_own"
-    ARCHIVES_REPRINT_ALL = "archives:reprint_all"
+    ARCHIVES_REPRINT_OWN = "archives:reprint_own"  # Reprint own archives; queue:create is also required to enqueue
+    ARCHIVES_REPRINT_ALL = "archives:reprint_all"  # Reprint any archive; queue:create is also required to enqueue
     ARCHIVES_PURGE = "archives:purge"
 
     # Queue
     QUEUE_READ = "queue:read"
     QUEUE_READ_OWN = "queue:read_own"
     QUEUE_READ_ALL = "queue:read_all"
-    QUEUE_CREATE = "queue:create"
+    QUEUE_CREATE = "queue:create"  # Create queue items, including ASAP items eligible for immediate dispatch
     QUEUE_UPDATE_OWN = "queue:update_own"
     QUEUE_UPDATE_ALL = "queue:update_all"
     QUEUE_DELETE_OWN = "queue:delete_own"
@@ -55,7 +55,7 @@ class Permission(StrEnum):
     LIBRARY_READ = "library:read"
     LIBRARY_READ_OWN = "library:read_own"
     LIBRARY_READ_ALL = "library:read_all"
-    LIBRARY_UPLOAD = "library:upload"
+    LIBRARY_UPLOAD = "library:upload"  # Upload/import/slice library files; queue:create is also required to print
     LIBRARY_UPDATE_OWN = "library:update_own"
     LIBRARY_UPDATE_ALL = "library:update_all"
     LIBRARY_DELETE_OWN = "library:delete_own"

+ 9009 - 0
backend/app/data/hms_actions.json

@@ -0,0 +1,9009 @@
+{
+    "31B": {
+        "07008028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004039": [],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500C080": [
+            "OK_BUTTON"
+        ],
+        "18008026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "07018026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "1802802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18038026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "07008029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18058026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "18008029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "1807802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "1804802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18078029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07038029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18068026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "1805802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18068029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18018029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18048029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18028026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "1803802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07008026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "07038026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "0703802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "1806802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0701802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18028029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0700802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18038029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "1801802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18058029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0702802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07018029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18018026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "18048026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "18078026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "1800802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07028029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07028026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "18028027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07028027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008070": [
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "03008055": [],
+        "07018021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07028021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0502C032": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "05008065": [
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008042": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18FE8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008003": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0700860000020002": [],
+        "03004031": [],
+        "03004035": [],
+        "03004036": [],
+        "03004030": [],
+        "03004038": [],
+        "03004032": [],
+        "03004033": [],
+        "0500809C": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0502402C": [
+            "OK_BUTTON"
+        ],
+        "0500809B": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05008081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FF8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FE8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "1805700000020007": [],
+        "07FF200000020002": [],
+        "0701220000020001": [],
+        "1802700000020007": [],
+        "0703220000020001": [],
+        "07FE200000020002": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0500806D": [],
+        "18FE200000020002": [],
+        "0702200000020001": [],
+        "05008098": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FF200000020001": [],
+        "0702230000020001": [],
+        "18FE200000020001": [],
+        "0702210000020001": [],
+        "18FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0703210000020001": [],
+        "18FF200000020001": [],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0C00030000030008": [],
+        "0700220000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "1806700000020007": [],
+        "07FE200000020001": [],
+        "18FF200000020002": [],
+        "0701210000020001": [],
+        "1801700000020007": [],
+        "1807700000020007": [],
+        "0703700000020007": [],
+        "0300401F": [
+            "OK_BUTTON"
+        ],
+        "0500809A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "IGNORE_RESUME"
+        ],
+        "05008099": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "18FE8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "07028028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18058028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008051": [],
+        "07018028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18038028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18048028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "050080A6": [
+            "OK_BUTTON"
+        ],
+        "18028028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18068028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18078028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008043": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500808B": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C00803F": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008054": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C008053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008096": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "050080A7": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008097": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500C059": [
+            "OK_BUTTON"
+        ],
+        "05008054": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03002E0000030001": [],
+        "03008054": [
+            "OK_BUTTON"
+        ],
+        "07018004": [
+            "CONTINUE"
+        ],
+        "18FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008055": [],
+        "07018037": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07048033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008048": [
+            "OK_BUTTON"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C004026": [
+            "OK_BUTTON"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "05008093": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "07FFC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008047": [
+            "OK_BUTTON"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500806F": [],
+        "05024019": [
+            "OK_BUTTON"
+        ],
+        "03008080": [
+            "CHECK_ASSISTANT"
+        ],
+        "0300807D": [
+            "OK_BUTTON"
+        ],
+        "0500C036": [
+            "OK_BUTTON"
+        ],
+        "0300801C": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500806B": [],
+        "05008087": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "03008024": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05004047": [
+            "OK_BUTTON"
+        ],
+        "05FE8069": [
+            "REFRESH_NOZZLE"
+        ],
+        "0500040000030054": [],
+        "03008046": [
+            "OK_BUTTON"
+        ],
+        "05008071": [],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "03008025": [
+            "RESUME_PRINTING"
+        ],
+        "03004016": [
+            "CHECK_ASSISTANT"
+        ],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "07FFC012": [],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "05008067": [],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500808A": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C004041": [
+            "OK_BUTTON"
+        ],
+        "07038034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "07008018": [
+            "RESUME_PRINTING"
+        ],
+        "07FE8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000020037": [],
+        "0300C012": [
+            "OK_BUTTON"
+        ],
+        "03008041": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008051": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "03004011": [
+            "OK_BUTTON"
+        ],
+        "07FEC010": [
+            "CONTINUE"
+        ],
+        "0500040000020031": [],
+        "05008061": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "03008044": [
+            "TURN_OFF_FIRE_ALARM",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "05008073": [],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "05024023": [
+            "OK_JUMP_RACK"
+        ],
+        "18038033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05024021": [
+            "OK_BUTTON"
+        ],
+        "05FF8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0500805B": [],
+        "03008063": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C004020": [
+            "OK_BUTTON"
+        ],
+        "0300802A": [
+            "RESUME_PRINTING",
+            "CHECK_ASSISTANT"
+        ],
+        "05008072": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "05008084": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05FE8053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0501040000030010": [],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05028022": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18008033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008018": [
+            "RESUME_PRINTING"
+        ],
+        "03008022": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03008026": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07038030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05024017": [
+            "OK_JUMP_RACK"
+        ],
+        "0300C056": [
+            "TURN_OFF_FIRE_ALARM",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18FE8012": [
+            "RESUME_PRINTING"
+        ],
+        "0C004029": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008063": [],
+        "05FF8053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0500807C": [],
+        "07FEC011": [],
+        "05008056": [],
+        "05FE806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07028034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07058037": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FE8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C00800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C004021": [
+            "OK_BUTTON"
+        ],
+        "0500040000020036": [],
+        "07008036": [
+            "REMOVE_CLOSE_BTN",
+            "RETRY_PROBLEM_SOLVED",
+            "ABORT"
+        ],
+        "0500806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0501040000030004": [],
+        "0500040000020041": [],
+        "05008077": [],
+        "05008078": [],
+        "07FEC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC012": [],
+        "1A004007": [
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC010": [
+            "CONTINUE"
+        ],
+        "0500806C": [],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008062": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05008086": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "18018030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0700550000020001": [],
+        "18028034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07028030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008029": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008058": [],
+        "0501040000030002": [],
+        "0500808C": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "18058033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500807D": [],
+        "18038030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300804F": [
+            "OK_BUTTON"
+        ],
+        "03008071": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500808D": [
+            "IGNORE_RESUME"
+        ],
+        "05024005": [],
+        "0C008018": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05024018": [
+            "OK_BUTTON"
+        ],
+        "07FE8013": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008058": [
+            "CHECK_ASSISTANT"
+        ],
+        "18028030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008084": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008080": [
+            "REFRESH_NOZZLE"
+        ],
+        "18038034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500807A": [
+            "CHECK_ASSISTANT"
+        ],
+        "07048037": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0300804B": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008017": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500040000020034": [],
+        "0502400E": [
+            "OK_BUTTON"
+        ],
+        "05008091": [
+            "IGNORE_RESUME"
+        ],
+        "03008042": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "03008057": [
+            "CHECK_ASSISTANT"
+        ],
+        "05008082": [],
+        "0500808F": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "07018033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000020035": [],
+        "03008081": [],
+        "07FF8013": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03008053": [
+            "OK_BUTTON"
+        ],
+        "0C00403D": [
+            "CHECK_ASSISTANT",
+            "OK_BUTTON"
+        ],
+        "07018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300804E": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008088": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500807B": [],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1A00120000020010": [],
+        "03008021": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05004046": [
+            "OK_BUTTON"
+        ],
+        "03008049": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0501040000030006": [],
+        "0500403E": [
+            "OK_BUTTON"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "05008074": [],
+        "18048034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008059": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07FE8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008045": [
+            "OK_BUTTON"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC006": [
+            "CONTINUE"
+        ],
+        "18FE8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300801E": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05FE8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "07FF8002": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300800A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008048": [
+            "STOP_PRINTING"
+        ],
+        "0500C032": [
+            "OK_BUTTON"
+        ],
+        "1A008004": [
+            "OK_BUTTON"
+        ],
+        "03008023": [
+            "RESUME_PRINTING"
+        ],
+        "0300807F": [
+            "CHECK_ASSISTANT"
+        ],
+        "05008083": [],
+        "03008064": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07058034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07048034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC011": [],
+        "18018033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500403D": [
+            "OK_BUTTON"
+        ],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "05008059": [],
+        "05008079": [],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8025": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07018030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500807E": [],
+        "05008069": [
+            "REFRESH_NOZZLE"
+        ],
+        "05008066": [],
+        "05008068": [],
+        "07008033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C004022": [
+            "OK_BUTTON"
+        ],
+        "07008035": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0300802B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1A004008": [
+            "OK_BUTTON"
+        ],
+        "0C008034": [
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "1A004009": [],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "0502C00F": [
+            "OK_BUTTON"
+        ],
+        "0300404B": [
+            "OK_BUTTON"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500805C": [],
+        "07FE8025": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0500805A": [],
+        "18008030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008085": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0300C070": [
+            "OK_BUTTON"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0501040000030005": [],
+        "07008032": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008031": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07028037": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FEC009": [
+            "CONTINUE"
+        ],
+        "05008090": [],
+        "18058034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C00040000030024": [],
+        "07058033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FE8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C008040": [
+            "RESUME_PRINTING_DEFECTS",
+            "CHECK_ASSISTANT"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008064": [],
+        "18048033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8006": [
+            "CONTINUE"
+        ],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "07FE8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "03008062": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008037": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0300801A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500806E": [],
+        "05024003": [
+            "OK_BUTTON"
+        ],
+        "05FF806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "18028033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05024016": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008033": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FE8012": [
+            "RESUME_PRINTING"
+        ],
+        "07028033": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008061": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FE8002": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05004070": [
+            "OK_BUTTON"
+        ],
+        "07038037": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18018034": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "050080A0": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "03008027": [
+            "RESUME_PRINTING"
+        ],
+        "1A008002": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ]
+    },
+    "20P": {
+        "07008028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "1805700000020007": [],
+        "05008072": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "18008026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "03008070": [
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07028028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C004026": [
+            "OK_BUTTON"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FE8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03004031": [],
+        "18028027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1802802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05FE803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "07018026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "07018021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0300801C": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0701220000020001": [],
+        "18058028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03004035": [],
+        "03008046": [
+            "OK_BUTTON"
+        ],
+        "03008051": [],
+        "18038026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "0500C036": [
+            "OK_BUTTON"
+        ],
+        "0C00803F": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FEC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "07008029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18058026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "18008029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07018028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "1807802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "03004016": [
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "07FFC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03004036": [],
+        "07FE8021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500808C": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "18FE8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "0500803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "18078027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "1802700000020007": [],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0502802B": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008041": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0703220000020001": [],
+        "07018027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008051": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "03004011": [
+            "OK_BUTTON"
+        ],
+        "07038028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05028037": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "05008061": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FE8053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8002": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1804802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "05FE8069": [
+            "REFRESH_NOZZLE"
+        ],
+        "18078029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07038027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05FF8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "0502C032": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "18058021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18068026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "18038028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0700200000020001": [],
+        "18008028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18068021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "03008022": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FEC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FE8012": [
+            "RESUME_PRINTING"
+        ],
+        "07FF8002": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "1805802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07FE8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03004037": [],
+        "0702700000020007": [],
+        "18068029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0702220000020001": [],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0700700000020007": [],
+        "05008053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "18018021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008069": [
+            "REFRESH_NOZZLE"
+        ],
+        "0702200000020001": [],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05FF803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "18048021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008062": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07FEC012": [],
+        "18018029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC009": [
+            "CONTINUE"
+        ],
+        "0700550000020001": [],
+        "07018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18048028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0502C033": [
+            "PROCEED",
+            "DONT_REMIND_NEXT_TIME"
+        ],
+        "0501040000030002": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8013": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0300804F": [
+            "OK_BUTTON"
+        ],
+        "0500808D": [
+            "IGNORE_RESUME"
+        ],
+        "07028021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05024005": [],
+        "0C008018": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18FE8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05024029": [
+            "OK_BUTTON"
+        ],
+        "05008088": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008080": [
+            "REFRESH_NOZZLE"
+        ],
+        "18048029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0500807A": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500C080": [
+            "OK_BUTTON"
+        ],
+        "07FE8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500808B": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008017": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC011": [],
+        "0702230000020001": [],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "05008091": [
+            "IGNORE_RESUME"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "07FEC010": [
+            "CONTINUE"
+        ],
+        "18028026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "03008063": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0502400E": [
+            "OK_BUTTON"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "05004045": [
+            "OK_BUTTON"
+        ],
+        "07FFC012": [],
+        "07FF8013": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03004039": [],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0C00403D": [
+            "CHECK_ASSISTANT",
+            "OK_BUTTON"
+        ],
+        "0500809A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "IGNORE_RESUME"
+        ],
+        "0300804E": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18018028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "1803802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "05024035": [
+            "OK_BUTTON"
+        ],
+        "0702210000020001": [],
+        "07FEC011": [],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004030": [],
+        "18FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008021": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500403E": [
+            "OK_BUTTON"
+        ],
+        "07FE8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008099": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "05008040": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "07008026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0703210000020001": [],
+        "03004038": [],
+        "03008042": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0703802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07038026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05FF8069": [
+            "REFRESH_NOZZLE"
+        ],
+        "07FE8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FE8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF800D": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT",
+            "STOP_PRINTING"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300801E": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "1806802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05FE8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0300C012": [
+            "OK_BUTTON",
+            "IGNORE_NO_REMINDER_NEXT_TIME"
+        ],
+        "0701802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0C008043": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500808A": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0300800A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0701230000020001": [],
+        "0500C032": [
+            "OK_BUTTON"
+        ],
+        "0700230000020001": [],
+        "03004032": [],
+        "0700210000020001": [],
+        "18038021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500C059": [
+            "OK_BUTTON"
+        ],
+        "18038027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03004033": [],
+        "1800700000020007": [],
+        "18FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "18028029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8025": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "1804700000020007": [],
+        "05FF8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0700802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18058027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008087": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05FF8053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05FF806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07028027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008062": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008034": [
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "0700860000020002": [],
+        "18038029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0700220000020001": [],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008003": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "1801802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18058029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0702802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "05008054": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "CHECK_ASSISTANT"
+        ],
+        "18078021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "18018026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "18068027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05FE8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07018029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "05008085": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "07FEC006": [
+            "CONTINUE"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500809B": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0703200000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FE8025": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "05008084": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0502802A": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "18048026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "18078026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "07038021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0C004021": [
+            "OK_BUTTON"
+        ],
+        "05FE806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8017": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "05028036": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078028": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FE8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008086": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "1806700000020007": [],
+        "0C008040": [
+            "RESUME_PRINTING_DEFECTS",
+            "CHECK_ASSISTANT"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC010": [
+            "CONTINUE"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1800802A": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0501040000030003": [],
+        "07028029": [
+            "CONTINUE",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18048027": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03002E0000030001": [],
+        "0300801A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500806E": [],
+        "05024003": [
+            "OK_BUTTON"
+        ],
+        "07FE8012": [
+            "RESUME_PRINTING"
+        ],
+        "0701210000020001": [],
+        "03008061": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FE8006": [
+            "CONTINUE"
+        ],
+        "07008021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05004070": [
+            "OK_BUTTON"
+        ],
+        "07028026": [
+            "CHECK_ASSISTANT",
+            "RETRY_PROBLEM_SOLVED"
+        ],
+        "18008021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801700000020007": [],
+        "07FE8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "0C008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1807700000020007": [],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "0703700000020007": [],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "050080A0": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ]
+    },
+    "094": {
+        "18FF8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004039": [],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500C080": [
+            "OK_BUTTON"
+        ],
+        "03008070": [
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "03008055": [],
+        "07018021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07028021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0502C032": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "05008065": [
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008042": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18FE8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008003": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03004031": [],
+        "03004035": [],
+        "03004036": [],
+        "03004030": [],
+        "03004038": [],
+        "03004032": [],
+        "03004033": [],
+        "0500809B": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05008081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FF8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FE8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "1805700000020007": [],
+        "07FF200000020002": [],
+        "0701220000020001": [],
+        "1802700000020007": [],
+        "0703220000020001": [],
+        "07FE200000020002": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0500806D": [],
+        "18FE200000020002": [],
+        "0702200000020001": [],
+        "05008098": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FF200000020001": [],
+        "0702230000020001": [],
+        "18FE200000020001": [],
+        "0702210000020001": [],
+        "18FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0703210000020001": [],
+        "18FF200000020001": [],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0C00030000030008": [],
+        "0700220000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "1806700000020007": [],
+        "07FE200000020001": [],
+        "18FF200000020002": [],
+        "0701210000020001": [],
+        "1801700000020007": [],
+        "1807700000020007": [],
+        "0703700000020007": [],
+        "0500809A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "IGNORE_RESUME"
+        ],
+        "05008099": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "03008051": [],
+        "18FE8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "050080A6": [
+            "OK_BUTTON"
+        ],
+        "18FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "0C008043": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500808B": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008054": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C00803F": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008096": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "050080A7": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008097": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500C059": [
+            "OK_BUTTON"
+        ],
+        "05008054": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03002E0000030001": [],
+        "03008059": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "03008058": [
+            "CHECK_ASSISTANT"
+        ],
+        "03008057": [
+            "CHECK_ASSISTANT"
+        ],
+        "05008048": [
+            "STOP_PRINTING"
+        ],
+        "03008071": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03004016": [
+            "CHECK_ASSISTANT"
+        ],
+        "05008072": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C004021": [
+            "OK_BUTTON"
+        ],
+        "03008042": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008053": [
+            "OK_BUTTON"
+        ],
+        "03008064": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500808F": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "03008062": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500807E": [],
+        "05004070": [
+            "OK_BUTTON"
+        ],
+        "03008080": [
+            "CHECK_ASSISTANT"
+        ],
+        "0300807D": [
+            "OK_BUTTON"
+        ],
+        "0500040000030054": [],
+        "0500808C": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0300C012": [
+            "OK_BUTTON"
+        ],
+        "07FE8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008051": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "03008044": [
+            "TURN_OFF_FIRE_ALARM",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0501040000030010": [],
+        "0300C056": [
+            "TURN_OFF_FIRE_ALARM",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0501040000030004": [],
+        "0501040000030002": [],
+        "03008081": [],
+        "05008091": [
+            "IGNORE_RESUME"
+        ],
+        "0300807F": [
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300404B": [
+            "OK_BUTTON"
+        ],
+        "0300C070": [
+            "OK_BUTTON"
+        ],
+        "05024003": [
+            "OK_BUTTON"
+        ],
+        "05008080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0502400E": [
+            "OK_BUTTON"
+        ],
+        "0C004022": [
+            "OK_BUTTON"
+        ],
+        "0C008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008022": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500808D": [
+            "IGNORE_RESUME"
+        ],
+        "0C008018": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008017": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0300800A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0502C00F": [
+            "OK_BUTTON"
+        ],
+        "0C008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018004": [
+            "CONTINUE"
+        ],
+        "18FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300801C": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8002": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18FE8012": [
+            "RESUME_PRINTING"
+        ],
+        "07FE8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05024005": [],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "07FF8002": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8013": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FE8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300801E": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05FE8069": [
+            "REFRESH_NOZZLE"
+        ],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008069": [
+            "REFRESH_NOZZLE"
+        ],
+        "05FF8069": [
+            "REFRESH_NOZZLE"
+        ],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008090": [],
+        "18FE8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300801A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8012": [
+            "RESUME_PRINTING"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "050080A0": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0C008034": [
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "05008086": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008077": [],
+        "03008054": [
+            "OK_BUTTON"
+        ],
+        "03008048": [
+            "OK_BUTTON"
+        ],
+        "0C004026": [
+            "OK_BUTTON"
+        ],
+        "05008055": [],
+        "07FE8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "0500806F": [],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "03008047": [
+            "OK_BUTTON"
+        ],
+        "0500806B": [],
+        "05008087": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "03008046": [
+            "OK_BUTTON"
+        ],
+        "05008071": [],
+        "05008067": [],
+        "0C004041": [
+            "OK_BUTTON"
+        ],
+        "0500808A": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FEC010": [
+            "CONTINUE"
+        ],
+        "0500806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0500040000020037": [],
+        "03008041": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000020031": [],
+        "03004011": [
+            "OK_BUTTON"
+        ],
+        "05FE8053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008061": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05008073": [],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "03008063": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C004020": [
+            "OK_BUTTON"
+        ],
+        "05FF8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0500805B": [],
+        "05008084": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "07FEC011": [],
+        "05FF8053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C004029": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008063": [],
+        "0500807C": [],
+        "0C00800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008056": [],
+        "0500040000020041": [],
+        "0500040000020036": [],
+        "05008078": [],
+        "0700550000020001": [],
+        "0500806C": [],
+        "05008062": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05008058": [],
+        "0500807A": [
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "0300804B": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500807D": [],
+        "0300804F": [
+            "OK_BUTTON"
+        ],
+        "07FE8013": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008082": [],
+        "0500040000020034": [],
+        "05008088": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0500807B": [],
+        "0500040000020035": [],
+        "0C00403D": [
+            "CHECK_ASSISTANT",
+            "OK_BUTTON"
+        ],
+        "0300804E": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "03008021": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "03008049": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008074": [],
+        "0500403E": [
+            "OK_BUTTON"
+        ],
+        "03008045": [
+            "OK_BUTTON"
+        ],
+        "07FEC006": [
+            "CONTINUE"
+        ],
+        "07FFC012": [],
+        "05FE8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0500C032": [
+            "OK_BUTTON"
+        ],
+        "05008083": [],
+        "07FEC012": [],
+        "05008059": [],
+        "0500403D": [
+            "OK_BUTTON"
+        ],
+        "05008079": [],
+        "07FF8025": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "05008066": [],
+        "05008068": [],
+        "0500805C": [],
+        "07FE8025": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0500805A": [],
+        "05008085": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "07FFC010": [
+            "CONTINUE"
+        ],
+        "07FEC009": [
+            "CONTINUE"
+        ],
+        "0500C036": [
+            "OK_BUTTON"
+        ],
+        "0C008040": [
+            "RESUME_PRINTING_DEFECTS",
+            "CHECK_ASSISTANT"
+        ],
+        "05FE806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0C00040000030024": [],
+        "05008064": [],
+        "05FF806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0C008033": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FE8006": [
+            "CONTINUE"
+        ],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "07FFC011": [],
+        "03008061": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500806E": []
+    },
+    "239": {
+        "18FF8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8020": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004039": [],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500C080": [
+            "OK_BUTTON"
+        ],
+        "03008070": [
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "03008055": [],
+        "07018021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07028021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028021": [
+            "RETRY_PROBLEM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0502C032": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "05008065": [
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008042": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18FE8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008003": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03004031": [],
+        "03004035": [],
+        "03004036": [],
+        "03004030": [],
+        "03004038": [],
+        "03004032": [],
+        "03004033": [],
+        "0500809B": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05008081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FF8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FE8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "1805700000020007": [],
+        "07FF200000020002": [],
+        "0701220000020001": [],
+        "1802700000020007": [],
+        "0703220000020001": [],
+        "07FE200000020002": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0500806D": [],
+        "18FE200000020002": [],
+        "0702200000020001": [],
+        "05008098": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FF200000020001": [],
+        "0702230000020001": [],
+        "18FE200000020001": [],
+        "0702210000020001": [],
+        "18FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0703210000020001": [],
+        "18FF200000020001": [],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0C00030000030008": [],
+        "0700220000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "1806700000020007": [],
+        "07FE200000020001": [],
+        "18FF200000020002": [],
+        "0701210000020001": [],
+        "1801700000020007": [],
+        "1807700000020007": [],
+        "0703700000020007": [],
+        "0500809A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "IGNORE_RESUME"
+        ],
+        "05008099": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "03008051": [],
+        "18FE8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "050080A6": [
+            "OK_BUTTON"
+        ],
+        "18FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "0C008043": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500808B": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008054": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C00803F": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008096": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "050080A7": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008097": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500C059": [
+            "OK_BUTTON"
+        ],
+        "05008054": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03002E0000030001": [],
+        "03008059": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "03008058": [
+            "CHECK_ASSISTANT"
+        ],
+        "03008057": [
+            "CHECK_ASSISTANT"
+        ],
+        "05008048": [
+            "STOP_PRINTING"
+        ],
+        "03008071": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03004016": [
+            "CHECK_ASSISTANT"
+        ],
+        "05008072": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C004021": [
+            "OK_BUTTON"
+        ],
+        "03008042": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008053": [
+            "OK_BUTTON"
+        ],
+        "03008064": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500808F": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "03008062": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500807E": [],
+        "05004070": [
+            "OK_BUTTON"
+        ],
+        "07FE8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "03008054": [
+            "OK_BUTTON"
+        ],
+        "07018004": [
+            "CONTINUE"
+        ],
+        "18FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0C008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008048": [
+            "OK_BUTTON"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C004026": [
+            "OK_BUTTON"
+        ],
+        "05008055": [],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "0500806F": [],
+        "07FFC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008047": [
+            "OK_BUTTON"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008080": [
+            "CHECK_ASSISTANT"
+        ],
+        "0300807D": [
+            "OK_BUTTON"
+        ],
+        "0500C036": [
+            "OK_BUTTON"
+        ],
+        "0300801C": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500806B": [],
+        "05008071": [],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "07FE8002": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008087": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0500040000030054": [],
+        "03008046": [
+            "OK_BUTTON"
+        ],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "05008067": [],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500808C": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0500808A": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FEC010": [
+            "CONTINUE"
+        ],
+        "0C004041": [
+            "OK_BUTTON"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000020037": [],
+        "0300C012": [
+            "OK_BUTTON"
+        ],
+        "03008041": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000020031": [],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008051": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "03004011": [
+            "OK_BUTTON"
+        ],
+        "0500806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FE8053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008061": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "03008044": [
+            "TURN_OFF_FIRE_ALARM",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "05008073": [],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "03008063": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C004020": [
+            "OK_BUTTON"
+        ],
+        "05FF8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0500805B": [],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "05008084": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008063": [],
+        "07FEC011": [],
+        "0501040000030010": [],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03008022": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500807C": [],
+        "05FF8053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0300C056": [
+            "TURN_OFF_FIRE_ALARM",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "18FE8012": [
+            "RESUME_PRINTING"
+        ],
+        "0C004029": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008056": [],
+        "07FE8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008086": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C00800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FEC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000020041": [],
+        "05008077": [],
+        "0500040000020036": [],
+        "0501040000030004": [],
+        "0500806C": [],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008062": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05008078": [],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008058": [],
+        "0501040000030002": [],
+        "0700550000020001": [],
+        "07018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500807D": [],
+        "0300804F": [
+            "OK_BUTTON"
+        ],
+        "0500808D": [
+            "IGNORE_RESUME"
+        ],
+        "05024005": [],
+        "0C008018": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FE8013": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0500807A": [
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "0300804B": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "05008082": [],
+        "0C008017": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500040000020034": [],
+        "05008088": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500807B": [],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8002": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000020035": [],
+        "03008081": [],
+        "07FF8013": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0C00403D": [
+            "CHECK_ASSISTANT",
+            "OK_BUTTON"
+        ],
+        "0300804E": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0502400E": [
+            "OK_BUTTON"
+        ],
+        "05008091": [
+            "IGNORE_RESUME"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "03008021": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "03008049": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008074": [],
+        "0500403E": [
+            "OK_BUTTON"
+        ],
+        "07FE8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008045": [
+            "OK_BUTTON"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC006": [
+            "CONTINUE"
+        ],
+        "18FE8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300801E": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC012": [],
+        "0300800A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05FE8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0500C032": [
+            "OK_BUTTON"
+        ],
+        "0300807F": [
+            "CHECK_ASSISTANT"
+        ],
+        "05008083": [],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05FE8069": [
+            "REFRESH_NOZZLE"
+        ],
+        "07FEC012": [],
+        "05008059": [],
+        "0500403D": [
+            "OK_BUTTON"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008079": [],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8025": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "05008069": [
+            "REFRESH_NOZZLE"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "0C004022": [
+            "OK_BUTTON"
+        ],
+        "05FF8069": [
+            "REFRESH_NOZZLE"
+        ],
+        "05008066": [],
+        "05008068": [],
+        "0502C00F": [
+            "OK_BUTTON"
+        ],
+        "0300404B": [
+            "OK_BUTTON"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500805C": [],
+        "0C008034": [
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500805A": [],
+        "05008085": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0300C070": [
+            "OK_BUTTON"
+        ],
+        "07FE8025": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FFC010": [
+            "CONTINUE"
+        ],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FEC009": [
+            "CONTINUE"
+        ],
+        "05008090": [],
+        "18FE8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05FE806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0C00040000030024": [],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008040": [
+            "RESUME_PRINTING_DEFECTS",
+            "CHECK_ASSISTANT"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "05008064": [],
+        "05FF806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0C008033": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FE8012": [
+            "RESUME_PRINTING"
+        ],
+        "07FE8006": [
+            "CONTINUE"
+        ],
+        "0300801A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500806E": [],
+        "05024003": [
+            "OK_BUTTON"
+        ],
+        "07FFC011": [],
+        "03008061": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "050080A0": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ]
+    },
+    "00W": {
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0502C032": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008004": [
+            "LOAD_VIRTUAL_TRAY",
+            "FILAMENT_LOAD_RESUME"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008003": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500809B": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "1805700000020007": [],
+        "0701220000020001": [],
+        "18FF200000020002": [],
+        "1802700000020007": [],
+        "0703220000020001": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0702200000020001": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0702230000020001": [],
+        "07FF200000020001": [],
+        "07FF200000020002": [],
+        "0702210000020001": [],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0703210000020001": [],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0700220000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "18FF200000020001": [],
+        "1806700000020007": [],
+        "0701210000020001": [],
+        "1801700000020007": [],
+        "1807700000020007": [],
+        "0703700000020007": [],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "05004015": [],
+        "0500402E": [],
+        "0500402F": [],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500403C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "0500803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008006": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "STOP_PRINTING"
+        ],
+        "07FFC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0501040000030002": [],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008040": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "0501040000030001": [],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008001": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018004": [
+            "CONTINUE"
+        ],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "03008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "STOP_PRINTING"
+        ],
+        "0300800A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008002": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07008002": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008008": [
+            "CHECK_ASSISTANT"
+        ],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "03008010": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "07004001": [
+            "OK_BUTTON"
+        ]
+    },
+    "00M": {
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0502C032": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008004": [
+            "LOAD_VIRTUAL_TRAY",
+            "FILAMENT_LOAD_RESUME"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008003": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500809B": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "1805700000020007": [],
+        "0701220000020001": [],
+        "18FF200000020002": [],
+        "1802700000020007": [],
+        "0703220000020001": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0702200000020001": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0702230000020001": [],
+        "07FF200000020001": [],
+        "07FF200000020002": [],
+        "0702210000020001": [],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0703210000020001": [],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0700220000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "18FF200000020001": [],
+        "1806700000020007": [],
+        "0701210000020001": [],
+        "1807700000020007": [],
+        "1801700000020007": [],
+        "0703700000020007": [],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "05004015": [],
+        "0500402E": [],
+        "0500402F": [],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500403C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "0500803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008006": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "STOP_PRINTING"
+        ],
+        "07FFC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0501040000030002": [],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008040": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "0501040000030001": [],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008001": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018004": [
+            "CONTINUE"
+        ],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "03008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "STOP_PRINTING"
+        ],
+        "0300800A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008002": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07008002": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008008": [
+            "CHECK_ASSISTANT"
+        ],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "03008010": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "07004001": [
+            "OK_BUTTON"
+        ]
+    },
+    "03W": {
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008070": [
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0502C032": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008004": [
+            "LOAD_VIRTUAL_TRAY",
+            "FILAMENT_LOAD_RESUME"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008003": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1805700000020007": [],
+        "0701220000020001": [],
+        "18FF200000020002": [],
+        "1802700000020007": [],
+        "0703220000020001": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0702200000020001": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0702230000020001": [],
+        "07FF200000020001": [],
+        "07FF200000020002": [],
+        "0702210000020001": [],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0703210000020001": [],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0700220000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "18FF200000020001": [],
+        "1806700000020007": [],
+        "0701210000020001": [],
+        "1807700000020007": [],
+        "1801700000020007": [],
+        "0703700000020007": [],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "05004015": [],
+        "0500402E": [],
+        "0500402F": [],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500403C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "0500803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008006": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "STOP_PRINTING"
+        ],
+        "07FFC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0501040000030002": [],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008040": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "0501040000030001": [],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C008001": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018004": [
+            "CONTINUE"
+        ],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "03008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "STOP_PRINTING"
+        ],
+        "0300800A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008002": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07008002": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008008": [
+            "CHECK_ASSISTANT"
+        ],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0300801B": [
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "03008010": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "07004001": [
+            "OK_BUTTON"
+        ]
+    },
+    "01S": {
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008004": [
+            "LOAD_VIRTUAL_TRAY",
+            "FILAMENT_LOAD_RESUME"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0502C032": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "0500809B": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "1805700000020007": [],
+        "0701220000020001": [],
+        "18FF200000020002": [],
+        "1802700000020007": [],
+        "0703220000020001": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0702200000020001": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0702230000020001": [],
+        "07FF200000020001": [],
+        "07FF200000020002": [],
+        "0702210000020001": [],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0703210000020001": [],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0700220000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "18FF200000020001": [],
+        "1806700000020007": [],
+        "0701210000020001": [],
+        "1807700000020007": [],
+        "1801700000020007": [],
+        "0703700000020007": [],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "05004015": [],
+        "0500402E": [],
+        "0500402F": [],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500403C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "0500803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018004": [
+            "CONTINUE"
+        ],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "03008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008002": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008008": [
+            "CHECK_ASSISTANT"
+        ],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "03008010": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "07004001": [
+            "OK_BUTTON"
+        ]
+    },
+    "01P": {
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008004": [
+            "LOAD_VIRTUAL_TRAY",
+            "FILAMENT_LOAD_RESUME"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0502C032": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "0500809B": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "1805700000020007": [],
+        "0701220000020001": [],
+        "18FF200000020002": [],
+        "1802700000020007": [],
+        "0703220000020001": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0702200000020001": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0702230000020001": [],
+        "07FF200000020001": [],
+        "07FF200000020002": [],
+        "0702210000020001": [],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0703210000020001": [],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0700220000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "18FF200000020001": [],
+        "1806700000020007": [],
+        "0701210000020001": [],
+        "1807700000020007": [],
+        "1801700000020007": [],
+        "0703700000020007": [],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "05004015": [],
+        "0500402E": [],
+        "0500402F": [],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500403C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "0500803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018004": [
+            "CONTINUE"
+        ],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "03008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008002": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008008": [
+            "CHECK_ASSISTANT"
+        ],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "03008010": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "07004001": [
+            "OK_BUTTON"
+        ]
+    },
+    "093": {
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004039": [],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500C080": [
+            "OK_BUTTON"
+        ],
+        "03008070": [
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "03008055": [],
+        "05008098": [],
+        "0502C032": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008003": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03004031": [],
+        "03004035": [],
+        "03004037": [],
+        "03004030": [],
+        "03004038": [],
+        "03004032": [],
+        "03004033": [],
+        "0500809B": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05008081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FF8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FE8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "1805700000020007": [],
+        "0701220000020001": [],
+        "1802700000020007": [],
+        "0C008043": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0703220000020001": [],
+        "18FF200000020002": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0500806D": [],
+        "0702200000020001": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "03008042": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0702230000020001": [],
+        "18FF200000020001": [],
+        "0702210000020001": [],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0703210000020001": [],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0700220000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "1806700000020007": [],
+        "07FF200000020002": [],
+        "0701210000020001": [],
+        "07FF200000020001": [],
+        "1801700000020007": [],
+        "1807700000020007": [],
+        "0703700000020007": [],
+        "0300401F": [
+            "OK_BUTTON"
+        ],
+        "0500809A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "IGNORE_RESUME"
+        ],
+        "05008099": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "03008051": [],
+        "050080A6": [
+            "OK_BUTTON"
+        ],
+        "03008063": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03008064": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03008062": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008054": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C008053": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008072": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008084": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008096": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "050080A7": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008097": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500C059": [
+            "OK_BUTTON"
+        ],
+        "05008054": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03002E0000030001": [],
+        "03008059": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "03008058": [
+            "CHECK_ASSISTANT"
+        ],
+        "03008057": [
+            "CHECK_ASSISTANT"
+        ],
+        "05004045": [
+            "OK_BUTTON"
+        ],
+        "05008048": [
+            "STOP_PRINTING"
+        ],
+        "0500403C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "0500803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03004016": [
+            "CHECK_ASSISTANT"
+        ],
+        "05008086": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "07018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500808C": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008054": [
+            "OK_BUTTON"
+        ],
+        "07018004": [
+            "CONTINUE"
+        ],
+        "05008055": [],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0C008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008048": [
+            "OK_BUTTON"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C004026": [
+            "OK_BUTTON"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008050": [
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "03008047": [
+            "OK_BUTTON"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500806F": [],
+        "0500806B": [],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000030054": [],
+        "03008046": [
+            "OK_BUTTON"
+        ],
+        "03008080": [
+            "CHECK_ASSISTANT"
+        ],
+        "0300807D": [
+            "OK_BUTTON"
+        ],
+        "0500C036": [
+            "OK_BUTTON"
+        ],
+        "0300801C": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0501040000030004": [],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "05008071": [],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "05008067": [],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0C004041": [
+            "OK_BUTTON"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000020037": [],
+        "0300C012": [
+            "OK_BUTTON"
+        ],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008041": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000020031": [],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008051": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "03004011": [
+            "OK_BUTTON"
+        ],
+        "05008061": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "03008044": [
+            "TURN_OFF_FIRE_ALARM",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "05008073": [],
+        "0C004020": [
+            "OK_BUTTON"
+        ],
+        "05FF8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0500805B": [],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "0501040000030010": [],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03008022": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0300C056": [
+            "TURN_OFF_FIRE_ALARM",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0C004029": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008063": [],
+        "0500807C": [],
+        "05008056": [],
+        "0C00800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FFC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000020036": [],
+        "0500040000020041": [],
+        "05008077": [],
+        "05008078": [],
+        "0500806C": [],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008062": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008002": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0501040000030002": [],
+        "07018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008058": [],
+        "0300804B": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500807D": [],
+        "0300804F": [
+            "OK_BUTTON"
+        ],
+        "0500808D": [
+            "IGNORE_RESUME"
+        ],
+        "05024005": [],
+        "0C008018": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC012": [],
+        "0500808B": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008017": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0500040000020034": [],
+        "05008088": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0500807A": [
+            "CHECK_ASSISTANT"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "05008082": [],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500807B": [],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500040000020035": [],
+        "03008081": [],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0C00403D": [
+            "CHECK_ASSISTANT",
+            "OK_BUTTON"
+        ],
+        "0300804E": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0502400E": [
+            "OK_BUTTON"
+        ],
+        "05008091": [
+            "IGNORE_RESUME"
+        ],
+        "03008021": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03008049": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500403E": [
+            "OK_BUTTON"
+        ],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008074": [],
+        "03008045": [
+            "OK_BUTTON"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "0500808A": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0300800A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05FE8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0300801E": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008083": [],
+        "0300807F": [
+            "CHECK_ASSISTANT"
+        ],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008059": [],
+        "0500403D": [
+            "OK_BUTTON"
+        ],
+        "07FFC011": [],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "05008079": [],
+        "07FF8025": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0500807E": [],
+        "05008087": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008068": [],
+        "0C004022": [
+            "OK_BUTTON"
+        ],
+        "05FF806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05008066": [],
+        "0C008034": [
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "0502C00F": [
+            "OK_BUTTON"
+        ],
+        "0300404B": [
+            "OK_BUTTON"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500805C": [],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500805A": [],
+        "05008085": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0300C070": [
+            "OK_BUTTON"
+        ],
+        "0500808F": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008090": [],
+        "0C004021": [
+            "OK_BUTTON"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "0C00040000030024": [],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008064": [],
+        "0300801A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500806E": [],
+        "05024003": [
+            "OK_BUTTON"
+        ],
+        "0C008033": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07FFC010": [
+            "CONTINUE"
+        ],
+        "03008061": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05004070": [
+            "OK_BUTTON"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "050080A0": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ]
+    },
+    "039": {
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12018011": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12028011": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12008011": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "12FFC030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "03008015": [
+            "LOAD_VIRTUAL_TRAY",
+            "FILAMENT_LOAD_RESUME"
+        ],
+        "1200210000020001": [],
+        "18FF200000020002": [],
+        "12FF200000020001": [],
+        "1200220000020001": [],
+        "1200230000020001": [],
+        "1200200000020001": [],
+        "07FF200000020001": [],
+        "18FF200000020001": [],
+        "07FF200000020002": [],
+        "1805700000020007": [],
+        "0701220000020001": [],
+        "1802700000020007": [],
+        "0703220000020001": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0702200000020001": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0702230000020001": [],
+        "0702210000020001": [],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0703210000020001": [],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0700220000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "1806700000020007": [],
+        "0701210000020001": [],
+        "1807700000020007": [],
+        "1801700000020007": [],
+        "0703700000020007": [],
+        "0300401F": [
+            "OK_BUTTON"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "05004015": [],
+        "0500402E": [],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500402F": [],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07008002": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "12008006": [
+            "CONTINUE"
+        ],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "12FF8010": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018004": [
+            "CONTINUE"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07004001": [
+            "OK_BUTTON"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "050040A4": [
+            "OK_BUTTON"
+        ],
+        "050040A5": [
+            "OK_BUTTON"
+        ],
+        "12008012": [
+            "RESUME_PRINTING"
+        ],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "12008001": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12FF8006": [
+            "CONTINUE"
+        ],
+        "12008010": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "12008015": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "12008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "12FFC006": [
+            "CONTINUE"
+        ],
+        "03008010": [
+            "CHECK_ASSISTANT"
+        ],
+        "12FF8003": [
+            "CONTINUE"
+        ],
+        "12FFC003": [
+            "CONTINUE"
+        ],
+        "12008014": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03008008": [
+            "CHECK_ASSISTANT"
+        ],
+        "12008016": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500403A": [
+            "OK_BUTTON"
+        ]
+    },
+    "030": {
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12018011": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12028011": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12008011": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "12FFC030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "03008015": [
+            "LOAD_VIRTUAL_TRAY",
+            "FILAMENT_LOAD_RESUME"
+        ],
+        "1200210000020001": [],
+        "18FF200000020002": [],
+        "12FF200000020001": [],
+        "1200220000020001": [],
+        "1200230000020001": [],
+        "1200200000020001": [],
+        "07FF200000020001": [],
+        "18FF200000020001": [],
+        "07FF200000020002": [],
+        "1805700000020007": [],
+        "0701220000020001": [],
+        "1802700000020007": [],
+        "0703220000020001": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0702200000020001": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0702230000020001": [],
+        "0702210000020001": [],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0703210000020001": [],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0700220000020001": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "1806700000020007": [],
+        "0701210000020001": [],
+        "1807700000020007": [],
+        "1801700000020007": [],
+        "0703700000020007": [],
+        "0300401F": [
+            "OK_BUTTON"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "05004015": [],
+        "0500402E": [],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500402F": [],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07008002": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "12008006": [
+            "CONTINUE"
+        ],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "12FF8010": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018004": [
+            "CONTINUE"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07004001": [
+            "OK_BUTTON"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "050040A4": [
+            "OK_BUTTON"
+        ],
+        "050040A5": [
+            "OK_BUTTON"
+        ],
+        "12008012": [
+            "RESUME_PRINTING"
+        ],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "12008001": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "12FF8006": [
+            "CONTINUE"
+        ],
+        "12008010": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "12008015": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "12008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "12FFC006": [
+            "CONTINUE"
+        ],
+        "03008010": [
+            "CHECK_ASSISTANT"
+        ],
+        "12FF8003": [
+            "CONTINUE"
+        ],
+        "12FFC003": [
+            "CONTINUE"
+        ],
+        "12008014": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03008008": [
+            "CHECK_ASSISTANT"
+        ],
+        "12008016": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ]
+    },
+    "22E": {
+        "03008019": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "1801C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1805C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1802C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1803C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1800C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1804C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1807C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "1806C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004039": [],
+        "07028016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07038016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0502C032": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "07028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18028011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18068011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18078011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18058011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18048011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008003": [
+            "RESUME_PRINTING_DEFECTS",
+            "STOP_PRINTING"
+        ],
+        "07038011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018011": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03004031": [],
+        "03004035": [],
+        "03004037": [],
+        "03004030": [],
+        "03004038": [],
+        "03004032": [],
+        "03004033": [],
+        "0500808C": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0500809B": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05008081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FF8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "05FE8081": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "03008062": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03008063": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "1805700000020007": [],
+        "0701220000020001": [],
+        "1802700000020007": [],
+        "0703220000020001": [],
+        "07FF200000020001": [],
+        "0700200000020001": [],
+        "0702700000020007": [],
+        "0702220000020001": [],
+        "0700700000020007": [],
+        "0702200000020001": [],
+        "18008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0702230000020001": [],
+        "0702210000020001": [],
+        "0703230000020001": [],
+        "0701200000020001": [],
+        "0C008043": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0703210000020001": [],
+        "07FFC009": [
+            "CONTINUE"
+        ],
+        "0701230000020001": [],
+        "0700230000020001": [],
+        "0700210000020001": [],
+        "18FF200000020002": [],
+        "1800700000020007": [],
+        "0701700000020007": [],
+        "1803700000020007": [],
+        "1804700000020007": [],
+        "0700220000020001": [],
+        "07FF200000020002": [],
+        "18018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0703200000020001": [],
+        "1806700000020007": [],
+        "18FF200000020001": [],
+        "0701210000020001": [],
+        "1801700000020007": [],
+        "1807700000020007": [],
+        "0703700000020007": [],
+        "0300401F": [
+            "OK_BUTTON"
+        ],
+        "0500809A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "IGNORE_RESUME"
+        ],
+        "03008051": [],
+        "05008072": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05008084": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "03008041": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500C059": [
+            "OK_BUTTON"
+        ],
+        "05008054": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8010": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "03008014": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008093": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "03002E0000030001": [],
+        "05004045": [
+            "OK_BUTTON"
+        ],
+        "0500403C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "07FFC006": [
+            "CONTINUE"
+        ],
+        "07018010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "0C008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0C004026": [
+            "OK_BUTTON"
+        ],
+        "03008005": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07008002": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC00A": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "18018004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0500C036": [
+            "OK_BUTTON"
+        ],
+        "0300801C": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07018005": [
+            "CONTINUE"
+        ],
+        "03008046": [
+            "OK_BUTTON"
+        ],
+        "03004016": [
+            "CHECK_ASSISTANT"
+        ],
+        "07004025": [
+            "OK_BUTTON"
+        ],
+        "0700C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "18018005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300C012": [
+            "OK_BUTTON"
+        ],
+        "18008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008051": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "03004011": [
+            "OK_BUTTON"
+        ],
+        "05008061": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05FF8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "18008012": [
+            "RESUME_PRINTING"
+        ],
+        "07008006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03008022": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07028006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC011": [],
+        "18008004": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008062": [
+            "IGNORE_RESUME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "07FFC010": [
+            "CONTINUE"
+        ],
+        "07018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18018007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07008010": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "18018003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0501040000030002": [],
+        "0C008018": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05008080": [
+            "REFRESH_NOZZLE"
+        ],
+        "0500807A": [
+            "CHECK_ASSISTANT"
+        ],
+        "0300804F": [
+            "OK_BUTTON"
+        ],
+        "0500808D": [
+            "IGNORE_RESUME"
+        ],
+        "05024005": [],
+        "0500808B": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008017": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "05008088": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "07018012": [
+            "RESUME_PRINTING"
+        ],
+        "18008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0701C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "0C00403D": [
+            "CHECK_ASSISTANT",
+            "OK_BUTTON"
+        ],
+        "0300804E": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0502400E": [
+            "OK_BUTTON"
+        ],
+        "07FF8007": [
+            "FILAMENT_EXTRUDED",
+            "RETRY_FILAMENT_EXTRUDED"
+        ],
+        "05008091": [
+            "IGNORE_RESUME"
+        ],
+        "0703C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "03008021": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "07038006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008040": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "0500403E": [
+            "OK_BUTTON"
+        ],
+        "07008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "03008042": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "18008003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC012": [],
+        "0300801E": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500808A": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0300800A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05FE8080": [
+            "REFRESH_NOZZLE"
+        ],
+        "07FFC008": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "18018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300800B": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "18018012": [
+            "RESUME_PRINTING"
+        ],
+        "07018016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8025": [
+            "CHECK_ASSISTANT",
+            "CONTINUE"
+        ],
+        "07FF8006": [
+            "CONTINUE"
+        ],
+        "05008087": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "05FF806A": [
+            "IGNORE_NO_REMINDER_NEXT_TIME",
+            "PROBLEM_SOLVED_RESUME"
+        ],
+        "0C008034": [
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "07008012": [
+            "RESUME_PRINTING"
+        ],
+        "03004000": [
+            "CHECK_ASSISTANT"
+        ],
+        "03004002": [
+            "CHECK_ASSISTANT"
+        ],
+        "0500803C": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "0702C069": [
+            "CHECK_ASSISTANT"
+        ],
+        "07018006": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "05008085": [
+            "PROBLEM_SOLVED_RESUME",
+            "IGNORE_RESUME"
+        ],
+        "0C004021": [
+            "OK_BUTTON"
+        ],
+        "07FFC003": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FF8005": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0501040000030003": [],
+        "03008061": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0300801A": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "0500806E": [],
+        "05024003": [
+            "OK_BUTTON"
+        ],
+        "18008007": [
+            "CHECK_ASSISTANT",
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "0C008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "CHECK_ASSISTANT"
+        ],
+        "05004070": [
+            "OK_BUTTON"
+        ]
+    },
+    "default": {
+        "07FF8030": [
+            "CONTINUE"
+        ],
+        "07FE8030": [
+            "CONTINUE"
+        ],
+        "07FEC030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "07FFC030": [
+            "CONTINUE",
+            "CHECK_ASSISTANT"
+        ],
+        "0300806F": [
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "0500040000030057": [
+            "DISABLE_PURIFICATION"
+        ],
+        "0502C031": [
+            "PROCEED",
+            "CANCLE"
+        ],
+        "0300806E": [
+            "CHECK_ASSISTANT",
+            "REMOVE_CLOSE_BTN"
+        ],
+        "05004095": [],
+        "05008057": [
+            "STOP_PRINTING",
+            "IGNORE_RESUME"
+        ],
+        "0502C028": [
+            "OK_BUTTON"
+        ],
+        "0502C026": [
+            "OK_BUTTON"
+        ],
+        "0500400E": [
+            "OK_BUTTON"
+        ],
+        "05004037": [
+            "OK_BUTTON"
+        ],
+        "0502C014": [
+            "OK_BUTTON"
+        ],
+        "0502C012": [
+            "OK_BUTTON"
+        ],
+        "05008092": [
+            "IGNORE_RESUME",
+            "STOP_PRINTING"
+        ],
+        "05004042": [
+            "CANCLE"
+        ],
+        "05004040": [
+            "OK_BUTTON"
+        ],
+        "05004007": [
+            "OK_BUTTON"
+        ],
+        "0502C010": [
+            "STOP_DRYING"
+        ],
+        "05004041": [
+            "OK_BUTTON"
+        ],
+        "05004043": [
+            "OK_BUTTON"
+        ],
+        "03008000": [
+            "RESUME_PRINTING"
+        ],
+        "0300800C": [
+            "RESUME_PRINTING"
+        ],
+        "0300800D": [
+            "RESUME_PRINTING",
+            "CHECK_ASSISTANT"
+        ],
+        "03008017": [
+            "RESUME_PRINTING_PROBELM_SOLVED"
+        ],
+        "03008016": [
+            "RESUME_PRINTING_PROBELM_SOLVED",
+            "STOP_PRINTING",
+            "CHECK_ASSISTANT"
+        ],
+        "05004003": [
+            "OK_BUTTON"
+        ],
+        "0C00402D": [
+            "OK_BUTTON"
+        ],
+        "05024001": [
+            "OK_BUTTON"
+        ],
+        "05004004": [
+            "OK_BUTTON"
+        ],
+        "05004014": [
+            "OK_BUTTON"
+        ],
+        "03008013": [
+            "RESUME_PRINTING"
+        ],
+        "0C00402C": [
+            "OK_BUTTON"
+        ],
+        "03008007": [
+            "RESUME_PRINTING",
+            "STOP_PRINTING"
+        ]
+    }
+}

+ 0 - 7
backend/app/main.py

@@ -23,7 +23,6 @@ from backend.app.api.routes import (
     archive_purge,
     archives,
     auth,
-    background_dispatch as background_dispatch_routes,
     bug_report,
     camera,
     cloud,
@@ -81,7 +80,6 @@ from backend.app.core.websocket import ws_manager
 from backend.app.models.smart_plug import SmartPlug
 from backend.app.services.archive import ArchiveService, peek_plate_index_in_3mf, swap_plate_suffix
 from backend.app.services.archive_purge import archive_purge_service
-from backend.app.services.background_dispatch import background_dispatch
 from backend.app.services.bambu_ftp import (
     FileNotOnPrinterError,
     cache_3mf_download,
@@ -6197,9 +6195,6 @@ async def lifespan(app: FastAPI):
     # Start the print scheduler
     spawn_background_task(print_scheduler.run(), name="print-scheduler")
 
-    # Start background dispatch worker for send/start operations
-    await background_dispatch.start()
-
     # Start the smart plug scheduler for time-based on/off
     smart_plug_manager.start_scheduler()
 
@@ -6265,7 +6260,6 @@ async def lifespan(app: FastAPI):
 
     # Shutdown
     print_scheduler.stop()
-    await background_dispatch.stop()
     smart_plug_manager.stop_scheduler()
     notification_service.stop_digest_scheduler()
     github_backup_service.stop_scheduler()
@@ -6740,7 +6734,6 @@ app.include_router(local_presets.router, prefix=app_settings.api_prefix)
 app.include_router(smart_plugs.router, prefix=app_settings.api_prefix)
 app.include_router(print_log.router, prefix=app_settings.api_prefix)
 app.include_router(print_queue.router, prefix=app_settings.api_prefix)
-app.include_router(background_dispatch_routes.router, prefix=app_settings.api_prefix)
 app.include_router(kprofiles.router, prefix=app_settings.api_prefix)
 app.include_router(notifications.router, prefix=app_settings.api_prefix)
 app.include_router(notification_templates.router, prefix=app_settings.api_prefix)

+ 4 - 0
backend/app/models/print_queue.py

@@ -77,6 +77,10 @@ class PrintQueueItem(Base):
     nozzle_mapping: Mapped[str | None] = mapped_column(Text, nullable=True)
     nozzles_info: Mapped[str | None] = mapped_column(Text, nullable=True)
 
+    # Printer-card direct uploads create transient library rows. When this is
+    # true, the scheduler deletes the source row/files after archiving a copy.
+    cleanup_library_after_dispatch: Mapped[bool] = mapped_column(Boolean, default=False)
+
     # Print options
     bed_levelling: Mapped[bool] = mapped_column(Boolean, default=True)
     flow_cali: Mapped[bool] = mapped_column(Boolean, default=False)

+ 0 - 22
backend/app/schemas/archive.py

@@ -219,25 +219,3 @@ class ProjectPageUpdate(BaseModel):
     copyright: str | None = None
     profile_title: str | None = None
     profile_description: str | None = None
-
-
-class ReprintRequest(BaseModel):
-    """Request body for reprinting an archive."""
-
-    # Plate selection for multi-plate 3MF files
-    # If not specified, auto-detects from file (legacy behavior for single-plate files)
-    plate_id: int | None = None
-    plate_name: str | None = None
-
-    # AMS slot mapping: list of tray IDs for each filament slot in the 3MF
-    # Global tray ID = (ams_id * 4) + slot_id, external = 254
-    ams_mapping: list[int] | None = None
-
-    # Print options
-    bed_levelling: bool = True
-    flow_cali: bool = False
-    vibration_cali: bool = True
-    layer_inspect: bool = False
-    timelapse: bool = False
-    use_ams: bool = True  # Not exposed in UI, but needed for API
-    nozzle_offset_cali: bool = True  # Dual-nozzle printers only — MQTT-gated (#1682)

+ 0 - 27
backend/app/schemas/library.py

@@ -278,33 +278,6 @@ class FileMoveRequest(BaseModel):
     folder_id: int | None = None  # None = move to root
 
 
-class FilePrintRequest(BaseModel):
-    """Schema for printing a file from the library.
-
-    Note: printer_id is passed as a query parameter, not in the body.
-    """
-
-    # Print options (same as archive reprint)
-    plate_id: int | None = None
-    plate_name: str | None = None
-    ams_mapping: list[int] | None = None
-    bed_levelling: bool = True
-    flow_cali: bool = False
-    vibration_cali: bool = True
-    layer_inspect: bool = False
-    timelapse: bool = False
-    use_ams: bool = True
-    nozzle_offset_cali: bool = True  # Dual-nozzle printers only — MQTT-gated (#1682)
-    # Project to associate the resulting archive with
-    project_id: int | None = None
-    # When true, delete the LibraryFile row + disk file after the archive has
-    # been created and the print has been dispatched. Used by the Printers-page
-    # Direct-Print flow (click / drag-drop a file onto a printer card) so the
-    # transient upload doesn't linger in File Manager. Cleanup is skipped on
-    # external library files.
-    cleanup_library_after_dispatch: bool = False
-
-
 class FileUploadResponse(BaseModel):
     """Schema for file upload response."""
 

+ 31 - 1
backend/app/schemas/print_queue.py

@@ -1,7 +1,7 @@
 from datetime import datetime
 from typing import Annotated, Literal
 
-from pydantic import BaseModel, PlainSerializer
+from pydantic import BaseModel, PlainSerializer, model_validator
 
 
 # Custom serializer to ensure UTC datetimes have Z suffix
@@ -28,6 +28,8 @@ class PrintQueueItemCreate(BaseModel):
     require_previous_success: bool = False
     auto_off_after: bool = False  # Power off printer after print completes
     manual_start: bool = False  # Requires manual trigger to start (staged)
+    insert_at_top: bool = False  # Insert ahead of other pending items in the same queue scope
+    insert_position: int | None = None  # 1-indexed insertion position for priority queueing
     # Persistent "Print Anyway" acknowledgement (#1698-followup). When set,
     # PrintModal already showed the deficit warning and the user confirmed,
     # so the scheduler does not re-flag this item on the next tick.
@@ -58,6 +60,9 @@ class PrintQueueItemCreate(BaseModel):
     batch_id: int | None = None
     # Project to associate the resulting archive with
     project_id: int | None = None
+    # Direct printer-card uploads are temporary library files. The scheduler
+    # deletes them after creating the durable archive copy.
+    cleanup_library_after_dispatch: bool = False
 
 
 class PrintQueueItemUpdate(BaseModel):
@@ -166,6 +171,7 @@ class PrintQueueItemResponse(BaseModel):
 
     # Auto-print G-code injection
     gcode_injection: bool = False
+    cleanup_library_after_dispatch: bool = False
 
     # H2C dual-nozzle-rack slicer pick (#1780). Surface for any future
     # "edit print → choose nozzle" UI; null on every model except O1C2
@@ -184,6 +190,30 @@ class PrintQueueReorderItem(BaseModel):
 class PrintQueueReorder(BaseModel):
     items: list[PrintQueueReorderItem]
 
+    @model_validator(mode="after")
+    def _validate_positions_unique(self) -> "PrintQueueReorder":
+        """Reject reorder requests with duplicate positions in the payload
+        (#1625-followup).
+
+        The /reorder route is the drag-drop renumber path on the queue UI;
+        a well-behaved client sends a contiguous renumbering of a single
+        printer's pending queue. A buggy client that sends two items at
+        the same position would leave the queue in an inconsistent state
+        (scheduler's ORDER BY (printer_id, position) ties get broken by
+        physical row order). Fail closed at the schema boundary so the
+        bug is caught before any DB mutation.
+
+        Uniqueness is enforced WITHIN THE PAYLOAD only — cross-printer
+        reorders that intentionally share positions across different
+        printer queues are a non-goal of the drag-drop UI, so this is the
+        right scope.
+        """
+        positions = [it.position for it in self.items]
+        if len(positions) != len(set(positions)):
+            duplicates = sorted({p for p in positions if positions.count(p) > 1})
+            raise ValueError(f"Duplicate positions in reorder request: {duplicates}")
+        return self
+
 
 class PrintQueueBulkUpdate(BaseModel):
     """Bulk update multiple queue items with the same values."""

+ 14 - 0
backend/app/schemas/printer.py

@@ -153,6 +153,8 @@ class HMSErrorResponse(BaseModel):
     attr: int = 0  # Attribute value for constructing wiki URL
     module: int
     severity: int  # 1=fatal, 2=serious, 3=common, 4=info
+    actions: list[str] = []  # List of user-facing action keys (e.g. "CHECK_FILAMENT")
+    job_id: str | None = None  # Optional job ID for actions that require it (e.g. "CHECK_ASSISTANT")
 
 
 class AMSTray(BaseModel):
@@ -216,6 +218,18 @@ class AmsLabelBody(BaseModel):
     ams_serial: str = Field(default="", max_length=50)
 
 
+class HmsActionBody(BaseModel):
+    # 8-char hex short code without separator (e.g. "05000070") — frontend strips
+    # the underscore from the displayed `MMMM_EEEE` before sending.
+    print_error: str = Field(..., min_length=8, max_length=8, pattern=r"^[0-9A-Fa-f]{8}$")
+    # One of the HMSAction enum values. Length-capped to keep stray input from
+    # reaching the dispatcher's `match` statement.
+    action: str = Field(..., min_length=1, max_length=64)
+    # The `subtask_id` snapshot from the HMSError that surfaced this dialog.
+    # Bambu echoes it back in HMS-aware commands. Optional for idle errors.
+    job_id: str | None = Field(default=None, max_length=64)
+
+
 class FilaSwitchResponse(BaseModel):
     """Filament Track Switch (FTS) state — accessory that mediates AMS-to-extruder routing.
 

+ 0 - 1100
backend/app/services/background_dispatch.py

@@ -1,1100 +0,0 @@
-"""Background dispatch for print/reprint jobs.
-
-This service is separate from the app's print queue feature. It exists only to
-decouple "send/start print" operations (FTP upload + start command) from API
-request latency so the UI can continue immediately after dispatch.
-"""
-
-from __future__ import annotations
-
-import asyncio
-import logging
-import time
-import zipfile
-from collections import deque
-from dataclasses import dataclass, field
-from pathlib import Path
-from typing import Any, Literal
-
-from sqlalchemy import select
-
-from backend.app.core.config import settings
-from backend.app.core.database import async_session
-from backend.app.core.tasks import spawn_background_task
-from backend.app.core.websocket import ws_manager
-from backend.app.models.library import LibraryFile
-from backend.app.models.printer import Printer
-from backend.app.services.archive import ArchiveService
-from backend.app.services.bambu_ftp import (
-    cache_3mf_download,
-    delete_file_async,
-    get_ftp_retry_settings,
-    upload_file_async,
-    with_ftp_retry,
-)
-from backend.app.services.printer_manager import printer_manager
-from backend.app.utils.filename import derive_remote_filename
-
-logger = logging.getLogger(__name__)
-
-# Bambu firmware states that mean the project_file has actually been accepted
-# and the printer is now processing / running / paused mid-print. Used by the
-# direct-dispatch verifier (#1370): a transition into one of these states means
-# the print landed, anything else (e.g. FINISH -> IDLE after the user dismisses
-# a post-print prompt) is NOT a valid "command landed" signal even though the
-# state value did change. Mirrors the same constant in print_scheduler.py —
-# kept duplicated rather than imported to avoid coupling the two services and
-# to keep the value at the point of use.
-_ACTIVE_PRINT_STATES: frozenset[str] = frozenset({"PREPARE", "SLICING", "RUNNING", "PAUSE"})
-
-
-class DispatchJobCancelled(Exception):
-    """Raised when a dispatch job is cancelled by the user."""
-
-
-class DispatchEnqueueRejected(Exception):
-    """Raised when a dispatch job should not be accepted."""
-
-
-@dataclass(slots=True)
-class PrintDispatchJob:
-    id: int
-    kind: Literal["reprint_archive", "print_library_file"]
-    source_id: int
-    source_name: str
-    printer_id: int
-    printer_name: str
-    options: dict[str, Any] = field(default_factory=dict)
-    requested_by_user_id: int | None = None
-    requested_by_username: str | None = None
-    project_id: int | None = None
-    cleanup_library_after_dispatch: bool = False
-
-
-@dataclass(slots=True)
-class ActiveDispatchState:
-    job: PrintDispatchJob
-    message: str
-    upload_bytes: int | None = None
-    upload_total_bytes: int | None = None
-
-
-class BackgroundDispatchService:
-    def __init__(self):
-        self._queued_jobs: deque[PrintDispatchJob] = deque()
-        self._dispatcher_task: asyncio.Task | None = None
-        self._running_tasks: dict[int, asyncio.Task] = {}
-        self._lock = asyncio.Lock()
-        self._job_event = asyncio.Event()
-        self._next_job_id = 1
-        self._active_jobs: dict[int, ActiveDispatchState] = {}
-        self._cancel_requested_job_ids: set[int] = set()
-
-        # Progress for the current "batch" (since queue became non-empty)
-        self._batch_total = 0
-        self._batch_completed = 0
-        self._batch_failed = 0
-
-    @staticmethod
-    def _printer_is_busy_printing(printer_id: int) -> bool:
-        state = printer_manager.get_status(printer_id)
-        if not state:
-            return False
-        return state.state in ("RUNNING", "PAUSE", "PAUSED") and bool(state.gcode_file)
-
-    async def start(self):
-        async with self._lock:
-            if self._dispatcher_task and not self._dispatcher_task.done():
-                return
-            self._dispatcher_task = asyncio.create_task(self._dispatcher_loop(), name="background-dispatch-dispatcher")
-            logger.info("Background dispatch dispatcher started")
-
-    async def stop(self):
-        dispatcher: asyncio.Task | None = None
-        running_tasks: list[asyncio.Task] = []
-        async with self._lock:
-            dispatcher = self._dispatcher_task
-            self._dispatcher_task = None
-            running_tasks = list(self._running_tasks.values())
-            self._running_tasks.clear()
-            self._active_jobs.clear()
-            self._queued_jobs.clear()
-            self._cancel_requested_job_ids.clear()
-            self._job_event.set()
-
-        if dispatcher:
-            dispatcher.cancel()
-        for task in running_tasks:
-            task.cancel()
-
-        if dispatcher:
-            try:
-                await dispatcher
-            except asyncio.CancelledError:
-                pass
-
-        if running_tasks:
-            await asyncio.gather(*running_tasks, return_exceptions=True)
-
-        logger.info("Background dispatch dispatcher stopped")
-
-    async def dispatch_reprint_archive(
-        self,
-        *,
-        archive_id: int,
-        archive_name: str,
-        printer_id: int,
-        printer_name: str,
-        options: dict[str, Any],
-        requested_by_user_id: int | None,
-        requested_by_username: str | None,
-    ) -> dict[str, Any]:
-        return await self._dispatch(
-            kind="reprint_archive",
-            source_id=archive_id,
-            source_name=archive_name,
-            printer_id=printer_id,
-            printer_name=printer_name,
-            options=options,
-            requested_by_user_id=requested_by_user_id,
-            requested_by_username=requested_by_username,
-        )
-
-    async def get_state(self) -> dict[str, Any]:
-        """Get current dispatch queue state snapshot for newly connected clients."""
-        async with self._lock:
-            return self._build_state_payload_unlocked()
-
-    async def dispatch_print_library_file(
-        self,
-        *,
-        file_id: int,
-        filename: str,
-        printer_id: int,
-        printer_name: str,
-        options: dict[str, Any],
-        requested_by_user_id: int | None,
-        requested_by_username: str | None,
-        project_id: int | None = None,
-        cleanup_library_after_dispatch: bool = False,
-    ) -> dict[str, Any]:
-        return await self._dispatch(
-            kind="print_library_file",
-            source_id=file_id,
-            source_name=filename,
-            printer_id=printer_id,
-            printer_name=printer_name,
-            options=options,
-            requested_by_user_id=requested_by_user_id,
-            requested_by_username=requested_by_username,
-            project_id=project_id,
-            cleanup_library_after_dispatch=cleanup_library_after_dispatch,
-        )
-
-    async def cancel_job(self, job_id: int) -> dict[str, Any]:
-        """Cancel a queued dispatch job.
-
-        Queued jobs are removed immediately. Active jobs are cancelled
-        cooperatively and will stop at the next cancellation checkpoint.
-        """
-        async with self._lock:
-            # Check active jobs first
-            active_state = self._active_jobs.get(job_id)
-            if active_state is not None:
-                logger.info("Cancel requested for active dispatch job %s", job_id)
-                self._cancel_requested_job_ids.add(job_id)
-                active_job = active_state.job
-                payload = self._build_state_payload_unlocked(
-                    recent_event={
-                        "status": "cancelling",
-                        "job_id": active_job.id,
-                        "source_name": active_job.source_name,
-                        "printer_id": active_job.printer_id,
-                        "printer_name": active_job.printer_name,
-                        "message": "Cancelling current dispatch...",
-                    }
-                )
-                result = {
-                    "cancelled": True,
-                    "pending": True,
-                    "job_id": active_job.id,
-                    "source_name": active_job.source_name,
-                    "printer_id": active_job.printer_id,
-                    "printer_name": active_job.printer_name,
-                }
-                await ws_manager.broadcast({"type": "background_dispatch", "data": payload})
-                return result
-
-            # Check queued jobs
-            cancelled_job: PrintDispatchJob | None = None
-            for job in self._queued_jobs:
-                if job.id == job_id:
-                    cancelled_job = job
-                    break
-
-            if not cancelled_job:
-                logger.info("Cancel requested for unknown dispatch job %s", job_id)
-                return {"cancelled": False, "reason": "not_found"}
-
-            self._queued_jobs.remove(cancelled_job)
-            logger.info("Cancelled queued dispatch job %s", cancelled_job.id)
-            self._batch_total = max(0, self._batch_total - 1)
-
-            if self._batch_total == 0 and len(self._queued_jobs) == 0 and len(self._active_jobs) == 0:
-                self._batch_completed = 0
-                self._batch_failed = 0
-
-            payload = self._build_state_payload_unlocked(
-                recent_event={
-                    "status": "cancelled",
-                    "job_id": cancelled_job.id,
-                    "source_name": cancelled_job.source_name,
-                    "printer_id": cancelled_job.printer_id,
-                    "printer_name": cancelled_job.printer_name,
-                    "message": "Cancelled from queue",
-                }
-            )
-
-        await ws_manager.broadcast({"type": "background_dispatch", "data": payload})
-        return {
-            "cancelled": True,
-            "pending": False,
-            "job_id": cancelled_job.id,
-            "source_name": cancelled_job.source_name,
-            "printer_id": cancelled_job.printer_id,
-            "printer_name": cancelled_job.printer_name,
-        }
-
-    async def _dispatch(
-        self,
-        *,
-        kind: Literal["reprint_archive", "print_library_file"],
-        source_id: int,
-        source_name: str,
-        printer_id: int,
-        printer_name: str,
-        options: dict[str, Any],
-        requested_by_user_id: int | None,
-        requested_by_username: str | None,
-        project_id: int | None = None,
-        cleanup_library_after_dispatch: bool = False,
-    ) -> dict[str, Any]:
-        async with self._lock:
-            has_pending_for_printer = any(job.printer_id == printer_id for job in self._queued_jobs)
-            has_active_for_printer = any(active.job.printer_id == printer_id for active in self._active_jobs.values())
-
-            if has_pending_for_printer or has_active_for_printer:
-                raise DispatchEnqueueRejected(f"Printer {printer_name} already has a background dispatch in progress")
-
-            if self._printer_is_busy_printing(printer_id):
-                raise DispatchEnqueueRejected(f"Printer {printer_name} is currently busy printing")
-
-            dispatch_position = len(self._queued_jobs) + len(self._active_jobs) + 1
-            job = PrintDispatchJob(
-                id=self._next_job_id,
-                kind=kind,
-                source_id=source_id,
-                source_name=source_name,
-                printer_id=printer_id,
-                printer_name=printer_name,
-                options=options,
-                requested_by_user_id=requested_by_user_id,
-                requested_by_username=requested_by_username,
-                project_id=project_id,
-                cleanup_library_after_dispatch=cleanup_library_after_dispatch,
-            )
-            self._next_job_id += 1
-            self._batch_total += 1
-            self._queued_jobs.append(job)
-            self._job_event.set()
-
-            payload = self._build_state_payload_unlocked(
-                recent_event={
-                    "status": "dispatched",
-                    "job_id": job.id,
-                    "source_name": source_name,
-                    "printer_id": printer_id,
-                    "printer_name": printer_name,
-                    "message": f"Dispatched to {printer_name}",
-                }
-            )
-
-        await ws_manager.broadcast({"type": "background_dispatch", "data": payload})
-
-        return {
-            "dispatch_job_id": job.id,
-            "dispatch_position": dispatch_position,
-            "status": "dispatched",
-            "printer_id": printer_id,
-            "source_id": source_id,
-            "source_name": source_name,
-        }
-
-    async def _dispatcher_loop(self):
-        while True:
-            await self._job_event.wait()
-            self._job_event.clear()
-
-            while True:
-                payload: dict[str, Any] | None = None
-                job_to_start: PrintDispatchJob | None = None
-                async with self._lock:
-                    busy_printer_ids = {state.job.printer_id for state in self._active_jobs.values()}
-                    start_index = next(
-                        (
-                            idx
-                            for idx, queued_job in enumerate(self._queued_jobs)
-                            if queued_job.printer_id not in busy_printer_ids
-                        ),
-                        None,
-                    )
-
-                    if start_index is None:
-                        break
-
-                    job_to_start = self._queued_jobs[start_index]
-                    del self._queued_jobs[start_index]
-                    self._active_jobs[job_to_start.id] = ActiveDispatchState(
-                        job=job_to_start,
-                        message="Preparing background dispatch...",
-                    )
-
-                    task = asyncio.create_task(
-                        self._run_active_job(job_to_start), name=f"background-dispatch-job-{job_to_start.id}"
-                    )
-                    self._running_tasks[job_to_start.id] = task
-
-                    payload = self._build_state_payload_unlocked(
-                        recent_event={
-                            "status": "processing",
-                            "job_id": job_to_start.id,
-                            "source_name": job_to_start.source_name,
-                            "printer_id": job_to_start.printer_id,
-                            "printer_name": job_to_start.printer_name,
-                            "message": "Preparing background dispatch...",
-                        }
-                    )
-
-                if payload:
-                    await ws_manager.broadcast({"type": "background_dispatch", "data": payload})
-
-    async def _run_active_job(self, job: PrintDispatchJob):
-        try:
-            await self._process_job(job)
-            await self._mark_job_finished(job, failed=False, message="Background dispatch complete")
-        except DispatchJobCancelled:
-            await self._mark_job_cancelled(job)
-        except asyncio.CancelledError:
-            raise
-        except Exception as e:
-            logger.error("Background dispatch job %s failed: %s", job.id, e, exc_info=True)
-            await self._mark_job_finished(job, failed=True, message=str(e))
-        finally:
-            self._job_event.set()
-
-    async def _set_active_message(self, job: PrintDispatchJob, message: str):
-        async with self._lock:
-            active = self._active_jobs.get(job.id)
-            if not active:
-                return
-            active.message = message
-            payload = self._build_state_payload_unlocked(
-                recent_event={
-                    "status": "processing",
-                    "job_id": active.job.id,
-                    "source_name": active.job.source_name,
-                    "printer_id": active.job.printer_id,
-                    "printer_name": active.job.printer_name,
-                    "message": message,
-                }
-            )
-        await ws_manager.broadcast({"type": "background_dispatch", "data": payload})
-
-    async def _set_active_upload_progress(self, job: PrintDispatchJob, uploaded: int, total: int):
-        async with self._lock:
-            active = self._active_jobs.get(job.id)
-            if not active:
-                return
-
-            active.upload_bytes = max(0, int(uploaded))
-            active.upload_total_bytes = max(0, int(total))
-            payload = self._build_state_payload_unlocked(
-                recent_event={
-                    "status": "processing",
-                    "job_id": active.job.id,
-                    "source_name": active.job.source_name,
-                    "printer_id": active.job.printer_id,
-                    "printer_name": active.job.printer_name,
-                    "message": active.message,
-                }
-            )
-        await ws_manager.broadcast({"type": "background_dispatch", "data": payload})
-
-    async def _mark_job_finished(self, job: PrintDispatchJob, *, failed: bool, message: str):
-        async with self._lock:
-            if failed:
-                self._batch_failed += 1
-            else:
-                self._batch_completed += 1
-
-            self._active_jobs.pop(job.id, None)
-            self._running_tasks.pop(job.id, None)
-            self._cancel_requested_job_ids.discard(job.id)
-
-            payload = self._build_state_payload_unlocked(
-                recent_event={
-                    "status": "failed" if failed else "completed",
-                    "job_id": job.id,
-                    "source_name": job.source_name,
-                    "printer_id": job.printer_id,
-                    "printer_name": job.printer_name,
-                    "message": message,
-                }
-            )
-            should_reset_batch = len(self._queued_jobs) == 0 and len(self._active_jobs) == 0
-
-        await ws_manager.broadcast({"type": "background_dispatch", "data": payload})
-
-        if should_reset_batch:
-            async with self._lock:
-                if len(self._queued_jobs) == 0 and len(self._active_jobs) == 0:
-                    self._batch_total = 0
-                    self._batch_completed = 0
-                    self._batch_failed = 0
-
-    async def _mark_job_cancelled(self, job: PrintDispatchJob):
-        async with self._lock:
-            self._active_jobs.pop(job.id, None)
-            self._running_tasks.pop(job.id, None)
-            self._cancel_requested_job_ids.discard(job.id)
-            self._batch_total = max(0, self._batch_total - 1)
-
-            if self._batch_total == 0 and len(self._queued_jobs) == 0 and len(self._active_jobs) == 0:
-                self._batch_completed = 0
-                self._batch_failed = 0
-
-            payload = self._build_state_payload_unlocked(
-                recent_event={
-                    "status": "cancelled",
-                    "job_id": job.id,
-                    "source_name": job.source_name,
-                    "printer_id": job.printer_id,
-                    "printer_name": job.printer_name,
-                    "message": "Cancelled during dispatch",
-                }
-            )
-
-        await ws_manager.broadcast({"type": "background_dispatch", "data": payload})
-
-    def _is_cancel_requested(self, job_id: int) -> bool:
-        return job_id in self._cancel_requested_job_ids
-
-    def _raise_if_cancel_requested(self, job: PrintDispatchJob):
-        if self._is_cancel_requested(job.id):
-            raise DispatchJobCancelled(f"Dispatch job {job.id} cancelled")
-
-    def _build_state_payload_unlocked(self, recent_event: dict[str, Any] | None = None) -> dict[str, Any]:
-        processing = len(self._active_jobs)
-        dispatched = len(self._queued_jobs)
-
-        dispatched_jobs = [
-            {
-                "job_id": job.id,
-                "kind": job.kind,
-                "source_id": job.source_id,
-                "source_name": job.source_name,
-                "printer_id": job.printer_id,
-                "printer_name": job.printer_name,
-            }
-            for job in list(self._queued_jobs)
-        ]
-
-        active_jobs: list[dict[str, Any]] = []
-        for active in self._active_jobs.values():
-            upload_progress_pct = None
-            if active.upload_total_bytes and active.upload_total_bytes > 0 and active.upload_bytes is not None:
-                upload_progress_pct = round(
-                    max(0.0, min(100.0, (active.upload_bytes / active.upload_total_bytes) * 100.0)), 1
-                )
-
-            active_jobs.append(
-                {
-                    "job_id": active.job.id,
-                    "kind": active.job.kind,
-                    "source_id": active.job.source_id,
-                    "source_name": active.job.source_name,
-                    "printer_id": active.job.printer_id,
-                    "printer_name": active.job.printer_name,
-                    "message": active.message,
-                    "upload_bytes": active.upload_bytes,
-                    "upload_total_bytes": active.upload_total_bytes,
-                    "upload_progress_pct": upload_progress_pct,
-                }
-            )
-
-        active_jobs.sort(key=lambda item: int(item["job_id"]))
-        active_job = active_jobs[0] if active_jobs else None
-
-        return {
-            "total": self._batch_total,
-            "dispatched": dispatched,
-            "processing": processing,
-            "completed": self._batch_completed,
-            "failed": self._batch_failed,
-            "dispatched_jobs": dispatched_jobs,
-            "active_jobs": active_jobs,
-            "active_job": active_job,
-            "recent_event": recent_event,
-        }
-
-    async def _process_job(self, job: PrintDispatchJob):
-        if job.kind == "reprint_archive":
-            await self._run_reprint_archive(job)
-            return
-        if job.kind == "print_library_file":
-            await self._run_print_library_file(job)
-            return
-        raise RuntimeError(f"Unknown dispatch job kind: {job.kind}")
-
-    async def _run_reprint_archive(self, job: PrintDispatchJob):
-        from backend.app.main import register_expected_print
-
-        async with async_session() as db:
-            service = ArchiveService(db)
-            archive = await service.get_archive(job.source_id)
-            if not archive:
-                raise RuntimeError("Archive not found")
-
-            printer = await db.scalar(select(Printer).where(Printer.id == job.printer_id))
-            if not printer:
-                raise RuntimeError("Printer not found")
-
-            printer_name = printer.name
-            printer_ip = printer.ip_address
-            printer_access_code = printer.access_code
-            printer_model = printer.model
-            archive_filename = archive.filename
-
-            if not printer_manager.is_connected(job.printer_id):
-                raise RuntimeError("Printer is not connected")
-
-            file_path = settings.base_dir / archive.file_path
-            if not file_path.exists():
-                raise RuntimeError("Archive file not found")
-
-            remote_filename = derive_remote_filename(archive.filename)
-            remote_path = f"/{remote_filename}"
-
-            ftp_retry_enabled, ftp_retry_count, ftp_retry_delay, ftp_timeout = await get_ftp_retry_settings()
-            self._raise_if_cancel_requested(job)
-
-            await self._set_active_message(job, f"Preparing upload to {printer_name}...")
-            await delete_file_async(
-                printer_ip,
-                printer_access_code,
-                remote_path,
-                socket_timeout=ftp_timeout,
-                printer_model=printer_model,
-            )
-
-            self._raise_if_cancel_requested(job)
-
-            try:
-                await self._set_active_message(job, f"Uploading {archive_filename} to {printer_name}...")
-                loop = asyncio.get_running_loop()
-                progress_state = {"last_emit": 0.0, "last_bytes": 0}
-
-                def upload_progress_callback(uploaded: int, total: int):
-                    if self._is_cancel_requested(job.id):
-                        raise DispatchJobCancelled(f"Dispatch job {job.id} cancelled during upload")
-
-                    now = time.monotonic()
-                    should_emit = (
-                        uploaded >= total
-                        or now - progress_state["last_emit"] >= 0.2
-                        or uploaded - progress_state["last_bytes"] >= 256 * 1024
-                    )
-
-                    if should_emit:
-                        progress_state["last_emit"] = now
-                        progress_state["last_bytes"] = uploaded
-                        loop.call_soon_threadsafe(
-                            lambda u=uploaded, t=total: spawn_background_task(
-                                self._set_active_upload_progress(job, u, t),
-                                name=f"upload-progress-{job.id}",
-                            )
-                        )
-
-                if ftp_retry_enabled:
-                    uploaded = await with_ftp_retry(
-                        upload_file_async,
-                        printer_ip,
-                        printer_access_code,
-                        file_path,
-                        remote_path,
-                        progress_callback=upload_progress_callback,
-                        socket_timeout=ftp_timeout,
-                        printer_model=printer_model,
-                        max_retries=ftp_retry_count,
-                        retry_delay=ftp_retry_delay,
-                        operation_name=f"Upload for reprint to {printer_name}",
-                        non_retry_exceptions=(DispatchJobCancelled,),
-                    )
-                else:
-                    uploaded = await upload_file_async(
-                        printer_ip,
-                        printer_access_code,
-                        file_path,
-                        remote_path,
-                        progress_callback=upload_progress_callback,
-                        socket_timeout=ftp_timeout,
-                        printer_model=printer_model,
-                    )
-
-                if uploaded:
-                    await self._set_active_upload_progress(job, 1, 1)
-
-                if not uploaded:
-                    raise RuntimeError(
-                        "Failed to upload file to printer. Check if SD card is inserted and properly formatted (FAT32/exFAT)."
-                    )
-
-                # Resolve plate_id before register so usage tracking can scope the
-                # 3MF parse to the dispatched plate at print-start (#1697). Pure
-                # transform of file_path + options, safe to reorder.
-                plate_id = self._resolve_plate_id(file_path, job.options.get("plate_id"))
-
-                register_expected_print(
-                    job.printer_id,
-                    remote_filename,
-                    job.source_id,
-                    ams_mapping=job.options.get("ams_mapping"),
-                    plate_id=plate_id,
-                )
-
-                self._raise_if_cancel_requested(job)
-
-                effective_timelapse = bool(job.options.get("timelapse", False))
-
-                await self._set_active_message(job, f"Starting print on {printer_name}...")
-                started = printer_manager.start_print(
-                    job.printer_id,
-                    remote_filename,
-                    plate_id,
-                    ams_mapping=job.options.get("ams_mapping"),
-                    timelapse=effective_timelapse,
-                    bed_levelling=job.options.get("bed_levelling", True),
-                    flow_cali=job.options.get("flow_cali", False),
-                    vibration_cali=job.options.get("vibration_cali", True),
-                    layer_inspect=job.options.get("layer_inspect", False),
-                    use_ams=job.options.get("use_ams", True),
-                    nozzle_offset_cali=job.options.get("nozzle_offset_cali", False),
-                )
-
-                if not started:
-                    await self._cleanup_sd_card_file(
-                        printer_ip,
-                        printer_access_code,
-                        remote_path,
-                        printer_model,
-                    )
-                    raise RuntimeError("Failed to start print")
-
-                # Register the archive's local 3MF in the cover-cache so the
-                # /cover endpoint can skip FTP — we already have the file on
-                # disk, no need to refetch 36 MB from a printer whose FTP is
-                # busy serving the active print (#1166 follow-up).
-                cache_3mf_download(job.printer_id, remote_filename, file_path)
-
-                # Wait for the printer to actually pick up the command before
-                # marking the dispatch job complete (#1042). MQTT-publish success
-                # only proves the command queued locally; the printer can still
-                # reject it (HMS error pending, half-broken session, SD card
-                # missing) and never transition. Until #1042 this watchdog was
-                # fire-and-forget — the job was reported successful and the
-                # user had no signal that the print never started. The uploaded
-                # file is intentionally left on the printer's SD card on
-                # timeout: the next dispatch will overwrite it via the existing
-                # delete-then-upload step, and the printer may still be in the
-                # middle of reading it if it picked up just past the timeout.
-                pre_status = printer_manager.get_status(job.printer_id)
-                pre_state = getattr(pre_status, "state", None) if pre_status else None
-                pre_subtask_id = getattr(pre_status, "subtask_id", None) if pre_status else None
-                pre_gcode_file = getattr(pre_status, "gcode_file", None) if pre_status else None
-                if pre_state:
-                    await self._set_active_message(job, f"Waiting for {printer_name} to acknowledge print...")
-                    transitioned = await self._verify_print_response(
-                        job.printer_id,
-                        printer_name,
-                        pre_state,
-                        pre_subtask_id=pre_subtask_id,
-                        pre_gcode_file=pre_gcode_file,
-                    )
-                    if not transitioned:
-                        raise RuntimeError(
-                            f"Printer did not acknowledge print command — state still {pre_state}. "
-                            f"Check the printer for a pending error (HMS code, plate-clear prompt, "
-                            f"SD card) and try again."
-                        )
-
-                if job.requested_by_user_id and job.requested_by_username:
-                    printer_manager.set_current_print_user(
-                        job.printer_id,
-                        job.requested_by_user_id,
-                        job.requested_by_username,
-                    )
-            except DispatchJobCancelled:
-                await self._set_active_message(job, f"Cancelled upload on {printer_name}.")
-                raise
-
-    async def _run_print_library_file(self, job: PrintDispatchJob):
-        from backend.app.main import register_expected_print
-
-        async with async_session() as db:
-            lib_file = await db.scalar(LibraryFile.active().where(LibraryFile.id == job.source_id))
-            if not lib_file:
-                raise RuntimeError("File not found")
-
-            if not self._is_sliced_file(lib_file.filename):
-                raise RuntimeError("Not a sliced file. Only .gcode or .gcode.3mf files can be printed.")
-
-            file_path = Path(settings.base_dir) / lib_file.file_path
-            if not file_path.exists():
-                raise RuntimeError("File not found on disk")
-
-            printer = await db.scalar(select(Printer).where(Printer.id == job.printer_id))
-            if not printer:
-                raise RuntimeError("Printer not found")
-
-            printer_name = printer.name
-            printer_ip = printer.ip_address
-            printer_access_code = printer.access_code
-            printer_model = printer.model
-            library_filename = lib_file.filename
-
-            if not printer_manager.is_connected(job.printer_id):
-                raise RuntimeError("Printer is not connected")
-
-            await self._set_active_message(job, f"Creating archive for {lib_file.filename}...")
-            archive_service = ArchiveService(db)
-            archive = await archive_service.archive_print(
-                printer_id=job.printer_id,
-                source_file=file_path,
-                original_filename=lib_file.filename,
-                project_id=job.project_id,
-                created_by_id=job.requested_by_user_id,
-            )
-            if not archive:
-                raise RuntimeError("Failed to create archive")
-
-            await db.flush()
-
-            remote_filename = derive_remote_filename(lib_file.filename)
-            remote_path = f"/{remote_filename}"
-
-            ftp_retry_enabled, ftp_retry_count, ftp_retry_delay, ftp_timeout = await get_ftp_retry_settings()
-            self._raise_if_cancel_requested(job)
-
-            await self._set_active_message(job, f"Preparing upload to {printer_name}...")
-            await delete_file_async(
-                printer_ip,
-                printer_access_code,
-                remote_path,
-                socket_timeout=ftp_timeout,
-                printer_model=printer_model,
-            )
-
-            self._raise_if_cancel_requested(job)
-
-            try:
-                await self._set_active_message(job, f"Uploading {library_filename} to {printer_name}...")
-                loop = asyncio.get_running_loop()
-                progress_state = {"last_emit": 0.0, "last_bytes": 0}
-
-                def upload_progress_callback(uploaded: int, total: int):
-                    if self._is_cancel_requested(job.id):
-                        raise DispatchJobCancelled(f"Dispatch job {job.id} cancelled during upload")
-
-                    now = time.monotonic()
-                    should_emit = (
-                        uploaded >= total
-                        or now - progress_state["last_emit"] >= 0.2
-                        or uploaded - progress_state["last_bytes"] >= 256 * 1024
-                    )
-
-                    if should_emit:
-                        progress_state["last_emit"] = now
-                        progress_state["last_bytes"] = uploaded
-                        loop.call_soon_threadsafe(
-                            lambda u=uploaded, t=total: spawn_background_task(
-                                self._set_active_upload_progress(job, u, t),
-                                name=f"upload-progress-{job.id}",
-                            )
-                        )
-
-                if ftp_retry_enabled:
-                    uploaded = await with_ftp_retry(
-                        upload_file_async,
-                        printer_ip,
-                        printer_access_code,
-                        file_path,
-                        remote_path,
-                        progress_callback=upload_progress_callback,
-                        socket_timeout=ftp_timeout,
-                        printer_model=printer_model,
-                        max_retries=ftp_retry_count,
-                        retry_delay=ftp_retry_delay,
-                        operation_name=f"Upload for print to {printer_name}",
-                        non_retry_exceptions=(DispatchJobCancelled,),
-                    )
-                else:
-                    uploaded = await upload_file_async(
-                        printer_ip,
-                        printer_access_code,
-                        file_path,
-                        remote_path,
-                        progress_callback=upload_progress_callback,
-                        socket_timeout=ftp_timeout,
-                        printer_model=printer_model,
-                    )
-
-                if uploaded:
-                    await self._set_active_upload_progress(job, 1, 1)
-
-                if not uploaded:
-                    await db.rollback()
-                    raise RuntimeError(
-                        "Failed to upload file to printer. Check if SD card is inserted and properly formatted (FAT32/exFAT)."
-                    )
-
-                # Resolve plate_id before register so usage tracking can scope the
-                # 3MF parse to the dispatched plate at print-start (#1697).
-                plate_id = self._resolve_plate_id(file_path, job.options.get("plate_id"))
-
-                register_expected_print(
-                    job.printer_id,
-                    remote_filename,
-                    archive.id,
-                    ams_mapping=job.options.get("ams_mapping"),
-                    plate_id=plate_id,
-                )
-
-                self._raise_if_cancel_requested(job)
-
-                effective_timelapse = bool(job.options.get("timelapse", False))
-
-                await self._set_active_message(job, f"Starting print on {printer_name}...")
-                started = printer_manager.start_print(
-                    job.printer_id,
-                    remote_filename,
-                    plate_id,
-                    ams_mapping=job.options.get("ams_mapping"),
-                    timelapse=effective_timelapse,
-                    bed_levelling=job.options.get("bed_levelling", True),
-                    flow_cali=job.options.get("flow_cali", False),
-                    vibration_cali=job.options.get("vibration_cali", True),
-                    layer_inspect=job.options.get("layer_inspect", False),
-                    use_ams=job.options.get("use_ams", True),
-                    nozzle_offset_cali=job.options.get("nozzle_offset_cali", False),
-                )
-
-                if not started:
-                    await self._cleanup_sd_card_file(
-                        printer_ip,
-                        printer_access_code,
-                        remote_path,
-                        printer_model,
-                    )
-                    await db.rollback()
-                    raise RuntimeError("Failed to start print")
-
-                # Same as the archive path: register the library file's local
-                # 3MF in the cover-cache so /cover skips FTP (#1166 follow-up).
-                cache_3mf_download(job.printer_id, remote_filename, file_path)
-
-                # See _run_reprint_archive for rationale (#1042). On timeout
-                # also rolls back the freshly-created archive so the library
-                # flow doesn't leave behind a phantom row for a print that
-                # never started.
-                pre_status = printer_manager.get_status(job.printer_id)
-                pre_state = getattr(pre_status, "state", None) if pre_status else None
-                pre_subtask_id = getattr(pre_status, "subtask_id", None) if pre_status else None
-                pre_gcode_file = getattr(pre_status, "gcode_file", None) if pre_status else None
-                if pre_state:
-                    await self._set_active_message(job, f"Waiting for {printer_name} to acknowledge print...")
-                    transitioned = await self._verify_print_response(
-                        job.printer_id,
-                        printer_name,
-                        pre_state,
-                        pre_subtask_id=pre_subtask_id,
-                        pre_gcode_file=pre_gcode_file,
-                    )
-                    if not transitioned:
-                        await db.rollback()
-                        raise RuntimeError(
-                            f"Printer did not acknowledge print command — state still {pre_state}. "
-                            f"Check the printer for a pending error (HMS code, plate-clear prompt, "
-                            f"SD card) and try again."
-                        )
-
-                if job.requested_by_user_id and job.requested_by_username:
-                    printer_manager.set_current_print_user(
-                        job.printer_id,
-                        job.requested_by_user_id,
-                        job.requested_by_username,
-                    )
-
-                # Direct-Print flow only: archive_print copies, so deleting the
-                # transient library row + files here leaves archive intact. Disk
-                # deletes run only after commit so a rollback leaves no orphan.
-                cleanup_disk_paths: list[Path] = []
-                if job.cleanup_library_after_dispatch and not lib_file.is_external:
-                    cleanup_disk_paths.append(file_path)
-                    if lib_file.thumbnail_path:
-                        thumb_path = Path(lib_file.thumbnail_path)
-                        if not thumb_path.is_absolute():
-                            thumb_path = Path(settings.base_dir) / lib_file.thumbnail_path
-                        cleanup_disk_paths.append(thumb_path)
-                    await db.delete(lib_file)
-
-                await db.commit()
-
-                for cleanup_path in cleanup_disk_paths:
-                    try:
-                        if cleanup_path.exists():
-                            cleanup_path.unlink()
-                    except OSError as cleanup_err:
-                        logger.warning("Failed to delete transient library file %s: %s", cleanup_path, cleanup_err)
-            except DispatchJobCancelled:
-                await db.rollback()
-                await self._set_active_message(job, f"Cancelled upload on {printer_name}.")
-                raise
-
-    @staticmethod
-    async def _verify_print_response(
-        printer_id: int,
-        printer_name: str,
-        pre_state: str,
-        pre_subtask_id: str | None = None,
-        pre_gcode_file: str | None = None,
-        timeout: float = 90.0,
-        poll_interval: float = 3.0,
-    ) -> bool:
-        """Wait for the printer to acknowledge a print command.
-
-        Returns True if the printer transitioned (state advanced past pre_state
-        or subtask_id advanced past pre_subtask_id). Returns False on timeout —
-        in that case logs a warning and forces an MQTT reconnect, mirroring the
-        queue-side watchdog (`_watchdog_print_start`). Caller is responsible
-        for surfacing the False result to the user (typically by raising so the
-        dispatch job is marked failed).
-
-        Both transition signals are checked because H2D can sit at FINISH for
-        ~50 s after accepting `project_file` before flipping to PREPARE; the
-        printer echoes our per-dispatch identity back as `subtask_id` on
-        `push_status` first, so a subtask_id change is a definitive "command
-        landed" signal even while state is still FINISH (#1078).
-        """
-        deadline = time.monotonic() + timeout
-        last_status = None
-        while time.monotonic() < deadline:
-            await asyncio.sleep(poll_interval)
-            state = printer_manager.get_status(printer_id)
-            if not state:
-                # Printer momentarily not reporting — could be a brief MQTT
-                # disconnect mid-window. Keep polling rather than declaring
-                # failure on the first missed tick; the printer may reconnect
-                # within the remaining timeout and still surface a transition.
-                continue
-            last_status = state
-            if state.state in _ACTIVE_PRINT_STATES:
-                # Printer is actively processing the job. We do NOT accept
-                # arbitrary state transitions: a printer going FINISH -> IDLE
-                # (user dismissed the post-print prompt without accepting our
-                # project_file) would otherwise look like "command landed"
-                # and the dispatch job would be marked successful even though
-                # no print is running (#1370).
-                return True
-            if pre_subtask_id is not None and state.subtask_id is not None and state.subtask_id != pre_subtask_id:
-                # Printer picked up the job (subtask_id advanced). H2D can
-                # sit at FINISH for ~50 s after accepting project_file before
-                # transitioning to PREPARE, but the subtask_id flips to our
-                # submission_id almost immediately (#1078).
-                return True
-        logger.warning(
-            "Printer %s (%d) did not respond to print command within %.0fs "
-            "(state still %s, subtask_id still %s) — printer may need restart",
-            printer_name,
-            printer_id,
-            timeout,
-            pre_state,
-            pre_subtask_id,
-        )
-        # Distinguish #1150 (slow parse) from #887/#936 (half-broken session)
-        # via gcode_file: if the printer is now showing a different file than
-        # before dispatch, the project_file command landed and the printer is
-        # parsing — a forced reconnect mid-parse causes 0500_4003. If
-        # gcode_file is unchanged, the publish was silently swallowed and the
-        # original #936 recovery (force_reconnect → fresh client_id) is what
-        # we want. Caveat: in the rare retry-same-file-after-timeout case the
-        # printer's gcode_file looks identical before and after the publish
-        # lands, so a slow parse on retry-same-file still falls through to the
-        # reconnect (and the original 0500_4003) — accepted to avoid breaking
-        # the half-broken-session recovery path.
-        client = printer_manager.get_client(printer_id)
-        current_gcode_file = getattr(last_status, "gcode_file", None) if last_status else None
-        publish_landed = current_gcode_file is not None and current_gcode_file != pre_gcode_file
-        if publish_landed:
-            logger.warning(
-                "Printer %s (%d) gcode_file changed to %r (was %r) — printer "
-                "received the command and is parsing slowly. Skipping forced "
-                "MQTT reconnect to avoid 0500_4003 mid-parse (#1150).",
-                printer_name,
-                printer_id,
-                current_gcode_file,
-                pre_gcode_file,
-            )
-        elif client and hasattr(client, "force_reconnect_stale_session"):
-            client.force_reconnect_stale_session(
-                f"print command unacknowledged after {timeout:.0f}s "
-                f"(state still {pre_state}, gcode_file {current_gcode_file!r})"
-            )
-        return False
-
-    @staticmethod
-    async def _cleanup_sd_card_file(
-        printer_ip: str,
-        access_code: str,
-        remote_path: str,
-        printer_model: str | None,
-    ):
-        """Best-effort delete of uploaded file from printer SD card."""
-        try:
-            await delete_file_async(printer_ip, access_code, remote_path, printer_model=printer_model)
-        except Exception:
-            pass  # Best-effort — don't fail the error handler
-
-    @staticmethod
-    def _resolve_plate_id(file_path: Path, requested_plate_id: int | None) -> int:
-        if requested_plate_id is not None:
-            return requested_plate_id
-
-        plate_id = 1
-        try:
-            with zipfile.ZipFile(file_path, "r") as zf:
-                for name in zf.namelist():
-                    if name.startswith("Metadata/plate_") and name.endswith(".gcode"):
-                        plate_str = name[15:-6]
-                        plate_id = int(plate_str)
-                        break
-        except (ValueError, zipfile.BadZipFile, OSError):
-            pass
-        return plate_id
-
-    @staticmethod
-    def _is_sliced_file(filename: str) -> bool:
-        lower = filename.lower()
-        return lower.endswith(".gcode") or lower.endswith(".gcode.3mf")
-
-
-background_dispatch = BackgroundDispatchService()

+ 228 - 1
backend/app/services/bambu_mqtt.py

@@ -21,6 +21,8 @@ from datetime import datetime, timezone
 
 import paho.mqtt.client as mqtt
 
+from backend.app.services.hms_actions import HMSAction, get_actions_for_error_code
+
 logger = logging.getLogger(__name__)
 
 # AMS module name prefixes used in get_version responses.
@@ -169,6 +171,15 @@ class HMSError:
     module: int
     severity: int  # 1=fatal, 2=serious, 3=common, 4=info
     message: str = ""
+    # User-facing remediation actions from the bundled HMS catalog (e.g. "RESUME_PRINTING",
+    # "CHECK_ASSISTANT"). Defaults to an empty list rather than None so the field always
+    # satisfies HMSErrorResponse.actions: list[str] — a future code path that builds an
+    # HMSError without explicitly passing actions can't silently land None on the schema
+    # boundary and raise ValidationError at routes/printers.py response time.
+    actions: list[str] = field(default_factory=list)
+    # The `subtask_id` snapshotted from PrinterState when this error surfaced; Bambu's
+    # HMS-aware commands echo it back as `job_id`. None for idle errors with no job.
+    job_id: str | None = None
 
 
 # HMS short codes the firmware emits during normal user-cancel sequences.
@@ -680,7 +691,7 @@ class BambuMQTTClient:
         #
         # Two routing paths:
         #
-        # Async-context callers (background_dispatch.py:993 — dispatch deadline)
+        # Async-context callers (queue dispatch deadline)
         #   → full client teardown + fresh client_id. Wipes paho's client-side
         #     QoS 1 queue, which is exactly the #1136 reproducer: an unacked
         #     `project_file` from the broken session would otherwise replay on
@@ -2729,12 +2740,15 @@ class BambuMQTTClient:
                         short_code = f"{(attr >> 16) & 0xFFFF:04X}_{code & 0xFFFF:04X}"
                         if short_code in _HMS_USER_ACTION_CODES:
                             continue
+                        actions = get_actions_for_error_code(self.serial_number[:3], short_code.replace("_", ""))
                         self.state.hms_errors.append(
                             HMSError(
                                 code=f"0x{code:x}" if code else "0x0",
                                 attr=attr,
                                 module=module,
                                 severity=severity if severity > 0 else 2,
+                                actions=actions,
+                                job_id=self.state.subtask_id,
                             )
                         )
 
@@ -2780,12 +2794,32 @@ class BambuMQTTClient:
                             existing_short_codes.add(f"{e_module:04X}_{e_error:04X}")
 
                         if short_code not in existing_short_codes:
+                            # Bambu's HMS catalog keys by 3-letter device code (the SN
+                            # prefix) and a 16-char short error code without the
+                            # underscore separator we store internally.
+                            actions = get_actions_for_error_code(self.serial_number[:3], short_code.replace("_", ""))
+                            # Bambu pushes the current job as `subtask_id` on the
+                            # state stream; the HMS-action commands echo it back as
+                            # `job_id`. The error payload itself doesn't carry the
+                            # id, so snapshot it from the live state at parse time
+                            # and freeze it on the HMSError so subsequent
+                            # job changes don't invalidate the action.
+                            job_id = self.state.subtask_id
+                            logger.debug(
+                                "[%s, %s] HMS available actions: %s (job_id=%s)",
+                                self.serial_number[:3],
+                                short_code.replace("_", ""),
+                                actions,
+                                job_id,
+                            )
                             self.state.hms_errors.append(
                                 HMSError(
                                     code=f"0x{error:x}",
                                     attr=print_error,  # Store full value for display
                                     module=module >> 8,  # High byte of module (e.g., 0x05)
                                     severity=3,  # Warning level for print_error
+                                    actions=actions,
+                                    job_id=job_id,
                                 )
                             )
 
@@ -5378,3 +5412,196 @@ class BambuMQTTClient:
         self._client.publish(self.topic_publish, json.dumps(pushall), qos=1)
         logger.info("[%s] Set liveview %s", self.serial_number, "enabled" if enable else "disabled")
         return True
+
+    def execute_hms_action(self, print_error: str, action: str, job_id: str | None = None) -> bool:
+        """Dispatch the user's choice from the HMS-error modal as a printer command.
+
+        Args:
+            print_error: 8-char hex short code with no separator (e.g. "05000070").
+                The frontend strips the underscore from the displayed `MMMM_EEEE`
+                before sending.
+            action: One of HMSAction's string values.
+            job_id: The `subtask_id` snapshotted onto the HMSError at parse-time.
+                Bambu's HMS-aware commands echo it back as `job_id`. May be None
+                for idle errors that never had a job.
+
+        Returns False when the MQTT client is offline or when `action` is unknown
+        so the route surfaces it as a 4xx rather than a silent no-op.
+        """
+
+        if not self._client or not self.state.connected:
+            logger.warning("[%s] Cannot execute HMS action: not connected", self.serial_number)
+            return False
+
+        # Always re-push the full state after a command so the modal's underlying
+        # status query reflects the new error list (or absence) on the next tick.
+        def publish(payload: dict):
+            self._client.publish(self.topic_publish, json.dumps(payload), qos=1)
+            self._client.publish(
+                self.topic_publish, json.dumps({"pushing": {"command": "pushall", "sequence_id": "0"}}), qos=1
+            )
+
+        def hms_resume():
+            publish(
+                {
+                    "print": {
+                        "command": "resume",
+                        "err": print_error,
+                        "param": "reserve",
+                        "job_id": job_id,
+                        "sequence_id": "0",
+                    }
+                }
+            )
+
+        def hms_stop():
+            publish(
+                {
+                    "print": {
+                        "command": "stop",
+                        "err": print_error,
+                        "param": "reserve",
+                        "job_id": job_id,
+                        "sequence_id": "0",
+                    }
+                }
+            )
+
+        def hms_ignore(persistent: bool = False):
+            # `idle_ignore` is BambuStudio's "dismiss this warning" command.
+            # type=0 dismisses once, type=1 hides the same warning permanently.
+            publish(
+                {
+                    "print": {
+                        "command": "idle_ignore",
+                        "err": print_error,
+                        "type": 1 if persistent else 0,
+                        "sequence_id": "0",
+                    }
+                }
+            )
+
+        def ams_control(param: str):
+            publish(
+                {
+                    "print": {
+                        "command": "ams_control",
+                        "param": param,
+                        "sequence_id": "0",
+                    }
+                }
+            )
+
+        def clean_print_error():
+            # Matches the existing `clear_hms_errors` shape — Bambu does not
+            # expect `print_error` in the body; the command clears whatever
+            # error dialog is currently active on the printer.
+            publish(
+                {
+                    "print": {
+                        "command": "clean_print_error",
+                        "sequence_id": "0",
+                    }
+                }
+            )
+
+        def uiop_close():
+            # `err` is the 8-char hex short code (already a string from the
+            # frontend), uppercased for consistency with how BambuStudio sends it.
+            publish(
+                {
+                    "system": {
+                        "command": "uiop",
+                        "name": "print_error",
+                        "action": "close",
+                        "source": 1,
+                        "type": "dialog",
+                        "err": print_error.upper(),
+                        "sequence_id": "0",
+                    }
+                }
+            )
+
+        match action:
+            case (
+                HMSAction.RESUME_PRINTING
+                | HMSAction.RESUME_PRINTING_DEFECTS
+                | HMSAction.RESUME_PRINTING_PROBELM_SOLVED
+                | HMSAction.PROBLEM_SOLVED_RESUME
+                | HMSAction.FILAMENT_LOAD_RESUME
+                | HMSAction.PROCEED
+            ):
+                hms_resume()
+
+            case HMSAction.STOP_PRINTING:
+                hms_stop()
+
+            case HMSAction.IGNORE_RESUME | HMSAction.NO_REMINDER_NEXT_TIME:
+                hms_ignore(persistent=False)
+
+            case HMSAction.IGNORE_NO_REMINDER_NEXT_TIME | HMSAction.DONT_REMIND_NEXT_TIME:
+                hms_ignore(persistent=True)
+
+            case HMSAction.FILAMENT_EXTRUDED | HMSAction.DBL_CHECK_DONE:
+                ams_control("done")
+
+            case (
+                HMSAction.RETRY_FILAMENT_EXTRUDED
+                | HMSAction.CONTINUE
+                | HMSAction.RETRY_PROBLEM_SOLVED
+                | HMSAction.DBL_CHECK_RETRY
+            ):
+                ams_control("resume")
+
+            case HMSAction.ABORT:
+                ams_control("abort")
+
+            case HMSAction.OK_BUTTON:
+                clean_print_error()
+
+            case HMSAction.DBL_CHECK_OK:
+                clean_print_error()
+                uiop_close()
+
+            case HMSAction.DBL_CHECK_RESUME:
+                # Plain resume — not HMS-aware, no err/job_id.
+                publish(
+                    {
+                        "print": {
+                            "command": "resume",
+                            "param": "",
+                            "sequence_id": "0",
+                        }
+                    }
+                )
+
+            case HMSAction.REFRESH_NOZZLE:
+                publish({"print": {"command": "refresh_nozzle", "sequence_id": "0"}})
+
+            case HMSAction.TURN_OFF_FIRE_ALARM:
+                publish({"print": {"command": "buzzer_ctrl", "mode": 0, "sequence_id": "0"}})
+
+            case HMSAction.STOP_DRYING:
+                publish({"print": {"command": "auto_stop_ams_dry", "sequence_id": "0"}})
+
+            case HMSAction.DISABLE_PURIFICATION:
+                publish({"print": {"command": "close_air_filt", "sequence_id": "0"}})
+
+            case (
+                HMSAction.CHECK_ASSISTANT
+                | HMSAction.JUMP_TO_LIVEVIEW
+                | HMSAction.OK_JUMP_RACK
+                | HMSAction.REMOVE_CLOSE_BTN
+                | HMSAction.LOAD_VIRTUAL_TRAY
+                | HMSAction.CANCLE
+                | HMSAction.DBL_CHECK_CANCEL
+            ):
+                # UI-only actions — the printer's own screen handles these; the
+                # modal still surfaces them so the user has parity with Studio.
+                pass
+
+            case _:
+                logger.warning("[%s] Unknown HMS action '%s'", self.serial_number, action)
+                return False
+
+        return True

+ 14 - 0
backend/app/services/camera_fanout.py

@@ -236,6 +236,20 @@ def active_broadcaster_keys() -> list[str]:
     return [k for k, bc in _broadcasters.items() if not bc.stopped]
 
 
+def get_subscriber_count(key: str) -> int:
+    """Return the number of live subscribers attached to ``key``, or 0.
+
+    Used by ``/camera/stop`` to decide whether to force-shutdown the broadcaster
+    or defer to natural cleanup. Other viewers (cam-wall tile, embedded viewer,
+    popup window) all subscribe to the same broadcaster, so a force-shutdown
+    triggered by one leaving viewer would kill the others' streams.
+    """
+    bc = _broadcasters.get(key)
+    if bc is None or bc.stopped:
+        return 0
+    return bc.subscriber_count
+
+
 # ---------------------------------------------------------------------------
 # AsyncGenerator helper — turns a subscriber queue into an async generator
 # that yields MJPEG chunks until the upstream signals it's gone.

+ 75 - 0
backend/app/services/hms_actions.py

@@ -0,0 +1,75 @@
+"""HMS action lookup.
+
+Bambu printers report HMS errors with a fixed catalog of remediation actions
+(resume / stop / check assistant / etc.). The catalog is bundled as JSON, keyed
+by the 3-letter SN prefix (printer model code: 03W = A1, 31B = X1C, etc.) and
+the short error code with no separator.
+
+The action IDs and their string names are derived from BambuStudio's source via
+`scripts/update_hms_actions.py`. The data file itself is fetched from Bambu's
+public `e.bambulab.com/hms/GetActionImage.php` endpoint.
+"""
+
+import json
+from enum import StrEnum
+from pathlib import Path
+
+_DATA_FILE = Path(__file__).resolve().parent.parent / "data" / "hms_actions.json"
+
+# Loaded eagerly at import — the file is ~150KB and only read once. Using an
+# absolute path keeps the load independent of CWD (systemd unit, Docker
+# entrypoint, pytest run from `backend/`).
+with _DATA_FILE.open("r", encoding="utf-8") as _f:
+    _actions: dict[str, dict[str, list[str]]] = json.load(_f)
+
+
+class HMSAction(StrEnum):
+    """Remediation actions a Bambu printer can offer for an HMS error.
+
+    Values intentionally match the constants used in BambuStudio's source so the
+    HMS-data fetcher can map Bambu's integer action IDs straight to these
+    strings. The CANCLE typo is preserved verbatim — it's how BambuStudio spells
+    it, and changing it would break the action lookup against the catalog.
+    """
+
+    RESUME_PRINTING = "RESUME_PRINTING"
+    RESUME_PRINTING_DEFECTS = "RESUME_PRINTING_DEFECTS"
+    RESUME_PRINTING_PROBELM_SOLVED = "RESUME_PRINTING_PROBELM_SOLVED"
+    STOP_PRINTING = "STOP_PRINTING"
+    CHECK_ASSISTANT = "CHECK_ASSISTANT"
+    FILAMENT_EXTRUDED = "FILAMENT_EXTRUDED"
+    RETRY_FILAMENT_EXTRUDED = "RETRY_FILAMENT_EXTRUDED"
+    CONTINUE = "CONTINUE"
+    LOAD_VIRTUAL_TRAY = "LOAD_VIRTUAL_TRAY"
+    OK_BUTTON = "OK_BUTTON"
+    FILAMENT_LOAD_RESUME = "FILAMENT_LOAD_RESUME"
+    JUMP_TO_LIVEVIEW = "JUMP_TO_LIVEVIEW"
+    NO_REMINDER_NEXT_TIME = "NO_REMINDER_NEXT_TIME"
+    REFRESH_NOZZLE = "REFRESH_NOZZLE"
+    IGNORE_NO_REMINDER_NEXT_TIME = "IGNORE_NO_REMINDER_NEXT_TIME"
+    IGNORE_RESUME = "IGNORE_RESUME"
+    PROBLEM_SOLVED_RESUME = "PROBLEM_SOLVED_RESUME"
+    TURN_OFF_FIRE_ALARM = "TURN_OFF_FIRE_ALARM"
+    RETRY_PROBLEM_SOLVED = "RETRY_PROBLEM_SOLVED"
+    STOP_DRYING = "STOP_DRYING"
+    CANCLE = "CANCLE"  # sic — verbatim from BambuStudio
+    REMOVE_CLOSE_BTN = "REMOVE_CLOSE_BTN"
+    PROCEED = "PROCEED"
+    OK_JUMP_RACK = "OK_JUMP_RACK"
+    ABORT = "ABORT"
+    DISABLE_PURIFICATION = "DISABLE_PURIFICATION"
+    DONT_REMIND_NEXT_TIME = "DONT_REMIND_NEXT_TIME"
+    DBL_CHECK_CANCEL = "DBL_CHECK_CANCEL"
+    DBL_CHECK_DONE = "DBL_CHECK_DONE"
+    DBL_CHECK_RETRY = "DBL_CHECK_RETRY"
+    DBL_CHECK_RESUME = "DBL_CHECK_RESUME"
+    DBL_CHECK_OK = "DBL_CHECK_OK"
+
+
+def get_actions_for_error_code(device: str, error_code: str) -> list[str]:
+    """Look up the action list for a printer SN prefix + short error code.
+
+    Returns the empty list if the printer model or the error code is unknown —
+    the modal renders no buttons in that case, which is the correct fallback.
+    """
+    return _actions.get(device, {}).get(error_code, [])

+ 60 - 13
backend/app/services/print_scheduler.py

@@ -2117,6 +2117,7 @@ class PrintScheduler:
         library_file = None
         file_path = None
         filename = None
+        cleanup_disk_paths: list[Path] = []
 
         if item.archive_id:
             # Print from archive
@@ -2152,6 +2153,7 @@ class PrintScheduler:
             filename = library_file.filename
 
             # Create archive from library file so usage tracking has access to the 3MF
+            queue_item_id = item.id
             try:
                 from backend.app.services.archive import ArchiveService
 
@@ -2165,6 +2167,17 @@ class PrintScheduler:
                 )
                 if archive:
                     item.archive_id = archive.id
+                    if item.cleanup_library_after_dispatch and not library_file.is_external:
+                        item.library_file_id = None
+                        cleanup_disk_paths.append(file_path)
+                        if library_file.thumbnail_path:
+                            thumb_path = Path(library_file.thumbnail_path)
+                            if not thumb_path.is_absolute():
+                                thumb_path = settings.base_dir / library_file.thumbnail_path
+                            cleanup_disk_paths.append(thumb_path)
+                        await db.delete(library_file)
+                        file_path = settings.base_dir / archive.file_path
+                        filename = archive.filename
                     await db.flush()
                     logger.info(
                         "Queue item %s: Created archive %s from library file %s",
@@ -2173,7 +2186,30 @@ class PrintScheduler:
                         item.library_file_id,
                     )
             except Exception as e:
-                logger.warning("Queue item %s: Failed to create archive from library file: %s", item.id, e)
+                logger.warning(
+                    "Queue item %s: Failed to create archive from library file: %s",
+                    queue_item_id,
+                    e,
+                    exc_info=True,
+                )
+                await db.rollback()
+                item = await db.get(PrintQueueItem, queue_item_id)
+                if item:
+                    item.status = "failed"
+                    item.error_message = "Failed to create archive from library file"
+                    item.completed_at = datetime.now(timezone.utc)
+                    await db.commit()
+                    await self._power_off_if_needed(db, item)
+                return
+
+            if not archive:
+                item.status = "failed"
+                item.error_message = "Failed to create archive from library file"
+                item.completed_at = datetime.now(timezone.utc)
+                await db.commit()
+                logger.error("Queue item %s: Archive creation from library file returned no archive", item.id)
+                await self._power_off_if_needed(db, item)
+                return
 
         else:
             # Neither archive nor library file specified
@@ -2329,12 +2365,8 @@ class PrintScheduler:
 
         # Propagate the queue item's owner into printer_manager so the
         # print-complete callback can credit the user in the PrintLogEntry
-        # (#1670). The dispatch path in `background_dispatch.py` does the
-        # equivalent for archive/library "Print" flows; the queue path was
-        # missing this hop, which left the print log's User column blank
-        # for any print started from the queue. `created_by_id` is set
-        # either at queue-add time (UI-added items) or when the user
-        # clicks the manual-start button (#1670 fix in print_queue.py).
+        # (#1670). `created_by_id` is set either at queue-add time (UI-added
+        # items) or when the user clicks the manual-start button.
         await self._propagate_owner_to_printer_manager(db, item)
 
         # IMPORTANT: Set status to "printing" BEFORE sending the print command.
@@ -2347,6 +2379,23 @@ class PrintScheduler:
         item.started_at = datetime.now(timezone.utc)
         await db.commit()
 
+        for cleanup_path in cleanup_disk_paths:
+            try:
+                if cleanup_path.exists():
+                    cleanup_path.unlink()
+            except OSError as cleanup_err:
+                logger.warning(
+                    "TRANSIENT_LIBRARY_FILE_ORPHAN %s",
+                    json.dumps(
+                        {
+                            "queue_item_id": item.id,
+                            "path": str(cleanup_path),
+                            "error": str(cleanup_err),
+                        },
+                        sort_keys=True,
+                    ),
+                )
+
         # Clear the awaiting-plate-clear flag now that we're starting a new print
         printer_manager.set_awaiting_plate_clear(item.printer_id, False)
         logger.info("Queue item %s: Status set to 'printing', sending print command...", item.id)
@@ -2643,13 +2692,11 @@ class PrintScheduler:
         if landed_on_subtask:
             return
 
-        # Phase A timeout path — same #1150 / #887/#936 discriminator as
-        # background_dispatch: if the printer's gcode_file changed since
+        # Phase A timeout path: if the printer's gcode_file changed since
         # pre-dispatch, the project_file command landed and the printer is
-        # parsing — a forced reconnect mid-parse triggers 0500_4003. If
-        # gcode_file is unchanged, the publish was silently swallowed
-        # (#887/#936) and the original force_reconnect recovery is what we
-        # want.
+        # parsing — a forced reconnect mid-parse triggers 0500_4003 (#1150).
+        # If gcode_file is unchanged, the publish was silently swallowed
+        # (#887/#936) and force_reconnect recovery is what we want.
         client = printer_manager.get_client(printer_id)
         current_gcode_file = getattr(last_status, "gcode_file", None) if last_status else None
         publish_landed = current_gcode_file is not None and current_gcode_file != pre_gcode_file

+ 8 - 1
backend/app/services/printer_manager.py

@@ -1137,7 +1137,14 @@ def printer_state_to_dict(
         "total_layers": state.total_layers,
         "temperatures": temperatures,
         "hms_errors": [
-            {"code": e.code, "attr": e.attr, "module": e.module, "severity": e.severity}
+            {
+                "code": e.code,
+                "attr": e.attr,
+                "module": e.module,
+                "severity": e.severity,
+                "actions": e.actions,
+                "job_id": e.job_id,
+            }
             for e in (state.hms_errors or [])
         ],
         # AMS data for filament colors

+ 3 - 4
backend/app/services/slice_dispatch.py

@@ -1,9 +1,8 @@
 """In-memory background dispatcher for slice jobs.
 
-Mirrors the shape of `background_dispatch.py` (the print-upload dispatcher)
-but tailored for slicing: jobs are independent (no printer-busy gating),
-short-lived (typically 5-60s), and the result is a `LibraryFile` or
-`PrintArchive` row rather than a printer-side dispatch.
+Slice jobs are independent (no printer-busy gating), short-lived (typically
+5-60s), and the result is a `LibraryFile` or `PrintArchive` row rather than a
+printer-side dispatch.
 
 The frontend kicks off a slice via `POST /library/files/{id}/slice` or
 `POST /archives/{id}/slice`, gets back `{job_id, status_url}`, then polls

+ 19 - 289
backend/tests/integration/test_background_dispatch_api.py

@@ -1,306 +1,36 @@
-"""Integration tests for background dispatch API behavior."""
-
-from unittest.mock import AsyncMock, patch
+"""Integration tests for removed direct print API behavior."""
 
 import pytest
 from httpx import AsyncClient
 
-from backend.app.services.background_dispatch import DispatchEnqueueRejected
-
 
-class TestBackgroundDispatchArchivesAPI:
-    """Tests for archive reprint dispatch endpoint."""
+class TestLegacyArchivePrintAPI:
+    """Tests for the removed archive reprint dispatch endpoint."""
 
     @pytest.mark.asyncio
     @pytest.mark.integration
-    async def test_reprint_returns_dispatched_payload(
-        self, async_client: AsyncClient, archive_factory, printer_factory, db_session, tmp_path
-    ):
-        """Reprint endpoint returns background dispatch metadata."""
-        printer = await printer_factory()
-        archive = await archive_factory(
-            printer.id,
-            filename="widget.gcode.3mf",
-            file_path="archives/test/widget.gcode.3mf",
+    async def test_reprint_route_returns_410_and_does_not_dispatch(self, async_client: AsyncClient):
+        """Legacy direct reprint endpoint is gone; callers must create queue items."""
+        response = await async_client.post(
+            "/api/v1/archives/123/reprint?printer_id=456",
+            json={"plate_id": 2},
         )
 
-        archive_file = tmp_path / archive.file_path
-        archive_file.parent.mkdir(parents=True, exist_ok=True)
-        archive_file.write_bytes(b"3mf-data")
+        assert response.status_code == 410
+        assert "POST /queue/" in response.json()["detail"]
 
-        with (
-            patch("backend.app.api.routes.archives.settings.base_dir", tmp_path),
-            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
-            patch(
-                "backend.app.services.background_dispatch.background_dispatch.dispatch_reprint_archive",
-                new=AsyncMock(return_value={"dispatch_job_id": 15, "dispatch_position": 1}),
-            ) as mock_dispatch,
-        ):
-            response = await async_client.post(
-                f"/api/v1/archives/{archive.id}/reprint?printer_id={printer.id}",
-                json={"plate_id": 2},
-            )
 
-        assert response.status_code == 200
-        data = response.json()
-        assert data["status"] == "dispatched"
-        assert data["dispatch_job_id"] == 15
-        assert data["dispatch_position"] == 1
-        assert data["filename"] == "widget.gcode.3mf"
-
-        mock_dispatch.assert_awaited_once()
-        kwargs = mock_dispatch.await_args.kwargs
-        assert kwargs["archive_name"].endswith("• Plate 2")
-        assert kwargs["options"]["plate_id"] == 2
+class TestLegacyLibraryPrintAPI:
+    """Tests for the removed library print dispatch endpoint."""
 
     @pytest.mark.asyncio
     @pytest.mark.integration
-    async def test_reprint_returns_409_when_enqueue_rejected(
-        self, async_client: AsyncClient, archive_factory, printer_factory, db_session, tmp_path
-    ):
-        """Reprint endpoint maps enqueue rejection to HTTP 409."""
-        printer = await printer_factory()
-        archive = await archive_factory(
-            printer.id,
-            filename="widget2.gcode.3mf",
-            file_path="archives/test/widget2.gcode.3mf",
+    async def test_library_print_route_returns_410_and_does_not_dispatch(self, async_client: AsyncClient):
+        """Legacy direct library print endpoint is gone; callers must create queue items."""
+        response = await async_client.post(
+            "/api/v1/library/files/123/print?printer_id=456",
+            json={"plate_id": 4},
         )
 
-        archive_file = tmp_path / archive.file_path
-        archive_file.parent.mkdir(parents=True, exist_ok=True)
-        archive_file.write_bytes(b"3mf-data")
-
-        with (
-            patch("backend.app.api.routes.archives.settings.base_dir", tmp_path),
-            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
-            patch(
-                "backend.app.services.background_dispatch.background_dispatch.dispatch_reprint_archive",
-                new=AsyncMock(side_effect=DispatchEnqueueRejected("already has a background dispatch")),
-            ),
-        ):
-            response = await async_client.post(
-                f"/api/v1/archives/{archive.id}/reprint?printer_id={printer.id}",
-                json={"plate_id": 1},
-            )
-
-        assert response.status_code == 409
-        assert "already has a background dispatch" in response.json()["detail"]
-
-
-class TestBackgroundDispatchLibraryAPI:
-    """Tests for library print dispatch endpoint."""
-
-    @pytest.fixture
-    async def library_file_factory(self, db_session):
-        """Factory to create library files."""
-
-        async def _create_file(**kwargs):
-            from backend.app.models.library import LibraryFile
-
-            defaults = {
-                "filename": "library_part.gcode.3mf",
-                "file_path": "library/files/library_part.gcode.3mf",
-                "file_type": "gcode",
-                "file_size": 1024,
-            }
-            defaults.update(kwargs)
-            lib_file = LibraryFile(**defaults)
-            db_session.add(lib_file)
-            await db_session.commit()
-            await db_session.refresh(lib_file)
-            return lib_file
-
-        return _create_file
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_library_print_returns_dispatched_payload(
-        self, async_client: AsyncClient, library_file_factory, printer_factory, db_session, tmp_path
-    ):
-        """Library print endpoint returns dispatch job metadata."""
-        printer = await printer_factory()
-        lib_file = await library_file_factory()
-
-        disk_path = tmp_path / lib_file.file_path
-        disk_path.parent.mkdir(parents=True, exist_ok=True)
-        disk_path.write_bytes(b"library data")
-
-        with (
-            patch("backend.app.api.routes.library.app_settings.base_dir", tmp_path),
-            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
-            patch(
-                "backend.app.services.background_dispatch.background_dispatch.dispatch_print_library_file",
-                new=AsyncMock(return_value={"dispatch_job_id": 21, "dispatch_position": 2}),
-            ) as mock_dispatch,
-        ):
-            response = await async_client.post(
-                f"/api/v1/library/files/{lib_file.id}/print?printer_id={printer.id}",
-                json={"plate_id": 4},
-            )
-
-        assert response.status_code == 200
-        data = response.json()
-        assert data["status"] == "dispatched"
-        assert data["dispatch_job_id"] == 21
-        assert data["dispatch_position"] == 2
-        assert data["archive_id"] is None
-
-        mock_dispatch.assert_awaited_once()
-        kwargs = mock_dispatch.await_args.kwargs
-        assert kwargs["filename"].endswith("• Plate 4")
-        assert kwargs["options"]["plate_id"] == 4
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_library_print_returns_409_when_enqueue_rejected(
-        self, async_client: AsyncClient, library_file_factory, printer_factory, db_session, tmp_path
-    ):
-        """Library print endpoint maps enqueue rejection to HTTP 409."""
-        printer = await printer_factory()
-        lib_file = await library_file_factory(filename="another_part.gcode")
-
-        disk_path = tmp_path / lib_file.file_path
-        disk_path.parent.mkdir(parents=True, exist_ok=True)
-        disk_path.write_bytes(b"library data")
-
-        with (
-            patch("backend.app.api.routes.library.app_settings.base_dir", tmp_path),
-            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
-            patch(
-                "backend.app.services.background_dispatch.background_dispatch.dispatch_print_library_file",
-                new=AsyncMock(side_effect=DispatchEnqueueRejected("queue conflict")),
-            ),
-        ):
-            response = await async_client.post(
-                f"/api/v1/library/files/{lib_file.id}/print?printer_id={printer.id}",
-                json={"plate_id": 1},
-            )
-
-        assert response.status_code == 409
-        assert "queue conflict" in response.json()["detail"]
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_library_print_cleanup_flag_defaults_false(
-        self, async_client: AsyncClient, library_file_factory, printer_factory, db_session, tmp_path
-    ):
-        """Absent cleanup_library_after_dispatch in the request body ⇒ False reaches the dispatcher.
-        Guards the File Manager / Project Detail paths from accidental deletion."""
-        printer = await printer_factory()
-        lib_file = await library_file_factory(filename="filemgr_part.gcode.3mf")
-
-        disk_path = tmp_path / lib_file.file_path
-        disk_path.parent.mkdir(parents=True, exist_ok=True)
-        disk_path.write_bytes(b"library data")
-
-        with (
-            patch("backend.app.api.routes.library.app_settings.base_dir", tmp_path),
-            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
-            patch(
-                "backend.app.services.background_dispatch.background_dispatch.dispatch_print_library_file",
-                new=AsyncMock(return_value={"dispatch_job_id": 30, "dispatch_position": 1}),
-            ) as mock_dispatch,
-        ):
-            response = await async_client.post(
-                f"/api/v1/library/files/{lib_file.id}/print?printer_id={printer.id}",
-                json={},
-            )
-
-        assert response.status_code == 200
-        mock_dispatch.assert_awaited_once()
-        assert mock_dispatch.await_args.kwargs["cleanup_library_after_dispatch"] is False
-        # cleanup flag must never leak into the print-option dict forwarded to MQTT
-        assert "cleanup_library_after_dispatch" not in mock_dispatch.await_args.kwargs["options"]
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_library_print_forwards_cleanup_flag_true(
-        self, async_client: AsyncClient, library_file_factory, printer_factory, db_session, tmp_path
-    ):
-        """Direct-Print flow sends cleanup_library_after_dispatch=True, which must reach the dispatcher."""
-        printer = await printer_factory()
-        lib_file = await library_file_factory(filename="transient_part.gcode.3mf")
-
-        disk_path = tmp_path / lib_file.file_path
-        disk_path.parent.mkdir(parents=True, exist_ok=True)
-        disk_path.write_bytes(b"library data")
-
-        with (
-            patch("backend.app.api.routes.library.app_settings.base_dir", tmp_path),
-            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
-            patch(
-                "backend.app.services.background_dispatch.background_dispatch.dispatch_print_library_file",
-                new=AsyncMock(return_value={"dispatch_job_id": 31, "dispatch_position": 1}),
-            ) as mock_dispatch,
-        ):
-            response = await async_client.post(
-                f"/api/v1/library/files/{lib_file.id}/print?printer_id={printer.id}",
-                json={"cleanup_library_after_dispatch": True},
-            )
-
-        assert response.status_code == 200
-        mock_dispatch.assert_awaited_once()
-        assert mock_dispatch.await_args.kwargs["cleanup_library_after_dispatch"] is True
-
-
-class TestBackgroundDispatchCancelAPI:
-    """Tests for /background-dispatch cancel endpoint."""
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_cancel_job_returns_cancelled(self, async_client: AsyncClient):
-        """Cancel endpoint returns cancelled for queued job."""
-        with patch(
-            "backend.app.services.background_dispatch.background_dispatch.cancel_job",
-            new=AsyncMock(
-                return_value={
-                    "cancelled": True,
-                    "pending": False,
-                    "job_id": 9,
-                    "source_name": "cube.gcode.3mf",
-                    "printer_id": 1,
-                    "printer_name": "Printer A",
-                }
-            ),
-        ):
-            response = await async_client.delete("/api/v1/background-dispatch/9")
-
-        assert response.status_code == 200
-        data = response.json()
-        assert data["status"] == "cancelled"
-        assert data["job_id"] == 9
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_cancel_job_returns_cancelling_for_active_job(self, async_client: AsyncClient):
-        """Cancel endpoint returns cancelling while active upload is being interrupted."""
-        with patch(
-            "backend.app.services.background_dispatch.background_dispatch.cancel_job",
-            new=AsyncMock(
-                return_value={
-                    "cancelled": True,
-                    "pending": True,
-                    "job_id": 10,
-                    "source_name": "cube.gcode.3mf",
-                    "printer_id": 1,
-                    "printer_name": "Printer A",
-                }
-            ),
-        ):
-            response = await async_client.delete("/api/v1/background-dispatch/10")
-
-        assert response.status_code == 200
-        assert response.json()["status"] == "cancelling"
-
-    @pytest.mark.asyncio
-    @pytest.mark.integration
-    async def test_cancel_job_returns_404_when_not_found(self, async_client: AsyncClient):
-        """Cancel endpoint returns 404 for unknown job id."""
-        with patch(
-            "backend.app.services.background_dispatch.background_dispatch.cancel_job",
-            new=AsyncMock(return_value={"cancelled": False, "reason": "not_found"}),
-        ):
-            response = await async_client.delete("/api/v1/background-dispatch/999")
-
-        assert response.status_code == 404
-        assert response.json()["detail"] == "Dispatch job not found"
+        assert response.status_code == 410
+        assert "POST /queue/" in response.json()["detail"]

+ 33 - 0
backend/tests/integration/test_camera_api.py

@@ -147,6 +147,39 @@ class TestCameraAPI:
         assert response.status_code == 200
         mock_shutdown.assert_awaited_once_with(f"printer-{printer.id}")
 
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_stop_camera_stream_skips_shutdown_when_subscribers_remain(
+        self, async_client: AsyncClient, printer_factory
+    ):
+        """Reference-count guard: when other viewers are still subscribed to the
+        broadcaster, /camera/stop must NOT force-shutdown — otherwise closing
+        the embedded viewer kills the cam-wall tile of the same printer.
+        Natural cleanup tears it down when the last HTTP connection closes.
+        """
+        printer = await printer_factory()
+
+        mock_shutdown = AsyncMock(return_value=True)
+        mock_process = MagicMock()
+        mock_process.returncode = None
+        mock_process.pid = 88888
+        mock_process.terminate = MagicMock()
+        mock_process.wait = AsyncMock()
+
+        with (
+            patch("backend.app.api.routes.camera.get_subscriber_count", return_value=2),
+            patch("backend.app.api.routes.camera.shutdown_broadcaster", mock_shutdown),
+            patch("backend.app.api.routes.camera._active_streams", {f"{printer.id}-abc": mock_process}),
+        ):
+            response = await async_client.post(f"/api/v1/printers/{printer.id}/camera/stop")
+
+        assert response.status_code == 200
+        result = response.json()
+        assert result["stopped"] == 0
+        assert result.get("skipped") is True
+        mock_shutdown.assert_not_awaited()
+        mock_process.terminate.assert_not_called()
+
     # ========================================================================
     # Camera Test Endpoint
     # ========================================================================

+ 251 - 3
backend/tests/integration/test_ownership_permissions.py

@@ -287,15 +287,15 @@ class TestArchiveOwnershipPermissions(TestOwnershipPermissionsSetup):
         assert response.status_code == 403
 
     # ========================================================================
-    # REPRINT permissions
+    # Legacy reprint endpoint
     # ========================================================================
 
     @pytest.mark.asyncio
     @pytest.mark.integration
-    async def test_operator_cannot_reprint_others_archive(
+    async def test_reprint_endpoint_is_gone_for_all_callers(
         self, async_client: AsyncClient, auth_setup, archive_factory, printer_factory, db_session
     ):
-        """Operator cannot reprint another user's archive."""
+        """Direct archive reprint no longer exists; callers must use the queue."""
         printer = await printer_factory()
         archive = await archive_factory(
             printer.id,
@@ -307,7 +307,122 @@ class TestArchiveOwnershipPermissions(TestOwnershipPermissionsSetup):
             headers={"Authorization": f"Bearer {auth_setup['operator_token']}"},
         )
 
+        assert response.status_code == 410
+
+    # ========================================================================
+    # Queue route — archives:reprint_* gate (#1625)
+    # ========================================================================
+    # The unified /queue/ route replaced the legacy /reprint endpoint; the
+    # reprint permission gate must move with it. Without these checks a
+    # caller with QUEUE_CREATE + ARCHIVES_READ_OWN could reprint their own
+    # archives even if explicitly denied ARCHIVES_REPRINT_OWN.
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_queue_route_operator_can_reprint_own_archive(
+        self, async_client: AsyncClient, auth_setup, archive_factory, printer_factory, db_session
+    ):
+        """Operator with REPRINT_OWN can queue their own archive."""
+        printer = await printer_factory()
+        archive = await archive_factory(
+            printer.id,
+            created_by_id=auth_setup["operator_user"]["id"],
+        )
+
+        response = await async_client.post(
+            "/api/v1/queue/",
+            headers={"Authorization": f"Bearer {auth_setup['operator_token']}"},
+            json={"printer_id": printer.id, "archive_id": archive.id},
+        )
+
+        assert response.status_code == 200
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_queue_route_user_without_reprint_gets_403(
+        self, async_client: AsyncClient, auth_setup, archive_factory, printer_factory, db_session
+    ):
+        """User with QUEUE_CREATE + ARCHIVES_READ_OWN but no reprint perm → 403.
+
+        Custom group mirrors a real operator policy where someone is allowed
+        to enqueue freshly-uploaded library files but explicitly NOT allowed
+        to re-run completed archives.
+        """
+        # Create custom group with queue:create + archives:read_own but no reprint perm.
+        admin_headers = {"Authorization": f"Bearer {auth_setup['admin_token']}"}
+        group_resp = await async_client.post(
+            "/api/v1/groups/",
+            headers=admin_headers,
+            json={
+                "name": "QueueOnlyNoReprint",
+                "description": "Test group: can queue library files but not reprint",
+                "permissions": [
+                    "queue:create",
+                    "queue:read_own",
+                    "archives:read_own",
+                    "library:read_own",
+                    "library:upload",
+                    "printers:read",
+                ],
+            },
+        )
+        assert group_resp.status_code in (200, 201)
+        group_id = group_resp.json()["id"]
+
+        await async_client.post(
+            "/api/v1/users/",
+            headers=admin_headers,
+            json={
+                "username": "noreprint_user",
+                "password": "NoreprintPass1!",
+                "group_ids": [group_id],
+            },
+        )
+        login = await async_client.post(
+            "/api/v1/auth/login",
+            json={"username": "noreprint_user", "password": "NoreprintPass1!"},
+        )
+        token = login.json()["access_token"]
+        user_id = login.json()["user"]["id"]
+
+        # Archive owned by the no-reprint user.
+        printer = await printer_factory()
+        archive = await archive_factory(printer.id, created_by_id=user_id)
+
+        response = await async_client.post(
+            "/api/v1/queue/",
+            headers={"Authorization": f"Bearer {token}"},
+            json={"printer_id": printer.id, "archive_id": archive.id},
+        )
+
         assert response.status_code == 403
+        assert "reprint" in response.json()["detail"].lower()
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_queue_route_ownerless_archive_requires_reprint_all(
+        self, async_client: AsyncClient, auth_setup, archive_factory, printer_factory, db_session
+    ):
+        """Ownerless archive (created_by_id=null) requires REPRINT_ALL.
+
+        Pre-IDOR-fix legacy data has no creator; an operator with
+        REPRINT_OWN can't fall back to "I own this" — fail-closed.
+        The existing IDOR check returns 404 first (operator lacks
+        READ_ALL and doesn't own the row), so this is also a regression
+        guard against accidentally surfacing 403-instead-of-404 if the
+        IDOR check is ever loosened.
+        """
+        printer = await printer_factory()
+        archive = await archive_factory(printer.id, created_by_id=None)
+
+        response = await async_client.post(
+            "/api/v1/queue/",
+            headers={"Authorization": f"Bearer {auth_setup['operator_token']}"},
+            json={"printer_id": printer.id, "archive_id": archive.id},
+        )
+
+        # IDOR returns 404 before the new gate fires for this operator.
+        assert response.status_code == 404
 
 
 class TestQueueOwnershipPermissions(TestOwnershipPermissionsSetup):
@@ -426,6 +541,139 @@ class TestQueueOwnershipPermissions(TestOwnershipPermissionsSetup):
 
         assert response.status_code == 403
 
+    # ========================================================================
+    # Start / Stop ownership gates (#1625-followup)
+    # ========================================================================
+    # Pre-fix /stop required QUEUE_UPDATE_ALL (admin-only) — operators saw the
+    # Stop button in the queue UI but got 403 on click. /start required
+    # QUEUE_UPDATE_OWN with no ownership check — operators could start anyone's
+    # queue items via direct API. Both now use require_ownership_permission.
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_operator_can_start_own_queue_item(self, async_client: AsyncClient, auth_setup, queue_item_factory):
+        """Operator can start their own staged queue item."""
+        item = await queue_item_factory(
+            created_by_id=auth_setup["operator_user"]["id"],
+            manual_start=True,
+        )
+
+        response = await async_client.post(
+            f"/api/v1/queue/{item.id}/start?skip_filament_check=true",
+            headers={"Authorization": f"Bearer {auth_setup['operator_token']}"},
+        )
+
+        assert response.status_code == 200
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_operator_cannot_start_others_queue_item(
+        self, async_client: AsyncClient, auth_setup, queue_item_factory
+    ):
+        """Operator cannot start another user's queue item."""
+        item = await queue_item_factory(
+            created_by_id=auth_setup["operator2_user"]["id"],
+            manual_start=True,
+        )
+
+        response = await async_client.post(
+            f"/api/v1/queue/{item.id}/start?skip_filament_check=true",
+            headers={"Authorization": f"Bearer {auth_setup['operator_token']}"},
+        )
+
+        assert response.status_code == 403
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_operator_can_start_unowned_queue_item(
+        self, async_client: AsyncClient, auth_setup, queue_item_factory, db_session
+    ):
+        """Operator can start a NULL-owner queue item (VP-uploaded, #1670)
+        and claims ownership in the process.
+
+        Stop and Cancel reject unowned items for _OWN holders (destructive,
+        no "I own it" claim available), but Start is the entry point for the
+        VP-import flow where attribution happens at click-time.
+        """
+        from backend.app.models.print_queue import PrintQueueItem
+
+        item = await queue_item_factory(created_by_id=None, manual_start=True)
+
+        response = await async_client.post(
+            f"/api/v1/queue/{item.id}/start?skip_filament_check=true",
+            headers={"Authorization": f"Bearer {auth_setup['operator_token']}"},
+        )
+
+        assert response.status_code == 200
+        # Ownership claimed: operator is now the item's owner.
+        await db_session.refresh(item)
+        refetch = await db_session.get(PrintQueueItem, item.id)
+        assert refetch.created_by_id == auth_setup["operator_user"]["id"]
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_operator_can_stop_own_queue_item(self, async_client: AsyncClient, auth_setup, queue_item_factory):
+        """Operator can stop their own currently-printing queue item."""
+        item = await queue_item_factory(
+            created_by_id=auth_setup["operator_user"]["id"],
+            status="printing",
+        )
+
+        response = await async_client.post(
+            f"/api/v1/queue/{item.id}/stop",
+            headers={"Authorization": f"Bearer {auth_setup['operator_token']}"},
+        )
+
+        assert response.status_code == 200
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_operator_cannot_stop_others_queue_item(
+        self, async_client: AsyncClient, auth_setup, queue_item_factory
+    ):
+        """Operator cannot stop another user's printing queue item."""
+        item = await queue_item_factory(
+            created_by_id=auth_setup["operator2_user"]["id"],
+            status="printing",
+        )
+
+        response = await async_client.post(
+            f"/api/v1/queue/{item.id}/stop",
+            headers={"Authorization": f"Bearer {auth_setup['operator_token']}"},
+        )
+
+        assert response.status_code == 403
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_operator_cannot_stop_unowned_queue_item(
+        self, async_client: AsyncClient, auth_setup, queue_item_factory
+    ):
+        """Operator cannot stop a NULL-owner printing queue item — stop mirrors
+        cancel (destructive, no claim semantics). Admins with _ALL can still stop it.
+        """
+        item = await queue_item_factory(created_by_id=None, status="printing")
+
+        response = await async_client.post(
+            f"/api/v1/queue/{item.id}/stop",
+            headers={"Authorization": f"Bearer {auth_setup['operator_token']}"},
+        )
+
+        assert response.status_code == 403
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_admin_can_stop_any_queue_item(self, async_client: AsyncClient, auth_setup, queue_item_factory):
+        """Admin with _ALL can stop any printing queue item including unowned."""
+        item = await queue_item_factory(created_by_id=None, status="printing")
+
+        response = await async_client.post(
+            f"/api/v1/queue/{item.id}/stop",
+            headers={"Authorization": f"Bearer {auth_setup['admin_token']}"},
+        )
+
+        assert response.status_code == 200
+
     @pytest.mark.asyncio
     @pytest.mark.integration
     async def test_bulk_update_skips_non_owned_items(self, async_client: AsyncClient, auth_setup, queue_item_factory):

+ 316 - 0
backend/tests/integration/test_print_queue_api.py

@@ -1893,6 +1893,199 @@ class TestAbortedStatusNormalisation:
         positions = [i["position"] for i in batch_items]
         assert positions == [positions[0], positions[0] + 1, positions[0] + 2]
 
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_add_to_queue_insert_position_shifts_existing_items(
+        self, async_client: AsyncClient, printer_factory, archive_factory, db_session
+    ):
+        """Verify priority insertion shifts existing pending items in the same printer queue."""
+        printer = await printer_factory()
+        first = await archive_factory(print_name="First")
+        second = await archive_factory(print_name="Second")
+        priority = await archive_factory(print_name="Priority")
+
+        assert (
+            await async_client.post("/api/v1/queue/", json={"printer_id": printer.id, "archive_id": first.id})
+        ).status_code == 200
+        assert (
+            await async_client.post("/api/v1/queue/", json={"printer_id": printer.id, "archive_id": second.id})
+        ).status_code == 200
+
+        response = await async_client.post(
+            "/api/v1/queue/",
+            json={
+                "printer_id": printer.id,
+                "archive_id": priority.id,
+                "insert_position": 1,
+            },
+        )
+        assert response.status_code == 200
+
+        list_response = await async_client.get(f"/api/v1/queue/?printer_id={printer.id}")
+        items = sorted(list_response.json(), key=lambda item: item["position"])
+        assert [item["archive_id"] for item in items[:3]] == [priority.id, first.id, second.id]
+        assert [item["position"] for item in items[:3]] == [1, 2, 3]
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_add_to_queue_insert_position_quantity_shifts_existing_by_quantity(
+        self, async_client: AsyncClient, printer_factory, archive_factory, db_session
+    ):
+        """ASAP batch insertion shifts existing pending items by the inserted quantity."""
+        printer = await printer_factory()
+        first = await archive_factory(print_name="First")
+        second = await archive_factory(print_name="Second")
+        priority = await archive_factory(print_name="Priority")
+
+        assert (
+            await async_client.post("/api/v1/queue/", json={"printer_id": printer.id, "archive_id": first.id})
+        ).status_code == 200
+        assert (
+            await async_client.post("/api/v1/queue/", json={"printer_id": printer.id, "archive_id": second.id})
+        ).status_code == 200
+
+        response = await async_client.post(
+            "/api/v1/queue/",
+            json={
+                "printer_id": printer.id,
+                "archive_id": priority.id,
+                "quantity": 3,
+                "insert_position": 1,
+            },
+        )
+        assert response.status_code == 200
+        batch_id = response.json()["batch_id"]
+
+        list_response = await async_client.get(f"/api/v1/queue/?printer_id={printer.id}")
+        items = sorted(list_response.json(), key=lambda item: item["position"])
+        assert [item["archive_id"] for item in items] == [
+            priority.id,
+            priority.id,
+            priority.id,
+            first.id,
+            second.id,
+        ]
+        assert [item["position"] for item in items] == [1, 2, 3, 4, 5]
+        assert [item["batch_id"] for item in items[:3]] == [batch_id, batch_id, batch_id]
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_add_to_queue_insert_position_scopes_unassigned_items(
+        self, async_client: AsyncClient, printer_factory, archive_factory, db_session
+    ):
+        """Unassigned inserts shift only the unassigned queue scope."""
+        printer = await printer_factory()
+        unassigned_first = await archive_factory(print_name="Unassigned First")
+        unassigned_second = await archive_factory(print_name="Unassigned Second")
+        assigned = await archive_factory(print_name="Assigned")
+        priority = await archive_factory(print_name="Unassigned Priority")
+
+        assert (await async_client.post("/api/v1/queue/", json={"archive_id": unassigned_first.id})).status_code == 200
+        assert (await async_client.post("/api/v1/queue/", json={"archive_id": unassigned_second.id})).status_code == 200
+        assigned_response = await async_client.post(
+            "/api/v1/queue/",
+            json={"printer_id": printer.id, "archive_id": assigned.id},
+        )
+        assert assigned_response.status_code == 200
+        assert assigned_response.json()["position"] == 1
+
+        response = await async_client.post(
+            "/api/v1/queue/",
+            json={
+                "archive_id": priority.id,
+                "insert_position": 1,
+            },
+        )
+        assert response.status_code == 200
+
+        unassigned_response = await async_client.get("/api/v1/queue/?printer_id=-1")
+        unassigned_items = sorted(unassigned_response.json(), key=lambda item: item["position"])
+        assert [item["archive_id"] for item in unassigned_items] == [
+            priority.id,
+            unassigned_first.id,
+            unassigned_second.id,
+        ]
+        assert [item["position"] for item in unassigned_items] == [1, 2, 3]
+
+        assigned_scope_response = await async_client.get(f"/api/v1/queue/?printer_id={printer.id}&target_model=NONE")
+        assigned_items = sorted(assigned_scope_response.json(), key=lambda item: item["position"])
+        assert [item["archive_id"] for item in assigned_items] == [assigned.id]
+        assert [item["position"] for item in assigned_items] == [1]
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_add_to_queue_insert_position_greater_than_max_appends_without_gap(
+        self, async_client: AsyncClient, printer_factory, archive_factory, db_session
+    ):
+        """Oversized explicit insert_position appends at max+1 instead of creating sparse positions."""
+        printer = await printer_factory()
+        first = await archive_factory(print_name="First")
+        second = await archive_factory(print_name="Second")
+        appended = await archive_factory(print_name="Append")
+
+        assert (
+            await async_client.post("/api/v1/queue/", json={"printer_id": printer.id, "archive_id": first.id})
+        ).status_code == 200
+        assert (
+            await async_client.post("/api/v1/queue/", json={"printer_id": printer.id, "archive_id": second.id})
+        ).status_code == 200
+
+        response = await async_client.post(
+            "/api/v1/queue/",
+            json={
+                "printer_id": printer.id,
+                "archive_id": appended.id,
+                "insert_position": 99,
+            },
+        )
+        assert response.status_code == 200
+        assert response.json()["position"] == 3
+
+        list_response = await async_client.get(f"/api/v1/queue/?printer_id={printer.id}")
+        items = sorted(list_response.json(), key=lambda item: item["position"])
+        assert [item["archive_id"] for item in items] == [first.id, second.id, appended.id]
+        assert [item["position"] for item in items] == [1, 2, 3]
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_add_to_queue_consecutive_asap_inserts_stack_in_submission_order(
+        self, async_client: AsyncClient, printer_factory, archive_factory, db_session
+    ):
+        """Consecutive ASAP inserts to the same printer preserve the client submission order."""
+        printer = await printer_factory()
+        existing = await archive_factory(print_name="Existing")
+        first_asap = await archive_factory(print_name="First ASAP")
+        second_asap = await archive_factory(print_name="Second ASAP")
+
+        assert (
+            await async_client.post("/api/v1/queue/", json={"printer_id": printer.id, "archive_id": existing.id})
+        ).status_code == 200
+
+        first_response = await async_client.post(
+            "/api/v1/queue/",
+            json={
+                "printer_id": printer.id,
+                "archive_id": first_asap.id,
+                "insert_position": 1,
+            },
+        )
+        assert first_response.status_code == 200
+
+        second_response = await async_client.post(
+            "/api/v1/queue/",
+            json={
+                "printer_id": printer.id,
+                "archive_id": second_asap.id,
+                "insert_position": 2,
+            },
+        )
+        assert second_response.status_code == 200
+
+        list_response = await async_client.get(f"/api/v1/queue/?printer_id={printer.id}")
+        items = sorted(list_response.json(), key=lambda item: item["position"])
+        assert [item["archive_id"] for item in items] == [first_asap.id, second_asap.id, existing.id]
+        assert [item["position"] for item in items] == [1, 2, 3]
+
     @pytest.mark.asyncio
     @pytest.mark.integration
     async def test_add_to_queue_quantity_with_print_options(
@@ -2491,3 +2684,126 @@ class TestResumeQueueAfterFailure:
 
         second = await async_client.post(f"/api/v1/queue/printer/{printer.id}/resume")
         assert second.json() == {"acknowledged": 0, "restored": 0}
+
+
+class TestReorderEndpoint:
+    """Tests for the /queue/reorder endpoint (#1625-followup duplicate-position validator)."""
+
+    @pytest.fixture
+    async def printer_factory(self, db_session):
+        async def _create(**kwargs):
+            from backend.app.models.printer import Printer
+
+            defaults = {
+                "name": "Reorder Test Printer",
+                "ip_address": "192.168.1.220",
+                "serial_number": "TESTREORDER001",
+                "access_code": "12345678",
+                "model": "X1C",
+            }
+            defaults.update(kwargs)
+            printer = Printer(**defaults)
+            db_session.add(printer)
+            await db_session.commit()
+            await db_session.refresh(printer)
+            return printer
+
+        return _create
+
+    @pytest.fixture
+    async def archive_factory(self, db_session):
+        _counter = [0]
+
+        async def _create(**kwargs):
+            from backend.app.models.archive import PrintArchive
+
+            _counter[0] += 1
+            defaults = {
+                "filename": f"reorder_{_counter[0]}.3mf",
+                "print_name": f"Reorder {_counter[0]}",
+                "file_path": f"/tmp/reorder_{_counter[0]}.3mf",
+                "file_size": 1024,
+                "content_hash": f"reorderhash{_counter[0]:06d}",
+                "status": "completed",
+            }
+            defaults.update(kwargs)
+            archive = PrintArchive(**defaults)
+            db_session.add(archive)
+            await db_session.commit()
+            await db_session.refresh(archive)
+            return archive
+
+        return _create
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_reorder_rejects_duplicate_positions(
+        self, async_client: AsyncClient, db_session, printer_factory, archive_factory
+    ):
+        """Reorder payload with duplicate positions → 422 at schema layer.
+
+        Regression guard: pre-fix, a buggy client sending two items at the
+        same position would leave the queue in an inconsistent state (the
+        scheduler's ORDER BY (printer_id, position) tie would be broken by
+        physical row order — non-deterministic dispatch order).
+        """
+        from backend.app.models.print_queue import PrintQueueItem
+
+        printer = await printer_factory()
+        a1 = await archive_factory()
+        a2 = await archive_factory()
+        item1 = PrintQueueItem(printer_id=printer.id, archive_id=a1.id, status="pending", position=1)
+        item2 = PrintQueueItem(printer_id=printer.id, archive_id=a2.id, status="pending", position=2)
+        db_session.add_all([item1, item2])
+        await db_session.commit()
+        await db_session.refresh(item1)
+        await db_session.refresh(item2)
+
+        response = await async_client.post(
+            "/api/v1/queue/reorder",
+            json={
+                "items": [
+                    {"id": item1.id, "position": 1},
+                    {"id": item2.id, "position": 1},  # duplicate
+                ]
+            },
+        )
+        assert response.status_code == 422
+        body = response.json()
+        # Pydantic v2 wraps custom validator errors; the message must mention "Duplicate"
+        # so the FE can surface the actionable detail.
+        assert any("duplicate" in str(err).lower() for err in body.get("detail", []))
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_reorder_accepts_unique_positions(
+        self, async_client: AsyncClient, db_session, printer_factory, archive_factory
+    ):
+        """Reorder with unique positions succeeds and updates them in DB."""
+        from backend.app.models.print_queue import PrintQueueItem
+
+        printer = await printer_factory()
+        a1 = await archive_factory()
+        a2 = await archive_factory()
+        item1 = PrintQueueItem(printer_id=printer.id, archive_id=a1.id, status="pending", position=1)
+        item2 = PrintQueueItem(printer_id=printer.id, archive_id=a2.id, status="pending", position=2)
+        db_session.add_all([item1, item2])
+        await db_session.commit()
+        await db_session.refresh(item1)
+        await db_session.refresh(item2)
+
+        response = await async_client.post(
+            "/api/v1/queue/reorder",
+            json={
+                "items": [
+                    {"id": item1.id, "position": 2},
+                    {"id": item2.id, "position": 1},
+                ]
+            },
+        )
+        assert response.status_code == 200
+
+        await db_session.refresh(item1)
+        await db_session.refresh(item2)
+        assert item1.position == 2
+        assert item2.position == 1

+ 97 - 0
backend/tests/integration/test_printers_api.py

@@ -1729,6 +1729,103 @@ class TestClearHMSErrorsAPI:
             assert "failed" in response.json()["detail"].lower()
 
 
+class TestExecuteHMSActionAPI:
+    """Integration tests for the /hms/execute-action endpoint (#1743).
+
+    Mirrors TestClearHMSErrorsAPI's shape — the two routes share the same
+    permission gate, the same DB-lookup + client-existence flow, and the
+    same dispatch-then-return-success pattern. The body-validation cases
+    add coverage that the bare clear endpoint doesn't need.
+    """
+
+    _VALID_BODY = {
+        "print_error": "03008070",
+        "action": "OK_BUTTON",
+        "job_id": None,
+    }
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_execute_hms_action_not_found(self, async_client: AsyncClient):
+        """404 for a printer id that doesn't exist."""
+        response = await async_client.post("/api/v1/printers/99999/hms/execute-action", json=self._VALID_BODY)
+        assert response.status_code == 404
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_execute_hms_action_not_connected(self, async_client: AsyncClient, printer_factory):
+        """400 when the printer record exists but the MQTT client is offline."""
+        printer = await printer_factory(name="Disconnected Printer")
+
+        with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
+            mock_pm.get_client.return_value = None
+
+            response = await async_client.post(
+                f"/api/v1/printers/{printer.id}/hms/execute-action", json=self._VALID_BODY
+            )
+
+            assert response.status_code == 400
+            assert "not connected" in response.json()["detail"].lower()
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_execute_hms_action_success(self, async_client: AsyncClient, printer_factory):
+        """200 happy path — dispatcher returns True, body forwarded verbatim."""
+        printer = await printer_factory(name="Test Printer")
+
+        mock_client = MagicMock()
+        mock_client.execute_hms_action.return_value = True
+
+        with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
+            mock_pm.get_client.return_value = mock_client
+
+            body = {"print_error": "07008029", "action": "FILAMENT_EXTRUDED", "job_id": "task-7"}
+            response = await async_client.post(f"/api/v1/printers/{printer.id}/hms/execute-action", json=body)
+
+            assert response.status_code == 200
+            result = response.json()
+            assert result["success"] is True
+            assert "executed" in result["message"].lower()
+            # Body args reach the client method in (print_error, action, job_id) order.
+            mock_client.execute_hms_action.assert_called_once_with("07008029", "FILAMENT_EXTRUDED", "task-7")
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_execute_hms_action_dispatcher_failure(self, async_client: AsyncClient, printer_factory):
+        """400 when the dispatcher returns False (unknown action, mid-flight disconnect)."""
+        printer = await printer_factory(name="Test Printer")
+
+        mock_client = MagicMock()
+        mock_client.execute_hms_action.return_value = False
+
+        with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
+            mock_pm.get_client.return_value = mock_client
+
+            response = await async_client.post(
+                f"/api/v1/printers/{printer.id}/hms/execute-action", json=self._VALID_BODY
+            )
+
+            assert response.status_code == 400
+            assert "failed" in response.json()["detail"].lower()
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_execute_hms_action_rejects_malformed_print_error(self, async_client: AsyncClient, printer_factory):
+        """422 when print_error fails the ^[0-9A-Fa-f]{8}$ pattern — stray
+        input can't reach the dispatcher's match statement."""
+        printer = await printer_factory(name="Test Printer")
+
+        bad_bodies = [
+            {"print_error": "0300_8070", "action": "OK_BUTTON"},  # underscore
+            {"print_error": "0300807", "action": "OK_BUTTON"},  # 7 chars
+            {"print_error": "030080700", "action": "OK_BUTTON"},  # 9 chars
+            {"print_error": "0300GGGG", "action": "OK_BUTTON"},  # non-hex
+        ]
+        for body in bad_bodies:
+            response = await async_client.post(f"/api/v1/printers/{printer.id}/hms/execute-action", json=body)
+            assert response.status_code == 422, body
+
+
 def _build_h2d_state(*, ams_id: int = 0, tray_id: int = 2, cali_idx: int = 5):
     """Build a MagicMock PrinterState for an H2D printer with a single BL spool tray.
 

+ 0 - 420
backend/tests/unit/services/test_background_dispatch.py

@@ -1,420 +0,0 @@
-"""Unit tests for background dispatch service."""
-
-from types import SimpleNamespace
-from unittest.mock import AsyncMock, patch
-
-import pytest
-
-from backend.app.services.background_dispatch import (
-    ActiveDispatchState,
-    BackgroundDispatchService,
-    DispatchEnqueueRejected,
-    PrintDispatchJob,
-)
-
-
-@pytest.mark.asyncio
-async def test_dispatch_rejects_when_printer_busy_printing():
-    """Reject enqueue when target printer is already printing."""
-    service = BackgroundDispatchService()
-
-    with (
-        patch(
-            "backend.app.services.background_dispatch.printer_manager.get_status",
-            return_value=SimpleNamespace(state="RUNNING", gcode_file="active.gcode.3mf"),
-        ),
-        pytest.raises(DispatchEnqueueRejected, match="currently busy printing"),
-    ):
-        await service.dispatch_reprint_archive(
-            archive_id=1,
-            archive_name="Test Archive",
-            printer_id=10,
-            printer_name="Printer A",
-            options={},
-            requested_by_user_id=None,
-            requested_by_username=None,
-        )
-
-
-@pytest.mark.asyncio
-async def test_dispatch_enqueues_job_and_broadcasts_state():
-    """Enqueue succeeds and emits websocket queue update."""
-    service = BackgroundDispatchService()
-
-    with (
-        patch("backend.app.services.background_dispatch.printer_manager.get_status", return_value=None),
-        patch(
-            "backend.app.services.background_dispatch.ws_manager.broadcast", new_callable=AsyncMock
-        ) as mock_broadcast,
-    ):
-        result = await service.dispatch_print_library_file(
-            file_id=22,
-            filename="cube.gcode.3mf",
-            printer_id=7,
-            printer_name="Printer B",
-            options={"plate_id": 2},
-            requested_by_user_id=5,
-            requested_by_username="tester",
-        )
-
-    assert result["status"] == "dispatched"
-    assert result["dispatch_job_id"] == 1
-    assert result["dispatch_position"] == 1
-    assert len(service._queued_jobs) == 1
-
-    mock_broadcast.assert_awaited_once()
-    payload = mock_broadcast.await_args.args[0]
-    assert payload["type"] == "background_dispatch"
-    assert payload["data"]["recent_event"]["status"] == "dispatched"
-
-
-@pytest.mark.asyncio
-async def test_dispatch_library_file_defaults_cleanup_flag_false():
-    """cleanup_library_after_dispatch defaults to False when not passed — protects
-    File Manager / Project Detail / queued-library-file paths from surprise deletion."""
-    service = BackgroundDispatchService()
-
-    with (
-        patch("backend.app.services.background_dispatch.printer_manager.get_status", return_value=None),
-        patch("backend.app.services.background_dispatch.ws_manager.broadcast", new_callable=AsyncMock),
-    ):
-        await service.dispatch_print_library_file(
-            file_id=1,
-            filename="cube.gcode.3mf",
-            printer_id=1,
-            printer_name="Printer A",
-            options={},
-            requested_by_user_id=None,
-            requested_by_username=None,
-        )
-
-    assert len(service._queued_jobs) == 1
-    assert service._queued_jobs[0].cleanup_library_after_dispatch is False
-
-
-@pytest.mark.asyncio
-async def test_dispatch_library_file_propagates_cleanup_flag_true():
-    """cleanup_library_after_dispatch=True arrives on the queued job so the runner
-    can delete the transient LibraryFile after the print is accepted by the printer."""
-    service = BackgroundDispatchService()
-
-    with (
-        patch("backend.app.services.background_dispatch.printer_manager.get_status", return_value=None),
-        patch("backend.app.services.background_dispatch.ws_manager.broadcast", new_callable=AsyncMock),
-    ):
-        await service.dispatch_print_library_file(
-            file_id=1,
-            filename="cube.gcode.3mf",
-            printer_id=1,
-            printer_name="Printer A",
-            options={},
-            requested_by_user_id=42,
-            requested_by_username="alice",
-            cleanup_library_after_dispatch=True,
-        )
-
-    assert len(service._queued_jobs) == 1
-    job = service._queued_jobs[0]
-    assert job.cleanup_library_after_dispatch is True
-    # Sanity: other fields still wired correctly
-    assert job.requested_by_user_id == 42
-    assert job.requested_by_username == "alice"
-    assert job.kind == "print_library_file"
-
-
-@pytest.mark.asyncio
-async def test_cancel_queued_job_removes_it_and_broadcasts():
-    """Cancelling queued job removes it immediately."""
-    service = BackgroundDispatchService()
-
-    with (
-        patch("backend.app.services.background_dispatch.printer_manager.get_status", return_value=None),
-        patch(
-            "backend.app.services.background_dispatch.ws_manager.broadcast", new_callable=AsyncMock
-        ) as mock_broadcast,
-    ):
-        result = await service.dispatch_reprint_archive(
-            archive_id=1,
-            archive_name="benchy.gcode.3mf",
-            printer_id=1,
-            printer_name="Printer 1",
-            options={},
-            requested_by_user_id=None,
-            requested_by_username=None,
-        )
-        mock_broadcast.reset_mock()
-
-        cancel_result = await service.cancel_job(result["dispatch_job_id"])
-
-    assert cancel_result["cancelled"] is True
-    assert cancel_result["pending"] is False
-    assert len(service._queued_jobs) == 0
-    assert service._batch_total == 0
-
-    mock_broadcast.assert_awaited_once()
-    payload = mock_broadcast.await_args.args[0]
-    assert payload["data"]["recent_event"]["status"] == "cancelled"
-
-
-@pytest.mark.asyncio
-async def test_cancel_active_job_marks_pending_and_sets_cancel_flag():
-    """Cancelling active job marks it as pending cancellation."""
-    service = BackgroundDispatchService()
-    job = PrintDispatchJob(
-        id=42,
-        kind="reprint_archive",
-        source_id=100,
-        source_name="gearbox.gcode.3mf",
-        printer_id=3,
-        printer_name="Printer C",
-    )
-    service._active_jobs[job.id] = ActiveDispatchState(job=job, message="Uploading...")
-
-    with patch(
-        "backend.app.services.background_dispatch.ws_manager.broadcast", new_callable=AsyncMock
-    ) as mock_broadcast:
-        result = await service.cancel_job(job.id)
-
-    assert result["cancelled"] is True
-    assert result["pending"] is True
-    assert job.id in service._cancel_requested_job_ids
-
-    mock_broadcast.assert_awaited_once()
-    payload = mock_broadcast.await_args.args[0]
-    assert payload["data"]["recent_event"]["status"] == "cancelling"
-
-
-def test_resolve_plate_id_uses_request_value_when_provided(tmp_path):
-    """Explicit plate_id wins over auto-detection."""
-    file_path = tmp_path / "dummy.3mf"
-    file_path.write_text("not-a-zip")
-
-    plate_id = BackgroundDispatchService._resolve_plate_id(file_path, requested_plate_id=9)
-    assert plate_id == 9
-
-
-def test_resolve_plate_id_auto_detects_from_3mf(tmp_path):
-    """Auto-detect plate from Metadata/plate_X.gcode entry."""
-    import zipfile
-
-    file_path = tmp_path / "multi.3mf"
-    with zipfile.ZipFile(file_path, "w") as zf:
-        zf.writestr("Metadata/plate_7.gcode", b"G1 X0 Y0")
-
-    plate_id = BackgroundDispatchService._resolve_plate_id(file_path, requested_plate_id=None)
-    assert plate_id == 7
-
-
-def test_is_sliced_file_recognizes_supported_extensions():
-    """Only .gcode and .gcode.3mf should be accepted."""
-    assert BackgroundDispatchService._is_sliced_file("part.gcode") is True
-    assert BackgroundDispatchService._is_sliced_file("part.gcode.3mf") is True
-    assert BackgroundDispatchService._is_sliced_file("part.3mf") is False
-
-
-def test_dispatch_option_defaults_align_with_request_schema_defaults():
-    """The `job.options.get("<field>", <default>)` calls in the dispatch
-    loop must use the same default as the Pydantic request schema. If a
-    field is missing from options (e.g. an internal caller bypassing the
-    schema), the resulting print command must match what a fresh
-    `ReprintRequest()` / `FilePrintRequest()` would produce — anything
-    else means certain fields silently flip depending on which entry
-    point queued the job.
-
-    Earlier `vibration_cali` had a False default in the dispatch loop
-    against a True schema default, latent only because every existing
-    caller always sent the field.
-    """
-    import inspect
-
-    from backend.app.schemas.archive import ReprintRequest
-    from backend.app.schemas.library import FilePrintRequest
-    from backend.app.services import background_dispatch as bd
-
-    # `timelapse` deliberately excluded — the dispatcher wraps it in
-    # ``bool(...)`` (``effective_timelapse = bool(job.options.get("timelapse",
-    # False))``) so the bare-pattern needle in the loop below would miss it.
-    # The wrap exists to coerce None / non-bool option payloads to a bool
-    # boundary the printer firmware accepts (#1721 follow-up).
-    fields = ("bed_levelling", "flow_cali", "vibration_cali", "layer_inspect", "use_ams")
-    reprint_defaults = {f: getattr(ReprintRequest(), f) for f in fields}
-    libprint_defaults = {f: getattr(FilePrintRequest(), f) for f in fields}
-    assert reprint_defaults == libprint_defaults, (
-        "ReprintRequest and FilePrintRequest must share the same defaults for these fields"
-    )
-
-    src = inspect.getsource(bd)
-    for field, expected_default in reprint_defaults.items():
-        literal = "True" if expected_default else "False"
-        needle = f'{field}=job.options.get("{field}", {literal})'
-        count = src.count(needle)
-        assert count == 2, (
-            f"Expected exactly 2 occurrences of `{needle}` in background_dispatch (one per "
-            f"`_process_job` branch). Found {count}. A drift between the request schema's "
-            f"default for `{field}` and the dispatch loop's `.get()` default means callers "
-            f"that bypass the schema will get inconsistent behaviour."
-        )
-
-
-@pytest.mark.asyncio
-async def test_cancel_job_not_found_returns_false():
-    """Cancelling a nonexistent job returns not_found."""
-    service = BackgroundDispatchService()
-
-    with patch("backend.app.services.background_dispatch.ws_manager.broadcast", new_callable=AsyncMock):
-        result = await service.cancel_job(999)
-
-    assert result["cancelled"] is False
-    assert result["reason"] == "not_found"
-
-
-@pytest.mark.asyncio
-async def test_cancel_job_single_lock_covers_both_active_and_queued():
-    """cancel_job checks both active and queued jobs under a single lock acquisition.
-
-    Regression test for TOCTOU race: previously two separate lock acquisitions allowed
-    the dispatcher loop to move a job from queue to active between them, causing cancel
-    to find it in neither place.
-    """
-    service = BackgroundDispatchService()
-
-    # Set up a job in the queue AND an active job for a different printer
-    active_job = PrintDispatchJob(
-        id=1,
-        kind="reprint_archive",
-        source_id=10,
-        source_name="active.3mf",
-        printer_id=1,
-        printer_name="Printer 1",
-    )
-    service._active_jobs[active_job.id] = ActiveDispatchState(job=active_job, message="Uploading...")
-
-    queued_job = PrintDispatchJob(
-        id=2,
-        kind="reprint_archive",
-        source_id=20,
-        source_name="queued.3mf",
-        printer_id=2,
-        printer_name="Printer 2",
-    )
-    service._queued_jobs.append(queued_job)
-    service._batch_total = 2
-
-    with patch(
-        "backend.app.services.background_dispatch.ws_manager.broadcast", new_callable=AsyncMock
-    ) as mock_broadcast:
-        # Cancel the queued job — should find it in single lock acquisition
-        result = await service.cancel_job(2)
-
-    assert result["cancelled"] is True
-    assert result["pending"] is False
-    assert len(service._queued_jobs) == 0
-    # Active job should be untouched
-    assert 1 in service._active_jobs
-
-    mock_broadcast.assert_awaited_once()
-    payload = mock_broadcast.await_args.args[0]
-    assert payload["data"]["recent_event"]["status"] == "cancelled"
-
-
-@pytest.mark.asyncio
-async def test_mark_job_finished_resets_batch_when_all_done():
-    """Batch counters reset after last job completes."""
-    service = BackgroundDispatchService()
-    job = PrintDispatchJob(
-        id=1,
-        kind="reprint_archive",
-        source_id=10,
-        source_name="test.3mf",
-        printer_id=1,
-        printer_name="Printer 1",
-    )
-    service._active_jobs[job.id] = ActiveDispatchState(job=job, message="Done")
-    service._batch_total = 1
-
-    with patch("backend.app.services.background_dispatch.ws_manager.broadcast", new_callable=AsyncMock):
-        await service._mark_job_finished(job, failed=False, message="Complete")
-
-    assert service._batch_total == 0
-    assert service._batch_completed == 0
-    assert service._batch_failed == 0
-
-
-@pytest.mark.asyncio
-async def test_mark_job_finished_no_reset_when_jobs_remain():
-    """Batch counters NOT reset when queued jobs remain."""
-    service = BackgroundDispatchService()
-    job = PrintDispatchJob(
-        id=1,
-        kind="reprint_archive",
-        source_id=10,
-        source_name="test.3mf",
-        printer_id=1,
-        printer_name="Printer 1",
-    )
-    remaining_job = PrintDispatchJob(
-        id=2,
-        kind="reprint_archive",
-        source_id=20,
-        source_name="next.3mf",
-        printer_id=2,
-        printer_name="Printer 2",
-    )
-    service._active_jobs[job.id] = ActiveDispatchState(job=job, message="Done")
-    service._queued_jobs.append(remaining_job)
-    service._batch_total = 2
-
-    with patch("backend.app.services.background_dispatch.ws_manager.broadcast", new_callable=AsyncMock):
-        await service._mark_job_finished(job, failed=False, message="Complete")
-
-    # Batch counters should NOT be reset — remaining job still queued
-    assert service._batch_total == 2
-    assert service._batch_completed == 1
-
-
-@pytest.mark.asyncio
-async def test_mark_job_finished_batch_reset_rechecks_under_lock():
-    """Batch reset re-checks condition inside second lock acquisition.
-
-    Regression test for TOCTOU: a new dispatch between the two lock acquisitions
-    could get its counters zeroed if the re-check is missing.
-    """
-    service = BackgroundDispatchService()
-    job = PrintDispatchJob(
-        id=1,
-        kind="reprint_archive",
-        source_id=10,
-        source_name="test.3mf",
-        printer_id=1,
-        printer_name="Printer 1",
-    )
-    service._active_jobs[job.id] = ActiveDispatchState(job=job, message="Done")
-    service._batch_total = 1
-
-    original_broadcast = AsyncMock()
-
-    async def inject_new_job_during_broadcast(msg):
-        """Simulate a new dispatch arriving between the two lock acquisitions."""
-        await original_broadcast(msg)
-        # After broadcast (lock released), inject a new job before reset re-check
-        if not service._queued_jobs:
-            new_job = PrintDispatchJob(
-                id=99,
-                kind="reprint_archive",
-                source_id=99,
-                source_name="injected.3mf",
-                printer_id=5,
-                printer_name="Printer 5",
-            )
-            service._queued_jobs.append(new_job)
-            service._batch_total = 1
-
-    with patch(
-        "backend.app.services.background_dispatch.ws_manager.broadcast",
-        side_effect=inject_new_job_during_broadcast,
-    ):
-        await service._mark_job_finished(job, failed=False, message="Complete")
-
-    # Re-check should prevent reset since a new job appeared
-    assert service._batch_total == 1
-    assert len(service._queued_jobs) == 1

+ 0 - 721
backend/tests/unit/services/test_background_dispatch_watchdog.py

@@ -1,721 +0,0 @@
-"""Regression tests for ``BackgroundDispatchService._verify_print_response``.
-
-The background-dispatch watchdog used to be fire-and-forget — it logged a
-warning and force-reconnected MQTT, but the dispatch job had already been
-marked successful. The user therefore saw "Print started successfully" while
-the printer never actually transitioned (#1042 follow-up). The watchdog now
-returns a bool so the caller can fail the dispatch job when the printer
-doesn't acknowledge the command, mirroring what `_watchdog_print_start` does
-on the queue side.
-
-Both transition signals are accepted: ``state`` advancing past ``pre_state``
-*or* ``subtask_id`` advancing past ``pre_subtask_id`` — H2D firmware can sit
-at FINISH for ~50 s after accepting ``project_file`` while echoing the new
-subtask_id back almost immediately (#1078).
-"""
-
-from types import SimpleNamespace
-from unittest.mock import MagicMock, patch
-
-import pytest
-
-from backend.app.services.background_dispatch import BackgroundDispatchService
-
-
-def _status(state: str, subtask_id: str | None = None, gcode_file: str | None = None):
-    """Minimal stand-in for PrinterState — only the fields the watchdog reads."""
-    return SimpleNamespace(state=state, subtask_id=subtask_id, gcode_file=gcode_file)
-
-
-class TestReturnsTrueOnPickup:
-    @pytest.mark.asyncio
-    async def test_returns_true_on_state_change(self):
-        get_status = MagicMock(return_value=_status("RUNNING", "OLD_SUBTASK"))
-        with patch(
-            "backend.app.services.background_dispatch.printer_manager.get_status",
-            get_status,
-        ):
-            result = await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="P1S",
-                pre_state="FINISH",
-                pre_subtask_id="OLD_SUBTASK",
-                timeout=0.3,
-                poll_interval=0.05,
-            )
-
-        assert result is True
-
-    @pytest.mark.asyncio
-    async def test_returns_true_on_subtask_id_change_even_if_state_still_finish(self):
-        """#1078: H2D keeps state=FINISH for ~50 s after accepting project_file
-        but flips subtask_id immediately. Must be accepted as a pickup signal."""
-        get_status = MagicMock(return_value=_status("FINISH", "NEW_SUBTASK_12345"))
-        with patch(
-            "backend.app.services.background_dispatch.printer_manager.get_status",
-            get_status,
-        ):
-            result = await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="H2D",
-                pre_state="FINISH",
-                pre_subtask_id="OLD_SUBTASK_99999",
-                timeout=0.3,
-                poll_interval=0.05,
-            )
-
-        assert result is True
-
-
-class TestReturnsFalseOnTimeout:
-    @pytest.mark.asyncio
-    async def test_returns_false_when_neither_state_nor_subtask_id_changes(self):
-        """The exact #1042 scenario: P1S sits in FAILED with HMS pending,
-        accepts the MQTT publish, never transitions. Watchdog must report
-        failure so the caller fails the dispatch job."""
-        get_status = MagicMock(return_value=_status("FINISH", "OLD_SUBTASK"))
-        client = MagicMock()
-        get_client = MagicMock(return_value=client)
-
-        with (
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                get_status,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_client",
-                get_client,
-            ),
-        ):
-            result = await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="P1S",
-                pre_state="FINISH",
-                pre_subtask_id="OLD_SUBTASK",
-                timeout=0.2,
-                poll_interval=0.05,
-            )
-
-        assert result is False
-        client.force_reconnect_stale_session.assert_called_once()
-
-    @pytest.mark.asyncio
-    async def test_returns_false_on_finish_to_idle_user_dismissed_prompt(self):
-        """Regression for #1370 in the direct-dispatch path: when pre_state is
-        FINISH and the printer transitions to IDLE during the verifier window,
-        that's the user dismissing a post-print prompt — NOT acceptance of our
-        project_file. The original ``state != pre_state`` check incorrectly
-        returned True on this transition, so the dispatch job was marked
-        successful even though no print was running. Must now report failure
-        so the caller raises RuntimeError and the user sees the actual error.
-        """
-        get_status = MagicMock(return_value=_status("IDLE", "OLD_SUBTASK"))
-        client = MagicMock()
-        get_client = MagicMock(return_value=client)
-
-        with (
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                get_status,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_client",
-                get_client,
-            ),
-        ):
-            result = await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="P1S",
-                pre_state="FINISH",
-                pre_subtask_id="OLD_SUBTASK",
-                timeout=0.2,
-                poll_interval=0.05,
-            )
-
-        assert result is False, (
-            "FINISH -> IDLE is the user dismissing a screen prompt, not the "
-            "printer accepting project_file — verifier must report failure (#1370)"
-        )
-
-    @pytest.mark.asyncio
-    async def test_returns_true_on_each_active_print_state(self):
-        """Counterpart to the #1370 fix: transitions into the active-print
-        state set ARE valid "command landed" signals. PREPARE / SLICING /
-        RUNNING / PAUSE all return True.
-        """
-        for active_state in ("PREPARE", "SLICING", "RUNNING", "PAUSE"):
-            get_status = MagicMock(return_value=_status(active_state, "OLD_SUBTASK"))
-            with patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                get_status,
-            ):
-                result = await BackgroundDispatchService._verify_print_response(
-                    printer_id=42,
-                    printer_name="P1S",
-                    pre_state="IDLE",
-                    pre_subtask_id="OLD_SUBTASK",
-                    timeout=0.2,
-                    poll_interval=0.05,
-                )
-            assert result is True, (
-                f"transition IDLE -> {active_state} must be treated as a valid 'command landed' signal"
-            )
-
-    @pytest.mark.asyncio
-    async def test_returns_false_when_pre_subtask_id_none_and_state_unchanged(self):
-        """Backward-compat: callers without a captured pre_subtask_id (e.g. the
-        printer never reported one) must still get the timeout failure path
-        based on state alone."""
-        get_status = MagicMock(return_value=_status("FINISH", "ANYTHING"))
-        get_client = MagicMock(return_value=None)
-
-        with (
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                get_status,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_client",
-                get_client,
-            ),
-        ):
-            result = await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="P1S",
-                pre_state="FINISH",
-                pre_subtask_id=None,
-                timeout=0.2,
-                poll_interval=0.05,
-            )
-
-        assert result is False
-
-    @pytest.mark.asyncio
-    async def test_subtask_id_none_post_dispatch_does_not_count_as_change(self):
-        """If the printer transiently reports subtask_id=None during the
-        watchdog window (e.g. mid-reconnect), that must not be treated as
-        "advanced past pre_subtask_id" — otherwise we'd false-pass and mark
-        a never-started print as successful."""
-        get_status = MagicMock(return_value=_status("FINISH", None))
-        get_client = MagicMock(return_value=None)
-
-        with (
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                get_status,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_client",
-                get_client,
-            ),
-        ):
-            result = await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="P1S",
-                pre_state="FINISH",
-                pre_subtask_id="OLD_SUBTASK",
-                timeout=0.2,
-                poll_interval=0.05,
-            )
-
-        assert result is False
-
-
-class TestDisconnectHandling:
-    @pytest.mark.asyncio
-    async def test_disconnect_does_not_short_circuit_window(self):
-        """A momentary ``get_status() is None`` (brief MQTT disconnect mid-window)
-        must not immediately fail the dispatch — the printer may reconnect and
-        still produce a valid transition before timeout. Falsely failing on the
-        first missed tick is the previous bug class we're moving away from."""
-        # First call: disconnected. Second call onward: reconnected and transitioned.
-        get_status = MagicMock(side_effect=[None, _status("RUNNING")])
-        with patch(
-            "backend.app.services.background_dispatch.printer_manager.get_status",
-            get_status,
-        ):
-            result = await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="P1S",
-                pre_state="FINISH",
-                pre_subtask_id="OLD_SUBTASK",
-                timeout=0.3,
-                poll_interval=0.05,
-            )
-
-        assert result is True
-        assert get_status.call_count >= 2
-
-    @pytest.mark.asyncio
-    async def test_disconnect_for_full_window_returns_false(self):
-        """Persistent disconnect for the full window is treated as failure.
-        Better to false-fail and let the user retry than to false-succeed and
-        leave them watching an idle printer (#1042)."""
-        get_status = MagicMock(return_value=None)
-        get_client = MagicMock(return_value=None)
-
-        with (
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                get_status,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_client",
-                get_client,
-            ),
-        ):
-            result = await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="P1S",
-                pre_state="FINISH",
-                pre_subtask_id="OLD_SUBTASK",
-                timeout=0.2,
-                poll_interval=0.05,
-            )
-
-        assert result is False
-
-
-class TestDefaults:
-    def test_default_timeout_matches_queue_watchdog(self):
-        """Queue and background watchdogs need the same 90 s default to give
-        slow H2D FINISH→PREPARE transitions the same headroom on both paths."""
-        import inspect
-
-        sig = inspect.signature(BackgroundDispatchService._verify_print_response)
-        assert sig.parameters["timeout"].default == 90.0
-
-
-class TestGcodeFileDiscriminator:
-    """#1150 vs #887/#936 discriminator: skip the forced reconnect when the
-    printer's gcode_file changed since pre-dispatch (project_file landed,
-    printer is parsing slowly — reconnecting mid-parse causes 0500_4003).
-    Reconnect when gcode_file is unchanged (publish was silently swallowed —
-    half-broken session needs the original recovery)."""
-
-    @pytest.mark.asyncio
-    async def test_skips_reconnect_when_gcode_file_changed(self):
-        get_status = MagicMock(
-            return_value=_status("FINISH", "OLD_SUBTASK", gcode_file="/new.3mf"),
-        )
-        client = MagicMock()
-        get_client = MagicMock(return_value=client)
-
-        with (
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                get_status,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_client",
-                get_client,
-            ),
-        ):
-            result = await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="P1P",
-                pre_state="FINISH",
-                pre_subtask_id="OLD_SUBTASK",
-                pre_gcode_file="/old.3mf",
-                timeout=0.2,
-                poll_interval=0.05,
-            )
-
-        assert result is False
-        client.force_reconnect_stale_session.assert_not_called()
-
-    @pytest.mark.asyncio
-    async def test_reconnects_when_gcode_file_unchanged(self):
-        # The half-broken-session case (#887/#936): publish was dropped, so
-        # the printer is still showing the previous file. Reconnect to clear
-        # the broken paho QoS-1 queue.
-        get_status = MagicMock(
-            return_value=_status("FINISH", "OLD_SUBTASK", gcode_file="/old.3mf"),
-        )
-        client = MagicMock()
-        get_client = MagicMock(return_value=client)
-
-        with (
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                get_status,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_client",
-                get_client,
-            ),
-        ):
-            await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="P1P",
-                pre_state="FINISH",
-                pre_subtask_id="OLD_SUBTASK",
-                pre_gcode_file="/old.3mf",
-                timeout=0.2,
-                poll_interval=0.05,
-            )
-
-        client.force_reconnect_stale_session.assert_called_once()
-
-    @pytest.mark.asyncio
-    async def test_skips_reconnect_when_pre_gcode_file_was_none(self):
-        # Printer just connected (pre_gcode_file=None) and now reports a
-        # file — that's a clear "command landed" signal too.
-        get_status = MagicMock(
-            return_value=_status("FINISH", "OLD_SUBTASK", gcode_file="/new.3mf"),
-        )
-        client = MagicMock()
-        get_client = MagicMock(return_value=client)
-
-        with (
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                get_status,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_client",
-                get_client,
-            ),
-        ):
-            await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="P1P",
-                pre_state="FINISH",
-                pre_subtask_id="OLD_SUBTASK",
-                pre_gcode_file=None,
-                timeout=0.2,
-                poll_interval=0.05,
-            )
-
-        client.force_reconnect_stale_session.assert_not_called()
-
-    @pytest.mark.asyncio
-    async def test_reconnects_when_no_pre_gcode_file_arg_supplied(self):
-        # Backward-compat: callers that don't pass pre_gcode_file at all
-        # (everything but our updated dispatch sites) must still get the
-        # original reconnect-on-timeout behaviour. Here pre_gcode_file
-        # defaults to None and the printer's current gcode_file is also
-        # None → publish_landed=False → reconnect.
-        get_status = MagicMock(
-            return_value=_status("FINISH", "OLD_SUBTASK", gcode_file=None),
-        )
-        client = MagicMock()
-        get_client = MagicMock(return_value=client)
-
-        with (
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                get_status,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_client",
-                get_client,
-            ),
-        ):
-            await BackgroundDispatchService._verify_print_response(
-                printer_id=42,
-                printer_name="P1P",
-                pre_state="FINISH",
-                pre_subtask_id="OLD_SUBTASK",
-                timeout=0.2,
-                poll_interval=0.05,
-            )
-
-        client.force_reconnect_stale_session.assert_called_once()
-
-
-# ---------------------------------------------------------------------------
-# Integration tests: the call sites in _run_reprint_archive and
-# _run_print_library_file must (a) await the watchdog instead of fire-and-
-# forget, (b) raise RuntimeError on watchdog False so _run_active_job marks
-# the job failed, (c) rollback the library-file flow's freshly-created
-# archive on timeout. Heavy mocking — the goal is to verify the new wiring,
-# not to re-test the dependencies.
-# ---------------------------------------------------------------------------
-
-from contextlib import asynccontextmanager  # noqa: E402
-from unittest.mock import AsyncMock  # noqa: E402
-
-from backend.app.services.background_dispatch import (  # noqa: E402
-    ActiveDispatchState,
-    PrintDispatchJob,
-)
-
-
-def _make_session_factory(db_mock):
-    """Build an async-session factory whose context manager yields ``db_mock``.
-
-    Mirrors the ``async with async_session() as db`` shape used by both
-    ``_run_*`` methods so the test can intercept ``db.rollback`` / ``db.scalar``.
-    """
-
-    @asynccontextmanager
-    async def _factory():
-        yield db_mock
-
-    return _factory
-
-
-def _printer_namespace():
-    return SimpleNamespace(
-        id=10,
-        name="P1S",
-        ip_address="1.2.3.4",
-        access_code="abc",
-        model="P1S",
-    )
-
-
-def _make_dispatch_job(kind: str = "reprint_archive") -> PrintDispatchJob:
-    return PrintDispatchJob(
-        id=1,
-        kind=kind,
-        source_id=99,
-        source_name="Test.gcode.3mf",
-        printer_id=10,
-        printer_name="P1S",
-        options={},
-        requested_by_user_id=None,
-        requested_by_username=None,
-    )
-
-
-@pytest.fixture
-def reprint_archive_mocks(tmp_path):
-    """Mock harness for ``_run_reprint_archive`` covering every external
-    dependency up to (and including) ``start_print``. The watchdog is left
-    real so the caller can patch ``_verify_print_response`` per-test."""
-    archive_file = tmp_path / "test.3mf"
-    archive_file.write_bytes(b"fake-3mf-content")
-
-    archive = SimpleNamespace(
-        id=99,
-        filename="Test.gcode.3mf",
-        file_path=str(archive_file),
-    )
-
-    db = MagicMock()
-    db.scalar = AsyncMock(return_value=_printer_namespace())
-    db.rollback = AsyncMock()
-
-    archive_service = MagicMock()
-    archive_service.get_archive = AsyncMock(return_value=archive)
-
-    return {
-        "archive": archive,
-        "archive_file": archive_file,
-        "db": db,
-        "archive_service": archive_service,
-        "session_factory": _make_session_factory(db),
-    }
-
-
-@pytest.fixture
-def library_file_mocks(tmp_path):
-    """Mock harness for ``_run_print_library_file`` — separate from the
-    reprint fixture because the library flow creates its archive via
-    ``archive_service.archive_print(...)`` rather than fetching one."""
-    src_file = tmp_path / "lib_src.3mf"
-    src_file.write_bytes(b"fake-3mf-content")
-
-    lib_file = SimpleNamespace(
-        id=22,
-        filename="cube.gcode.3mf",
-        file_path=str(src_file.relative_to(tmp_path)),
-    )
-    lib_file.active = staticmethod(lambda: lib_file)  # mimic LibraryFile.active() chainable
-
-    new_archive = SimpleNamespace(id=500, filename="cube.gcode.3mf", file_path=str(src_file))
-
-    db = MagicMock()
-    db.scalar = AsyncMock()  # configured per-test
-    db.flush = AsyncMock()
-    db.commit = AsyncMock()
-    db.rollback = AsyncMock()
-
-    archive_service = MagicMock()
-    archive_service.archive_print = AsyncMock(return_value=new_archive)
-
-    return {
-        "lib_file": lib_file,
-        "src_file": src_file,
-        "new_archive": new_archive,
-        "db": db,
-        "archive_service": archive_service,
-        "session_factory": _make_session_factory(db),
-    }
-
-
-class TestReprintArchiveDispatchWiring:
-    """Verify ``_run_reprint_archive`` (a) awaits the watchdog inline and
-    (b) raises RuntimeError on False so the dispatch job is marked failed."""
-
-    @pytest.mark.asyncio
-    async def test_raises_runtime_error_when_watchdog_returns_false(self, reprint_archive_mocks):
-        """The exact #1042 propagation gap: watchdog detects non-transition,
-        _run_reprint_archive must surface it as a RuntimeError so the surrounding
-        _run_active_job marks the job failed (instead of silently completing)."""
-        from backend.app.services.background_dispatch import BackgroundDispatchService
-
-        m = reprint_archive_mocks
-        service = BackgroundDispatchService()
-        job = _make_dispatch_job(kind="reprint_archive")
-
-        watchdog = AsyncMock(return_value=False)
-
-        with (
-            patch("backend.app.services.background_dispatch.async_session", m["session_factory"]),
-            patch(
-                "backend.app.services.background_dispatch.ArchiveService",
-                return_value=m["archive_service"],
-            ),
-            patch.object(BackgroundDispatchService, "_verify_print_response", watchdog),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.is_connected",
-                return_value=True,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                return_value=SimpleNamespace(state="FINISH", subtask_id="OLD_SUBTASK"),
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.start_print",
-                return_value=True,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.delete_file_async",
-                new_callable=AsyncMock,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.with_ftp_retry",
-                new_callable=AsyncMock,
-                return_value=True,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.get_ftp_retry_settings",
-                new_callable=AsyncMock,
-                return_value=(False, 0, 0, 30.0),
-            ),
-            patch(
-                "backend.app.services.background_dispatch.upload_file_async",
-                new_callable=AsyncMock,
-                return_value=True,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.ws_manager.broadcast",
-                new_callable=AsyncMock,
-            ),
-            patch("backend.app.main.register_expected_print"),
-            pytest.raises(RuntimeError, match="did not acknowledge print command"),
-        ):
-            await service._run_reprint_archive(job)
-
-        # Watchdog received the captured pre-state and pre_subtask_id.
-        watchdog.assert_awaited_once()
-        kwargs = watchdog.await_args.kwargs
-        args = watchdog.await_args.args
-        assert "FINISH" in args  # pre_state
-        assert kwargs["pre_subtask_id"] == "OLD_SUBTASK"
-
-    @pytest.mark.asyncio
-    async def test_succeeds_when_watchdog_returns_true(self, reprint_archive_mocks):
-        """Happy path: watchdog confirms pickup; _run_reprint_archive returns
-        without raising. Guards against the wiring accidentally raising on True."""
-        from backend.app.services.background_dispatch import BackgroundDispatchService
-
-        m = reprint_archive_mocks
-        service = BackgroundDispatchService()
-        job = _make_dispatch_job(kind="reprint_archive")
-
-        with (
-            patch("backend.app.services.background_dispatch.async_session", m["session_factory"]),
-            patch(
-                "backend.app.services.background_dispatch.ArchiveService",
-                return_value=m["archive_service"],
-            ),
-            patch.object(
-                BackgroundDispatchService,
-                "_verify_print_response",
-                AsyncMock(return_value=True),
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.is_connected",
-                return_value=True,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.get_status",
-                return_value=SimpleNamespace(state="FINISH", subtask_id="OLD_SUBTASK"),
-            ),
-            patch(
-                "backend.app.services.background_dispatch.printer_manager.start_print",
-                return_value=True,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.delete_file_async",
-                new_callable=AsyncMock,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.with_ftp_retry",
-                new_callable=AsyncMock,
-                return_value=True,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.get_ftp_retry_settings",
-                new_callable=AsyncMock,
-                return_value=(False, 0, 0, 30.0),
-            ),
-            patch(
-                "backend.app.services.background_dispatch.upload_file_async",
-                new_callable=AsyncMock,
-                return_value=True,
-            ),
-            patch(
-                "backend.app.services.background_dispatch.ws_manager.broadcast",
-                new_callable=AsyncMock,
-            ),
-            patch("backend.app.main.register_expected_print"),
-        ):
-            await service._run_reprint_archive(job)  # must not raise
-
-        # Reprint flow does not touch the existing archive — no rollback expected.
-        m["db"].rollback.assert_not_called()
-
-
-class TestRunActiveJobMarksFailedOnRuntimeError:
-    """End-to-end: a watchdog-driven RuntimeError must reach
-    `_mark_job_finished(failed=True)` via the existing ``_run_active_job``
-    catch-all, so the dispatch UI shows a real failure (not "Done")."""
-
-    @pytest.mark.asyncio
-    async def test_runtime_error_from_process_job_marks_failed_with_message(self):
-        from backend.app.services.background_dispatch import BackgroundDispatchService
-
-        service = BackgroundDispatchService()
-        job = _make_dispatch_job()
-        # Place the job into _active_jobs so _set_active_message has a target.
-        service._active_jobs[job.id] = ActiveDispatchState(job=job, message="")
-
-        failure_message = (
-            "Printer did not acknowledge print command — state still FINISH. "
-            "Check the printer for a pending error (HMS code, plate-clear prompt, "
-            "SD card) and try again."
-        )
-
-        with (
-            patch.object(
-                BackgroundDispatchService,
-                "_process_job",
-                AsyncMock(side_effect=RuntimeError(failure_message)),
-            ),
-            patch.object(
-                BackgroundDispatchService,
-                "_mark_job_finished",
-                new_callable=AsyncMock,
-            ) as mark_finished,
-        ):
-            await service._run_active_job(job)
-
-        mark_finished.assert_awaited_once()
-        kwargs = mark_finished.await_args.kwargs
-        assert kwargs["failed"] is True
-        assert "did not acknowledge print command" in kwargs["message"]

+ 3 - 4
backend/tests/unit/services/test_bambu_mqtt.py

@@ -4742,7 +4742,7 @@ class TestZombieSessionDetection:
         the routing in force_reconnect_stale_session falls back to socket-close
         — which is the safe option since loop_stop() from inside the loop
         thread would deadlock. Hard-reset is reserved for async-context callers
-        (background_dispatch dispatch path)."""
+        on the queue dispatch path."""
         import time
 
         state_change_called = []
@@ -5026,9 +5026,8 @@ class TestHardResetClientDirect:
 
     def test_swallows_disconnect_exception(self, mqtt_client):
         """A failing disconnect() (e.g. paho already in error state) must not
-        propagate — the await chain in background_dispatch.py would otherwise
-        raise instead of moving on, and a single broken client could brick
-        every future dispatch."""
+        propagate through async dispatch callers, and a single broken client
+        could brick every future dispatch."""
         original = mqtt_client._client
         original.disconnect.side_effect = RuntimeError("boom")
         # No exception escapes the call (test would fail if it did).

+ 229 - 0
backend/tests/unit/services/test_hms_actions.py

@@ -0,0 +1,229 @@
+"""Tests for HMS-action lookup and the MQTT dispatcher in execute_hms_action.
+
+The lookup tests confirm the bundled catalog round-trips correctly. The
+dispatcher tests are payload-shape contracts — wrong shape sends a bogus
+command to the printer, which is the failure mode this PR is most exposed to,
+so each HMSAction case publishes the expected JSON.
+"""
+
+import json
+from unittest.mock import MagicMock
+
+import pytest
+
+from backend.app.services.bambu_mqtt import BambuMQTTClient
+from backend.app.services.hms_actions import (
+    HMSAction,
+    get_actions_for_error_code,
+)
+
+
+class TestActionLookup:
+    def test_known_a1_error_returns_actions(self):
+        # 03W is the A1 model code; 03008070 is "Heat the nozzle…" and Bambu's
+        # catalog lists CHECK_ASSISTANT for it.
+        actions = get_actions_for_error_code("03W", "03008070")
+        assert isinstance(actions, list)
+        assert len(actions) > 0
+        for a in actions:
+            assert isinstance(a, str)
+
+    def test_unknown_device_returns_empty_list(self):
+        assert get_actions_for_error_code("ZZZ", "03008070") == []
+
+    def test_unknown_error_returns_empty_list(self):
+        # Real model code, made-up error.
+        assert get_actions_for_error_code("03W", "DEADBEEF") == []
+
+    def test_underscore_form_does_not_match(self):
+        # Caller is responsible for stripping the `_` before lookup. Guards
+        # against accidental rewires that pass the underscore form.
+        assert get_actions_for_error_code("03W", "0300_8070") == []
+
+    def test_action_enum_values_are_uppercase_strings(self):
+        # The catalog stores actions verbatim from BambuStudio. Drift here
+        # silently breaks the dispatcher's `match` because StrEnum compares
+        # by value.
+        assert HMSAction.RESUME_PRINTING == "RESUME_PRINTING"
+        assert HMSAction.CANCLE == "CANCLE"  # sic — kept from BambuStudio
+
+
+class TestExecuteHmsActionDispatch:
+    """Each case in the `match` publishes a specific JSON shape. These tests
+    pin those shapes so silent regressions surface as test failures, not as
+    a printer receiving a malformed command on a live print.
+    """
+
+    @pytest.fixture
+    def client(self):
+        c = BambuMQTTClient(
+            ip_address="192.168.1.100",
+            serial_number="03W-TEST",
+            access_code="12345678",
+        )
+        c._client = MagicMock()
+        c.state.connected = True
+        return c
+
+    def _published_commands(self, client):
+        """Return the list of `print`/`system` command dicts from publish calls,
+        skipping the `pushing.pushall` echoes that follow every action."""
+        out = []
+        for call in client._client.publish.call_args_list:
+            _topic, payload = call.args[0], call.args[1]
+            data = json.loads(payload)
+            if "pushing" in data:
+                continue
+            out.append(data)
+        return out
+
+    def test_returns_false_when_disconnected(self, client):
+        client.state.connected = False
+        assert client.execute_hms_action("03008070", HMSAction.OK_BUTTON) is False
+        client._client.publish.assert_not_called()
+
+    def test_returns_false_on_unknown_action(self, client):
+        assert client.execute_hms_action("03008070", "DOES_NOT_EXIST") is False
+        # No printer command, but the publish-list check tolerates the pushall
+        # tail — just confirm no command went out by inspecting the helper.
+        assert self._published_commands(client) == []
+
+    def test_resume_carries_err_param_and_job_id(self, client):
+        ok = client.execute_hms_action("03008070", HMSAction.RESUME_PRINTING, job_id="task-42")
+        assert ok is True
+        cmds = self._published_commands(client)
+        assert cmds == [
+            {
+                "print": {
+                    "command": "resume",
+                    "err": "03008070",
+                    "param": "reserve",
+                    "job_id": "task-42",
+                    "sequence_id": "0",
+                }
+            }
+        ]
+
+    def test_proceed_falls_through_to_resume(self, client):
+        client.execute_hms_action("03008070", HMSAction.PROCEED, job_id="task-1")
+        cmds = self._published_commands(client)
+        assert cmds[0]["print"]["command"] == "resume"
+        assert cmds[0]["print"]["err"] == "03008070"
+
+    def test_stop_carries_err_and_job_id(self, client):
+        client.execute_hms_action("03008070", HMSAction.STOP_PRINTING, job_id="task-1")
+        cmds = self._published_commands(client)
+        assert cmds[0]["print"]["command"] == "stop"
+        assert cmds[0]["print"]["job_id"] == "task-1"
+
+    def test_ignore_resume_uses_idle_ignore_type_zero(self, client):
+        client.execute_hms_action("03008070", HMSAction.IGNORE_RESUME)
+        cmds = self._published_commands(client)
+        assert cmds[0] == {
+            "print": {
+                "command": "idle_ignore",
+                "err": "03008070",
+                "type": 0,
+                "sequence_id": "0",
+            }
+        }
+
+    def test_dont_remind_uses_idle_ignore_type_one(self, client):
+        # DONT_REMIND_NEXT_TIME and IGNORE_NO_REMINDER_NEXT_TIME are the
+        # persistent variants — Bambu hides the warning for future prints.
+        client.execute_hms_action("03008070", HMSAction.DONT_REMIND_NEXT_TIME)
+        cmds = self._published_commands(client)
+        assert cmds[0]["print"]["command"] == "idle_ignore"
+        assert cmds[0]["print"]["type"] == 1
+
+    def test_filament_extruded_sends_ams_done(self, client):
+        client.execute_hms_action("07008029", HMSAction.FILAMENT_EXTRUDED)
+        cmds = self._published_commands(client)
+        assert cmds[0] == {"print": {"command": "ams_control", "param": "done", "sequence_id": "0"}}
+
+    def test_retry_sends_ams_resume(self, client):
+        client.execute_hms_action("07008029", HMSAction.RETRY_FILAMENT_EXTRUDED)
+        cmds = self._published_commands(client)
+        assert cmds[0]["print"]["param"] == "resume"
+        assert cmds[0]["print"]["command"] == "ams_control"
+
+    def test_abort_sends_ams_abort(self, client):
+        client.execute_hms_action("07008029", HMSAction.ABORT)
+        cmds = self._published_commands(client)
+        assert cmds[0]["print"]["param"] == "abort"
+
+    def test_ok_button_sends_bare_clean_print_error(self, client):
+        # Matches the existing `clear_hms_errors` shape — no `print_error` body
+        # field, which the original PR mistakenly added.
+        client.execute_hms_action("03008070", HMSAction.OK_BUTTON)
+        cmds = self._published_commands(client)
+        assert cmds[0] == {"print": {"command": "clean_print_error", "sequence_id": "0"}}
+
+    def test_dbl_check_ok_sends_clean_then_uiop_close(self, client):
+        client.execute_hms_action("03008070", HMSAction.DBL_CHECK_OK)
+        cmds = self._published_commands(client)
+        assert len(cmds) == 2
+        assert cmds[0]["print"]["command"] == "clean_print_error"
+        assert cmds[1]["system"]["command"] == "uiop"
+        # `err` is the already-string short code, NOT `f"{x:08X}"` against a
+        # str (which would TypeError on the old code path).
+        assert cmds[1]["system"]["err"] == "03008070"
+
+    def test_uiop_close_uppercases_lowercase_input(self, client):
+        # Frontend may send the short code in either case; we normalise.
+        client.execute_hms_action("0300abcd", HMSAction.DBL_CHECK_OK)
+        cmds = self._published_commands(client)
+        assert cmds[1]["system"]["err"] == "0300ABCD"
+
+    def test_dbl_check_resume_is_plain_resume(self, client):
+        # No err/job_id — explicitly different from RESUME_PRINTING.
+        client.execute_hms_action("03008070", HMSAction.DBL_CHECK_RESUME)
+        cmds = self._published_commands(client)
+        assert cmds[0] == {"print": {"command": "resume", "param": "", "sequence_id": "0"}}
+        assert "err" not in cmds[0]["print"]
+
+    def test_refresh_nozzle(self, client):
+        client.execute_hms_action("03008070", HMSAction.REFRESH_NOZZLE)
+        cmds = self._published_commands(client)
+        assert cmds[0] == {"print": {"command": "refresh_nozzle", "sequence_id": "0"}}
+
+    def test_turn_off_fire_alarm_sends_buzzer_off(self, client):
+        client.execute_hms_action("03008044", HMSAction.TURN_OFF_FIRE_ALARM)
+        cmds = self._published_commands(client)
+        assert cmds[0]["print"]["command"] == "buzzer_ctrl"
+        assert cmds[0]["print"]["mode"] == 0
+
+    def test_stop_drying_sends_auto_stop_ams_dry(self, client):
+        client.execute_hms_action("07008017", HMSAction.STOP_DRYING)
+        cmds = self._published_commands(client)
+        assert cmds[0]["print"]["command"] == "auto_stop_ams_dry"
+
+    def test_disable_purification_sends_close_air_filt(self, client):
+        client.execute_hms_action("03008063", HMSAction.DISABLE_PURIFICATION)
+        cmds = self._published_commands(client)
+        assert cmds[0]["print"]["command"] == "close_air_filt"
+
+    @pytest.mark.parametrize(
+        "action",
+        [
+            HMSAction.CHECK_ASSISTANT,
+            HMSAction.JUMP_TO_LIVEVIEW,
+            HMSAction.OK_JUMP_RACK,
+            HMSAction.REMOVE_CLOSE_BTN,
+            HMSAction.LOAD_VIRTUAL_TRAY,
+            HMSAction.CANCLE,
+            HMSAction.DBL_CHECK_CANCEL,
+        ],
+    )
+    def test_ui_only_actions_publish_nothing(self, client, action):
+        # These actions exist for parity with BambuStudio's modal but have no
+        # MQTT counterpart — the printer's own screen drives them.
+        assert client.execute_hms_action("03008070", action) is True
+        assert self._published_commands(client) == []
+
+    def test_every_publish_is_followed_by_pushall(self, client):
+        # The dispatcher pairs every command with a `pushing.pushall` echo so
+        # the state stream refreshes on the next tick. Regression guard.
+        client.execute_hms_action("03008070", HMSAction.RESUME_PRINTING)
+        payloads = [json.loads(c.args[1]) for c in client._client.publish.call_args_list]
+        assert any("pushing" in p for p in payloads)

+ 276 - 0
backend/tests/unit/test_scheduler_cleanup_library.py

@@ -0,0 +1,276 @@
+from contextlib import ExitStack
+from pathlib import Path
+from types import SimpleNamespace
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
+
+import backend.app.models  # noqa: F401 - populate Base.metadata
+import backend.app.services.print_scheduler as scheduler_module
+from backend.app.core.database import Base
+from backend.app.models.archive import PrintArchive
+from backend.app.models.library import LibraryFile
+from backend.app.models.print_queue import PrintQueueItem
+from backend.app.models.printer import Printer
+from backend.app.services.print_scheduler import PrintScheduler
+
+
+@pytest.fixture
+async def queue_factory(tmp_path):
+    engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
+    async with engine.begin() as conn:
+        await conn.run_sync(Base.metadata.create_all)
+
+    session_maker = async_sessionmaker(engine, expire_on_commit=False)
+    case_counter = 0
+
+    async def make_case(*, cleanup=True, is_external=False, thumbnail_path=None):
+        nonlocal case_counter
+        case_counter += 1
+
+        base_dir = tmp_path / f"case-{case_counter}"
+        base_dir.mkdir()
+        source_path = base_dir / "library" / f"source-{case_counter}.3mf"
+        source_path.parent.mkdir()
+        source_path.write_bytes(b"library source")
+
+        thumbnail_actual_path = None
+        thumbnail_db_path = None
+        if thumbnail_path == "relative":
+            thumbnail_db_path = f"thumbs/preview-{case_counter}.png"
+            thumbnail_actual_path = base_dir / thumbnail_db_path
+        elif thumbnail_path == "absolute":
+            thumbnail_actual_path = tmp_path / f"absolute-preview-{case_counter}.png"
+            thumbnail_db_path = str(thumbnail_actual_path)
+        elif thumbnail_path is not None:
+            thumbnail_actual_path = Path(thumbnail_path)
+            thumbnail_db_path = str(thumbnail_path)
+
+        if thumbnail_actual_path:
+            thumbnail_actual_path.parent.mkdir(parents=True, exist_ok=True)
+            thumbnail_actual_path.write_bytes(b"thumbnail")
+
+        async with session_maker() as db:
+            printer = Printer(
+                name=f"Printer {case_counter}",
+                serial_number=f"SERIAL-{case_counter}",
+                ip_address="127.0.0.1",
+                access_code="access-code",
+                model="X1C",
+            )
+            library_file = LibraryFile(
+                filename=f"source-{case_counter}.3mf",
+                file_path=str(source_path),
+                file_type="3mf",
+                file_size=source_path.stat().st_size,
+                file_hash=None,
+                thumbnail_path=thumbnail_db_path,
+                file_metadata=None,
+                is_external=is_external,
+            )
+            db.add_all([printer, library_file])
+            await db.flush()
+
+            item = PrintQueueItem(
+                printer_id=printer.id,
+                library_file_id=library_file.id,
+                status="pending",
+                cleanup_library_after_dispatch=cleanup,
+                bed_levelling=True,
+                flow_cali=False,
+                vibration_cali=True,
+                layer_inspect=False,
+                timelapse=False,
+                use_ams=True,
+                nozzle_offset_cali=True,
+            )
+            db.add(item)
+            await db.commit()
+
+            return SimpleNamespace(
+                session_maker=session_maker,
+                base_dir=base_dir,
+                source_path=source_path,
+                thumbnail_path=thumbnail_actual_path,
+                printer_id=printer.id,
+                library_file_id=library_file.id,
+                queue_item_id=item.id,
+                archive_path=None,
+                upload=AsyncMock(return_value=True),
+                start_print=MagicMock(return_value=True),
+            )
+
+    try:
+        yield make_case
+    finally:
+        await engine.dispose()
+
+
+async def _dispatch_library_item(ctx, *, archive_failure=False, unlink_side_effect=None):
+    scheduler = PrintScheduler()
+
+    async def archive_print(self, *, printer_id, source_file, original_filename, created_by_id=None, project_id=None):
+        if archive_failure:
+            raise RuntimeError("archive copy failed")
+
+        archive_rel_path = Path("archives") / f"archive-{ctx.queue_item_id}.3mf"
+        ctx.archive_path = ctx.base_dir / archive_rel_path
+        ctx.archive_path.parent.mkdir(parents=True, exist_ok=True)
+        ctx.archive_path.write_bytes(Path(source_file).read_bytes())
+
+        archive = PrintArchive(
+            printer_id=printer_id,
+            filename=original_filename,
+            file_path=str(archive_rel_path),
+            file_size=ctx.archive_path.stat().st_size,
+            content_hash=None,
+            thumbnail_path=None,
+            timelapse_path=None,
+            print_time_seconds=120,
+            status="completed",
+            project_id=project_id,
+            created_by_id=created_by_id,
+        )
+        self.db.add(archive)
+        await self.db.flush()
+        return archive
+
+    patches = [
+        patch.object(scheduler_module.settings, "base_dir", ctx.base_dir),
+        patch("backend.app.services.archive.ArchiveService.archive_print", new=archive_print),
+        patch("backend.app.services.print_scheduler.printer_manager.is_connected", MagicMock(return_value=True)),
+        patch("backend.app.services.print_scheduler.printer_manager.get_status", MagicMock(return_value=None)),
+        patch("backend.app.services.print_scheduler.printer_manager.start_print", ctx.start_print),
+        patch("backend.app.services.print_scheduler.printer_manager.set_awaiting_plate_clear", MagicMock()),
+        patch(
+            "backend.app.services.print_scheduler.get_ftp_retry_settings", AsyncMock(return_value=(False, 0, 0, 1.0))
+        ),
+        patch("backend.app.services.print_scheduler.delete_file_async", AsyncMock(return_value=True)),
+        patch("backend.app.services.print_scheduler.upload_file_async", ctx.upload),
+        patch("backend.app.services.print_scheduler.cache_3mf_download", MagicMock()),
+        patch("backend.app.services.print_scheduler.spawn_background_task", MagicMock()),
+        patch("backend.app.services.notification_service.notification_service.on_queue_job_started", AsyncMock()),
+        patch("backend.app.services.notification_service.notification_service.on_queue_job_failed", AsyncMock()),
+        patch("backend.app.services.mqtt_relay.mqtt_relay.on_queue_job_started", AsyncMock()),
+        patch.object(scheduler, "_propagate_owner_to_printer_manager", AsyncMock()),
+        patch.object(scheduler, "_power_off_if_needed", AsyncMock()),
+    ]
+    if unlink_side_effect:
+        patches.append(patch.object(type(ctx.source_path), "unlink", unlink_side_effect))
+
+    with ExitStack() as stack:
+        for patcher in patches:
+            stack.enter_context(patcher)
+
+        async with ctx.session_maker() as db:
+            item = await db.get(PrintQueueItem, ctx.queue_item_id)
+            await scheduler._start_print(db, item)
+
+
+async def _queue_snapshot(ctx):
+    async with ctx.session_maker() as db:
+        item = await db.get(PrintQueueItem, ctx.queue_item_id)
+        library_file = await db.get(LibraryFile, ctx.library_file_id)
+        archive = await db.get(PrintArchive, item.archive_id) if item.archive_id else None
+        return item, library_file, archive
+
+
+@pytest.mark.asyncio
+async def test_cleanup_unlinks_library_file_and_removes_db_row(queue_factory):
+    ctx = await queue_factory(cleanup=True)
+
+    await _dispatch_library_item(ctx)
+
+    item, library_file, archive = await _queue_snapshot(ctx)
+    assert item.status == "printing"
+    assert item.library_file_id is None
+    assert item.archive_id == archive.id
+    assert library_file is None
+    assert not ctx.source_path.exists()
+
+
+@pytest.mark.asyncio
+async def test_external_library_file_skips_cleanup(queue_factory):
+    ctx = await queue_factory(cleanup=True, is_external=True)
+
+    await _dispatch_library_item(ctx)
+
+    item, library_file, archive = await _queue_snapshot(ctx)
+    assert item.status == "printing"
+    assert item.library_file_id == ctx.library_file_id
+    assert item.archive_id == archive.id
+    assert library_file is not None
+    assert ctx.source_path.exists()
+
+
+@pytest.mark.asyncio
+async def test_archive_creation_failure_skips_cleanup_and_dispatch(queue_factory):
+    ctx = await queue_factory(cleanup=True, thumbnail_path="relative")
+
+    await _dispatch_library_item(ctx, archive_failure=True)
+
+    item, library_file, archive = await _queue_snapshot(ctx)
+    assert item.status == "failed"
+    assert item.error_message == "Failed to create archive from library file"
+    assert item.archive_id is None
+    assert archive is None
+    assert library_file is not None
+    assert ctx.source_path.exists()
+    assert ctx.thumbnail_path.exists()
+    ctx.upload.assert_not_awaited()
+    ctx.start_print.assert_not_called()
+
+
+@pytest.mark.parametrize("thumbnail_path", ["absolute", "relative"])
+@pytest.mark.asyncio
+async def test_cleanup_resolves_absolute_and_relative_thumbnail_paths(queue_factory, thumbnail_path):
+    ctx = await queue_factory(cleanup=True, thumbnail_path=thumbnail_path)
+
+    await _dispatch_library_item(ctx)
+
+    item, library_file, archive = await _queue_snapshot(ctx)
+    assert item.status == "printing"
+    assert item.archive_id == archive.id
+    assert library_file is None
+    assert not ctx.source_path.exists()
+    assert not ctx.thumbnail_path.exists()
+
+
+@pytest.mark.asyncio
+async def test_archive_copy_survives_library_cleanup(queue_factory):
+    ctx = await queue_factory(cleanup=True)
+
+    await _dispatch_library_item(ctx)
+
+    assert not ctx.source_path.exists()
+    assert ctx.archive_path.exists()
+    assert ctx.archive_path.read_bytes() == b"library source"
+    uploaded_path = ctx.upload.await_args.args[2]
+    assert uploaded_path == ctx.archive_path
+
+
+@pytest.mark.asyncio
+async def test_oserror_during_unlink_logs_orphan_path_and_does_not_crash_dispatch(queue_factory, caplog):
+    ctx = await queue_factory(cleanup=True, thumbnail_path="relative")
+    original_unlink = type(ctx.source_path).unlink
+
+    def unlink_with_source_failure(path, *args, **kwargs):
+        if Path(path) == ctx.source_path:
+            raise OSError("permission denied")
+        return original_unlink(path, *args, **kwargs)
+
+    with caplog.at_level("WARNING", logger="backend.app.services.print_scheduler"):
+        await _dispatch_library_item(ctx, unlink_side_effect=unlink_with_source_failure)
+
+    item, library_file, archive = await _queue_snapshot(ctx)
+    assert item.status == "printing"
+    assert item.archive_id == archive.id
+    assert item.library_file_id is None
+    assert library_file is None
+    assert ctx.source_path.exists()
+    assert not ctx.thumbnail_path.exists()
+    assert ctx.archive_path.exists()
+    assert "TRANSIENT_LIBRARY_FILE_ORPHAN" in caplog.text
+    assert str(ctx.source_path) in caplog.text
+    assert "permission denied" in caplog.text

+ 2 - 0
frontend/scripts/check-i18n-parity.mjs

@@ -207,6 +207,7 @@ const FR_COGNATES = [
   'Cancelling upload...', 'Backup in progress...', 'Searching directory...',
   'EC984C,#6CD4BC,A66EB9,D87694',
   'Proxy', 'Navigation', 'Budget', 'Commit', 'Designer',
+  'Compact',  // cam-wall status overlay mode — same word in French
   'ntfy, Pushover, Discord, etc.',
   '{{filament}} @ {{temp}}°C',  // drying badge: filament code + universal °C
 ];
@@ -237,6 +238,7 @@ const IT_COGNATES = [
   'Hex: #{{hex}}',
   'EC984C,#6CD4BC,A66EB9,D87694',
   'Proxy', 'Designer',
+  'Off',  // cam-wall status overlay mode — common loanword in Italian UI
   '{{filament}} @ {{temp}}°C',  // drying badge: filament code + universal °C
 ];
 

+ 187 - 123
frontend/src/__tests__/components/PrintModal.test.tsx

@@ -1,9 +1,8 @@
 /**
  * Tests for the unified PrintModal component.
  *
- * The PrintModal supports three modes:
- * - 'reprint': Immediate print from archive (multi-printer support)
- * - 'add-to-queue': Schedule print to queue (multi-printer support)
+ * The PrintModal supports two modes:
+ * - 'create': Create a print queue item
  * - 'edit-queue-item': Edit existing queue item (single printer)
  */
 
@@ -73,9 +72,6 @@ describe('PrintModal', () => {
       http.get('/api/v1/printers/:id/status', () => {
         return HttpResponse.json({ connected: true, state: 'IDLE', ams: [], vt_tray: [] });
       }),
-      http.post('/api/v1/archives/:id/reprint', () => {
-        return HttpResponse.json({ success: true });
-      }),
       http.post('/api/v1/queue/', () => {
         return HttpResponse.json({ id: 1, status: 'pending' });
       }),
@@ -85,25 +81,26 @@ describe('PrintModal', () => {
     );
   });
 
-  describe('reprint mode', () => {
+  describe('create mode', () => {
     it('renders the modal title', () => {
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
+          initialSelectedPrinterIds={[1]}
           onClose={mockOnClose}
           onSuccess={mockOnSuccess}
         />
       );
 
-      expect(screen.getByText('Re-print')).toBeInTheDocument();
+      expect(screen.getByRole('heading', { name: 'Print' })).toBeInTheDocument();
     });
 
     it('shows archive name', () => {
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -117,7 +114,7 @@ describe('PrintModal', () => {
     it('shows printer selection with checkboxes for multi-select', async () => {
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -134,7 +131,7 @@ describe('PrintModal', () => {
     it('has print button', () => {
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -150,7 +147,7 @@ describe('PrintModal', () => {
     it('has cancel button', () => {
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -165,7 +162,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -181,7 +178,7 @@ describe('PrintModal', () => {
     it('print button is disabled until printer is selected', () => {
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -203,7 +200,7 @@ describe('PrintModal', () => {
 
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -216,39 +213,42 @@ describe('PrintModal', () => {
       });
     });
 
-    it('shows print options toggle', () => {
+    it('shows print options toggle', async () => {
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
+          initialSelectedPrinterIds={[1]}
           onClose={mockOnClose}
           onSuccess={mockOnSuccess}
         />
       );
 
-      expect(screen.getByText('Print Options')).toBeInTheDocument();
+      await waitFor(() => {
+        expect(screen.getByText('Print Options')).toBeInTheDocument();
+      });
     });
   });
 
-  describe('add-to-queue mode', () => {
+  describe('create mode', () => {
     it('renders the modal title', () => {
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
         />
       );
 
-      expect(screen.getByText('Schedule Print')).toBeInTheDocument();
+      expect(screen.getByRole('heading', { name: 'Print' })).toBeInTheDocument();
     });
 
     it('shows archive name', () => {
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
@@ -261,20 +261,20 @@ describe('PrintModal', () => {
     it('shows add button', () => {
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
         />
       );
 
-      expect(screen.getByRole('button', { name: /add to queue/i })).toBeInTheDocument();
+      expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument();
     });
 
     it('shows cancel button', () => {
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
@@ -284,23 +284,23 @@ describe('PrintModal', () => {
       expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
     });
 
-    it('shows Queue Only option', () => {
+    it('shows Queue option', () => {
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
         />
       );
 
-      expect(screen.getByText('Queue Only')).toBeInTheDocument();
+      expect(screen.getByRole('button', { name: /queue/i })).toBeInTheDocument();
     });
 
     it('shows power off option', () => {
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
@@ -313,22 +313,37 @@ describe('PrintModal', () => {
     it('shows schedule options', () => {
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
+          archiveId={1}
+          archiveName="Test Print"
+          onClose={mockOnClose}
+        />
+      );
+
+      expect(screen.getByRole('button', { name: /asap/i })).toBeInTheDocument();
+      expect(screen.getByRole('button', { name: /queue/i })).toBeInTheDocument();
+      expect(screen.getByRole('button', { name: /schedule/i })).toBeInTheDocument();
+    });
+
+    it('orders schedule options by time', () => {
+      render(
+        <PrintModal
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
         />
       );
 
-      expect(screen.getByText('ASAP')).toBeInTheDocument();
-      expect(screen.getByText('Scheduled')).toBeInTheDocument();
+      const options = screen.getAllByRole('button', { name: /^(asap|queue|schedule)$/i });
+      expect(options.map(button => button.textContent?.trim())).toEqual(['ASAP', 'Queue', 'Schedule']);
     });
 
     it('calls onClose when cancel is clicked', async () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
@@ -406,7 +421,7 @@ describe('PrintModal', () => {
       expect(screen.getByText('Print Options')).toBeInTheDocument();
     });
 
-    it('shows Queue Only option', () => {
+    it('shows Queue option', () => {
       const item = createMockQueueItem();
 
       render(
@@ -419,7 +434,7 @@ describe('PrintModal', () => {
         />
       );
 
-      expect(screen.getByText('Queue Only')).toBeInTheDocument();
+      expect(screen.getByRole('button', { name: /queue/i })).toBeInTheDocument();
     });
 
     it('shows power off option', () => {
@@ -482,7 +497,7 @@ describe('PrintModal', () => {
     it('shows select all button when multiple printers available', async () => {
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -498,7 +513,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -520,7 +535,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -534,7 +549,7 @@ describe('PrintModal', () => {
       await user.click(screen.getByText('Select all'));
 
       await waitFor(() => {
-        expect(screen.getByRole('button', { name: /print to 3 printers/i })).toBeInTheDocument();
+        expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument();
       });
     });
   });
@@ -566,10 +581,10 @@ describe('PrintModal', () => {
       );
     });
 
-    it('shows state badges on printers in reprint mode', async () => {
+    it('shows state badges on printers in create mode', async () => {
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -583,11 +598,11 @@ describe('PrintModal', () => {
       });
     });
 
-    it('prevents selecting a busy printer in reprint mode', async () => {
+    it('allows selecting a busy printer in create mode', async () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -598,28 +613,20 @@ describe('PrintModal', () => {
         expect(screen.getByText('Printing')).toBeInTheDocument();
       });
 
-      // The busy printer button should be disabled
       const busyButton = screen.getByText('X1 Carbon').closest('button');
-      expect(busyButton).toBeDisabled();
-
-      // Click the busy printer — selection should not change
+      expect(busyButton).not.toBeDisabled();
       await user.click(busyButton!);
 
-      // Idle printer should still be selectable
-      const idleButton = screen.getByText('P1S').closest('button');
-      expect(idleButton).not.toBeDisabled();
-      await user.click(idleButton!);
-
       await waitFor(() => {
         expect(screen.getByText('1 printer selected')).toBeInTheDocument();
       });
     });
 
-    it('select all skips busy printers in reprint mode', async () => {
+    it('select all includes busy printers in create mode', async () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -634,16 +641,15 @@ describe('PrintModal', () => {
       await user.click(screen.getByText('Select all'));
 
       await waitFor(() => {
-        // Only 2 available printers selected (IDLE + FINISH), not the RUNNING one
-        expect(screen.getByText(/2 printers selected/)).toBeInTheDocument();
+        expect(screen.getByText(/3 printers selected/)).toBeInTheDocument();
       });
     });
 
-    it('allows selecting busy printers in add-to-queue mode', async () => {
+    it('allows selecting busy printers in create mode', async () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -677,7 +683,7 @@ describe('PrintModal', () => {
 
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -702,7 +708,7 @@ describe('PrintModal', () => {
 
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -721,7 +727,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
@@ -742,7 +748,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
@@ -760,11 +766,11 @@ describe('PrintModal', () => {
       });
     });
 
-    it('shows stagger option in reprint mode with multiple printers', async () => {
+    it('shows stagger option in create mode with multiple printers', async () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
@@ -784,11 +790,11 @@ describe('PrintModal', () => {
       expect(screen.getByText('Stagger printer starts')).toBeInTheDocument();
     });
 
-    it('shows stagger preview in reprint mode when enabled', async () => {
+    it('shows stagger preview in create mode when enabled', async () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
@@ -813,11 +819,11 @@ describe('PrintModal', () => {
       });
     });
 
-    it('does not show stagger option in reprint mode with single printer', async () => {
+    it('does not show stagger option in create mode with single printer', async () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
@@ -838,7 +844,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
@@ -867,7 +873,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Test Print"
           onClose={mockOnClose}
@@ -911,10 +917,10 @@ describe('PrintModal', () => {
       );
     });
 
-    it('shows "Select All" button only in add-to-queue mode', async () => {
+    it('shows "Select All" button only in create mode', async () => {
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="MultiPlate.3mf"
           onClose={mockOnClose}
@@ -926,10 +932,10 @@ describe('PrintModal', () => {
       });
     });
 
-    it('does not show "Select All" button in reprint mode', async () => {
+    it('shows "Select All" button in create mode', async () => {
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="MultiPlate.3mf"
           initialSelectedPrinterIds={[1]}
@@ -940,14 +946,14 @@ describe('PrintModal', () => {
       await waitFor(() => {
         expect(screen.getByText('Plate 1')).toBeInTheDocument();
       });
-      expect(screen.queryByText('Select All 3 Plates')).not.toBeInTheDocument();
+      expect(screen.getByText('Select All 3 Plates')).toBeInTheDocument();
     });
 
     it('selects all plates when "Select All" is clicked', async () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="MultiPlate.3mf"
           onClose={mockOnClose}
@@ -981,7 +987,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="MultiPlate.3mf"
           onClose={mockOnClose}
@@ -998,7 +1004,7 @@ describe('PrintModal', () => {
       // Select printer
       await user.click(screen.getByText('X1 Carbon'));
 
-      // Plate 1 is auto-selected. Click Plate 3 to add it (multi-select in add-to-queue mode)
+      // Plate 1 is auto-selected. Click Plate 3 to add it (multi-select in create mode)
       await user.click(screen.getByText('Plate 3'));
 
       // Submit — should queue plates 1 and 3
@@ -1026,7 +1032,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="MultiPlate.3mf"
           onClose={mockOnClose}
@@ -1062,10 +1068,10 @@ describe('PrintModal', () => {
   });
 
   describe('batch quantity', () => {
-    it('shows quantity input in reprint mode', () => {
+    it('shows quantity input in create mode', () => {
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -1076,10 +1082,10 @@ describe('PrintModal', () => {
       expect(screen.getByLabelText('Quantity')).toBeInTheDocument();
     });
 
-    it('shows quantity input in add-to-queue mode', () => {
+    it('shows quantity input in create mode', () => {
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -1108,7 +1114,7 @@ describe('PrintModal', () => {
     it('defaults quantity to 1', () => {
       render(
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           onClose={mockOnClose}
@@ -1124,7 +1130,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           initialSelectedPrinterIds={[1]}
@@ -1143,10 +1149,10 @@ describe('PrintModal', () => {
   });
 
   describe('reprint G-code injection dispatch (#422 / auto-eject)', () => {
-    // Guards the fix: when "Inject auto-print G-code" is ticked on a reprint with
+    // Guards the fix: when "Inject auto-print G-code" is ticked on a create with
     // quantity > 1, ALL copies must go through the queue so every one is injected by
     // the scheduler. The first copy must NOT be dispatched immediately via the direct
-    // reprint path — that path bypasses injection and would leave the first copy stuck
+    // create path — that path bypasses injection and would leave the first copy stuck
     // on the plate for auto-eject setups.
     const withSnippets = () =>
       http.get('/api/v1/settings/', () =>
@@ -1154,14 +1160,9 @@ describe('PrintModal', () => {
       );
 
     it('injection ON queues all copies and dispatches none immediately', async () => {
-      const reprintCalls: unknown[] = [];
       const queueCalls: Record<string, unknown>[] = [];
       server.use(
         withSnippets(),
-        http.post('/api/v1/archives/:id/reprint', async ({ request }) => {
-          reprintCalls.push(await request.json().catch(() => ({})));
-          return HttpResponse.json({ status: 'dispatched' });
-        }),
         http.post('/api/v1/queue/', async ({ request }) => {
           queueCalls.push((await request.json()) as Record<string, unknown>);
           return HttpResponse.json({ id: queueCalls.length, status: 'pending' });
@@ -1171,7 +1172,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           initialSelectedPrinterIds={[1]}
@@ -1194,18 +1195,12 @@ describe('PrintModal', () => {
       await waitFor(() => expect(queueCalls.length).toBe(1));
       // One queue item carrying all copies, and zero immediate reprint dispatches
       expect(queueCalls[0].quantity).toBe(3);
-      expect(reprintCalls.length).toBe(0);
     });
 
-    it('injection OFF keeps the immediate first copy and queues the rest', async () => {
-      const reprintCalls: unknown[] = [];
+    it('injection OFF queues all copies through the scheduler path', async () => {
       const queueCalls: Record<string, unknown>[] = [];
       server.use(
         withSnippets(),
-        http.post('/api/v1/archives/:id/reprint', async ({ request }) => {
-          reprintCalls.push(await request.json().catch(() => ({})));
-          return HttpResponse.json({ status: 'dispatched' });
-        }),
         http.post('/api/v1/queue/', async ({ request }) => {
           queueCalls.push((await request.json()) as Record<string, unknown>);
           return HttpResponse.json({ id: queueCalls.length, status: 'pending' });
@@ -1215,7 +1210,7 @@ describe('PrintModal', () => {
       const user = userEvent.setup();
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           initialSelectedPrinterIds={[1]}
@@ -1229,12 +1224,11 @@ describe('PrintModal', () => {
       await user.keyboard('3');
       expect(qty.value).toBe('3');
 
-      // Leave injection unticked → first copy prints immediately, rest queue
+      // Leave injection unticked: unified dispatch still queues all copies.
       await user.click(document.querySelector('button[type="submit"]') as HTMLElement);
 
-      await waitFor(() => expect(reprintCalls.length).toBe(1));
-      expect(queueCalls.length).toBe(1);
-      expect(queueCalls[0].quantity).toBe(2);
+      await waitFor(() => expect(queueCalls.length).toBe(1));
+      expect(queueCalls[0].quantity).toBe(3);
     });
   });
 
@@ -1269,19 +1263,19 @@ describe('PrintModal', () => {
       );
     });
 
-    it('includes project_id in printLibraryFile call when projectId prop is set', async () => {
+    it('includes project_id in queue item when printing a library file with projectId set', async () => {
       let capturedBody: Record<string, unknown> | null = null;
       server.use(
-        http.post('/api/v1/library/files/:id/print', async ({ request }) => {
+        http.post('/api/v1/queue/', async ({ request }) => {
           capturedBody = await request.json() as Record<string, unknown>;
-          return HttpResponse.json({ status: 'dispatched', dispatch_job_id: 'abc', dispatch_position: 0 });
+          return HttpResponse.json({ id: 1, status: 'pending' });
         })
       );
       const user = userEvent.setup();
 
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           libraryFileId={5}
           archiveName="Benchy"
           projectId={42}
@@ -1300,25 +1294,24 @@ describe('PrintModal', () => {
 
       await waitFor(() => {
         expect(capturedBody).not.toBeNull();
+        expect(capturedBody?.library_file_id).toBe(5);
         expect(capturedBody?.project_id).toBe(42);
       });
     });
 
-    it('does NOT include project_id in reprintArchive call (archives carry their own project association)', async () => {
-      // The reprintArchive branch omits project_id by design — archives already carry
-      // their project association from the original print. This test guards that intent.
+    it('queues archive prints through the scheduler path', async () => {
       let capturedBody: Record<string, unknown> | null = null;
       server.use(
-        http.post('/api/v1/archives/:id/reprint', async ({ request }) => {
+        http.post('/api/v1/queue/', async ({ request }) => {
           capturedBody = await request.json() as Record<string, unknown>;
-          return HttpResponse.json({ status: 'dispatched' });
+          return HttpResponse.json({ id: 1, status: 'pending' });
         })
       );
       const user = userEvent.setup();
 
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={1}
           archiveName="Benchy"
           projectId={42}
@@ -1336,7 +1329,78 @@ describe('PrintModal', () => {
 
       await waitFor(() => {
         expect(capturedBody).not.toBeNull();
-        expect(capturedBody).not.toHaveProperty('project_id');
+        expect(capturedBody?.archive_id).toBe(1);
+        expect(capturedBody?.project_id).toBe(42);
+      });
+    });
+
+    it('adds ASAP prints to the top of the queue', async () => {
+      let capturedBody: Record<string, unknown> | null = null;
+      server.use(
+        http.post('/api/v1/queue/', async ({ request }) => {
+          capturedBody = await request.json() as Record<string, unknown>;
+          return HttpResponse.json({ id: 1, status: 'pending' });
+        })
+      );
+      const user = userEvent.setup();
+
+      render(
+        <PrintModal
+          mode="create"
+          archiveId={1}
+          archiveName="Benchy"
+          initialSelectedPrinterIds={[1]}
+          onClose={mockOnClose}
+          onSuccess={mockOnSuccess}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument();
+      });
+
+      await user.click(screen.getByRole('button', { name: /^print$/i }));
+
+      await waitFor(() => {
+        expect(capturedBody).not.toBeNull();
+        expect(capturedBody?.insert_at_top).toBe(true);
+        expect(capturedBody?.insert_position).toBe(1);
+        expect(capturedBody?.manual_start).toBe(false);
+        expect(capturedBody?.scheduled_time).toBeUndefined();
+      });
+    });
+
+    it('adds Queue prints to the back unless manual start is required', async () => {
+      let capturedBody: Record<string, unknown> | null = null;
+      server.use(
+        http.post('/api/v1/queue/', async ({ request }) => {
+          capturedBody = await request.json() as Record<string, unknown>;
+          return HttpResponse.json({ id: 1, status: 'pending' });
+        })
+      );
+      const user = userEvent.setup();
+
+      render(
+        <PrintModal
+          mode="create"
+          archiveId={1}
+          archiveName="Benchy"
+          initialSelectedPrinterIds={[1]}
+          onClose={mockOnClose}
+          onSuccess={mockOnSuccess}
+        />
+      );
+
+      await user.click(screen.getByRole('button', { name: /^queue$/i }));
+      expect(screen.getByLabelText(/require manual start/i)).toBeInTheDocument();
+      await user.click(screen.getByLabelText(/require manual start/i));
+      await user.click(screen.getByRole('button', { name: /^print$/i }));
+
+      await waitFor(() => {
+        expect(capturedBody).not.toBeNull();
+        expect(capturedBody?.insert_at_top).toBeUndefined();
+        expect(capturedBody?.insert_position).toBeUndefined();
+        expect(capturedBody?.manual_start).toBe(true);
       });
     });
   });
@@ -1377,16 +1441,16 @@ describe('PrintModal', () => {
     it('forwards cleanup_library_after_dispatch=true when the Direct-Print prop is set', async () => {
       let capturedBody: Record<string, unknown> | null = null;
       server.use(
-        http.post('/api/v1/library/files/:id/print', async ({ request }) => {
+        http.post('/api/v1/queue/', async ({ request }) => {
           capturedBody = (await request.json()) as Record<string, unknown>;
-          return HttpResponse.json({ status: 'dispatched', dispatch_job_id: 'abc', dispatch_position: 0 });
+          return HttpResponse.json({ id: 1, status: 'pending' });
         })
       );
       const user = userEvent.setup();
 
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           libraryFileId={5}
           archiveName="Benchy"
           cleanupLibraryAfterDispatch
@@ -1411,16 +1475,16 @@ describe('PrintModal', () => {
     it('defaults to omitting cleanup_library_after_dispatch (File Manager / Project flows survive)', async () => {
       let capturedBody: Record<string, unknown> | null = null;
       server.use(
-        http.post('/api/v1/library/files/:id/print', async ({ request }) => {
+        http.post('/api/v1/queue/', async ({ request }) => {
           capturedBody = (await request.json()) as Record<string, unknown>;
-          return HttpResponse.json({ status: 'dispatched', dispatch_job_id: 'abc', dispatch_position: 0 });
+          return HttpResponse.json({ id: 1, status: 'pending' });
         })
       );
       const user = userEvent.setup();
 
       render(
         <PrintModal
-          mode="reprint"
+          mode="create"
           libraryFileId={5}
           archiveName="Benchy"
           initialSelectedPrinterIds={[1]}

+ 115 - 8
frontend/src/__tests__/components/PrintModalDispatchToast.test.tsx

@@ -1,6 +1,5 @@
 /**
- * Test that reprint mode does not show the "Print queued for printer" toast.
- * The background dispatch websocket toast handles feedback instead.
+ * Test that create mode now goes through the queue-backed create path.
  *
  * Separate file because vi.mock(ToastContext) must be module-scoped
  * and would interfere with the main PrintModal test suite.
@@ -48,17 +47,17 @@ describe('PrintModal dispatch toast', () => {
       http.get('/api/v1/printers/:id/status', () => {
         return HttpResponse.json({ connected: true, state: 'IDLE', ams: [], vt_tray: [] });
       }),
-      http.post('/api/v1/archives/:id/reprint', () => {
-        return HttpResponse.json({ status: 'dispatched', dispatch_job_id: 1 });
+      http.post('/api/v1/queue/', () => {
+        return HttpResponse.json({ id: 1, status: 'pending' });
       }),
     );
   });
 
-  it('does not show "queued" toast in reprint mode (dispatch toast handles it)', async () => {
+  it('shows queued toast in create mode', async () => {
     const user = userEvent.setup();
     render(
       <PrintModal
-        mode="reprint"
+        mode="create"
         archiveId={1}
         archiveName="Benchy"
         onClose={mockOnClose}
@@ -81,8 +80,116 @@ describe('PrintModal dispatch toast', () => {
       expect(mockOnClose).toHaveBeenCalled();
     });
 
-    // showToast should NOT have been called with "Print queued for printer"
     const toastMessages = mockShowToast.mock.calls.map(call => call[0]);
-    expect(toastMessages).not.toContain('Print queued for printer');
+    expect(toastMessages).toContain('Print queued');
+  });
+
+  it('uses wait-for-idle copy when an ASAP target is offline', async () => {
+    server.use(
+      http.get('/api/v1/printers/:id/status', () => {
+        return HttpResponse.json({ connected: false, state: null, ams: [], vt_tray: [] });
+      }),
+    );
+
+    const user = userEvent.setup();
+    render(
+      <PrintModal
+        mode="create"
+        archiveId={1}
+        archiveName="Benchy"
+        initialSelectedPrinterIds={[1]}
+        onClose={mockOnClose}
+        onSuccess={mockOnSuccess}
+      />
+    );
+
+    await waitFor(() => {
+      expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument();
+    });
+    await user.click(screen.getByRole('button', { name: /^print$/i }));
+
+    await waitFor(() => {
+      expect(mockOnClose).toHaveBeenCalled();
+    });
+
+    const toastMessages = mockShowToast.mock.calls.map(call => call[0]);
+    expect(toastMessages).toContain('Will start when printer is idle');
+    expect(toastMessages).not.toContain('Print queued');
+  });
+
+  it('uses wait-for-idle copy when an ASAP target is held for plate clear', async () => {
+    server.use(
+      http.get('/api/v1/printers/:id/status', () => {
+        return HttpResponse.json({
+          connected: true,
+          state: 'FINISH',
+          awaiting_plate_clear: true,
+          ams: [],
+          vt_tray: [],
+        });
+      }),
+    );
+
+    const user = userEvent.setup();
+    render(
+      <PrintModal
+        mode="create"
+        archiveId={1}
+        archiveName="Benchy"
+        initialSelectedPrinterIds={[1]}
+        onClose={mockOnClose}
+        onSuccess={mockOnSuccess}
+      />
+    );
+
+    await waitFor(() => {
+      expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument();
+    });
+    await user.click(screen.getByRole('button', { name: /^print$/i }));
+
+    await waitFor(() => {
+      expect(mockOnClose).toHaveBeenCalled();
+    });
+
+    const toastMessages = mockShowToast.mock.calls.map(call => call[0]);
+    expect(toastMessages).toContain('Will start when printer is idle');
+  });
+
+  it('uses wait-for-idle copy when an ASAP target is drying filament', async () => {
+    server.use(
+      http.get('/api/v1/printers/:id/status', () => {
+        return HttpResponse.json({
+          connected: true,
+          state: 'IDLE',
+          awaiting_plate_clear: false,
+          ams: [{ id: 0, dry_time: 25, tray: [] }],
+          vt_tray: [],
+        });
+      }),
+    );
+
+    const user = userEvent.setup();
+    render(
+      <PrintModal
+        mode="create"
+        archiveId={1}
+        archiveName="Benchy"
+        initialSelectedPrinterIds={[1]}
+        onClose={mockOnClose}
+        onSuccess={mockOnSuccess}
+      />
+    );
+
+    await waitFor(() => {
+      expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument();
+    });
+    await user.click(screen.getByRole('button', { name: /^print$/i }));
+
+    await waitFor(() => {
+      expect(mockOnClose).toHaveBeenCalled();
+    });
+
+    const toastMessages = mockShowToast.mock.calls.map(call => call[0]);
+    expect(toastMessages).toContain('Will start when printer is idle');
   });
 });

+ 2 - 82
frontend/src/__tests__/contexts/ToastContext.test.tsx

@@ -95,90 +95,10 @@ describe('ToastContext post-unmount safety', () => {
   });
 });
 
-describe('ToastContext background dispatch — upload-done UX', () => {
-  // Small fast files reach 100% upload before the printer's MQTT confirmation
-  // arrives, leaving the bar parked at 100% for what feels like "stuck". When
-  // status is still 'processing' but uploadProgressPct >= 99.9 the byte-count
-  // line should switch to "Awaiting printer..." and the bar gets a pulse.
-  function dispatchBackgroundEvent(detail: Record<string, unknown>) {
-    window.dispatchEvent(new CustomEvent('background-dispatch', { detail }));
-  }
-
-  it('shows "Awaiting printer..." once upload is complete but printer has not confirmed', () => {
-    const { container } = render(
-      <ToastProvider>
-        <div />
-      </ToastProvider>
-    );
-
-    act(() => {
-      dispatchBackgroundEvent({
-        total: 1,
-        dispatched: 0,
-        processing: 1,
-        completed: 0,
-        failed: 0,
-        active_jobs: [
-          {
-            job_id: 42,
-            printer_name: 'X1C-2',
-            source_name: 'Benchy.3mf',
-            upload_bytes: 102400,
-            upload_total_bytes: 102400,
-            upload_progress_pct: 100.0,
-          },
-        ],
-      });
-    });
-
-    // The byte-count line should be replaced with the awaiting-printer text.
-    expect(container.textContent).toContain('Awaiting printer');
-    // And the original bytes-progressed format must not be visible at the
-    // same time — that is the "stuck at 100%" symptom we are fixing.
-    expect(container.textContent).not.toContain('100.0%');
-
-    // Bar gets the pulse class when in this state.
-    const bar = container.querySelector('.animate-pulse');
-    expect(bar).not.toBeNull();
-  });
-
-  it('still shows the byte/percent counter while upload is mid-flight', () => {
-    const { container } = render(
-      <ToastProvider>
-        <div />
-      </ToastProvider>
-    );
-
-    act(() => {
-      dispatchBackgroundEvent({
-        total: 1,
-        dispatched: 0,
-        processing: 1,
-        completed: 0,
-        failed: 0,
-        active_jobs: [
-          {
-            job_id: 7,
-            printer_name: 'X1C-2',
-            source_name: 'Benchy.3mf',
-            upload_bytes: 51200,
-            upload_total_bytes: 102400,
-            upload_progress_pct: 50.0,
-          },
-        ],
-      });
-    });
-
-    expect(container.textContent).toContain('50.0%');
-    expect(container.textContent).not.toContain('Awaiting printer');
-    expect(container.querySelector('.animate-pulse')).toBeNull();
-  });
-});
-
 describe('ToastContext viewport suppression', () => {
   // The kiosk layout flips setViewportSuppressed(true) on mount so the
-  // SpoolBuddy display stays free of main-app toasts (background dispatch
-  // progress, login flows, etc.). Verify the gate hides the visible viewport
+  // SpoolBuddy display stays free of main-app toasts (login flows, etc.).
+  // Verify the gate hides the visible viewport
   // without affecting the underlying state machine.
   function ViewportProbe() {
     const { showToast, setViewportSuppressed } = useToast();

+ 0 - 105
frontend/src/__tests__/hooks/useDispatchedPrinterIds.test.ts

@@ -1,105 +0,0 @@
-/**
- * Tests for useDispatchedPrinterIds — the hook that exposes printer IDs with
- * a queued/active background-dispatch job so PrinterSelector can grey them
- * out between dispatch-accepted and the printer's PRINT_START report.
- */
-
-import { describe, it, expect, beforeEach, afterEach } from 'vitest';
-import { renderHook, act } from '@testing-library/react';
-import {
-  useDispatchedPrinterIds,
-  __resetDispatchedPrinterIdsForTests,
-} from '../../hooks/useDispatchedPrinterIds';
-
-function fire(detail: Record<string, unknown>) {
-  act(() => {
-    window.dispatchEvent(new CustomEvent('background-dispatch', { detail }));
-  });
-}
-
-describe('useDispatchedPrinterIds', () => {
-  beforeEach(() => {
-    __resetDispatchedPrinterIdsForTests();
-  });
-
-  afterEach(() => {
-    __resetDispatchedPrinterIdsForTests();
-  });
-
-  it('returns an empty set initially', () => {
-    const { result } = renderHook(() => useDispatchedPrinterIds());
-    expect(result.current.size).toBe(0);
-  });
-
-  it('picks up printer IDs from dispatched_jobs', () => {
-    const { result } = renderHook(() => useDispatchedPrinterIds());
-    fire({
-      dispatched_jobs: [
-        { job_id: 1, printer_id: 42, printer_name: 'Farm-A' },
-      ],
-      active_jobs: [],
-    });
-    expect(result.current.has(42)).toBe(true);
-    expect(result.current.size).toBe(1);
-  });
-
-  it('picks up printer IDs from active_jobs', () => {
-    const { result } = renderHook(() => useDispatchedPrinterIds());
-    fire({
-      dispatched_jobs: [],
-      active_jobs: [
-        { job_id: 1, printer_id: 7, printer_name: 'Farm-B' },
-      ],
-    });
-    expect(result.current.has(7)).toBe(true);
-  });
-
-  it('unions both lists', () => {
-    const { result } = renderHook(() => useDispatchedPrinterIds());
-    fire({
-      dispatched_jobs: [{ job_id: 1, printer_id: 1 }],
-      active_jobs: [{ job_id: 2, printer_id: 2 }],
-    });
-    expect(result.current.size).toBe(2);
-    expect(result.current.has(1)).toBe(true);
-    expect(result.current.has(2)).toBe(true);
-  });
-
-  it('clears printers when subsequent event reports no jobs', () => {
-    const { result } = renderHook(() => useDispatchedPrinterIds());
-    fire({ dispatched_jobs: [{ job_id: 1, printer_id: 9 }], active_jobs: [] });
-    expect(result.current.has(9)).toBe(true);
-    fire({ dispatched_jobs: [], active_jobs: [] });
-    expect(result.current.size).toBe(0);
-  });
-
-  it('ignores jobs without a numeric printer_id', () => {
-    const { result } = renderHook(() => useDispatchedPrinterIds());
-    fire({
-      dispatched_jobs: [
-        { job_id: 1, printer_id: 'not-a-number' },
-        { job_id: 2 },
-        { job_id: 3, printer_id: 5 },
-      ],
-      active_jobs: [],
-    });
-    expect(result.current.size).toBe(1);
-    expect(result.current.has(5)).toBe(true);
-  });
-
-  it('keeps snapshot reference stable when content is unchanged', () => {
-    const { result } = renderHook(() => useDispatchedPrinterIds());
-    fire({ dispatched_jobs: [{ printer_id: 1 }], active_jobs: [] });
-    const first = result.current;
-    fire({ dispatched_jobs: [{ printer_id: 1 }], active_jobs: [] });
-    expect(result.current).toBe(first);
-  });
-
-  it('shares state across hook instances', () => {
-    const a = renderHook(() => useDispatchedPrinterIds());
-    const b = renderHook(() => useDispatchedPrinterIds());
-    fire({ dispatched_jobs: [{ printer_id: 11 }], active_jobs: [] });
-    expect(a.result.current.has(11)).toBe(true);
-    expect(b.result.current.has(11)).toBe(true);
-  });
-});

+ 12 - 6
frontend/src/__tests__/pages/FileManagerPage.test.tsx

@@ -473,8 +473,15 @@ describe('FileManagerPage', () => {
     });
   });
 
-  describe('schedule print', () => {
-    it('shows schedule print button when one sliced file is selected', async () => {
+  describe('bulk-action print button', () => {
+    // PR #1625 consolidated print actions: the old single-file-selected
+    // "Schedule" button now opens the unified PrintModal (which carries
+    // schedule options inside). The bulk-action toolbar shows a single
+    // "Print" button only when exactly one sliced file is selected, and
+    // hides it for multi-selection. The button is targeted by its accessible
+    // name ("Print") + role to disambiguate from the file-card dropdown's
+    // own Print entry, which stays collapsed unless its kebab is opened.
+    it('shows a Print button in the bulk toolbar when one sliced file is selected', async () => {
       const user = userEvent.setup();
       render(<FileManagerPage />);
 
@@ -489,11 +496,11 @@ describe('FileManagerPage', () => {
       }
 
       await waitFor(() => {
-        expect(screen.getByText(/Schedule/)).toBeInTheDocument();
+        expect(screen.getByRole('button', { name: /^Print$/ })).toBeInTheDocument();
       });
     });
 
-    it('hides schedule print button when multiple files are selected', async () => {
+    it('hides the bulk Print button when multiple files are selected', async () => {
       const user = userEvent.setup();
       render(<FileManagerPage />);
 
@@ -505,8 +512,7 @@ describe('FileManagerPage', () => {
       await user.click(screen.getByText('Select All'));
 
       await waitFor(() => {
-        // Schedule button should not be present when multiple files are selected
-        expect(screen.queryByText(/Schedule/)).not.toBeInTheDocument();
+        expect(screen.queryByRole('button', { name: /^Print$/ })).not.toBeInTheDocument();
       });
     });
   });

+ 6 - 6
frontend/src/__tests__/pages/ProjectDetailPage.test.tsx

@@ -101,7 +101,7 @@ describe('ProjectDetailPage', () => {
       render(<ProjectDetailPage />);
 
       await waitFor(() => {
-        expect(screen.getByTitle('Print Now')).toBeInTheDocument();
+        expect(screen.getByTitle('Print')).toBeInTheDocument();
       });
     });
 
@@ -115,7 +115,7 @@ describe('ProjectDetailPage', () => {
       render(<ProjectDetailPage />);
 
       await waitFor(() => {
-        expect(screen.getByTitle('Print Now')).toBeInTheDocument();
+        expect(screen.getByTitle('Print')).toBeInTheDocument();
       });
     });
 
@@ -132,7 +132,7 @@ describe('ProjectDetailPage', () => {
         expect(screen.getByText('benchy.gcode.bak')).toBeInTheDocument();
       });
 
-      expect(screen.queryByTitle('Print Now')).not.toBeInTheDocument();
+      expect(screen.queryByTitle('Print')).not.toBeInTheDocument();
     });
 
     it('does NOT show print button for .stl files', async () => {
@@ -148,7 +148,7 @@ describe('ProjectDetailPage', () => {
         expect(screen.getByText('model.stl')).toBeInTheDocument();
       });
 
-      expect(screen.queryByTitle('Print Now')).not.toBeInTheDocument();
+      expect(screen.queryByTitle('Print')).not.toBeInTheDocument();
     });
   });
 
@@ -211,10 +211,10 @@ describe('ProjectDetailPage', () => {
       render(<ProjectDetailPage />);
 
       await waitFor(() => {
-        expect(screen.getByTitle('Print Now')).toBeInTheDocument();
+        expect(screen.getByTitle('Print')).toBeInTheDocument();
       });
 
-      await user.click(screen.getByTitle('Print Now'));
+      await user.click(screen.getByTitle('Print'));
 
       // PrintModal should open — look for the modal heading "Print"
       await waitFor(() => {

+ 18 - 68
frontend/src/api/client.ts

@@ -333,6 +333,14 @@ export interface HMSError {
   attr: number;  // Attribute value for constructing wiki URL
   module: number;
   severity: number;  // 1=fatal, 2=serious, 3=common, 4=info
+  actions?: string[];  // List of user-facing action keys (e.g. "CHECK_FILAMENT")
+  job_id?: string;  // Optional job ID for actions that require it (e.g. "CHECK_ASSISTANT")
+}
+
+export interface HMSActionBody {
+  print_error: string;  // HMS error code (e.g. "05000070")
+  action: string;  // "HMS action to execute (e.g. 'resume_after_error')"
+  job_id: string | null;  // Optional job ID for context (if applicable)
 }
 
 export interface AMSTray {
@@ -1958,6 +1966,7 @@ export interface PrintQueueItem {
   been_jumped?: boolean;
   // Auto-print G-code injection
   gcode_injection?: boolean;
+  cleanup_library_after_dispatch?: boolean;
 }
 
 export interface PrintBatch {
@@ -1988,6 +1997,8 @@ export interface PrintQueueItemCreate {
   require_previous_success?: boolean;
   auto_off_after?: boolean;
   manual_start?: boolean;  // Requires manual trigger to start (staged)
+  insert_at_top?: boolean;  // Insert ahead of other pending items in the same queue scope
+  insert_position?: number | null;  // 1-indexed insertion position for priority queueing
   // PrintModal "Print Anyway" on the deficit warning — persisted so the
   // scheduler doesn't immediately re-flag this item (#1698-followup).
   skip_filament_check?: boolean;
@@ -2009,6 +2020,8 @@ export interface PrintQueueItemCreate {
   batch_id?: number | null;
   // Project to associate the resulting archive with
   project_id?: number;
+  // Delete transient uploaded library file after scheduler creates the archive
+  cleanup_library_after_dispatch?: boolean;
 }
 
 export interface PrintBatchCreate {
@@ -2476,15 +2489,6 @@ export interface NotificationTestResponse {
   message: string;
 }
 
-export interface BackgroundDispatchResponse {
-  status: 'dispatched' | string;
-  printer_id: number;
-  archive_id?: number | null;
-  filename: string;
-  dispatch_job_id: number;
-  dispatch_position: number;
-}
-
 // Provider-specific config types for reference
 export interface CallMeBotConfig {
   phone: string;
@@ -3707,6 +3711,11 @@ export const api = {
   // HMS Errors
   clearHMSErrors: (printerId: number) =>
     request<{ success: boolean; message: string }>(`/printers/${printerId}/hms/clear`, { method: 'POST' }),
+  executeHMSAction: (printerId: number, data: HMSActionBody) =>
+    request<{ success: boolean; message: string }>(`/printers/${printerId}/hms/execute-action`, {
+      method: 'POST',
+      body: JSON.stringify(data),
+    }),
 
   // AMS Control
   refreshAmsSlot: (printerId: number, amsId: number, slotId: number) =>
@@ -4381,30 +4390,6 @@ export const api = {
       }>;
     }>(`/archives/${archiveId}/filament-requirements${qs.toString() ? `?${qs}` : ''}`);
   },
-  reprintArchive: (
-    archiveId: number,
-    printerId: number,
-    options?: {
-      plate_id?: number;
-      plate_name?: string;
-      ams_mapping?: number[];
-      timelapse?: boolean;
-      bed_levelling?: boolean;
-      flow_cali?: boolean;
-      vibration_cali?: boolean;
-      layer_inspect?: boolean;
-      use_ams?: boolean;
-      nozzle_offset_cali?: boolean;
-    }
-  ) =>
-    request<BackgroundDispatchResponse>(
-      `/archives/${archiveId}/reprint?printer_id=${printerId}`,
-      {
-        method: 'POST',
-        headers: options ? { 'Content-Type': 'application/json' } : undefined,
-        body: options ? JSON.stringify(options) : undefined,
-      }
-    ),
   uploadArchive: async (file: File, printerId?: number): Promise<Archive> => {
     const formData = new FormData();
     formData.append('file', file);
@@ -6070,41 +6055,6 @@ export const api = {
       method: 'POST',
       body: JSON.stringify({ file_ids: fileIds }),
     }),
-  printLibraryFile: (
-    fileId: number,
-    printerId: number,
-    options?: {
-      plate_id?: number;
-      plate_name?: string;
-      ams_mapping?: number[];
-      bed_levelling?: boolean;
-      flow_cali?: boolean;
-      vibration_cali?: boolean;
-      layer_inspect?: boolean;
-      timelapse?: boolean;
-      use_ams?: boolean;
-      nozzle_offset_cali?: boolean;
-      project_id?: number;
-      cleanup_library_after_dispatch?: boolean;
-    }
-  ) =>
-    request<BackgroundDispatchResponse>(
-      `/library/files/${fileId}/print?printer_id=${printerId}`,
-      {
-        method: 'POST',
-        body: options ? JSON.stringify(options) : undefined,
-      }
-    ),
-  cancelBackgroundDispatchJob: (jobId: number) =>
-    request<{
-      status: 'cancelled' | 'cancelling';
-      job_id: number;
-      source_name: string;
-      printer_id: number;
-      printer_name: string;
-    }>(`/background-dispatch/${jobId}`, {
-      method: 'DELETE',
-    }),
   getLibraryFilePlates: (fileId: number) =>
     request<LibraryFilePlatesResponse>(`/library/files/${fileId}/plates`),
   getLibraryFileFilamentRequirements: (

+ 106 - 6
frontend/src/components/CameraTile.tsx

@@ -1,9 +1,11 @@
 import { useEffect, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { VideoOff, WifiOff } from 'lucide-react';
+import { AlertTriangle, VideoOff, WifiOff } from 'lucide-react';
 import { getAuthToken, withStreamToken } from '../api/client';
+import { formatDuration } from '../utils/date';
 
 export type CameraTileMode = 'live' | 'snapshot' | 'paused';
+export type CameraTileStatusMode = 'off' | 'compact' | 'full';
 
 interface CameraTileProps {
   printerId: number;
@@ -13,6 +15,16 @@ interface CameraTileProps {
   snapshotIntervalMs: number;
   connected: boolean;
   onClick?: () => void;
+  // Optional status overlay — wired by CameraWall from the shared
+  // ['printerStatus', id] query. All optional so existing tests don't break.
+  statusMode?: CameraTileStatusMode;
+  printerState?: string | null;
+  progress?: number | null;
+  remainingMin?: number | null;
+  layerNum?: number | null;
+  totalLayers?: number | null;
+  printName?: string | null;
+  hmsErrorCount?: number;
 }
 
 // Tiles render lighter than EmbeddedCameraViewer's full window: lower fps,
@@ -20,6 +32,31 @@ interface CameraTileProps {
 // still does the MJPEG fan-out, so per-tile cost is one TLS pull on the wire.
 const LIVE_FPS = 8;
 
+type StatusBucket = 'printing' | 'paused' | 'finished' | 'error' | 'idle';
+
+function classifyState(state: string | null | undefined, hmsErrorCount: number): StatusBucket {
+  if (hmsErrorCount > 0) return 'error';
+  switch (state) {
+    case 'RUNNING':
+      return 'printing';
+    case 'PAUSE':
+      return 'paused';
+    case 'FINISH':
+    case 'FAILED':
+      return 'finished';
+    default:
+      return 'idle';
+  }
+}
+
+const BUCKET_CHIP_CLASS: Record<StatusBucket, string> = {
+  printing: 'bg-bambu-green/85 text-black',
+  paused: 'bg-amber-500/85 text-black',
+  finished: 'bg-sky-500/80 text-white',
+  error: 'bg-red-500/85 text-white',
+  idle: 'bg-bambu-dark-tertiary/80 text-bambu-gray',
+};
+
 export function CameraTile({
   printerId,
   printerName,
@@ -28,6 +65,14 @@ export function CameraTile({
   snapshotIntervalMs,
   connected,
   onClick,
+  statusMode = 'off',
+  printerState = null,
+  progress = null,
+  remainingMin = null,
+  layerNum = null,
+  totalLayers = null,
+  printName = null,
+  hmsErrorCount = 0,
 }: CameraTileProps) {
   const { t } = useTranslation();
   const [bust, setBust] = useState(0);
@@ -89,6 +134,17 @@ export function CameraTile({
 
   const transform = cameraRotation ? `rotate(${cameraRotation}deg)` : undefined;
 
+  const bucket = classifyState(printerState, hmsErrorCount);
+  // Hide chip for idle to keep cold walls clean; always show when something
+  // is happening (printing/paused/finished/error).
+  const showChip = connected && statusMode !== 'off' && bucket !== 'idle';
+  const isPrintingOrPaused = bucket === 'printing' || bucket === 'paused';
+  const showInfoStrip = connected && statusMode === 'full' && isPrintingOrPaused;
+  const fileLabel = printName ?? null;
+  const progressPct = progress != null ? Math.round(progress) : null;
+  const hasLayers = layerNum != null && totalLayers != null && totalLayers > 0;
+  const hasRemaining = remainingMin != null && remainingMin > 0;
+
   return (
     <button
       type="button"
@@ -122,7 +178,22 @@ export function CameraTile({
         />
       )}
 
-      {/* Mode indicator */}
+      {/* Status chip (top-left) */}
+      {showChip && (
+        <span
+          className={`absolute left-2 top-2 flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${BUCKET_CHIP_CLASS[bucket]}`}
+        >
+          {hmsErrorCount > 0 && (
+            <AlertTriangle
+              className="h-3 w-3"
+              aria-hidden="true"
+            />
+          )}
+          <span>{t(`printers.status.${bucket}`)}</span>
+        </span>
+      )}
+
+      {/* Mode indicator (top-right) */}
       <span
         className={`absolute right-2 top-2 rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${
           mode === 'live'
@@ -139,10 +210,39 @@ export function CameraTile({
             : t('printers.camWall.off')}
       </span>
 
-      {/* Name overlay */}
-      <span className="absolute inset-x-0 bottom-0 truncate bg-gradient-to-t from-black/80 to-transparent px-2 pb-1.5 pt-3 text-xs font-medium text-white">
-        {printerName}
-      </span>
+      {/* Bottom overlay: name + (when full) print info */}
+      <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/55 to-transparent px-2 pb-1.5 pt-3 text-white">
+        {showInfoStrip && (
+          <div className="mb-0.5 space-y-0.5 text-[11px] leading-tight text-white/90">
+            {fileLabel && (
+              <div className="truncate" title={fileLabel}>
+                {fileLabel}
+              </div>
+            )}
+            <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-bambu-gray">
+              {progressPct != null && (
+                <span className="font-semibold text-white">{progressPct}%</span>
+              )}
+              {hasLayers && (
+                <span>
+                  {t('printers.camWall.layer', {
+                    cur: layerNum,
+                    total: totalLayers,
+                  })}
+                </span>
+              )}
+              {hasRemaining && (
+                <span>
+                  {t('printers.camWall.timeLeft', {
+                    time: formatDuration((remainingMin ?? 0) * 60),
+                  })}
+                </span>
+              )}
+            </div>
+          </div>
+        )}
+        <span className="block truncate text-xs font-medium">{printerName}</span>
+      </div>
     </button>
   );
 }

+ 62 - 9
frontend/src/components/CameraWall.tsx

@@ -2,30 +2,36 @@ import { useEffect, useMemo, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useQueries } from '@tanstack/react-query';
 import { Settings as SettingsIcon } from 'lucide-react';
-import { CameraTile, type CameraTileMode } from './CameraTile';
-import { api, type Printer } from '../api/client';
+import { CameraTile, type CameraTileMode, type CameraTileStatusMode } from './CameraTile';
+import { filterKnownHMSErrors } from './HMSErrorModal';
+import { api, type Printer, type PrinterStatus } from '../api/client';
 
 interface CameraWallProps {
   printers: Printer[];
   maxLive: number;
   snapshotIntervalSec: number;
+  statusMode: CameraTileStatusMode;
   onTileClick: (printerId: number, printerName: string) => void;
   onChangeMaxLive: (next: number) => void;
   onChangeSnapshotIntervalSec: (next: number) => void;
+  onChangeStatusMode: (next: CameraTileStatusMode) => void;
 }
 
 const MIN_MAX_LIVE = 1;
 const MAX_MAX_LIVE = 16;
 const MIN_SNAPSHOT_SEC = 2;
 const MAX_SNAPSHOT_SEC = 60;
+const STATUS_MODES: CameraTileStatusMode[] = ['off', 'compact', 'full'];
 
 export function CameraWall({
   printers,
   maxLive,
   snapshotIntervalSec,
+  statusMode,
   onTileClick,
   onChangeMaxLive,
   onChangeSnapshotIntervalSec,
+  onChangeStatusMode,
 }: CameraWallProps) {
   const { t } = useTranslation();
   const tileRefs = useRef<Map<number, HTMLDivElement | null>>(new Map());
@@ -39,10 +45,10 @@ export function CameraWall({
       staleTime: 5000,
     })),
   });
-  const printerConnected = useMemo(() => {
-    const map = new Map<number, boolean>();
+  const statusByPrinter = useMemo(() => {
+    const map = new Map<number, PrinterStatus | undefined>();
     printers.forEach((p, i) => {
-      map.set(p.id, statusQueries[i]?.data?.connected ?? false);
+      map.set(p.id, statusQueries[i]?.data);
     });
     return map;
   }, [printers, statusQueries]);
@@ -89,12 +95,15 @@ export function CameraWall({
 
   // Live slot allocation: visible tiles get live up to `maxLive`, in printer
   // list order so the assignment is stable. Visible-but-over-cap fall back to
-  // snapshot polling. Off-screen tiles render paused (no network).
+  // snapshot polling. Off-screen tiles render paused (no network). Disconnected
+  // printers also render paused regardless of visibility — there's nothing to
+  // stream and burning a live-budget slot on them would starve a working tile.
   const modeByPrinter = useMemo(() => {
     const map = new Map<number, CameraTileMode>();
     let liveBudget = Math.max(0, maxLive);
     for (const p of printers) {
-      if (!visibleIds.has(p.id)) {
+      const connected = statusByPrinter.get(p.id)?.connected ?? false;
+      if (!visibleIds.has(p.id) || !connected) {
         map.set(p.id, 'paused');
         continue;
       }
@@ -106,7 +115,7 @@ export function CameraWall({
       }
     }
     return map;
-  }, [printers, visibleIds, maxLive]);
+  }, [printers, visibleIds, maxLive, statusByPrinter]);
 
   if (printers.length === 0) {
     return (
@@ -182,6 +191,36 @@ export function CameraWall({
                   {t('printers.camWall.settings.snapshotIntervalHint')}
                 </span>
               </label>
+              <div className="space-y-1">
+                <span className="block text-xs font-medium text-white">
+                  {t('printers.camWall.settings.statusOverlay')}
+                </span>
+                <div
+                  role="radiogroup"
+                  aria-label={t('printers.camWall.settings.statusOverlay')}
+                  className="flex overflow-hidden rounded-md border border-bambu-dark-tertiary"
+                >
+                  {STATUS_MODES.map((m) => (
+                    <button
+                      key={m}
+                      type="button"
+                      role="radio"
+                      aria-checked={statusMode === m}
+                      onClick={() => onChangeStatusMode(m)}
+                      className={`flex-1 px-2 py-1 text-xs ${
+                        statusMode === m
+                          ? 'bg-bambu-green text-black font-semibold'
+                          : 'bg-bambu-dark text-white hover:bg-bambu-dark-tertiary'
+                      }`}
+                    >
+                      {t(`printers.camWall.statusMode.${m}`)}
+                    </button>
+                  ))}
+                </div>
+                <span className="block text-[11px] text-bambu-gray">
+                  {t('printers.camWall.settings.statusOverlayHint')}
+                </span>
+              </div>
             </div>
           )}
         </div>
@@ -204,7 +243,21 @@ export function CameraWall({
                 cameraRotation={p.camera_rotation}
                 mode={mode}
                 snapshotIntervalMs={snapshotIntervalSec * 1000}
-                connected={printerConnected.get(p.id) ?? false}
+                connected={statusByPrinter.get(p.id)?.connected ?? false}
+                statusMode={statusMode}
+                printerState={statusByPrinter.get(p.id)?.state ?? null}
+                progress={statusByPrinter.get(p.id)?.progress ?? null}
+                remainingMin={statusByPrinter.get(p.id)?.remaining_time ?? null}
+                layerNum={statusByPrinter.get(p.id)?.layer_num ?? null}
+                totalLayers={statusByPrinter.get(p.id)?.total_layers ?? null}
+                printName={
+                  statusByPrinter.get(p.id)?.subtask_name ??
+                  statusByPrinter.get(p.id)?.gcode_file ??
+                  null
+                }
+                hmsErrorCount={
+                  filterKnownHMSErrors(statusByPrinter.get(p.id)?.hms_errors ?? []).length
+                }
                 onClick={() => onTileClick(p.id, p.name)}
               />
             </div>

+ 5 - 2
frontend/src/components/FilamentSlotCircle.tsx

@@ -13,7 +13,10 @@
  *                confirmed no spool (state 9/10), "reset" for slots where
  *                the user cleared the assignment but the firmware hasn't
  *                positively confirmed emptiness. Ignored when isEmpty is false.
- *   slotNumber - 1-based slot number to display inside the circle.
+ *   slotNumber - 1-based slot number to display inside the circle. Accepts
+ *                a string for non-numeric labels (e.g. "L" / "R" for the
+ *                dual-nozzle external trays, where carrying a separate
+ *                Ext-L/Ext-R caption underneath made the row taller).
  */
 
 interface FilamentSlotCircleProps {
@@ -21,7 +24,7 @@ interface FilamentSlotCircleProps {
   trayType?: string | null;
   isEmpty: boolean;
   emptyKind?: 'physical' | 'reset' | null;
-  slotNumber: number;
+  slotNumber: number | string;
 }
 
 function isLightFilamentColor(hex: string): boolean {

+ 54 - 7
frontend/src/components/HMSErrorModal.tsx

@@ -2,7 +2,7 @@
 // Source: https://github.com/greghesp/ha-bambulab
 import { useEffect } from 'react';
 import { useTranslation } from 'react-i18next';
-import { useMutation } from '@tanstack/react-query';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
 import { X, AlertTriangle, AlertCircle, Info, ExternalLink, Loader2, Trash2 } from 'lucide-react';
 import type { HMSError, Permission } from '../api/client';
 import { api } from '../api/client';
@@ -874,17 +874,17 @@ const ERROR_DESCRIPTIONS: Record<string, string> = {
   '18FF_C00A': 'Please observe the nozzle of the right extruder. If the filament has been extruded, select \'Continue\'; if not, please push the filament forward slightly and then select \'Retry\'.',
 };
 
-function getSeverityInfo(severity: number): { label: string; color: string; bgColor: string; Icon: typeof AlertTriangle } {
+function getSeverityInfo(severity: number): { label: string; color: string; bgColor: string; buttonHoverColor: string; Icon: typeof AlertTriangle } {
   switch (severity) {
     case 1:
-      return { label: 'Fatal', color: 'text-red-500', bgColor: 'bg-red-500/20', Icon: AlertTriangle };
+      return { label: 'Fatal', color: 'text-red-500', bgColor: 'bg-red-500/20', buttonHoverColor: 'bg-red-500/10', Icon: AlertTriangle };
     case 2:
-      return { label: 'Serious', color: 'text-red-400', bgColor: 'bg-red-500/15', Icon: AlertTriangle };
+      return { label: 'Serious', color: 'text-red-400', bgColor: 'bg-red-500/15', buttonHoverColor: 'bg-red-500/10', Icon: AlertTriangle };
     case 3:
-      return { label: 'Warning', color: 'text-orange-400', bgColor: 'bg-orange-500/20', Icon: AlertCircle };
+      return { label: 'Warning', color: 'text-orange-400', bgColor: 'bg-orange-500/20', buttonHoverColor: 'bg-orange-500/10', Icon: AlertCircle };
     case 4:
     default:
-      return { label: 'Info', color: 'text-blue-400', bgColor: 'bg-blue-500/20', Icon: Info };
+      return { label: 'Info', color: 'text-blue-400', bgColor: 'bg-blue-500/20', buttonHoverColor: 'bg-blue-500/10', Icon: Info };
   }
 }
 
@@ -912,6 +912,7 @@ function getHMSHomeUrl(): string {
 export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPermission }: HMSErrorModalProps) {
   const { t } = useTranslation();
   const { showToast } = useToast();
+  const queryClient = useQueryClient();
 
   const clearMutation = useMutation({
     mutationFn: () => api.clearHMSErrors(printerId),
@@ -940,6 +941,33 @@ export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPerm
     return () => window.removeEventListener('keydown', handleKeyDown);
   }, [onClose]);
 
+  // printerStatusMutation with optimistic update
+  const activateActionMutation = useMutation({
+    mutationFn: (data: {
+      action: string,
+      print_error: string,
+      job_id: string | null,
+    }) => api.executeHMSAction(printerId, {
+      action: data.action,
+      print_error: data.print_error,
+      job_id: data.job_id,
+    }),
+    onSuccess: () => {
+      // Scope the invalidation to THIS printer. The prefix form
+      // `['printerStatus']` would refresh every printer card on the page,
+      // which is wasteful when only one printer's state actually changed.
+      queryClient.invalidateQueries({ queryKey: ['printerStatus', printerId] });
+      showToast(t('hmsErrors.actionSuccess', 'Action sent to printer'), 'success');
+      onClose();
+    },
+    onError: (error: Error) => {
+      showToast(
+        `${t('hmsErrors.actionFailed', 'Failed to send action')}: ${error.message}`,
+        'error',
+      );
+    },
+  });
+
   return (
     <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
       <div className="bg-bambu-dark-secondary rounded-lg shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col">
@@ -967,7 +995,7 @@ export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPerm
           ) : (
             <div className="space-y-3">
               {knownErrors.map((error, index) => {
-                const { label, color, bgColor, Icon } = getSeverityInfo(error.severity);
+                const { label, color, bgColor, buttonHoverColor, Icon } = getSeverityInfo(error.severity);
                 const codeNum = parseInt(error.code.replace('0x', ''), 16) || 0;
                 const shortCode = getShortCode(error.attr, codeNum);
                 const description = ERROR_DESCRIPTIONS[shortCode];
@@ -989,6 +1017,25 @@ export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPerm
                           </span>
                         </div>
                         <p className="text-sm text-bambu-gray mb-2">{description}</p>
+                        {error.actions && error.actions.length > 0 && (
+                          <div className="flex flex-wrap gap-2 my-2">
+                            {error.actions.map((action) => (
+                              <button
+                                key={action}
+                                onClick={() => {
+                                  activateActionMutation.mutate({
+                                    action,
+                                    print_error: shortCode.replace("_", ""),
+                                    job_id: error.job_id ?? null,
+                                  });
+                                }}
+                                className={`flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg ${bgColor} ${color} hover:${buttonHoverColor} transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0`}
+                              >
+                                {t(`hmsErrors.actions.${action}`, action)}
+                              </button>
+                            ))}
+                          </div>
+                        )}
                         <a
                           href={hmsHomeUrl}
                           target="_blank"

+ 2 - 2
frontend/src/components/PrintModal/PlateSelector.tsx

@@ -8,8 +8,8 @@ import { getBedTypeInfo } from '../../utils/bedType';
 /**
  * Plate selection grid for multi-plate 3MF files.
  * Shows thumbnails, names, objects, and print times for each plate.
- * In multi-select mode (add-to-queue), plates have checkboxes for selecting a subset.
- * In single-select mode (reprint/edit), only one plate can be selected at a time.
+ * In multi-select mode, plates have checkboxes for selecting a subset.
+ * In single-select mode, only one plate can be selected at a time.
  */
 export function PlateSelector({
   plates,

+ 0 - 9
frontend/src/components/PrintModal/PrinterSelector.tsx

@@ -12,7 +12,6 @@ import {
   Users,
 } from 'lucide-react';
 import { api, type PrinterStatus } from '../../api/client';
-import { useDispatchedPrinterIds } from '../../hooks/useDispatchedPrinterIds';
 import { getColorName } from '../../utils/colors';
 import {
   normalizeColorForCompare,
@@ -256,14 +255,7 @@ export function PrinterSelector({
     return map;
   }, [activePrinters, statusQueries]);
 
-  // Printers with a queued/active background dispatch — accepted by Bambuddy
-  // but not yet reflected in PrinterStatus.state (which only flips on
-  // PRINT_START from the printer itself). Backend rejects double-sends with
-  // 409 anyway; this just stops the operator from picking them in the modal.
-  const dispatchedPrinterIds = useDispatchedPrinterIds();
-
   const isPrinterBusy = (printerId: number): boolean => {
-    if (dispatchedPrinterIds.has(printerId)) return true;
     const status = printerStatusMap.get(printerId);
     if (!status) return false; // Unknown state — don't block
     if (!status.connected) return true;
@@ -271,7 +263,6 @@ export function PrinterSelector({
   };
 
   const getPrinterStateLabel = (printerId: number): string | null => {
-    if (dispatchedPrinterIds.has(printerId)) return 'Dispatching...';
     const status = printerStatusMap.get(printerId);
     if (!status) return null;
     if (!status.connected) return 'Offline';

+ 43 - 22
frontend/src/components/PrintModal/ScheduleOptions.tsx

@@ -1,6 +1,6 @@
 import { useState, useEffect, useRef } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Calendar, Clock, Hand, Power, Layers, Code } from 'lucide-react';
+import { Calendar, Clock, Hand, Power, Layers, Code, ListOrdered } from 'lucide-react';
 import type { ScheduleOptionsProps, ScheduleType } from './types';
 import {
   formatDateInput,
@@ -16,7 +16,7 @@ import {
 
 /**
  * Schedule options component for queue items.
- * Includes schedule type (ASAP/Scheduled/Queue Only), datetime picker,
+ * Includes schedule type (ASAP/Queue/Schedule), datetime picker,
  * and options for require previous success and auto power off.
  */
 export function ScheduleOptionsPanel({
@@ -70,7 +70,11 @@ export function ScheduleOptionsPanel({
   }, [options.scheduleType, options.scheduledTime, dateFormat, timeFormat, onChange, options]);
 
   const handleScheduleTypeChange = (scheduleType: ScheduleType) => {
-    onChange({ ...options, scheduleType });
+    onChange({
+      ...options,
+      scheduleType,
+      requireManualStart: scheduleType === 'queue' ? options.requireManualStart : false,
+    });
   };
 
   const updateScheduledTime = (newDateValue: string, newTimeValue: string) => {
@@ -122,7 +126,7 @@ export function ScheduleOptionsPanel({
     <div className="space-y-4">
       {/* Schedule type */}
       <div>
-        <label className="block text-sm text-bambu-gray mb-2">When to print</label>
+        <label className="block text-sm text-bambu-gray mb-2">{t('printModal.whenToPrint')}</label>
         <div className="flex gap-2">
           <button
             type="button"
@@ -134,31 +138,31 @@ export function ScheduleOptionsPanel({
             onClick={() => handleScheduleTypeChange('asap')}
           >
             <Clock className="w-4 h-4" />
-            ASAP
+            {t('printModal.asap')}
           </button>
           <button
             type="button"
             className={`flex-1 px-2 py-2 rounded-lg border text-sm flex items-center justify-center gap-1.5 transition-colors ${
-              options.scheduleType === 'scheduled'
+              options.scheduleType === 'queue'
                 ? 'bg-bambu-green border-bambu-green text-white'
                 : 'bg-bambu-dark border-bambu-dark-tertiary text-bambu-gray hover:text-white'
             }`}
-            onClick={() => handleScheduleTypeChange('scheduled')}
+            onClick={() => handleScheduleTypeChange('queue')}
           >
-            <Calendar className="w-4 h-4" />
-            Scheduled
+            <ListOrdered className="w-4 h-4" />
+            {t('printModal.queue')}
           </button>
           <button
             type="button"
             className={`flex-1 px-2 py-2 rounded-lg border text-sm flex items-center justify-center gap-1.5 transition-colors ${
-              options.scheduleType === 'manual'
+              options.scheduleType === 'scheduled'
                 ? 'bg-bambu-green border-bambu-green text-white'
                 : 'bg-bambu-dark border-bambu-dark-tertiary text-bambu-gray hover:text-white'
             }`}
-            onClick={() => handleScheduleTypeChange('manual')}
+            onClick={() => handleScheduleTypeChange('scheduled')}
           >
-            <Hand className="w-4 h-4" />
-            Queue Only
+            <Calendar className="w-4 h-4" />
+            {t('printModal.schedule')}
           </button>
         </div>
       </div>
@@ -166,7 +170,7 @@ export function ScheduleOptionsPanel({
       {/* Scheduled time input */}
       {options.scheduleType === 'scheduled' && (
         <div>
-          <label className="block text-sm text-bambu-gray mb-1">Date & Time</label>
+          <label className="block text-sm text-bambu-gray mb-1">{t('printModal.dateTime')}</label>
           <div className="flex gap-2">
             {/* Date input */}
             <div className="flex-1 relative">
@@ -185,7 +189,7 @@ export function ScheduleOptionsPanel({
                 type="button"
                 onClick={openCalendar}
                 className="absolute right-2 top-1/2 -translate-y-1/2 text-bambu-gray hover:text-white"
-                title="Open calendar"
+                title={t('printModal.openCalendar')}
               >
                 <Calendar className="w-4 h-4" />
               </button>
@@ -216,12 +220,29 @@ export function ScheduleOptionsPanel({
           </div>
           {(!isDateValid || !isTimeValid) && (
             <p className="mt-1 text-xs text-red-400">
-              Please enter a valid date and time
+              {t('printModal.invalidDateTime')}
             </p>
           )}
         </div>
       )}
 
+      {/* Manual start */}
+      {options.scheduleType === 'queue' && (
+        <div className="flex items-center gap-2">
+          <input
+            type="checkbox"
+            id="requireManualStart"
+            checked={options.requireManualStart}
+            onChange={(e) => onChange({ ...options, requireManualStart: e.target.checked })}
+            className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
+          />
+          <label htmlFor="requireManualStart" className="text-sm flex items-center gap-1 text-bambu-gray">
+            <Hand className="w-3.5 h-3.5" />
+            {t('printModal.requireManualStart')}
+          </label>
+        </div>
+      )}
+
       {/* Require previous success */}
       <div className="flex items-center gap-2">
         <input
@@ -232,7 +253,7 @@ export function ScheduleOptionsPanel({
           className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
         />
         <label htmlFor="requirePrevious" className="text-sm text-bambu-gray">
-          Only start if previous print succeeded
+          {t('printModal.requirePreviousSuccess')}
         </label>
       </div>
 
@@ -248,7 +269,7 @@ export function ScheduleOptionsPanel({
         />
         <label htmlFor="autoOffAfter" className={`text-sm flex items-center gap-1 ${canControlPrinter ? 'text-bambu-gray' : 'text-bambu-gray/50'}`}>
           <Power className="w-3.5 h-3.5" />
-          Power off printer when done
+          {t('printModal.autoOffAfter')}
         </label>
       </div>
 
@@ -270,7 +291,7 @@ export function ScheduleOptionsPanel({
       )}
 
       {/* Stagger start */}
-      {showStagger && options.scheduleType !== 'manual' && (
+      {showStagger && options.scheduleType !== 'queue' && (
         <div className="space-y-3">
           <div className="flex items-center gap-2">
             <input
@@ -341,10 +362,10 @@ export function ScheduleOptionsPanel({
       {/* Help text */}
       <p className="text-xs text-bambu-gray">
         {options.scheduleType === 'asap'
-          ? 'Print will start as soon as the printer is idle.'
+          ? t('printModal.helpAsap')
           : options.scheduleType === 'scheduled'
-          ? 'Print will start at the scheduled time if the printer is idle. If busy, it will wait until the printer becomes available.'
-          : "Print will be staged but won't start automatically. Use the Start button to release it to the queue."}
+          ? t('printModal.helpSchedule')
+          : t('printModal.helpQueue')}
       </p>
     </div>
   );

+ 122 - 227
frontend/src/components/PrintModal/index.tsx

@@ -1,8 +1,8 @@
 import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
-import { AlertCircle, AlertTriangle, Calendar, Code, Layers, Loader2, Pencil, Printer, X } from 'lucide-react';
+import { AlertCircle, AlertTriangle, Loader2, Pencil, Printer, X } from 'lucide-react';
 import { useEffect, useMemo, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import type { PrintQueueItemCreate, PrintQueueItemUpdate, SpoolAssignment } from '../../api/client';
+import type { PrinterStatus, PrintQueueItemCreate, PrintQueueItemUpdate, SpoolAssignment } from '../../api/client';
 import { api } from '../../api/client';
 import { useAuth } from '../../contexts/AuthContext';
 import { Card, CardContent } from '../Card';
@@ -32,13 +32,12 @@ import type {
 import { DEFAULT_PRINT_OPTIONS, DEFAULT_SCHEDULE_OPTIONS } from './types';
 
 /**
- * Unified PrintModal component that handles three modes:
- * - 'reprint': Immediate print from archive or library file (supports multi-printer)
- * - 'add-to-queue': Schedule print to queue from archive or library file (supports multi-printer)
- * - 'edit-queue-item': Edit existing queue item (supports multi-printer)
+ * Unified PrintModal component that handles queue item creation and editing.
+ * - 'create': Create a print queue item from an archive or library file
+ * - 'edit-queue-item': Edit existing queue item
  *
- * Both archiveId and libraryFileId are supported. Library files can be printed immediately
- * or added to queue (archive is created at print start time, not when queued).
+ * Both archiveId and libraryFileId are supported. Library files are archived at
+ * print start time by the scheduler, not when queued.
  */
 export function PrintModal({
   mode,
@@ -59,6 +58,7 @@ export function PrintModal({
 
   // Determine if we're printing a library file
   const isLibraryFile = !!libraryFileId && !archiveId;
+  const isEditing = mode === 'edit-queue-item';
 
   type FilamentWarningItem = {
     printerName: string;
@@ -79,7 +79,7 @@ export function PrintModal({
     return [];
   });
 
-  // Multi-select plates: in add-to-queue mode users can pick a subset of plates
+  // Multi-select plates: create mode users can pick a subset of plates
   const [selectedPlates, setSelectedPlates] = useState<Set<number>>(() => {
     if (mode === 'edit-queue-item' && queueItem?.plate_id != null) {
       return new Set([queueItem.plate_id]);
@@ -109,10 +109,8 @@ export function PrintModal({
 
   const [scheduleOptions, setScheduleOptions] = useState<ScheduleOptions>(() => {
     if (mode === 'edit-queue-item' && queueItem) {
-      let scheduleType: ScheduleType = 'asap';
-      if (queueItem.manual_start) {
-        scheduleType = 'manual';
-      } else if (queueItem.scheduled_time && !isPlaceholderDate(queueItem.scheduled_time)) {
+      let scheduleType: ScheduleType = 'queue';
+      if (queueItem.scheduled_time && !isPlaceholderDate(queueItem.scheduled_time)) {
         scheduleType = 'scheduled';
       }
 
@@ -126,6 +124,7 @@ export function PrintModal({
       return {
         scheduleType,
         scheduledTime,
+        requireManualStart: queueItem.manual_start,
         requirePreviousSuccess: queueItem.require_previous_success,
         autoOffAfter: queueItem.auto_off_after,
         gcodeInjection: queueItem.gcode_injection ?? false,
@@ -267,7 +266,7 @@ export function PrintModal({
     queryKey: ['spool-assignments'],
     queryFn: () => api.getAssignments(),
     staleTime: 30 * 1000,
-    enabled: ((mode === 'reprint' || mode === 'add-to-queue') && assignmentMode === 'printer') || (isLibraryFile && mode === 'reprint'),
+    enabled: !isEditing && assignmentMode === 'printer',
   });
 
   // Fetch per-printer Map<globalTrayId, gramsRemaining> via the dedicated
@@ -353,12 +352,6 @@ export function PrintModal({
 
   // Combine filament requirements from either source
   const effectiveFilamentReqs = isLibraryFile ? libraryFilamentReqs : archiveFilamentReqs;
-  const selectedPlateName = useMemo(() => {
-    if (selectedPlate === null || !platesData?.plates?.length) {
-      return undefined;
-    }
-    return platesData.plates.find((plate) => plate.index === selectedPlate)?.name || undefined;
-  }, [platesData, selectedPlate]);
 
   // Fetch available filaments for model-based assignment (for filament override UI)
   const { data: availableFilaments } = useQuery({
@@ -382,6 +375,33 @@ export function PrintModal({
     printerStatus?.ams_filament_backup,
   );
 
+  const isPrinterCurrentlyDispatchable = (status: PrinterStatus | undefined): boolean => {
+    if (!status?.connected) return false;
+    if (status.awaiting_plate_clear) return false;
+    if (status.ams?.some((ams) => ams.dry_time > 0)) return false;
+    return ['IDLE', 'FINISH', 'FAILED'].includes(status.state ?? '');
+  };
+
+  const asapToastShouldPromiseLaterStart = async (): Promise<boolean> => {
+    if (scheduleOptions.scheduleType !== 'asap' || assignmentMode !== 'printer') return false;
+    if (selectedPrinters.length === 0) return false;
+
+    try {
+      const statuses = await Promise.all(
+        selectedPrinters.map((printerId) =>
+          queryClient.fetchQuery({
+            queryKey: ['printer-status', printerId],
+            queryFn: () => api.getPrinterStatus(printerId),
+            staleTime: 0,
+          }),
+        ),
+      );
+      return statuses.some((status) => !isPrinterCurrentlyDispatchable(status));
+    } catch {
+      return true;
+    }
+  };
+
   // Get AMS mapping from hook (only when single printer selected)
   const { amsMapping } = useFilamentMapping(
     effectiveFilamentReqs,
@@ -545,15 +565,13 @@ export function PrintModal({
     },
   });
 
-  const willUseStagger = scheduleOptions.staggerEnabled && selectedPrinters.length > 1;
-
   const handleSubmit = async (e?: React.FormEvent, options?: { skipFilamentCheck?: boolean }) => {
     e?.preventDefault();
 
     if (
       !options?.skipFilamentCheck &&
       !settings?.disable_filament_warnings &&
-      (mode === 'reprint' || mode === 'add-to-queue') &&
+      !isEditing &&
       assignmentMode === 'printer'
     ) {
       const warningItems: FilamentWarningItem[] = [];
@@ -684,12 +702,12 @@ export function PrintModal({
     const filamentOverridesArray = buildFilamentOverridesArray();
 
     // Multi-plate auto-batch: when the user adds 2+ plates from one source in
-    // a single add-to-queue submission, pre-create a PrintBatch and pass its
+    // a single create submission, pre-create a PrintBatch and pass its
     // id to each subsequent addToQueue call so the queue UI groups them as a
     // collapsible batch. Only triggered for single-target submissions —
     // multi-printer fan-out keeps the old per-item shape.
     const shouldAutoBatch =
-      mode === 'add-to-queue'
+      mode === 'create'
       && platesToQueue.length > 1
       && (assignmentMode === 'model' || selectedPrinters.length === 1);
     let autoBatchId: number | null = null;
@@ -709,7 +727,22 @@ export function PrintModal({
       }
     }
 
-    // Common queue data for add-to-queue and edit modes
+    const asapInsertionCounts = new Map<string, number>();
+
+    const applyAsapInsertion = (
+      queueData: PrintQueueItemCreate,
+      printerId: number | null,
+      itemCount = 1,
+    ) => {
+      if (scheduleOptions.scheduleType !== 'asap') return;
+      const scopeKey = printerId !== null ? `printer:${printerId}` : 'unassigned';
+      const insertPosition = (asapInsertionCounts.get(scopeKey) ?? 0) + 1;
+      queueData.insert_at_top = true;
+      queueData.insert_position = insertPosition;
+      asapInsertionCounts.set(scopeKey, insertPosition + itemCount - 1);
+    };
+
+    // Common queue data for create and edit modes
     const getQueueData = (printerId: number | null, plateOverride?: number | null): PrintQueueItemCreate => ({
       printer_id: assignmentMode === 'printer' ? printerId : null,
       target_model: assignmentMode === 'model' ? targetModel : null,
@@ -721,7 +754,7 @@ export function PrintModal({
       require_previous_success: scheduleOptions.requirePreviousSuccess,
       auto_off_after: scheduleOptions.autoOffAfter,
       gcode_injection: scheduleOptions.gcodeInjection,
-      manual_start: scheduleOptions.scheduleType === 'manual',
+      manual_start: scheduleOptions.scheduleType === 'queue' && scheduleOptions.requireManualStart,
       // When the user clicks "Print Anyway" on the frontend deficit warning,
       // persist that acknowledgement so the scheduler doesn't immediately
       // re-flag the item on its first dispatch tick (#1698-followup).
@@ -734,16 +767,11 @@ export function PrintModal({
       ...printOptions,
       project_id: projectId ?? undefined,
       batch_id: autoBatchId ?? undefined,
+      cleanup_library_after_dispatch: cleanupLibraryAfterDispatch,
     });
 
     // Model-based assignment
     if (assignmentMode === 'model') {
-      if (mode === 'reprint') {
-        showToast('Model-based assignment only works with queue mode', 'error');
-        setIsSubmitting(false);
-        return;
-      }
-
       let progressCounter = 0;
       for (const plate of platesToQueue) {
         progressCounter++;
@@ -761,7 +789,7 @@ export function PrintModal({
               require_previous_success: scheduleOptions.requirePreviousSuccess,
               auto_off_after: scheduleOptions.autoOffAfter,
               gcode_injection: scheduleOptions.gcodeInjection,
-              manual_start: scheduleOptions.scheduleType === 'manual',
+              manual_start: scheduleOptions.scheduleType === 'queue' && scheduleOptions.requireManualStart,
               ams_mapping: undefined,
               plate_id: plateId,
               scheduled_time: scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime
@@ -774,6 +802,7 @@ export function PrintModal({
             // Add-to-queue mode with model-based assignment
             const queueData = getQueueData(null, plateId);
             if (effectiveQuantity > 1) queueData.quantity = effectiveQuantity;
+            applyAsapInsertion(queueData, null, effectiveQuantity);
             await addToQueueMutation.mutateAsync(queueData);
           }
           results.success++;
@@ -787,7 +816,7 @@ export function PrintModal({
       // Printer-based assignment: loop through plates × printers
       // Compute stagger base time once before the loop
       const useStagger = scheduleOptions.staggerEnabled
-        && (mode === 'add-to-queue' || mode === 'reprint')
+        && !isEditing
         && selectedPrinters.length > 1;
       const staggerBaseTime = useStagger
         ? (scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime
@@ -805,46 +834,7 @@ export function PrintModal({
           setSubmitProgress({ current: progressCounter, total: totalCount });
 
           try {
-            if (mode === 'reprint' && !useStagger) {
-              const printerMapping = getMappingForPrinter(printerId);
-              if (scheduleOptions.gcodeInjection && effectiveQuantity > 1) {
-                // Auto-print injection only happens in the scheduler path. A direct
-                // immediate reprint bypasses it, so the first copy would print without
-                // its end-snippet and stay stuck on the plate — defeating auto-eject and
-                // blocking the injected copies queued behind it. Queue *all* copies so
-                // every one is dispatched (and injected) by the scheduler.
-                const queueData = getQueueData(printerId, plateId);
-                queueData.quantity = effectiveQuantity;
-                await addToQueueMutation.mutateAsync(queueData);
-              } else {
-                // Reprint mode - start print immediately (single plate only, multi-select not available)
-                if (isLibraryFile) {
-                  await api.printLibraryFile(libraryFileId!, printerId, {
-                    plate_id: selectedPlate ?? undefined,
-                    plate_name: selectedPlateName,
-                    ams_mapping: printerMapping,
-                    ...printOptions,
-                    project_id: projectId,
-                    cleanup_library_after_dispatch: cleanupLibraryAfterDispatch,
-                  });
-                } else {
-                  // project_id is intentionally omitted here: reprintArchive targets an existing
-                  // archive that already carries its own project association from the original print.
-                  await api.reprintArchive(archiveId!, printerId, {
-                    plate_id: selectedPlate ?? undefined,
-                    plate_name: selectedPlateName,
-                    ams_mapping: printerMapping,
-                    ...printOptions,
-                  });
-                }
-                // Queue remaining copies if quantity > 1
-                if (effectiveQuantity > 1) {
-                  const queueData = getQueueData(printerId, plateId);
-                  queueData.quantity = effectiveQuantity - 1;
-                  await addToQueueMutation.mutateAsync(queueData);
-                }
-              }
-            } else if (mode === 'edit-queue-item' && progressCounter === 1) {
+            if (isEditing && progressCounter === 1) {
               // Edit mode - update the original queue item for the first entry
               const printerMapping = getMappingForPrinter(printerId);
               const updateData: PrintQueueItemUpdate = {
@@ -854,7 +844,7 @@ export function PrintModal({
                 require_previous_success: scheduleOptions.requirePreviousSuccess,
                 auto_off_after: scheduleOptions.autoOffAfter,
                 gcode_injection: scheduleOptions.gcodeInjection,
-                manual_start: scheduleOptions.scheduleType === 'manual',
+                manual_start: scheduleOptions.scheduleType === 'queue' && scheduleOptions.requireManualStart,
                 ams_mapping: printerMapping,
                 plate_id: plateId,
                 scheduled_time: scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime
@@ -864,9 +854,10 @@ export function PrintModal({
               };
               await updateQueueMutation.mutateAsync(updateData);
             } else {
-              // Add-to-queue mode, stagger-reprint mode, or edit mode with additional entries
+              // New print mode, staggered print, or edit mode with additional entries
               const queueData = getQueueData(printerId, plateId);
               if (effectiveQuantity > 1) queueData.quantity = effectiveQuantity;
+              applyAsapInsertion(queueData, printerId, effectiveQuantity);
               // Apply stagger offset for groups after the first
               if (useStagger) {
                 const groupIndex = Math.floor(i / scheduleOptions.staggerGroupSize);
@@ -893,19 +884,28 @@ export function PrintModal({
 
     setIsSubmitting(false);
 
-    // Show result toast (skip for direct reprint — the dispatch toast handles it)
+    // Show result toast
     if (results.failed === 0) {
-      if (mode === 'reprint' && willUseStagger) {
-        // Stagger-reprint routed through queue
-        showToast(t('queue.itemsQueued', { count: results.success }));
-      } else if (mode !== 'reprint') {
+      if (isEditing) {
         if (mode === 'edit-queue-item') {
           showToast('Queue item updated');
-        } else if (results.success === 1) {
-          showToast(assignmentMode === 'model' ? `Queued for any ${targetModel}` : t('queue.printQueued'));
-        } else {
-          showToast(t('queue.itemsQueued', { count: results.success }));
         }
+      } else if (results.success === 1) {
+        const waitForIdleToast = await asapToastShouldPromiseLaterStart();
+        showToast(
+          waitForIdleToast
+            ? t('queue.printQueuedWillStartWhenIdle')
+            : assignmentMode === 'model'
+              ? `Queued for any ${targetModel}`
+              : t('queue.printQueued'),
+        );
+      } else {
+        const waitForIdleToast = await asapToastShouldPromiseLaterStart();
+        showToast(
+          waitForIdleToast
+            ? t('queue.printQueuedWillStartWhenIdle')
+            : t('queue.itemsQueued', { count: results.success }),
+        );
       }
       queryClient.invalidateQueries({ queryKey: ['queue'] });
       onSuccess?.();
@@ -927,26 +927,22 @@ export function PrintModal({
     if (assignmentMode === 'printer' && selectedPrinters.length === 0) return false;
     if (assignmentMode === 'model' && !targetModel) return false;
 
-    // Model-based assignment only works in queue modes (not immediate reprint)
-    if (assignmentMode === 'model' && mode === 'reprint') return false;
-
     // For multi-plate files, need at least one plate selected
     if (isMultiPlate && selectedPlates.size === 0) return false;
 
     return true;
-  }, [selectedPrinters.length, assignmentMode, targetModel, mode, isMultiPlate, selectedPlates.size, isPending]);
+  }, [selectedPrinters.length, assignmentMode, targetModel, isMultiPlate, selectedPlates.size, isPending]);
 
   // Quantity only applies for single-printer or model-based assignment (not multi-printer)
   const effectiveQuantity = (assignmentMode === 'printer' && selectedPrinters.length > 1) ? 1 : quantity;
 
   // Keep scheduleOptions.gcodeInjection in sync with the checkbox's render
-  // condition. The checkbox only renders for reprint + snippets configured +
+  // condition. The checkbox only renders for create + snippets configured +
   // quantity > 1, so if the user ticks it at quantity 2 then drops back to 1
-  // the box hides but the state stays true — and the immediate-reprint path
-  // would then silently bypass injection.
+  // the box hides but the state stays true.
   useEffect(() => {
     if (
-      mode === 'reprint' &&
+      mode === 'create' &&
       scheduleOptions.gcodeInjection &&
       (effectiveQuantity <= 1 || !settings?.gcode_snippets)
     ) {
@@ -956,41 +952,12 @@ export function PrintModal({
 
   // Modal title and action button text based on mode
   const getModalConfig = () => {
-    const printerCount = selectedPrinters.length;
-
-    if (mode === 'reprint') {
-      const staggerReprint = willUseStagger && printerCount > 1;
-      let submitText = staggerReprint
-        ? t('printModal.staggerToPrinters', { count: printerCount, defaultValue: 'Stagger to {{count}} printers' })
-        : printerCount > 1 ? t('queue.printToPrinters', { count: printerCount }) : t('queue.print');
-      if (effectiveQuantity > 1) {
-        submitText = `${submitText} ×${effectiveQuantity}`;
-      }
+    if (!isEditing) {
       return {
-        title: isLibraryFile ? t('queue.print') : t('queue.reprint'),
+        title: t('common.print'),
         icon: Printer,
-        submitText,
-        submitIcon: staggerReprint ? Calendar : Printer,
-        loadingText: submitProgress.total > 1
-          ? t('queue.sendingProgress', { current: submitProgress.current, total: submitProgress.total })
-          : t('queue.sending'),
-      };
-    }
-    if (mode === 'add-to-queue') {
-      let submitText = t('queue.addToQueue');
-      if (selectedPlates.size > 1) {
-        submitText = t('queue.queueSelectedPlates', { count: selectedPlates.size });
-      } else if (printerCount > 1) {
-        submitText = t('queue.queueToPrinters', { count: printerCount });
-      }
-      if (effectiveQuantity > 1) {
-        submitText = `${submitText} ×${effectiveQuantity}`;
-      }
-      return {
-        title: t('queue.schedulePrint'),
-        icon: Calendar,
-        submitText,
-        submitIcon: Calendar,
+        submitText: t('common.print'),
+        submitIcon: Printer,
         loadingText: submitProgress.total > 1
           ? t('queue.addingProgress', { current: submitProgress.current, total: submitProgress.total })
           : t('queue.adding'),
@@ -1046,13 +1013,9 @@ export function PrintModal({
         className="w-full max-w-2xl max-h-[90vh] overflow-y-auto"
         onClick={(e) => e.stopPropagation()}
       >
-        <CardContent className={mode === 'reprint' ? '' : 'p-0'}>
+        <CardContent className="p-0">
           {/* Header */}
-          <div
-            className={`flex items-center justify-between ${
-              mode === 'reprint' ? 'mb-4' : 'p-4 border-b border-bambu-dark-tertiary'
-            }`}
-          >
+          <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
             <div className="flex items-center gap-2">
               <TitleIcon className="w-5 h-5 text-bambu-green" />
               <h2 className="text-lg font-semibold text-white">{modalConfig.title}</h2>
@@ -1062,22 +1025,11 @@ export function PrintModal({
             </Button>
           </div>
 
-          <form onSubmit={handleSubmit} className={mode === 'reprint' ? '' : 'p-4 space-y-4'}>
+          <form onSubmit={handleSubmit} className="p-4 space-y-4">
             {/* Archive name */}
-            <p className={`text-sm text-bambu-gray ${mode === 'reprint' ? 'mb-4' : ''}`}>
-              {mode === 'reprint' ? (
-                <>
-                  Send <span className="text-white">{archiveName}</span> to{' '}
-                  {initialSelectedPrinterIds?.length === 1 && printers
-                    ? <span className="text-white">{printers.find(p => p.id === initialSelectedPrinterIds[0])?.name ?? 'printer(s)'}</span>
-                    : 'printer(s)'}
-                </>
-              ) : (
-                <>
-                  <span className="block text-bambu-gray mb-1">Print Job</span>
-                  <span className="text-white font-medium truncate block">{archiveName}</span>
-                </>
-              )}
+            <p className="text-sm text-bambu-gray">
+              <span className="block text-bambu-gray mb-1">Print Job</span>
+              <span className="text-white font-medium truncate block">{archiveName}</span>
             </p>
 
             {/* Build-plate badge for the selected (or sole) plate — surfaced
@@ -1108,7 +1060,7 @@ export function PrintModal({
               onToggle={(plateIndex) => {
                 setSelectedPlates(prev => {
                   const next = new Set(prev);
-                  if (mode === 'add-to-queue') {
+                  if (!isEditing) {
                     // Multi-select: toggle the plate
                     if (next.has(plateIndex)) {
                       next.delete(plateIndex);
@@ -1123,9 +1075,9 @@ export function PrintModal({
                   return next;
                 });
               }}
-              onSelectAll={mode === 'add-to-queue' ? () => setSelectedPlates(new Set(plates.map(p => p.index))) : undefined}
-              onDeselectAll={mode === 'add-to-queue' ? () => setSelectedPlates(new Set()) : undefined}
-              multiSelect={mode === 'add-to-queue'}
+              onSelectAll={!isEditing ? () => setSelectedPlates(new Set(plates.map(p => p.index))) : undefined}
+              onDeselectAll={!isEditing ? () => setSelectedPlates(new Set()) : undefined}
+              multiSelect={!isEditing}
             />
 
             {/* Printer selection with per-printer mapping — hidden when printer is pre-selected via props */}
@@ -1137,17 +1089,17 @@ export function PrintModal({
                 isLoading={loadingPrinters}
                 allowMultiple={true}
                 showInactive={mode === 'edit-queue-item'}
-                disableBusy={mode === 'reprint'}
+                disableBusy={false}
                 printerMappingResults={multiPrinterMapping.printerResults}
                 filamentReqs={effectiveFilamentReqs}
                 onAutoConfigurePrinter={multiPrinterMapping.autoConfigurePrinter}
                 onUpdatePrinterConfig={multiPrinterMapping.updatePrinterConfig}
-                assignmentMode={mode === 'reprint' ? 'printer' : assignmentMode}
-                onAssignmentModeChange={mode !== 'reprint' ? setAssignmentMode : undefined}
+                assignmentMode={assignmentMode}
+                onAssignmentModeChange={!isEditing ? setAssignmentMode : undefined}
                 targetModel={targetModel}
-                onTargetModelChange={mode !== 'reprint' ? setTargetModel : undefined}
+                onTargetModelChange={!isEditing ? setTargetModel : undefined}
                 targetLocation={targetLocation}
-                onTargetLocationChange={mode !== 'reprint' ? setTargetLocation : undefined}
+                onTargetLocationChange={!isEditing ? setTargetLocation : undefined}
                 slicedForModel={slicedForModel}
               />
             )}
@@ -1210,7 +1162,7 @@ export function PrintModal({
             )}
 
             {/* Print options */}
-            {(mode === 'reprint' || effectivePrinterCount > 0 || (assignmentMode === 'model' && targetModel)) && (
+            {(mode === 'create' || effectivePrinterCount > 0 || (assignmentMode === 'model' && targetModel)) && (
               <PrintOptionsPanel
                 options={printOptions}
                 onChange={setPrintOptions}
@@ -1242,74 +1194,17 @@ export function PrintModal({
               </div>
             )}
 
-            {/* Stagger option for reprint mode with multiple printers */}
-            {mode === 'reprint' && assignmentMode === 'printer' && selectedPrinters.length > 1 && (
-              <div className="space-y-2 pb-2">
-                <div className="flex items-center gap-2">
-                  <input
-                    type="checkbox"
-                    id="staggerEnabledReprint"
-                    checked={scheduleOptions.staggerEnabled}
-                    onChange={(e) => setScheduleOptions({ ...scheduleOptions, staggerEnabled: e.target.checked })}
-                    className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
-                  />
-                  <label htmlFor="staggerEnabledReprint" className="text-sm flex items-center gap-1 text-bambu-gray">
-                    <Layers className="w-3.5 h-3.5" />
-                    {t('printModal.staggerPrinterStarts', 'Stagger printer starts')}
-                  </label>
-                </div>
-                {scheduleOptions.staggerEnabled && (() => {
-                  const groupSize = scheduleOptions.staggerGroupSize;
-                  const interval = scheduleOptions.staggerIntervalMinutes;
-                  const groupCount = Math.ceil(selectedPrinters.length / groupSize);
-                  const totalMinutes = (groupCount - 1) * interval;
-                  return (
-                    <p className="ml-6 text-xs text-bambu-gray">
-                      {t('printModal.staggerPreview', '{{printers}} printers → {{groups}} groups of {{size}}, starting every {{interval}} min', {
-                        printers: selectedPrinters.length,
-                        groups: groupCount,
-                        size: groupSize,
-                        interval,
-                      })}
-                      {groupCount > 1
-                        ? ` (${t('printModal.staggerTotal', 'total: {{minutes}} min', { minutes: totalMinutes })})`
-                        : ''}
-                    </p>
-                  );
-                })()}
-              </div>
-            )}
-
-            {/* Schedule options - only for queue modes */}
-            {mode !== 'reprint' && (
-              <ScheduleOptionsPanel
-                options={scheduleOptions}
-                onChange={setScheduleOptions}
-                dateFormat={settings?.date_format || 'system'}
-                timeFormat={settings?.time_format || 'system'}
-                canControlPrinter={hasPermission('printers:control')}
-                showStagger={mode === 'add-to-queue' && assignmentMode === 'printer' && selectedPrinters.length > 1}
-                printerCount={selectedPrinters.length}
-                hasGcodeSnippets={!!settings?.gcode_snippets}
-              />
-            )}
-
-            {/* G-code injection for reprint mode (only shown when quantity > 1 — applies to queued copies) */}
-            {mode === 'reprint' && !!settings?.gcode_snippets && effectiveQuantity > 1 && (
-              <div className="flex items-center gap-2">
-                <input
-                  type="checkbox"
-                  id="gcodeInjectionReprint"
-                  checked={scheduleOptions.gcodeInjection}
-                  onChange={(e) => setScheduleOptions({ ...scheduleOptions, gcodeInjection: e.target.checked })}
-                  className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
-                />
-                <label htmlFor="gcodeInjectionReprint" className="text-sm flex items-center gap-1 text-bambu-gray">
-                  <Code className="w-3.5 h-3.5" />
-                  {t('printModal.gcodeInjection', 'Inject auto-print G-code')}
-                </label>
-              </div>
-            )}
+            {/* Schedule options */}
+            <ScheduleOptionsPanel
+              options={scheduleOptions}
+              onChange={setScheduleOptions}
+              dateFormat={settings?.date_format || 'system'}
+              timeFormat={settings?.time_format || 'system'}
+              canControlPrinter={hasPermission('printers:control')}
+              showStagger={!isEditing && assignmentMode === 'printer' && selectedPrinters.length > 1}
+              printerCount={selectedPrinters.length}
+              hasGcodeSnippets={!!settings?.gcode_snippets}
+            />
 
             {/* Error message */}
             {updateQueueMutation.isError && (
@@ -1319,7 +1214,7 @@ export function PrintModal({
             )}
 
             {/* Actions */}
-            <div className={`flex gap-3 ${mode === 'reprint' ? '' : 'pt-2'}`}>
+            <div className="flex gap-3 pt-2">
               <Button type="button" variant="secondary" onClick={onClose} className="flex-1" disabled={isSubmitting}>
                 Cancel
               </Button>

+ 7 - 6
frontend/src/components/PrintModal/types.ts

@@ -2,11 +2,10 @@ import type { PrintQueueItem, Printer } from '../../api/client';
 
 /**
  * Mode of operation for the PrintModal.
- * - 'reprint': Immediate print from archive (no schedule options)
- * - 'add-to-queue': Schedule print to queue (includes schedule options)
+ * - 'create': Create a print queue item from an archive or library file
  * - 'edit-queue-item': Edit existing queue item (all options + existing values)
  */
-export type PrintModalMode = 'reprint' | 'add-to-queue' | 'edit-queue-item';
+export type PrintModalMode = 'create' | 'edit-queue-item';
 
 /**
  * Props for the unified PrintModal component.
@@ -66,7 +65,7 @@ export const DEFAULT_PRINT_OPTIONS: PrintOptions = {
 /**
  * Schedule type for queue items.
  */
-export type ScheduleType = 'asap' | 'scheduled' | 'manual';
+export type ScheduleType = 'asap' | 'queue' | 'scheduled';
 
 /**
  * Schedule options for queue items.
@@ -74,6 +73,7 @@ export type ScheduleType = 'asap' | 'scheduled' | 'manual';
 export interface ScheduleOptions {
   scheduleType: ScheduleType;
   scheduledTime: string;
+  requireManualStart: boolean;
   requirePreviousSuccess: boolean;
   autoOffAfter: boolean;
   gcodeInjection: boolean;
@@ -88,6 +88,7 @@ export interface ScheduleOptions {
 export const DEFAULT_SCHEDULE_OPTIONS: ScheduleOptions = {
   scheduleType: 'asap',
   scheduledTime: '',
+  requireManualStart: false,
   requirePreviousSuccess: false,
   autoOffAfter: false,
   gcodeInjection: false,
@@ -140,7 +141,7 @@ export interface PrinterSelectorProps {
   allowMultiple?: boolean;
   /** Show inactive printers (for edit mode where original assignment may be inactive) */
   showInactive?: boolean;
-  /** Disable selection of busy printers (used in reprint mode) */
+  /** Disable selection of busy printers */
   disableBusy?: boolean;
   /** Current assignment mode */
   assignmentMode?: AssignmentMode;
@@ -168,7 +169,7 @@ export interface PlateSelectorProps {
   onToggle: (plateIndex: number) => void;
   onSelectAll?: () => void;
   onDeselectAll?: () => void;
-  /** Whether multi-select (checkboxes) is enabled — true in add-to-queue mode */
+  /** Whether multi-select (checkboxes) is enabled */
   multiSelect?: boolean;
 }
 

+ 2 - 3
frontend/src/components/spoolbuddy/SpoolBuddyLayout.tsx

@@ -25,9 +25,8 @@ export function SpoolBuddyLayout() {
   const location = useLocation();
   const sbState = useSpoolBuddyState();
 
-  // Hide the global toast viewport (background-dispatch progress, etc.) on the
-  // kiosk display. Restore on unmount so navigating back to the main app sees
-  // its toasts again.
+  // Hide the global toast viewport on the kiosk display. Restore on unmount so
+  // navigating back to the main app sees its toasts again.
   const { setViewportSuppressed } = useToast();
   useEffect(() => {
     setViewportSuppressed(true);

+ 24 - 532
frontend/src/contexts/ToastContext.tsx

@@ -1,8 +1,5 @@
-import { AlertCircle, CheckCircle, ChevronDown, ChevronUp, Info, Loader2, X, XCircle } from 'lucide-react';
+import { AlertCircle, CheckCircle, Info, Loader2, X, XCircle } from 'lucide-react';
 import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react';
-import { useTranslation } from 'react-i18next';
-import { api } from '../api/client';
-import { formatFileSize } from '../utils/file';
 
 type ToastType = 'success' | 'error' | 'warning' | 'info' | 'loading';
 
@@ -25,29 +22,6 @@ interface Toast {
   type: ToastType;
   persistent?: boolean;
   action?: ToastAction;
-  dispatchData?: DispatchToastData;
-}
-
-type DispatchJobStatus = 'dispatched' | 'processing' | 'completed' | 'failed' | 'cancelled';
-
-interface DispatchToastJob {
-  jobId: number;
-  sourceName: string;
-  printerName: string;
-  status: DispatchJobStatus;
-  message?: string;
-  uploadBytes?: number;
-  uploadTotalBytes?: number;
-  uploadProgressPct?: number;
-}
-
-interface DispatchToastData {
-  total: number;
-  dispatched: number;
-  processing: number;
-  completed: number;
-  failed: number;
-  jobs: DispatchToastJob[];
 }
 
 interface ToastContextType {
@@ -57,8 +31,7 @@ interface ToastContextType {
   /**
    * Suppress the visible toast viewport while keeping the state machine alive.
    * Used by the SpoolBuddy kiosk layout to keep the kiosk display free of
-   * main-app notifications (background dispatch progress, etc.) without
-   * tearing down the dispatch-job subscription that other tabs rely on.
+   * main-app notifications.
    */
   setViewportSuppressed: (suppressed: boolean) => void;
 }
@@ -90,14 +63,9 @@ const bgColors = {
 };
 
 export function ToastProvider({ children }: { children: ReactNode }) {
-  const { t } = useTranslation();
   const [toasts, setToasts] = useState<Toast[]>([]);
-  const [isDispatchCollapsed, setIsDispatchCollapsed] = useState(false);
   const [viewportSuppressed, setViewportSuppressed] = useState(false);
-  const [cancellingDispatchJobIds, setCancellingDispatchJobIds] = useState<Set<number>>(new Set());
   const timeoutRefs = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
-  const dispatchToastId = 'background-dispatch';
-  const lastDispatchSummaryRef = useRef<string | null>(null);
   // Tracks whether the provider is still mounted. A toast can be triggered by
   // an async callback that resolves AFTER React has unmounted us (common in
   // tests: `cleanup()` runs while a login promise is still in flight, then
@@ -160,349 +128,6 @@ export function ToastProvider({ children }: { children: ReactNode }) {
     setToasts((prev) => prev.filter((t) => t.id !== id));
   }, []);
 
-  const cancelDispatchJob = useCallback(async (jobId: number) => {
-    setCancellingDispatchJobIds((prev) => {
-      const next = new Set(prev);
-      next.add(jobId);
-      return next;
-    });
-
-    try {
-      const result = await api.cancelBackgroundDispatchJob(jobId);
-      showToast(
-        result.status === 'cancelling'
-          ? t('backgroundDispatch.toast.cancellingUpload')
-          : t('backgroundDispatch.toast.cancelled'),
-        'info'
-      );
-    } catch (error) {
-      const message = error instanceof Error ? error.message : t('backgroundDispatch.toast.cancelFailed');
-      showToast(message, 'error');
-    } finally {
-      setCancellingDispatchJobIds((prev) => {
-        const next = new Set(prev);
-        next.delete(jobId);
-        return next;
-      });
-    }
-  }, [showToast, t]);
-
-  useEffect(() => {
-    interface DispatchEventDetail {
-      total?: number;
-      dispatched?: number;
-      processing?: number;
-      completed?: number;
-      failed?: number;
-      dispatched_jobs?: Array<{
-        job_id: number;
-        source_name?: string;
-        printer_name?: string;
-      }>;
-      active_job?: {
-        job_id?: number;
-        printer_name?: string;
-        source_name?: string;
-        message?: string;
-        upload_bytes?: number;
-        upload_total_bytes?: number;
-        upload_progress_pct?: number;
-      } | null;
-      active_jobs?: Array<{
-        job_id?: number;
-        printer_name?: string;
-        source_name?: string;
-        message?: string;
-        upload_bytes?: number;
-        upload_total_bytes?: number;
-        upload_progress_pct?: number;
-      }>;
-      recent_event?: {
-        status?: string;
-        job_id?: number;
-        source_name?: string;
-        printer_name?: string;
-        message?: string;
-      };
-    }
-
-    const updateJob = (
-      jobs: DispatchToastJob[],
-      jobId: number,
-      next: Partial<DispatchToastJob> & {
-        status: DispatchJobStatus;
-        sourceName: string;
-        printerName: string;
-      }
-    ) => {
-      const index = jobs.findIndex((job) => job.jobId === jobId);
-      if (index === -1) {
-        return [...jobs, { jobId, ...next }];
-      }
-      const copy = [...jobs];
-      copy[index] = {
-        ...copy[index],
-        ...next,
-      };
-      return copy;
-    };
-
-    const statusWeight = (status: DispatchJobStatus) => {
-      switch (status) {
-        case 'failed':
-          return 0;
-        case 'processing':
-          return 1;
-        case 'dispatched':
-          return 2;
-        case 'completed':
-          return 3;
-        case 'cancelled':
-          return 4;
-      }
-    };
-
-    const onDispatchEvent = (event: Event) => {
-      const detail = (event as CustomEvent<DispatchEventDetail>).detail || {};
-      const total = detail.total ?? 0;
-      const dispatched = detail.dispatched ?? 0;
-      const processing = detail.processing ?? 0;
-      const completed = detail.completed ?? 0;
-      const failed = detail.failed ?? 0;
-
-      const hasActiveWork = dispatched + processing > 0;
-      const allDone = total > 0 && completed + failed >= total && !hasActiveWork;
-      const recentStatus = detail.recent_event?.status;
-
-      // Once any print starts successfully, dismiss the dispatch toast (#615)
-      // Remaining jobs continue in the background silently
-      if (recentStatus === 'completed' && completed > 0) {
-        const summaryKey = `first-complete:${completed}:${failed}`;
-        if (lastDispatchSummaryRef.current !== summaryKey) {
-          lastDispatchSummaryRef.current = summaryKey;
-
-          const remaining = total - completed - failed;
-          const doneMessage = remaining > 0
-            ? t('backgroundDispatch.toast.printStartedRemaining', { completed, remaining })
-            : failed > 0
-              ? t('backgroundDispatch.toast.completeWithFailures', { completed, failed })
-              : t('backgroundDispatch.toast.completeSuccess', { completed });
-
-          setToasts((prev) => {
-            const doneToast: Toast = {
-              id: dispatchToastId,
-              message: doneMessage,
-              type: failed > 0 ? 'warning' : 'success',
-              persistent: true,
-            };
-            const exists = prev.find((toastItem) => toastItem.id === dispatchToastId);
-            if (exists) {
-              return prev.map((toastItem) =>
-                toastItem.id === dispatchToastId ? doneToast : toastItem
-              );
-            }
-            return [...prev, doneToast];
-          });
-
-          const existingTimeout = timeoutRefs.current.get(dispatchToastId);
-          if (existingTimeout) clearTimeout(existingTimeout);
-          const timeout = setTimeout(() => {
-            if (!isMountedRef.current) return;
-            setToasts((prev) => prev.filter((t) => t.id !== dispatchToastId));
-            timeoutRefs.current.delete(dispatchToastId);
-            lastDispatchSummaryRef.current = null;
-          }, 3000);
-          timeoutRefs.current.set(dispatchToastId, timeout);
-        }
-        return;
-      }
-
-      if (hasActiveWork) {
-        // New batch starting — reset dedup guard so completion toast works
-        lastDispatchSummaryRef.current = null;
-        setToasts((prev) => {
-          const existing = prev.find((toastItem) => toastItem.id === dispatchToastId);
-          const existingJobs = existing?.dispatchData?.jobs || [];
-
-          const dispatchedJobs: DispatchToastJob[] = (detail.dispatched_jobs || []).map((job) => ({
-            jobId: job.job_id,
-            sourceName: job.source_name || t('backgroundDispatch.unknownFile'),
-            printerName: job.printer_name || t('backgroundDispatch.unknownPrinter'),
-            status: 'dispatched',
-          }));
-
-          const activeJobsPayload =
-            detail.active_jobs && detail.active_jobs.length > 0
-              ? detail.active_jobs
-              : detail.active_job?.job_id
-                ? [detail.active_job]
-                : [];
-
-          const activeJobs: DispatchToastJob[] = activeJobsPayload
-            .filter((job) => typeof job.job_id === 'number')
-            .map((job) => ({
-              jobId: job.job_id as number,
-              sourceName: job.source_name || t('backgroundDispatch.unknownFile'),
-              printerName: job.printer_name || t('backgroundDispatch.unknownPrinter'),
-              status: 'processing',
-              message: job.message,
-              uploadBytes: job.upload_bytes,
-              uploadTotalBytes: job.upload_total_bytes,
-              uploadProgressPct: job.upload_progress_pct,
-            }));
-
-          const activeIds = new Set([...dispatchedJobs, ...activeJobs].map((job) => job.jobId));
-          const historicalJobs = existingJobs.filter(
-            (job) => !activeIds.has(job.jobId) && ['completed', 'failed', 'cancelled'].includes(job.status)
-          );
-
-          let jobs = [...dispatchedJobs, ...activeJobs, ...historicalJobs];
-
-          if (detail.recent_event?.job_id && detail.recent_event?.status) {
-            const rawStatus = detail.recent_event.status;
-            const eventStatus = (
-              rawStatus === 'cancelled' ? 'cancelled' : rawStatus === 'cancelling' ? 'processing' : rawStatus
-            ) as DispatchJobStatus;
-            const sourceName = detail.recent_event.source_name || t('backgroundDispatch.unknownFile');
-            const printerName = detail.recent_event.printer_name || t('backgroundDispatch.unknownPrinter');
-            jobs = updateJob(jobs, detail.recent_event.job_id, {
-              status: eventStatus,
-              sourceName,
-              printerName,
-              message: detail.recent_event.message,
-            });
-          }
-
-          activeJobs.forEach((activeJob) => {
-            jobs = updateJob(jobs, activeJob.jobId, {
-              status: 'processing',
-              sourceName: activeJob.sourceName,
-              printerName: activeJob.printerName,
-              message: activeJob.message,
-              uploadBytes: activeJob.uploadBytes,
-              uploadTotalBytes: activeJob.uploadTotalBytes,
-              uploadProgressPct: activeJob.uploadProgressPct,
-            });
-          });
-
-          const dispatchData: DispatchToastData = {
-            total,
-            dispatched,
-            processing,
-            completed,
-            failed,
-            jobs: [...jobs].sort((a, b) => {
-              const byStatus = statusWeight(a.status) - statusWeight(b.status);
-              if (byStatus !== 0) {
-                return byStatus;
-              }
-              return a.jobId - b.jobId;
-            }),
-          };
-
-          const exists = prev.find((toastItem) => toastItem.id === dispatchToastId);
-          if (exists) {
-            return prev.map((toastItem) =>
-              toastItem.id === dispatchToastId
-                ? {
-                    ...toastItem,
-                    message: t('backgroundDispatch.startingPrints'),
-                    type: 'loading',
-                    persistent: true,
-                    dispatchData,
-                  }
-                : toastItem
-            );
-          }
-          return [
-            ...prev,
-            {
-              id: dispatchToastId,
-              message: t('backgroundDispatch.startingPrints'),
-              type: 'loading',
-              persistent: true,
-              dispatchData,
-            },
-          ];
-        });
-        return;
-      }
-
-      if (allDone) {
-        const summaryKey = `${completed}:${failed}`;
-        if (lastDispatchSummaryRef.current === summaryKey) {
-          return;
-        }
-        lastDispatchSummaryRef.current = summaryKey;
-
-        const doneMessage = failed > 0
-          ? t('backgroundDispatch.toast.completeWithFailures', { completed, failed })
-          : t('backgroundDispatch.toast.completeSuccess', { completed });
-
-        // Show a brief "completed" state on the dispatch toast before replacing with summary
-        // This ensures the user sees confirmation even for fast uploads (#615)
-        setToasts((prev) => {
-          const doneToast: Toast = {
-            id: dispatchToastId,
-            message: doneMessage,
-            type: failed > 0 ? 'warning' : 'success',
-            persistent: true,
-            // Clear dispatchData so it renders as a simple text toast
-          };
-          const exists = prev.find((toastItem) => toastItem.id === dispatchToastId);
-          if (exists) {
-            return prev.map((toastItem) =>
-              toastItem.id === dispatchToastId ? doneToast : toastItem
-            );
-          }
-          return [...prev, doneToast];
-        });
-
-        // Auto-dismiss after 3 seconds
-        const existingTimeout = timeoutRefs.current.get(dispatchToastId);
-        if (existingTimeout) clearTimeout(existingTimeout);
-        const timeout = setTimeout(() => {
-          setToasts((prev) => prev.filter((t) => t.id !== dispatchToastId));
-          timeoutRefs.current.delete(dispatchToastId);
-          lastDispatchSummaryRef.current = null;
-        }, 3000);
-        timeoutRefs.current.set(dispatchToastId, timeout);
-        return;
-      }
-
-      if (!hasActiveWork && recentStatus && ['cancelled', 'failed', 'completed', 'idle'].includes(recentStatus)) {
-        setToasts((prev) => prev.filter((t) => t.id !== dispatchToastId));
-        lastDispatchSummaryRef.current = null;
-      }
-
-      if (detail.recent_event?.status === 'idle' && !hasActiveWork) {
-        setToasts((prev) => prev.filter((t) => t.id !== dispatchToastId));
-        lastDispatchSummaryRef.current = null;
-      }
-
-      if (!hasActiveWork) {
-        setCancellingDispatchJobIds(new Set());
-      }
-
-      if (detail.dispatched_jobs) {
-        const dispatchedIds = new Set(detail.dispatched_jobs.map((job) => job.job_id));
-        setCancellingDispatchJobIds((prev) => {
-          const next = new Set<number>();
-          prev.forEach((id) => {
-            if (dispatchedIds.has(id)) {
-              next.add(id);
-            }
-          });
-          return next;
-        });
-      }
-    };
-
-    window.addEventListener('background-dispatch', onDispatchEvent);
-    return () => window.removeEventListener('background-dispatch', onDispatchEvent);
-  }, [t]);
-
-
   return (
     <ToastContext.Provider value={{ showToast, showPersistentToast, dismissToast, setViewportSuppressed }}>
       {children}
@@ -514,163 +139,30 @@ export function ToastProvider({ children }: { children: ReactNode }) {
         {toasts.map((toast) => (
           <div
             key={toast.id}
-            className={`rounded-lg border shadow-lg backdrop-blur-sm animate-slide-in ${bgColors[toast.type]} ${
-              toast.dispatchData ? 'w-[420px] p-3' : 'flex items-center gap-3 px-4 py-3'
-            }`}
+            className={`rounded-lg border shadow-lg backdrop-blur-sm animate-slide-in ${bgColors[toast.type]} flex items-center gap-3 px-4 py-3`}
           >
-            {toast.dispatchData ? (
-              <>
-                <div className="flex items-start justify-between gap-3">
-                  <div className="flex items-start gap-2">
-                    {icons[toast.type]}
-                    <div>
-                      <p className="text-white text-sm font-medium">{t('backgroundDispatch.startingPrints')}</p>
-                      <p className="text-xs text-bambu-gray mt-0.5">
-                        {t('backgroundDispatch.progressSummary', {
-                          complete: toast.dispatchData.completed + toast.dispatchData.failed,
-                          total: toast.dispatchData.total,
-                          dispatched: toast.dispatchData.dispatched,
-                          processing: toast.dispatchData.processing,
-                        })}
-                      </p>
-                    </div>
-                  </div>
-                  <div className="flex items-center gap-1">
-                    <button
-                      onClick={() => setIsDispatchCollapsed((prev) => !prev)}
-                      className="text-bambu-gray hover:text-white transition-colors"
-                      aria-label={
-                        isDispatchCollapsed
-                          ? t('backgroundDispatch.expandDetails')
-                          : t('backgroundDispatch.collapseDetails')
-                      }
-                    >
-                      {isDispatchCollapsed ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
-                    </button>
-                    <button
-                      onClick={() => dismissToast(toast.id)}
-                      className="text-bambu-gray hover:text-white transition-colors"
-                      aria-label={t('backgroundDispatch.dismissToast')}
-                    >
-                      <X className="w-4 h-4" />
-                    </button>
-                  </div>
-                </div>
-
-                {!isDispatchCollapsed && (
-                  <div className="mt-3 space-y-2 max-h-64 overflow-y-auto pr-1">
-                    {toast.dispatchData.jobs.map((job) => {
-                      const progressByStatus: Record<DispatchJobStatus, number> = {
-                        dispatched: 15,
-                        processing: 60,
-                        completed: 100,
-                        failed: 100,
-                        cancelled: 100,
-                      };
-                      // Upload byte count reached the total — the printer hasn't yet
-                      // confirmed it received the file (state is still 'processing').
-                      // Without distinguishing this we show a frozen 100% bar that
-                      // reads as "stuck" on small files where the upload completed
-                      // in <500ms.
-                      const uploadDoneAwaitingPrinter =
-                        job.status === 'processing' &&
-                        typeof job.uploadProgressPct === 'number' &&
-                        job.uploadProgressPct >= 99.9;
-                      const barColorByStatus: Record<DispatchJobStatus, string> = {
-                        dispatched: 'bg-bambu-gray/60',
-                        processing: 'bg-bambu-green',
-                        completed: 'bg-green-500',
-                        failed: 'bg-red-500',
-                        cancelled: 'bg-yellow-500',
-                      };
-                      return (
-                        <div key={job.jobId} className="rounded border border-white/10 bg-black/15 p-2">
-                          <div className="flex items-center justify-between gap-2">
-                            <span className="text-xs text-white truncate" title={job.sourceName}>
-                              {job.sourceName}
-                            </span>
-                            <div className="flex items-center gap-2">
-                              {(job.status === 'dispatched' || job.status === 'processing') && (
-                                <button
-                                  onClick={() => void cancelDispatchJob(job.jobId)}
-                                  disabled={cancellingDispatchJobIds.has(job.jobId)}
-                                  className="text-[11px] text-red-300 hover:text-red-200 disabled:opacity-50 disabled:cursor-not-allowed"
-                                  title={t('backgroundDispatch.cancelDispatchJob')}
-                                >
-                                  {cancellingDispatchJobIds.has(job.jobId)
-                                    ? t('backgroundDispatch.cancelling')
-                                    : t('backgroundDispatch.cancel')}
-                                </button>
-                              )}
-                              <span className="text-[11px] uppercase tracking-wide text-bambu-gray">
-                                {t(`backgroundDispatch.status.${job.status}`)}
-                              </span>
-                            </div>
-                          </div>
-                          <div className="text-[11px] text-bambu-gray truncate" title={job.printerName}>
-                            {job.printerName}
-                          </div>
-                          {job.message && (
-                            <div className="text-[11px] text-bambu-gray truncate" title={job.message}>
-                              {job.message}
-                            </div>
-                          )}
-                          {job.status === 'processing' && (
-                            uploadDoneAwaitingPrinter ? (
-                              <div className="text-[11px] text-bambu-gray truncate">
-                                {t('backgroundDispatch.awaitingPrinter')}
-                              </div>
-                            ) : typeof job.uploadBytes === 'number' && typeof job.uploadTotalBytes === 'number' && job.uploadTotalBytes > 0 ? (
-                              <div className="text-[11px] text-bambu-gray truncate">
-                                {formatFileSize(job.uploadBytes)} / {formatFileSize(job.uploadTotalBytes)}
-                                {typeof job.uploadProgressPct === 'number' ? ` (${job.uploadProgressPct.toFixed(1)}%)` : ''}
-                              </div>
-                            ) : null
-                          )}
-                          <div className="mt-1 h-1.5 w-full rounded bg-white/10 overflow-hidden">
-                            <div
-                              className={`h-full ${barColorByStatus[job.status]} transition-all duration-300 ${uploadDoneAwaitingPrinter ? 'animate-pulse' : ''}`}
-                              style={{
-                                width: `${
-                                  job.status === 'processing' && typeof job.uploadProgressPct === 'number'
-                                    ? Math.max(0, Math.min(100, job.uploadProgressPct))
-                                    : progressByStatus[job.status]
-                                }%`,
-                              }}
-                            />
-                          </div>
-                        </div>
-                      );
-                    })}
-                  </div>
-                )}
-              </>
-            ) : (
-              <>
-                {icons[toast.type]}
-                <span className="text-white text-sm">{toast.message}</span>
-                {toast.action && (
-                  <a
-                    href={toast.action.href}
-                    target="_blank"
-                    rel="noopener noreferrer"
-                    onClick={() => {
-                      toast.action?.onClick?.();
-                      dismissToast(toast.id);
-                    }}
-                    className="ml-2 px-2 py-1 rounded text-xs font-medium bg-bambu-green/20 text-bambu-green hover:bg-bambu-green/30 whitespace-nowrap"
-                  >
-                    {toast.action.label}
-                  </a>
-                )}
-                <button
-                  onClick={() => dismissToast(toast.id)}
-                  className="ml-2 text-bambu-gray hover:text-white transition-colors"
-                >
-                  <X className="w-4 h-4" />
-                </button>
-              </>
+            {icons[toast.type]}
+            <span className="text-white text-sm">{toast.message}</span>
+            {toast.action && (
+              <a
+                href={toast.action.href}
+                target="_blank"
+                rel="noopener noreferrer"
+                onClick={() => {
+                  toast.action?.onClick?.();
+                  dismissToast(toast.id);
+                }}
+                className="ml-2 px-2 py-1 rounded text-xs font-medium bg-bambu-green/20 text-bambu-green hover:bg-bambu-green/30 whitespace-nowrap"
+              >
+                {toast.action.label}
+              </a>
             )}
+            <button
+              onClick={() => dismissToast(toast.id)}
+              className="ml-2 text-bambu-gray hover:text-white transition-colors"
+            >
+              <X className="w-4 h-4" />
+            </button>
           </div>
         ))}
       </div>

+ 0 - 85
frontend/src/hooks/useDispatchedPrinterIds.ts

@@ -1,85 +0,0 @@
-/**
- * Subscribes to background-dispatch WebSocket events and returns the set of
- * printer IDs that currently have a queued or active dispatch job.
- *
- * Used by PrinterSelector to disable printers between the moment Bambuddy
- * accepts a dispatch (FTP upload, print command) and the moment the printer
- * itself reports PRINT_START. The backend already rejects double-sends with
- * HTTP 409, but the UI gap still let operators pick a printer the server would
- * refuse — surfaced by a corporate user running multi-operator farm shifts.
- *
- * Module-level state + useSyncExternalStore so every PrinterSelector instance
- * sees the same snapshot, and component mounts mid-batch pick up the latest
- * state without re-fetching.
- */
-import { useSyncExternalStore } from 'react';
-
-interface DispatchEventJob {
-  printer_id?: unknown;
-}
-
-interface DispatchEventDetail {
-  dispatched_jobs?: DispatchEventJob[];
-  active_jobs?: DispatchEventJob[];
-  total?: number;
-  dispatched?: number;
-  processing?: number;
-}
-
-const EMPTY: ReadonlySet<number> = new Set();
-let currentSet: ReadonlySet<number> = EMPTY;
-const subscribers = new Set<() => void>();
-let attached = false;
-
-function recompute(detail: DispatchEventDetail): ReadonlySet<number> {
-  const next = new Set<number>();
-  for (const job of detail.dispatched_jobs ?? []) {
-    if (typeof job.printer_id === 'number') next.add(job.printer_id);
-  }
-  for (const job of detail.active_jobs ?? []) {
-    if (typeof job.printer_id === 'number') next.add(job.printer_id);
-  }
-  return next;
-}
-
-function handleEvent(event: Event) {
-  const detail = (event as CustomEvent<DispatchEventDetail>).detail ?? {};
-  const next = recompute(detail);
-  // Keep reference stable when content didn't change — useSyncExternalStore
-  // compares snapshots via Object.is and re-renders on any new reference.
-  if (next.size === currentSet.size && [...next].every((id) => currentSet.has(id))) {
-    return;
-  }
-  currentSet = next;
-  subscribers.forEach((cb) => cb());
-}
-
-function ensureAttached() {
-  if (attached || typeof window === 'undefined') return;
-  window.addEventListener('background-dispatch', handleEvent);
-  attached = true;
-}
-
-const subscribe = (callback: () => void): (() => void) => {
-  ensureAttached();
-  subscribers.add(callback);
-  return () => {
-    subscribers.delete(callback);
-  };
-};
-
-const getSnapshot = (): ReadonlySet<number> => currentSet;
-
-export function useDispatchedPrinterIds(): ReadonlySet<number> {
-  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
-}
-
-/** Test-only helper — resets the module-level singleton between tests. */
-export function __resetDispatchedPrinterIdsForTests(): void {
-  currentSet = EMPTY;
-  subscribers.clear();
-  if (attached && typeof window !== 'undefined') {
-    window.removeEventListener('background-dispatch', handleEvent);
-    attached = false;
-  }
-}

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

@@ -342,14 +342,6 @@ export function useWebSocket() {
         break;
       }
 
-      case 'background_dispatch':
-        window.dispatchEvent(
-          new CustomEvent('background-dispatch', {
-            detail: (message as unknown as { data?: Record<string, unknown> }).data || {},
-          })
-        );
-        break;
-
       case 'spoolbuddy_weight':
         window.dispatchEvent(new CustomEvent('spoolbuddy-weight', { detail: message }));
         break;

+ 4 - 2
frontend/src/i18n/index.ts

@@ -72,7 +72,9 @@ i18n
  */
 function applyApplianceLocale() {
   if (typeof window === 'undefined' || !window.localStorage) return;
-  if (window.localStorage.getItem(APPLIANCE_CONSUMED_KEY)) return;
+  const storage = window.localStorage;
+  if (typeof storage.getItem !== 'function' || typeof storage.setItem !== 'function') return;
+  if (storage.getItem(APPLIANCE_CONSUMED_KEY)) return;
 
   fetch('/api/v1/system/appliance')
     .then((r) => (r.ok ? r.json() : null))
@@ -80,7 +82,7 @@ function applyApplianceLocale() {
       if (!data || typeof data.locale !== 'string') return;
       if (!SUPPORTED_LNGS.includes(data.locale)) return;
       i18n.changeLanguage(data.locale);
-      window.localStorage.setItem(APPLIANCE_CONSUMED_KEY, '1');
+      storage.setItem(APPLIANCE_CONSUMED_KEY, '1');
     })
     .catch(() => {
       // Endpoint absent or unreachable — non-appliance install or dev environment.

+ 59 - 49
frontend/src/i18n/locales/de.ts

@@ -204,12 +204,21 @@ export default {
       snap: 'Foto',
       off: 'Aus',
       summary: '{{live}} live, {{snap}} Schnappschüsse, {{total}} insgesamt',
+      layer: 'Schicht {{cur}}/{{total}}',
+      timeLeft: 'noch {{time}}',
+      statusMode: {
+        off: 'Aus',
+        compact: 'Kompakt',
+        full: 'Voll',
+      },
       settings: {
         title: 'Kamera-Wand-Einstellungen',
         maxLive: 'Max. Live-Streams',
         maxLiveHint: 'Wie viele Kacheln gleichzeitig live streamen. Andere aktualisieren als Schnappschüsse.',
         snapshotInterval: 'Schnappschuss-Intervall (Sekunden)',
         snapshotIntervalHint: 'Wie oft Nicht-Live-Kacheln einen neuen Schnappschuss abrufen.',
+        statusOverlay: 'Status-Overlay',
+        statusOverlayHint: 'Kompakt: nur Status-Plakette. Voll: + Fortschritt, Schicht, Restzeit.',
       },
     },
     // Controls
@@ -747,7 +756,6 @@ export default {
     printTime: 'Druckzeit',
     filamentUsed: 'Verbrauchtes Filament',
     cost: 'Kosten',
-    reprint: 'Drucken',
     preview: 'Vorschau',
     deleteArchive: 'Archiv löschen',
     deleteConfirm: 'Möchten Sie dieses Archiv wirklich löschen?',
@@ -794,7 +802,6 @@ export default {
     },
     menu: {
       print: 'Drucken',
-      schedule: 'Planen',
       openInBambuStudio: 'Im Slicer öffnen',
       slice: 'Slicen',
       externalLink: 'Externer Link',
@@ -889,9 +896,6 @@ export default {
       noFileForReprint: 'Keine 3MF-Datei verfügbar — die Datei konnte beim Aufzeichnen des Drucks nicht vom Drucker heruntergeladen werden',
       noPermissionEdit: 'Sie haben keine Berechtigung, Archive zu bearbeiten',
       noPermissionDelete: 'Sie haben keine Berechtigung, Archive zu löschen',
-      reprint: 'Drucken',
-      schedulePrint: 'Druck planen',
-      schedule: 'Planen',
       openInBambuStudio: 'Im Slicer öffnen',
       openInBambuStudioToSlice: 'Im Slicer öffnen zum Slicen',
       slice: 'Slicen',
@@ -1032,18 +1036,12 @@ export default {
     },
     title: 'Druckwarteschlange',
     subtitle: 'Planen und verwalten Sie Ihre Druckaufträge',
-    addToQueue: 'Zur Warteschlange hinzufügen',
     // Print modal
-    print: 'Drucken',
-    reprint: 'Erneut drucken',
-    schedulePrint: 'Druck planen',
     editQueueItem: 'Warteschlangeneintrag bearbeiten',
-    printToPrinters: 'Auf {{count}} Druckern drucken',
-    queueToPrinters: 'Zu {{count}} Druckern hinzufügen',
-    queueSelectedPlates: '{{count}} Platten in die Warteschlange',
     selectAllPlates: 'Alle {{count}} Platten auswählen',
     deselectAll: 'Alle abwählen',
     printQueued: 'Druck in Warteschlange',
+    printQueuedWillStartWhenIdle: 'Startet, sobald der Drucker im Leerlauf ist',
     itemsQueued: '{{count}} Einträge in Warteschlange',
     sending: 'Wird gesendet...',
     sendingProgress: 'Sende {{current}}/{{total}}...',
@@ -1185,8 +1183,8 @@ export default {
     },
     // Actions
     actions: {
-      stopPrint: 'Druck stoppen',
       startPrint: 'Druck starten',
+      stopPrint: 'Druck stoppen',
       requeue: 'Erneut einreihen',
     },
     // Bulk edit
@@ -1304,35 +1302,6 @@ export default {
     },
   },
 
-  backgroundDispatch: {
-    unknownFile: 'Unbekannte Datei',
-    unknownPrinter: 'Unbekannter Drucker',
-    startingPrints: 'Starte Drucke',
-    progressSummary: '{{complete}}/{{total}} abgeschlossen • Geplant: {{dispatched}} • In Bearbeitung: {{processing}}',
-    expandDetails: 'Dispatch-Details ausklappen',
-    collapseDetails: 'Dispatch-Details einklappen',
-    dismissToast: 'Dispatch-Hinweis schließen',
-    cancelDispatchJob: 'Dispatch-Job abbrechen',
-    cancel: 'Abbrechen',
-    cancelling: 'Wird abgebrochen…',
-    awaitingPrinter: 'Warte auf Drucker…',
-    status: {
-      dispatched: 'Geplant',
-      processing: 'In Bearbeitung',
-      completed: 'Abgeschlossen',
-      failed: 'Fehlgeschlagen',
-      cancelled: 'Abgebrochen',
-    },
-    toast: {
-      cancellingUpload: 'Upload wird abgebrochen...',
-      cancelled: 'Dispatch abgebrochen',
-      cancelFailed: 'Dispatch konnte nicht abgebrochen werden',
-      completeWithFailures: 'Background Dispatch abgeschlossen: {{completed}} erfolgreich, {{failed}} fehlgeschlagen',
-      completeSuccess: 'Background Dispatch abgeschlossen: {{completed}} erfolgreich',
-      printStartedRemaining: '{{completed}} Druck(e) gestartet, {{remaining}} weitere werden gesendet...',
-    },
-  },
-
   // Statistics page
   stats: {
     title: 'Statistiken',
@@ -2626,6 +2595,42 @@ export default {
     clearErrors: 'Fehler löschen',
     clearSuccess: 'HMS-Fehler gelöscht',
     clearFailed: 'HMS-Fehler konnten nicht gelöscht werden',
+    actionSuccess: 'Aktion an Drucker gesendet',
+    actionFailed: 'Aktion konnte nicht gesendet werden',
+    actions: {
+      RESUME_PRINTING: 'Druck fortsetzen',
+      RESUME_PRINTING_DEFECTS: 'Fortsetzen (Mängel akzeptabel)',
+      RESUME_PRINTING_PROBELM_SOLVED: 'Fortsetzen (Problem gelöst)',
+      STOP_PRINTING: 'Druck stoppen',
+      CHECK_ASSISTANT: 'Assistent öffnen',
+      FILAMENT_EXTRUDED: 'Filament extrudiert, weiter',
+      RETRY_FILAMENT_EXTRUDED: 'Noch nicht extrudiert, erneut',
+      CONTINUE: 'Fertig, weiter',
+      LOAD_VIRTUAL_TRAY: 'Filament laden',
+      OK_BUTTON: 'OK',
+      FILAMENT_LOAD_RESUME: 'Filament geladen, fortsetzen',
+      JUMP_TO_LIVEVIEW: 'Live-Ansicht öffnen',
+      NO_REMINDER_NEXT_TIME: 'Nicht mehr erinnern',
+      REFRESH_NOZZLE: 'Erneut prüfen',
+      IGNORE_NO_REMINDER_NEXT_TIME: 'Ignorieren und nicht mehr erinnern',
+      IGNORE_RESUME: 'Ignorieren und fortsetzen',
+      PROBLEM_SOLVED_RESUME: 'Problem gelöst, fortsetzen',
+      TURN_OFF_FIRE_ALARM: 'Verstanden, Brandalarm ausschalten',
+      RETRY_PROBLEM_SOLVED: 'Erneut versuchen (Problem gelöst)',
+      CANCLE: 'Abbrechen',
+      STOP_DRYING: 'Trocknen stoppen',
+      PROCEED: 'Fortfahren',
+      OK_JUMP_RACK: 'OK',
+      ABORT: 'Abbrechen',
+      DISABLE_PURIFICATION: 'Luftreinigung für diesen Druck deaktivieren',
+      DONT_REMIND_NEXT_TIME: 'Nicht mehr erinnern',
+      DBL_CHECK_CANCEL: 'Abbrechen',
+      DBL_CHECK_DONE: 'Fertig',
+      DBL_CHECK_RETRY: 'Erneut versuchen',
+      DBL_CHECK_RESUME: 'Fortsetzen',
+      DBL_CHECK_OK: 'Bestätigen',
+      REMOVE_CLOSE_BTN: 'Schließen',
+    },
   },
 
   // MQTT Debug modal
@@ -3354,8 +3359,6 @@ export default {
     changeLink: 'Verknüpfung ändern...',
     linkTo: 'Verknüpfen mit...',
     linkToProjectOrArchive: 'Mit Projekt oder Archiv verknüpfen',
-    addToQueue: 'Zur Warteschlange',
-    schedulePrint: 'Planen',
     generateThumbnail: 'Vorschaubild generieren',
     generateThumbnails: 'Vorschaubilder generieren',
     generateThumbnailsForMissing: 'Vorschaubilder für STL-Dateien ohne Vorschau generieren',
@@ -3658,8 +3661,6 @@ export default {
       fileCount: '{{count}} Datei(en)',
       empty: 'Keine Ordner verknüpft. Gehen Sie zum Dateimanager und verknüpfen Sie einen Ordner mit diesem Projekt.',
       noFiles: 'Keine Dateien in diesem Ordner.',
-      print: 'Jetzt drucken',
-      addToQueue: 'Zur Warteschlange',
     },
     bom: {
       title: 'Stückliste',
@@ -4298,7 +4299,6 @@ export default {
 
   // Print modal
   printModal: {
-    title: 'Druck starten',
     selectPrinter: 'Drucker auswählen',
     selectPlate: 'Platte auswählen',
     filamentMapping: 'Filamentzuordnung',
@@ -4310,15 +4310,25 @@ export default {
     vibrationCalibration: 'Vibrations-Kalibrierung',
     layerInspection: 'Erste-Schicht-Prüfung',
     timelapse: 'Zeitraffer',
-    startPrint: 'Druck starten',
-    addToQueue: 'Zur Warteschlange hinzufügen',
     cancel: 'Abbrechen',
     noPrintersAvailable: 'Keine Drucker verfügbar',
     printerBusy: 'Drucker ist beschäftigt',
     printerOffline: 'Drucker ist offline',
     sameTypeDifferentColor: 'Gleicher Typ, andere Farbe',
     filamentTypeNotLoaded: 'Filamenttyp nicht geladen',
+    whenToPrint: 'Wann drucken',
+    asap: 'Sofort',
+    queue: 'Warteschlange',
+    schedule: 'Planen',
+    dateTime: 'Datum & Uhrzeit',
+    invalidDateTime: 'Bitte ein gültiges Datum und eine gültige Uhrzeit eingeben',
     openCalendar: 'Kalender öffnen',
+    requireManualStart: 'Manuellen Start erfordern',
+    requirePreviousSuccess: 'Nur starten, wenn der vorherige Druck erfolgreich war',
+    autoOffAfter: 'Drucker nach Abschluss ausschalten',
+    helpAsap: 'Der Druck wird oben in die Warteschlange eingefügt und startet, sobald ein geeigneter Drucker im Leerlauf ist.',
+    helpSchedule: 'Der Druck startet zur geplanten Zeit, wenn der Drucker im Leerlauf ist. Wenn er belegt ist, wartet er, bis der Drucker verfügbar ist.',
+    helpQueue: 'Der Druck wird hinten in die Warteschlange eingefügt.',
     leftNozzle: 'L',
     rightNozzle: 'R',
     leftNozzleTooltip: 'Linke Düse',

+ 59 - 49
frontend/src/i18n/locales/en.ts

@@ -204,12 +204,21 @@ export default {
       snap: 'Snap',
       off: 'Off',
       summary: '{{live}} live, {{snap}} snapshots, {{total}} total',
+      layer: 'Layer {{cur}}/{{total}}',
+      timeLeft: '{{time}} left',
+      statusMode: {
+        off: 'Off',
+        compact: 'Compact',
+        full: 'Full',
+      },
       settings: {
         title: 'Cam wall settings',
         maxLive: 'Max live streams',
         maxLiveHint: 'How many tiles stream live at once. Others refresh as snapshots.',
         snapshotInterval: 'Snapshot interval (seconds)',
         snapshotIntervalHint: 'How often non-live tiles fetch a fresh snapshot.',
+        statusOverlay: 'Status overlay',
+        statusOverlayHint: 'Compact: state badge only. Full: + progress, layer, time left.',
       },
     },
     // Controls
@@ -751,7 +760,6 @@ export default {
     printTime: 'Print Time',
     filamentUsed: 'Filament Used',
     cost: 'Cost',
-    reprint: 'Reprint',
     preview: 'Preview',
     deleteArchive: 'Delete Archive',
     deleteConfirm: 'Are you sure you want to delete this archive?',
@@ -798,7 +806,6 @@ export default {
     },
     menu: {
       print: 'Print',
-      schedule: 'Schedule',
       openInBambuStudio: 'Open in Slicer',
       slice: 'Slice',
       externalLink: 'External Link',
@@ -893,9 +900,6 @@ export default {
       noFileForReprint: 'No 3MF file available — the file could not be downloaded from the printer when the print was recorded',
       noPermissionEdit: 'You do not have permission to edit archives',
       noPermissionDelete: 'You do not have permission to delete archives',
-      reprint: 'Reprint',
-      schedulePrint: 'Schedule Print',
-      schedule: 'Schedule',
       openInBambuStudio: 'Open in Slicer',
       openInBambuStudioToSlice: 'Open in Slicer to slice',
       slice: 'Slice',
@@ -1027,7 +1031,6 @@ export default {
   queue: {
     title: 'Print Queue',
     subtitle: 'Schedule and manage your print jobs',
-    addToQueue: 'Add to Queue',
     filamentShort: {
       rowBadge: 'Insufficient filament for the assigned spool',
       rowTooltip: 'The dispatch scheduler flagged this item. Click Play to see the per-slot deficit and decide whether to print anyway.',
@@ -1038,16 +1041,11 @@ export default {
       printAnyway: 'Print Anyway',
     },
     // Print modal
-    print: 'Print',
-    reprint: 'Re-print',
-    schedulePrint: 'Schedule Print',
     editQueueItem: 'Edit Queue Item',
-    printToPrinters: 'Print to {{count}} Printers',
-    queueToPrinters: 'Queue to {{count}} Printers',
-    queueSelectedPlates: 'Queue {{count}} Plates',
     selectAllPlates: 'Select All {{count}} Plates',
     deselectAll: 'Deselect All',
     printQueued: 'Print queued',
+    printQueuedWillStartWhenIdle: 'Will start when printer is idle',
     itemsQueued: '{{count}} items queued',
     sending: 'Sending...',
     sendingProgress: 'Sending {{current}}/{{total}}...',
@@ -1195,8 +1193,8 @@ export default {
     },
     // Actions
     actions: {
-      stopPrint: 'Stop Print',
       startPrint: 'Start Print',
+      stopPrint: 'Stop Print',
       requeue: 'Re-queue',
     },
     // Bulk edit
@@ -1315,35 +1313,6 @@ export default {
     },
   },
 
-  backgroundDispatch: {
-    unknownFile: 'Unknown file',
-    unknownPrinter: 'Unknown printer',
-    startingPrints: 'Starting prints',
-    progressSummary: '{{complete}}/{{total}} complete • Dispatched: {{dispatched}} • Processing: {{processing}}',
-    expandDetails: 'Expand dispatch details',
-    collapseDetails: 'Collapse dispatch details',
-    dismissToast: 'Dismiss dispatch toast',
-    cancelDispatchJob: 'Cancel dispatch job',
-    cancel: 'Cancel',
-    cancelling: 'Cancelling…',
-    awaitingPrinter: 'Awaiting printer…',
-    status: {
-      dispatched: 'Dispatched',
-      processing: 'Processing',
-      completed: 'Completed',
-      failed: 'Failed',
-      cancelled: 'Cancelled',
-    },
-    toast: {
-      cancellingUpload: 'Cancelling upload...',
-      cancelled: 'Dispatch cancelled',
-      cancelFailed: 'Failed to cancel dispatch',
-      completeWithFailures: 'Background dispatch complete: {{completed}} succeeded, {{failed}} failed',
-      completeSuccess: 'Background dispatch complete: {{completed}} succeeded',
-      printStartedRemaining: '{{completed}} print(s) started, {{remaining}} more sending...',
-    },
-  },
-
   // Statistics page
   stats: {
     title: 'Statistics',
@@ -2641,6 +2610,42 @@ export default {
     clearErrors: 'Clear Errors',
     clearSuccess: 'HMS errors cleared',
     clearFailed: 'Failed to clear HMS errors',
+    actionSuccess: 'Action sent to printer',
+    actionFailed: 'Failed to send action',
+    actions: {
+      RESUME_PRINTING: "Resume Printing",
+      RESUME_PRINTING_DEFECTS: "Resume (defects acceptable)",
+      RESUME_PRINTING_PROBELM_SOLVED: "Resume (problem solved)",
+      STOP_PRINTING: "Stop Printing",
+      CHECK_ASSISTANT: "Check Assistant",
+      FILAMENT_EXTRUDED: "Filament Extruded, Continue",
+      RETRY_FILAMENT_EXTRUDED: "Not Extruded Yet, Retry",
+      CONTINUE: "Finished, Continue",
+      LOAD_VIRTUAL_TRAY: "Load Filament",
+      OK_BUTTON: "OK",
+      FILAMENT_LOAD_RESUME: "Filament Loaded, Resume",
+      JUMP_TO_LIVEVIEW: "View Liveview",
+      NO_REMINDER_NEXT_TIME: "No Reminder Next Time",
+      REFRESH_NOZZLE: "Recheck",
+      IGNORE_NO_REMINDER_NEXT_TIME: "Ignore. Don't Remind Next Time",
+      IGNORE_RESUME: "Ignore this and Resume",
+      PROBLEM_SOLVED_RESUME: "Problem Solved and Resume",
+      TURN_OFF_FIRE_ALARM: "Got it, Turn off the Fire Alarm.",
+      RETRY_PROBLEM_SOLVED: "Retry (problem solved)",
+      CANCLE: "Cancle",
+      STOP_DRYING: "Stop Drying",
+      PROCEED: "Proceed",
+      OK_JUMP_RACK: "OK",
+      ABORT: "Abort",
+      DISABLE_PURIFICATION: "Disable Purification for This Print",
+      DONT_REMIND_NEXT_TIME: "Don't Remind Me",
+      DBL_CHECK_CANCEL: "Cancel",
+      DBL_CHECK_DONE: "Done",
+      DBL_CHECK_RETRY: "Retry",
+      DBL_CHECK_RESUME: "Resume",
+      DBL_CHECK_OK: "Confirm",
+      REMOVE_CLOSE_BTN: "Close",
+    }
   },
 
   // MQTT Debug modal
@@ -3369,8 +3374,6 @@ export default {
     changeLink: 'Change Link...',
     linkTo: 'Link to...',
     linkToProjectOrArchive: 'Link to project or archive',
-    addToQueue: 'Add to Queue',
-    schedulePrint: 'Schedule',
     generateThumbnail: 'Generate Thumbnail',
     generateThumbnails: 'Generate Thumbnails',
     generateThumbnailsForMissing: 'Generate thumbnails for STL files missing them',
@@ -3673,8 +3676,6 @@ export default {
       fileCount: '{{count}} file(s)',
       empty: 'No folders linked. Go to File Manager and link a folder to this project.',
       noFiles: 'No files in this folder.',
-      print: 'Print Now',
-      addToQueue: 'Add to Queue',
     },
     bom: {
       title: 'Bill of Materials',
@@ -4322,7 +4323,6 @@ export default {
 
   // Print modal
   printModal: {
-    title: 'Start Print',
     selectPrinter: 'Select Printer',
     selectPlate: 'Select Plate',
     filamentMapping: 'Filament Mapping',
@@ -4334,15 +4334,25 @@ export default {
     vibrationCalibration: 'Vibration Calibration',
     layerInspection: 'First Layer Inspection',
     timelapse: 'Timelapse',
-    startPrint: 'Start Print',
-    addToQueue: 'Add to Queue',
     cancel: 'Cancel',
     noPrintersAvailable: 'No printers available',
     printerBusy: 'Printer is busy',
     printerOffline: 'Printer is offline',
     sameTypeDifferentColor: 'Same type, different color',
     filamentTypeNotLoaded: 'Filament type not loaded',
+    whenToPrint: 'When to print',
+    asap: 'ASAP',
+    queue: 'Queue',
+    schedule: 'Schedule',
+    dateTime: 'Date & Time',
+    invalidDateTime: 'Please enter a valid date and time',
     openCalendar: 'Open calendar',
+    requireManualStart: 'Require manual start',
+    requirePreviousSuccess: 'Only start if previous print succeeded',
+    autoOffAfter: 'Power off printer when done',
+    helpAsap: 'Print will be added to the top of the queue and start as soon as an eligible printer is idle.',
+    helpSchedule: 'Print will start at the scheduled time if the printer is idle. If busy, it will wait until the printer becomes available.',
+    helpQueue: 'Print will be added to the back of the queue.',
     leftNozzle: 'L',
     rightNozzle: 'R',
     leftNozzleTooltip: 'Left nozzle',

+ 59 - 49
frontend/src/i18n/locales/es.ts

@@ -204,12 +204,21 @@ export default {
       snap: 'Foto',
       off: 'Inactivo',
       summary: '{{live}} en vivo, {{snap}} fotos, {{total}} en total',
+      layer: 'Capa {{cur}}/{{total}}',
+      timeLeft: 'quedan {{time}}',
+      statusMode: {
+        off: 'Apagado',
+        compact: 'Compacto',
+        full: 'Completo',
+      },
       settings: {
         title: 'Ajustes del muro de cámaras',
         maxLive: 'Máx. transmisiones en vivo',
         maxLiveHint: 'Cuántos mosaicos transmiten en vivo a la vez. Los demás se actualizan como fotos.',
         snapshotInterval: 'Intervalo de fotos (segundos)',
         snapshotIntervalHint: 'Con qué frecuencia los mosaicos no en vivo obtienen una nueva foto.',
+        statusOverlay: 'Superposición de estado',
+        statusOverlayHint: 'Compacto: solo insignia de estado. Completo: + progreso, capa, tiempo restante.',
       },
     },
     // Controls
@@ -747,7 +756,6 @@ export default {
     printTime: 'Tiempo de impresión',
     filamentUsed: 'Filamento usado',
     cost: 'Coste',
-    reprint: 'Reimprimir',
     preview: 'Vista previa',
     deleteArchive: 'Eliminar archivo',
     deleteConfirm: '¿Está seguro de que desea eliminar este archivo?',
@@ -794,7 +802,6 @@ export default {
     },
     menu: {
       print: 'Imprimir',
-      schedule: 'Programar',
       openInBambuStudio: 'Abrir en el laminador',
       slice: 'Laminar',
       externalLink: 'Enlace externo',
@@ -889,9 +896,6 @@ export default {
       noFileForReprint: 'No hay archivo 3MF disponible — no se pudo descargar el archivo de la impresora cuando se registró la impresión',
       noPermissionEdit: 'No tiene permiso para editar archivos',
       noPermissionDelete: 'No tiene permiso para eliminar archivos',
-      reprint: 'Reimprimir',
-      schedulePrint: 'Programar impresión',
-      schedule: 'Programar',
       openInBambuStudio: 'Abrir en el laminador',
       openInBambuStudioToSlice: 'Abrir en el laminador para laminar',
       slice: 'Laminar',
@@ -1032,18 +1036,12 @@ export default {
     },
     title: 'Cola de impresión',
     subtitle: 'Programe y gestione sus trabajos de impresión',
-    addToQueue: 'Añadir a la cola',
     // Print modal
-    print: 'Imprimir',
-    reprint: 'Reimprimir',
-    schedulePrint: 'Programar impresión',
     editQueueItem: 'Editar elemento de la cola',
-    printToPrinters: 'Imprimir en {{count}} impresoras',
-    queueToPrinters: 'Encolar en {{count}} impresoras',
-    queueSelectedPlates: 'Encolar {{count}} camas',
     selectAllPlates: 'Seleccionar las {{count}} camas',
     deselectAll: 'Deseleccionar todo',
     printQueued: 'Impresión encolada',
+    printQueuedWillStartWhenIdle: 'Comenzará cuando la impresora esté inactiva',
     itemsQueued: '{{count}} elementos encolados',
     sending: 'Enviando...',
     sendingProgress: 'Enviando {{current}}/{{total}}...',
@@ -1185,8 +1183,8 @@ export default {
     },
     // Actions
     actions: {
-      stopPrint: 'Detener impresión',
       startPrint: 'Iniciar impresión',
+      stopPrint: 'Detener impresión',
       requeue: 'Volver a encolar',
     },
     // Bulk edit
@@ -1304,35 +1302,6 @@ export default {
     },
   },
 
-  backgroundDispatch: {
-    unknownFile: 'Archivo desconocido',
-    unknownPrinter: 'Impresora desconocida',
-    startingPrints: 'Iniciando impresiones',
-    progressSummary: '{{complete}}/{{total}} completadas • Enviadas: {{dispatched}} • Procesando: {{processing}}',
-    expandDetails: 'Expandir los detalles del envío',
-    collapseDetails: 'Contraer los detalles del envío',
-    dismissToast: 'Descartar la notificación de envío',
-    cancelDispatchJob: 'Cancelar el trabajo de envío',
-    cancel: 'Cancelar',
-    cancelling: 'Cancelando…',
-    awaitingPrinter: 'Esperando a la impresora…',
-    status: {
-      dispatched: 'Enviada',
-      processing: 'Procesando',
-      completed: 'Completada',
-      failed: 'Fallida',
-      cancelled: 'Cancelada',
-    },
-    toast: {
-      cancellingUpload: 'Cancelando la subida...',
-      cancelled: 'Envío cancelado',
-      cancelFailed: 'Error al cancelar el envío',
-      completeWithFailures: 'Envío en segundo plano completado: {{completed}} con éxito, {{failed}} con error',
-      completeSuccess: 'Envío en segundo plano completado: {{completed}} con éxito',
-      printStartedRemaining: '{{completed}} impresión(es) iniciada(s), enviando {{remaining}} más...',
-    },
-  },
-
   // Statistics page
   stats: {
     title: 'Estadísticas',
@@ -2629,6 +2598,42 @@ export default {
     clearErrors: 'Borrar errores',
     clearSuccess: 'Errores HMS borrados',
     clearFailed: 'Error al borrar los errores HMS',
+    actionSuccess: 'Acción enviada a la impresora',
+    actionFailed: 'No se pudo enviar la acción',
+    actions: {
+      RESUME_PRINTING: 'Reanudar impresión',
+      RESUME_PRINTING_DEFECTS: 'Reanudar (defectos aceptables)',
+      RESUME_PRINTING_PROBELM_SOLVED: 'Reanudar (problema resuelto)',
+      STOP_PRINTING: 'Detener impresión',
+      CHECK_ASSISTANT: 'Ver asistente',
+      FILAMENT_EXTRUDED: 'Filamento extruido, continuar',
+      RETRY_FILAMENT_EXTRUDED: 'Aún no extruido, reintentar',
+      CONTINUE: 'Finalizado, continuar',
+      LOAD_VIRTUAL_TRAY: 'Cargar filamento',
+      OK_BUTTON: 'OK',
+      FILAMENT_LOAD_RESUME: 'Filamento cargado, reanudar',
+      JUMP_TO_LIVEVIEW: 'Ver en vivo',
+      NO_REMINDER_NEXT_TIME: 'No recordar la próxima vez',
+      REFRESH_NOZZLE: 'Volver a comprobar',
+      IGNORE_NO_REMINDER_NEXT_TIME: 'Ignorar y no recordar',
+      IGNORE_RESUME: 'Ignorar y reanudar',
+      PROBLEM_SOLVED_RESUME: 'Problema resuelto, reanudar',
+      TURN_OFF_FIRE_ALARM: 'Entendido, apagar alarma de incendio',
+      RETRY_PROBLEM_SOLVED: 'Reintentar (problema resuelto)',
+      CANCLE: 'Cancelar',
+      STOP_DRYING: 'Detener secado',
+      PROCEED: 'Continuar',
+      OK_JUMP_RACK: 'OK',
+      ABORT: 'Cancelar',
+      DISABLE_PURIFICATION: 'Desactivar purificación para esta impresión',
+      DONT_REMIND_NEXT_TIME: 'No recordarme',
+      DBL_CHECK_CANCEL: 'Cancelar',
+      DBL_CHECK_DONE: 'Hecho',
+      DBL_CHECK_RETRY: 'Reintentar',
+      DBL_CHECK_RESUME: 'Reanudar',
+      DBL_CHECK_OK: 'Confirmar',
+      REMOVE_CLOSE_BTN: 'Cerrar',
+    },
   },
 
   // MQTT Debug modal
@@ -3357,8 +3362,6 @@ export default {
     changeLink: 'Cambiar enlace...',
     linkTo: 'Vincular a...',
     linkToProjectOrArchive: 'Vincular a un proyecto o archivo',
-    addToQueue: 'Añadir a la cola',
-    schedulePrint: 'Programar',
     generateThumbnail: 'Generar miniatura',
     generateThumbnails: 'Generar miniaturas',
     generateThumbnailsForMissing: 'Generar miniaturas para los archivos STL que no las tienen',
@@ -3661,8 +3664,6 @@ export default {
       fileCount: '{{count}} archivo(s)',
       empty: 'No hay carpetas vinculadas. Vaya al gestor de archivos y vincule una carpeta a este proyecto.',
       noFiles: 'No hay archivos en esta carpeta.',
-      print: 'Imprimir ahora',
-      addToQueue: 'Añadir a la cola',
     },
     bom: {
       title: 'Lista de materiales',
@@ -4306,7 +4307,6 @@ export default {
 
   // Print modal
   printModal: {
-    title: 'Iniciar impresión',
     selectPrinter: 'Seleccionar impresora',
     selectPlate: 'Seleccionar cama',
     filamentMapping: 'Mapeo de filamentos',
@@ -4318,15 +4318,25 @@ export default {
     vibrationCalibration: 'Calibración de vibración',
     layerInspection: 'Inspección de la primera capa',
     timelapse: 'Time-lapse',
-    startPrint: 'Iniciar impresión',
-    addToQueue: 'Añadir a la cola',
     cancel: 'Cancelar',
     noPrintersAvailable: 'No hay impresoras disponibles',
     printerBusy: 'La impresora está ocupada',
     printerOffline: 'La impresora está desconectada',
     sameTypeDifferentColor: 'Mismo tipo, color distinto',
     filamentTypeNotLoaded: 'Tipo de filamento no cargado',
+    whenToPrint: 'Cuándo imprimir',
+    asap: 'Lo antes posible',
+    queue: 'Cola',
+    schedule: 'Programar',
+    dateTime: 'Fecha y hora',
+    invalidDateTime: 'Introduzca una fecha y hora válidas',
     openCalendar: 'Abrir calendario',
+    requireManualStart: 'Requerir inicio manual',
+    requirePreviousSuccess: 'Iniciar solo si la impresión anterior se completó correctamente',
+    autoOffAfter: 'Apagar la impresora al terminar',
+    helpAsap: 'La impresión se añadirá al principio de la cola y comenzará en cuanto haya una impresora elegible inactiva.',
+    helpSchedule: 'La impresión comenzará a la hora programada si la impresora está inactiva. Si está ocupada, esperará hasta que esté disponible.',
+    helpQueue: 'La impresión se añadirá al final de la cola.',
     leftNozzle: 'I',
     rightNozzle: 'D',
     leftNozzleTooltip: 'Boquilla izquierda',

+ 59 - 49
frontend/src/i18n/locales/fr.ts

@@ -204,12 +204,21 @@ export default {
       snap: 'Photo',
       off: 'Arrêt',
       summary: '{{live}} en direct, {{snap}} captures, {{total}} au total',
+      layer: 'Couche {{cur}}/{{total}}',
+      timeLeft: '{{time}} restantes',
+      statusMode: {
+        off: 'Arrêt',
+        compact: 'Compact',
+        full: 'Complet',
+      },
       settings: {
         title: 'Paramètres du mur de caméras',
         maxLive: 'Flux en direct max.',
         maxLiveHint: 'Combien de vignettes diffusent en direct à la fois. Les autres se rafraîchissent en captures.',
         snapshotInterval: 'Intervalle de capture (secondes)',
         snapshotIntervalHint: 'À quelle fréquence les vignettes hors direct récupèrent une nouvelle capture.',
+        statusOverlay: 'Overlay de statut',
+        statusOverlayHint: 'Compact : badge de statut seul. Complet : + progression, couche, temps restant.',
       },
     },
     // Controls
@@ -747,7 +756,6 @@ export default {
     printTime: 'Temps d\'impression',
     filamentUsed: 'Filament utilisé',
     cost: 'Coût',
-    reprint: 'Réimprimer',
     preview: 'Aperçu',
     deleteArchive: 'Supprimer l\'archive',
     deleteConfirm: 'Supprimer cette archive ?',
@@ -794,7 +802,6 @@ export default {
     },
     menu: {
       print: 'Imprimer',
-      schedule: 'Planifier',
       openInBambuStudio: 'Ouvrir dans le Slicer',
       slice: 'Découper',
       externalLink: 'Lien externe',
@@ -889,9 +896,6 @@ export default {
       noFileForReprint: 'Aucun fichier 3MF disponible — le fichier n\'a pas pu être téléchargé depuis l\'imprimante lors de l\'enregistrement',
       noPermissionEdit: 'Pas d\'autorisation de modification',
       noPermissionDelete: 'Pas d\'autorisation de suppression',
-      reprint: 'Réimprimer',
-      schedulePrint: 'Planifier',
-      schedule: 'Planifier',
       openInBambuStudio: 'Ouvrir dans le Slicer',
       openInBambuStudioToSlice: 'Ouvrir dans le Slicer pour découper',
       slice: 'Découper',
@@ -1032,18 +1036,12 @@ export default {
     },
     title: 'File d\'attente',
     subtitle: 'Gérez vos travaux d\'impression',
-    addToQueue: 'Ajouter à la file',
     // Print modal
-    print: 'Imprimer',
-    reprint: 'Réimprimer',
-    schedulePrint: 'Planifier',
     editQueueItem: 'Modifier l\'élément',
-    printToPrinters: 'Imprimer sur {{count}} imprimantes',
-    queueToPrinters: 'Ajouter à la file pour {{count}} imprimantes',
-    queueSelectedPlates: 'Ajouter {{count}} plaques à la file',
     selectAllPlates: 'Sélectionner les {{count}} plaques',
     deselectAll: 'Tout désélectionner',
     printQueued: 'Impression ajoutée à la file',
+    printQueuedWillStartWhenIdle: 'Démarrera lorsque l\'imprimante sera inactive',
     itemsQueued: '{{count}} éléments ajoutés à la file',
     sending: 'Envoi...',
     sendingProgress: 'Envoi {{current}}/{{total}}...',
@@ -1185,8 +1183,8 @@ export default {
     },
     // Actions
     actions: {
-      stopPrint: 'Arrêter',
       startPrint: 'Démarrer',
+      stopPrint: 'Arrêter',
       requeue: 'Remettre en file',
     },
     // Bulk edit
@@ -1304,35 +1302,6 @@ export default {
     },
   },
 
-  backgroundDispatch: {
-    unknownFile: 'Fichier inconnu',
-    unknownPrinter: 'Imprimante inconnue',
-    startingPrints: 'Démarrage des impressions',
-    progressSummary: '{{complete}}/{{total}} terminés • Envoyés : {{dispatched}} • En cours : {{processing}}',
-    expandDetails: 'Expand dispatch details',
-    collapseDetails: 'Collapse dispatch details',
-    dismissToast: 'Ignorer la notification d\'envoi',
-    cancelDispatchJob: 'Annuler l\'envoi',
-    cancel: 'Annuler',
-    cancelling: 'Annulation…',
-    awaitingPrinter: 'En attente de l\'imprimante…',
-    status: {
-      dispatched: 'Dispatché',
-      processing: 'Traitement',
-      completed: 'Terminé',
-      failed: 'Échoué',
-      cancelled: 'Annulé',
-    },
-    toast: {
-      cancellingUpload: 'Cancelling upload...',
-      cancelled: 'Envoi annulé',
-      cancelFailed: 'Échec de l\'annulation de l\'envoi',
-      completeWithFailures: 'Envoi en arrière-plan terminé : {{completed}} réussi(s), {{failed}} échoué(s)',
-      completeSuccess: 'Envoi en arrière-plan terminé : {{completed}} réussi(s)',
-      printStartedRemaining: '{{completed}} impression(s) lancée(s), {{remaining}} en cours d\'envoi...',
-    },
-  },
-
   // Statistics page
   stats: {
     title: 'Statistiques',
@@ -2615,6 +2584,42 @@ export default {
     clearErrors: 'Effacer les erreurs',
     clearSuccess: 'Erreurs HMS effacées',
     clearFailed: 'Échec de l\'effacement des erreurs HMS',
+    actionSuccess: 'Action envoyée à l\'imprimante',
+    actionFailed: 'Échec de l\'envoi de l\'action',
+    actions: {
+      RESUME_PRINTING: 'Reprendre l\'impression',
+      RESUME_PRINTING_DEFECTS: 'Reprendre (défauts acceptables)',
+      RESUME_PRINTING_PROBELM_SOLVED: 'Reprendre (problème résolu)',
+      STOP_PRINTING: 'Arrêter l\'impression',
+      CHECK_ASSISTANT: 'Ouvrir l\'assistant',
+      FILAMENT_EXTRUDED: 'Filament extrudé, continuer',
+      RETRY_FILAMENT_EXTRUDED: 'Pas encore extrudé, réessayer',
+      CONTINUE: 'Terminé, continuer',
+      LOAD_VIRTUAL_TRAY: 'Charger le filament',
+      OK_BUTTON: 'OK',
+      FILAMENT_LOAD_RESUME: 'Filament chargé, reprendre',
+      JUMP_TO_LIVEVIEW: 'Voir en direct',
+      NO_REMINDER_NEXT_TIME: 'Ne plus rappeler la prochaine fois',
+      REFRESH_NOZZLE: 'Revérifier',
+      IGNORE_NO_REMINDER_NEXT_TIME: 'Ignorer et ne plus rappeler',
+      IGNORE_RESUME: 'Ignorer et reprendre',
+      PROBLEM_SOLVED_RESUME: 'Problème résolu, reprendre',
+      TURN_OFF_FIRE_ALARM: 'Compris, désactiver l\'alarme incendie',
+      RETRY_PROBLEM_SOLVED: 'Réessayer (problème résolu)',
+      CANCLE: 'Annuler',
+      STOP_DRYING: 'Arrêter le séchage',
+      PROCEED: 'Continuer',
+      OK_JUMP_RACK: 'OK',
+      ABORT: 'Annuler',
+      DISABLE_PURIFICATION: 'Désactiver la purification pour cette impression',
+      DONT_REMIND_NEXT_TIME: 'Ne plus me rappeler',
+      DBL_CHECK_CANCEL: 'Annuler',
+      DBL_CHECK_DONE: 'Terminé',
+      DBL_CHECK_RETRY: 'Réessayer',
+      DBL_CHECK_RESUME: 'Reprendre',
+      DBL_CHECK_OK: 'Confirmer',
+      REMOVE_CLOSE_BTN: 'Fermer',
+    },
   },
 
   // MQTT Debug modal
@@ -3343,8 +3348,6 @@ export default {
     changeLink: 'Modifier lien...',
     linkTo: 'Lier à...',
     linkToProjectOrArchive: 'Lier à projet ou archive',
-    addToQueue: 'Ajouter à la file',
-    schedulePrint: 'Planifier',
     generateThumbnail: 'Générer vignette',
     generateThumbnails: 'Générer vignettes',
     generateThumbnailsForMissing: 'Vignettes STL manquantes',
@@ -3647,8 +3650,6 @@ export default {
       fileCount: '{{count}} fichier(s)',
       empty: 'Aucun dossier lié.',
       noFiles: 'Aucun fichier dans ce dossier.',
-      print: 'Imprimer maintenant',
-      addToQueue: 'Ajouter à la file',
     },
     bom: {
       title: 'BOM (Liste matériel)',
@@ -4287,7 +4288,6 @@ export default {
 
   // Print modal
   printModal: {
-    title: 'Lancer l\'impression',
     selectPrinter: 'Choisir l\'imprimante',
     selectPlate: 'Choisir le plateau',
     filamentMapping: 'Mapping Filament',
@@ -4299,15 +4299,25 @@ export default {
     vibrationCalibration: 'Vibration (Input Shaper)',
     layerInspection: 'Inspection 1ère couche',
     timelapse: 'Time-lapse',
-    startPrint: 'Démarrer',
-    addToQueue: 'Ajouter à la file',
     cancel: 'Annuler',
     noPrintersAvailable: 'Aucune imprimante disponible',
     printerBusy: 'L\'imprimante est occupée',
     printerOffline: 'L\'imprimante est hors ligne',
     sameTypeDifferentColor: 'Même type, couleur différente',
     filamentTypeNotLoaded: 'Type de filament non chargé',
+    whenToPrint: 'Quand imprimer',
+    asap: 'Dès que possible',
+    queue: 'File',
+    schedule: 'Planifier',
+    dateTime: 'Date et heure',
+    invalidDateTime: 'Veuillez saisir une date et une heure valides',
     openCalendar: 'Ouvrir calendrier',
+    requireManualStart: 'Démarrage manuel requis',
+    requirePreviousSuccess: 'Démarrer seulement si l\'impression précédente a réussi',
+    autoOffAfter: 'Éteindre l\'imprimante à la fin',
+    helpAsap: 'L\'impression sera ajoutée en haut de la file et démarrera dès qu\'une imprimante éligible sera inactive.',
+    helpSchedule: 'L\'impression démarrera à l\'heure planifiée si l\'imprimante est inactive. Si elle est occupée, elle attendra qu\'elle soit disponible.',
+    helpQueue: 'L\'impression sera ajoutée à la fin de la file.',
     leftNozzle: 'G',
     rightNozzle: 'D',
     leftNozzleTooltip: 'Buse gauche',

+ 59 - 49
frontend/src/i18n/locales/it.ts

@@ -204,12 +204,21 @@ export default {
       snap: 'Foto',
       off: 'Spento',
       summary: '{{live}} live, {{snap}} foto, {{total}} totali',
+      layer: 'Strato {{cur}}/{{total}}',
+      timeLeft: '{{time}} rimanenti',
+      statusMode: {
+        off: 'Off',
+        compact: 'Compatto',
+        full: 'Completo',
+      },
       settings: {
         title: 'Impostazioni muro telecamere',
         maxLive: 'Max stream live',
         maxLiveHint: 'Quante tessere trasmettono in live contemporaneamente. Le altre si aggiornano come foto.',
         snapshotInterval: 'Intervallo foto (secondi)',
         snapshotIntervalHint: 'Con quale frequenza le tessere non live scaricano una nuova foto.',
+        statusOverlay: 'Overlay di stato',
+        statusOverlayHint: 'Compatto: solo badge di stato. Completo: + avanzamento, strato, tempo residuo.',
       },
     },
     // Controls
@@ -747,7 +756,6 @@ export default {
     printTime: 'Tempo di stampa',
     filamentUsed: 'Filamento usato',
     cost: 'Costo',
-    reprint: 'Ristampa',
     preview: 'Anteprima',
     deleteArchive: 'Elimina archivio',
     deleteConfirm: 'Sei sicuro di eliminare questo archivio?',
@@ -794,7 +802,6 @@ export default {
     },
     menu: {
       print: 'Stampa',
-      schedule: 'Programma',
       openInBambuStudio: 'Apri nello slicer',
       slice: 'Slice',
       externalLink: 'Link esterno',
@@ -889,9 +896,6 @@ export default {
       noFileForReprint: 'Nessun file 3MF disponibile — il file non è stato scaricato dalla stampante durante la registrazione',
       noPermissionEdit: 'Non hai il permesso di modificare archivi',
       noPermissionDelete: 'Non hai il permesso di eliminare archivi',
-      reprint: 'Ristampa',
-      schedulePrint: 'Programma Stampa',
-      schedule: 'Programma',
       openInBambuStudio: 'Apri nello slicer',
       openInBambuStudioToSlice: 'Apri nello slicer per slicing',
       slice: 'Slice',
@@ -1032,18 +1036,12 @@ export default {
     },
     title: 'Coda di stampa',
     subtitle: 'Programma e gestisci i tuoi lavori di stampa',
-    addToQueue: 'Aggiungi alla coda',
     // Print modal
-    print: 'Stampa',
-    reprint: 'Ristampa',
-    schedulePrint: 'Programma Stampa',
     editQueueItem: 'Modifica elemento coda',
-    printToPrinters: 'Stampa su {{count}} Stampanti',
-    queueToPrinters: 'Metti in coda su {{count}} Stampanti',
-    queueSelectedPlates: 'Metti in coda {{count}} piastre',
     selectAllPlates: 'Seleziona tutte le {{count}} piastre',
     deselectAll: 'Deseleziona tutto',
     printQueued: 'Stampa in coda',
+    printQueuedWillStartWhenIdle: 'Si avvierà quando la stampante sarà inattiva',
     itemsQueued: '{{count}} elementi in coda',
     sending: 'Invio...',
     sendingProgress: 'Invio {{current}}/{{total}}...',
@@ -1185,8 +1183,8 @@ export default {
     },
     // Actions
     actions: {
-      stopPrint: 'Ferma Stampa',
       startPrint: 'Avvia Stampa',
+      stopPrint: 'Ferma Stampa',
       requeue: 'Rimetti in coda',
     },
     // Bulk edit
@@ -1304,35 +1302,6 @@ export default {
     },
   },
 
-  backgroundDispatch: {
-    unknownFile: 'File sconosciuto',
-    unknownPrinter: 'Stampante sconosciuta',
-    startingPrints: 'Avvio stampe',
-    progressSummary: '{{complete}}/{{total}} completati • Inviati: {{dispatched}} • In elaborazione: {{processing}}',
-    expandDetails: 'Espandi dettagli dispatch',
-    collapseDetails: 'Comprimi dettagli dispatch',
-    dismissToast: 'Chiudi notifica dispatch',
-    cancelDispatchJob: 'Annulla job dispatch',
-    cancel: 'Annulla',
-    cancelling: 'Annullamento…',
-    awaitingPrinter: 'In attesa della stampante…',
-    status: {
-      dispatched: 'Inviato',
-      processing: 'In elaborazione',
-      completed: 'Completato',
-      failed: 'Fallito',
-      cancelled: 'Annullato',
-    },
-    toast: {
-      cancellingUpload: 'Annullamento upload...',
-      cancelled: 'Dispatch annullato',
-      cancelFailed: 'Impossibile annullare il dispatch',
-      completeWithFailures: 'Dispatch in background completato: {{completed}} riusciti, {{failed}} falliti',
-      completeSuccess: 'Dispatch in background completato: {{completed}} riusciti',
-      printStartedRemaining: '{{completed}} stampa/e avviata/e, {{remaining}} in invio...',
-    },
-  },
-
   // Statistics page
   stats: {
     title: 'Statistiche',
@@ -2614,6 +2583,42 @@ export default {
     clearErrors: 'Cancella errori',
     clearSuccess: 'Errori HMS cancellati',
     clearFailed: 'Impossibile cancellare gli errori HMS',
+    actionSuccess: 'Azione inviata alla stampante',
+    actionFailed: 'Impossibile inviare l\'azione',
+    actions: {
+      RESUME_PRINTING: 'Riprendi stampa',
+      RESUME_PRINTING_DEFECTS: 'Riprendi (difetti accettabili)',
+      RESUME_PRINTING_PROBELM_SOLVED: 'Riprendi (problema risolto)',
+      STOP_PRINTING: 'Ferma stampa',
+      CHECK_ASSISTANT: 'Apri assistente',
+      FILAMENT_EXTRUDED: 'Filamento estruso, continua',
+      RETRY_FILAMENT_EXTRUDED: 'Non ancora estruso, riprova',
+      CONTINUE: 'Completato, continua',
+      LOAD_VIRTUAL_TRAY: 'Carica filamento',
+      OK_BUTTON: 'OK',
+      FILAMENT_LOAD_RESUME: 'Filamento caricato, riprendi',
+      JUMP_TO_LIVEVIEW: 'Visualizza in diretta',
+      NO_REMINDER_NEXT_TIME: 'Non ricordare la prossima volta',
+      REFRESH_NOZZLE: 'Ricontrolla',
+      IGNORE_NO_REMINDER_NEXT_TIME: 'Ignora e non ricordare',
+      IGNORE_RESUME: 'Ignora e riprendi',
+      PROBLEM_SOLVED_RESUME: 'Problema risolto, riprendi',
+      TURN_OFF_FIRE_ALARM: 'Capito, spegni allarme antincendio',
+      RETRY_PROBLEM_SOLVED: 'Riprova (problema risolto)',
+      CANCLE: 'Annulla',
+      STOP_DRYING: 'Ferma essiccazione',
+      PROCEED: 'Procedi',
+      OK_JUMP_RACK: 'OK',
+      ABORT: 'Annulla',
+      DISABLE_PURIFICATION: 'Disattiva purificazione per questa stampa',
+      DONT_REMIND_NEXT_TIME: 'Non ricordarmelo',
+      DBL_CHECK_CANCEL: 'Annulla',
+      DBL_CHECK_DONE: 'Fatto',
+      DBL_CHECK_RETRY: 'Riprova',
+      DBL_CHECK_RESUME: 'Riprendi',
+      DBL_CHECK_OK: 'Conferma',
+      REMOVE_CLOSE_BTN: 'Chiudi',
+    },
   },
 
   // MQTT Debug modal
@@ -3342,8 +3347,6 @@ export default {
     changeLink: 'Cambia collegamento...',
     linkTo: 'Collega a...',
     linkToProjectOrArchive: 'Collega a progetto o archivio',
-    addToQueue: 'Aggiungi alla coda',
-    schedulePrint: 'Pianifica',
     generateThumbnail: 'Genera miniatura',
     generateThumbnails: 'Genera miniature',
     generateThumbnailsForMissing: 'Genera miniature per STL senza miniatura',
@@ -3646,8 +3649,6 @@ export default {
       fileCount: '{{count}} file',
       empty: 'Nessuna cartella collegata. Vai a Gestore file e collega una cartella a questo progetto.',
       noFiles: 'Nessun file in questa cartella.',
-      print: 'Stampa ora',
-      addToQueue: 'Aggiungi alla coda',
     },
     bom: {
       title: 'Distinta materiali',
@@ -4286,7 +4287,6 @@ export default {
 
   // Print modal
   printModal: {
-    title: 'Avvia stampa',
     selectPrinter: 'Seleziona stampante',
     selectPlate: 'Seleziona piatto',
     filamentMapping: 'Mappatura filamento',
@@ -4298,15 +4298,25 @@ export default {
     vibrationCalibration: 'Calibrazione vibrazioni',
     layerInspection: 'Ispezione primo layer',
     timelapse: 'Timelapse',
-    startPrint: 'Avvia stampa',
-    addToQueue: 'Aggiungi alla coda',
     cancel: 'Annulla',
     noPrintersAvailable: 'Nessuna stampante disponibile',
     printerBusy: 'Stampante occupata',
     printerOffline: 'Stampante offline',
     sameTypeDifferentColor: 'Stesso tipo, colore diverso',
     filamentTypeNotLoaded: 'Tipo di filamento non caricato',
+    whenToPrint: 'Quando stampare',
+    asap: 'Subito',
+    queue: 'Coda',
+    schedule: 'Programma',
+    dateTime: 'Data e ora',
+    invalidDateTime: 'Inserisci data e ora valide',
     openCalendar: 'Apri calendario',
+    requireManualStart: 'Richiedi avvio manuale',
+    requirePreviousSuccess: 'Avvia solo se la stampa precedente è riuscita',
+    autoOffAfter: 'Spegni la stampante al termine',
+    helpAsap: 'La stampa verrà aggiunta in cima alla coda e partirà appena una stampante idonea è inattiva.',
+    helpSchedule: 'La stampa partirà all\'ora programmata se la stampante è inattiva. Se è occupata, attenderà che diventi disponibile.',
+    helpQueue: 'La stampa verrà aggiunta in fondo alla coda.',
     leftNozzle: 'L',
     rightNozzle: 'R',
     leftNozzleTooltip: 'Ugello sinistro',

+ 59 - 49
frontend/src/i18n/locales/ja.ts

@@ -203,12 +203,21 @@ export default {
       snap: 'スナップ',
       off: 'オフ',
       summary: 'ライブ {{live}}件、スナップ {{snap}}件、合計 {{total}}件',
+      layer: 'レイヤー {{cur}}/{{total}}',
+      timeLeft: '残り {{time}}',
+      statusMode: {
+        off: 'オフ',
+        compact: 'コンパクト',
+        full: 'フル',
+      },
       settings: {
         title: 'カメラウォール設定',
         maxLive: '最大ライブ配信数',
         maxLiveHint: '同時にライブ配信するタイル数。残りはスナップショットとして更新されます。',
         snapshotInterval: 'スナップショット間隔(秒)',
         snapshotIntervalHint: '非ライブのタイルが新しいスナップショットを取得する頻度。',
+        statusOverlay: 'ステータス表示',
+        statusOverlayHint: 'コンパクト:状態バッジのみ。フル:+進捗・レイヤー・残り時間。',
       },
     },
     // Controls
@@ -746,7 +755,6 @@ export default {
     printTime: '印刷時間',
     filamentUsed: 'フィラメント使用量',
     cost: 'コスト',
-    reprint: '再印刷',
     preview: 'プレビュー',
     deleteArchive: 'アーカイブを削除',
     deleteConfirm: 'このアーカイブを削除しますか?',
@@ -793,7 +801,6 @@ export default {
     },
     menu: {
       print: '印刷',
-      schedule: 'スケジュール',
       openInBambuStudio: 'スライサーで開く',
       slice: 'スライス',
       externalLink: '外部リンク',
@@ -888,9 +895,6 @@ export default {
       noFileForReprint: '3MFファイルがありません — 印刷記録時にプリンターからファイルをダウンロードできませんでした',
       noPermissionEdit: 'プロファイルを編集する権限がありません',
       noPermissionDelete: 'アーカイブを削除する権限がありません',
-      reprint: '再印刷',
-      schedulePrint: '印刷をスケジュール',
-      schedule: 'スケジュール',
       openInBambuStudio: 'スライサーで開く',
       openInBambuStudioToSlice: 'スライサーでスライス',
       slice: 'スライス',
@@ -1031,18 +1035,12 @@ export default {
     },
     title: '印刷キュー',
     subtitle: '印刷ジョブのスケジュールと管理',
-    addToQueue: 'キューに追加',
     // Print modal
-    print: '印刷',
-    reprint: '再印刷',
-    schedulePrint: '印刷をスケジュール',
     editQueueItem: 'キューアイテムを編集',
-    printToPrinters: '{{count}}台のプリンターで印刷',
-    queueToPrinters: '{{count}}台のプリンターでキュー追加',
-    queueSelectedPlates: '{{count}}プレートをキューに追加',
     selectAllPlates: '全{{count}}プレートを選択',
     deselectAll: '全て解除',
     printQueued: 'キューに追加しました',
+    printQueuedWillStartWhenIdle: 'プリンターがアイドル状態になると開始します',
     itemsQueued: '{{count}}件をキューに追加しました',
     sending: '送信中...',
     sendingProgress: '送信中 {{current}}/{{total}}...',
@@ -1184,8 +1182,8 @@ export default {
     },
     // Actions
     actions: {
-      stopPrint: '印刷を停止',
       startPrint: '印刷を開始',
+      stopPrint: '印刷を停止',
       requeue: '再キュー',
     },
     // Bulk edit
@@ -1303,35 +1301,6 @@ export default {
     },
   },
 
-  backgroundDispatch: {
-    unknownFile: '不明なファイル',
-    unknownPrinter: '不明なプリンター',
-    startingPrints: '印刷開始中',
-    progressSummary: '{{complete}}/{{total}} 完了 • 配信済み: {{dispatched}} • 処理中: {{processing}}',
-    expandDetails: '配信詳細を展開',
-    collapseDetails: '配信詳細を折りたたむ',
-    dismissToast: '配信トーストを閉じる',
-    cancelDispatchJob: '配信ジョブをキャンセル',
-    cancel: 'キャンセル',
-    cancelling: 'キャンセル中…',
-    awaitingPrinter: 'プリンターを待機中…',
-    status: {
-      dispatched: '配信済み',
-      processing: '処理中',
-      completed: '完了',
-      failed: '失敗',
-      cancelled: 'キャンセル済み',
-    },
-    toast: {
-      cancellingUpload: 'アップロードをキャンセル中...',
-      cancelled: '配信をキャンセルしました',
-      cancelFailed: '配信のキャンセルに失敗しました',
-      completeWithFailures: 'バックグラウンド配信完了: {{completed}} 件成功、{{failed}} 件失敗',
-      completeSuccess: 'バックグラウンド配信完了: {{completed}} 件成功',
-      printStartedRemaining: '{{completed}} 件の印刷を開始、残り {{remaining}} 件送信中...',
-    },
-  },
-
   // Statistics page
   stats: {
     title: '統計',
@@ -2626,6 +2595,42 @@ export default {
     clearErrors: 'エラーをクリア',
     clearSuccess: 'HMSエラーをクリアしました',
     clearFailed: 'HMSエラーのクリアに失敗しました',
+    actionSuccess: 'アクションをプリンターに送信しました',
+    actionFailed: 'アクションの送信に失敗しました',
+    actions: {
+      RESUME_PRINTING: '印刷を再開',
+      RESUME_PRINTING_DEFECTS: '再開(不具合を許容)',
+      RESUME_PRINTING_PROBELM_SOLVED: '再開(問題解決)',
+      STOP_PRINTING: '印刷を停止',
+      CHECK_ASSISTANT: 'アシスタントを開く',
+      FILAMENT_EXTRUDED: 'フィラメント排出済み、続行',
+      RETRY_FILAMENT_EXTRUDED: 'まだ排出されていない、再試行',
+      CONTINUE: '完了、続行',
+      LOAD_VIRTUAL_TRAY: 'フィラメントを装填',
+      OK_BUTTON: 'OK',
+      FILAMENT_LOAD_RESUME: 'フィラメント装填完了、再開',
+      JUMP_TO_LIVEVIEW: 'ライブビューを表示',
+      NO_REMINDER_NEXT_TIME: '次回は通知しない',
+      REFRESH_NOZZLE: '再確認',
+      IGNORE_NO_REMINDER_NEXT_TIME: '無視して次回は通知しない',
+      IGNORE_RESUME: '無視して再開',
+      PROBLEM_SOLVED_RESUME: '問題解決、再開',
+      TURN_OFF_FIRE_ALARM: '了解、火災警報をオフ',
+      RETRY_PROBLEM_SOLVED: '再試行(問題解決)',
+      CANCLE: 'キャンセル',
+      STOP_DRYING: '乾燥を停止',
+      PROCEED: '続行',
+      OK_JUMP_RACK: 'OK',
+      ABORT: '中止',
+      DISABLE_PURIFICATION: 'この印刷では空気清浄を無効化',
+      DONT_REMIND_NEXT_TIME: '今後表示しない',
+      DBL_CHECK_CANCEL: 'キャンセル',
+      DBL_CHECK_DONE: '完了',
+      DBL_CHECK_RETRY: '再試行',
+      DBL_CHECK_RESUME: '再開',
+      DBL_CHECK_OK: '確認',
+      REMOVE_CLOSE_BTN: '閉じる',
+    },
   },
 
   // MQTT Debug modal
@@ -3354,8 +3359,6 @@ export default {
     changeLink: 'リンクを変更...',
     linkTo: 'リンク先...',
     linkToProjectOrArchive: 'プロジェクトまたはアーカイブにリンク',
-    addToQueue: 'キューに追加',
-    schedulePrint: '印刷をスケジュール',
     generateThumbnail: 'サムネイルを生成',
     generateThumbnails: 'サムネイルを生成',
     generateThumbnailsForMissing: 'サムネイルのないSTLファイルのサムネイルを生成',
@@ -3658,8 +3661,6 @@ export default {
       fileCount: '{{count}}ファイル',
       empty: '<空>',
       noFiles: 'このフォルダにファイルはありません。',
-      print: '今すぐ印刷',
-      addToQueue: 'キューに追加',
     },
     bom: {
       title: '部品表',
@@ -4298,7 +4299,6 @@ export default {
 
   // Print modal
   printModal: {
-    title: '印刷を開始',
     selectPrinter: 'プリンターを選択',
     selectPlate: 'プレートを選択',
     filamentMapping: 'フィラメントマッピング',
@@ -4310,15 +4310,25 @@ export default {
     vibrationCalibration: '振動キャリブレーション',
     layerInspection: '第一層検査',
     timelapse: 'タイムラプス',
-    startPrint: '印刷を開始',
-    addToQueue: 'キューに追加',
     cancel: 'キャンセル',
     noPrintersAvailable: '利用可能なプリンターがありません',
     printerBusy: 'プリンターは使用中です',
     printerOffline: 'プリンターはオフラインです',
     sameTypeDifferentColor: '同じ種類、異なる色',
     filamentTypeNotLoaded: 'フィラメントタイプが未読み込み',
+    whenToPrint: '印刷タイミング',
+    asap: '即時',
+    queue: 'キュー',
+    schedule: 'スケジュール',
+    dateTime: '日時',
+    invalidDateTime: '有効な日時を入力してください',
     openCalendar: 'カレンダーを開く',
+    requireManualStart: '手動開始を要求',
+    requirePreviousSuccess: '前の印刷が成功した場合のみ開始',
+    autoOffAfter: '完了後にプリンターの電源を切る',
+    helpAsap: '印刷はキューの先頭に追加され、対象プリンターがアイドルになるとすぐに開始します。',
+    helpSchedule: '予定時刻にプリンターがアイドルであれば印刷を開始します。使用中の場合は、利用可能になるまで待機します。',
+    helpQueue: '印刷はキューの最後に追加されます。',
     leftNozzle: 'L',
     rightNozzle: 'R',
     leftNozzleTooltip: '左ノズル',

+ 62 - 51
frontend/src/i18n/locales/ko.ts

@@ -191,12 +191,21 @@ export default {
       snap: '스냅',
       off: '꺼짐',
       summary: '라이브 {{live}}개, 스냅 {{snap}}개, 총 {{total}}개',
+      layer: '레이어 {{cur}}/{{total}}',
+      timeLeft: '{{time}} 남음',
+      statusMode: {
+        off: '꺼짐',
+        compact: '간단',
+        full: '전체'
+      },
       settings: {
         title: '카메라 월 설정',
         maxLive: '최대 라이브 스트림',
         maxLiveHint: '동시에 라이브 스트리밍할 타일 수. 나머지는 스냅샷으로 갱신됩니다.',
         snapshotInterval: '스냅샷 간격(초)',
-        snapshotIntervalHint: '비라이브 타일이 새 스냅샷을 가져오는 주기.'
+        snapshotIntervalHint: '비라이브 타일이 새 스냅샷을 가져오는 주기.',
+        statusOverlay: '상태 표시',
+        statusOverlayHint: '간단: 상태 배지만 표시. 전체: + 진행률, 레이어, 남은 시간.'
       }
     },
     hideOffline: '오프라인 숨기기',
@@ -704,7 +713,6 @@ export default {
     printTime: '인쇄 시간',
     filamentUsed: '사용된 필라멘트',
     cost: '비용',
-    reprint: '재인쇄',
     preview: '미리보기',
     deleteArchive: '아카이브 삭제',
     deleteConfirm: '이 아카이브를 삭제하시겠습니까?',
@@ -751,7 +759,6 @@ export default {
     },
     menu: {
       print: '인쇄',
-      schedule: '예약',
       openInBambuStudio: '슬라이서에서 열기',
       slice: '슬라이스',
       externalLink: '외부 링크',
@@ -844,9 +851,6 @@ export default {
       noFileForReprint: '3MF 파일 없음 — 인쇄 기록 시 프린터에서 파일을 다운로드할 수 없었습니다',
       noPermissionEdit: '아카이브를 편집할 권한이 없습니다',
       noPermissionDelete: '아카이브를 삭제할 권한이 없습니다',
-      reprint: '재인쇄',
-      schedulePrint: '인쇄 예약',
-      schedule: '예약',
       openInBambuStudio: '슬라이서에서 열기',
       openInBambuStudioToSlice: '슬라이스하려면 슬라이서에서 열기',
       slice: '슬라이스',
@@ -978,17 +982,11 @@ export default {
   queue: {
     title: '인쇄 대기열',
     subtitle: '인쇄 작업을 예약하고 관리하세요',
-    addToQueue: '대기열에 추가',
-    print: '인쇄',
-    reprint: '재인쇄',
-    schedulePrint: '인쇄 예약',
     editQueueItem: '대기열 항목 편집',
-    printToPrinters: '{{count}}개 프린터에 인쇄',
-    queueToPrinters: '{{count}}개 프린터에 대기',
-    queueSelectedPlates: '{{count}}개 플레이트 대기',
     selectAllPlates: '{{count}}개 플레이트 전체 선택',
     deselectAll: '전체 해제',
     printQueued: '인쇄가 대기열에 추가됨',
+    printQueuedWillStartWhenIdle: '프린터가 유휴 상태가 되면 시작됩니다',
     itemsQueued: '{{count}}개 항목이 대기열에 추가됨',
     sending: '전송 중...',
     sendingProgress: '{{current}}/{{total}} 전송 중...',
@@ -1120,8 +1118,8 @@ export default {
       inHours: '{{count}}시간 후'
     },
     actions: {
-      stopPrint: '인쇄 정지',
       startPrint: '인쇄 시작',
+      stopPrint: '인쇄 정지',
       requeue: '재대기'
     },
     bulkEdit: {
@@ -1242,34 +1240,6 @@ export default {
       printAnyway: '그냥 인쇄'
     }
   },
-  backgroundDispatch: {
-    unknownFile: '알 수 없는 파일',
-    unknownPrinter: '알 수 없는 프린터',
-    startingPrints: '인쇄 시작 중',
-    progressSummary: '{{complete}}/{{total}} 완료 • 전송됨: {{dispatched}} • 처리 중: {{processing}}',
-    expandDetails: '전송 상세 펼치기',
-    collapseDetails: '전송 상세 접기',
-    dismissToast: '전송 알림 닫기',
-    cancelDispatchJob: '전송 작업 취소',
-    cancel: '취소',
-    cancelling: '취소 중…',
-    awaitingPrinter: '프린터 대기 중…',
-    status: {
-      dispatched: '전송됨',
-      processing: '처리 중',
-      completed: '완료',
-      failed: '실패',
-      cancelled: '취소됨'
-    },
-    toast: {
-      cancellingUpload: '업로드 취소 중...',
-      cancelled: '전송이 취소되었습니다',
-      cancelFailed: '전송 취소 실패',
-      completeWithFailures: '백그라운드 전송 완료: {{completed}}개 성공, {{failed}}개 실패',
-      completeSuccess: '백그라운드 전송 완료: {{completed}}개 성공',
-      printStartedRemaining: '{{completed}}개 인쇄 시작, {{remaining}}개 더 전송 중...'
-    }
-  },
   stats: {
     title: '대시보드',
     subtitle: '위젯을 드래그하여 정렬하세요. 눈 아이콘을 클릭하여 숨기세요.',
@@ -2476,7 +2446,43 @@ export default {
     clearInstructions: '오류를 해제하려면 프린터에서 오류를 지우세요.',
     clearErrors: '오류 지우기',
     clearSuccess: 'HMS 오류가 지워졌습니다',
-    clearFailed: 'HMS 오류 지우기 실패'
+    clearFailed: 'HMS 오류 지우기 실패',
+    actionSuccess: '프린터에 작업을 전송함',
+    actionFailed: '작업 전송 실패',
+    actions: {
+      RESUME_PRINTING: '인쇄 재개',
+      RESUME_PRINTING_DEFECTS: '재개 (결함 허용)',
+      RESUME_PRINTING_PROBELM_SOLVED: '재개 (문제 해결됨)',
+      STOP_PRINTING: '인쇄 중지',
+      CHECK_ASSISTANT: '도우미 보기',
+      FILAMENT_EXTRUDED: '필라멘트 압출됨, 계속',
+      RETRY_FILAMENT_EXTRUDED: '아직 압출되지 않음, 다시 시도',
+      CONTINUE: '완료, 계속',
+      LOAD_VIRTUAL_TRAY: '필라멘트 로드',
+      OK_BUTTON: '확인',
+      FILAMENT_LOAD_RESUME: '필라멘트 로드됨, 재개',
+      JUMP_TO_LIVEVIEW: '실시간 보기',
+      NO_REMINDER_NEXT_TIME: '다음에 알리지 않음',
+      REFRESH_NOZZLE: '다시 확인',
+      IGNORE_NO_REMINDER_NEXT_TIME: '무시 및 다시 알리지 않음',
+      IGNORE_RESUME: '무시하고 재개',
+      PROBLEM_SOLVED_RESUME: '문제 해결됨, 재개',
+      TURN_OFF_FIRE_ALARM: '확인, 화재 경보 끄기',
+      RETRY_PROBLEM_SOLVED: '다시 시도 (문제 해결됨)',
+      CANCLE: '취소',
+      STOP_DRYING: '건조 중지',
+      PROCEED: '계속',
+      OK_JUMP_RACK: '확인',
+      ABORT: '중단',
+      DISABLE_PURIFICATION: '이 인쇄에 대해 공기 정화 비활성화',
+      DONT_REMIND_NEXT_TIME: '알리지 않음',
+      DBL_CHECK_CANCEL: '취소',
+      DBL_CHECK_DONE: '완료',
+      DBL_CHECK_RETRY: '다시 시도',
+      DBL_CHECK_RESUME: '재개',
+      DBL_CHECK_OK: '확인',
+      REMOVE_CLOSE_BTN: '닫기',
+    },
   },
   mqttDebug: {
     title: 'MQTT 디버그 로그',
@@ -3167,8 +3173,6 @@ export default {
     changeLink: '링크 변경...',
     linkTo: '연결 대상...',
     linkToProjectOrArchive: '프로젝트 또는 아카이브에 연결',
-    addToQueue: '대기열에 추가',
-    schedulePrint: '예약',
     generateThumbnail: '썸네일 생성',
     generateThumbnails: '썸네일 생성',
     generateThumbnailsForMissing: '썸네일이 없는 STL 파일의 썸네일 생성',
@@ -3459,9 +3463,7 @@ export default {
       forQuickAccess: '빠른 접근을 위해 이 프로젝트에 연결합니다.',
       fileCount: '{{count}}개 파일',
       empty: '연결된 폴더가 없습니다. 파일 관리자로 이동하여 폴더를 이 프로젝트에 연결하세요.',
-      noFiles: '이 폴더에 파일이 없습니다.',
-      print: '지금 인쇄',
-      addToQueue: '대기열에 추가'
+      noFiles: '이 폴더에 파일이 없습니다.'
     },
     bom: {
       title: '부품 목록',
@@ -4071,7 +4073,6 @@ export default {
     emptySlotReset: '필라멘트가 할당되지 않음'
   },
   printModal: {
-    title: '인쇄 시작',
     selectPrinter: '프린터 선택',
     selectPlate: '플레이트 선택',
     filamentMapping: '필라멘트 매핑',
@@ -4083,15 +4084,25 @@ export default {
     vibrationCalibration: '진동 보정',
     layerInspection: '첫 번째 레이어 검사',
     timelapse: '타임랩스',
-    startPrint: '인쇄 시작',
-    addToQueue: '대기열에 추가',
     cancel: '취소',
     noPrintersAvailable: '사용 가능한 프린터 없음',
     printerBusy: '프린터가 사용 중입니다',
     printerOffline: '프린터가 오프라인입니다',
     sameTypeDifferentColor: '같은 유형, 다른 색상',
     filamentTypeNotLoaded: '필라멘트 유형이 장착되지 않음',
+    whenToPrint: '인쇄 시점',
+    asap: '최대한 빨리',
+    queue: '대기열',
+    schedule: '예약',
+    dateTime: '날짜 및 시간',
+    invalidDateTime: '유효한 날짜와 시간을 입력하세요',
     openCalendar: '달력 열기',
+    requireManualStart: '수동 시작 필요',
+    requirePreviousSuccess: '이전 인쇄가 성공한 경우에만 시작',
+    autoOffAfter: '완료 후 프린터 전원 끄기',
+    helpAsap: '인쇄가 대기열 맨 앞에 추가되고 적합한 프린터가 유휴 상태가 되는 즉시 시작됩니다.',
+    helpSchedule: '프린터가 유휴 상태이면 예약된 시간에 인쇄가 시작됩니다. 사용 중이면 프린터가 사용 가능해질 때까지 대기합니다.',
+    helpQueue: '인쇄가 대기열 맨 뒤에 추가됩니다.',
     leftNozzle: 'L',
     rightNozzle: 'R',
     leftNozzleTooltip: '왼쪽 노즐',

+ 59 - 49
frontend/src/i18n/locales/pt-BR.ts

@@ -204,12 +204,21 @@ export default {
       snap: 'Foto',
       off: 'Desligado',
       summary: '{{live}} ao vivo, {{snap}} fotos, {{total}} no total',
+      layer: 'Camada {{cur}}/{{total}}',
+      timeLeft: 'faltam {{time}}',
+      statusMode: {
+        off: 'Desligado',
+        compact: 'Compacto',
+        full: 'Completo',
+      },
       settings: {
         title: 'Configurações do mural de câmeras',
         maxLive: 'Máx. transmissões ao vivo',
         maxLiveHint: 'Quantos blocos transmitem ao vivo simultaneamente. Os demais atualizam como fotos.',
         snapshotInterval: 'Intervalo de foto (segundos)',
         snapshotIntervalHint: 'Com que frequência os blocos não ao vivo buscam uma nova foto.',
+        statusOverlay: 'Sobreposição de status',
+        statusOverlayHint: 'Compacto: apenas o selo de status. Completo: + progresso, camada e tempo restante.',
       },
     },
     // Controls
@@ -747,7 +756,6 @@ export default {
     printTime: 'Tempo de impressão',
     filamentUsed: 'Filamento usado',
     cost: 'Custo',
-    reprint: 'Reimprimir',
     preview: 'Pré-visualizar',
     deleteArchive: 'Excluir arquivo',
     deleteConfirm: 'Tem certeza de que deseja excluir este arquivo?',
@@ -794,7 +802,6 @@ export default {
     },
     menu: {
       print: 'Imprimir',
-      schedule: 'Agendar',
       openInBambuStudio: 'Abrir no Slicer',
       slice: 'Fatiar',
       externalLink: 'Link externo',
@@ -889,9 +896,6 @@ export default {
       noFileForReprint: 'Nenhum arquivo 3MF disponível — o arquivo não pôde ser baixado da impressora quando a impressão foi registrada',
       noPermissionEdit: 'Você não tem permissão para editar arquivos',
       noPermissionDelete: 'Você não tem permissão para excluir arquivos',
-      reprint: 'Reimprimir',
-      schedulePrint: 'Agendar impressão',
-      schedule: 'Agendar',
       openInBambuStudio: 'Abrir no Bambu Studio',
       openInBambuStudioToSlice: 'Abrir no Bambu Studio para fatiar',
       slice: 'Fatiar',
@@ -1032,18 +1036,12 @@ export default {
     },
     title: 'Fila de Impressão',
     subtitle: 'Agende e gerencie seus trabalhos de impressão',
-    addToQueue: 'Adicionar à Fila',
     // Print modal
-    print: 'Imprimir',
-    reprint: 'Reimprimir',
-    schedulePrint: 'Agendar Impressão',
     editQueueItem: 'Editar Item da Fila',
-    printToPrinters: 'Imprimir para {{count}} Impressoras',
-    queueToPrinters: 'Adicionar à Fila para {{count}} Impressoras',
-    queueSelectedPlates: 'Adicionar {{count}} placas à fila',
     selectAllPlates: 'Selecionar todas as {{count}} placas',
     deselectAll: 'Desmarcar tudo',
     printQueued: 'Impressão adicionada à fila',
+    printQueuedWillStartWhenIdle: 'Iniciará quando a impressora estiver ociosa',
     itemsQueued: '{{count}} itens adicionados à fila',
     sending: 'Enviando...',
     sendingProgress: 'Enviando {{current}}/{{total}}...',
@@ -1185,8 +1183,8 @@ export default {
     },
     // Actions
     actions: {
-      stopPrint: 'Parar Impressão',
       startPrint: 'Iniciar Impressão',
+      stopPrint: 'Parar Impressão',
       requeue: 'Reenfileirar',
     },
     // Bulk edit
@@ -1304,35 +1302,6 @@ export default {
     },
   },
 
-  backgroundDispatch: {
-    unknownFile: 'Arquivo desconhecido',
-    unknownPrinter: 'Impressora desconhecida',
-    startingPrints: 'Iniciando impressões',
-    progressSummary: '{{complete}}/{{total}} concluídos • Despachados: {{dispatched}} • Processando: {{processing}}',
-    expandDetails: 'Expand dispatch details',
-    collapseDetails: 'Collapse dispatch details',
-    dismissToast: 'Dispensar notificação de despacho',
-    cancelDispatchJob: 'Cancelar despacho',
-    cancel: 'Cancelar',
-    cancelling: 'Cancelando…',
-    awaitingPrinter: 'Aguardando impressora…',
-    status: {
-      dispatched: 'Despachado',
-      processing: 'Processando',
-      completed: 'Concluído',
-      failed: 'Falhou',
-      cancelled: 'Cancelado',
-    },
-    toast: {
-      cancellingUpload: 'Cancelling upload...',
-      cancelled: 'Despacho cancelado',
-      cancelFailed: 'Falha ao cancelar despacho',
-      completeWithFailures: 'Despacho em segundo plano concluído: {{completed}} sucesso(s), {{failed}} falha(s)',
-      completeSuccess: 'Despacho em segundo plano concluído: {{completed}} sucesso(s)',
-      printStartedRemaining: '{{completed}} impressão(ões) iniciada(s), {{remaining}} enviando...',
-    },
-  },
-
   // Statistics page
   stats: {
     title: 'Estatísticas',
@@ -2614,6 +2583,42 @@ export default {
     clearErrors: 'Limpar Erros',
     clearSuccess: 'Erros HMS limpos',
     clearFailed: 'Falha ao limpar erros HMS',
+    actionSuccess: 'Ação enviada à impressora',
+    actionFailed: 'Falha ao enviar ação',
+    actions: {
+      RESUME_PRINTING: 'Retomar impressão',
+      RESUME_PRINTING_DEFECTS: 'Retomar (defeitos aceitáveis)',
+      RESUME_PRINTING_PROBELM_SOLVED: 'Retomar (problema resolvido)',
+      STOP_PRINTING: 'Parar impressão',
+      CHECK_ASSISTANT: 'Ver assistente',
+      FILAMENT_EXTRUDED: 'Filamento extrudado, continuar',
+      RETRY_FILAMENT_EXTRUDED: 'Ainda não extrudado, tentar novamente',
+      CONTINUE: 'Finalizado, continuar',
+      LOAD_VIRTUAL_TRAY: 'Carregar filamento',
+      OK_BUTTON: 'OK',
+      FILAMENT_LOAD_RESUME: 'Filamento carregado, retomar',
+      JUMP_TO_LIVEVIEW: 'Ver ao vivo',
+      NO_REMINDER_NEXT_TIME: 'Não lembrar da próxima vez',
+      REFRESH_NOZZLE: 'Verificar novamente',
+      IGNORE_NO_REMINDER_NEXT_TIME: 'Ignorar e não lembrar',
+      IGNORE_RESUME: 'Ignorar e retomar',
+      PROBLEM_SOLVED_RESUME: 'Problema resolvido, retomar',
+      TURN_OFF_FIRE_ALARM: 'Entendido, desligar alarme de incêndio',
+      RETRY_PROBLEM_SOLVED: 'Tentar novamente (problema resolvido)',
+      CANCLE: 'Cancelar',
+      STOP_DRYING: 'Parar secagem',
+      PROCEED: 'Prosseguir',
+      OK_JUMP_RACK: 'OK',
+      ABORT: 'Abortar',
+      DISABLE_PURIFICATION: 'Desativar purificação para esta impressão',
+      DONT_REMIND_NEXT_TIME: 'Não me lembrar',
+      DBL_CHECK_CANCEL: 'Cancelar',
+      DBL_CHECK_DONE: 'Concluído',
+      DBL_CHECK_RETRY: 'Tentar novamente',
+      DBL_CHECK_RESUME: 'Retomar',
+      DBL_CHECK_OK: 'Confirmar',
+      REMOVE_CLOSE_BTN: 'Fechar',
+    },
   },
 
   // MQTT Debug modal
@@ -3342,8 +3347,6 @@ export default {
     changeLink: 'Alterar link...',
     linkTo: 'Vincular a...',
     linkToProjectOrArchive: 'Vincular a projeto ou arquivo',
-    addToQueue: 'Adicionar à fila',
-    schedulePrint: 'Agendar impressão',
     generateThumbnail: 'Gerar miniatura',
     generateThumbnails: 'Gerar miniaturas',
     generateThumbnailsForMissing: 'Gerar miniaturas para arquivos STL que não possuem',
@@ -3646,8 +3649,6 @@ export default {
       fileCount: '{{count}} arquivo(s)',
       empty: 'Nenhuma pasta vinculada. Vá para o Gerenciador de Arquivos e vincule uma pasta a este projeto.',
       noFiles: 'Nenhum arquivo nesta pasta.',
-      print: 'Imprimir agora',
-      addToQueue: 'Adicionar à fila',
     },
     bom: {
       title: 'Lista de Materiais',
@@ -4286,7 +4287,6 @@ export default {
 
   // Print modal
   printModal: {
-    title: 'Iniciar Impressão',
     selectPrinter: 'Selecionar Impressora',
     selectPlate: 'Selecionar Placa',
     filamentMapping: 'Mapeamento de Filamento',
@@ -4298,15 +4298,25 @@ export default {
     vibrationCalibration: 'Calibração de Vibração',
     layerInspection: 'Inspeção da Primeira Camada',
     timelapse: 'Timelapse',
-    startPrint: 'Iniciar Impressão',
-    addToQueue: 'Adicionar à Fila',
     cancel: 'Cancelar',
     noPrintersAvailable: 'Nenhuma impressora disponível',
     printerBusy: 'Impressora ocupada',
     printerOffline: 'Impressora offline',
     sameTypeDifferentColor: 'Mesmo tipo, cor diferente',
     filamentTypeNotLoaded: 'Tipo de filamento não carregado',
+    whenToPrint: 'Quando imprimir',
+    asap: 'O quanto antes',
+    queue: 'Fila',
+    schedule: 'Agendar',
+    dateTime: 'Data e hora',
+    invalidDateTime: 'Digite uma data e hora válidas',
     openCalendar: 'Abrir calendário',
+    requireManualStart: 'Exigir início manual',
+    requirePreviousSuccess: 'Iniciar somente se a impressão anterior foi concluída com sucesso',
+    autoOffAfter: 'Desligar impressora ao finalizar',
+    helpAsap: 'A impressão será adicionada ao topo da fila e começará assim que uma impressora elegível estiver ociosa.',
+    helpSchedule: 'A impressão começará no horário agendado se a impressora estiver ociosa. Se estiver ocupada, aguardará até ficar disponível.',
+    helpQueue: 'A impressão será adicionada ao fim da fila.',
     leftNozzle: 'L',
     rightNozzle: 'R',
     leftNozzleTooltip: 'Bico esquerdo',

+ 59 - 48
frontend/src/i18n/locales/tr.ts

@@ -204,12 +204,21 @@ export default {
       snap: 'Foto',
       off: 'Kapalı',
       summary: '{{live}} canlı, {{snap}} fotoğraf, toplam {{total}}',
+      layer: 'Katman {{cur}}/{{total}}',
+      timeLeft: '{{time}} kaldı',
+      statusMode: {
+        off: 'Kapalı',
+        compact: 'Sade',
+        full: 'Tam',
+      },
       settings: {
         title: 'Kamera duvarı ayarları',
         maxLive: 'Maks. canlı yayın',
         maxLiveHint: 'Aynı anda kaç döşemenin canlı yayın yaptığı. Diğerleri foto olarak yenilenir.',
         snapshotInterval: 'Foto aralığı (saniye)',
         snapshotIntervalHint: 'Canlı olmayan döşemelerin ne sıklıkla yeni bir foto aldığı.',
+        statusOverlay: 'Durum kaplaması',
+        statusOverlayHint: 'Sade: yalnızca durum rozeti. Tam: + ilerleme, katman, kalan süre.',
       },
     },
     // Kontroller
@@ -747,7 +756,6 @@ export default {
     printTime: 'Baskı Süresi',
     filamentUsed: 'Kullanılan Filament',
     cost: 'Maliyet',
-    reprint: 'Tekrar Yazdır',
     preview: 'Önizleme',
     deleteArchive: 'Arşivi Sil',
     deleteConfirm: 'Bu arşivi silmek istediğinizden emin misiniz?',
@@ -794,7 +802,6 @@ export default {
     },
     menu: {
       print: 'Yazdır',
-      schedule: 'Zamanla',
       openInBambuStudio: 'Dilimleyicide Aç',
       slice: 'Dilimle',
       externalLink: 'Harici Bağlantı',
@@ -889,9 +896,6 @@ export default {
       noFileForReprint: 'Kullanılabilir 3MF dosyası yok — baskı kaydedildiğinde dosya yazıcıdan indirilemedi',
       noPermissionEdit: 'Arşivleri düzenleme izniniz yok',
       noPermissionDelete: 'Arşivleri silme izniniz yok',
-      reprint: 'Tekrar Yazdır',
-      schedulePrint: 'Baskıyı Zamanla',
-      schedule: 'Zamanla',
       openInBambuStudio: 'Dilimleyicide Aç',
       openInBambuStudioToSlice: 'Dilimlemek için Dilimleyicide Aç',
       slice: 'Dilimle',
@@ -1023,7 +1027,6 @@ export default {
   queue: {
     title: 'Baskı Kuyruğu',
     subtitle: 'Baskı işlerinizi zamanlayın ve yönetin',
-    addToQueue: 'Kuyruğa Ekle',
     filamentShort: {
       rowBadge: 'Atanan makara için yetersiz filament',
       rowTooltip: 'Sevk planlayıcı bu öğeyi işaretledi. Yuva başına eksikliği görmek ve yine de yazdırıp yazdırmayacağınıza karar vermek için Oynat\'a tıklayın.',
@@ -1034,16 +1037,11 @@ export default {
       printAnyway: 'Yine de Yazdır',
     },
     // Baskı modali
-    print: 'Yazdır',
-    reprint: 'Tekrar Yazdır',
-    schedulePrint: 'Baskıyı Zamanla',
     editQueueItem: 'Kuyruk Öğesini Düzenle',
-    printToPrinters: '{{count}} Yazıcıya Yazdır',
-    queueToPrinters: '{{count}} Yazıcıya Kuyrukla',
-    queueSelectedPlates: '{{count}} Plakayı Kuyrukla',
     selectAllPlates: 'Tüm {{count}} Plakayı Seç',
     deselectAll: 'Seçimi Kaldır',
     printQueued: 'Baskı kuyruğa eklendi',
+    printQueuedWillStartWhenIdle: 'Yazıcı boştayken başlayacak',
     itemsQueued: '{{count}} öğe kuyruğa eklendi',
     sending: 'Gönderiliyor...',
     sendingProgress: 'Gönderiliyor {{current}}/{{total}}...',
@@ -1185,8 +1183,8 @@ export default {
     },
     // İşlemler
     actions: {
-      stopPrint: 'Baskıyı Durdur',
       startPrint: 'Baskıyı Başlat',
+      stopPrint: 'Baskıyı Durdur',
       requeue: 'Yeniden Kuyrukla',
     },
     // Toplu düzenleme
@@ -1304,34 +1302,6 @@ export default {
     },
   },
 
-  backgroundDispatch: {
-    unknownFile: 'Bilinmeyen dosya',
-    unknownPrinter: 'Bilinmeyen yazıcı',
-    startingPrints: 'Baskılar başlatılıyor',
-    progressSummary: '{{complete}}/{{total}} tamamlandı • Sevk edildi: {{dispatched}} • İşleniyor: {{processing}}',
-    expandDetails: 'Sevk ayrıntılarını genişlet',
-    collapseDetails: 'Sevk ayrıntılarını daralt',
-    dismissToast: 'Sevk bildirimini kapat',
-    cancelDispatchJob: 'Sevk işini iptal et',
-    cancel: 'İptal',
-    cancelling: 'İptal ediliyor…',
-    awaitingPrinter: 'Yazıcı bekleniyor…',
-    status: {
-      dispatched: 'Sevk Edildi',
-      processing: 'İşleniyor',
-      completed: 'Tamamlandı',
-      failed: 'Başarısız',
-      cancelled: 'İptal edildi',
-    },
-    toast: {
-      cancellingUpload: 'Yükleme iptal ediliyor...',
-      cancelled: 'Sevk iptal edildi',
-      cancelFailed: 'Sevk iptal edilemedi',
-      completeWithFailures: 'Arka plan sevki tamamlandı: {{completed}} başarılı, {{failed}} başarısız',
-      completeSuccess: 'Arka plan sevki tamamlandı: {{completed}} başarılı',
-      printStartedRemaining: '{{completed}} baskı başlatıldı, {{remaining}} tane daha gönderiliyor...',
-    },
-  },
 
   // İstatistikler sayfası
   stats: {
@@ -2629,6 +2599,42 @@ export default {
     clearErrors: 'Hataları Temizle',
     clearSuccess: 'HMS hataları temizlendi',
     clearFailed: 'HMS hataları temizlenemedi',
+    actionSuccess: 'Eylem yazıcıya gönderildi',
+    actionFailed: 'Eylem gönderilemedi',
+    actions: {
+      RESUME_PRINTING: 'Baskıyı sürdür',
+      RESUME_PRINTING_DEFECTS: 'Sürdür (kusurlar kabul edilebilir)',
+      RESUME_PRINTING_PROBELM_SOLVED: 'Sürdür (sorun çözüldü)',
+      STOP_PRINTING: 'Baskıyı durdur',
+      CHECK_ASSISTANT: 'Asistanı aç',
+      FILAMENT_EXTRUDED: 'Filament ekstrude edildi, devam et',
+      RETRY_FILAMENT_EXTRUDED: 'Henüz ekstrude edilmedi, tekrar dene',
+      CONTINUE: 'Tamamlandı, devam et',
+      LOAD_VIRTUAL_TRAY: 'Filament yükle',
+      OK_BUTTON: 'Tamam',
+      FILAMENT_LOAD_RESUME: 'Filament yüklendi, sürdür',
+      JUMP_TO_LIVEVIEW: 'Canlı görünümü aç',
+      NO_REMINDER_NEXT_TIME: 'Bir daha hatırlatma',
+      REFRESH_NOZZLE: 'Tekrar kontrol et',
+      IGNORE_NO_REMINDER_NEXT_TIME: 'Yok say ve bir daha hatırlatma',
+      IGNORE_RESUME: 'Yok say ve sürdür',
+      PROBLEM_SOLVED_RESUME: 'Sorun çözüldü, sürdür',
+      TURN_OFF_FIRE_ALARM: 'Anlaşıldı, yangın alarmını kapat',
+      RETRY_PROBLEM_SOLVED: 'Tekrar dene (sorun çözüldü)',
+      CANCLE: 'İptal',
+      STOP_DRYING: 'Kurutmayı durdur',
+      PROCEED: 'Devam et',
+      OK_JUMP_RACK: 'Tamam',
+      ABORT: 'İptal',
+      DISABLE_PURIFICATION: 'Bu baskı için arıtmayı devre dışı bırak',
+      DONT_REMIND_NEXT_TIME: 'Beni hatırlatma',
+      DBL_CHECK_CANCEL: 'İptal',
+      DBL_CHECK_DONE: 'Tamam',
+      DBL_CHECK_RETRY: 'Tekrar dene',
+      DBL_CHECK_RESUME: 'Sürdür',
+      DBL_CHECK_OK: 'Onayla',
+      REMOVE_CLOSE_BTN: 'Kapat',
+    },
   },
 
   // MQTT Hata Ayıklama modali
@@ -3349,8 +3355,6 @@ export default {
     changeLink: 'Bağlantıyı Değiştir...',
     linkTo: 'Şuna bağla...',
     linkToProjectOrArchive: 'Projeye veya arşive bağla',
-    addToQueue: 'Kuyruğa Ekle',
-    schedulePrint: 'Zamanla',
     generateThumbnail: 'Küçük Resim Oluştur',
     generateThumbnails: 'Küçük Resimler Oluştur',
     generateThumbnailsForMissing: 'Eksik olan STL dosyaları için küçük resimler oluştur',
@@ -3647,8 +3651,6 @@ export default {
       fileCount: '{{count}} dosya',
       empty: 'Bağlı klasör yok. Dosya Yöneticisine gidin ve bu projeye bir klasör bağlayın.',
       noFiles: 'Bu klasörde dosya yok.',
-      print: 'Şimdi Yazdır',
-      addToQueue: 'Kuyruğa Ekle',
     },
     bom: {
       title: 'Malzeme Listesi',
@@ -4275,7 +4277,6 @@ export default {
 
   // Baskı modali
   printModal: {
-    title: 'Baskıyı Başlat',
     selectPrinter: 'Yazıcı Seç',
     selectPlate: 'Plaka Seç',
     filamentMapping: 'Filament Eşlemesi',
@@ -4287,15 +4288,25 @@ export default {
     vibrationCalibration: 'Titreşim Kalibrasyonu',
     layerInspection: 'İlk Katman Denetimi',
     timelapse: 'Zaman Atlamalı Video',
-    startPrint: 'Baskıyı Başlat',
-    addToQueue: 'Kuyruğa Ekle',
     cancel: 'İptal',
     noPrintersAvailable: 'Kullanılabilir yazıcı yok',
     printerBusy: 'Yazıcı meşgul',
     printerOffline: 'Yazıcı çevrimdışı',
     sameTypeDifferentColor: 'Aynı tür, farklı renk',
     filamentTypeNotLoaded: 'Filament türü yüklenmedi',
+    whenToPrint: 'Ne zaman yazdırılsın',
+    asap: 'En kısa sürede',
+    queue: 'Kuyruk',
+    schedule: 'Zamanla',
+    dateTime: 'Tarih ve saat',
+    invalidDateTime: 'Lütfen geçerli bir tarih ve saat girin',
     openCalendar: 'Takvimi aç',
+    requireManualStart: 'Manuel başlatma gerektir',
+    requirePreviousSuccess: 'Yalnızca önceki baskı başarılıysa başlat',
+    autoOffAfter: 'Bittiğinde yazıcıyı kapat',
+    helpAsap: 'Baskı kuyruğun en üstüne eklenir ve uygun bir yazıcı boştaysa başlar.',
+    helpSchedule: 'Yazıcı boşta ise baskı zamanlanan saatte başlar. Meşgulse yazıcı uygun olana kadar bekler.',
+    helpQueue: 'Baskı kuyruğun sonuna eklenir.',
     leftNozzle: 'S',
     rightNozzle: 'R',
     leftNozzleTooltip: 'Sol nozul',

+ 59 - 49
frontend/src/i18n/locales/zh-CN.ts

@@ -204,12 +204,21 @@ export default {
       snap: '快照',
       off: '关闭',
       summary: '直播 {{live}} 个,快照 {{snap}} 个,共 {{total}} 个',
+      layer: '第 {{cur}}/{{total}} 层',
+      timeLeft: '剩余 {{time}}',
+      statusMode: {
+        off: '关闭',
+        compact: '简洁',
+        full: '完整',
+      },
       settings: {
         title: '摄像头墙设置',
         maxLive: '最大直播数',
         maxLiveHint: '同时直播的画面数量。其他画面以快照刷新。',
         snapshotInterval: '快照刷新间隔(秒)',
         snapshotIntervalHint: '非直播画面获取新快照的频率。',
+        statusOverlay: '状态叠加',
+        statusOverlayHint: '简洁:仅显示状态标签。完整:加上进度、层数、剩余时间。',
       },
     },
     // Controls
@@ -747,7 +756,6 @@ export default {
     printTime: '打印时间',
     filamentUsed: '耗材用量',
     cost: '成本',
-    reprint: '重新打印',
     preview: '预览',
     deleteArchive: '删除归档',
     deleteConfirm: '确定要删除此归档吗?',
@@ -794,7 +802,6 @@ export default {
     },
     menu: {
       print: '打印',
-      schedule: '排程',
       openInBambuStudio: '在切片软件中打开',
       slice: '切片',
       externalLink: '外部链接',
@@ -889,9 +896,6 @@ export default {
       noFileForReprint: '无可用的 3MF 文件 — 打印记录时无法从打印机下载该文件',
       noPermissionEdit: '您没有编辑归档的权限',
       noPermissionDelete: '您没有删除归档的权限',
-      reprint: '重新打印',
-      schedulePrint: '排程打印',
-      schedule: '排程',
       openInBambuStudio: '在切片软件中打开',
       openInBambuStudioToSlice: '在切片软件中打开进行切片',
       slice: '切片',
@@ -1032,18 +1036,12 @@ export default {
     },
     title: '打印队列',
     subtitle: '排程和管理您的打印任务',
-    addToQueue: '添加到队列',
     // Print modal
-    print: '打印',
-    reprint: '重新打印',
-    schedulePrint: '排程打印',
     editQueueItem: '编辑队列项目',
-    printToPrinters: '打印到 {{count}} 台打印机',
-    queueToPrinters: '排队到 {{count}} 台打印机',
-    queueSelectedPlates: '将 {{count}} 个热床加入队列',
     selectAllPlates: '选择全部 {{count}} 个热床',
     deselectAll: '取消全选',
     printQueued: '已加入打印队列',
+    printQueuedWillStartWhenIdle: '打印机空闲时将开始',
     itemsQueued: '{{count}} 个任务已加入队列',
     sending: '发送中...',
     sendingProgress: '发送中 {{current}}/{{total}}...',
@@ -1185,8 +1183,8 @@ export default {
     },
     // Actions
     actions: {
-      stopPrint: '停止打印',
       startPrint: '开始打印',
+      stopPrint: '停止打印',
       requeue: '重新排队',
     },
     // Bulk edit
@@ -1304,35 +1302,6 @@ export default {
     },
   },
 
-  backgroundDispatch: {
-    unknownFile: '未知文件',
-    unknownPrinter: '未知打印机',
-    startingPrints: '正在开始打印',
-    progressSummary: '{{complete}}/{{total}} 完成 • 已分发:{{dispatched}} • 处理中:{{processing}}',
-    expandDetails: '展开分发详情',
-    collapseDetails: '收起分发详情',
-    dismissToast: '关闭分发通知',
-    cancelDispatchJob: '取消分发任务',
-    cancel: '取消',
-    cancelling: '取消中…',
-    awaitingPrinter: '等待打印机…',
-    status: {
-      dispatched: '已分发',
-      processing: '处理中',
-      completed: '已完成',
-      failed: '失败',
-      cancelled: '已取消',
-    },
-    toast: {
-      cancellingUpload: '取消上传中...',
-      cancelled: '分发已取消',
-      cancelFailed: '取消分发失败',
-      completeWithFailures: '后台分发完成:{{completed}} 成功,{{failed}} 失败',
-      completeSuccess: '后台分发完成:{{completed}} 成功',
-      printStartedRemaining: '{{completed}} 个打印已开始,{{remaining}} 个正在发送...',
-    },
-  },
-
   // Statistics page
   stats: {
     title: '统计',
@@ -2614,6 +2583,42 @@ export default {
     clearErrors: '清除错误',
     clearSuccess: 'HMS 错误已清除',
     clearFailed: '清除 HMS 错误失败',
+    actionSuccess: '已向打印机发送操作',
+    actionFailed: '操作发送失败',
+    actions: {
+      RESUME_PRINTING: '恢复打印',
+      RESUME_PRINTING_DEFECTS: '恢复 (缺陷可接受)',
+      RESUME_PRINTING_PROBELM_SOLVED: '恢复 (问题已解决)',
+      STOP_PRINTING: '停止打印',
+      CHECK_ASSISTANT: '查看助手',
+      FILAMENT_EXTRUDED: '已挤出耗材,继续',
+      RETRY_FILAMENT_EXTRUDED: '尚未挤出,重试',
+      CONTINUE: '已完成,继续',
+      LOAD_VIRTUAL_TRAY: '加载耗材',
+      OK_BUTTON: '确定',
+      FILAMENT_LOAD_RESUME: '耗材已加载,恢复',
+      JUMP_TO_LIVEVIEW: '查看实时画面',
+      NO_REMINDER_NEXT_TIME: '下次不再提醒',
+      REFRESH_NOZZLE: '重新检查',
+      IGNORE_NO_REMINDER_NEXT_TIME: '忽略,下次不再提醒',
+      IGNORE_RESUME: '忽略并恢复',
+      PROBLEM_SOLVED_RESUME: '问题已解决,恢复',
+      TURN_OFF_FIRE_ALARM: '知道了,关闭火警',
+      RETRY_PROBLEM_SOLVED: '重试 (问题已解决)',
+      CANCLE: '取消',
+      STOP_DRYING: '停止干燥',
+      PROCEED: '继续',
+      OK_JUMP_RACK: '确定',
+      ABORT: '终止',
+      DISABLE_PURIFICATION: '本次打印禁用空气净化',
+      DONT_REMIND_NEXT_TIME: '不再提醒',
+      DBL_CHECK_CANCEL: '取消',
+      DBL_CHECK_DONE: '完成',
+      DBL_CHECK_RETRY: '重试',
+      DBL_CHECK_RESUME: '恢复',
+      DBL_CHECK_OK: '确认',
+      REMOVE_CLOSE_BTN: '关闭',
+    },
   },
 
   // MQTT Debug modal
@@ -3342,8 +3347,6 @@ export default {
     changeLink: '更改链接...',
     linkTo: '链接到...',
     linkToProjectOrArchive: '链接到项目或归档',
-    addToQueue: '添加到队列',
-    schedulePrint: '排程',
     generateThumbnail: '生成缩略图',
     generateThumbnails: '生成缩略图',
     generateThumbnailsForMissing: '为缺少缩略图的 STL 文件生成缩略图',
@@ -3646,8 +3649,6 @@ export default {
       fileCount: '{{count}} 个文件',
       empty: '未链接文件夹。前往文件管理器将文件夹链接到此项目。',
       noFiles: '此文件夹中没有文件。',
-      print: '立即打印',
-      addToQueue: '加入队列',
     },
     bom: {
       title: '材料清单',
@@ -4286,7 +4287,6 @@ export default {
 
   // Print modal
   printModal: {
-    title: '开始打印',
     selectPrinter: '选择打印机',
     selectPlate: '选择板',
     filamentMapping: '耗材映射',
@@ -4298,15 +4298,25 @@ export default {
     vibrationCalibration: '振动校准',
     layerInspection: '首层检查',
     timelapse: '延时摄影',
-    startPrint: '开始打印',
-    addToQueue: '添加到队列',
     cancel: '取消',
     noPrintersAvailable: '无可用打印机',
     printerBusy: '打印机忙碌',
     printerOffline: '打印机离线',
     sameTypeDifferentColor: '相同类型,不同颜色',
     filamentTypeNotLoaded: '耗材类型未装载',
+    whenToPrint: '何时打印',
+    asap: '尽快',
+    queue: '队列',
+    schedule: '计划',
+    dateTime: '日期和时间',
+    invalidDateTime: '请输入有效的日期和时间',
     openCalendar: '打开日历',
+    requireManualStart: '要求手动开始',
+    requirePreviousSuccess: '仅在上一次打印成功后开始',
+    autoOffAfter: '完成后关闭打印机',
+    helpAsap: '打印将添加到队列顶部,并在符合条件的打印机空闲后立即开始。',
+    helpSchedule: '如果打印机空闲,打印将在计划时间开始。如果打印机忙碌,则会等待其可用。',
+    helpQueue: '打印将添加到队列末尾。',
     leftNozzle: '左',
     rightNozzle: '右',
     leftNozzleTooltip: '左喷嘴',

+ 59 - 49
frontend/src/i18n/locales/zh-TW.ts

@@ -204,12 +204,21 @@ export default {
       snap: '快照',
       off: '關閉',
       summary: '直播 {{live}} 個,快照 {{snap}} 個,共 {{total}} 個',
+      layer: '第 {{cur}}/{{total}} 層',
+      timeLeft: '剩餘 {{time}}',
+      statusMode: {
+        off: '關閉',
+        compact: '精簡',
+        full: '完整',
+      },
       settings: {
         title: '攝影機牆設定',
         maxLive: '最大直播數',
         maxLiveHint: '同時直播的畫面數量。其他畫面以快照重新整理。',
         snapshotInterval: '快照重新整理間隔(秒)',
         snapshotIntervalHint: '非直播畫面取得新快照的頻率。',
+        statusOverlay: '狀態疊加',
+        statusOverlayHint: '精簡:僅顯示狀態標籤。完整:加上進度、層數、剩餘時間。',
       },
     },
     // Controls
@@ -747,7 +756,6 @@ export default {
     printTime: '列印時間',
     filamentUsed: '耗材用量',
     cost: '成本',
-    reprint: '重新列印',
     preview: '預覽',
     deleteArchive: '刪除歸檔',
     deleteConfirm: '確定要刪除此歸檔嗎?',
@@ -794,7 +802,6 @@ export default {
     },
     menu: {
       print: '列印',
-      schedule: '排程',
       openInBambuStudio: '在切片軟體中開啟',
       slice: '切片',
       externalLink: '外部連結',
@@ -889,9 +896,6 @@ export default {
       noFileForReprint: '無可用的 3MF 檔案 — 列印紀錄時無法從印表機下載該檔案',
       noPermissionEdit: '您沒有編輯歸檔的權限',
       noPermissionDelete: '您沒有刪除歸檔的權限',
-      reprint: '重新列印',
-      schedulePrint: '排程列印',
-      schedule: '排程',
       openInBambuStudio: '在切片軟體中開啟',
       openInBambuStudioToSlice: '在切片軟體中開啟進行切片',
       slice: '切片',
@@ -1032,18 +1036,12 @@ export default {
     },
     title: '列印佇列',
     subtitle: '排程和管理您的列印任務',
-    addToQueue: '新增到佇列',
     // Print modal
-    print: '列印',
-    reprint: '重新列印',
-    schedulePrint: '排程列印',
     editQueueItem: '編輯佇列項目',
-    printToPrinters: '列印到 {{count}} 臺印表機',
-    queueToPrinters: '佇列到 {{count}} 臺印表機',
-    queueSelectedPlates: '將 {{count}} 個熱床加入佇列',
     selectAllPlates: '選擇全部 {{count}} 個熱床',
     deselectAll: '取消全選',
     printQueued: '已加入列印佇列',
+    printQueuedWillStartWhenIdle: '印表機閒置時將開始',
     itemsQueued: '{{count}} 個任務已加入佇列',
     sending: '傳送中...',
     sendingProgress: '傳送中 {{current}}/{{total}}...',
@@ -1185,8 +1183,8 @@ export default {
     },
     // Actions
     actions: {
-      stopPrint: '停止列印',
       startPrint: '開始列印',
+      stopPrint: '停止列印',
       requeue: '重新佇列',
     },
     // Bulk edit
@@ -1304,35 +1302,6 @@ export default {
     },
   },
 
-  backgroundDispatch: {
-    unknownFile: '未知檔案',
-    unknownPrinter: '未知印表機',
-    startingPrints: '正在開始列印',
-    progressSummary: '{{complete}}/{{total}} 完成 • 已分發:{{dispatched}} • 處理中:{{processing}}',
-    expandDetails: '展開分發詳情',
-    collapseDetails: '收起分發詳情',
-    dismissToast: '關閉分發通知',
-    cancelDispatchJob: '取消分發任務',
-    cancel: '取消',
-    cancelling: '取消中…',
-    awaitingPrinter: '等待印表機…',
-    status: {
-      dispatched: '已分發',
-      processing: '處理中',
-      completed: '已完成',
-      failed: '失敗',
-      cancelled: '已取消',
-    },
-    toast: {
-      cancellingUpload: '取消上傳中...',
-      cancelled: '分發已取消',
-      cancelFailed: '取消分發失敗',
-      completeWithFailures: '後台分發完成:{{completed}} 成功,{{failed}} 失敗',
-      completeSuccess: '後台分發完成:{{completed}} 成功',
-      printStartedRemaining: '{{completed}} 個列印已開始,{{remaining}} 個正在傳送...',
-    },
-  },
-
   // Statistics page
   stats: {
     title: '統計',
@@ -2614,6 +2583,42 @@ export default {
     clearErrors: '清除錯誤',
     clearSuccess: 'HMS 錯誤已清除',
     clearFailed: '清除 HMS 錯誤失敗',
+    actionSuccess: '已向印表機傳送動作',
+    actionFailed: '動作傳送失敗',
+    actions: {
+      RESUME_PRINTING: '恢復列印',
+      RESUME_PRINTING_DEFECTS: '恢復 (瑕疵可接受)',
+      RESUME_PRINTING_PROBELM_SOLVED: '恢復 (問題已解決)',
+      STOP_PRINTING: '停止列印',
+      CHECK_ASSISTANT: '檢視助理',
+      FILAMENT_EXTRUDED: '已擠出耗材,繼續',
+      RETRY_FILAMENT_EXTRUDED: '尚未擠出,重試',
+      CONTINUE: '已完成,繼續',
+      LOAD_VIRTUAL_TRAY: '載入耗材',
+      OK_BUTTON: '確定',
+      FILAMENT_LOAD_RESUME: '耗材已載入,恢復',
+      JUMP_TO_LIVEVIEW: '檢視即時畫面',
+      NO_REMINDER_NEXT_TIME: '下次不再提醒',
+      REFRESH_NOZZLE: '重新檢查',
+      IGNORE_NO_REMINDER_NEXT_TIME: '忽略,下次不再提醒',
+      IGNORE_RESUME: '忽略並恢復',
+      PROBLEM_SOLVED_RESUME: '問題已解決,恢復',
+      TURN_OFF_FIRE_ALARM: '知道了,關閉火警',
+      RETRY_PROBLEM_SOLVED: '重試 (問題已解決)',
+      CANCLE: '取消',
+      STOP_DRYING: '停止乾燥',
+      PROCEED: '繼續',
+      OK_JUMP_RACK: '確定',
+      ABORT: '中止',
+      DISABLE_PURIFICATION: '本次列印停用空氣淨化',
+      DONT_REMIND_NEXT_TIME: '不再提醒',
+      DBL_CHECK_CANCEL: '取消',
+      DBL_CHECK_DONE: '完成',
+      DBL_CHECK_RETRY: '重試',
+      DBL_CHECK_RESUME: '恢復',
+      DBL_CHECK_OK: '確認',
+      REMOVE_CLOSE_BTN: '關閉',
+    },
   },
 
   // MQTT Debug modal
@@ -3342,8 +3347,6 @@ export default {
     changeLink: '更改連結...',
     linkTo: '連結到...',
     linkToProjectOrArchive: '連結到專案或歸檔',
-    addToQueue: '新增到佇列',
-    schedulePrint: '排程',
     generateThumbnail: '產生縮圖',
     generateThumbnails: '產生縮圖',
     generateThumbnailsForMissing: '為缺少縮圖的 STL 檔案產生縮圖',
@@ -3646,8 +3649,6 @@ export default {
       fileCount: '{{count}} 個檔案',
       empty: '未連結資料夾。前往檔案管理器將資料夾連結到此項目。',
       noFiles: '此資料夾中沒有檔案。',
-      print: '立即列印',
-      addToQueue: '加入佇列',
     },
     bom: {
       title: '材料清單',
@@ -4286,7 +4287,6 @@ export default {
 
   // Print modal
   printModal: {
-    title: '開始列印',
     selectPrinter: '選擇印表機',
     selectPlate: '選擇板',
     filamentMapping: '耗材對應',
@@ -4298,15 +4298,25 @@ export default {
     vibrationCalibration: '振動校準',
     layerInspection: '首層檢查',
     timelapse: '縮時攝影',
-    startPrint: '開始列印',
-    addToQueue: '新增到佇列',
     cancel: '取消',
     noPrintersAvailable: '無可用印表機',
     printerBusy: '印表機忙碌',
     printerOffline: '印表機離線',
     sameTypeDifferentColor: '相同類型,不同顏色',
     filamentTypeNotLoaded: '耗材類型未裝載',
+    whenToPrint: '何時列印',
+    asap: '盡快',
+    queue: '佇列',
+    schedule: '排程',
+    dateTime: '日期和時間',
+    invalidDateTime: '請輸入有效的日期和時間',
     openCalendar: '開啟日曆',
+    requireManualStart: '要求手動開始',
+    requirePreviousSuccess: '僅在上一次列印成功後開始',
+    autoOffAfter: '完成後關閉印表機',
+    helpAsap: '列印將新增到佇列頂端,並在符合條件的印表機閒置後立即開始。',
+    helpSchedule: '如果印表機閒置,列印將在排程時間開始。如果印表機忙碌,則會等待其可用。',
+    helpQueue: '列印將新增到佇列末端。',
     leftNozzle: '左',
     rightNozzle: '右',
     leftNozzleTooltip: '左噴嘴',

+ 25 - 59
frontend/src/pages/ArchivesPage.tsx

@@ -204,7 +204,6 @@ function ArchiveCard({
   const [showQRCode, setShowQRCode] = useState(false);
   const [showPhotos, setShowPhotos] = useState(false);
   const [showProjectPage, setShowProjectPage] = useState(false);
-  const [showSchedule, setShowSchedule] = useState(false);
   const [showDeleteSource3mfConfirm, setShowDeleteSource3mfConfirm] = useState(false);
   const [showDeleteF3dConfirm, setShowDeleteF3dConfirm] = useState(false);
   const [showDeleteTimelapseConfirm, setShowDeleteTimelapseConfirm] = useState(false);
@@ -404,18 +403,17 @@ function ArchiveCard({
     // For source files: show Slice as the primary action
     ...(isGcodeFile ? [
       {
-        label: t('archives.menu.print'),
+        label: t('common.print'),
         icon: <Printer className="w-4 h-4" />,
         onClick: () => setShowReprint(true),
-        disabled: !archive.file_path || !canModify('archives', 'reprint', archive.created_by_id),
-        title: !archive.file_path ? t('archives.card.noFileForReprint') : !canModify('archives', 'reprint', archive.created_by_id) ? t('archives.permission.noReprint') : undefined,
-      },
-      {
-        label: t('archives.menu.schedule'),
-        icon: <Calendar className="w-4 h-4" />,
-        onClick: () => setShowSchedule(true),
-        disabled: !archive.file_path || !hasPermission('queue:create'),
-        title: !archive.file_path ? t('archives.card.noFileForReprint') : !hasPermission('queue:create') ? t('archives.permission.noAddToQueue') : undefined,
+        disabled: !archive.file_path || !hasPermission('queue:create') || !canModify('archives', 'reprint', archive.created_by_id),
+        title: !archive.file_path
+          ? t('archives.card.noFileForReprint')
+          : !hasPermission('queue:create')
+            ? t('archives.permission.noAddToQueue')
+            : !canModify('archives', 'reprint', archive.created_by_id)
+              ? t('archives.permission.noReprint')
+              : undefined,
       },
       {
         label: t('archives.menu.openInBambuStudio'),
@@ -1144,22 +1142,11 @@ function ArchiveCard({
                 size="sm"
                 className="flex-1 min-w-0 overflow-hidden"
                 onClick={() => setShowReprint(true)}
-                disabled={!archive.file_path || !canModify('archives', 'reprint', archive.created_by_id)}
-                title={!archive.file_path ? t('archives.card.noFileForReprint') : !canModify('archives', 'reprint', archive.created_by_id) ? t('archives.card.noPermissionReprint') : undefined}
+                disabled={!archive.file_path || !hasPermission('queue:create') || !canModify('archives', 'reprint', archive.created_by_id)}
+                title={!archive.file_path ? t('archives.card.noFileForReprint') : !hasPermission('queue:create') ? t('archives.permission.noAddToQueue') : !canModify('archives', 'reprint', archive.created_by_id) ? t('archives.card.noPermissionReprint') : undefined}
               >
                 <Printer className="w-3 h-3 flex-shrink-0" />
-                <span className="hidden xl:inline truncate">{t('archives.card.reprint')}</span>
-              </Button>
-              <Button
-                variant="secondary"
-                size="sm"
-                className="flex-1 min-w-0 overflow-hidden"
-                onClick={() => setShowSchedule(true)}
-                disabled={!archive.file_path || !hasPermission('queue:create')}
-                title={!archive.file_path ? t('archives.card.noFileForReprint') : !hasPermission('queue:create') ? t('archives.permission.noAddToQueue') : t('archives.card.schedulePrint')}
-              >
-                <Calendar className="w-3 h-3 flex-shrink-0" />
-                <span className="hidden xl:inline truncate">{t('archives.card.schedule')}</span>
+                <span className="hidden xl:inline truncate">{t('common.print')}</span>
               </Button>
               <Button
                 variant="secondary"
@@ -1275,7 +1262,7 @@ function ArchiveCard({
       {/* Reprint Modal */}
       {showReprint && (
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={archive.id}
           archiveName={archive.print_name || archive.filename}
           onClose={() => setShowReprint(false)}
@@ -1504,15 +1491,6 @@ function ArchiveCard({
         />
       )}
 
-      {showSchedule && (
-        <PrintModal
-          mode="add-to-queue"
-          archiveId={archive.id}
-          archiveName={archive.print_name || archive.filename}
-          onClose={() => setShowSchedule(false)}
-        />
-      )}
-
       {/* Hidden file input for source 3MF upload */}
       <input
         ref={source3mfInputRef}
@@ -1604,7 +1582,6 @@ function ArchiveListRow({
   const navigate = useNavigate();
   const [showReprint, setShowReprint] = useState(false);
   const [showSliceModal, setShowSliceModal] = useState(false);
-  const [showSchedule, setShowSchedule] = useState(false);
   const [showTimelapse, setShowTimelapse] = useState(false);
   const [showTimelapseSelect, setShowTimelapseSelect] = useState(false);
   const [availableTimelapses, setAvailableTimelapses] = useState<Array<{ name: string; path: string; size: number; mtime: string | null }>>([]);
@@ -1785,18 +1762,17 @@ function ArchiveListRow({
   const contextMenuItems: ContextMenuItem[] = [
     ...(isGcodeFile ? [
       {
-        label: t('archives.menu.print'),
+        label: t('common.print'),
         icon: <Printer className="w-4 h-4" />,
         onClick: () => setShowReprint(true),
-        disabled: !archive.file_path || !canModify('archives', 'reprint', archive.created_by_id),
-        title: !archive.file_path ? t('archives.card.noFileForReprint') : !canModify('archives', 'reprint', archive.created_by_id) ? t('archives.permission.noReprint') : undefined,
-      },
-      {
-        label: t('archives.menu.schedule'),
-        icon: <Calendar className="w-4 h-4" />,
-        onClick: () => setShowSchedule(true),
-        disabled: !archive.file_path || !hasPermission('queue:create'),
-        title: !archive.file_path ? t('archives.card.noFileForReprint') : !hasPermission('queue:create') ? t('archives.permission.noAddToQueue') : undefined,
+        disabled: !archive.file_path || !hasPermission('queue:create') || !canModify('archives', 'reprint', archive.created_by_id),
+        title: !archive.file_path
+          ? t('archives.card.noFileForReprint')
+          : !hasPermission('queue:create')
+            ? t('archives.permission.noAddToQueue')
+            : !canModify('archives', 'reprint', archive.created_by_id)
+              ? t('archives.permission.noReprint')
+              : undefined,
       },
       {
         label: t('archives.menu.openInBambuStudio'),
@@ -2185,8 +2161,8 @@ function ArchiveListRow({
               variant="ghost"
               size="sm"
               onClick={() => setShowReprint(true)}
-              disabled={!canModify('archives', 'reprint', archive.created_by_id)}
-              title={!canModify('archives', 'reprint', archive.created_by_id) ? t('archives.card.noPermissionReprint') : t('archives.card.reprint')}
+              disabled={!archive.file_path || !hasPermission('queue:create') || !canModify('archives', 'reprint', archive.created_by_id)}
+              title={!archive.file_path ? t('archives.card.noFileForReprint') : !hasPermission('queue:create') ? t('archives.permission.noAddToQueue') : !canModify('archives', 'reprint', archive.created_by_id) ? t('archives.card.noPermissionReprint') : t('common.print')}
               className="text-bambu-green hover:text-bambu-green-light hover:bg-bambu-green/10"
             >
               <Play className="w-4 h-4" />
@@ -2289,7 +2265,7 @@ function ArchiveListRow({
       {/* Reprint Modal */}
       {showReprint && (
         <PrintModal
-          mode="reprint"
+          mode="create"
           archiveId={archive.id}
           archiveName={archive.print_name || archive.filename}
           onClose={() => setShowReprint(false)}
@@ -2505,16 +2481,6 @@ function ArchiveListRow({
         />
       )}
 
-      {/* Schedule Modal */}
-      {showSchedule && (
-        <PrintModal
-          mode="add-to-queue"
-          archiveId={archive.id}
-          archiveName={archive.print_name || archive.filename}
-          onClose={() => setShowSchedule(false)}
-        />
-      )}
-
       {/* Hidden file input for source 3MF upload */}
       <input
         ref={source3mfInputRef}

+ 13 - 94
frontend/src/pages/FileManagerPage.tsx

@@ -34,7 +34,6 @@ import {
   Cog,
   Printer,
   Pencil,
-  Play,
   Image,
   User,
   Box,
@@ -728,7 +727,6 @@ interface FileCardProps {
   onSelect: (id: number) => void;
   onDelete: (id: number) => void;
   onDownload: (id: number) => void;
-  onAddToQueue?: (id: number) => void;
   onPrint?: (file: LibraryFileListItem) => void;
   onSlice?: (file: LibraryFileListItem) => void;
   useSlicerApi?: boolean;
@@ -743,7 +741,7 @@ interface FileCardProps {
   t: TFunction;
 }
 
-function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload, onAddToQueue, onPrint, onSlice, useSlicerApi, onPreview3d, onRename, onGenerateThumbnail, onTagClick, thumbnailVersion, hasPermission, canModify, authEnabled, t }: FileCardProps) {
+function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload, onPrint, onSlice, useSlicerApi, onPreview3d, onRename, onGenerateThumbnail, onTagClick, thumbnailVersion, hasPermission, canModify, authEnabled, t }: FileCardProps) {
   const [showActions, setShowActions] = useState(false);
 
   return (
@@ -843,27 +841,14 @@ function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload,
               {onPrint && isSlicedFilename(file.filename) && (
                 <button
                   className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
-                    hasPermission('printers:control') ? 'text-bambu-green hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
+                    hasPermission('queue:create') ? 'text-bambu-green hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
                   }`}
-                  onClick={() => { if (hasPermission('printers:control')) { onPrint(file); setShowActions(false); } }}
-                  disabled={!hasPermission('printers:control')}
-                  title={!hasPermission('printers:control') ? t('fileManager.noPermissionPrint') : undefined}
-                >
-                  <Printer className="w-3.5 h-3.5" />
-                  {t('common.print')}
-                </button>
-              )}
-              {onAddToQueue && isSlicedFilename(file.filename) && (
-                <button
-                  className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
-                    hasPermission('queue:create') ? 'text-white hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
-                  }`}
-                  onClick={() => { if (hasPermission('queue:create')) { onAddToQueue(file.id); setShowActions(false); } }}
+                  onClick={() => { if (hasPermission('queue:create')) { onPrint(file); setShowActions(false); } }}
                   disabled={!hasPermission('queue:create')}
                   title={!hasPermission('queue:create') ? t('fileManager.noPermissionAddToQueue') : undefined}
                 >
-                  <Clock className="w-3.5 h-3.5" />
-                  {t('fileManager.schedulePrint')}
+                  <Printer className="w-3.5 h-3.5" />
+                  {t('common.print')}
                 </button>
               )}
               {onSlice && useSlicerApi && isSliceableFilename(file.filename) && (
@@ -992,8 +977,6 @@ export function FileManagerPage() {
   const [linkFolder, setLinkFolder] = useState<LibraryFolderTree | null>(null);
   const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'file' | 'folder' | 'bulk'; id: number; count?: number } | null>(null);
   const [printFile, setPrintFile] = useState<LibraryFileListItem | null>(null);
-  const [printMultiFile, setPrintMultiFile] = useState<LibraryFileListItem | null>(null);
-  const [scheduleFile, setScheduleFile] = useState<LibraryFileListItem | null>(null);
   const [sliceFile, setSliceFile] = useState<LibraryFileListItem | null>(null);
   const [renameItem, setRenameItem] = useState<{ type: 'file' | 'folder'; id: number; name: string } | null>(null);
   const [thumbnailVersions, setThumbnailVersions] = useState<Record<number, number>>({});
@@ -2160,27 +2143,12 @@ export function FileManagerPage() {
                       <Button
                         variant="primary"
                         size="sm"
-                        onClick={() => setPrintMultiFile(selectedSlicedFiles[0])}
-                        disabled={!hasPermission('printers:control')}
-                        title={!hasPermission('printers:control') ? t('fileManager.noPermissionPrint') : undefined}
-                      >
-                        <Play className="w-4 h-4 sm:mr-1" />
-                        <span className="hidden sm:inline">{t('common.print')}</span>
-                      </Button>
-                    )}
-                    {selectedSlicedFiles.length === 1 && (
-                      <Button
-                        variant="secondary"
-                        size="sm"
-                        // Note: Schedule dialog (PrintModal) is designed for single file at a time
-                        // but supports scheduling to multiple printers. This provides more control
-                        // over scheduling options compared to the previous bulk queue mutation.
-                        onClick={() => setScheduleFile(selectedSlicedFiles[0])}
+                        onClick={() => setPrintFile(selectedSlicedFiles[0])}
                         disabled={!hasPermission('queue:create')}
                         title={!hasPermission('queue:create') ? t('fileManager.noPermissionAddToQueue') : undefined}
                       >
-                        <Clock className="w-4 h-4 sm:mr-1" />
-                        <span className="hidden sm:inline">{t('fileManager.schedulePrint')}</span>
+                        <Printer className="w-4 h-4 sm:mr-1" />
+                        <span className="hidden sm:inline">{t('common.print')}</span>
                       </Button>
                     )}
                     <Button
@@ -2295,10 +2263,6 @@ export function FileManagerPage() {
                     onSelect={handleFileSelect}
                     onDelete={(id) => setDeleteConfirm({ type: 'file', id })}
                     onDownload={handleDownload}
-                    onAddToQueue={(id) => {
-                      const file = files?.find(f => f.id === id);
-                      if (file) setScheduleFile(file);
-                    }}
                     onPrint={setPrintFile}
                     onSlice={setSliceFile}
                     useSlicerApi={settings?.use_slicer_api ?? false}
@@ -2455,32 +2419,16 @@ export function FileManagerPage() {
                       {isSlicedFilename(file.filename) && (
                         <>
                           <button
-                            onClick={() => hasPermission('printers:control') && setPrintFile(file)}
-                            className={`p-1.5 rounded transition-colors ${
-                              hasPermission('printers:control')
-                                ? 'hover:bg-bambu-dark text-bambu-gray hover:text-bambu-green'
-                                : 'text-bambu-gray/50 cursor-not-allowed'
-                            }`}
-                            title={hasPermission('printers:control') ? t('common.print') : t('fileManager.noPermissionPrint')}
-                            disabled={!hasPermission('printers:control')}
-                          >
-                            <Printer className="w-4 h-4" />
-                          </button>
-                          <button
-                            onClick={() => {
-                              if (hasPermission('queue:create')) {
-                                setScheduleFile(file);
-                              }
-                            }}
+                            onClick={() => hasPermission('queue:create') && setPrintFile(file)}
                             className={`p-1.5 rounded transition-colors ${
                               hasPermission('queue:create')
-                                ? 'hover:bg-bambu-dark text-bambu-gray hover:text-white'
+                                ? 'hover:bg-bambu-dark text-bambu-gray hover:text-bambu-green'
                                 : 'text-bambu-gray/50 cursor-not-allowed'
                             }`}
-                            title={hasPermission('queue:create') ? t('fileManager.schedulePrint') : t('fileManager.noPermissionAddToQueue')}
+                            title={hasPermission('queue:create') ? t('common.print') : t('fileManager.noPermissionAddToQueue')}
                             disabled={!hasPermission('queue:create')}
                           >
-                            <Clock className="w-4 h-4" />
+                            <Printer className="w-4 h-4" />
                           </button>
                         </>
                       )}
@@ -2679,41 +2627,12 @@ export function FileManagerPage() {
 
       {printFile && (
         <PrintModal
-          mode="reprint"
+          mode="create"
           libraryFileId={printFile.id}
           archiveName={printFile.print_name || printFile.filename}
           onClose={() => setPrintFile(null)}
           onSuccess={() => {
             setPrintFile(null);
-            queryClient.invalidateQueries({ queryKey: ['library-files'] });
-            queryClient.invalidateQueries({ queryKey: ['archives'] });
-          }}
-        />
-      )}
-
-      {printMultiFile && (
-        <PrintModal
-          mode="reprint"
-          libraryFileId={printMultiFile.id}
-          archiveName={printMultiFile.print_name || printMultiFile.filename}
-          onClose={() => setPrintMultiFile(null)}
-          onSuccess={() => {
-            setPrintMultiFile(null);
-            setSelectedFiles([]);
-            queryClient.invalidateQueries({ queryKey: ['library-files'] });
-            queryClient.invalidateQueries({ queryKey: ['archives'] });
-          }}
-        />
-      )}
-
-      {scheduleFile && (
-        <PrintModal
-          mode="add-to-queue"
-          libraryFileId={scheduleFile.id}
-          archiveName={scheduleFile.print_name || scheduleFile.filename}
-          onClose={() => setScheduleFile(null)}
-          onSuccess={() => {
-            setScheduleFile(null);
             setSelectedFiles([]);
             queryClient.invalidateQueries({ queryKey: ['library-files'] });
             queryClient.invalidateQueries({ queryKey: ['queue'] });

+ 27 - 7
frontend/src/pages/PrintersPage.tsx

@@ -5300,13 +5300,15 @@ function PrinterCard({
                               const emptyKind = getEmptySlotKind(extTray);
                               const extSlotContent = (
                                 <div className={`w-full bg-bambu-dark-secondary rounded-lg p-1 text-center ${isEmpty ? 'opacity-50' : ''} ${isExtActive ? 'ring-2 ring-bambu-green ring-offset-1 ring-offset-bambu-dark' : ''}`}>
-                                  {/* Filament color circle with 1-based slot number centered inside */}
+                                  {/* Color circle: L/R inside on dual-nozzle external (replaces
+                                      the separate Ext-L/Ext-R caption that made the row taller than
+                                      regular AMS slots), 1-based slot number on single-nozzle. */}
                                   <FilamentSlotCircle
                                     trayColor={extTray.tray_color}
                                     trayType={extTray.tray_type}
                                     isEmpty={isEmpty}
                                     emptyKind={emptyKind}
-                                    slotNumber={slotTrayId + 1}
+                                    slotNumber={isDualNozzle ? (extTrayId === 254 ? 'L' : 'R') : slotTrayId + 1}
                                   />
                                   <div className={`text-[9px] font-bold truncate ${isEmpty ? 'text-white/40' : 'text-white'}`}>
                                     {extTray.tray_type || t('ams.slotEmpty')}
@@ -5322,7 +5324,6 @@ function PrinterCard({
                                       />
                                     )}
                                   </div>
-                                  {extLabel && <div className="text-[7px] text-white/40 mt-0.5 truncate">{extLabel}</div>}
                                 </div>
                               );
 
@@ -5484,7 +5485,7 @@ function PrinterCard({
         {viewMode === 'expanded' && (
           <div className="mt-auto">
         {smartPlug && (
-          <div className="pt-4">
+          <div className="pt-3">
             <div className="flex items-center gap-2 mb-2">
               <span className="text-[10px] uppercase tracking-wider text-bambu-gray font-medium">
                 {t('printers.power', 'Power')}
@@ -5629,8 +5630,14 @@ function PrinterCard({
                   <Button
                     size="sm"
                     onClick={() => setShowUploadForPrint(true)}
-                    disabled={!hasPermission('printers:control')}
-                    title={!hasPermission('printers:control') ? t('printers.permission.noControl') : t('common.print')}
+                    disabled={!hasPermission('library:upload') || !hasPermission('queue:create')}
+                    title={
+                      !hasPermission('library:upload')
+                        ? t('fileManager.noPermissionUpload')
+                        : !hasPermission('queue:create')
+                          ? t('fileManager.noPermissionAddToQueue')
+                          : t('common.print')
+                    }
                     className={`${footerActionButtonClass} !bg-bambu-green hover:!bg-bambu-green/80 !text-white`}
                   >
                     <PrinterIcon className="w-4 h-4" />
@@ -5683,7 +5690,7 @@ function PrinterCard({
       {/* Print Modal (after upload) */}
       {printAfterUpload && (
         <PrintModal
-          mode="reprint"
+          mode="create"
           libraryFileId={printAfterUpload.id}
           archiveName={printAfterUpload.filename}
           initialSelectedPrinterIds={[printer.id]}
@@ -7621,6 +7628,14 @@ export function PrintersPage() {
     const saved = parseInt(localStorage.getItem('camWallSnapshotSec') || '', 10);
     return Number.isFinite(saved) && saved > 0 ? saved : 8;
   });
+  // 'off' hides the printer-state overlay; 'compact' shows only a state chip;
+  // 'full' adds progress, layer, and time-left on printing/paused tiles.
+  // Defaulting to 'full' because the cards already show this info — users who
+  // pick cam-wall view still want to glance the same details without flipping.
+  const [camWallStatusMode, setCamWallStatusMode] = useState<'off' | 'compact' | 'full'>(() => {
+    const saved = localStorage.getItem('camWallStatusMode');
+    return saved === 'off' || saved === 'compact' || saved === 'full' ? saved : 'full';
+  });
   // Derive viewMode from cardSize: S=compact, M/L/XL=expanded
   const viewMode: ViewMode = cardSize === 1 ? 'compact' : 'expanded';
   const [compactDrilldownPrinterId, setCompactDrilldownPrinterId] = useState<number | null>(null);
@@ -8612,6 +8627,7 @@ export function PrintersPage() {
               window.open(`/camera/${id}`, `camera-${id}`, features);
             }
           }}
+          statusMode={camWallStatusMode}
           onChangeMaxLive={(next) => {
             setCamWallMaxLive(next);
             localStorage.setItem('camWallMaxLive', String(next));
@@ -8620,6 +8636,10 @@ export function PrintersPage() {
             setCamWallSnapshotSec(next);
             localStorage.setItem('camWallSnapshotSec', String(next));
           }}
+          onChangeStatusMode={(next) => {
+            setCamWallStatusMode(next);
+            localStorage.setItem('camWallStatusMode', next);
+          }}
         />
       ) : groupedPrinters ? (
         /* Grouped view (location, status, or model) */

+ 4 - 28
frontend/src/pages/ProjectDetailPage.tsx

@@ -31,8 +31,6 @@ import {
   FolderOpen,
   Download,
   Pencil,
-  Play,
-  CalendarPlus,
   FileBox,
 } from 'lucide-react';
 import { api } from '../api/client';
@@ -212,7 +210,6 @@ export function ProjectDetailPage() {
   const [editingNotes, setEditingNotes] = useState(false);
   const [notesContent, setNotesContent] = useState('');
   const [printFile, setPrintFile] = useState<LibraryFileListItem | null>(null);
-  const [scheduleFile, setScheduleFile] = useState<LibraryFileListItem | null>(null);
 
   const projectId = parseInt(id || '0', 10);
 
@@ -961,17 +958,10 @@ export function ProjectDetailPage() {
                                 <div className="flex items-center gap-1 shrink-0">
                                   <button
                                     onClick={() => setPrintFile(file)}
-                                    title={t('projectDetail.files.print')}
+                                    title={t('common.print')}
                                     className="p-1.5 rounded hover:bg-bambu-green/20 text-bambu-green transition-colors"
                                   >
-                                    <Play className="w-4 h-4" />
-                                  </button>
-                                  <button
-                                    onClick={() => setScheduleFile(file)}
-                                    title={t('projectDetail.files.addToQueue')}
-                                    className="p-1.5 rounded hover:bg-blue-500/20 text-blue-400 transition-colors"
-                                  >
-                                    <CalendarPlus className="w-4 h-4" />
+                                    <Printer className="w-4 h-4" />
                                   </button>
                                 </div>
                               )}
@@ -1425,10 +1415,10 @@ export function ProjectDetailPage() {
         />
       )}
 
-      {/* Print directly from project — reprint mode */}
+      {/* Print from project */}
       {printFile && (
         <PrintModal
-          mode="reprint"
+          mode="create"
           libraryFileId={printFile.id}
           archiveName={printFile.print_name || printFile.filename}
           projectId={projectId}
@@ -1436,20 +1426,6 @@ export function ProjectDetailPage() {
           onSuccess={() => {
             setPrintFile(null);
             queryClient.invalidateQueries({ queryKey: ['archives'] });
-          }}
-        />
-      )}
-
-      {/* Add to queue from project */}
-      {scheduleFile && (
-        <PrintModal
-          mode="add-to-queue"
-          libraryFileId={scheduleFile.id}
-          archiveName={scheduleFile.print_name || scheduleFile.filename}
-          projectId={projectId}
-          onClose={() => setScheduleFile(null)}
-          onSuccess={() => {
-            setScheduleFile(null);
             queryClient.invalidateQueries({ queryKey: ['queue'] });
           }}
         />

+ 5 - 5
frontend/src/pages/QueuePage.tsx

@@ -672,8 +672,8 @@ function SortableQueueItem({
                 variant="ghost"
                 size="sm"
                 onClick={onStop}
-                disabled={!hasPermission('printers:control')}
-                title={!hasPermission('printers:control') ? t('queue.permissions.noStopPrint') : t('queue.actions.stopPrint')}
+                disabled={!canModify('queue', 'update', item.created_by_id)}
+                title={!canModify('queue', 'update', item.created_by_id) ? t('queue.permissions.noStopPrint') : t('queue.actions.stopPrint')}
                 className="text-red-400 hover:text-red-300 hover:bg-red-500/10 p-1.5 sm:p-2"
               >
                 <StopCircle className="w-4 h-4" />
@@ -686,8 +686,8 @@ function SortableQueueItem({
                     variant="ghost"
                     size="sm"
                     onClick={onStart}
-                    disabled={!hasPermission('printers:control')}
-                    title={!hasPermission('printers:control') ? t('queue.permissions.noStartPrint') : t('queue.actions.startPrint')}
+                    disabled={!canModify('queue', 'update', item.created_by_id)}
+                    title={!canModify('queue', 'update', item.created_by_id) ? t('queue.permissions.noStartPrint') : t('queue.actions.startPrint')}
                     className="text-bambu-green hover:text-bambu-green-light hover:bg-bambu-green/10 p-1.5 sm:p-2"
                   >
                     <Play className="w-4 h-4" />
@@ -2396,7 +2396,7 @@ export function QueuePage() {
       {/* Re-queue Modal */}
       {requeueItem && (
         <PrintModal
-          mode="add-to-queue"
+          mode="create"
           archiveId={requeueItem.archive_id ?? undefined}
           libraryFileId={requeueItem.library_file_id ?? undefined}
           archiveName={requeueItem.archive_name || requeueItem.library_file_name || `File #${requeueItem.archive_id || requeueItem.library_file_id}`}

+ 81 - 0
scripts/update_hms_actions.py

@@ -0,0 +1,81 @@
+import asyncio
+import json
+
+import requests
+
+HMS_ACTIONS_JSON_PATH = "backend/app/data/hms_actions.json"
+HMS_REQUEST_URL = "https://e.bambulab.com/hms/GetActionImage.php"
+
+HMS_ID_TO_ACTION_NAME_MAP: dict[int, str] = {
+    2: "RESUME_PRINTING",
+    3: "RESUME_PRINTING_DEFECTS",
+    4: "RESUME_PRINTING_PROBELM_SOLVED",
+    5: "STOP_PRINTING",
+    6: "CHECK_ASSISTANT",
+    7: "FILAMENT_EXTRUDED",
+    8: "RETRY_FILAMENT_EXTRUDED",
+    9: "CONTINUE",
+    10: "LOAD_VIRTUAL_TRAY",
+    11: "OK_BUTTON",
+    12: "FILAMENT_LOAD_RESUME",
+    13: "JUMP_TO_LIVEVIEW",
+    23: "NO_REMINDER_NEXT_TIME",
+    24: "REFRESH_NOZZLE",
+    25: "IGNORE_NO_REMINDER_NEXT_TIME",
+    27: "IGNORE_RESUME",
+    28: "PROBLEM_SOLVED_RESUME",
+    29: "TURN_OFF_FIRE_ALARM",
+    34: "RETRY_PROBLEM_SOLVED",
+    35: "STOP_DRYING",
+    37: "CANCLE",  # Note: "CANCLE" is intentionally misspelled in the BambuStudio source code
+    39: "REMOVE_CLOSE_BTN",
+    41: "PROCEED",
+    49: "OK_JUMP_RACK",
+    51: "ABORT",
+    54: "DISABLE_PURIFICATION",
+    57: "DONT_REMIND_NEXT_TIME",
+    10000: "DBL_CHECK_CANCEL",
+    10001: "DBL_CHECK_DONE",
+    10002: "DBL_CHECK_RETRY",
+    10003: "DBL_CHECK_RESUME",
+    10004: "DBL_CHECK_OK",
+}
+
+
+async def main():
+    error_to_action_map: dict[str, list[str]] = {}
+    # get the json response from the url
+    response = requests.get(HMS_REQUEST_URL)
+    if response.status_code == 200:
+        data = response.json()
+        ready_data = {}
+        for item in data["data"]:
+            # error_code = item["ecode"][:4] + "_" + item["ecode"][4:]
+            mapped_actions = []
+            for hms_id in item["actions"]:
+                if hms_id not in HMS_ID_TO_ACTION_NAME_MAP:
+                    print(f"Warning: Unrecognized HMS action ID {hms_id} for error code {item['ecode']}")
+                else:
+                    mapped_actions.append(HMS_ID_TO_ACTION_NAME_MAP.get(hms_id, f"UNKNOWN_ACTION_{hms_id}"))
+            print(f"ecode: {item['ecode']}, actions: {mapped_actions}, device: {item['device']}")
+            if item["device"] not in ready_data:
+                ready_data[item["device"]] = {}
+            ready_data[item["device"]][item["ecode"]] = mapped_actions
+            # ready_data.append(
+            #     {
+            #         "ecode": item["ecode"],
+            #         "actions": mapped_actions,
+            #         "device": item["device"],
+            #     }
+            # )
+        with open(HMS_ACTIONS_JSON_PATH, "w") as f:
+            json.dump(ready_data, f, indent=4)
+    else:
+        print("Failed to fetch data")
+    print(error_to_action_map)
+
+    # autogenerate
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

Някои файлове не бяха показани, защото твърде много файлове са промени