Pārlūkot izejas kodu

Compact Profile defaults list with Apply all

- Settings -> Filament -> Profile defaults: one line per default
  (product, colour, preset, counts), sorted, searchable, scrolling;
  presets and K profiles fold out per line
- Apply and delete are icon buttons on the line
- New Apply all button applies every default in turn, carries on
  after a failure and reports the spools filled in
- New strings in all locales; component tests
maziggy 3 dienas atpakaļ
vecāks
revīzija
6c0f8fc6b8
53 mainītis faili ar 384 papildinājumiem un 132 dzēšanām
  1. 1 0
      CHANGELOG.md
  2. 93 60
      frontend/src/__tests__/components/ProfileDefaultsSettings.test.tsx
  3. 173 67
      frontend/src/components/ProfileDefaultsSettings.tsx
  4. 7 0
      frontend/src/i18n/locales/de.ts
  5. 7 0
      frontend/src/i18n/locales/en.ts
  6. 7 0
      frontend/src/i18n/locales/es.ts
  7. 7 0
      frontend/src/i18n/locales/fr.ts
  8. 7 0
      frontend/src/i18n/locales/it.ts
  9. 7 0
      frontend/src/i18n/locales/ja.ts
  10. 7 0
      frontend/src/i18n/locales/ko.ts
  11. 7 0
      frontend/src/i18n/locales/nl.ts
  12. 7 0
      frontend/src/i18n/locales/pl.ts
  13. 7 0
      frontend/src/i18n/locales/pt-BR.ts
  14. 7 0
      frontend/src/i18n/locales/ru.ts
  15. 7 0
      frontend/src/i18n/locales/sv.ts
  16. 7 0
      frontend/src/i18n/locales/tr.ts
  17. 7 0
      frontend/src/i18n/locales/uk.ts
  18. 7 0
      frontend/src/i18n/locales/zh-CN.ts
  19. 7 0
      frontend/src/i18n/locales/zh-TW.ts
  20. 0 0
      static/assets/ArchivesPage-Bgu-mmQF.js
  21. 0 0
      static/assets/CamWallPage-Bl4ZuHI8.js
  22. 0 0
      static/assets/CameraPage-IQKnDOPO.js
  23. 0 0
      static/assets/FileManagerPage-BXTBdJgD.js
  24. 0 0
      static/assets/InventoryPage-olfG5Ifl.js
  25. 0 0
      static/assets/Layout-D1aBDvSX.js
  26. 0 0
      static/assets/ModelSourcesPage-D0_nbWH9.js
  27. 0 0
      static/assets/PrintersPage-CTQY9esQ.js
  28. 0 0
      static/assets/SettingsPage-tsvqDoR0.js
  29. 0 0
      static/assets/SliceModal-D1lpDVod.js
  30. 0 0
      static/assets/SpoolBuddyInventoryPage-gmOavjs5.js
  31. 1 1
      static/assets/SpoolBuddySettingsPage-D3RYvOqg.js
  32. 0 0
      static/assets/de-abRfFq1c.js
  33. 0 0
      static/assets/es-RafEYlKI.js
  34. 0 0
      static/assets/fr-CzX6oxc5.js
  35. 1 0
      static/assets/index-C7v2Gy3m.css
  36. 0 0
      static/assets/index-DuovVT2X.js
  37. 0 1
      static/assets/index-PFRn_rWo.css
  38. 0 0
      static/assets/it-pwWRTwxx.js
  39. 0 0
      static/assets/ja-B48V9hcb.js
  40. 0 0
      static/assets/ko-C3sVX6WY.js
  41. 0 0
      static/assets/locationSensorDefaults-CNNhkqbY.js
  42. 0 0
      static/assets/nl-TJluoHO7.js
  43. 0 0
      static/assets/pl-OCLT5eNo.js
  44. 0 0
      static/assets/pt-BR-BjNYQCfH.js
  45. 0 0
      static/assets/ru-B0J8LbB0.js
  46. 0 0
      static/assets/spoolCost-VHWCiUPP.js
  47. 0 0
      static/assets/sv-Bqlxc2AD.js
  48. 0 0
      static/assets/tr-BKW5c47u.js
  49. 0 0
      static/assets/uk-2BbTIkg8.js
  50. 0 0
      static/assets/usePageFileDrop-CxD-1UXx.js
  51. 0 0
      static/assets/zh-CN-DlG9KG26.js
  52. 0 0
      static/assets/zh-TW-B98JAdfW.js
  53. 3 3
      static/index.html

+ 1 - 0
CHANGELOG.md

