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

feat(diagnostic): printer_publishing check + countdown UI (#1622)

  The existing connection diagnostic proved TCP + TLS + auth + SUBSCRIBE but
  not that the printer was actually publishing reports. A wrong-cased serial
  passes mqtt_auth because the broker accepts the subscription regardless;
  the user-visible symptom is empty AMS / no K-profiles / no custom filaments
  in the slicer Device tab because the VP cached state is empty. Bambuddy
  already logged the actionable hint at bambu_mqtt.py:498 but only to
  container logs.

  New printer_publishing check turns that warning into a structured
  diagnostic result. Pass = bridge has seen at least one report since the
  last (re)connect; fail = zero reports across the wait window with fix-text
  pointing at the case-sensitive serial. Bounded 10s poll on the on-demand
  UI route, no wait on the support-package gathering path so bundling stays
  fast. Exits the moment a message arrives — typical wall-clock is 1-2s.

  Frontend renders an elapsed-seconds counter plus a "Listening for status
  report — up to 10s" hint during the pending state so the wait doesn't look
  hung. PUBLISH_WAIT_DEFAULT_SECONDS pinned on both sides.

  report_messages_since_connect exposed as a public property on
  BambuMQTTClient so the diagnostic doesn't reach into private state.
maziggy преди 3 месеца
родител
ревизия
c571ad86dd

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


+ 15 - 2
backend/app/api/routes/printers.py

@@ -803,12 +803,25 @@ async def diagnose_printer(
     _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_READ),
     db: AsyncSession = Depends(get_db),
 ):
-    """Run connection diagnostics for an existing saved printer."""
+    """Run connection diagnostics for an existing saved printer.
+
+    On-demand run from the UI: wait up to PUBLISH_WAIT_DEFAULT seconds for the
+    printer to publish a status report so a fresh reconnect (counter reset to
+    0) isn't reported as `printer_publishing: fail` prematurely. The support
+    package code path calls run_connection_diagnostic without the wait so
+    bundling stays fast.
+    """
+    from backend.app.services.printer_diagnostic import PUBLISH_WAIT_DEFAULT
+
     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")
-    return await run_connection_diagnostic(printer.ip_address, printer=printer)
+    return await run_connection_diagnostic(
+        printer.ip_address,
+        printer=printer,
+        wait_for_publish_seconds=PUBLISH_WAIT_DEFAULT,
+    )
 
 
 # Cache for cover images (printer_id -> {(subtask_name, view_key) -> image_bytes}).

+ 13 - 0
backend/app/services/bambu_mqtt.py

@@ -462,6 +462,19 @@ class BambuMQTTClient:
     def topic_publish(self) -> str:
         return f"device/{self.serial_number}/request"
 
+    @property
+    def report_messages_since_connect(self) -> int:
+        """Count of report-topic messages received since the latest (re)connect.
+
+        Exposed for the connection diagnostic so it can distinguish "MQTT
+        broker accepted us but the printer never published" (typically a
+        wrong / mis-cased serial — #1622 follow-up to #1602) from a healthy
+        bridge that happens to be idle right now. Zero immediately after a
+        fresh connect is normal; zero after a full status push cycle is the
+        wrong-serial failure mode.
+        """
+        return self._report_messages_since_connect
+
     # Maximum time (seconds) without a message before considering connection stale
     STALE_TIMEOUT = 60.0
 

+ 56 - 0
backend/app/services/printer_diagnostic.py

@@ -28,6 +28,16 @@ PORT_RTSPS = 322  # RTSPS — camera stream; optional.
 
 _PORT_PROBE_TIMEOUT = 3.0
 
+# Default seconds the `printer_publishing` check will wait for the first
+# report-topic message before declaring fail. Bambu printers in idle publish
+# push_status every few seconds; 10s catches healthy bridges with margin while
+# staying short enough that the spinner-with-countdown UX stays acceptable.
+# The check exits the moment a message arrives, so the typical wall-clock is
+# 1–2s, not the full 10. Passed as ``wait_for_publish_seconds`` per call so
+# the support-package code path can skip the wait entirely (defaults to 0).
+PUBLISH_WAIT_DEFAULT = 10.0
+_PUBLISH_POLL_INTERVAL = 0.5
+
 
 async def _check_port(ip: str, port: int, timeout: float = _PORT_PROBE_TIMEOUT) -> bool:
     """Test TCP connectivity to ip:port. Returns True if reachable."""
