浏览代码

feat(queue): a grouping that ended in failures is marked failed

A grouping whose jobs all failed (or failed and were cancelled) with
nothing printed stayed active for good, even through the start-up
clean-up. It is now marked failed, with its own filter on the Batches
tab, and opens again when a job is added. Orders are unchanged.
maziggy 6 天之前
父节点
当前提交
e250281c17
共有 100 个文件被更改,包括 132 次插入 和 8 次删除
  1. 1 0
      CHANGELOG.md
  2. 4 1
      backend/app/api/routes/print_queue.py
  3. 13 0
      backend/app/services/print_batch.py
  4. 59 0
      backend/tests/integration/test_batch_status_after_cancel.py
  5. 18 0
      frontend/src/__tests__/components/BatchOrdersView.test.tsx
  6. 5 3
      frontend/src/components/BatchOrdersView.tsx
  7. 2 0
      frontend/src/i18n/locales/de.ts
  8. 2 0
      frontend/src/i18n/locales/en.ts
  9. 2 0
      frontend/src/i18n/locales/es.ts
  10. 2 0
      frontend/src/i18n/locales/fr.ts
  11. 2 0
      frontend/src/i18n/locales/it.ts
  12. 2 0
      frontend/src/i18n/locales/ja.ts
  13. 2 0
      frontend/src/i18n/locales/ko.ts
  14. 2 0
      frontend/src/i18n/locales/nl.ts
  15. 2 0
      frontend/src/i18n/locales/pt-BR.ts
  16. 2 0
      frontend/src/i18n/locales/ru.ts
  17. 2 0
      frontend/src/i18n/locales/sv.ts
  18. 2 0
      frontend/src/i18n/locales/tr.ts
  19. 2 0
      frontend/src/i18n/locales/uk.ts
  20. 2 0
      frontend/src/i18n/locales/zh-CN.ts
  21. 2 0
      frontend/src/i18n/locales/zh-TW.ts
  22. 0 0
      static/assets/AdditionalSection-CI-0KBFW.js
  23. 0 0
      static/assets/AmsUnitCard-uk_Yz2li.js
  24. 0 0
      static/assets/ArchivesPage-BZdJItGk.js
  25. 0 0
      static/assets/AssignToAmsModal-48OyiBbN.js
  26. 0 0
      static/assets/CamWallPage-CVbyMMay.js
  27. 0 0
      static/assets/CameraDiagnoseModal-CXdDEg4k.js
  28. 0 0
      static/assets/CameraPage-BaQjdvRN.js
  29. 0 0
      static/assets/CameraWall-BEXi3nn0.js
  30. 0 0
      static/assets/ColumnConfigModal-OEbqCEBN.js
  31. 0 0
      static/assets/ConfigureAmsSlotModal-CxFz477y.js
  32. 0 0
      static/assets/ConnectAuthorizePage-nPUvdTAQ.js
  33. 0 0
      static/assets/ContextMenu-BVg9corE.js
  34. 0 0
      static/assets/ExternalLinkPage-D-pgsGvR.js
  35. 0 1
      static/assets/FileManagerPage-fEFq1oUw.js
  36. 0 0
      static/assets/FileUploadModal-DERiZEBE.js
  37. 0 0
      static/assets/FinancePage-Clfgc0bh.js
  38. 0 0
      static/assets/GCodeViewerPage-Dr-NcWaV.js
  39. 0 0
      static/assets/GroupEditPage-CLT4-gaV.js
  40. 0 0
      static/assets/ImagePreviewModal-9flDLipq.js
  41. 0 0
      static/assets/InventoryPage-B92r6vzO.js
  42. 0 0
      static/assets/LibraryTrashPage-C7BqW13p.js
  43. 0 0
      static/assets/LoginPage-Bjur0F2F.js
  44. 0 0
      static/assets/MaintenancePage-BVgAs8TF.js
  45. 0 0
      static/assets/ModelSourcesPage-Bl79vyK8.js
  46. 0 0
      static/assets/NotificationsPage--knw47Ta.js
  47. 0 1
      static/assets/PdfPreviewModal-IgxhcxJk.js
  48. 0 0
      static/assets/PhotoGalleryModal-Bk0pa9rP.js
  49. 0 0
      static/assets/PrinterLocationsPage-Lld-Dc3R.js
  50. 0 0
      static/assets/PrintersPage-BLHdTFMY.js
  51. 0 0
      static/assets/ProfilesPage-vwWO97NT.js
  52. 0 0
      static/assets/ProjectDetailPage-BoFr9JAg.js
  53. 0 0
      static/assets/ProjectsPage-DDEt6Z8U.js
  54. 0 0
      static/assets/QueuePage-4Q_oQ-9g.js
  55. 0 0
      static/assets/RichTextEditor-lt0MiHaI.js
  56. 0 0
      static/assets/SettingsPage-fW8--KK3.js
  57. 0 0
      static/assets/SetupPage-Dugyes-s.js
  58. 0 0
      static/assets/SliceModal-CzN92RR_.js
  59. 0 0
      static/assets/SpoolBuddyAmsPage-CpdB5fMO.js
  60. 0 0
      static/assets/SpoolBuddyCalibrationPage-CRXi2HuU.js
  61. 0 0
      static/assets/SpoolBuddyDashboard-CE0cofZ3.js
  62. 0 0
      static/assets/SpoolBuddyInventoryPage-DRKhAfVd.js
  63. 1 1
      static/assets/SpoolBuddySettingsPage-DEPi8KKQ.js
  64. 0 0
      static/assets/SpoolBuddyWriteTagPage-C1JGNZuY.js
  65. 0 0
      static/assets/SpreadsheetPreviewModal-Bjc9EWkA.js
  66. 0 0
      static/assets/StatsPage-CJYWrOWo.js
  67. 0 0
      static/assets/StreamOverlayPage-CRchWOoO.js
  68. 0 0
      static/assets/SystemInfoPage-CLkFbCdY.js
  69. 0 0
      static/assets/de-BVGbU5OF.js
  70. 0 0
      static/assets/de-T8J78eNF.js
  71. 0 0
      static/assets/es-BUAHoD2j.js
  72. 0 0
      static/assets/es-Bthu5UqK.js
  73. 0 0
      static/assets/fr-B6P7fapK.js
  74. 0 0
      static/assets/fr-CwOKGQyw.js
  75. 0 0
      static/assets/index-CG6Y41X9.js
  76. 1 1
      static/assets/inventorySearch-DQzOI6l_.js
  77. 0 0
      static/assets/it-DeToAb6i.js
  78. 0 0
      static/assets/it-Dp8_cxYB.js
  79. 0 0
      static/assets/ja-BILvVHhC.js
  80. 0 0
      static/assets/ja-CAEFjCw8.js
  81. 0 0
      static/assets/ko-CIvDHJhw.js
  82. 0 0
      static/assets/ko-CKpxNDDi.js
  83. 0 0
      static/assets/locationSensorDefaults-C3XHAD4m.js
  84. 0 0
      static/assets/nl-CpQP78IA.js
  85. 0 0
      static/assets/nl-G345lvTL.js
  86. 0 0
      static/assets/pdf-BHb93ajN.js
  87. 0 0
      static/assets/pt-BR-CovR6TdE.js
  88. 0 0
      static/assets/pt-BR-DkeCUjal.js
  89. 0 0
      static/assets/ru-B2tEWDib.js
  90. 0 0
      static/assets/ru-D92Ru8kS.js
  91. 0 0
      static/assets/sv-Clf_EJLB.js
  92. 0 0
      static/assets/sv-D8AJmhzM.js
  93. 0 0
      static/assets/tr-C98GeVrf.js
  94. 0 0
      static/assets/tr-Ko3FJs1z.js
  95. 0 0
      static/assets/uk-1twqs9yi.js
  96. 0 0
      static/assets/uk-8To0FdY_.js
  97. 0 0
      static/assets/utils-ByjKiHuB.js
  98. 0 0
      static/assets/zh-CN-BRXK6czx.js
  99. 0 0
      static/assets/zh-CN-lliRZJky.js
  100. 0 0
      static/assets/zh-TW-C0vyLIaL.js

