Ver código fonte

refactor(settings): Spool Catalog now identical in internal and Spoolman modes

  Drop the Spoolman-mode hijack that replaced the local spool tare catalog
  with an inline filament editor — two unrelated concepts that should never
  have shared a card. Spool Catalog now renders the same way in both modes;
  Spoolman users edit filament name and spool_weight in Spoolman's own UI.

  Also eliminates the GET /spoolman/inventory/filaments 400 probe that fired
  on the Filament settings page whenever Spoolman was disabled.

  - frontend/src/components/SpoolCatalogSettings.tsx rewritten (752 -> 444 lines)
  - frontend/src/components/SpoolWeightUpdateModal.tsx deleted (orphan)
  - SpoolCatalogSettings test file rewritten to match the simplified component
  - PATCH /spoolman/inventory/filaments/{id} backend route left in place
maziggy 1 semana atrás
pai
commit
74dcaf5142

Diferenças do arquivo suprimidas por serem muito extensas
+ 3 - 0
CHANGELOG.md


+ 12 - 417
frontend/src/__tests__/components/SpoolCatalogSettings.test.tsx

@@ -1,6 +1,5 @@
-import React from 'react';
 import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen, waitFor, fireEvent } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
 import { render } from '../utils';
 import { SpoolCatalogSettings } from '../../components/SpoolCatalogSettings';
 
