Browse Source

Add Fusion 360 (.f3d) design file attachments to archives
- Add f3d_path field to PrintArchive model and ArchiveResponse schema
- Add API endpoints for F3D upload/download/delete
- Add cyan badge on archive cards when F3D file is attached
- Add context menu options: Upload/Replace F3D, Download F3D, Remove F3D
- Include F3D files in backup/restore
- Add database migration for f3d_path column
- Add API tests for F3D endpoints
- Rebuild CHANGELOG from GitHub releases

Resolves #90

maziggy 4 months ago
parent
commit
0b2c0c214d

+ 143 - 649
CHANGELOG.md

@@ -2,706 +2,200 @@
 
 All notable changes to Bambuddy will be documented in this file.
 
-## [0.1.6b15] - 2026-01-17
+## [Unreleased]
 
 ### Added
-- **Virtual Printer "Queue" mode** - New mode that archives files and adds them to the print queue:
-  - Three modes now available: Archive (immediate), Review (pending list), Queue (print queue)
-  - Queue mode creates unassigned queue items that can be assigned to a printer later
-  - Renamed old "Queue for Review" to "Review" to avoid confusion with print queue
-- **Unassigned queue items** - Print queue now supports items without an assigned printer:
-  - Queue items can be created without a printer (printer_id nullable)
-  - Queue page shows "Unassigned" filter option and highlights unassigned items in orange
-  - Edit modal allows assigning a printer to unassigned items
-  - Useful for virtual printer uploads where target printer is decided later
-- **Sidebar badge indicators** - Visual indicators on sidebar icons for pending items:
-  - Queue icon shows yellow badge with count of pending queue items
-  - Archive icon shows blue badge with count of pending uploads (virtual printer review items)
-  - Badges auto-update every 5 seconds and on window focus
+- **Fusion 360 design file attachments** - Attach F3D files to archives for complete design tracking:
+  - Upload F3D files via archive context menu ("Upload F3D" / "Replace F3D")
+  - Cyan badge on archive card indicates attached F3D file (next to source 3MF badge)
+  - Click badge to download, or use "Download F3D" in context menu
+  - F3D files included in backup/restore
+  - API tests for F3D endpoints
 