+ 1 - 0
CHANGELOG.md

@@ -135,6 +135,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Dutch (nl) is now a supported interface language (#2891, requested and contributed by @Igiegel)** — Adds `nl` as the fourteenth locale, listed as "Nederlands" in the language picker. The translation was contributed as a file on the issue and needed three corrections before it could be wired up, all of which the parity gate found. First, the nine `stats.timeframe.*` entries had their **keys** translated along with their values (`'today'` had become `'vandaag'`), which would have left the Statistics timeframe selector resolving nothing and rendering raw key names for every Dutch user — the values were kept and the keys restored. Second, the file was translated against an older `en.ts` and was 84 leaves short, missing the Filament Track Switch feed prompts, the AI-detection status strings, the no-3MF internal-history banner, the batch-order stranded-plate notices, the Avery starting-position field and the whole `locationHaSensors` section from #2824; those were translated and added. Rather than splice them in, `nl.ts` was regenerated from the `en.ts` skeleton with the contributor's strings carried over, so its structure, key order and section comments now match the reference locale exactly and a future diff against `en.ts` reads as content rather than as reordering. Third, 229 leaves were identical to English; each was checked individually and all were kept, because Dutch takes most technical UI vocabulary verbatim — printer, filament, status, nozzle, timelapse, dashboard — and Dutch slicer users use the English feature names (support, ironing, prime tower, gap fill) untranslated. Those 123 distinct values are now listed explicitly in a `NL_COGNATES` allow-list in `check-i18n-parity.mjs`, the same shape the other twelve locales use, so the exemption is an enumerated translator decision rather than a blanket skip. Parity green at 6264 leaves across all 14 locales.
 
 ### Changed
+- **A grouping that ended in failures is marked failed** — A grouping whose jobs all failed, or failed and were cancelled, with nothing printed, stayed **active** for good, even through the start-up clean-up, and piled up next to real work. It is now marked **failed**, shown in red with a **Failed** filter on the Batches tab, and opens again if a job is added to it. Orders are unchanged: a failed run still leaves an order owing it.
 - **Deleting a custom maintenance type says it applies to every printer** — Maintenance types are shared, so deleting a custom one removes it, with its maintenance history, from every printer, including those of other teams when printer access is limited. The confirmation now says so.
 - **The web interface downloads about 70% less on each new version (#3175, requested by @mason9468)** — The interface's script files were sent uncompressed and with no caching instructions, so a phone fetched the whole 11 MB again and the browser asked for it on every load.
   - **Compressed:** scripts, styles and other text files under `/assets` now go out gzip-compressed when the browser accepts it. The main script drops from 11.0 MB to 3.0 MB.

+ 4 - 1
backend/app/api/routes/print_queue.py

@@ -1006,8 +1006,11 @@ async def add_to_queue(
         if not existing_batch:
             raise HTTPException(404, "Batch not found")
         await _ensure_batch_in_scope(db, existing_batch.id, printer_scope)
-        if existing_batch.status != "active":
+        # A grouping whose jobs all failed takes new ones, and is open again.
+        if existing_batch.status not in ("active", "failed"):
             raise HTTPException(400, "Cannot add items to a non-active batch")
+        if existing_batch.status == "failed":
+            existing_batch.status = "active"
         if (
             actor is not None
             and existing_batch.created_by_id is not None

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

@@ -365,6 +365,19 @@ async def refresh_batch_status(db: AsyncSession, batch: PrintBatch) -> bool:
             batch.status = "cancelled"
             logger.info("Batch %s had every item cancelled — marked cancelled", batch.id)
             return True
+        # Likewise finished, but with real failures among its jobs and nothing
+        # produced: neither "completed" nor "cancelled" says that. Not while a
+        # job is skipped: resuming the printer puts it back in the queue.
+        if settled and progress.failed > 0 and progress.skipped == 0:
+            batch.status = "failed"
+            logger.info("Batch %s ended with failures and nothing printed — marked failed", batch.id)
+            return True
+
+    # A failed grouping that has open work again (a job added to it) is active.
+    if batch.status == "failed" and (progress.pending > 0 or progress.printing > 0):
+        batch.status = "active"
+        logger.info("Batch %s has open work again — reopened", batch.id)
+        return True
 
     if batch.status == "completed" and not progress.is_fulfilled:
         batch.status = "active"

+ 59 - 0
backend/tests/integration/test_batch_status_after_cancel.py

@@ -122,3 +122,62 @@ async def test_a_grouping_whose_last_open_job_is_deleted_is_finished(
     assert (await async_client.delete(f"/api/v1/queue/{last}")).status_code == 200
 
     assert await _batch_status(async_client, grouping["id"]) == "cancelled"
+
+
+async def _settle(db_session, statuses: dict[int, str]) -> None:
+    from backend.app.services.print_batch import refresh_batches_for_items
+
+    for item_id, status in statuses.items():
+        await db_session.execute(update(PrintQueueItem).where(PrintQueueItem.id == item_id).values(status=status))
+    await refresh_batches_for_items(db_session, list(statuses))
+    await db_session.commit()
+
+
+@pytest.mark.parametrize(
+    ("ends", "expected"),
+    [
+        (("failed", "failed"), "failed"),
+        (("failed", "cancelled"), "failed"),
+        (("failed", "skipped"), "active"),
+        (("failed", "completed"), "completed"),
+    ],
+)
+async def test_a_grouping_that_produced_nothing_but_failures_is_failed(
+    async_client: AsyncClient, db_session, printer_factory, archive_factory, ends, expected
+):
+    printer = await printer_factory()
+    archive = await archive_factory(printer.id)
+    grouping, ids = await _grouping(async_client, printer.id, archive.id)
+
+    await _settle(db_session, dict(zip(ids, ends, strict=True)))
+
+    assert await _batch_status(async_client, grouping["id"]) == expected
+
+
+async def test_a_failed_grouping_takes_a_new_job_and_is_open_again(
+    async_client: AsyncClient, db_session, printer_factory, archive_factory
+):
+    printer = await printer_factory()
+    archive = await archive_factory(printer.id)
+    grouping, ids = await _grouping(async_client, printer.id, archive.id)
+    await _settle(db_session, dict.fromkeys(ids, "failed"))
+
+    added = await async_client.post(
+        "/api/v1/queue/", json={"printer_id": printer.id, "archive_id": archive.id, "batch_id": grouping["id"]}
+    )
+
+    assert added.status_code == 200, added.text
+    assert await _batch_status(async_client, grouping["id"]) == "active"
+
+
+async def test_an_order_whose_runs_all_failed_still_owes_them(
+    async_client: AsyncClient, db_session, printer_factory, archive_factory
+):
+    printer = await printer_factory()
+    archive = await archive_factory(printer.id)
+    order = await _create_order(async_client, archive.id, [{"plate_id": 1, "quantity_target": 1}])
+    run = await _queue_item(async_client, printer.id, archive.id, order["id"], plate_id=1)
+
+    await _settle(db_session, {run["id"]: "failed"})
+
+    assert await _batch_status(async_client, order["id"]) == "active"

+ 18 - 0
frontend/src/__tests__/components/BatchOrdersView.test.tsx

@@ -122,6 +122,24 @@ describe('BatchOrdersView (#342)', () => {
     expect(screen.getByText('queue.batchOrders.status.completed')).toBeInTheDocument();
   });
 
+  it('lists a grouping that ended in failures under Failed', async () => {
+    server.use(
+      http.get('/api/v1/queue/batches', ({ request }) => {
+        const status = new URL(request.url).searchParams.get('status');
+        if (status !== 'failed') return HttpResponse.json([]);
+        return HttpResponse.json([batch({ status: 'failed', failed_count: 2 })]);
+      }),
+    );
+    const user = userEvent.setup();
+    render(<BatchOrdersView hasPermission={allow} t={passthroughT} />);
+
+    await waitFor(() => expect(screen.getByText('queue.batchOrders.emptyTitle')).toBeInTheDocument());
+    await user.click(screen.getByRole('button', { name: 'queue.batchOrders.filter.failed' }));
+
+    await waitFor(() => expect(screen.getByText('Widget run')).toBeInTheDocument());
+    expect(screen.getByText('queue.batchOrders.status.failed')).toBeInTheDocument();
+  });
+
   it('offers to queue what a failed run still owes', async () => {
     let dispatched: { plate_id?: number | null; only_plate?: boolean } | null = null;
     server.use(

+ 5 - 3
frontend/src/components/BatchOrdersView.tsx

@@ -11,7 +11,7 @@ import { formatDuration, parseUTCDate } from '../utils/date';
 import { getCurrencySymbol } from '../utils/currency';
 import { useUiSettings } from '../hooks/useUiSettings';
 
-type StatusFilter = 'active' | 'completed' | 'cancelled' | 'all';
+type StatusFilter = 'active' | 'completed' | 'failed' | 'cancelled' | 'all';
 
 interface BatchOrdersViewProps {
   hasPermission: (p: Permission) => boolean;
@@ -77,7 +77,7 @@ export function BatchOrdersView({ hasPermission, t, focusBatchId = null }: Batch
   const canDispatch = hasPermission('queue:create' as Permission);
   const canCancel = hasPermission('queue:delete_all' as Permission);
 
-  const filters: StatusFilter[] = ['active', 'completed', 'cancelled', 'all'];
+  const filters: StatusFilter[] = ['active', 'completed', 'failed', 'cancelled', 'all'];
 
   return (
     <div>
@@ -147,6 +147,8 @@ const STATUS_STYLES: Record<string, string> = {
   active: 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300',
   completed: 'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300',
   cancelled: 'bg-bambu-dark-tertiary text-bambu-gray',
+  // A grouping whose jobs ended in failures with nothing printed.
+  failed: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-300',
 };
 
 function BatchOrderCard({
@@ -237,7 +239,7 @@ function BatchOrderCard({
         <div className="flex-1 h-2 bg-bambu-dark-tertiary rounded-full overflow-hidden">
           <div
             className={`h-full rounded-full transition-all ${
-              batch.status === 'completed' ? 'bg-bambu-green' : 'bg-blue-500'
+              batch.status === 'completed' ? 'bg-bambu-green' : batch.status === 'failed' ? 'bg-red-500' : 'bg-blue-500'
             }`}
             style={{ width: `${Math.min(100, percent)}%` }}
           />

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

@@ -1459,12 +1459,14 @@ export default {
       filter: {
         active: 'Aktiv',
         completed: 'Abgeschlossen',
+        failed: 'Fehlgeschlagen',
         cancelled: 'Abgebrochen',
         all: 'Alle',
       },
       status: {
         active: 'Aktiv',
         completed: 'Abgeschlossen',
+        failed: 'Fehlgeschlagen',
         cancelled: 'Abgebrochen',
       },
       noTargets: 'Nur Gruppierung',

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

@@ -1471,12 +1471,14 @@ export default {
       filter: {
         active: 'Active',
         completed: 'Completed',
+        failed: 'Failed',
         cancelled: 'Cancelled',
         all: 'All',
       },
       status: {
         active: 'Active',
         completed: 'Completed',
+        failed: 'Failed',
         cancelled: 'Cancelled',
       },
       noTargets: 'Grouping only',

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

@@ -1459,12 +1459,14 @@ export default {
       filter: {
         active: 'Activos',
         completed: 'Completados',
+        failed: 'Fallido',
         cancelled: 'Cancelados',
         all: 'Todos',
       },
       status: {
         active: 'Activo',
         completed: 'Completado',
+        failed: 'Fallido',
         cancelled: 'Cancelado',
       },
       noTargets: 'Solo agrupación',

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

@@ -1459,12 +1459,14 @@ export default {
       filter: {
         active: 'Actives',
         completed: 'Terminées',
+        failed: 'Échoué',
         cancelled: 'Annulées',
         all: 'Toutes',
       },
       status: {
         active: 'Active',
         completed: 'Terminée',
+        failed: 'Échoué',
         cancelled: 'Annulée',
       },
       noTargets: 'Regroupement seul',

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

@@ -1459,12 +1459,14 @@ export default {
       filter: {
         active: 'Attivi',
         completed: 'Completati',
+        failed: 'Non riuscito',
         cancelled: 'Annullati',
         all: 'Tutti',
       },
       status: {
         active: 'Attivo',
         completed: 'Completato',
+        failed: 'Non riuscito',
         cancelled: 'Annullato',
       },
       noTargets: 'Solo raggruppamento',

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

@@ -1458,12 +1458,14 @@ export default {
       filter: {
         active: '進行中',
         completed: '完了',
+        failed: '失敗',
         cancelled: 'キャンセル済み',
         all: 'すべて',
       },
       status: {
         active: '進行中',
         completed: '完了',
+        failed: '失敗',
         cancelled: 'キャンセル済み',
       },
       noTargets: 'グループのみ',

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

@@ -1392,12 +1392,14 @@ export default {
       filter: {
         active: '진행 중',
         completed: '완료',
+        failed: '실패',
         cancelled: '취소됨',
         all: '전체',
       },
       status: {
         active: '진행 중',
         completed: '완료',
+        failed: '실패',
         cancelled: '취소됨',
       },
       noTargets: '그룹만',

+ 2 - 0
frontend/src/i18n/locales/nl.ts

@@ -1469,12 +1469,14 @@ export default {
       filter: {
         active: 'Actief',
         completed: 'Voltooid',
+        failed: 'Mislukt',
         cancelled: 'Geannuleerd',
         all: 'Alle',
       },
       status: {
         active: 'Actief',
         completed: 'Voltooid',
+        failed: 'Mislukt',
         cancelled: 'Geannuleerd',
       },
       noTargets: 'Alleen groepering',

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

@@ -1459,12 +1459,14 @@ export default {
       filter: {
         active: 'Ativos',
         completed: 'Concluídos',
+        failed: 'Falhou',
         cancelled: 'Cancelados',
         all: 'Todos',
       },
       status: {
         active: 'Ativo',
         completed: 'Concluído',
+        failed: 'Falhou',
         cancelled: 'Cancelado',
       },
       noTargets: 'Apenas agrupamento',

+ 2 - 0
frontend/src/i18n/locales/ru.ts

@@ -1406,12 +1406,14 @@ export default {
       filter: {
         active: "Активные",
         completed: "Завершённые",
+        failed: 'Не удалось',
         cancelled: "Отменённые",
         all: "Все",
       },
       status: {
         active: "Активен",
         completed: "Завершён",
+        failed: 'Не удалось',
         cancelled: "Отменён",
       },
       noTargets: "Только группировка",

+ 2 - 0
frontend/src/i18n/locales/sv.ts

@@ -1469,12 +1469,14 @@ export default {
       filter: {
         active: 'Aktiv',
         completed: 'Slutförd',
+        failed: 'Misslyckad',
         cancelled: 'Avbruten',
         all: 'Alla',
       },
       status: {
         active: 'Aktiv',
         completed: 'Slutförd',
+        failed: 'Misslyckad',
         cancelled: 'Avbruten',
       },
       noTargets: 'Endast gruppering',

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

@@ -1459,12 +1459,14 @@ export default {
       filter: {
         active: 'Etkin',
         completed: 'Tamamlanan',
+        failed: 'Başarısız',
         cancelled: 'İptal edilen',
         all: 'Tümü',
       },
       status: {
         active: 'Etkin',
         completed: 'Tamamlandı',
+        failed: 'Başarısız',
         cancelled: 'İptal edildi',
       },
       noTargets: 'Yalnızca gruplama',

+ 2 - 0
frontend/src/i18n/locales/uk.ts

@@ -1468,12 +1468,14 @@ export default {
       filter: {
         active: "Активні",
         completed: "Завершені",
+        failed: 'Не вдалося',
         cancelled: "Скасовані",
         all: "Усі",
       },
       status: {
         active: "Активне",
         completed: "Завершене",
+        failed: 'Не вдалося',
         cancelled: "Скасоване",
       },
       noTargets: "Лише групування",

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

@@ -1459,12 +1459,14 @@ export default {
       filter: {
         active: '进行中',
         completed: '已完成',
+        failed: '失败',
         cancelled: '已取消',
         all: '全部',
       },
       status: {
         active: '进行中',
         completed: '已完成',
+        failed: '失败',
         cancelled: '已取消',
       },
       noTargets: '仅分组',

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

@@ -1459,12 +1459,14 @@ export default {
       filter: {
         active: '進行中',
         completed: '已完成',
+        failed: '失敗',
         cancelled: '已取消',
         all: '全部',
       },
       status: {
         active: '進行中',
         completed: '已完成',
+        failed: '失敗',
         cancelled: '已取消',
       },
       noTargets: '僅分組',

文件差异内容过多而无法显示
+ 0 - 0
static/assets/AdditionalSection-CI-0KBFW.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/AmsUnitCard-uk_Yz2li.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ArchivesPage-BZdJItGk.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/AssignToAmsModal-48OyiBbN.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/CamWallPage-CVbyMMay.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/CameraDiagnoseModal-CXdDEg4k.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/CameraPage-BaQjdvRN.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/CameraWall-BEXi3nn0.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ColumnConfigModal-OEbqCEBN.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ConfigureAmsSlotModal-CxFz477y.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ConnectAuthorizePage-nPUvdTAQ.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ContextMenu-BVg9corE.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ExternalLinkPage-D-pgsGvR.js


文件差异内容过多而无法显示
+ 0 - 1
static/assets/FileManagerPage-fEFq1oUw.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/FileUploadModal-DERiZEBE.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/FinancePage-Clfgc0bh.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/GCodeViewerPage-Dr-NcWaV.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/GroupEditPage-CLT4-gaV.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ImagePreviewModal-9flDLipq.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/InventoryPage-B92r6vzO.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/LibraryTrashPage-C7BqW13p.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/LoginPage-Bjur0F2F.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/MaintenancePage-BVgAs8TF.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ModelSourcesPage-Bl79vyK8.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/NotificationsPage--knw47Ta.js


文件差异内容过多而无法显示
+ 0 - 1
static/assets/PdfPreviewModal-IgxhcxJk.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/PhotoGalleryModal-Bk0pa9rP.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/PrinterLocationsPage-Lld-Dc3R.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/PrintersPage-BLHdTFMY.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ProfilesPage-vwWO97NT.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ProjectDetailPage-BoFr9JAg.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ProjectsPage-DDEt6Z8U.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/QueuePage-4Q_oQ-9g.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/RichTextEditor-lt0MiHaI.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SettingsPage-fW8--KK3.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SetupPage-Dugyes-s.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SliceModal-CzN92RR_.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SpoolBuddyAmsPage-CpdB5fMO.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SpoolBuddyCalibrationPage-CRXi2HuU.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SpoolBuddyDashboard-CE0cofZ3.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-DRKhAfVd.js


文件差异内容过多而无法显示
+ 1 - 1
static/assets/SpoolBuddySettingsPage-DEPi8KKQ.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SpoolBuddyWriteTagPage-C1JGNZuY.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SpreadsheetPreviewModal-Bjc9EWkA.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/StatsPage-CJYWrOWo.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/StreamOverlayPage-CRchWOoO.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/SystemInfoPage-CLkFbCdY.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/de-BVGbU5OF.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/de-T8J78eNF.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/es-BUAHoD2j.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/es-Bthu5UqK.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/fr-B6P7fapK.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/fr-CwOKGQyw.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/index-CG6Y41X9.js


+ 1 - 1
static/assets/inventorySearch-CIUduptj.js → static/assets/inventorySearch-DQzOI6l_.js

@@ -1 +1 @@
-import{mt as e}from"./index-D3tAzFbt.js";function t(t,n){let r=n.trim().toLowerCase();if(!r)return!0;let i=r.match(/^#(\d+)$/);return i?t.id===Number(i[1]):String(t.id).includes(r)||t.material.toLowerCase().includes(r)||(t.brand?.toLowerCase().includes(r)??!1)||(t.color_name?.toLowerCase().includes(r)??!1)||(e(t.color_name,t.rgba,t.color_name_is_synthesized)?.toLowerCase().includes(r)??!1)||(t.subtype?.toLowerCase().includes(r)??!1)||(t.note?.toLowerCase().includes(r)??!1)||(t.slicer_filament_name?.toLowerCase().includes(r)??!1)||(t.storage_location?.toLowerCase().includes(r)??!1)||(t.material_number?.toLowerCase().includes(r)??!1)||(t.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(r)||(e.supplier_article_number?.toLowerCase().includes(r)??!1))??!1)}function n(e,n){return n?e.filter(e=>t(e,n)):e}export{n as t};
+import{mt as e}from"./index-CG6Y41X9.js";function t(t,n){let r=n.trim().toLowerCase();if(!r)return!0;let i=r.match(/^#(\d+)$/);return i?t.id===Number(i[1]):String(t.id).includes(r)||t.material.toLowerCase().includes(r)||(t.brand?.toLowerCase().includes(r)??!1)||(t.color_name?.toLowerCase().includes(r)??!1)||(e(t.color_name,t.rgba,t.color_name_is_synthesized)?.toLowerCase().includes(r)??!1)||(t.subtype?.toLowerCase().includes(r)??!1)||(t.note?.toLowerCase().includes(r)??!1)||(t.slicer_filament_name?.toLowerCase().includes(r)??!1)||(t.storage_location?.toLowerCase().includes(r)??!1)||(t.material_number?.toLowerCase().includes(r)??!1)||(t.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(r)||(e.supplier_article_number?.toLowerCase().includes(r)??!1))??!1)}function n(e,n){return n?e.filter(e=>t(e,n)):e}export{n as t};

文件差异内容过多而无法显示
+ 0 - 0
static/assets/it-DeToAb6i.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/it-Dp8_cxYB.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ja-BILvVHhC.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ja-CAEFjCw8.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ko-CIvDHJhw.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ko-CKpxNDDi.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/locationSensorDefaults-C3XHAD4m.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/nl-CpQP78IA.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/nl-G345lvTL.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/pdf-BHb93ajN.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/pt-BR-CovR6TdE.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/pt-BR-DkeCUjal.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ru-B2tEWDib.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/ru-D92Ru8kS.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/sv-Clf_EJLB.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/sv-D8AJmhzM.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/tr-C98GeVrf.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/tr-Ko3FJs1z.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/uk-1twqs9yi.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/uk-8To0FdY_.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/utils-ByjKiHuB.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/zh-CN-BRXK6czx.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/zh-CN-lliRZJky.js


文件差异内容过多而无法显示
+ 0 - 0
static/assets/zh-TW-C0vyLIaL.js


部分文件因为文件数量过多而无法显示