@@ -20,17 +19,6 @@ vi.mock('../../api/client', () => ({
   api: {
     getSettings: vi.fn().mockResolvedValue({}),
     getSpoolCatalog: vi.fn().mockResolvedValue([]),
-    getSpoolmanInventoryFilaments: vi.fn().mockResolvedValue([]),
-    patchSpoolmanFilament: vi.fn().mockResolvedValue({
-      id: 1,
-      name: 'PLA Basic',
-      material: 'PLA',
-      color_hex: 'FF0000',
-      color_name: 'Red',
-      weight: 1000,
-      spool_weight: 196,
-      vendor: { id: 1, name: 'Bambu Lab' },
-    }),
   },
   ApiError: class ApiError extends Error {
     status: number;
@@ -41,87 +29,15 @@ vi.mock('../../api/client', () => ({
   },
 }));
 
-import { api, ApiError } from '../../api/client';
+import { api } from '../../api/client';
 
-const sampleFilament = {
-  id: 1,
-  name: 'PLA Basic',
-  material: 'PLA',
-  color_hex: 'FF0000',
-  color_name: 'Red',
-  weight: 1000,
-  spool_weight: 196,
-  vendor: { id: 1, name: 'Bambu Lab' },
-};
-
-describe('SpoolCatalogSettings — mode switching', () => {
+describe('SpoolCatalogSettings — local catalog UI', () => {
   beforeEach(() => {
     vi.clearAllMocks();
     vi.mocked(api.getSpoolCatalog).mockResolvedValue([]);
   });
 
-  it('hides Spoolman table and shows local CRUD buttons when Spoolman is disabled (400)', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockRejectedValue(
-      new ApiError('disabled', 400)
-    );
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      // Local mode: Add button visible
-      expect(screen.getByText('common.add')).toBeTruthy();
-    });
-
-    // Spoolman table columns must NOT appear
-    expect(screen.queryByText('settings.catalog.material')).toBeNull();
-    expect(screen.queryByText('settings.catalog.spoolWeight')).toBeNull();
-    // Spoolman catalog title must NOT appear
-    expect(screen.queryByText('settings.spoolmanFilamentCatalogTitle')).toBeNull();
-  });
-
-  it('shows Spoolman error row when Spoolman is unreachable (503)', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockRejectedValue(
-      new ApiError('unreachable', 503)
-    );
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByText('inventory.spoolmanCatalogLoadFailed')).toBeTruthy();
-    });
-
-    // Local CRUD buttons must NOT appear in Spoolman mode
-    expect(screen.queryByText('common.add')).toBeNull();
-  });
-
-  it('shows empty state when Spoolman returns an empty list', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByText('inventory.noSpoolmanFilaments')).toBeTruthy();
-    });
-
-    // Local CRUD buttons must NOT appear
-    expect(screen.queryByText('common.add')).toBeNull();
-  });
-
-  it('renders Spoolman filament rows with vendor and name combined', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByText(/Bambu Lab — PLA Basic/)).toBeTruthy();
-    });
-  });
-
-  it('(local mode) shows Export, Import, Reset, Add buttons when Spoolman disabled', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockRejectedValue(
-      new ApiError('disabled', 400)
-    );
-
+  it('shows local CRUD buttons regardless of Spoolman state', async () => {
     render(<SpoolCatalogSettings />);
 
     await waitFor(() => {
@@ -133,341 +49,20 @@ describe('SpoolCatalogSettings — mode switching', () => {
     expect(screen.getByText('common.reset')).toBeTruthy();
   });
 
-  it('(spoolman mode) hides Export, Import, Reset, Add buttons when Spoolman is enabled', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByText(/Bambu Lab — PLA Basic/)).toBeTruthy();
-    });
-
-    expect(screen.queryByText('common.add')).toBeNull();
-    expect(screen.queryByText('common.export')).toBeNull();
-    expect(screen.queryByText('common.import')).toBeNull();
-    expect(screen.queryByText('common.reset')).toBeNull();
-  });
-
-  it('(spoolman mode) renders correct column headers — Name, Material, Weight, Spool Weight', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
+  it('renders the local Spool Catalog header and column layout', async () => {
     render(<SpoolCatalogSettings />);
 
     await waitFor(() => {
-      expect(screen.getByText('common.name')).toBeTruthy();
+      expect(screen.getByText('settings.catalog.spoolCatalog')).toBeTruthy();
     });
 
-    expect(screen.getByText('settings.catalog.material')).toBeTruthy();
+    expect(screen.getByText('common.name')).toBeTruthy();
     expect(screen.getByText('settings.catalog.weight')).toBeTruthy();
-    expect(screen.getByText('settings.catalog.spoolWeight')).toBeTruthy();
-  });
-
-  it('(spoolman mode) renders all data fields for a filament row', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByText(/Bambu Lab — PLA Basic/)).toBeTruthy();
-    });
-
-    // Material column
-    expect(screen.getByText('PLA')).toBeTruthy();
-    // Filament weight
-    expect(screen.getByText('1000g')).toBeTruthy();
-    // Spool (empty) weight
-    expect(screen.getByText('196g')).toBeTruthy();
-  });
-
-  it('(spoolman mode) renders color swatch with correct background color', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([
-      { ...sampleFilament, color_hex: 'FF5500' },
-    ]);
+    expect(screen.getByText('settings.catalog.type')).toBeTruthy();
 
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByText(/Bambu Lab — PLA Basic/)).toBeTruthy();
-    });
-
-    const swatch = screen.getByLabelText('inventory.spoolmanFilamentColorSwatch');
-    const bg = (swatch as HTMLElement).style.backgroundColor;
-    // Accepts both hex-like and rgb() representations
-    expect(bg).toBeTruthy();
-    expect(bg).not.toBe('');
-  });
-
-  it('(spoolman mode) renders fallback color when color_hex is null', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([
-      { ...sampleFilament, color_hex: null },
-    ]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByText(/Bambu Lab — PLA Basic/)).toBeTruthy();
-    });
-
-    const swatch = screen.getByLabelText('inventory.spoolmanFilamentColorSwatch');
-    expect((swatch as HTMLElement).style.backgroundColor).toContain('128');
-  });
-
-  it('(spoolman mode) renders dash for null material, weight, and spool_weight', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([
-      { ...sampleFilament, material: null, weight: null, spool_weight: null },
-    ]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByText(/Bambu Lab — PLA Basic/)).toBeTruthy();
-    });
-
-    // All three nullable fields must show '—', not 'nullg' or empty string
-    const dashes = screen.getAllByText('—');
-    expect(dashes.length).toBeGreaterThanOrEqual(3);
-  });
-
-  it('(spoolman mode) shows Spoolman catalog title, not local catalog title', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByText('settings.spoolmanFilamentCatalogTitle')).toBeTruthy();
-    });
-
-    expect(screen.queryByText('settings.catalog.spoolCatalog')).toBeNull();
-  });
-
-  it('(spoolman mode) shows pencil edit button in each filament row', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByText(/Bambu Lab — PLA Basic/)).toBeTruthy();
-    });
-
-    const editButtons = screen.getAllByLabelText('common.edit');
-    expect(editButtons.length).toBeGreaterThanOrEqual(1);
-  });
-
-  it('(spoolman mode) clicking pencil shows name and weight inputs', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('common.edit')).toBeTruthy();
-    });
-
-    fireEvent.click(screen.getByLabelText('common.edit'));
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('common.name')).toBeTruthy();
-      expect(screen.getByLabelText('settings.catalog.spoolWeight')).toBeTruthy();
-    });
-  });
-
-  it('(spoolman mode) name input is pre-filled with current name', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('common.edit')).toBeTruthy();
-    });
-
-    fireEvent.click(screen.getByLabelText('common.edit'));
-
-    await waitFor(() => {
-      const nameInput = screen.getByLabelText('common.name') as HTMLInputElement;
-      expect(nameInput.value).toBe('PLA Basic');
-    });
-  });
-
-  it('(spoolman mode) weight input is pre-filled with current spool_weight', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('common.edit')).toBeTruthy();
-    });
-
-    fireEvent.click(screen.getByLabelText('common.edit'));
-
-    await waitFor(() => {
-      const weightInput = screen.getByLabelText('settings.catalog.spoolWeight') as HTMLInputElement;
-      expect(weightInput.value).toBe('196');
-    });
-  });
-
-  it('(spoolman mode) cancel edit restores read-only display', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('common.edit')).toBeTruthy();
-    });
-
-    fireEvent.click(screen.getByLabelText('common.edit'));
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('common.cancel')).toBeTruthy();
-    });
-
-    fireEvent.click(screen.getByLabelText('common.cancel'));
-
-    await waitFor(() => {
-      expect(screen.queryByLabelText('common.name')).toBeNull();
-      expect(screen.getByLabelText('common.edit')).toBeTruthy();
-    });
-  });
-
-  it('(spoolman mode) empty name input disables save button', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('common.edit')).toBeTruthy();
-    });
-
-    fireEvent.click(screen.getByLabelText('common.edit'));
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('common.name')).toBeTruthy();
-    });
-
-    const nameInput = screen.getByLabelText('common.name');
-    fireEvent.change(nameInput, { target: { value: '' } });
-
-    const saveBtn = screen.getByLabelText('common.save') as HTMLButtonElement;
-    expect(saveBtn.disabled).toBe(true);
-  });
-
-  it('(spoolman mode) saving name-only calls patchSpoolmanFilament without modal', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('common.edit')).toBeTruthy();
-    });
-
-    fireEvent.click(screen.getByLabelText('common.edit'));
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('common.name')).toBeTruthy();
-    });
-
-    const nameInput = screen.getByLabelText('common.name');
-    fireEvent.change(nameInput, { target: { value: 'PLA Basic Renamed' } });
-
-    fireEvent.click(screen.getByLabelText('common.save'));
-
-    await waitFor(() => {
-      expect(vi.mocked(api.patchSpoolmanFilament)).toHaveBeenCalledWith(1, { name: 'PLA Basic Renamed' });
-    });
-
-    // Modal must NOT appear
-    expect(screen.queryByText('settings.catalog.updateSpoolWeight')).toBeNull();
-  });
-
-  it('(spoolman mode) saving changed spool_weight opens SpoolWeightUpdateModal', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('common.edit')).toBeTruthy();
-    });
-
-    fireEvent.click(screen.getByLabelText('common.edit'));
-
-    await waitFor(() => {
-      expect(screen.getByLabelText('settings.catalog.spoolWeight')).toBeTruthy();
-    });
-
-    const weightInput = screen.getByLabelText('settings.catalog.spoolWeight');
-    fireEvent.change(weightInput, { target: { value: '100' } });
-
-    fireEvent.click(screen.getByLabelText('common.save'));
-
-    await waitFor(() => {
-      expect(screen.getByText('settings.catalog.updateSpoolWeight')).toBeTruthy();
-    });
-  });
-
-  it('(spoolman mode) confirming option B calls patchSpoolmanFilament with keep_existing_spools=false', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => { expect(screen.getByLabelText('common.edit')).toBeTruthy(); });
-    fireEvent.click(screen.getByLabelText('common.edit'));
-
-    await waitFor(() => { expect(screen.getByLabelText('settings.catalog.spoolWeight')).toBeTruthy(); });
-    fireEvent.change(screen.getByLabelText('settings.catalog.spoolWeight'), { target: { value: '100' } });
-    fireEvent.click(screen.getByLabelText('common.save'));
-
-    await waitFor(() => { expect(screen.getByText('settings.catalog.updateSpoolWeight')).toBeTruthy(); });
-
-    // Confirm with option B selected by default
-    fireEvent.click(screen.getByText('common.confirm'));
-
-    await waitFor(() => {
-      expect(vi.mocked(api.patchSpoolmanFilament)).toHaveBeenCalledWith(
-        1,
-        expect.objectContaining({ spool_weight: 100, keep_existing_spools: false }),
-      );
-    });
-  });
-
-  it('(spoolman mode) confirming option A calls patchSpoolmanFilament with keep_existing_spools=true', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => { expect(screen.getByLabelText('common.edit')).toBeTruthy(); });
-    fireEvent.click(screen.getByLabelText('common.edit'));
-
-    await waitFor(() => { expect(screen.getByLabelText('settings.catalog.spoolWeight')).toBeTruthy(); });
-    fireEvent.change(screen.getByLabelText('settings.catalog.spoolWeight'), { target: { value: '100' } });
-    fireEvent.click(screen.getByLabelText('common.save'));
-
-    await waitFor(() => { expect(screen.getByText('settings.catalog.updateSpoolWeight')).toBeTruthy(); });
-
-    // Select option A (keep existing)
-    const radios = screen.getAllByRole('radio');
-    fireEvent.click(radios[1]); // Option A = second radio = keepExisting=true
-
-    fireEvent.click(screen.getByText('common.confirm'));
-
-    await waitFor(() => {
-      expect(vi.mocked(api.patchSpoolmanFilament)).toHaveBeenCalledWith(
-        1,
-        expect.objectContaining({ spool_weight: 100, keep_existing_spools: true }),
-      );
-    });
-  });
-
-  it('(spoolman mode) negative weight input disables save button', async () => {
-    vi.mocked(api.getSpoolmanInventoryFilaments).mockResolvedValue([sampleFilament]);
-
-    render(<SpoolCatalogSettings />);
-
-    await waitFor(() => { expect(screen.getByLabelText('common.edit')).toBeTruthy(); });
-    fireEvent.click(screen.getByLabelText('common.edit'));
-
-    await waitFor(() => { expect(screen.getByLabelText('settings.catalog.spoolWeight')).toBeTruthy(); });
-
-    fireEvent.change(screen.getByLabelText('settings.catalog.spoolWeight'), { target: { value: '-5' } });
-
-    const saveBtn = screen.getByLabelText('common.save') as HTMLButtonElement;
-    expect(saveBtn.disabled).toBe(true);
+    // No Spoolman-only columns leak in
+    expect(screen.queryByText('settings.catalog.material')).toBeNull();
+    expect(screen.queryByText('settings.catalog.spoolWeight')).toBeNull();
+    expect(screen.queryByText('settings.spoolmanFilamentCatalogTitle')).toBeNull();
   });
 });

+ 0 - 97
frontend/src/__tests__/components/SpoolWeightUpdateModal.test.tsx

@@ -1,97 +0,0 @@
-import React from 'react';
-import { describe, it, expect, vi } from 'vitest';
-import { screen, fireEvent } from '@testing-library/react';
-import { render } from '../utils';
-import { SpoolWeightUpdateModal } from '../../components/SpoolWeightUpdateModal';
-
-vi.mock('react-i18next', () => ({
-  useTranslation: () => ({
-    t: (key: string) => key,
-  }),
-}));
-
-const defaultProps = {
-  isOpen: true,
-  filamentName: 'PLA Basic',
-  oldWeight: 250,
-  newWeight: 196,
-  onConfirm: vi.fn(),
-  onClose: vi.fn(),
-};
-
-describe('SpoolWeightUpdateModal', () => {
-  it('renders filament name, old weight, and new weight', () => {
-    render(<SpoolWeightUpdateModal {...defaultProps} />);
-
-    expect(screen.getByText('settings.catalog.updateSpoolWeight')).toBeTruthy();
-    expect(screen.getByText(/PLA Basic/)).toBeTruthy();
-    expect(screen.getByText(/250g → 196g/)).toBeTruthy();
-  });
-
-  it('renders option labels', () => {
-    render(<SpoolWeightUpdateModal {...defaultProps} />);
-
-    expect(screen.getByText('settings.catalog.applyToAllSpools')).toBeTruthy();
-    expect(screen.getByText('settings.catalog.keepExistingSpoolWeight')).toBeTruthy();
-  });
-
-  it('option B (apply to all) is selected by default', () => {
-    render(<SpoolWeightUpdateModal {...defaultProps} />);
-
-    const radios = screen.getAllByRole('radio') as HTMLInputElement[];
-    // First radio = apply-to-all (Option B, keepExisting=false)
-    expect(radios[0].checked).toBe(true);
-    expect(radios[1].checked).toBe(false);
-  });
-
-  it('calls onConfirm(false) when option B is selected and Confirm clicked', () => {
-    const onConfirm = vi.fn();
-    render(<SpoolWeightUpdateModal {...defaultProps} onConfirm={onConfirm} />);
-
-    fireEvent.click(screen.getByText('common.confirm'));
-
-    expect(onConfirm).toHaveBeenCalledWith(false);
-  });
-
-  it('calls onConfirm(true) when option A is selected and Confirm clicked', () => {
-    const onConfirm = vi.fn();
-    render(<SpoolWeightUpdateModal {...defaultProps} onConfirm={onConfirm} />);
-
-    const radios = screen.getAllByRole('radio');
-    fireEvent.click(radios[1]); // Option A: keep existing
-
-    fireEvent.click(screen.getByText('common.confirm'));
-
-    expect(onConfirm).toHaveBeenCalledWith(true);
-  });
-
-  it('calls onClose on Cancel click', () => {
-    const onClose = vi.fn();
-    render(<SpoolWeightUpdateModal {...defaultProps} onClose={onClose} />);
-
-    fireEvent.click(screen.getByText('common.cancel'));
-
-    expect(onClose).toHaveBeenCalled();
-  });
-
-  it('does not call onConfirm on Cancel click', () => {
-    const onConfirm = vi.fn();
-    render(<SpoolWeightUpdateModal {...defaultProps} onConfirm={onConfirm} />);
-
-    fireEvent.click(screen.getByText('common.cancel'));
-
-    expect(onConfirm).not.toHaveBeenCalled();
-  });
-
-  it('renders dash when oldWeight is null', () => {
-    render(<SpoolWeightUpdateModal {...defaultProps} oldWeight={null} />);
-
-    expect(screen.getByText(/— → 196g/)).toBeTruthy();
-  });
-
-  it('returns null when isOpen is false', () => {
-    render(<SpoolWeightUpdateModal {...defaultProps} isOpen={false} />);
-
-    expect(screen.queryByText('settings.catalog.updateSpoolWeight')).toBeNull();
-  });
-});

+ 185 - 433
frontend/src/components/SpoolCatalogSettings.tsx

@@ -1,34 +1,20 @@
 import { useState, useEffect, useCallback, useRef } from 'react';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import { Database, Plus, Trash2, RotateCcw, Loader2, Pencil, Check, X, Search, Download, Upload } from 'lucide-react';
-import { api, ApiError } from '../api/client';
-import type { SpoolCatalogEntry, SpoolmanFilamentEntry } from '../api/client';
+import { api } from '../api/client';
+import type { SpoolCatalogEntry } from '../api/client';
 import { useToast } from '../contexts/ToastContext';
 import { Card, CardHeader, CardContent } from './Card';
 import { ConfirmModal } from './ConfirmModal';
-import { SpoolWeightUpdateModal } from './SpoolWeightUpdateModal';
 
 export function SpoolCatalogSettings() {
   const { t } = useTranslation();
   const { showToast } = useToast();
-  const queryClient = useQueryClient();
   const [catalog, setCatalog] = useState<SpoolCatalogEntry[]>([]);
   const [loading, setLoading] = useState(true);
   const [search, setSearch] = useState('');
   const fileInputRef = useRef<HTMLInputElement>(null);
 
-  // Spoolman inline-edit state
-  const [editingFilamentId, setEditingFilamentId] = useState<number | null>(null);
-  const [editingFilamentName, setEditingFilamentName] = useState('');
-  const [editingFilamentWeight, setEditingFilamentWeight] = useState('');
-  const [pendingWeightEdit, setPendingWeightEdit] = useState<{
-    filamentId: number;
-    name: string;
-    oldWeight: number | null;
-    newWeight: number;
-  } | null>(null);
-
   // Add/Edit form state
   const [showAddForm, setShowAddForm] = useState(false);
   const [editingId, setEditingId] = useState<number | null>(null);
@@ -44,86 +30,6 @@ export function SpoolCatalogSettings() {
   const [deleteEntry, setDeleteEntry] = useState<SpoolCatalogEntry | null>(null);
   const [showResetConfirm, setShowResetConfirm] = useState(false);
 
-  // Spoolman filament query — hoisted to determine display mode
-  const {
-    data: spoolmanFilaments,
-    isLoading: spoolmanLoading,
-    error: spoolmanError,
-  } = useQuery<SpoolmanFilamentEntry[], Error>({
-    queryKey: ['spoolman-inventory-filaments'],
-    queryFn: () => api.getSpoolmanInventoryFilaments(),
-    retry: false, // Spoolman may be intentionally disabled (400) — don't retry
-    staleTime: 60_000,
-  });
-
-  // 400 = Spoolman explicitly disabled; all other states (data / 503 / …) mean Spoolman mode
-  const isSpoolmanDisabled =
-    !spoolmanLoading &&
-    spoolmanError instanceof ApiError &&
-    spoolmanError.status === 400;
-  const isSpoolmanMode = !spoolmanLoading && !isSpoolmanDisabled;
-
-  const patchFilamentMutation = useMutation<
-    SpoolmanFilamentEntry,
-    Error,
-    { filamentId: number; data: { name?: string; spool_weight?: number | null; keep_existing_spools?: boolean } }
-  >({
-    mutationFn: ({ filamentId, data }) => api.patchSpoolmanFilament(filamentId, data),
-    onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ['spoolman-inventory-filaments'] });
-      showToast(t('settings.catalog.filamentUpdated'), 'success');
-      setEditingFilamentId(null);
-      setEditingFilamentName('');
-      setEditingFilamentWeight('');
-      setPendingWeightEdit(null);
-    },
-    onError: (error: Error) => {
-      if (error instanceof ApiError && error.status === 503) {
-        showToast(t('inventory.spoolmanUnreachable'), 'error');
-      } else if (error instanceof ApiError && error.status === 422) {
-        showToast(t('settings.catalog.filamentUpdateInvalid'), 'error');
-      } else {
-        showToast(t('settings.catalog.filamentUpdateFailed'), 'error');
-      }
-    },
-  });
-
-  const handleFilamentSave = (f: SpoolmanFilamentEntry) => {
-    const nameChanged = editingFilamentName.trim() !== f.name;
-    const weightChanged = editingFilamentWeight !== '' && editingFilamentWeight !== String(f.spool_weight ?? '');
-    const parsedWeight = editingFilamentWeight !== '' ? parseFloat(editingFilamentWeight) : null;
-
-    if (weightChanged && parsedWeight !== null && !isNaN(parsedWeight) && parsedWeight > 0) {
-      setPendingWeightEdit({
-        filamentId: f.id,
-        name: nameChanged ? editingFilamentName.trim() : f.name,
-        oldWeight: f.spool_weight,
-        newWeight: parsedWeight,
-      });
-    } else {
-      const data: { name?: string } = {};
-      if (nameChanged && editingFilamentName.trim()) data.name = editingFilamentName.trim();
-      if (Object.keys(data).length > 0) {
-        patchFilamentMutation.mutate({ filamentId: f.id, data });
-      } else {
-        setEditingFilamentId(null);
-      }
-    }
-  };
-
-  const handleWeightModalConfirm = (keepExisting: boolean) => {
-    if (!pendingWeightEdit) return;
-    const { filamentId, name, newWeight } = pendingWeightEdit;
-    const currentFilament = spoolmanFilaments?.find(f => f.id === filamentId);
-    const nameChanged = currentFilament && name !== currentFilament.name;
-    const data: { name?: string; spool_weight: number; keep_existing_spools: boolean } = {
-      spool_weight: newWeight,
-      keep_existing_spools: keepExisting,
-    };
-    if (nameChanged) data.name = name;
-    patchFilamentMutation.mutate({ filamentId, data });
-  };
-
   const loadCatalog = useCallback(async () => {
     try {
       const entries = await api.getSpoolCatalog();
@@ -144,11 +50,6 @@ export function SpoolCatalogSettings() {
     entry.name.toLowerCase().includes(search.toLowerCase())
   );
 
-  const filteredSpoolmanFilaments = (spoolmanFilaments ?? []).filter(f =>
-    f.name.toLowerCase().includes(search.toLowerCase()) ||
-    (f.vendor?.name ?? '').toLowerCase().includes(search.toLowerCase())
-  );
-
   const handleAdd = async () => {
     if (!formName.trim() || !formWeight) {
       showToast(t('settings.catalog.nameWeightRequired'), 'error');
@@ -313,55 +214,47 @@ export function SpoolCatalogSettings() {
         <div className="flex items-center gap-2 mb-3">
           <Database className="w-5 h-5 text-bambu-gray" />
           <h2 className="text-lg font-semibold text-white">
-            {isSpoolmanMode
-              ? t('settings.spoolmanFilamentCatalogTitle')
-              : t('settings.catalog.spoolCatalog')}
+            {t('settings.catalog.spoolCatalog')}
           </h2>
-          <span className="text-sm text-bambu-gray">
-            ({spoolmanLoading ? '…' : isSpoolmanMode ? (spoolmanFilaments?.length ?? 0) : catalog.length})
-          </span>
+          <span className="text-sm text-bambu-gray">({catalog.length})</span>
         </div>
 
-        {/* CRUD buttons — local mode only */}
-        {!isSpoolmanMode && !spoolmanLoading && (
-          <div className="flex items-center gap-2 flex-wrap">
-            <button
-              onClick={handleExport}
-              className="px-3 py-1.5 text-sm bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-bambu-gray hover:text-white transition-colors flex items-center gap-1.5"
-              title={t('settings.catalog.exportTooltip')}
-            >
-              <Download className="w-4 h-4" />
-              <span className="hidden sm:inline">{t('common.export')}</span>
-            </button>
-            <button
-              onClick={() => fileInputRef.current?.click()}
-              className="px-3 py-1.5 text-sm bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-bambu-gray hover:text-white transition-colors flex items-center gap-1.5"
-              title={t('settings.catalog.importTooltip')}
-            >
-              <Upload className="w-4 h-4" />
-              <span className="hidden sm:inline">{t('common.import')}</span>
-            </button>
-            <input ref={fileInputRef} type="file" accept=".json" className="hidden" onChange={handleImport} />
-            <button
-              onClick={() => setShowResetConfirm(true)}
-              className="px-3 py-1.5 text-sm bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-bambu-gray hover:text-white transition-colors flex items-center gap-1.5"
-              title={t('settings.catalog.resetTooltip')}
-            >
-              <RotateCcw className="w-4 h-4" />
-              <span className="hidden sm:inline">{t('common.reset')}</span>
-            </button>
-            <button
-              onClick={() => setShowAddForm(true)}
-              className="px-3 py-1.5 text-sm bg-bambu-green text-white rounded-lg hover:bg-bambu-green/80 transition-colors flex items-center gap-1.5"
-            >
-              <Plus className="w-4 h-4" />
-              <span className="hidden sm:inline">{t('common.add')}</span>
-            </button>
-          </div>
-        )}
+        <div className="flex items-center gap-2 flex-wrap">
+          <button
+            onClick={handleExport}
+            className="px-3 py-1.5 text-sm bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-bambu-gray hover:text-white transition-colors flex items-center gap-1.5"
+            title={t('settings.catalog.exportTooltip')}
+          >
+            <Download className="w-4 h-4" />
+            <span className="hidden sm:inline">{t('common.export')}</span>
+          </button>
+          <button
+            onClick={() => fileInputRef.current?.click()}
+            className="px-3 py-1.5 text-sm bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-bambu-gray hover:text-white transition-colors flex items-center gap-1.5"
+            title={t('settings.catalog.importTooltip')}
+          >
+            <Upload className="w-4 h-4" />
+            <span className="hidden sm:inline">{t('common.import')}</span>
+          </button>
+          <input ref={fileInputRef} type="file" accept=".json" className="hidden" onChange={handleImport} />
+          <button
+            onClick={() => setShowResetConfirm(true)}
+            className="px-3 py-1.5 text-sm bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-bambu-gray hover:text-white transition-colors flex items-center gap-1.5"
+            title={t('settings.catalog.resetTooltip')}
+          >
+            <RotateCcw className="w-4 h-4" />
+            <span className="hidden sm:inline">{t('common.reset')}</span>
+          </button>
+          <button
+            onClick={() => setShowAddForm(true)}
+            className="px-3 py-1.5 text-sm bg-bambu-green text-white rounded-lg hover:bg-bambu-green/80 transition-colors flex items-center gap-1.5"
+          >
+            <Plus className="w-4 h-4" />
+            <span className="hidden sm:inline">{t('common.add')}</span>
+          </button>
+        </div>
 
-        {/* Bulk-delete bar — local mode only */}
-        {!isSpoolmanMode && selectedIds.size > 0 && (
+        {selectedIds.size > 0 && (
           <div className="flex items-center gap-2 mt-2 px-3 py-2 bg-red-500/10 border border-red-500/30 rounded-lg">
             <span className="text-sm text-red-400">
               {t('settings.catalog.selectedCount', { count: selectedIds.size })}
@@ -384,14 +277,10 @@ export function SpoolCatalogSettings() {
       </CardHeader>
 
       <CardContent className="space-y-4">
-        {/* Description */}
         <p className="text-sm text-bambu-gray">
-          {isSpoolmanMode
-            ? t('settings.spoolmanFilamentCatalogDesc')
-            : t('settings.catalog.spoolCatalogDescription')}
+          {t('settings.catalog.spoolCatalogDescription')}
         </p>
 
-        {/* Search — always shown */}
         <div className="relative">
           <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray" />
           <input
@@ -403,302 +292,174 @@ export function SpoolCatalogSettings() {
           />
         </div>
 
-        {/* Mode-determination loading spinner */}
-        {spoolmanLoading && (
+        {showAddForm && (
+          <div className="p-4 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
+            <h3 className="text-sm font-medium text-white mb-3">{t('settings.catalog.addNewEntry')}</h3>
+            <div className="flex gap-2 items-center">
+              <div className="flex-1 min-w-0">
+                <input
+                  type="text"
+                  className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
+                  placeholder={t('settings.catalog.namePlaceholder')}
+                  value={formName}
+                  onChange={(e) => setFormName(e.target.value)}
+                />
+              </div>
+              <input
+                type="number"
+                className="w-20 px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white text-center focus:border-bambu-green focus:outline-none"
+                placeholder="g"
+                value={formWeight}
+                onChange={(e) => setFormWeight(e.target.value)}
+              />
+              <span className="text-bambu-gray shrink-0">g</span>
+              <button
+                onClick={handleAdd}
+                disabled={saving}
+                className="px-3 py-2 bg-bambu-green text-white rounded-lg hover:bg-bambu-green/80 flex items-center gap-1 shrink-0"
+              >
+                {saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
+                {t('common.add')}
+              </button>
+              <button
+                onClick={() => { setShowAddForm(false); setFormName(''); setFormWeight(''); }}
+                className="p-2 rounded-lg text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary"
+              >
+                <X className="w-4 h-4" />
+              </button>
+            </div>
+          </div>
+        )}
+
+        {loading ? (
           <div className="flex items-center justify-center py-8 text-bambu-gray">
             <Loader2 className="w-5 h-5 animate-spin mr-2" />
             {t('common.loading')}
           </div>
-        )}
-
-        {/* ── LOCAL MODE ── */}
-        {!spoolmanLoading && !isSpoolmanMode && (
-          <>
-            {/* Add form */}
-            {showAddForm && (
-              <div className="p-4 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
-                <h3 className="text-sm font-medium text-white mb-3">{t('settings.catalog.addNewEntry')}</h3>
-                <div className="flex gap-2 items-center">
-                  <div className="flex-1 min-w-0">
+        ) : (
+          <div className="max-h-[600px] overflow-y-auto border border-bambu-dark-tertiary rounded-lg">
+            <table className="w-full text-sm">
+              <thead className="bg-bambu-dark sticky top-0">
+                <tr>
+                  <th className="px-2 py-2 w-10">
                     <input
-                      type="text"
-                      className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
-                      placeholder={t('settings.catalog.namePlaceholder')}
-                      value={formName}
-                      onChange={(e) => setFormName(e.target.value)}
+                      type="checkbox"
+                      checked={filteredCatalog.length > 0 && selectedIds.size === filteredCatalog.length}
+                      onChange={toggleSelectAll}
+                      className="w-4 h-4 accent-bambu-green cursor-pointer"
                     />
-                  </div>
-                  <input
-                    type="number"
-                    className="w-20 px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white text-center focus:border-bambu-green focus:outline-none"
-                    placeholder="g"
-                    value={formWeight}
-                    onChange={(e) => setFormWeight(e.target.value)}
-                  />
-                  <span className="text-bambu-gray shrink-0">g</span>
-                  <button
-                    onClick={handleAdd}
-                    disabled={saving}
-                    className="px-3 py-2 bg-bambu-green text-white rounded-lg hover:bg-bambu-green/80 flex items-center gap-1 shrink-0"
-                  >
-                    {saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
-                    {t('common.add')}
-                  </button>
-                  <button
-                    onClick={() => { setShowAddForm(false); setFormName(''); setFormWeight(''); }}
-                    className="p-2 rounded-lg text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary"
-                  >
-                    <X className="w-4 h-4" />
-                  </button>
-                </div>
-              </div>
-            )}
-
-            {/* Local catalog table */}
-            {loading ? (
-              <div className="flex items-center justify-center py-8 text-bambu-gray">
-                <Loader2 className="w-5 h-5 animate-spin mr-2" />
-                {t('common.loading')}
-              </div>
-            ) : (
-              <div className="max-h-[600px] overflow-y-auto border border-bambu-dark-tertiary rounded-lg">
-                <table className="w-full text-sm">
-                  <thead className="bg-bambu-dark sticky top-0">
-                    <tr>
-                      <th className="px-2 py-2 w-10">
-                        <input
-                          type="checkbox"
-                          checked={filteredCatalog.length > 0 && selectedIds.size === filteredCatalog.length}
-                          onChange={toggleSelectAll}
-                          className="w-4 h-4 accent-bambu-green cursor-pointer"
-                        />
-                      </th>
-                      <th className="px-4 py-2 text-left text-bambu-gray font-medium">{t('common.name')}</th>
-                      <th className="px-4 py-2 text-right text-bambu-gray font-medium w-24">{t('settings.catalog.weight')}</th>
-                      <th className="px-4 py-2 text-center text-bambu-gray font-medium w-20">{t('settings.catalog.type')}</th>
-                      <th className="px-4 py-2 w-24"></th>
-                    </tr>
-                  </thead>
-                  <tbody>
-                    {filteredCatalog.length === 0 ? (
-                      <tr>
-                        <td colSpan={5} className="px-4 py-8 text-center text-bambu-gray">
-                          {search ? t('settings.catalog.noMatch') : t('settings.catalog.empty')}
-                        </td>
-                      </tr>
-                    ) : (
-                      filteredCatalog.map(entry => (
-                        <tr key={entry.id} className={`border-t border-bambu-dark-tertiary hover:bg-bambu-dark ${selectedIds.has(entry.id) ? 'bg-bambu-dark' : ''}`}>
-                          {editingId === entry.id ? (
-                            <>
-                              <td className="px-2 py-2">
-                                <input
-                                  type="checkbox"
-                                  checked={selectedIds.has(entry.id)}
-                                  onChange={() => toggleSelect(entry.id)}
-                                  className="w-4 h-4 accent-bambu-green cursor-pointer"
-                                />
-                              </td>
-                              <td className="px-4 py-2">
-                                <input
-                                  type="text"
-                                  className="w-full px-2 py-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-white focus:border-bambu-green focus:outline-none"
-                                  value={formName}
-                                  onChange={(e) => setFormName(e.target.value)}
-                                />
-                              </td>
-                              <td className="px-4 py-2">
-                                <input
-                                  type="number"
-                                  className="w-full px-2 py-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-white text-right focus:border-bambu-green focus:outline-none"
-                                  value={formWeight}
-                                  onChange={(e) => setFormWeight(e.target.value)}
-                                />
-                              </td>
-                              <td className="px-4 py-2 text-center">
-                                <span className="text-xs text-bambu-gray">-</span>
-                              </td>
-                              <td className="px-4 py-2">
-                                <div className="flex justify-end gap-1">
-                                  <button
-                                    onClick={() => handleUpdate(entry.id)}
-                                    disabled={saving}
-                                    className="p-1.5 rounded hover:bg-green-500/20 text-green-500"
-                                  >
-                                    {saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
-                                  </button>
-                                  <button onClick={cancelEdit} className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray">
-                                    <X className="w-4 h-4" />
-                                  </button>
-                                </div>
-                              </td>
-                            </>
-                          ) : (
-                            <>
-                              <td className="px-2 py-2">
-                                <input
-                                  type="checkbox"
-                                  checked={selectedIds.has(entry.id)}
-                                  onChange={() => toggleSelect(entry.id)}
-                                  className="w-4 h-4 accent-bambu-green cursor-pointer"
-                                />
-                              </td>
-                              <td className="px-4 py-2 text-white">{entry.name}</td>
-                              <td className="px-4 py-2 text-right font-mono text-white">{entry.weight}g</td>
-                              <td className="px-4 py-2 text-center">
-                                {entry.is_default ? (
-                                  <span className="text-xs px-2 py-0.5 rounded bg-bambu-dark-tertiary text-bambu-gray">
-                                    {t('settings.catalog.default')}
-                                  </span>
-                                ) : (
-                                  <span className="text-xs px-2 py-0.5 rounded bg-bambu-green/20 text-bambu-green">
-                                    {t('settings.catalog.custom')}
-                                  </span>
-                                )}
-                              </td>
-                              <td className="px-4 py-2">
-                                <div className="flex justify-end gap-1">
-                                  <button
-                                    onClick={() => startEdit(entry)}
-                                    className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white"
-                                  >
-                                    <Pencil className="w-4 h-4" />
-                                  </button>
-                                  <button
-                                    onClick={() => setDeleteEntry(entry)}
-                                    className="p-1.5 rounded bg-red-500/10 hover:bg-red-500/20 text-red-500"
-                                  >
-                                    <Trash2 className="w-4 h-4" />
-                                  </button>
-                                </div>
-                              </td>
-                            </>
-                          )}
-                        </tr>
-                      ))
-                    )}
-                  </tbody>
-                </table>
-              </div>
-            )}
-          </>
-        )}
-
-        {/* ── SPOOLMAN MODE ── */}
-        {!spoolmanLoading && isSpoolmanMode && (
-          <div className="max-h-[600px] overflow-y-auto border border-bambu-dark-tertiary rounded-lg">
-            {spoolmanError ? (
-              <p className="px-4 py-8 text-center text-sm text-red-400">
-                {t('inventory.spoolmanCatalogLoadFailed')}
-              </p>
-            ) : filteredSpoolmanFilaments.length === 0 ? (
-              <p className="px-4 py-8 text-center text-sm text-bambu-gray">
-                {t('inventory.noSpoolmanFilaments')}
-              </p>
-            ) : (
-              <table className="w-full text-sm">
-                <thead className="bg-bambu-dark sticky top-0">
+                  </th>
+                  <th className="px-4 py-2 text-left text-bambu-gray font-medium">{t('common.name')}</th>
+                  <th className="px-4 py-2 text-right text-bambu-gray font-medium w-24">{t('settings.catalog.weight')}</th>
+                  <th className="px-4 py-2 text-center text-bambu-gray font-medium w-20">{t('settings.catalog.type')}</th>
+                  <th className="px-4 py-2 w-24"></th>
+                </tr>
+              </thead>
+              <tbody>
+                {filteredCatalog.length === 0 ? (
                   <tr>
-                    <th className="px-3 py-2 w-8"></th>
-                    <th className="px-4 py-2 text-left text-bambu-gray font-medium">{t('common.name')}</th>
-                    <th className="px-4 py-2 text-left text-bambu-gray font-medium w-28">{t('settings.catalog.material')}</th>
-                    <th className="px-4 py-2 text-right text-bambu-gray font-medium w-24">{t('settings.catalog.weight')}</th>
-                    <th className="px-4 py-2 text-right text-bambu-gray font-medium w-28">{t('settings.catalog.spoolWeight')}</th>
-                    <th className="px-3 py-2 w-20"></th>
+                    <td colSpan={5} className="px-4 py-8 text-center text-bambu-gray">
+                      {search ? t('settings.catalog.noMatch') : t('settings.catalog.empty')}
+                    </td>
                   </tr>
-                </thead>
-                <tbody>
-                  {filteredSpoolmanFilaments.map(f => {
-                    const isEditing = editingFilamentId === f.id;
-                    const isSaving = patchFilamentMutation.isPending && editingFilamentId === f.id;
-                    return (
-                      <tr key={f.id} className="border-t border-bambu-dark-tertiary hover:bg-bambu-dark">
-                        <td className="px-3 py-2">
-                          <span
-                            className="w-4 h-4 rounded-full block shrink-0 border border-white/20"
-                            style={{ backgroundColor: f.color_hex ? `#${f.color_hex.replace('#', '')}` : '#808080' }}
-                            aria-label={t('inventory.spoolmanFilamentColorSwatch')}
-                          />
-                        </td>
-                        <td className="px-4 py-2 text-white truncate max-w-0">
-                          {isEditing ? (
+                ) : (
+                  filteredCatalog.map(entry => (
+                    <tr key={entry.id} className={`border-t border-bambu-dark-tertiary hover:bg-bambu-dark ${selectedIds.has(entry.id) ? 'bg-bambu-dark' : ''}`}>
+                      {editingId === entry.id ? (
+                        <>
+                          <td className="px-2 py-2">
+                            <input
+                              type="checkbox"
+                              checked={selectedIds.has(entry.id)}
+                              onChange={() => toggleSelect(entry.id)}
+                              className="w-4 h-4 accent-bambu-green cursor-pointer"
+                            />
+                          </td>
+                          <td className="px-4 py-2">
                             <input
                               type="text"
-                              value={editingFilamentName}
-                              onChange={e => setEditingFilamentName(e.target.value)}
-                              className="w-full bg-bambu-dark-tertiary text-white rounded px-2 py-0.5 text-sm border border-bambu-dark-secondary focus:outline-none focus:border-bambu-green"
-                              aria-label={t('common.name')}
+                              className="w-full px-2 py-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-white focus:border-bambu-green focus:outline-none"
+                              value={formName}
+                              onChange={(e) => setFormName(e.target.value)}
                             />
-                          ) : (
-                            <span className="block truncate">
-                              {f.vendor?.name ? `${f.vendor.name} — ` : ''}{f.name}
-                            </span>
-                          )}
-                        </td>
-                        <td className="px-4 py-2 text-bambu-gray">{f.material ?? '—'}</td>
-                        <td className="px-4 py-2 text-right font-mono text-white">
-                          {f.weight ? `${f.weight}g` : '—'}
-                        </td>
-                        <td className="px-4 py-2 text-right font-mono text-bambu-gray">
-                          {isEditing ? (
+                          </td>
+                          <td className="px-4 py-2">
                             <input
                               type="number"
-                              value={editingFilamentWeight}
-                              onChange={e => setEditingFilamentWeight(e.target.value)}
-                              min="0"
-                              step="1"
-                              className="w-20 bg-bambu-dark-tertiary text-white rounded px-2 py-0.5 text-sm border border-bambu-dark-secondary focus:outline-none focus:border-bambu-green text-right"
-                              aria-label={t('settings.catalog.spoolWeight')}
+                              className="w-full px-2 py-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-white text-right focus:border-bambu-green focus:outline-none"
+                              value={formWeight}
+                              onChange={(e) => setFormWeight(e.target.value)}
+                            />
+                          </td>
+                          <td className="px-4 py-2 text-center">
+                            <span className="text-xs text-bambu-gray">-</span>
+                          </td>
+                          <td className="px-4 py-2">
+                            <div className="flex justify-end gap-1">
+                              <button
+                                onClick={() => handleUpdate(entry.id)}
+                                disabled={saving}
+                                className="p-1.5 rounded hover:bg-green-500/20 text-green-500"
+                              >
+                                {saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
+                              </button>
+                              <button onClick={cancelEdit} className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray">
+                                <X className="w-4 h-4" />
+                              </button>
+                            </div>
+                          </td>
+                        </>
+                      ) : (
+                        <>
+                          <td className="px-2 py-2">
+                            <input
+                              type="checkbox"
+                              checked={selectedIds.has(entry.id)}
+                              onChange={() => toggleSelect(entry.id)}
+                              className="w-4 h-4 accent-bambu-green cursor-pointer"
                             />
-                          ) : (
-                            f.spool_weight != null ? `${f.spool_weight}g` : '—'
-                          )}
-                        </td>
-                        <td className="px-3 py-2">
-                          {isEditing ? (
-                            <div className="flex items-center gap-1 justify-end">
+                          </td>
+                          <td className="px-4 py-2 text-white">{entry.name}</td>
+                          <td className="px-4 py-2 text-right font-mono text-white">{entry.weight}g</td>
+                          <td className="px-4 py-2 text-center">
+                            {entry.is_default ? (
+                              <span className="text-xs px-2 py-0.5 rounded bg-bambu-dark-tertiary text-bambu-gray">
+                                {t('settings.catalog.default')}
+                              </span>
+                            ) : (
+                              <span className="text-xs px-2 py-0.5 rounded bg-bambu-green/20 text-bambu-green">
+                                {t('settings.catalog.custom')}
+                              </span>
+                            )}
+                          </td>
+                          <td className="px-4 py-2">
+                            <div className="flex justify-end gap-1">
                               <button
-                                onClick={() => handleFilamentSave(f)}
-                                disabled={isSaving || !editingFilamentName.trim() || (editingFilamentWeight !== '' && (isNaN(parseFloat(editingFilamentWeight)) || parseFloat(editingFilamentWeight) <= 0))}
-                                className="p-1 text-bambu-green hover:text-white disabled:opacity-40 disabled:cursor-not-allowed"
-                                aria-label={t('common.save')}
+                                onClick={() => startEdit(entry)}
+                                className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white"
                               >
-                                {isSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
+                                <Pencil className="w-4 h-4" />
                               </button>
                               <button
-                                onClick={() => { setEditingFilamentId(null); setEditingFilamentName(''); setEditingFilamentWeight(''); }}
-                                className="p-1 text-bambu-gray hover:text-white"
-                                aria-label={t('common.cancel')}
+                                onClick={() => setDeleteEntry(entry)}
+                                className="p-1.5 rounded bg-red-500/10 hover:bg-red-500/20 text-red-500"
                               >
-                                <X className="w-4 h-4" />
+                                <Trash2 className="w-4 h-4" />
                               </button>
                             </div>
-                          ) : (
-                            <button
-                              onClick={() => {
-                                setEditingFilamentId(f.id);
-                                setEditingFilamentName(f.name);
-                                setEditingFilamentWeight(f.spool_weight != null ? String(f.spool_weight) : '');
-                              }}
-                              className="p-1 text-bambu-gray hover:text-white"
-                              aria-label={t('common.edit')}
-                            >
-                              <Pencil className="w-4 h-4" />
-                            </button>
-                          )}
-                        </td>
-                      </tr>
-                    );
-                  })}
-                </tbody>
-              </table>
-            )}
+                          </td>
+                        </>
+                      )}
+                    </tr>
+                  ))
+                )}
+              </tbody>
+            </table>
           </div>
         )}
       </CardContent>
 
-      {/* Confirmation modals — local mode only */}
-      {!isSpoolmanMode && deleteEntry && (
+      {deleteEntry && (
         <ConfirmModal
           title={t('settings.catalog.deleteEntry')}
           message={t('settings.catalog.deleteConfirm', { name: deleteEntry.name })}
@@ -709,7 +470,7 @@ export function SpoolCatalogSettings() {
         />
       )}
 
-      {!isSpoolmanMode && showBulkDeleteConfirm && (
+      {showBulkDeleteConfirm && (
         <ConfirmModal
           title={t('settings.catalog.deleteSelected')}
           message={t('settings.catalog.bulkDeleteConfirm', { count: selectedIds.size })}
@@ -720,7 +481,7 @@ export function SpoolCatalogSettings() {
         />
       )}
 
-      {!isSpoolmanMode && showResetConfirm && (
+      {showResetConfirm && (
         <ConfirmModal
           title={t('settings.catalog.resetCatalog')}
           message={t('settings.catalog.resetConfirm')}
@@ -730,15 +491,6 @@ export function SpoolCatalogSettings() {
           onCancel={() => setShowResetConfirm(false)}
         />
       )}
-
-      <SpoolWeightUpdateModal
-        isOpen={pendingWeightEdit !== null}
-        filamentName={pendingWeightEdit?.name ?? ''}
-        oldWeight={pendingWeightEdit?.oldWeight ?? null}
-        newWeight={pendingWeightEdit?.newWeight ?? 0}
-        onConfirm={handleWeightModalConfirm}
-        onClose={() => setPendingWeightEdit(null)}
-      />
     </Card>
   );
 }

+ 0 - 102
frontend/src/components/SpoolWeightUpdateModal.tsx

@@ -1,102 +0,0 @@
-import { useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { Card, CardContent } from './Card';
-import { Button } from './Button';
-
-interface SpoolWeightUpdateModalProps {
-  isOpen: boolean;
-  filamentName: string;
-  oldWeight: number | null;
-  newWeight: number;
-  onConfirm: (keepExisting: boolean) => void;
-  onClose: () => void;
-}
-
-export function SpoolWeightUpdateModal({
-  isOpen,
-  filamentName,
-  oldWeight,
-  newWeight,
-  onConfirm,
-  onClose,
-}: SpoolWeightUpdateModalProps) {
-  const { t } = useTranslation();
-  const [keepExisting, setKeepExisting] = useState(false);
-
-  useEffect(() => {
-    if (isOpen) setKeepExisting(false);
-  }, [isOpen]);
-
-  if (!isOpen) return null;
-
-  const oldWeightLabel = oldWeight !== null ? `${oldWeight}g` : '—';
-  const newWeightLabel = `${newWeight}g`;
-
-  return (
-    <div
-      className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
-      onClick={onClose}
-    >
-      <Card className="w-full max-w-md" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
-        <CardContent className="p-6">
-          <h3 className="text-lg font-semibold text-white mb-1">
-            {t('settings.catalog.updateSpoolWeight')}
-          </h3>
-          <p className="text-bambu-gray text-sm mb-4">
-            {filamentName}: {oldWeightLabel} → {newWeightLabel}
-          </p>
-
-          <div className="space-y-3">
-            <label className="flex items-start gap-3 cursor-pointer p-3 rounded-lg border border-bambu-dark-tertiary hover:bg-bambu-dark transition-colors">
-              <input
-                type="radio"
-                name="weight-update-mode"
-                checked={!keepExisting}
-                onChange={() => setKeepExisting(false)}
-                className="mt-1 accent-bambu-green"
-              />
-              <div>
-                <div className="text-sm font-medium text-white">
-                  {t('settings.catalog.applyToAllSpools')}
-                </div>
-                <div className="text-xs text-bambu-gray mt-0.5">
-                  {t('settings.catalog.applyToAllSpoolsDesc')}
-                </div>
-              </div>
-            </label>
-
-            <label className="flex items-start gap-3 cursor-pointer p-3 rounded-lg border border-bambu-dark-tertiary hover:bg-bambu-dark transition-colors">
-              <input
-                type="radio"
-                name="weight-update-mode"
-                checked={keepExisting}
-                onChange={() => setKeepExisting(true)}
-                className="mt-1 accent-bambu-green"
-              />
-              <div>
-                <div className="text-sm font-medium text-white">
-                  {t('settings.catalog.keepExistingSpoolWeight')}
-                </div>
-                <div className="text-xs text-bambu-gray mt-0.5">
-                  {t('settings.catalog.keepExistingSpoolWeightDesc')}
-                </div>
-              </div>
-            </label>
-          </div>
-
-          <div className="flex gap-3 mt-6">
-            <Button variant="secondary" onClick={onClose} className="flex-1">
-              {t('common.cancel')}
-            </Button>
-            <Button
-              onClick={() => onConfirm(keepExisting)}
-              className="flex-1 bg-bambu-green hover:bg-bambu-green-dark"
-            >
-              {t('common.confirm')}
-            </Button>
-          </div>
-        </CardContent>
-      </Card>
-    </div>
-  );
-}

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
static/assets/index-Baw5c3Hn.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
static/assets/index-CLI--QzS.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
static/assets/index-CdDo4ToZ.js


+ 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-CfUZn1UK.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-Baw5c3Hn.css">
+    <script type="module" crossorigin src="/assets/index-CdDo4ToZ.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-CLI--QzS.css">
   </head>
   <body>
     <div id="root"></div>

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff