Browse Source

Removed new Spoolbuddy frontend

maziggy 3 months ago
parent
commit
9381edf0cb

+ 0 - 15
frontend/src/App.tsx

@@ -19,12 +19,6 @@ import InventoryPage from './pages/InventoryPage';
 import { SystemInfoPage } from './pages/SystemInfoPage';
 import { LoginPage } from './pages/LoginPage';
 import { SetupPage } from './pages/SetupPage';
-import { SpoolBuddyLayout } from './components/spoolbuddy/SpoolBuddyLayout';
-import { SpoolBuddyDashboard } from './pages/spoolbuddy/SpoolBuddyDashboard';
-import { SpoolBuddyAmsPage } from './pages/spoolbuddy/SpoolBuddyAmsPage';
-import { SpoolBuddyInventoryPage } from './pages/spoolbuddy/SpoolBuddyInventoryPage';
-import { SpoolBuddyPrintersPage } from './pages/spoolbuddy/SpoolBuddyPrintersPage';
-import { SpoolBuddySettingsPage } from './pages/spoolbuddy/SpoolBuddySettingsPage';
 import { useWebSocket } from './hooks/useWebSocket';
 import { ThemeProvider } from './contexts/ThemeContext';
 import { ToastProvider } from './contexts/ToastContext';
