Copy disabled (too large)
Download .txt
Showing preview only (19,278K chars total). Download the full file to get everything.
Repository: maziggy/bambuddy
Branch: main
Commit: 8dd4efa55540
Files: 718
Total size: 17.9 MB
Directory structure:
gitextract_t40zu6xr/
├── .codeql/
│ ├── codeql-config.yml
│ ├── javascript-bambuddy.qls
│ └── python-bambuddy.qls
├── .coverage
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ ├── MAINTAINERS.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── workflows/
│ │ ├── ci.yml
│ │ ├── cleanup-ghcr.yml
│ │ ├── codeql.yml
│ │ ├── issue-closed.yml
│ │ ├── repo-stats.yml
│ │ ├── security.yml
│ │ └── stale.yml
│ └── workflows.disabled/
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .trivyignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DOCKERHUB.md
├── Dockerfile
├── Dockerfile.test
├── LICENSE
├── README.md
├── SECURITY.md
├── UPDATING.md
├── backend/
│ ├── __init__.py
│ ├── app/
│ │ ├── __init__.py
│ │ ├── api/
│ │ │ ├── __init__.py
│ │ │ └── routes/
│ │ │ ├── __init__.py
│ │ │ ├── ams_history.py
│ │ │ ├── api_keys.py
│ │ │ ├── archives.py
│ │ │ ├── auth.py
│ │ │ ├── background_dispatch.py
│ │ │ ├── bug_report.py
│ │ │ ├── camera.py
│ │ │ ├── cloud.py
│ │ │ ├── discovery.py
│ │ │ ├── external_links.py
│ │ │ ├── filaments.py
│ │ │ ├── firmware.py
│ │ │ ├── github_backup.py
│ │ │ ├── groups.py
│ │ │ ├── inventory.py
│ │ │ ├── kprofiles.py
│ │ │ ├── library.py
│ │ │ ├── local_backup.py
│ │ │ ├── local_presets.py
│ │ │ ├── maintenance.py
│ │ │ ├── metrics.py
│ │ │ ├── mfa.py
│ │ │ ├── notification_templates.py
│ │ │ ├── notifications.py
│ │ │ ├── obico.py
│ │ │ ├── pending_uploads.py
│ │ │ ├── print_log.py
│ │ │ ├── print_queue.py
│ │ │ ├── printers.py
│ │ │ ├── projects.py
│ │ │ ├── settings.py
│ │ │ ├── smart_plugs.py
│ │ │ ├── spoolbuddy.py
│ │ │ ├── spoolman.py
│ │ │ ├── support.py
│ │ │ ├── system.py
│ │ │ ├── updates.py
│ │ │ ├── user_notifications.py
│ │ │ ├── users.py
│ │ │ ├── virtual_printers.py
│ │ │ ├── webhook.py
│ │ │ └── websocket.py
│ │ ├── cli.py
│ │ ├── core/
│ │ │ ├── __init__.py
│ │ │ ├── auth.py
│ │ │ ├── catalog_defaults.py
│ │ │ ├── compat.py
│ │ │ ├── config.py
│ │ │ ├── database.py
│ │ │ ├── db_dialect.py
│ │ │ ├── encryption.py
│ │ │ ├── permissions.py
│ │ │ └── websocket.py
│ │ ├── i18n/
│ │ │ └── __init__.py
│ │ ├── main.py
│ │ ├── models/
│ │ │ ├── __init__.py
│ │ │ ├── active_print_spoolman.py
│ │ │ ├── ams_history.py
│ │ │ ├── ams_label.py
│ │ │ ├── api_key.py
│ │ │ ├── archive.py
│ │ │ ├── auth_ephemeral.py
│ │ │ ├── bug_report.py
│ │ │ ├── color_catalog.py
│ │ │ ├── external_link.py
│ │ │ ├── filament.py
│ │ │ ├── github_backup.py
│ │ │ ├── group.py
│ │ │ ├── kprofile_note.py
│ │ │ ├── library.py
│ │ │ ├── local_preset.py
│ │ │ ├── maintenance.py
│ │ │ ├── notification.py
│ │ │ ├── notification_template.py
│ │ │ ├── oidc_provider.py
│ │ │ ├── orca_base_cache.py
│ │ │ ├── pending_upload.py
│ │ │ ├── print_batch.py
│ │ │ ├── print_log.py
│ │ │ ├── print_queue.py
│ │ │ ├── printer.py
│ │ │ ├── project.py
│ │ │ ├── project_bom.py
│ │ │ ├── settings.py
│ │ │ ├── slot_preset.py
│ │ │ ├── smart_plug.py
│ │ │ ├── smart_plug_energy_snapshot.py
│ │ │ ├── spool.py
│ │ │ ├── spool_assignment.py
│ │ │ ├── spool_catalog.py
│ │ │ ├── spool_k_profile.py
│ │ │ ├── spool_usage_history.py
│ │ │ ├── spoolbuddy_device.py
│ │ │ ├── user.py
│ │ │ ├── user_email_pref.py
│ │ │ ├── user_otp_code.py
│ │ │ ├── user_totp.py
│ │ │ └── virtual_printer.py
│ │ ├── schemas/
│ │ │ ├── __init__.py
│ │ │ ├── api_key.py
│ │ │ ├── archive.py
│ │ │ ├── auth.py
│ │ │ ├── cloud.py
│ │ │ ├── external_link.py
│ │ │ ├── filament.py
│ │ │ ├── github_backup.py
│ │ │ ├── group.py
│ │ │ ├── kprofile.py
│ │ │ ├── library.py
│ │ │ ├── local_preset.py
│ │ │ ├── maintenance.py
│ │ │ ├── notification.py
│ │ │ ├── notification_template.py
│ │ │ ├── print_log.py
│ │ │ ├── print_queue.py
│ │ │ ├── printer.py
│ │ │ ├── project.py
│ │ │ ├── settings.py
│ │ │ ├── smart_plug.py
│ │ │ ├── spool.py
│ │ │ ├── spool_usage.py
│ │ │ ├── spoolbuddy.py
│ │ │ ├── timelapse.py
│ │ │ └── user_notifications.py
│ │ ├── services/
│ │ │ ├── __init__.py
│ │ │ ├── archive.py
│ │ │ ├── archive_comparison.py
│ │ │ ├── background_dispatch.py
│ │ │ ├── bambu_cloud.py
│ │ │ ├── bambu_ftp.py
│ │ │ ├── bambu_mqtt.py
│ │ │ ├── bug_report.py
│ │ │ ├── camera.py
│ │ │ ├── discovery.py
│ │ │ ├── email_service.py
│ │ │ ├── export.py
│ │ │ ├── external_camera.py
│ │ │ ├── failure_analysis.py
│ │ │ ├── firmware_check.py
│ │ │ ├── firmware_update.py
│ │ │ ├── github_backup.py
│ │ │ ├── hms_errors.py
│ │ │ ├── homeassistant.py
│ │ │ ├── layer_timelapse.py
│ │ │ ├── ldap_service.py
│ │ │ ├── local_backup.py
│ │ │ ├── mqtt_relay.py
│ │ │ ├── mqtt_smart_plug.py
│ │ │ ├── network_utils.py
│ │ │ ├── notification_service.py
│ │ │ ├── obico_actions.py
│ │ │ ├── obico_detection.py
│ │ │ ├── obico_smoothing.py
│ │ │ ├── opentag3d.py
│ │ │ ├── orca_profiles.py
│ │ │ ├── plate_detection.py
│ │ │ ├── print_log.py
│ │ │ ├── print_scheduler.py
│ │ │ ├── printer_manager.py
│ │ │ ├── rest_smart_plug.py
│ │ │ ├── smart_plug_manager.py
│ │ │ ├── spool_assignment_notifications.py
│ │ │ ├── spool_tag_matcher.py
│ │ │ ├── spoolbuddy_ssh.py
│ │ │ ├── spoolman.py
│ │ │ ├── spoolman_tracking.py
│ │ │ ├── stl_thumbnail.py
│ │ │ ├── tasmota.py
│ │ │ ├── timelapse_processor.py
│ │ │ ├── usage_tracker.py
│ │ │ └── virtual_printer/
│ │ │ ├── __init__.py
│ │ │ ├── bind_server.py
│ │ │ ├── certificate.py
│ │ │ ├── ftp_server.py
│ │ │ ├── manager.py
│ │ │ ├── mqtt_server.py
│ │ │ ├── ssdp_server.py
│ │ │ └── tcp_proxy.py
│ │ └── utils/
│ │ ├── color_utils.py
│ │ ├── filament_ids.py
│ │ ├── printer_models.py
│ │ ├── tag_normalization.py
│ │ └── threemf_tools.py
│ └── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── integration/
│ │ ├── __init__.py
│ │ ├── test_advanced_auth_api.py
│ │ ├── test_ams_history_api.py
│ │ ├── test_ams_labels_api.py
│ │ ├── test_archives_api.py
│ │ ├── test_auth_api.py
│ │ ├── test_available_filaments.py
│ │ ├── test_background_dispatch_api.py
│ │ ├── test_camera_api.py
│ │ ├── test_client_ip.py
│ │ ├── test_cloud_auth.py
│ │ ├── test_color_map_api.py
│ │ ├── test_cost_statistics.py
│ │ ├── test_discovery_api.py
│ │ ├── test_endpoint_auth.py
│ │ ├── test_external_folders_api.py
│ │ ├── test_external_links_api.py
│ │ ├── test_filaments_api.py
│ │ ├── test_github_backup_api.py
│ │ ├── test_inventory_assign.py
│ │ ├── test_library_api.py
│ │ ├── test_maintenance_api.py
│ │ ├── test_metrics_api.py
│ │ ├── test_mfa_api.py
│ │ ├── test_notifications_api.py
│ │ ├── test_obico_api.py
│ │ ├── test_ownership_permissions.py
│ │ ├── test_print_lifecycle.py
│ │ ├── test_print_queue_api.py
│ │ ├── test_printers_api.py
│ │ ├── test_projects_api.py
│ │ ├── test_security.py
│ │ ├── test_settings_api.py
│ │ ├── test_sjf_scheduling.py
│ │ ├── test_smart_plugs_api.py
│ │ ├── test_spoolbuddy.py
│ │ ├── test_spoolman_api.py
│ │ ├── test_support_api.py
│ │ ├── test_system_api.py
│ │ ├── test_updates_api.py
│ │ ├── test_user_notifications_api.py
│ │ └── test_virtual_printer_api.py
│ ├── pytest.ini
│ └── unit/
│ ├── __init__.py
│ ├── services/
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── mock_ftp_server.py
│ │ ├── test_archive_copy.py
│ │ ├── test_archive_service.py
│ │ ├── test_background_dispatch.py
│ │ ├── test_bambu_cloud.py
│ │ ├── test_bambu_ftp.py
│ │ ├── test_bambu_mqtt.py
│ │ ├── test_camera_tls_proxy.py
│ │ ├── test_email_service.py
│ │ ├── test_external_camera.py
│ │ ├── test_hms_errors.py
│ │ ├── test_layer_timelapse.py
│ │ ├── test_ldap_service.py
│ │ ├── test_mqtt_smart_plug_subscribe.py
│ │ ├── test_notification_service.py
│ │ ├── test_plate_detection.py
│ │ ├── test_printer_manager.py
│ │ ├── test_rest_smart_plug.py
│ │ ├── test_smart_plug_manager.py
│ │ ├── test_spool_assignment_notifications.py
│ │ ├── test_spool_tag_matcher.py
│ │ ├── test_spoolbuddy_ssh.py
│ │ ├── test_spoolman_service.py
│ │ ├── test_spoolman_tracking.py
│ │ ├── test_stl_thumbnail.py
│ │ ├── test_tasmota.py
│ │ ├── test_usage_tracker.py
│ │ └── test_virtual_printer.py
│ ├── test_archive_file_path_guard.py
│ ├── test_archive_filtering.py
│ ├── test_bed_jog.py
│ ├── test_bug_report.py
│ ├── test_bulk_spool_create.py
│ ├── test_camera_stderr_summary.py
│ ├── test_capture_pid_tracking.py
│ ├── test_catalog_bulk_delete.py
│ ├── test_cli.py
│ ├── test_code_quality.py
│ ├── test_color_utils.py
│ ├── test_cost_tracking.py
│ ├── test_db_dialect.py
│ ├── test_energy_snapshots.py
│ ├── test_firmware_versions.py
│ ├── test_gcode_injection.py
│ ├── test_homeassistant_settings.py
│ ├── test_ldap_migration.py
│ ├── test_local_backup.py
│ ├── test_log_error_detection.py
│ ├── test_maintenance_rod_filtering.py
│ ├── test_mfa_helpers.py
│ ├── test_obico_detection.py
│ ├── test_obico_smoothing.py
│ ├── test_opentag3d.py
│ ├── test_orca_profiles.py
│ ├── test_permissions.py
│ ├── test_permissions_stats_filter.py
│ ├── test_phantom_print_hardening.py
│ ├── test_plate_object_extraction.py
│ ├── test_print_log.py
│ ├── test_print_speed.py
│ ├── test_print_start_expected_promotion.py
│ ├── test_printer_models.py
│ ├── test_run_with_retry.py
│ ├── test_scheduler_ams_mapping.py
│ ├── test_scheduler_auto_drying.py
│ ├── test_scheduler_busy_only.py
│ ├── test_scheduler_clear_plate.py
│ ├── test_scheduler_filament_override.py
│ ├── test_scheduler_watchdog.py
│ ├── test_slicer_settings.py
│ ├── test_slot_preset_key.py
│ ├── test_spool_schemas_rgba.py
│ ├── test_spoolbuddy_system_stats.py
│ ├── test_spoolman_clear_location.py
│ ├── test_subtask_archive_resume.py
│ ├── test_support_helpers.py
│ ├── test_sync_ams_weights.py
│ ├── test_threemf_tools.py
│ ├── test_usage_tracker.py
│ ├── test_user_notifications.py
│ ├── test_vp_ftp_port.py
│ └── test_vp_mqtt_server.py
├── build_docker.sh
├── deploy/
│ └── bambuddy.service
├── docker-compose.test.yml
├── docker-compose.yml
├── docker-publish-beta.sh
├── docker-publish-daily-beta.sh
├── docker-publish.sh
├── docs/
│ ├── ams_slot_printer_matrix.txt
│ ├── bambu_lab_preset_sync_api.md
│ └── migration-vp-ftp-port.md
├── frontend/
│ ├── .gitignore
│ ├── .npmrc
│ ├── README.md
│ ├── docs/
│ │ └── create_proxy_diagram.py
│ ├── eslint.config.js
│ ├── index.html
│ ├── mockups/
│ │ └── ams-redesign.html
│ ├── package.json
│ ├── postcss.config.js
│ ├── public/
│ │ ├── manifest.json
│ │ ├── sw-register.js
│ │ └── sw.js
│ ├── scripts/
│ │ └── check-i18n-parity.mjs
│ ├── src/
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── __tests__/
│ │ │ ├── api/
│ │ │ │ ├── client.test.ts
│ │ │ │ └── githubBackupApi.test.ts
│ │ │ ├── components/
│ │ │ │ ├── AMSHistoryModal.test.tsx
│ │ │ │ ├── AddPrinterDiscovery.test.tsx
│ │ │ │ ├── AssignSpoolModal.test.tsx
│ │ │ │ ├── BackupModal.test.tsx
│ │ │ │ ├── BugReportBubble.test.tsx
│ │ │ │ ├── Button.test.tsx
│ │ │ │ ├── Card.test.tsx
│ │ │ │ ├── ConfigureAmsSlotModal.test.tsx
│ │ │ │ ├── ConfirmModal.test.tsx
│ │ │ │ ├── ContextMenu.test.tsx
│ │ │ │ ├── Dashboard.test.tsx
│ │ │ │ ├── EditArchiveModal.test.tsx
│ │ │ │ ├── FailureDetectionSettings.test.tsx
│ │ │ │ ├── FilamentHoverCard.test.tsx
│ │ │ │ ├── FilamentOverride.test.tsx
│ │ │ │ ├── FilamentSlotCircle.test.tsx
│ │ │ │ ├── FileManagerModal.test.tsx
│ │ │ │ ├── FileUploadModal.test.tsx
│ │ │ │ ├── GitHubBackupSettings.scheduled.test.tsx
│ │ │ │ ├── HMSErrorModal.test.tsx
│ │ │ │ ├── Layout.test.tsx
│ │ │ │ ├── LinkSpoolModal.test.tsx
│ │ │ │ ├── LocalProfilesView.test.tsx
│ │ │ │ ├── ModelViewerModal.test.tsx
│ │ │ │ ├── NotificationProviderCard.test.tsx
│ │ │ │ ├── PrintModal.test.tsx
│ │ │ │ ├── PrintModalDispatchToast.test.tsx
│ │ │ │ ├── PrinterQueueWidget.test.tsx
│ │ │ │ ├── PrinterSelector.test.ts
│ │ │ │ ├── RestoreModal.test.tsx
│ │ │ │ ├── SmartPlugCard.test.tsx
│ │ │ │ ├── SpoolBuddySettings.test.tsx
│ │ │ │ ├── SpoolFormBulk.test.tsx
│ │ │ │ ├── SpoolFormModal.test.tsx
│ │ │ │ ├── SpoolInfoCard.test.tsx
│ │ │ │ ├── SpoolmanSettings.test.tsx
│ │ │ │ ├── TagDetectedModal.test.tsx
│ │ │ │ ├── TagManagementModal.test.tsx
│ │ │ │ ├── Toggle.test.tsx
│ │ │ │ ├── UploadModal.test.tsx
│ │ │ │ ├── VirtualPrinterCard.test.tsx
│ │ │ │ ├── VirtualPrinterSettings.test.tsx
│ │ │ │ ├── WeightDisplay.test.tsx
│ │ │ │ ├── spool-form/
│ │ │ │ │ └── ColorSectionHexInput.test.tsx
│ │ │ │ └── spoolbuddy/
│ │ │ │ ├── AmsUnitCard.test.tsx
│ │ │ │ ├── SpoolBuddyBottomNav.test.tsx
│ │ │ │ ├── SpoolBuddyLayout.test.tsx
│ │ │ │ ├── SpoolBuddyQuickMenu.test.tsx
│ │ │ │ ├── SpoolBuddyStatusBar.test.tsx
│ │ │ │ ├── SpoolBuddyTopBar.test.tsx
│ │ │ │ └── SpoolIcon.test.tsx
│ │ │ ├── contexts/
│ │ │ │ ├── AuthContext.test.tsx
│ │ │ │ ├── ColorCatalogContext.test.tsx
│ │ │ │ └── ToastContext.test.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useCameraStreamToken.test.ts
│ │ │ │ ├── useFilamentMapping.test.ts
│ │ │ │ ├── useIsMobile.test.ts
│ │ │ │ ├── useLongPress.test.ts
│ │ │ │ ├── useSpoolBuddyState.test.ts
│ │ │ │ └── useWebSocket.test.ts
│ │ │ ├── i18n/
│ │ │ │ ├── locales.test.ts
│ │ │ │ └── parity-script.test.ts
│ │ │ ├── mocks/
│ │ │ │ ├── handlers.ts
│ │ │ │ └── server.ts
│ │ │ ├── pages/
│ │ │ │ ├── ArchivesPage.test.tsx
│ │ │ │ ├── CameraPage.test.tsx
│ │ │ │ ├── FileManagerExternalFolder.test.tsx
│ │ │ │ ├── FileManagerPage.test.tsx
│ │ │ │ ├── GroupEditPage.test.tsx
│ │ │ │ ├── InventoryPageGrouping.test.ts
│ │ │ │ ├── InventoryPageLowStock.test.tsx
│ │ │ │ ├── LoginPage.test.tsx
│ │ │ │ ├── MaintenancePage.test.tsx
│ │ │ │ ├── NotificationsPage.test.tsx
│ │ │ │ ├── PrintersPage.test.tsx
│ │ │ │ ├── PrintersPageDrying.test.ts
│ │ │ │ ├── PrintersPageFillLevel.test.ts
│ │ │ │ ├── PrintersPageFormatPrintName.test.ts
│ │ │ │ ├── PrintersPageSpeed.test.tsx
│ │ │ │ ├── ProjectDetailPage.test.tsx
│ │ │ │ ├── ProjectsPage.test.tsx
│ │ │ │ ├── QueuePage.test.tsx
│ │ │ │ ├── SettingsPage.test.tsx
│ │ │ │ ├── SpoolBuddyAmsPageLogic.test.ts
│ │ │ │ ├── SpoolBuddyCalibrationPage.test.tsx
│ │ │ │ ├── SpoolBuddyDashboard.test.tsx
│ │ │ │ ├── SpoolBuddySettingsPage.test.tsx
│ │ │ │ ├── SpoolBuddyWriteTagPage.test.tsx
│ │ │ │ ├── StatsPage.test.tsx
│ │ │ │ ├── StreamOverlayPage.test.tsx
│ │ │ │ └── SystemInfoPage.test.tsx
│ │ │ ├── setup.ts
│ │ │ ├── utils/
│ │ │ │ ├── colors.test.ts
│ │ │ │ ├── currency.test.ts
│ │ │ │ ├── date.test.ts
│ │ │ │ ├── file.test.ts
│ │ │ │ ├── firmwareVersion.test.ts
│ │ │ │ ├── getSpoolmanFillLevel.test.ts
│ │ │ │ ├── maintenanceWikiUrls.test.ts
│ │ │ │ ├── printer.test.ts
│ │ │ │ └── slicer.test.ts
│ │ │ └── utils.tsx
│ │ ├── api/
│ │ │ └── client.ts
│ │ ├── components/
│ │ │ ├── AMSHistoryModal.tsx
│ │ │ ├── APIBrowser.tsx
│ │ │ ├── AddExternalLinkModal.tsx
│ │ │ ├── AddNotificationModal.tsx
│ │ │ ├── AddSmartPlugModal.tsx
│ │ │ ├── AssignSpoolModal.tsx
│ │ │ ├── BackupModal.tsx
│ │ │ ├── BatchProjectModal.tsx
│ │ │ ├── BatchTagModal.tsx
│ │ │ ├── BugReportBubble.tsx
│ │ │ ├── BulkPrinterToolbar.tsx
│ │ │ ├── Button.tsx
│ │ │ ├── CalendarView.tsx
│ │ │ ├── Card.tsx
│ │ │ ├── Collapsible.tsx
│ │ │ ├── ColorCatalogSettings.tsx
│ │ │ ├── ColumnConfigModal.tsx
│ │ │ ├── CompactHistoryRow.tsx
│ │ │ ├── CompareArchivesModal.tsx
│ │ │ ├── ConfigureAmsSlotModal.tsx
│ │ │ ├── ConfirmModal.tsx
│ │ │ ├── ContextMenu.tsx
│ │ │ ├── CreateUserAdvancedAuthModal.tsx
│ │ │ ├── Dashboard.tsx
│ │ │ ├── EditArchiveModal.tsx
│ │ │ ├── EmailSettings.tsx
│ │ │ ├── EmbeddedCameraViewer.tsx
│ │ │ ├── ExternalLinksSettings.tsx
│ │ │ ├── FailureDetectionSettings.tsx
│ │ │ ├── FilamentHoverCard.tsx
│ │ │ ├── FilamentSlotCircle.tsx
│ │ │ ├── FilamentTrends.tsx
│ │ │ ├── FileManagerModal.tsx
│ │ │ ├── FileUploadModal.tsx
│ │ │ ├── GcodeViewer.tsx
│ │ │ ├── GitHubBackupSettings.tsx
│ │ │ ├── HMSErrorModal.tsx
│ │ │ ├── IconPicker.tsx
│ │ │ ├── KProfilesView.tsx
│ │ │ ├── KeyboardShortcutsModal.tsx
│ │ │ ├── LDAPSettings.tsx
│ │ │ ├── Layout.tsx
│ │ │ ├── LinkSpoolModal.tsx
│ │ │ ├── LocalProfilesView.tsx
│ │ │ ├── LogViewer.tsx
│ │ │ ├── MQTTDebugModal.tsx
│ │ │ ├── MetricToggle.tsx
│ │ │ ├── ModelViewer.tsx
│ │ │ ├── ModelViewerModal.tsx
│ │ │ ├── NotificationLogViewer.tsx
│ │ │ ├── NotificationProviderCard.tsx
│ │ │ ├── NotificationTemplateEditor.tsx
│ │ │ ├── OIDCProviderSettings.tsx
│ │ │ ├── PendingUploadsPanel.tsx
│ │ │ ├── PhotoGalleryModal.tsx
│ │ │ ├── PrintCalendar.tsx
│ │ │ ├── PrintModal/
│ │ │ │ ├── FilamentMapping.tsx
│ │ │ │ ├── FilamentOverride.tsx
│ │ │ │ ├── PlateSelector.tsx
│ │ │ │ ├── PrintOptions.tsx
│ │ │ │ ├── PrinterSelector.tsx
│ │ │ │ ├── ScheduleOptions.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── types.ts
│ │ │ ├── PrinterInfoModal.tsx
│ │ │ ├── PrinterQueueWidget.tsx
│ │ │ ├── ProjectPageModal.tsx
│ │ │ ├── QRCodeModal.tsx
│ │ │ ├── QueueStatsBar.tsx
│ │ │ ├── QueueTimelineView.tsx
│ │ │ ├── RestoreModal.tsx
│ │ │ ├── RichTextEditor.tsx
│ │ │ ├── SkipObjectsModal.tsx
│ │ │ ├── SmartPlugCard.tsx
│ │ │ ├── SpoolBuddySettings.tsx
│ │ │ ├── SpoolCatalogSettings.tsx
│ │ │ ├── SpoolFormModal.tsx
│ │ │ ├── SpoolUsageHistory.tsx
│ │ │ ├── SpoolmanSettings.tsx
│ │ │ ├── SwitchbarPopover.tsx
│ │ │ ├── TagManagementModal.tsx
│ │ │ ├── TimelapseEditorModal.tsx
│ │ │ ├── TimelapseViewer.tsx
│ │ │ ├── Toggle.tsx
│ │ │ ├── TwoFactorSettings.tsx
│ │ │ ├── UploadModal.tsx
│ │ │ ├── VirtualKeyboard.css
│ │ │ ├── VirtualKeyboard.tsx
│ │ │ ├── VirtualPrinterAddDialog.tsx
│ │ │ ├── VirtualPrinterCard.tsx
│ │ │ ├── VirtualPrinterList.tsx
│ │ │ ├── VirtualPrinterSettings.tsx
│ │ │ ├── icons/
│ │ │ │ ├── ChamberLight.tsx
│ │ │ │ ├── PlateClearedIcon.tsx
│ │ │ │ └── WifiSignal.tsx
│ │ │ ├── spool-form/
│ │ │ │ ├── AdditionalSection.tsx
│ │ │ │ ├── ColorSection.tsx
│ │ │ │ ├── FilamentSection.tsx
│ │ │ │ ├── PAProfileSection.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils.ts
│ │ │ └── spoolbuddy/
│ │ │ ├── AmsUnitCard.tsx
│ │ │ ├── AssignToAmsModal.tsx
│ │ │ ├── DiagnosticModal.tsx
│ │ │ ├── InventorySpoolInfoCard.tsx
│ │ │ ├── LinkSpoolModal.tsx
│ │ │ ├── SpoolBuddyBottomNav.tsx
│ │ │ ├── SpoolBuddyLayout.tsx
│ │ │ ├── SpoolBuddyQuickMenu.tsx
│ │ │ ├── SpoolBuddyStatusBar.tsx
│ │ │ ├── SpoolBuddyTopBar.tsx
│ │ │ ├── SpoolIcon.tsx
│ │ │ ├── SpoolInfoCard.tsx
│ │ │ ├── TagDetectedModal.tsx
│ │ │ └── WeightDisplay.tsx
│ │ ├── contexts/
│ │ │ ├── AuthContext.tsx
│ │ │ ├── ColorCatalogContext.tsx
│ │ │ ├── ThemeContext.tsx
│ │ │ └── ToastContext.tsx
│ │ ├── hooks/
│ │ │ ├── useCameraStreamToken.ts
│ │ │ ├── useColorCatalogVersion.ts
│ │ │ ├── useFilamentMapping.ts
│ │ │ ├── useIsMobile.ts
│ │ │ ├── useIsSidebarCompact.ts
│ │ │ ├── useLongPress.ts
│ │ │ ├── useMultiPrinterFilamentMapping.ts
│ │ │ ├── useSpoolBuddyState.ts
│ │ │ └── useWebSocket.ts
│ │ ├── i18n/
│ │ │ ├── index.ts
│ │ │ └── locales/
│ │ │ ├── de.ts
│ │ │ ├── en.ts
│ │ │ ├── fr.ts
│ │ │ ├── it.ts
│ │ │ ├── ja.ts
│ │ │ ├── pt-BR.ts
│ │ │ ├── zh-CN.ts
│ │ │ └── zh-TW.ts
│ │ ├── index.css
│ │ ├── lib/
│ │ │ └── settingsSearch.ts
│ │ ├── main.tsx
│ │ ├── pages/
│ │ │ ├── ArchivesPage.tsx
│ │ │ ├── CameraPage.tsx
│ │ │ ├── ExternalLinkPage.tsx
│ │ │ ├── FileManagerPage.tsx
│ │ │ ├── GroupEditPage.tsx
│ │ │ ├── InventoryPage.tsx
│ │ │ ├── LoginPage.tsx
│ │ │ ├── MaintenancePage.tsx
│ │ │ ├── NotificationsPage.tsx
│ │ │ ├── PrintersPage.tsx
│ │ │ ├── ProfilesPage.tsx
│ │ │ ├── ProjectDetailPage.tsx
│ │ │ ├── ProjectsPage.tsx
│ │ │ ├── QueuePage.tsx
│ │ │ ├── SettingsPage.tsx
│ │ │ ├── SetupPage.tsx
│ │ │ ├── StatsPage.tsx
│ │ │ ├── StreamOverlayPage.tsx
│ │ │ ├── SystemInfoPage.tsx
│ │ │ ├── UsersPage.tsx
│ │ │ └── spoolbuddy/
│ │ │ ├── SpoolBuddyAmsPage.tsx
│ │ │ ├── SpoolBuddyCalibrationPage.tsx
│ │ │ ├── SpoolBuddyDashboard.tsx
│ │ │ ├── SpoolBuddyInventoryPage.tsx
│ │ │ ├── SpoolBuddySettingsPage.tsx
│ │ │ └── SpoolBuddyWriteTagPage.tsx
│ │ ├── types/
│ │ │ └── plates.ts
│ │ └── utils/
│ │ ├── amsHelpers.ts
│ │ ├── colors.ts
│ │ ├── currency.ts
│ │ ├── date.ts
│ │ ├── file.ts
│ │ ├── firmwareVersion.ts
│ │ ├── maintenanceWikiUrls.ts
│ │ ├── printName.ts
│ │ ├── printer.ts
│ │ ├── slicer.ts
│ │ └── weight.ts
│ ├── tailwind.config.js
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── vitest.config.ts
├── install/
│ ├── README.md
│ ├── docker-install.sh
│ ├── install.sh
│ ├── update.sh
│ └── update_macos.sh
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── scripts/
│ ├── debug_preset.py
│ ├── import_spoolman.py
│ ├── mqtt_sniffer.py
│ ├── update_archive_date.py
│ └── update_archive_quantities.py
├── spoolbuddy/
│ ├── README.md
│ ├── daemon/
│ │ ├── __init__.py
│ │ ├── api_client.py
│ │ ├── config.py
│ │ ├── display_control.py
│ │ ├── main.py
│ │ ├── nau7802.py
│ │ ├── nfc_reader.py
│ │ ├── pn5180.py
│ │ ├── scale_reader.py
│ │ ├── system_stats.py
│ │ ├── systemd/
│ │ │ └── spoolbuddy.service
│ │ └── tag_parser.py
│ ├── install/
│ │ ├── generate_splash.py
│ │ ├── install.sh
│ │ └── spoolbuddy-idle.sh
│ ├── scripts/
│ │ ├── pn5180_diag.py
│ │ ├── read_tag.py
│ │ └── scale_diag.py
│ └── tests/
│ ├── __init__.py
│ ├── test_api_client.py
│ ├── test_config.py
│ ├── test_display_control.py
│ ├── test_main.py
│ └── test_tag_parser.py
├── static/
│ ├── assets/
│ │ ├── index-BoxU3Y8Y.css
│ │ └── index-NbcE7Ots.js
│ ├── index.html
│ ├── manifest.json
│ ├── sw-register.js
│ └── sw.js
├── test_all.sh
├── test_backend.sh
├── test_docker.sh
├── test_frontend.sh
├── test_security.sh
├── tests/
│ ├── e2e_comprehensive_test.py
│ └── e2e_toggle_persistence_test.py
└── update_website_wiki.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .codeql/codeql-config.yml
================================================
name: "Bambuddy CodeQL Configuration"
# Uses the default query suite with accepted-risk exclusions.
# Each exclusion is reviewed and documented below.
query-filters:
# ── Python Accepted Risk ─────────────────────────────────────
# Log injection: All logging uses %s parameterized style.
# Remaining findings are CodeQL taint-tracking printer/device data
# to parameterized log args. Accepted risk for local network tool.
- exclude:
id: py/log-injection
# Cyclic imports: SQLAlchemy ORM pattern — models import
# database base class, database imports models for migrations.
- exclude:
id: py/cyclic-import
- exclude:
id: py/unsafe-cyclic-import
# Unused local variables: Python _ prefix convention for
# intentional discards (tuple unpacking, test fixture side effects).
- exclude:
id: py/unused-local-variable
# Path injection: All paths validated — extension whitelists,
# traversal checks (rejects .. / \), UUID-based naming, or
# constructed from integer IDs in controlled base directories.
- exclude:
id: py/path-injection
# Stack trace exposure: str(e) replaced with generic messages
# in HTTP responses. Remaining findings are CodeQL tracing through
# _update_status dict returns, not actual exposures.
- exclude:
id: py/stack-trace-exposure
# Socket bind to 0.0.0.0: Virtual printer SSDP/discovery
# services must bind all interfaces for LAN discoverability.
- exclude:
id: py/bind-socket-all-network-interfaces
# SSRF: URLs come from admin-configured settings (external
# cameras, Home Assistant, Tasmota). Validation added for scheme,
# hostname, and metadata-service blocking.
- exclude:
id: py/partial-ssrf
- exclude:
id: py/full-ssrf
# Unused global variables: False positives — module-level
# cache variables written via `global` in one function, read in another.
- exclude:
id: py/unused-global-variable
# Clear-text logging sensitive data: False positive —
# `api_key` in firmware_check.py is a printer model identifier
# string ("x1", "p1", "a1-mini"), not a secret.
- exclude:
id: py/clear-text-logging-sensitive-data
# Clear-text storage sensitive data: JWT secret stored in
# file with 0600 permissions. Standard for single-host deployment.
- exclude:
id: py/clear-text-storage-sensitive-data
# Weak hashing on sensitive data: MD5 used with
# usedforsecurity=False for AMS tray fingerprinting, not security.
- exclude:
id: py/weak-sensitive-data-hashing
# Catch base exception: In frontend/node_modules third-party
# code (flatted/python/flatted.py), outside our control.
- exclude:
id: py/catch-base-exception
# LDAP injection: All user input is RFC 4515 escaped via
# _ldap_escape() (ldap_service.py:282) before interpolation
# into search filters. CodeQL does not trace through the
# escape replace-loop and reports false positives on lines
# 131 / 183 / 198 where escaped values are reused.
- exclude:
id: py/ldap-injection
# Incomplete URL substring sanitization: Only triggers in
# test assertions (test_cloud_auth.py) that verify the
# mocked HTTP client saw the right hostname
# (e.g. `"api.bambulab.cn" in captured_url`). URLs come
# from a mock's captured_urls list, not user input.
- exclude:
id: py/incomplete-url-substring-sanitization
# ── JavaScript Accepted Risk ─────────────────────────────────
# XSS through DOM: False positives —
# 1. coverage/sorter.js: generated Istanbul coverage report
# 2. TimelapseEditorModal.tsx: URL.createObjectURL(file) creates
# a safe blob: URL used as <audio src>, not HTML injection.
- exclude:
id: js/xss-through-dom
================================================
FILE: .codeql/javascript-bambuddy.qls
================================================
# Bambuddy JavaScript Security & Quality Suite
#
# Extends the standard javascript-security-and-quality suite,
# excluding false positives documented below.
- description: "Bambuddy JavaScript security and quality"
- import: codeql-suites/javascript-security-and-quality.qls
from: codeql/javascript-queries
# XSS through DOM (2): False positives —
# 1. coverage/sorter.js: generated Istanbul coverage report, not our code
# 2. TimelapseEditorModal.tsx: URL.createObjectURL(file) creates a safe
# blob: URL used as <audio src>, not HTML content injection
- exclude:
id: js/xss-through-dom
================================================
FILE: .codeql/python-bambuddy.qls
================================================
# Bambuddy Python Security & Quality Suite
#
# Extends the standard python-security-and-quality suite, excluding
# accepted-risk findings documented below.
#
# All excluded findings have been reviewed and either:
# - Fixed in code (validation added) but CodeQL still traces taint
# - Confirmed false positive after code inspection
# - Accepted risk for a local-network admin tool
- description: "Bambuddy Python security and quality"
- import: codeql-suites/python-security-and-quality.qls
from: codeql/python-queries
# ── Accepted Risk ─────────────────────────────────────────────
# Log injection (131): All logging uses %s parameterized style.
# Remaining findings are CodeQL taint-tracking printer/device data
# to parameterized log args. Accepted risk for local network tool.
- exclude:
id: py/log-injection
# Cyclic imports (70+2): SQLAlchemy ORM pattern — models import
# database base class, database imports models for migrations.
- exclude:
id: py/cyclic-import
- exclude:
id: py/unsafe-cyclic-import
# Unused local variables (11): Python _ prefix convention for
# intentional discards (tuple unpacking, test fixture side effects).
- exclude:
id: py/unused-local-variable
# Path injection (11): All paths validated — extension whitelists,
# traversal checks (rejects .. / \), UUID-based naming, or
# constructed from integer IDs in controlled base directories.
- exclude:
id: py/path-injection
# Stack trace exposure (5): str(e) replaced with generic messages
# in HTTP responses. Remaining findings are CodeQL tracing through
# _update_status dict returns, not actual new exposures.
- exclude:
id: py/stack-trace-exposure
# Socket bind to 0.0.0.0 (4): Virtual printer SSDP/discovery
# services must bind all interfaces for LAN discoverability.
- exclude:
id: py/bind-socket-all-network-interfaces
# SSRF (3+1): URLs come from admin-configured settings (external
# cameras, Home Assistant, Tasmota). Validation added for scheme,
# hostname, and metadata-service blocking. CodeQL still traces
# taint through the validated URLs.
- exclude:
id: py/partial-ssrf
- exclude:
id: py/full-ssrf
# Unused global variables (2): False positives — module-level
# cache variables written via `global` in one function, read in
# another. CodeQL doesn't track cross-function global reads.
- exclude:
id: py/unused-global-variable
# Clear-text logging sensitive data (2): False positive —
# `api_key` in firmware_check.py is a printer model identifier
# string ("x1", "p1", "a1-mini"), not a secret.
- exclude:
id: py/clear-text-logging-sensitive-data
# Clear-text storage sensitive data (1): JWT secret stored in
# SQLite config with 0600 file permissions. Standard approach
# for single-host deployment.
- exclude:
id: py/clear-text-storage-sensitive-data
# Weak hashing on sensitive data (1): MD5 in bambu_mqtt.py used
# with usedforsecurity=False for AMS tray fingerprinting, not
# for security purposes.
- exclude:
id: py/weak-sensitive-data-hashing
# Catch base exception (1): In frontend/node_modules third-party
# code (flatted/python/flatted.py), outside our control.
- exclude:
id: py/catch-base-exception
================================================
FILE: .dockerignore
================================================
# Git
# Exclude all .git contents EXCEPT HEAD. HEAD is a tiny text file (under
# 100 bytes) containing e.g. `ref: refs/heads/dev`, which the Dockerfile
# copies into the image so detect_current_branch() in spoolbuddy_ssh.py
# can report the correct branch for SpoolBuddy remote updates. Without
# this, the production image has no git metadata at all and always falls
# back to "main" regardless of which branch the operator built from.
.git/*
!.git/HEAD
.gitignore
# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
venv/
.venv/
ENV/
env/
.env
*.egg-info/
.eggs/
dist/
build/
# Node
frontend/node_modules/
frontend/.npm
# IDE
.idea/
.vscode/
*.swp
*.swo
# Testing
.pytest_cache/
.coverage
htmlcov/
# Logs and data (will be mounted as volumes)
logs/
data/
*.log
*.db
# Build artifacts
static/
# Documentation
docs/
*.md
!requirements.txt
# Docker
Dockerfile
docker-compose*.yml
.dockerignore
================================================
FILE: .gitattributes
================================================
# Force CRLF line endings for Windows batch files so cmd.exe can parse them
# regardless of the user's core.autocrlf setting.
*.bat text eol=crlf
================================================
FILE: .github/CODEOWNERS
================================================
# CODEOWNERS - Defines code owners who will be requested for review
# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Default owner for everything
* @maziggy
# Backend code
/backend/ @maziggy
# Frontend code
/frontend/ @maziggy
# Infrastructure and deployment
/Dockerfile* @maziggy
/docker-compose*.yml @maziggy
/.github/ @maziggy
# Documentation
/*.md @maziggy
/docs/ @maziggy
================================================
FILE: .github/FUNDING.yml
================================================
github: maziggy
ko_fi: maziggy
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: Report a bug or unexpected behavior
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug! Please fill out the form below.
- type: dropdown
id: component
attributes:
label: Component
description: Which part of the project is affected?
options:
- Bambuddy
- SpoolBuddy
- Both
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug Description
description: A clear and concise description of what the bug is.
placeholder: Describe what happened...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What did you expect to happen?
placeholder: Describe what you expected...
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to Reproduce
description: How can we reproduce this issue?
placeholder: |
1. Go to '...'
2. Click on '...'
3. See error
validations:
required: true
- type: dropdown
id: printer
attributes:
label: Printer Model
description: Which printer model are you using?
options:
- X1 Carbon
- X1
- X1E
- X2D
- P1S
- P1P
- P2S
- A1
- A1 Mini
- H2D
- H2D Pro
- H2C
- H2S
- Multiple printers
- Not printer-related
validations:
required: false
- type: input
id: version
attributes:
label: Bambuddy Version
description: Which version of Bambuddy are you running? (Check Settings page)
placeholder: e.g., 0.1.5
validations:
required: true
- type: input
id: spoolbuddy_version
attributes:
label: SpoolBuddy Version
description: If SpoolBuddy-related, which version is running? (Check SpoolBuddy Settings → Updates)
placeholder: e.g., 0.1.0
- type: input
id: firmware
attributes:
label: Printer Firmware Version
description: Which firmware version is your printer running? (Check printer screen or Bambu Handy app). Leave blank if not printer-related.
placeholder: e.g., 01.08.00.00
- type: dropdown
id: installation
attributes:
label: Installation Method
description: How did you install Bambuddy?
options:
- Manual (git clone)
- Docker
- Other
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating System
description: What OS is Bambuddy running on?
options:
- Linux (Ubuntu/Debian)
- Linux (Other)
- macOS
- Windows
- Docker
- Other
validations:
required: true
- type: markdown
attributes:
value: |
---
### 📦 Support Package
For faster debugging, please create and attach a **Support Package** from **Settings → System Info → Download Support Package**.
This includes logs, system info, and configuration (with sensitive data redacted).
For detailed instructions on enabling debug logging, see: [Debug Logging Guide](https://wiki.bambuddy.cool/features/system-info/?h=debug#enable-debug-logging)
- type: textarea
id: logs
attributes:
label: Relevant Logs / Support Package
description: |
Attach a support package (.zip) or paste relevant logs here. Enable DEBUG mode for verbose logging.
💡 Tip: You can drag and drop files directly into this text box.
placeholder: |
Drag and drop your support package .zip file here, or paste logs...
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: |
If applicable, add screenshots to help explain your problem.
💡 Tip: You can drag and drop images directly into this text box.
- type: textarea
id: additional
attributes:
label: Additional Context
description: Add any other context about the problem here.
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I have searched existing issues to ensure this bug hasn't already been reported
required: true
- label: I am using the latest version of Bambuddy
required: true
- label: My printer is set to LAN Only mode
required: true
- label: My printer has Developer Mode enabled
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Documentation
url: https://github.com/maziggy/bambuddy-wiki
about: Check the documentation for guides and troubleshooting
- name: Community Forum
url: https://forum.bambuddy.cool
about: Ask questions and share ideas with the community
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Suggest a new feature or enhancement
title: "[Feature]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting a feature! Please fill out the form below.
- type: textarea
id: problem
attributes:
label: Problem or Use Case
description: Is your feature request related to a problem? Please describe the use case.
placeholder: I'm always frustrated when...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: Describe the solution you'd like to see.
placeholder: It would be great if...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Have you considered any alternative solutions or workarounds?
placeholder: I've tried...
- type: dropdown
id: category
attributes:
label: Feature Category
description: What area does this feature relate to?
options:
- Print Archiving
- Monitoring & Stats
- Print Queue & Scheduling
- Smart Plugs
- Notifications
- Spool Inventory
- Spoolman Integration
- Cloud Profiles
- K-Profiles
- Maintenance Tracking
- File Manager
- SpoolBuddy - Dashboard
- SpoolBuddy - AMS Management
- SpoolBuddy - NFC / Tag Writing
- SpoolBuddy - Scale / Calibration
- SpoolBuddy - Inventory
- SpoolBuddy - Settings / Updates
- SpoolBuddy - Hardware / Installation
- UI/UX
- API
- Other
validations:
required: true
- type: dropdown
id: priority
attributes:
label: Priority
description: How important is this feature to you?
options:
- Nice to have
- Would improve my workflow
- Critical for my use case
validations:
required: true
- type: textarea
id: mockups
attributes:
label: Mockups or Examples
description: |
If you have any mockups, screenshots, or examples from other software, please share them.
💡 Tip: You can drag and drop images directly into this text box.
- type: checkboxes
id: contribution
attributes:
label: Contribution
options:
- label: I would be willing to help implement this feature
required: false
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I have searched existing issues to ensure this feature hasn't already been requested
required: true
================================================
FILE: .github/MAINTAINERS.md
================================================
# Maintainer Guide
This document provides setup instructions for repository maintainers.
## Branch Protection Setup
To protect the `main` branch, go to **Settings > Rules > Rulesets > New ruleset > New branch ruleset**.
### Step 1: Basic Settings
| Field | Value |
|-------|-------|
| Ruleset name | `Protect main` |
| Enforcement status | `Active` |
### Step 2: Bypass List (optional)
Add yourself (`@maziggy`) to bypass if you want to push directly in emergencies.
Set "Always" or "Pull requests only" based on preference.
### Step 3: Target Branches
Click **Add target** > **Include by pattern** and enter: `main`
### Step 4: Branch Rules
Enable these rules:
**Restrict deletions** - Prevents branch deletion
**Require a pull request before merging**
- Required approvals: `1`
- [x] Dismiss stale pull request approvals when new commits are pushed
- [ ] Require review from Code Owners (optional)
- [x] Require approval of the most recent reviewable push
**Require status checks to pass**
- [x] Require branches to be up to date before merging
- Add these status checks (they appear after CI runs once):
- `Backend Lint`
- `Backend Tests`
- `Frontend Lint`
- `Frontend Type Check`
- `Frontend Tests`
- `Frontend Build`
- `Docker Build`
**Block force pushes** - Prevents history rewriting
### Optional (stricter)
- [ ] Require conversation resolution before merging
- [ ] Require signed commits
- [ ] Require linear history
## CI Workflow
The CI workflow (`.github/workflows/ci.yml`) runs on:
- All pull requests to `main`
- All pushes to `main`
### Jobs
| Job | Purpose | Required for PR |
|-----|---------|-----------------|
| `backend-lint` | Ruff linting + format check | Yes |
| `backend-tests` | Unit tests | Yes |
| `frontend-lint` | ESLint | Yes |
| `frontend-typecheck` | TypeScript compilation | Yes |
| `frontend-tests` | Vitest unit tests | Yes |
| `frontend-build` | Vite production build | Yes |
| `docker-build` | Docker image builds | Yes |
### Fixing CI Failures
**Backend lint failures:**
```bash
ruff check --fix backend/
ruff format backend/
```
**Frontend lint failures:**
```bash
cd frontend
npm run lint -- --fix
```
**Frontend type errors:**
```bash
cd frontend
npx tsc --noEmit
# Fix the errors shown
```
**Frontend test failures:**
```bash
cd frontend
npm run test:run
# Fix failing tests
```
## CODEOWNERS
The `CODEOWNERS` file automatically requests reviews from `@maziggy` for all changes.
To add more code owners:
1. Edit `.github/CODEOWNERS`
2. Add GitHub usernames with `@` prefix
3. Assign specific paths to specific owners
Example:
```
/backend/ @maziggy @backend-contributor
/frontend/ @maziggy @frontend-contributor
```
## Release Process
1. Update version in `pyproject.toml`
2. Update `CHANGELOG.md`
3. Create a PR with these changes
4. After merge, tag the release:
```bash
git tag v0.1.x
git push origin v0.1.x
```
5. Run `docker-publish.sh` to publish Docker image
## Dependabot (Optional)
To enable automated dependency updates, create `.github/dependabot.yml`:
```yaml
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
groups:
python-dependencies:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
groups:
npm-dependencies:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
```
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Description
<!-- Provide a brief description of your changes -->
## Related Issue
<!-- Link to the issue this PR addresses (if applicable) -->
Fixes #
## Documentation
<!--
If this PR changes user-visible behavior, config keys, ports, CLI flags,
URLs, or installation steps, link matching PRs in the docs repos below.
Internal refactors, bug fixes with no observable change, and test-only
changes are exempt — just check the "not required" box and say why.
See CONTRIBUTING.md → Documentation Requirements for the full rules.
-->
**Companion docs PRs** (delete lines that don't apply):
- Wiki: maziggy/bambuddy-wiki#___
- Website: maziggy/bambuddy-website#___
**Pick one**:
- [ ] Docs PR(s) linked above
- [ ] No docs update required — reason: ___
## Type of Change
<!-- Mark the relevant option with an "x" -->
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update
- [ ] Code refactoring
- [ ] Performance improvement
- [ ] Test addition or update
## Changes Made
<!-- List the specific changes made in this PR -->
-
-
-
## Screenshots
<!-- If applicable, add screenshots to demonstrate your changes -->
## Testing
<!-- Describe how you tested your changes -->
- [ ] I have tested this on my local machine
- [ ] I have tested with my printer model: <!-- e.g., X1C, P1S, A1 -->
## Checklist
- [ ] My code follows the project's coding style
- [ ] I have commented my code where necessary
- [ ] My changes generate no new warnings
- [ ] I have tested my changes thoroughly
## Additional Notes
<!-- Add any additional information that reviewers should know -->
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
# Run on PRs targeting main, but skip for repo owner (runs local tests)
# Skip CI for PRs authored by repo owner (they run tests locally)
# Uses PR author instead of triggering actor so rebasing by owner doesn't skip CI
env:
PYTHON_VERSION: '3.11'
NODE_VERSION: '22'
# Cancel in-progress runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Minimum permissions for all jobs
permissions:
contents: read
jobs:
# ============================================================================
# Backend Checks
# ============================================================================
backend-lint:
name: Backend Lint
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install ruff
run: pip install ruff
- name: Run ruff check
run: ruff check backend/
- name: Run ruff format check
run: ruff format --check backend/
backend-security:
name: Backend Security
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pip-audit
- name: Run pip-audit
run: |
# CVE-2026-4539: low-severity ReDoS in Pygments AdlLexer (indirect dep via mkdocs-material/pytest/rich).
# No fix available yet. Remove --ignore-vuln once Pygments releases a patched version.
pip-audit --desc on --ignore-vuln CVE-2026-4539
backend-tests:
name: Backend Tests
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner
needs: backend-lint
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run tests
timeout-minutes: 10
run: |
cd backend
python -m pytest tests/ -v --tb=short --timeout=60 --timeout-method=thread -n auto
# ============================================================================
# Frontend Checks
# ============================================================================
frontend-lint:
name: Frontend Lint
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run ESLint
working-directory: frontend
run: npm run lint
frontend-security:
name: Frontend Security
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run npm audit
working-directory: frontend
run: |
# Only audit production dependencies and filter out npm-internal packages.
# npm 10.x audit/ls reports vulns in its own bundled deps (npm, tar, minimatch)
# so we parse package-lock.json directly to get the real prod dep list.
npm audit --omit=dev --json > /tmp/audit.json 2>/dev/null || true
python3 -c "
import json, sys
data = json.load(open('/tmp/audit.json'))
lock = json.load(open('package-lock.json'))
prod = set()
for path, info in lock.get('packages', {}).items():
if path and not info.get('dev') and not info.get('devOptional'):
prod.add(path.split('node_modules/')[-1])
vulns = data.get('vulnerabilities', {})
fixable = {n: v for n, v in vulns.items()
if n in prod and v.get('severity') in ('high', 'critical') and v.get('fixAvailable')}
skipped = len(vulns) - len({n: v for n, v in vulns.items() if n in prod})
if fixable:
for name, v in fixable.items():
print(f'FIXABLE {v[\"severity\"].upper()}: {name}')
sys.exit(1)
total = sum(1 for n, v in vulns.items() if n in prod and v.get('severity') in ('high', 'critical'))
print(f'npm audit: {total} high/critical (0 fixable), {len(vulns)} total ({skipped} npm-internal filtered)')
"
frontend-typecheck:
name: Frontend Type Check
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run TypeScript check
working-directory: frontend
run: npx tsc --noEmit
frontend-tests:
name: Frontend Tests
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner
needs: [frontend-lint, frontend-typecheck]
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run tests
timeout-minutes: 10
working-directory: frontend
run: npm run test:run
frontend-build:
name: Frontend Build
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner
needs: [frontend-tests]
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Build
working-directory: frontend
run: npm run build
# ============================================================================
# Docker Tests (matches test_docker.sh)
# ============================================================================
docker-test:
name: Docker Build
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner
timeout-minutes: 20
needs: [backend-tests, frontend-build]
steps:
- uses: actions/checkout@v4
# Test 1: Docker Build
- name: Build production image
run: docker build -t bambuddy:test .
- name: Verify backend imports
run: docker run --rm bambuddy:test python -c "import backend.app.main; print('Backend imports OK')"
- name: Verify static files exist
run: docker run --rm bambuddy:test test -d /app/static
# Test 2: Backend Unit Tests in Docker
- name: Build backend test image
run: docker compose -f docker-compose.test.yml build backend-test
- name: Run backend tests in Docker
run: docker compose -f docker-compose.test.yml run --rm backend-test
# Test 3: Frontend Unit Tests in Docker
- name: Build frontend test image
run: docker compose -f docker-compose.test.yml build frontend-test
- name: Run frontend tests in Docker
run: docker compose -f docker-compose.test.yml run --rm frontend-test
# Test 4: Integration Tests
- name: Build integration container
run: docker compose -f docker-compose.test.yml build integration
- name: Start integration container
run: |
docker compose -f docker-compose.test.yml up -d integration
echo "Waiting for container to be healthy..."
for i in {1..30}; do
if docker compose -f docker-compose.test.yml ps integration | grep -q "healthy"; then
echo "Container is healthy"
break
fi
sleep 2
done
- name: Test health endpoint
run: |
HEALTH=$(docker compose -f docker-compose.test.yml exec -T integration curl -s http://localhost:8000/health)
echo "$HEALTH"
echo "$HEALTH" | grep -q "healthy"
- name: Test API endpoint
run: |
docker compose -f docker-compose.test.yml exec -T integration curl -s http://localhost:8000/api/v1/settings
- name: Test static files served
run: |
STATUS=$(docker compose -f docker-compose.test.yml exec -T integration curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/)
echo "Static files HTTP status: $STATUS"
[ "$STATUS" = "200" ]
# Test 5: Integration Test Suite (pytest)
- name: Build integration test runner
run: docker compose -f docker-compose.test.yml build integration-test-runner
- name: Run integration test suite
run: docker compose -f docker-compose.test.yml run --rm integration-test-runner
- name: Show logs on failure
if: failure()
run: docker compose -f docker-compose.test.yml logs
- name: Cleanup
if: always()
run: docker compose -f docker-compose.test.yml down -v --remove-orphans
================================================
FILE: .github/workflows/cleanup-ghcr.yml
================================================
name: Cleanup GHCR untagged images
# Deletes untagged (orphan) container versions from GHCR while preserving
# any digest that is still referenced by a tagged multi-arch manifest list.
# Without that cross-reference, off-the-shelf cleanup actions can silently
# break multi-arch tags by deleting referenced platform manifests.
#
# Requires a repo secret `GHCR_CLEANUP_TOKEN` — a classic PAT with
# `read:packages` + `delete:packages` scope. GITHUB_TOKEN cannot delete
# versions of user-owned packages (only org-owned).
on:
schedule:
- cron: '0 3 * * 0' # Sundays at 03:00 UTC
workflow_dispatch:
inputs:
dry_run:
description: 'List orphans without deleting'
type: boolean
default: false
# This workflow authenticates exclusively via GHCR_CLEANUP_TOKEN (a classic PAT)
# and never reads/writes via the default GITHUB_TOKEN. Strip every permission
# from the GITHUB_TOKEN so a stolen workflow run can't reach the repo at all
# — least privilege per CodeQL `actions/missing-workflow-permissions`.
permissions: {}
jobs:
cleanup:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package: [bambuddy, bambuddy-beta]
steps:
- name: Cleanup ${{ matrix.package }}
env:
GH_TOKEN: ${{ secrets.GHCR_CLEANUP_TOKEN }}
OWNER: ${{ github.repository_owner }}
PACKAGE: ${{ matrix.package }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
# 1. Fetch all versions for the package.
gh api "users/${OWNER}/packages/container/${PACKAGE}/versions?per_page=100" \
--paginate --slurp > all.json
total=$(jq 'add | length' all.json)
echo "Total versions: $total"
# 2. Build the live-digest set: digests referenced by any tagged
# multi-arch manifest list. Use the registry token (separate from
# GH_TOKEN) for ghcr.io manifest reads.
REG_TOKEN=$(curl -sS \
"https://ghcr.io/token?scope=repository:${OWNER}/${PACKAGE}:pull&service=ghcr.io" \
| jq -r .token)
jq -r 'add | map(select(.metadata.container.tags | length > 0))
| .[] | .metadata.container.tags[0]' all.json > tags.txt
: > live.txt
while IFS= read -r tag; do
curl -sS \
-H "Authorization: Bearer $REG_TOKEN" \
-H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json" \
"https://ghcr.io/v2/${OWNER}/${PACKAGE}/manifests/${tag}" \
| jq -r '(.manifests // []) | .[].digest' >> live.txt 2>/dev/null || true
done < tags.txt
sort -u live.txt -o live.txt
echo "Live digests referenced by manifest lists: $(wc -l < live.txt)"
# 3. Untagged digests minus live = true orphans.
jq -r 'add | map(select(.metadata.container.tags | length == 0))
| .[] | .name' all.json | sort -u > untagged.txt
comm -23 untagged.txt live.txt > orphan_digests.txt
orphan_count=$(wc -l < orphan_digests.txt)
echo "Orphan digests safe to delete: $orphan_count"
if [ "$orphan_count" -eq 0 ]; then
echo "Nothing to delete."
exit 0
fi
# 4. Map orphan digests -> version IDs.
jq -r --slurpfile orphans <(jq -R . orphan_digests.txt) '
add
| map(select(.name as $n | ($orphans | flatten | index($n))))
| .[] | .id
' all.json > delete_ids.txt
echo "Version IDs queued for deletion: $(wc -l < delete_ids.txt)"
if [ "${DRY_RUN:-false}" = "true" ]; then
echo "Dry run — not deleting."
head -20 delete_ids.txt
exit 0
fi
# 5. Delete.
deleted=0
failed=0
while IFS= read -r id; do
if gh api -X DELETE "users/${OWNER}/packages/container/${PACKAGE}/versions/${id}" --silent; then
deleted=$((deleted + 1))
else
failed=$((failed + 1))
fi
done < delete_ids.txt
echo "Deleted: $deleted Failed: $failed"
[ "$failed" -eq 0 ]
================================================
FILE: .github/workflows/codeql.yml
================================================
name: "CodeQL"
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1'
permissions:
contents: read
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
security-events: write
strategy:
fail-fast: false
matrix:
language: [python, javascript-typescript, actions]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
config-file: ./.codeql/codeql-config.yml
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
================================================
FILE: .github/workflows/issue-closed.yml
================================================
name: Clean up closed issues
on:
issues:
types: [closed]
permissions:
issues: write
jobs:
remove-labels:
runs-on: ubuntu-latest
steps:
- name: Remove feedback label
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const hasLabel = issue.labels.some(l => l.name === 'feedback');
if (hasLabel) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: 'feedback'
});
console.log(`Removed 'feedback' label from issue #${issue.number}`);
} catch (error) {
if (error.status === 404) {
console.log(`Label 'feedback' already removed from issue #${issue.number}`);
} else {
throw error;
}
}
}
================================================
FILE: .github/workflows/repo-stats.yml
================================================
name: GitHub Repo Stats
on:
schedule:
- cron: "0 23 * * *"
workflow_dispatch:
permissions:
contents: write
jobs:
repo-stats:
name: repo-stats
runs-on: ubuntu-latest
steps:
- name: run-ghrs
uses: jgehrcke/github-repo-stats@v1.4.2
with:
repository: maziggy/bambuddy
ghtoken: ${{ secrets.GHRS_GITHUB_API_TOKEN }}
ghpagesprefix: https://maziggy.github.io/bambuddy
================================================
FILE: .github/workflows/security.yml
================================================
name: Security Audit
on:
schedule:
# Run weekly on Monday at 6:00 UTC
- cron: '0 6 * * 1'
push:
paths:
- 'backend/**'
- 'frontend/**'
- 'spoolbuddy/**'
- 'Dockerfile'
- 'docker-compose*.yml'
- 'requirements.txt'
- 'frontend/package*.json'
- '.github/workflows/security.yml'
pull_request:
paths:
- 'backend/**'
- 'frontend/**'
- 'spoolbuddy/**'
- 'Dockerfile'
- 'docker-compose*.yml'
- 'requirements.txt'
- 'frontend/package*.json'
- '.github/workflows/security.yml'
workflow_dispatch:
# Allow manual trigger
env:
PYTHON_VERSION: '3.11'
NODE_VERSION: '22'
# Default permissions for all jobs
permissions:
contents: read
jobs:
bandit:
name: Python Security Analysis (Bandit)
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install Bandit
run: pip install bandit[sarif]
- name: Run Bandit
run: |
bandit -r backend/ -f sarif -o bandit-results.sarif --severity-level medium || true
- name: Upload Bandit results to GitHub Security
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: bandit-results.sarif
category: bandit
trivy:
name: Container Security Scan (Trivy)
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t bambuddy:security-scan .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@v0.35.0
with:
image-ref: 'bambuddy:security-scan'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM'
version: 'v0.69.1'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-results.sarif') != ''
with:
sarif_file: trivy-results.sarif
category: trivy
- name: Run Trivy for Dockerfile/IaC
uses: aquasecurity/trivy-action@v0.35.0
with:
scan-type: 'config'
scan-ref: '.'
format: 'sarif'
output: 'trivy-config-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM'
version: 'v0.69.1'
- name: Upload Trivy config results
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-config-results.sarif') != ''
with:
sarif_file: trivy-config-results.sarif
category: trivy-config
backend-audit:
name: Backend Security Audit
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pip-audit
- name: Run pip-audit
id: pip-audit
run: |
# CVE-2026-4539: low-severity ReDoS in Pygments AdlLexer (indirect dep via mkdocs-material/pytest/rich).
# No fix available yet. Remove --ignore-vuln once Pygments releases a patched version.
pip-audit --desc on --format json --output pip-audit-results.json --ignore-vuln CVE-2026-4539 || echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT
pip-audit --desc on --ignore-vuln CVE-2026-4539 || true
- name: Upload audit results
if: always()
uses: actions/upload-artifact@v4
with:
name: pip-audit-results
path: pip-audit-results.json
retention-days: 30
- name: Create or close pip security issue
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Check for existing open issue
const existingIssues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'security,automated'
});
const existingIssue = existingIssues.data.find(i => i.title.startsWith('Security Alert:') && i.title.includes('Python'));
// If no vulnerabilities found, auto-close any stale issue
if ('${{ steps.pip-audit.outputs.vulnerabilities_found }}' !== 'true') {
if (existingIssue) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: 'All Python vulnerabilities have been resolved. Closing automatically.'
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
state: 'closed'
});
console.log(`Auto-closed resolved issue #${existingIssue.number}`);
}
return;
}
let results;
try {
results = JSON.parse(fs.readFileSync('pip-audit-results.json', 'utf8'));
} catch {
console.log('Could not read audit results');
return;
}
// Build vulnerability table
let table = '| Package | Version | Vulnerability | Fix Version |\n';
table += '|---------|---------|---------------|-------------|\n';
for (const vuln of results.dependencies || []) {
for (const v of vuln.vulns || []) {
table += `| ${vuln.name} | ${vuln.version} | ${v.id} | ${v.fix_versions?.join(', ') || 'N/A'} |\n`;
}
}
const vulnCount = results.dependencies?.reduce((acc, d) => acc + (d.vulns?.length || 0), 0) || 0;
if (vulnCount === 0) {
console.log('No vulnerabilities to report');
if (existingIssue) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: 'All Python vulnerabilities have been resolved. Closing automatically.'
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
state: 'closed'
});
console.log(`Auto-closed resolved issue #${existingIssue.number}`);
}
return;
}
const title = `Security Alert: ${vulnCount} Python vulnerabilities found`;
const body = `## Automated Security Audit Results
The weekly security audit found vulnerabilities in Python dependencies.
${table}
### Recommended Actions
1. Review each vulnerability
2. Update affected packages: \`pip install --upgrade <package>\`
3. Run \`pip-audit\` locally to verify fixes
---
*This issue was automatically created by the security audit workflow.*`;
if (existingIssue) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: body
});
console.log(`Updated existing issue #${existingIssue.number}`);
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['security', 'automated', 'dependencies']
});
console.log('Created new security issue');
}
frontend-audit:
name: Frontend Security Audit
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run npm audit
id: npm-audit
working-directory: frontend
run: |
npm audit --omit=dev --json > npm-audit-raw.json 2>/dev/null || true
# Filter audit results to only include actual project dependencies.
# npm 10.x audit/ls reports vulns in its own bundled deps (npm, tar, minimatch)
# so we parse package-lock.json directly to get the real prod dep list.
node -e "
const fs = require('fs');
const raw = fs.readFileSync('npm-audit-raw.json', 'utf8');
let results;
try { results = JSON.parse(raw); } catch { results = { vulnerabilities: {} }; }
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
const prodDeps = new Set();
for (const [path, info] of Object.entries(lock.packages || {})) {
if (path && !info.dev && !info.devOptional) {
prodDeps.add(path.split('node_modules/').pop());
}
}
const vulns = results.vulnerabilities || {};
const filtered = {};
for (const [name, info] of Object.entries(vulns)) {
if (prodDeps.has(name)) filtered[name] = info;
}
results.vulnerabilities = filtered;
fs.writeFileSync('npm-audit-results.json', JSON.stringify(results, null, 2));
const count = Object.keys(filtered).length;
console.log(count > 0
? count + ' production vulnerabilities found'
: 'No production vulnerabilities (filtered ' + Object.keys(vulns).length + ' npm-internal entries)');
if (count > 0) process.exit(1);
" || echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT
npm audit --omit=dev --audit-level=high || true
- name: Upload audit results
if: always()
uses: actions/upload-artifact@v4
with:
name: npm-audit-results
path: frontend/npm-audit-results.json
retention-days: 30
- name: Create or close npm security issue
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Check for existing open issue
const existingIssues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'security,automated'
});
const existingIssue = existingIssues.data.find(i => i.title.startsWith('Security Alert:') && i.title.includes('npm'));
// If filter didn't flag vulnerabilities, auto-close any stale issue
if ('${{ steps.npm-audit.outputs.vulnerabilities_found }}' !== 'true') {
if (existingIssue) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: 'All npm production vulnerabilities have been resolved. Closing automatically.'
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
state: 'closed'
});
console.log(`Auto-closed resolved issue #${existingIssue.number}`);
}
return;
}
let results;
try {
results = JSON.parse(fs.readFileSync('frontend/npm-audit-results.json', 'utf8'));
} catch {
console.log('Could not read filtered audit results');
return;
}
const vulns = results.vulnerabilities || {};
const vulnCount = Object.keys(vulns).length;
if (vulnCount === 0) {
console.log('No vulnerabilities to report');
if (existingIssue) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: 'All npm production vulnerabilities have been resolved. Closing automatically.'
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
state: 'closed'
});
console.log(`Auto-closed resolved issue #${existingIssue.number}`);
}
return;
}
// Build vulnerability table
let table = '| Package | Severity | Via | Fix |\n';
table += '|---------|----------|-----|-----|\n';
for (const [name, info] of Object.entries(vulns)) {
const via = Array.isArray(info.via) ? info.via.map(v => typeof v === 'string' ? v : v.name).join(', ') : info.via;
table += `| ${name} | ${info.severity} | ${via} | ${info.fixAvailable ? 'Yes' : 'No'} |\n`;
}
const title = `Security Alert: ${vulnCount} npm vulnerabilities found`;
const body = `## Automated Security Audit Results
The weekly security audit found vulnerabilities in npm dependencies.
${table}
### Recommended Actions
1. Review each vulnerability: \`npm audit\`
2. Auto-fix if possible: \`npm audit fix\`
3. Manual fix for breaking changes: \`npm audit fix --force\` (review changes!)
---
*This issue was automatically created by the security audit workflow.*`;
if (existingIssue) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: body
});
console.log(`Updated existing issue #${existingIssue.number}`);
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['security', 'automated', 'dependencies']
});
console.log('Created new security issue');
}
================================================
FILE: .github/workflows/stale.yml
================================================
name: Close stale issues
on:
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
permissions:
issues: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-message: 'This issue has been marked as stale due to inactivity. It will be closed in 7 days if there is no further activity.'
close-issue-message: 'Closed due to inactivity. Feel free to reopen if this is still relevant.'
days-before-stale: 21
days-before-close: 7
stale-issue-label: 'stale'
only-labels: 'feedback'
================================================
FILE: .github/workflows.disabled/ci.yml
================================================
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
env:
PYTHON_VERSION: '3.11'
NODE_VERSION: '20'
jobs:
# ============================================================================
# Backend Jobs
# ============================================================================
backend-lint:
name: Backend Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install ruff
run: pip install ruff
- name: Run ruff check
run: ruff check backend/
backend-unit-tests:
name: Backend Unit Tests
runs-on: ubuntu-latest
needs: backend-lint
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-cov
- name: Run unit tests
run: |
cd backend
python -m pytest tests/unit/ -v --cov=app --cov-report=xml -m "not slow"
- name: Upload coverage
uses: codecov/codecov-action@v4
if: always()
with:
files: backend/coverage.xml
flags: backend-unit
fail_ci_if_error: false
backend-integration-tests:
name: Backend Integration Tests
runs-on: ubuntu-latest
needs: backend-unit-tests
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-cov
- name: Run integration tests
run: |
cd backend
python -m pytest tests/integration/ -v --cov=app --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
if: always()
with:
files: backend/coverage.xml
flags: backend-integration
fail_ci_if_error: false
# ============================================================================
# Frontend Jobs
# ============================================================================
frontend-lint:
name: Frontend Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run ESLint
working-directory: frontend
run: npm run lint
frontend-type-check:
name: Frontend Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run TypeScript check
working-directory: frontend
run: npx tsc --noEmit
frontend-unit-tests:
name: Frontend Unit Tests
runs-on: ubuntu-latest
needs: [frontend-lint, frontend-type-check]
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run tests with coverage
working-directory: frontend
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
if: always()
with:
files: frontend/coverage/coverage-final.json
flags: frontend-unit
fail_ci_if_error: false
frontend-build:
name: Frontend Build
runs-on: ubuntu-latest
needs: frontend-unit-tests
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Build
working-directory: frontend
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: static/
# ============================================================================
# E2E Tests
# ============================================================================
e2e-tests:
name: E2E Tests
runs-on: ubuntu-latest
needs: [backend-integration-tests, frontend-build]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install backend dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install playwright
playwright install chromium --with-deps
- name: Download frontend build
uses: actions/download-artifact@v4
with:
name: frontend-build
path: static/
- name: Start backend server
run: |
python -m uvicorn backend.app.main:app --host 0.0.0.0 --port 8000 &
sleep 15
env:
DEBUG: 'false'
- name: Run E2E tests
run: |
python tests/e2e_comprehensive_test.py || true
python tests/e2e_toggle_persistence_test.py
- name: Upload test screenshots
uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-screenshots
path: /tmp/bambuddy_*.png
if-no-files-found: ignore
# ============================================================================
# Summary Job
# ============================================================================
ci-summary:
name: CI Summary
runs-on: ubuntu-latest
needs: [backend-integration-tests, frontend-build, e2e-tests]
if: always()
steps:
- name: Check results
run: |
echo "Backend Integration Tests: ${{ needs.backend-integration-tests.result }}"
echo "Frontend Build: ${{ needs.frontend-build.result }}"
echo "E2E Tests: ${{ needs.e2e-tests.result }}"
if [[ "${{ needs.backend-integration-tests.result }}" == "failure" ]] || \
[[ "${{ needs.frontend-build.result }}" == "failure" ]]; then
echo "CI failed!"
exit 1
fi
echo "CI passed!"
================================================
FILE: .gitignore
================================================
# Claude
.claude/
CLAUDE.md
# macOS
.DS_Store
**/.DS_Store
**/._.DS_Store
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
venv/
.venv/
env/
.env
docker-compose.override.yml
*.egg-info/
dist/
build/
# Node
frontend/node_modules/
frontend/coverage/
npm-debug.log*
# Database
*.db
*.db-journal
*.db-wal
*.db-shm
# Archive files (user data)
archive/
# Firmware cache (downloaded firmware files)
firmware/
# Virtual printer (auto-generated certs and uploads at repo root)
/virtual_printer/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Screenshots (development - root folder only)
/screenshots/
# Logs
*.log
logs/
*.log*
bambutrack.log.*
firmware/
# Node modules
node_modules/
data/
# JWT secret file (should be in data dir, but protect project root too)
.jwt_secret
# SpoolBuddy SSH keys (generated at runtime for remote updates)
spoolbuddy/ssh/
# Security scan output
*.sarif
debug_logs/
db_backup/
support-packages/
backups/
bin/
advertisements/
================================================
FILE: .pre-commit-config.yaml
================================================
# Pre-commit hooks for BamBuddy
# Install with: pip install pre-commit && pre-commit install
repos:
# Ruff - Fast Python linter and formatter
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.11
hooks:
# Linter
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
types_or: [python, pyi]
# Formatter
- id: ruff-format
types_or: [python, pyi]
# Standard pre-commit hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: ^static/
- id: end-of-file-fixer
exclude: ^static/
- id: check-yaml
- id: check-json
exclude: ^(static/|frontend/tsconfig\.)
- id: check-added-large-files
args: ['--maxkb=1000']
exclude: ^static/assets/
- id: check-merge-conflict
- id: debug-statements
- id: detect-private-key
# Check for import shadowing (custom)
- repo: local
hooks:
- id: check-import-shadowing
name: Check for dangerous import shadowing
entry: python -m pytest backend/tests/unit/test_code_quality.py::TestImportShadowing -v --tb=short
language: system
pass_filenames: false
types: [python]
files: ^backend/app/
- id: frontend-typecheck
name: TypeScript type check
entry: bash -c 'cd frontend && npx tsc --noEmit'
language: system
pass_filenames: false
files: ^frontend/src/
types_or: [ts, tsx]
================================================
FILE: .trivyignore
================================================
# Dockerfile USER directive (DS-0002): Bambuddy runs as a single-host
# Docker container where root is needed for device access and FFmpeg.
DS-0002
# util-linux hostname canonicalization (LOW, no fix available in Debian bookworm).
# Affects mount, login, libuuid1, libsmartcols1, etc. — not exploitable in container context.
CVE-2026-3184
# libtiff denial-of-service bugs (pulled in by ffmpeg, not directly used).
# No fix available in Debian bookworm.
CVE-2025-61143
CVE-2025-61144
CVE-2025-61145
# iptables --syn flag bypass (LOW, no fix available, not relevant — container doesn't use iptables).
CVE-2012-2663
# ffmpeg DVD subtitle parser heap OOB write (MEDIUM). Debian Security Tracker
# marks it "postponed" for both bookworm and trixie; no upstream fix yet.
# Not reachable in Bambuddy — ffmpeg here only ingests printer-camera RTSP
# and MJPEG/H.264/H.265 streams, never DVD/VOB files with subtitle tracks.
CVE-2026-6385
# ffmpeg AV1 decoder OOB read → DoS (MEDIUM, "minor issue" per Debian).
# Same "postponed" status in bookworm and trixie; no upstream fix yet.
# Not reachable — Bambu printer cameras emit H.264/H.265/MJPEG, not AV1.
CVE-2026-30997
# openjpeg JPEG 2000 integer overflow (LOW). No Debian fix available.
# libopenjp2-7 is pulled in transitively by ffmpeg but Bambuddy never
# decodes JPEG 2000 files (printer thumbnails are PNG, camera is MJPEG/H.264).
CVE-2026-6192
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to Bambuddy will be documented in this file.
## [0.2.3.2] - 2020-04-22
### Improved
- **GCode Viewer Reshaped as an Archive Preview Tool** ([#963](https://github.com/maziggy/bambuddy/pull/963) follow-up) — PR #963 landed the embedded PrettyGCode viewer with a library file picker, a connected-printer selector with live WebSocket status, and auto-load of the currently-printing file. In practice those three didn't match Bambuddy's data model: the library file picker only listed `.gcode` files (Bambuddy stores `.gcode.3mf`), the printer selector wasn't useful when the real goal is previewing an existing archive, and the auto-load path had the same `.gcode`-filter gap as the picker. The viewer is now scoped to a single focused workflow — "show me the G-code for this archive" — reached from the Archives page 3D-preview button (menu item + the card-corner badge + list-row menu, all three paths navigate the same way). Entry URL is `/gcode-viewer?archive=<id>[&plate=<N>]`; the route falls through to the SPA catch-all so a full-page reload keeps the Bambuddy layout shell, with the iframe at `/gcode-viewer/?archive=<id>…` serving the raw viewer. Bed size is fetched from `GET /archives/{id}/capabilities.build_volume` (already parsing `printable_area` + `printable_height` from the 3MF's `Metadata/project_settings.config`) so any printer model renders the correct bed — 350×320×325 for H2D etc. — with no hardcoded per-model map to maintain. Multi-plate archives now surface a dedicated plate picker modal (`components/PlatePickerModal.tsx`) with thumbnails and object lists matching the existing Re-print modal's visual language; source-only 3MFs (no sliced gcode) show a `archives.platePicker.noGcode` toast instead of sending the user to an empty viewer. Behind the scenes: `GET /archives/{id}/gcode` accepts `?plate=N` and resolves the filename by integer-matching the suffix (zero-padded names like `Metadata/plate_01.gcode` now resolve as plate 1, fixing a class of picker-claimed-but-404 archives); `GET /archives/{id}/plates` gained a top-level `has_gcode: bool` flag so the frontend can suppress the picker when the archive is source-only; `printer_state_to_dict` now injects `name` and `model` into every WebSocket snapshot so consumers don't race a separate `/printers` fetch for proper labels. Removed from the viewer: printer selector + WS subscription, library file picker, `BAMBU_BED_SIZES` hardcoded map, auto-load-currently-printing, sidebar nav entry, 32 orphaned `gcodeViewer` locale keys, and the unreachable `ModelViewerModal` render paths on archive cards (the File Manager still uses `ModelViewerModal` for library file previews — scope preserved). Added test coverage: `?plate=N` happy path, zero-padded filename resolution, missing-plate 404, no-plate fallback to first, `?plate=0` 400 rejection, `has_gcode=true/false` branch, plus `PlatePickerModal.test.tsx` (6 tests covering render, plate-name label, onSelect payload, backdrop close, thumbnail fallback) and `printer_state_to_dict` name/model surfacing tests. A toast replaces the old silent empty viewer for source-only archives; reload stays in the Bambuddy layout; H2D previews no longer overflow the bed.
>>>>>>> d4533c38 ( chore(deps): bump postcss to 8.5.12 to clear GHSA-qx2v-qp2m-jg93)
### Improved
- **Printer Card Shows Plate Name on Multi-Plate Prints** ([#881](https://github.com/maziggy/bambuddy/issues/881)) — When two printers were running different plates of the same multi-plate 3MF, the Printers page cards displayed the same file name on both and gave no visual way to tell them apart. The Queue view already showed the plate name by querying the archive's plate list; the Printers page didn't have that linkage. The `GET /printers/{id}/status` endpoint now returns `current_archive_id` (resolved by matching the MQTT `subtask_id` against `PrintArchive.subtask_id`, the same bridge introduced in #972 for restart-resume) and `current_plate_id` (parsed from the MQTT `gcode_file` path by a new shared `parse_plate_id` helper that's also used by the WebSocket push path, so plate transitions within a running print reflect immediately instead of waiting 30 s for the next REST poll). The card fetches plate metadata via the same `api.getArchivePlates()` call the Queue page uses — shared React Query cache keeps it cheap across polls — and renders the actual plate name (or a "Plate N" fallback) only when the source 3MF is multi-plate, so single-plate prints stay noise-free. Falls back to the previous `plate_(\d+).gcode` regex when there's no archive linkage (e.g. prints started directly from the printer LCD). Regression tests cover the plate-id extraction across Bambu Studio path shapes and the label-override precedence in `formatPrintName`. Thanks to @stringham for the follow-up and screenshot.
- **Printer Card: Remove Redundant In-Widget "Clear Plate & Start Next" Button** — In expanded view, the "Next in queue" widget rendered its own `Clear Plate & Start Next` button inside a yellow-bordered card (`PrinterQueueWidget.tsx`) whenever the plate-clear gate was up and an auto-dispatch item was queued — on top of the card-level "Mark plate as cleared" button introduced by #939. Both POSTed to the exact same `/printers/{id}/clear-plate` endpoint with identical optimistic-update semantics, so in that one state combination users saw two visually distinct affordances doing the same thing. Removed the widget's button and its entire `needsClearPlate` render branch; the card-level button (which is unconditional when plate-clear is required, and therefore already handles the staged-only and empty-queue cases that the widget couldn't) is now the single entry point. The widget becomes a pure passive "Next in queue" preview linking to `/queue`. No backend change, no change to the plate-status pill placement inside the Status box (deliberately kept where it is), and no change to compact-view (Size S) behaviour — the `plateStatusPill` at `PrintersPage.tsx:2664/2671` and the icon-only round clear-plate button at `:2673` are untouched. Also dropped the now-dead `awaitingPlateClear` / `requirePlateClear` / `printerState` props from `PrinterQueueWidgetProps` and the matching call site at `PrintersPage.tsx:2810`, and the orphaned `queue.clearPlate` / `queue.plateReady` translations from all eight locale files (`queue.clearPlateSuccess` is retained — still used by the card-level button's success toast). The dedicated `PrinterQueueWidgetClearPlate.test.tsx` suite (654 lines) was removed since every test asserted the behaviour of the now-gone button; `PrinterQueueWidget.test.tsx` continues to cover the passive-link path. Thanks to @EdwardChamberlain for flagging the duplication in #1079.
### Fixed
- **Print Scheduler Reprints the Just-Finished Job When Queue Has One Item Left (H2D)** ([#1078](https://github.com/maziggy/bambuddy/issues/1078)) — On H2D, clearing the plate and starting the next (and only) queued item caused the printer to re-run the job it had just finished while the UI reported the queued one as started. With multiple items left the symptom was hidden by forward progress. Root cause: `_watchdog_print_start` in `print_scheduler.py` gives up at 45 s and reverts the queue item to `pending` if `gcode_state` hasn't flipped away from `pre_state`, on the assumption that a non-transitioning printer means the MQTT `project_file` publish was swallowed by a half-broken session (#887/#967). H2D Pro firmware (01.01.00.00) routinely keeps `gcode_state=FINISH` for 48–55 s after actually accepting the command before transitioning to `PREPARE` — logs from the reporter show the revert firing at +45 s and a legitimate `PRINT START detected` arriving just ~3 s later — so the watchdog reverted an item that the printer *had* already started physically printing. The physical print ran to completion and updated the linked archive (via `register_expected_print`), but the queue item was now `pending` again; on the next scheduler tick after the user cleared the plate, the same item was re-dispatched as if it had never run. With multiple items queued, item N+1 getting dispatched during the 45 s race window looked like forward progress to the user and masked the duplicate revert/re-dispatch of item N. Fixed in `_watchdog_print_start` by adding a second "command landed" signal: `subtask_id` changing past the pre-dispatch value. Bambuddy already mints a unique `submission_id` per `project_file` publish (capped at int32 post-#1042) and assigns it to `subtask_id` / `task_id` in the command payload; the printer echoes this back on the next `push_status` as soon as it starts processing — well before `gcode_state` transitions on slow-transition models. `_start_print` now captures `pre_subtask_id` alongside `pre_state` and passes both to the watchdog, which treats *either* a state change *or* a `subtask_id` advance as proof the command landed. Timeout raised 45 s → 90 s as belt-and-braces for printers that neither transition state nor echo `subtask_id` inside the polling window. None of the earlier exit paths are weakened — genuine half-broken sessions (state *and* `subtask_id` both unchanged across the full window) still revert, still force the MQTT reconnect, and are still recoverable without a power cycle. Added eight regression tests in `test_scheduler_watchdog.py` covering: pickup via state change, pickup via `subtask_id` change while state stays at `FINISH` (the exact #1078 case), revert when neither signal changes, default timeout of 90 s, `pre_subtask_id=None` fallback to state-only, `status.subtask_id=None` not mis-detected as a change, printer disconnect mid-watchdog (no DB write), and the `#967` race where the item already moved on (`completed`). No frontend or MQTT changes — purely tightens the "did the printer accept?" decision. Thanks to @VREmma for the clear reproduction and the full support bundle that made pinpointing the H2D state-lag behaviour possible.
- **Printers-Page "Clear Plate" Button Takes 30–300+ s to Appear After Print Completes** ([#939](https://github.com/maziggy/bambuddy/pull/939) follow-up) — A trusted user reported that on every printer (A1, H2D, X1C), the "Clear Plate & Start Next" button didn't show for 60+ seconds after a print finished; refreshing didn't help; one H2D sat in the "Finished" state for 5 minutes without the button ever appearing. Root cause: PR #939 added the `awaiting_plate_clear` gate but stored it on `PrinterManager._awaiting_plate_clear` (a per-process set, persisted to `printers.awaiting_plate_clear` via #961), not on `PrinterState` — and `printer_state_to_dict()` in `printer_manager.py`, which builds every WebSocket `printer_status` payload, was never updated to emit it. Only the HTTP endpoint `GET /printers/{id}/status` (line 634) surfaced the flag. That left the frontend in a deadlock: when `print_complete` arrived over the WebSocket, `useWebSocket.ts` intentionally *didn't* invalidate `['printerStatus']` (avoiding the render-cascade freeze the comment at line 235 warns about), expecting the subsequent `printer_status` WS messages to "naturally update the status" — but those messages carried no `awaiting_plate_clear` field, so the merge at line 146 preserved the stale `false`. The only path that ever surfaced `true` was the 30 s HTTP fallback poll at `PrintersPage.tsx:1430`, and on a chatty printer each incoming WS tick's `setQueryData` bumped React Query's `dataUpdatedAt`, pushing the next fetch further out — which is why the delay varied from ~30 s to several minutes. The plate-status pill at `PrintersPage.tsx:1672-1675` rendered "Plate Clear" (the fallback label for falsy `awaiting_plate_clear`) during the entire stale window, compounding the confusion. Fixed by emitting `awaiting_plate_clear` from `printer_state_to_dict`: the function already has `printer_id`, so it reads `printer_manager.is_awaiting_plate_clear(printer_id)` directly and returns `False` when no id is passed (for the few callsites that don't have one). No frontend change needed — the existing WS merge path now carries the flag end-to-end, the "Clear Plate" button appears instantly on completion, and the queue-dispatch side of the gate (which already reads the in-memory set directly via `print_scheduler.py:1125`) is unaffected. Regression tests in `test_printer_manager.py` assert the WS dict always contains the key and that it surfaces `True` when the manager has the flag set for that printer_id. Affects every printer equally because the path is transport-agnostic — not an H2D- or A1-specific problem, just more visible on H2D because its longer finish sequence gave the poll slip more opportunities to miss.
- **Printers-Page Search Turns Into a Password Field After Opening Change-Password Modal** — On the Printers page, clicking the key icon in the sidebar to open the Change Password modal caused the "Search printers" input to render as a password field (masked dots); closing the modal didn't restore it, requiring a full reload. Root cause: the Change Password modal has three `<input type="password">` fields but no accompanying username input, so password-manager browser extensions (1Password, Bitwarden, Chrome/Safari built-in) scanned the current DOM for a matching username anchor and latched onto the nearest `type="text"` input with no `name`/`autoComplete` — which happened to be the Printers-page search bar — and overrode its rendering. Fixed on two levels: (1) added a hidden `<input type="text" name="username" autoComplete="username" value={user.username} readOnly hidden>` at the top of the Change Password modal so password managers have a proper anchor and stop hunting elsewhere — as a bonus, saved new passwords are now correctly keyed to the logged-in user; (2) hardened the Printers-page search input with `type="search"`, `name="printer-search"`, `autoComplete="off"`, and `data-1p-ignore` / `data-lpignore="true"` so any future heuristic-based autofill also skips it.
- **AMS Slot Configure: Custom Cloud Preset Resolves to "Generic" in Slicer & Printer LCD** ([#1053](https://github.com/maziggy/bambuddy/issues/1053) follow-up) — After configuring any AMS slot (HT or regular) with a user custom Bambu Cloud preset built on top of a Bambu base profile (e.g. "Sting3D ABS" inheriting from "Generic ABS @BBL H2D"), OrcaSlicer's *Sync Filaments* continued to resolve the slot to "Generic ABS" and the custom preset never appeared on the printer's own LCD — independent of the earlier UI fix (commit `87a5aa36`) which only corrected Bambuddy's own modal. Root cause: when Bambu Cloud's `GET /cloud/settings/{setting_id}` returns a user preset with `filament_id: null` and `base_id: "GFSB99_07"` (cloud doesn't mint a distinct filament_id for presets that only override fields of a generic base), `ConfigureAmsSlotModal.tsx:382-384` fell back to `convertToTrayInfoIdx(base_id)` which strips the version suffix and the `S` prefix → `"GFB99"` — Generic ABS's filament_id. The printer accepted and reported back `GFB99`, so both the LCD and OrcaSlicer correctly resolved the slot to Generic ABS. The fallback was never right: the preceding default already set `tray_info_idx = convertToTrayInfoIdx(selectedPresetId)` which for any `PFUS*`/`PFSP*` setting_id returns the base setting_id itself (via the helper's `startsWith('PFUS')` branch added earlier), and the printer + both slicers round-trip that format unchanged — confirmed by existing backend integration tests (`test_configure_pfus_sent_directly`, `test_pfus_slicer_filament_used_directly`), by the print scheduler's slot-matching which already expects `P*` short-form IDs in the printer's reported `tray_info_idx` (`print_scheduler.py:910`), and by the inventory Assign Spool flow which has been sending `PFUS*` preset IDs to the printer for months. The buggy fallback *overwrote* the correct default with a generic mapping. Fixed by removing the base_id branch: when cloud detail carries a distinct `filament_id` we still prefer it, otherwise we keep the setting_id-derived default. BambuStudio Sync now resolves the custom preset cleanly; OrcaSlicer (whose user presets don't carry a `filament_id` field at all, only `inherits`) will continue to fall back to the inherited generic — that's an OrcaSlicer preset-format limitation, not something Bambuddy can fix on its side, and the behaviour is strictly not worse than before. Regression tests in `ConfigureAmsSlotModal.test.tsx` pin four paths: (1) cloud detail with `filament_id: null` → `tray_info_idx` is the `PFUS*` setting_id, (2) cloud detail with a concrete `filament_id` → that filament_id wins over the default, (3) GFS* Bambu presets skip the cloud-detail fetch entirely and still map to the short `GF*` filament_id, and (4) a 5xx / network error on the cloud-detail fetch degrades gracefully to the `PFUS*` default instead of aborting the configure flow. An end-to-end backend test (`test_configure_pfus_preserves_setting_id_pair`) locks in that both `tray_info_idx=PFUS…` and `setting_id=PFUS…` survive the HT-slot `POST /slots/{ams}/{tray}/configure` path untouched. Thanks to @mrnoisytiger for the detailed browser-console / network / backend-log diagnostic data that isolated the fallback path, and for sharing the OrcaSlicer preset JSON that showed the missing `filament_id` field.
- **Single Malformed `rgba` Bricks the Entire Filaments Inventory Page** ([#1055](https://github.com/maziggy/bambuddy/issues/1055)) — A user's Filaments page went blank and "Add Spool" became a no-op with no visible error. The backend was returning HTTP 500 from `GET /api/v1/inventory/spools` with `fastapi.exceptions.ResponseValidationError: rgba → 'FFFFFFF' should match pattern '^[0-9A-Fa-f]{8}$'` — a single legacy spool row had a 7-char rgba (missing one trailing `F`) and Pydantic's strict pattern on `SpoolResponse` refused to serialize the whole list because of it. Root cause spans three layers: (1) `SpoolUpdate` had no rgba pattern constraint, so PATCH calls could plant malformed values straight into the DB (`SpoolCreate` did validate, but only on initial create); (2) the `ColorSection` hex input's onChange ternary `val.length <= 6 ? 'FF' : ''` silently emitted 7-char strings for 5-char or 7-char typed input (5 chars + `FF` alpha = 7 chars; 7 chars got no alpha appended at all), which then flowed to the unvalidated PATCH endpoint; (3) `SpoolResponse` inherited the same pattern as `SpoolCreate`, so any malformed row already in the DB exploded the entire list endpoint on serialize even though write-side validation was the right place for the check. Fixed on all three layers: `SpoolUpdate.rgba` now carries the same `^[0-9A-Fa-f]{8}$` pattern as `SpoolCreate`, so PATCH requests with malformed rgba are rejected with 422 at the boundary. The hex input always emits a fully-formed 8-char RRGGBBAA on every keystroke — 8-char paste passes through, 7-char drops the stray char, shorter input is right-padded with `'0'` and given FF alpha. `SpoolResponse.rgba` is now an unconstrained `Optional[str]`: the pattern belongs on request schemas where Pydantic can reject bad input, not on responses where it turns a single bad row into a total page failure. A legacy malformed row still appears in the UI (the color just renders as whatever browser default applies) but the user can see, edit, and delete it instead of having to hand-edit SQLite. Backend tests cover all three schema contracts (16 cases across `SpoolCreate` accept/reject, `SpoolUpdate` accept/reject, `SpoolResponse` lenient-tolerance on 7-char / null / garbage). Frontend tests cover the hex-input normalization for every input length 0–8 plus non-hex strip-and-pad. Thanks to @fdsghy4a for the end-to-end debugging and for locating the exact malformed row in their DB.
- **Printer-Card "Print" Button Leaves Transient Copy in File Manager** ([#730](https://github.com/maziggy/bambuddy/issues/730)) — The "Print" button on a printer card (and the equivalent drag-drop-onto-card flow) was silently uploading the chosen file into the Library file manager as a side effect before printing. Root cause is structural: the frontend opened `FileUploadModal` to persist the file as a `LibraryFile`, then `PrintModal` dispatched a library print through `POST /library/files/{id}/print`, which uses the LibraryFile as the source for both the archive copy and the FTP upload to the printer. When the dispatch finished, both the `LibraryFile` row and its disk file in `data/library/` were left behind, so every one-off Direct-Print accumulated an unwanted File Manager entry that the user had to find and delete manually. The other three print entry points are untouched: Archive "Reprint" never involved the library, and File Manager "Print" / Project Detail "Print" are paths where the user deliberately put the file in the library, so their entries are preserved. `POST /library/files/{id}/print` now accepts an optional `cleanup_library_after_dispatch` boolean. When true, `_run_print_library_file` stages the LibraryFile row for deletion in the same transaction as the archive insert (so a mid-flight FTP or `start_print` failure rolls back both at once, leaving no orphan), commits together, then unlinks the library disk file and thumbnail from disk after commit succeeds. External library files (`is_external = True`, pointing at user-managed folders outside Bambuddy's control) are never touched regardless of the flag. The Printers-page Direct-Print flow is the only caller that sends `true`; every other `api.printLibraryFile` call site leaves the flag unset so default-False preserves their library entries. Added two unit tests at the enqueue level (default-false + flag-propagates-true), two integration tests at the endpoint level (default-false + forwards-true + cleanup flag never leaks into the MQTT options dict), and two frontend tests on `PrintModal` guarding that `cleanupLibraryAfterDispatch` only forwards when explicitly set — so future File Manager / Project Detail entry points can't accidentally inherit the Direct-Print semantics. Thanks to @3823u44238 for flagging the surprising side effect.
- **Direct / File Manager / Library Prints Still Unattributed to User** ([#730](https://github.com/maziggy/bambuddy/issues/730)) — The 0.2.3.1 fix (commit `f03d0c4c`) plumbed the authenticated user from `POST /library/files/{id}/print` into the background-dispatch job object, but the dispatcher itself never read it back out: `_run_print_library_file` called `ArchiveService.archive_print()` without the `created_by_id` parameter and never called `printer_manager.set_current_print_user()`. Net effect: direct prints from the printer-card "Print" button, File Manager prints, and Library prints all continued to land archives with `created_by_id = NULL` (invisible to the per-user stats filter), and the post-print email notification had no user to target. The dispatcher now forwards `job.requested_by_user_id` to the archive at creation time and registers the current-print user after `start_print` succeeds — matching the reprint path's behaviour. Reprint-from-Archive attribution is a separate bug (the reprint reuses the source archive row as-is, so a NULL `created_by_id` stays NULL) and is tracked on #730. Thanks to @3823u44238 for the thorough end-to-end retest.
- **Spoolman Iframe Blocked by CSP on HTTP Instances** ([#1054](https://github.com/maziggy/bambuddy/issues/1054)) — The Filament tab showed a blank page with a brief Spoolman flash on reload. Browser console reported `Content-Security-Policy: The page's settings blocked the loading of a resource (frame-src) at http://<host>:7912/spool because it violates the following directive: "frame-src 'self' https:"`. Root cause: commit `53a70e37` (#995) tightened the CSP to allow external sidebar iframes but only whitelisted `https:`, overlooking that self-hosted services on LANs — Spoolman, OctoPrint, etc. — almost always run over plain HTTP. The `frame-src` directive now allows `http:` as well (`frame-src 'self' http: https:`), matching the `connect-src 'self' ws: wss:` pattern already used for WebSockets. `frame-ancestors 'none'` still prevents Bambuddy itself from being framed cross-origin. Thanks to @saint-hh for reporting.
- **AMS-HT: Custom Filament Preset Reverts to "Generic" in UI After Configure** ([#1053](https://github.com/maziggy/bambuddy/issues/1053)) — After configuring an AMS-HT slot (HT-A/HT-B) with a custom Bambu Cloud preset (e.g. "Devil Design PLA Basic"), the slot card and Configure modal kept showing "Generic PLA" even though the `ams_filament_setting` command succeeded and BambuStudio / the printer's LCD both rendered the correct custom preset. Root cause: the `GET /api/v1/printers/{id}/slot-presets` endpoint keyed its response dict by `ams_id * 4 + tray_id`, which collapses cleanly to the same integer the frontend uses for regular AMS slots (0 through 15) but produces `128 * 4 + 0 = 512` for HT-A — a key nothing looks up. The frontend's PrintersPage HT render path calls `getGlobalTrayId(ams.id, …, false)` which returns the ams_id itself (`128` for HT-A), and SpoolBuddy's AMS page used a third, unrelated formula (`(amsId - 128) * 4 + trayId + 64 = 64`). All three agreed for regular AMS so the mismatch only surfaced on HT, where the saved preset name never reached the UI and the render fell through to `tray.tray_type` → rendered as "Generic PLA". Backend now keys the response via a `_slot_preset_key` helper that mirrors frontend `getGlobalTrayId` (HT → `ams_id`, regular/external → `ams_id * 4 + tray_id`), and SpoolBuddyAmsPage uses the shared `getGlobalTrayId` helper instead of its home-grown formula. Regression test covers the key scheme for regular, HT, and external slots. Thanks to @mrnoisytiger for the detailed reproduction.
- **AMS: Configure / Assign Spool Hidden on Reset Slots, and Assign Spool Missing Matching-Material Inventory** ([#1047](https://github.com/maziggy/bambuddy/issues/1047)) — Two separate symptoms from the same report. (1) After resetting an AMS slot from the printer UI, the Bambuddy printer card showed "Empty Slot" with no Configure or Assign Spool actions on hover, while the same slot in SpoolBuddy's AMS page still let the user re-configure it. Root cause: commit `c9efa4b8` (#784) added a `tray?.state === 10` gate to the `EmptySlotHoverCard` actions, intended to show the buttons only when a spool was physically present but not loaded (state=10) and hide them on truly empty slots (state=9). In practice, firmware often reports `state=9` (or no `state` field at all) after a user-initiated reset — even when a spool is still physically in the slot — so the actions disappeared exactly when the user needed them. The gate is redundant anyway (`EmptySlotHoverCard` is only rendered when the slot has no `tray_type`, so it's definitionally empty from Bambuddy's perspective), and configuring an empty slot is a valid "tell the printer what will be loaded here" operation. The gate is now removed at both the standard-AMS and AMS-HT render paths. (2) After configuring a slot with a Generic profile (e.g. "Devil Design PLA Basic Red"), the Assign Spool modal didn't list the matching inventory spool unless the user enabled the "Show all spools" toggle. Root cause: the filter at `AssignSpoolModal.tsx:144` required `normalizeValue(spool.slicer_filament_name) === normalizeValue(trayInfo.profile)` — manually-added inventory spools typically don't have `slicer_filament_name` populated, so they failed the exact-profile check even when the material matched. The filter now prefers an exact slicer-profile match when both sides advertise one, and falls back to partial material match in either direction (so e.g. a spool with `material="PLA"` is selectable for a slot reporting `"PLA Basic"`) when profile info is missing. (3) Once the matching spool was assignable, a "profile mismatch" confirmation dialog still warned on every assignment because Bambu Studio / OrcaSlicer slicer-profile names carry a printer/nozzle/variant qualifier after `@` (e.g. `"Devil Design PLA Basic @Bambu Lab H2D 0.4 nozzle (Custom)"`) while the tray stores only the bare base name (`"Devil Design PLA Basic"`), and `checkProfileMatch` compared the full strings. Both the filter and the mismatch check now strip the `@…` qualifier before comparing, so identical base profiles are treated as a match. Regression test covers a spool with no slicer profile being surfaced for a slot whose profile + material are both set. Thanks to @TravisWilder for the report.
## [0.2.3.1] - 2026-04-20
### Fixed
- **⚠️ Bed-Jog "Home Z" Could Crash the Bed Into the Toolhead** ([#1052](https://github.com/maziggy/bambuddy/issues/1052)) — **Critical safety fix.** On H2C (and by extension any Bambu printer where Z-home moves the bed UP toward an endstop — H2D, H2S, and X1 family all share this kinematics) the bed-jog modal's "Home Z" button sent a raw `G28 Z` over the `gcode_line` MQTT command. Bare `G28 Z` skips the toolhead-park step that a full `G28` runs first, so the bed raised without stopping at a safe height — in the reporter's case the toolhead happened to be parked on the purge chute and no damage was caused, but hitting the button with a toolhead anywhere else would have driven the bed into it at full Z speed. Root cause was the `/api/v1/printers/{id}/home-axes` endpoint's per-axis gcode mapping (`"z" → "G28 Z"`, `"xy" → "G28 X Y"`, `"all" → "G28"`). The endpoint now ignores the `axes` argument entirely and always sends a bare `G28`, which Bambu firmware expands into the safe multi-step sequence (park toolhead → home XY → home Z). The MQTT client helper `BambuClient.home_axes()` has the same change. The bed-jog modal is retitled "Auto Home" and its copy now says "parks the toolhead, then homes X, Y, and Z" so users aren't surprised when X/Y motion happens first. After a successful Auto Home click, the modal no longer re-prompts on the next jog in the same session — the "not homed" warning is gated on a session-scoped acknowledgement flag that was only being set by "Move anyway" and now also fires on successful Auto Home. Regression test covers all three axes arguments producing the same bare `G28`. Thanks to @mikefromdot for catching this with an undamaged retest.
- **Skip Objects: Enlarged Preview Image Fails to Load on Auth-Enabled Instances** ([#1046](https://github.com/maziggy/bambuddy/issues/1046)) — Clicking the mini print-preview thumbnail inside the Skip Objects modal opened a lightbox that showed a broken-image icon instead of the full-size plate preview. The thumbnail `<img>` wrapped its `src` with `withStreamToken()` (which appends the short-lived camera-stream token to `/api/v1/` URLs that `<img>` tags can't attach an `Authorization` header to), but the enlarged lightbox `<img>` used a bare `${status.cover_url}?view=top` so the browser's unauthenticated request was rejected by the backend. Both images now go through `withStreamToken()`. Thanks to @elit3ge for the report and screenshot.
- **P1S Print Dispatches Stuck at IDLE Due to task_id Int32 Overflow** ([#1042](https://github.com/maziggy/bambuddy/issues/1042)) — Since the #1011 fix switched `project_id` / `subtask_id` / `task_id` from hardcoded `"0"` to `str(int(time.time() * 1000))`, each submission sent a 13-digit epoch-millisecond value (~1.7×10¹²). P1S firmware (observed on 01.10.00.00) clamps oversized task identity fields to signed int32 max (`2147483647`), so every dispatch looked identical from the printer's perspective — it treated a fresh print as a continuation of the prior FAILED job, returned `result: success` for `project_file` (command accepted), but then sat at `gcode_state: IDLE` with an empty `gcode_file` instead of transitioning to `PREPARE`/`RUNNING`. Thanks to @EdwardChamberlain for pinpointing the exact line and suggesting the mod fix. The three identity fields are now set to `str(int(time.time() * 1000) % 2_147_483_647 or 1)`: modulo keeps values inside the signed-int31 window with a ~24-day uniqueness cycle (more than enough for reprint deduplication), and `or 1` guards against the astronomically unlikely zero case (the printer rejects `task_id=0`). Regression test `test_submission_id_fits_signed_int32` asserts all three IDs are `< 2**31`. Two of @EdwardChamberlain's other suggestions — resolving `bed_type` from the sliced 3MF's per-plate JSON instead of hardcoding `"auto"`, and gating dispatch success on an actual state transition to `PREPARE`/`RUNNING` rather than on `project_file`'s `result: success` — are larger changes tracked separately.
- **FTP Download Zombie-Thread Race on Slow WiFi** ([#1014](https://github.com/maziggy/bambuddy/issues/1014)) — Users on 2.4 GHz WiFi with heavy neighborhood interference saw "Successfully downloaded" log lines for queued prints that Bambuddy nonetheless reported as failed, and the slicer file landed in `/app/data/archives/temp/` with the File Manager unable to find it. Root cause: `download_file_async` wrapped the blocking FTP `RETR` in `asyncio.wait_for` with a 30–60 s timeout (user-configurable via `ftp_timeout`), but the wrapped thread couldn't be cancelled. On a slow link the download would overshoot the timeout by 15–30 s, at which point `_run()` waited a hard-coded 0.5 s for the zombie to finish, gave up, and returned failure — which triggered `with_ftp_retry` attempt 2, whose `_download` spawned a brand-new FTP session that contended with attempt 1's still-running transfer. Attempt 1's zombie eventually completed and wrote the file to disk, but by then attempt 2 (and 3, 4) had long since run out their own timeouts with their own fresh `completion` dicts and reported failure; the archive pipeline saw only the final `None` from `with_ftp_retry` and created a fallback archive row with no 3MF data, which is why Skip-Object couldn't find the plate's objects even though the 3MF was on disk. Two fixes: the 0.5 s post-timeout sleep is replaced with a `threading.Event` the worker sets in its `finally` block, and `_run()` waits for that event with a bounded grace of `max(min(ftp_timeout, 30), 0.5)` s — covering the slow-WiFi overshoot case without extending a genuinely stuck connection indefinitely. The log line now includes the grace window (`timed out after Xs (plus Ys grace)`). Regression test `test_download_file_async_timeout_waits_for_slow_zombie` simulates a 1.5 s zombie with a 1.0 s wait_for timeout; old 0.5 s sleep would give up, new 1.0 s grace salvages. The existing `test_download_file_async_timeout_no_salvage_when_incomplete` still passes — a thread that never completes within the grace window still returns failure. Thanks to @heffe2001 for the detailed reproduction and support logs.
- **Obico: Cold-Start Capture Timeout Sticks in Status Banner** ([#172](https://github.com/maziggy/bambuddy/issues/172)) — On the very first detection poll after a restart, the initial RTSP snapshot capture occasionally exceeded the 20 s `SNAPSHOT_CAPTURE_TIMEOUT` (the first keyframe from the printer's camera can take a while on a cold RTSP connection). Subsequent polls every ~8 s recovered and captured in ~1.2 s, but the red `× Failed to capture snapshot for printer N` banner in Settings → Failure Detection → Status stayed up forever because `ObicoDetectionService._last_error` was written on failure and never cleared on the next successful poll. The successful branch in `_check_printer` now clears `_last_error` to `None` once a capture + ML call + classification complete, so the banner reflects only errors from recent cycles. Configuration-level errors (missing `external_url`, missing `ml_url`) still persist because they return before the clearing line — users still see them until they fix the setting. Regression test covers: seed `_last_error`, run one successful `_check_printer`, assert `_last_error is None`. Thanks to @fblix for the reproduction and screenshot.
- **Printer Card Controls Row Overflows in Chrome** — At Medium card size on a wide viewport, the printer-card controls row (fan badges, airduct mode, print speed, bed jog, then Stop / Pause on the right) visibly overlapped in Chrome while rendering fine in Firefox and Safari. The controls-row layout had a `max-[550px]:flex-wrap` rule on the left badge group that only fires below 550 **viewport** pixels, so on a wide viewport with a narrow card the left group never wrapped — and since its badges don't truncate, Chrome painted the overflowing speed/bed-jog badges on top of the right-pinned Stop/Pause buttons. German locales made it obvious ("Pausieren" is 9 characters). The left group now uses unconditional `flex-wrap`, so when badges don't all fit on one line they wrap inside the left cell instead of colliding with the right cell; the parent row also wraps `gap-y` so Stop/Pause drops to a new line in the worst case. Pre-existing (commit `4ff3e2a6`, Feb 2026), surfaced while testing #939.
- **MQTT Smart Plug Subscription Lost After Every Restart** ([#1010](https://github.com/maziggy/bambuddy/issues/1010)) — Users integrating a Shelly (or any other) plug through an external MQTT broker (e.g. ioBroker, Zigbee2MQTT, Home Assistant's MQTT broker) saw the plug's power / state / energy readings go dark after every Bambuddy restart, and the only fix was to open Settings → Smart Plugs, rename the topic to a dummy value, save, rename it back and save again. Root cause: the startup restore path in `main.py` (~line 4120) still used the legacy single-topic model (`mqtt_topic` plus `*_path` kwargs), while the Settings UI save path had been upgraded to the newer per-type model (`mqtt_power_topic` / `mqtt_energy_topic` / `mqtt_state_topic` each with their own paths, multipliers and `mqtt_state_on_value`). Plugs configured entirely with the new per-type fields got skipped at startup because the `if plug.mqtt_topic:` guard short-circuited — which is exactly what a Shelly-via-ioBroker setup looks like, since those publish power and state on separate topics. The "rename, save, rename back" workaround triggered the update endpoint, which was using the correct per-type code and re-established the subscription. Fix: extracted the topic-resolution + `service.subscribe()` call into a single `subscribe_plug_to_mqtt(service, plug)` helper in `backend/app/services/mqtt_smart_plug.py` that preserves legacy fallback, and routed the startup restore, create, and update routes all through it so future schema changes can't cause the three paths to drift again. Regression tests cover: per-type topics restored without a legacy topic set, legacy single-topic backward compat, per-type multipliers overriding legacy, per-type winning when both are set, the empty-config skip case, and topic-list de-duplication. Thanks to @saint-hh for the clear repro steps.
- **Large 3MF Uploads Archived as Corrupted ZIPs** ([#1032](https://github.com/maziggy/bambuddy/issues/1032)) — On bare-metal Raspberry Pi installs (armv7l / Python 3.11 / Bookworm), 3MF files larger than a few MB arrived complete via the virtual-printer FTP server but the copy into `data/archives/` ended up not being a valid ZIP. The archive row was still written, the printer card looked fine, and the problem only surfaced later when opening the archive in the UI, where `GET /archives/{id}/plates` logged `Failed to parse plates from archive N: File is not a zip file` and the thumbnail / plate / filament panels came up blank. Two things conspired: `shutil.copy2` takes the Linux `sendfile()` fast path on Python ≥ 3.8, and a partial-return from that syscall silently truncated the destination for the upload sizes users hit; and `ThreeMFParser.parse()` had a bare `except: pass` around its `zipfile.ZipFile` open, so the archive pipeline kept going with empty metadata and left the bad file on disk. The copy is now an explicit chunked read/write with `fsync()` — no sendfile involved — with a post-condition `zipfile.is_zipfile()` check that refuses to create the archive row (and cleans up the archive directory) when the source was a valid ZIP and the destination isn't, logging both sizes at `ERROR`. The parser's silent catch now logs at `WARNING` so corrupted 3MFs are visible in support bundles instead of disappearing into empty metadata. Regression tests cover small / multi-chunk copies, ZIP roundtrips, the post-copy `is_zipfile` sentinel on a truncated file, and the new parser WARNING. Thanks to @saint-hh for the detailed diagnosis.
- **Thumbnails Blank Until Reload After Sign-In** — On auth-enabled instances, signing out and back in left the File Manager (and occasionally the Archives page) full of broken thumbnails until the page was manually reloaded. Thumbnail URLs are gated by a short-lived camera-stream token that `<img>` tags can't send via `Authorization` headers, so the token is appended as `?token=…` at render time. Two race conditions conspired to break this: (1) the token query was keyed only on `['camera-stream-token']` and fired while the user was still on the login page, 401'd, and stayed cached — after sign-in nothing invalidated it; (2) when the token did eventually arrive, the global variable holding it was not reactive, so any File Manager / Archives page that had already rendered kept serving image URLs with no token. The token query now includes the user id in its key and is gated on `!!user`, so a new login always triggers a fresh fetch; and when the token transitions from null to a value, `useStreamTokenSync` walks the DOM once and updates `src` on every already-rendered `<img>`/`<video>` pointing at `/api/v1/` without the current token, reloading them in place.
- **P2S Firmware Check Shows Stale "Latest" Version** ([#1030](https://github.com/maziggy/bambuddy/issues/1030)) — On P2S (and X2D) the Firmware Info modal reported `01.01.01.00` as the newest available release even though `01.02.00.00` had shipped on the Bambu Lab wiki weeks earlier, so the "update available" badge never appeared. Two silent regex mismatches in the wiki scraper caused `_fetch_all_versions_from_wiki()` to return an empty list: (1) the section-heading anchor parser required a dash between the version bytes and the release date (`id="h-01020000-20260409"`), but P2S and X2D publish anchors without the dash (`id="h-0102000020260409"`); (2) the text-based fallback only accepted ASCII parens around the date, while P2S, X2D, A1 and A1-mini headings render dates in full-width `(YYYYMMDD)` (U+FF08/U+FF09). When both paths failed, the code silently fell back to the Bambu Lab download page, which still lagged at `01.01.01.00`. The anchor regex now accepts an optional dash and the fallback accepts both paren styles; added regression tests for the no-dash anchor and full-width paren shapes. Thanks to @Minebuddy for reporting.
- **Library File Print-Usage Tracking** ([#1008](https://github.com/maziggy/bambuddy/issues/1008)) — `LibraryFile.print_count` and `last_printed_at` are now updated on every successful queued print completion. Previously both fields were defined on the model and displayed in the File Manager, but nothing ever wrote to them — every file in every library showed as never printed. Now counts increment cumulatively and `last_printed_at` stamps the completion timestamp (UTC). Failed, cancelled and user-aborted prints are intentionally excluded, so the fields represent "successful usage" rather than "attempted usage." This unblocks sorting the File Manager by last-printed date and is a prerequisite for the scheduled-purge feature requested in #1008. Thanks to @cadtoolbox for the report.
### Improved
- **Color Catalog Default Filter Set to "All Manufacturers"** ([#1039](https://github.com/maziggy/bambuddy/issues/1039)) — Settings → Color Catalog opened with the manufacturer dropdown pre-filtered to *Bambu Lab*, so users searching for a third-party color had to change the dropdown to *All Manufacturers* on every visit. The page now defaults to *All Manufacturers* and lets you narrow down from there. Thanks to @VID-PRO for the suggestion.
- **File Manager: Collapse Folders by Default** ([#996](https://github.com/maziggy/bambuddy/issues/996)) — Added a **Collapse** toggle next to **Wrap** in the File Manager sidebar header. When enabled, the folder tree opens with only top-level folders visible on every page load; disabling it restores the previous fully-expanded default. Toggling the preference also immediately re-collapses/re-expands the current tree — no reload required. Persisted to localStorage under `library-collapse-folders`, matching the existing `library-*` preference pattern. Thanks to @AshieTashi for the request.
### Changed
- **Docker runtime image on Debian Trixie** — The production Docker image now builds on `python:3.13-slim-trixie` instead of the Bookworm-based `python:3.13-slim`. Picks up ffmpeg 5 → 7 (HEVC/AV1 improvements for camera capture), OpenSSL 3.0 → 3.3, and two more years of APT package freshness. Frontend-builder stays on Bookworm until the Node.js image team publishes Trixie variants — users never see that stage.
## [0.2.3] - 2026-04-19
### New Features
- **Move Build Plate from Printer Card** ([#791](https://github.com/maziggy/bambuddy/issues/791)) — The printer card controls row now has a Z-jog badge between the speed control and the stop/pause buttons. Click the up/down arrows to move the build plate; click the middle label to switch the step size (1 / 10 / 50 mm). When the printer is not homed (typical right after a print finishes), the first jog opens a Bambu Studio-style warning modal with **Home Z**, **Move anyway** (bypasses soft endstops for this move), or **Cancel**. After the first "Move anyway" in a session, subsequent jogs skip the dialog. Disabled while a print is running. Backed by new `POST /printers/{id}/bed-jog` and `POST /printers/{id}/home-axes` endpoints, both gated behind `printers:control`. Thanks to @cadtoolbox for the request.
- **Printer Card Status Badges & Quick Controls** — The Printers page printer card now exposes new at-a-glance controls inspired by the Home Assistant Bambu Lab integration:
- **Enclosure Door badge** in the top status row (DoorOpen/DoorClosed icons, green when closed, yellow when open). Detection uses the right MQTT field per printer family — `home_flag` bit 23 on X1/X1C/X1E and the top-level `stat` hex string bit 23 on P1/P2/H2 — and falls through the existing WebSocket push (status-change dedup key now includes door state, so toggling the door alone triggers a live badge update without waiting for the 30 s REST poll).
- **Airduct Mode badge** beside the print speed control (Snowflake/Flame icons, sky for Cooling and orange for Heating). One-click dropdown switches the printer between cooling and heating via the existing `set_airduct` MQTT command. Gated to P2S/H2D/H2C/H2S.
- **Force Refresh** menu entry in the printer card kebab menu (RotateCw icon) that re-requests a full `pushall` MQTT status report from the printer without forcing a reconnect.
- **AI Print-Failure Detection via self-hosted Obico ML API** ([#172](https://github.com/maziggy/bambuddy/issues/172)) — New Settings → Failure Detection tab wires Bambuddy to a self-hosted [Obico](https://github.com/TheSpaghettiDetective/obico-server) `ml_api` container (no Obico account, no cloud, no WebSocket). While a print is running, the detection service periodically hands the printer's camera snapshot URL to the ML API, which returns YOLO failure-detection scores. Scores are smoothed over time using Obico's own EWM + short/long rolling-mean math (30-frame warmup, alpha = 2/13, short window ≈ 5 min at 10s/frame, long window ≈ 20 h) so a single noisy frame cannot trigger an action. Sensitivity (Low / Medium / High) scales the LOW/HIGH thresholds; when the smoothed score crosses HIGH, the configured action runs exactly once per print: *Notify only*, *Pause print* (MQTT pause command), or *Pause and cut power* (pause + turn off any smart plug linked to that printer). A per-printer toggle lets you monitor all connected printers or just a subset. The Status card shows whether the service is running, the active thresholds, each monitored print's current verdict (safe / warning / failure), and a live rolling detection history. Snapshots are captured locally with a 20 s timeout we control and stashed under a one-shot 32-byte nonce; the ML API fetches them via an unauthenticated `/api/v1/obico/cached-frame/{nonce}` URL that sidesteps Obico's hardcoded 5 s read timeout.
### Improved
- **Firmware Update Modal Shows All Announced Versions** ([#568](https://github.com/maziggy/bambuddy/issues/568)) — The firmware update dialog now lists every version announced on Bambu Lab's wiki release history, not just the single newest one. Each row shows whether an offline firmware file is actually available for that version — rows marked **Usable** (green) can be installed, rows marked **Unavailable** (gray) are announced but have no downloadable package yet (common for hot-fix releases like `01.01.03.00` which Bambu only ships as OTA). The currently installed version is highlighted with a blue **Installed** badge. Selecting any usable row swaps the release-notes block at the top to that version's notes and enables the Install button for it — including older-than-current versions, so you can roll back to a previous firmware without having to hand-flash a file. The wiki scraper was tightened to only extract version numbers from heading anchors (e.g. `id="h-01030000-20260303"`) so incidental version mentions in release-note prose — like an AMS firmware reference in an H2D changelog — no longer get mistaken for H2D firmware releases. Thanks to @Cornelicorn for the request.
- **Spoolbuddy Device Controls in Settings** ([#962](https://github.com/maziggy/bambuddy/issues/962)) — Each Spoolbuddy device card in Settings → Spoolbuddy now exposes five one-click actions alongside the existing Unregister button: Update (trigger daemon software update), Restart Browser (kiosk UI), Restart Daemon, Reboot (device), and Shutdown. Each action shows a confirmation dialog before queueing the command; buttons are disabled when the device is offline. Uses the existing `/spoolbuddy/devices/{id}/update` and `/spoolbuddy/devices/{id}/system/command` endpoints — no new backend work needed. Thanks to @TravisWilder for the request.
- **Support Bundle Covers All Settings & SpoolBuddy** — The support bundle / bug-report payload now dumps every row in the `Settings` table instead of filtering by a hard-coded allowlist: sensitive keys (tokens, passwords, URLs, paths, emails, etc.) have their values replaced with `[REDACTED]` but the key itself is kept, so new config flags automatically show up in future bundles without a code change. Also adds an `integrations.spoolbuddy` section listing registered SpoolBuddy devices (firmware version, NFC/scale hardware, calibration, online state, uptime) — anonymized, no hostnames/IPs/device IDs.
- **Settings Search Finds More Cards** — The cross-tab search field at the top of Settings now finds Sidebar Links, Spoolman, Spool Catalog, Color Catalog, all four Failure Detection sections, Advanced Email Authentication, SMTP Test, Authenticator App (TOTP), Email OTP, 2FA Linked Accounts, Single Sign-On (OIDC), LDAP Server Configuration, and the four Backup sub-cards (GitHub, History, Local, Scheduled). Powered by a new module-level registry (`frontend/src/lib/settingsSearch.ts`) so future settings register themselves next to their component instead of being forgotten in a central array.
### Changed
- **Plate-Clear Confirmation Disabled by Default** — New installs ship with Settings → Workflow → "Require Plate-Clear Confirmation" off. Multiple new users reported queued prints appearing to not start because the prompt was waiting for acknowledgement; opt in from Workflow if you want the confirmation gate.
### Security
- **Dependency Updates for Published Advisories** — Bumped two dependencies flagged by vulnerability scanners. `python-multipart` 0.0.22 → 0.0.26 closes CVE-2026-40347 (GHSA-mj87-hwqh-73pj), a denial-of-service triggered by large preamble or epilogue data around a multipart boundary — the 0.0.26 release now skips the preamble before the first boundary and silently discards the epilogue after the closing one. Bambuddy uses `python-multipart` transitively through FastAPI/Starlette for form and file-upload parsing, so any authenticated endpoint accepting `multipart/form-data` (e.g. backup restore, project thumbnail upload) was exposed. `dompurify` 3.3.3 → 3.4.0 picks up the fix for GHSA-39q2-94rc-95cp (the function-form `ADD_TAGS` could bypass `FORBID_TAGS`); Bambuddy's two call sites (`ProjectDetailPage`, `ProjectPageModal`) only use array-form `ALLOWED_TAGS`/`ALLOWED_ATTR`, so the specific bypass was not reachable, but the bump still hardens the sanitizer against future misconfiguration and clears the audit warning.
### Fixed
- **Virtual Printer "Synchronizing device information" Timeout with OrcaSlicer on Linux** ([#927](https://github.com/maziggy/bambuddy/issues/927)) — Follow-up to the b069b521 serial-adaptation fix. OrcaSlicer's Linux builds publish MQTT payloads with the C-string null terminator included in the length (same pattern as [paho.mqtt.c #1198](https://github.com/eclipse-paho/paho.mqtt.c/issues/1198)), so every decoded message arrived as `{…}\x00`. The virtual printer's strict `json.loads()` raised `JSONDecodeError: Extra data` and the handler silently returned — no pushall, get_version, or project_file was ever answered, so the slicer hit its 60 s sync timeout and reconnected in a loop. Real Bambu firmware's mosquitto passed the trailing byte through, which is why direct LAN connections worked, and why print_queue mode was the only affected path (proxy mode tunnels MQTT to the real printer instead of running the VP broker). The handler now strips trailing `\x00`/whitespace before parsing and logs the raw payload on any remaining decode failure so future silent variants are visible in support bundles. Thanks to @EdwardChamberlain for the debug-enabled support log that made the null byte visible in the raw bytes.
- **SpoolBuddy Kiosk Unusable After Full-Mode Install** — A bundled Bambuddy + SpoolBuddy install via `spoolbuddy/install/install.sh --mode full` produced an unusable kiosk on first boot: Chromium raced ahead of uvicorn and showed "can't connect to localhost"; after a manual reload the kiosk URL `/spoolbuddy?token=…` was hijacked by Bambuddy's first-run wizard (`AuthContext` force-redirects to `/setup` whenever `requires_setup=true`, regardless of the target path); the wizard asks for admin credentials, but a touch-only Pi has no on-screen keyboard; if the user skipped auth the browser landed at `/` instead of the kiosk, and if they tried to enable auth they were stranded. Standalone mode was unaffected because it runs against an already-configured remote Bambuddy. Fixed in three parts: (a) new `backend/app/cli.py` with a `kiosk-bootstrap` subcommand that in a single DB transaction creates a scoped API key (`can_read_status=True`, `can_queue=False`, `can_control_printer=False`) and upserts `setup_completed=true`, so the first-run wizard never triggers and the kiosk URL loads the SpoolBuddy page directly; users can still enable authentication later from the admin UI and the pre-provisioned key keeps working. (b) `install.sh` full-mode now runs the CLI as the bambuddy service user immediately after `create_bambuddy_service` and `sed`-replaces the `CHANGE_ME_AFTER_SETUP` placeholder in `spoolbuddy/.env`. (c) The generated `spoolbuddy-kiosk-launch` now polls `${backend_url}/health` with a 60 s timeout before exec'ing Chromium, so cold boots wait for uvicorn instead of flashing the connection-refused error. The CLI is idempotent with `--force` for re-installs.
- **Bambu Lab X2D Support** ([#988](https://github.com/maziggy/bambuddy/issues/988)) — Added X2D to the Add Printer and Edit Printer model dropdowns (both were missing the new model, so manual printer setup had no X2D option — auto-discovery via SSDP was unaffected). The newly released X2D (dual-nozzle, enclosed, hardened steel rod gantry, AMS 2 Pro compatible) identifies itself as internal model code `N6` via SSDP/MQTT, and serials begin with `20P9`. Because neither the code nor the prefix existed in any of Bambuddy's model tables, multiple paths silently fell back to wrong defaults: the camera service routed to the chamber-image protocol on port 6000 (which the X2D doesn't speak) instead of RTSP on port 322 — the reporter saw `Chamber image: data is not a valid JPEG` spam and no stream; the K-profile edit/delete path conditioned its in-place `cali_idx` write on the H2D serial prefix `094` and would therefore have treated X2D as a single-nozzle printer even though its dual-extruder layout matches H2D; the firmware-update check logged `Unknown printer model: N6`; and the virtual-printer model registry had no way to emulate X2D. Added the `N6 → X2D` mapping across every registry (`PRINTER_MODEL_ID_MAP`, `PRINTER_MODEL_MAP`, `ETHERNET_MODELS`, `STEEL_ROD_MODELS`, `CHAMBER_TEMP_SUPPORTED_MODELS`, firmware-check API keys and wiki path, virtual-printer SSDP product names and serial prefix, DB migration `vp_model_fixes`), extended `supports_rtsp()` to match `X2` display names and the `N6` internal code (camera now goes to port 322), expanded the dual-nozzle serial prefix check in `kprofiles.py` and the K-profile delete command in `bambu_mqtt.py` to also accept `20P9` so the H2D-style `cali_idx` in-place edit path runs on X2D, added X2D to the `is_h2d` model-family gate that selects the integer-format `timelapse`/`bed_leveling`/`flow_cali`/`vibration_cali`/`layer_inspect` fields in the MQTT print command, and added X2D to the frontend's door-badge and airduct-mode whitelists, `mapModelCode` lookups on both the Printers page and Spoolbuddy AMS page, and the MaintenancePage wiki-URL resolver (X2D inherits P2S's steel-rod lubrication, belt-tension, nozzle cold-pull and PTFE wiki pages, since its hardware is closer to P2S than to H2). Credit to @krautech for the report and the debug bundle, and to @legend813 for the initial PR (#989) that seeded most of the registry changes — the classification was corrected (X2D uses hardened steel rods like P2S, not carbon rods) and the dual-nozzle/K-profile gaps were added on top.
- **Print Speed Icon Not Updating Live When Changed on Printer** ([#993](https://github.com/maziggy/bambuddy/issues/993)) — Changing the print speed mode from the printer's own panel (instead of from Bambuddy) did not update the speed icon on the Printers page card; the new value only appeared after a full page reload. The MQTT parser was already tracking `spd_lvl` and updating `state.speed_level` correctly, but the WebSocket serializer (`printer_state_to_dict`) was missing the field — so live status pushes never carried `speed_level`, and the frontend's merge-over-old-cache update left the icon stuck on its previous value. The REST `/status` endpoint used on initial page load already included it, which is why reloads worked. Added `speed_level` to the WebSocket payload. Thanks to @chesterakl for reporting.
- **Camera Popup Shows "Valid camera stream token required" With Auth Enabled** ([#979](https://github.com/maziggy/bambuddy/issues/979)) — When Camera View Mode was set to "Window" and authentication was enabled, clicking the camera button opened a popup that immediately failed with `"Valid camera stream token required"`, while the embedded overlay kept working. Two root causes: (1) `window.open(...)` passed `noopener` in the popup features, which severed the opener link and prevented the browser from copying sessionStorage (where the auth token lives) into the popup — so the new window booted unauthenticated and the `POST /printers/camera/stream-token` fetch returned 401, leaving the `<img>` src without the required `?token=` query param; (2) even once the token arrived, `CameraPage` computed its URL from the module-level stream-token cache on render and never re-rendered when the cache was updated in a `useEffect`, so the first paint locked in a tokenless URL that the backend kept rejecting. Fixed by dropping `noopener` from the camera popup features (same-origin, trusted window) so sessionStorage is inherited, subscribing `CameraPage` to the `camera-stream-token` React Query so it re-renders the moment the token resolves, and appending the token directly from the reactive query value instead of the effect-synced module cache — the `<img>` src stays empty until the token is ready, so no tokenless request ever leaves the popup. Embedded-overlay mode was unaffected. Thanks to @VREmma for the reproducer.
- **AMS Slot Changes Stop Reaching Printer After Long Idle** ([#887](https://github.com/maziggy/bambuddy/issues/887)) — After printers sat idle for several hours, spool changes published by Bambuddy silently stopped reaching the printer — the UI updated but the printer ignored the command, and only a manual reconnect restored functionality. Root cause: the MQTT connection degraded into a zombie state where the receive path still worked (push_status telemetry kept flowing, so Bambuddy considered the connection alive) but the publish path was dead. The existing zombie detector — the developer mode probe — only ran on first connect when `developer_mode` was unknown; after the initial probe cached the value, subsequent zombie states went undetected because neither the staleness timer nor the keepalive could distinguish a half-open connection from a healthy one. The MQTT client now tracks `ams_filament_setting` command/response pairs: when a published command receives no response within 10 seconds, it's counted as unanswered. After two consecutive unanswered commands, the session is force-reconnected using the same `force_reconnect_stale_session()` mechanism. This catches zombie sessions at the moment the user encounters them — on their second failed spool change — rather than requiring a manual reconnect. Thanks to @RosdasHH for the detailed support bundles that made the diagnosis possible.
- **Obico Detection ML API Call Fails Silently With Empty Error** ([#172](https://github.com/maziggy/bambuddy/issues/172), [#1003](https://github.com/maziggy/bambuddy/issues/1003)) — The previous attempt at #1003 (0.2.3b4 dev) switched Bambuddy to **POST the JPEG bytes directly** to Obico's ML API as multipart form data, hoping to eliminate the callback-URL dependency for users behind reverse proxies with external auth. That approach cannot work: Obico's `/p/` endpoint is declared `methods=['GET']` upstream and only reads `?img=URL` as a query string (verified against `obico-server/ml_api/server.py`). Flask's router rejected every POST with 405 Method Not Allowed before any handler ran, which is why the Obico container logs showed zero activity while Bambuddy kept reporting `ML API call failed for printer N:` with a blank suffix — `raise_for_status()` on the 405 response produced an exception whose `str()` rendered empty in this path. Reverted to the pre-#1003 nonce-URL approach: the detection loop captures the JPEG locally with a 20 s timeout, stashes it under a 32-byte single-use nonce, and hands Obico a `GET /api/v1/obico/cached-frame/{nonce}` URL that resolves in <50 ms (so Obico's hardcoded 5 s read timeout never races our RTSP keyframe wait). The cached-frame route is un-authenticated at the Bambuddy layer — the unguessable 32-byte nonce with ~30 s TTL IS the credential. The warning log now also falls back to `type(exc).__name__` when `str(exc)` is empty, so future silent exceptions can never produce a blank error again. **For users behind reverse-proxy external auth (Authelia/Authentik/Cloudflare Access)**: the `/api/v1/obico/cached-frame/` path must be whitelisted from external auth — it's already public on Bambuddy's side. Thanks to @fblix for the ml-api-shows-zero-logs clue that pinpointed the 405 root cause.
- **Obico Detection Snapshot Killed by Stream Cleanup** ([#172](https://github.com/maziggy/bambuddy/issues/172)) — Third wave of #172 — once the cached-frame fix landed, `fblix` reported a permanent "Failed to capture snapshot" warning in the UI. The periodic camera stream cleanup task scans `/proc` for ffmpeg processes with Bambu RTSP URLs and kills any that aren't in the active-streams registry. The Obico detection service's `capture_camera_frame_bytes()` spawns its own short-lived ffmpeg process to grab a single JPEG frame, but that process was never registered with the stream cleanup — so when the 60-second cleanup cycle happened to run during the 5–10 s capture window, it killed the ffmpeg as "orphaned" (exit code -9). The detection service recovered on the next poll, but the kill produced unnecessary error logs and a missed detection frame. Fixed by tracking capture PIDs in a module-level set (`_active_capture_pids`) and excluding them from the `/proc`-scan kill list. Thanks to @fblix for the detailed timing analysis.
- **Direct Print from Library Not Attributed to User** — Clicking the Print button on a library file dispatched the job with no `created_by_id`, so the resulting archive had no owner and the print didn't show up in per-user statistics. The Queue and Reprint paths already forwarded the authenticated user; the library `POST /files/{file_id}/print` endpoint now does the same, reading the user from the JWT and passing it through to the dispatcher so direct prints are attributed like queued and reprinted ones.
- **Add/Edit Printer Modal Clipped on Short Viewports** ([#964](https://github.com/maziggy/bambuddy/issues/964)) — On short or zoomed-in browser windows, the Add Printer and Edit Printer dialogs exceeded the viewport height with no scroll, hiding the lower fields (Access Code, Model, Location) and the Save button. Users had to zoom the browser out to complete the form. The modal overlay now scrolls and the card caps at `calc(100vh - 2rem)` with internal overflow so every field stays reachable regardless of viewport height. Thanks to @MartinNYHC for reporting.
- **AMS Drying Silently Does Nothing** ([#971](https://github.com/maziggy/bambuddy/issues/971)) — Clicking Start Drying on a supported printer (e.g. P1S with AMS 2 Pro) could publish the MQTT command successfully but leave the AMS idle with no UI feedback. Two issues: (1) the firmware rejects the command when `dry_sf_reason` reports a blocking state (most commonly code 8 — AMS 2 Pro external power adapter not plugged in — but also "AMS busy", "already drying", etc.), and Bambuddy parsed that array but never surfaced it to the user; (2) the payload sent `filament: ""`, which some firmwares treat as an invalid-field refusal. The `/drying/start` endpoint now inspects the live `dry_sf_reason` for the target AMS unit and returns a descriptive 409 (e.g. "Plug in the external AMS power adapter to start drying") instead of silently publishing, and backfills an empty `filament` from the first loaded tray's type (defaulting to `PLA`) so the printer never rejects the command for a missing field. Thanks to @MartinNYHC for reporting.
- **Webhook Tokens Leaked into Logs When Debug Logging Enabled (Security)** — Turning on Settings → Support → Debug Logging elevated the `httpx` and `httpcore` loggers to DEBUG, which caused httpx to log the full URL of every outbound HTTP request. For Discord notifications and generic webhook notifications, the URL *is* the secret — the bearer token is embedded in the path — so any user who enabled debug logging (typically to capture logs for a bug report) was writing their Discord webhook token to `bambuddy.log` and then pasting it into GitHub issues or support bundles. `httpx`/`httpcore` are now pinned to `WARNING` regardless of the debug toggle; `paho.mqtt` still honours debug. If you enabled debug logging while notifications were sending, rotate any exposed Discord/webhook URLs — the token is in the path, so the whole URL must be regenerated in the provider's UI.
- **Queue Item Stuck in "Printing" When Start Command is Dropped** ([#967](https://github.com/maziggy/bambuddy/issues/967)) — If the physical printer dropped or ignored the MQTT `project_file` start command (same half-broken-session shape as #887/#936), the queue item was permanently orphaned in the `printing` status at 100% because the scheduler optimistically flipped the DB row to `printing` right after the publish succeeded locally and had no watchdog to revert it. Recovery required manually editing the SQLite `print_queue` table. A new watchdog now captures the printer's pre-dispatch state and polls for up to 45 s after `start_print()` returns; if the printer never transitions, the item is reverted to `pending` so the scheduler picks it up again, and the MQTT session is force-reconnected so the retry lands without a printer reboot. Thanks to @stringham for reporting.
- **Queued Prints Require Printer Reboot to Start** ([#936](https://github.com/maziggy/bambuddy/issues/936)) — On some printers, a queued print would be uploaded via FTP and the `project_file` MQTT command would be sent, but the printer never transitioned out of `FINISH`/`IDLE` and required a power cycle to unstick — after which it often started a previously cancelled print rather than the intended one. Root cause is a half-broken MQTT session (same shape as #887): the printer keeps publishing telemetry so Bambuddy reports it as connected, but our publishes on the command topic never reach the firmware. Existing recovery only triggered via the developer-mode probe path, which skips printers that already have a known `developer_mode` value. The print-dispatch verifier now treats an unacknowledged `project_file` (state unchanged after 15 s) as the same "commands not reaching printer" signal and forces a fresh MQTT session so the next dispatch can land without a printer reboot. The existing dev-mode probe path is refactored to share the same helper.
- **Clear Plate Confirmation Bypassed on Power Cycle** ([#961](https://github.com/maziggy/bambuddy/issues/961)) — With Auto Off enabled and another job queued, the smart plug would cut power when a print finished and immediately re-power when the scheduler saw the queue, at which point the printer booted fresh into `IDLE` and the next job auto-dispatched without the "Clear Plate & Start Next" confirmation. Root cause: the plate-cleared gate lived only in the in-memory `PrinterManager._plate_cleared` set, and the scheduler's idle check treated `IDLE` as always-idle regardless of whether a previous finish had been acknowledged — so the gate was lost across both Bambuddy restarts and the IDLE-on-boot state transition. The gate is now an `awaiting_plate_clear` column on the `printers` table, set by `on_print_complete` when a print finishes or fails, cleared by the `/printers/{id}/clear-plate` endpoint and by the scheduler when it dispatches the next job, and rehydrated from the DB into `PrinterManager` on startup. `_is_printer_idle` now short-circuits to not-idle whenever `require_plate_clear` is on and the printer is awaiting ack, regardless of the currently reported state — so the prompt survives Auto Off cycles, Bambuddy restarts, and the printer booting back into `IDLE`. The clear-plate endpoint no longer requires the printer to currently report `FINISH`/`FAILED` (it accepts the ack whenever the awaiting flag is set), and the Printers page widget prompts based on the flag rather than the reported state. Thanks to @miaopas for reporting.
- **Insecure Temp File Creation in Backup Export** — The manual backup download endpoint used `tempfile.mktemp()`, which is vulnerable to a symlink race condition (CWE-377). Replaced with `tempfile.mkstemp()` which atomically creates the file, eliminating the TOCTOU window.
- **Spoolman Iframe Blocked After 0.2.3b4 Security Headers** — The Spoolman page (Inventory → Spoolman iframe) failed to load when Spoolman was served from the same host as Bambuddy via a reverse proxy. The security-headers middleware added in 0.2.3b4 set `X-Frame-Options: DENY` on every response, which blocked even same-origin iframing. Relaxed to `SAMEORIGIN` so Spoolman (and any other same-origin tool behind the same reverse proxy) can be embedded again, while still preventing cross-origin clickjacking.
- **Large 3MF Print Restart Mid-Job Kept Duplicate Archive With Wrong Duration** ([#972](https://github.com/maziggy/bambuddy/issues/972)) — Second wave of #972 reports — a reproducer on a 37.5 MB BambuStudio-pushed print to an A1 surfaced three distinct problems that compounded across a Bambuddy container restart mid-print. (1) *Archive start_time lost*: the print-start handler only deduped existing `printing` archives by filename and marked them cancelled once older than 4 h — so a 13 h print that had a restart 10 h in got its archive cancelled, a brand-new archive created with `started_at = now()`, and the final duration displayed as ~1.5 h for a job that actually ran 13 h. Fixed by persisting the MQTT-provided `subtask_id` on every archive row (new `subtask_id` column, auto-added via the existing inline migration runner) and matching on that id first, regardless of age. Same id means same print; the row is resumed in place with its original `started_at`. Also revives `Stale`-cancelled rows from the legacy path if an earlier Bambuddy version already ran the old cancel-then-recreate logic. (2) *3MF search retried non-existent paths for ~48 min*: the path order was `/cache/ → /model/ → /data/ → /data/Metadata/ → /`, and every missing path burned the full retry budget (user had `ftp_retry_count = 10` with 30 s delay ⇒ 11 × 30 s × 4 missing paths ≈ 22 min before the real `/` root path was even tried). BambuStudio/OrcaSlicer actually push to `/` on A1-family printers, so the "most likely" path was tested last. Fixed by reordering to try `/` first, and by raising a new `FileNotOnPrinterError` sentinel from `download_to_file` when the FTP response is a 550 (file not found) so `with_ftp_retry`'s `non_retry_exceptions` short-circuits instead of waiting out the full delay ×11 retries against a path that will never have the file. Transient errors (425 "can't open data connection", SSL EOF, connection resets) still retry as before. (3) *Same 36 MB downloaded twice* — the cover-thumbnail endpoint and the archive-metadata handler each opened their own FTP session for the same file during the print, and the second session often hit 425 because the first was still using the printer's single FTP socket. Added a small in-memory `_threemf_path_cache` keyed on (printer_id, normalized filename): whichever flow fetches the 3MF first populates the cache, the other flow reuses the file read-only, and `on_print_complete` evicts the entry + deletes the temp file. Normalization collapses `Broly_X`, `Broly_X.3mf`, `Broly_X.gcode.3mf`, `Broly X`, and case variants to the same slot so both flows agree on the key. Net effect for the reproducer: what took ~48 min with a lost start time now takes seconds and the archive keeps its original row + timestamps. Thanks to @mstko for the reproducer and support bundles.
- **Large 3MF Files Silently Dropped After Print Finish** ([#972](https://github.com/maziggy/bambuddy/issues/972)) — After large prints, the Files tab rows arrived with no thumbnail, no filament breakdown and no cost — the archive row got created as a fallback with no 3MF even when the file was sittable on disk. Two root causes in the 3MF-fetch path. (1) The configured `ftp_timeout` setting (default 30 s, reporter had raised it to 300 s) was only plumbed through as the FTP *socket* timeout; the outer `asyncio.wait_for` wrapping `run_in_executor` was stuck on the hardcoded 60 s default, so the user's 300 s value never applied — every 3MF download was capped at 60 s regardless. (2) `asyncio.wait_for` cannot cancel `run_in_executor` threads: when the 60 s outer timeout fired, the executor thread kept running `ftplib.retrbinary` and frequently completed the download successfully ~30–60 s later — logging `"Successfully downloaded … N bytes"` and caching the working FTP mode — but by then the async wrapper had already returned `False`, so the retry loop kept re-attempting the same path, each attempt truncating the file the zombie thread had just written. After all 4 attempts the wrapper reported `failed after 4 attempts` and the archive was persisted as a fallback (no 3MF, empty `file_path`). The async wrapper now (a) accepts and uses `timeout` at each call site so `ftp_timeout` controls both the asyncio deadline and the socket deadline, and (b) salvages a post-timeout success: when the executor thread has set an explicit completion flag and the file is on disk, the wrapper returns `True` instead of discarding the result. Also fixes a cosmetic `//` prefix in the directory-search download path (`posixpath.join` replaces string concatenation that produced `"//file.3mf"` when the search dir was `"/"`). Thanks to @MartinNYHC for the report and @PurseChicken for the P1S support bundle.
- **SD Card Badge Removed** — After four rounds of fixes the printer-card SD status badge still flipped red on H2D when unrelated activity happened on the network (e.g. powering on an A1 caused every H2D to go red simultaneously). The underlying problem is that Bambu firmware SD-state signaling is not reliably derivable from MQTT: the legacy top-level `sdcard` field is only sent on some pushes with inconsistent typing, and `home_flag` bits 8-9 are cleared on heartbeat pushes even when a card is inserted, with no reliable way to distinguish heartbeats from full status reports. The badge has been removed entirely from the Printers page card and the Printer Info modal. Underlying `state.sdcard` parsing is retained (simplified to a plain truthy read of the `sdcard` field only, no more `home_flag` derivation, no heartbeat latches) because the firmware-update precondition check still needs to know whether a card is inserted before starting an update. Thanks to @MartinNYHC for the extensive reporting across all four rounds. Previously, this entry described the H2D badge flap and its three attempted fixes — kept here for history: The original bug toggled between "inserted" (green) and "not inserted" (red) every few seconds on H2D. Root cause: the MQTT parser used a strict identity check (`data["sdcard"] is True`) on the top-level `sdcard` field, but real firmware ships that field inconsistently — bool on some models, int `1`, or a string enum like `"HAS_SDCARD_NORMAL"` on others — so any message carrying a non-bool value flipped the state to `False`. Fixed by deriving the badge from `home_flag` bits 8–9 (`HAS_SDCARD_NORMAL` / `HAS_SDCARD_ABNORMAL`) when present — the canonical firmware source, same as door and store-to-SD parsing — and falling back to a truthy check on the top-level field for firmwares that only send that. Follow-up: the badge was still flapping because Bambu firmwares send partial MQTT pushes that carry the legacy `sdcard` field alone (without `home_flag`), and the fallback was re-engaging on every such push. The parser now latches `home_flag` as the canonical source for the session once seen, so partial pushes carrying only `sdcard` can no longer flip the badge; the latch resets on reconnect so a firmware change still re-learns. Second follow-up: on H2D the badge still showed red on initial Printers-page navigation and flipped to green on reload, because H2D also sends heartbeat-style `home_flag` pushes where bits 8–9 are clear even when a card is inserted. Downgrades from true→false now require three consecutive clear reads (upgrades false→true still apply immediately), so a single heartbeat no longer turns the badge red. Third follow-up: the three-strike counter still lost the race on idle printers — once an A1 or other printer connecting nearby triggered a burst of MQTT activity, idle H2Ds could accumulate ≥3 heartbeat pushes before the next full status report and all flip to red simultaneously. Reworked the derivation: the legacy top-level `sdcard` field is now authoritative when present (truthy check covers bool/int/string firmware variants), `home_flag` bits 8–9 are only consulted on full `push_status` reports (identified by the presence of multiple state markers like `gcode_state`, `mc_percent`, `nozzle_temper`, `print_type`, `stg_cur`, or `ams`), and bare heartbeat pushes carrying `home_flag` alone no longer affect SD state at all. Thanks to @MartinNYHC for reporting.
- **Archive Reprints Show Wrong Duration in Third-Party MQTT Monitors** ([#1011](https://github.com/maziggy/bambuddy/issues/1011)) — Re-printing a file from Bambuddy's archive caused external MQTT observers like OctoEverywhere to report wildly wrong durations: a 40 min job first reprint would show ~1 h 40 min, and a second reprint of the same file would compound further (~4 h for a ~45 min print), with the excess roughly matching the wall-clock gap since the previous archive replay. The same file printed via BambuStudio → Bambuddy proxy → printer reported correct durations every time. Root cause: the archive-reprint path built the MQTT `project_file` command with hardcoded `project_id="0"`, `subtask_id="0"`, `task_id="0"`, and `md5=""`, while BambuStudio mints unique identity fields per submission. The printer uses those IDs to key per-job state (including `gcode_start_time`), so when every reprint arrived under the same `task_id=0`, the printer reused the prior job's start timestamp instead of emitting a fresh state-transition event — third-party tools that derive duration from that timestamp latched onto a stale value, and successive replays compounded the error. `bambu_mqtt.start_print()` now generates a per-submission millisecond timestamp for `project_id`/`subtask_id`/`task_id` and a unique `md5` derived from the filename + timestamp, matching BambuStudio's per-submission-unique-ID behavior. Covers both archive reprints and direct prints from the Library. Thanks to @PurseChicken for the controlled A/B reproducer (Studio vs archive reprint) that pinpointed the divergence to the print-start command payload.
- **CSP Blocked Sidebar Iframes, Service-Worker Registration, and Google Fonts** — The strict `Content-Security-Policy` header added in 0.2.3b4 broke three things at once: (1) custom sidebar links pointing at external HTTPS URLs (e.g. a Grafana/telemetry dashboard) rendered in `ExternalLinkPage` were blocked because no `frame-src` was declared and iframes fell back to `default-src 'self'`; (2) the inline service-worker registration `<script>` at the bottom of `index.html` was blocked by `script-src 'self'`, silently preventing the PWA service worker from installing; (3) the `@import` of Google Fonts' Inter from `index.css` was blocked by `style-src` and `font-src`. Fixed by adding `frame-src 'self' https:` for user-configured HTTPS iframe targets, moving the inline SW-registration script into `/sw-register.js` so `script-src 'self'` covers it without needing `'unsafe-inline'` or per-build hashes, and allowing `https://fonts.googleapis.com` in `style-src` and `https://fonts.gstatic.com` in `font-src`. `frame-ancestors 'none'` is preserved so Bambuddy itself still cannot be framed cross-origin.
## [0.2.3b3] - 2026-04-12
### Improved
- **AMS Drying Support for P2S** — Remote AMS drying and queue auto-drying now work on P2S printers with firmware 01.02.00.00 or later. Previously P2S was hard-blocked from the drying feature.
### New Features
- **Scheduled Local Backups** ([#884](https://github.com/maziggy/bambuddy/issues/884)) — Settings → Backup now includes a "Scheduled Backups" card that automatically creates complete backup snapshots (database + all data directories) on an hourly, daily, or weekly schedule with configurable time-of-day and retention count. Backups are written as ZIP files to a configurable output directory (defaults to `DATA_DIR/backups/`), which Docker users can mount as a volume to their NAS or external storage. Each backup in the list can be downloaded, restored directly from the UI, or deleted individually. The manual backup download endpoint has also been optimized to stream directly from disk instead of loading the entire ZIP into memory, significantly reducing download wait times for large backups. Works with both SQLite and PostgreSQL installs. Fully localized across all 7 UI languages.
- **SpoolBuddy Device Management Tab** — Settings → SpoolBuddy now lists every registered SpoolBuddy device with live connection status, system details (firmware, IP, CPU temperature, memory, disk, OS, daemon and system uptime), hardware health flags (NFC / scale OK), and an Unregister button gated by a confirm modal. Previously, when a daemon crash caused SpoolBuddy to register itself twice, the kiosk UI silently used only the first device and there was no UI path to delete the orphaned duplicate — administrators had to delete the row directly in the database. A new `DELETE /spoolbuddy/devices/{device_id}` endpoint (gated by `inventory:delete`) handles the removal and broadcasts a `spoolbuddy_unregistered` websocket event so other tabs refresh immediately. A yellow warning banner appears when more than one device is registered to flag likely crash-duplicates. If an online device is accidentally unregistered, it will re-register itself on its next heartbeat. The Settings tab header also shows a device-count badge and a green/gray bullet indicating whether at least one registered device is online. Fully localized in English, German, and Japanese.
- **Print Files Directly from Project View** ([#930](https://github.com/maziggy/bambuddy/issues/930)) — The project detail page now lists the printable files from every linked library folder inline, with Play (Print Now) and CalendarPlus (Add to Queue) action buttons on each sliced file (`.gcode` and `.gcode.3mf`). No more round-tripping through File Manager to reprint project files. Prints triggered from the project view are automatically associated with the originating project, so the resulting archive shows up in that project's history without any manual assignment. Backend adds a `project_id` query parameter to `GET /library/files` that returns all files across linked folders in a single query (replacing the prior one-request-per-folder pattern) and validates `project_id` on both the direct-print and queue paths so a stale ID yields a 404 instead of a FK-constraint 500. Fully localized across all 7 UI languages. Thanks to @legend813 for the contribution.
- **Printers Page Search and Filters** ([#852](https://github.com/maziggy/bambuddy/issues/852)) — The Printers page now has a live search bar and two filter dropdowns (status and location) to make finding specific printers in large setups easier, especially on mobile where Ctrl+F is impractical. Search matches printer name, model, location, and serial number (case-insensitive, whitespace-trimmed) and has a clear button. The status filter covers All / Printing / Paused / Idle / Finished / Error / Offline and is reactive to WebSocket status updates via a React Query cache subscription — so a print finishing while "Printing" is selected immediately removes the printer from the filtered list. The location filter is only shown when at least one printer has a location configured. All three filters are combinable; the controls are hidden when no printers are configured yet; and an empty-state message appears when no printer matches the current search/filters. Fully localized across all 7 UI languages. Thanks to @legend813 for the contribution.
- **LDAP Default Fallback Group** — Settings → Authentication → LDAP → Advanced now has a "Default group" selector. When an LDAP user authenticates but is not listed in any mapped LDAP group, they are automatically assigned to this fallback group instead of being left without permissions. Previously such users could log in successfully but landed on empty pages because every permission check failed. Leave the setting empty to preserve the old behavior. A warning is logged each time the fallback is applied so administrators can spot missing group assignments.
### Changed
- **SpoolBuddy Auto-Wake on NFC/Scale** ([#945](https://github.com/maziggy/bambuddy/issues/945)) — The SpoolBuddy kiosk display now wakes automatically when a spool is placed on the scale or an NFC tag is scanned, without requiring a touch first. The daemon discovers the Wayland session from the shared runtime directory and toggles HDMI power via `wlopm`, coexisting with `swayidle` which continues to handle touch-based wake independently. Gracefully degrades when `wlopm` is not installed or no Wayland session is available. Thanks to @TravisWilder for the suggestion.
- **SpoolBuddy Kiosk LCD Now Powers Off on Idle** ([#937](https://github.com/maziggy/bambuddy/issues/937)) — The SpoolBuddy kiosk's "screen blank timeout" setting previously only painted a black CSS overlay over the browser window; the HDMI panel's backlight stayed on indefinitely, wasting power and letting OLED/LED panels burn in. The blanking path is now moved down to the OS layer: the install script installs `swayidle` and `wlopm`, and labwc's autostart launches a new watchdog (`spoolbuddy/install/spoolbuddy-idle.sh`) that queries the backend once on boot for the device's `display_blank_timeout` and hands it to `swayidle`, which powers HDMI off via `wlopm --off HDMI-A-1` after the configured idle period and powers it back on via `wlopm --on` when labwc delivers any input event (touch, keypress). The redundant CSS overlay and its pointer/keyboard listeners have been removed from `SpoolBuddyLayout` — one source of truth now. Screen blanking is opt-in: `display_blank_timeout=0` (the default) skips launching swayidle entirely and the display stays on forever, preserving current behavior for users who didn't pick a timeout. The default for users who newly enable blanking is 300 seconds. Changes made to the timeout in SpoolBuddy Settings → Display take effect on the next kiosk restart — tap Quick Menu → Restart Browser to apply without a full reboot. A new `GET /api/v1/spoolbuddy/devices/{device_id}/display` endpoint (gated on `inventory:update`, same as the existing `PUT` and heartbeat endpoints) is what the kiosk-side watchdog reads, so no new permissions are required on the device's API key. The watchdog also writes a full startup trace (env vars, resolved timeout, the exact `swayidle` command it execs) to `~/.cache/spoolbuddy-idle.log` so any future breakage on a different kiosk setup is trivially diagnosable, and auto-detects `WAYLAND_DISPLAY` from `XDG_RUNTIME_DIR` with a short retry loop in case labwc hasn't finished exporting its env by the time autostart runs. Thanks to @TravisWilder for reporting.
### Fixed
- **H2C Nozzle Rack Slot Numbering Off When Slot 1's Nozzle Is Mounted** ([#943](https://github.com/maziggy/bambuddy/issues/943)) — The H2C nozzle rack card on the Printers page rendered every rack slot shifted by one position whenever the lowest-numbered slot (rack ID 16, displayed as "slot 1") had its nozzle currently picked up into a hotend. In that state the printer firmware omits the mounted slot's ID from `device.nozzle.info` entirely instead of sending an empty placeholder, so the rack arrived with 5 entries (IDs 17..21) plus the 2 L/R hotends. The frontend was computing its rack base ID via `min(present_ids)`, which then became 17 instead of the fixed 16, and every remaining nozzle was rendered one position to the left — the nozzle physically in slot 2 appeared as "slot 1", slot 3 appeared as "slot 2", and so on, with the single empty placeholder falling off the right end as a phantom "slot 6" that should have been the actual empty "slot 1". The rack base is now hardcoded to 16 to match the fixed H2C rack ID layout (already encoded in the `test_h2c_nozzle_rack_populated_with_8_entries` backend test), so the empty slot stays anchored to its physical position regardless of which nozzle is currently in use. A frontend regression test exercises exactly this case (ID 16 missing, remaining slots in order) and asserts the rendered slot row reads `[—, 0.2, 0.6, 0.8, 1.0, 1.2]`. Thanks to @netscout2001 for reporting.
- **Energy Snapshot Capture Crashes on PostgreSQL** — With an external PostgreSQL database configured, the hourly smart-plug energy snapshot loop (introduced with the #941 fix) logged `asyncpg.DataError: invalid input for query argument $2: ... can't subtract offset-naive and offset-aware datetimes` every hour and failed to persist any snapshots, so date-filtered energy statistics in total-consumption mode stayed empty on Postgres installs. The engine already had a `before_cursor_execute` hook that strips `tzinfo` from bound datetime parameters before they reach asyncpg (the `smart_plug_energy_snapshots.recorded_at` column is `TIMESTAMP WITHOUT TIME ZONE` to match the rest of the schema), but the hook only stripped datetimes one level deep — when SQLAlchemy's `insertmanyvalues` feature batched multiple snapshot rows into a single `INSERT ... SELECT FROM (VALUES ...)` statement, parameters arrived as nested containers (lists of tuples, or a list inside an outer container) and the inner datetimes slipped through untouched. The hook now recursively walks any nesting of dict/list/tuple and strips `tzinfo` at any depth, so every parameter shape SQLAlchemy may use is handled. SQLite installs were never affected (SQLite ignores tzinfo entirely).
- **Wrong Filament Color Name Shown on Printer Tab AMS Popup** ([#857](https://github.com/maziggy/bambuddy/issues/857)) — PLA Translucent Cherry Pink (and other colors outside a small hand-maintained list) appeared as "Scarlet Red" on the Printer tab AMS slot popup, and was also auto-provisioned into the inventory under the wrong name on the first RFID read. Root cause: both the backend spool auto-provisioner and the frontend AMS popup resolved color names by looking up the Bambu `tray_id_name` code (e.g. `A17-R1`) in a hardcoded table, and when the exact code wasn't listed they fell back to a suffix-only lookup (`R1 → Scarlet Red`). The suffix half of that code is **not** globally unique across material families — `A17-R1` is PLA Translucent Cherry Pink, while `A01-R1` is PLA Matte Scarlet Red — so the fallback was structurally guaranteed to produce wrong names for any color the hand-maintained list didn't happen to cover. The resolver has been rewritten to use the existing `color_catalog` table (seeded from `catalog_defaults.py` plus the FilamentColors.xyz sync) as the single source of truth. Backend lookup is now by hex color against the catalog; the frontend fetches a compact `{hex: name}` map once per session via a new `GET /api/inventory/colors/map` endpoint (available to any authenticated user, not gated on `inventory:read`), stores it in a `ColorCatalogProvider` context, and uses it for all `getColorName()` calls. The hardcoded tables in `backend/app/core/bambu_colors.py`, `frontend/src/utils/colors.ts`, and `frontend/src/pages/PrintersPage.tsx` have been removed entirely. Existing spools that were auto-created with a wrong name before this fix need to be renamed manually — the fix only affects new auto-provisioning and live display. Thanks to @lightmaster for reporting.
- **LDAP Auto-Provisioning Fails on Upgraded SQLite Installs** ([#794](https://github.com/maziggy/bambuddy/issues/794)) — First LDAP login on an upgraded SQLite install hit `sqlite3.IntegrityError: NOT NULL constraint failed: users.password_hash` and fell through to a 500 response, because the `users` table on disk had been created before LDAP support landed with `password_hash VARCHAR(255) NOT NULL`. The model was already `nullable=True` and the migration to drop the constraint existed, but only ran on PostgreSQL — SQLite was skipped entirely because it has no `ALTER COLUMN ... DROP NOT NULL`. The migration now patches `sqlite_master` directly via `PRAGMA writable_schema` and bumps `PRAGMA schema_version` so the current connection reloads the table definition without requiring a restart. Fresh installs were never affected (they go through `Base.metadata.create_all` which uses the current nullable model). Thanks to @DylanBrass for reporting.
- **Energy Statistics Empty for Week/Month/Day in Total Consumption Mode** ([#941](https://github.com/maziggy/bambuddy/issues/941)) — With "Total consumption" selected as the energy tracking mode, the Statistics page showed the correct kWh total for All Time but zero for every time-filtered range (Today, This Week, This Month, …). The backend fell back to summing per-print archive energy whenever a date filter was active, but in total-consumption mode the per-print column was often empty for two reasons: (1) the starting-kWh value was held in an in-memory dict (`_print_energy_start`) that was lost on any backend restart mid-print, so prints that spanned a restart never got an energy delta computed; (2) historical prints from before a smart plug was added had no value at all. The fix replaces the in-memory dict with a persisted `energy_start_kwh` column on the archive row, and adds an hourly snapshot loop (`smart_plug_energy_snapshots` table) that captures each plug's lifetime counter. The `/archives/stats` endpoint now computes date-range totals via per-plug `(last-in-range − baseline)` deltas from those snapshots, clamping counter resets to zero. A warming-up flag is returned (and rendered as a tooltip next to the Energy stats on StatsPage) when the query runs on incomplete snapshot history — e.g. right after upgrade, before the hourly loop has built up a baseline before the selected range — so the "low" values during the first hours after upgrading are explained in-product rather than misread as a bug. Fully localized across all 7 UI languages. Per-print energy tracking is now restart-resilient in all modes as a side-effect. Thanks to Mike (@TheMadMike23) for reporting.
- **Virtual Printer "Synchronizing device information" Times Out in Orca** ([#927](https://github.com/maziggy/bambuddy/issues/927)) — OrcaSlicer's "Send job" flow sat on "Synchronizing device information…" until it gave up, even though the FTP upload itself worked when the user clicked "Send job anyway". The virtual printer's MQTT server gated all incoming command handling on `f"device/{self.serial}/request" in topic` — if the slicer's cached serial for the VP didn't exactly equal the VP's computed `self.serial` (which depends on model prefix + per-VP `serial_suffix`), every `get_version`, `pushall`, and `project_file` publish was silently dropped. Nothing was logged past the initial "MQTT publish to …" line, so the slicer never received a `push_status` or `get_version` response on its subscribed `device/{serial}/report` topic and hit its sync timeout. Status pushes, version responses, and project_file acknowledgments were *also* being published on `device/{self.serial}/report`, so even when the incoming check happened to pass, replies targeted a topic the slicer wasn't listening on if its serial had drifted. Both directions are now serial-adaptive: the handler accepts any authenticated publish on a `device/*/request` topic, extracts the serial the slicer is actually using from the topic, stores it per-connection, and uses it for every outgoing status report, version response, print acknowledgment, and periodic push so responses always land on the topic the slicer subscribed to. The client's serial is cleared when the connection closes and when the server stops. Regression tests cover the mismatched-serial publish path, the non-request-topic rejection path, the pushall→status_report routing, and the client-serial lifecycle.
- **External Sidebar Link Icon Not Showing** ([#878](https://github.com/maziggy/bambuddy/issues/878)) — Custom icons uploaded for external sidebar links rendered correctly in the edit dialog but were missing from the sidebar itself, and opening the icon URL directly returned `{"detail":"Valid camera stream token required..."}`. The sidebar `<img>` tag in `Layout.tsx` used a raw `/api/v1/external-links/{id}/icon` URL, but that endpoint is protected by a query-string stream token (the same mechanism used for camera streams and archive thumbnails, because `<img>` tags cannot send Authorization headers). The edit dialog already routed through `api.getExternalLinkIconUrl()`, which wraps the URL via `withStreamToken()`; the sidebar now does the same, so icons appear when auth is enabled.
- **Shortest Job First Toggle Disappears After Clicking** ([#879](https://github.com/maziggy/bambuddy/issues/879)) — The SJF toggle badge on the queue page was rendered inside the Pending Queue section header, which is only shown when there is at least one pending item and the list view is active. Clicking the toggle often coincided with the scheduler starting the only pending print, at which point the Pending section unmounted and the toggle vanished along with it — making it look like the button had disappeared after clicking. The toggle has been moved to the top of the queue page, next to the list/timeline view switcher, so it stays reachable regardless of pending-item count, active filters, or the selected view mode.
- **SpoolBuddy Update Fails in Docker with "no user exists for uid 1000/1001"** — The SpoolBuddy remote-update flow shelled out to the OpenSSH `ssh-keygen` and `ssh` binaries for keypair creation and command execution. Both binaries call `getpwuid(getuid())` at startup and abort with `No user exists for uid <N>` when the container runs under an arbitrary PUID that is not listed in `/etc/passwd` (the stock `python:3.13-slim` image only has an entry for root, so running with `user: "1000:1000"`, `"1001:1001"`, or any non-root user tripped the same error). The entire SpoolBuddy update path is now subprocess-free: keypairs are generated in-process via the `cryptography` library (already a dependency), SSH commands run through the pure-Python `asyncssh` client, and git-branch detection reads `.git/HEAD` directly instead of shelling out to `git`. asyncssh also calls `getpass.getuser()` for local `~/.ssh/config` host matching, which hit the same passwd lookup failure; the Docker image now sets `LOGNAME=bambuddy`, `USER=bambuddy`, and `HOME=/app` so `getpass.getuser()` resolves via env vars before touching the passwd database, and `asyncssh.connect()` is called with `config=[]` so it does not attempt to load `~/.ssh/config` at all. Branch detection also now looks for `.git/HEAD` in the *application root* rather than `settings.base_dir` — in Docker the data directory is a separate volume (`DATA_DIR=/app/data`) that never contains `.git`. Finally, the Docker build now bakes `.git/HEAD` into the image (`.dockerignore` allows this single 20-byte file through the context filter) so the production image knows which branch it was built from; previously the `.git` directory was excluded from the build context entirely, leaving the container with no git metadata and causing the SpoolBuddy update flow to always pull `main` on the remote device regardless of which branch Bambuddy itself was built from. Native installs behave identically — they already worked because the running user was always in `/etc/passwd` and `.git/HEAD` was readable from the project root. Regression tests assert that neither keypair creation nor command execution spawns any subprocess, and that branch detection reads from the application root even when a decoy `.git` sits inside the data dir.
- **Camera Stream "6 of 5" Reconnect Counter + ffmpeg Log Flood** ([#925](https://github.com/maziggy/bambuddy/issues/925)) — Two bugs surfaced while investigating camera reconnect behaviour. First, the camera page briefly displayed "Reconnecting attempt 6 of 5" before giving up, because the attempt counter could be incremented to the maximum while the reconnect banner was still rendering. The displayed value is now clamped to the configured maximum. Second, every failed ffmpeg spawn logged the full ~20-line ffmpeg version/configuration banner, producing hundreds of lines of noise per failed camera click (one reported click produced 555 log lines across 30 retries). A new stderr summarizer strips the ffmpeg banner before logging so only the actual error lines remain. The underlying "camera service stops accepting new connections after prolonged uptime" behaviour in the X1C firmware is still under investigation.
- **LDAP POSIX Primary Group Ignored** — LDAP authentication only looked at groups that listed the user explicitly via `memberUid` (supplementary group membership). A user's POSIX primary group — referenced by the `gidNumber` attribute on the user object and matching the `gidNumber` on a `posixGroup` — was ignored entirely, so users whose role came from their primary group landed without the expected permissions. The authenticator now also searches for `posixGroup` entries whose `gidNumber` matches the user's primary `gidNumber`, and dedupes DNs case-insensitively before resolving the group mapping (LDAP DNs are case-insensitive by spec).
- **Support Bundle Leaks Virtual Printer IP Address** — The debug support bundle included the `virtual_printer_remote_interface_ip` setting value unmasked in `support-info.json`. The setting key didn't match any of the existing sensitive-key filters, so the raw IP address was included in the bundle. Added `_ip` to the sensitive key filter so IP address settings are excluded from support bundles. Log file content was already covered by the existing IPv4 regex redaction.
- **"Build Plate Cleared" Button Unclickable After Second Print** ([#912](https://github.com/maziggy/bambuddy/issues/912)) — After completing the first queued print and confirming the plate was cleared, the "Build plate cleared — ready for next print" button became unresponsive after the second print finished. The React Query mutation's `isSuccess` state persisted from the first plate-clear confirmation, causing the component to render the static "Plate Ready" confirmation instead of the clickable button. The mutation state is now reset when the printer leaves the FINISH/FAILED state, so the button works correctly on every print cycle.
- **Spoolman Location Not Cleared When Spool Removed from AMS** ([#921](https://github.com/maziggy/bambuddy/issues/921)) — When Spoolman auto-sync was enabled and a spool was removed from an AMS slot, its location in Spoolman was never cleared, causing "double-booked" slots where multiple spools shared the same location. The auto-sync callback set locations for newly inserted spools but skipped the cleanup step that clears stale locations. The location clearing logic now runs after every auto-sync cycle. Also fixed the single-printer manual sync endpoint which didn't track synced spool IDs, risking incorrect location clearing for location-matched (non-RFID) spools.
## [0.2.3b2] - 2026-04-08
### New Features
- **Optional PostgreSQL Database Support** — Bambuddy can now use an external PostgreSQL database instead of the built-in SQLite. Set the `DATABASE_URL` environment variable (e.g., `postgresql+asyncpg://user:pass@host:5432/bambuddy`) to connect to Postgres. SQLite remains the default when no `DATABASE_URL` is set. All features work with both backends including full-text archive search (FTS5 on SQLite, tsvector+GIN on PostgreSQL), backup/restore (file copy vs pg_dump/pg_restore), health diagnostics, and cross-database restore (import a SQLite backup into PostgreSQL with automatic type conversion and FK handling).
- **Shortest Job First Queue Scheduling** ([#879](https://github.com/maziggy/bambuddy/issues/879)) — New SJF toggle badge on the queue page header. When enabled, the scheduler starts shorter print jobs before longer ones instead of FIFO order. A starvation guard ensures long jobs that get skipped once are protected from being skipped again — they move to the front of the queue on the next cycle. The queue display automatically reorders to show the scheduler's actual execution order. Print duration is cached on queue items at creation time from the 3MF metadata.
- **Auto-Print G-code Injection** ([#422](https://github.com/maziggy/bambuddy/issues/422)) — Configure custom start and end G-code snippets per printer model in Settings (Workflow tab) for bed-clearing systems like Farmloop, SwapMod, AutoClear, and Printflow 3D. When adding a print to the queue, enable "Inject G-code" to have the scheduler inject the configured snippets into the 3MF before uploading to the printer. The original file is never modified — injection creates a temporary copy for upload only.
- **External Folder Subfolder Preservation** ([#890](https://github.com/maziggy/bambuddy/issues/890)) — Scanning an external folder now mirrors the real directory structure into the file manager folder tree instead of flattening all files into the root. Subdirectories are created as child LibraryFolders with correct parent/child hierarchy, and files are assigned to their matching subfolder. Hidden directories are skipped when "Show hidden files" is disabled. Subfolders that are deleted from disk are automatically cleaned up on the next scan. Created subfolders inherit the parent's read-only and show-hidden settings.
- **LDAP Authentication** ([#794](https://github.com/maziggy/bambuddy/issues/794)) — Users can now authenticate against an LDAP/Active Directory server. Configure the LDAP server URL, bind DN, search base, and user filter in Settings > Authentication > LDAP. Supports StartTLS, LDAPS (SSL), and plaintext connections. LDAP groups can be mapped to BamBuddy groups (Administrators, Operators, Viewers) for automatic role assignment. Auto-provisioning creates BamBuddy accounts on first LDAP login when enabled. Local admin accounts remain as fallback when the LDAP server is unreachable. Password management features (change password, forgot password, admin reset) are automatically disabled for LDAP users.
- **SpoolBuddy Quick Menu** ([#893](https://github.com/maziggy/bambuddy/issues/893)) — Swipe down from the top of the SpoolBuddy display to open a quick-access control panel. Toggle printer power via smart plugs directly from the display, and manage the SpoolBuddy system with restart daemon, restart browser, reboot, and shutdown controls. All destructive actions require confirmation. The menu shows real-time smart plug state (ON/OFF) for each printer that has a linked power plug.
### Improved
- **Database Engine Info on System Page** — The System Information page now shows the active database engine (SQLite or PostgreSQL) and its version in the Database section, making it easy to verify which backend is in use.
- **Plate Number in Printer View** ([#881](https://github.com/maziggy/bambuddy/issues/881)) — Printer cards and the stream overlay now show the plate number alongside the filename when printing plate 2+ of a multi-plate 3MF file (e.g. "MyModel — Plate 3"). Single-plate prints are unchanged.
- **Printer Name in Queue for Model-Based Jobs** ([#881](https://github.com/maziggy/bambuddy/issues/881)) — Queue items assigned to a printer type ("Any P1S") now show the actual printer name once the scheduler assigns a specific printer, instead of continuing to display the generic model target while printing or in history.
- **AMS Drying Support for H2S** ([#886](https://github.com/maziggy/bambuddy/issues/886)) — Remote AMS drying and queue auto-drying now work on H2S printers with firmware 01.02.00.00 or later.
- **REST Smart Plug: Separate Power/Energy URLs and Unit Multipliers** ([#472](https://github.com/maziggy/bambuddy/issues/472)) — REST/Webhook smart plugs can now use individual URLs for power and energy data instead of requiring all values in a single status response. Each value falls back to the shared Status URL when no separate URL is configured, so existing setups work without changes. Added power and energy multipliers for unit conversion (e.g., set energy multiplier to `0.001` to convert Wh to kWh). Useful for platforms like ioBroker that expose each data point as a separate API endpoint.
### Security
- **Path Traversal in File Upload Endpoints** — Archive upload endpoints (`/upload`, `/upload-bulk`, `/{id}/source`, `/source-by-name`, `/{id}/f3d`, `/{id}/timelapse`) used the client-supplied filename directly in file paths without stripping directory components. An authenticated attacker could write files outside the intended directory via directory traversal (e.g. `../../evil.3mf`). All upload endpoints now sanitize filenames by extracting only the basename before constructing paths. Reported responsibly by Sacha Vaudey via security@bambuddy.cool.
- **Unauthenticated Bug Report Endpoints** — The bug report endpoints (`/start-logging`, `/stop-logging`, `/submit`) had no authentication, allowing anyone on the network to enable debug logging, retrieve system logs, and trigger bug report submissions with system diagnostics when authentication was enabled. All three endpoints now require authentication — `start-logging` requires `settings:update` permission, `stop-logging` and `submit` require `settings:read`. Endpoints remain open when authentication is disabled (the default). Reported responsibly by Sacha Vaudey via security@bambuddy.cool.
- **API Key Empty Printer List Grants Full Access** — An API key with an empty `printer_ids` list (`[]`) was treated identically to `null` (global access to all printers), granting full printer access instead of no access. Now `null` means global access (admin key) and `[]` means no printer access. Existing API keys with empty lists are automatically migrated to `null` on startup. Also fixed the webhook queue endpoint which used a falsy check that would bypass the filter for empty lists. Reported responsibly by Sacha Vaudey via security@bambuddy.cool.
- **Missing HTTP Security Headers** — API responses did not include standard security headers. Added a middleware that sets `X-Content-Type-Options: nosniff` (prevents MIME-sniffing), `X-Frame-Options: DENY` (prevents clickjacking via iframe embedding), and `Referrer-Policy: strict-origin-when-cross-origin` (limits URL leakage to external services) on every response. `Content-Security-Policy` was omitted because the React SPA uses inline styles extensively and a permissive CSP would provide no meaningful protection. `Strict-Transport-Security` was omitted because Bambuddy is a LAN application commonly accessed over HTTP — HSTS would lock users out. Reported responsibly by Sacha Vaudey via security@bambuddy.cool.
- **Camera Snapshot Temp Files World-Readable** — Camera snapshot and plate detection endpoints created temporary JPEG files in `/tmp` with default 0644 permissions, making them readable by any local user. Switched from `NamedTemporaryFile(delete=False)` to `mkstemp` with explicit 0600 permissions so only the application user can read them. Cleanup was already handled via `finally` blocks. Reported responsibly by Sacha Vaudey via security@bambuddy.cool.
### Fixed
- **Spool Weight Not Updated After Print** ([#839](https://github.com/maziggy/bambuddy/issues/839)) — Filament usage tracking failed silently in several scenarios: (1) when FTP download failed and a fallback archive was created without a 3MF file, the primary tracking path was skipped entirely — now falls back to matching the 3MF from the library or a previous archive of the same file; (2) external/VT tray spools were never tracked by the AMS remain% fallback because it only iterated AMS unit trays — now captures and tracks VT tray remain% deltas; (3) notifications showed "Unknown" for time and filament on fallback archives — now enriches notifications with usage tracker results and captures estimated print time from MQTT at archive creation; (4) when auto-archive was disabled, `archive_id` was None at print completion so the entire 3MF tracking path was skipped — now searches library files and previous archives by filename to find the 3MF even without an archive, and captures the AMS slot-to-tray mapping at print start so it's available at completion regardless of archive state; (5) when auto-archive was disabled but the print was dispatched by BamBuddy (queue/reprint), the on_print_start callback discarded the expected print entry and returned early — the archive was never promoted to `_active_prints`, so at completion `archive_id` and `ams_mapping` were both None, making all tracking paths fail. Now detects expected prints before the auto-archive early-return and falls through to the normal promotion path, also injecting the stored `ams_mapping` into the usage tracker session.
- **File Manager Stale UI After Deleting Folders/Files** — Deleting a folder, file, or bulk-deleting items in the file manager appeared to succeed (toast shown) but the UI didn't update until a page reload. The delete endpoints (`delete_folder`, `delete_file`, `bulk_delete`) relied on FastAPI's dependency cleanup auto-commit which runs after the response is sent — the frontend received the success response, refetched the folder/file list, but the delete hadn't been committed yet. Added explicit `db.commit()` before returning in all three endpoints.
- **Spool Manager Deducts Double the Filament Used** ([#880](https://github.com/maziggy/bambuddy/issues/880)) — After a print completed, the built-in spool manager subtracted twice the actual filament consumption. The printer's MQTT status message contains both updated AMS remain percentages and the `FINISH` state, which triggered two independent deduction paths in the same event loop cycle: the AMS weight sync (absolute SET from remain%) and the usage tracker (additive delta from 3MF data). The AMS weight sync now skips updates while a print session is active, letting the usage tracker handle deductions precisely via 3MF slicer data.
- **Thumbnails Broken After Backend Restart** — Archive and library thumbnails returned 401 Unauthorized after a backend restart because stream tokens are stored in memory and lost on restart. The frontend now detects failed token-protected image loads and automatically refreshes the stream token, so thumbnails recover without a page reload.
- **SpoolBuddy Kiosk Screen Blanks on Boot** — The touchscreen display would blank immediately after the RPi booted, requiring a touch to wake. Added `consoleblank=0` to the kernel cmdline to disable Linux console blanking during the Plymouth-to-labwc transition, and changed the `wlr-randr` anti-blank loop to fire immediately instead of sleeping 60 seconds first.
- **Queue Widget Ignores Plate-Clear Setting** ([#752](https://github.com/maziggy/bambuddy/issues/752)) — The "Clear Plate & Start Next" button on printer cards appeared even when "Require plate-clear confirmation" was disabled in Settings → Queue. The backend correctly auto-dispatched without waiting, but the frontend widget always showed the prompt. The widget now respects the setting and shows a passive queue link instead when plate-clear confirmation is disabled.
- **Ghost Jobs From SQLite Lock on Print Completion** ([#897](https://github.com/maziggy/bambuddy/issues/897)) — When a print finished, the queue status update (`printing` → `completed`) could fail silently if the SQLite database was locked by another writer (e.g. the runtime tracker). The failed commit left the job permanently stuck in `printing` status — a "ghost job" that caused the UI to show false double-assignments when the next job started. The critical queue status commit now retries up to 3 times with backoff on SQLite lock errors (PostgreSQL is unaffected — it uses row-level locking). Additionally, the runtime tracker was holding a single long transaction across all printers; it now commits per-printer to minimize lock hold time.
- **Multi-Plug Automation Only Works for First Plug** ([#903](https://github.com/maziggy/bambuddy/issues/903)) — When multiple smart plugs were assigned to the same printer (e.g. a TUYA printer plug and a particle filter plug via Home Assistant), only the first plug's automation worked. The auto-on at print start, auto-off at print completion, and queue auto-off all queried for a single plug instead of iterating all plugs linked to the printer. All automation paths now control every assigned plug. Also fixed the queue auto-off path which was hardcoded to Tasmota instead of using the correct service for the plug type (HA, MQTT, REST).
- **SpoolBuddy Inventory Not Updating on Spool Changes** — Adding, editing, deleting, archiving, or restoring a spool in the internal inventory did not update SpoolBuddy's frontend views until the next manual refresh or 30-second poll. The spool CRUD endpoints did not emit websocket events, and the SpoolBuddy Dashboard had no polling fallback. All inventory mutation endpoints now broadcast an `inventory_changed` websocket event, and the frontend invalidates the spool cache on receipt — so SpoolBuddy (and all other tabs) reflect changes instantly.
- **AMS Slot Changes Fail Until Reconnect** ([#887](https://github.com/maziggy/bambuddy/issues/887)) — After a keep-alive timeout, paho-mqtt auto-reconnects but the new session can be half-broken: the printer continues sending status updates but silently ignores commands. The developer mode probe detected this (no response, leaving `developer_mode` as `null`), but had no timeout or recovery — one unanswered probe permanently blocked retries. Added a 10-second probe timeout with one retry; after two consecutive unanswered probes, Bambuddy force-closes the socket to trigger a clean reconnect with a fresh session. Additionally, the developer mode probe was firing on every auto-reconnect, which destabilized some firmware MQTT brokers (A1/P1 series) — causing a reconnect → probe → disconnect feedback loop. The probe result is now cached across reconnects and only runs once on the first connection, with a 5-second delay after connect to let the session stabilize.
- **WebSocket Crash on Printers Without `fun` Field** ([#873](https://github.com/maziggy/bambuddy/issues/873)) — Connecting to printers that don't send the MQTT `fun` field (A1, P1 series, X1Plus firmware) caused a repeating `'str' object has no attribute 'get'` crash in the WebSocket handler, showing the printer as offline with missing AMS and SD card info. The developer mode probe introduced in 0.2.3b1 published an MQTT message inside `_update_state()` between overwriting `raw_data` with the full MQTT dict (where `vt_tray` is a raw dict) and restoring the previously normalized list — the `publish()` call released the GIL, letting the event loop read the un-normalized dict and iterate over string keys instead of spool dicts. Fixed by normalizing `vt_tray` dict→list in the MQTT data before assignment, and moving preserved field restoration before the probe. Added defensive normalization in `printer_state_to_dict` as a belt-and-suspenders guard.
## [0.2.3b1] - 2026-04-02
### New Features
- **Queue Timeline View** ([#823](https://github.com/maziggy/bambuddy/issues/823)) — The queue page now has a production schedule view showing when each print is estimated to finish. Events are sorted chronologically and grouped by hour, with cards showing the file name, printer, estimated completion time, and time remaining. Active prints show a live progress bar. Filter by "Show All", "Printing", or "Queued", and navigate between days. Click any event to edit or stop it. Toggle between List and Timeline views with the button group above the queue.
- **Staggered Batch Start for Multi-Printer Jobs** ([#752](https://github.com/maziggy/bambuddy/issues/752)) — When sending a print to multiple printers via the queue, you can now stagger the starts to avoid power spikes from simultaneous bed heating. Enable "Stagger printer starts" in the schedule options to define a group size (how many printers start at once) and interval (minutes between groups). For example, 10 printers with group size 2 and interval 5 min will start in 5 waves over 25 minutes. Default group size and interval are configurable in Settings → Queue. Works with both ASAP and Scheduled timing — ASAP starts the first group immediately, subsequent groups get computed scheduled times. The stagger option is also available in the direct Print dialog when multiple printers are selected — prints are automatically queued with staggered start times, so you can close the browser and walk away.
- **Plate-Clear Confirmation Setting** ([#752](https://github.com/maziggy/bambuddy/issues/752)) — New "Require plate-clear confirmation" toggle in Settings → Queue. When disabled, the scheduler starts queued prints automatically on printers with finished jobs without waiting for per-printer plate confirmation. Useful for farm workflows where plates are verified physically before starting a batch. Default is enabled (existing behavior preserved).
- **Settings Queue Tab** — New dedicated Queue tab in Settings consolidates queue-related settings: staggered start defaults and auto-drying configuration (moved from the Filament tab).
- **Per-User Statistics Filtering** ([#730](https://github.com/maziggy/bambuddy/issues/730)) — Admins can now filter the Statistics page by user. A user dropdown appears in the stats header for users with the new `stats:filter_by_user` permission (Administrators only by default). Filter by a specific user to see their prints, filament usage, and costs, or select "No User (System)" to view prints without user attribution (e.g. slicer-initiated or pre-auth prints). The filter applies to all stats widgets and exports.
- **Bulk Printer Actions** ([#825](https://github.com/maziggy/bambuddy/issues/825)) — Select multiple printer cards and apply bulk actions from a floating toolbar. Toggle selection mode from the header, then click cards to select. Use "Select All", "Select by State" (printing, paused, finished, idle, error, offline), or "Select by Location" to quickly pick printers. Available actions: Stop, Pause, Resume, Clear Notifications, and Clear Bed — each button is smart-enabled based on the selected printers' current states. Confirmation modals for destructive actions (Stop, Pause, Clear Bed). The status summary bar now shows all printer states (printing, paused, finished, idle, error, offline).
- **Prefer Lowest Remaining Filament** ([#805](https://github.com/maziggy/bambuddy/issues/805)) — New optional setting in Settings → Filament that prefers AMS spools with the lowest remaining filament during auto-matching. When multiple spools match the same type and color, the one with the least filament remaining is selected first. Helps consume partial spools before starting new ones. Applies to queue scheduling, print modal, and multi-printer mapping. Unknown remain values (e.g. external spools without sensors) are treated as full. Disabled by default.
- **REST/Webhook Smart Plug Type** ([#472](https://github.com/maziggy/bambuddy/issues/472)) — New "REST" smart plug type for controlling power via generic HTTP APIs. Works with any home automation platform that has an HTTP endpoint (openHAB, ioBroker, FHEM, Node-RED, etc.). Configure separate ON/OFF URLs with custom HTTP methods (GET/POST/PUT/PATCH), request bodies, and headers. Optional status polling via a GET endpoint with JSON path extraction for state, power, and energy monitoring. Fully controllable — supports auto on/off with prints, daily scheduling, sidebar quick-toggle, and power alerts.
- **Configurable Default Print Options** ([#858](https://github.com/maziggy/bambuddy/issues/858)) — Print options (bed levelling, flow calibration, vibration calibration, first layer inspection, timelapse) now have configurable defaults in Settings → Workflow. Set your preferred defaults once and every new print dialog starts with those values. Still overridable per print.
- **Batch Print Quantity** ([#342](https://github.com/maziggy/bambuddy/issues/342)) — Print multiple copies of a file in one step. The print and schedule dialogs now have a quantity field — set it to any number and the system creates that many queue items automatically. When quantity is greater than one, items are grouped into a batch for tracking. In the direct print dialog, the first copy prints immediately while the remaining copies are queued. The queue page shows a batch badge on grouped items. Batch progress and cancellation are available via the API.
- **GitHub Backup: Spool Inventory & Print Archives** ([#870](https://github.com/maziggy/bambuddy/issues/870)) — GitHub backup can now include spool inventory and print archive history as optional toggles alongside the existing K-profiles, cloud profiles, and settings. Spool backup exports all spools with their material, brand, color, weight, cost tracking, RFID tags, and full usage history. Archive backup exports print history metadata (filament, temperatures, times, costs, energy) — no gcode/3MF binary files. Both are off by default and can be enabled independently in Settings → Backup & Restore.
### Improved
- **Standardized Webhook Notification Payloads** ([#871](https:/
gitextract_t40zu6xr/ ├── .codeql/ │ ├── codeql-config.yml │ ├── javascript-bambuddy.qls │ └── python-bambuddy.qls ├── .coverage ├── .dockerignore ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── MAINTAINERS.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── workflows/ │ │ ├── ci.yml │ │ ├── cleanup-ghcr.yml │ │ ├── codeql.yml │ │ ├── issue-closed.yml │ │ ├── repo-stats.yml │ │ ├── security.yml │ │ └── stale.yml │ └── workflows.disabled/ │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .trivyignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DOCKERHUB.md ├── Dockerfile ├── Dockerfile.test ├── LICENSE ├── README.md ├── SECURITY.md ├── UPDATING.md ├── backend/ │ ├── __init__.py │ ├── app/ │ │ ├── __init__.py │ │ ├── api/ │ │ │ ├── __init__.py │ │ │ └── routes/ │ │ │ ├── __init__.py │ │ │ ├── ams_history.py │ │ │ ├── api_keys.py │ │ │ ├── archives.py │ │ │ ├── auth.py │ │ │ ├── background_dispatch.py │ │ │ ├── bug_report.py │ │ │ ├── camera.py │ │ │ ├── cloud.py │ │ │ ├── discovery.py │ │ │ ├── external_links.py │ │ │ ├── filaments.py │ │ │ ├── firmware.py │ │ │ ├── github_backup.py │ │ │ ├── groups.py │ │ │ ├── inventory.py │ │ │ ├── kprofiles.py │ │ │ ├── library.py │ │ │ ├── local_backup.py │ │ │ ├── local_presets.py │ │ │ ├── maintenance.py │ │ │ ├── metrics.py │ │ │ ├── mfa.py │ │ │ ├── notification_templates.py │ │ │ ├── notifications.py │ │ │ ├── obico.py │ │ │ ├── pending_uploads.py │ │ │ ├── print_log.py │ │ │ ├── print_queue.py │ │ │ ├── printers.py │ │ │ ├── projects.py │ │ │ ├── settings.py │ │ │ ├── smart_plugs.py │ │ │ ├── spoolbuddy.py │ │ │ ├── spoolman.py │ │ │ ├── support.py │ │ │ ├── system.py │ │ │ ├── updates.py │ │ │ ├── user_notifications.py │ │ │ ├── users.py │ │ │ ├── virtual_printers.py │ │ │ ├── webhook.py │ │ │ └── websocket.py │ │ ├── cli.py │ │ ├── core/ │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ ├── catalog_defaults.py │ │ │ ├── compat.py │ │ │ ├── config.py │ │ │ ├── database.py │ │ │ ├── db_dialect.py │ │ │ ├── encryption.py │ │ │ ├── permissions.py │ │ │ └── websocket.py │ │ ├── i18n/ │ │ │ └── __init__.py │ │ ├── main.py │ │ ├── models/ │ │ │ ├── __init__.py │ │ │ ├── active_print_spoolman.py │ │ │ ├── ams_history.py │ │ │ ├── ams_label.py │ │ │ ├── api_key.py │ │ │ ├── archive.py │ │ │ ├── auth_ephemeral.py │ │ │ ├── bug_report.py │ │ │ ├── color_catalog.py │ │ │ ├── external_link.py │ │ │ ├── filament.py │ │ │ ├── github_backup.py │ │ │ ├── group.py │ │ │ ├── kprofile_note.py │ │ │ ├── library.py │ │ │ ├── local_preset.py │ │ │ ├── maintenance.py │ │ │ ├── notification.py │ │ │ ├── notification_template.py │ │ │ ├── oidc_provider.py │ │ │ ├── orca_base_cache.py │ │ │ ├── pending_upload.py │ │ │ ├── print_batch.py │ │ │ ├── print_log.py │ │ │ ├── print_queue.py │ │ │ ├── printer.py │ │ │ ├── project.py │ │ │ ├── project_bom.py │ │ │ ├── settings.py │ │ │ ├── slot_preset.py │ │ │ ├── smart_plug.py │ │ │ ├── smart_plug_energy_snapshot.py │ │ │ ├── spool.py │ │ │ ├── spool_assignment.py │ │ │ ├── spool_catalog.py │ │ │ ├── spool_k_profile.py │ │ │ ├── spool_usage_history.py │ │ │ ├── spoolbuddy_device.py │ │ │ ├── user.py │ │ │ ├── user_email_pref.py │ │ │ ├── user_otp_code.py │ │ │ ├── user_totp.py │ │ │ └── virtual_printer.py │ │ ├── schemas/ │ │ │ ├── __init__.py │ │ │ ├── api_key.py │ │ │ ├── archive.py │ │ │ ├── auth.py │ │ │ ├── cloud.py │ │ │ ├── external_link.py │ │ │ ├── filament.py │ │ │ ├── github_backup.py │ │ │ ├── group.py │ │ │ ├── kprofile.py │ │ │ ├── library.py │ │ │ ├── local_preset.py │ │ │ ├── maintenance.py │ │ │ ├── notification.py │ │ │ ├── notification_template.py │ │ │ ├── print_log.py │ │ │ ├── print_queue.py │ │ │ ├── printer.py │ │ │ ├── project.py │ │ │ ├── settings.py │ │ │ ├── smart_plug.py │ │ │ ├── spool.py │ │ │ ├── spool_usage.py │ │ │ ├── spoolbuddy.py │ │ │ ├── timelapse.py │ │ │ └── user_notifications.py │ │ ├── services/ │ │ │ ├── __init__.py │ │ │ ├── archive.py │ │ │ ├── archive_comparison.py │ │ │ ├── background_dispatch.py │ │ │ ├── bambu_cloud.py │ │ │ ├── bambu_ftp.py │ │ │ ├── bambu_mqtt.py │ │ │ ├── bug_report.py │ │ │ ├── camera.py │ │ │ ├── discovery.py │ │ │ ├── email_service.py │ │ │ ├── export.py │ │ │ ├── external_camera.py │ │ │ ├── failure_analysis.py │ │ │ ├── firmware_check.py │ │ │ ├── firmware_update.py │ │ │ ├── github_backup.py │ │ │ ├── hms_errors.py │ │ │ ├── homeassistant.py │ │ │ ├── layer_timelapse.py │ │ │ ├── ldap_service.py │ │ │ ├── local_backup.py │ │ │ ├── mqtt_relay.py │ │ │ ├── mqtt_smart_plug.py │ │ │ ├── network_utils.py │ │ │ ├── notification_service.py │ │ │ ├── obico_actions.py │ │ │ ├── obico_detection.py │ │ │ ├── obico_smoothing.py │ │ │ ├── opentag3d.py │ │ │ ├── orca_profiles.py │ │ │ ├── plate_detection.py │ │ │ ├── print_log.py │ │ │ ├── print_scheduler.py │ │ │ ├── printer_manager.py │ │ │ ├── rest_smart_plug.py │ │ │ ├── smart_plug_manager.py │ │ │ ├── spool_assignment_notifications.py │ │ │ ├── spool_tag_matcher.py │ │ │ ├── spoolbuddy_ssh.py │ │ │ ├── spoolman.py │ │ │ ├── spoolman_tracking.py │ │ │ ├── stl_thumbnail.py │ │ │ ├── tasmota.py │ │ │ ├── timelapse_processor.py │ │ │ ├── usage_tracker.py │ │ │ └── virtual_printer/ │ │ │ ├── __init__.py │ │ │ ├── bind_server.py │ │ │ ├── certificate.py │ │ │ ├── ftp_server.py │ │ │ ├── manager.py │ │ │ ├── mqtt_server.py │ │ │ ├── ssdp_server.py │ │ │ └── tcp_proxy.py │ │ └── utils/ │ │ ├── color_utils.py │ │ ├── filament_ids.py │ │ ├── printer_models.py │ │ ├── tag_normalization.py │ │ └── threemf_tools.py │ └── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── integration/ │ │ ├── __init__.py │ │ ├── test_advanced_auth_api.py │ │ ├── test_ams_history_api.py │ │ ├── test_ams_labels_api.py │ │ ├── test_archives_api.py │ │ ├── test_auth_api.py │ │ ├── test_available_filaments.py │ │ ├── test_background_dispatch_api.py │ │ ├── test_camera_api.py │ │ ├── test_client_ip.py │ │ ├── test_cloud_auth.py │ │ ├── test_color_map_api.py │ │ ├── test_cost_statistics.py │ │ ├── test_discovery_api.py │ │ ├── test_endpoint_auth.py │ │ ├── test_external_folders_api.py │ │ ├── test_external_links_api.py │ │ ├── test_filaments_api.py │ │ ├── test_github_backup_api.py │ │ ├── test_inventory_assign.py │ │ ├── test_library_api.py │ │ ├── test_maintenance_api.py │ │ ├── test_metrics_api.py │ │ ├── test_mfa_api.py │ │ ├── test_notifications_api.py │ │ ├── test_obico_api.py │ │ ├── test_ownership_permissions.py │ │ ├── test_print_lifecycle.py │ │ ├── test_print_queue_api.py │ │ ├── test_printers_api.py │ │ ├── test_projects_api.py │ │ ├── test_security.py │ │ ├── test_settings_api.py │ │ ├── test_sjf_scheduling.py │ │ ├── test_smart_plugs_api.py │ │ ├── test_spoolbuddy.py │ │ ├── test_spoolman_api.py │ │ ├── test_support_api.py │ │ ├── test_system_api.py │ │ ├── test_updates_api.py │ │ ├── test_user_notifications_api.py │ │ └── test_virtual_printer_api.py │ ├── pytest.ini │ └── unit/ │ ├── __init__.py │ ├── services/ │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── mock_ftp_server.py │ │ ├── test_archive_copy.py │ │ ├── test_archive_service.py │ │ ├── test_background_dispatch.py │ │ ├── test_bambu_cloud.py │ │ ├── test_bambu_ftp.py │ │ ├── test_bambu_mqtt.py │ │ ├── test_camera_tls_proxy.py │ │ ├── test_email_service.py │ │ ├── test_external_camera.py │ │ ├── test_hms_errors.py │ │ ├── test_layer_timelapse.py │ │ ├── test_ldap_service.py │ │ ├── test_mqtt_smart_plug_subscribe.py │ │ ├── test_notification_service.py │ │ ├── test_plate_detection.py │ │ ├── test_printer_manager.py │ │ ├── test_rest_smart_plug.py │ │ ├── test_smart_plug_manager.py │ │ ├── test_spool_assignment_notifications.py │ │ ├── test_spool_tag_matcher.py │ │ ├── test_spoolbuddy_ssh.py │ │ ├── test_spoolman_service.py │ │ ├── test_spoolman_tracking.py │ │ ├── test_stl_thumbnail.py │ │ ├── test_tasmota.py │ │ ├── test_usage_tracker.py │ │ └── test_virtual_printer.py │ ├── test_archive_file_path_guard.py │ ├── test_archive_filtering.py │ ├── test_bed_jog.py │ ├── test_bug_report.py │ ├── test_bulk_spool_create.py │ ├── test_camera_stderr_summary.py │ ├── test_capture_pid_tracking.py │ ├── test_catalog_bulk_delete.py │ ├── test_cli.py │ ├── test_code_quality.py │ ├── test_color_utils.py │ ├── test_cost_tracking.py │ ├── test_db_dialect.py │ ├── test_energy_snapshots.py │ ├── test_firmware_versions.py │ ├── test_gcode_injection.py │ ├── test_homeassistant_settings.py │ ├── test_ldap_migration.py │ ├── test_local_backup.py │ ├── test_log_error_detection.py │ ├── test_maintenance_rod_filtering.py │ ├── test_mfa_helpers.py │ ├── test_obico_detection.py │ ├── test_obico_smoothing.py │ ├── test_opentag3d.py │ ├── test_orca_profiles.py │ ├── test_permissions.py │ ├── test_permissions_stats_filter.py │ ├── test_phantom_print_hardening.py │ ├── test_plate_object_extraction.py │ ├── test_print_log.py │ ├── test_print_speed.py │ ├── test_print_start_expected_promotion.py │ ├── test_printer_models.py │ ├── test_run_with_retry.py │ ├── test_scheduler_ams_mapping.py │ ├── test_scheduler_auto_drying.py │ ├── test_scheduler_busy_only.py │ ├── test_scheduler_clear_plate.py │ ├── test_scheduler_filament_override.py │ ├── test_scheduler_watchdog.py │ ├── test_slicer_settings.py │ ├── test_slot_preset_key.py │ ├── test_spool_schemas_rgba.py │ ├── test_spoolbuddy_system_stats.py │ ├── test_spoolman_clear_location.py │ ├── test_subtask_archive_resume.py │ ├── test_support_helpers.py │ ├── test_sync_ams_weights.py │ ├── test_threemf_tools.py │ ├── test_usage_tracker.py │ ├── test_user_notifications.py │ ├── test_vp_ftp_port.py │ └── test_vp_mqtt_server.py ├── build_docker.sh ├── deploy/ │ └── bambuddy.service ├── docker-compose.test.yml ├── docker-compose.yml ├── docker-publish-beta.sh ├── docker-publish-daily-beta.sh ├── docker-publish.sh ├── docs/ │ ├── ams_slot_printer_matrix.txt │ ├── bambu_lab_preset_sync_api.md │ └── migration-vp-ftp-port.md ├── frontend/ │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── docs/ │ │ └── create_proxy_diagram.py │ ├── eslint.config.js │ ├── index.html │ ├── mockups/ │ │ └── ams-redesign.html │ ├── package.json │ ├── postcss.config.js │ ├── public/ │ │ ├── manifest.json │ │ ├── sw-register.js │ │ └── sw.js │ ├── scripts/ │ │ └── check-i18n-parity.mjs │ ├── src/ │ │ ├── App.css │ │ ├── App.tsx │ │ ├── __tests__/ │ │ │ ├── api/ │ │ │ │ ├── client.test.ts │ │ │ │ └── githubBackupApi.test.ts │ │ │ ├── components/ │ │ │ │ ├── AMSHistoryModal.test.tsx │ │ │ │ ├── AddPrinterDiscovery.test.tsx │ │ │ │ ├── AssignSpoolModal.test.tsx │ │ │ │ ├── BackupModal.test.tsx │ │ │ │ ├── BugReportBubble.test.tsx │ │ │ │ ├── Button.test.tsx │ │ │ │ ├── Card.test.tsx │ │ │ │ ├── ConfigureAmsSlotModal.test.tsx │ │ │ │ ├── ConfirmModal.test.tsx │ │ │ │ ├── ContextMenu.test.tsx │ │ │ │ ├── Dashboard.test.tsx │ │ │ │ ├── EditArchiveModal.test.tsx │ │ │ │ ├── FailureDetectionSettings.test.tsx │ │ │ │ ├── FilamentHoverCard.test.tsx │ │ │ │ ├── FilamentOverride.test.tsx │ │ │ │ ├── FilamentSlotCircle.test.tsx │ │ │ │ ├── FileManagerModal.test.tsx │ │ │ │ ├── FileUploadModal.test.tsx │ │ │ │ ├── GitHubBackupSettings.scheduled.test.tsx │ │ │ │ ├── HMSErrorModal.test.tsx │ │ │ │ ├── Layout.test.tsx │ │ │ │ ├── LinkSpoolModal.test.tsx │ │ │ │ ├── LocalProfilesView.test.tsx │ │ │ │ ├── ModelViewerModal.test.tsx │ │ │ │ ├── NotificationProviderCard.test.tsx │ │ │ │ ├── PrintModal.test.tsx │ │ │ │ ├── PrintModalDispatchToast.test.tsx │ │ │ │ ├── PrinterQueueWidget.test.tsx │ │ │ │ ├── PrinterSelector.test.ts │ │ │ │ ├── RestoreModal.test.tsx │ │ │ │ ├── SmartPlugCard.test.tsx │ │ │ │ ├── SpoolBuddySettings.test.tsx │ │ │ │ ├── SpoolFormBulk.test.tsx │ │ │ │ ├── SpoolFormModal.test.tsx │ │ │ │ ├── SpoolInfoCard.test.tsx │ │ │ │ ├── SpoolmanSettings.test.tsx │ │ │ │ ├── TagDetectedModal.test.tsx │ │ │ │ ├── TagManagementModal.test.tsx │ │ │ │ ├── Toggle.test.tsx │ │ │ │ ├── UploadModal.test.tsx │ │ │ │ ├── VirtualPrinterCard.test.tsx │ │ │ │ ├── VirtualPrinterSettings.test.tsx │ │ │ │ ├── WeightDisplay.test.tsx │ │ │ │ ├── spool-form/ │ │ │ │ │ └── ColorSectionHexInput.test.tsx │ │ │ │ └── spoolbuddy/ │ │ │ │ ├── AmsUnitCard.test.tsx │ │ │ │ ├── SpoolBuddyBottomNav.test.tsx │ │ │ │ ├── SpoolBuddyLayout.test.tsx │ │ │ │ ├── SpoolBuddyQuickMenu.test.tsx │ │ │ │ ├── SpoolBuddyStatusBar.test.tsx │ │ │ │ ├── SpoolBuddyTopBar.test.tsx │ │ │ │ └── SpoolIcon.test.tsx │ │ │ ├── contexts/ │ │ │ │ ├── AuthContext.test.tsx │ │ │ │ ├── ColorCatalogContext.test.tsx │ │ │ │ └── ToastContext.test.tsx │ │ │ ├── hooks/ │ │ │ │ ├── useCameraStreamToken.test.ts │ │ │ │ ├── useFilamentMapping.test.ts │ │ │ │ ├── useIsMobile.test.ts │ │ │ │ ├── useLongPress.test.ts │ │ │ │ ├── useSpoolBuddyState.test.ts │ │ │ │ └── useWebSocket.test.ts │ │ │ ├── i18n/ │ │ │ │ ├── locales.test.ts │ │ │ │ └── parity-script.test.ts │ │ │ ├── mocks/ │ │ │ │ ├── handlers.ts │ │ │ │ └── server.ts │ │ │ ├── pages/ │ │ │ │ ├── ArchivesPage.test.tsx │ │ │ │ ├── CameraPage.test.tsx │ │ │ │ ├── FileManagerExternalFolder.test.tsx │ │ │ │ ├── FileManagerPage.test.tsx │ │ │ │ ├── GroupEditPage.test.tsx │ │ │ │ ├── InventoryPageGrouping.test.ts │ │ │ │ ├── InventoryPageLowStock.test.tsx │ │ │ │ ├── LoginPage.test.tsx │ │ │ │ ├── MaintenancePage.test.tsx │ │ │ │ ├── NotificationsPage.test.tsx │ │ │ │ ├── PrintersPage.test.tsx │ │ │ │ ├── PrintersPageDrying.test.ts │ │ │ │ ├── PrintersPageFillLevel.test.ts │ │ │ │ ├── PrintersPageFormatPrintName.test.ts │ │ │ │ ├── PrintersPageSpeed.test.tsx │ │ │ │ ├── ProjectDetailPage.test.tsx │ │ │ │ ├── ProjectsPage.test.tsx │ │ │ │ ├── QueuePage.test.tsx │ │ │ │ ├── SettingsPage.test.tsx │ │ │ │ ├── SpoolBuddyAmsPageLogic.test.ts │ │ │ │ ├── SpoolBuddyCalibrationPage.test.tsx │ │ │ │ ├── SpoolBuddyDashboard.test.tsx │ │ │ │ ├── SpoolBuddySettingsPage.test.tsx │ │ │ │ ├── SpoolBuddyWriteTagPage.test.tsx │ │ │ │ ├── StatsPage.test.tsx │ │ │ │ ├── StreamOverlayPage.test.tsx │ │ │ │ └── SystemInfoPage.test.tsx │ │ │ ├── setup.ts │ │ │ ├── utils/ │ │ │ │ ├── colors.test.ts │ │ │ │ ├── currency.test.ts │ │ │ │ ├── date.test.ts │ │ │ │ ├── file.test.ts │ │ │ │ ├── firmwareVersion.test.ts │ │ │ │ ├── getSpoolmanFillLevel.test.ts │ │ │ │ ├── maintenanceWikiUrls.test.ts │ │ │ │ ├── printer.test.ts │ │ │ │ └── slicer.test.ts │ │ │ └── utils.tsx │ │ ├── api/ │ │ │ └── client.ts │ │ ├── components/ │ │ │ ├── AMSHistoryModal.tsx │ │ │ ├── APIBrowser.tsx │ │ │ ├── AddExternalLinkModal.tsx │ │ │ ├── AddNotificationModal.tsx │ │ │ ├── AddSmartPlugModal.tsx │ │ │ ├── AssignSpoolModal.tsx │ │ │ ├── BackupModal.tsx │ │ │ ├── BatchProjectModal.tsx │ │ │ ├── BatchTagModal.tsx │ │ │ ├── BugReportBubble.tsx │ │ │ ├── BulkPrinterToolbar.tsx │ │ │ ├── Button.tsx │ │ │ ├── CalendarView.tsx │ │ │ ├── Card.tsx │ │ │ ├── Collapsible.tsx │ │ │ ├── ColorCatalogSettings.tsx │ │ │ ├── ColumnConfigModal.tsx │ │ │ ├── CompactHistoryRow.tsx │ │ │ ├── CompareArchivesModal.tsx │ │ │ ├── ConfigureAmsSlotModal.tsx │ │ │ ├── ConfirmModal.tsx │ │ │ ├── ContextMenu.tsx │ │ │ ├── CreateUserAdvancedAuthModal.tsx │ │ │ ├── Dashboard.tsx │ │ │ ├── EditArchiveModal.tsx │ │ │ ├── EmailSettings.tsx │ │ │ ├── EmbeddedCameraViewer.tsx │ │ │ ├── ExternalLinksSettings.tsx │ │ │ ├── FailureDetectionSettings.tsx │ │ │ ├── FilamentHoverCard.tsx │ │ │ ├── FilamentSlotCircle.tsx │ │ │ ├── FilamentTrends.tsx │ │ │ ├── FileManagerModal.tsx │ │ │ ├── FileUploadModal.tsx │ │ │ ├── GcodeViewer.tsx │ │ │ ├── GitHubBackupSettings.tsx │ │ │ ├── HMSErrorModal.tsx │ │ │ ├── IconPicker.tsx │ │ │ ├── KProfilesView.tsx │ │ │ ├── KeyboardShortcutsModal.tsx │ │ │ ├── LDAPSettings.tsx │ │ │ ├── Layout.tsx │ │ │ ├── LinkSpoolModal.tsx │ │ │ ├── LocalProfilesView.tsx │ │ │ ├── LogViewer.tsx │ │ │ ├── MQTTDebugModal.tsx │ │ │ ├── MetricToggle.tsx │ │ │ ├── ModelViewer.tsx │ │ │ ├── ModelViewerModal.tsx │ │ │ ├── NotificationLogViewer.tsx │ │ │ ├── NotificationProviderCard.tsx │ │ │ ├── NotificationTemplateEditor.tsx │ │ │ ├── OIDCProviderSettings.tsx │ │ │ ├── PendingUploadsPanel.tsx │ │ │ ├── PhotoGalleryModal.tsx │ │ │ ├── PrintCalendar.tsx │ │ │ ├── PrintModal/ │ │ │ │ ├── FilamentMapping.tsx │ │ │ │ ├── FilamentOverride.tsx │ │ │ │ ├── PlateSelector.tsx │ │ │ │ ├── PrintOptions.tsx │ │ │ │ ├── PrinterSelector.tsx │ │ │ │ ├── ScheduleOptions.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── types.ts │ │ │ ├── PrinterInfoModal.tsx │ │ │ ├── PrinterQueueWidget.tsx │ │ │ ├── ProjectPageModal.tsx │ │ │ ├── QRCodeModal.tsx │ │ │ ├── QueueStatsBar.tsx │ │ │ ├── QueueTimelineView.tsx │ │ │ ├── RestoreModal.tsx │ │ │ ├── RichTextEditor.tsx │ │ │ ├── SkipObjectsModal.tsx │ │ │ ├── SmartPlugCard.tsx │ │ │ ├── SpoolBuddySettings.tsx │ │ │ ├── SpoolCatalogSettings.tsx │ │ │ ├── SpoolFormModal.tsx │ │ │ ├── SpoolUsageHistory.tsx │ │ │ ├── SpoolmanSettings.tsx │ │ │ ├── SwitchbarPopover.tsx │ │ │ ├── TagManagementModal.tsx │ │ │ ├── TimelapseEditorModal.tsx │ │ │ ├── TimelapseViewer.tsx │ │ │ ├── Toggle.tsx │ │ │ ├── TwoFactorSettings.tsx │ │ │ ├── UploadModal.tsx │ │ │ ├── VirtualKeyboard.css │ │ │ ├── VirtualKeyboard.tsx │ │ │ ├── VirtualPrinterAddDialog.tsx │ │ │ ├── VirtualPrinterCard.tsx │ │ │ ├── VirtualPrinterList.tsx │ │ │ ├── VirtualPrinterSettings.tsx │ │ │ ├── icons/ │ │ │ │ ├── ChamberLight.tsx │ │ │ │ ├── PlateClearedIcon.tsx │ │ │ │ └── WifiSignal.tsx │ │ │ ├── spool-form/ │ │ │ │ ├── AdditionalSection.tsx │ │ │ │ ├── ColorSection.tsx │ │ │ │ ├── FilamentSection.tsx │ │ │ │ ├── PAProfileSection.tsx │ │ │ │ ├── constants.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ └── spoolbuddy/ │ │ │ ├── AmsUnitCard.tsx │ │ │ ├── AssignToAmsModal.tsx │ │ │ ├── DiagnosticModal.tsx │ │ │ ├── InventorySpoolInfoCard.tsx │ │ │ ├── LinkSpoolModal.tsx │ │ │ ├── SpoolBuddyBottomNav.tsx │ │ │ ├── SpoolBuddyLayout.tsx │ │ │ ├── SpoolBuddyQuickMenu.tsx │ │ │ ├── SpoolBuddyStatusBar.tsx │ │ │ ├── SpoolBuddyTopBar.tsx │ │ │ ├── SpoolIcon.tsx │ │ │ ├── SpoolInfoCard.tsx │ │ │ ├── TagDetectedModal.tsx │ │ │ └── WeightDisplay.tsx │ │ ├── contexts/ │ │ │ ├── AuthContext.tsx │ │ │ ├── ColorCatalogContext.tsx │ │ │ ├── ThemeContext.tsx │ │ │ └── ToastContext.tsx │ │ ├── hooks/ │ │ │ ├── useCameraStreamToken.ts │ │ │ ├── useColorCatalogVersion.ts │ │ │ ├── useFilamentMapping.ts │ │ │ ├── useIsMobile.ts │ │ │ ├── useIsSidebarCompact.ts │ │ │ ├── useLongPress.ts │ │ │ ├── useMultiPrinterFilamentMapping.ts │ │ │ ├── useSpoolBuddyState.ts │ │ │ └── useWebSocket.ts │ │ ├── i18n/ │ │ │ ├── index.ts │ │ │ └── locales/ │ │ │ ├── de.ts │ │ │ ├── en.ts │ │ │ ├── fr.ts │ │ │ ├── it.ts │ │ │ ├── ja.ts │ │ │ ├── pt-BR.ts │ │ │ ├── zh-CN.ts │ │ │ └── zh-TW.ts │ │ ├── index.css │ │ ├── lib/ │ │ │ └── settingsSearch.ts │ │ ├── main.tsx │ │ ├── pages/ │ │ │ ├── ArchivesPage.tsx │ │ │ ├── CameraPage.tsx │ │ │ ├── ExternalLinkPage.tsx │ │ │ ├── FileManagerPage.tsx │ │ │ ├── GroupEditPage.tsx │ │ │ ├── InventoryPage.tsx │ │ │ ├── LoginPage.tsx │ │ │ ├── MaintenancePage.tsx │ │ │ ├── NotificationsPage.tsx │ │ │ ├── PrintersPage.tsx │ │ │ ├── ProfilesPage.tsx │ │ │ ├── ProjectDetailPage.tsx │ │ │ ├── ProjectsPage.tsx │ │ │ ├── QueuePage.tsx │ │ │ ├── SettingsPage.tsx │ │ │ ├── SetupPage.tsx │ │ │ ├── StatsPage.tsx │ │ │ ├── StreamOverlayPage.tsx │ │ │ ├── SystemInfoPage.tsx │ │ │ ├── UsersPage.tsx │ │ │ └── spoolbuddy/ │ │ │ ├── SpoolBuddyAmsPage.tsx │ │ │ ├── SpoolBuddyCalibrationPage.tsx │ │ │ ├── SpoolBuddyDashboard.tsx │ │ │ ├── SpoolBuddyInventoryPage.tsx │ │ │ ├── SpoolBuddySettingsPage.tsx │ │ │ └── SpoolBuddyWriteTagPage.tsx │ │ ├── types/ │ │ │ └── plates.ts │ │ └── utils/ │ │ ├── amsHelpers.ts │ │ ├── colors.ts │ │ ├── currency.ts │ │ ├── date.ts │ │ ├── file.ts │ │ ├── firmwareVersion.ts │ │ ├── maintenanceWikiUrls.ts │ │ ├── printName.ts │ │ ├── printer.ts │ │ ├── slicer.ts │ │ └── weight.ts │ ├── tailwind.config.js │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.config.ts ├── install/ │ ├── README.md │ ├── docker-install.sh │ ├── install.sh │ ├── update.sh │ └── update_macos.sh ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── scripts/ │ ├── debug_preset.py │ ├── import_spoolman.py │ ├── mqtt_sniffer.py │ ├── update_archive_date.py │ └── update_archive_quantities.py ├── spoolbuddy/ │ ├── README.md │ ├── daemon/ │ │ ├── __init__.py │ │ ├── api_client.py │ │ ├── config.py │ │ ├── display_control.py │ │ ├── main.py │ │ ├── nau7802.py │ │ ├── nfc_reader.py │ │ ├── pn5180.py │ │ ├── scale_reader.py │ │ ├── system_stats.py │ │ ├── systemd/ │ │ │ └── spoolbuddy.service │ │ └── tag_parser.py │ ├── install/ │ │ ├── generate_splash.py │ │ ├── install.sh │ │ └── spoolbuddy-idle.sh │ ├── scripts/ │ │ ├── pn5180_diag.py │ │ ├── read_tag.py │ │ └── scale_diag.py │ └── tests/ │ ├── __init__.py │ ├── test_api_client.py │ ├── test_config.py │ ├── test_display_control.py │ ├── test_main.py │ └── test_tag_parser.py ├── static/ │ ├── assets/ │ │ ├── index-BoxU3Y8Y.css │ │ └── index-NbcE7Ots.js │ ├── index.html │ ├── manifest.json │ ├── sw-register.js │ └── sw.js ├── test_all.sh ├── test_backend.sh ├── test_docker.sh ├── test_frontend.sh ├── test_security.sh ├── tests/ │ ├── e2e_comprehensive_test.py │ └── e2e_toggle_persistence_test.py └── update_website_wiki.sh
Showing preview only (1,368K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (13751 symbols across 535 files)
FILE: backend/app/api/routes/ams_history.py
class AMSHistoryPoint (line 19) | class AMSHistoryPoint(BaseModel):
class AMSHistoryResponse (line 26) | class AMSHistoryResponse(BaseModel):
function get_ams_history (line 39) | async def get_ams_history(
function delete_old_history (line 104) | async def delete_old_history(
FILE: backend/app/api/routes/api_keys.py
function list_api_keys (line 25) | async def list_api_keys(
function create_api_key (line 35) | async def create_api_key(
function get_api_key (line 80) | async def get_api_key(
function update_api_key (line 96) | async def update_api_key(
function delete_api_key (line 132) | async def delete_api_key(
FILE: backend/app/api/routes/archives.py
function _safe_filename (line 36) | def _safe_filename(filename: str) -> str:
function _validate_user_filter_permission (line 45) | def _validate_user_filter_permission(current_user: User | None, created_...
function _apply_user_filter (line 55) | def _apply_user_filter(conditions: list, created_by_id: int | None):
function compute_time_accuracy (line 64) | def compute_time_accuracy(archive: PrintArchive) -> dict:
function archive_to_response (line 91) | def archive_to_response(
function list_archives (line 157) | async def list_archives(
function list_archives_slim (line 275) | async def list_archives_slim(
function search_archives (line 350) | async def search_archives(
function rebuild_search_index (line 458) | async def rebuild_search_index(
function analyze_failures (line 500) | async def analyze_failures(
function compare_archives (line 535) | async def compare_archives(
function export_archives (line 569) | async def export_archives(
function export_stats (line 636) | async def export_stats(
function get_archive_stats (line 675) | async def get_archive_stats(
function _sum_live_plug_totals (line 843) | async def _sum_live_plug_totals(db: AsyncSession) -> float:
function _sum_snapshot_deltas (line 886) | async def _sum_snapshot_deltas(
function get_all_tags (line 964) | async def get_all_tags(
function rename_tag (line 993) | async def rename_tag(
function delete_tag (line 1040) | async def delete_tag(
function get_archive (line 1069) | async def get_archive(
function find_similar_archives (line 1092) | async def find_similar_archives(
function update_archive (line 1115) | async def update_archive(
function toggle_favorite (line 1162) | async def toggle_favorite(
function rescan_archive (line 1180) | async def rescan_archive(
function recalculate_all_costs (line 1259) | async def recalculate_all_costs(
function rescan_all_archives (line 1316) | async def rescan_all_archives(
function get_archive_duplicates (line 1366) | async def get_archive_duplicates(
function backfill_content_hashes (line 1388) | async def backfill_content_hashes(
function delete_archive (line 1417) | async def delete_archive(
function download_archive (line 1448) | async def download_archive(
function download_archive_with_filename (line 1476) | async def download_archive_with_filename(
function create_archive_slicer_token (line 1500) | async def create_archive_slicer_token(
function download_archive_for_slicer (line 1522) | async def download_archive_for_slicer(
function get_thumbnail (line 1556) | async def get_thumbnail(
function get_timelapse (line 1588) | async def get_timelapse(
function delete_timelapse (line 1626) | async def delete_timelapse(
function scan_timelapse (line 1653) | async def scan_timelapse(
function select_timelapse (line 1885) | async def select_timelapse(
function upload_timelapse (line 1972) | async def upload_timelapse(
function get_timelapse_info (line 1998) | async def get_timelapse_info(
function get_timelapse_thumbnails (line 2026) | async def get_timelapse_thumbnails(
function process_timelapse (line 2062) | async def process_timelapse(
function upload_photo (line 2177) | async def upload_photo(
function get_photo (line 2220) | async def get_photo(
function delete_photo (line 2255) | async def delete_photo(
function get_qrcode (line 2291) | async def get_qrcode(
function get_archive_capabilities (line 2349) | async def get_archive_capabilities(
function get_gcode (line 2569) | async def get_gcode(
function get_plate_preview (line 2606) | async def get_plate_preview(
function upload_archive (line 2675) | async def upload_archive(
function upload_archives_bulk (line 2711) | async def upload_archives_bulk(
function get_archive_plates (line 2767) | async def get_archive_plates(
function get_plate_thumbnail (line 3040) | async def get_plate_thumbnail(
function get_filament_requirements (line 3072) | async def get_filament_requirements(
function reprint_archive (line 3199) | async def reprint_archive(
function get_project_page (line 3300) | async def get_project_page(
function update_project_page (line 3325) | async def update_project_page(
function get_project_image (line 3355) | async def get_project_image(
function upload_source_3mf (line 3396) | async def upload_source_3mf(
function download_source_3mf (line 3444) | async def download_source_3mf(
function download_source_3mf_for_slicer (line 3473) | async def download_source_3mf_for_slicer(
function create_source_slicer_token (line 3500) | async def create_source_slicer_token(
function download_source_3mf_for_slicer_with_token (line 3520) | async def download_source_3mf_for_slicer_with_token(
function upload_source_3mf_by_name (line 3556) | async def upload_source_3mf_by_name(
function delete_source_3mf (line 3646) | async def delete_source_3mf(
function upload_f3d (line 3678) | async def upload_f3d(
function download_f3d (line 3726) | async def download_f3d(
function delete_f3d (line 3755) | async def delete_f3d(
FILE: backend/app/api/routes/auth.py
function _user_to_response (line 67) | def _user_to_response(user: User) -> UserResponse:
function _api_key_to_user_response (line 83) | def _api_key_to_user_response(api_key) -> UserResponse:
function _get_client_ip (line 108) | def _get_client_ip(request: Request) -> str:
function is_auth_enabled (line 145) | async def is_auth_enabled(db: AsyncSession) -> bool:
function is_advanced_auth_enabled (line 154) | async def is_advanced_auth_enabled(db: AsyncSession) -> bool:
function set_advanced_auth_enabled (line 163) | async def set_advanced_auth_enabled(db: AsyncSession, enabled: bool) -> ...
function set_auth_enabled (line 170) | async def set_auth_enabled(db: AsyncSession, enabled: bool) -> None:
function is_setup_completed (line 178) | async def is_setup_completed(db: AsyncSession) -> bool:
function set_setup_completed (line 185) | async def set_setup_completed(db: AsyncSession, completed: bool) -> None:
function setup_auth (line 194) | async def setup_auth(request: SetupRequest, db: AsyncSession = Depends(g...
function get_auth_status (line 290) | async def get_auth_status(db: AsyncSession = Depends(get_db)):
function disable_auth (line 300) | async def disable_auth(
function login (line 335) | async def login(raw_request: Request, request: LoginRequest, response: R...
function get_current_user_info (line 492) | async def get_current_user_info(
function logout (line 579) | async def logout(
function test_smtp_connection (line 624) | async def test_smtp_connection(
function get_smtp_config (line 656) | async def get_smtp_config(
function save_smtp_config (line 669) | async def save_smtp_config(
function enable_advanced_auth (line 694) | async def enable_advanced_auth(
function disable_advanced_auth (line 739) | async def disable_advanced_auth(
function get_advanced_auth_status (line 773) | async def get_advanced_auth_status(db: AsyncSession = Depends(get_db)):
function _send_reset_email_or_delete_token (line 793) | async def _send_reset_email_or_delete_token(
function forgot_password (line 833) | async def forgot_password(
function forgot_password_confirm (line 963) | async def forgot_password_confirm(request: ForgotPasswordConfirmRequest,...
function reset_user_password (line 1005) | async def reset_user_password(
function _get_ldap_settings (line 1112) | async def _get_ldap_settings(db: AsyncSession) -> dict[str, str] | None:
function _provision_ldap_user (line 1134) | async def _provision_ldap_user(db: AsyncSession, ldap_user, ldap_config)...
function _sync_ldap_user (line 1172) | async def _sync_ldap_user(db: AsyncSession, user: User, ldap_user, ldap_...
function test_ldap (line 1215) | async def test_ldap(
function get_ldap_status (line 1258) | async def get_ldap_status(db: AsyncSession = Depends(get_db)):
FILE: backend/app/api/routes/background_dispatch.py
function cancel_dispatch_job (line 12) | async def cancel_dispatch_job(
FILE: backend/app/api/routes/bug_report.py
class BugReportRequest (line 26) | class BugReportRequest(BaseModel):
class BugReportResponse (line 34) | class BugReportResponse(BaseModel):
class StartLoggingResponse (line 41) | class StartLoggingResponse(BaseModel):
class StopLoggingResponse (line 46) | class StopLoggingResponse(BaseModel):
function start_logging (line 51) | async def start_logging(
function stop_logging (line 74) | async def stop_logging(
function submit_bug_report (line 91) | async def submit_bug_report(
FILE: backend/app/api/routes/camera.py
function get_buffered_frame (line 68) | def get_buffered_frame(printer_id: int) -> bytes | None:
function get_printer_or_404 (line 76) | async def get_printer_or_404(printer_id: int, db: AsyncSession) -> Printer:
function generate_chamber_mjpeg_stream (line 85) | async def generate_chamber_mjpeg_stream(
function _terminate_ffmpeg (line 185) | async def _terminate_ffmpeg(process: asyncio.subprocess.Process, stream_...
function _summarize_ffmpeg_stderr (line 204) | def _summarize_ffmpeg_stderr(text: str | None) -> str:
function _read_ffmpeg_stderr (line 231) | async def _read_ffmpeg_stderr(process: asyncio.subprocess.Process) -> st...
function generate_rtsp_mjpeg_stream (line 255) | async def generate_rtsp_mjpeg_stream(
function create_stream_token (line 520) | async def create_stream_token(
function camera_stream (line 532) | async def camera_stream(
function stop_camera_stream (line 703) | async def stop_camera_stream(
function camera_snapshot (line 771) | async def camera_snapshot(
function test_camera (line 846) | async def test_camera(
function camera_status (line 867) | async def camera_status(
function test_external_camera (line 932) | async def test_external_camera(
function check_plate_empty (line 958) | async def check_plate_empty(
function calibrate_plate_detection (line 1067) | async def calibrate_plate_detection(
function delete_plate_calibration (line 1132) | async def delete_plate_calibration(
function get_plate_detection_status (line 1173) | async def get_plate_detection_status(
function get_plate_references (line 1218) | async def get_plate_references(
function get_reference_thumbnail (line 1251) | async def get_reference_thumbnail(
function update_reference_label (line 1281) | async def update_reference_label(
function delete_reference (line 1307) | async def delete_reference(
function _scan_bambu_ffmpeg_pids (line 1331) | def _scan_bambu_ffmpeg_pids() -> list[int]:
function cleanup_orphaned_streams (line 1357) | async def cleanup_orphaned_streams():
FILE: backend/app/api/routes/cloud.py
function _normalise_region (line 54) | def _normalise_region(region: str | None) -> str:
function get_stored_token (line 59) | async def get_stored_token(db: AsyncSession, user: User | None = None) -...
function store_token (line 82) | async def store_token(db: AsyncSession, token: str, email: str, region: ...
function clear_token (line 111) | async def clear_token(db: AsyncSession, user: User | None = None) -> None:
function build_authenticated_cloud (line 135) | async def build_authenticated_cloud(db: AsyncSession, user: User | None)...
function get_auth_status (line 150) | async def get_auth_status(
function login (line 179) | async def login(
function verify_code (line 219) | async def verify_code(
function set_token (line 264) | async def set_token(
function logout (line 291) | async def logout(
function get_slicer_settings (line 301) | async def get_slicer_settings(
function get_setting_detail (line 372) | async def get_setting_detail(
function get_filament_presets (line 399) | async def get_filament_presets(
function _enrich_from_local_presets (line 511) | async def _enrich_from_local_presets(
function get_filament_info (line 594) | async def get_filament_info(
function get_devices (line 674) | async def get_devices(
function get_firmware_updates (line 711) | async def get_firmware_updates(
function create_setting (line 786) | async def create_setting(
function update_setting (line 822) | async def update_setting(
function delete_setting (line 854) | async def delete_setting(
function _load_fields (line 890) | def _load_fields(preset_type: str) -> dict:
function get_builtin_filaments (line 919) | async def get_builtin_filaments(
function get_filament_id_map (line 938) | async def get_filament_id_map(
function get_preset_fields (line 993) | async def get_preset_fields(
function get_all_preset_fields (line 1015) | async def get_all_preset_fields(
FILE: backend/app/api/routes/discovery.py
class DiscoveryStatus (line 27) | class DiscoveryStatus(BaseModel):
class DiscoveryInfo (line 33) | class DiscoveryInfo(BaseModel):
class SubnetScanRequest (line 42) | class SubnetScanRequest(BaseModel):
class SubnetScanStatus (line 49) | class SubnetScanStatus(BaseModel):
class DiscoveredPrinterResponse (line 57) | class DiscoveredPrinterResponse(BaseModel):
function get_discovery_info (line 68) | async def get_discovery_info(
function get_discovery_status (line 82) | async def get_discovery_status(
function start_discovery (line 90) | async def start_discovery(
function stop_discovery (line 104) | async def stop_discovery(
function get_discovered_printers (line 113) | async def get_discovered_printers(
function start_subnet_scan (line 145) | async def start_subnet_scan(
function get_scan_status (line 171) | async def get_scan_status(
function stop_subnet_scan (line 184) | async def stop_subnet_scan(
FILE: backend/app/api/routes/external_links.py
function list_external_links (line 35) | async def list_external_links(
function create_external_link (line 46) | async def create_external_link(
function get_external_link (line 74) | async def get_external_link(
function update_external_link (line 90) | async def update_external_link(
function delete_external_link (line 117) | async def delete_external_link(
function reorder_external_links (line 139) | async def reorder_external_links(
function upload_icon (line 164) | async def upload_icon(
function delete_icon (line 214) | async def delete_icon(
function get_icon (line 239) | async def get_icon(
FILE: backend/app/api/routes/filaments.py
function list_filaments (line 21) | async def list_filaments(
function create_filament (line 31) | async def create_filament(
function get_filament (line 45) | async def get_filament(
function update_filament (line 59) | async def update_filament(
function delete_filament (line 80) | async def delete_filament(
function calculate_cost (line 97) | async def calculate_cost(
function get_filaments_by_type (line 121) | async def get_filaments_by_type(
function seed_default_filaments (line 132) | async def seed_default_filaments(
FILE: backend/app/api/routes/firmware.py
class AvailableFirmwareVersion (line 33) | class AvailableFirmwareVersion(BaseModel):
class FirmwareUpdateInfo (line 43) | class FirmwareUpdateInfo(BaseModel):
class FirmwareUpdatesResponse (line 57) | class FirmwareUpdatesResponse(BaseModel):
class LatestFirmwareInfo (line 64) | class LatestFirmwareInfo(BaseModel):
function check_firmware_updates (line 74) | async def check_firmware_updates(
function check_printer_firmware (line 128) | async def check_printer_firmware(
function get_all_latest_firmware (line 169) | async def get_all_latest_firmware(
class FirmwareUploadPrepareResponse (line 197) | class FirmwareUploadPrepareResponse(BaseModel):
class FirmwareUploadStatusResponse (line 213) | class FirmwareUploadStatusResponse(BaseModel):
class FirmwareUploadStartResponse (line 224) | class FirmwareUploadStartResponse(BaseModel):
function prepare_firmware_upload (line 232) | async def prepare_firmware_upload(
function start_firmware_upload (line 255) | async def start_firmware_upload(
function get_firmware_upload_status (line 307) | async def get_firmware_upload_status(
FILE: backend/app/api/routes/github_backup.py
function _config_to_response (line 30) | def _config_to_response(config: GitHubBackupConfig) -> dict:
function get_config (line 56) | async def get_config(
function save_config (line 71) | async def save_config(
function update_config (line 134) | async def update_config(
function delete_config (line 170) | async def delete_config(
function test_connection (line 190) | async def test_connection(
function test_stored_connection (line 201) | async def test_stored_connection(
function trigger_backup (line 220) | async def trigger_backup(
function get_status (line 240) | async def get_status(
function get_logs (line 271) | async def get_logs(
function clear_logs (line 310) | async def clear_logs(
FILE: backend/app/api/routes/groups.py
function _permission_label (line 31) | def _permission_label(perm: Permission) -> str:
function list_permissions (line 44) | async def list_permissions(
function list_groups (line 64) | async def list_groups(
function create_group (line 88) | async def create_group(
function get_group (line 133) | async def get_group(
function update_group (line 161) | async def update_group(
function delete_group (line 221) | async def delete_group(
function add_user_to_group (line 246) | async def add_user_to_group(
function remove_user_from_group (line 283) | async def remove_user_from_group(
FILE: backend/app/api/routes/inventory.py
class CatalogEntryResponse (line 64) | class CatalogEntryResponse(BaseModel):
class Config (line 70) | class Config:
class CatalogEntryCreate (line 74) | class CatalogEntryCreate(BaseModel):
class CatalogEntryUpdate (line 79) | class CatalogEntryUpdate(BaseModel):
class BulkDeleteIdsRequest (line 84) | class BulkDeleteIdsRequest(BaseModel):
class ColorEntryResponse (line 91) | class ColorEntryResponse(BaseModel):
class Config (line 99) | class Config:
class ColorEntryCreate (line 103) | class ColorEntryCreate(BaseModel):
class ColorEntryUpdate (line 110) | class ColorEntryUpdate(BaseModel):
class ColorLookupResult (line 117) | class ColorLookupResult(BaseModel):
function get_spool_catalog (line 127) | async def get_spool_catalog(
function add_catalog_entry (line 137) | async def add_catalog_entry(
function update_catalog_entry (line 151) | async def update_catalog_entry(
function delete_catalog_entry (line 170) | async def delete_catalog_entry(
function bulk_delete_catalog_entries (line 186) | async def bulk_delete_catalog_entries(
function reset_spool_catalog (line 203) | async def reset_spool_catalog(
function get_color_catalog (line 224) | async def get_color_catalog(
function get_color_name_map (line 238) | async def get_color_name_map(
function add_color_entry (line 277) | async def add_color_entry(
function update_color_entry (line 297) | async def update_color_entry(
function delete_color_entry (line 318) | async def delete_color_entry(
function bulk_delete_color_entries (line 334) | async def bulk_delete_color_entries(
function reset_color_catalog (line 351) | async def reset_color_catalog(
function lookup_color (line 374) | async def lookup_color(
function search_colors (line 397) | async def search_colors(
function sync_from_filamentcolors (line 415) | async def sync_from_filamentcolors(
function list_spools (line 524) | async def list_spools(
function get_spool (line 539) | async def get_spool(
function create_spool (line 553) | async def create_spool(
function bulk_create_spools (line 569) | async def bulk_create_spools(
function update_spool (line 588) | async def update_spool(
function delete_spool (line 615) | async def delete_spool(
function archive_spool (line 633) | async def archive_spool(
function restore_spool (line 654) | async def restore_spool(
function list_k_profiles (line 676) | async def list_k_profiles(
function replace_k_profiles (line 687) | async def replace_k_profiles(
function list_assignments (line 721) | async def list_assignments(
function assign_spool (line 786) | async def assign_spool(
function unassign_spool (line 1168) | async def unassign_spool(
class LinkTagRequest (line 1205) | class LinkTagRequest(BaseModel):
function _validate_tag_input (line 1212) | def _validate_tag_input(
function link_tag_to_spool (line 1229) | async def link_tag_to_spool(
function get_spool_usage_history (line 1309) | async def get_spool_usage_history(
function get_all_usage_history (line 1333) | async def get_all_usage_history(
function clear_spool_usage_history (line 1350) | async def clear_spool_usage_history(
function sync_weights_from_ams (line 1369) | async def sync_weights_from_ams(
function _find_tray_in_ams_data (line 1466) | def _find_tray_in_ams_data(ams_data: list, ams_id: int, tray_id: int) ->...
FILE: backend/app/api/routes/kprofiles.py
function get_kprofiles (line 32) | async def get_kprofiles(
function set_kprofile (line 81) | async def set_kprofile(
function set_kprofiles_batch (line 183) | async def set_kprofiles_batch(
function delete_kprofile (line 242) | async def delete_kprofile(
function get_kprofile_notes (line 289) | async def get_kprofile_notes(
function set_kprofile_note (line 316) | async def set_kprofile_note(
function delete_kprofile_note (line 365) | async def delete_kprofile_note(
FILE: backend/app/api/routes/library.py
function get_library_dir (line 68) | def get_library_dir() -> Path:
function get_library_files_dir (line 76) | def get_library_files_dir() -> Path:
function get_library_thumbnails_dir (line 83) | def get_library_thumbnails_dir() -> Path:
function to_relative_path (line 90) | def to_relative_path(absolute_path: Path | str) -> str:
function to_absolute_path (line 103) | def to_absolute_path(relative_path: str | None) -> Path | None:
function calculate_file_hash (line 114) | def calculate_file_hash(file_path: Path) -> str:
function extract_gcode_thumbnail (line 123) | def extract_gcode_thumbnail(file_path: Path) -> bytes | None:
function create_image_thumbnail (line 186) | def create_image_thumbnail(file_path: Path, thumbnails_dir: Path, max_si...
function list_folders (line 244) | async def list_folders(
function get_folders_by_project (line 303) | async def get_folders_by_project(
function get_folders_by_archive (line 348) | async def get_folders_by_archive(
function create_folder (line 394) | async def create_folder(
function get_folder (line 453) | async def get_folder(
function update_folder (line 495) | async def update_folder(
function delete_folder (line 591) | async def delete_folder(
function _validate_external_path (line 684) | def _validate_external_path(path_str: str) -> Path:
function create_external_folder (line 709) | async def create_external_folder(
function scan_external_folder (line 762) | async def scan_external_folder(
function list_files (line 1044) | async def list_files(
function upload_file (line 1129) | async def upload_file(
function extract_zip_file (line 1266) | async def extract_zip_file(
function batch_generate_stl_thumbnails (line 1531) | async def batch_generate_stl_thumbnails(
function is_sliced_file (line 1647) | def is_sliced_file(filename: str) -> bool:
function add_files_to_queue (line 1659) | async def add_files_to_queue(
function get_library_file_plates (line 1738) | async def get_library_file_plates(
function get_library_file_plate_thumbnail (line 2002) | async def get_library_file_plate_thumbnail(
function get_library_file_filament_requirements (line 2034) | async def get_library_file_filament_requirements(
function print_library_file (line 2167) | async def print_library_file(
function get_file (line 2262) | async def get_file(
function update_file (line 2350) | async def update_file(
function delete_file (line 2415) | async def delete_file(
function download_file (line 2462) | async def download_file(
function create_library_slicer_token (line 2486) | async def create_library_slicer_token(
function download_library_file_for_slicer (line 2508) | async def download_library_file_for_slicer(
function get_thumbnail (line 2542) | async def get_thumbnail(
function get_gcode (line 2573) | async def get_gcode(
function move_files (line 2613) | async def move_files(
function bulk_delete (line 2660) | async def bulk_delete(
function get_library_stats (line 2729) | async def get_library_stats(
FILE: backend/app/api/routes/local_backup.py
function get_status (line 19) | async def get_status(
function trigger_backup (line 37) | async def trigger_backup(
function list_backups (line 46) | async def list_backups(
function download_backup (line 55) | async def download_backup(
function restore_backup (line 72) | async def restore_backup(
function delete_backup (line 98) | async def delete_backup(
FILE: backend/app/api/routes/local_presets.py
function list_local_presets (line 38) | async def list_local_presets(
function get_local_preset (line 60) | async def get_local_preset(
function import_presets (line 81) | async def import_presets(
function create_local_preset (line 99) | async def create_local_preset(
function update_local_preset (line 125) | async def update_local_preset(
function delete_local_preset (line 161) | async def delete_local_preset(
function base_cache_status (line 177) | async def base_cache_status(
function refresh_cache (line 186) | async def refresh_cache(
function reclassify (line 195) | async def reclassify(
FILE: backend/app/api/routes/maintenance.py
function _should_apply_to_printer (line 111) | def _should_apply_to_printer(type_name: str, printer_model: str | None) ...
function get_printer_total_hours (line 125) | async def get_printer_total_hours(db: AsyncSession, printer_id: int) -> ...
function ensure_default_types (line 146) | async def ensure_default_types(db: AsyncSession) -> None:
function get_maintenance_types (line 184) | async def get_maintenance_types(
function create_maintenance_type (line 199) | async def create_maintenance_type(
function update_maintenance_type (line 220) | async def update_maintenance_type(
function delete_maintenance_type (line 242) | async def delete_maintenance_type(
function restore_default_maintenance_types (line 264) | async def restore_default_maintenance_types(
function _get_printer_maintenance_internal (line 284) | async def _get_printer_maintenance_internal(
function get_printer_maintenance (line 438) | async def get_printer_maintenance(
function get_all_maintenance_overview (line 448) | async def get_all_maintenance_overview(
function update_printer_maintenance (line 471) | async def update_printer_maintenance(
function assign_maintenance_type (line 497) | async def assign_maintenance_type(
function remove_maintenance_item (line 551) | async def remove_maintenance_item(
function perform_maintenance (line 577) | async def perform_maintenance(
function get_maintenance_history (line 656) | async def get_maintenance_history(
function get_maintenance_summary (line 671) | async def get_maintenance_summary(
function set_printer_hours (line 707) | async def set_printer_hours(
FILE: backend/app/api/routes/metrics.py
function get_prometheus_settings (line 20) | async def get_prometheus_settings(db: AsyncSession) -> tuple[bool, str]:
function format_labels (line 30) | def format_labels(**labels: str) -> str:
function state_to_numeric (line 38) | def state_to_numeric(state: str) -> int:
function get_metrics (line 54) | async def get_metrics(
FILE: backend/app/api/routes/mfa.py
function _as_utc (line 89) | def _as_utc(dt: datetime) -> datetime:
function _user_to_response (line 125) | def _user_to_response(user: User) -> UserResponse:
function _generate_totp_qr_b64 (line 142) | def _generate_totp_qr_b64(provisioning_uri: str) -> str:
function _generate_backup_codes (line 158) | def _generate_backup_codes() -> tuple[list[str], list[str]]:
function create_pre_auth_token (line 169) | async def create_pre_auth_token(db: AsyncSession, username: str, challen...
function consume_pre_auth_token (line 198) | async def consume_pre_auth_token(db: AsyncSession, token: str, challenge...
function peek_pre_auth_token (line 230) | async def peek_pre_auth_token(db: AsyncSession, token: str, challenge_id...
function check_rate_limit (line 258) | async def check_rate_limit(
function record_failed_attempt (line 296) | async def record_failed_attempt(db: AsyncSession, username: str, event_t...
function clear_failed_attempts (line 302) | async def clear_failed_attempts(db: AsyncSession, username: str, event_t...
function check_email_otp_send_rate (line 313) | async def check_email_otp_send_rate(db: AsyncSession, username: str) -> ...
function record_email_otp_send (line 340) | async def record_email_otp_send(db: AsyncSession, username: str) -> None:
function _assert_totp_not_replayed (line 353) | def _assert_totp_not_replayed(totp_obj: pyotp.TOTP, totp_record: UserTOT...
function _get_email_2fa_enabled (line 379) | async def _get_email_2fa_enabled(db: AsyncSession, user_id: int) -> bool:
function _set_email_2fa_enabled (line 384) | async def _set_email_2fa_enabled(db: AsyncSession, user_id: int, enabled...
function get_2fa_status (line 394) | async def get_2fa_status(
function setup_totp (line 414) | async def setup_totp(
function enable_totp (line 466) | async def enable_totp(
function disable_totp (line 504) | async def disable_totp(
function regenerate_backup_codes (line 544) | async def regenerate_backup_codes(
function enable_email_otp (line 591) | async def enable_email_otp(
function confirm_enable_email_otp (line 668) | async def confirm_enable_email_otp(
function disable_email_otp (line 725) | async def disable_email_otp(
function send_email_otp (line 748) | async def send_email_otp(
function verify_2fa (line 837) | async def verify_2fa(
function admin_disable_2fa (line 976) | async def admin_disable_2fa(
function list_oidc_providers (line 1024) | async def list_oidc_providers(
function list_all_oidc_providers (line 1034) | async def list_all_oidc_providers(
function create_oidc_provider (line 1045) | async def create_oidc_provider(
function update_oidc_provider (line 1068) | async def update_oidc_provider(
function delete_oidc_provider (line 1091) | async def delete_oidc_provider(
function oidc_authorize (line 1108) | async def oidc_authorize(
function oidc_callback (line 1191) | async def oidc_callback(
function oidc_exchange (line 1557) | async def oidc_exchange(
function list_oidc_links (line 1643) | async def list_oidc_links(
function remove_oidc_link (line 1665) | async def remove_oidc_link(
function _get_base_external_url (line 1688) | async def _get_base_external_url(db: AsyncSession) -> str:
FILE: backend/app/api/routes/notification_templates.py
function get_templates (line 59) | async def get_templates(
function get_variables (line 69) | async def get_variables(
function get_template (line 84) | async def get_template(
function update_template (line 98) | async def update_template(
function reset_template (line 125) | async def reset_template(
function preview_template (line 157) | async def preview_template(
FILE: backend/app/api/routes/notifications.py
function _provider_to_dict (line 32) | def _provider_to_dict(provider: NotificationProvider) -> dict:
function list_notification_providers (line 97) | async def list_notification_providers(
function create_notification_provider (line 109) | async def create_notification_provider(
function test_notification_config (line 178) | async def test_notification_config(
function test_all_notification_providers (line 192) | async def test_all_notification_providers(
function get_notification_logs (line 246) | async def get_notification_logs(
function get_notification_log_stats (line 308) | async def get_notification_log_stats(
function clear_notification_logs (line 354) | async def clear_notification_logs(
function get_notification_provider (line 377) | async def get_notification_provider(
function update_notification_provider (line 393) | async def update_notification_provider(
function delete_notification_provider (line 426) | async def delete_notification_provider(
function test_notification_provider (line 448) | async def test_notification_provider(
FILE: backend/app/api/routes/obico.py
class TestConnectionRequest (line 18) | class TestConnectionRequest(BaseModel):
function get_status (line 23) | async def get_status(
function test_connection (line 41) | async def test_connection(
function cached_frame (line 52) | async def cached_frame(nonce: str):
FILE: backend/app/api/routes/pending_uploads.py
class ArchiveRequest (line 21) | class ArchiveRequest(BaseModel):
class PendingUploadResponse (line 29) | class PendingUploadResponse(BaseModel):
class Config (line 42) | class Config:
function list_pending_uploads (line 47) | async def list_pending_uploads(
function get_pending_count (line 60) | async def get_pending_count(
function archive_all_pending (line 76) | async def archive_all_pending(
function discard_all_pending (line 132) | async def discard_all_pending(
function get_pending_upload (line 159) | async def get_pending_upload(
function archive_pending_upload (line 175) | async def archive_pending_upload(
function discard_pending_upload (line 244) | async def discard_pending_upload(
FILE: backend/app/api/routes/print_log.py
function get_print_log (line 23) | async def get_print_log(
function get_print_log_thumbnail (line 92) | async def get_print_log_thumbnail(
function clear_print_log (line 117) | async def clear_print_log(
FILE: backend/app/api/routes/print_queue.py
function _extract_filament_types_from_3mf (line 44) | def _extract_filament_types_from_3mf(file_path: Path, plate_id: int | No...
function _extract_print_time_from_3mf (line 105) | def _extract_print_time_from_3mf(file_path: Path, plate_id: int | None =...
function _enrich_response (line 157) | def _enrich_response(item: PrintQueueItem) -> PrintQueueItemResponse:
function list_queue (line 277) | async def list_queue(
function add_to_queue (line 338) | async def add_to_queue(
function bulk_update_queue_items (line 582) | async def bulk_update_queue_items(
function list_batches (line 649) | async def list_batches(
function get_batch (line 668) | async def get_batch(
function cancel_batch (line 682) | async def cancel_batch(
function _build_batch_response (line 709) | async def _build_batch_response(db: AsyncSession, batch: PrintBatch) -> ...
function get_queue_item (line 746) | async def get_queue_item(
function update_queue_item (line 770) | async def update_queue_item(
function delete_queue_item (line 848) | async def delete_queue_item(
function reorder_queue (line 882) | async def reorder_queue(
function cancel_queue_item (line 900) | async def cancel_queue_item(
function stop_queue_item (line 935) | async def stop_queue_item(
function start_queue_item (line 1017) | async def start_queue_item(
FILE: backend/app/api/routes/printers.py
function list_printers (line 53) | async def list_printers(
function create_printer (line 63) | async def create_printer(
function list_usb_cameras (line 87) | async def list_usb_cameras(
function get_available_filaments (line 105) | async def get_available_filaments(
function get_developer_mode_warnings (line 203) | async def get_developer_mode_warnings(
function get_printer (line 226) | async def get_printer(
function update_printer (line 240) | async def update_printer(
function delete_printer (line 285) | async def delete_printer(
function get_printer_status (line 339) | async def get_printer_status(
function get_current_print_user (line 642) | async def get_current_print_user(
function refresh_printer_status (line 663) | async def refresh_printer_status(
function connect_printer (line 682) | async def connect_printer(
function disconnect_printer (line 698) | async def disconnect_printer(
function test_printer_connection (line 714) | async def test_printer_connection(
function clear_cover_cache (line 733) | def clear_cover_cache(printer_id: int) -> None:
function get_printer_cover (line 739) | async def get_printer_cover(
function list_printer_files (line 960) | async def list_printer_files(
function download_printer_file (line 985) | async def download_printer_file(
function get_printer_file_gcode (line 1026) | async def get_printer_file_gcode(
function get_printer_file_plates (line 1065) | async def get_printer_file_plates(
function get_printer_file_plate_thumbnail (line 1309) | async def get_printer_file_plate_thumbnail(
function download_printer_files_as_zip (line 1341) | async def download_printer_files_as_zip(
function delete_printer_file (line 1388) | async def delete_printer_file(
function get_printer_storage (line 1408) | async def get_printer_storage(
function enable_mqtt_logging (line 1430) | async def enable_mqtt_logging(
function disable_mqtt_logging (line 1449) | async def disable_mqtt_logging(
function get_mqtt_logs (line 1468) | async def get_mqtt_logs(
function clear_mqtt_logs (line 1495) | async def clear_mqtt_logs(
function start_drying (line 1516) | async def start_drying(
function stop_drying (line 1595) | async def stop_drying(
function set_print_option (line 1619) | async def set_print_option(
function start_calibration (line 1691) | async def start_calibration(
function _slot_preset_key (line 1751) | def _slot_preset_key(ams_id: int, tray_id: int) -> int:
function get_slot_presets (line 1761) | async def get_slot_presets(
function get_slot_preset (line 1782) | async def get_slot_preset(
function save_slot_preset (line 1811) | async def save_slot_preset(
function delete_slot_preset (line 1867) | async def delete_slot_preset(
function configure_ams_slot (line 1892) | async def configure_ams_slot(
function reset_ams_slot (line 2097) | async def reset_ams_slot(
function get_ams_labels (line 2142) | async def get_ams_labels(
function save_ams_label (line 2195) | async def save_ams_label(
function delete_ams_label (line 2232) | async def delete_ams_label(
function debug_simulate_print_complete (line 2254) | async def debug_simulate_print_complete(
function stop_print (line 2307) | async def stop_print(
function clear_plate (line 2330) | async def clear_plate(
function pause_print (line 2366) | async def pause_print(
function resume_print (line 2389) | async def resume_print(
function set_print_speed (line 2412) | async def set_print_speed(
function set_airduct_mode (line 2437) | async def set_airduct_mode(
function set_chamber_light (line 2464) | async def set_chamber_light(
function bed_jog (line 2488) | async def bed_jog(
function home_axes (line 2530) | async def home_axes(
function clear_hms_errors (line 2574) | async def clear_hms_errors(
function get_printable_objects (line 2597) | async def get_printable_objects(
function skip_objects (line 2708) | async def skip_objects(
function refresh_ams_slot (line 2762) | async def refresh_ams_slot(
function _apply_pa_after_refresh (line 2789) | async def _apply_pa_after_refresh(printer_id: int, ams_id: int, slot_id:...
function get_runtime_debug (line 2911) | async def get_runtime_debug(
FILE: backend/app/api/routes/projects.py
function compute_project_stats (line 49) | async def compute_project_stats(
function list_projects (line 159) | async def list_projects(
function create_project (line 265) | async def create_project(
function list_templates (line 328) | async def list_templates(
function create_project_from_template (line 365) | async def create_project_from_template(
function get_child_previews (line 449) | async def get_child_previews(db: AsyncSession, parent_id: int) -> list[P...
function get_project (line 481) | async def get_project(
function update_project (line 530) | async def update_project(
function delete_project (line 622) | async def delete_project(
function list_project_archives (line 640) | async def list_project_archives(
function list_project_queue (line 672) | async def list_project_queue(
function add_archives_to_project (line 692) | async def add_archives_to_project(
function add_queue_items_to_project (line 717) | async def add_queue_items_to_project(
function remove_archives_from_project (line 742) | async def remove_archives_from_project(
function get_project_attachments_dir (line 765) | def get_project_attachments_dir(project_id: int) -> Path:
function upload_attachment (line 829) | async def upload_attachment(
function download_attachment (line 907) | async def download_attachment(
function delete_attachment (line 943) | async def delete_attachment(
function list_bom_items (line 992) | async def list_bom_items(
function create_bom_item (line 1043) | async def create_bom_item(
function update_bom_item (line 1102) | async def update_bom_item(
function delete_bom_item (line 1167) | async def delete_bom_item(
function create_template_from_project (line 1191) | async def create_template_from_project(
function get_project_timeline (line 1272) | async def get_project_timeline(
function export_project (line 1373) | async def export_project(
function import_project (line 1495) | async def import_project(
function import_project_file (line 1589) | async def import_project_file(
FILE: backend/app/api/routes/settings.py
function get_setting (line 29) | async def get_setting(db: AsyncSession, key: str) -> str | None:
function get_external_login_url (line 36) | async def get_external_login_url(db: AsyncSession) -> str:
function set_setting (line 57) | async def set_setting(db: AsyncSession, key: str, value: str) -> None:
function get_settings (line 66) | async def get_settings(
function update_settings (line 152) | async def update_settings(
function patch_settings (line 210) | async def patch_settings(
function reset_settings (line 220) | async def reset_settings(
function get_default_sidebar_order (line 236) | async def get_default_sidebar_order(
function check_ffmpeg (line 250) | async def check_ffmpeg():
function get_spoolman_settings (line 263) | async def get_spoolman_settings(
function update_spoolman_settings (line 284) | async def update_spoolman_settings(
function get_homeassistant_settings (line 317) | async def get_homeassistant_settings(db: AsyncSession) -> dict:
function create_backup_zip (line 354) | async def create_backup_zip(output_path: Path | None = None) -> tuple[Pa...
function create_backup (line 475) | async def create_backup(
function _import_sqlite_to_postgres (line 498) | async def _import_sqlite_to_postgres(sqlite_path: Path, postgres_url: str):
function restore_backup (line 673) | async def restore_backup(
function get_network_interfaces (line 804) | async def get_network_interfaces(
function get_virtual_printer_models (line 815) | async def get_virtual_printer_models(
function get_virtual_printer_settings (line 831) | async def get_virtual_printer_settings(
function update_virtual_printer_settings (line 860) | async def update_virtual_printer_settings(
function get_mqtt_status (line 1015) | async def get_mqtt_status(
FILE: backend/app/api/routes/smart_plugs.py
function list_smart_plugs (line 48) | async def list_smart_plugs(
function create_smart_plug (line 58) | async def create_smart_plug(
function get_smart_plug_by_printer (line 139) | async def get_smart_plug_by_printer(
function get_script_plugs_by_printer (line 166) | async def get_script_plugs_by_printer(
class TasmotaScanRequest (line 191) | class TasmotaScanRequest(BaseModel):
function get_local_network_range (line 199) | def get_local_network_range() -> tuple[str, str]:
class TasmotaScanStatus (line 225) | class TasmotaScanStatus(BaseModel):
class DiscoveredTasmotaDevice (line 233) | class DiscoveredTasmotaDevice(BaseModel):
function start_tasmota_scan (line 244) | async def start_tasmota_scan(
function get_tasmota_scan_status (line 271) | async def get_tasmota_scan_status(
function stop_tasmota_scan (line 284) | async def stop_tasmota_scan(
function get_discovered_tasmota_devices (line 298) | async def get_discovered_tasmota_devices(
function test_ha_connection (line 318) | async def test_ha_connection(
function test_rest_connection (line 328) | async def test_rest_connection(
function list_ha_entities (line 338) | async def list_ha_entities(
function list_ha_sensor_entities (line 366) | async def list_ha_sensor_entities(
function get_smart_plug (line 391) | async def get_smart_plug(
function update_smart_plug (line 405) | async def update_smart_plug(
function delete_smart_plug (line 497) | async def delete_smart_plug(
function _get_service_for_plug (line 522) | async def _get_service_for_plug(plug: SmartPlug, db: AsyncSession):
function control_smart_plug (line 540) | async def control_smart_plug(
function trigger_associated_scripts (line 619) | async def trigger_associated_scripts(printer_id: int, plug_state: str, d...
function get_plug_status (line 651) | async def get_plug_status(
function check_power_alerts (line 727) | async def check_power_alerts(plug: SmartPlug, current_power: float | Non...
function test_connection (line 786) | async def test_connection(
FILE: backend/app/api/routes/spoolbuddy.py
function _is_online (line 52) | def _is_online(device: SpoolBuddyDevice) -> bool:
function _device_to_response (line 60) | def _device_to_response(device: SpoolBuddyDevice) -> DeviceResponse:
function _should_broadcast_online (line 92) | def _should_broadcast_online(device_id: str, force: bool = False) -> bool:
function register_device (line 109) | async def register_device(
function list_devices (line 181) | async def list_devices(
function unregister_device (line 192) | async def unregister_device(
function device_heartbeat (line 212) | async def device_heartbeat(
function nfc_tag_scanned (line 297) | async def nfc_tag_scanned(
function nfc_tag_removed (line 349) | async def nfc_tag_removed(
function nfc_write_tag (line 365) | async def nfc_write_tag(
function nfc_write_result (line 406) | async def nfc_write_result(
function cancel_write (line 459) | async def cancel_write(
function scale_reading (line 483) | async def scale_reading(
function update_spool_weight (line 501) | async def update_spool_weight(
function tare_scale (line 534) | async def tare_scale(
function set_tare_offset (line 551) | async def set_tare_offset(
function set_calibration_factor (line 575) | async def set_calibration_factor(
function get_calibration (line 613) | async def get_calibration(
function get_display_settings (line 634) | async def get_display_settings(
function update_display_settings (line 656) | async def update_display_settings(
function queue_system_config_update (line 682) | async def queue_system_config_update(
function queue_system_command (line 719) | async def queue_system_command(
function system_command_result (line 748) | async def system_command_result(
function queue_diagnostic (line 789) | async def queue_diagnostic(
function get_diagnostic_result (line 821) | async def get_diagnostic_result(
function report_diagnostic_result (line 851) | async def report_diagnostic_result(
function check_daemon_update (line 884) | async def check_daemon_update(
function trigger_daemon_update (line 908) | async def trigger_daemon_update(
function get_ssh_public_key (line 952) | async def get_ssh_public_key(
function report_update_status (line 966) | async def report_update_status(
function spoolbuddy_watchdog (line 1004) | async def spoolbuddy_watchdog():
FILE: backend/app/api/routes/spoolman.py
class SpoolmanStatus (line 31) | class SpoolmanStatus(BaseModel):
class SkippedSpool (line 39) | class SkippedSpool(BaseModel):
class SyncResult (line 48) | class SyncResult(BaseModel):
function get_spoolman_settings (line 58) | async def get_spoolman_settings(db: AsyncSession) -> dict:
function get_spoolman_status (line 86) | async def get_spoolman_status(
function connect_spoolman (line 107) | async def connect_spoolman(
function disconnect_spoolman (line 141) | async def disconnect_spoolman(
function sync_printer_ams (line 150) | async def sync_printer_ams(
function sync_all_printers (line 341) | async def sync_all_printers(
function get_spools (line 520) | async def get_spools(
function get_filaments (line 545) | async def get_filaments(
class UnlinkedSpool (line 569) | class UnlinkedSpool(BaseModel):
function get_unlinked_spools (line 582) | async def get_unlinked_spools(
function get_linked_spools (line 629) | async def get_linked_spools(
class LinkSpoolRequest (line 670) | class LinkSpoolRequest(BaseModel):
function link_spool (line 682) | async def link_spool(
function unlink_spool (line 746) | async def unlink_spool(
FILE: backend/app/api/routes/support.py
class DebugLoggingState (line 43) | class DebugLoggingState(BaseModel):
class DebugLoggingToggle (line 49) | class DebugLoggingToggle(BaseModel):
function _get_debug_setting (line 53) | async def _get_debug_setting(db: AsyncSession) -> tuple[bool, datetime |...
function _set_debug_setting (line 74) | async def _set_debug_setting(db: AsyncSession, enabled: bool) -> datetim...
function _apply_log_level (line 97) | def _apply_log_level(debug: bool):
function get_debug_logging_state (line 119) | async def get_debug_logging_state(
function toggle_debug_logging (line 138) | async def toggle_debug_logging(
class LogEntry (line 159) | class LogEntry(BaseModel):
class LogsResponse (line 168) | class LogsResponse(BaseModel):
function _parse_log_line (line 180) | def _parse_log_line(line: str) -> LogEntry | None:
function _read_log_entries (line 193) | def _read_log_entries(
function get_logs (line 278) | async def get_logs(
function clear_logs (line 295) | async def clear_logs(
function _sanitize_path (line 315) | def _sanitize_path(path: str) -> str:
function _detect_docker_network_mode (line 326) | def _detect_docker_network_mode() -> str:
function _mask_subnet (line 344) | def _mask_subnet(subnet: str) -> str:
function _anonymize_mqtt_broker (line 357) | def _anonymize_mqtt_broker(broker: str) -> str:
function _check_port (line 372) | async def _check_port(ip: str, port: int, timeout: float = 2.0) -> bool:
function _get_container_memory_limit (line 383) | def _get_container_memory_limit() -> int | None:
function _format_bytes (line 407) | def _format_bytes(size_bytes: int) -> str:
function _collect_support_info (line 418) | async def _collect_support_info() -> dict:
function _sanitize_log_content (line 783) | def _sanitize_log_content(content: str, sensitive_strings: dict[str, str...
function _get_log_content (line 819) | def _get_log_content(max_bytes: int = 10 * 1024 * 1024, sensitive_string...
function _get_recent_sanitized_logs (line 841) | async def _get_recent_sanitized_logs(max_lines: int = 200) -> str:
function generate_support_bundle (line 883) | async def generate_support_bundle(
function init_debug_logging (line 950) | async def init_debug_logging():
FILE: backend/app/api/routes/system.py
function get_directory_size (line 36) | def get_directory_size(path: Path) -> int:
function format_bytes (line 48) | def format_bytes(bytes_value: int) -> str:
function format_uptime (line 57) | def format_uptime(seconds: float) -> str:
function _is_under (line 74) | def _is_under(path: Path, root: Path) -> bool:
function _get_database_paths (line 82) | def _get_database_paths() -> list[Path]:
function _get_database_items (line 91) | def _get_database_items() -> list[dict]:
function _get_app_dir (line 110) | def _get_app_dir() -> Path:
function _get_data_dirs (line 114) | def _get_data_dirs() -> list[Path]:
function _is_system_path (line 124) | def _is_system_path(path: Path) -> bool:
function _get_storage_rules (line 131) | def _get_storage_rules() -> list[tuple[str, str, Callable]]:
function _classify_file (line 214) | def _classify_file(path: Path, rules: list[tuple[str, str, Callable]]) -...
function _format_percentage (line 224) | def _format_percentage(part: int, total: int) -> float:
function _get_other_bucket (line 230) | def _get_other_bucket(path: Path, base_dir: Path) -> str:
function _walk_files (line 240) | def _walk_files(roots: list[Path]) -> list[Path]:
function _scan_storage_usage (line 262) | def _scan_storage_usage() -> dict:
function _get_storage_usage_cached (line 351) | async def _get_storage_usage_cached(refresh: bool, max_age_seconds: int)...
function get_system_info (line 399) | async def get_system_info(
function get_storage_usage (line 569) | async def get_storage_usage(
FILE: backend/app/api/routes/updates.py
function _is_docker_environment (line 35) | def _is_docker_environment() -> bool:
function _find_executable (line 57) | def _find_executable(name: str) -> str | None:
function parse_version (line 81) | def parse_version(version: str) -> tuple:
function is_newer_version (line 127) | def is_newer_version(latest: str, current: str) -> bool:
function get_version (line 173) | async def get_version():
function check_for_updates (line 185) | async def check_for_updates(
function _perform_update (line 314) | async def _perform_update():
function apply_update (line 504) | async def apply_update(
function get_update_status (line 548) | async def get_update_status(
FILE: backend/app/api/routes/user_notifications.py
function get_user_email_preferences (line 22) | async def get_user_email_preferences(
function update_user_email_preferences (line 55) | async def update_user_email_preferences(
FILE: backend/app/api/routes/users.py
function _user_to_response (line 41) | def _user_to_response(user: User) -> UserResponse:
function list_users (line 59) | async def list_users(
function create_user (line 71) | async def create_user(
function get_user (line 177) | async def get_user(
function update_user (line 195) | async def update_user(
function get_user_items_count (line 303) | async def get_user_items_count(
function delete_user (line 339) | async def delete_user(
function change_own_password (line 408) | async def change_own_password(
FILE: backend/app/api/routes/virtual_printers.py
class VirtualPrinterCreate (line 19) | class VirtualPrinterCreate(BaseModel):
class VirtualPrinterUpdate (line 31) | class VirtualPrinterUpdate(BaseModel):
function _resolve_printer_model (line 43) | def _resolve_printer_model(printer_model: str | None) -> str | None:
function _vp_to_dict (line 60) | def _vp_to_dict(vp, status: dict | None = None) -> dict:
function list_virtual_printers (line 87) | async def list_virtual_printers(
function create_virtual_printer (line 111) | async def create_virtual_printer(
function get_virtual_printer (line 219) | async def get_virtual_printer(
function update_virtual_printer (line 240) | async def update_virtual_printer(
function delete_virtual_printer (line 387) | async def delete_virtual_printer(
FILE: backend/app/api/routes/webhook.py
class QueueAddRequest (line 22) | class QueueAddRequest(BaseModel):
class QueueAddResponse (line 31) | class QueueAddResponse(BaseModel):
class PrinterStatusResponse (line 40) | class PrinterStatusResponse(BaseModel):
class QueueStatusResponse (line 50) | class QueueStatusResponse(BaseModel):
function webhook_add_to_queue (line 62) | async def webhook_add_to_queue(
function webhook_start_print (line 134) | async def webhook_start_print(
function webhook_stop_print (line 189) | async def webhook_stop_print(
function webhook_cancel_print (line 217) | async def webhook_cancel_print(
function webhook_get_printer_status (line 245) | async def webhook_get_printer_status(
function webhook_get_queue_status (line 277) | async def webhook_get_queue_status(
FILE: backend/app/api/routes/websocket.py
function websocket_endpoint (line 14) | async def websocket_endpoint(websocket: WebSocket):
FILE: backend/app/cli.py
class KioskBootstrapError (line 27) | class KioskBootstrapError(RuntimeError):
function kiosk_bootstrap (line 31) | async def kiosk_bootstrap(
function main (line 86) | def main(argv: list[str] | None = None) -> int:
FILE: backend/app/core/auth.py
function _get_jwt_secret (line 34) | def _get_jwt_secret() -> str:
function create_slicer_download_token (line 109) | async def create_slicer_download_token(resource_type: str, resource_id: ...
function verify_slicer_download_token (line 135) | async def verify_slicer_download_token(token: str, resource_type: str, r...
function create_camera_stream_token (line 173) | async def create_camera_stream_token() -> str:
function verify_camera_stream_token (line 197) | async def verify_camera_stream_token(token: str) -> bool:
function verify_password (line 211) | def verify_password(plain_password: str, hashed_password: str) -> bool:
function get_password_hash (line 219) | def get_password_hash(password: str) -> str:
function create_access_token (line 227) | def create_access_token(data: dict, expires_delta: timedelta | None = No...
function _is_token_fresh (line 241) | def _is_token_fresh(iat: int | float | None, user: User) -> bool:
function revoke_jti (line 262) | async def revoke_jti(jti: str, expires_at: datetime, username: str | Non...
function is_jti_revoked (line 283) | async def is_jti_revoked(jti: str) -> bool:
function get_user_by_username (line 295) | async def get_user_by_username(db: AsyncSession, username: str) -> User ...
function get_user_by_email (line 303) | async def get_user_by_email(db: AsyncSession, email: str) -> User | None:
function authenticate_user (line 311) | async def authenticate_user(db: AsyncSession, username: str, password: s...
function authenticate_user_by_email (line 329) | async def authenticate_user_by_email(db: AsyncSession, email: str, passw...
function is_auth_enabled (line 347) | async def is_auth_enabled(db: AsyncSession) -> bool:
function _validate_api_key (line 360) | async def _validate_api_key(db: AsyncSession, api_key_value: str) -> API...
function get_current_user_optional (line 400) | async def get_current_user_optional(
function get_current_user (line 440) | async def get_current_user(
function get_current_active_user (line 478) | async def get_current_active_user(current_user: Annotated[User, Depends(...
function require_auth_if_enabled (line 483) | async def require_auth_if_enabled(
function require_role (line 565) | def require_role(required_role: str):
function require_admin_if_auth_enabled (line 579) | def require_admin_if_auth_enabled():
function generate_api_key (line 597) | def generate_api_key() -> tuple[str, str, str]:
function get_api_key (line 613) | async def get_api_key(
function check_permission (line 672) | def check_permission(api_key: APIKey, permission: str) -> None:
function check_printer_access (line 702) | def check_printer_access(api_key: APIKey, printer_id: int) -> None:
function RequireAdmin (line 725) | def RequireAdmin():
function RequireAdminIfAuthEnabled (line 730) | def RequireAdminIfAuthEnabled():
function require_permission (line 735) | def require_permission(*permissions: str | Permission):
function require_permission_if_auth_enabled (line 811) | def require_permission_if_auth_enabled(*permissions: str | Permission):
function RequirePermission (line 912) | def RequirePermission(*permissions: str | Permission):
function RequirePermissionIfAuthEnabled (line 917) | def RequirePermissionIfAuthEnabled(*permissions: str | Permission):
function require_camera_stream_token_if_auth_enabled (line 922) | def require_camera_stream_token_if_auth_enabled():
function require_ownership_permission (line 946) | def require_ownership_permission(
FILE: backend/app/core/compat.py
class StrEnum (line 10) | class StrEnum(str, Enum):
FILE: backend/app/core/config.py
function _migrate_database (line 30) | def _migrate_database() -> Path:
class Settings (line 57) | class Settings(BaseSettings):
class Config (line 76) | class Config:
FILE: backend/app/core/database.py
function _set_sqlite_pragmas (line 15) | def _set_sqlite_pragmas(dbapi_conn, connection_record):
function _create_engine (line 26) | def _create_engine():
function run_with_retry (line 82) | async def run_with_retry(fn, *, max_attempts: int = 3, label: str = ""):
function close_all_connections (line 118) | async def close_all_connections():
function reinitialize_database (line 124) | async def reinitialize_database():
class Base (line 135) | class Base(DeclarativeBase):
function get_db (line 139) | async def get_db() -> AsyncSession:
function init_db (line 151) | async def init_db():
function _safe_execute (line 215) | async def _safe_execute(conn, sql):
function run_migrations (line 230) | async def run_migrations(conn):
function seed_notification_templates (line 1519) | async def seed_notification_templates():
function seed_default_groups (line 1557) | async def seed_default_groups():
function seed_spool_catalog (line 1701) | async def seed_spool_catalog():
function seed_color_catalog (line 1724) | async def seed_color_catalog():
FILE: backend/app/core/db_dialect.py
function is_postgres (line 10) | def is_postgres() -> bool:
function is_sqlite (line 17) | def is_sqlite() -> bool:
function upsert_setting (line 24) | async def upsert_setting(db, model, key: str, value: str):
function run_pragma (line 45) | async def run_pragma(conn, pragma_sql: str):
FILE: backend/app/core/encryption.py
function _get_fernet (line 24) | def _get_fernet():
function mfa_encrypt (line 47) | def mfa_encrypt(plaintext: str) -> str:
function mfa_decrypt (line 56) | def mfa_decrypt(value: str) -> str:
FILE: backend/app/core/permissions.py
class Permission (line 10) | class Permission(StrEnum):
FILE: backend/app/core/websocket.py
class ConnectionManager (line 8) | class ConnectionManager:
method __init__ (line 11) | def __init__(self):
method connect (line 15) | async def connect(self, websocket: WebSocket):
method disconnect (line 21) | async def disconnect(self, websocket: WebSocket):
method broadcast (line 27) | async def broadcast(self, message: dict[str, Any]):
method send_printer_status (line 46) | async def send_printer_status(self, printer_id: int, status: dict):
method send_print_start (line 56) | async def send_print_start(self, printer_id: int, data: dict):
method send_print_complete (line 66) | async def send_print_complete(self, printer_id: int, data: dict):
method send_archive_created (line 76) | async def send_archive_created(self, archive: dict):
method send_archive_updated (line 85) | async def send_archive_updated(self, archive: dict):
method send_missing_spool_assignment (line 94) | async def send_missing_spool_assignment(
FILE: backend/app/i18n/__init__.py
function get_translation (line 76) | def get_translation(lang: str, key: str, **kwargs: Any) -> str:
class Translator (line 117) | class Translator:
method __init__ (line 120) | def __init__(self, lang: str = "en"):
method t (line 123) | def t(self, key: str, **kwargs: Any) -> str:
FILE: backend/app/main.py
function _start_error_server (line 105) | def _start_error_server(missing_packages: list):
function check_dependencies (line 185) | def check_dependencies():
function _get_plug_energy (line 323) | async def _get_plug_energy(plug, db) -> dict | None:
function _record_energy_start (line 355) | async def _record_energy_start(archive, printer_id: int, db, *, context:...
function register_expected_print (line 388) | def register_expected_print(
function _get_start_ams_mapping (line 426) | def _get_start_ams_mapping(data: dict, archive_id: int | None) -> list[i...
function _bump_library_file_usage_if_completed (line 434) | async def _bump_library_file_usage_if_completed(db, item, queue_status: ...
function mark_printer_stopped_by_user (line 451) | def mark_printer_stopped_by_user(printer_id: int) -> None:
function on_printer_status_change (line 467) | async def on_printer_status_change(printer_id: int, state: PrinterState):
function _is_bambu_uuid (line 698) | def _is_bambu_uuid(tray_uuid: str) -> bool:
function on_ams_change (line 703) | async def on_ams_change(printer_id: int, ams_data: list):
function _capture_snapshot_for_notification (line 1132) | async def _capture_snapshot_for_notification(printer_id: int, printer, l...
function _apply_camera_rotation (line 1189) | def _apply_camera_rotation(image_data: bytes, printer, logger) -> bytes:
function _send_print_start_notification (line 1213) | async def _send_print_start_notification(
function _dispatch_user_print_email (line 1253) | async def _dispatch_user_print_email(
function _load_objects_from_archive (line 1288) | def _load_objects_from_archive(archive, printer_id: int, logger) -> None:
function on_print_start (line 1310) | async def on_print_start(printer_id: int, data: dict):
function _list_timelapse_videos (line 2194) | async def _list_timelapse_videos(printer) -> tuple[list[dict], str | None]:
function _scan_for_timelapse_with_retries (line 2225) | async def _scan_for_timelapse_with_retries(archive_id: int, baseline_nam...
function on_print_complete (line 2416) | async def on_print_complete(printer_id: int, data: dict):
function record_ams_history (line 3448) | async def record_ams_history():
function start_ams_history_recording (line 3631) | def start_ams_history_recording():
function stop_ams_history_recording (line 3639) | def stop_ams_history_recording():
function track_printer_runtime (line 3653) | async def track_printer_runtime():
function start_runtime_tracking (line 3738) | def start_runtime_tracking():
function stop_runtime_tracking (line 3746) | def stop_runtime_tracking():
function _spoolbuddy_watchdog_loop (line 3760) | async def _spoolbuddy_watchdog_loop():
function start_spoolbuddy_watchdog (line 3774) | def start_spoolbuddy_watchdog():
function stop_spoolbuddy_watchdog (line 3781) | def stop_spoolbuddy_watchdog():
function _camera_cleanup_loop (line 3794) | async def _camera_cleanup_loop():
function start_camera_cleanup (line 3808) | def start_camera_cleanup():
function stop_camera_cleanup (line 3815) | def stop_camera_cleanup():
function _evict_stale_expected_prints (line 3828) | def _evict_stale_expected_prints() -> None:
function _expected_prints_cleanup_loop (line 3864) | async def _expected_prints_cleanup_loop() -> None:
function start_expected_prints_cleanup (line 3876) | def start_expected_prints_cleanup() -> None:
function stop_expected_prints_cleanup (line 3883) | def stop_expected_prints_cleanup() -> None:
function _run_auth_cleanup (line 3899) | async def _run_auth_cleanup() -> None:
function _auth_cleanup_loop (line 3959) | async def _auth_cleanup_loop() -> None:
function start_auth_cleanup (line 3971) | def start_auth_cleanup() -> None:
function stop_auth_cleanup (line 3978) | def stop_auth_cleanup() -> None:
function lifespan (line 3987) | async def lifespan(app: FastAPI):
function security_headers_middleware (line 4321) | async def security_headers_middleware(request, call_next):
function auth_middleware (line 4353) | async def auth_middleware(request, call_next):
function serve_frontend (line 4541) | async def serve_frontend():
function health_check (line 4554) | async def health_check():
function serve_manifest (line 4560) | async def serve_manifest():
function serve_service_worker (line 4569) | async def serve_service_worker():
function serve_sw_register (line 4582) | async def serve_sw_register():
function serve_spa (line 4596) | async def serve_spa(full_path: str):
FILE: backend/app/models/active_print_spoolman.py
class ActivePrintSpoolman (line 9) | class ActivePrintSpoolman(Base):
FILE: backend/app/models/ams_history.py
class AMSSensorHistory (line 9) | class AMSSensorHistory(Base):
FILE: backend/app/models/ams_label.py
class AmsLabel (line 19) | class AmsLabel(Base):
FILE: backend/app/models/api_key.py
class APIKey (line 9) | class APIKey(Base):
FILE: backend/app/models/archive.py
class PrintArchive (line 9) | class PrintArchive(Base):
FILE: backend/app/models/auth_ephemeral.py
class TokenType (line 32) | class TokenType(str, Enum):
class EventType (line 47) | class EventType(str, Enum):
class AuthEphemeralToken (line 62) | class AuthEphemeralToken(Base):
method new_pre_auth (line 100) | def new_pre_auth(
method new_oidc_state (line 117) | def new_oidc_state(
method new_oidc_exchange (line 136) | def new_oidc_exchange(
method new_password_reset (line 151) | def new_password_reset(
method new_email_otp_setup (line 166) | def new_email_otp_setup(
class AuthRateLimitEvent (line 187) | class AuthRateLimitEvent(Base):
FILE: backend/app/models/bug_report.py
class BugReport (line 9) | class BugReport(Base):
FILE: backend/app/models/color_catalog.py
class ColorCatalogEntry (line 9) | class ColorCatalogEntry(Base):
FILE: backend/app/models/external_link.py
class ExternalLink (line 9) | class ExternalLink(Base):
FILE: backend/app/models/filament.py
class Filament (line 9) | class Filament(Base):
FILE: backend/app/models/github_backup.py
class GitHubBackupConfig (line 11) | class GitHubBackupConfig(Base):
class GitHubBackupLog (line 49) | class GitHubBackupLog(Base):
FILE: backend/app/models/group.py
class Group (line 27) | class Group(Base):
method __repr__ (line 53) | def __repr__(self) -> str:
FILE: backend/app/models/kprofile_note.py
class KProfileNote (line 11) | class KProfileNote(Base):
FILE: backend/app/models/library.py
class LibraryFolder (line 11) | class LibraryFolder(Base):
class LibraryFile (line 55) | class LibraryFile(Base):
FILE: backend/app/models/local_preset.py
class LocalPreset (line 11) | class LocalPreset(Base):
FILE: backend/app/models/maintenance.py
class MaintenanceType (line 11) | class MaintenanceType(Base):
class PrinterMaintenance (line 34) | class PrinterMaintenance(Base):
class MaintenanceHistory (line 64) | class MaintenanceHistory(Base):
FILE: backend/app/models/notification.py
class NotificationDigestQueue (line 11) | class NotificationDigestQueue(Base):
class NotificationLog (line 29) | class NotificationLog(Base):
class NotificationProvider (line 49) | class NotificationProvider(Base):
FILE: backend/app/models/notification_template.py
class NotificationTemplate (line 11) | class NotificationTemplate(Base):
FILE: backend/app/models/oidc_provider.py
class OIDCProvider (line 12) | class OIDCProvider(Base):
method client_secret (line 36) | def client_secret(self) -> str:
method client_secret (line 40) | def client_secret(self, value: str) -> None:
method __repr__ (line 65) | def __repr__(self) -> str:
class UserOIDCLink (line 69) | class UserOIDCLink(Base):
method __repr__ (line 92) | def __repr__(self) -> str:
FILE: backend/app/models/orca_base_cache.py
class OrcaBaseProfile (line 11) | class OrcaBaseProfile(Base):
FILE: backend/app/models/pending_upload.py
class PendingUpload (line 11) | class PendingUpload(Base):
FILE: backend/app/models/print_batch.py
class PrintBatch (line 9) | class PrintBatch(Base):
FILE: backend/app/models/print_log.py
class PrintLogEntry (line 9) | class PrintLogEntry(Base):
FILE: backend/app/models/print_queue.py
class PrintQueueItem (line 9) | class PrintQueueItem(Base):
FILE: backend/app/models/printer.py
class Printer (line 9) | class Printer(Base):
FILE: backend/app/models/project.py
class Project (line 9) | class Project(Base):
FILE: backend/app/models/project_bom.py
class ProjectBOMItem (line 9) | class ProjectBOMItem(Base):
FILE: backend/app/models/settings.py
class Settings (line 9) | class Settings(Base):
FILE: backend/app/models/slot_preset.py
class SlotPresetMapping (line 15) | class SlotPresetMapping(Base):
FILE: backend/app/models/smart_plug.py
class SmartPlug (line 9) | class SmartPlug(Base):
FILE: backend/app/models/smart_plug_energy_snapshot.py
class SmartPlugEnergySnapshot (line 9) | class SmartPlugEnergySnapshot(Base):
FILE: backend/app/models/spool.py
class Spool (line 9) | class Spool(Base):
FILE: backend/app/models/spool_assignment.py
class SpoolAssignment (line 9) | class SpoolAssignment(Base):
method printer_name (line 29) | def printer_name(self) -> str | None:
FILE: backend/app/models/spool_catalog.py
class SpoolCatalogEntry (line 9) | class SpoolCatalogEntry(Base):
FILE: backend/app/models/spool_k_profile.py
class SpoolKProfile (line 9) | class SpoolKProfile(Base):
FILE: backend/app/models/spool_usage_history.py
class SpoolUsageHistory (line 9) | class SpoolUsageHistory(Base):
FILE: backend/app/models/spoolbuddy_device.py
class SpoolBuddyDevice (line 9) | class SpoolBuddyDevice(Base):
FILE: backend/app/models/user.py
class User (line 16) | class User(Base):
method is_admin (line 66) | def is_admin(self) -> bool:
method get_permissions (line 77) | def get_permissions(self) -> set[str]:
method has_permission (line 88) | def has_permission(self, permission: str) -> bool:
method has_all_permissions (line 98) | def has_all_permissions(self, *permissions: str) -> bool:
method has_any_permission (line 109) | def has_any_permission(self, *permissions: str) -> bool:
method __repr__ (line 120) | def __repr__(self) -> str:
FILE: backend/app/models/user_email_pref.py
class UserEmailPreference (line 17) | class UserEmailPreference(Base):
FILE: backend/app/models/user_otp_code.py
class UserOTPCode (line 11) | class UserOTPCode(Base):
method consume (line 35) | def consume(self) -> None:
method __repr__ (line 54) | def __repr__(self) -> str:
FILE: backend/app/models/user_totp.py
class UserTOTP (line 14) | class UserTOTP(Base):
method secret (line 40) | def secret(self) -> str:
method secret (line 45) | def secret(self, value: str) -> None:
method backup_code_hashes (line 50) | def backup_code_hashes(self) -> list[str]:
method backup_code_hashes (line 62) | def backup_code_hashes(self, hashes: list[str]) -> None:
method accept_counter (line 66) | def accept_counter(self, new_counter: int) -> None:
method __repr__ (line 83) | def __repr__(self) -> str:
FILE: backend/app/models/virtual_printer.py
class VirtualPrinter (line 9) | class VirtualPrinter(Base):
FILE: backend/app/schemas/api_key.py
class APIKeyCreate (line 6) | class APIKeyCreate(BaseModel):
class APIKeyUpdate (line 17) | class APIKeyUpdate(BaseModel):
class APIKeyResponse (line 29) | class APIKeyResponse(BaseModel):
class Config (line 44) | class Config:
class APIKeyCreateResponse (line 48) | class APIKeyCreateResponse(APIKeyResponse):
FILE: backend/app/schemas/archive.py
class ArchiveBase (line 6) | class ArchiveBase(BaseModel):
class ArchiveUpdate (line 18) | class ArchiveUpdate(ArchiveBase):
class ArchiveDuplicate (line 25) | class ArchiveDuplicate(BaseModel):
class ArchiveResponse (line 34) | class ArchiveResponse(BaseModel):
method compute_object_count (line 103) | def compute_object_count(self) -> "ArchiveResponse":
class Config (line 111) | class Config:
class ArchiveSlim (line 115) | class ArchiveSlim(BaseModel):
class Config (line 132) | class Config:
class ArchiveStats (line 136) | class ArchiveStats(BaseModel):
class ProjectPageImage (line 158) | class ProjectPageImage(BaseModel):
class ProjectPageResponse (line 166) | class ProjectPageResponse(BaseModel):
class ProjectPageUpdate (line 198) | class ProjectPageUpdate(BaseModel):
class ReprintRequest (line 210) | class ReprintRequest(BaseModel):
FILE: backend/app/schemas/auth.py
function _validate_password_complexity (line 7) | def _validate_password_complexity(v: str) -> str:
class GroupBrief (line 24) | class GroupBrief(BaseModel):
class Config (line 30) | class Config:
class LoginRequest (line 34) | class LoginRequest(BaseModel):
class LoginResponse (line 39) | class LoginResponse(BaseModel):
class UserCreate (line 49) | class UserCreate(BaseModel):
method validate_password (line 58) | def validate_password(cls, v: str | None) -> str | None:
class UserUpdate (line 64) | class UserUpdate(BaseModel):
method validate_password (line 74) | def validate_password(cls, v: str | None) -> str | None:
class UserResponse (line 80) | class UserResponse(BaseModel):
class Config (line 92) | class Config:
class ChangePasswordRequest (line 96) | class ChangePasswordRequest(BaseModel):
method validate_new_password (line 102) | def validate_new_password(cls, v: str) -> str:
class SetupRequest (line 106) | class SetupRequest(BaseModel):
method validate_admin_password (line 113) | def validate_admin_password(cls, v: str | None) -> str | None:
class SetupResponse (line 119) | class SetupResponse(BaseModel):
class ForgotPasswordRequest (line 124) | class ForgotPasswordRequest(BaseModel):
class ForgotPasswordConfirmRequest (line 128) | class ForgotPasswordConfirmRequest(BaseModel):
method validate_new_password (line 134) | def validate_new_password(cls, v: str) -> str:
class ForgotPasswordResponse (line 138) | class ForgotPasswordResponse(BaseModel):
class ResetPasswordRequest (line 142) | class ResetPasswordRequest(BaseModel):
class ResetPasswordResponse (line 146) | class ResetPasswordResponse(BaseModel):
class SMTPSettings (line 150) | class SMTPSettings(BaseModel):
class TestSMTPRequest (line 163) | class TestSMTPRequest(BaseModel):
class TestSMTPResponse (line 167) | class TestSMTPResponse(BaseModel):
class TwoFAStatusResponse (line 177) | class TwoFAStatusResponse(BaseModel):
class TOTPSetupResponse (line 183) | class TOTPSetupResponse(BaseModel):
class TOTPSetupRequest (line 193) | class TOTPSetupRequest(BaseModel):
class TOTPEnableRequest (line 204) | class TOTPEnableRequest(BaseModel):
method validate_code (line 209) | def validate_code(cls, v: str) -> str:
class TOTPEnableResponse (line 216) | class TOTPEnableResponse(BaseModel):
class TOTPDisableRequest (line 221) | class TOTPDisableRequest(BaseModel):
class BackupCodesResponse (line 227) | class BackupCodesResponse(BaseModel):
class EmailOTPEnableRequest (line 232) | class EmailOTPEnableRequest(BaseModel):
class TwoFAVerifyRequest (line 238) | class TwoFAVerifyRequest(BaseModel):
method validate_code_format (line 247) | def validate_code_format(cls, v: str) -> str:
class TwoFAVerifyResponse (line 254) | class TwoFAVerifyResponse(BaseModel):
class EmailOTPSendRequest (line 260) | class EmailOTPSendRequest(BaseModel):
class EmailOTPEnableConfirmRequest (line 264) | class EmailOTPEnableConfirmRequest(BaseModel):
method validate_code_digits (line 273) | def validate_code_digits(cls, v: str) -> str:
class EmailOTPDisableRequest (line 280) | class EmailOTPDisableRequest(BaseModel):
class AdminDisable2FARequest (line 286) | class AdminDisable2FARequest(BaseModel):
function _validate_icon_url (line 300) | def _validate_icon_url(v: str | None) -> str | None:
function _validate_issuer_url (line 309) | def _validate_issuer_url(v: str | None) -> str | None:
function _validate_scopes (line 335) | def _validate_scopes(v: str | None) -> str | None:
class OIDCProviderCreate (line 350) | class OIDCProviderCreate(BaseModel):
method validate_issuer_url (line 363) | def validate_issuer_url(cls, v: str) -> str:
method validate_scopes (line 370) | def validate_scopes(cls, v: str) -> str:
method validate_icon_url (line 377) | def validate_icon_url(cls, v: str | None) -> str | None:
class OIDCProviderUpdate (line 381) | class OIDCProviderUpdate(BaseModel):
method validate_issuer_url (line 387) | def validate_issuer_url(cls, v: str | None) -> str | None:
method validate_scopes (line 400) | def validate_scopes(cls, v: str | None) -> str | None:
method validate_icon_url (line 405) | def validate_icon_url(cls, v: str | None) -> str | None:
class OIDCProviderResponse (line 409) | class OIDCProviderResponse(BaseModel):
class Config (line 420) | class Config:
class OIDCAuthorizeResponse (line 424) | class OIDCAuthorizeResponse(BaseModel):
class OIDCExchangeRequest (line 428) | class OIDCExchangeRequest(BaseModel):
class OIDCLinkResponse (line 432) | class OIDCLinkResponse(BaseModel):
FILE: backend/app/schemas/cloud.py
class CloudLoginRequest (line 8) | class CloudLoginRequest(BaseModel):
class CloudVerifyRequest (line 16) | class CloudVerifyRequest(BaseModel):
class CloudLoginResponse (line 25) | class CloudLoginResponse(BaseModel):
class CloudAuthStatus (line 35) | class CloudAuthStatus(BaseModel):
class CloudTokenRequest (line 43) | class CloudTokenRequest(BaseModel):
class SlicerSetting (line 50) | class SlicerSetting(BaseModel):
class SlicerSettingsResponse (line 62) | class SlicerSettingsResponse(BaseModel):
class CloudDevice (line 70) | class CloudDevice(BaseModel):
class SlicerSettingCreate (line 80) | class SlicerSettingCreate(BaseModel):
class SlicerSettingUpdate (line 90) | class SlicerSettingUpdate(BaseModel):
class SlicerSettingDetail (line 97) | class SlicerSettingDetail(BaseModel):
class SlicerSettingDeleteResponse (line 115) | class SlicerSettingDeleteResponse(BaseModel):
class FirmwareUpdateInfo (line 122) | class FirmwareUpdateInfo(BaseModel):
class FirmwareUpdatesResponse (line 133) | class FirmwareUpdatesResponse(BaseModel):
FILE: backend/app/schemas/external_link.py
class ExternalLinkBase (line 6) | class ExternalLinkBase(BaseModel):
method validate_url (line 16) | def validate_url(cls, v: str) -> str:
class ExternalLinkCreate (line 23) | class ExternalLinkCreate(ExternalLinkBase):
class ExternalLinkUpdate (line 29) | class ExternalLinkUpdate(BaseModel):
method validate_url (line 39) | def validate_url(cls, v: str | None) -> str | None:
class ExternalLinkResponse (line 46) | class ExternalLinkResponse(ExternalLinkBase):
class ExternalLinkReorder (line 59) | class ExternalLinkReorder(BaseModel):
FILE: backend/app/schemas/filament.py
class FilamentBase (line 6) | class FilamentBase(BaseModel):
class FilamentCreate (line 22) | class FilamentCreate(FilamentBase):
class FilamentUpdate (line 26) | class FilamentUpdate(BaseModel):
class FilamentResponse (line 42) | class FilamentResponse(FilamentBase):
class Config (line 47) | class Config:
class FilamentCostCalculation (line 51) | class FilamentCostCalculation(BaseModel):
FILE: backend/app/schemas/github_backup.py
class ScheduleType (line 11) | class ScheduleType(StrEnum):
class GitHubBackupConfigCreate (line 19) | class GitHubBackupConfigCreate(BaseModel):
method validate_repo_url (line 39) | def validate_repo_url(cls, v: str) -> str:
class GitHubBackupConfigUpdate (line 52) | class GitHubBackupConfigUpdate(BaseModel):
method validate_repo_url (line 72) | def validate_repo_url(cls, v: str | None) -> str | None:
class GitHubBackupConfigResponse (line 85) | class GitHubBackupConfigResponse(BaseModel):
class Config (line 112) | class Config:
class GitHubBackupLogResponse (line 116) | class GitHubBackupLogResponse(BaseModel):
class Config (line 129) | class Config:
class GitHubBackupStatus (line 133) | class GitHubBackupStatus(BaseModel):
class GitHubTestConnectionResponse (line 145) | class GitHubTestConnectionResponse(BaseModel):
class GitHubBackupTriggerResponse (line 154) | class GitHubBackupTriggerResponse(BaseModel):
FILE: backend/app/schemas/group.py
class GroupBrief (line 8) | class GroupBrief(BaseModel):
class Config (line 14) | class Config:
class GroupCreate (line 18) | class GroupCreate(BaseModel):
class GroupUpdate (line 26) | class GroupUpdate(BaseModel):
class GroupResponse (line 34) | class GroupResponse(BaseModel):
class Config (line 46) | class Config:
class GroupDetailResponse (line 50) | class GroupDetailResponse(GroupResponse):
class UserBrief (line 56) | class UserBrief(BaseModel):
class Config (line 63) | class Config:
class PermissionInfo (line 67) | class PermissionInfo(BaseModel):
class PermissionCategory (line 74) | class PermissionCategory(BaseModel):
class PermissionsListResponse (line 81) | class PermissionsListResponse(BaseModel):
FILE: backend/app/schemas/kprofile.py
class KProfile (line 6) | class KProfile(BaseModel):
class KProfileCreate (line 22) | class KProfileCreate(BaseModel):
class KProfilesResponse (line 38) | class KProfilesResponse(BaseModel):
class KProfileDelete (line 45) | class KProfileDelete(BaseModel):
class KProfileNote (line 56) | class KProfileNote(BaseModel):
class KProfileNoteResponse (line 63) | class KProfileNoteResponse(BaseModel):
FILE: backend/app/schemas/library.py
class FolderCreate (line 10) | class FolderCreate(BaseModel):
class ExternalFolderCreate (line 19) | class ExternalFolderCreate(BaseModel):
class FolderUpdate (line 29) | class FolderUpdate(BaseModel):
class FolderResponse (line 38) | class FolderResponse(BaseModel):
class Config (line 56) | class Config:
class FolderTreeItem (line 60) | class FolderTreeItem(BaseModel):
class Config (line 76) | class Config:
class FileCreate (line 83) | class FileCreate(BaseModel):
class FileUpdate (line 97) | class FileUpdate(BaseModel):
class FileDuplicate (line 106) | class FileDuplicate(BaseModel):
class FileResponse (line 116) | class FileResponse(BaseModel):
class Config (line 157) | class Config:
class FileListResponse (line 161) | class FileListResponse(BaseModel):
class Config (line 184) | class Config:
class FileMoveRequest (line 188) | class FileMoveRequest(BaseModel):
class FilePrintRequest (line 195) | class FilePrintRequest(BaseModel):
class FileUploadResponse (line 221) | class FileUploadResponse(BaseModel):
class BulkDeleteRequest (line 236) | class BulkDeleteRequest(BaseModel):
class BulkDeleteResponse (line 243) | class BulkDeleteResponse(BaseModel):
class AddToQueueRequest (line 253) | class AddToQueueRequest(BaseModel):
class AddToQueueResult (line 259) | class AddToQueueResult(BaseModel):
class AddToQueueError (line 267) | class AddToQueueError(BaseModel):
class AddToQueueResponse (line 275) | class AddToQueueResponse(BaseModel):
class ZipExtractResult (line 285) | class ZipExtractResult(BaseModel):
class ZipExtractError (line 293) | class ZipExtractError(BaseModel):
class ZipExtractResponse (line 300) | class ZipExtractResponse(BaseModel):
class BatchThumbnailRequest (line 312) | class BatchThumbnailRequest(BaseModel):
class BatchThumbnailResult (line 320) | class BatchThumbnailResult(BaseModel):
class BatchThumbnailResponse (line 329) | class BatchThumbnailResponse(BaseModel):
FILE: backend/app/schemas/local_preset.py
class LocalPresetResponse (line 8) | class LocalPresetResponse(BaseModel):
class LocalPresetDetail (line 32) | class LocalPresetDetail(LocalPresetResponse):
class LocalPresetCreate (line 38) | class LocalPresetCreate(BaseModel):
class LocalPresetUpdate (line 46) | class LocalPresetUpdate(BaseModel):
class LocalPresetsResponse (line 53) | class LocalPresetsResponse(BaseModel):
class ImportResponse (line 61) | class ImportResponse(BaseModel):
FILE: backend/app/schemas/maintenance.py
class MaintenanceTypeBase (line 9) | class MaintenanceTypeBase(BaseModel):
class MaintenanceTypeCreate (line 19) | class MaintenanceTypeCreate(MaintenanceTypeBase):
class MaintenanceTypeUpdate (line 23) | class MaintenanceTypeUpdate(BaseModel):
class MaintenanceTypeResponse (line 32) | class MaintenanceTypeResponse(MaintenanceTypeBase):
class Config (line 37) | class Config:
class PrinterMaintenanceBase (line 42) | class PrinterMaintenanceBase(BaseModel):
class PrinterMaintenanceCreate (line 49) | class PrinterMaintenanceCreate(PrinterMaintenanceBase):
class PrinterMaintenanceUpdate (line 53) | class PrinterMaintenanceUpdate(BaseModel):
class PrinterMaintenanceResponse (line 59) | class PrinterMaintenanceResponse(BaseModel):
class Config (line 71) | class Config:
class MaintenanceHistoryBase (line 76) | class MaintenanceHistoryBase(BaseModel):
class MaintenanceHistoryCreate (line 80) | class MaintenanceHistoryCreate(MaintenanceHistoryBase):
class MaintenanceHistoryResponse (line 84) | class MaintenanceHistoryResponse(MaintenanceHistoryBase):
class Config (line 90) | class Config:
class MaintenanceStatus (line 95) | class MaintenanceStatus(BaseModel):
class PrinterMaintenanceOverview (line 123) | class PrinterMaintenanceOverview(BaseModel):
class PerformMaintenanceRequest (line 135) | class PerformMaintenanceRequest(BaseModel):
FILE: backend/app/schemas/notification.py
class ProviderType (line 11) | class ProviderType(StrEnum):
class NotificationProviderBase (line 24) | class NotificationProviderBase(BaseModel):
method validate_time_format (line 91) | def validate_time_format(cls, v: str | None) -> str | None:
class NotificationProviderCreate (line 106) | class NotificationProviderCreate(NotificationProviderBase):
class NotificationProviderUpdate (line 112) | class NotificationProviderUpdate(BaseModel):
class NotificationProviderResponse (line 173) | class NotificationProviderResponse(NotificationProviderBase):
class Config (line 183) | class Config:
class NotificationTestRequest (line 187) | class NotificationTestRequest(BaseModel):
class NotificationTestResponse (line 194) | class NotificationTestResponse(BaseModel):
class CallMeBotConfig (line 202) | class CallMeBotConfig(BaseModel):
class NtfyConfig (line 209) | class NtfyConfig(BaseModel):
class PushoverConfig (line 217) | class PushoverConfig(BaseModel):
class TelegramConfig (line 225) | class TelegramConfig(BaseModel):
class EmailConfig (line 232) | class EmailConfig(BaseModel):
class NotificationLogResponse (line 245) | class NotificationLogResponse(BaseModel):
class Config (line 261) | class Config:
class NotificationLogStats (line 265) | class NotificationLogStats(BaseModel):
FILE: backend/app/schemas/notification_template.py
class EventType (line 10) | class EventType(StrEnum):
class NotificationTemplateBase (line 307) | class NotificationTemplateBase(BaseModel):
class NotificationTemplateUpdate (line 314) | class NotificationTemplateUpdate(BaseModel):
class NotificationTemplateResponse (line 321) | class NotificationTemplateResponse(NotificationTemplateBase):
class Config (line 331) | class Config:
class TemplateVariableInfo (line 335) | class TemplateVariableInfo(BaseModel):
class EventVariablesResponse (line 342) | class EventVariablesResponse(BaseModel):
class TemplatePreviewRequest (line 350) | class TemplatePreviewRequest(BaseModel):
class TemplatePreviewResponse (line 358) | class TemplatePreviewResponse(BaseModel):
FILE: backend/app/schemas/print_log.py
class PrintLogEntrySchema (line 6) | class PrintLogEntrySchema(BaseModel):
class PrintLogResponse (line 23) | class PrintLogResponse(BaseModel):
FILE: backend/app/schemas/print_queue.py
function serialize_utc_datetime (line 8) | def serialize_utc_datetime(dt: datetime | None) -> str | None:
class PrintQueueItemCreate (line 18) | class PrintQueueItemCreate(BaseModel):
class PrintQueueItemUpdate (line 51) | class PrintQueueItemUpdate(BaseModel):
class PrintQueueItemResponse (line 74) | class PrintQueueItemResponse(BaseModel):
class Config (line 132) | class Config:
class PrintQueueReorderItem (line 136) | class PrintQueueReorderItem(BaseModel):
class PrintQueueReorder (line 141) | class PrintQueueReorder(BaseModel):
class PrintQueueBulkUpdate (line 145) | class PrintQueueBulkUpdate(BaseModel):
class PrintQueueBulkUpdateResponse (line 166) | class PrintQueueBulkUpdateResponse(BaseModel):
class PrintBatchResponse (line 174) | class PrintBatchResponse(BaseModel):
class Config (line 193) | class Config:
FILE: backend/app/schemas/printer.py
class PrinterBase (line 6) | class PrinterBase(BaseModel):
class PrinterCreate (line 24) | class PrinterCreate(PrinterBase):
class PlateDetectionROI (line 28) | class PlateDetectionROI(BaseModel):
class PrinterUpdate (line 37) | class PrinterUpdate(BaseModel):
class PrinterResponse (line 58) | class PrinterResponse(PrinterBase):
class Config (line 72) | class Config:
method from_orm_with_roi (line 76) | def from_orm_with_roi(cls, printer) -> "PrinterResponse":
class HMSErrorResponse (line 116) | class HMSErrorResponse(BaseModel):
class AMSTray (line 123) | class AMSTray(BaseModel):
class AMSUnit (line 142) | class AMSUnit(BaseModel):
class NozzleInfoResponse (line 157) | class NozzleInfoResponse(BaseModel):
class NozzleRackSlot (line 162) | class NozzleRackSlot(BaseModel):
class AmsLabelBody (line 177) | class AmsLabelBody(BaseModel):
class PrintOptionsResponse (line 182) | class PrintOptionsResponse(BaseModel):
class PrinterStatus (line 204) | class PrinterStatus(BaseModel):
FILE: backend/app/schemas/project.py
class ProjectCreate (line 6) | class ProjectCreate(BaseModel):
class ProjectUpdate (line 22) | class ProjectUpdate(BaseModel):
class ProjectStats (line 39) | class ProjectStats(BaseModel):
class ProjectChildPreview (line 64) | class ProjectChildPreview(BaseModel):
class ProjectResponse (line 74) | class ProjectResponse(BaseModel):
class Config (line 99) | class Config:
class ArchivePreview (line 103) | class ArchivePreview(BaseModel):
class ProjectListResponse (line 114) | class ProjectListResponse(BaseModel):
class Config (line 136) | class Config:
class BatchAddArchives (line 140) | class BatchAddArchives(BaseModel):
class BatchAddQueueItems (line 146) | class BatchAddQueueItems(BaseModel):
class BOMItemCreate (line 153) | class BOMItemCreate(BaseModel):
class BOMItemUpdate (line 165) | class BOMItemUpdate(BaseModel):
class BOMItemResponse (line 178) | class BOMItemResponse(BaseModel):
class Config (line 197) | class Config:
class TimelineEvent (line 202) | class TimelineEvent(BaseModel):
class BOMItemExport (line 213) | class BOMItemExport(BaseModel):
class LinkedFolderExport (line 225) | class LinkedFolderExport(BaseModel):
class ProjectExport (line 231) | class ProjectExport(BaseModel):
class ProjectImport (line 249) | class ProjectImport(BaseModel):
FILE: backend/app/schemas/settings.py
class AppSettings (line 6) | class AppSettings(BaseModel):
class AppSettingsUpdate (line 291) | class AppSettingsUpdate(BaseModel):
method validate_gcode_snippets (line 394) | def validate_gcode_snippets(cls, v: str | None) -> str | None:
method validate_ldap_group_mapping (line 407) | def validate_ldap_group_mapping(cls, v: str | None) -> str | None:
method validate_obico_enabled_printers (line 420) | def validate_obico_enabled_printers(cls, v: str | None) -> str | None:
method validate_obico_sensitivity (line 433) | def validate_obico_sensitivity(cls, v: str | None) -> str | None:
method validate_obico_action (line 442) | def validate_obico_action(cls, v: str | None) -> str | None:
method validate_default_sidebar_order (line 451) | def validate_default_sidebar_order(cls, v: str | None) -> str | None:
FILE: backend/app/schemas/smart_plug.py
class SmartPlugBase (line 7) | class SmartPlugBase(BaseModel):
method validate_plug_type_fields (line 85) | def validate_plug_type_fields(self) -> "SmartPlugBase":
class SmartPlugCreate (line 107) | class SmartPlugCreate(SmartPlugBase):
class SmartPlugUpdate (line 111) | class SmartPlugUpdate(BaseModel):
class SmartPlugResponse (line 174) | class SmartPlugResponse(SmartPlugBase):
class Config (line 183) | class Config:
class SmartPlugControl (line 187) | class SmartPlugControl(BaseModel):
class SmartPlugEnergy (line 191) | class SmartPlugEnergy(BaseModel):
class SmartPlugStatus (line 205) | class SmartPlugStatus(BaseModel):
class SmartPlugTestConnection (line 212) | class SmartPlugTestConnection(BaseModel):
class HATestConnectionRequest (line 219) | class HATestConnectionRequest(BaseModel):
class HATestConnectionResponse (line 226) | class HATestConnectionResponse(BaseModel):
class HAEntity (line 234) | class HAEntity(BaseModel):
class HASensorEntity (line 243) | class HASensorEntity(BaseModel):
class RESTTestConnectionRequest (line 252) | class RESTTestConnectionRequest(BaseModel):
class RESTTestConnectionResponse (line 260) | class RESTTestConnectionResponse(BaseModel):
FILE: backend/app/schemas/spool.py
class SpoolBase (line 6) | class SpoolBase(BaseModel):
class SpoolCreate (line 31) | class SpoolCreate(SpoolBase):
class SpoolBulkCreate (line 35) | class SpoolBulkCreate(BaseModel):
class SpoolUpdate (line 40) | class SpoolUpdate(BaseModel):
class SpoolKProfileBase (line 63) | class SpoolKProfileBase(BaseModel):
class SpoolKProfileResponse (line 74) | class SpoolKProfileResponse(SpoolKProfileBase):
class Config (line 79) | class Config:
class SpoolResponse (line 83) | class SpoolResponse(SpoolBase):
class Config (line 102) | class Config:
class SpoolAssignmentCreate (line 106) | class SpoolAssignmentCreate(BaseModel):
class SpoolAssignmentResponse (line 113) | class SpoolAssignmentResponse(BaseModel):
class Config (line 127) | class Config:
FILE: backend/app/schemas/spool_usage.py
class SpoolUsageHistoryResponse (line 6) | class SpoolUsageHistoryResponse(BaseModel):
class Config (line 17) | class Config:
FILE: backend/app/schemas/spoolbuddy.py
class DeviceRegisterRequest (line 8) | class DeviceRegisterRequest(BaseModel):
class DeviceResponse (line 23) | class DeviceResponse(BaseModel):
class Config (line 53) | class Config:
class HeartbeatRequest (line 57) | class HeartbeatRequest(BaseModel):
class HeartbeatResponse (line 69) | class HeartbeatResponse(BaseModel):
class TagScannedRequest (line 82) | class TagScannedRequest(BaseModel):
class TagRemovedRequest (line 91) | class TagRemovedRequest(BaseModel):
class ScaleReadingRequest (line 99) | class ScaleReadingRequest(BaseModel):
class UpdateSpoolWeightRequest (line 106) | class UpdateSpoolWeightRequest(BaseModel):
class SetTareRequest (line 114) | class SetTareRequest(BaseModel):
class SetCalibrationFactorRequest (line 118) | class SetCalibrationFactorRequest(BaseModel):
class CalibrationResponse (line 124) | class CalibrationResponse(BaseModel):
class WriteTagRequest (line 132) | class WriteTagRequest(BaseModel):
class WriteTagResultRequest (line 137) | class WriteTagResultRequest(BaseModel):
class DisplaySettingsRequest (line 145) | class DisplaySettingsRequest(BaseModel):
class SystemConfigRequest (line 150) | class SystemConfigRequest(BaseModel):
class SystemCommandRequest (line 155) | class SystemCommandRequest(BaseModel):
class SystemCommandResultRequest (line 159) | class SystemCommandResultRequest(BaseModel):
class DiagnosticResultRequest (line 168) | class DiagnosticResultRequest(BaseModel):
FILE: backend/app/schemas/timelapse.py
class TimelapseInfoResponse (line 6) | class TimelapseInfoResponse(BaseModel):
class ThumbnailResponse (line 18) | class ThumbnailResponse(BaseModel):
class ProcessResponse (line 25) | class ProcessResponse(BaseModel):
FILE: backend/app/schemas/user_notifications.py
class UserEmailPreferenceResponse (line 6) | class UserEmailPreferenceResponse(BaseModel):
class Config (line 14) | class Config:
class UserEmailPreferenceUpdate (line 18) | class UserEmailPreferenceUpdate(BaseModel):
FILE: backend/app/services/archive.py
function _copy_and_fsync (line 23) | def _copy_and_fsync(src: Path, dst: Path, chunk_size: int = 1024 * 1024)...
class ThreeMFParser (line 44) | class ThreeMFParser:
method __init__ (line 47) | def __init__(self, file_path: Path, plate_number: int | None = None):
method parse (line 52) | def parse(self) -> dict:
method _parse_slice_info (line 93) | def _parse_slice_info(self, zf: zipfile.ZipFile):
method _parse_project_settings (line 208) | def _parse_project_settings(self, zf: zipfile.ZipFile):
method _parse_gcode_header (line 222) | def _parse_gcode_header(self, zf: zipfile.ZipFile):
method _extract_filament_info (line 252) | def _extract_filament_info(self, data: dict):
method _extract_print_settings (line 293) | def _extract_print_settings(self, data: dict):
method _extract_settings_from_content (line 340) | def _extract_settings_from_content(self, content: str):
method _parse_3dmodel (line 364) | def _parse_3dmodel(self, zf: zipfile.ZipFile):
method _extract_thumbnail (line 409) | def _extract_thumbnail(self, zf: zipfile.ZipFile):
function extract_printable_objects_from_3mf (line 436) | def extract_printable_objects_from_3mf(
class ProjectPageParser (line 539) | class ProjectPageParser:
method __init__ (line 542) | def __init__(self, file_path: Path):
method parse (line 545) | def parse(self, archive_id: int) -> dict:
method get_image (line 656) | def get_image(self, image_path: str) -> tuple[bytes, str] | None:
method update_metadata (line 680) | def update_metadata(self, updates: dict) -> bool:
class ArchiveService (line 744) | class ArchiveService:
method __init__ (line 747) | def __init__(self, db: AsyncSession):
method compute_file_hash (line 751) | def compute_file_hash(file_path: Path) -> str:
method get_duplicate_hashes_and_names (line 760) | async def get_duplicate_hashes_and_names(self) -> tuple[set[str], set[...
method find_duplicates (line 791) | async def find_duplicates(
method archive_print (line 880) | async def archive_print(
method get_archive (line 1061) | async def get_archive(self, archive_id: int) -> PrintArchive | None:
method update_archive_status (line 1072) | async def update_archive_status(
method list_archives (line 1093) | async def list_archives(
method delete_archive (line 1129) | async def delete_archive(self, archive_id: int) -> bool:
method attach_timelapse (line 1187) | async def attach_timelapse(
function _convert_timelapse_to_mp4 (line 1227) | async def _convert_timelapse_to_mp4(archive_id: int, source_path: Path) ...
FILE: backend/app/services/archive_comparison.py
class ArchiveComparisonService (line 8) | class ArchiveComparisonService:
method __init__ (line 24) | def __init__(self, db: AsyncSession):
method compare_archives (line 27) | async def compare_archives(self, archive_ids: list[int]) -> dict:
method _analyze_success_correlation (line 117) | def _analyze_success_correlation(self, archives: list[PrintArchive]) -...
method find_similar_archives (line 180) | async def find_similar_archives(
FILE: backend/app/services/background_dispatch.py
class DispatchJobCancelled (line 38) | class DispatchJobCancelled(Exception):
class DispatchEnqueueRejected (line 42) | class DispatchEnqueueRejected(Exception):
class PrintDispatchJob (line 47) | class PrintDispatchJob:
class ActiveDispatchState (line 62) | class ActiveDispatchState:
class BackgroundDispatchService (line 69) | class BackgroundDispatchService:
method __init__ (line 70) | def __init__(self):
method _printer_is_busy_printing (line 86) | def _printer_is_busy_printing(printer_id: int) -> bool:
method start (line 92) | async def start(self):
method stop (line 99) | async def stop(self):
method dispatch_reprint_archive (line 128) | async def dispatch_reprint_archive(
method get_state (line 150) | async def get_state(self) -> dict[str, Any]:
method dispatch_print_library_file (line 155) | async def dispatch_print_library_file(
method cancel_job (line 181) | async def cancel_job(self, job_id: int) -> dict[str, Any]:
method _dispatch (line 255) | async def _dispatch(
method _dispatcher_loop (line 320) | async def _dispatcher_loop(self):
method _run_active_job (line 368) | async def _run_active_job(self, job: PrintDispatchJob):
method _set_active_message (line 382) | async def _set_active_message(self, job: PrintDispatchJob, message: str):
method _set_active_upload_progress (line 400) | async def _set_active_upload_progress(self, job: PrintDispatchJob, upl...
method _mark_job_finished (line 420) | async def _mark_job_finished(self, job: PrintDispatchJob, *, failed: b...
method _mark_job_cancelled (line 452) | async def _mark_job_cancelled(self, job: PrintDispatchJob):
method _is_cancel_requested (line 476) | def _is_cancel_requested(self, job_id: int) -> bool:
method _raise_if_cancel_requested (line 479) | def _raise_if_cancel_requested(self, job: PrintDispatchJob):
method _build_state_payload_unlocked (line 483) | def _build_state_payload_unlocked(self, recent_event: dict[str, Any] |...
method _process_job (line 537) | async def _process_job(self, job: PrintDispatchJob):
method _run_reprint_archive (line 546) | async def _run_reprint_archive(self, job: PrintDispatchJob):
method _run_print_library_file (line 701) | async def _run_print_library_file(self, job: PrintDispatchJob):
method _verify_print_response (line 898) | async def _verify_print_response(
method _cleanup_sd_card_file (line 936) | async def _cleanup_sd_card_file(
method _resolve_plate_id (line 949) | def _resolve_plate_id(file_path: Path, requested_plate_id: int | None)...
method _is_sliced_file (line 966) | def _is_sliced_file(filename: str) -> bool:
FILE: backend/app/services/bambu_cloud.py
class BambuCloudError (line 18) | class BambuCloudError(Exception):
class BambuCloudAuthError (line 24) | class BambuCloudAuthError(BambuCloudError):
function set_shared_http_client (line 33) | def set_shared_http_client(client: httpx.AsyncClient | None) -> None:
class BambuCloudService (line 45) | class BambuCloudService:
method __init__ (line 48) | def __init__(self, region: str = "global", client: httpx.AsyncClient |...
method is_authenticated (line 67) | def is_authenticated(self) -> bool:
method _get_headers (line 73) | def _get_headers(self) -> dict:
method login_request (line 83) | async def login_request(self, email: str, password: str) -> dict:
method verify_code (line 141) | async def verify_code(self, email: str, code: str) -> dict:
method verify_totp (line 168) | async def verify_totp(self, tfa_key: str, code: str) -> dict:
method _set_tokens (line 248) | def _set_tokens(self, data: dict):
method set_token (line 255) | def set_token(self, access_token: str):
method logout (line 260) | def logout(self):
method get_user_profile (line 266) | async def get_user_profile(self) -> dict:
method get_slicer_settings (line 284) | async def get_slicer_settings(self, version: str = "02.04.00.70") -> d...
method get_setting_detail (line 311) | async def get_setting_detail(self, setting_id: str) -> dict:
method create_setting (line 329) | async def create_setting(
method update_setting (line 378) | async def update_setting(self, setting_id: str, name: str | None = Non...
method delete_setting (line 463) | async def delete_setting(self, setting_id: str) -> dict:
method get_devices (line 491) | async def get_devices(self) -> dict:
method get_firmware_version (line 509) | async def get_firmware_version(self, device_id: str) -> dict:
method close (line 539) | async def close(self):
FILE: backend/app/services/bambu_ftp.py
class FileNotOnPrinterError (line 20) | class FileNotOnPrinterError(Exception):
class ImplicitFTP_TLS (line 31) | class ImplicitFTP_TLS(FTP_TLS):
method __init__ (line 40) | def __init__(self, *args, skip_session_reuse: bool = False, **kwargs):
method connect (line 48) | def connect(self, host="", port=990, timeout=-999, source_address=None):
method ntransfercmd (line 67) | def ntransfercmd(self, cmd, rest=None):
class BambuFTPClient (line 88) | class BambuFTPClient:
method __init__ (line 106) | def __init__(
method _is_a1_model (line 121) | def _is_a1_model(self) -> bool:
method _get_cached_mode (line 127) | def _get_cached_mode(self) -> str | None:
method cache_mode (line 132) | def cache_mode(cls, ip_address: str, mode: str):
method _should_use_prot_c (line 137) | def _should_use_prot_c(self) -> bool:
method connect (line 149) | def connect(self) -> bool:
method disconnect (line 194) | def disconnect(self):
method list_files (line 203) | def list_files(self, path: str = "/") -> list[dict]:
method download_file (line 262) | def download_file(self, remote_path: str) -> bytes | None:
method download_to_file (line 274) | def download_to_file(self, remote_path: str, local_path: Path) -> bool:
method diagnose_storage (line 312) | def diagnose_storage(self) -> dict:
method upload_file (line 368) | def upload_file(
method upload_bytes (line 504) | def upload_bytes(self, data: bytes, remote_path: str) -> bool:
method delete_file (line 544) | def delete_file(self, remote_path: str) -> bool:
method get_file_size (line 556) | def get_file_size(self, remote_path: str) -> int | None:
method get_storage_info (line 566) | def get_storage_info(self) -> dict | None:
function normalize_3mf_name (line 634) | def normalize_3mf_name(name: str) -> str:
function cache_3mf_download (line 649) | def cache_3mf_download(printer_id: int, name: str, local_path: Path) -> ...
function get_cached_3mf (line 654) | def get_cached_3mf(printer_id: int, name: str) -> Path | None:
function clear_3mf_cache (line 667) | def clear_3mf_cache(printer_id: int | None = None, delete_files: bool = ...
function download_file_async (line 692) | async def download_file_async(
function download_file_try_paths_async (line 806) | async def download_file_try_paths_async(
function upload_file_async (line 844) | async def upload_file_async(
function list_files_async (line 922) | async def list_files_async(
function delete_file_async (line 954) | async def delete_file_async(
function download_file_bytes_async (line 981) | async def download_file_bytes_async(
function get_storage_info_async (line 1008) | async def get_storage_info_async(
function get_ftp_retry_settings (line 1034) | async def get_ftp_retry_settings() -> tuple[bool, int, float, float]:
function with_ftp_retry (line 1051) | async def with_ftp_retry(
FILE: backend/app/services/bambu_mqtt.py
class MQTTLogEntry (line 35) | class MQTTLogEntry:
class HMSError (line 45) | class HMSError:
class KProfile (line 56) | class KProfile:
class NozzleInfo (line 73) | class NozzleInfo:
class PrintOptions (line 81) | class PrintOptions:
class PrinterState (line 104) | class PrinterState:
function get_stage_name (line 260) | def get_stage_name(stage: int) -> str:
class BambuMQTTClient (line 265) | class BambuMQTTClient:
method __init__ (line 276) | def __init__(
method topic_subscribe (line 377) | def topic_subscribe(self) -> str:
method topic_publish (line 381) | def topic_publish(self) -> str:
method is_stale (line 387) | def is_stale(self) -> bool:
method check_staleness (line 399) | def check_staleness(self) -> bool:
method force_reconnect_stale_session (line 431) | def force_reconnect_stale_session(self, reason: str) -> None:
method _on_connect (line 448) | def _on_connect(self, client, userdata, flags, rc, properties=None):
method _on_subscribe (line 497) | def _on_subscribe(self, client, userdata, mid, reason_code_list, prope...
method _on_disconnect (line 522) | def _on_disconnect(self, client, userdata, disconnect_flags=None, rc=N...
method _on_message (line 575) | def _on_message(self, client, userdata, msg):
method _handle_request_message (line 613) | def _handle_request_message(self, data: dict) -> None:
method _process_message (line 627) | def _process_message(self, payload: dict):
method _handle_system_response (line 791) | def _handle_system_response(self, data: dict):
method _handle_version_info (line 807) | def _handle_version_info(self, data: dict):
method _apply_ams_version_cache (line 929) | def _apply_ams_version_cache(self, ams_list: list) -> None:
method _parse_xcam_data (line 965) | def _parse_xcam_data(self, xcam_data):
method _resolve_local_slot_from_mapping (line 1176) | def _resolve_local_slot_from_mapping(local_slot: int, mapping_raw: lis...
method _handle_ams_data (line 1203) | def _handle_ams_data(self, ams_data):
method _update_state (line 1688) | def _update_state(self, data: dict):
method _request_push_all (line 2747) | def _request_push_all(self):
method _probe_developer_mode (line 2753) | def _probe_developer_mode(self):
method _handle_dev_mode_probe_response (line 2800) | def _handle_dev_mode_probe_response(self, data: dict):
method _request_version (line 2821) | def _request_version(self):
method request_status_update (line 2834) | def request_status_update(self) -> bool:
method _request_accessories (line 2852) | def _request_accessories(self):
method _prime_kprofile_request (line 2866) | def _prime_kprofile_request(self):
method connect (line 2885) | def connect(self, loop: asyncio.AbstractEventLoop | None = None):
method start_print (line 2925) | def start_print(
method stop_print (line 3091) | def stop_print(self) -> bool:
method set_xcam_option (line 3100) | def set_xcam_option(
method _set_print_option (line 3196) | def _set_print_option(self, option_name: str, enabled: bool) -> bool:
method start_calibration (line 3240) | def start_calibration(
method disconnect (line 3308) | def disconnect(self, timeout: float = 0):
method send_command (line 3318) | def send_command(self, command: dict):
method enable_logging (line 3333) | def enable_logging(self, enabled: bool = True):
method get_logs (line 3338) | def get_logs(self) -> list[MQTTLogEntry]:
method clear_logs (line 3342) | def clear_logs(self):
method logging_enabled (line 3347) | def logging_enabled(self) -> bool:
method send_drying_command (line 3351) | def send_drying_command(
method _handle_kprofile_response (line 3393) | def _handle_kprofile_response(self, data: dict):
method get_kprofiles (line 3486) | async def get_kprofiles(
method set_kprofile (line 3558) | def set_kprofile(
method set_kprofiles_batch (line 3638) | def set_kprofiles_batch(
method delete_kprofile (line 3706) | def delete_kprofile(
method pause_print (line 3782) | def pause_print(self) -> bool:
method resume_print (line 3793) | def resume_print(self) -> bool:
method clear_hms_errors (line 3804) | def clear_hms_errors(self) -> bool:
method skip_objects (line 3816) | def skip_objects(self, object_ids: list[int]) -> bool:
method send_gcode (line 3861) | def send_gcode(self, gcode: str) -> bool:
method set_bed_temperature (line 3883) | def set_bed_temperature(self, target: int) -> bool:
method set_nozzle_temperature (line 3894) | def set_nozzle_temperature(self, target: int, nozzle: int = 0) -> bool:
method set_chamber_temperature (line 3915) | def set_chamber_temperature(self, target: int) -> bool:
method set_print_speed (line 3938) | def set_print_speed(self, mode: int) -> bool:
method set_fan_speed (line 3960) | def set_fan_speed(self, fan: int, speed: int) -> bool:
method set_part_fan (line 3977) | def set_part_fan(self, speed: int) -> bool:
method set_aux_fan (line 3981) | def set_aux_fan(self, speed: int) -> bool:
method set_chamber_fan (line 3985) | def set_chamber_fan(self, speed: int) -> bool:
method set_airduct_mode (line 3989) | def set_airduct_mode(self, mode: str) -> bool:
method set_chamber_light (line 4017) | def set_chamber_light(self, on: bool) -> bool:
method select_extruder (line 4050) | def select_extruder(self, extruder: int) -> bool:
method home_axes (line 4081) | def home_axes(self, axes: str = "XYZ") -> bool:
method move_axis (line 4092) | def move_axis(self, axis: str, distance: float, speed: int = 3000) -> ...
method disable_motors (line 4112) | def disable_motors(self) -> bool:
method enable_motors (line 4123) | def enable_motors(self) -> bool:
method ams_load_filament (line 4131) | def ams_load_filament(self, tray_id: int, extruder_id: int | None = No...
method ams_unload_filament (line 4182) | def ams_unload_filament(self) -> bool:
method ams_control (line 4236) | def ams_control(self, action: str) -> bool:
method ams_refresh_tray (line 4258) | def ams_refresh_tray(self, ams_id: int, tray_id: int) -> tuple[bool, s...
method ams_set_filament_setting (line 4296) | def ams_set_filament_setting(
method reset_ams_slot (line 4381) | def reset_ams_slot(self, ams_id: int, tray_id: int) -> bool:
method extrusion_cali_sel (line 4439) | def extrusion_cali_sel(
method extrusion_cali_set (line 4520) | def extrusion_cali_set(
method set_timelapse (line 4583) | def set_timelapse(self, enable: bool) -> bool:
method set_liveview (line 4607) | def set_liveview(self, enable: bool) -> bool:
FILE: backend/app/services/bug_report.py
function _check_rate_limit (line 20) | def _check_rate_limit() -> bool:
function submit_report (line 30) | async def submit_report(
FILE: backend/app/services/camera.py
function get_ffmpeg_path (line 32) | def get_ffmpeg_path() -> str | None:
function supports_rtsp (line 69) | def supports_rtsp(model: str | None) -> bool:
function get_camera_port (line 98) | def get_camera_port(model: str | None) -> int:
function rewrite_rtsp_request_url (line 109) | def rewrite_rtsp_request_url(data: bytes, proxy_url: bytes, real_url: by...
function create_tls_proxy (line 127) | async def create_tls_proxy(target_host: str, target_port: int) -> tuple[...
function is_chamber_image_model (line 222) | def is_chamber_image_model(model: str | None) -> bool:
function build_camera_url (line 230) | def build_camera_url(ip_address: str, access_code: str, model: str | Non...
function _create_chamber_auth_payload (line 236) | def _create_chamber_auth_payload(access_code: str) -> bytes:
function _create_ssl_context (line 261) | def _create_ssl_context() -> ssl.SSLContext:
function read_chamber_image_frame (line 272) | async def read_chamber_image_frame(
function generate_chamber_image_stream (line 353) | async def generate_chamber_image_stream(
function read_next_chamber_frame (line 384) | async def read_next_chamber_frame(reader: asyncio.StreamReader, timeout:...
function capture_camera_frame (line 416) | async def capture_camera_frame(
function capture_camera_frame_bytes (line 453) | async def capture_camera_frame_bytes(
function capture_finish_photo (line 551) | async def capture_finish_photo(
function test_camera_connection (line 595) | async def test_camera_connection(
FILE: backend/app/services/discovery.py
function is_running_in_docker (line 26) | def is_running_in_docker() -> bool:
class DiscoveredPrinter (line 68) | class DiscoveredPrinter:
method to_dict (line 77) | def to_dict(self) -> dict:
class PrinterDiscoveryService (line 87) | class PrinterDiscoveryService:
method __init__ (line 90) | def __init__(self):
method is_running (line 96) | def is_running(self) -> bool:
method discovered_printers (line 100) | def discovered_printers(self) -> list[DiscoveredPrinter]:
method clear (line 103) | def clear(self):
method start (line 107) | async def start(self, duration: float = 10.0):
method stop (line 116) | async def stop(self):
method _discover (line 127) | async def _discover(self, duration: float):
method _discover_alternative (line 211) | async def _discover_alternative(self, duration: float):
method _handle_response (line 259) | def _handle_response(self, response: str, ip_address: str):
class SubnetScanner (line 314) | class SubnetScanner:
method __init__ (line 321) | def __init__(self):
method is_running (line 328) | def is_running(self) -> bool:
method discovered_printers (line 332) | def discovered_printers(self) -> list[DiscoveredPrinter]:
method progress (line 336) | def progress(self) -> tuple[int, int]:
method scan_subnet (line 340) | async def scan_subnet(self, subnet: str, timeout: float = 1.0) -> list...
method _probe_host (line 389) | async def _probe_host(self, ip: str, timeout: float):
method _get_printer_info_ssdp (line 421) | async def _get_printer_info_ssdp(self, ip: str, timeout: float) -> tup...
method _check_port (line 473) | async def _check_port(self, ip: str, port: int, timeout: float) -> bool:
method stop (line 491) | def stop(self):
class TasmotaScanner (line 496) | class TasmotaScanner:
method __init__ (line 501) | def __init__(self):
method is_running (line 508) | def is_running(self) -> bool:
method discovered_devices (line 512) | def discovered_devices(self) -> list[dict]:
method progress (line 516) | def progress(self) -> tuple[int, int]:
method scan_range (line 520) | async def scan_range(self, from_ip: str, to_ip: str, timeout: float = ...
method _probe_host (line 582) | async def _probe_host(self, ip: str):
method _do_probe (line 592) | async def _do_probe(self, ip: str):
method stop (line 684) | def stop(self):
FILE: backend/app/services/email_service.py
function generate_secure_password (line 26) | def generate_secure_password(length: int = 16) -> str:
function get_notification_template (line 59) | async def get_notification_template(db: AsyncSession, event_type: str) -...
function render_template (line 73) | def render_template(template_str: str, variables: dict[str, Any]) -> str:
function get_smtp_settings (line 91) | async def get_smtp_settings(db: AsyncSession) -> SMTPSettings | None:
function save_smtp_settings (line 146) | async def save_smtp_settings(db: AsyncSession, smtp_settings: SMTPSettin...
function send_email (line 176) | def send_email(
function create_welcome_email (line 242) | def create_welcome_email(username: str, password: str, login_url: str) -...
function create_password_reset_email (line 308) | def create_password_reset_email(username: str, password: str, login_url:...
function create_password_reset_link_email (line 382) | def create_password_reset_link_email(username: str, reset_url: str) -> t...
function create_password_reset_link_email_from_template (line 430) | async def create_password_reset_link_email_from_template(
function create_welcome_email_from_template (line 447) | async def create_welcome_email_from_template(
function create_password_reset_email_from_template (line 509) | async def create_password_reset_email_from_template(
function send_user_print_notification (line 577) | async def send_user_print_notification(
FILE: backend/app/services/export.py
class ExportService (line 13) | class ExportService:
method __init__ (line 70) | def __init__(self, db: AsyncSession):
method export_archives (line 73) | async def export_archives(
method export_stats (line 155) | async def export_stats(
method _archive_to_row (line 239) | def _archive_to_row(self, archive: PrintArchive, fields: list[str]) ->...
method _generate_csv (line 254) | def _generate_csv(self, headers: list[str], rows: list[list]) -> bytes:
method _generate_csv_simple (line 262) | def _generate_csv_simple(self, rows: list[list]) -> bytes:
method _generate_xlsx (line 269) | def _generate_xlsx(self, headers: list[str], rows: list[list], fields:...
method _generate_xlsx_simple (line 316) | def _generate_xlsx_simple(self, rows: list[list]) -> bytes:
FILE: backend/app/services/external_camera.py
function _sanitize_camera_url (line 23) | def _sanitize_camera_url(url: str, allowed_schemes: tuple[str, ...] = ("...
function _validate_camera_url (line 87) | def _validate_camera_url(url: str, allowed_schemes: tuple[str, ...] = ("...
function list_usb_cameras (line 100) | def list_usb_cameras() -> list[dict]:
function get_ffmpeg_path (line 163) | def get_ffmpeg_path() -> str | None:
function capture_frame (line 176) | async def capture_frame(url: str, camera_type: str, timeout: int = 15) -...
function _capture_usb_frame (line 201) | async def _capture_usb_frame(device: str, timeout: int) -> bytes | None:
function _capture_mjpeg_frame (line 282) | async def _capture_mjpeg_frame(url: str, timeout: int) -> bytes | None:
function _capture_rtsp_frame (line 338) | async def _capture_rtsp_frame(url: str, timeout: int) -> bytes | None:
function _capture_snapshot (line 430) | async def _capture_snapshot(url: str, timeout: int) -> bytes | None:
function test_connection (line 469) | async def test_connection(url: str, camera_type: str) -> dict:
function generate_mjpeg_stream (line 508) | async def generate_mjpeg_stream(url: str, camera_type: str, fps: int = 1...
function _format_mjpeg_frame (line 579) | def _format_mjpeg_frame(frame: bytes) -> bytes:
function _stream_mjpeg (line 589) | async def _stream_mjpeg(url: str) -> AsyncGenerator[bytes, None]:
function _stream_rtsp (line 640) | async def _stream_rtsp(url: str, fps: int) -> AsyncGenerator[bytes, None]:
function _stream_usb (line 777) | async def _stream_usb(device: str, fps: int) -> AsyncGenerator[bytes, No...
FILE: backend/app/services/failure_analysis.py
class FailureAnalysisService (line 11) | class FailureAnalysisService:
method __init__ (line 14) | def __init__(self, db: AsyncSession):
method analyze_failures (line 17) | async def analyze_failures(
FILE: backend/app/services/firmware_check.py
class FirmwareVersion (line 104) | class FirmwareVersion:
class FirmwareCheckService (line 113) | class FirmwareCheckService:
method __init__ (line 116) | def __init__(self):
method _get_build_id (line 129) | async def _get_build_id(self) -> str | None:
method _fetch_version_from_wiki (line 151) | async def _fetch_version_from_wiki(self, api_key: str) -> str | None:
method _fetch_all_versions_from_wiki (line 159) | async def _fetch_all_versions_from_wiki(self, api_key: str) -> list[tu...
method _fetch_all_versions_from_download_page (line 214) | async def _fetch_all_versions_from_download_page(self, api_key: str) -...
method _fetch_from_download_page (line 246) | async def _fetch_from_download_page(self, api_key: str) -> FirmwareVer...
method _fetch_firmware_versions (line 251) | async def _fetch_firmware_versions(self, api_key: str) -> FirmwareVers...
method get_latest_version (line 278) | async def get_latest_version(self, model: str) -> FirmwareVersion | None:
method _resolve_api_key (line 319) | def _resolve_api_key(self, model: str) -> str | None:
method _version_tuple (line 328) | def _version_tuple(v: str) -> tuple[int, ...]:
method get_available_versions (line 334) | async def get_available_versions(self, model: str) -> list[FirmwareVer...
method get_version_info (line 378) | async def get_version_info(self, model: str, version: str) -> Firmware...
method check_for_update (line 385) | async def check_for_update(self, model: str, current_version: str) -> ...
method get_all_latest_versions (line 450) | async def get_all_latest_versions(self) -> dict[str, FirmwareVersion]:
method _get_firmware_cache_dir (line 466) | def _get_firmware_cache_dir(self) -> Path:
method get_firmware_file_info (line 472) | async def get_firmware_file_info(self, model: str, version: str | None...
method download_firmware (line 496) | async def download_firmware(
method close (line 571) | async def close(self):
function get_firmware_service (line 580) | def get_firmware_service() -> FirmwareCheckService:
FILE: backend/app/services/firmware_update.py
class FirmwareUploadStatus (line 33) | class FirmwareUploadStatus(StrEnum):
class FirmwareUploadState (line 45) | class FirmwareUploadState:
function get_upload_state (line 60) | def get_upload_state(printer_id: int) -> FirmwareUploadState:
function reset_upload_state (line 67) | def reset_upload_state(printer_id: int):
class FirmwareUpdateService (line 72) | class FirmwareUpdateService:
method prepare_update (line 78) | async def prepare_update(
method start_upload (line 209) | async def start_upload(
method _do_upload (line 261) | async def _do_upload(
method _broadcast_progress (line 369) | async def _broadcast_progress(self, printer_id: int, state: FirmwareUp...
function get_firmware_update_service (line 389) | def get_firmware_update_service() -> FirmwareUpdateService:
FILE: backend/app/services/github_backup.py
class GitHubBackupService (line 37) | class GitHubBackupService:
method __init__ (line 40) | def __init__(self):
method _get_client (line 47) | async def _get_client(self) -> httpx.AsyncClient:
method start_scheduler (line 53) | async def start_scheduler(self):
method stop_scheduler (line 60) | def stop_scheduler(self):
method _scheduler_loop (line 67) | async def _scheduler_loop(self):
method _check_scheduled_backups (line 79) | async def _check_scheduled_backups(self):
method _calculate_next_run (line 100) | def _calculate_next_run(self, schedule_type: str, from_time: datetime ...
method test_connection (line 106) | async def test_connection(self, repo_url: str, token: str) -> dict:
method _parse_repo_url (line 179) | def _parse_repo_url(self, url: str) -> tuple[str, str]:
method run_backup (line 199) | async def run_backup(self, config_id: int, trigger: str = "manual") ->...
method _collect_backup_data (line 313) | async def _collect_backup_data(self, db: AsyncSession, config: GitHubB...
method _collect_kprofiles (line 369) | async def _collect_kprofiles(self, db: AsyncSession, files: dict):
method _collect_cloud_profiles (line 415) | async def _collect_cloud_profiles(self, db: AsyncSession, files: dict):
method _collect_settings (line 476) | async def _collect_settings(self, db: AsyncSession, files: dict):
method _collect_spools (line 490) | async def _collect_spools(self, db: AsyncSession, files: dict):
method _collect_archives (line 559) | async def _collect_archives(self, db: AsyncSession, files: dict):
method _push_to_github (line 612) | async def _push_to_github(self, config: GitHubBackupConfig, files: dic...
method _create_branch_and_push (line 747) | async def _create_branch_and_push(
method _create_initial_commit (line 796) | async def _create_initial_commit(
method is_running (line 860) | def is_running(self) -> bool:
method progress (line 865) | def progress(self) -> str | None:
method get_logs (line 869) | async def get_logs(self, config_id: int, limit: int = 50, offset: int ...
FILE: backend/app/services/hms_errors.py
function get_error_description (line 866) | def get_error_description(error_code: str) -> str | None:
FILE: backend/app/services/homeassistant.py
class HomeAssistantService (line 15) | class HomeAssistantService:
method __init__ (line 18) | def __init__(self, timeout: float = 10.0):
method configure (line 23) | def configure(self, url: str, token: str):
method _headers (line 28) | def _headers(self) -> dict:
method get_status (line 34) | async def get_status(self, plug: "SmartPlug") -> dict:
method turn_on (line 72) | async def turn_on(self, plug: "SmartPlug") -> bool:
method turn_off (line 79) | async def turn_off(self, plug: "SmartPlug") -> bool:
method toggle (line 86) | async def toggle(self, plug: "SmartPlug") -> bool:
method _call_service (line 93) | async def _call_service(self, plug: "SmartPlug", action: str) -> bool:
method get_energy (line 113) | async def get_energy(self, plug: "SmartPlug") -> dict | None:
method _get_sensor_value (line 173) | async def _get_sensor_value(self, client: httpx.AsyncClient, entity_id...
method _validate_url (line 189) | def _validate_url(url: str) -> str | None:
method test_connection (line 202) | async def test_connection(self, url: str, token: str) -> dict:
method list_entities (line 237) | async def list_entities(self, url: str, token: str, search: str | None...
method list_sensor_entities (line 291) | async def list_sensor_entities(self, url: str, token: str) -> list[dict]:
FILE: backend/app/services/layer_timelapse.py
function get_ffmpeg_path (line 22) | def get_ffmpeg_path() -> str | None:
class TimelapseSession (line 36) | class TimelapseSession:
method __post_init__ (line 48) | def __post_init__(self):
method capture_layer (line 53) | async def capture_layer(self, layer_num: int) -> bool:
method stitch (line 85) | async def stitch(self, output_path: Path, fps: int = 30) -> bool:
method cleanup (line 173) | def cleanup(self):
function start_session (line 183) | def start_session(printer_id: int, archive_id: int | None, url: str, cam...
function get_session (line 209) | def get_session(printer_id: int) -> TimelapseSession | None:
function on_layer_change (line 214) | async def on_layer_change(printer_id: int, layer_num: int):
function on_print_complete (line 226) | async def on_print_complete(printer_id: int) -> Path | None:
function cancel_session (line 262) | def cancel_session(printer_id: int):
function get_active_sessions (line 274) | def get_active_sessions() -> dict[int, TimelapseSession]:
FILE: backend/app/services/ldap_service.py
class LDAPUserInfo (line 22) | class LDAPUserInfo:
class LDAPConfig (line 32) | class LDAPConfig:
function parse_ldap_config (line 47) | def parse_ldap_config(settings: dict[str, str]) -> LDAPConfig | None:
function _create_server (line 76) | def _create_server(config: LDAPConfig) -> Server:
function authenticate_ldap_user (line 94) | def authenticate_ldap_user(config: LDAPConfig, username: str, password: ...
function resolve_group_mapping (line 230) | def resolve_group_mapping(ldap_groups: list[str], group_mapping: dict[st...
function test_ldap_connection (line 249) | def test_ldap_connection(config: LDAPConfig) -> tuple[bool, str]:
function _ldap_escape (line 282) | def _ldap_escape(value: str) -> str:
FILE: backend/app/services/local_backup.py
function _default_backup_dir (line 27) | def _default_backup_dir() -> Path:
class LocalBackupService (line 31) | class LocalBackupService:
method __init__ (line 34) | def __init__(self):
method start_scheduler (line 43) | async def start_scheduler(self):
method stop_scheduler (line 52) | def stop_scheduler(self):
method _scheduler_loop (line 59) | async def _scheduler_loop(self):
method _seed_next_run (line 71) | async def _seed_next_run(self):
method _load_settings (line 83) | async def _load_settings(self) -> dict:
method _check_scheduled_backup (line 103) | async def _check_scheduled_backup(self):
method _calculate_next_run (line 122) | def _calculate_next_run(self, schedule_type: str, time_str: str = "03:...
method _resolve_backup_dir (line 153) | def _resolve_backup_dir(self, path_setting: str) -> Path:
method run_backup (line 159) | async def run_backup(self, settings: dict | None = None) -> dict:
method _prune_backups (line 195) | def _prune_backups(self, backup_dir: Path, retention: int):
method get_status (line 209) | def get_status(self) -> dict:
method resolve_backup_file (line 219) | def resolve_backup_file(self, path_setting: str, filename: str) -> Pat...
method list_backups (line 231) | def list_backups(self, path_setting: str) -> list[dict]:
method delete_backup (line 249) | def delete_backup(self, path_setting: str, filename: str) -> dict:
FILE: backend/app/services/mqtt_relay.py
class MQTTRelayService (line 21) | class MQTTRelayService:
method __init__ (line 27) | def __init__(self):
method configure (line 41) | async def configure(self, settings: dict) -> bool:
method _configure_smart_plug_service (line 83) | async def _configure_smart_plug_service(self, settings: dict):
method smart_plug_service (line 96) | def smart_plug_service(self):
method _connect (line 104) | async def _connect(self, broker: str, port: int, username: str, passwo...
method _on_connect (line 154) | def _on_connect(
method _on_disconnect (line 174) | def _on_disconnect(
method disconnect (line 194) | async def disconnect(self, timeout: float = 0):
method _publish_status (line 210) | def _publish_status(self, status: str):
method _publish (line 218) | def _publish(self, topic: str, payload: dict, retain: bool = False):
method get_status (line 229) | def get_status(self) -> dict:
method on_printer_status (line 243) | async def on_printer_status(self, printer_id: int, state: Any, printer...
method on_printer_online (line 286) | async def on_printer_online(self, printer_id: int, printer_name: str, ...
method on_printer_offline (line 301) | async def on_printer_offline(self, printer_id: int, printer_name: str,...
method on_print_start (line 316) | async def on_print_start(
method on_print_complete (line 340) | async def on_print_complete(
method on_ams_change (line 372) | async def on_ams_change(
method on_printer_error (line 394) | async def on_printer_error(
method on_queue_job_added (line 420) | async def on_queue_job_added(
method on_queue_job_started (line 442) | async def on_queue_job_started(
method on_queue_job_completed (line 466) | async def on_queue_job_completed(
method on_maintenance_alert (line 500) | async def on_maintenance_alert(
method on_maintenance_acknowledged (line 524) | async def on_maintenance_acknowledged(
method on_maintenance_reset (line 544) | async def on_maintenance_reset(
method on_archive_created (line 568) | async def on_archive_created(
method on_archive_updated (line 590) | async def on_archive_updated(
method on_filament_low (line 614) | async def on_filament_low(
method on_smart_plug_state (line 640) | async def on_smart_plug_state(
method on_smart_plug_energy (line 666) | async def on_smart_plug_energy(
FILE: backend/app/services/mqtt_smart_plug.py
class SmartPlugMQTTData (line 20) | class SmartPlugMQTTData:
class MQTTDataSourceConfig (line 31) | class MQTTDataSourceConfig:
class MQTTSmartPlugService (line 40) | class MQTTSmartPlugService:
method __init__ (line 46) | def __init__(self):
method is_configured (line 64) | def is_configured(self) -> bool:
method has_broker_settings (line 68) | def has_broker_settings(self) -> bool:
method configure (line 72) | async def configure(self, settings: dict) -> bool:
method _connect (line 123) | async def _connect(self) -> bool:
method _on_connect (line 179) | def _on_connect(
method _on_disconnect (line 198) | def _on_disconnect(
method _on_message (line 217) | def _on_message(self, client: mqtt.Client, userdata: Any, msg: mqtt.MQ...
method _extract_json_path (line 302) | def _extract_json_path(self, data: dict, path: str) -> Any:
method _resubscribe_all (line 321) | def _resubscribe_all(self):
method subscribe (line 335) | def subscribe(
method _add_subscription (line 414) | def _add_subscription(self, plug_id: int, topic: str, data_type: str):
method unsubscribe (line 430) | def unsubscribe(self, plug_id: int):
method get_plug_data (line 464) | def get_plug_data(self, plug_id: int) -> SmartPlugMQTTData | None:
method is_reachable (line 469) | def is_reachable(self, plug_id: int) -> bool:
method disconnect (line 478) | async def disconnect(self, timeout: float = 0):
function subscribe_plug_to_mqtt (line 493) | def subscribe_plug_to_mqtt(service: "MQTTSmartPlugService", plug: Any) -...
FILE: backend/app/services/network_utils.py
function _is_excluded (line 20) | def _is_excluded(name: str) -> bool:
function get_network_interfaces (line 25) | def get_network_interfaces() -> list[dict]:
function get_all_interface_ips (line 90) | def get_all_interface_ips() -> list[dict]:
function _fallback_get_all_ips (line 160) | def _fallback_get_all_ips() -> list[dict]:
function find_interface_for_ip (line 172) | def find_interface_for_ip(target_ip: str) -> dict | None:
function get_other_interfaces (line 204) | def get_other_interfaces(exclude_ip: str) -> list[dict]:
FILE: backend/app/services/notification_service.py
class NotificationService (line 24) | class NotificationService:
method __init__ (line 27) | def __init__(self):
method _get_client (line 33) | async def _get_client(self) -> httpx.AsyncClient:
method close (line 39) | async def close(self):
method _is_in_quiet_hours (line 44) | def _is_in_quiet_hours(self, provider: NotificationProvider) -> bool:
method _get_template (line 73) | async def _get_template(self, db: AsyncSession, event_type: str) -> No...
method _render_template (line 87) | def _render_template(self, template_str: str, variables: dict[str, Any...
method _format_eta (line 96) | async def _format_eta(self, seconds: int | None, db: AsyncSession) -> ...
method _format_duration (line 111) | def _format_duration(self, seconds: int | None) -> str:
method _clean_filename (line 121) | def _clean_filename(self, filename: str) -> str:
method _build_message_from_template (line 136) | async def _build_message_from_template(
method send_test_notification (line 155) | async def send_test_notification(
method _send_callmebot (line 188) | async def _send_callmebot(self, config: dict, message: str) -> tuple[b...
method _send_ntfy (line 208) | async def _send_ntfy(
method _send_pushover (line 252) | async def _send_pushover(
method _send_telegram (line 298) | async def _send_telegram(self, config: dict, message: str, image_data:...
method _send_email (line 343) | async def _send_email(self, config: dict, subject: str, body: str) -> ...
method _send_discord (line 394) | async def _send_discord(
method _send_webhook (line 432) | async def _send_webhook(
method _send_homeassistant (line 503) | async def _send_homeassistant(
method _send_to_provider (line 584) | async def _send_to_provider(
method _update_provider_status (line 627) | async def _update_provider_status(
method _get_providers_for_event (line 641) | async def _get_providers_for_event(
method _log_notification (line 662) | async def _log_notification(
method _send_to_providers (line 692) | async def _send_to_providers(
method on_print_start (line 759) | async def on_print_start(
method on_print_complete (line 843) | async def on_print_complete(
method on_print_progress (line 945) | async def on_print_progress(
method on_print_missing_spool_assignment (line 983) | async def on_print_missing_spool_assignment(
method on_printer_offline (line 1025) | async def on_printer_offline(self, printer_id: int, printer_name: str,...
method on_printer_error (line 1038) | async def on_printer_error(
method on_plate_not_empty (line 1071) | async def on_plate_not_empty(
method on_filament_low (line 1101) | async def on_filament_low(
method on_maintenance_due (line 1127) | async def on_maintenance_due(
method on_ams_humidity_high (line 1161) | async def on_ams_humidity_high(
method on_ams_temperature_high (line 1196) | async def on_ams_temperature_high(
method on_ams_ht_humidity_high (line 1231) | async def on_ams_ht_humidity_high(
method on_ams_ht_temperature_high (line 1267) | async def on_ams_ht_temperature_high(
method on_bed_cooled (line 1303) | async def on_bed_cooled(
method on_first_layer_complete (line 1329) | async def on_first_layer_complete(
method clear_template_cache (line 1362) | def clear_template_cache(self):
method send_user_print_email (line 1366) | async def send_user_print_email(
method on_queue_job_added (line 1482) | async def on_queue_job_added(
method on_queue_job_assigned (line 1506) | async def on_queue_job_assigned(
method on_queue_job_started (line 1530) | async def on_queue_job_started(
method on_queue_job_waiting (line 1557) | async def on_queue_job_waiting(
method on_queue_job_skipped (line 1578) | async def on_queue_job_skipped(
method on_queue_job_failed (line 1602) | async def on_queue_job_failed(
method on_queue_completed (line 1626) | async def on_queue_completed(
method _queue_for_digest (line 1643) | async def _queue_for_digest(
method send_digest (line 1669) | async def send_digest(self, provider_id: int):
method check_and_send_digests (line 1740) | async def check_and_send_digests(self):
method start_digest_scheduler (line 1768) | def start_digest_scheduler(self):
method stop_digest_scheduler (line 1774) | def stop_digest_scheduler(self):
method _digest_scheduler_loop (line 1781) | async def _digest_scheduler_loop(self):
FILE: backend/app/services/obico_actions.py
function execute_action (line 16) | async def execute_action(printer_id: int, action: str, task_name: str, s...
function _get_printer_name (line 32) | async def _get_printer_name(printer_id: int) -> str:
function _pause_print (line 39) | def _pause_print(printer_id: int) -> None:
function _turn_off_linked_plugs (line 50) | async def _turn_off_linked_plugs(printer_id: int) -> None:
function _notify (line 66) | async def _notify(printer_id: int, printer_name: str, task_name: str, sc...
FILE: backend/app/services/obico_detection.py
function _prune_frame_cache (line 47) | def _prune_frame_cache() -> None:
function stash_frame (line 55) | async def stash_frame(data: bytes) -> str:
function pop_frame (line 64) | async def pop_frame(nonce: str) -> bytes | None:
class ObicoDetectionService (line 77) | class ObicoDetectionService:
method __init__ (line 80) | def __init__(self):
method start (line 96) | async def start(self):
method stop (line 102) | def stop(self):
method _load_settings (line 110) | async def _load_settings(self) -> dict:
method _loop (line 145) | async def _loop(self):
method _poll_once (line 164) | async def _poll_once(self, settings: dict):
method _capture_frame (line 183) | async def _capture_frame(self, printer_id: int) -> bytes | None:
method _check_printer (line 208) | async def _check_printer(self, printer_id: int, status, settings: dict):
method _dispatch_action (line 279) | async def _dispatch_action(self, printer_id: int, action: str, task_na...
method get_status (line 297) | def get_status(self) -> dict:
method test_connection (line 314) | async def test_connection(self, url: str) -> dict:
FILE: backend/app/services/obico_smoothing.py
function thresholds (line 34) | def thresholds(sensitivity: str) -> tuple[float, float]:
class PrintState (line 40) | class PrintState:
method update (line 50) | def update(self, current_p: float) -> float:
function classify (line 80) | def classify(score: float, sensitivity: str) -> str:
function score_from_detections (line 90) | def score_from_detections(detections: list) -> float:
FILE: backend/app/services/opentag3d.py
function _build_payload (line 24) | def _build_payload(spool: Spool) -> bytes:
function encode_opentag3d (line 76) | def encode_opentag3d(spool: Spool) -> bytes:
FILE: backend/app/services/orca_profiles.py
function get_cached_base_profile (line 30) | async def get_cached_base_profile(name: str, db: AsyncSession) -> dict |...
function fetch_and_cache_base_profile (line 51) | async def fetch_and_cache_base_profile(name: str, profile_type: str, db:...
function resolve_preset (line 110) | async def resolve_preset(preset_data: dict, profile_type: str, db: Async...
function extract_core_fields (line 137) | def extract_core_fields(data: dict) -> dict:
function _parse_material_from_name (line 232) | def _parse_material_from_name(name: str) -> str | None:
function _parse_vendor_from_name (line 256) | def _parse_vendor_from_name(name: str) -> str | None:
function _type_from_path (line 272) | def _type_from_path(zip_entry: str) -> str | None:
function _guess_profile_type (line 285) | def _guess_profile_type(data: dict, path_hint: str | None = None) -> str:
function import_orca_file (line 345) | async def import_orca_file(filename: str, content: bytes, db: AsyncSessi...
function _import_single_preset (line 403) | async def _import_single_preset(data: dict, db: AsyncSession, path_hint:...
function refresh_base_cache (line 452) | async def refresh_base_cache(db: AsyncSession) -> dict:
function get_cache_status (line 475) | async def get_cache_status(db: AsyncSession) -> dict:
function reclassify_presets (line 501) | async def reclassify_presets(db: AsyncSession) -> dict:
FILE: backend/app/services/plate_detection.py
function _get_calibration_dir (line 27) | def _get_calibration_dir() -> Path:
class PlateDetectionResult (line 34) | class PlateDetectionResult:
method __init__ (line 37) | def __init__(
method to_dict (line 53) | def to_dict(self) -> dict:
class PlateDetector (line 64) | class PlateDetector:
method __init__ (line 78) | def __init__(
method _get_metadata_path (line 101) | def _get_metadata_path(self, printer_id: int) -> Path:
method _load_metadata (line 106) | def _load_metadata(self, printer_id: int) -> dict:
method _save_metadata (line 119) | def _save_metadata(self, printer_id: int, metadata: dict) -> None:
method _get_reference_paths (line 127) | def _get_reference_paths(self, printer_id: int) -> list[Path]:
method _get_next_reference_slot (line 137) | def _get_next_reference_slot(self, printer_id: int) -> Path:
method _rotate_references (line 148) | def _rotate_references(self, printer_id: int) -> None:
method get_references (line 172) | def get_references(self, printer_id: int) -> list[dict]:
method update_reference_label (line 197) | def update_reference_label(self, printer_id: int, index: int, label: s...
method delete_reference (line 216) | def delete_reference(self, printer_id: int, index: int) -> bool:
method get_reference_thumbnail (line 252) | def get_reference_thumbnail(self, printer_id: int, index: int, max_siz...
method _extract_roi (line 282) | def _extract_roi(self, frame: np.ndarray) -> tuple[np.ndarray, int, in...
method _preprocess_for_comparison (line 296) | def _preprocess_for_comparison(self, frame: np.ndarray) -> np.ndarray:
method calibrate (line 310) | def calibrate(self, image_data: bytes, printer_id: int, label: str | N...
method get_calibration_count (line 386) | def get_calibration_count(self, printer_id: int) -> int:
method has_calibration (line 390) | def has_calibration(self, printer_id: int, plate_type: str | None = No...
method delete_calibration (line 394) | def delete_calibration(self, printer_id: int, plate_type: str | None =...
method analyze_frame (line 404) | def analyze_frame(
function capture_camera_image (line 583) | async def capture_camera_image(
function check_plate_empty (line 657) | async def check_plate_empty(
function calibrate_plate (line 716) | async def calibrate_plate(
function get_calibration_status (line 760) | def get_calibration_status(printer_id: int, plate_type: str | None = Non...
function delete_calibration (line 793) | def delete_calibration(printer_id: int, plate_type: str | None = None) -...
function is_plate_detection_available (line 802) | def is_plate_detection_available() -> bool:
FILE: backend/app/services/print_log.py
function write_log_entry (line 16) | async def write_log_entry(
FILE: backend/app/services/print_scheduler.py
function _canonical_filament_type (line 44) | def _canonical_filament_type(ftype: str) -> str:
class PrintScheduler (line 50) | class PrintScheduler:
method __init__ (line 66) | def __init__(self):
method run (line 75) | async def run(self):
method stop (line 88) | def stop(self):
method check_queue (line 93) | async def check_queue(self):
method _find_idle_printer_for_model (line 427) | async def _find_idle_printer_for_model(
method _is_busy_only (line 607) | def _is_busy_only(waiting_reason: str) -> bool:
method _get_missing_force_color_slots (line 617) | def _get_missing_force_color_slots(self, printer_id: int, force_overri...
method _get_missing_filament_types (line 655) | def _get_missing_filament_types(self, printer_id: int, required_types:...
method _count_override_color_matches (line 696) | def _count_override_color_matches(self, printer_id: int, overrides: li...
method _compute_ams_mapping_for_printer (line 728) | async def _compute_ams_mapping_for_printer(
method _get_filament_requirements (line 791) | async def _get_filament_requirements(self, db: AsyncSession, item: Pri...
method _build_loaded_filaments (line 896) | def _build_loaded_filaments(self, status) -> list[dict]:
method _normalize_color (line 967) | def _normalize_color(self, color: str | None) -> str:
method _normalize_color_for_compare (line 974) | def _normalize_color_for_compare(self, color: str | None) -> str:
method _colors_are_similar (line 980) | def _colors_are_similar(self, color1: str | None, color2: str | None, ...
method _match_filaments_to_slots (line 998) | def _match_filaments_to_slots(
method _is_printer_idle (line 1121) | def _is_printer_idle(self, printer_id: int, require_plate_clear: bool ...
method _get_setting (line 1150) | async def _get_setting(self, db: AsyncSession, key: str) -> str | None:
method _get_bool_setting (line 1156) | async def _get_bool_setting(self, db: AsyncSession, key: str, default:...
method _get_drying_presets (line 1164) | async def _get_drying_presets(self, db: AsyncSession) -> dict[str, dic...
method _get_conservative_drying_params (line 1177) | def _get_conservative_drying_params(
method _check_auto_drying (line 1216) | async def _check_auto_drying(
method _sync_drying_state (line 1415) | def _sync_drying_state(self):
method _stop_drying (line 1435) | async def _stop_drying(self, printer_id: int):
method _get_smart_plugs (line 1455) | async def _get_smart_plugs(self, db: AsyncSession, printer_id: int) ->...
method _power_on_and_wait (line 1460) | async def _power_on_and_wait(self, plug: SmartPlug, printer_id: int, d...
method _check_previous_success (line 1515) | async def _check_previous_success(self, db: AsyncSession, item: PrintQ...
method _power_off_if_needed (line 1534) | async def _power_off_if_needed(self, db: AsyncSession, item: PrintQueu...
method _get_job_name (line 1560) | async def _get_job_name(self, db: AsyncSession, item: PrintQueueItem) ...
method _get_printer (line 1574) | async def _get_printer(self, db: AsyncSession, printer_id: int) -> Pri...
method _start_print (line 1579) | async def _start_print(self, db: AsyncSession, item: PrintQueueItem):
method _watchdog_print_start (line 1955) | async def _watchdog_print_start(
FILE: backend/app/services/printer_manager.py
function supports_chamber_temp (line 75) | def supports_chamber_temp(model: str | None) -> bool:
function has_stg_cur_idle_bug (line 88) | def has_stg_cur_idle_bug(model: str | None) -> bool:
function supports_drying (line 118) | def supports_drying(model: str | None, firmware: str | None) -> bool:
class PrinterInfo (line 137) | class PrinterInfo:
method __init__ (line 140) | def __init__(self, name: str, serial_number: str):
class PrinterManager (line 145) | class PrinterManager:
method __init__ (line 148) | def __init__(self):
method get_printer (line 166) | def get_printer(self, printer_id: int) -> PrinterInfo | None:
method set_current_print_user (line 170) | def set_current_print_user(self, printer_id: int, user_id: int, userna...
method get_current_print_user (line 174) | def get_current_print_user(self, printer_id: int) -> dict | None:
method clear_current_print_user (line 178) | def clear_current_print_user(self, printer_id: int):
method is_awaiting_plate_clear (line 182) | def is_awaiting_plate_clear(self, printer_id: int) -> bool:
method set_awaiting_plate_clear (line 188) | def set_awaiting_plate_clear(self, printer_id: int, awaiting: bool):
method _persist_awaiting_plate_clear (line 204) | async def _persist_awaiting_plate_clear(self, printer_id: int, awaitin...
method load_awaiting_plate_clear_from_db (line 216) | async def load_awaiting_plate_clear_from_db(self):
method set_event_loop (line 230) | def set_event_loop(self, loop: asyncio.AbstractEventLoop):
method set_print_start_callback (line 234) | def set_print_start_callback(self, callback: Callable[[int, dict], Non...
method set_print_complete_callback (line 238) | def set_print_complete_callback(self, callback: Callable[[int, dict], ...
method set_status_change_callback (line 242) | def set_status_change_callback(self, callback: Callable[[int, PrinterS...
method set_ams_change_callback (line 246) | def set_ams_change_callback(self, callback: Callable[[int, list], None]):
method set_layer_change_callback (line 250) | def set_layer_change_callback(self, callback: Callable[[int, int], Non...
method set_bed_temp_update_callback (line 254) | def set_bed_temp_update_callback(self, callback: Callable[[int, float]...
method _schedule_async (line 258) | def _schedule_async(self, coro):
method connect_printer (line 278) | async def connect_printer(self, printer: Printer) -> bool:
method disconnect_printer (line 331) | def disconnect_printer(self, printer_id: int, timeout: float = 0):
method disconnect_all (line 339) | def disconnect_all(self, timeout: float = 0):
method get_status (line 344) | def get_status(self, printer_id: int) -> PrinterState | None:
method get_model (line 353) | def get_model(self, printer_id: int) -> str | None:
method get_all_statuses (line 357) | def get_all_statuses(self) -> dict[int, PrinterState]:
method is_connected (line 366) | def is_connected(self, printer_id: int) -> bool:
method get_client (line 374) | def get_client(self, printer_id: int) -> BambuMQTTClient | None:
method mark_printer_offline (line 378) | def mark_printer_offline(self, printer_id: int):
method start_print (line 398) | def start_print(
method stop_print (line 435) | def stop_print(self, printer_id: int) -> bool:
method wait_for_cooldown (line 441) | async def wait_for_cooldown(
method enable_logging (line 486) | def enable_logging(self, printer_id: int, enabled: bool = True) -> bool:
method get_logs (line 493) | def get_logs(self, printer_id: int) -> list[MQTTLogEntry]:
method clear_logs (line 499) | def clear_logs(self, printer_id: int) -> bool:
method is_logging_enabled (line 506) | def is_logging_enabled(self, printer_id: int) -> bool:
method send_drying_command (line 512) | def send_drying_command(
method request_status_update (line 527) | def request_status_update(self, printer_id: int) -> bool:
method test_connection (line 536) | async def test_connection(
function get_derived_status_name (line 564) | def get_derived_status_name(state: PrinterState, model: str | None = Non...
function parse_plate_id (line 629) | def parse_plate_id(gcode_file: str | None) -> int | None:
function printer_state_to_dict (line 642) | def printer_state_to_dict(state: PrinterState, printer_id: int | None = ...
function init_printer_connections (line 882) | async def init_printer_connections(db: AsyncSession):
FILE: backend/app/services/rest_smart_plug.py
class RESTSmartPlugService (line 17) | class RESTSmartPlugService:
method __init__ (line 23) | def __init__(self, timeout: float = 10.0):
method _validate_url (line 27) | def _validate_url(url: str) -> bool:
method _parse_headers (line 40) | def _parse_headers(self, headers_json: str | None) -> dict[str, str]:
method _extract_json_path (line 53) | def _extract_json_path(data: Any, path: str) -> Any:
method _send_request (line 69) | async def _send_request(
method turn_on (line 110) | async def turn_on(self, plug: "SmartPlug") -> bool:
method turn_off (line 127) | async def turn_off(self, plug: "SmartPlug") -> bool:
method toggle (line 144) | async def toggle(self, plug: "SmartPlug") -> bool:
method get_status (line 152) | async def get_status(self, plug: "SmartPlug") -> dict:
method get_energy (line 193) | async def get_energy(self, plug: "SmartPlug") -> dict | None:
method _fetch_json (line 236) | async def _fetch_json(self, url: str, headers: dict[str, str]) -> Any:
method test_connection (line 246) | async def test_connection(self, url: str, method: str = "GET", headers...
FILE: backend/app/services/smart_plug_manager.py
class SmartPlugManager (line 22) | class SmartPlugManager:
method __init__ (line 25) | def __init__(self):
method get_service_for_plug (line 32) | async def get_service_for_plug(self, plug: "SmartPlug", db: AsyncSessi...
method _configure_ha_service (line 45) | async def _configure_ha_service(self, db: AsyncSession | None = None):
method set_event_loop (line 64) | def set_event_loop(self, loop: asyncio.AbstractEventLoop):
method start_scheduler (line 68) | def start_scheduler(self):
method stop_scheduler (line 77) | def stop_scheduler(self):
method _schedule_loop (line 88) | async def _schedule_loop(self):
method _snapshot_loop (line 99) | async def _snapshot_loop(self):
method _capture_energy_snapshots (line 116) | async def _capture_energy_snapshots(self):
method _check_schedules (line 164) | async def _check_schedules(self):
method _get_plugs_for_printer (line 210) | async def _get_plugs_for_printer(self, printer_id: int, db: AsyncSessi...
method on_print_start (line 217) | async def on_print_start(self, printer_id: int, db: AsyncSession):
method on_print_complete (line 251) | async def on_print_complete(self, printer_id: int, status: str, db: As...
method _schedule_delayed_off (line 296) | def _schedule_delayed_off(self, plug: "SmartPlug", printer_id: int, de...
method _delayed_off (line 324) | async def _delayed_off(
method _schedule_temp_based_off (line 375) | def _schedule_temp_based_off(self, plug: "SmartPlug", printer_id: int,...
method _temp_based_off (line 403) | async def _temp_based_off(
method _mark_auto_off_pending (line 494) | async def _mark_auto_off_pending(self, plug_id: int, pending: bool):
method _mark_auto_off_executed (line 511) | async def _mark_auto_off_executed(self, plug_id: int):
method _cancel_pending_off (line 536) | def _cancel_pending_off(self, plug_id: int):
method cancel_all_pending (line 545) | def cancel_all_pending(self):
method resume_pending_auto_offs (line 550) | async def resume_pending_auto_offs(self):
FILE: backend/app/services/spool_assignment_notifications.py
function _global_tray_from_assignment (line 12) | def _global_tray_from_assignment(ams_id: int, tray_id: int) -> int:
function _slot_label_from_global_tray (line 21) | def _slot_label_from_global_tray(global_tray_id: int) -> str:
function _tray_profile_and_color_for_global_id (line 34) | def _tray_profile_and_color_for_global_id(state: PrinterState | None, gl...
function _decode_mqtt_mapping_to_global_trays (line 74) | def _decode_mqtt_mapping_to_global_trays(mapping_raw: object) -> list[int]:
function notify_missing_spool_assignments_on_print_start (line 107) | async def notify_missing_spool_assignments_on_print_start(
FILE: backend/app/services/spool_tag_matcher.py
function is_valid_tag (line 23) | def is_valid_tag(tag_uid: str, tray_uuid: str) -> bool:
function is_bambu_tag (line 32) | def is_bambu_tag(tag_uid: str, tray_uuid: str, tray_info_idx: str) -> bool:
function create_spool_from_tray (line 40) | async def create_spool_from_tray(db: AsyncSession, tray_data: dict) -> S...
function find_matching_untagged_spool (line 193) | async def find_matching_untagged_spool(db: AsyncSession, tray_data: dict...
function link_tag_to_inventory_spool (line 270) | async def link_tag_to_inventory_spool(db: AsyncSession, spool: Spool, tr...
function get_spool_by_tag (line 304) | async def get_spool_by_tag(db: AsyncSession, tag_uid: str, tray_uuid: st...
function auto_assign_spool (line 399) | async def auto_assign_spool(
FILE: backend/app/services/spoolbuddy_ssh.py
function _get_ssh_key_dir (line 45) | def _get_ssh_key_dir() -> Path:
function get_or_create_keypair (line 53) | async def get_or_create_keypair() -> tuple[Path, Path]:
function get_public_key (line 94) | async def get_public_key() -> str:
function detect_current_branch (line 100) | def detect_current_branch() -> str:
function _run_ssh_command (line 138) | async def _run_ssh_command(
function perform_ssh_update (line 176) | async def perform_ssh_update(device_id: str, ip_address: str, install_pa...
FILE: backend/app/services/spoolman.py
class SpoolmanSpool (line 16) | class SpoolmanSpool:
class SpoolmanFilament (line 32) | class SpoolmanFilament:
class AMSTray (line 44) | class AMSTray:
class SpoolmanClient (line 59) | class SpoolmanClient:
method __init__ (line 62) | def __init__(self, base_url: str):
method _get_client (line 73) | async def _get_client(self) -> httpx.AsyncClient:
method close (line 92) | async def close(self):
method health_check (line 98) | async def health_check(self) -> bool:
method is_connected (line 115) | def is_connected(self) -> bool:
method get_spools (line 119) | async def get_spools(self) -> list[dict]:
method get_filaments (line 174) | async def get_filaments(self) -> list[dict]:
method get_external_filaments (line 189) | async def get_external_filaments(self) -> list[dict]:
method get_vendors (line 204) | async def get_vendors(self) -> list[dict]:
method create_vendor (line 219) | async def create_vendor(self, name: str) -> dict | None:
method _get_material_density (line 237) | def _get_material_density(self, material: str | None) -> float:
method create_filament (line 271) | async def create_filament(
method create_spool (line 333) | async def create_spool(
method update_spool (line 382) | async def update_spool(
method use_spool (line 424) | async def use_spool(self, spool_id: int, used_weight: float) -> dict |...
method find_spool_by_tag (line 446) | async def find_spool_by_tag(self, tag_uid: str, cached_spools: list[di...
method _find_spool_by_location (line 473) | def _find_spool_by_location(self, location: str, cached_spools: list[d...
method find_spools_by_location_prefix (line 493) | async def find_spools_by_location_prefix(
method clear_location_for_removed_spools (line 514) | async def clear_location_for_removed_spools(
method ensure_bambu_vendor (line 574) | async def ensure_bambu_vendor(self) -> int | None:
method ensure_tag_extra_field (line 589) | async def ensure_tag_extra_field(self) -> bool:
method parse_ams_tray (line 625) | def parse_ams_tray(self, ams_id: int, tray_data: dict) -> AMSTray | None:
method convert_ams_slot_to_location (line 683) | def convert_ams_slot_to_location(self, ams_id: int, tray_id: int) -> str:
method is_bambu_lab_spool (line 704) | def is_bambu_lab_spool(self, tray_uuid: str, tag_uid: str = "", tray_i...
method calculate_remaining_weight (line 747) | def calculate_remaining_weight(self, remain_percent: int, spool_weight...
method sync_ams_tray (line 759) | async def sync_ams_tray(
method _find_or_create_filament (line 885) | async def _find_or_create_filament(self, tray: AMSTray) -> dict | None:
method _create_filament_from_external (line 934) | async def _create_filament_from_external(self, external: dict, tray: A...
function get_spoolman_client (line 958) | async def get_spoolman_client() -> SpoolmanClient | None:
function init_spoolman_client (line 967) | async def init_spoolman_client(url: str) -> SpoolmanClient:
function close_spoolman_client (line 984) | async def close_spoolman_client():
FILE: backend/app/services/spoolman_tracking.py
function _is_non_zero_identifier (line 24) | def _is_non_zero_identifier(value: str) -> bool:
function _to_fixed_hex (line 31) | def _to_fixed_hex(value: int, width: int) -> str:
function _hash_serial_to_hex32 (line 37) | def _hash_serial_to_hex32(serial: str) -> str:
function _global_tray_id_to_ams_slot (line 47) | def _global_tray_id_to_ams_slot(global_tray_id: int) -> tuple[int, int]:
function _get_fallback_spool_tag (line 59) | def _get_fallback_spool_tag(printer_serial: str, global_tray_id: int) ->...
function _resolve_spool_tag (line 67) | def _resolve_spool_tag(tray_info: dict, printer_serial: str = "", global...
function _get_printer_serial (line 83) | async def _get_printer_serial(printer_id: int) -> str:
function _resolve_global_tray_id (line 98) | def _resolve_global_tray_id(slot_id: int, slot_to_tray: list | None, ams...
function build_ams_tray_lookup (line 120) | def build_ams_tray_lookup(raw_data: dict) -> dict[int, dict]:
function store_print_data (line 152) | async def store_print_data(
function cleanup_tracking (line 260) | async def cleanup_tracking(
function _get_spoolman_client_with_fallback (line 303) | async def _get_spoolman_client_with_fallback():
function _report_spool_usage_for_slots (line 323) | async def _report_spool_usage_for_slots(
function _report_partial_usage (line 374) | async def _report_partial_usage(
function report_usage (line 530) | async def report_usage(printer_id: int, archive_id: int):
FILE: backend/app/services/stl_thumbnail.py
function generate_stl_thumbnail (line 20) | def generate_stl_thumbnail(
FILE: backend/app/services/tasmota.py
class TasmotaService (line 15) | class TasmotaService:
method __init__ (line 18) | def __init__(self, timeout: float = 5.0):
method _build_url (line 21) | def _build_url(self, ip: str, command: str) -> str:
method _validate_ip (line 28) | def _validate_ip(ip: str) -> bool:
method _send_command (line 36) | async def _send_command(
method get_status (line 68) | async def get_status(self, plug: "SmartPlug") -> dict:
method turn_on (line 91) | async def turn_on(self, plug: "SmartPlug") -> bool:
method turn_off (line 109) | async def turn_off(self, plug: "SmartPlug") -> bool:
method toggle (line 127) | async def toggle(self, plug: "SmartPlug") -> bool:
method get_energy (line 142) | async def get_energy(self, plug: "SmartPlug") -> dict | None:
method test_connection (line 178) | async def test_connection(
FILE: backend/app/services/timelapse_processor.py
class TimelapseProcessor (line 14) | class TimelapseProcessor:
method __init__ (line 17) | def __init__(self, input_path: Path):
method get_info (line 25) | async def get_info(self) -> dict:
method generate_thumbnails (line 81) | async def generate_thumbnails(
method process (line 129) | async def process(
method _build_atempo_chain (line 238) | def _build_atempo_chain(self, speed: float) -> str:
FILE: backend/app/services/usage_tracker.py
function _decode_mqtt_mapping (line 25) | def _decode_mqtt_mapping(mapping_raw: list | None) -> list[int] | None:
function _match_slots_by_color (line 66) | def _match_slots_by_color(
class PrintSession (line 151) | class PrintSession:
function _to_epoch_seconds (line 169) | def _to_epoch_seconds(value: datetime | None) -> float | None:
function _resolve_spool_id_for_tray (line 179) | async def _resolve_spool_id_for_tray(
function on_print_start (line 233) | async def on_print_start(printer_id: int, data: dict, printer_manager, d...
function on_print_complete (line 353) | async def on_print_complete(
function _resolve_3mf_fallback (line 590) | async def _resolve_3mf_fallback(archive, db: AsyncSession, base_dir):
function _find_3mf_by_filename (line 658) | async def _find_3mf_by_filename(
function _track_from_3mf (line 725) | async def _track_from_3mf(
FILE: backend/app/services/virtual_printer/bind_server.py
class BindServer (line 37) | class BindServer:
method __init__ (line 47) | def __init__(
method _create_tls_context (line 68) | def _create_tls_context(self) -> ssl.SSLContext | None:
method start (line 78) | async def start(self) -> None:
method stop (line 136) | async def stop(self) -> None:
method _handle_client (line 149) | async def _handle_client(
method _parse_frame (line 212) | def _parse_frame(self, data: bytes) -> dict | None:
method _build_frame (line 236) | def _build_frame(self, payload: dict) -> bytes:
FILE: backend/app/services/virtual_printer/certificate.py
function _get_local_ip (line 31) | def _get_local_ip() -> str:
class CertificateService (line 43) | class CertificateService:
method __init__ (line 51) | def __init__(self, cert_dir: Path, serial: str = DEFAULT_SERIAL, share...
method ensure_certificates (line 68) | def ensure_certificates(self) -> tuple[Path, Path]:
method _load_existing_ca (line 79) | def _load_existing_ca(self) -> tuple[rsa.RSAPrivateKey, x509.Certifica...
method _get_or_create_ca (line 112) | def _get_or_create_ca(self) -> tuple[rsa.RSAPrivateKey, x509.Certifica...
method _generate_ca_certificate (line 141) | def _generate_ca_certificate(self) -> tuple[rsa.RSAPrivateKey, x509.Ce...
method _build_san_entries (line 199) | def _build_san_entries(self, local_ip: str, additional_ips: list[str] ...
method generate_certificates (line 220) | def generate_certificates(self, additional_ips: list[str] | None = Non...
method delete_printer_certificate (line 327) | def delete_printer_certificate(self) -> None:
method delete_certificates (line 334) | def delete_certificates(self, include_ca: bool = False) -> None:
FILE: backend/app/services/virtual_printer/ftp_server.py
class FTPSession (line 28) | class FTPSession:
method __init__ (line 31) | def __init__(
method send (line 72) | async def send(self, code: int, message: str) -> None:
method handle (line 79) | async def handle(self) -> None:
method _cleanup (line 133) | async def _cleanup(self) -> None:
method cmd_USER (line 154) | async def cmd_USER(self, arg: str) -> None:
method cmd_PASS (line 162) | async def cmd_PASS(self, arg: str) -> None:
method cmd_SYST (line 175) | async def cmd_SYST(self, arg: str) -> None:
method cmd_FEAT (line 179) | async def cmd_FEAT(self, arg: str) -> None:
method cmd_PWD (line 195) | async def cmd_PWD(self, arg: str) -> None:
method cmd_CWD (line 202) | async def cmd_CWD(self, arg: str) -> None:
method cmd_TYPE (line 210) | async def cmd_TYPE(self, arg: str) -> None:
method _bind_passive_port (line 222) | async def _bind_passive_port(self) -> bool:
method cmd_EPSV (line 244) | async def cmd_EPSV(self, arg: str) -> None:
method cmd_PASV (line 267) | async def cmd_PASV(self, arg: str) -> None:
method _handle_data_connection (line 309) | async def _handle_data_connection(self, reader: asyncio.StreamReader, ...
method _close_data_connection (line 353) | async def _close_data_connection(self) -> None:
method cmd_STOR (line 382) | async def cmd_STOR(self, arg: str) -> None:
method cmd_SIZE (line 464) | async def cmd_SIZE(self, arg: str) -> None:
method cmd_QUIT (line 472) | async def cmd_QUIT(self, arg: str) -> None:
method cmd_NOOP (line 477) | async def cmd_NOOP(self, arg: str) -> None:
method cmd_OPTS (line 481) | async def cmd_OPTS(self, arg: str) -> None:
method cmd_PBSZ (line 488) | async def cmd_PBSZ(self, arg: str) -> None:
method cmd_PROT (line 495) | async def cmd_PROT(self, arg: str) -> None:
method cmd_MKD (line 508) | async def cmd_MKD(self, arg: str) -> None:
method cmd_LIST (line 516) | async def cmd_LIST(self, arg: str) -> None:
class VirtualPrinterFTPServer (line 526) | class VirtualPrinterFTPServer:
method __init__ (line 532) | def __init__(
method start (line 570) | async def start(self) -> None:
method _handle_client (line 627) | async def _handle_client(self, reader: asyncio.StreamReader, writer: a...
method stop (line 656) | async def stop(self) -> None:
FILE: backend/app/services/virtual_printer/manager.py
function _get_serial_for_model (line 87) | def _get_serial_for_model(model: str, serial_suffix: str) -> str:
class VirtualPrinterInstance (line 93) | class VirtualPrinterInstance:
method __init__ (line 100) | def __init__(
method serial (line 162) | def serial(self) -> str:
method cert_path (line 167) | def cert_path(self) -> Path:
method key_path (line 171) | def key_path(self) -> Path:
method is_proxy (line 175) | def is_proxy(self) -> bool:
method is_running (line 179) | def is_running(self) -> bool:
method generate_certificates (line 182) | def generate_certificates(self) -> tuple[Path, Path]:
method on_file_received (line 194) | async def on_file_received(self, file_path: Path, source_ip: str) -> N...
method on_print_command (line 211) | async def on_print_command(self, filename: str, data: dict) -> None:
method _archive_file (line 215) | async def _archive_file(self, file_path: Path, source_ip: str) -> None:
method _queue_file (line 256) | async def _queue_file(self, file_path: Path, source_ip: str) -> None:
method _add_to_print_queue (line 289) | async def _add_to_print_queue(self, file_path: Path, source_ip: str) -...
method _extract_plate_id (line 348) | def _extract_plate_id(file_path: Path) -> int | None:
method start_server (line 369) | async def start_server(self) -> None:
method stop_server (line 452) | async def stop_server(self) -> None:
method start_proxy (line 468) | async def start_proxy(self) -> None:
method _start_fallback_ssdp (line 527) | def _start_fallback_ssdp(self, proxy_serial: str, run_with_logging) ->...
method stop_proxy (line 543) | async def stop_proxy(self) -> None:
method _cancel_tasks (line 556) | async def _cancel_tasks(self) -> None:
method get_status (line 567) | def get_status(self) -> dict:
class VirtualPrinterManager (line 578) | class VirtualPrinterManager:
method __init__ (line 584) | def __init__(self):
method _ensure_base_directories (line 594) | def _ensure_base_directories(self) -> None:
method set_session_factory (line 606) | def set_session_factory(self, session_factory: Callable) -> None:
method is_enabled (line 611) | def is_enabled(self) -> bool:
method sync_from_db (line 615) | async def sync_from_db(self) -> None:
method remove_instance (line 723) | async def remove_instance(self, vp_id: int) -> None:
method stop_all (line 733) | async def stop_all(self) -> None:
method get_instance (line 742) | def get_instance(self, vp_id: int) -> VirtualPrinterInstance | None:
method get_all_status (line 746) | def get_all_status(self) -> list[dict]:
method get_status (line 760) | def get_status(self) -> dict:
method configure (line 790) | async def configure(
FILE: backend/app/services/virtual_printer/mqtt_server.py
class VirtualPrinterMQTTServer (line 37) | class VirtualPrinterMQTTServer:
method __init__ (line 47) | def __init__(
method start (line 76) | async def start(self) -> None:
method _authenticate (line 139) | async def _authenticate(self, session) -> bool:
method stop (line 159) | async def stop(self) -> None:
class SimpleMQTTServer (line 172) | class SimpleMQTTServer:
method __init__ (line 180) | def __init__(
method start (line 221) | async def start(self) -> None:
method stop (line 301) | async def stop(self) -> None:
method _extract_serial_from_topic (line 334) | def _extract_serial_from_topic(topic: str) -> str | None:
method _periodic_status_push (line 349) | async def _periodic_status_push(self) -> None:
method _handle_client (line 381) | async def _handle_client(self, reader: asyncio.StreamReader, writer: a...
method _read_remaining_length (line 447) | async def _read_remaining_length(self, reader: asyncio.StreamReader) -...
method _handle_connect (line 467) | async def _handle_connect(self, payload: bytes, writer: asyncio.Stream...
method _handle_subscribe (line 527) | async def _handle_subscribe(self, payload: bytes, writer: asyncio.Stre...
method _send_status_report (line 577) | async def _send_status_report(self, writer: asyncio.StreamWriter, seri...
method _send_version_response (line 660) | async def _send_version_response(
method set_gcode_state (line 733) | def set_gcode_state(self, state: str, filename: str = "", prepare_perc...
method _publish_to_report (line 742) | async def _publish_to_report(self, writer: asyncio.StreamWriter, paylo...
method _send_print_response (line 772) | async def _send_print_response(
method _handle_publish (line 803) | async def _handle_publish(self, header: int, payload: bytes, writer: a...
method _notify_print_command (line 905) | async def _notify_print_command(self, filename: str, data: dict) -> None:
FILE: backend/app/services/virtual_printer/ssdp_server.py
class VirtualPrinterSSDPServer (line 28) | class VirtualPrinterSSDPServer:
method __init__ (line 31) | def __init__(
method _get_local_ip (line 63) | def _get_local_ip(self) -> str:
method _build_notify_message (line 79) | def _build_notify_message(self) -> bytes:
method _build_response_message (line 104) | def _build_response_message(self) -> bytes:
method start (line 127) | async def start(self) -> None:
method stop (line 248) | async def stop(self) -> None:
method _cleanup (line 254) | async def _cleanup(self) -> None:
method _send_notify (line 276) | async def _send_notify(self) -> None:
method _send_byebye (line 299) | async def _send_byebye(self) -> None:
method _handle_message (line 319) | async def _handle_message(
class SSDPProxy (line 356) | class SSDPProxy:
method __init__ (line 366) | def __init__(
method _parse_ssdp_message (line 391) | def _parse_ssdp_message(self, data: bytes) -> dict[str, str]:
method _rewrite_ssdp (line 404) | def _rewrite_ssdp(self, data: bytes) -> bytes:
method start (line 453) | async def start(self) -> None:
method stop (line 538) | async def stop(self) -> None:
method _cleanup (line 544) | async def _cleanup(self) -> None:
method _handle_local_packet (line 555) | async def _handle_local_packet(self, data: bytes, addr: tuple[str, int...
method _respond_to_msearch (line 594) | async def _respond_to_msearch(self, data: bytes, addr: tuple[str, int]...
method _broadcast_to_remote (line 632) | async def _broadcast_to_remote(self) -> None:
FILE: backend/app/services/virtual_printer/tcp_proxy.py
class _SessionReuseSSLContext (line 27) | class _SessionReuseSSLContext:
method __init__ (line 40) | def __init__(self, ctx: ssl.SSLContext, session: ssl.SSLSession) -> None:
method __getattr__ (line 44) | def __getattr__(self, name: str) -> object:
method wrap_bio (line 47) | def wrap_bio(
function detect_port_redirect (line 65) | def detect_port_redirect(port: int) -> int | None:
class TLSProxy (line 107) | class TLSProxy:
method __init__ (line 114) | def __init__(
method _ip_to_le_int_bytes (line 176) | def _ip_to_le_int_bytes(ip: str) -> bytes:
method _create_server_ssl_context (line 189) | def _create_server_ssl_context(self) -> ssl.SSLContext:
method _create_client_ssl_context (line 199) | def _create_client_ssl_context(self) -> ssl.SSLContext:
method start (line 211) | async def start(self) -> None:
method stop (line 263) | async def stop(self) -> None:
method _handle_client (line 288) | async def _handle_client(
method _rewrite_mqtt_ip (line 383) | def _rewrite_mqtt_ip(
method _forward (line 527) | async def _forward(
class TCPProxy (line 602) | class TCPProxy:
method __init__ (line 609) | def __init__(
method start (line 631) | async def start(self) -> None:
method stop (line 671) | async def stop(self) -> None:
method _handle_client (line 695) | async def _handle_client(
method _forward (line 772) | async def _forward(
class FTPTLSProxy (line 801) | class FTPTLSProxy(TLSProxy):
method stop (line 815) | async def stop(self) -> None:
method start (line 827) | async def start(self) -> None:
method _handle_client (line 832) | async def _handle_client(
method _forward_ftp_commands (line 949) | async def _forward_ftp_commands(
method _forward_ftp_control (line 1020) | async def _forward_ftp_control(
method _maybe_rewrite_pasv (line 1081) | async def _maybe_rewrite_pasv(
method _create_data_proxy (line 1132) | async def _create_data_proxy(
method _start_data_proxy_server (line 1188) | async def _start_data_proxy_server(
class SlicerProxyManager (line 1416) | class SlicerProxyManager:
method __init__ (line 1434) | def __init__(
method start (line 1479) | async def start(self) -> None:
method stop (line 1711) | async def stop(self) -> None:
method _probe_handler (line 1768) | async def _probe_handler(self, reader: asyncio.StreamReader, writer: a...
method _log_activity (line 1783) | def _log_activity(self, name: str, message: str) -> None:
method is_running (line 1792) | def is_running(self) -> bool:
method get_status (line 1796) | def get_status(self) -> dict:
FILE: backend/app/utils/color_utils.py
function colors_similar (line 4) | def colors_similar(hex_a: str, hex_b: str, threshold: int = 50) -> bool:
FILE: backend/app/utils/filament_ids.py
function filament_id_to_setting_id (line 14) | def filament_id_to_setting_id(filament_id: str) -> str:
function setting_id_to_filament_id (line 38) | def setting_id_to_filament_id(setting_id: str) -> str:
function normalize_slicer_filament (line 59) | def normalize_slicer_filament(slicer_filament: str | None) -> tuple[str,...
FILE: backend/app/utils/printer_models.py
function has_ethernet (line 140) | def has_ethernet(model: str | None) -> bool:
function get_rod_type (line 148) | def get_rod_type(model: str | None) -> str | None:
function normalize_printer_model_id (line 169) | def normalize_printer_model_id(model_id: str | None) -> str | None:
function normalize_printer_model (line 189) | def normalize_printer_model(raw_model: str | None) -> str | None:
FILE: backend/app/utils/tag_normalization.py
function normalize_hex (line 4) | def normalize_hex(value: str | None) -> str:
function normalize_tag_uid (line 11) | def normalize_tag_uid(value: str | None) -> str:
function normalize_tray_uuid (line 19) | def normalize_tray_uuid(value: str | None) -> str:
FILE: backend/app/utils/threemf_tools.py
function parse_gcode_layer_filament_usage (line 21) | def parse_gcode_layer_filament_usage(gcode_content: str) -> dict[int, di...
function mm_to_grams (line 133) | def mm_to_grams(
function extract_layer_filament_usage_from_3mf (line 157) | def extract_layer_filament_usage_from_3mf(file_path: Path) -> dict[int, ...
function get_cumulative_usage_at_layer (line 183) | def get_cumulative_usage_at_layer(
function extract_filament_properties_from_3mf (line 210) | def extract_filament_properties_from_3mf(file_path: Path) -> dict[int, d...
function extract_nozzle_mapping_from_3mf (line 267) | def extract_nozzle_mapping_from_3mf(zf: zipfile.ZipFile) -> dict[int, in...
function extract_filament_usage_from_3mf (line 367) | def extract_filament_usage_from_3mf(file_path: Path, plate_id: int | Non...
function inject_gcode_into_3mf (line 445) | def inject_gcode_into_3mf(
FILE: backend/tests/conftest.py
function _cleanup_test_plate_cal_dir (line 35) | def _cleanup_test_plate_cal_dir():
function event_loop (line 49) | def event_loop():
function test_engine (line 63) | async def test_engine():
function db_session (line 119) | async def db_session(test_engine) -> AsyncGenerator[AsyncSession, None]:
function async_client (line 127) | async def async_client(test_engine, db_session) -> AsyncGenerator[AsyncC...
function mock_tasmota_service (line 176) | def mock_tasmota_service():
function mock_homeassistant_service (line 209) | def mock_homeassistant_service():
function mock_mqtt_client (line 247) | def mock_mqtt_client():
function mock_mqtt_smart_plug_service (line 259) | def mock_mqtt_smart_plug_service():
function mock_ftp_client (line 277) | def mock_ftp_client():
function mock_httpx_client (line 289) | def mock_httpx_client():
function mock_printer_manager (line 308) | def mock_printer_manager():
function smart_plug_factory (line 330) | def smart_plug_factory(db_session):
function printer_factory (line 394) | def printer_factory(db_session):
function notification_provider_factory (line 425) | def notification_provider_factory(db_session):
function archive_factory (line 468) | def archive_factory(db_session):
function sample_mqtt_print_start (line 502) | def sample_mqtt_print_start():
function sample_mqtt_print_complete (line 516) | def sample_mqtt_print_complete():
function sample_printer_status (line 528) | def sample_printer_status():
class LogCapture (line 551) | class LogCapture(logging.Handler):
method __init__ (line 554) | def __init__(self):
method emit (line 558) | def emit(self, record: logging.LogRecord):
method clear (line 561) | def clear(self):
method get_errors (line 564) | def get_errors(self) -> list[logging.LogRecord]:
method get_warnings (line 568) | def get_warnings(self) -> list[logging.LogRecord]:
method has_errors (line 572) | def has_errors(self) -> bool:
method format_errors (line 576) | def format_errors(self) -> str:
function capture_logs (line 586) | def capture_logs():
function assert_no_log_errors (line 610) | def assert_no_log_errors(capture_logs):
FILE: backend/tests/integration/test_advanced_auth_api.py
function _setup_admin (line 25) | async def _setup_admin(async_client: AsyncClient, username: str = "admin...
function _setup_smtp_and_advanced_auth (line 42) | async def _setup_smtp_and_advanced_auth(async_client: AsyncClient, token...
function _create_regular_user (line 49) | async def _create_regular_user(
class TestSMTPConfigAPI (line 66) | class TestSMTPConfigAPI:
method admin_token (line 70) | async def admin_token(self, async_client: AsyncClient):
method test_save_smtp_settings (line 75) | async def test_save_smtp_settings(self, async_client: AsyncClient, adm...
method test_get_smtp_settings_masks_password (line 87) | async def test_get_smtp_settings_masks_password(self, async_client: As...
method test_smtp_settings_requires_admin (line 101) | async def test_smtp_settings_requires_admin(self, async_client: AsyncC...
method test_save_smtp_settings_no_auth (line 114) | async def test_save_smtp_settings_no_auth(self, async_client: AsyncCli...
method test_test_smtp_connection (line 121) | async def test_test_smtp_connection(self, async_client: AsyncClient, a...
class TestAdvancedAuthToggleAPI (line 141) | class TestAdvancedAuthToggleAPI:
method admin_token (line 145) | async def admin_token(self, async_client: AsyncClient):
method test_enable_advanced_auth (line 150) | async def test_enable_advanced_auth(self, async_client: AsyncClient, a...
method test_enable_advanced_auth_without_smtp (line 162) | async def test_enable_advanced_auth_without_smtp(self, async_client: A...
method test_disable_advanced_auth (line 171) | async def test_disable_advanced_auth(self, async_client: AsyncClient, ...
method test_advanced_auth_status_public (line 184) | async def test_advanced_auth_status_public(self, async_client: AsyncCl...
method test_enable_requires_admin (line 194) | async def test_enable_requires_admin(self, async_client: AsyncClient, ...
class TestEmailLoginAPI (line 206) | class TestEmailLoginAPI:
method admin_token (line 210) | async def admin_token(self, async_client: AsyncClient):
method test_login_with_email (line 215) | async def test_login_with_email(self, async_client: AsyncClient, admin...
method test_login_with_email_case_insensitive (line 249) | async def test_login_with_email_case_insensitive(self, async_client: A...
method test_login_with_email_advanced_auth_disabled (line 277) | async def test_login_with_email_advanced_auth_disabled(self, async_cli...
method test_login_with_username_still_works (line 297) | async def test_login_with_username_still_works(self, async_client: Asy...
class TestForgotPasswordAPI (line 325) | class TestForgotPasswordAPI:
method admin_token (line 329) | async def admin_token(self, async_client: AsyncClient):
method test_forgot_password_sends_email (line 334) | async def test_forgot_password_sends_email(self, async_client: AsyncCl...
method test_forgot_password_unknown_email (line 362) | async def test_forgot_password_unknown_email(self, async_client: Async...
method test_forgot_password_requires_advanced_auth (line 379) | async def test_forgot_password_requires_advanced_auth(self, async_clie...
method test_forgot_password_changes_password (line 390) | async def test_forgot_password_changes_password(self, async_client: As...
class TestAdminResetPasswordAPI (line 466) | class TestAdminResetPasswordAPI:
method admin_token (line 470) | async def admin_token(self, async_client: AsyncClient):
method test_reset_password_sends_email (line 475) | async def test_reset_password_sends_email(self, async_client: AsyncCli...
method test_reset_password_requires_admin (line 502) | async def test_reset_password_requires_admin(self, async_client: Async...
method test_reset_password_requires_advanced_auth (line 519) | async def test_reset_password_requires_advanced_auth(self, async_clien...
method test_reset_password_user_not_found (line 533) | async def test_reset_password_user_not_found(self, async_client: Async...
method test_reset_password_user_no_email (line 548) | async def test_reset_password_user_no_email(self, async_client: AsyncC...
class TestUserCreationAdvancedAuth (line 576) | class TestUserCreationAdvancedAuth:
method admin_token (line 580) | async def admin_token(self, async_client: AsyncClient):
method test_create_user_advanced_auth_requires_email (line 585) | async def test_create_user_advanced_auth_requires_email(self, async_cl...
method test_create_user_advanced_auth_auto_password (line 601) | async def test_create_user_advanced_auth_auto_password(self, async_cli...
method test_create_user_duplicate_email (line 624) | async def test_create_user_duplicate_email(self, async_client: AsyncCl...
method test_create_user_response_includes_email (line 649) | async def test_create_user_response_includes_email(self, async_client:...
class TestAuthSourcePasswordResetBlocking (line 673) | class TestAuthSourcePasswordResetBlocking:
method admin_token (line 677) | async def admin_token(self, async_client: AsyncClient):
method test_forgot_password_silently_skips_oidc_user (line 682) | async def test_forgot_password_silently_skips_oidc_user(
FILE: backend/tests/integration/test_ams_history_api.py
class TestAMSHistoryAPI (line 9) | class TestAMSHistoryAPI:
method ams_history_factory (line 13) | async def ams_history_factory(self, db_session, printer_factory):
method test_get_ams_history_empty (line 43) | async def test_get_ams_history_empty(self, async_client: AsyncClient, ...
method test_get_ams_history_with_data (line 55) | async def test_get_ams_history_with_data(self, async_client: AsyncClie...
method test_get_ams_history_with_stats (line 68) | async def test_get_ams_history_with_stats(
method test_get_ams_history_with_hours_filter (line 90) | async def test_get_ams_history_with_hours_filter(
method test_get_ams_history_custom_hours (line 109) | async def test_get_ams_history_custom_hours(self, async_client: AsyncC...
method test_get_ams_history_different_ams_units (line 119) | async def test_get_ams_history_different_ams_units(
method test_delete_old_history (line 143) | async def test_delete_old_history(
method test_delete_old_history_no_records (line 159) | async def test_delete_old_history_no_records(self, async_client: Async...
FILE: backend/tests/integration/test_ams_labels_api.py
class TestAmsLabelsAPI (line 11) | class TestAmsLabelsAPI:
method _mock_printer_state (line 14) | def _mock_printer_state(self, ams_units=None):
method test_get_labels_empty (line 29) | async def test_get_labels_empty(self, async_client: AsyncClient, print...
method test_save_label_with_serial (line 40) | async def test_save_label_with_serial(self, async_client: AsyncClient,...
method test_save_label_without_serial_uses_synthetic_key (line 52) | async def test_save_label_without_serial_uses_synthetic_key(
method test_save_label_whitespace_serial_uses_synthetic_key (line 73) | async def test_save_label_whitespace_serial_uses_synthetic_key(
method test_save_label_updates_existing (line 93) | async def test_save_label_updates_existing(self, async_client: AsyncCl...
method test_save_label_printer_not_found (line 109) | async def test_save_label_printer_not_found(self, async_client: AsyncC...
method test_save_label_validation_empty_label (line 119) | async def test_save_label_validation_empty_label(self, async_client: A...
method test_get_labels_resolves_serial_to_ams_id (line 130) | async def test_get_labels_resolves_serial_to_ams_id(self, async_client...
method test_get_labels_no_printer_state (line 150) | async def test_get_labels_no_printer_state(self, async_client: AsyncCl...
method test_delete_label (line 161) | async def test_delete_label(self, async_client: AsyncClient, printer_f...
method test_delete_nonexistent_label_succeeds (line 181) | async def test_delete_nonexistent_label_succeeds(self, async_client: A...
method test_delete_label_whitespace_serial_uses_synthetic_key (line 190) | async def test_delete_label_whitespace_serial_uses_synthetic_key(
FILE: backend/tests/integration/test_archives_api.py
class TestArchivesAPI (line 10) | class TestArchivesAPI:
method test_list_archives_empty (line 19) | async def test_list_archives_empty(self, async_client: AsyncClient):
method test_list_archives_with_data (line 30) | async def test_list_archives_with_data(
method test_list_archives_pagination (line 47) | async def test_list_archives_pagination(
method test_list_archives_filter_by_printer (line 66) | async def test_list_archives_filter_by_printer(
method test_get_archive (line 87) | async def test_get_archive(self, async_client: AsyncClient, archive_fa...
method test_get_archive_not_found (line 101) | async def test_get_archive_not_found(self, async_cl
Copy disabled (too large)
Download .json
Condensed preview — 718 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (19,871K chars).
[
{
"path": ".codeql/codeql-config.yml",
"chars": 3736,
"preview": "name: \"Bambuddy CodeQL Configuration\"\n\n# Uses the default query suite with accepted-risk exclusions.\n# Each exclusion is"
},
{
"path": ".codeql/javascript-bambuddy.qls",
"chars": 600,
"preview": "# Bambuddy JavaScript Security & Quality Suite\n#\n# Extends the standard javascript-security-and-quality suite,\n# excludi"
},
{
"path": ".codeql/python-bambuddy.qls",
"chars": 3189,
"preview": "# Bambuddy Python Security & Quality Suite\n#\n# Extends the standard python-security-and-quality suite, excluding\n# accep"
},
{
"path": ".dockerignore",
"chars": 904,
"preview": "# Git\n# Exclude all .git contents EXCEPT HEAD. HEAD is a tiny text file (under\n# 100 bytes) containing e.g. `ref: refs/h"
},
{
"path": ".gitattributes",
"chars": 146,
"preview": "# Force CRLF line endings for Windows batch files so cmd.exe can parse them\n# regardless of the user's core.autocrlf set"
},
{
"path": ".github/CODEOWNERS",
"chars": 476,
"preview": "# CODEOWNERS - Defines code owners who will be requested for review\n# See: https://docs.github.com/en/repositories/manag"
},
{
"path": ".github/FUNDING.yml",
"chars": 31,
"preview": "github: maziggy\nko_fi: maziggy\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 4679,
"preview": "name: Bug Report\ndescription: Report a bug or unexpected behavior\ntitle: \"[Bug]: \"\nlabels: [\"bug\", \"triage\"]\nbody:\n - t"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 306,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: Documentation\n url: https://github.com/maziggy/bambuddy-wiki\n "
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 2726,
"preview": "name: Feature Request\ndescription: Suggest a new feature or enhancement\ntitle: \"[Feature]: \"\nlabels: [\"enhancement\"]\nbod"
},
{
"path": ".github/MAINTAINERS.md",
"chars": 3524,
"preview": "# Maintainer Guide\n\nThis document provides setup instructions for repository maintainers.\n\n## Branch Protection Setup\n\nT"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 1766,
"preview": "## Description\n\n<!-- Provide a brief description of your changes -->\n\n## Related Issue\n\n<!-- Link to the issue this PR a"
},
{
"path": ".github/workflows/ci.yml",
"chars": 11176,
"preview": "name: CI\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n workflow_dispatch:\n # Run on PRs ta"
},
{
"path": ".github/workflows/cleanup-ghcr.yml",
"chars": 4373,
"preview": "name: Cleanup GHCR untagged images\n\n# Deletes untagged (orphan) container versions from GHCR while preserving\n# any dige"
},
{
"path": ".github/workflows/codeql.yml",
"chars": 888,
"preview": "name: \"CodeQL\"\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n schedule:\n - cron: '0 6 * * 1"
},
{
"path": ".github/workflows/issue-closed.yml",
"chars": 1043,
"preview": "name: Clean up closed issues\n\non:\n issues:\n types: [closed]\n\npermissions:\n issues: write\n\njobs:\n remove-labels:\n "
},
{
"path": ".github/workflows/repo-stats.yml",
"chars": 439,
"preview": "name: GitHub Repo Stats\n\non:\n schedule:\n - cron: \"0 23 * * *\"\n workflow_dispatch:\n\npermissions:\n contents: write\n\n"
},
{
"path": ".github/workflows/security.yml",
"chars": 15559,
"preview": "name: Security Audit\n\non:\n schedule:\n # Run weekly on Monday at 6:00 UTC\n - cron: '0 6 * * 1'\n push:\n paths:\n"
},
{
"path": ".github/workflows/stale.yml",
"chars": 613,
"preview": "name: Close stale issues\n\non:\n schedule:\n - cron: '0 0 * * *' # Run daily at midnight UTC\n\npermissions:\n issues: w"
},
{
"path": ".github/workflows.disabled/ci.yml",
"chars": 7784,
"preview": "name: CI\n\non:\n push:\n branches: [main, develop]\n pull_request:\n branches: [main, develop]\n\nenv:\n PYTHON_VERSION"
},
{
"path": ".gitignore",
"chars": 950,
"preview": "# Claude\n.claude/\nCLAUDE.md\n\n# macOS\n.DS_Store\n**/.DS_Store\n**/._.DS_Store\n\n# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*"
},
{
"path": ".pre-commit-config.yaml",
"chars": 1535,
"preview": "# Pre-commit hooks for BamBuddy\n# Install with: pip install pre-commit && pre-commit install\n\nrepos:\n # Ruff - Fast Pyt"
},
{
"path": ".trivyignore",
"chars": 1399,
"preview": "# Dockerfile USER directive (DS-0002): Bambuddy runs as a single-host\n# Docker container where root is needed for device"
},
{
"path": "CHANGELOG.md",
"chars": 487597,
"preview": "# Changelog\n\nAll notable changes to Bambuddy will be documented in this file.\n\n## [0.2.3.2] - 2020-04-22\n\n### Improved\n-"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 1659,
"preview": "# Code of Conduct\n\n## Our Commitment\n\nThe Bambuddy community is dedicated to providing a welcoming and supportive enviro"
},
{
"path": "CONTRIBUTING.md",
"chars": 13171,
"preview": "# Contributing to Bambuddy\n\nThank you for your interest in contributing to Bambuddy! This document provides guidelines a"
},
{
"path": "DOCKERHUB.md",
"chars": 4750,
"preview": "# Bambuddy\n\n**Self-hosted print archive and management system for Bambu Lab 3D printers.**\n\nNo cloud dependency. Complet"
},
{
"path": "Dockerfile",
"chars": 3296,
"preview": "# Build frontend\nFROM node:22-bookworm-slim AS frontend-builder\n\nWORKDIR /app/frontend\n\n# Copy package files first for b"
},
{
"path": "Dockerfile.test",
"chars": 1239,
"preview": "# Test image for running backend and frontend tests\nFROM python:3.13-slim AS backend-test\n\nWORKDIR /app\n\n# Install syste"
},
{
"path": "LICENSE",
"chars": 34523,
"preview": " GNU AFFERO GENERAL PUBLIC LICENSE\n Version 3, 19 November 2007\n\n Copyright (C)"
},
{
"path": "README.md",
"chars": 31042,
"preview": "<p align=\"center\">\n <img src=\"static/img/bambuddy_logo_dark.png\" alt=\"Bambuddy Logo\" width=\"300\">\n</p>\n\n<h1 align=\"cent"
},
{
"path": "SECURITY.md",
"chars": 2939,
"preview": "# Security Policy\n\n## Reporting a Vulnerability\n\nThe Bambuddy team takes security seriously. We appreciate your efforts "
},
{
"path": "UPDATING.md",
"chars": 2951,
"preview": "# Updating Bambuddy\n\n> **One-time note for 0.2.2.x → 0.2.3:** the in-app **Update** button does not\n> reliably perform t"
},
{
"path": "backend/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "backend/app/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "backend/app/api/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "backend/app/api/routes/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "backend/app/api/routes/ams_history.py",
"chars": 4421,
"preview": "\"\"\"API routes for AMS sensor history.\"\"\"\n\nfrom datetime import datetime, timedelta, timezone\n\nfrom fastapi import APIRou"
},
{
"path": "backend/app/api/routes/api_keys.py",
"chars": 4613,
"preview": "import logging\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.ext."
},
{
"path": "backend/app/api/routes/archives.py",
"chars": 146306,
"preview": "import io\nimport json\nimport logging\nimport zipfile\nfrom collections import defaultdict\nfrom datetime import date, datet"
},
{
"path": "backend/app/api/routes/auth.py",
"chars": 51595,
"preview": "import logging\nimport os\nimport secrets\nfrom datetime import datetime, timedelta, timezone\nfrom typing import Annotated\n"
},
{
"path": "backend/app/api/routes/background_dispatch.py",
"chars": 1158,
"preview": "from fastapi import APIRouter, HTTPException\n\nfrom backend.app.core.auth import RequirePermissionIfAuthEnabled\nfrom back"
},
{
"path": "backend/app/api/routes/bug_report.py",
"chars": 3502,
"preview": "\"\"\"Bug report endpoint for submitting user bug reports to GitHub.\"\"\"\n\nimport logging\n\nfrom fastapi import APIRouter, Que"
},
{
"path": "backend/app/api/routes/camera.py",
"chars": 52723,
"preview": "\"\"\"Camera streaming API endpoints for Bambu Lab printers.\"\"\"\n\nimport asyncio\nimport logging\nimport os\nimport subprocess\n"
},
{
"path": "backend/app/api/routes/cloud.py",
"chars": 36339,
"preview": "\"\"\"\nBambu Lab Cloud API Routes\n\nHandles authentication and profile management with Bambu Cloud.\n\"\"\"\n\nimport json\nimport "
},
{
"path": "backend/app/api/routes/discovery.py",
"chars": 5541,
"preview": "\"\"\"\nPrinter discovery API endpoints.\n\nProvides endpoints for discovering Bambu Lab printers on the local network.\nSuppor"
},
{
"path": "backend/app/api/routes/external_links.py",
"chars": 8500,
"preview": "\"\"\"API routes for external sidebar links.\"\"\"\n\nimport logging\nimport uuid\nfrom pathlib import Path\n\nfrom fastapi import A"
},
{
"path": "backend/app/api/routes/filaments.py",
"chars": 7860,
"preview": "from fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import A"
},
{
"path": "backend/app/api/routes/firmware.py",
"chars": 11137,
"preview": "\"\"\"\nFirmware Update API Routes\n\nCheck for firmware updates from Bambu Lab.\nAlso provides endpoints for uploading firmwar"
},
{
"path": "backend/app/api/routes/github_backup.py",
"chars": 12672,
"preview": "\"\"\"API routes for GitHub profile backup.\"\"\"\n\nimport logging\n\nfrom fastapi import APIRouter, Depends, HTTPException, Quer"
},
{
"path": "backend/app/api/routes/groups.py",
"chars": 10445,
"preview": "\"\"\"Group management API routes.\"\"\"\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom sqlalchemy import"
},
{
"path": "backend/app/api/routes/inventory.py",
"chars": 56612,
"preview": "import json\nimport logging\n\nimport httpx\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom fastapi.responses im"
},
{
"path": "backend/app/api/routes/kprofiles.py",
"chars": 13748,
"preview": "\"\"\"API routes for K-profile (pressure advance) management.\"\"\"\n\nimport asyncio\nimport logging\n\nfrom fastapi import APIRou"
},
{
"path": "backend/app/api/routes/library.py",
"chars": 109540,
"preview": "\"\"\"API routes for File Manager (Library) functionality.\"\"\"\n\nimport base64\nimport binascii\nimport hashlib\nimport logging\n"
},
{
"path": "backend/app/api/routes/local_backup.py",
"chars": 3722,
"preview": "\"\"\"API routes for scheduled local backups.\"\"\"\n\nimport logging\n\nfrom fastapi import APIRouter, Path\nfrom fastapi.response"
},
{
"path": "backend/app/api/routes/local_presets.py",
"chars": 6799,
"preview": "\"\"\"API routes for local slicer presets (imported from OrcaSlicer, etc.).\"\"\"\n\nimport json\nimport logging\n\nfrom fastapi im"
},
{
"path": "backend/app/api/routes/maintenance.py",
"chars": 27185,
"preview": "\"\"\"Maintenance tracking API routes.\"\"\"\n\nimport logging\nfrom datetime import datetime, timezone\n\nfrom fastapi import APIR"
},
{
"path": "backend/app/api/routes/metrics.py",
"chars": 17963,
"preview": "\"\"\"Prometheus metrics endpoint for external monitoring.\"\"\"\n\nimport platform\n\nfrom fastapi import APIRouter, Depends, Hea"
},
{
"path": "backend/app/api/routes/mfa.py",
"chars": 75057,
"preview": "\"\"\"2FA (TOTP + Email OTP) and OIDC authentication routes.\n\nSecurity model\n--------------\n* Pre-auth tokens : secrets.to"
},
{
"path": "backend/app/api/routes/notification_templates.py",
"chars": 6548,
"preview": "\"\"\"API routes for notification template management.\"\"\"\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom sqlal"
},
{
"path": "backend/app/api/routes/notifications.py",
"chars": 18937,
"preview": "\"\"\"API routes for notification providers.\"\"\"\n\nimport json\nimport logging\nfrom datetime import datetime, timedelta, timez"
},
{
"path": "backend/app/api/routes/obico.py",
"chars": 2429,
"preview": "\"\"\"API routes for Obico AI failure detection.\"\"\"\n\nimport logging\n\nfrom fastapi import APIRouter, HTTPException, Response"
},
{
"path": "backend/app/api/routes/pending_uploads.py",
"chars": 7945,
"preview": "\"\"\"API routes for pending uploads (virtual printer queue mode).\"\"\"\n\nfrom datetime import datetime, timezone\nfrom pathlib"
},
{
"path": "backend/app/api/routes/print_log.py",
"chars": 4784,
"preview": "import logging\nfrom datetime import datetime\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfrom fastapi."
},
{
"path": "backend/app/api/routes/print_queue.py",
"chars": 42875,
"preview": "\"\"\"API routes for print queue management.\"\"\"\n\nimport json\nimport logging\nimport zipfile\nfrom datetime import datetime, t"
},
{
"path": "backend/app/api/routes/printers.py",
"chars": 113328,
"preview": "import asyncio\nimport logging\nimport re\nimport zipfile\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfro"
},
{
"path": "backend/app/api/routes/projects.py",
"chars": 61033,
"preview": "import io\nimport json\nimport logging\nimport os\nimport uuid\nimport zipfile\nfrom datetime import datetime\nfrom pathlib imp"
},
{
"path": "backend/app/api/routes/settings.py",
"chars": 41483,
"preview": "import io\nimport logging\nimport os\nimport zipfile\nfrom datetime import datetime\nfrom pathlib import Path\n\nfrom fastapi i"
},
{
"path": "backend/app/api/routes/smart_plugs.py",
"chars": 29638,
"preview": "\"\"\"API routes for smart plug management.\"\"\"\n\nimport logging\nfrom datetime import datetime, timedelta, timezone\n\nfrom fas"
},
{
"path": "backend/app/api/routes/spoolbuddy.py",
"chars": 37081,
"preview": "\"\"\"SpoolBuddy device management API routes.\"\"\"\n\nimport asyncio\nimport json\nimport logging\nimport time\nfrom datetime impo"
},
{
"path": "backend/app/api/routes/spoolman.py",
"chars": 30338,
"preview": "\"\"\"Spoolman integration API routes.\"\"\"\n\nimport json\nimport logging\n\nfrom fastapi import APIRouter, Depends, HTTPExceptio"
},
{
"path": "backend/app/api/routes/support.py",
"chars": 37417,
"preview": "\"\"\"Support endpoints for debug logging and support bundle generation.\"\"\"\n\nimport asyncio\nimport importlib.metadata\nimpor"
},
{
"path": "backend/app/api/routes/system.py",
"chars": 19032,
"preview": "\"\"\"System information API routes.\"\"\"\n\nimport asyncio\nimport os\nimport platform\nimport time\nfrom collections.abc import C"
},
{
"path": "backend/app/api/routes/updates.py",
"chars": 18237,
"preview": "\"\"\"Update checking and management routes.\"\"\"\n\nimport asyncio\nimport logging\nimport os\nimport re\nimport shutil\nimport sys"
},
{
"path": "backend/app/api/routes/user_notifications.py",
"chars": 3754,
"preview": "\"\"\"API routes for user email notification preferences.\"\"\"\n\nimport logging\n\nfrom fastapi import APIRouter, Depends, HTTPE"
},
{
"path": "backend/app/api/routes/users.py",
"chars": 19594,
"preview": "from datetime import datetime, timezone\nfrom typing import Annotated\n\nimport jwt as _jwt\nfrom fastapi import APIRouter, "
},
{
"path": "backend/app/api/routes/virtual_printers.py",
"chars": 16345,
"preview": "import logging\n\nfrom fastapi import APIRouter, Depends\nfrom fastapi.responses import JSONResponse\nfrom pydantic import B"
},
{
"path": "backend/app/api/routes/webhook.py",
"chars": 10550,
"preview": "import logging\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel\nfrom sqlalchemy imp"
},
{
"path": "backend/app/api/routes/websocket.py",
"chars": 2682,
"preview": "import logging\n\nfrom fastapi import APIRouter, WebSocket, WebSocketDisconnect\n\nfrom backend.app.core.websocket import ws"
},
{
"path": "backend/app/cli.py",
"chars": 4091,
"preview": "\"\"\"Bambuddy administrative CLI.\n\nInvoked via ``python -m backend.app.cli <subcommand>``.\n\nCurrently provides ``kiosk-boo"
},
{
"path": "backend/app/core/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "backend/app/core/auth.py",
"chars": 41799,
"preview": "from __future__ import annotations\n\nimport logging\nimport os\nimport secrets\nfrom datetime import datetime, timedelta, ti"
},
{
"path": "backend/app/core/catalog_defaults.py",
"chars": 40741,
"preview": "\"\"\"Default spool and color catalog entries.\"\"\"\n\n# (name, weight_in_grams)\nDEFAULT_SPOOL_CATALOG: list[tuple[str, int]] ="
},
{
"path": "backend/app/core/compat.py",
"chars": 259,
"preview": "\"\"\"Compatibility shims for older Python versions.\"\"\"\n\nimport sys\n\nif sys.version_info >= (3, 11):\n from enum import S"
},
{
"path": "backend/app/core/config.py",
"chars": 3319,
"preview": "import logging\nimport os\nfrom pathlib import Path\n\nfrom pydantic_settings import BaseSettings\n\n# Application version - s"
},
{
"path": "backend/app/core/database.py",
"chars": 84711,
"preview": "import asyncio\nimport logging\n\nfrom sqlalchemy import event\nfrom sqlalchemy.exc import IntegrityError, OperationalError,"
},
{
"path": "backend/app/core/db_dialect.py",
"chars": 1590,
"preview": "\"\"\"Database dialect helpers for SQLite/PostgreSQL dual support.\n\nBambuddy defaults to SQLite (zero-config). When DATABAS"
},
{
"path": "backend/app/core/encryption.py",
"chars": 3266,
"preview": "\"\"\"At-rest encryption for high-value secrets (TOTP keys, OIDC client_secret).\n\nSet the ``MFA_ENCRYPTION_KEY`` environmen"
},
{
"path": "backend/app/core/permissions.py",
"chars": 14997,
"preview": "\"\"\"Permission definitions for the group-based access control system.\n\nThis module defines all permissions using a string"
},
{
"path": "backend/app/core/websocket.py",
"chars": 3434,
"preview": "import asyncio\nimport json\nfrom typing import Any\n\nfrom fastapi import WebSocket\n\n\nclass ConnectionManager:\n \"\"\"Manag"
},
{
"path": "backend/app/i18n/__init__.py",
"chars": 3987,
"preview": "\"\"\"Internationalization module for backend notifications.\"\"\"\n\nfrom typing import Any\n\n# English translations\nEN = {\n "
},
{
"path": "backend/app/main.py",
"chars": 217464,
"preview": "import asyncio\nimport logging\nimport posixpath\nimport time\nfrom contextlib import asynccontextmanager\nfrom datetime impo"
},
{
"path": "backend/app/models/__init__.py",
"chars": 2931,
"preview": "from backend.app.models.ams_history import AMSSensorHistory\nfrom backend.app.models.ams_label import AmsLabel\nfrom backe"
},
{
"path": "backend/app/models/active_print_spoolman.py",
"chars": 1891,
"preview": "\"\"\"Track Spoolman data for active prints.\"\"\"\n\nfrom sqlalchemy import JSON, ForeignKey, UniqueConstraint\nfrom sqlalchemy."
},
{
"path": "backend/app/models/ams_history.py",
"chars": 1239,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, func\nfrom sqlalchemy."
},
{
"path": "backend/app/models/ams_label.py",
"chars": 1396,
"preview": "\"\"\"Model for storing user-defined friendly names for AMS units.\n\nUsers can assign a custom label to each AMS (e.g. \"Work"
},
{
"path": "backend/app/models/api_key.py",
"chars": 1336,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import JSON, Boolean, DateTime, String, func\nfrom sqlalchemy.orm import M"
},
{
"path": "backend/app/models/archive.py",
"chars": 4712,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import JSON, Boolean, DateTime, Float, ForeignKey, Integer, String, Text,"
},
{
"path": "backend/app/models/auth_ephemeral.py",
"chars": 6616,
"preview": "\"\"\"Ephemeral authentication tokens and rate-limit events.\n\nThese tables replace the module-level in-memory dicts in mfa."
},
{
"path": "backend/app/models/bug_report.py",
"chars": 914,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, Integer, String, Text, func\nfrom sqlalchemy.orm"
},
{
"path": "backend/app/models/color_catalog.py",
"chars": 779,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, String, func\nfrom sqlalchemy.orm import Mapped,"
},
{
"path": "backend/app/models/external_link.py",
"chars": 964,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, Integer, String, func\nfrom sqlalchemy.orm impor"
},
{
"path": "backend/app/models/filament.py",
"chars": 1385,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import DateTime, Float, String, func\nfrom sqlalchemy.orm import Mapped, m"
},
{
"path": "backend/app/models/github_backup.py",
"chars": 3202,
"preview": "\"\"\"GitHub backup configuration and log models.\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTi"
},
{
"path": "backend/app/models/group.py",
"chars": 1940,
"preview": "\"\"\"Group model for permission-based access control.\"\"\"\n\nfrom __future__ import annotations\n\nfrom datetime import datetim"
},
{
"path": "backend/app/models/kprofile_note.py",
"chars": 1274,
"preview": "\"\"\"Model for K-profile notes stored locally (not on printer).\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy import "
},
{
"path": "backend/app/models/library.py",
"chars": 4248,
"preview": "\"\"\"Library models for file manager functionality.\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy import JSON, Boolea"
},
{
"path": "backend/app/models/local_preset.py",
"chars": 1796,
"preview": "\"\"\"Model for locally stored slicer presets (imported from OrcaSlicer, etc.).\"\"\"\n\nfrom datetime import datetime\n\nfrom sql"
},
{
"path": "backend/app/models/maintenance.py",
"chars": 3651,
"preview": "\"\"\"Maintenance tracking models.\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, Float, Fore"
},
{
"path": "backend/app/models/notification.py",
"chars": 6196,
"preview": "\"\"\"Notification provider and log models for push notifications.\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy impor"
},
{
"path": "backend/app/models/notification_template.py",
"chars": 7894,
"preview": "\"\"\"Notification template model for customizable notification messages.\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchem"
},
{
"path": "backend/app/models/oidc_provider.py",
"chars": 4518,
"preview": "from __future__ import annotations\n\nfrom datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, ForeignKey,"
},
{
"path": "backend/app/models/orca_base_cache.py",
"chars": 828,
"preview": "\"\"\"Cache model for OrcaSlicer base profiles fetched from GitHub.\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy impo"
},
{
"path": "backend/app/models/pending_upload.py",
"chars": 1784,
"preview": "\"\"\"Pending upload model for virtual printer queue mode.\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy import DateTi"
},
{
"path": "backend/app/models/print_batch.py",
"chars": 1791,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import DateTime, ForeignKey, Integer, String, func\nfrom sqlalchemy.orm im"
},
{
"path": "backend/app/models/print_log.py",
"chars": 1421,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import DateTime, Float, Integer, String, func\nfrom sqlalchemy.orm import "
},
{
"path": "backend/app/models/print_queue.py",
"chars": 5568,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func\nfrom sq"
},
{
"path": "backend/app/models/printer.py",
"chars": 3863,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, Float, String, func\nfrom sqlalchemy.orm import "
},
{
"path": "backend/app/models/project.py",
"chars": 3199,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import JSON, Boolean, DateTime, Float, ForeignKey, Integer, String, Text,"
},
{
"path": "backend/app/models/project_bom.py",
"chars": 1947,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text, func\nfrom sqla"
},
{
"path": "backend/app/models/settings.py",
"chars": 650,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import DateTime, String, Text, func\nfrom sqlalchemy.orm import Mapped, ma"
},
{
"path": "backend/app/models/slot_preset.py",
"chars": 1531,
"preview": "\"\"\"Model for storing AMS slot to filament preset mappings.\n\nThis stores the user's preferred filament preset for each AM"
},
{
"path": "backend/app/models/smart_plug.py",
"chars": 7792,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String, Text, func\n"
},
{
"path": "backend/app/models/smart_plug_energy_snapshot.py",
"chars": 929,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import DateTime, Float, ForeignKey, Index, Integer\nfrom sqlalchemy.orm im"
},
{
"path": "backend/app/models/spool.py",
"chars": 3409,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, Float, Integer, String, func\nfrom sqlalchemy.or"
},
{
"path": "backend/app/models/spool_assignment.py",
"chars": 1506,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint, func\nfrom"
},
{
"path": "backend/app/models/spool_catalog.py",
"chars": 643,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, Integer, String, func\nfrom sqlalchemy.orm impor"
},
{
"path": "backend/app/models/spool_k_profile.py",
"chars": 1488,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import DateTime, Float, ForeignKey, Integer, String, func\nfrom sqlalchemy"
},
{
"path": "backend/app/models/spool_usage_history.py",
"chars": 1158,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import DateTime, Float, ForeignKey, Integer, String, func\nfrom sqlalchemy"
},
{
"path": "backend/app/models/spoolbuddy_device.py",
"chars": 2381,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, Float, Integer, String, Text, func\nfrom sqlalch"
},
{
"path": "backend/app/models/user.py",
"chars": 4761,
"preview": "from __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import TYPE_CHECKING\n\nfrom sqlalchemy impo"
},
{
"path": "backend/app/models/user_email_pref.py",
"chars": 1515,
"preview": "\"\"\"User email notification preference model.\"\"\"\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom "
},
{
"path": "backend/app/models/user_otp_code.py",
"chars": 2102,
"preview": "from __future__ import annotations\n\nfrom datetime import datetime, timezone\n\nfrom sqlalchemy import Boolean, DateTime, F"
},
{
"path": "backend/app/models/user_totp.py",
"chars": 3696,
"preview": "from __future__ import annotations\n\nimport json\nfrom datetime import datetime\n\nfrom fastapi import HTTPException, status"
},
{
"path": "backend/app/models/virtual_printer.py",
"chars": 1710,
"preview": "from datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, func\nfrom sqlalche"
},
{
"path": "backend/app/schemas/__init__.py",
"chars": 894,
"preview": "from backend.app.schemas.archive import (\n ArchiveBase,\n ArchiveResponse,\n ArchiveUpdate,\n ProjectPageImage,"
},
{
"path": "backend/app/schemas/api_key.py",
"chars": 1310,
"preview": "from datetime import datetime\n\nfrom pydantic import BaseModel\n\n\nclass APIKeyCreate(BaseModel):\n \"\"\"Schema for creatin"
},
{
"path": "backend/app/schemas/archive.py",
"chars": 7047,
"preview": "from datetime import datetime\n\nfrom pydantic import BaseModel, model_validator\n\n\nclass ArchiveBase(BaseModel):\n print"
},
{
"path": "backend/app/schemas/auth.py",
"chars": 13438,
"preview": "import re\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _validate_password_co"
},
{
"path": "backend/app/schemas/cloud.py",
"chars": 4479,
"preview": "from typing import Literal\n\nfrom pydantic import BaseModel, Field\n\nRegion = Literal[\"global\", \"china\"]\n\n\nclass CloudLogi"
},
{
"path": "backend/app/schemas/external_link.py",
"chars": 1940,
"preview": "from datetime import datetime\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\nclass ExternalLinkBase(BaseModel"
},
{
"path": "backend/app/schemas/filament.py",
"chars": 1432,
"preview": "from datetime import datetime\n\nfrom pydantic import BaseModel, Field\n\n\nclass FilamentBase(BaseModel):\n name: str = Fi"
},
{
"path": "backend/app/schemas/github_backup.py",
"chars": 5122,
"preview": "\"\"\"Pydantic schemas for GitHub backup configuration.\"\"\"\n\nimport re\nfrom datetime import datetime\n\nfrom pydantic import B"
},
{
"path": "backend/app/schemas/group.py",
"chars": 1735,
"preview": "\"\"\"Pydantic schemas for Group CRUD operations.\"\"\"\n\nfrom datetime import datetime\n\nfrom pydantic import BaseModel\n\n\nclass"
},
{
"path": "backend/app/schemas/kprofile.py",
"chars": 2085,
"preview": "\"\"\"Pydantic schemas for K-profile (pressure advance) management.\"\"\"\n\nfrom pydantic import BaseModel\n\n\nclass KProfile(Bas"
},
{
"path": "backend/app/schemas/library.py",
"chars": 8100,
"preview": "\"\"\"Pydantic schemas for library (File Manager) functionality.\"\"\"\n\nfrom datetime import datetime\n\nfrom pydantic import Ba"
},
{
"path": "backend/app/schemas/local_preset.py",
"chars": 1620,
"preview": "\"\"\"Pydantic schemas for local preset API.\"\"\"\n\nfrom datetime import datetime\n\nfrom pydantic import BaseModel\n\n\nclass Loca"
},
{
"path": "backend/app/schemas/maintenance.py",
"chars": 3972,
"preview": "\"\"\"Maintenance tracking schemas.\"\"\"\n\nfrom datetime import datetime\n\nfrom pydantic import BaseModel, Field\n\n\n# Maintenanc"
},
{
"path": "backend/app/schemas/notification.py",
"chars": 10454,
"preview": "\"\"\"Pydantic schemas for notification providers.\"\"\"\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic "
},
{
"path": "backend/app/schemas/notification_template.py",
"chars": 11605,
"preview": "\"\"\"Pydantic schemas for notification templates.\"\"\"\n\nfrom datetime import datetime\n\nfrom pydantic import BaseModel, Field"
},
{
"path": "backend/app/schemas/print_log.py",
"chars": 669,
"preview": "from datetime import datetime\n\nfrom pydantic import BaseModel\n\n\nclass PrintLogEntrySchema(BaseModel):\n id: int\n pr"
},
{
"path": "backend/app/schemas/print_queue.py",
"chars": 7342,
"preview": "from datetime import datetime\nfrom typing import Annotated, Literal\n\nfrom pydantic import BaseModel, PlainSerializer\n\n\n#"
},
{
"path": "backend/app/schemas/printer.py",
"chars": 12106,
"preview": "from datetime import datetime\n\nfrom pydantic import BaseModel, Field\n\n\nclass PrinterBase(BaseModel):\n name: str = Fie"
},
{
"path": "backend/app/schemas/project.py",
"chars": 7270,
"preview": "from datetime import datetime\n\nfrom pydantic import BaseModel\n\n\nclass ProjectCreate(BaseModel):\n \"\"\"Schema for creati"
},
{
"path": "backend/app/schemas/settings.py",
"chars": 21837,
"preview": "import json\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\nclass AppSettings(BaseModel):\n \"\"\"Application s"
},
{
"path": "backend/app/schemas/smart_plug.py",
"chars": 11237,
"preview": "from datetime import datetime\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field, model_validator\n\n\nclass"
},
{
"path": "backend/app/schemas/spool.py",
"chars": 3706,
"preview": "from datetime import datetime\n\nfrom pydantic import BaseModel, Field\n\n\nclass SpoolBase(BaseModel):\n material: str = F"
},
{
"path": "backend/app/schemas/spool_usage.py",
"chars": 372,
"preview": "from datetime import datetime\n\nfrom pydantic import BaseModel\n\n\nclass SpoolUsageHistoryResponse(BaseModel):\n id: int\n"
},
{
"path": "backend/app/schemas/spoolbuddy.py",
"chars": 4002,
"preview": "from datetime import datetime\n\nfrom pydantic import BaseModel, Field\n\n# --- Device schemas ---\n\n\nclass DeviceRegisterReq"
},
{
"path": "backend/app/schemas/timelapse.py",
"chars": 1147,
"preview": "\"\"\"Schemas for timelapse video processing.\"\"\"\n\nfrom pydantic import BaseModel, Field\n\n\nclass TimelapseInfoResponse(BaseM"
},
{
"path": "backend/app/schemas/user_notifications.py",
"chars": 609,
"preview": "\"\"\"Schemas for user email notification preferences.\"\"\"\n\nfrom pydantic import BaseModel\n\n\nclass UserEmailPreferenceRespon"
},
{
"path": "backend/app/services/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "backend/app/services/archive.py",
"chars": 57007,
"preview": "import hashlib\nimport json\nimport logging\nimport os\nimport re\nimport shutil\nimport zipfile\nfrom datetime import date, da"
},
{
"path": "backend/app/services/archive_comparison.py",
"chars": 11033,
"preview": "from sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom sqlalchemy.orm import selectinload\n\nf"
},
{
"path": "backend/app/services/background_dispatch.py",
"chars": 39613,
"preview": "\"\"\"Background dispatch for print/reprint jobs.\n\nThis service is separate from the app's print queue feature. It exists o"
},
{
"path": "backend/app/services/bambu_cloud.py",
"chars": 21308,
"preview": "\"\"\"\nBambu Lab Cloud API Service\n\nHandles authentication and profile management with Bambu Lab's cloud services.\n\"\"\"\n\nimp"
},
{
"path": "backend/app/services/bambu_ftp.py",
"chars": 44208,
"preview": "import asyncio\nimport ftplib # nosec B402\nimport logging\nimport os\nimport socket\nimport ssl\nimport threading\nimport tim"
},
{
"path": "backend/app/services/bambu_mqtt.py",
"chars": 229100,
"preview": "\"\"\"Bambu Lab MQTT communication service.\n\nIMPORTANT: Always use qos=1 for all MQTT publish calls!\nThe printer ignores qo"
},
{
"path": "backend/app/services/bug_report.py",
"chars": 4746,
"preview": "\"\"\"Bug report service — posts to the bambuddy.cool relay which holds the GitHub PAT.\"\"\"\n\nimport logging\nimport time\n\nimp"
},
{
"path": "backend/app/services/camera.py",
"chars": 21299,
"preview": "\"\"\"Camera capture service for Bambu Lab printers.\n\nSupports two camera protocols:\n- RTSP: Used by X1, X1C, X1E, X2D, H2C"
},
{
"path": "backend/app/services/discovery.py",
"chars": 25754,
"preview": "\"\"\"\nBambu Lab printer discovery service using SSDP and subnet scanning.\n\nBambu Lab printers advertise themselves via SSD"
},
{
"path": "backend/app/services/email_service.py",
"chars": 25834,
"preview": "\"\"\"Email service for sending authentication-related emails.\"\"\"\n\nfrom __future__ import annotations\n\nimport html\nimport l"
},
{
"path": "backend/app/services/export.py",
"chars": 11712,
"preview": "import csv\nimport io\nfrom datetime import datetime\nfrom typing import Any\n\nfrom sqlalchemy import select\nfrom sqlalchemy"
},
{
"path": "backend/app/services/external_camera.py",
"chars": 29881,
"preview": "\"\"\"External camera service.\n\nSupports MJPEG streams, RTSP streams (via ffmpeg), HTTP snapshot URLs, and USB cameras.\n\nSe"
},
{
"path": "backend/app/services/failure_analysis.py",
"chars": 8604,
"preview": "from collections import defaultdict\nfrom datetime import date, datetime, time, timedelta, timezone\n\nfrom sqlalchemy impo"
},
{
"path": "backend/app/services/firmware_check.py",
"chars": 21474,
"preview": "\"\"\"\nFirmware Check Service\n\nChecks for firmware updates by fetching from Bambu Lab's official wiki and firmware\ndownload"
},
{
"path": "backend/app/services/firmware_update.py",
"chars": 14305,
"preview": "\"\"\"\nFirmware Update Service\n\nOrchestrates firmware updates for Bambu Lab printers:\n1. Check prerequisites (SD card, spac"
},
{
"path": "backend/app/services/github_backup.py",
"chars": 35884,
"preview": "\"\"\"GitHub backup service for printer profiles.\n\nHandles scheduled and on-demand backups of K-profiles and cloud profiles"
},
{
"path": "backend/app/services/hms_errors.py",
"chars": 104871,
"preview": "\"\"\"HMS Error Code Descriptions.\n\nAuto-generated from frontend/src/components/HMSErrorModal.tsx\nSource: https://github.co"
},
{
"path": "backend/app/services/homeassistant.py",
"chars": 13850,
"preview": "\"\"\"Service for communicating with Home Assistant via REST API.\"\"\"\n\nimport logging\nfrom typing import TYPE_CHECKING\nfrom "
},
{
"path": "backend/app/services/layer_timelapse.py",
"chars": 8968,
"preview": "\"\"\"Layer-based timelapse for external cameras.\n\nCaptures a frame on each layer change and stitches them into a video on "
},
{
"path": "backend/app/services/ldap_service.py",
"chars": 10237,
"preview": "\"\"\"LDAP authentication service for BamBuddy (#794).\n\nSupports:\n- LDAP bind authentication (simple bind with user's crede"
},
{
"path": "backend/app/services/local_backup.py",
"chars": 10239,
"preview": "\"\"\"Scheduled local backup service.\n\nCreates ZIP snapshots of the full Bambuddy data (database + data directories)\non a c"
},
{
"path": "backend/app/services/mqtt_relay.py",
"chars": 23592,
"preview": "\"\"\"MQTT Relay Service for publishing BamBuddy events to external MQTT brokers.\n\nThis service enables integration with ex"
},
{
"path": "backend/app/services/mqtt_smart_plug.py",
"chars": 21086,
"preview": "\"\"\"MQTT Smart Plug Service for subscribing to external MQTT topics and extracting power/energy data.\n\nThis service enabl"
},
{
"path": "backend/app/services/network_utils.py",
"chars": 6560,
"preview": "\"\"\"Network utility functions for interface detection.\"\"\"\n\nimport ipaddress\nimport json\nimport logging\nimport shutil\nimpo"
},
{
"path": "backend/app/services/notification_service.py",
"chars": 69865,
"preview": "\"\"\"Notification service for sending push notifications via various providers.\"\"\"\n\nimport asyncio\nimport json\nimport logg"
},
{
"path": "backend/app/services/obico_actions.py",
"chars": 3005,
"preview": "\"\"\"Action dispatch for Obico failure detection.\n\nSeparated from the detection loop so actions can be unit-tested and swa"
},
{
"path": "backend/app/services/obico_detection.py",
"chars": 13243,
"preview": "\"\"\"Obico AI print-failure detection service.\n\nPolls a self-hosted Obico ML API with snapshots from each monitored printe"
},
{
"path": "backend/app/services/obico_smoothing.py",
"chars": 3399,
"preview": "\"\"\"Temporal smoothing for Obico ML detection scores.\n\nPorts Obico's failure-detection math:\n- per-frame `current_p` = su"
},
{
"path": "backend/app/services/opentag3d.py",
"chars": 3423,
"preview": "\"\"\"OpenTag3D NDEF encoder for NTAG tags.\n\nEncodes spool data as an OpenTag3D NDEF message ready to write to NTAG\nstartin"
},
{
"path": "backend/app/services/orca_profiles.py",
"chars": 17052,
"preview": "\"\"\"Service for importing and resolving OrcaSlicer profiles.\n\nHandles:\n- Parsing .json, .orca_filament, .zip exports\n- Fe"
},
{
"path": "backend/app/services/plate_detection.py",
"chars": 31594,
"preview": "\"\"\"Build plate empty detection using OpenCV.\n\nAnalyzes camera frames to detect if there are objects on the build plate.\n"
},
{
"path": "backend/app/services/print_log.py",
"chars": 1506,
"preview": "\"\"\"Service for writing independent print log entries.\n\nLog entries are written to a separate table and never touch archi"
},
{
"path": "backend/app/services/print_scheduler.py",
"chars": 98465,
"preview": "\"\"\"Print scheduler service - processes the print queue.\"\"\"\n\nimport asyncio\nimport json\nimport logging\nimport time\nimport"
},
{
"path": "backend/app/services/printer_manager.py",
"chars": 36597,
"preview": "import asyncio\nimport logging\nimport re\nimport traceback\nfrom collections.abc import Callable\n\nfrom sqlalchemy import se"
},
{
"path": "backend/app/services/rest_smart_plug.py",
"chars": 10836,
"preview": "\"\"\"Service for controlling smart plugs via generic REST/HTTP API.\"\"\"\n\nimport ipaddress\nimport json\nimport logging\nfrom t"
},
{
"path": "backend/app/services/smart_plug_manager.py",
"chars": 26670,
"preview": "\"\"\"Manager for smart plug automation and delayed turn-off.\"\"\"\n\nimport asyncio\nimport logging\nfrom datetime import dateti"
},
{
"path": "backend/app/services/spool_assignment_notifications.py",
"chars": 6223,
"preview": "import logging\n\nfrom backend.app.core.database import async_session\nfrom backend.app.core.websocket import ws_manager\nfr"
},
{
"path": "backend/app/services/spool_tag_matcher.py",
"chars": 20922,
"preview": "\"\"\"RFID tag matching and auto-assignment for spool inventory.\"\"\"\n\nimport logging\n\nfrom sqlalchemy import func, or_, sele"
}
]
// ... and 518 more files (download for full content)
About this extraction
This page contains the full source code of the maziggy/bambuddy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 718 files (17.9 MB), approximately 4.7M tokens, and a symbol index with 13751 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.