Browse Source

fix(ui): collapse bug-report connection diagnostic for multi-printer setups

  The "Report a Bug" panel scans every configured printer on open and shows
  connection problems inline. The first cut rendered a full ~6-row checklist
  per problem printer, stacked — on a large fleet that pushed the description
  box and Submit button below the fold in the max-w-md / max-h-80vh panel.

  The diagnostic section is now a compact summary: one "N of M printers have
  connection issues" line plus the affected printers as collapsed rows
  (healthy printers count toward M, render no detail). Each row expands on
  demand to that printer's full checklist via the shared Collapsible widget;
  a single problem auto-expands since that's the case where inline detail is
  wanted without a click. Panel height is now fixed regardless of fleet size.

  - BugReportBubble.tsx: diagnostic query pairs each result with the printer
    name (new DiagnosticEntry type); render block uses Collapsible rows
  - i18n: new bugReport.diagnosticSummary ({{problems}}/{{total}}) replaces
    static diagnosticHeading; diagnosticIntro reworded count-neutral — all 9
    locales
  - 2 new BugReportBubble tests (collapsed-then-expand; single auto-expand)
maziggy 6 ngày trước cách đây
mục cha
commit
ed31b8f4

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
CHANGELOG.md


+ 75 - 0
frontend/src/__tests__/components/BugReportBubble.test.tsx

@@ -34,6 +34,37 @@ function setupLoggingEndpoints() {
   );
 }
 