@@ -93,6 +103,7 @@ async def run_connection_diagnostic(
     printer: Printer | None = None,
     serial_number: str | None = None,
     access_code: str | None = None,
+    wait_for_publish_seconds: float = 0.0,
 ) -> PrinterDiagnosticResult:
     """Run connection checks for a printer.
 
@@ -185,6 +196,51 @@ async def run_connection_diagnostic(
     else:
         checks.append(DiagnosticCheck(id="developer_mode", status="skip"))
 
+    # --- Printer is actually publishing on its report topic ---
+    # The mqtt_auth check above only proves TCP + TLS + auth + SUBSCRIBE
+    # succeed. A printer with a wrong-cased serial — or one that simply isn't
+    # publishing for some other reason — still passes mqtt_auth because the
+    # broker accepts the subscription regardless. The user-visible symptom in
+    # that case is "AMS / K-profiles / custom filaments missing on the slicer
+    # side": the VP bridge has nothing cached to mirror because no reports
+    # arrived. #1622 surfaced this: bridge keep-alive timeouts paired with
+    # the `Connected and subscribed, but the printer has sent zero status
+    # reports` warning. The check below turns that warning into a structured
+    # diagnostic result the user can act on without grepping container logs.
+    #
+    # If ``_report_messages_since_connect`` is already > 0, we exit
+    # immediately — the bridge has seen reports. If it's 0 and a wait is
+    # requested, we poll every PUBLISH_POLL_INTERVAL up to
+    # ``wait_for_publish_seconds`` so a fresh reconnect (counter reset to 0)
+    # isn't reported as fail before the printer's first idle push lands.
+    publishing_params: dict[str, int | float] | None = None
+    publishing_status = "skip"
+    if printer is not None and state is not None and state.connected:
+        client = printer_manager.get_client(printer.id)
+        if client is not None:
+            wait_budget = max(wait_for_publish_seconds, 0.0)
+            if wait_budget > 0:
+                # Expose the budget so the UI can render a countdown next to
+                # the spinner — the user knows how long this check might take.
+                publishing_params = {"max_wait_seconds": wait_budget}
+            loop = asyncio.get_running_loop()
+            deadline = loop.time() + wait_budget
+            while True:
+                if client.report_messages_since_connect > 0:
+                    publishing_status = "pass"
+                    break
+                if loop.time() >= deadline:
+                    publishing_status = "fail"
+                    break
+                await asyncio.sleep(_PUBLISH_POLL_INTERVAL)
+    checks.append(
+        DiagnosticCheck(
+            id="printer_publishing",
+            status=publishing_status,
+            params=publishing_params or {},
+        )
+    )
+
     statuses = {c.status for c in checks}
     if "fail" in statuses:
         overall = "problems"

+ 61 - 1
backend/tests/unit/services/test_printer_diagnostic.py

@@ -46,6 +46,7 @@ class _Env:
         host_ip="192.168.1.5",
         state=None,
         test_connection_success=True,
+        report_messages_since_connect: int | None = 5,
     ):
         self.ports = ports or _port_probe()
         self.in_docker = in_docker
@@ -53,12 +54,21 @@ class _Env:
         self.host_ip = host_ip
         self.state = state
         self.test_connection_success = test_connection_success
+        # ``None`` means get_client returns None (e.g. pre-add flow); an int
+        # means there's a client with that counter value.
+        self.report_messages_since_connect = report_messages_since_connect
         self._stack = ExitStack()
 
     def __enter__(self):
         manager = MagicMock()
         manager.get_status.return_value = self.state
         manager.test_connection = AsyncMock(return_value={"success": self.test_connection_success})
+        if self.report_messages_since_connect is None:
+            manager.get_client.return_value = None
+        else:
+            client = MagicMock()
+            client.report_messages_since_connect = self.report_messages_since_connect
+            manager.get_client.return_value = client
         self._stack.enter_context(patch(f"{MOD}._check_port", new_callable=AsyncMock, side_effect=self.ports))
         self._stack.enter_context(patch(f"{MOD}.is_running_in_docker", return_value=self.in_docker))
         self._stack.enter_context(patch(f"{MOD}._detect_docker_network_mode", return_value=self.network_mode))
@@ -91,7 +101,7 @@ class TestSameSubnet:
 
 class TestExistingPrinter:
     async def test_all_healthy(self):
-        with _Env(state=_state(connected=True, developer_mode=True)):
+        with _Env(state=_state(connected=True, developer_mode=True), report_messages_since_connect=42):
             result = await run_connection_diagnostic("192.168.1.50", printer=_printer())
         s = _statuses(result)
         assert result.overall == "ok"
@@ -103,6 +113,7 @@ class TestExistingPrinter:
             "subnet": "pass",
             "mqtt_auth": "pass",
             "developer_mode": "pass",
+            "printer_publishing": "pass",
         }
 
     async def test_mqtt_port_unreachable_is_a_problem(self):
@@ -138,6 +149,8 @@ class TestExistingPrinter:
         assert s["developer_mode"] == "skip"
         # Reachable port but no connection -> credential failure class.
         assert s["mqtt_auth"] == "fail"
+        # Can't observe report messages without a connection.
+        assert s["printer_publishing"] == "skip"
 
     async def test_bridge_mode_warns_and_skips_subnet(self):
         with _Env(network_mode="bridge", state=_state()):
@@ -157,6 +170,53 @@ class TestExistingPrinter:
             result = await run_connection_diagnostic("192.168.1.50", printer=_printer())
         assert _statuses(result)["subnet"] == "warn"
 
+    async def test_printer_publishing_passes_when_reports_seen(self):
+        # Counter > 0 means the printer is publishing on the report topic.
+        with _Env(state=_state(), report_messages_since_connect=1):
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer())
+        assert _statuses(result)["printer_publishing"] == "pass"
+
+    async def test_printer_publishing_fails_when_zero_reports_after_wait(self):
+        # Counter stays at 0 across the wait window — printer never published.
+        # Tiny wait_for_publish_seconds keeps the test sub-second.
+        with _Env(state=_state(), report_messages_since_connect=0):
+            result = await run_connection_diagnostic(
+                "192.168.1.50",
+                printer=_printer(),
+                wait_for_publish_seconds=0.05,
+            )
+        s = _statuses(result)
+        assert s["printer_publishing"] == "fail"
+        # Overall escalates because fail is present.
+        assert result.overall == "problems"
+        # The check exposes the wait budget so the UI can render a countdown.
+        params = next(c.params for c in result.checks if c.id == "printer_publishing")
+        assert params == {"max_wait_seconds": 0.05}
+
+    async def test_printer_publishing_skips_when_disconnected(self):
+        # No live MQTT connection -> can't observe report messages.
+        with _Env(state=_state(connected=False), report_messages_since_connect=0):
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer())
+        assert _statuses(result)["printer_publishing"] == "skip"
+
+    async def test_printer_publishing_skips_when_no_client(self):
+        # State says connected but printer_manager has no client object
+        # (race between client teardown and a fresh diagnostic request).
+        with _Env(state=_state(), report_messages_since_connect=None):
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer())
+        assert _statuses(result)["printer_publishing"] == "skip"
+
+    async def test_printer_publishing_no_wait_returns_instantly_on_zero(self):
+        # Default wait is 0 — instant pass/fail without polling. Used by the
+        # support-package code path so bundling stays fast.
+        with _Env(state=_state(), report_messages_since_connect=0):
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer())
+        s = _statuses(result)
+        assert s["printer_publishing"] == "fail"
+        params = next(c.params for c in result.checks if c.id == "printer_publishing")
+        # No wait -> no max_wait_seconds param surfaced to the UI.
+        assert params == {}
+
 
 class TestPreAddFlow:
     async def test_bad_credentials_fail_mqtt_auth(self):

+ 40 - 4
frontend/src/components/ConnectionDiagnostic.tsx

@@ -1,4 +1,4 @@
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
 import { useMutation } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import {
@@ -78,6 +78,13 @@ type Connection = {
   access_code?: string;
 };
 
+// Keep in sync with backend `PUBLISH_WAIT_DEFAULT` in
+// backend/app/services/printer_diagnostic.py — that's the upper bound on how
+// long the existing-printer route waits for the printer's first status report
+// after a bridge reconnect. The countdown is purely cosmetic; if the two
+// drift the worst case is the hint text being off by a couple of seconds.
+const PUBLISH_WAIT_DEFAULT_SECONDS = 10;
+
 type ConnectionDiagnosticModalProps = {
   onClose: () => void;
   printerName?: string | null;
@@ -114,6 +121,24 @@ export function ConnectionDiagnosticModal(props: ConnectionDiagnosticModalProps)
     return () => window.removeEventListener('keydown', handleKeyDown);
   }, [onClose]);
 
+  // Tick an elapsed-seconds counter while the diagnostic is running so the
+  // existing-printer flow (which waits up to PUBLISH_WAIT_DEFAULT_SECONDS for
+  // the printer's first status report) doesn't look hung. Resets on each
+  // (re)run. No effect on the pre-add flow other than a ticking counter,
+  // which is still useful feedback.
+  const [elapsedSeconds, setElapsedSeconds] = useState(0);
+  useEffect(() => {
+    if (!diagnose.isPending) {
+      setElapsedSeconds(0);
+      return;
+    }
+    const startedAt = Date.now();
+    const interval = window.setInterval(() => {
+      setElapsedSeconds(Math.floor((Date.now() - startedAt) / 1000));
+    }, 500);
+    return () => window.clearInterval(interval);
+  }, [diagnose.isPending]);
+
   const result = diagnose.data as PrinterDiagnosticResult | undefined;
 
   return (
@@ -140,9 +165,20 @@ export function ConnectionDiagnosticModal(props: ConnectionDiagnosticModalProps)
 
         <div className="p-6 space-y-4 overflow-y-auto">
           {diagnose.isPending && (
-            <div className="flex items-center gap-2 text-bambu-gray">
-              <Loader2 className="w-4 h-4 animate-spin" />
-              <span>{t('diagnostic.running')}</span>
+            <div className="space-y-1.5">
+              <div className="flex items-center gap-2 text-bambu-gray">
+                <Loader2 className="w-4 h-4 animate-spin" />
+                <span>
+                  {elapsedSeconds > 0
+                    ? t('diagnostic.runningElapsed', { elapsed: elapsedSeconds })
+                    : t('diagnostic.running')}
+                </span>
+              </div>
+              {printerId !== undefined && (
+                <p className="text-xs text-bambu-gray-light pl-6">
+                  {t('diagnostic.waitingForReportHint', { max: PUBLISH_WAIT_DEFAULT_SECONDS })}
+                </p>
+              )}
             </div>
           )}
 

+ 8 - 0
frontend/src/i18n/locales/de.ts

@@ -5519,6 +5519,8 @@ export default {
   diagnostic: {
     modalTitle: 'Verbindungsdiagnose — {{name}}',
     running: 'Diagnose läuft...',
+    runningElapsed: 'Diagnose läuft... ({{elapsed}}s)',
+    waitingForReportHint: 'Warten darauf, dass der Drucker einen Statusbericht sendet — kann bis zu {{max}} Sekunden dauern.',
     runFailed: 'Diagnose konnte nicht ausgeführt werden: {{error}}',
     retry: 'Erneut ausführen',
     runButton: 'Diagnose ausführen',
@@ -5570,6 +5572,12 @@ export default {
         fail: 'Der Entwicklermodus ist am Drucker AUS. Aktivieren Sie ihn in den LAN-Einstellungen des Druckers — und bestätigen Sie mit OK. Ohne ihn starten Drucke nicht.',
         skip: 'Konnte nicht geprüft werden — erfordert eine aktive Verbindung zum Drucker.',
       },
+      printer_publishing: {
+        title: 'Drucker sendet Statusmeldungen',
+        pass: 'Der Drucker sendet Statusmeldungen — AMS, Filamente und K-Profile werden korrekt zum Slicer gespiegelt.',
+        fail: 'Der MQTT-Broker hat die Verbindung akzeptiert, der Drucker sendet aber keine Statusmeldungen. Fast immer liegt eine falsche oder falsch geschriebene Seriennummer vor — das Topic device/<serial>/report unterscheidet Groß- und Kleinschreibung. Prüfen Sie die Seriennummer in den Druckereinstellungen gegen die Anzeige am Drucker.',
+        skip: 'Konnte nicht geprüft werden — erfordert eine aktive Verbindung zum Drucker.',
+      },
     },
   },
 

+ 8 - 0
frontend/src/i18n/locales/en.ts

@@ -5532,6 +5532,8 @@ export default {
   diagnostic: {
     modalTitle: 'Connection diagnostic — {{name}}',
     running: 'Running diagnostic...',
+    runningElapsed: 'Running diagnostic... ({{elapsed}}s)',
+    waitingForReportHint: 'Listening for the printer to publish a status report — this can take up to {{max}} seconds.',
     runFailed: 'Diagnostic could not run: {{error}}',
     retry: 'Run again',
     runButton: 'Run diagnostic',
@@ -5583,6 +5585,12 @@ export default {
         fail: 'Developer Mode is OFF on the printer. Enable it in the printer\'s LAN settings — and confirm with OK. Without it, prints will not start.',
         skip: 'Could not be checked — requires a live connection to the printer.',
       },
+      printer_publishing: {
+        title: 'Printer is publishing status',
+        pass: 'The printer is publishing status updates — AMS, filaments, and K-profiles will mirror correctly to the slicer.',
+        fail: 'The MQTT broker accepted the connection but the printer has not published any status reports. This is almost always a wrong or mis-cased serial number — the device/<serial>/report topic is case-sensitive. Re-check the serial in printer settings against the screen on the printer.',
+        skip: 'Could not be checked — requires a live connection to the printer.',
+      },
     },
   },
 

+ 8 - 0
frontend/src/i18n/locales/es.ts

@@ -5528,6 +5528,8 @@ export default {
   diagnostic: {
     modalTitle: 'Diagnóstico de conexión — {{name}}',
     running: 'Ejecutando el diagnóstico...',
+    runningElapsed: 'Ejecutando el diagnóstico... ({{elapsed}}s)',
+    waitingForReportHint: 'Esperando a que la impresora publique un informe de estado — puede tardar hasta {{max}} segundos.',
     runFailed: 'No se pudo ejecutar el diagnóstico: {{error}}',
     retry: 'Ejecutar de nuevo',
     runButton: 'Ejecutar diagnóstico',
@@ -5579,6 +5581,12 @@ export default {
         fail: 'El modo desarrollador está DESACTIVADO en la impresora. Actívelo en los ajustes de LAN de la impresora — y confirme con Aceptar. Sin él, las impresiones no comenzarán.',
         skip: 'No se pudo comprobar — requiere una conexión activa con la impresora.',
       },
+      printer_publishing: {
+        title: 'La impresora publica estado',
+        pass: 'La impresora está publicando actualizaciones de estado — AMS, filamentos y perfiles K se reflejarán correctamente en el slicer.',
+        fail: 'El broker MQTT aceptó la conexión, pero la impresora no ha publicado ningún informe de estado. Casi siempre se trata de un número de serie incorrecto o con mayúsculas/minúsculas mal escritas — el topic device/<serial>/report distingue entre mayúsculas y minúsculas. Revise el número de serie en los ajustes de la impresora comparándolo con la pantalla del aparato.',
+        skip: 'No se pudo comprobar — requiere una conexión activa con la impresora.',
+      },
     },
   },
 

+ 8 - 0
frontend/src/i18n/locales/fr.ts

@@ -5509,6 +5509,8 @@ export default {
   diagnostic: {
     modalTitle: 'Diagnostic de connexion — {{name}}',
     running: 'Diagnostic en cours...',
+    runningElapsed: 'Diagnostic en cours... ({{elapsed}}s)',
+    waitingForReportHint: 'En attente que l\'imprimante publie un rapport d\'état — cela peut prendre jusqu\'à {{max}} secondes.',
     runFailed: 'Le diagnostic n\'a pas pu s\'exécuter : {{error}}',
     retry: 'Relancer',
     runButton: 'Lancer le diagnostic',
@@ -5560,6 +5562,12 @@ export default {
         fail: 'Le mode développeur est DÉSACTIVÉ sur l\'imprimante. Activez-le dans les paramètres LAN de l\'imprimante — et confirmez avec OK. Sans lui, les impressions ne démarreront pas.',
         skip: 'Impossible à vérifier — nécessite une connexion active à l\'imprimante.',
       },
+      printer_publishing: {
+        title: 'L\'imprimante publie son état',
+        pass: 'L\'imprimante publie des mises à jour d\'état — l\'AMS, les filaments et les profils K seront correctement reflétés dans le slicer.',
+        fail: 'Le broker MQTT a accepté la connexion mais l\'imprimante n\'a publié aucun rapport d\'état. Il s\'agit presque toujours d\'un numéro de série erroné ou mal capitalisé — le topic device/<serial>/report est sensible à la casse. Vérifiez le numéro de série dans les paramètres de l\'imprimante par rapport à l\'écran de la machine.',
+        skip: 'Impossible à vérifier — nécessite une connexion active à l\'imprimante.',
+      },
     },
   },
 

+ 8 - 0
frontend/src/i18n/locales/it.ts

@@ -5508,6 +5508,8 @@ export default {
   diagnostic: {
     modalTitle: 'Diagnostica connessione — {{name}}',
     running: 'Diagnostica in corso...',
+    runningElapsed: 'Diagnostica in corso... ({{elapsed}}s)',
+    waitingForReportHint: 'In attesa che la stampante pubblichi un report di stato — può richiedere fino a {{max}} secondi.',
     runFailed: 'Impossibile eseguire la diagnostica: {{error}}',
     retry: 'Esegui di nuovo',
     runButton: 'Esegui diagnostica',
@@ -5559,6 +5561,12 @@ export default {
         fail: 'La modalità sviluppatore è DISATTIVATA sulla stampante. Attivala nelle impostazioni LAN della stampante — e conferma con OK. Senza di essa le stampe non verranno avviate.',
         skip: 'Impossibile verificare — richiede una connessione attiva alla stampante.',
       },
+      printer_publishing: {
+        title: 'La stampante pubblica lo stato',
+        pass: 'La stampante sta pubblicando aggiornamenti di stato — AMS, filamenti e profili K saranno correttamente replicati nello slicer.',
+        fail: 'Il broker MQTT ha accettato la connessione ma la stampante non ha pubblicato alcun report di stato. È quasi sempre un numero di serie errato o con maiuscole/minuscole sbagliate — il topic device/<serial>/report distingue tra maiuscole e minuscole. Confronta il numero di serie nelle impostazioni della stampante con quello mostrato sullo schermo del dispositivo.',
+        skip: 'Impossibile verificare — richiede una connessione attiva alla stampante.',
+      },
     },
   },
 

+ 8 - 0
frontend/src/i18n/locales/ja.ts

@@ -5520,6 +5520,8 @@ export default {
   diagnostic: {
     modalTitle: '接続診断 — {{name}}',
     running: '診断を実行中...',
+    runningElapsed: '診断を実行中... ({{elapsed}}秒)',
+    waitingForReportHint: 'プリンターがステータスレポートを送信するのを待っています — 最大 {{max}} 秒かかる場合があります。',
     runFailed: '診断を実行できませんでした: {{error}}',
     retry: '再実行',
     runButton: '診断を実行',
@@ -5571,6 +5573,12 @@ export default {
         fail: 'プリンターの開発者モードがオフです。プリンターのLAN設定で有効にし、OKで確定してください。これがないと印刷は開始されません。',
         skip: '確認できませんでした — プリンターへのアクティブな接続が必要です。',
       },
+      printer_publishing: {
+        title: 'プリンターがステータスを送信中',
+        pass: 'プリンターはステータス更新を送信しています — AMS、フィラメント、Kプロファイルがスライサーに正しくミラーされます。',
+        fail: 'MQTTブローカーは接続を受け付けましたが、プリンターはステータスレポートを一切送信していません。ほぼ常にシリアル番号の誤りまたは大文字小文字の不一致が原因です — device/<serial>/report トピックは大文字小文字を区別します。プリンター設定のシリアル番号をプリンター本体の画面表示と照合してください。',
+        skip: '確認できませんでした — プリンターへのアクティブな接続が必要です。',
+      },
     },
   },
 

+ 8 - 0
frontend/src/i18n/locales/ko.ts

@@ -5569,6 +5569,8 @@ export default {
   diagnostic: {
     modalTitle: '연결 진단 — {{name}}',
     running: '진단 실행 중...',
+    runningElapsed: '진단 실행 중... ({{elapsed}}초)',
+    waitingForReportHint: '프린터가 상태 보고를 게시하기를 기다리는 중 — 최대 {{max}}초 걸릴 수 있습니다.',
     runFailed: '진단 실행 실패: {{error}}',
     retry: '다시 실행',
     runButton: '진단 실행',
@@ -5619,6 +5621,12 @@ export default {
         pass: '개발자 모드가 활성화되어 있습니다.',
         fail: '프린터에서 개발자 모드가 꺼져 있습니다. 프린터의 LAN 설정에서 활성화하고 확인을 누르세요. 이 없으면 인쇄가 시작되지 않습니다.',
         skip: '확인할 수 없음 — 프린터에 연결되어 있어야 합니다.'
+      },
+      printer_publishing: {
+        title: '프린터가 상태를 게시 중',
+        pass: '프린터가 상태 업데이트를 게시하고 있습니다 — AMS, 필라멘트, K-프로파일이 슬라이서에 올바르게 반영됩니다.',
+        fail: 'MQTT 브로커는 연결을 수락했지만 프린터가 상태 보고를 게시하지 않았습니다. 거의 항상 시리얼 번호가 잘못되었거나 대소문자가 일치하지 않아서 발생합니다 — device/<serial>/report 토픽은 대소문자를 구분합니다. 프린터 설정의 시리얼 번호를 프린터 화면 표시와 비교해 확인하세요.',
+        skip: '확인할 수 없음 — 프린터에 연결되어 있어야 합니다.'
       }
     }
   },

+ 8 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -5508,6 +5508,8 @@ export default {
   diagnostic: {
     modalTitle: 'Diagnóstico de conexão — {{name}}',
     running: 'Executando diagnóstico...',
+    runningElapsed: 'Executando diagnóstico... ({{elapsed}}s)',
+    waitingForReportHint: 'Aguardando a impressora publicar um relatório de status — pode levar até {{max}} segundos.',
     runFailed: 'Não foi possível executar o diagnóstico: {{error}}',
     retry: 'Executar novamente',
     runButton: 'Executar diagnóstico',
@@ -5559,6 +5561,12 @@ export default {
         fail: 'O Modo Desenvolvedor está DESLIGADO na impressora. Ative-o nas configurações de LAN da impressora — e confirme com OK. Sem ele, as impressões não iniciarão.',
         skip: 'Não foi possível verificar — requer uma conexão ativa com a impressora.',
       },
+      printer_publishing: {
+        title: 'Impressora publicando status',
+        pass: 'A impressora está publicando atualizações de status — AMS, filamentos e perfis K serão espelhados corretamente para o slicer.',
+        fail: 'O broker MQTT aceitou a conexão, mas a impressora não publicou nenhum relatório de status. Quase sempre é um número de série errado ou com maiúsculas/minúsculas incorretas — o tópico device/<serial>/report diferencia maiúsculas de minúsculas. Verifique o número de série nas configurações da impressora comparando com a tela do equipamento.',
+        skip: 'Não foi possível verificar — requer uma conexão ativa com a impressora.',
+      },
     },
   },
 

+ 8 - 0
frontend/src/i18n/locales/tr.ts

@@ -5458,6 +5458,8 @@ export default {
   diagnostic: {
     modalTitle: 'Bağlantı tanılaması — {{name}}',
     running: 'Tanılama çalışıyor...',
+    runningElapsed: 'Tanılama çalışıyor... ({{elapsed}}s)',
+    waitingForReportHint: 'Yazıcının durum raporu yayınlamasını bekliyor — bu işlem en fazla {{max}} saniye sürebilir.',
     runFailed: 'Tanılama çalıştırılamadı: {{error}}',
     retry: 'Tekrar çalıştır',
     runButton: 'Tanılamayı çalıştır',
@@ -5509,6 +5511,12 @@ export default {
         fail: 'Yazıcıda Geliştirici Modu KAPALI. Yazıcının LAN ayarlarında etkinleştirin — ve OK ile onaylayın. Bu olmadan baskılar başlamayacak.',
         skip: 'Kontrol edilemedi — yazıcıya canlı bir bağlantı gerektirir.',
       },
+      printer_publishing: {
+        title: 'Yazıcı durum yayını yapıyor',
+        pass: 'Yazıcı durum güncellemelerini yayınlıyor — AMS, filamentler ve K profilleri dilimleyiciye doğru şekilde yansıtılacak.',
+        fail: 'MQTT aracısı bağlantıyı kabul etti, ancak yazıcı hiç durum raporu yayınlamadı. Bu neredeyse her zaman yanlış ya da büyük/küçük harf hatalı bir seri numarasından kaynaklanır — device/<serial>/report konusu büyük/küçük harfe duyarlıdır. Yazıcı ayarlarındaki seri numarasını cihazın ekranındaki ile karşılaştırarak doğrulayın.',
+        skip: 'Kontrol edilemedi — yazıcıya canlı bir bağlantı gerektirir.',
+      },
     },
   },
 

+ 8 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -5507,6 +5507,8 @@ export default {
   diagnostic: {
     modalTitle: '连接诊断 — {{name}}',
     running: '正在运行诊断...',
+    runningElapsed: '正在运行诊断... ({{elapsed}}秒)',
+    waitingForReportHint: '正在等待打印机发布状态报告 — 最长可能需要 {{max}} 秒。',
     runFailed: '无法运行诊断:{{error}}',
     retry: '重新运行',
     runButton: '运行诊断',
@@ -5558,6 +5560,12 @@ export default {
         fail: '打印机上的开发者模式已关闭。请在打印机的 LAN 设置中启用它 — 并按 OK 确认。否则打印将无法开始。',
         skip: '无法检查 — 需要与打印机的实时连接。',
       },
+      printer_publishing: {
+        title: '打印机正在发布状态',
+        pass: '打印机正在发布状态更新 — AMS、耗材和 K 配置将正确镜像到切片软件。',
+        fail: 'MQTT 代理已接受连接,但打印机未发布任何状态报告。这几乎总是因为序列号错误或大小写不一致 — device/<serial>/report 主题区分大小写。请将打印机设置中的序列号与打印机屏幕上的显示进行核对。',
+        skip: '无法检查 — 需要与打印机的实时连接。',
+      },
     },
   },
 

+ 8 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -5507,6 +5507,8 @@ export default {
   diagnostic: {
     modalTitle: '連線診斷 — {{name}}',
     running: '正在執行診斷...',
+    runningElapsed: '正在執行診斷... ({{elapsed}}秒)',
+    waitingForReportHint: '正在等待印表機發佈狀態報告 — 最長可能需要 {{max}} 秒。',
     runFailed: '無法執行診斷:{{error}}',
     retry: '重新執行',
     runButton: '執行診斷',
@@ -5558,6 +5560,12 @@ export default {
         fail: '印表機上的開發者模式已關閉。請在印表機的 LAN 設定中啟用它 — 並按 OK 確認。否則列印將無法開始。',
         skip: '無法檢查 — 需要與印表機的即時連線。',
       },
+      printer_publishing: {
+        title: '印表機正在發佈狀態',
+        pass: '印表機正在發佈狀態更新 — AMS、耗材和 K 配置將正確鏡像到切片軟體。',
+        fail: 'MQTT 代理已接受連線,但印表機未發佈任何狀態報告。這幾乎總是因為序號錯誤或大小寫不一致 — device/<serial>/report 主題區分大小寫。請將印表機設定中的序號與印表機螢幕上的顯示進行核對。',
+        skip: '無法檢查 — 需要與印表機的即時連線。',
+      },
     },
   },
 

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
static/assets/index-B_QEe6y0.js


+ 1 - 1
static/index.html

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

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