-### Changed
-- Virtual printer mode labels: "Immediate" → "Archive", "Queue for Review" → "Review"
-- Queue page printer filter now includes "Unassigned" option
-
-## [0.1.6b14] - 2026-01-17
-
-### Fixed
-- **Chamber temp showing on printers without sensor in multi-printer setups** - Fixed regression where A1/P1S chamber temperature would appear after adding a second printer:
-  - Root cause: REST API `/printers/{id}/status` endpoint wasn't filtering chamber temp by model
-  - WebSocket broadcasts filtered correctly, but initial REST fetch didn't
-  - Now both REST and WebSocket consistently filter chamber temp for P1P/P1S/A1/A1Mini
-  - Fixes [#82](https://github.com/maziggy/bambuddy/issues/82) (multi-printer regression)
-
-## [0.1.6b13] - 2026-01-16
-
-### Fixed
-- **Queue prints failing on A1 printers** - Fixed "MicroSD Card read/write exception error" when starting prints from queue:
-  - Root cause: Queue uploaded files to `/cache/` but print command referenced root `/`
-  - Queue now uploads to root directory like archive reprint does
-  - Added filename cleaning (handles `.gcode.3mf` double extensions)
-  - Added pre-delete of existing files to avoid FTP 553 errors
-  - Fixes [#86](https://github.com/maziggy/bambuddy/issues/86)
-
-## [0.1.6b12] - 2026-01-16
-
-### Added
-- **Project parts tracking** - Track individual parts/objects separately from print plates:
-  - New "Target Parts" field in project create/edit modal alongside "Target Plates"
-  - Separate progress bars for plates (print jobs) vs parts (objects printed)
-  - Parts count auto-detected from 3MF files when archiving prints
-  - Project cards show both plates and parts counts in footer stats
-  - Example: Voron build with 25 plates producing 150 parts total
-  - Closes [#85](https://github.com/maziggy/bambuddy/issues/85)
-- **Archive quantity auto-detection** - Automatically set parts count when archiving:
-  - Extracts printable object count from 3MF `slice_info.config`
-  - Respects skipped objects (not counted)
-  - Falls back to 1 if extraction fails
-- **Migration script for existing archives** - Update quantities on existing archives:
-  - Run `python scripts/update_archive_quantities.py` to update existing archives
-  - Use `--dry-run` flag to preview changes without applying them
-  - Parses 3MF files to extract correct object counts
-
-### Changed
-- Project stats now correctly distinguish between plates (archive count) and parts (sum of quantities)
-- Project list `completed_count` now represents parts printed, not print jobs
-- Progress calculations: plates use `archive_count/target_count`, parts use `completed_count/target_parts_count`
-
-## [0.1.6b11] - 2026-01-13
+## [0.1.6b8] - 2026-01-17
 
 ### Added
 - **MQTT Publishing** - Publish BamBuddy events to external MQTT brokers for integration with Home Assistant, Node-RED, and other automation platforms:
-  - New "Network" tab in Settings (between Filament and API Keys)
-  - Configure broker hostname, port, username/password, TLS, and topic prefix
-  - Auto-populate port when toggling TLS (1883 ↔ 8883)
-  - Real-time connection status indicator in tab and settings card
-  - Published topics include:
-    - `bambuddy/status` - Online/offline status
-    - `bambuddy/printers/{serial}/status` - Real-time printer state (throttled to 1/sec)
-    - `bambuddy/printers/{serial}/print/started|completed|failed` - Print lifecycle events
-    - `bambuddy/printers/{serial}/ams/changed` - AMS filament changes
-    - `bambuddy/queue/job_added|job_started|job_completed` - Print queue events
-    - `bambuddy/maintenance/alert|reset` - Maintenance notifications
-    - `bambuddy/smart_plugs/on|off|energy` - Smart plug state changes
-    - `bambuddy/archive/created|updated` - Archive events
-  - Supports TLS/SSL encryption with self-signed certificates
-  - Auto-reconnect when settings change
-  - Settings included in backup/restore
-- **FTP Retry moved to Network tab** - FTP retry settings relocated from General to Network tab for better organization
-
-## [0.1.6b10] - 2026-01-11
-
-### Added
-- **AMS color mapping** - Manually select which AMS slot to use for each filament:
-  - ReprintModal: Click dropdown to override auto-matched slots
-  - AddToQueueModal: Collapsible filament mapping section with slot selection
-  - Mapping stored with queued prints and used when print starts
-  - Blue ring indicator shows manually selected slots vs auto-matched
+  - New "Network" tab in Settings for MQTT configuration
+  - Configure broker, port, credentials, TLS, and topic prefix
+  - Real-time connection status indicator
+  - Topics: printer status, print lifecycle, AMS changes, queue events, maintenance alerts, smart plug states, archive events
+- **Virtual Printer Queue Mode** - New mode that archives files and adds them directly to the print queue:
+  - Three modes: Archive (immediate), Review (pending list), Queue (print queue)
+  - Queue mode creates unassigned items that can be assigned to a printer later
+- **Unassigned Queue Items** - Print queue now supports items without an assigned printer:
+  - "Unassigned" filter option on Queue page
+  - Unassigned items highlighted in orange
+  - Assign printer via edit modal
+- **Sidebar Badge Indicators** - Visual indicators on sidebar icons:
+  - Queue icon: yellow badge with pending item count
+  - Archive icon: blue badge with pending uploads count
+  - Auto-updates every 5 seconds and on window focus
+- **Project Parts Tracking** - Track individual parts/objects separately from print plates:
+  - "Target Parts" field alongside "Target Plates"
+  - Separate progress bars for plates vs parts
+  - Parts count auto-detected from 3MF files
+
+### Fixed
+- Chamber temp on A1/P1S - Fixed regression where chamber temperature appeared on printers without sensors in multi-printer setups
+- Queue prints on A1 - Fixed "MicroSD Card read/write exception error" when starting prints from queue
+- Spoolman sync - Fixed Bambu Lab spool detection and AMS tray data persistence
+- FTP downloads - Fixed downloads failing for .3mf files without .gcode extension
+- Project statistics - Fixed inconsistent display between project list and detail views
+- Chamber light state - Fixed WebSocket broadcasts not including light state changes
+- Backup/restore - Improved handling of nullable fields and AMS mapping data
+
+## [0.1.6b7] - 2026-01-12
+
+### Added
+- **AMS Color Mapping** - Manual AMS slot selection in ReprintModal, AddToQueueModal, EditQueueItemModal:
+  - Dropdown to override auto-matched AMS slots with any loaded filament
+  - Blue ring indicator distinguishes manual selections from auto-matches
   - Status indicators: green (match), yellow (type only), orange (not found)
-  - Re-read button to refresh AMS status if spools were swapped
-  - Color names shown in dropdowns and tooltips (decoded from Bambu filament codes or derived from hex)
-
-## [0.1.6b9] - 2026-01-09
-
-### Added
-- **Chamber light control** - Toggle the printer's chamber LED from the UI:
-  - Light button next to camera button at bottom of printer card
-  - Custom icon with on/off states (yellow when lit)
-  - Works on all Bambu Lab printers (X1, P1, A1 series, H2D)
-  - H2D dual chamber lights controlled together
-- **Archive list view improvements** - Full feature parity with card view:
-  - Edit and delete buttons inline with each row
-  - Three-dot menu button for context menu access
-  - All context menu actions (re-print, compare, add to project, etc.)
-- **Archive object count** - Shows number of printable objects on archive cards:
-  - Displays in stats grid (e.g., "3 objs")
-  - Extracted from 3MF metadata automatically
-- **Cross-view archive highlight** - Click an archive in calendar or project view to highlight it:
-  - Switches to card/list view and scrolls to the archive
-  - Yellow border highlight for 5 seconds
-  - Works in card, list, and calendar views
-- **Context menu visual indicator** - Three-dot button on cards and list items:
-  - Shows on hover (desktop) or always visible (mobile)
-  - Provides quick access to context menu actions
-  - Positioned on left side for easy access
-- **Spoolman location clearing** - When spools are removed from AMS, their location field is now cleared in Spoolman:
-  - Previously, location persisted even after spool removal
-  - Now correctly clears "Printer Name - AMS X Slot Y" when spool is no longer present
-- **FTP retry for unreliable WiFi** - Configurable retry logic for all FTP operations:
-  - Enable/disable retry in Settings > General > FTP Retry
-  - Configure retry count (1-10 attempts) and delay (1-30 seconds)
-  - Configurable connection timeout (10-120 seconds, default 30s)
-  - Applies to: 3MF archiving, print uploads, timelapse downloads, firmware updates
-  - Helps P1S, X1C, and other printers with weak WiFi connections
-- **A1/A1 Mini FTP fix** - Resolved FTP upload failures on A1 series printers:
-  - A1 printers have issues with SSL on the FTP data channel
-  - Automatic detection skips data channel SSL for A1 and A1 Mini models
-  - Control channel remains encrypted via implicit FTPS (port 990)
-  - Fixes "read operation timed out" errors during file uploads
-- **Bulk project assignment** - Assign multiple archives to a project at once:
-  - New "Project" button in multi-select toolbar (next to Tags)
-  - Select a project to assign all selected archives
-  - "Remove from project" option to clear assignments
-  - Updates Projects page and Project Detail page instantly
-
-### Fixed
-- **Time format setting not applied** - Fixed 24-hour time format not being respected across the UI:
-  - ETA display on printer cards now uses configured time format
-  - Archive cards and timelapse file lists respect the setting
-  - AMS history charts use the configured format
-  - Project timeline, queue page, notification logs, and system info all updated
-  - Settings > General > Time Format now works consistently everywhere
-- **QR code endpoint** - Fixed 500 error on archive QR code generation:
-  - Added `qrcode[pil]` to requirements.txt
-  - Improved error handling for missing dependencies
-  - Fixed PIL Image resizing method
-
-## [0.1.6b8] - 2026-01-08
-
-### Added
-- **Reprint modal print options** - Configure print settings when reprinting from archive:
-  - Bed leveling toggle (default: enabled)
-  - Flow calibration toggle (default: disabled)
-  - Vibration calibration toggle (default: enabled)
-  - First layer inspection toggle (default: disabled)
-  - Timelapse recording toggle (default: disabled)
-  - Collapsible "Print Options" section with toggle switches
-  - Settings sent to printer match Bambu Studio's format exactly
-
-### Fixed
-- **AMS mapping for multi-color reprints** - Fixed filament slot mapping for multi-color prints:
-  - AMS mapping now matches Bambu Studio's exact format with `ams_mapping2` detailed structure
-  - Correct handling of 3MF filament slot IDs (1-indexed to 0-indexed conversion)
-  - Unused filament slots properly filled with `-1` placeholder
-  - Prevents duplicate tray assignment when multiple filaments match the same type
-  - Resolves "stuck at filament change" issue on X1C and other multi-AMS printers
-- **Print command format** - Updated MQTT print command to match Bambu Studio exactly:
-  - Added `auto_bed_leveling`, `cfg`, `extrude_cali_flag`, `extrude_cali_manual_mode`, `nozzle_offset_cali` fields
-  - Uses American spelling `bed_leveling` (not British `bed_levelling`)
-  - Proper `sequence_id` format matching official apps
-
-## [0.1.6b7] - 2026-01-04
-
-### Added
-- **Support bundle for issue reporting** - Collect debug logs and system info for troubleshooting:
-  - Toggle debug logging from System Information page
-  - Debug logging indicator banner shows across all pages with live timer
-  - Download support bundle as ZIP file with sanitized logs and system info
-  - Privacy-focused: filters sensitive data (passwords, tokens, emails, IPs)
-  - Clear explanation of what data is/isn't collected
-- **Firmware update helper** - Check and upload firmware updates for LAN-only printers:
-  - Automatic firmware update checking against Bambu Lab's servers
-  - Orange "Update" badge on printer cards when updates are available
-  - Click badge to open firmware update modal with version info and release notes
-  - One-click firmware upload to printer's SD card via FTP
-  - Real-time upload progress tracking with actual bytes transferred
-  - Step-by-step instructions for triggering update from printer screen
-  - Supports all Bambu Lab printer models (X1C, X1, X1E, P1S, P1P, P2S, A1, A1 Mini, H2D, H2C, H2S)
-  - Firmware files cached locally for faster re-uploads
+  - Shared color utility with ~200 Bambu color mappings
+  - Fixed AMS mapping format to match Bambu Studio exactly
+- **Print Options in Reprint Modal** - Bed leveling, flow calibration, vibration calibration, first layer inspection, timelapse toggles
+- **Time Format Setting** - New date utilities applied to 12 components, fixes archive times showing in UTC
+- **Statistics Dashboard Improvements** - Size-aware rendering for PrintCalendar, SuccessRateWidget, TimeAccuracyWidget, FilamentTypesWidget, FailureAnalysisWidget
+- **Firmware Update Helper** - Check firmware versions against Bambu Lab servers for LAN-only printers with one-click upload
+- **FTP Reliability** - Configurable retry (1-10 attempts, 1-30s delay), A1/A1 Mini SSL fix, configurable timeout
+- **Bulk Project Assignment** - Assign multiple archives to a project at once from multi-select toolbar
+- **Chamber Light Control** - Light toggle button on printer cards
+- **Support Bundle Feature** - Debug logging toggle with ZIP generation for issue reporting
+- **Archive Improvements** - List view with full parity, object count display, cross-view highlighting, context menu button
+- **Maintenance Improvements** - wiki_url field for documentation links, model-specific Bambu Lab wiki URLs
+- **Spoolman Integration** - Clear location when spools removed from AMS during sync
+
+### Fixed
+- Browser freeze from CameraPage WebSocket
+- Project card filament badges showing duplicates and raw color codes
+- Print object label positioning in skip objects modal
+- Printer hour counter not updated on backend restart
+- Virtual printer excluded from discovery
+- Print cover fetch in Docker environments
+- Archive delete safety checks prevent deleting parent dirs
 
 ## [0.1.6b6] - 2026-01-04
 
 ### Added
-- **Resizable printer cards** - Adjust printer card size from the Printers page toolbar:
-  - Four sizes: Small, Medium (default), Large, XL
-  - Plus/minus buttons in toolbar header
-  - Size preference saved to localStorage
-  - Responsive grid adapts to selected size
-- **Queue Only mode** - Stage prints without automatic scheduling:
-  - New "Queue Only" option when adding prints to queue
-  - Staged prints show purple "Staged" badge
-  - Play button to manually release staged prints to the queue
-  - Edit queue items to switch between ASAP, Scheduled, and Queue Only modes
-  - Useful for preparing print batches before activating
-- **Virtual printer model selection** - Choose which Bambu printer model to emulate:
-  - Dropdown in Settings > Virtual Printer to select model
-  - Supports X1 series (X1C, X1, X1E), P series (P1S, P1P, P2S), A1 series (A1, A1 Mini), and H2 series (H2D, H2C, H2S)
-  - Affects how slicers detect and interact with the virtual printer
-  - Model change automatically restarts the virtual printer (no manual disable/enable needed)
-  - Models sorted alphabetically in dropdown
-- **Pending upload delete confirmation** - Confirmation modal when discarding pending uploads in queue review mode
-- **Tasmota admin link** - Direct link to Tasmota web interface on smart plug cards:
-  - Auto-login using stored credentials (when available)
-  - Opens in new tab for quick access to plug settings
-- **Debug logging** - Added debug logging for printer hour counter and AMS slot mapping
-- **Demo video recorder** - Playwright-based tool for recording demo videos:
-  - Automated walkthrough of all pages
-  - Outputs WebM format, easy conversion to MP4
-  - Located in `demo-video/` directory
+- **Resizable Printer Cards** - Four sizes (S/M/L/XL) with +/- buttons in toolbar
+- **Queue Only Mode** - Stage prints without auto-start, release when ready with purple "Staged" badge
+- **Virtual Printer Model Selection** - Choose which Bambu printer model to emulate
+- **Tasmota Admin Link** - Quick access to smart plug web interface with auto-login
+- **Pending Upload Delete Confirmation** - Confirmation modal when discarding pending uploads
 
 ### Fixed
-- **Camera stream reconnection** - Improved detection of stuck camera streams with automatic reconnection:
-  - Periodic stall detection checks every 5 seconds
-  - Automatic reconnection when stream stops receiving frames
-  - New `/api/v1/printers/{id}/camera/status` endpoint for stream health monitoring
-- **Spoolman sync** - Fixed sync issues with Spoolman integration:
-  - Now only matches Bambu Lab vendor filaments when syncing
-  - Prevents incorrect matching with third-party filaments by color alone
-  - Improved filament matching accuracy
-- **Archive card context menu** - Fixed context menu positioning issues (#46)
-- **Printer card cover image** - Fixed wrong cover image displayed for multi-plate print files
-- **Skip objects modal** - Fixed object ID markers not correctly positioned over build plate preview:
-  - Now uses `bbox_all` from plate metadata for accurate coordinate mapping
-  - Markers correctly position relative to the actual object bounds in the preview image
-  - Works correctly for multi-plate projects
-- **Active AMS slot display (H2D)** - Fixed incorrect slot display on H2D printers with multiple AMS units:
-  - Now parses `snow` field from `device.extruder.info` which contains actual AMS ID
-  - Previously picked first AMS on the extruder, causing wrong display when multiple AMS connected
-  - Example: Switching from B2 to C1 now correctly shows C1 instead of A1
-- **Spoolman link function** - Improved "Link to Spoolman" in AMS slot detail modal
-- **GCode viewer** - Minor improvements to GCode visualization
-- **Cover image retrieval** - Improved reliability of cover image extraction
-- **Virtual printer SSDP model codes** - Corrected model codes for slicer compatibility:
-  - C11=P1P, C12=P1S (were incorrectly swapped)
-  - N7=P2S (was incorrectly using C13 which is X1E)
-  - 3DPrinter-X1-Carbon for X1C (full model name format)
-- **Virtual printer serial prefixes** - Fixed serial number prefixes to match real printers:
-  - Based on actual Bambu Lab serial number format (MMM??RYMDDUUUUU)
-  - X1C=00M, P1S=01P, P1P=01S, P2S=22E, A1=039, A1M=030, H2D=094, X1E=03W
-- **Virtual printer startup model** - Fixed model not loading from database on container restart:
-  - Virtual printer now correctly restores the saved model on startup
-  - Previously always started with default X1C model regardless of saved setting
-- **Virtual printer model change** - Fixed model changes not taking effect while running:
-  - Model changes now automatically restart the virtual printer
-  - Removed frontend restriction that required manual disable/enable
-- **Docker certificate persistence** - Fixed virtual printer certificate storage:
-  - Removed unused `bambuddy_vprinter` volume (was mounting to wrong path)
-  - Certificates now correctly persist in `bambuddy_data` volume
-  - Added optional bind mount for sharing certs between Docker and native installations
-
-### Changed
-- **Virtual printer setup documentation** - Improved setup instructions:
-  - Prominent "Setup Required" warning in UI linking to documentation
-  - Certificate must **replace** the last cert in slicer's printer.cer file (not append!)
-  - Clear guidance: one CA only per slicer, replace when switching hosts
-  - Platform-specific instructions for Linux, Docker, macOS, Windows, Unraid, Synology, TrueNAS, Proxmox
-  - Added troubleshooting for TLS connection errors
-
-### Tests
-- Added integration tests for print queue API endpoints (16 new tests)
-- Tests cover queue CRUD, manual_start flag, and start/cancel endpoints
-- Added unit tests for virtual printer model configuration (3 new tests)
-- Updated VirtualPrinterSettings tests for new UI layout and model codes
-- Fixed virtual printer tests with outdated model codes (BL-P001 → 3DPrinter-X1-Carbon)
+- Camera stream reconnection with automatic recovery from stalled streams
+- Active AMS slot display for H2D printers with multiple AMS units
+- Spoolman sync matching only Bambu Lab vendor filaments
+- Skip objects modal object ID markers positioning
+- Virtual printer model codes, serial prefixes, startup model, certificate persistence
+- Archive card context menu positioning
 
 ## [0.1.6b5] - 2026-01-02
 
 ### Added
-- **Pre-built Docker images** - Ready-to-use container images on GitHub Container Registry:
-  - Pull directly: `docker pull ghcr.io/maziggy/bambuddy:latest`
-  - Multi-architecture support: `linux/amd64` and `linux/arm64` (Raspberry Pi 4/5)
-  - No build required - just `docker compose up -d`
-  - Automatic architecture detection - Docker pulls the right image for your system
-- **Spoolman Link Spool feature** - Manually link existing Spoolman spools to AMS trays:
-  - Hover over any AMS slot to see "Link to Spoolman" button (when Spoolman enabled)
-  - Select from list of unlinked Spoolman spools
-  - Automatically sets the `extra.tag` field in Spoolman for proper sync
-  - Useful for connecting existing Spoolman inventory to physical spools
-- **Spool UUID display** - View and copy Bambu Lab spool UUID in AMS hover card:
-  - Shows first 8 chars of UUID with copy button
-  - Full UUID copied to clipboard (with fallback for HTTP contexts)
-  - Only visible when Spoolman integration is enabled
-- **Spoolman sync feedback** - Improved sync result display:
-  - Shows count of successfully synced spools
-  - Lists skipped spools with reasons (e.g., "Non-Bambu Lab spool")
-  - Expandable list when more than 5 spools skipped
-  - Color swatches for easy identification
-- **Printer control buttons** - Stop and Pause/Resume buttons on printer cards when printing:
-  - Stop button cancels the current print job
-  - Pause/Resume toggle for pausing and resuming prints
-  - Confirmation modals for all actions to prevent accidental clicks
-  - Toast notifications for action feedback
-- **Skip objects** - Skip individual objects during a print:
-  - Skip button in print status section (top right) when printing with 2+ objects
-  - Modal shows preview image with object ID markers overlaid
-  - Large ID badges to easily match with printer display
-  - Click to skip any object - it will not be printed
-  - Skipped objects shown with strikethrough styling and red badge on button
-  - Skip only available after layer 1 (printer limitation) with warning message
-  - Objects automatically loaded when print starts from 3MF metadata
-  - Parses skipped objects from printer MQTT for state persistence
-  - Light and dark theme support
-  - Close with ESC key or click outside
-  - Requires "Exclude Objects" option enabled in slicer
-- **Interactive API Browser** - Explore and test all API endpoints directly in Bambuddy:
-  - Settings → API Keys now includes a full API browser
-  - Fetches OpenAPI schema automatically
-  - Endpoints grouped by category (printers, archives, settings, etc.)
-  - Expandable sections with color-coded method badges (GET, POST, PATCH, DELETE)
-  - Parameter inputs for path, query, and JSON body
-  - Auto-populates request body with schema examples
-  - Live API execution with response display (status, timing, formatted JSON)
-  - Paste API key to test authenticated endpoints
-  - Search to filter endpoints across all categories
-  - Two-column layout: API key management + API browser side-by-side
-- **AMS slot RFID re-read** - Re-read RFID data for individual AMS slots:
-  - Menu button (⋮) appears on hover over AMS slots
-  - "Re-read RFID" option triggers filament info refresh
-  - Loading indicator shows while re-read is in progress
-  - Automatically tracks printer status to clear indicator when complete
-  - Menu hidden when printer is busy (printing)
-- **Print quantity tracking** - Track number of items per print job for project progress:
-  - Set "Items Printed" quantity when editing archived prints
-  - Project stats now show total items vs print jobs
-  - Progress bar tracks items toward target count
-  - Useful for batch printing (e.g., 10 copies in one print = 10 items)
-  - Default quantity of 1 for backwards compatibility
-- **Fan status display** - Real-time fan speeds in new Controls section:
-  - Part cooling fan, Auxiliary fan, Chamber fan status
-  - Distinct icons for each fan type (Fan, Wind, AirVent)
-  - Dynamic coloring: active fans show colored, off fans show gray
-  - Percentage display (0-100%)
-  - Real-time updates via WebSocket
-  - Always visible on printer cards in expanded view
-
-### Changed
-- **Printer card layout** - Reorganized expanded view with new Controls section:
-  - Temperature display is now standalone (no longer shares row with buttons)
-  - New Controls section contains fan status (left) and print buttons (right)
-  - Removed divider lines before Controls and Filaments sections for cleaner look
-- **Cover image availability** - Print cover image now shown in PAUSE/PAUSED states (not just RUNNING) for skip objects modal
-- **Spoolman info banner** - Updated settings UI with clearer sync documentation
+- **Pre-built Docker Images** - Pull directly from GitHub Container Registry (ghcr.io)
+- **Printer Controls** - Stop and Pause/Resume buttons on printer cards with confirmation modals
+- **Skip Objects** - Skip individual objects during print without canceling entire job
+- **Spoolman Improvements** - Link Spool, UUID Display, Sync Feedback
+- **AMS Slot RFID Re-read** - Re-read filament info via hover menu
+- **Print Quantity Tracking** - Track items per print for project progress
 
 ### Fixed
-- **Spoolman spool creation** - Fixed 400 Bad Request error when creating new spools:
-  - Spoolman `extra` field values must be valid JSON
-  - Now properly JSON-encodes the `tag` value
-  - Affects both auto-sync and manual link operations
-
-### Tests
-- Added integration tests for printer control endpoints (stop, pause, resume)
-- Added integration tests for AMS slot refresh endpoint
-- Added integration tests for skip objects endpoints (get objects, skip objects, objects with positions)
+- Spoolman 400 Bad Request when creating spools
+- Update module for Docker based installations
 
 ## [0.1.6b4] - 2026-01-01
 
-### Added
-- **Docker update detection** - Automatically detects Docker installations and shows appropriate update instructions instead of failing with git errors
-- **Camera popup window improvements**
-  - Auto-resize to fit video resolution on first open
-  - Persist window size and position to localStorage
-  - Restore saved window state for subsequent opens
-- **Slicer protocol handler** - OS detection for correct protocol (Windows: `bambustudio://`, macOS/Linux: `bambustudioopen://`)
+### Changed
+- Refactored AMS section for better visual grouping and spacing
 
 ### Fixed
-- **Maintenance duration display** - Show weeks instead of imprecise months for better countdown precision
-- **Print hours display** - Convert large hour values to readable units (e.g., 478h → 3w, 100h → 4d)
-- **Printer hour counter** - Fixed runtime_seconds not incrementing during prints (first timestamp was set but never committed)
-
-### Changed
-- **Printer cards** - Refactored AMS section for better visual grouping and spacing
+- Printer hour counter not incrementing during prints
+- Slicer protocol OS detection (Windows: bambustudio://, macOS/Linux: bambustudioopen://)
+- Camera popup window auto-resize and position persistence
+- Maintenance page duration display with better precision
+- Docker update detection for in-app updates
 
 ## [0.1.6b3] - 2025-12-31
 
 ### Added
-- **Customizable Theme System** - Comprehensive theme customization with independent settings for dark and light modes:
-  - **Style**: Classic (clean shadows), Glow (accent-colored glow effects), Vibrant (dramatic deep shadows)
-  - **Background**: Neutral, Warm, Cool (light mode) + OLED, Slate, Forest (dark mode only)
-  - **Accent Colors**: Green, Teal, Blue, Orange, Purple, Red
-  - All combinations work together (e.g., Glow style + Forest background + Teal accent)
-  - Settings sync across devices via database
-  - Live preview in Settings → Appearance
+- Confirmation modal for quick power switch in sidebar
 
 ### Fixed
-- **Printer hour counter** - Fixed bug in printer's hour counter display
-
-### Changed
-- **Sidebar power switch** - Added confirmation modal to sidebar's quick power switch
+- Printer hour counter inconsistency between card and maintenance page
+- Improved printer hour tracking accuracy with real-time runtime counter
+- Add Smart Plug modal scrolling on lower resolution screens
+- Excluded virtual printer from discovery results
+- Bottom sidebar layout
 
 ## [0.1.6b2] - 2025-12-29
 
 ### Added
-- **Virtual Printer** - Bambuddy now emulates a Bambu Lab printer on your network! Send prints directly from Bambu Studio or Orca Slicer without needing a physical printer connection. Features include:
-  - SSDP discovery (printer appears automatically in slicer)
-  - Secure TLS/MQTT communication with auto-generated certificates
-  - Queue mode (prints go to pending uploads) or auto-start mode
-  - Configurable access code for authentication
-  - Works with Docker (requires `network_mode: host`)
-  - Persistent certificates across container rebuilds via volume mount
+- **Virtual Printer** - Emulates a Bambu Lab printer on your network:
+  - Auto-discovery via SSDP protocol
+  - Send prints directly from Bambu Studio/Orca Slicer
+  - Queue mode or Auto-start mode
+  - TLS 1.3 encrypted MQTT + FTPS with auto-generated certificates
+- Persistent archive page filters
 
 ### Fixed
-- **Backup/restore for virtual printer settings** - Virtual printer settings (enabled, access code, mode) now correctly persist after restore without being overwritten by auto-save
+- AMS filament matching in reprint modal
+- Archive card cache bug with wrong cover image
+- Queueing module re-queue modal
 
 ## [0.1.6b] - 2025-12-28
 
 ### Added
-- **Tasmota device discovery** - Automatically discover Tasmota smart plugs on your network. Click "Discover Tasmota Devices" in the Add Smart Plug modal to scan your local subnet. Supports devices with and without authentication.
-- **Switchbar for quick smart plug access** - New sidebar widget for controlling smart plugs without leaving the current page. Enable "Show in Switchbar" for any plug to add it to the quick access panel. Shows real-time status, power consumption, and on/off controls.
-- **Timelapse editor** - Edit timelapse videos with trim, speed adjustment (0.25x-4x), and music overlay. Uses FFmpeg for server-side processing with browser-based preview.
-- **AMS filament preview** - Reprint modal shows filament comparison between what the print requires and what's currently loaded in the AMS. Compares both type and color with visual indicators (green=match, yellow=color mismatch, orange=type mismatch). Includes Re-read button to refresh AMS status from printer, fuzzy color matching for similar colors, and shows AMS slot location for each matched filament.
-- **File type badge** - Archive cards now show GCODE (green) or SOURCE (orange) badge to indicate whether the file is a sliced print-ready file or source-only.
-- **Docker printer discovery** - Subnet scanning for discovering printers when running in Docker with `network_mode: host`. Automatically detects Docker environment and shows subnet input field in Add Printer dialog.
-- **Printer model mapping** - Discovery now shows friendly model names (X1C, H2D, P1S) instead of raw SSDP codes (BL-P001, O1D, C11).
-- **Discovery API tests** - Comprehensive test coverage for discovery endpoints.
-- **Project filament colors** - Project cards now display filament color swatches from assigned archives.
-- **BOM filter** - Hide completed BOM items with "Hide done" toggle on project detail page.
-- **Projects in backup/restore** - Projects, BOM items, and attachments now included in database backup/restore.
-- **Attachment file validation** - File type validation for project attachments (images, documents, 3D files, archives, scripts, configs).
-
-### Changed
-- **Timelapse viewer** - Default playback speed changed from 2x to 1x.
-- **GitHub issue template** - Added mandatory printer firmware version field and LAN-only mode checkbox for better bug reports.
-- **Docker compose** - Clearer comments explaining `network_mode: host` requirement for printer discovery and camera streaming.
-- **Project card design** - Enhanced visual polish with gradients, shadows, and glow effects on hover.
-- **Project page layout** - Improved spacing and padding on project list and detail pages.
-- **Delete confirmations** - Replaced browser confirm dialogs with styled confirmation modals.
+- **Smart Plugs** - Tasmota device discovery and Switchbar quick access widget
+- **Timelapse Editor** - Trim, speed adjustment (0.25x-4x), and music overlay
+- **Printer Discovery** - Docker subnet scanning, printer model mapping, detailed status stages
+- **Archives & Projects** - AMS filament preview, file type badges, project filament colors, BOM filter
+- **Maintenance** - Custom maintenance types with manual per-printer assignment
+- Delete printer options to keep or delete archives
 
 ### Fixed
-- **Notification module** - Fixed bug where notifications were sent even when printer was offline.
-- **Attachment uploads** - Fixed file attachments not persisting due to SQLAlchemy JSON column mutation detection.
-- **Camera stream stability** - Fixed stream stopping after a few minutes by increasing ffmpeg read timeout (10s→30s), adding buffer options, and implementing auto-reconnection with exponential backoff in the frontend.
+- Notifications sent when printer offline
+- Camera stream stopping with auto-reconnection
+- A1/P1 camera streaming with extended timeouts
+- Attachment uploads not persisting
+- Total print hours calculation
 
 ## [0.1.5] - 2025-12-19
 
-### Fixed
-- **Browser freeze on print completion** - Fixed freeze when camera stream was open during print completion by using buffered camera frames instead of spawning duplicate ffmpeg processes
-- **Printer status "timelapse" effect** - Fixed issue where navigating to printer page after print showed metrics animating slowly from mid-print values to final state; printer_status messages now bypass the throttled queue
-- **Timelapse auto-download** - Complete rewrite with retry mechanism and multiple path support
-- **Timelapse detection for H2D** - H2D sends timelapse status in ipcam.timelapse field, not xcam.timelapse
-- **Reprint from archive** - Fixed bug where print button sent slicer source file instead of sliced gcode
-- **Import shadowing bugs** - Fixed ArchiveService import shadowing causing "cannot access local variable" error
-- **Timelapse race condition** - xcam data was parsed before print state was set
-
-### Added
-- **Failure reason detection** - Auto-detects failure reasons from HMS errors:
-  - Filament runout (Module 0x07)
-  - Layer shift (Module 0x0C)
-  - Clogged nozzle (Module 0x05)
-- **Hide failed prints filter** - Toggle to hide failed/aborted prints with localStorage persistence
-- **Docker test suite** - Comprehensive tests for build, backend, frontend, and integration
-- **Pre-commit hooks** - Ruff linter and formatter for code quality
-- **Code quality tests** - Static analysis to catch import shadowing bugs automatically
-
-### Changed
-- **Timelapse viewer** - Default playback speed changed from 0.5x to 2x
-- **Archive badges** - Shows "cancelled" for aborted prints, "failed" for failed prints
-- **WebSocket optimization** - Removed large raw_data field from print_complete message; reduced throttle to 100ms for smoother updates
-
-### Docker
-- Added ffmpeg to Docker image
-- Fixed build warnings (debconf, pip root user)
-- Added comprehensive Docker documentation to README
-- Added `--pull` flag to ensure fresh base images
-
-## [0.1.5b6] - 2025-12-12
-
-  Notifications:
-  - Separate AMS and AMS-HT notification switches (one per device type)
-  - Fix notification variables not showing (duration, filament, estimated_time)
-  - Add fallback values for empty notification variables ("Unknown" instead of blank)
-
-  Settings:
-  - Fix API keys badge count only showing after visiting tab
-  - Move External Links card to third column above Updates
-  - Add Release Notes modal for viewing full notes before updating
-
-  Statistics:
-  - Fix filament usage trends not showing (wrong API parameters)
-  - Move dashboard controls (Hidden, Reset Layout) to header row
-
-  Camera:
-  - Fix ffmpeg processes not killed when closing webcam window
-  - Add /camera/stop endpoint with POST support for sendBeacon
-  - Track active streams and proper cleanup on disconnect
-
-  Documentation:
-  - Update README with missing features (camera streaming, AMS/AMS-HT monitoring,
-    chamber control, printer control, AI detection, calibration, energy tracking,
-    database backup/restore, system info dashboard)
-
-## [0.1.5b5] - 2025-12-11
-
-### Added
-- Anonymous telemetry system with opt-out support
-- System info page with database and resource statistics
-
-## [0.1.5b4] - 2025-12-11
-
-New Features
-
-    Mobile PWA Support - Progressive Web App support for mobile devices
-    AMS Humidity/Temperature History - Clickable indicators open charts with 6h/24h/48h/7d history, min/max/avg statistics, and threshold reference lines
-    Webhooks & API Keys - API key authentication with granular permissions for external integrations
-    System Info Page - New page showing system information
-    Multi-plate Cover Image - Archive cards now show cover image of the printed plate for multi-plate files
-    Quick Notification Disable - Button to quickly disable notifications
-    Projects / Print Grouping - Group related prints into projects with progress tracking
-    Full-Text Search (FTS5) - Efficient search across print names, filenames, tags, notes, designer, and filament type
-    Failure Analysis - Dashboard widget showing failure rate with correlations and trends
-    Archive Comparison - Compare 2-5 archives side-by-side with highlighted differences
-    CSV/Excel Export - Export archives and statistics with current filters
-
-Improvements
-
-    Improved archive card context menu with submenu support
-    Improved notification scheduler and templates
-    Improved auto power off scheduler
-    Improved email notification provider
-    Configurable AMS data retention (default 30 days)
-
-Bug Fixes
-
-    Fixed bug where not all AMS spools were synced to Spoolman
-    Fixed bug where external links were not respected by hotkeys
-    Fixed context menu submenu not showing
-    Fixed project card thumbnails using correct API endpoint
-    Fixed archive PATCH 500 error (FTS5 index rebuild)
-    Fixed clipboard API fallback for HTTP contexts
-
-Infrastructure
-
-    Added comprehensive automated testing (pytest, vitest, playwright)
-    GitHub Actions CI/CD workflow for automated testing
-    Removed PWA push notifications
-
-## [0.1.5b4] - 2025-12-10
-
 ### Added
-- Docker support with containerized deployment
-- Comprehensive mobile support with responsive navigation
-  - Hamburger drawer navigation for mobile (< 768px)
-  - Touch gesture context menus with long press support
-  - WCAG-compliant touch targets (44px minimum)
-  - Safe area insets support for notched devices
-- External links can be embedded into sidebar navigation
-- External links included in backup/restore module
-- Filament spool fill levels on printer cards
-- Issue and pull request templates
-
-### Changed
-- Improved external link module with better icon layout
-- Documentation moved to separate repository
-
-### Fixed
-- Notification module now properly saves newly added notification types
-- External link icons layout improvements
-
-## [0.1.5b3] - 2025-12-09
-
-### Added
-- Comprehensive backup/restore module improvements
-
-### Fixed
-- Switched off printers no longer incorrectly show as active
-- os.path issue in update module
-
-## [0.1.5b2] - 2025-12-09
-
-### Added
-- User options to backup module
-
-### Changed
-- App renamed to "Bambuddy"
-
-### Fixed
-- HTTP 500 error in backup module
-
-## [0.1.5b] - 2025-12-08
-
-### Added
-- Smart plug monitoring and scheduling
-- Daily digest notifications
-- Notification template system
-- Maintenance interval type: calendar days
-- Cloud Profiles template visibility and preset diff view
-- AMS humidity/temperature indicators with configurable thresholds
-- Printer image on printer card
-- WiFi signal strength indicator on printer card
-- Power switch dropdown for offline printers
-- MQTT debug viewer with filter and search
-- Total printer hours display on printer card
-- AMS discovery module
-- Dual-nozzle AMS wiring visualization
+- **Docker Support** - One-command deployment with docker compose
+- **Mobile PWA** - Full mobile support with responsive navigation and touch gestures
+- **Projects** - Group related prints with progress tracking
+- **Archive Comparison** - Compare 2-5 archives side-by-side
+- **Smart Plug Automation** - Tasmota integration with auto power-on/off
+- **Telemetry Dashboard** - Anonymous usage statistics (opt-out available)
+- **Full-Text Search** - Efficient search across print names, filenames, tags, notes, designer, filament type
+- **Failure Analysis** - Dashboard widget showing failure rate with correlations and trends
+- **CSV/Excel Export** - Export archives and statistics with current filters
+- **AMS Humidity/Temperature History** - Clickable indicators with charts and statistics
+- **Daily Digest Notifications** - Consolidated daily summary
+- **Notification Template System** - Customizable message templates
+- **Webhooks & API Keys** - API key authentication with granular permissions
+- **System Info Page** - Database and resource statistics
+- **Comprehensive Backup/Restore** - Including user options and external links
 
 ### Changed
 - Redesigned AMS section with BambuStudio-style device icons
 - Tabbed design and auto-save for settings page
-- Replaced camera settings with WiFi signal in top bar
-- Completely refactored K-profile module
-- Refactored maintenance settings
+- Improved archive card context menu with submenu support
+- WebSocket throttle reduced to 100ms for smoother updates
 
 ### Fixed
-- HMS module bug
-- Camera buttons appearance in light theme
+- Browser freeze on print completion when camera stream was open
+- Printer status "timelapse" effect after print completion
+- Complete rewrite of timelapse auto-download with retry mechanism
+- Reprint from archive sending slicer source file instead of sliced gcode
+- Import shadowing bugs causing "cannot access local variable" error
+- Archive PATCH 500 error
+- ffmpeg processes not killed when closing webcam window
 
 ### Removed
-- Control page (removed all related code)
-
-## [0.1.4] - 2025-12-01
-
-### Added
-- Multi-language support
-- Auto app update functionality
-- Maintenance module with notifications
-- Spoolman support for adding unknown Bambu Lab spools
-- Source 3MF file upload to archive cards
-
-### Fixed
-- K profiles retrieval from printer
-
-## [0.1.3] - 2025-11-30
-
-### Added
-- Push notification support (WhatsApp, ntfy, Pushover, Telegram, Email)
-- K profile management
-- Configurable logging with log levels
-- Sidebar item reordering
-- Default view settings
-- Option to track energy per print or in total
-- Timelapse viewer improvements
-
-### Fixed
-- WebSocket connection stability
-- Power stage updates not reflecting in frontend
-
-## [0.1.2-bugfix] - 2025-11-30
-
-### Fixed
-- WebSocket disconnection issues
-
-## [0.1.2-final] - 2025-11-29
-
-### Added
-- Print scheduling and queueing system
-- Power consumption cost calculation
-- HMS (Health Management System) error handling on printer cards
-- Camera snapshot on print completion
-- Power switch and automation controls on printer card
-- Timelapse video player with speed controls
-- Print time accuracy calculation
-- Duplicate detection and filtering
-
-### Changed
-- Unified printer card layout
-
-### Fixed
-- Auto poweroff feature improvements
-- Archive file handling on print start
-- Statistics display issues
-
-## [0.1.2] - 2025-11-28
-
-### Added
-- HMS health status monitoring
-- MQTT debug log window
-- Tasmota smart power plug support with automation
-- Project page viewer and editor
-- Button to show/hide disconnected printers
-- Favicons
-
-### Fixed
-- Collapsed sidebar layout
-
-## [0.1.1] - 2025-11-28
-
-### Added
-- Initial public release
-- Multi-printer support via MQTT
-- Real-time printer status monitoring
-- Print archives with history tracking
-- Statistics and analytics dashboard
-- Timelapse video support
-- Light and dark theme support
-
----
-
-For more information, visit the [Bambuddy GitHub repository](https://github.com/maziggy/bambuddy).
+- Control page
+- PWA push notifications (replaced with standard notification providers)

+ 107 - 0
backend/app/api/routes/archives.py

@@ -65,6 +65,7 @@ def archive_to_response(
         "thumbnail_path": archive.thumbnail_path,
         "timelapse_path": archive.timelapse_path,
         "source_3mf_path": archive.source_3mf_path,
+        "f3d_path": archive.f3d_path,
         "duplicates": duplicates,
         "duplicate_count": duplicate_count if duplicates is None else len(duplicates),
         "print_name": archive.print_name,
@@ -2477,3 +2478,109 @@ async def delete_source_3mf(
     await db.commit()
 
     return {"status": "deleted"}
+
+
+# =============================================================================
+# F3D API (Fusion 360 Design Files)
+# =============================================================================
+
+
+@router.post("/{archive_id}/f3d")
+async def upload_f3d(
+    archive_id: int,
+    file: UploadFile = File(...),
+    db: AsyncSession = Depends(get_db),
+):
+    """Upload a Fusion 360 design file for an archive."""
+    result = await db.execute(select(PrintArchive).where(PrintArchive.id == archive_id))
+    archive = result.scalar_one_or_none()
+    if not archive:
+        raise HTTPException(404, "Archive not found")
+
+    if not file.filename or not file.filename.endswith(".f3d"):
+        raise HTTPException(400, "File must be a .f3d file")
+
+    # Get archive directory and create f3d subdirectory
+    file_path = settings.base_dir / archive.file_path
+    archive_dir = file_path.parent
+    f3d_dir = archive_dir / "f3d"
+    f3d_dir.mkdir(exist_ok=True)
+
+    # Delete old F3D file if exists
+    if archive.f3d_path:
+        old_f3d_path = settings.base_dir / archive.f3d_path
+        if old_f3d_path.exists():
+            old_f3d_path.unlink()
+
+    # Save the F3D file - preserve original filename
+    f3d_filename = file.filename
+    f3d_path = f3d_dir / f3d_filename
+
+    content = await file.read()
+    f3d_path.write_bytes(content)
+
+    # Update archive with F3D path (relative to base_dir)
+    archive.f3d_path = str(f3d_path.relative_to(settings.base_dir))
+
+    await db.commit()
+    await db.refresh(archive)
+
+    return {
+        "status": "uploaded",
+        "f3d_path": archive.f3d_path,
+        "filename": f3d_filename,
+    }
+
+
+@router.get("/{archive_id}/f3d")
+async def download_f3d(
+    archive_id: int,
+    db: AsyncSession = Depends(get_db),
+):
+    """Download the Fusion 360 design file."""
+    result = await db.execute(select(PrintArchive).where(PrintArchive.id == archive_id))
+    archive = result.scalar_one_or_none()
+    if not archive:
+        raise HTTPException(404, "Archive not found")
+
+    if not archive.f3d_path:
+        raise HTTPException(404, "No F3D file attached to this archive")
+
+    f3d_path = settings.base_dir / archive.f3d_path
+    if not f3d_path.exists():
+        raise HTTPException(404, "F3D file not found on disk")
+
+    # Use the actual filename from the path
+    filename = f3d_path.name
+
+    return FileResponse(
+        path=f3d_path,
+        filename=filename,
+        media_type="application/octet-stream",
+    )
+
+
+@router.delete("/{archive_id}/f3d")
+async def delete_f3d(
+    archive_id: int,
+    db: AsyncSession = Depends(get_db),
+):
+    """Delete the Fusion 360 design file from an archive."""
+    result = await db.execute(select(PrintArchive).where(PrintArchive.id == archive_id))
+    archive = result.scalar_one_or_none()
+    if not archive:
+        raise HTTPException(404, "Archive not found")
+
+    if not archive.f3d_path:
+        raise HTTPException(404, "No F3D file attached to this archive")
+
+    # Delete the file
+    f3d_path = settings.base_dir / archive.f3d_path
+    if f3d_path.exists():
+        f3d_path.unlink()
+
+    # Clear the path in database
+    archive.f3d_path = None
+    await db.commit()
+
+    return {"status": "deleted"}

+ 7 - 0
backend/app/api/routes/settings.py

@@ -622,6 +622,12 @@ async def export_backup(
                     archive_data["source_3mf_path"] = a.source_3mf_path
                     backup_files.append((a.source_3mf_path, source_path))
 
+            if a.f3d_path:
+                f3d_path = base_dir / a.f3d_path
+                if f3d_path.exists():
+                    archive_data["f3d_path"] = a.f3d_path
+                    backup_files.append((a.f3d_path, f3d_path))
+
             # Include photos
             if a.photos:
                 for photo in a.photos:
@@ -1366,6 +1372,7 @@ async def import_backup(
                     thumbnail_path=archive_data.get("thumbnail_path"),
                     timelapse_path=archive_data.get("timelapse_path"),
                     source_3mf_path=archive_data.get("source_3mf_path"),
+                    f3d_path=archive_data.get("f3d_path"),
                     print_name=archive_data.get("print_name"),
                     print_time_seconds=archive_data.get("print_time_seconds"),
                     filament_used_grams=archive_data.get("filament_used_grams"),

+ 7 - 0
backend/app/core/database.py

@@ -99,6 +99,13 @@ async def run_migrations(conn):
         # Column already exists
         pass
 
+    # Migration: Add f3d_path column to print_archives for Fusion 360 design files
+    try:
+        await conn.execute(text("ALTER TABLE print_archives ADD COLUMN f3d_path VARCHAR(500)"))
+    except Exception:
+        # Column already exists
+        pass
+
     # Migration: Add on_maintenance_due column to notification_providers
     try:
         await conn.execute(text("ALTER TABLE notification_providers ADD COLUMN on_maintenance_due BOOLEAN DEFAULT 0"))

+ 1 - 0
backend/app/models/archive.py

@@ -21,6 +21,7 @@ class PrintArchive(Base):
     thumbnail_path: Mapped[str | None] = mapped_column(String(500))
     timelapse_path: Mapped[str | None] = mapped_column(String(500))
     source_3mf_path: Mapped[str | None] = mapped_column(String(500))  # Original project 3MF from slicer
+    f3d_path: Mapped[str | None] = mapped_column(String(500))  # Fusion 360 design file
 
     # Print details from 3MF / printer
     print_name: Mapped[str | None] = mapped_column(String(255))

+ 1 - 0
backend/app/schemas/archive.py

@@ -40,6 +40,7 @@ class ArchiveResponse(BaseModel):
     thumbnail_path: str | None
     timelapse_path: str | None
     source_3mf_path: str | None = None  # Original project 3MF from slicer
+    f3d_path: str | None = None  # Fusion 360 design file
 
     # Duplicate detection
     duplicates: list[ArchiveDuplicate] | None = None

+ 112 - 0
backend/tests/integration/test_archives_api.py

@@ -268,3 +268,115 @@ class TestArchiveDataIntegrity:
         result = response.json()
         assert result["notes"] == "Updated notes"
         assert result["is_favorite"] is True
+
+
+class TestArchiveF3DEndpoints:
+    """Tests for F3D (Fusion 360 design file) attachment endpoints."""
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_archive_response_includes_f3d_path(
+        self, async_client: AsyncClient, archive_factory, printer_factory, db_session
+    ):
+        """Verify f3d_path is included in archive response."""
+        printer = await printer_factory()
+        archive = await archive_factory(printer.id, f3d_path="archives/test/design.f3d")
+
+        response = await async_client.get(f"/api/v1/archives/{archive.id}")
+
+        assert response.status_code == 200
+        result = response.json()
+        assert "f3d_path" in result
+        assert result["f3d_path"] == "archives/test/design.f3d"
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_archive_response_f3d_path_null_when_not_set(
+        self, async_client: AsyncClient, archive_factory, printer_factory, db_session
+    ):
+        """Verify f3d_path is null when no F3D file attached."""
+        printer = await printer_factory()
+        archive = await archive_factory(printer.id)
+
+        response = await async_client.get(f"/api/v1/archives/{archive.id}")
+
+        assert response.status_code == 200
+        result = response.json()
+        assert "f3d_path" in result
+        assert result["f3d_path"] is None
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_upload_f3d_to_nonexistent_archive(self, async_client: AsyncClient):
+        """Verify 404 when uploading F3D to non-existent archive."""
+        # Create a minimal file-like upload
+        files = {"file": ("design.f3d", b"fake f3d content", "application/octet-stream")}
+        response = await async_client.post("/api/v1/archives/9999/f3d", files=files)
+
+        assert response.status_code == 404
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_download_f3d_not_found_when_no_file(
+        self, async_client: AsyncClient, archive_factory, printer_factory, db_session
+    ):
+        """Verify 404 when downloading F3D from archive without F3D file."""
+        printer = await printer_factory()
+        archive = await archive_factory(printer.id)
+
+        response = await async_client.get(f"/api/v1/archives/{archive.id}/f3d")
+
+        assert response.status_code == 404
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_download_f3d_nonexistent_archive(self, async_client: AsyncClient):
+        """Verify 404 when downloading F3D from non-existent archive."""
+        response = await async_client.get("/api/v1/archives/9999/f3d")
+
+        assert response.status_code == 404
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_delete_f3d_nonexistent_archive(self, async_client: AsyncClient):
+        """Verify 404 when deleting F3D from non-existent archive."""
+        response = await async_client.delete("/api/v1/archives/9999/f3d")
+
+        assert response.status_code == 404
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_delete_f3d_when_no_file(
+        self, async_client: AsyncClient, archive_factory, printer_factory, db_session
+    ):
+        """Verify 404 when deleting F3D from archive without F3D file."""
+        printer = await printer_factory()
+        archive = await archive_factory(printer.id)
+
+        response = await async_client.delete(f"/api/v1/archives/{archive.id}/f3d")
+
+        assert response.status_code == 404
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_list_archives_includes_f3d_path(
+        self, async_client: AsyncClient, archive_factory, printer_factory, db_session
+    ):
+        """Verify f3d_path is included in archive list responses."""
+        printer = await printer_factory()
+        await archive_factory(printer.id, print_name="With F3D", f3d_path="archives/test/design.f3d")
+        await archive_factory(printer.id, print_name="Without F3D")
+
+        response = await async_client.get("/api/v1/archives/")
+
+        assert response.status_code == 200
+        data = response.json()
+        assert len(data) >= 2
+
+        with_f3d = next((a for a in data if a["print_name"] == "With F3D"), None)
+        without_f3d = next((a for a in data if a["print_name"] == "Without F3D"), None)
+
+        assert with_f3d is not None
+        assert with_f3d["f3d_path"] == "archives/test/design.f3d"
+        assert without_f3d is not None
+        assert without_f3d["f3d_path"] is None

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

@@ -202,6 +202,7 @@ export interface Archive {
   thumbnail_path: string | null;
   timelapse_path: string | null;
   source_3mf_path: string | null;
+  f3d_path: string | null;
   duplicates: ArchiveDuplicate[] | null;
   duplicate_count: number;
   object_count: number | null;
@@ -1682,6 +1683,26 @@ export const api = {
     request<{ status: string }>(`/archives/${archiveId}/source`, {
       method: 'DELETE',
     }),
+  // F3D (Fusion 360 design file)
+  getF3dDownloadUrl: (archiveId: number) =>
+    `${API_BASE}/archives/${archiveId}/f3d`,
+  uploadF3d: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
+    const formData = new FormData();
+    formData.append('file', file);
+    const response = await fetch(`${API_BASE}/archives/${archiveId}/f3d`, {
+      method: 'POST',
+      body: formData,
+    });
+    if (!response.ok) {
+      const error = await response.json().catch(() => ({}));
+      throw new Error(error.detail || `HTTP ${response.status}`);
+    }
+    return response.json();
+  },
+  deleteF3d: (archiveId: number) =>
+    request<{ status: string }>(`/archives/${archiveId}/f3d`, {
+      method: 'DELETE',
+    }),
 
   // QR Code
   getArchiveQRCodeUrl: (archiveId: number, size = 200) =>

+ 162 - 0
frontend/src/pages/ArchivesPage.tsx

@@ -120,8 +120,10 @@ function ArchiveCard({
   const [showProjectPage, setShowProjectPage] = useState(false);
   const [showSchedule, setShowSchedule] = useState(false);
   const [showDeleteSource3mfConfirm, setShowDeleteSource3mfConfirm] = useState(false);
+  const [showDeleteF3dConfirm, setShowDeleteF3dConfirm] = useState(false);
   const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
   const source3mfInputRef = useRef<HTMLInputElement>(null);
+  const f3dInputRef = useRef<HTMLInputElement>(null);
 
   const source3mfUploadMutation = useMutation({
     mutationFn: (file: File) => api.uploadSource3mf(archive.id, file),
@@ -145,6 +147,28 @@ function ArchiveCard({
     },
   });
 
+  const f3dUploadMutation = useMutation({
+    mutationFn: (file: File) => api.uploadF3d(archive.id, file),
+    onSuccess: (data) => {
+      queryClient.invalidateQueries({ queryKey: ['archives'] });
+      showToast(`F3D attached: ${data.filename}`);
+    },
+    onError: (error: Error) => {
+      showToast(error.message || 'Failed to upload F3D', 'error');
+    },
+  });
+
+  const f3dDeleteMutation = useMutation({
+    mutationFn: () => api.deleteF3d(archive.id),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['archives'] });
+      showToast('F3D removed');
+    },
+    onError: (error: Error) => {
+      showToast(error.message || 'Failed to remove F3D', 'error');
+    },
+  });
+
   const timelapseScanMutation = useMutation({
     mutationFn: () => api.scanArchiveTimelapse(archive.id),
     onSuccess: (data) => {
@@ -301,6 +325,27 @@ function ArchiveCard({
       onClick: () => setShowDeleteSource3mfConfirm(true),
       danger: true,
     }] : []),
+    {
+      label: archive.f3d_path ? 'Replace F3D' : 'Upload F3D',
+      icon: <Box className="w-4 h-4" />,
+      onClick: () => f3dInputRef.current?.click(),
+    },
+    ...(archive.f3d_path ? [{
+      label: 'Download F3D',
+      icon: <Download className="w-4 h-4" />,
+      onClick: () => {
+        const link = document.createElement('a');
+        link.href = api.getF3dDownloadUrl(archive.id);
+        link.download = `${archive.print_name || archive.filename}.f3d`;
+        link.click();
+      },
+    },
+    {
+      label: 'Remove F3D',
+      icon: <Trash2 className="w-4 h-4" />,
+      onClick: () => setShowDeleteF3dConfirm(true),
+      danger: true,
+    }] : []),
     { label: '', divider: true, onClick: () => {} },
     {
       label: 'Download',
@@ -511,6 +556,20 @@ function ArchiveCard({
             <FileCode className="w-4 h-4 text-orange-400" />
           </button>
         )}
+        {/* F3D badge */}
+        {archive.f3d_path && (
+          <button
+            className={`absolute bottom-2 ${archive.source_3mf_path ? 'left-12' : 'left-2'} p-1.5 rounded bg-black/60 hover:bg-black/80 transition-colors`}
+            onClick={(e) => {
+              e.stopPropagation();
+              // Download F3D file
+              window.location.href = api.getF3dDownloadUrl(archive.id);
+            }}
+            title="Download Fusion 360 design file"
+          >
+            <Box className="w-4 h-4 text-cyan-400" />
+          </button>
+        )}
         {/* Timelapse badge */}
         {archive.timelapse_path && (
           <button
@@ -833,6 +892,21 @@ function ArchiveCard({
         />
       )}
 
+      {/* Delete F3D Confirmation */}
+      {showDeleteF3dConfirm && (
+        <ConfirmModal
+          title="Remove F3D"
+          message={`Are you sure you want to remove the Fusion 360 design file from "${archive.print_name || archive.filename}"?`}
+          confirmText="Remove"
+          variant="danger"
+          onConfirm={() => {
+            f3dDeleteMutation.mutate();
+            setShowDeleteF3dConfirm(false);
+          }}
+          onCancel={() => setShowDeleteF3dConfirm(false)}
+        />
+      )}
+
       {/* Context Menu */}
       {contextMenu && (
         <ContextMenu
@@ -973,6 +1047,20 @@ function ArchiveCard({
           e.target.value = '';
         }}
       />
+      {/* Hidden file input for F3D upload */}
+      <input
+        ref={f3dInputRef}
+        type="file"
+        accept=".f3d"
+        className="hidden"
+        onChange={(e) => {
+          const file = e.target.files?.[0];
+          if (file) {
+            f3dUploadMutation.mutate(file);
+          }
+          e.target.value = '';
+        }}
+      />
     </Card>
   );
 }
@@ -1008,8 +1096,10 @@ function ArchiveListRow({
   const [showPhotos, setShowPhotos] = useState(false);
   const [showProjectPage, setShowProjectPage] = useState(false);
   const [showDeleteSource3mfConfirm, setShowDeleteSource3mfConfirm] = useState(false);
+  const [showDeleteF3dConfirm, setShowDeleteF3dConfirm] = useState(false);
   const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
   const source3mfInputRef = useRef<HTMLInputElement>(null);
+  const f3dInputRef = useRef<HTMLInputElement>(null);
 
   const source3mfUploadMutation = useMutation({
     mutationFn: (file: File) => api.uploadSource3mf(archive.id, file),
@@ -1033,6 +1123,28 @@ function ArchiveListRow({
     },
   });
 
+  const f3dUploadMutation = useMutation({
+    mutationFn: (file: File) => api.uploadF3d(archive.id, file),
+    onSuccess: (data) => {
+      queryClient.invalidateQueries({ queryKey: ['archives'] });
+      showToast(`F3D attached: ${data.filename}`);
+    },
+    onError: (error: Error) => {
+      showToast(error.message || 'Failed to upload F3D', 'error');
+    },
+  });
+
+  const f3dDeleteMutation = useMutation({
+    mutationFn: () => api.deleteF3d(archive.id),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['archives'] });
+      showToast('F3D removed');
+    },
+    onError: (error: Error) => {
+      showToast(error.message || 'Failed to remove F3D', 'error');
+    },
+  });
+
   const timelapseScanMutation = useMutation({
     mutationFn: () => api.scanArchiveTimelapse(archive.id),
     onSuccess: (data) => {
@@ -1186,6 +1298,27 @@ function ArchiveListRow({
       onClick: () => setShowDeleteSource3mfConfirm(true),
       danger: true,
     }] : []),
+    {
+      label: archive.f3d_path ? 'Replace F3D' : 'Upload F3D',
+      icon: <Box className="w-4 h-4" />,
+      onClick: () => f3dInputRef.current?.click(),
+    },
+    ...(archive.f3d_path ? [{
+      label: 'Download F3D',
+      icon: <Download className="w-4 h-4" />,
+      onClick: () => {
+        const link = document.createElement('a');
+        link.href = api.getF3dDownloadUrl(archive.id);
+        link.download = `${archive.print_name || archive.filename}.f3d`;
+        link.click();
+      },
+    },
+    {
+      label: 'Remove F3D',
+      icon: <Trash2 className="w-4 h-4" />,
+      onClick: () => setShowDeleteF3dConfirm(true),
+      danger: true,
+    }] : []),
     { label: '', divider: true, onClick: () => {} },
     {
       label: 'Download',
@@ -1489,6 +1622,21 @@ function ArchiveListRow({
         />
       )}
 
+      {/* Delete F3D Confirmation */}
+      {showDeleteF3dConfirm && (
+        <ConfirmModal
+          title="Remove F3D"
+          message={`Are you sure you want to remove the Fusion 360 design file from "${archive.print_name || archive.filename}"?`}
+          confirmText="Remove"
+          variant="danger"
+          onConfirm={() => {
+            f3dDeleteMutation.mutate();
+            setShowDeleteF3dConfirm(false);
+          }}
+          onCancel={() => setShowDeleteF3dConfirm(false)}
+        />
+      )}
+
       {/* Context Menu */}
       {contextMenu && (
         <ContextMenu
@@ -1617,6 +1765,20 @@ function ArchiveListRow({
           e.target.value = '';
         }}
       />
+      {/* Hidden file input for F3D upload */}
+      <input
+        ref={f3dInputRef}
+        type="file"
+        accept=".f3d"
+        className="hidden"
+        onChange={(e) => {
+          const file = e.target.files?.[0];
+          if (file) {
+            f3dUploadMutation.mutate(file);
+          }
+          e.target.value = '';
+        }}
+      />
     </>
   );
 }

File diff suppressed because it is too large
+ 0 - 0
icons/fc3b0d1fbe8b4815b0f7dbd92f3b68fd.svg


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


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-BEswNysN.js"></script>
+    <script type="module" crossorigin src="/assets/index-BlbHG-iv.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-Dzh7xD3q.css">
   </head>
   <body>

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