+/** Mocks the printer list and per-printer diagnostic the form scans on open. */
+function setupDiagnosticEndpoints(
+  printers: { id: number; name: string }[],
+  results: Record<number, 'ok' | 'problems'>
+) {
+  server.use(
+    http.get('*/printers/', () =>
+      HttpResponse.json(
+        printers.map((p) => ({
+          id: p.id,
+          name: p.name,
+          serial_number: '00M09A000000000',
+          ip_address: `192.168.1.${20 + p.id}`,
+          is_active: true,
+          model: 'X1C',
+          nozzle_count: 1,
+        }))
+      )
+    ),
+    http.get('*/printers/:id/diagnostic', ({ params }) => {
+      const overall = results[Number(params.id)] ?? 'ok';
+      return HttpResponse.json({
+        printer_id: Number(params.id),
+        ip_address: `192.168.1.${20 + Number(params.id)}`,
+        overall,
+        checks: [{ id: 'port_mqtt', status: overall === 'problems' ? 'fail' : 'pass', params: {} }],
+      });
+    })
+  );
+}
+
 describe('BugReportBubble', () => {
   it('renders the floating bug button', () => {
     render(<BugReportBubble />);
@@ -207,4 +238,48 @@ describe('BugReportBubble', () => {
     const details = document.querySelector('details');
     expect(details).toBeInTheDocument();
   });
+
+  it('lists affected printers as collapsed rows, not stacked checklists', async () => {
+    const user = userEvent.setup();
+    setupDiagnosticEndpoints(
+      [
+        { id: 1, name: 'Printer Alpha' },
+        { id: 2, name: 'Printer Beta' },
+        { id: 3, name: 'Printer Gamma' },
+      ],
+      { 1: 'problems', 2: 'problems', 3: 'ok' }
+    );
+
+    render(<BugReportBubble />);
+    await user.click(screen.getByRole('button'));
+
+    // Summary counts problem printers against all scanned printers.
+    expect(
+      await screen.findByText('2 of 3 printers have connection issues')
+    ).toBeInTheDocument();
+    // Affected printers are listed by name; the healthy one is not.
+    expect(screen.getByText('Printer Alpha')).toBeInTheDocument();
+    expect(screen.getByText('Printer Beta')).toBeInTheDocument();
+    expect(screen.queryByText('Printer Gamma')).not.toBeInTheDocument();
+    // With more than one problem the per-printer checklists stay collapsed.
+    expect(screen.queryByText(/Found problems that explain/)).not.toBeInTheDocument();
+
+    // Expanding a row reveals just that printer's checklist.
+    await user.click(screen.getByText('Printer Alpha'));
+    expect(await screen.findByText(/Found problems that explain/)).toBeInTheDocument();
+  });
+
+  it('auto-expands the checklist when only one printer has problems', async () => {
+    const user = userEvent.setup();
+    setupDiagnosticEndpoints([{ id: 1, name: 'Solo Printer' }], { 1: 'problems' });
+
+    render(<BugReportBubble />);
+    await user.click(screen.getByRole('button'));
+
+    expect(
+      await screen.findByText('1 of 1 printers have connection issues')
+    ).toBeInTheDocument();
+    // Single problem → the checklist is expanded without a click.
+    expect(await screen.findByText(/Found problems that explain/)).toBeInTheDocument();
+  });
 });

+ 44 - 15
frontend/src/components/BugReportBubble.tsx

@@ -1,12 +1,17 @@
 import { useState, useRef, useCallback, useEffect } from 'react';
-import { Bug, X, Loader2, CheckCircle, AlertCircle, Trash2, Upload, Circle, CheckCircle2, Stethoscope } from 'lucide-react';
+import { Bug, X, Loader2, CheckCircle, AlertCircle, AlertTriangle, Trash2, Upload, Circle, CheckCircle2, Stethoscope } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import { useQuery } from '@tanstack/react-query';
 import { api, bugReportApi, type PrinterDiagnosticResult } from '../api/client';
 import { DiagnosticChecklist } from './ConnectionDiagnostic';
+import { Collapsible } from './Collapsible';
 
 type ViewState = 'form' | 'logging' | 'stopping' | 'submitting' | 'success' | 'error';
 
+/** One scanned printer paired with its name — the diagnostic result alone
+ *  carries no name, and the bug-report panel lists affected printers by name. */
+type DiagnosticEntry = { name: string; result: PrinterDiagnosticResult };
+
 const MAX_DIMENSION = 1920;
 const JPEG_QUALITY = 0.7;
 const MAX_LOG_SECONDS = 300; // 5 minutes
@@ -66,16 +71,19 @@ export function BugReportBubble() {
     queryKey: ['bugReportDiagnostic'],
     enabled: isOpen && viewState === 'form',
     staleTime: 30_000,
-    queryFn: async (): Promise<PrinterDiagnosticResult[]> => {
+    queryFn: async (): Promise<DiagnosticEntry[]> => {
       const printers = await api.getPrinters();
-      const results = await Promise.all(
-        printers.map((p) => api.diagnosePrinter(p.id).catch(() => null)),
+      const entries = await Promise.all(
+        printers.map(async (p) => {
+          const result = await api.diagnosePrinter(p.id).catch(() => null);
+          return result ? { name: p.name, result } : null;
+        }),
       );
-      return results.filter((r): r is PrinterDiagnosticResult => r !== null);
+      return entries.filter((e): e is DiagnosticEntry => e !== null);
     },
   });
-  const diagnosticResults = diagnosticScan.data ?? [];
-  const diagnosticProblems = diagnosticResults.filter((r) => r.overall === 'problems');
+  const diagnosticEntries = diagnosticScan.data ?? [];
+  const diagnosticProblems = diagnosticEntries.filter((e) => e.result.overall === 'problems');
 
   // Elapsed timer for logging phase — auto-stop at 5 minutes
   useEffect(() => {
@@ -232,9 +240,11 @@ export function BugReportBubble() {
             <div className="p-4 space-y-4">
               {viewState === 'form' && (
                 <>
-                  {/* Connection diagnostic — scanned on form-open. The result
-                      is always shown: a problem panel when a printer has a
-                      detected setup issue, otherwise a healthy confirmation. */}
+                  {/* Connection diagnostic — scanned on form-open. A healthy
+                      fleet shows a single confirmation line. When printers
+                      have problems, each is a collapsed row (auto-expanded
+                      when only one) so the form stays reachable regardless
+                      of how many printers are configured. */}
                   {diagnosticScan.isLoading && (
                     <div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
                       <Loader2 className="w-3.5 h-3.5 animate-spin" />
@@ -247,20 +257,39 @@ export function BugReportBubble() {
                         <Stethoscope className="w-4 h-4 mt-0.5 flex-shrink-0 text-amber-600 dark:text-amber-400" />
                         <div>
                           <p className="text-sm font-medium text-amber-700 dark:text-amber-300">
-                            {t('bugReport.diagnosticHeading')}
+                            {t('bugReport.diagnosticSummary', {
+                              problems: diagnosticProblems.length,
+                              total: diagnosticEntries.length,
+                            })}
                           </p>
                           <p className="text-xs text-amber-800 dark:text-amber-200 mt-0.5">
                             {t('bugReport.diagnosticIntro')}
                           </p>
                         </div>
                       </div>
-                      {diagnosticProblems.map((result) => (
-                        <DiagnosticChecklist key={result.printer_id ?? result.ip_address} result={result} />
-                      ))}
+                      <div className="space-y-2">
+                        {diagnosticProblems.map((entry) => (
+                          <Collapsible
+                            key={entry.result.printer_id ?? entry.result.ip_address}
+                            defaultOpen={diagnosticProblems.length === 1}
+                            className="rounded-lg bg-amber-100/60 dark:bg-amber-900/30 px-3 py-2"
+                            summary={
+                              <div className="flex items-center gap-2 min-w-0">
+                                <AlertTriangle className="w-4 h-4 flex-shrink-0 text-amber-600 dark:text-amber-400" />
+                                <span className="text-sm font-medium text-amber-800 dark:text-amber-200 truncate">
+                                  {entry.name}
+                                </span>
+                              </div>
+                            }
+                          >
+                            <DiagnosticChecklist result={entry.result} />
+                          </Collapsible>
+                        ))}
+                      </div>
                     </div>
                   )}
                   {!diagnosticScan.isLoading &&
-                    diagnosticResults.length > 0 &&
+                    diagnosticEntries.length > 0 &&
                     diagnosticProblems.length === 0 && (
                       <div className="flex items-start gap-2 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 p-3">
                         <CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0 text-green-600 dark:text-green-400" />

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

@@ -5553,8 +5553,8 @@ export default {
     submitFailed: 'Fehlerbericht konnte nicht gesendet werden',
     diagnosticChecking: 'Druckerverbindungen werden geprüft...',
     diagnosticHealthy: 'Verbindungsprüfung bestanden — keine Probleme an Ihren Druckern gefunden.',
-    diagnosticHeading: 'Mögliches Konfigurationsproblem erkannt',
-    diagnosticIntro: 'Ein Drucker hat ein Verbindungsproblem, das die Ursache Ihres Problems sein könnte. Prüfen Sie die Lösung unten — sie zu beheben könnte das Problem ohne Fehlerbericht lösen. Sie können unten dennoch einen Bericht senden.',
+    diagnosticSummary: '{{problems}} von {{total}} Druckern haben Verbindungsprobleme',
+    diagnosticIntro: 'Einer oder mehrere Drucker haben ein Verbindungsproblem, das die Ursache Ihres Problems sein könnte. Klappen Sie einen Drucker unten auf, um die Lösung zu sehen — sie zu beheben könnte das Problem ohne Fehlerbericht lösen. Sie können unten dennoch einen Bericht senden.',
     thankYou: 'Vielen Dank!',
     submitted: 'Ihr Fehlerbericht wurde eingereicht.',
     viewIssue: 'Issue ansehen',

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

@@ -5566,8 +5566,8 @@ export default {
     submitFailed: 'Failed to submit bug report',
     diagnosticChecking: 'Checking printer connections...',
     diagnosticHealthy: 'Connection check passed — no problems found on your printers.',
-    diagnosticHeading: 'Possible setup issue detected',
-    diagnosticIntro: 'A printer has a connection problem that may be causing your issue. Check the fix below — resolving it could solve the problem without a bug report. You can still submit a report below.',
+    diagnosticSummary: '{{problems}} of {{total}} printers have connection issues',
+    diagnosticIntro: 'One or more printers have a connection problem that may be causing your issue. Expand a printer below to see the fix — resolving it could solve the problem without a bug report. You can still submit a report below.',
     thankYou: 'Thank you!',
     submitted: 'Your bug report has been submitted.',
     viewIssue: 'View Issue',

+ 2 - 2
frontend/src/i18n/locales/es.ts

@@ -5562,8 +5562,8 @@ export default {
     submitFailed: 'Error al enviar el informe de error',
     diagnosticChecking: 'Comprobando las conexiones de las impresoras...',
     diagnosticHealthy: 'La comprobación de conexión se superó — no se encontraron problemas en sus impresoras.',
-    diagnosticHeading: 'Posible problema de configuración detectado',
-    diagnosticIntro: 'Una impresora tiene un problema de conexión que puede estar causando su problema. Compruebe la solución de abajo — resolverla podría solucionar el problema sin un informe de error. Aún puede enviar un informe a continuación.',
+    diagnosticSummary: '{{problems}} de {{total}} impresoras tienen problemas de conexión',
+    diagnosticIntro: 'Una o más impresoras tienen un problema de conexión que puede estar causando su problema. Despliegue una impresora a continuación para ver la solución — resolverla podría solucionar el problema sin un informe de error. Aún puede enviar un informe a continuación.',
     thankYou: '¡Gracias!',
     submitted: 'Su informe de error se ha enviado.',
     viewIssue: 'Ver incidencia',

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

@@ -5543,8 +5543,8 @@ export default {
     submitFailed: 'Échec de l\'envoi du rapport de bug',
     diagnosticChecking: 'Vérification des connexions des imprimantes...',
     diagnosticHealthy: 'Vérification de connexion réussie — aucun problème détecté sur vos imprimantes.',
-    diagnosticHeading: 'Problème de configuration possible détecté',
-    diagnosticIntro: 'Une imprimante a un problème de connexion qui pourrait être à l\'origine de votre problème. Consultez la solution ci-dessous — la résoudre pourrait régler le problème sans rapport de bug. Vous pouvez tout de même envoyer un rapport ci-dessous.',
+    diagnosticSummary: '{{problems}} imprimante(s) sur {{total}} ont des problèmes de connexion',
+    diagnosticIntro: 'Une ou plusieurs imprimantes ont un problème de connexion qui pourrait être à l\'origine de votre problème. Dépliez une imprimante ci-dessous pour voir la solution — la résoudre pourrait régler le problème sans rapport de bug. Vous pouvez tout de même envoyer un rapport ci-dessous.',
     thankYou: 'Merci !',
     submitted: 'Votre rapport de bug a été soumis.',
     viewIssue: 'Voir l\'issue',

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

@@ -5542,8 +5542,8 @@ export default {
     submitFailed: 'Impossibile inviare la segnalazione bug',
     diagnosticChecking: 'Verifica delle connessioni delle stampanti...',
     diagnosticHealthy: 'Verifica della connessione superata — nessun problema rilevato sulle tue stampanti.',
-    diagnosticHeading: 'Possibile problema di configurazione rilevato',
-    diagnosticIntro: 'Una stampante ha un problema di connessione che potrebbe essere la causa del tuo problema. Controlla la soluzione qui sotto — risolverla potrebbe sistemare il problema senza una segnalazione di bug. Puoi comunque inviare una segnalazione qui sotto.',
+    diagnosticSummary: '{{problems}} stampanti su {{total}} hanno problemi di connessione',
+    diagnosticIntro: 'Una o più stampanti hanno un problema di connessione che potrebbe essere la causa del tuo problema. Espandi una stampante qui sotto per vedere la soluzione — risolverla potrebbe sistemare il problema senza una segnalazione di bug. Puoi comunque inviare una segnalazione qui sotto.',
     thankYou: 'Grazie!',
     submitted: 'La tua segnalazione bug è stata inviata.',
     viewIssue: 'Vedi issue',

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

@@ -5554,8 +5554,8 @@ export default {
     submitFailed: 'バグレポートの送信に失敗しました',
     diagnosticChecking: 'プリンター接続を確認中...',
     diagnosticHealthy: '接続チェックに合格しました — プリンターに問題は見つかりませんでした。',
-    diagnosticHeading: '設定の問題の可能性を検出しました',
-    diagnosticIntro: 'あるプリンターに接続の問題があり、それが今回の問題の原因である可能性があります。下記の対処法を確認してください — それを解決すれば、バグ報告なしで問題が解決するかもしれません。下記から報告を送信することもできます。',
+    diagnosticSummary: '{{total}} 台中 {{problems}} 台のプリンターに接続の問題があります',
+    diagnosticIntro: '1 台以上のプリンターに接続の問題があり、それが今回の問題の原因である可能性があります。下記のプリンターを展開して対処法を確認してください — それを解決すれば、バグ報告なしで問題が解決するかもしれません。下記から報告を送信することもできます。',
     thankYou: 'ありがとうございます!',
     submitted: 'バグレポートが送信されました。',
     viewIssue: 'Issueを表示',

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

@@ -5542,8 +5542,8 @@ export default {
     submitFailed: 'Falha ao enviar relatório de bug',
     diagnosticChecking: 'Verificando as conexões das impressoras...',
     diagnosticHealthy: 'Verificação de conexão aprovada — nenhum problema encontrado nas suas impressoras.',
-    diagnosticHeading: 'Possível problema de configuração detectado',
-    diagnosticIntro: 'Uma impressora tem um problema de conexão que pode estar causando seu problema. Confira a solução abaixo — resolvê-la pode solucionar o problema sem um relatório de bug. Você ainda pode enviar um relatório abaixo.',
+    diagnosticSummary: '{{problems}} de {{total}} impressoras têm problemas de conexão',
+    diagnosticIntro: 'Uma ou mais impressoras têm um problema de conexão que pode estar causando seu problema. Expanda uma impressora abaixo para ver a solução — resolvê-la pode solucionar o problema sem um relatório de bug. Você ainda pode enviar um relatório abaixo.',
     thankYou: 'Obrigado!',
     submitted: 'Seu relatório de bug foi enviado.',
     viewIssue: 'Ver issue',

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

@@ -5541,8 +5541,8 @@ export default {
     submitFailed: '提交错误报告失败',
     diagnosticChecking: '正在检查打印机连接...',
     diagnosticHealthy: '连接检查通过 — 未在您的打印机上发现问题。',
-    diagnosticHeading: '检测到可能的设置问题',
-    diagnosticIntro: '某台打印机存在连接问题,可能正是您遇到问题的原因。请查看下方的解决方法 — 解决它也许无需提交错误报告即可解决问题。您仍然可以在下方提交报告。',
+    diagnosticSummary: '{{total}} 台打印机中有 {{problems}} 台存在连接问题',
+    diagnosticIntro: '一台或多台打印机存在连接问题,可能正是您遇到问题的原因。请展开下方的打印机查看解决方法 — 解决它也许无需提交错误报告即可解决问题。您仍然可以在下方提交报告。',
     thankYou: '谢谢!',
     submitted: '您的错误报告已提交。',
     viewIssue: '查看Issue',

+ 2 - 2
frontend/src/i18n/locales/zh-TW.ts

@@ -5541,8 +5541,8 @@ export default {
     submitFailed: '提交錯誤報告失敗',
     diagnosticChecking: '正在檢查印表機連線...',
     diagnosticHealthy: '連線檢查通過 — 未在您的印表機上發現問題。',
-    diagnosticHeading: '偵測到可能的設定問題',
-    diagnosticIntro: '某台印表機存在連線問題,可能正是您遇到問題的原因。請查看下方的解決方法 — 解決它也許無需提交錯誤報告即可解決問題。您仍然可以在下方提交報告。',
+    diagnosticSummary: '{{total}} 台印表機中有 {{problems}} 台存在連線問題',
+    diagnosticIntro: '一台或多台印表機存在連線問題,可能正是您遇到問題的原因。請展開下方的印表機查看解決方法 — 解決它也許無需提交錯誤報告即可解決問題。您仍然可以在下方提交報告。',
     thankYou: '謝謝!',
     submitted: '您的錯誤報告已提交。',
     viewIssue: '檢視 Issue',

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
static/assets/index-CVVS0VHp.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
static/assets/index-Cqtgi1Io.css


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
static/assets/index-DYdMf_Qm.css


+ 2 - 2
static/index.html

@@ -26,8 +26,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DOnLEVBo.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-Cqtgi1Io.css">
+    <script type="module" crossorigin src="/assets/index-CVVS0VHp.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-DYdMf_Qm.css">
   </head>
   <body>
     <div id="root"></div>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác