# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview Spoolman is a self-hosted web service for managing 3D printer filament spools. It tracks inventory, monitors usage, and integrates with OctoPrint and Klipper/Moonraker. **Tech Stack:** - Backend: Python 3.9-3.12, FastAPI, SQLAlchemy 2.0 (async), Alembic - Frontend: React 19, TypeScript, Vite, Ant Design, Refine framework - Databases: SQLite (default), PostgreSQL, MySQL, CockroachDB ## Common Commands ### Python (PDM) ```bash pdm install # Install all dependencies including dev pdm sync --prod --no-editable # Install production dependencies only pdm run app # Run backend server (uvicorn) pdm run itest # Run integration tests (all databases) pdm run docs # Generate API documentation ``` ### Frontend ```bash cd client npm ci # Install dependencies npm run dev # Development server with hot reload npm run build # Production build npm run tsc # Type check only ``` ### Linting/Formatting ```bash black . # Format Python code ruff check . --fix # Lint and auto-fix Python pre-commit run --all-files # Run all pre-commit hooks ``` ### Integration Tests Tests run in Docker containers against multiple database types: ```bash python tests_integration/run.py sqlite # Single database python tests_integration/run.py sqlite postgres # Multiple databases pdm run itest # All databases ``` Run a single test: ```bash pytest tests_integration/tests/spool/test_add.py::test_add_spool_remaining_weight -v ``` ### Database Migrations 1. Edit models in `spoolman/database/models.py` 2. Run the server to update local SQLite 3. `pdm run alembic revision -m "description" --autogenerate` 4. Review generated migration, format with Black/Ruff ## Architecture ### Backend (`/spoolman/`) - `main.py` - FastAPI app entry, startup/shutdown, SPA mounting - `env.py` - Environment variable parsing and configuration - `ws.py` - WebSocket manager for real-time updates - `api/v1/` - REST endpoints and WebSocket handlers - `router.py` - Main router with WebSocket root endpoint - `spool.py`, `filament.py`, `vendor.py` - CRUD endpoints per resource - `models.py` - Pydantic response models - `database/` - SQLAlchemy models and async database operations - `models.py` - ORM definitions (Vendor, Filament, Spool, Setting, *Field) - `database.py` - Connection setup, backup logic, migrations ### Frontend (`/client/src/`) - `App.tsx` - Routing and Refine framework setup - `pages/` - Page components (spools/, filaments/, vendors/, settings/) - `components/dataProvider.ts` - REST API integration - `components/liveProvider.ts` - WebSocket live updates ### Data Model ``` Vendor (1) ──→ (many) Filament (1) ──→ (many) Spool ``` All entities support custom fields via *Field tables (VendorField, FilamentField, SpoolField). ### API Patterns - REST: Standard CRUD at `/api/v1/{resource}` - WebSocket: Real-time events at `WS /api/v1/` or `WS /api/v1/{resource}` - Query params: `?skip=0&limit=50&sort_by=name&sort_order=asc` ### Key Configuration Environment variables (see `.env.example`): - `SPOOLMAN_DB_TYPE` - Database type (sqlite/mysql/postgres/cockroachdb) - `SPOOLMAN_PORT` - Server port (default: 7912) - `SPOOLMAN_DEBUG_MODE` - Enables permissive CORS - `VITE_APIURL` - Frontend API base URL (e.g., `/api/v1`) ## Code Quality - Line length: 120 characters - Python target: 3.9 - Pre-commit hooks enforce Black formatting and Ruff linting - Backend is fully async using SQLAlchemy AsyncSession ## Gitea Repository - **URL:** http://192.168.0.5:3022/tonym/spoolman2 - **API Token:** 8a04b3cb5dbb54e2d895b707305523c3ad83a945 - **Upstream:** https://github.com/Donkie/Spoolman (remote: `upstream`) This is a fork with UX improvements. Issues are tracked on the Gitea instance above. ## UX Improvement Issues | # | Title | Status | |---|-------|--------| | 1 | Inline Creation Modals | ✅ DONE | | 2 | Smart Density Defaults | ✅ DONE | | 3 | QR Code Sizing Fixes | ✅ DONE | | 4 | Layout Max-Width | ✅ DONE | | 5 | Temperature Ranges | ✅ DONE | | 6 | Spool Adjustment History | ✅ DONE | | 7 | 3dfilamentprofiles Integration | ✅ DONE | | 8 | (see Gitea #8) | ✅ DONE | | 9 | Hierarchical Locations (Room/Bin) | Open | | 10 | Database Import/Export | ✅ DONE | ### Issue #1 Complete - Inline Creation Modals Added ability to create filament/vendor inline without leaving the page: - `client/src/components/vendorCreateModal.tsx` - Quick vendor creation - `client/src/components/filamentCreateModal.tsx` - Quick filament creation with nested vendor modal - Modified spool create to add "Create Filament" in dropdown - Modified filament create to add "Create Manufacturer" in dropdown - Uses `dropdownRender` pattern (same as location select) - Uses `useInvalidate` to refresh lists after creation ### Issue #2 Complete - Smart Density Defaults Auto-fill density based on material type: - `client/src/utils/materialDefaults.ts` - Density lookup table with fuzzy matching - Material field converted to AutoComplete (dropdown suggestions + free text) - Fuzzy matching: "PLA Silk" → PLA (1.24 g/cm³) - New filaments default to 1.75mm diameter and 1.24 g/cm³ (PLA) - Applied to both full create page and FilamentCreateModal - Edit page not affected (preserves user customizations) Material densities in lookup: ``` PLA: 1.24, ABS: 1.04, PETG: 1.27, NYLON: 1.52, TPU: 1.21 PC: 1.3, WOOD: 1.28, CF: 1.3, PC/ABS: 1.19, HIPS: 1.03 PVA: 1.23, ASA: 1.05, PP: 0.9, POM: 1.4, PMMA: 1.18, FPE: 2.16 ``` ### Issue #3 Complete - QR Code Sizing Fixes See upstream issue: https://github.com/Donkie/Spoolman/issues/671 Changes: - Removed fixed 50% max-width constraint (now uses fit-content by default) - Added `qrCodePadding` setting (0-10mm, default 2mm) - Added `qrCodeMaxWidth` setting (0-100%, 0 = auto) - Added UI controls with slider + input number for both settings Files modified: - `client/src/pages/printing/printing.tsx` - Added settings to interface - `client/src/pages/printing/qrCodePrintingDialog.tsx` - CSS and UI controls ### Issue #4 Complete - Layout Max-Width Changes: - Added 1400px max-width container in `SpoolmanLayout` - Added 800px max-width for forms via CSS override in `overrides.css` - Content is centered on wide screens ### Issue #5 Complete - Temperature Ranges Store extruder and bed temperatures as min/max ranges: - Added 4 new DB columns: `settings_extruder_temp_min/max`, `settings_bed_temp_min/max` - Old single-value columns kept for backward compatibility (deprecated) - Alembic migration copies existing values to both min and max - Frontend uses Space.Compact with min/max input pairs - Label template shows ranges: "ET: 190-220 °C" Files: - `spoolman/database/models.py` - New columns - `migrations/versions/2025_01_15_0200-*.py` - Migration - `spoolman/api/v1/models.py`, `filament.py` - API changes - `client/src/pages/filaments/create.tsx` - Form updates - `client/src/pages/printing/spoolQrCodePrintingDialog.tsx` - Template tags ### Issue #6 Complete - Spool Adjustment History Track history of all spool weight/length adjustments with timestamps and comments. Backend: - `spoolman/database/models.py` - Added `SpoolAdjustment` model with relationship to Spool - `migrations/versions/2025_01_15_0300-*.py` - Migration for spool_adjustment table - `spoolman/api/v1/models.py` - Added `SpoolAdjustment` Pydantic model - `spoolman/api/v1/spool.py` - Added comment field to use/measure endpoints, GET /spool/{id}/adjustments endpoint - `spoolman/database/spool.py` - Updated use_weight, use_length, measure functions to record adjustments 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 ### Issue #10 Complete - Database Import/Export Full backup/restore functionality for migrating between databases (PostgreSQL ↔ SQLite). Backend (`spoolman/api/v1/backup.py`): - `GET /api/v1/backup/export` - Downloads all data as single JSON file - `POST /api/v1/backup/import` - Imports JSON with `replace` option - Exports: vendors, filaments, spools, spool_adjustments, settings - Handles ID remapping to maintain relationships - Requires `python-multipart` package for file uploads Frontend (`client/src/pages/settings/generalSettings.tsx`): - "Backup & Restore" section in Settings page - "Download Backup" button for export - File upload with "Import (Merge)" or "Import (Replace All)" options - Confirmation dialog for destructive replace operation Migration workflow: 1. Export from PostgreSQL instance via Settings → Download Backup 2. Reconfigure container for SQLite 3. Import via Settings → Import (Replace All) --- ## 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. ### 3dfilamentprofiles Sample Data The 3dfp GitHub repo only contains sample data (3 filaments). The full dataset may require: - Using their website API directly (may need auth) - Waiting for them to publish full JSON exports - Current implementation uses `sample-filaments.json` from the repo ### Next Up: Docker Deployment to Unraid Build and deploy this fork as a Docker container on Unraid with SQLite. Steps: 1. Build Docker image from this repo 2. Push to registry (Docker Hub or Gitea) 3. Deploy on Unraid with br0 network (own IP) 4. Use SQLite (no external DB dependency) 5. Import data via Settings → Import ### Future: Issue #9 - Hierarchical Locations Add room/bin/shelf support for locations. See Gitea issue #9 for details.