Browse Source

[Fix] Suppress spurious "waiting for filament" notification when printers are busy (#753)

  When all matching printers were busy and a job was queued with ASAP
  timing, the scheduler immediately fired a "Job Waiting for Filament"
  notification even though the job was just waiting for a printer to
  finish — no user action required.

  Added _is_busy_only() check to skip the waiting notification when the
  only reason is "Busy". Notifications still fire for actionable reasons
  (missing filament, offline, wrong color). Also renamed the default
  notification template title to "Queue Job Waiting" and updated
  descriptions across all 7 locales.
maziggy 2 months ago
parent
commit
35ce075486

+ 1 - 0
CHANGELOG.md

@@ -26,6 +26,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Native Install Missing CAP_NET_BIND_SERVICE** — The `install.sh` systemd service template was missing `AmbientCapabilities=CAP_NET_BIND_SERVICE`, causing Virtual Printer proxy mode to silently fail to bind privileged ports (322, 990) on native installations.
 - **Virtual Printer Proxy A1 Diagnostics** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — Added diagnostic port probing (ports 21, 80, 443) on proxy VP bind IPs to detect if BambuStudio tries to connect on ports the proxy doesn't handle. Logs a warning when an unexpected connection is detected. Helps diagnose A1/A1 Mini proxy issues where the slicer may use a different connection flow.
 - **File Rename Removes Extension** ([#751](https://github.com/maziggy/bambuddy/issues/751)) — Renaming a file in the File Manager included the file extension in the editable text, so users could accidentally remove it (e.g. renaming `bracket.gcode.3mf` to `bracket`), making the file unprintable. The rename modal now only lets users edit the base name, with the extension shown as a non-editable suffix. Reported by @fleishmaab, confirmed by @cadtoolbox.
+- **Spurious "Job Waiting for Filament" Notification** ([#753](https://github.com/maziggy/bambuddy/issues/753)) — When all printers of a model were busy and a job was queued with ASAP timing, a "Job Waiting for Filament" notification fired immediately even though no filament issue existed. The job was simply waiting for a printer to finish. The scheduler now skips the waiting notification when all matching printers are just busy, since the job will auto-start when one finishes. Also renamed the default notification title from "Job Waiting for Filament" to "Queue Job Waiting" to accurately reflect all waiting reasons. Reported by @maziggy.
 
 ### Added
 - **Quick Print Speed Control** ([#256](https://github.com/maziggy/bambuddy/issues/256)) — Added a print speed control badge to the printer card controls row, next to the fan status badges. Click to choose between Silent (50%), Standard (100%), Sport (124%), and Ludicrous (166%) speed presets. The badge shows the current speed percentage with a gauge icon, always visible but disabled when no print is active. Includes optimistic UI updates for instant feedback. Requested by @Sllepper.

+ 1 - 1
backend/app/models/notification.py

@@ -91,7 +91,7 @@ class NotificationProvider(Base):
     on_queue_job_added = Column(Boolean, default=False)  # Job added to queue
     on_queue_job_assigned = Column(Boolean, default=False)  # Model-based job assigned to printer
     on_queue_job_started = Column(Boolean, default=False)  # Queue job started printing
-    on_queue_job_waiting = Column(Boolean, default=True)  # Job waiting for filament
+    on_queue_job_waiting = Column(Boolean, default=True)  # Job waiting for filament or printer
     on_queue_job_skipped = Column(Boolean, default=True)  # Job skipped (previous print failed)
     on_queue_job_failed = Column(Boolean, default=True)  # Job failed to start
     on_queue_completed = Column(Boolean, default=False)  # All pending jobs finished

+ 1 - 1
backend/app/models/notification_template.py

@@ -137,7 +137,7 @@ DEFAULT_TEMPLATES = [
     {
         "event_type": "queue_job_waiting",
         "name": "Queue Job Waiting",
-        "title_template": "Job Waiting for Filament",
+        "title_template": "Queue Job Waiting",
         "body_template": "{job_name} waiting for {target_model}\n{waiting_reason}",
     },
     {

+ 1 - 1
backend/app/schemas/notification.py

@@ -65,7 +65,7 @@ class NotificationProviderBase(BaseModel):
     on_queue_job_added: bool = Field(default=False, description="Notify when job is added to queue")
     on_queue_job_assigned: bool = Field(default=False, description="Notify when model-based job is assigned to printer")
     on_queue_job_started: bool = Field(default=False, description="Notify when queue job starts printing")
-    on_queue_job_waiting: bool = Field(default=True, description="Notify when job is waiting for filament")
+    on_queue_job_waiting: bool = Field(default=True, description="Notify when job is waiting for filament or printer")
     on_queue_job_skipped: bool = Field(default=True, description="Notify when job is skipped")
     on_queue_job_failed: bool = Field(default=True, description="Notify when job fails to start")
     on_queue_completed: bool = Field(default=False, description="Notify when all queue jobs finish")

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

@@ -258,7 +258,8 @@ class PrintScheduler:
                         await db.commit()
 
                         # Send waiting notification only when transitioning to waiting state
-                        if waiting_reason and not was_waiting:
+                        # and the reason requires user action (not just "all printers busy")
+                        if waiting_reason and not was_waiting and not self._is_busy_only(waiting_reason):
                             job_name = await self._get_job_name(db, item)
                             await notification_service.on_queue_job_waiting(
                                 job_name=job_name,
@@ -518,6 +519,17 @@ class PrintScheduler:
 
         return None, " | ".join(reasons) if reasons else f"No available {model} printers{location_suffix}"
 
+    @staticmethod
+    def _is_busy_only(waiting_reason: str) -> bool:
+        """Check if the waiting reason only contains 'Busy' entries.
+
+        When all matching printers are simply busy printing, the queued job
+        will start automatically once a printer finishes — no user action
+        is required, so we skip the notification.
+        """
+        parts = [p.strip() for p in waiting_reason.split(" | ")]
+        return all(p.startswith("Busy:") for p in parts)
+
     def _get_missing_force_color_slots(self, printer_id: int, force_overrides: list[dict]) -> list[str]:
         """Return descriptive strings for force_color_match slots not satisfied by the printer.
 

+ 31 - 0
backend/tests/unit/test_scheduler_busy_only.py

@@ -0,0 +1,31 @@
+"""Tests for _is_busy_only() in the print scheduler."""
+
+from backend.app.services.print_scheduler import PrintScheduler
+
+
+class TestIsBusyOnly:
+    """Test the _is_busy_only static method."""
+
+    def test_single_busy(self):
+        assert PrintScheduler._is_busy_only("Busy: Printer1") is True
+
+    def test_multiple_busy(self):
+        assert PrintScheduler._is_busy_only("Busy: Printer1, Printer2") is True
+
+    def test_busy_and_offline(self):
+        assert PrintScheduler._is_busy_only("Busy: Printer1 | Offline: Printer2") is False
+
+    def test_busy_and_filament(self):
+        assert PrintScheduler._is_busy_only("Busy: Printer1 | Waiting for filament: Printer2 (needs PLA)") is False
+
+    def test_offline_only(self):
+        assert PrintScheduler._is_busy_only("Offline: Printer1") is False
+
+    def test_filament_only(self):
+        assert PrintScheduler._is_busy_only("Waiting for filament: Printer1 (needs PLA)") is False
+
+    def test_no_matching_color(self):
+        assert PrintScheduler._is_busy_only("No matching material/color. Waiting on PLA (Blue)") is False
+
+    def test_no_available_printers(self):
+        assert PrintScheduler._is_busy_only("No available P1S printers configured") is False

+ 1 - 1
frontend/src/i18n/locales/de.ts

@@ -3823,7 +3823,7 @@ export default {
     jobStarted: 'Auftrag gestartet',
     jobStartedDescription: 'Warteschlangenauftrag hat Druck begonnen',
     jobWaiting: 'Auftrag wartet',
-    jobWaitingDescription: 'Auftrag wartet auf Filament',
+    jobWaitingDescription: 'Auftrag wartet auf Filament oder Drucker',
     jobSkipped: 'Auftrag übersprungen',
     jobSkippedDescription: 'Auftrag übersprungen (vorheriger fehlgeschlagen)',
     jobFailed: 'Auftrag fehlgeschlagen',

+ 1 - 1
frontend/src/i18n/locales/en.ts

@@ -3828,7 +3828,7 @@ export default {
     jobStarted: 'Job Started',
     jobStartedDescription: 'Queue job started printing',
     jobWaiting: 'Job Waiting',
-    jobWaitingDescription: 'Job waiting for filament',
+    jobWaitingDescription: 'Job waiting for filament or printer',
     jobSkipped: 'Job Skipped',
     jobSkippedDescription: 'Job skipped (previous failed)',
     jobFailed: 'Job Failed',

+ 1 - 1
frontend/src/i18n/locales/fr.ts

@@ -3815,7 +3815,7 @@ export default {
     jobStarted: 'Tâche démarrée',
     jobStartedDescription: 'La tâche de la file a commencé l\'impression',
     jobWaiting: 'Tâche en attente',
-    jobWaitingDescription: 'Tâche en attente de filament',
+    jobWaitingDescription: 'Tâche en attente de filament ou imprimante',
     jobSkipped: 'Tâche ignorée',
     jobSkippedDescription: 'Tâche ignorée (échec précédent)',
     jobFailed: 'Tâche échouée',

+ 1 - 1
frontend/src/i18n/locales/it.ts

@@ -3814,7 +3814,7 @@ export default {
     jobStarted: 'Lavoro avviato',
     jobStartedDescription: 'Lavoro in coda avviato per la stampa',
     jobWaiting: 'Lavoro in attesa',
-    jobWaitingDescription: 'Lavoro in attesa di filamento',
+    jobWaitingDescription: 'Lavoro in attesa di filamento o stampante',
     jobSkipped: 'Lavoro saltato',
     jobSkippedDescription: 'Lavoro saltato (precedente fallito)',
     jobFailed: 'Lavoro fallito',

+ 1 - 1
frontend/src/i18n/locales/ja.ts

@@ -3827,7 +3827,7 @@ export default {
     jobStarted: 'ジョブ開始',
     jobStartedDescription: 'キュージョブの印刷が開始されました',
     jobWaiting: 'ジョブ待機中',
-    jobWaitingDescription: 'フィラメント待ちのジョブ',
+    jobWaitingDescription: 'フィラメントまたはプリンター待ちのジョブ',
     jobSkipped: 'ジョブスキップ',
     jobSkippedDescription: 'ジョブがスキップされました(前のジョブが失敗)',
     jobFailed: 'ジョブ失敗',

+ 1 - 1
frontend/src/i18n/locales/pt-BR.ts

@@ -3814,7 +3814,7 @@ export default {
     jobStarted: 'Trabalho Iniciado',
     jobStartedDescription: 'Trabalho da fila começou a imprimir',
     jobWaiting: 'Trabalho Aguardando',
-    jobWaitingDescription: 'Trabalho aguardando filamento',
+    jobWaitingDescription: 'Trabalho aguardando filamento ou impressora',
     jobSkipped: 'Trabalho Pulado',
     jobSkippedDescription: 'Trabalho pulado (anterior falhou)',
     jobFailed: 'Trabalho Falhou',

+ 1 - 1
frontend/src/i18n/locales/zh-CN.ts

@@ -3814,7 +3814,7 @@ export default {
     jobStarted: '任务已开始',
     jobStartedDescription: '队列任务已开始打印',
     jobWaiting: '任务等待中',
-    jobWaitingDescription: '任务正在等待耗材',
+    jobWaitingDescription: '任务正在等待耗材或打印机',
     jobSkipped: '任务已跳过',
     jobSkippedDescription: '任务已跳过(上一个失败)',
     jobFailed: '任务失败',

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-Ca5aA-MZ.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-Ceor3BCc.js"></script>
+    <script type="module" crossorigin src="/assets/index-Ca5aA-MZ.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-hPK_4Ftq.css">
   </head>
   <body>

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