diff --git a/.gitignore b/.gitignore index 57914d1aa..dbb6d7a75 100644 --- a/.gitignore +++ b/.gitignore @@ -35,8 +35,8 @@ archive/ # Firmware cache (downloaded firmware files) firmware/ -# Virtual printer (auto-generated certs and uploads) -virtual_printer/ +# Virtual printer (auto-generated certs and uploads at repo root) +/virtual_printer/ # IDE .idea/ @@ -58,3 +58,6 @@ firmware/ node_modules/ data/ + +# JWT secret file (should be in data dir, but protect project root too) +.jwt_secret diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d4847ae..07719c84b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,116 @@ All notable changes to Bambuddy will be documented in this file. -## [0.1.7b] - Not released + +## [0.1.8b] - Not released + +### Added +- **Windows Portable Launcher** (contributed by nmori): + - New `start_bambuddy.bat` for Windows users - double-click to run, no installation required + - Automatically downloads Python 3.13 and Node.js 22 on first run (portable, no system changes) + - Everything stored in `.portable\` folder for easy cleanup + - Commands: `start_bambuddy.bat` (launch), `start_bambuddy.bat update` (update deps), `start_bambuddy.bat reset` (clean start) + - Custom port via `set PORT=9000 & start_bambuddy.bat` + - Verifies all downloads with SHA256 checksums for security + - Supports both x64 and ARM64 Windows systems + +## [0.1.7] - 2026-02-03 + +### Security +- **Critical: Missing API Endpoint Authentication** (CVE-2026-25505, CVSS 9.8): + - Added authentication to 200+ API endpoints that were previously unprotected + - All route files now use `RequirePermissionIfAuthEnabled()` for permission checks + - Protected endpoints: archives, projects, settings, API keys, groups, cloud, notifications, maintenance, filaments, external links, smart plugs, discovery, firmware, camera, k-profiles, AMS history, pending uploads, updates, spoolman, system, print queue, printers + - Image-serving endpoints (thumbnails, timelapse, photos, camera streams) remain public as they require knowing the resource ID and are loaded via `` tags which cannot send Authorization headers + - Backend integration tests added to verify endpoint authentication enforcement ### Enhancements +- **TOTP Authenticator Support for Bambu Cloud** (Issue #182): + - Added support for TOTP-based two-factor authentication when connecting to Bambu Cloud + - Accounts with authenticator apps (Google Authenticator, Authy, etc.) now work correctly + - Proper detection of verification type: email code vs TOTP code + - Uses browser-like headers to bypass Cloudflare protection on TFA endpoint + - Frontend shows appropriate message for each verification type + - Added translations for TOTP UI in English, German, and Japanese +- **Spoolman: Open in Spoolman Button** (Issue #210): + - FilamentHoverCard now shows "Open in Spoolman" button when spool is already linked in Spoolman + - Button links directly to the spool's page in Spoolman for quick editing + - "Link to Spoolman" button now only shows when spool is not yet linked + - Link button correctly disabled when no unlinked spools are available in Spoolman + - Toast notification shown on successful/failed spool linking + - Added `/api/v1/spoolman/spools/linked` endpoint returning map of linked spool tags to IDs +- **Complete German Translations**: + - All UI strings now fully translated to German (1800+ translation keys) + - Pages translated: Settings, Archives, File Manager, Queue, Printers, Profiles, Projects, Stats, Maintenance, Camera, Groups, Users, Login, Setup, Stream Overlay + - Components translated: ConfirmModal, LinkSpoolModal, FilamentHoverCard, Layout + - Added locale parity test to ensure English and German stay in sync +- **Virtual Printer Proxy Mode**: + - New "Proxy" mode allows remote printing over any network by relaying slicer traffic to a real printer + - Configure a target printer and Bambuddy acts as a TLS proxy between your slicer and the printer + - Supports both FTP (port 9990) and MQTT (port 8883) protocols with full TLS encryption + - Slicer connects to Bambuddy using the real printer's access code + - Real-time status display showing active FTP/MQTT connections + - Target printer selector with validation (must be configured in Bambuddy) + - Proxy mode bypasses the access code requirement (uses the real printer's credentials) + - Full i18n support for all proxy mode UI strings (English, German, Japanese) + +### Fixed +- **Cannot Link Multiple HA Entities to Same Printer** (Issue #214): + - Fixed Home Assistant entities being limited to one per printer + - Both frontend and backend were blocking printers that already had any smart plug linked + - Now only Tasmota plugs are limited to one per printer (physical device constraint) + - Multiple HA entities (switches, scripts, lights, etc.) can be linked to the same printer + - Restored "Show on Printer Card" toggle for HA entities to control visibility on printer cards + - Fixed printer card only showing `script.*` entities; now shows all HA entities with toggle enabled + - HA entities now default to auto_on=False and auto_off=False (appropriate for automations) + - Printer cards now update immediately when HA entities are added/modified/deleted +- **Monthly Comparison Calculation Off** (Issue #229): + - Fixed filament statistics not accounting for quantity multiplier + - Monthly comparison chart now correctly multiplies `filament_used_grams` by `quantity` + - Daily and weekly charts also now account for quantity + - Filament type breakdown includes quantity in calculations + - Backend stats endpoint (`/archives/stats`) and Prometheus metrics also fixed + - Prints count now shows total items (sum of quantities) instead of archive count +- **Authentication Required for Downloads** (Issue #231): + - Fixed support bundle download returning 401 Unauthorized when auth is enabled + - Fixed archive export (CSV/XLSX) failing with authentication enabled + - Fixed statistics export failing with authentication enabled + - Fixed printer file ZIP download failing with authentication enabled + - Root cause: These endpoints used raw `fetch()` without Authorization header +- **Queue Schedule Date Picker Ignores User Format Settings** (Issue #233): + - Replaced native datetime picker with custom date/time inputs respecting user settings + - Date input shows in user's format (DD/MM/YYYY for EU, MM/DD/YYYY for US, YYYY-MM-DD for ISO) + - Time input shows in user's format (24H or 12H with AM/PM) + - Calendar button opens native picker for convenience; selection is formatted to user's preference + - Placeholder text shows expected format (e.g., "DD/MM/YYYY" or "HH:MM AM/PM") + - Added date utilities: `formatDateInput`, `parseDateInput`, `getDatePlaceholder` + - Added time utilities: `formatTimeInput`, `parseTimeInput`, `getTimePlaceholder` +- **500 Error on Archive Detail Page**: + - Fixed internal server error when viewing individual archive details + - Root cause: `project` relationship not eagerly loaded in `get_archive()` service method + - Async SQLAlchemy requires explicit eager loading; lazy loading is not supported + +## [0.1.6.2] - 2026-02-02 + +> **Security Release**: This release addresses critical security vulnerabilities. Users running authentication-enabled instances should upgrade immediately. + +### Security +- **Critical: Hardcoded JWT Secret Key** (GHSA-gc24-px2r-5qmf, CWE-321) - Fixed hardcoded JWT secret key that could allow attackers to forge authentication tokens: + - JWT secret now loaded from `JWT_SECRET_KEY` environment variable (recommended for production) + - Falls back to auto-generated `.jwt_secret` file in data directory with secure permissions (0600) + - Generates cryptographically secure 64-byte random secret if neither exists + - **Action Required**: Existing users will need to re-login after upgrading +- **Critical: Missing API Authentication** (GHSA-gc24-px2r-5qmf, CWE-306) - Fixed 77+ API endpoints that lacked authentication checks: + - Added HTTP middleware enforcing authentication on ALL `/api/` routes when auth is enabled + - Only essential public endpoints are exempt (login, auth status, version check, WebSocket) + - All other API calls now require valid JWT token or API key + +### Enhancements +- **Location Filter for Queue** (Issue #220): + - Filter queue jobs by printer location in the Queue page + - "Any {Model}" queue assignments can now specify a target location (e.g., "Any X1C in Workshop") + - Location filter dropdown shows all unique locations from printers and queue items + - Location is saved with queue items and displayed in the queue list - **Ownership-Based Permissions** (Issue #205): - Users can now only update/delete their own items unless they have elevated permissions - Update/delete permissions split into `*_own` and `*_all` variants: @@ -51,11 +158,28 @@ All notable changes to Bambuddy will be documented in this file. - Removed ~2000 lines of legacy JSON-based backup/restore code ### Fixes +- **File Manager permissions not enforced** (Issue #224) - Fixed backend not checking `library:read` permission for File Manager endpoints: + - Added `library:read` permission check to all list/view endpoints (files, folders, stats) + - Added `library:upload` permission check to upload and folder creation endpoints + - Added `queue:create` permission check to add-to-queue endpoint + - Added `printers:control` permission check to direct print endpoint + - Added ownership-based permission checks to file move operation + - Users without `library:read` permission can no longer view files in the File Manager + - Users can now only delete/update their own files unless they have `*_all` permissions +- **JWT secret key not persistent across restarts** - Fixed JWT secret key generation to properly use data directory, ensuring tokens remain valid across container restarts +- **Images/thumbnails returning 401 when auth enabled** - Fixed auth middleware to allow public access to image/media endpoints (thumbnails, photos, QR codes, timelapses, camera streams) since browser elements like `` don't send Authorization headers - **Library thumbnails missing after restore** - Fixed library files using absolute paths that break after restore on different systems: - Library now stores relative paths in database for portability - Automatic migration converts existing absolute paths to relative on startup - Thumbnails and files now display correctly after restoring backups - **File uploads failing with authentication enabled** - Fixed all file upload functions (archives, photos, timelapses, library files, etc.) not sending authentication headers when auth is enabled +- **External spool AMS mapping causing "Failed to get AMS mapping table"** (Issue #213) - Fixed external spool `ams_mapping2` slot_id handling that caused AMS mapping failures +- **Filename matching for files with spaces** (Issue #218) - Fixed file detection when filenames contain spaces +- **P2S FTP upload failure** (Issue #218) - Fixed FTP uploads to P2S printers by passing `skip_session_reuse` to ImplicitFTP_TLS +- **Printer deletion freeze** (Issue #214) - Fixed UI freeze when deleting printers, and now allows multiple smart plugs per printer +- **Stack trace exposure in error responses** (CodeQL Alert #68) - Fixed stack traces being exposed in API error responses in archives.py +- **Printer serial numbers exposed in support bundle** (Issue #216) - Sanitized printer serial numbers in support bundle logs for privacy +- **Missing sliced_for_model migration** (Issue #211) - Fixed database migration for `sliced_for_model` column that was missing in some upgrade paths ## [0.1.6-final] - 2026-01-31 diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 27cb52dd9..000000000 --- a/PLAN.md +++ /dev/null @@ -1,247 +0,0 @@ -# Notification Templates Management System - -## Overview - -Replace hardcoded notification messages with a flexible template system that allows users to customize notification content per event type, with provider-specific formatting support. - ---- - -## Data Model - -### New Table: `notification_templates` - -```sql -CREATE TABLE notification_templates ( - id INTEGER PRIMARY KEY, - event_type VARCHAR(50) NOT NULL, -- print_start, print_complete, etc. - name VARCHAR(100) NOT NULL, -- User-friendly name - title_template TEXT NOT NULL, -- Template for notification title - body_template TEXT NOT NULL, -- Template for notification body - is_default BOOLEAN DEFAULT 0, -- System default (non-deletable) - created_at DATETIME, - updated_at DATETIME -); -``` - -**Event Types:** -- `print_start` -- `print_complete` -- `print_failed` -- `print_stopped` -- `print_progress` -- `printer_offline` -- `printer_error` -- `filament_low` -- `maintenance_due` -- `test` (for test notifications) - ---- - -## Template Variables - -Variables use `{variable_name}` syntax (Python format strings). - -### Per-Event Variables: - -| Event | Variables | -|-------|-----------| -| `print_start` | `{printer}`, `{filename}`, `{estimated_time}` | -| `print_complete` | `{printer}`, `{filename}`, `{duration}`, `{filament_grams}` | -| `print_failed` | `{printer}`, `{filename}`, `{duration}`, `{reason}` | -| `print_stopped` | `{printer}`, `{filename}`, `{duration}` | -| `print_progress` | `{printer}`, `{filename}`, `{progress}`, `{remaining_time}` | -| `printer_offline` | `{printer}` | -| `printer_error` | `{printer}`, `{error_type}`, `{error_detail}` | -| `filament_low` | `{printer}`, `{slot}`, `{remaining_percent}`, `{color}` | -| `maintenance_due` | `{printer}`, `{items}` (formatted list) | -| `test` | `{app_name}` | - -### Common Variables (all events): -- `{timestamp}` - Current date/time -- `{app_name}` - "Bambuddy" - ---- - -## Default Templates - -Pre-seeded templates for each event (marked `is_default=True`): - -``` -print_start: - title: "Print Started" - body: "{printer}: {filename}\nEstimated: {estimated_time}" - -print_complete: - title: "Print Completed" - body: "{printer}: {filename}\nTime: {duration}\nFilament: {filament_grams}g" - -print_failed: - title: "Print Failed" - body: "{printer}: {filename}\nTime: {duration}\nReason: {reason}" - -print_stopped: - title: "Print Stopped" - body: "{printer}: {filename}\nTime: {duration}" - -print_progress: - title: "Print {progress}% Complete" - body: "{printer}: {filename}\nRemaining: {remaining_time}" - -printer_offline: - title: "Printer Offline" - body: "{printer} has disconnected" - -printer_error: - title: "Printer Error: {error_type}" - body: "{printer}\n{error_detail}" - -filament_low: - title: "Filament Low" - body: "{printer}: Slot {slot} at {remaining_percent}%" - -maintenance_due: - title: "Maintenance Due" - body: "{printer}:\n{items}" - -test: - title: "Bambuddy Test" - body: "This is a test notification. If you see this, notifications are working!" -``` - ---- - -## Provider-Specific Formatting - -The template system supports provider-specific formatting via a simple approach: - -1. **Plain text** (default) - Used for CallMeBot, ntfy, Pushover, Email -2. **Markdown** - Automatically applied for Telegram (wrap title in `*bold*`) - -The notification service will: -- Render the template with variables -- Apply provider-specific formatting when sending - ---- - -## Implementation Steps - -### Backend - -1. **Create model** `backend/app/models/notification_template.py` - - NotificationTemplate SQLAlchemy model - -2. **Create schemas** `backend/app/schemas/notification_template.py` - - NotificationTemplateCreate, Update, Response - - TemplateVariables (documentation of available vars per event) - -3. **Add migration** in `backend/app/core/database.py` - - Create table if not exists - - Seed default templates - -4. **Create API routes** `backend/app/api/routes/notification_templates.py` - - `GET /api/v1/notification-templates` - List all templates - - `GET /api/v1/notification-templates/{id}` - Get single template - - `PUT /api/v1/notification-templates/{id}` - Update template - - `POST /api/v1/notification-templates/{id}/reset` - Reset to default - - `GET /api/v1/notification-templates/variables` - List available variables per event - - `POST /api/v1/notification-templates/preview` - Preview template with sample data - -5. **Update notification service** `backend/app/services/notification_service.py` - - Load templates from database - - Render templates with variables - - Remove hardcoded message builders - -6. **Register routes** in `backend/app/main.py` - -### Frontend - -7. **Add API client methods** `frontend/src/api/client.ts` - - getNotificationTemplates, updateNotificationTemplate, etc. - -8. **Create template editor component** `frontend/src/components/NotificationTemplateEditor.tsx` - - Template editing UI with variable insertion buttons - - Live preview with sample data - - Reset to default button - -9. **Update SettingsPage** `frontend/src/pages/SettingsPage.tsx` - - Add "Templates" sub-section in Notifications tab - - List all templates with edit capability - ---- - -## UI Design - -### Templates Section (in Settings > Notifications) - -``` -+--------------------------------------------------+ -| Message Templates | -| Customize notification messages for each event | -+--------------------------------------------------+ -| | -| +----------------+ +----------------+ | -| | Print Started | | Print Complete | ... | -| | "Print Started"| | "Print Compl..." | | -| | [Edit] | | [Edit] | | -| +----------------+ +----------------+ | -| | -+--------------------------------------------------+ -``` - -### Template Editor Modal - -``` -+--------------------------------------------------+ -| Edit Template: Print Complete [X] | -+--------------------------------------------------+ -| Title: | -| [Print Completed_________________________] | -| | -| Body: | -| +----------------------------------------------+ | -| | {printer}: {filename} | | -| | Time: {duration} | | -| | Filament: {filament_grams}g | | -| +----------------------------------------------+ | -| | -| Available Variables: | -| [+printer] [+filename] [+duration] [+filament] | -| | -| Preview: | -| +----------------------------------------------+ | -| | Title: Print Completed | | -| | Body: Bambu X1C: Benchy.3mf | | -| | Time: 1h 23m | | -| | Filament: 15.2g | | -| +----------------------------------------------+ | -| | -| [Reset to Default] [Cancel] [Save] | -+--------------------------------------------------+ -``` - ---- - -## File Changes Summary - -| File | Action | -|------|--------| -| `backend/app/models/notification_template.py` | Create | -| `backend/app/schemas/notification_template.py` | Create | -| `backend/app/api/routes/notification_templates.py` | Create | -| `backend/app/core/database.py` | Modify (add migration + seeding) | -| `backend/app/models/__init__.py` | Modify (export new model) | -| `backend/app/services/notification_service.py` | Modify (use templates) | -| `backend/app/main.py` | Modify (register routes) | -| `frontend/src/api/client.ts` | Modify (add API methods + types) | -| `frontend/src/components/NotificationTemplateEditor.tsx` | Create | -| `frontend/src/pages/SettingsPage.tsx` | Modify (add templates section) | - ---- - -## Notes - -- Default templates cannot be deleted, only modified and reset -- Templates are language-agnostic (user writes in their preferred language) -- The existing `notification_language` setting can be removed later (templates replace i18n) -- Variables that are unavailable for an event will render as empty string -- Template rendering uses safe formatting (missing vars don't crash) diff --git a/README.md b/README.md index f305c2971..655ef8e4e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@

Release + License Stars Issues @@ -28,6 +29,25 @@ --- +## 🌐 NEW: Remote Printing with Proxy Mode + +

+ Proxy Mode Architecture +

+ +**Print from anywhere in the world** — Bambuddy's new Proxy Mode acts as a secure relay between your slicer and printer: + +- 🔒 **End-to-end TLS encryption** — Your print data is encrypted from slicer to printer +- 🌍 **No cloud dependency** — Direct connection through your own Bambuddy server +- 🔑 **Uses printer's access code** — No additional credentials needed +- ⚡ **Full-speed printing** — FTP and MQTT protocols proxied transparently + +Perfect for remote print farms, traveling makers, or accessing your home printer from work. + +👉 **[Setup Guide →](https://wiki.bambuddy.cool/features/virtual-printer/#proxy-mode-new-in-017)** + +--- + > **Testers Needed!** I only have X1C and H2D devices. Help make Bambuddy work with all Bambu Lab printers by [reporting your experience](https://github.com/maziggy/bambuddy/issues)! ## Why Bambuddy? @@ -77,7 +97,7 @@ ### ⏰ Scheduling & Automation - Print queue with drag-and-drop - Multi-printer selection (send to multiple printers at once) -- Model-based queue assignment (send to "any X1C" for load balancing) +- Model-based queue assignment (send to "any X1C" for load balancing) with location filtering - Filament validation (only assign to printers with required filaments) - Per-printer AMS mapping (individual slot configuration for print farms) - Scheduled prints (date/time) @@ -135,13 +155,14 @@ - Webhooks & API keys - Interactive API browser with live testing -### 🖨️ Virtual Printer +### 🖨️ Virtual Printer & Remote Printing +- **🌐 Proxy Mode (NEW!)** — Print remotely from anywhere via secure TLS relay - Emulates a Bambu Lab printer on your network - Send prints directly from Bambu Studio/Orca Slicer - Configurable printer model (X1C, P1S, A1, H2D, etc.) -- Queue mode or auto-start mode +- Archive mode, Review mode, Queue mode, or Proxy mode - SSDP discovery (appears in slicer automatically) -- Secure TLS/MQTT communication +- Secure TLS/MQTT/FTP communication ### 🛠️ Maintenance & Support - Maintenance scheduling & tracking @@ -158,6 +179,7 @@ - Group-based permissions (50+ granular permissions) - Default groups: Administrators, Operators, Viewers - JWT tokens with secure password hashing +- Comprehensive API protection (200+ endpoints secured) - User management (create, edit, delete, groups) - User activity tracking (who uploaded archives, library files, queued prints, started prints) @@ -421,7 +443,26 @@ services: -#### Manual Installation +#### Windows (Portable Launcher) + +The easiest way to run Bambuddy on Windows - no installation required: + +```batch +git clone https://github.com/maziggy/bambuddy.git +cd bambuddy +start_bambuddy.bat +``` + +Double-click `start_bambuddy.bat` and it will: +- Download Python and Node.js automatically (portable, no system changes) +- Install dependencies and build the frontend +- Open your browser to http://localhost:8000 + +Everything is stored in the `.portable\` folder. Use `start_bambuddy.bat reset` to clean up. + +> **Custom port:** `set PORT=9000 & start_bambuddy.bat` + +#### Manual Installation (Linux/macOS) ```bash # Clone and setup diff --git a/RELEASE_NOTES_0.1.6.md b/RELEASE_NOTES_0.1.6.md deleted file mode 100644 index aff06263b..000000000 --- a/RELEASE_NOTES_0.1.6.md +++ /dev/null @@ -1,318 +0,0 @@ -# Bambuddy v0.1.6 - Final Release - -**Release Date:** January 31, 2026 - -After 11 beta releases and extensive community testing, we're excited to announce **Bambuddy 0.1.6-final** - our biggest release yet! This release brings optional authentication, build plate detection, external camera support, model-based queue scheduling, and much more. - ---- - -## Highlights - -### Optional Authentication & User Management -Secure your Bambuddy instance for the first time: -- Enable/disable authentication via Settings -- **Role-based access**: Admin (full access) and User (prints only) roles -- **Group-based permissions**: 50+ granular permissions with custom groups -- JWT-based authentication with user management UI -- Users can change their own password from the sidebar - -### Build Plate Empty Detection -Never start a print with objects on the bed again: -- Per-printer toggle for plate detection -- Multi-reference calibration (up to 5 plate types) -- Automatic print pause when objects detected -- Push notifications and WebSocket alerts -- ROI calibration for precise detection area - -### External & USB Camera Support -Use any camera with your printers: -- **External cameras**: MJPEG, RTSP, HTTP snapshot support -- **USB cameras**: V4L2 webcam support on Linux -- Layer-based timelapse with external cameras -- Finish photo capture from external sources - -### Model-Based Queue Assignment -Perfect for print farms: -- Queue items to "Any X1C", "Any P1S", etc. -- Auto-assigns to available printer when ready -- Automatic filament validation and AMS mapping -- Matches required filaments to loaded spools - -### GitHub Profile Backup -Automated backup of your settings to GitHub: -- Schedule hourly, daily, or weekly backups -- Backs up K-profiles, cloud profiles, and app settings -- Skip unchanged commits (only commit when data changes) -- Backup history log with commit links - -### Prometheus Metrics -Export printer telemetry for external monitoring: -- Endpoint: `GET /api/v1/metrics` -- Printer temps, fans, WiFi, print progress -- Ready for Grafana dashboards -- Optional bearer token authentication - ---- - -## New Features - -### Authentication & Security -- **Optional Authentication** - Secure your Bambuddy instance with JWT-based user authentication -- **Group-Based Permissions** - 50+ granular permissions with custom groups (Administrators, Operators, Viewers) -- **Change Password** - Users can update their own password from sidebar -- **API Keys** - API key authentication with granular permissions - -### Virtual Printer -- **Virtual Printer** - Emulates a Bambu Lab printer on your network for Bambu Studio/Orca Slicer -- **Virtual Printer Queue Mode** - Auto-archive and queue prints from slicer -- **Virtual Printer Model Selection** - Choose which printer model to emulate -- **TLS 1.3 Encryption** - Secure MQTT + FTPS with auto-generated certificates - -### Print Queue & Scheduling -- **Model-Based Queue Assignment** - Queue to "Any X1C", "Any P1S" with auto filament matching -- **Multi-Printer Selection** - Send prints to multiple printers at once -- **Per-Printer AMS Mapping** - Configure filament mapping individually per printer -- **Queue Bulk Edit** - Select and edit multiple queue items at once -- **Queue Only Mode** - Stage prints without auto-start, release when ready -- **Unassigned Queue Items** - Queue items without assigned printer -- **Add to Queue from File Manager** - Queue sliced files directly from library -- **Print Queue Plate Selection** - Full print configuration in queue modal -- **Deferred Archive Creation** - Archives created when prints start, not when queued - -### Smart Plugs & Automation -- **MQTT Smart Plug Support** - Monitor energy from Zigbee2MQTT, Shelly, Tasmota -- **Home Assistant Integration** - Control any HA switch/light as a smart plug -- **HA Energy Sensors** - Use separate sensor entities for power monitoring -- **Tasmota Discovery** - Auto-discover Tasmota devices on network -- **Switchbar Widget** - Quick power toggle in sidebar -- **Tasmota Admin Link** - Quick access to plug web interface - -### Camera & Streaming -- **External Camera Support** - MJPEG, RTSP, HTTP snapshot cameras -- **USB Camera Support** - V4L2 webcam support on Linux -- **Build Plate Empty Detection** - AI-powered detection with multi-reference calibration -- **OBS Streaming Overlay** - Embeddable page at `/overlay/:printerId` -- **Camera Zoom & Fullscreen** - 100%-400% zoom with pan support -- **Multiple Embedded Viewers** - Open multiple camera streams simultaneously -- **Camera View Mode** - Choose between new window or embedded overlay -- **Layer-Based Timelapse** - External camera timelapse on layer change -- **Finish Photo in Notifications** - `{finish_photo_url}` template variable - -### File Manager -- **STL Thumbnail Generation** - Auto-generate 3D previews for STL files -- **ZIP File Support** - Upload and extract ZIP files directly -- **Create Folder from ZIP** - Auto-create folder named after ZIP file -- **File Manager Sorting** - Sort by name, size, or date -- **File Manager Rename** - Rename files and folders directly -- **File Manager Print Button** - Print directly from selection toolbar -- **Resizable Sidebar** - Drag to adjust width (200-500px) -- **Text Wrap Toggle** - Wrap long folder names instead of truncating -- **Mobile Accessibility** - Touch-friendly with always-visible menus - -### Archives & Projects -- **Multi-Plate Selection** - Select which plate to print from multi-plate 3MF -- **Archive Plate Browsing** - Navigate plate thumbnails in archive cards -- **External Links** - Link archives to Printables, Thingiverse, etc. -- **Fusion 360 Attachments** - Attach F3D design files to archives -- **Project Import/Export** - Export/import projects as ZIP with all files -- **BOM Item Editing** - Edit Bill of Materials items after creation -- **Bulk Project Assignment** - Assign multiple archives to project at once -- **Project Parts Tracking** - Track parts separately from plates -- **Tag Management** - Create, edit, and apply tags to archives -- **Archive Comparison** - Compare 2-5 archives side-by-side -- **AMS Filament Preview** - Preview filament colors in archive cards - -### Printer Controls -- **Printer Controls** - Stop and Pause/Resume buttons with confirmation -- **Skip Objects** - Skip individual objects without canceling print -- **Chamber Light Control** - Light toggle button on printer cards -- **Resizable Printer Cards** - Four sizes (S/M/L/XL) -- **H2D Pro Support** - Full support for H2D Pro printer model - -### AMS & Filament -- **AMS Color Mapping** - Manual slot selection with auto-matching -- **Expandable Color Picker** - 32 colors in configurable palette -- **AMS Slot RFID Re-read** - Re-read filament info via hover menu -- **Print Options in Modals** - Bed leveling, flow cal, vibration cal, timelapse toggles - -### Backup & Monitoring -- **GitHub Profile Backup** - Scheduled backup to GitHub repository -- **Prometheus Metrics** - Export telemetry for Grafana -- **MQTT Publishing** - Publish events to external MQTT brokers -- **Application Log Viewer** - Real-time log viewing with filters -- **Support Bundle** - Debug logging with ZIP generation -- **Comprehensive Backup/Restore** - All settings, users, groups included - -### Notifications -- **HMS Error Notifications** - 853 error codes translated to human-readable messages -- **Plate Not Empty Notification** - Dedicated category for plate detection -- **Daily Digest** - Consolidated daily notification summary -- **Notification Templates** - Customizable message templates -- **Slack/Mattermost Format** - Proper payload format support - -### Statistics & Dashboard -- **Failure Analysis Widget** - Failure rate with correlations and trends -- **Statistics Improvements** - Size-aware responsive widgets -- **Recalculate Costs** - Button to recalculate all archive costs -- **Time Format Setting** - Configurable date/time format -- **Print Quantity Tracking** - Track items per print for progress - -### Other Improvements -- **Firmware Update Helper** - Check versions against Bambu Lab servers -- **Disable Firmware Checks** - Toggle to prevent update checks -- **Printer Discovery** - Docker subnet scanning, model mapping -- **FTP Reliability** - Configurable retry with SSL fixes -- **Pre-built Docker Images** - Pull from GitHub Container Registry -- **One-Shot Install Scripts** - Simple `curl | bash` installation -- **Mobile PWA** - Full mobile support with touch gestures -- **Timelapse Editor** - Trim, speed adjustment, music overlay -- **Sidebar Badge Indicators** - Queue and upload counts - ---- - -## Bug Fixes - -### Print Queue & Scheduling -- **Home Assistant Auto-On for Queued Prints** - Fixed smart plug not turning on for queue-started prints (Issue #200) -- **AMS Mapping for Model-Based Queue** - Fixed "Any [Model]" queue jobs failing at filament loading (Issue #192) -- **Queue prints on A1** - Fixed "MicroSD Card read/write exception error" when starting prints from queue -- **Multi-Plate Queue Thumbnails** - Queue now shows correct plate thumbnail (Issue #166) -- **Queue items with library files** - Fixed 500 errors when listing/updating queue items from File Manager - -### Printer Status & Display -- **A1/A1 Mini Status Display** - Fixed incorrect "Printing" status when idle (Issue #168) -- **Chamber temp on A1/P1S** - Fixed regression where chamber temperature appeared on printers without sensors -- **Active AMS slot display** - Fixed for H2D printers with multiple AMS units -- **Printer hour counter** - Fixed not incrementing during prints and inconsistency between views - -### AMS & Filament -- **Empty AMS Slot Recognition** - Fixed removed spools still appearing in Bambuddy (Issue #147) -- **Spoolman Sync for Transparent Spools** - Fixed sync failures for natural/transparent filaments (Issue #190) -- **Spoolman tag field** - Now auto-created on first connect, fixing fresh installs (Issue #123) -- **Spoolman 400 Bad Request** - Fixed when creating spools -- **AMS filament matching** - Fixed in reprint modal -- **User preset AMS configuration** - Fixed user presets showing empty fields in Bambu Studio - -### Notifications & Webhooks -- **Progress Milestone Notifications** - Fixed showing wrong time (e.g., "17m" instead of "17h 47m") (Issue #157) -- **Mattermost/Slack Webhooks** - Added proper payload format support (Issue #133) -- **Telegram Notification Parsing** - Fixed markdown errors with underscores in error codes -- **HMS Error Notifications** - 853 error codes now translated to human-readable messages -- **Notifications sent when printer offline** - Fixed - -### Camera & Streaming -- **Camera stream reconnection** - Automatic recovery from stalled streams -- **Camera zoom & pan** - Fixed pan range and added pinch-to-zoom for mobile (Issue #132) -- **P2S/X1E/H2 completion photo** - Fixed internal model codes not recognized (Issue #127) -- **Browser freeze** - Fixed on print completion when camera stream was open -- **ffmpeg processes** - Fixed not being killed when closing webcam window - -### File Manager & Archives -- **P2S Empty Archive Tiles** - Fixed FTP search for printers without SD card (Issue #146) -- **File Manager folder navigation** - Fixed folder opening then jumping back to root (Issue #160) -- **File Manager upload** - Now accepts all file types, not just ZIP -- **Multi-plate 3MF metadata** - Single-plate exports now show correct thumbnail -- **Archive card cache** - Fixed wrong cover image bug -- **Archive delete safety** - Added checks to prevent deleting parent directories - -### Statistics & Tracking -- **Print time stats** - Now uses actual elapsed time instead of slicer estimates (Issue #137) -- **Filament cost** - Now uses "Default filament cost" setting instead of hardcoded €25 (Issue #120) -- **Reprint cost tracking** - Now adds cost to existing total instead of replacing -- **K-Profiles backup status** - Fixed showing incorrect printer connection count - -### Smart Plugs -- **HA Energy Sensors** - Fixed sensors with lowercase units (w, kwh) not detected (Issue #119) - -### UI & UX -- **Skip objects modal overflow** - Fixed modal going above browser window (Issue #134) -- **Project card filament badges** - Fixed showing duplicates and raw color codes -- **Subnet scan serial number** - Fixed A1 Mini showing "unknown-*" placeholder (Issue #140) -- **Slicer protocol** - Fixed OS detection (Windows vs macOS/Linux) - -### API & Backend -- **Settings API PATCH Method** - Added for Home Assistant rest_command compatibility (Issue #152) -- **GitHub Backup Timestamps** - Removed volatile timestamps for cleaner git diffs -- **Plate Calibration Persistence** - Fixed reference images not persisting in Docker -- **Update module** - Fixed for Docker-based installations - ---- - -## Maintenance - -- Upgraded vitest from 2.x to 3.x for security improvements -- Added security scanning (pip-audit, npm audit) to CI pipeline -- Replaced python-jose with PyJWT to eliminate ecdsa vulnerability -- Improved test coverage (796 backend tests, 518 frontend tests) - ---- - -## Thank You! - -This release wouldn't be possible without our amazing community. A huge thank you to everyone who contributed code, reported bugs, tested beta releases, and provided feedback! - -### Code Contributors - -| Contributor | Contribution | -|-------------|--------------| -| **[@maziggy](https://github.com/maziggy)** (MartinNYHC) | Lead developer, core features | -| **[@MisterBeardy](https://github.com/MisterBeardy)** (Wesley Reaves) | STL thumbnail generation | -| **[@JesseFPV](https://github.com/JesseFPV)** (Jesse Hulswit) | Optional authentication system | - -### Issue Reporters & Testers - -Special thanks to everyone who reported issues, tested beta releases, and provided valuable feedback: - -- **[@Locxion](https://github.com/Locxion)** (Markus Bender) - A1 Mini status bug, log viewer feature -- **[@cadtoolbox](https://github.com/cadtoolbox)** (Thomas Rambach) - H2D Pro support, model-based queue -- **[@Twilek-de](https://github.com/Twilek-de)** - Empty AMS slots, Mattermost webhooks, progress milestones -- **[@elit3ge](https://github.com/elit3ge)** - Archive tiles, completion photos, upload improvements -- **[@opensourcefan](https://github.com/opensourcefan)** - Subnet scan serial, status colors -- **[@1nv4lidus3r](https://github.com/1nv4lidus3r)** - Print time stats, skip objects modal -- **[@joaorgoncalves](https://github.com/joaorgoncalves)** (João Gonçalves) - Filament cost settings, HA energy sync -- **[@beardofbeespool](https://github.com/beardofbeespool)** (Morton Likely) - STL thumbnail feature request -- **[@PeterXQChen](https://github.com/PeterXQChen)** (Peter Chen) - File manager folder navigation -- **[@Robnex](https://github.com/Robnex)** - External links, external spool sync -- **[@caco3](https://github.com/caco3)** (CaCO3) - Disable firmware checks -- **[@sbcrumb](https://github.com/sbcrumb)** - Camera zoom feature -- **[@fcps3](https://github.com/fcps3)** - Spoolman transparent spool sync -- **[@LucHeart](https://github.com/LucHeart)** - MQTT connection issues -- **[@ouihq](https://github.com/ouihq)** (Jonas) - File manager queue bug -- **[@Schuermi7](https://github.com/Schuermi7)** - AMS mapping, cloud 2FA -- **[@stubbers](https://github.com/stubbers)** (Joseph Stubberfield) - X1C sync issues -- **[@nvdmedianl](https://github.com/nvdmedianl)** (Nathan) - Spoolman Bambu spool errors -- **[@lbeumer-bit](https://github.com/lbeumer-bit)** - Notification photos -- **[@IROKILLER](https://github.com/IROKILLER)** - Home Assistant automations -- **[@fgrfn](https://github.com/fgrfn)** (Florian) - Dynamic electricity cost -- **[@JasonSwindle](https://github.com/JasonSwindle)** (Jason Swindle) - Smart plug text overflow -- **[@Cassiopeia1980](https://github.com/Cassiopeia1980)** - Connection issues - -And many more community members who tested, provided feedback, and helped make Bambuddy better! - ---- - -## Upgrade Notes - -### From 0.1.5.x or earlier -- Database migrations run automatically on startup -- User authentication is optional and disabled by default -- Existing installations will continue to work without changes - -### From 0.1.6 beta -- All beta migrations are included in the final release -- No action required - just update and restart - ---- - -## What's Next? - -We're already planning 0.1.7 with more exciting features. Stay tuned and keep the feedback coming! - -- [GitHub Issues](https://github.com/MisterBeardy/bambuddy/issues) - Report bugs and request features -- [GitHub Discussions](https://github.com/MisterBeardy/bambuddy/discussions) - Join the conversation - ---- - -**Happy Printing!** 🎉 - -*— The Bambuddy Team* diff --git a/backend/app/api/routes/ams_history.py b/backend/app/api/routes/ams_history.py index ffe91e3cf..5896f7dd6 100644 --- a/backend/app/api/routes/ams_history.py +++ b/backend/app/api/routes/ams_history.py @@ -7,8 +7,11 @@ from pydantic import BaseModel from sqlalchemy import and_, func, select from sqlalchemy.ext.asyncio import AsyncSession +from backend.app.core.auth import RequirePermissionIfAuthEnabled from backend.app.core.database import get_db +from backend.app.core.permissions import Permission from backend.app.models.ams_history import AMSSensorHistory +from backend.app.models.user import User router = APIRouter(prefix="/ams-history", tags=["ams-history"]) @@ -38,6 +41,7 @@ async def get_ams_history( ams_id: int, hours: int = Query(default=24, ge=1, le=168, description="Hours of history (1-168)"), db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.AMS_HISTORY_READ), ): """Get AMS sensor history for a specific printer and AMS unit.""" since = datetime.now() - timedelta(hours=hours) @@ -101,6 +105,7 @@ async def delete_old_history( printer_id: int, days: int = Query(default=30, ge=1, le=365, description="Delete data older than X days"), db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.AMS_HISTORY_READ), ): """Delete old AMS history data for a printer.""" cutoff = datetime.now() - timedelta(days=days) diff --git a/backend/app/api/routes/api_keys.py b/backend/app/api/routes/api_keys.py index 033376213..8b289a65a 100644 --- a/backend/app/api/routes/api_keys.py +++ b/backend/app/api/routes/api_keys.py @@ -4,9 +4,11 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from backend.app.core.auth import generate_api_key +from backend.app.core.auth import RequirePermissionIfAuthEnabled, generate_api_key from backend.app.core.database import get_db +from backend.app.core.permissions import Permission from backend.app.models.api_key import APIKey +from backend.app.models.user import User from backend.app.schemas.api_key import ( APIKeyCreate, APIKeyCreateResponse, @@ -20,7 +22,10 @@ router = APIRouter(prefix="/api-keys", tags=["api-keys"]) @router.get("/", response_model=list[APIKeyResponse]) -async def list_api_keys(db: AsyncSession = Depends(get_db)): +async def list_api_keys( + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_READ), +): """List all API keys (without full key values).""" result = await db.execute(select(APIKey).order_by(APIKey.created_at.desc())) return list(result.scalars().all()) @@ -30,6 +35,7 @@ async def list_api_keys(db: AsyncSession = Depends(get_db)): async def create_api_key( data: APIKeyCreate, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_CREATE), ): """Create a new API key. @@ -74,6 +80,7 @@ async def create_api_key( async def get_api_key( key_id: int, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_READ), ): """Get an API key by ID.""" result = await db.execute(select(APIKey).where(APIKey.id == key_id)) @@ -90,6 +97,7 @@ async def update_api_key( key_id: int, data: APIKeyUpdate, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_UPDATE), ): """Update an API key.""" result = await db.execute(select(APIKey).where(APIKey.id == key_id)) @@ -124,6 +132,7 @@ async def update_api_key( async def delete_api_key( key_id: int, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_DELETE), ): """Delete (revoke) an API key.""" result = await db.execute(select(APIKey).where(APIKey.id == key_id)) diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py index d709ea5b7..356e40fc9 100644 --- a/backend/app/api/routes/archives.py +++ b/backend/app/api/routes/archives.py @@ -8,7 +8,10 @@ from fastapi.responses import FileResponse, Response from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from backend.app.core.auth import require_auth_if_enabled, require_ownership_permission +from backend.app.core.auth import ( + RequirePermissionIfAuthEnabled, + require_ownership_permission, +) from backend.app.core.config import settings from backend.app.core.database import get_db from backend.app.core.permissions import Permission @@ -118,6 +121,7 @@ async def list_archives( limit: int = 50, offset: int = 0, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), ): """List archived prints.""" service = ArchiveService(db) @@ -148,6 +152,7 @@ async def search_archives( limit: int = 50, offset: int = 0, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), ): """Full-text search across archives. @@ -229,7 +234,10 @@ async def search_archives( @router.post("/search/rebuild-index") -async def rebuild_search_index(db: AsyncSession = Depends(get_db)): +async def rebuild_search_index( + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_UPDATE_ALL), +): """Rebuild the full-text search index from existing archives. Use this if search results seem incomplete or incorrect. @@ -267,6 +275,7 @@ async def analyze_failures( printer_id: int | None = None, project_id: int | None = None, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), ): """Analyze failure patterns across prints. @@ -291,6 +300,7 @@ async def analyze_failures( async def compare_archives( archive_ids: str = Query(..., description="Comma-separated archive IDs (2-5)"), db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), ): """Compare multiple archives side by side. @@ -331,6 +341,7 @@ async def export_archives( date_to: str | None = Query(None, description="End date (ISO format)"), search: str | None = None, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), ): """Export archives to CSV or Excel format. @@ -393,6 +404,7 @@ async def export_stats( printer_id: int | None = None, project_id: int | None = None, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.STATS_READ), ): """Export statistics summary to CSV or Excel format.""" from fastapi.responses import StreamingResponse @@ -421,7 +433,10 @@ async def export_stats( @router.get("/stats", response_model=ArchiveStats) -async def get_archive_stats(db: AsyncSession = Depends(get_db)): +async def get_archive_stats( + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.STATS_READ), +): """Get statistics across all archives.""" # Total counts total_result = await db.execute(select(func.count(PrintArchive.id))) @@ -451,7 +466,10 @@ async def get_archive_stats(db: AsyncSession = Depends(get_db)): total_seconds += print_time_seconds total_time = total_seconds / 3600 # Convert to hours - filament_result = await db.execute(select(func.sum(PrintArchive.filament_used_grams))) + # Multiply filament by quantity to account for multiple items printed + filament_result = await db.execute( + select(func.sum(PrintArchive.filament_used_grams * func.coalesce(PrintArchive.quantity, 1))) + ) total_filament = filament_result.scalar() or 0 cost_result = await db.execute(select(func.sum(PrintArchive.cost))) @@ -571,7 +589,10 @@ async def get_archive_stats(db: AsyncSession = Depends(get_db)): @router.get("/tags") -async def get_all_tags(db: AsyncSession = Depends(get_db)): +async def get_all_tags( + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), +): """List all unique tags with usage counts. Returns a list of tags sorted by count (descending), then by name. @@ -601,6 +622,7 @@ async def rename_tag( tag_name: str, request: Request, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_UPDATE_ALL), ): """Rename a tag across all archives. @@ -643,7 +665,11 @@ async def rename_tag( @router.delete("/tags/{tag_name}") -async def delete_tag(tag_name: str, db: AsyncSession = Depends(get_db)): +async def delete_tag( + tag_name: str, + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_UPDATE_ALL), +): """Delete a tag from all archives. Returns the count of affected archives. @@ -668,7 +694,11 @@ async def delete_tag(tag_name: str, db: AsyncSession = Depends(get_db)): @router.get("/{archive_id}", response_model=ArchiveResponse) -async def get_archive(archive_id: int, db: AsyncSession = Depends(get_db)): +async def get_archive( + archive_id: int, + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), +): """Get a specific archive.""" service = ArchiveService(db) archive = await service.get_archive(archive_id) @@ -691,6 +721,7 @@ async def find_similar_archives( archive_id: int, limit: int = 10, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), ): """Find archives with similar settings for comparison. @@ -759,6 +790,7 @@ async def update_archive( async def toggle_favorite( archive_id: int, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_UPDATE_OWN), ): """Toggle favorite status for an archive.""" result = await db.execute(select(PrintArchive).where(PrintArchive.id == archive_id)) @@ -773,7 +805,11 @@ async def toggle_favorite( @router.post("/{archive_id}/rescan", response_model=ArchiveResponse) -async def rescan_archive(archive_id: int, db: AsyncSession = Depends(get_db)): +async def rescan_archive( + archive_id: int, + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_UPDATE_ALL), +): """Rescan the 3MF file and update metadata.""" from backend.app.api.routes.settings import get_setting from backend.app.services.archive import ThreeMFParser @@ -832,7 +868,10 @@ async def rescan_archive(archive_id: int, db: AsyncSession = Depends(get_db)): @router.post("/recalculate-costs") -async def recalculate_all_costs(db: AsyncSession = Depends(get_db)): +async def recalculate_all_costs( + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_UPDATE_ALL), +): """Recalculate costs for all archives based on filament usage and prices.""" from backend.app.api.routes.settings import get_setting @@ -862,7 +901,10 @@ async def recalculate_all_costs(db: AsyncSession = Depends(get_db)): @router.post("/rescan-all") -async def rescan_all_archives(db: AsyncSession = Depends(get_db)): +async def rescan_all_archives( + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_UPDATE_ALL), +): """Rescan all archives and update their metadata.""" from backend.app.services.archive import ThreeMFParser @@ -909,7 +951,11 @@ async def rescan_all_archives(db: AsyncSession = Depends(get_db)): @router.get("/{archive_id}/duplicates") -async def get_archive_duplicates(archive_id: int, db: AsyncSession = Depends(get_db)): +async def get_archive_duplicates( + archive_id: int, + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), +): """Get duplicates for a specific archive.""" service = ArchiveService(db) archive = await service.get_archive(archive_id) @@ -927,7 +973,10 @@ async def get_archive_duplicates(archive_id: int, db: AsyncSession = Depends(get @router.post("/backfill-hashes") -async def backfill_content_hashes(db: AsyncSession = Depends(get_db)): +async def backfill_content_hashes( + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_UPDATE_ALL), +): """Compute and store content hashes for all archives missing them.""" result = await db.execute(select(PrintArchive).where(PrintArchive.content_hash.is_(None))) archives = list(result.scalars().all()) @@ -988,6 +1037,7 @@ async def download_archive( archive_id: int, inline: bool = False, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), ): """Download the 3MF file.""" service = ArchiveService(db) @@ -1015,6 +1065,7 @@ async def download_archive_with_filename( archive_id: int, filename: str, db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), ): """Download the 3MF file with filename in URL (for Bambu Studio protocol).""" service = ArchiveService(db) @@ -1034,8 +1085,14 @@ async def download_archive_with_filename( @router.get("/{archive_id}/thumbnail") -async def get_thumbnail(archive_id: int, db: AsyncSession = Depends(get_db)): - """Get the thumbnail image.""" +async def get_thumbnail( + archive_id: int, + db: AsyncSession = Depends(get_db), +): + """Get the thumbnail image. + + Note: Unauthenticated - loaded via tags which can't send auth headers. + """ service = ArchiveService(db) archive = await service.get_archive(archive_id) if not archive or not archive.thumbnail_path: @@ -1059,8 +1116,14 @@ async def get_thumbnail(archive_id: int, db: AsyncSession = Depends(get_db)): @router.get("/{archive_id}/timelapse") -async def get_timelapse(archive_id: int, db: AsyncSession = Depends(get_db)): - """Get the timelapse video.""" +async def get_timelapse( + archive_id: int, + db: AsyncSession = Depends(get_db), +): + """Get the timelapse video. + + Note: Unauthenticated - loaded via