diff --git a/CLAUDE.md b/CLAUDE.md index d9e7437..aed0a27 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,7 +132,9 @@ This is a fork with UX improvements. Issues are tracked on the Gitea instance ab | 4 | Layout Max-Width | ✅ DONE | | 5 | Temperature Ranges | ✅ DONE | | 6 | Spool Adjustment History | ✅ DONE | -| 7 | 3dfilamentprofiles Integration | Open | +| 7 | 3dfilamentprofiles Integration | ✅ DONE | +| 8 | (see Gitea #8) | ✅ DONE | +| 9 | Hierarchical Locations (Room/Bin) | Open | ### Issue #1 Complete - Inline Creation Modals @@ -213,3 +215,73 @@ Frontend: - `client/src/pages/spools/model.tsx` - Added `ISpoolAdjustment` interface - `client/src/pages/spools/show.tsx` - Collapsible adjustment history table - `client/public/locales/en/common.json` - Translation keys for history table + +### Issue #7 Complete - 3dfilamentprofiles Integration + +Added 3dfilamentprofiles.com as a second external database source alongside SpoolmanDB. + +Backend (`spoolman/externaldb.py`): +- Added `ExternalSource` enum (SPOOLMANDB, FILAMENT_PROFILES_3D) +- Added `Filament3DFP` model for raw 3dfp data +- Added `_transform_3dfp_to_external()` to convert 3dfp format to ExternalFilament +- Updated `_sync()` to fetch from both sources and merge results +- Environment variables: `EXTERNAL_DB_3DFP_URL`, `EXTERNAL_DB_3DFP_ENABLED` + +Frontend: +- `client/src/utils/queryExternalDB.ts` - Added `ExternalSource` enum, `source` field, temp range fields +- `client/src/components/filamentImportModal.tsx` - Added tabs to filter by source (All/SpoolmanDB/3DFP) +- `client/src/pages/filaments/create.tsx` - Updated import handler for temp range fields + +Data mapping (3dfp → Spoolman): +- `brand_name` → `manufacturer` +- `color` + `material_type` → `name` +- `rgb` → `color_hex` (strip # prefix) +- `properties.temp_min/max` → `extruder_temp/extruder_temp_max` +- `properties.bed_temp_min/max` → `bed_temp/bed_temp_max` +- Diameter defaults to 1.75mm (3dfp doesn't provide) +- Density from material lookup table + +--- + +## Local Development Setup + +To run locally against the Unraid PostgreSQL database: + +```bash +# Create virtualenv and install deps +python3 -m venv .venv +source .venv/bin/activate +pip install -e ".[postgres]" + +# Start backend (reads .env file) +source .venv/bin/activate +export $(grep -v '^#' .env | xargs) +uvicorn spoolman.main:app --host 0.0.0.0 --port 7912 + +# Start frontend (separate terminal) +cd client +VITE_APIURL=http://localhost:7912/api/v1 npm run dev +``` + +Access at http://localhost:5173 + +### Dev Mode Styling + +Files modified for dev version visual distinction: +- `client/src/contexts/color-mode/index.tsx` - Teal primary color (#0891b2) +- `client/src/components/layout.tsx` - "DEV BUILD" banner and badge + +Set `IS_DEV = false` in layout.tsx to disable dev styling. + +--- + +## Known Issues / TODO + +### Filament Select Not Refreshing After Inline Creation +When creating a filament via the inline modal on spool create page, the select dropdown doesn't immediately show the new filament with its name (shows ID instead). + +Attempted fixes: +- Added `refetch` from `useGetFilamentSelectOptions` hook +- Called `await refetchFilaments()` before setting form value + +Still not working reliably. May need to investigate react-query cache invalidation timing or add the new option manually to the select options array. diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 8776863..35030cd 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -276,6 +276,8 @@ "filament_updated": "This filament has been updated by someone/something else since you opened this page. Saving will overwrite those changes!", "import_external": "Import from External", "import_external_description": "Select a filament in the list to automatically populate its details in the filament creation form. This will overwrite any data you have entered in the form.

A Manufacturer object will be created automatically when you click Ok, if necessary.", + "all_sources": "All Sources", + "select_filament": "Select a filament...", "created_success": "Created filament \"{{name}}\"", "vendor_placeholder": "Select a manufacturer", "name_placeholder": "E.g. Matte Black", diff --git a/client/src/components/filamentImportModal.tsx b/client/src/components/filamentImportModal.tsx index d0e48e5..ff9bf6b 100644 --- a/client/src/components/filamentImportModal.tsx +++ b/client/src/components/filamentImportModal.tsx @@ -1,9 +1,10 @@ import { useTranslate } from "@refinedev/core"; -import { Form, Modal, Select } from "antd"; +import { Form, Modal, Select, Tabs } from "antd"; +import { useMemo, useState } from "react"; import { Trans } from "react-i18next"; import { formatFilamentLabel } from "../pages/spools/functions"; import { searchMatches } from "../utils/filtering"; -import { ExternalFilament, useGetExternalDBFilaments } from "../utils/queryExternalDB"; +import { ExternalFilament, ExternalSource, useGetExternalDBFilaments } from "../utils/queryExternalDB"; export function FilamentImportModal(props: { isOpen: boolean; @@ -12,12 +13,25 @@ export function FilamentImportModal(props: { }) { const [form] = Form.useForm(); const t = useTranslate(); + const [selectedSource, setSelectedSource] = useState("all"); const externalFilaments = useGetExternalDBFilaments(); - const filamentOptions = - externalFilaments.data?.map((item) => { + + // Filter and format filaments based on selected source + const filamentOptions = useMemo(() => { + const filtered = externalFilaments.data?.filter((item) => { + if (selectedSource === "all") return true; + return item.source === selectedSource; + }) ?? []; + + const options = filtered.map((item) => { + // Add source indicator to label if showing all + const sourceIndicator = selectedSource === "all" && item.source === ExternalSource.FILAMENT_PROFILES_3D + ? "[3DFP] " + : ""; + return { - label: formatFilamentLabel( + label: sourceIndicator + formatFilamentLabel( item.name, item.diameter, item.manufacturer, @@ -28,8 +42,23 @@ export function FilamentImportModal(props: { value: item.id, item: item, }; - }) ?? []; - filamentOptions.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" })); + }); + + options.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" })); + return options; + }, [externalFilaments.data, selectedSource]); + + // Count filaments by source + const counts = useMemo(() => { + const spoolmandb = externalFilaments.data?.filter(f => f.source === ExternalSource.SPOOLMANDB || !f.source).length ?? 0; + const threeDFP = externalFilaments.data?.filter(f => f.source === ExternalSource.FILAMENT_PROFILES_3D).length ?? 0; + return { spoolmandb, threeDFP, total: spoolmandb + threeDFP }; + }, [externalFilaments.data]); + + const handleSourceChange = (key: string) => { + setSelectedSource(key as ExternalSource | "all"); + form.resetFields(["filament"]); + }; return ( form.submit()} onCancel={() => props.onClose()} + width={600} > +

+ , + }} + /> +

+ + +
-

- , - }} - /> -