@@ -120,15 +114,6 @@ function App() {
                 {/* Stream overlay page - standalone for OBS/streaming embeds, no auth required */}
                 <Route path="/overlay/:printerId" element={<StreamOverlayPage />} />
 
-                {/* SpoolBuddy — standalone kiosk-optimized UI with its own layout */}
-                <Route element={<ProtectedRoute><WebSocketProvider><SpoolBuddyLayout /></WebSocketProvider></ProtectedRoute>}>
-                  <Route path="spoolbuddy" element={<SpoolBuddyDashboard />} />
-                  <Route path="spoolbuddy/ams" element={<SpoolBuddyAmsPage />} />
-                  <Route path="spoolbuddy/inventory" element={<SpoolBuddyInventoryPage />} />
-                  <Route path="spoolbuddy/printers" element={<SpoolBuddyPrintersPage />} />
-                  <Route path="spoolbuddy/settings" element={<SpoolBuddySettingsPage />} />
-                </Route>
-
                 {/* Main app with WebSocket for real-time updates */}
                 <Route element={<ProtectedRoute><WebSocketProvider><Layout /></WebSocketProvider></ProtectedRoute>}>
                   <Route index element={<PrintersPage />} />

+ 0 - 32
frontend/src/api/client.ts

@@ -4806,35 +4806,3 @@ export const supportApi = {
   clearLogs: () =>
     request<{ message: string }>('/support/logs', { method: 'DELETE' }),
 };
-
-export const spoolBuddyApi = {
-  getDevices: () =>
-    request<unknown[]>('/spoolbuddy/devices'),
-
-  registerDevice: (data: Record<string, unknown>) =>
-    request<unknown>('/spoolbuddy/devices/register', {
-      method: 'POST',
-      body: JSON.stringify(data),
-    }),
-
-  tare: (deviceId: string) =>
-    request<{ status: string }>(`/spoolbuddy/devices/${deviceId}/calibration/tare`, {
-      method: 'POST',
-      body: JSON.stringify({}),
-    }),
-
-  setCalibrationFactor: (deviceId: string, knownWeightGrams: number, rawAdc: number) =>
-    request<{ tare_offset: number; calibration_factor: number }>(`/spoolbuddy/devices/${deviceId}/calibration/set-factor`, {
-      method: 'POST',
-      body: JSON.stringify({ known_weight_grams: knownWeightGrams, raw_adc: rawAdc }),
-    }),
-
-  getCalibration: (deviceId: string) =>
-    request<{ tare_offset: number; calibration_factor: number }>(`/spoolbuddy/devices/${deviceId}/calibration`),
-
-  updateSpoolWeight: (spoolId: number, weightGrams: number) =>
-    request<{ status: string; weight_used: number }>('/spoolbuddy/scale/update-spool-weight', {
-      method: 'POST',
-      body: JSON.stringify({ spool_id: spoolId, weight_grams: weightGrams }),
-    }),
-};

+ 0 - 41
frontend/src/components/spoolbuddy/AmsSlotCard.tsx

@@ -1,41 +0,0 @@
-interface AmsSlotCardProps {
-  material: string | null;
-  colorHex: string | null;
-  colorName: string | null;
-  remaining: number | null;
-  isEmpty: boolean;
-  onClick: () => void;
-}
-
-export function AmsSlotCard({ material, colorHex, remaining, isEmpty, onClick }: AmsSlotCardProps) {
-  const color = colorHex ? `#${colorHex.substring(0, 6)}` : '#808080';
-
-  return (
-    <button
-      onClick={onClick}
-      className="w-[120px] h-[120px] rounded-xl border border-bambu-dark-tertiary bg-bg-secondary flex flex-col items-center justify-center gap-2 active:bg-bambu-dark-tertiary transition-colors"
-    >
-      {isEmpty ? (
-        <span className="text-text-muted text-[13px]">Empty</span>
-      ) : (
-        <>
-          <div
-            className="w-[48px] h-[48px] rounded-full border-2 border-bambu-dark-tertiary"
-            style={{ backgroundColor: color }}
-          />
-          <span className="text-text-primary text-[13px] font-medium truncate max-w-[100px]">
-            {material || '---'}
-          </span>
-          {remaining !== null && remaining >= 0 && (
-            <div className="w-[80px] h-1.5 bg-bambu-dark-tertiary rounded-full overflow-hidden">
-              <div
-                className="h-full rounded-full bg-bambu-green"
-                style={{ width: `${Math.min(100, remaining)}%` }}
-              />
-            </div>
-          )}
-        </>
-      )}
-    </button>
-  );
-}

+ 0 - 37
frontend/src/components/spoolbuddy/QuickActionGrid.tsx

@@ -1,37 +0,0 @@
-import { useTranslation } from 'react-i18next';
-import { Button } from '../Button';
-import { Scale, Edit, Cpu, History } from 'lucide-react';
-
-interface QuickActionGridProps {
-  onUpdateWeight: () => void;
-  onEditSpool: () => void;
-  onAssignAms: () => void;
-  onViewHistory: () => void;
-}
-
-export function QuickActionGrid({ onUpdateWeight, onEditSpool, onAssignAms, onViewHistory }: QuickActionGridProps) {
-  const { t } = useTranslation();
-
-  const actions = [
-    { icon: Cpu, label: t('spoolbuddy.actions.assignAms'), onClick: onAssignAms, variant: 'primary' as const },
-    { icon: Scale, label: t('spoolbuddy.actions.updateWeight'), onClick: onUpdateWeight, variant: 'secondary' as const },
-    { icon: Edit, label: t('spoolbuddy.actions.editSpool'), onClick: onEditSpool, variant: 'secondary' as const },
-    { icon: History, label: t('spoolbuddy.actions.viewHistory'), onClick: onViewHistory, variant: 'secondary' as const },
-  ];
-
-  return (
-    <div className="grid grid-cols-2 gap-3">
-      {actions.map(({ icon: Icon, label, onClick, variant }) => (
-        <Button
-          key={label}
-          variant={variant}
-          className="h-[64px] text-[14px] flex-col gap-1"
-          onClick={onClick}
-        >
-          <Icon size={20} />
-          <span>{label}</span>
-        </Button>
-      ))}
-    </div>
-  );
-}

+ 0 - 30
frontend/src/components/spoolbuddy/SpoolBuddyLayout.tsx

@@ -1,30 +0,0 @@
-import { Outlet, useSearchParams } from 'react-router-dom';
-import { SpoolBuddyNav } from './SpoolBuddyNav';
-import { SpoolBuddyStatusBar } from './SpoolBuddyStatusBar';
-import { useSpoolBuddyState } from '../../hooks/useSpoolBuddyState';
-
-export function SpoolBuddyLayout() {
-  const [searchParams] = useSearchParams();
-  const isKiosk = searchParams.get('kiosk') === '1' || window.innerHeight <= 600;
-  const state = useSpoolBuddyState();
-
-  return (
-    <div
-      className="w-[1024px] h-[600px] mx-auto flex flex-col bg-bg-primary overflow-hidden"
-      style={{ touchAction: 'manipulation' }}
-    >
-      <SpoolBuddyNav isKiosk={isKiosk} />
-
-      <main className="flex-1 overflow-hidden">
-        <Outlet context={state} />
-      </main>
-
-      <SpoolBuddyStatusBar
-        weightGrams={state.weight?.weight_grams ?? null}
-        stable={state.weight?.stable ?? false}
-        nfcOk={state.deviceOnline}
-        deviceOnline={state.deviceOnline}
-      />
-    </div>
-  );
-}

+ 0 - 56
frontend/src/components/spoolbuddy/SpoolBuddyNav.tsx

@@ -1,56 +0,0 @@
-import { NavLink } from 'react-router-dom';
-import { useTranslation } from 'react-i18next';
-import { Scale, Cpu, Package, Printer, Settings, ArrowLeft } from 'lucide-react';
-
-const navItems = [
-  { to: '/spoolbuddy', icon: Scale, labelKey: 'spoolbuddy.nav.dashboard', end: true },
-  { to: '/spoolbuddy/ams', icon: Cpu, labelKey: 'spoolbuddy.nav.ams' },
-  { to: '/spoolbuddy/inventory', icon: Package, labelKey: 'spoolbuddy.nav.inventory' },
-  { to: '/spoolbuddy/printers', icon: Printer, labelKey: 'spoolbuddy.nav.printers' },
-  { to: '/spoolbuddy/settings', icon: Settings, labelKey: 'spoolbuddy.nav.settings' },
-];
-
-interface SpoolBuddyNavProps {
-  isKiosk: boolean;
-}
-
-export function SpoolBuddyNav({ isKiosk }: SpoolBuddyNavProps) {
-  const { t } = useTranslation();
-
-  return (
-    <nav className="h-[48px] bg-bg-secondary border-b border-bambu-dark-tertiary flex items-center px-2 gap-1">
-      {!isKiosk && (
-        <NavLink
-          to="/"
-          className="flex items-center gap-1 px-3 h-[40px] rounded-lg text-text-secondary hover:text-white hover:bg-bambu-dark-tertiary text-[13px]"
-        >
-          <ArrowLeft size={16} />
-        </NavLink>
-      )}
-
-      <div className="flex items-center gap-1 px-2">
-        <span className="text-bambu-green font-bold text-[15px]">SpoolBuddy</span>
-      </div>
-
-      <div className="flex items-center gap-1 flex-1">
-        {navItems.map(({ to, icon: Icon, labelKey, end }) => (
-          <NavLink
-            key={to}
-            to={to}
-            end={end}
-            className={({ isActive }) =>
-              `flex items-center gap-1.5 px-4 h-[40px] rounded-lg text-[13px] font-medium transition-colors ${
-                isActive
-                  ? 'bg-bambu-green text-white'
-                  : 'text-text-secondary hover:text-white hover:bg-bambu-dark-tertiary'
-              }`
-            }
-          >
-            <Icon size={16} />
-            <span>{t(labelKey)}</span>
-          </NavLink>
-        ))}
-      </div>
-    </nav>
-  );
-}

+ 0 - 42
frontend/src/components/spoolbuddy/SpoolBuddyStatusBar.tsx

@@ -1,42 +0,0 @@
-import { useTranslation } from 'react-i18next';
-import { Scale, Nfc } from 'lucide-react';
-
-interface SpoolBuddyStatusBarProps {
-  weightGrams: number | null;
-  stable: boolean;
-  nfcOk: boolean;
-  deviceOnline: boolean;
-}
-
-export function SpoolBuddyStatusBar({ weightGrams, stable, nfcOk, deviceOnline }: SpoolBuddyStatusBarProps) {
-  const { t } = useTranslation();
-
-  return (
-    <div className="h-[40px] bg-bg-secondary border-t border-bambu-dark-tertiary flex items-center px-4 text-[13px]">
-      <div className="flex items-center gap-4 flex-1">
-        <div className="flex items-center gap-2">
-          <Scale size={14} className="text-text-secondary" />
-          <span className="text-text-primary font-mono">
-            {weightGrams !== null ? `${weightGrams.toFixed(1)}g` : '---'}
-          </span>
-          {weightGrams !== null && (
-            <span className={`w-2 h-2 rounded-full ${stable ? 'bg-green-500' : 'bg-yellow-500 animate-pulse'}`} />
-          )}
-        </div>
-
-        <div className="flex items-center gap-2">
-          <Nfc size={14} className="text-text-secondary" />
-          <span className={nfcOk ? 'text-green-500' : 'text-text-muted'}>
-            {nfcOk ? t('spoolbuddy.status.nfcReady') : t('spoolbuddy.status.nfcOff')}
-          </span>
-        </div>
-
-        {!deviceOnline && (
-          <span className="text-red-400 text-[12px]">{t('spoolbuddy.status.offline')}</span>
-        )}
-      </div>
-
-      <span className="text-text-muted text-[12px]">SpoolBuddy</span>
-    </div>
-  );
-}

+ 0 - 68
frontend/src/components/spoolbuddy/SpoolInfoCard.tsx

@@ -1,68 +0,0 @@
-import { useTranslation } from 'react-i18next';
-
-interface SpoolInfo {
-  id: number;
-  material: string;
-  subtype: string | null;
-  color_name: string | null;
-  rgba: string | null;
-  brand: string | null;
-  label_weight: number;
-  core_weight: number;
-  weight_used: number;
-}
-
-interface SpoolInfoCardProps {
-  spool: SpoolInfo;
-}
-
-export function SpoolInfoCard({ spool }: SpoolInfoCardProps) {
-  const { t } = useTranslation();
-
-  const remaining = Math.max(0, spool.label_weight - spool.weight_used);
-  const pct = spool.label_weight > 0 ? Math.round((remaining / spool.label_weight) * 100) : 0;
-
-  // Convert RRGGBBAA to CSS color
-  const color = spool.rgba
-    ? `#${spool.rgba.substring(0, 6)}`
-    : '#808080';
-
-  const materialLabel = spool.subtype
-    ? `${spool.material} ${spool.subtype}`
-    : spool.material;
-
-  return (
-    <div className="space-y-4">
-      <div className="flex items-center gap-4">
-        <div
-          className="w-[48px] h-[48px] rounded-full border-2 border-bambu-dark-tertiary flex-shrink-0"
-          style={{ backgroundColor: color }}
-        />
-        <div className="flex-1 min-w-0">
-          <h3 className="text-[18px] font-semibold text-text-primary truncate">
-            {materialLabel}
-          </h3>
-          <p className="text-[14px] text-text-secondary truncate">
-            {[spool.brand, spool.color_name].filter(Boolean).join(' - ')}
-          </p>
-        </div>
-      </div>
-
-      <div className="space-y-2">
-        <div className="flex justify-between text-[14px]">
-          <span className="text-text-secondary">{t('spoolbuddy.spool.remaining')}</span>
-          <span className="text-text-primary font-medium">{remaining}g ({pct}%)</span>
-        </div>
-        <div className="w-full h-3 bg-bambu-dark-tertiary rounded-full overflow-hidden">
-          <div
-            className="h-full rounded-full transition-all duration-300"
-            style={{
-              width: `${pct}%`,
-              backgroundColor: pct > 20 ? 'var(--accent)' : pct > 5 ? '#f59e0b' : '#ef4444',
-            }}
-          />
-        </div>
-      </div>
-    </div>
-  );
-}

+ 0 - 47
frontend/src/components/spoolbuddy/UnknownTagCard.tsx

@@ -1,47 +0,0 @@
-import { useTranslation } from 'react-i18next';
-import { Button } from '../Button';
-import { AlertTriangle } from 'lucide-react';
-
-interface UnknownTagCardProps {
-  tagUid: string;
-  sak?: number;
-  tagType?: string;
-  onLinkExisting: () => void;
-  onCreateNew: () => void;
-}
-
-export function UnknownTagCard({ tagUid, sak, tagType, onLinkExisting, onCreateNew }: UnknownTagCardProps) {
-  const { t } = useTranslation();
-
-  return (
-    <div className="flex flex-col items-center justify-center h-full px-6">
-      <AlertTriangle size={48} className="text-yellow-500 mb-4" />
-      <h3 className="text-[24px] font-semibold text-text-primary mb-2">
-        {t('spoolbuddy.tag.unknownTitle')}
-      </h3>
-      <p className="text-[14px] text-text-secondary mb-1 font-mono">{tagUid}</p>
-      {tagType && (
-        <p className="text-[13px] text-text-muted mb-6">
-          {tagType}{sak !== undefined ? ` (SAK: 0x${sak.toString(16).toUpperCase().padStart(2, '0')})` : ''}
-        </p>
-      )}
-
-      <div className="w-full space-y-3 max-w-[320px]">
-        <Button
-          variant="primary"
-          className="w-full h-[64px] text-[16px]"
-          onClick={onLinkExisting}
-        >
-          {t('spoolbuddy.tag.linkExisting')}
-        </Button>
-        <Button
-          variant="secondary"
-          className="w-full h-[64px] text-[16px]"
-          onClick={onCreateNew}
-        >
-          {t('spoolbuddy.tag.createNew')}
-        </Button>
-      </div>
-    </div>
-  );
-}

+ 0 - 62
frontend/src/components/spoolbuddy/WeightDisplay.tsx

@@ -1,62 +0,0 @@
-import { useTranslation } from 'react-i18next';
-import { Button } from '../Button';
-
-interface WeightDisplayProps {
-  weightGrams: number | null;
-  stable: boolean;
-  rawAdc: number | null;
-  onTare: () => void;
-  onCalibrate: () => void;
-}
-
-export function WeightDisplay({ weightGrams, stable, onTare, onCalibrate }: WeightDisplayProps) {
-  const { t } = useTranslation();
-
-  return (
-    <div className="flex flex-col items-center justify-center h-full px-4">
-      <div className="flex-1 flex flex-col items-center justify-center">
-        <span
-          className="text-[72px] font-bold text-text-primary leading-none"
-          style={{ fontVariantNumeric: 'tabular-nums' }}
-        >
-          {weightGrams !== null ? weightGrams.toFixed(1) : '---'}
-        </span>
-        <span className="text-[24px] text-text-secondary mt-1">g</span>
-
-        <div className="flex items-center gap-2 mt-3">
-          <span className={`w-3 h-3 rounded-full ${
-            weightGrams === null
-              ? 'bg-bambu-gray'
-              : stable
-                ? 'bg-green-500'
-                : 'bg-yellow-500 animate-pulse'
-          }`} />
-          <span className="text-[16px] text-text-secondary">
-            {weightGrams === null
-              ? t('spoolbuddy.weight.noReading')
-              : stable
-                ? t('spoolbuddy.weight.stable')
-                : t('spoolbuddy.weight.measuring')}
-          </span>
-        </div>
-      </div>
-
-      <div className="w-full flex gap-3 pb-4">
-        <Button
-          variant="secondary"
-          className="flex-1 h-[64px] text-[16px]"
-          onClick={onTare}
-        >
-          {t('spoolbuddy.weight.tare')}
-        </Button>
-        <Button
-          variant="secondary"
-          className="flex-1 h-[64px] text-[16px]"
-          onClick={onCalibrate}
-        >
-          {t('spoolbuddy.weight.calibrate')}
-        </Button>
-      </div>
-    </div>
-  );
-}

+ 0 - 175
frontend/src/hooks/useSpoolBuddyState.ts

@@ -1,175 +0,0 @@
-import { useCallback, useEffect, useReducer } from 'react';
-
-// --- Types ---
-
-interface SpoolInfo {
-  id: number;
-  material: string;
-  subtype: string | null;
-  color_name: string | null;
-  rgba: string | null;
-  brand: string | null;
-  label_weight: number;
-  core_weight: number;
-  weight_used: number;
-}
-
-interface WeightData {
-  weight_grams: number;
-  stable: boolean;
-  raw_adc: number | null;
-  device_id: string;
-}
-
-interface TagData {
-  tag_uid: string;
-  sak?: number;
-  tag_type?: string;
-  tray_uuid?: string;
-  device_id: string;
-}
-
-type DashboardView = 'idle' | 'tag_known' | 'tag_unknown';
-
-interface SpoolBuddyState {
-  view: DashboardView;
-  weight: WeightData | null;
-  tag: TagData | null;
-  spool: SpoolInfo | null;
-  deviceOnline: boolean;
-}
-
-type Action =
-  | { type: 'WEIGHT_UPDATE'; payload: WeightData }
-  | { type: 'TAG_MATCHED'; payload: { tag: TagData; spool: SpoolInfo } }
-  | { type: 'TAG_UNKNOWN'; payload: TagData }
-  | { type: 'TAG_REMOVED' }
-  | { type: 'DEVICE_ONLINE' }
-  | { type: 'DEVICE_OFFLINE' };
-
-// --- Reducer ---
-
-const initialState: SpoolBuddyState = {
-  view: 'idle',
-  weight: null,
-  tag: null,
-  spool: null,
-  deviceOnline: false,
-};
-
-function reducer(state: SpoolBuddyState, action: Action): SpoolBuddyState {
-  switch (action.type) {
-    case 'WEIGHT_UPDATE':
-      return { ...state, weight: action.payload };
-
-    case 'TAG_MATCHED':
-      return {
-        ...state,
-        view: 'tag_known',
-        tag: action.payload.tag,
-        spool: action.payload.spool,
-      };
-
-    case 'TAG_UNKNOWN':
-      return {
-        ...state,
-        view: 'tag_unknown',
-        tag: action.payload,
-        spool: null,
-      };
-
-    case 'TAG_REMOVED':
-      return {
-        ...state,
-        view: 'idle',
-        tag: null,
-        spool: null,
-      };
-
-    case 'DEVICE_ONLINE':
-      return { ...state, deviceOnline: true };
-
-    case 'DEVICE_OFFLINE':
-      return { ...state, deviceOnline: false, weight: null };
-
-    default:
-      return state;
-  }
-}
-
-// --- Hook ---
-
-export function useSpoolBuddyState() {
-  const [state, dispatch] = useReducer(reducer, initialState);
-
-  const handleWeight = useCallback((e: Event) => {
-    const detail = (e as CustomEvent).detail;
-    dispatch({
-      type: 'WEIGHT_UPDATE',
-      payload: {
-        weight_grams: detail.weight_grams,
-        stable: detail.stable,
-        raw_adc: detail.raw_adc ?? null,
-        device_id: detail.device_id,
-      },
-    });
-  }, []);
-
-  const handleTagMatched = useCallback((e: Event) => {
-    const detail = (e as CustomEvent).detail;
-    dispatch({
-      type: 'TAG_MATCHED',
-      payload: {
-        tag: {
-          tag_uid: detail.tag_uid,
-          device_id: detail.device_id,
-        },
-        spool: detail.spool,
-      },
-    });
-  }, []);
-
-  const handleTagUnknown = useCallback((e: Event) => {
-    const detail = (e as CustomEvent).detail;
-    dispatch({
-      type: 'TAG_UNKNOWN',
-      payload: {
-        tag_uid: detail.tag_uid,
-        sak: detail.sak,
-        tag_type: detail.tag_type,
-        device_id: detail.device_id,
-      },
-    });
-  }, []);
-
-  const handleTagRemoved = useCallback(() => {
-    dispatch({ type: 'TAG_REMOVED' });
-  }, []);
-
-  const handleDeviceStatus = useCallback((e: Event) => {
-    const detail = (e as CustomEvent).detail;
-    if (detail.type === 'spoolbuddy_online') {
-      dispatch({ type: 'DEVICE_ONLINE' });
-    } else {
-      dispatch({ type: 'DEVICE_OFFLINE' });
-    }
-  }, []);
-
-  useEffect(() => {
-    window.addEventListener('spoolbuddy-weight', handleWeight);
-    window.addEventListener('spoolbuddy-tag-matched', handleTagMatched);
-    window.addEventListener('spoolbuddy-unknown-tag', handleTagUnknown);
-    window.addEventListener('spoolbuddy-tag-removed', handleTagRemoved);
-    window.addEventListener('spoolbuddy-device-status', handleDeviceStatus);
-
-    return () => {
-      window.removeEventListener('spoolbuddy-weight', handleWeight);
-      window.removeEventListener('spoolbuddy-tag-matched', handleTagMatched);
-      window.removeEventListener('spoolbuddy-unknown-tag', handleTagUnknown);
-      window.removeEventListener('spoolbuddy-tag-removed', handleTagRemoved);
-      window.removeEventListener('spoolbuddy-device-status', handleDeviceStatus);
-    };
-  }, [handleWeight, handleTagMatched, handleTagUnknown, handleTagRemoved, handleDeviceStatus]);
-
-  return state;
-}

+ 0 - 33
frontend/src/hooks/useWebSocket.ts

@@ -258,39 +258,6 @@ export function useWebSocket() {
           })
         );
         break;
-
-      case 'spoolbuddy_weight':
-        window.dispatchEvent(new CustomEvent('spoolbuddy-weight', {
-          detail: message as unknown as Record<string, unknown>,
-        }));
-        break;
-
-      case 'spoolbuddy_tag_matched':
-        window.dispatchEvent(new CustomEvent('spoolbuddy-tag-matched', {
-          detail: message as unknown as Record<string, unknown>,
-        }));
-        debouncedInvalidate('inventory-spools');
-        break;
-
-      case 'spoolbuddy_unknown_tag':
-        window.dispatchEvent(new CustomEvent('spoolbuddy-unknown-tag', {
-          detail: message as unknown as Record<string, unknown>,
-        }));
-        break;
-
-      case 'spoolbuddy_tag_removed':
-        window.dispatchEvent(new CustomEvent('spoolbuddy-tag-removed', {
-          detail: message as unknown as Record<string, unknown>,
-        }));
-        break;
-
-      case 'spoolbuddy_online':
-      case 'spoolbuddy_offline':
-        window.dispatchEvent(new CustomEvent('spoolbuddy-device-status', {
-          detail: message as unknown as Record<string, unknown>,
-        }));
-        debouncedInvalidate('spoolbuddy-devices');
-        break;
     }
   }, [queryClient, debouncedInvalidate, throttledPrinterStatusUpdate]);
 

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

@@ -3525,69 +3525,4 @@ export default {
     daysAgo: 'vor {{count}}d',
     inDays: 'in {{count}}d',
   },
-  spoolbuddy: {
-    nav: {
-      dashboard: 'Dashboard',
-      ams: 'AMS',
-      inventory: 'Inventar',
-      printers: 'Drucker',
-      settings: 'Einstellungen',
-    },
-    status: {
-      nfcReady: 'Bereit',
-      nfcOff: 'Aus',
-      offline: 'Gerät offline',
-    },
-    dashboard: {
-      idleMessage: 'Spule auf die Waage legen und NFC-Tag scannen',
-    },
-    weight: {
-      noReading: 'Kein Messwert',
-      stable: 'Stabil',
-      measuring: 'Messung',
-      tare: 'Tara',
-      calibrate: 'Kalibrieren',
-      tareQueued: 'Tara-Befehl gesendet',
-    },
-    spool: {
-      remaining: 'Verbleibend',
-    },
-    tag: {
-      unknownTitle: 'Unbekannter Tag',
-      linkExisting: 'Mit vorhandener Spule verknüpfen',
-      createNew: 'Neue Spule erstellen',
-    },
-    actions: {
-      assignAms: 'AMS zuweisen',
-      updateWeight: 'Gewicht aktualisieren',
-      editSpool: 'Spule bearbeiten',
-      viewHistory: 'Verlauf anzeigen',
-      weightUpdated: 'Spulengewicht aktualisiert',
-    },
-    ams: {
-      noData: 'Keine AMS-Daten verfügbar',
-    },
-    inventory: {
-      search: 'Spulen suchen...',
-      empty: 'Keine Spulen gefunden',
-    },
-    printers: {
-      noPrinters: 'Keine Drucker konfiguriert',
-    },
-    settings: {
-      scaleCalibration: 'Waagenkalibrierung',
-      currentWeight: 'Aktuelles Gewicht',
-      tareOffset: 'Tara-Offset',
-      knownWeight: 'Bekanntes Gewicht',
-      calibrated: 'Kalibrierung aktualisiert',
-      tareQueued: 'Tara-Befehl in Warteschlange',
-      nfcReader: 'NFC-Leser',
-      nfcConnected: 'Verbunden',
-      nfcDisconnected: 'Getrennt',
-      deviceInfo: 'Geräteinformation',
-      deviceId: 'Geräte-ID',
-      uptime: 'Betriebszeit',
-      firmware: 'Firmware',
-    },
-  },
 };

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

@@ -3530,69 +3530,4 @@ export default {
     daysAgo: '{{count}}d ago',
     inDays: 'in {{count}}d',
   },
-  spoolbuddy: {
-    nav: {
-      dashboard: 'Dashboard',
-      ams: 'AMS',
-      inventory: 'Inventory',
-      printers: 'Printers',
-      settings: 'Settings',
-    },
-    status: {
-      nfcReady: 'Ready',
-      nfcOff: 'Off',
-      offline: 'Device Offline',
-    },
-    dashboard: {
-      idleMessage: 'Place spool on scale and scan NFC tag',
-    },
-    weight: {
-      noReading: 'No reading',
-      stable: 'Stable',
-      measuring: 'Measuring',
-      tare: 'Tare',
-      calibrate: 'Calibrate',
-      tareQueued: 'Tare command sent',
-    },
-    spool: {
-      remaining: 'Remaining',
-    },
-    tag: {
-      unknownTitle: 'Unknown Tag',
-      linkExisting: 'Link to Existing Spool',
-      createNew: 'Create New Spool',
-    },
-    actions: {
-      assignAms: 'Assign AMS',
-      updateWeight: 'Update Weight',
-      editSpool: 'Edit Spool',
-      viewHistory: 'View History',
-      weightUpdated: 'Spool weight updated',
-    },
-    ams: {
-      noData: 'No AMS data available',
-    },
-    inventory: {
-      search: 'Search spools...',
-      empty: 'No spools found',
-    },
-    printers: {
-      noPrinters: 'No printers configured',
-    },
-    settings: {
-      scaleCalibration: 'Scale Calibration',
-      currentWeight: 'Current weight',
-      tareOffset: 'Tare offset',
-      knownWeight: 'Known weight',
-      calibrated: 'Calibration updated',
-      tareQueued: 'Tare command queued',
-      nfcReader: 'NFC Reader',
-      nfcConnected: 'Connected',
-      nfcDisconnected: 'Disconnected',
-      deviceInfo: 'Device Info',
-      deviceId: 'Device ID',
-      uptime: 'Uptime',
-      firmware: 'Firmware',
-    },
-  },
 };

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

@@ -3493,69 +3493,4 @@ export default {
     daysAgo: 'il y a {{count}}j',
     inDays: 'dans {{count}}j',
   },
-  spoolbuddy: {
-    nav: {
-      dashboard: 'Tableau de bord',
-      ams: 'AMS',
-      inventory: 'Inventaire',
-      printers: 'Imprimantes',
-      settings: 'Paramètres',
-    },
-    status: {
-      nfcReady: 'Prêt',
-      nfcOff: 'Désactivé',
-      offline: 'Appareil hors ligne',
-    },
-    dashboard: {
-      idleMessage: 'Placez la bobine sur la balance et scannez le tag NFC',
-    },
-    weight: {
-      noReading: 'Aucune mesure',
-      stable: 'Stable',
-      measuring: 'Mesure en cours',
-      tare: 'Tare',
-      calibrate: 'Calibrer',
-      tareQueued: 'Commande de tare envoyée',
-    },
-    spool: {
-      remaining: 'Restant',
-    },
-    tag: {
-      unknownTitle: 'Tag inconnu',
-      linkExisting: 'Lier à une bobine existante',
-      createNew: 'Créer une nouvelle bobine',
-    },
-    actions: {
-      assignAms: 'Assigner AMS',
-      updateWeight: 'Mettre à jour le poids',
-      editSpool: 'Modifier la bobine',
-      viewHistory: 'Voir l\'historique',
-      weightUpdated: 'Poids de la bobine mis à jour',
-    },
-    ams: {
-      noData: 'Aucune donnée AMS disponible',
-    },
-    inventory: {
-      search: 'Rechercher des bobines...',
-      empty: 'Aucune bobine trouvée',
-    },
-    printers: {
-      noPrinters: 'Aucune imprimante configurée',
-    },
-    settings: {
-      scaleCalibration: 'Calibration de la balance',
-      currentWeight: 'Poids actuel',
-      tareOffset: 'Offset de tare',
-      knownWeight: 'Poids connu',
-      calibrated: 'Calibration mise à jour',
-      tareQueued: 'Commande de tare en file d\'attente',
-      nfcReader: 'Lecteur NFC',
-      nfcConnected: 'Connecté',
-      nfcDisconnected: 'Déconnecté',
-      deviceInfo: 'Info appareil',
-      deviceId: 'ID appareil',
-      uptime: 'Temps de fonctionnement',
-      firmware: 'Firmware',
-    },
-  },
 };

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

@@ -2881,69 +2881,4 @@ export default {
     daysAgo: '{{count}}g fa',
     inDays: 'tra {{count}}g',
   },
-  spoolbuddy: {
-    nav: {
-      dashboard: 'Dashboard',
-      ams: 'AMS',
-      inventory: 'Inventario',
-      printers: 'Stampanti',
-      settings: 'Impostazioni',
-    },
-    status: {
-      nfcReady: 'Pronto',
-      nfcOff: 'Spento',
-      offline: 'Dispositivo offline',
-    },
-    dashboard: {
-      idleMessage: 'Posiziona la bobina sulla bilancia e scansiona il tag NFC',
-    },
-    weight: {
-      noReading: 'Nessuna lettura',
-      stable: 'Stabile',
-      measuring: 'Misurazione',
-      tare: 'Tara',
-      calibrate: 'Calibra',
-      tareQueued: 'Comando tara inviato',
-    },
-    spool: {
-      remaining: 'Rimanente',
-    },
-    tag: {
-      unknownTitle: 'Tag sconosciuto',
-      linkExisting: 'Collega a bobina esistente',
-      createNew: 'Crea nuova bobina',
-    },
-    actions: {
-      assignAms: 'Assegna AMS',
-      updateWeight: 'Aggiorna peso',
-      editSpool: 'Modifica bobina',
-      viewHistory: 'Visualizza cronologia',
-      weightUpdated: 'Peso bobina aggiornato',
-    },
-    ams: {
-      noData: 'Nessun dato AMS disponibile',
-    },
-    inventory: {
-      search: 'Cerca bobine...',
-      empty: 'Nessuna bobina trovata',
-    },
-    printers: {
-      noPrinters: 'Nessuna stampante configurata',
-    },
-    settings: {
-      scaleCalibration: 'Calibrazione bilancia',
-      currentWeight: 'Peso attuale',
-      tareOffset: 'Offset tara',
-      knownWeight: 'Peso noto',
-      calibrated: 'Calibrazione aggiornata',
-      tareQueued: 'Comando tara in coda',
-      nfcReader: 'Lettore NFC',
-      nfcConnected: 'Connesso',
-      nfcDisconnected: 'Disconnesso',
-      deviceInfo: 'Info dispositivo',
-      deviceId: 'ID dispositivo',
-      uptime: 'Tempo di attività',
-      firmware: 'Firmware',
-    },
-  },
 };

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

@@ -3359,69 +3359,4 @@ export default {
     daysAgo: '{{count}}日前',
     inDays: 'あと{{count}}日',
   },
-  spoolbuddy: {
-    nav: {
-      dashboard: 'ダッシュボード',
-      ams: 'AMS',
-      inventory: '在庫',
-      printers: 'プリンター',
-      settings: '設定',
-    },
-    status: {
-      nfcReady: '準備完了',
-      nfcOff: 'オフ',
-      offline: 'デバイスオフライン',
-    },
-    dashboard: {
-      idleMessage: 'スプールを秤に置き、NFCタグをスキャンしてください',
-    },
-    weight: {
-      noReading: '計測なし',
-      stable: '安定',
-      measuring: '計測中',
-      tare: '風袋',
-      calibrate: '校正',
-      tareQueued: '風袋コマンドを送信しました',
-    },
-    spool: {
-      remaining: '残量',
-    },
-    tag: {
-      unknownTitle: '不明なタグ',
-      linkExisting: '既存のスプールにリンク',
-      createNew: '新しいスプールを作成',
-    },
-    actions: {
-      assignAms: 'AMS割り当て',
-      updateWeight: '重量更新',
-      editSpool: 'スプール編集',
-      viewHistory: '履歴表示',
-      weightUpdated: 'スプール重量を更新しました',
-    },
-    ams: {
-      noData: 'AMSデータがありません',
-    },
-    inventory: {
-      search: 'スプールを検索...',
-      empty: 'スプールが見つかりません',
-    },
-    printers: {
-      noPrinters: 'プリンターが設定されていません',
-    },
-    settings: {
-      scaleCalibration: '秤の校正',
-      currentWeight: '現在の重量',
-      tareOffset: '風袋オフセット',
-      knownWeight: '既知の重量',
-      calibrated: '校正が更新されました',
-      tareQueued: '風袋コマンドをキューに追加しました',
-      nfcReader: 'NFCリーダー',
-      nfcConnected: '接続済み',
-      nfcDisconnected: '未接続',
-      deviceInfo: 'デバイス情報',
-      deviceId: 'デバイスID',
-      uptime: '稼働時間',
-      firmware: 'ファームウェア',
-    },
-  },
 };

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

@@ -3491,69 +3491,4 @@ export default {
 
   // Spoolman Settings
   spoolmanSettings: {},
-  spoolbuddy: {
-    nav: {
-      dashboard: 'Painel',
-      ams: 'AMS',
-      inventory: 'Inventário',
-      printers: 'Impressoras',
-      settings: 'Configurações',
-    },
-    status: {
-      nfcReady: 'Pronto',
-      nfcOff: 'Desligado',
-      offline: 'Dispositivo offline',
-    },
-    dashboard: {
-      idleMessage: 'Coloque o carretel na balança e escaneie a tag NFC',
-    },
-    weight: {
-      noReading: 'Sem leitura',
-      stable: 'Estável',
-      measuring: 'Medindo',
-      tare: 'Tara',
-      calibrate: 'Calibrar',
-      tareQueued: 'Comando de tara enviado',
-    },
-    spool: {
-      remaining: 'Restante',
-    },
-    tag: {
-      unknownTitle: 'Tag desconhecida',
-      linkExisting: 'Vincular a carretel existente',
-      createNew: 'Criar novo carretel',
-    },
-    actions: {
-      assignAms: 'Atribuir AMS',
-      updateWeight: 'Atualizar peso',
-      editSpool: 'Editar carretel',
-      viewHistory: 'Ver histórico',
-      weightUpdated: 'Peso do carretel atualizado',
-    },
-    ams: {
-      noData: 'Nenhum dado AMS disponível',
-    },
-    inventory: {
-      search: 'Buscar carretéis...',
-      empty: 'Nenhum carretel encontrado',
-    },
-    printers: {
-      noPrinters: 'Nenhuma impressora configurada',
-    },
-    settings: {
-      scaleCalibration: 'Calibração da balança',
-      currentWeight: 'Peso atual',
-      tareOffset: 'Offset de tara',
-      knownWeight: 'Peso conhecido',
-      calibrated: 'Calibração atualizada',
-      tareQueued: 'Comando de tara na fila',
-      nfcReader: 'Leitor NFC',
-      nfcConnected: 'Conectado',
-      nfcDisconnected: 'Desconectado',
-      deviceInfo: 'Info do dispositivo',
-      deviceId: 'ID do dispositivo',
-      uptime: 'Tempo ativo',
-      firmware: 'Firmware',
-    },
-  },
 };

+ 0 - 88
frontend/src/pages/spoolbuddy/SpoolBuddyAmsPage.tsx

@@ -1,88 +0,0 @@
-import { useState } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { useTranslation } from 'react-i18next';
-import { AmsSlotCard } from '../../components/spoolbuddy/AmsSlotCard';
-import { api } from '../../api/client';
-
-interface PrinterOption {
-  id: number;
-  name: string;
-}
-
-export function SpoolBuddyAmsPage() {
-  const { t } = useTranslation();
-  const [selectedPrinterId, setSelectedPrinterId] = useState<number | null>(null);
-
-  const { data: printers } = useQuery({
-    queryKey: ['printers'],
-    queryFn: api.getPrinters,
-  });
-
-  const printerList = (printers || []) as PrinterOption[];
-  const activePrinterId = selectedPrinterId ?? printerList[0]?.id ?? null;
-
-  const { data: status } = useQuery({
-    queryKey: ['printerStatus', activePrinterId],
-    enabled: activePrinterId !== null,
-  });
-
-  const amsData = (status as Record<string, unknown>)?.ams as Record<string, unknown> | undefined;
-  const amsUnits = amsData?.ams as Array<Record<string, unknown>> | undefined;
-  return (
-    <div className="h-[512px] p-4 overflow-y-auto">
-      {/* Printer selector */}
-      <div className="mb-4">
-        <select
-          value={activePrinterId ?? ''}
-          onChange={(e) => setSelectedPrinterId(Number(e.target.value))}
-          className="h-[48px] w-full bg-bg-secondary border border-bambu-dark-tertiary rounded-lg px-4 text-text-primary text-[14px]"
-        >
-          {printerList.map((p) => (
-            <option key={p.id} value={p.id}>{p.name}</option>
-          ))}
-        </select>
-      </div>
-
-      {/* AMS units */}
-      {amsUnits ? (
-        <div className="space-y-6">
-          {amsUnits.map((ams, amsIdx) => {
-            const trays = ams.tray as Array<Record<string, unknown>> | undefined;
-            if (!trays) return null;
-            const label = String.fromCharCode(65 + amsIdx); // A, B, C, D
-
-            return (
-              <div key={amsIdx}>
-                <h3 className="text-[16px] font-semibold text-text-primary mb-2">AMS-{label}</h3>
-                <div className="flex gap-2">
-                  {trays.map((tray, trayIdx) => {
-                    const trayType = tray.tray_type as string | undefined;
-                    const trayColor = tray.tray_color as string | undefined;
-                    const remain = tray.remain as number | undefined;
-                    const isEmpty = !trayType || trayType === '';
-
-                    return (
-                      <AmsSlotCard
-                        key={trayIdx}
-                        material={trayType || null}
-                        colorHex={trayColor || null}
-                        colorName={null}
-                        remaining={remain ?? null}
-                        isEmpty={isEmpty}
-                        onClick={() => {/* TODO: slot detail modal */}}
-                      />
-                    );
-                  })}
-                </div>
-              </div>
-            );
-          })}
-        </div>
-      ) : (
-        <div className="flex items-center justify-center h-[300px]">
-          <p className="text-text-muted text-[16px]">{t('spoolbuddy.ams.noData')}</p>
-        </div>
-      )}
-    </div>
-  );
-}

+ 0 - 115
frontend/src/pages/spoolbuddy/SpoolBuddyDashboard.tsx

@@ -1,115 +0,0 @@
-import { useOutletContext } from 'react-router-dom';
-import { useTranslation } from 'react-i18next';
-import { useMutation } from '@tanstack/react-query';
-import { Scale, Nfc } from 'lucide-react';
-import { WeightDisplay } from '../../components/spoolbuddy/WeightDisplay';
-import { SpoolInfoCard } from '../../components/spoolbuddy/SpoolInfoCard';
-import { UnknownTagCard } from '../../components/spoolbuddy/UnknownTagCard';
-import { QuickActionGrid } from '../../components/spoolbuddy/QuickActionGrid';
-import { useToast } from '../../contexts/ToastContext';
-import { spoolBuddyApi } from '../../api/client';
-
-interface SpoolBuddyState {
-  view: 'idle' | 'tag_known' | 'tag_unknown';
-  weight: { weight_grams: number; stable: boolean; raw_adc: number | null; device_id: string } | null;
-  tag: { tag_uid: string; sak?: number; tag_type?: string; device_id: string } | null;
-  spool: {
-    id: number;
-    material: string;
-    subtype: string | null;
-    color_name: string | null;
-    rgba: string | null;
-    brand: string | null;
-    label_weight: number;
-    core_weight: number;
-    weight_used: number;
-  } | null;
-  deviceOnline: boolean;
-}
-
-export function SpoolBuddyDashboard() {
-  const state = useOutletContext<SpoolBuddyState>();
-  const { t } = useTranslation();
-  const { showToast } = useToast();
-
-  const updateWeightMutation = useMutation({
-    mutationFn: (data: { spool_id: number; weight_grams: number }) =>
-      spoolBuddyApi.updateSpoolWeight(data.spool_id, data.weight_grams),
-    onSuccess: () => showToast(t('spoolbuddy.actions.weightUpdated'), 'success'),
-    onError: () => showToast(t('common.error'), 'error'),
-  });
-
-  const handleUpdateWeight = () => {
-    if (state.spool && state.weight) {
-      updateWeightMutation.mutate({
-        spool_id: state.spool.id,
-        weight_grams: state.weight.weight_grams,
-      });
-    }
-  };
-
-  const handleTare = async () => {
-    if (state.weight?.device_id) {
-      try {
-        await spoolBuddyApi.tare(state.weight.device_id);
-        showToast(t('spoolbuddy.weight.tareQueued'), 'success');
-      } catch {
-        showToast(t('common.error'), 'error');
-      }
-    }
-  };
-
-  return (
-    <div className="flex h-[512px]">
-      {/* Left panel — Weight */}
-      <div className="w-[512px] border-r border-bambu-dark-tertiary">
-        <WeightDisplay
-          weightGrams={state.weight?.weight_grams ?? null}
-          stable={state.weight?.stable ?? false}
-          rawAdc={state.weight?.raw_adc ?? null}
-          onTare={handleTare}
-          onCalibrate={() => {/* TODO: open calibration modal */}}
-        />
-      </div>
-
-      {/* Right panel — Tag state */}
-      <div className="w-[512px] p-6 overflow-y-auto">
-        {state.view === 'idle' && (
-          <div className="flex flex-col items-center justify-center h-full text-center">
-            <div className="flex gap-4 mb-6">
-              <Scale size={48} className="text-text-muted" />
-              <Nfc size={48} className="text-text-muted" />
-            </div>
-            <p className="text-[24px] text-text-secondary max-w-[360px]">
-              {t('spoolbuddy.dashboard.idleMessage')}
-            </p>
-          </div>
-        )}
-
-        {state.view === 'tag_known' && state.spool && (
-          <div className="flex flex-col h-full">
-            <div className="flex-1">
-              <SpoolInfoCard spool={state.spool} />
-            </div>
-            <QuickActionGrid
-              onUpdateWeight={handleUpdateWeight}
-              onEditSpool={() => {/* TODO: open spool form modal */}}
-              onAssignAms={() => {/* TODO: open assign modal */}}
-              onViewHistory={() => {/* TODO: navigate to history */}}
-            />
-          </div>
-        )}
-
-        {state.view === 'tag_unknown' && state.tag && (
-          <UnknownTagCard
-            tagUid={state.tag.tag_uid}
-            sak={state.tag.sak}
-            tagType={state.tag.tag_type}
-            onLinkExisting={() => {/* TODO: open link modal */}}
-            onCreateNew={() => {/* TODO: open create modal */}}
-          />
-        )}
-      </div>
-    </div>
-  );
-}

+ 0 - 105
frontend/src/pages/spoolbuddy/SpoolBuddyInventoryPage.tsx

@@ -1,105 +0,0 @@
-import { useState } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { useTranslation } from 'react-i18next';
-import { Search, Plus } from 'lucide-react';
-import { Button } from '../../components/Button';
-import { api } from '../../api/client';
-
-interface InventorySpool {
-  id: number;
-  material: string;
-  subtype: string | null;
-  color_name: string | null;
-  rgba: string | null;
-  brand: string | null;
-  label_weight: number;
-  weight_used: number;
-}
-
-export function SpoolBuddyInventoryPage() {
-  const { t } = useTranslation();
-  const [search, setSearch] = useState('');
-
-  const { data: spools, isLoading } = useQuery({
-    queryKey: ['inventory-spools'],
-    queryFn: () => api.getSpools(),
-  });
-
-  const spoolList = (spools || []) as InventorySpool[];
-  const filtered = spoolList.filter((s) => {
-    if (!search) return true;
-    const q = search.toLowerCase();
-    return (
-      s.material.toLowerCase().includes(q) ||
-      (s.brand?.toLowerCase().includes(q)) ||
-      (s.color_name?.toLowerCase().includes(q))
-    );
-  });
-
-  return (
-    <div className="h-[512px] flex flex-col p-4">
-      {/* Search + Add */}
-      <div className="flex gap-2 mb-4">
-        <div className="relative flex-1">
-          <Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
-          <input
-            type="text"
-            value={search}
-            onChange={(e) => setSearch(e.target.value)}
-            placeholder={t('spoolbuddy.inventory.search')}
-            className="w-full h-[48px] bg-bg-secondary border border-bambu-dark-tertiary rounded-lg pl-10 pr-4 text-text-primary text-[14px] placeholder:text-text-muted"
-          />
-        </div>
-        <Button variant="primary" className="h-[48px] px-4">
-          <Plus size={18} />
-          <span className="ml-1">{t('common.add')}</span>
-        </Button>
-      </div>
-
-      {/* Spool grid */}
-      <div className="flex-1 overflow-y-auto">
-        <div className="grid grid-cols-2 gap-2">
-          {filtered.map((spool) => {
-            const remaining = Math.max(0, spool.label_weight - spool.weight_used);
-            const pct = spool.label_weight > 0 ? Math.round((remaining / spool.label_weight) * 100) : 0;
-            const color = spool.rgba ? `#${spool.rgba.substring(0, 6)}` : '#808080';
-            const materialLabel = spool.subtype ? `${spool.material} ${spool.subtype}` : spool.material;
-
-            return (
-              <button
-                key={spool.id}
-                className="flex items-center gap-3 p-4 bg-bg-secondary rounded-xl border border-bambu-dark-tertiary active:bg-bambu-dark-tertiary transition-colors text-left"
-              >
-                <div
-                  className="w-[32px] h-[32px] rounded-full border-2 border-bambu-dark-tertiary flex-shrink-0"
-                  style={{ backgroundColor: color }}
-                />
-                <div className="flex-1 min-w-0">
-                  <p className="text-[14px] font-semibold text-text-primary truncate">{materialLabel}</p>
-                  <p className="text-[12px] text-text-secondary truncate">
-                    {[spool.brand, spool.color_name].filter(Boolean).join(' - ')}
-                  </p>
-                  <div className="flex items-center gap-2 mt-1">
-                    <span className="text-[12px] text-text-muted">{remaining}g</span>
-                    <div className="flex-1 h-1.5 bg-bambu-dark-tertiary rounded-full overflow-hidden">
-                      <div
-                        className="h-full rounded-full bg-bambu-green"
-                        style={{ width: `${pct}%` }}
-                      />
-                    </div>
-                  </div>
-                </div>
-              </button>
-            );
-          })}
-        </div>
-
-        {filtered.length === 0 && !isLoading && (
-          <div className="flex items-center justify-center h-[200px]">
-            <p className="text-text-muted text-[14px]">{t('spoolbuddy.inventory.empty')}</p>
-          </div>
-        )}
-      </div>
-    </div>
-  );
-}

+ 0 - 74
frontend/src/pages/spoolbuddy/SpoolBuddyPrintersPage.tsx

@@ -1,74 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-import { useTranslation } from 'react-i18next';
-import { api } from '../../api/client';
-
-interface PrinterInfo {
-  id: number;
-  name: string;
-  model: string;
-  ip_address: string;
-}
-
-export function SpoolBuddyPrintersPage() {
-  const { t } = useTranslation();
-
-  const { data: printers } = useQuery({
-    queryKey: ['printers'],
-    queryFn: api.getPrinters,
-  });
-
-  const printerList = (printers || []) as PrinterInfo[];
-
-  return (
-    <div className="h-[512px] p-4 overflow-y-auto space-y-2">
-      {printerList.map((printer) => {
-        return (
-          <PrinterCard key={printer.id} printer={printer} />
-        );
-      })}
-
-      {printerList.length === 0 && (
-        <div className="flex items-center justify-center h-[300px]">
-          <p className="text-text-muted text-[16px]">{t('spoolbuddy.printers.noPrinters')}</p>
-        </div>
-      )}
-    </div>
-  );
-}
-
-function PrinterCard({ printer }: { printer: PrinterInfo }) {
-  const { data: status } = useQuery({
-    queryKey: ['printerStatus', printer.id],
-  });
-
-  const st = status as Record<string, unknown> | undefined;
-  const isOnline = st?.online === true;
-  const printPct = st?.mc_percent as number | undefined;
-  const nozzleTemp = st?.nozzle_temper as number | undefined;
-  const bedTemp = st?.bed_temper as number | undefined;
-
-  return (
-    <div className="p-4 bg-bg-secondary rounded-xl border border-bambu-dark-tertiary">
-      <div className="flex items-center justify-between mb-1">
-        <div className="flex items-center gap-2">
-          <span className={`w-2.5 h-2.5 rounded-full ${isOnline ? 'bg-green-500' : 'bg-bambu-gray'}`} />
-          <h3 className="text-[16px] font-semibold text-text-primary">{printer.name}</h3>
-        </div>
-        <span className={`text-[13px] px-2 py-0.5 rounded ${isOnline ? 'bg-green-500/20 text-green-400' : 'bg-bambu-dark-tertiary text-text-muted'}`}>
-          {isOnline ? 'Online' : 'Offline'}
-        </span>
-      </div>
-
-      <div className="flex items-center gap-3 text-[13px] text-text-secondary">
-        <span>{printer.model}</span>
-        <span>{printer.ip_address}</span>
-        {printPct !== undefined && printPct > 0 && (
-          <span>Print: {printPct}%</span>
-        )}
-        {nozzleTemp !== undefined && bedTemp !== undefined && isOnline && (
-          <span>{Math.round(nozzleTemp)}° / {Math.round(bedTemp)}°</span>
-        )}
-      </div>
-    </div>
-  );
-}

+ 0 - 165
frontend/src/pages/spoolbuddy/SpoolBuddySettingsPage.tsx

@@ -1,165 +0,0 @@
-import { useState } from 'react';
-import { useOutletContext } from 'react-router-dom';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { useTranslation } from 'react-i18next';
-import { Button } from '../../components/Button';
-import { useToast } from '../../contexts/ToastContext';
-import { spoolBuddyApi } from '../../api/client';
-
-interface SpoolBuddyState {
-  weight: { weight_grams: number; stable: boolean; raw_adc: number | null; device_id: string } | null;
-  deviceOnline: boolean;
-}
-
-interface DeviceInfo {
-  device_id: string;
-  hostname: string;
-  ip_address: string;
-  firmware_version: string | null;
-  tare_offset: number;
-  calibration_factor: number;
-  nfc_ok: boolean;
-  scale_ok: boolean;
-  uptime_s: number;
-  online: boolean;
-}
-
-export function SpoolBuddySettingsPage() {
-  const state = useOutletContext<SpoolBuddyState>();
-  const { t } = useTranslation();
-  const { showToast } = useToast();
-  const queryClient = useQueryClient();
-  const [knownWeight, setKnownWeight] = useState('500');
-
-  const { data: devices } = useQuery({
-    queryKey: ['spoolbuddy-devices'],
-    queryFn: spoolBuddyApi.getDevices,
-  });
-
-  const deviceList = (devices || []) as DeviceInfo[];
-  const device = deviceList[0]; // Primary device
-  const deviceId = device?.device_id ?? state.weight?.device_id;
-
-  const tareMutation = useMutation({
-    mutationFn: () => spoolBuddyApi.tare(deviceId!),
-    onSuccess: () => {
-      showToast(t('spoolbuddy.settings.tareQueued'), 'success');
-      queryClient.invalidateQueries({ queryKey: ['spoolbuddy-devices'] });
-    },
-    onError: () => showToast(t('common.error'), 'error'),
-  });
-
-  const calibrateMutation = useMutation({
-    mutationFn: () =>
-      spoolBuddyApi.setCalibrationFactor(deviceId!, parseFloat(knownWeight), state.weight?.raw_adc ?? 0),
-    onSuccess: () => {
-      showToast(t('spoolbuddy.settings.calibrated'), 'success');
-      queryClient.invalidateQueries({ queryKey: ['spoolbuddy-devices'] });
-    },
-    onError: () => showToast(t('common.error'), 'error'),
-  });
-
-  const formatUptime = (s: number) => {
-    const h = Math.floor(s / 3600);
-    const m = Math.floor((s % 3600) / 60);
-    return `${h}h ${m}m`;
-  };
-
-  return (
-    <div className="h-[512px] p-4 overflow-y-auto space-y-4">
-      {/* Scale Calibration */}
-      <section>
-        <h2 className="text-[18px] font-semibold text-text-primary mb-3">
-          {t('spoolbuddy.settings.scaleCalibration')}
-        </h2>
-        <div className="bg-bg-secondary rounded-xl border border-bambu-dark-tertiary p-4 space-y-4">
-          <div className="flex justify-between text-[14px]">
-            <span className="text-text-secondary">{t('spoolbuddy.settings.currentWeight')}</span>
-            <span className="text-text-primary font-mono">
-              {state.weight ? `${state.weight.weight_grams.toFixed(1)}g (raw: ${state.weight.raw_adc ?? '---'})` : '---'}
-            </span>
-          </div>
-
-          <div className="flex justify-between items-center">
-            <span className="text-[14px] text-text-secondary">
-              {t('spoolbuddy.settings.tareOffset')}: {device?.tare_offset ?? '---'}
-            </span>
-            <Button
-              variant="secondary"
-              className="h-[48px] px-6"
-              onClick={() => tareMutation.mutate()}
-              disabled={!deviceId || tareMutation.isPending}
-            >
-              {t('spoolbuddy.weight.tare')}
-            </Button>
-          </div>
-
-          <div className="flex items-center gap-3">
-            <span className="text-[14px] text-text-secondary whitespace-nowrap">
-              {t('spoolbuddy.settings.knownWeight')}:
-            </span>
-            <input
-              type="number"
-              value={knownWeight}
-              onChange={(e) => setKnownWeight(e.target.value)}
-              className="h-[48px] w-[120px] bg-bg-primary border border-bambu-dark-tertiary rounded-lg px-3 text-text-primary text-[14px] text-right"
-            />
-            <span className="text-[14px] text-text-secondary">g</span>
-            <Button
-              variant="secondary"
-              className="h-[48px] px-6 ml-auto"
-              onClick={() => calibrateMutation.mutate()}
-              disabled={!deviceId || !state.weight?.raw_adc || calibrateMutation.isPending}
-            >
-              {t('spoolbuddy.weight.calibrate')}
-            </Button>
-          </div>
-        </div>
-      </section>
-
-      {/* NFC Reader */}
-      <section>
-        <h2 className="text-[18px] font-semibold text-text-primary mb-3">
-          {t('spoolbuddy.settings.nfcReader')}
-        </h2>
-        <div className="bg-bg-secondary rounded-xl border border-bambu-dark-tertiary p-4">
-          <div className="flex items-center gap-2">
-            <span className={`w-2.5 h-2.5 rounded-full ${device?.nfc_ok ? 'bg-green-500' : 'bg-bambu-gray'}`} />
-            <span className="text-[14px] text-text-primary">
-              {device?.nfc_ok ? t('spoolbuddy.settings.nfcConnected') : t('spoolbuddy.settings.nfcDisconnected')}
-            </span>
-          </div>
-        </div>
-      </section>
-
-      {/* Device Info */}
-      {device && (
-        <section>
-          <h2 className="text-[18px] font-semibold text-text-primary mb-3">
-            {t('spoolbuddy.settings.deviceInfo')}
-          </h2>
-          <div className="bg-bg-secondary rounded-xl border border-bambu-dark-tertiary p-4 text-[14px] space-y-2">
-            <div className="flex justify-between">
-              <span className="text-text-secondary">{t('spoolbuddy.settings.deviceId')}</span>
-              <span className="text-text-primary font-mono">{device.device_id}</span>
-            </div>
-            <div className="flex justify-between">
-              <span className="text-text-secondary">IP</span>
-              <span className="text-text-primary">{device.ip_address}</span>
-            </div>
-            <div className="flex justify-between">
-              <span className="text-text-secondary">{t('spoolbuddy.settings.uptime')}</span>
-              <span className="text-text-primary">{formatUptime(device.uptime_s)}</span>
-            </div>
-            {device.firmware_version && (
-              <div className="flex justify-between">
-                <span className="text-text-secondary">{t('spoolbuddy.settings.firmware')}</span>
-                <span className="text-text-primary">{device.firmware_version}</span>
-              </div>
-            )}
-          </div>
-        </section>
-      )}
-    </div>
-  );
-}

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-B3SoseiM.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-C9FSOSR8.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-CBMMkw9a.css


+ 2 - 2
static/index.html

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

Some files were not shown because too many files changed in this diff