Files
bambuddy/frontend/src/components/UnknownSpoolModal.tsx
T
maziggy 7cb905ad0c feat(inventory): toggle to disable auto-add of unknown RFID spools + global confirmation modal (issue #1764)
New setting "Auto-add unknown RFID spools" under Settings -> Filament -> Filament Tracking,
  default ON for back-compat. When turned off, the backend stops auto-creating an inventory
  record for an unknown RFID tag and instead broadcasts an unknown_tag WS event that pops
  a global confirmation modal in the Bambuddy UI showing the printer / AMS-X label / slot /
  material / colour. Add or Cancel; no nag on every MQTT push.

  Backend
  - Module-level _unknown_tag_last_broadcast dict dedupes per (printer, slot, tag). Set is
    committed AFTER ws_manager.broadcast() returns so a crashed broadcast doesn't poison
    the dedup and permanently silence the slot.
  - Empty-slot MQTT push clears that slot's entry, so remove+reinsert reliably re-prompts.
  - Successful matches via get_spool_by_tag / find_matching_untagged_spool / create_spool
    also clear the entry so a future tag swap re-prompts.
  - Tray data (tray_type, tray_color, tray_sub_brands, tray_count) shipped in the WS payload
    directly so the modal renders the real material / colour instead of relying on the
    React Query cache that lags the WS event by several seconds.
  - Two new endpoints back the modal's confirm action:
      POST /api/v1/inventory/spools/from-slot     (INVENTORY_UPDATE)
      POST /api/v1/spoolman/spools/from-slot      (FILAMENTS_UPDATE)
    Both look up the slot's tray data server-side and create + auto-assign atomically.
  - Spoolman /from-slot now raises HTTP 500 when the slot-assignment INSERT fails instead
    of returning success while the DB rolled back the binding.
  - sync_ams_tray gained an optional auto_add_unknown_rfid kwarg (default True so existing
    callers are unaffected); auto-sync and both manual sync routes thread the setting.

  Frontend
  - useUnknownTagPrompt hook listens for the unknown-tag CustomEvent, reads the tray fields
    out of the event detail, and feeds a single-modal queue. No long-lived dismissed set;
    the backend dedup handles spam suppression.
  - UnknownSpoolModal wraps the existing ConfirmModal with a material + colour-swatch
    preview block.
  - Mounted in Layout.tsx alongside useSponsorPrompt so SpoolBuddy kiosk / login / setup
    routes are excluded.
  - getAmsLabel moved to utils/amsHelpers.ts; ConfigureAmsSlotModal.tsx and PrintersPage.tsx
    both import the shared version (canonical AMS-A / HT-A / External labels).
  - AppSettings TS interface gained spoolman_enabled, auto_add_unknown_rfid, spoolman_url
    so the runtime cast in the hook is no longer needed.
  - SpoolmanSettings.tsx gets a new toggle row in the Filament Tracking card, visible in
    both built-in and Spoolman branches; auto-save + toast already wired.
2026-06-23 09:59:05 +02:00

52 lines
1.9 KiB
TypeScript

import { useTranslation } from 'react-i18next';
import { ConfirmModal } from './ConfirmModal';
import { getSwatchStyle } from '../utils/colors';
import type { UnknownSpoolPrompt } from '../hooks/useUnknownTagPrompt';
interface UnknownSpoolModalProps {
prompt: UnknownSpoolPrompt | null;
isPending: boolean;
onConfirm: () => void;
onCancel: () => void;
}
export function UnknownSpoolModal({ prompt, isPending, onConfirm, onCancel }: UnknownSpoolModalProps) {
const { t } = useTranslation();
if (!prompt) return null;
const location = `${prompt.printer_name} • ${prompt.ams_label} • ${prompt.slot_label}`;
const swatchStyle = prompt.color_hex ? getSwatchStyle(prompt.color_hex) : undefined;
return (
<ConfirmModal
title={t('inventory.unknownSpoolTitle')}
message={t('inventory.unknownSpoolMessage', { location })}
confirmText={t('inventory.addToInventory')}
cancelText={t('common.cancel')}
variant="default"
isLoading={isPending}
loadingText={t('inventory.addToInventoryPending')}
onConfirm={onConfirm}
onCancel={onCancel}
>
<div className="flex items-center gap-3 p-3 rounded-lg bg-bambu-dark-secondary border border-bambu-dark-tertiary">
{swatchStyle && (
<div
className="w-8 h-8 rounded-full border border-black/20 flex-shrink-0"
style={swatchStyle}
aria-label={prompt.color_hex ?? undefined}
/>
)}
<div className="min-w-0 flex-1">
<p className="text-white text-sm font-medium truncate">
{prompt.brand ? `${prompt.brand} ${prompt.material ?? ''}`.trim() : prompt.material ?? '—'}
</p>
{prompt.color_hex && (
<p className="text-xs text-bambu-gray font-mono uppercase">#{prompt.color_hex.replace(/^#/, '').slice(0, 6)}</p>
)}
</div>
</div>
</ConfirmModal>
);
}