@@ -9,6 +9,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Cam Wall tiles show temperatures and finish time, and can pause, stop and switch the light (#3479, sponsor vote Q4 2026)** — With the **Full** status overlay, every connected tile shows its nozzle (both on a dual-nozzle printer), bed and chamber temperatures, and a printing tile adds its finish time beside the time left. Hovering over a tile shows **Pause/Resume** and **Stop**, with the same confirmations as the printer card, and a **chamber light** switch; **Tile controls: Always** in the wall settings keeps them on screen, and a touch screen always shows them. The controls need the `printers:control` permission; a user without it, an offline printer and a Cam Wall token (kiosk) wall get none.
 
 ### Changed
+- **Profile defaults is compact and has Apply all** — Settings → Filament → Profile defaults showed every default as a large box with all its presets and K profiles, so a few products already filled the page. Each default is now one line (product, colour, slicer preset, and how many printer presets and K profiles it has), sorted by product, with a search box; click a line to see its presets and K profiles. The list scrolls on its own. **Apply all** applies every default to the spools in stock in one go, and like Apply to existing spools it only fills in what a spool doesn't have yet. Wiki updated.
 - **Slicer sidecars on Bambu Studio 02.09.01.70 and OrcaSlicer 2.5.0 (#3442)** — The Bambu Studio sidecar now ships the H3C printer presets (0.2, 0.4, 0.6 and 0.8 mm nozzle). OrcaSlicer has no H3C presets yet. OrcaSlicer 2.5.0 ships its bundled presets in a new packed format, so the sidecar image now fetches the same release's presets as JSON for its preset handling. Slicing with a printer preset saved under a name of its own keeps working on 2.5.0; the sliced file then names the Bambu preset it was saved from as its printer preset. Sidecar images built for older slicer versions work as before.
 - **"Any model" jobs go to the printer with the fewest print hours (#3480, sponsor vote Q4 2026)** — When several printers of a model can take a job, the queue used to pick the first one in its list, so on a farm the same printer got most of the work. It now picks the one with the fewest print hours (the Maintenance counter, including baseline hours entered there), so jobs and wear spread across the farm. Preferred colours still come first, and the queue powers on the least used printer too when it has to switch one on with an Auto Power On plug.
 

+ 93 - 60
frontend/src/__tests__/components/ProfileDefaultsSettings.test.tsx

@@ -1,24 +1,13 @@
-/**
- * Settings > Filament > Profile defaults (#1326): lists the defaults, applies
- * one to the inventory in use, deletes one after asking.
- */
-
 import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen, waitFor, fireEvent } from '@testing-library/react';
+import { fireEvent, screen, waitFor } from '@testing-library/react';
 import { render } from '../utils';
 import { ProfileDefaultsSettings } from '../../components/ProfileDefaultsSettings';
-import { api } from '../../api/client';
+import type { ProfileDefault } from '../../api/client';
 
-vi.mock('../../api/client', () => ({
-  api: {
-    getSettings: vi.fn().mockResolvedValue({}),
-    getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
-    getProfileDefaults: vi.fn(),
-    getPrinters: vi.fn().mockResolvedValue([{ id: 3, name: 'H2D Left Bench' }]),
-    getSpoolmanSettings: vi.fn(),
-    applyProfileDefault: vi.fn().mockResolvedValue({ updated: 4 }),
-    deleteProfileDefault: vi.fn().mockResolvedValue({ status: 'deleted' }),
-  },
+vi.mock('react-i18next', () => ({
+  useTranslation: () => ({
+    t: (key: string, opts?: Record<string, unknown>) => (opts && 'count' in opts ? `${key}:${opts.count}` : key),
+  }),
 }));
 
 const mockShowToast = vi.fn();
@@ -27,64 +16,108 @@ vi.mock('../../contexts/ToastContext', async (importOriginal) => {
   return { ...actual, useToast: () => ({ showToast: mockShowToast }) };
 });
 
-const mApi = api as unknown as Record<string, ReturnType<typeof vi.fn>>;
-
-const row = {
-  id: 7,
-  brand: 'Bambu Lab',
-  material: 'PLA',
-  subtype: 'Basic',
-  color_name: '',
-  slicer_filament: 'PFUS01',
-  slicer_filament_name: 'My PLA',
-  presets: [],
-  k_profiles: [{ printer_id: 3, extruder: 0, nozzle_diameter: '0.4', nozzle_type: null, k_value: 0.02, name: 'PLA HF', cali_idx: 5, setting_id: null }],
-  updated_at: '2026-10-25T10:00:00Z',
-};
+vi.mock('../../api/client', () => ({
+  api: {
+    getSettings: vi.fn().mockResolvedValue({}),
+    getUiSettings: vi.fn().mockResolvedValue({}),
+    getProfileDefaults: vi.fn(),
+    getPrinters: vi.fn().mockResolvedValue([]),
+    getSpoolmanSettings: vi.fn().mockResolvedValue({ spoolman_enabled: 'false', spoolman_url: '' }),
+    applyProfileDefault: vi.fn(),
+    deleteProfileDefault: vi.fn(),
+  },
+}));
+
+import { api } from '../../api/client';
+
+function makeDefault(id: number, brand: string, material: string, colour: string, preset: string): ProfileDefault {
+  return {
+    id,
+    brand,
+    material,
+    subtype: '',
+    color_name: colour,
+    slicer_filament: `GF${id}`,
+    slicer_filament_name: preset,
+    presets: [{ printer_model: 'X1C', nozzle_diameter: '0.4', slicer_filament: `GF${id}`, slicer_filament_name: preset }],
+    k_profiles: [],
+    updated_at: '2026-11-08T00:00:00Z',
+  } as unknown as ProfileDefault;
+}
+
+const DEFAULTS = [
+  makeDefault(1, 'Polymaker', 'PETG', '', 'Polymaker PETG'),
+  makeDefault(2, 'Bambu', 'PLA', 'Red', 'Bambu PLA Basic'),
+  makeDefault(3, 'Bambu', 'PLA', '', 'Bambu PLA Basic'),
+];
 
 describe('ProfileDefaultsSettings', () => {
   beforeEach(() => {
     vi.clearAllMocks();
-    mApi.getProfileDefaults.mockResolvedValue([row]);
-    mApi.getSpoolmanSettings.mockResolvedValue({ spoolman_enabled: 'false', spoolman_url: '' });
+    vi.mocked(api.getProfileDefaults).mockResolvedValue(DEFAULTS);
   });
 
-  it('lists a default with its preset and K profiles', async () => {
+  it('lists one sorted line per default with its details folded away', async () => {
     render(<ProfileDefaultsSettings />);
-    expect(await screen.findByText('Bambu Lab PLA Basic')).toBeInTheDocument();
-    expect(screen.getByText('All colours')).toBeInTheDocument();
-    expect(await screen.findByText('H2D Left Bench 0.4 mm: PLA HF K=0.020')).toBeInTheDocument();
+
+    const rows = await screen.findAllByRole('button', { name: 'common.expand' });
+    // Sorted by product.
+    expect(rows.map((r) => r.querySelector('span.text-white')?.textContent)).toEqual([
+      'Bambu PLA',
+      'Bambu PLA',
+      'Polymaker PETG',
+    ]);
+    expect(rows.some((r) => r.textContent?.includes('Red'))).toBe(true);
+    // The per-model presets show only once a row is opened.
+    expect(screen.queryByText(/^X1C 0.4 mm/)).toBeNull();
+
+    fireEvent.click(rows[2]);
+
+    expect(screen.getByText('X1C 0.4 mm: Polymaker PETG')).toBeTruthy();
+    expect(screen.getByRole('button', { name: 'common.collapse' })).toBeTruthy();
   });
 
-  it.each([
-    ['internal', 'false', false],
-    ['Spoolman', 'true', true],
-  ])('applies to the %s inventory', async (_label, enabled, spoolmanMode) => {
-    mApi.getSpoolmanSettings.mockResolvedValue({ spoolman_enabled: enabled, spoolman_url: 'http://spoolman.test' });
+  it('filters by product, colour or preset', async () => {
     render(<ProfileDefaultsSettings />);
-    await screen.findByText('Bambu Lab PLA Basic');
-    // Enabled once the inventory mode is known: the route depends on it.
-    const apply = screen.getByRole('button', { name: /Apply to existing spools/ });
-    await waitFor(() => expect(apply).not.toBeDisabled());
-    fireEvent.click(apply);
-    await waitFor(() => expect(mApi.applyProfileDefault).toHaveBeenCalledWith(7, spoolmanMode));
-    await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith('Filled in on 4 spools', 'success'));
+    await screen.findAllByRole('button', { name: 'common.expand' });
+
+    fireEvent.change(screen.getByPlaceholderText('inventory.profileDefaults.search'), { target: { value: 'red' } });
+    expect(screen.getAllByRole('button', { name: 'common.expand' })).toHaveLength(1);
+
+    fireEvent.change(screen.getByPlaceholderText('inventory.profileDefaults.search'), { target: { value: 'nothing' } });
+    expect(screen.queryAllByRole('button', { name: 'common.expand' })).toHaveLength(0);
+    expect(screen.getByText('inventory.profileDefaults.noMatch')).toBeTruthy();
   });
 
-  it('cannot apply before the inventory mode is known', async () => {
-    mApi.getSpoolmanSettings.mockReturnValue(new Promise(() => {}));
+  it('apply all applies every default and reports the spools filled in', async () => {
+    vi.mocked(api.applyProfileDefault).mockResolvedValue({ updated: 2 });
     render(<ProfileDefaultsSettings />);
-    await screen.findByText('Bambu Lab PLA Basic');
-    expect(screen.getByRole('button', { name: /Apply to existing spools/ })).toBeDisabled();
+    await screen.findAllByRole('button', { name: 'common.expand' });
+    // Also with a search active: it applies all of them, not only those shown.
+    fireEvent.change(screen.getByPlaceholderText('inventory.profileDefaults.search'), { target: { value: 'red' } });
+
+    const button = screen.getByRole('button', { name: /inventory.profileDefaults.applyAll$/ });
+    await waitFor(() => expect((button as HTMLButtonElement).disabled).toBe(false));
+    fireEvent.click(button);
+
+    await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith('inventory.profileDefaults.applied:6', 'success'));
+    expect(vi.mocked(api.applyProfileDefault).mock.calls.map((c) => c[0]).sort()).toEqual([1, 2, 3]);
   });
 
-  it('deletes only after confirming', async () => {
+  it('apply all goes on after a failure and says how many failed', async () => {
+    vi.mocked(api.applyProfileDefault)
+      .mockResolvedValueOnce({ updated: 1 })
+      .mockRejectedValueOnce(new Error('boom'))
+      .mockResolvedValueOnce({ updated: 1 });
     render(<ProfileDefaultsSettings />);
-    await screen.findByText('Bambu Lab PLA Basic');
-    fireEvent.click(screen.getByRole('button', { name: 'Delete' }));
-    expect(mApi.deleteProfileDefault).not.toHaveBeenCalled();
-    const confirm = await screen.findAllByRole('button', { name: 'Delete' });
-    fireEvent.click(confirm[confirm.length - 1]);
-    await waitFor(() => expect(mApi.deleteProfileDefault).toHaveBeenCalledWith(7));
+    const button = await screen.findByRole('button', { name: /inventory.profileDefaults.applyAll$/ });
+    await waitFor(() => expect((button as HTMLButtonElement).disabled).toBe(false));
+
+    fireEvent.click(button);
+
+    await waitFor(() =>
+      expect(mockShowToast).toHaveBeenCalledWith('inventory.profileDefaults.applyAllFailed', 'error'),
+    );
+    expect(api.applyProfileDefault).toHaveBeenCalledTimes(3);
   });
 });

+ 173 - 67
frontend/src/components/ProfileDefaultsSettings.tsx

@@ -1,25 +1,28 @@
 import { useState } from 'react';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
-import { Loader2, Sparkles, Trash2, Wand2 } from 'lucide-react';
+import { ChevronRight, Loader2, Search, Sparkles, Trash2, Wand2 } from 'lucide-react';
 import { api } from '../api/client';
 import type { ProfileDefault } from '../api/client';
 import { useToast } from '../contexts/ToastContext';
-import { Button } from './Button';
 import { Card, CardContent, CardHeader } from './Card';
+import { Button } from './Button';
 import { ConfirmModal } from './ConfirmModal';
 
 /**
  * Settings > Filament > Profile defaults (#1326): what new spools of each
  * product start with. A default is saved from a spool's Printers tab ("Save as
  * default"), which is also how one is changed: this card lists them, applies
- * one to the spools already in stock, and deletes one.
+ * one to the spools already in stock, and deletes one. There can be one per
+ * product and colour, so each is a single line and its profiles fold out.
  */
 export function ProfileDefaultsSettings() {
   const { t } = useTranslation();
   const { showToast } = useToast();
   const queryClient = useQueryClient();
   const [deleting, setDeleting] = useState<ProfileDefault | null>(null);
+  const [search, setSearch] = useState('');
+  const [expanded, setExpanded] = useState<Set<number>>(new Set());
 
   const { data: defaults = [], isLoading } = useQuery({
     queryKey: ['profile-defaults'],
@@ -46,6 +49,30 @@ export function ProfileDefaultsSettings() {
     onError: () => showToast(t('inventory.profileDefaults.applyFailed'), 'error'),
   });
 
+  // One request per default, one after the other. The order does not matter: each
+  // spool resolves its own colour's values, and nothing a spool has is overwritten.
+  const applyAllMutation = useMutation({
+    mutationFn: async (rows: ProfileDefault[]) => {
+      let updated = 0;
+      let failed = 0;
+      for (const row of rows) {
+        try {
+          updated += (await api.applyProfileDefault(row.id, spoolmanMode)).updated;
+        } catch {
+          failed += 1;
+        }
+      }
+      return { updated, failed };
+    },
+    onSuccess: async ({ updated, failed }) => {
+      if (failed > 0) showToast(t('inventory.profileDefaults.applyAllFailed', { failed, updated }), 'error');
+      else showToast(t('inventory.profileDefaults.applied', { count: updated }), 'success');
+      await queryClient.invalidateQueries({ queryKey: ['inventory-spools'] });
+      await queryClient.invalidateQueries({ queryKey: ['spoolman-inventory-spools'] });
+    },
+  });
+  const applying = applyMutation.isPending || applyAllMutation.isPending;
+
   const deleteMutation = useMutation({
     mutationFn: (row: ProfileDefault) => api.deleteProfileDefault(row.id),
     onSuccess: async () => {
@@ -60,14 +87,51 @@ export function ProfileDefaultsSettings() {
 
   const productOf = (row: ProfileDefault) =>
     [row.brand, row.material, row.subtype].filter((part) => part.trim()).join(' ');
+  const colourOf = (row: ProfileDefault) => row.color_name || t('inventory.profileDefaults.allColours');
+
+  // A list that can hold one row per product and colour in stock: sorted, searchable,
+  // one line each with the details folded away.
+  const query = search.trim().toLowerCase();
+  const rows = defaults
+    .map((row) => ({ row, product: productOf(row), colour: colourOf(row) }))
+    .filter(
+      ({ row, product, colour }) =>
+        !query ||
+        `${product} ${colour} ${row.slicer_filament_name || row.slicer_filament || ''}`.toLowerCase().includes(query),
+    )
+    .sort((a, b) => a.product.localeCompare(b.product) || a.colour.localeCompare(b.colour));
+
+  const toggle = (id: number) =>
+    setExpanded((prev) => {
+      const next = new Set(prev);
+      if (next.has(id)) next.delete(id);
+      else next.add(id);
+      return next;
+    });
 
   return (
     <Card id="card-profiledefaults">
       <CardHeader>
-        <h2 className="text-lg font-semibold text-white flex items-center gap-2">
-          <Sparkles className="w-5 h-5 text-bambu-green" />
-          {t('inventory.profileDefaults.title')}
-        </h2>
+        <div className="flex items-center justify-between gap-2">
+          <h2 className="text-lg font-semibold text-white flex items-center gap-2">
+            <Sparkles className="w-5 h-5 text-bambu-green" />
+            {t('inventory.profileDefaults.title')}
+            {defaults.length > 0 && <span className="text-sm font-normal text-bambu-gray">({defaults.length})</span>}
+          </h2>
+          {defaults.length > 0 && (
+            <Button
+              size="sm"
+              variant="secondary"
+              onClick={() => applyAllMutation.mutate(defaults)}
+              // Not before the inventory mode is known: it picks the spools written to.
+              disabled={applying || spoolmanSettings === undefined}
+              title={t('inventory.profileDefaults.applyAllHelp')}
+            >
+              {applyAllMutation.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Wand2 className="w-4 h-4" />}
+              {t('inventory.profileDefaults.applyAll')}
+            </Button>
+          )}
+        </div>
       </CardHeader>
       <CardContent className="space-y-3">
         <p className="text-sm text-bambu-gray">{t('inventory.profileDefaults.description')}</p>
@@ -78,73 +142,115 @@ export function ProfileDefaultsSettings() {
         ) : defaults.length === 0 ? (
           <p className="text-sm text-bambu-gray italic">{t('inventory.profileDefaults.empty')}</p>
         ) : (
-          <ul className="space-y-2">
-            {defaults.map((row) => (
-              <li key={row.id} className="p-3 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg">
-                <div className="flex flex-wrap items-start gap-2">
-                  <div className="min-w-0 flex-1">
-                    <p className="text-sm text-white">
-                      {productOf(row)}
-                      <span className="ml-2 text-xs text-bambu-gray">
-                        {row.color_name || t('inventory.profileDefaults.allColours')}
-                      </span>
-                    </p>
-                    <ul className="mt-1 space-y-0.5 text-xs text-bambu-gray">
-                      {row.slicer_filament && (
-                        <li>
-                          {t('inventory.filamentPreset')}: {row.slicer_filament_name || row.slicer_filament}
-                        </li>
-                      )}
-                      {row.presets.map((p) => (
-                        <li key={`${p.printer_model}:${p.nozzle_diameter}`}>
-                          {p.printer_model}
-                          {p.nozzle_diameter ? ` ${p.nozzle_diameter} mm` : ''}: {p.slicer_filament_name || p.slicer_filament}
-                        </li>
-                      ))}
-                      {row.k_profiles.map((k) => (
-                        <li key={`${k.printer_id}:${k.extruder}:${k.nozzle_diameter}`}>
-                          {`${printerName(k.printer_id)} ${k.nozzle_diameter ?? '0.4'} mm: ${k.name || '-'} K=${k.k_value.toFixed(3)}`}
-                          {(k.extruder ?? 0) > 0 ? ` (${t('inventory.leftNozzle')})` : ''}
-                        </li>
-                      ))}
-                    </ul>
-                  </div>
-                  <div className="flex gap-2 shrink-0">
-                    <Button
-                      size="sm"
-                      variant="secondary"
-                      onClick={() => applyMutation.mutate(row)}
-                      // Not before the inventory mode is known: it picks the spools written to.
-                      disabled={applyMutation.isPending || spoolmanSettings === undefined}
-                      title={t('inventory.profileDefaults.applyHelp')}
-                    >
-                      {applyMutation.isPending && applyMutation.variables?.id === row.id ? (
-                        <Loader2 className="w-4 h-4 animate-spin" />
-                      ) : (
-                        <Wand2 className="w-4 h-4" />
+          <>
+            <div className="relative">
+              <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray" />
+              <input
+                type="text"
+                className="w-full pl-10 pr-3 py-1.5 text-sm bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
+                placeholder={t('inventory.profileDefaults.search')}
+                aria-label={t('common.search')}
+                value={search}
+                onChange={(e) => setSearch(e.target.value)}
+              />
+            </div>
+            <ul className="max-h-[480px] overflow-y-auto border border-bambu-dark-tertiary rounded-lg divide-y divide-bambu-dark-tertiary">
+              {rows.length === 0 ? (
+                <li className="px-3 py-4 text-center text-sm text-bambu-gray">{t('inventory.profileDefaults.noMatch')}</li>
+              ) : (
+                rows.map(({ row, product, colour }) => {
+                  const open = expanded.has(row.id);
+                  const preset = row.slicer_filament_name || row.slicer_filament;
+                  return (
+                    <li key={row.id} className="text-sm">
+                      <div className="flex items-center gap-2 px-2 py-1.5 hover:bg-bambu-dark">
+                        <button
+                          type="button"
+                          onClick={() => toggle(row.id)}
+                          aria-expanded={open}
+                          aria-label={open ? t('common.collapse') : t('common.expand')}
+                          className="flex min-w-0 flex-1 items-center gap-2 text-left"
+                        >
+                          <ChevronRight
+                            className={`w-4 h-4 shrink-0 text-bambu-gray transition-transform ${open ? 'rotate-90' : ''}`}
+                          />
+                          <span className="truncate text-white">{product}</span>
+                          <span className="shrink-0 text-xs text-bambu-gray">{colour}</span>
+                          {preset && (
+                            <span className="hidden min-w-0 truncate text-xs text-bambu-gray md:inline">· {preset}</span>
+                          )}
+                          <span className="ml-auto hidden shrink-0 gap-1 text-xs text-bambu-gray sm:flex">
+                            {row.presets.length > 0 && (
+                              <span className="rounded bg-bambu-dark-tertiary px-1.5">
+                                {t('inventory.profileDefaults.presetsCount', { count: row.presets.length })}
+                              </span>
+                            )}
+                            {row.k_profiles.length > 0 && (
+                              <span className="rounded bg-bambu-dark-tertiary px-1.5">
+                                {t('inventory.profileDefaults.kProfilesCount', { count: row.k_profiles.length })}
+                              </span>
+                            )}
+                          </span>
+                        </button>
+                        <button
+                          type="button"
+                          onClick={() => applyMutation.mutate(row)}
+                          // Not before the inventory mode is known: it picks the spools written to.
+                          disabled={applying || spoolmanSettings === undefined}
+                          title={`${t('inventory.profileDefaults.apply')}: ${t('inventory.profileDefaults.applyHelp')}`}
+                          aria-label={t('inventory.profileDefaults.apply')}
+                          className="shrink-0 rounded p-1.5 text-bambu-gray hover:bg-bambu-dark-tertiary hover:text-bambu-green disabled:opacity-50"
+                        >
+                          {applyMutation.isPending && applyMutation.variables?.id === row.id ? (
+                            <Loader2 className="w-4 h-4 animate-spin" />
+                          ) : (
+                            <Wand2 className="w-4 h-4" />
+                          )}
+                        </button>
+                        <button
+                          type="button"
+                          onClick={() => setDeleting(row)}
+                          title={t('common.delete')}
+                          aria-label={t('common.delete')}
+                          className="shrink-0 rounded p-1.5 text-bambu-gray hover:bg-red-500/20 hover:text-red-400"
+                        >
+                          <Trash2 className="w-4 h-4" />
+                        </button>
+                      </div>
+                      {open && (
+                        <ul className="space-y-0.5 pb-2 pl-8 pr-3 text-xs text-bambu-gray">
+                          {preset && (
+                            <li>
+                              {t('inventory.filamentPreset')}: {preset}
+                            </li>
+                          )}
+                          {row.presets.map((p) => (
+                            <li key={`${p.printer_model}:${p.nozzle_diameter}`}>
+                              {p.printer_model}
+                              {p.nozzle_diameter ? ` ${p.nozzle_diameter} mm` : ''}: {p.slicer_filament_name || p.slicer_filament}
+                            </li>
+                          ))}
+                          {row.k_profiles.map((k) => (
+                            <li key={`${k.printer_id}:${k.extruder}:${k.nozzle_diameter}`}>
+                              {`${printerName(k.printer_id)} ${k.nozzle_diameter ?? '0.4'} mm: ${k.name || '-'} K=${k.k_value.toFixed(3)}`}
+                              {(k.extruder ?? 0) > 0 ? ` (${t('inventory.leftNozzle')})` : ''}
+                            </li>
+                          ))}
+                        </ul>
                       )}
-                      {t('inventory.profileDefaults.apply')}
-                    </Button>
-                    <Button
-                      size="sm"
-                      variant="secondary"
-                      onClick={() => setDeleting(row)}
-                      aria-label={t('common.delete')}
-                    >
-                      <Trash2 className="w-4 h-4" />
-                    </Button>
-                  </div>
-                </div>
-              </li>
-            ))}
-          </ul>
+                    </li>
+                  );
+                })
+              )}
+            </ul>
+          </>
         )}
       </CardContent>
       {deleting && (
         <ConfirmModal
           title={t('inventory.profileDefaults.deleteTitle')}
           message={t('inventory.profileDefaults.deleteMessage', {
-            product: `${productOf(deleting)} ${deleting.color_name || t('inventory.profileDefaults.allColours')}`,
+            product: `${productOf(deleting)} ${colourOf(deleting)}`,
           })}
           confirmText={t('common.delete')}
           variant="danger"

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

@@ -5336,6 +5336,13 @@ export default {
       deleteTitle: 'Profil-Standard löschen',
       deleteMessage: 'Profil-Standard für {{product}} löschen? Spulen, die seine Profile schon haben, behalten sie.',
       deleteFailed: 'Profil-Standard konnte nicht gelöscht werden',
+      search: 'Nach Produkt, Farbe oder Profil suchen',
+      noMatch: 'Kein Profil-Standard passt',
+      presetsCount: 'Druckerprofile: {{count}}',
+      kProfilesCount: 'K-Profile: {{count}}',
+      applyAll: 'Alle anwenden',
+      applyAllHelp: 'Bei aktiven Spulen aus allen Profil-Standards ergänzen, was noch fehlt. Vorhandenes wird nicht geändert.',
+      applyAllFailed: '{{failed}} Profil-Standards konnten nicht angewendet werden; bei {{updated}} Spulen ergänzt',
     },
     suppliers: {
       label: 'Lieferanten',

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

@@ -5378,6 +5378,13 @@ export default {
       deleteTitle: 'Delete profile default',
       deleteMessage: 'Delete the profile default for {{product}}? Spools that already have its profiles keep them.',
       deleteFailed: 'Could not delete the profile default',
+      search: 'Search by product, colour or preset',
+      noMatch: 'No profile default matches',
+      presetsCount: 'Printer presets: {{count}}',
+      kProfilesCount: 'K profiles: {{count}}',
+      applyAll: 'Apply all',
+      applyAllHelp: 'Fill in what active spools don\'t have yet, from every profile default. Nothing they already have is changed.',
+      applyAllFailed: '{{failed}} profile defaults could not be applied; filled in on {{updated}} spools',
     },
     suppliers: {
       label: 'Suppliers',

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

@@ -5339,6 +5339,13 @@ export default {
       deleteTitle: 'Eliminar perfil predeterminado',
       deleteMessage: '¿Eliminar el perfil predeterminado de {{product}}? Las bobinas que ya tienen sus perfiles los conservan.',
       deleteFailed: 'No se pudo eliminar el perfil predeterminado',
+      search: 'Buscar por producto, color o perfil',
+      noMatch: 'Ningún perfil predeterminado coincide',
+      presetsCount: 'Perfiles de impresora: {{count}}',
+      kProfilesCount: 'Perfiles K: {{count}}',
+      applyAll: 'Aplicar todo',
+      applyAllHelp: 'Completa lo que les falta a las bobinas activas con todos los perfiles predeterminados. No se cambia nada de lo que ya tienen.',
+      applyAllFailed: 'No se pudieron aplicar {{failed}} perfiles predeterminados; completado en {{updated}} bobinas',
     },
     suppliers: {
       label: 'Proveedores',

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

@@ -5323,6 +5323,13 @@ export default {
       deleteTitle: 'Supprimer le profil par défaut',
       deleteMessage: 'Supprimer le profil par défaut de {{product}} ? Les bobines qui ont déjà ses profils les gardent.',
       deleteFailed: 'Impossible de supprimer le profil par défaut',
+      search: 'Rechercher par produit, couleur ou profil',
+      noMatch: 'Aucun profil par défaut ne correspond',
+      presetsCount: 'Profils d\'imprimante : {{count}}',
+      kProfilesCount: 'Profils K : {{count}}',
+      applyAll: 'Tout appliquer',
+      applyAllHelp: 'Complète ce qui manque aux bobines actives à partir de tous les profils par défaut. Rien de ce qu\'elles ont déjà n\'est modifié.',
+      applyAllFailed: '{{failed}} profils par défaut n\'ont pas pu être appliqués ; complété sur {{updated}} bobines',
     },
     suppliers: {
       label: 'Fournisseurs',

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

@@ -5322,6 +5322,13 @@ export default {
       deleteTitle: 'Elimina profilo predefinito',
       deleteMessage: 'Eliminare il profilo predefinito di {{product}}? Le bobine che hanno già i suoi profili li mantengono.',
       deleteFailed: 'Impossibile eliminare il profilo predefinito',
+      search: 'Cerca per prodotto, colore o profilo',
+      noMatch: 'Nessun profilo predefinito corrisponde',
+      presetsCount: 'Profili stampante: {{count}}',
+      kProfilesCount: 'Profili K: {{count}}',
+      applyAll: 'Applica tutti',
+      applyAllHelp: 'Completa ciò che manca alle bobine attive da tutti i profili predefiniti. Nulla di ciò che hanno già viene modificato.',
+      applyAllFailed: 'Impossibile applicare {{failed}} profili predefiniti; completato su {{updated}} bobine',
     },
     suppliers: {
       label: 'Fornitori',

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

@@ -5336,6 +5336,13 @@ export default {
       deleteTitle: 'プロファイルの既定値を削除',
       deleteMessage: '{{product}} のプロファイル既定値を削除しますか? 既にそのプロファイルを持つスプールはそのまま保持します。',
       deleteFailed: 'プロファイルの既定値を削除できませんでした',
+      search: '製品、色、プリセットで検索',
+      noMatch: '一致するプロファイル既定値はありません',
+      presetsCount: 'プリンタープリセット: {{count}}',
+      kProfilesCount: 'K プロファイル: {{count}}',
+      applyAll: 'すべて適用',
+      applyAllHelp: 'すべてのプロファイル既定値から、使用中のスプールに未設定の項目を入力します。既に設定済みの項目は変更しません。',
+      applyAllFailed: '{{failed}} 件のプロファイル既定値を適用できませんでした。{{updated}} 本のスプールに入力しました',
     },
     suppliers: {
       label: 'サプライヤー',

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

@@ -5121,6 +5121,13 @@ export default {
       deleteTitle: '프로필 기본값 삭제',
       deleteMessage: '{{product}}의 프로필 기본값을 삭제할까요? 이미 해당 프로필이 있는 스풀은 그대로 유지합니다.',
       deleteFailed: '프로필 기본값을 삭제하지 못했습니다',
+      search: '제품, 색상 또는 프리셋으로 검색',
+      noMatch: '일치하는 프로필 기본값이 없습니다',
+      presetsCount: '프린터 프리셋: {{count}}',
+      kProfilesCount: 'K 프로필: {{count}}',
+      applyAll: '모두 적용',
+      applyAllHelp: '모든 프로필 기본값으로 활성 스풀에 아직 없는 항목을 채웁니다. 이미 있는 항목은 변경하지 않습니다.',
+      applyAllFailed: '프로필 기본값 {{failed}}개를 적용하지 못했습니다. 스풀 {{updated}}개에 채웠습니다',
     },
     suppliers: {
       label: '공급업체',

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

@@ -5376,6 +5376,13 @@ export default {
       deleteTitle: 'Profielstandaard verwijderen',
       deleteMessage: 'Profielstandaard voor {{product}} verwijderen? Spoelen die de profielen al hebben, houden ze.',
       deleteFailed: 'Profielstandaard kon niet worden verwijderd',
+      search: 'Zoeken op product, kleur of profiel',
+      noMatch: 'Geen profielstandaard komt overeen',
+      presetsCount: 'Printerprofielen: {{count}}',
+      kProfilesCount: 'K-profielen: {{count}}',
+      applyAll: 'Alles toepassen',
+      applyAllHelp: 'Vul bij actieve spoelen aan wat nog ontbreekt, uit alle profielstandaarden. Wat ze al hebben blijft ongewijzigd.',
+      applyAllFailed: '{{failed}} profielstandaarden konden niet worden toegepast; aangevuld bij {{updated}} spoelen',
     },
     suppliers: {
       label: 'Leveranciers',

+ 7 - 0
frontend/src/i18n/locales/pl.ts

@@ -5378,6 +5378,13 @@ export default {
       deleteTitle: 'Usuń domyślne profile',
       deleteMessage: 'Usunąć domyślne profile dla {{product}}? Szpule, które już mają te profile, zachowają je.',
       deleteFailed: 'Nie udało się usunąć domyślnych profili',
+      search: 'Szukaj według produktu, koloru lub profilu',
+      noMatch: 'Brak pasujących domyślnych profili',
+      presetsCount: 'Profile drukarek: {{count}}',
+      kProfilesCount: 'Profile K: {{count}}',
+      applyAll: 'Zastosuj wszystkie',
+      applyAllHelp: 'Uzupełnia brakujące dane aktywnych szpul ze wszystkich domyślnych profili. To, co już mają, nie jest zmieniane.',
+      applyAllFailed: 'Nie udało się zastosować domyślnych profili: {{failed}}; uzupełnione szpule: {{updated}}',
     },
     suppliers: {
       label: 'Dostawcy',

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

@@ -5322,6 +5322,13 @@ export default {
       deleteTitle: 'Excluir perfil padrão',
       deleteMessage: 'Excluir o perfil padrão de {{product}}? Carretéis que já têm os perfis os mantêm.',
       deleteFailed: 'Não foi possível excluir o perfil padrão',
+      search: 'Buscar por produto, cor ou perfil',
+      noMatch: 'Nenhum padrão de perfil corresponde',
+      presetsCount: 'Perfis de impressora: {{count}}',
+      kProfilesCount: 'Perfis K: {{count}}',
+      applyAll: 'Aplicar todos',
+      applyAllHelp: 'Preenche o que falta nos carretéis ativos com todos os padrões de perfil. Nada do que eles já têm é alterado.',
+      applyAllFailed: 'Não foi possível aplicar {{failed}} padrões de perfil; preenchido em {{updated}} carretéis',
     },
     suppliers: {
       label: 'Fornecedores',

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

@@ -5112,6 +5112,13 @@ export default {
       deleteTitle: "Удалить профиль по умолчанию",
       deleteMessage: "Удалить профиль по умолчанию для {{product}}? Катушки, у которых уже есть его профили, сохранят их.",
       deleteFailed: "Не удалось удалить профиль по умолчанию",
+      search: "Поиск по продукту, цвету или профилю",
+      noMatch: "Нет подходящих профилей по умолчанию",
+      presetsCount: "Профили принтеров: {{count}}",
+      kProfilesCount: "K-профили: {{count}}",
+      applyAll: "Применить все",
+      applyAllHelp: "Дополняет активные катушки тем, чего у них ещё нет, из всех профилей по умолчанию. Уже заданное не меняется.",
+      applyAllFailed: "Не удалось применить профилей по умолчанию: {{failed}}; дополнено катушек: {{updated}}",
     },
     suppliers: {
       label: "Поставщики",

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

@@ -5407,6 +5407,13 @@ errors: {
       deleteTitle: 'Ta bort profilstandard',
       deleteMessage: 'Ta bort profilstandarden för {{product}}? Spolar som redan har dess profiler behåller dem.',
       deleteFailed: 'Kunde inte ta bort profilstandarden',
+      search: 'Sök på produkt, färg eller profil',
+      noMatch: 'Ingen profilstandard matchar',
+      presetsCount: 'Skrivarprofiler: {{count}}',
+      kProfilesCount: 'K-profiler: {{count}}',
+      applyAll: 'Tillämpa alla',
+      applyAllHelp: 'Fyller i det som aktiva spolar saknar, från alla profilstandarder. Inget de redan har ändras.',
+      applyAllFailed: '{{failed}} profilstandarder kunde inte tillämpas; ifyllt på {{updated}} spolar',
     },
     suppliers: {
       label: 'Leverantörer',

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

@@ -5323,6 +5323,13 @@ export default {
       deleteTitle: 'Profil varsayılanını sil',
       deleteMessage: '{{product}} için profil varsayılanı silinsin mi? Profillerine zaten sahip makaralar onları korur.',
       deleteFailed: 'Profil varsayılanı silinemedi',
+      search: 'Ürün, renk veya profile göre ara',
+      noMatch: 'Eşleşen profil varsayılanı yok',
+      presetsCount: 'Yazıcı profilleri: {{count}}',
+      kProfilesCount: 'K profilleri: {{count}}',
+      applyAll: 'Tümünü uygula',
+      applyAllHelp: 'Tüm profil varsayılanlarından etkin makaralarda eksik olanları doldurur. Zaten olanlar değiştirilmez.',
+      applyAllFailed: '{{failed}} profil varsayılanı uygulanamadı; {{updated}} makarada dolduruldu',
     },
     suppliers: {
       label: 'Tedarikçiler',

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

@@ -5371,6 +5371,13 @@ export default {
       deleteTitle: "Видалити профіль за замовчуванням",
       deleteMessage: "Видалити профіль за замовчуванням для {{product}}? Котушки, які вже мають його профілі, збережуть їх.",
       deleteFailed: "Не вдалося видалити профіль за замовчуванням",
+      search: "Пошук за продуктом, кольором або профілем",
+      noMatch: "Немає відповідних профілів за замовчуванням",
+      presetsCount: "Профілі принтерів: {{count}}",
+      kProfilesCount: "K-профілі: {{count}}",
+      applyAll: "Застосувати всі",
+      applyAllHelp: "Доповнює активні котушки тим, чого в них ще немає, з усіх профілів за замовчуванням. Уже задане не змінюється.",
+      applyAllFailed: "Не вдалося застосувати профілів за замовчуванням: {{failed}}; доповнено котушок: {{updated}}",
     },
     suppliers: {
       label: "Постачальники",

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

@@ -5322,6 +5322,13 @@ export default {
       deleteTitle: '删除配置文件默认值',
       deleteMessage: '删除 {{product}} 的配置文件默认值?已有其配置文件的料盘会保留它们。',
       deleteFailed: '无法删除配置文件默认值',
+      search: '按产品、颜色或预设搜索',
+      noMatch: '没有匹配的配置默认值',
+      presetsCount: '打印机预设:{{count}}',
+      kProfilesCount: 'K 配置:{{count}}',
+      applyAll: '全部应用',
+      applyAllHelp: '用所有配置默认值为使用中的料盘填入尚未设置的项。已有的内容不会更改。',
+      applyAllFailed: '{{failed}} 个配置默认值无法应用;已为 {{updated}} 个料盘填入',
     },
     suppliers: {
       label: '供应商',

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

@@ -5322,6 +5322,13 @@ export default {
       deleteTitle: '刪除設定檔預設值',
       deleteMessage: '刪除 {{product}} 的設定檔預設值?已有其設定檔的料盤會保留它們。',
       deleteFailed: '無法刪除設定檔預設值',
+      search: '依產品、顏色或預設搜尋',
+      noMatch: '沒有符合的設定檔預設值',
+      presetsCount: '印表機預設:{{count}}',
+      kProfilesCount: 'K 設定檔:{{count}}',
+      applyAll: '全部套用',
+      applyAllHelp: '以所有設定檔預設值為使用中的料盤填入尚未設定的項目。已有的內容不會變更。',
+      applyAllFailed: '{{failed}} 個設定檔預設值無法套用;已為 {{updated}} 個料盤填入',
     },
     suppliers: {
       label: '供應商',

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/ArchivesPage-Bgu-mmQF.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/CamWallPage-Bl4ZuHI8.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/CameraPage-IQKnDOPO.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/FileManagerPage-BXTBdJgD.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/InventoryPage-olfG5Ifl.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/Layout-D1aBDvSX.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/ModelSourcesPage-D0_nbWH9.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/PrintersPage-CTQY9esQ.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/SettingsPage-tsvqDoR0.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/SliceModal-D1lpDVod.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-gmOavjs5.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
static/assets/SpoolBuddySettingsPage-D3RYvOqg.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/de-abRfFq1c.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/es-RafEYlKI.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/fr-CzX6oxc5.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
static/assets/index-C7v2Gy3m.css


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/index-DuovVT2X.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 1
static/assets/index-PFRn_rWo.css


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/it-pwWRTwxx.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/ja-B48V9hcb.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/ko-C3sVX6WY.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/locationSensorDefaults-CNNhkqbY.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/nl-TJluoHO7.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/pl-OCLT5eNo.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/pt-BR-BjNYQCfH.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/ru-B0J8LbB0.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/spoolCost-VHWCiUPP.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/sv-Bqlxc2AD.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/tr-BKW5c47u.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/uk-2BbTIkg8.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/usePageFileDrop-CxD-1UXx.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/zh-CN-DlG9KG26.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
static/assets/zh-TW-B98JAdfW.js


+ 3 - 3
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-ILyxj2o7.js"></script>
+    <script type="module" crossorigin src="/assets/index-DuovVT2X.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-MyvT0tak.js">
     <link rel="modulepreload" crossorigin href="/assets/react-dom-hYJuSgam.js">
@@ -122,8 +122,8 @@
     <link rel="modulepreload" crossorigin href="/assets/useColorCatalogVersion-DkwJC5Zr.js">
     <link rel="modulepreload" crossorigin href="/assets/circle-minus-DO36lhby.js">
     <link rel="modulepreload" crossorigin href="/assets/ConnectionDiagnostic-B3aZLqUp.js">
-    <link rel="modulepreload" crossorigin href="/assets/Layout-DRZxXDRe.js">
-    <link rel="stylesheet" crossorigin href="/assets/index-PFRn_rWo.css">
+    <link rel="modulepreload" crossorigin href="/assets/Layout-D1aBDvSX.js">
+    <link rel="stylesheet" crossorigin href="/assets/index-C7v2Gy3m.css">
   </head>
   <body>
     <div id="root"></div>

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels