const API_BASE = '/api/v1'; async function request( endpoint: string, options: RequestInit = {} ): Promise { const response = await fetch(`${API_BASE}${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, }); if (!response.ok) { const error = await response.json().catch(() => ({})); const detail = error.detail; const message = typeof detail === 'string' ? detail : (detail ? JSON.stringify(detail) : `HTTP ${response.status}`); throw new Error(message); } return await response.json(); } // Printer types export interface Printer { id: number; name: string; serial_number: string; ip_address: string; access_code: string; model: string | null; location: string | null; // Group/location name nozzle_count: number; // 1 or 2, auto-detected from MQTT is_active: boolean; auto_archive: boolean; created_at: string; updated_at: string; } export interface HMSError { code: string; attr: number; // Attribute value for constructing wiki URL module: number; severity: number; // 1=fatal, 2=serious, 3=common, 4=info } export interface AMSTray { id: number; tray_color: string | null; tray_type: string | null; tray_sub_brands: string | null; // Full name like "PLA Basic", "PETG HF" tray_id_name: string | null; // Bambu filament ID like "A00-Y2" (can decode to color) tray_info_idx: string | null; // Filament preset ID like "GFA00" - maps to cloud setting_id remain: number; k: number | null; // Pressure advance value (from tray or K-profile lookup) cali_idx: number | null; // Calibration index for K-profile lookup tag_uid: string | null; // RFID tag UID (any tag) tray_uuid: string | null; // Bambu Lab spool UUID (32-char hex, only valid for Bambu Lab spools) nozzle_temp_min: number | null; // Min nozzle temperature nozzle_temp_max: number | null; // Max nozzle temperature } export interface AMSUnit { id: number; humidity: number | null; temp: number | null; is_ams_ht: boolean; // True for AMS-HT (single spool), False for regular AMS (4 spools) tray: AMSTray[]; } export interface NozzleInfo { nozzle_type: string; // "stainless_steel" or "hardened_steel" nozzle_diameter: string; // e.g., "0.4" } export interface PrintOptions { // Core AI detectors spaghetti_detector: boolean; print_halt: boolean; halt_print_sensitivity: string; // "low", "medium", "high" - spaghetti sensitivity first_layer_inspector: boolean; printing_monitor: boolean; buildplate_marker_detector: boolean; allow_skip_parts: boolean; // Additional AI detectors (decoded from cfg bitmask) nozzle_clumping_detector: boolean; nozzle_clumping_sensitivity: string; // "low", "medium", "high" pileup_detector: boolean; pileup_sensitivity: string; // "low", "medium", "high" airprint_detector: boolean; airprint_sensitivity: string; // "low", "medium", "high" auto_recovery_step_loss: boolean; filament_tangle_detect: boolean; } export interface PrinterStatus { id: number; name: string; connected: boolean; state: string | null; current_print: string | null; subtask_name: string | null; gcode_file: string | null; progress: number | null; remaining_time: number | null; layer_num: number | null; total_layers: number | null; temperatures: { bed?: number; bed_target?: number; bed_heating?: boolean; // Actual heater state from MQTT nozzle?: number; nozzle_target?: number; nozzle_heating?: boolean; // Actual heater state from MQTT nozzle_2?: number; // Second nozzle for H2 series (dual nozzle) nozzle_2_target?: number; nozzle_2_heating?: boolean; // Actual heater state from MQTT chamber?: number; chamber_target?: number; chamber_heating?: boolean; // Actual heater state from MQTT } | null; cover_url: string | null; hms_errors: HMSError[]; ams: AMSUnit[]; ams_exists: boolean; vt_tray: AMSTray | null; // Virtual tray / external spool sdcard: boolean; // SD card inserted store_to_sdcard: boolean; // Store sent files on SD card timelapse: boolean; // Timelapse recording active ipcam: boolean; // Live view enabled wifi_signal: number | null; // WiFi signal strength in dBm nozzles: NozzleInfo[]; // Nozzle hardware info (index 0=left/primary, 1=right) print_options: PrintOptions | null; // AI detection and print options // Calibration stage tracking stg_cur: number; // Current stage number (-1 = not calibrating) stg_cur_name: string | null; // Human-readable current stage name stg: number[]; // List of stage numbers in calibration sequence // Air conditioning mode (0=cooling, 1=heating) airduct_mode: number; // Print speed level (1=silent, 2=standard, 3=sport, 4=ludicrous) speed_level: number; // Chamber light on/off chamber_light: boolean; // Active extruder for dual nozzle (0=right, 1=left) active_extruder: number; // AMS mapping - which AMS is connected to which nozzle // Format: [ams_id_for_nozzle0, ams_id_for_nozzle1, ...] where -1 means no AMS ams_mapping: number[]; // Per-AMS extruder mapping - extracted from each AMS unit's info field // Format: {ams_id: extruder_id} where extruder 0=right, 1=left // Note: JSON keys are always strings ams_extruder_map: Record; // Currently loaded tray (global tray ID, 255 = no filament loaded, 254 = external spool) tray_now: number; // AMS status for filament change tracking (0=idle, 1=filament_change, 2=rfid_identifying, 3=assist, 4=calibration) ams_status_main: number; // AMS sub-status for filament change step (when main=1): 4=retraction, 6=load verification, 7=purge ams_status_sub: number; // mc_print_sub_stage - filament change step indicator used by OrcaSlicer/BambuStudio mc_print_sub_stage: number; // Timestamp of last AMS data update (for RFID refresh detection) last_ams_update: number; // Number of printable objects in current print (for skip objects feature) printable_objects_count: number; } export interface PrinterCreate { name: string; serial_number: string; ip_address: string; access_code: string; model?: string; location?: string; auto_archive?: boolean; } // Archive types export interface ArchiveDuplicate { id: number; print_name: string | null; created_at: string; match_type: 'exact' | 'similar'; // 'exact' = hash match, 'similar' = name match } export interface Archive { id: number; printer_id: number | null; project_id: number | null; project_name: string | null; filename: string; file_path: string; file_size: number; content_hash: string | null; thumbnail_path: string | null; timelapse_path: string | null; source_3mf_path: string | null; duplicates: ArchiveDuplicate[] | null; duplicate_count: number; print_name: string | null; print_time_seconds: number | null; actual_time_seconds: number | null; // Computed from started_at/completed_at time_accuracy: number | null; // Percentage: 100 = perfect, >100 = faster than estimated filament_used_grams: number | null; filament_type: string | null; filament_color: string | null; layer_height: number | null; total_layers: number | null; nozzle_diameter: number | null; bed_temperature: number | null; nozzle_temperature: number | null; status: string; started_at: string | null; completed_at: string | null; extra_data: Record | null; makerworld_url: string | null; designer: string | null; is_favorite: boolean; tags: string | null; notes: string | null; cost: number | null; photos: string[] | null; failure_reason: string | null; energy_kwh: number | null; energy_cost: number | null; created_at: string; } export interface ArchiveStats { total_prints: number; successful_prints: number; failed_prints: number; total_print_time_hours: number; total_filament_grams: number; total_cost: number; prints_by_filament_type: Record; prints_by_printer: Record; average_time_accuracy: number | null; time_accuracy_by_printer: Record | null; total_energy_kwh: number; total_energy_cost: number; } export interface FailureAnalysis { period_days: number; total_prints: number; failed_prints: number; failure_rate: number; failures_by_reason: Record; failures_by_filament: Record; failures_by_printer: Record; failures_by_hour: Record; recent_failures: Array<{ id: number; print_name: string; failure_reason: string | null; filament_type: string | null; printer_id: number | null; created_at: string | null; }>; trend: Array<{ week_start: string; total_prints: number; failed_prints: number; failure_rate: number; }>; } export interface BulkUploadResult { uploaded: number; failed: number; results: Array<{ filename: string; id: number; status: string }>; errors: Array<{ filename: string; error: string }>; } // Archive Comparison types export interface ComparisonArchiveInfo { id: number; print_name: string; status: string; created_at: string | null; printer_id: number | null; project_name: string | null; } export interface ComparisonField { field: string; label: string; unit: string | null; values: (string | number | null)[]; raw_values: (string | number | null)[]; has_difference: boolean; } export interface SuccessCorrelationInsight { field: string; label: string; insight: string; success_avg?: number; failed_avg?: number; success_values?: string[]; failed_values?: string[]; } export interface SuccessCorrelation { has_both_outcomes: boolean; message?: string; successful_count?: number; failed_count?: number; insights?: SuccessCorrelationInsight[]; } export interface ArchiveComparison { archives: ComparisonArchiveInfo[]; comparison: ComparisonField[]; differences: ComparisonField[]; success_correlation: SuccessCorrelation; } export interface SimilarArchive { archive: { id: number; print_name: string; status: string; created_at: string | null; }; match_reason: string; match_score: number; } // Project types export interface ProjectStats { total_archives: number; completed_prints: number; failed_prints: number; queued_prints: number; in_progress_prints: number; total_print_time_hours: number; total_filament_grams: number; progress_percent: number | null; estimated_cost: number; total_energy_kwh: number; total_energy_cost: number; remaining_prints: number | null; bom_total_items: number; bom_completed_items: number; } export interface ProjectChildPreview { id: number; name: string; color: string | null; status: string; progress_percent: number | null; } export interface Project { id: number; name: string; description: string | null; color: string | null; status: string; // active, completed, archived target_count: number | null; notes: string | null; attachments: ProjectAttachment[] | null; tags: string | null; due_date: string | null; priority: string; // low, normal, high, urgent budget: number | null; is_template: boolean; template_source_id: number | null; parent_id: number | null; parent_name: string | null; children: ProjectChildPreview[]; created_at: string; updated_at: string; stats?: ProjectStats; } export interface ProjectAttachment { filename: string; original_name: string; size: number; uploaded_at: string; } export interface ArchivePreview { id: number; print_name: string | null; thumbnail_path: string | null; status: string; filament_type: string | null; filament_color: string | null; } export interface ProjectListItem { id: number; name: string; description: string | null; color: string | null; status: string; target_count: number | null; created_at: string; archive_count: number; queue_count: number; progress_percent: number | null; archives: ArchivePreview[]; } export interface ProjectCreate { name: string; description?: string; color?: string; target_count?: number; notes?: string; tags?: string; due_date?: string; priority?: string; budget?: number; parent_id?: number; } export interface ProjectUpdate { name?: string; description?: string; color?: string; status?: string; target_count?: number; notes?: string; tags?: string; due_date?: string; priority?: string; budget?: number; parent_id?: number; } // BOM Types - Tracks sourced/purchased parts (hardware, electronics, etc.) export interface BOMItem { id: number; project_id: number; name: string; quantity_needed: number; quantity_acquired: number; unit_price: number | null; sourcing_url: string | null; archive_id: number | null; archive_name: string | null; stl_filename: string | null; remarks: string | null; sort_order: number; is_complete: boolean; created_at: string; updated_at: string; } export interface BOMItemCreate { name: string; quantity_needed?: number; unit_price?: number; sourcing_url?: string; archive_id?: number; stl_filename?: string; remarks?: string; } export interface BOMItemUpdate { name?: string; quantity_needed?: number; quantity_acquired?: number; unit_price?: number; sourcing_url?: string; archive_id?: number; stl_filename?: string; remarks?: string; } // Timeline Types export interface TimelineEvent { event_type: string; timestamp: string; title: string; description: string | null; metadata: Record | null; } // API Key types export interface APIKey { id: number; name: string; key_prefix: string; can_queue: boolean; can_control_printer: boolean; can_read_status: boolean; printer_ids: number[] | null; enabled: boolean; last_used: string | null; created_at: string; expires_at: string | null; } export interface APIKeyCreate { name: string; can_queue?: boolean; can_control_printer?: boolean; can_read_status?: boolean; printer_ids?: number[] | null; expires_at?: string | null; } export interface APIKeyCreateResponse extends APIKey { key: string; // Full key, only shown on creation } export interface APIKeyUpdate { name?: string; can_queue?: boolean; can_control_printer?: boolean; can_read_status?: boolean; printer_ids?: number[] | null; enabled?: boolean; expires_at?: string | null; } // Settings types export interface AppSettings { auto_archive: boolean; save_thumbnails: boolean; capture_finish_photo: boolean; default_filament_cost: number; currency: string; energy_cost_per_kwh: number; energy_tracking_mode: 'print' | 'total'; check_updates: boolean; notification_language: string; // AMS threshold settings ams_humidity_good: number; // <= this is green ams_humidity_fair: number; // <= this is orange, > is red ams_temp_good: number; // <= this is green/blue ams_temp_fair: number; // <= this is orange, > is red ams_history_retention_days: number; // days to keep AMS sensor history // Date/time format settings date_format: 'system' | 'us' | 'eu' | 'iso'; time_format: 'system' | '12h' | '24h'; // Default printer default_printer_id: number | null; // Telemetry telemetry_enabled: boolean; // Dark mode theme settings dark_style: 'classic' | 'glow' | 'vibrant'; dark_background: 'neutral' | 'warm' | 'cool' | 'oled' | 'slate' | 'forest'; dark_accent: 'green' | 'teal' | 'blue' | 'orange' | 'purple' | 'red'; // Light mode theme settings light_style: 'classic' | 'glow' | 'vibrant'; light_background: 'neutral' | 'warm' | 'cool'; light_accent: 'green' | 'teal' | 'blue' | 'orange' | 'purple' | 'red'; } export type AppSettingsUpdate = Partial; // Cloud types export interface CloudAuthStatus { is_authenticated: boolean; email: string | null; } export interface CloudLoginResponse { success: boolean; needs_verification: boolean; message: string; } export interface SlicerSetting { setting_id: string; name: string; type: string; version: string | null; user_id: string | null; updated_time: string | null; } export interface SlicerSettingsResponse { filament: SlicerSetting[]; printer: SlicerSetting[]; process: SlicerSetting[]; } export interface SlicerSettingDetail { message?: string | null; code?: string | null; error?: string | null; public: boolean; version?: string | null; type: string; name: string; update_time?: string | null; nickname?: string | null; base_id?: string | null; setting: Record; filament_id?: string | null; setting_id?: string | null; } export interface SlicerSettingCreate { type: string; // 'filament', 'print', or 'printer' name: string; base_id: string; setting: Record; } export interface SlicerSettingUpdate { name?: string; setting?: Record; } export interface SlicerSettingDeleteResponse { success: boolean; message: string; } export interface FieldOption { value: string; label: string; } export interface FieldDefinition { key: string; label: string; type: 'text' | 'number' | 'boolean' | 'select'; category: string; description?: string; options?: FieldOption[]; unit?: string; min?: number; max?: number; step?: number; } export interface FieldDefinitionsResponse { version: string; description: string; fields: FieldDefinition[]; } export interface CloudDevice { dev_id: string; name: string; dev_model_name: string | null; dev_product_name: string | null; online: boolean; } // Smart Plug types export interface SmartPlug { id: number; name: string; ip_address: string; printer_id: number | null; enabled: boolean; auto_on: boolean; auto_off: boolean; off_delay_mode: 'time' | 'temperature'; off_delay_minutes: number; off_temp_threshold: number; username: string | null; password: string | null; // Power alerts power_alert_enabled: boolean; power_alert_high: number | null; power_alert_low: number | null; power_alert_last_triggered: string | null; // Schedule schedule_enabled: boolean; schedule_on_time: string | null; schedule_off_time: string | null; // Switchbar visibility show_in_switchbar: boolean; // Status last_state: string | null; last_checked: string | null; auto_off_executed: boolean; // True when auto-off was triggered after print created_at: string; updated_at: string; } export interface SmartPlugCreate { name: string; ip_address: string; printer_id?: number | null; enabled?: boolean; auto_on?: boolean; auto_off?: boolean; off_delay_mode?: 'time' | 'temperature'; off_delay_minutes?: number; off_temp_threshold?: number; username?: string | null; password?: string | null; // Power alerts power_alert_enabled?: boolean; power_alert_high?: number | null; power_alert_low?: number | null; // Schedule schedule_enabled?: boolean; schedule_on_time?: string | null; schedule_off_time?: string | null; // Switchbar visibility show_in_switchbar?: boolean; } export interface SmartPlugUpdate { name?: string; ip_address?: string; printer_id?: number | null; enabled?: boolean; auto_on?: boolean; auto_off?: boolean; off_delay_mode?: 'time' | 'temperature'; off_delay_minutes?: number; off_temp_threshold?: number; username?: string | null; password?: string | null; // Power alerts power_alert_enabled?: boolean; power_alert_high?: number | null; power_alert_low?: number | null; // Schedule schedule_enabled?: boolean; schedule_on_time?: string | null; schedule_off_time?: string | null; // Switchbar visibility show_in_switchbar?: boolean; } export interface SmartPlugEnergy { power: number | null; // Current watts voltage: number | null; // Volts current: number | null; // Amps today: number | null; // kWh used today yesterday: number | null; // kWh used yesterday total: number | null; // Total kWh factor: number | null; // Power factor (0-1) apparent_power: number | null; // VA reactive_power: number | null; // VAr } export interface SmartPlugStatus { state: string | null; reachable: boolean; device_name: string | null; energy: SmartPlugEnergy | null; } export interface SmartPlugTestResult { success: boolean; state: string | null; device_name: string | null; } // Tasmota Discovery types export interface TasmotaScanStatus { running: boolean; scanned: number; total: number; } export interface DiscoveredTasmotaDevice { ip_address: string; name: string; module: number | null; state: string | null; discovered_at: string | null; } // Print Queue types export interface PrintQueueItem { id: number; printer_id: number; archive_id: number; position: number; scheduled_time: string | null; require_previous_success: boolean; auto_off_after: boolean; status: 'pending' | 'printing' | 'completed' | 'failed' | 'skipped' | 'cancelled'; started_at: string | null; completed_at: string | null; error_message: string | null; created_at: string; archive_name?: string | null; archive_thumbnail?: string | null; printer_name?: string | null; print_time_seconds?: number | null; // Estimated print time from archive } export interface PrintQueueItemCreate { printer_id: number; archive_id: number; scheduled_time?: string | null; require_previous_success?: boolean; auto_off_after?: boolean; } export interface PrintQueueItemUpdate { printer_id?: number; position?: number; scheduled_time?: string | null; require_previous_success?: boolean; auto_off_after?: boolean; } // MQTT Logging types export interface MQTTLogEntry { timestamp: string; topic: string; direction: 'in' | 'out'; payload: Record; } export interface MQTTLogsResponse { logging_enabled: boolean; logs: MQTTLogEntry[]; } // K-Profile types export interface KProfile { slot_id: number; extruder_id: number; nozzle_id: string; nozzle_diameter: string; filament_id: string; name: string; k_value: string; n_coef: string; ams_id: number; tray_id: number; setting_id: string | null; } export interface KProfileCreate { slot_id?: number; // Storage slot, 0 for new profiles extruder_id?: number; nozzle_id: string; nozzle_diameter: string; filament_id: string; name: string; k_value: string; n_coef?: string; ams_id?: number; tray_id?: number; setting_id?: string | null; } export interface KProfileDelete { slot_id: number; // cali_idx - calibration index to delete extruder_id: number; nozzle_id: string; // e.g., "HH00-0.4" nozzle_diameter: string; // e.g., "0.4" filament_id: string; // Bambu filament identifier setting_id?: string | null; // Setting ID (for X1C series) } export interface KProfilesResponse { profiles: KProfile[]; nozzle_diameter: string; } export interface KProfileNote { setting_id: string; note: string; } export interface KProfileNotesResponse { notes: Record; // setting_id -> note } // Slot Preset Mapping export interface SlotPresetMapping { ams_id: number; tray_id: number; preset_id: string; preset_name: string; } // Filament types export interface Filament { id: number; name: string; type: string; // PLA, PETG, ABS, etc. brand: string | null; color: string | null; color_hex: string | null; cost_per_kg: number; spool_weight_g: number; currency: string; density: number | null; print_temp_min: number | null; print_temp_max: number | null; bed_temp_min: number | null; bed_temp_max: number | null; created_at: string; updated_at: string; } // Notification Provider types export type ProviderType = 'callmebot' | 'ntfy' | 'pushover' | 'telegram' | 'email' | 'discord' | 'webhook'; export interface NotificationProvider { id: number; name: string; provider_type: ProviderType; enabled: boolean; config: Record; // Print lifecycle events on_print_start: boolean; on_print_complete: boolean; on_print_failed: boolean; on_print_stopped: boolean; on_print_progress: boolean; // Printer status events on_printer_offline: boolean; on_printer_error: boolean; on_filament_low: boolean; on_maintenance_due: boolean; // AMS environmental alarms (regular AMS) on_ams_humidity_high: boolean; on_ams_temperature_high: boolean; // AMS-HT environmental alarms on_ams_ht_humidity_high: boolean; on_ams_ht_temperature_high: boolean; // Quiet hours quiet_hours_enabled: boolean; quiet_hours_start: string | null; quiet_hours_end: string | null; // Daily digest daily_digest_enabled: boolean; daily_digest_time: string | null; // Printer filter printer_id: number | null; // Status tracking last_success: string | null; last_error: string | null; last_error_at: string | null; // Timestamps created_at: string; updated_at: string; } export interface NotificationProviderCreate { name: string; provider_type: ProviderType; enabled?: boolean; config: Record; // Print lifecycle events on_print_start?: boolean; on_print_complete?: boolean; on_print_failed?: boolean; on_print_stopped?: boolean; on_print_progress?: boolean; // Printer status events on_printer_offline?: boolean; on_printer_error?: boolean; on_filament_low?: boolean; on_maintenance_due?: boolean; // AMS environmental alarms (regular AMS) on_ams_humidity_high?: boolean; on_ams_temperature_high?: boolean; // AMS-HT environmental alarms on_ams_ht_humidity_high?: boolean; on_ams_ht_temperature_high?: boolean; // Quiet hours quiet_hours_enabled?: boolean; quiet_hours_start?: string | null; quiet_hours_end?: string | null; // Daily digest daily_digest_enabled?: boolean; daily_digest_time?: string | null; // Printer filter printer_id?: number | null; } export interface NotificationProviderUpdate { name?: string; provider_type?: ProviderType; enabled?: boolean; config?: Record; // Print lifecycle events on_print_start?: boolean; on_print_complete?: boolean; on_print_failed?: boolean; on_print_stopped?: boolean; on_print_progress?: boolean; // Printer status events on_printer_offline?: boolean; on_printer_error?: boolean; on_filament_low?: boolean; on_maintenance_due?: boolean; // AMS environmental alarms (regular AMS) on_ams_humidity_high?: boolean; on_ams_temperature_high?: boolean; // AMS-HT environmental alarms on_ams_ht_humidity_high?: boolean; on_ams_ht_temperature_high?: boolean; // Quiet hours quiet_hours_enabled?: boolean; quiet_hours_start?: string | null; quiet_hours_end?: string | null; // Daily digest daily_digest_enabled?: boolean; daily_digest_time?: string | null; // Printer filter printer_id?: number | null; } export interface NotificationTestRequest { provider_type: ProviderType; config: Record; } export interface NotificationTestResponse { success: boolean; message: string; } // Provider-specific config types for reference export interface CallMeBotConfig { phone: string; apikey: string; } export interface NtfyConfig { server?: string; topic: string; auth_token?: string | null; } export interface PushoverConfig { user_key: string; app_token: string; priority?: number; } export interface TelegramConfig { bot_token: string; chat_id: string; } export interface EmailConfig { smtp_server: string; smtp_port?: number; username: string; password: string; from_email: string; to_email: string; use_tls?: boolean; } // Notification Template types export interface NotificationTemplate { id: number; event_type: string; name: string; title_template: string; body_template: string; is_default: boolean; created_at: string; updated_at: string; } export interface NotificationTemplateUpdate { title_template?: string; body_template?: string; } export interface EventVariablesResponse { event_type: string; event_name: string; variables: string[]; } export interface TemplatePreviewRequest { event_type: string; title_template: string; body_template: string; } export interface TemplatePreviewResponse { title: string; body: string; } // Notification Log types export interface NotificationLogEntry { id: number; provider_id: number; provider_name: string | null; provider_type: string | null; event_type: string; title: string; message: string; success: boolean; error_message: string | null; printer_id: number | null; printer_name: string | null; created_at: string; } export interface NotificationLogStats { total: number; success_count: number; failure_count: number; by_event_type: Record; by_provider: Record; } // Spoolman types export interface SpoolmanStatus { enabled: boolean; connected: boolean; url: string | null; } export interface SpoolmanSyncResult { success: boolean; synced_count: number; errors: string[]; } // Update types export interface VersionInfo { version: string; repo: string; } export interface UpdateCheckResult { update_available: boolean; current_version: string; latest_version: string | null; release_name?: string; release_notes?: string; release_url?: string; published_at?: string; error?: string; message?: string; } export interface UpdateStatus { status: 'idle' | 'checking' | 'downloading' | 'installing' | 'complete' | 'error'; progress: number; message: string; error: string | null; } // Maintenance types export interface MaintenanceType { id: number; name: string; description: string | null; default_interval_hours: number; interval_type: 'hours' | 'days'; // "hours" = print hours, "days" = calendar days icon: string | null; is_system: boolean; created_at: string; } export interface MaintenanceTypeCreate { name: string; description?: string | null; default_interval_hours?: number; interval_type?: 'hours' | 'days'; icon?: string | null; } export interface MaintenanceStatus { id: number; printer_id: number; printer_name: string; maintenance_type_id: number; maintenance_type_name: string; maintenance_type_icon: string | null; enabled: boolean; interval_hours: number; // For hours type: print hours; for days type: number of days interval_type: 'hours' | 'days'; current_hours: number; hours_since_maintenance: number; hours_until_due: number; days_since_maintenance: number | null; // For days type days_until_due: number | null; // For days type is_due: boolean; is_warning: boolean; last_performed_at: string | null; } export interface PrinterMaintenanceOverview { printer_id: number; printer_name: string; total_print_hours: number; maintenance_items: MaintenanceStatus[]; due_count: number; warning_count: number; } export interface MaintenanceHistory { id: number; printer_maintenance_id: number; performed_at: string; hours_at_maintenance: number; notes: string | null; } export interface MaintenanceSummary { total_due: number; total_warning: number; printers_with_issues: Array<{ printer_id: number; printer_name: string; due_count: number; warning_count: number; }>; } // External Links (sidebar) export interface ExternalLink { id: number; name: string; url: string; icon: string; custom_icon: string | null; sort_order: number; created_at: string; updated_at: string; } export interface ExternalLinkCreate { name: string; url: string; icon: string; } export interface ExternalLinkUpdate { name?: string; url?: string; icon?: string; } // API functions export const api = { // Printers getPrinters: () => request('/printers/'), getPrinter: (id: number) => request(`/printers/${id}`), createPrinter: (data: PrinterCreate) => request('/printers/', { method: 'POST', body: JSON.stringify(data), }), updatePrinter: (id: number, data: Partial) => request(`/printers/${id}`, { method: 'PATCH', body: JSON.stringify(data), }), deletePrinter: (id: number, deleteArchives: boolean = true) => request<{ status: string; archives_deleted: boolean }>( `/printers/${id}?delete_archives=${deleteArchives}`, { method: 'DELETE' } ), getPrinterStatus: (id: number) => request(`/printers/${id}/status`), refreshPrinterStatus: (id: number) => request<{ status: string }>(`/printers/${id}/refresh-status`, { method: 'POST', }), connectPrinter: (id: number) => request<{ connected: boolean }>(`/printers/${id}/connect`, { method: 'POST', }), disconnectPrinter: (id: number) => request<{ connected: boolean }>(`/printers/${id}/disconnect`, { method: 'POST', }), // Print Control stopPrint: (printerId: number) => request<{ success: boolean; message: string }>(`/printers/${printerId}/print/stop`, { method: 'POST', }), pausePrint: (printerId: number) => request<{ success: boolean; message: string }>(`/printers/${printerId}/print/pause`, { method: 'POST', }), resumePrint: (printerId: number) => request<{ success: boolean; message: string }>(`/printers/${printerId}/print/resume`, { method: 'POST', }), // Skip Objects getPrintableObjects: (printerId: number) => request<{ objects: Array<{ id: number; name: string; x: number | null; y: number | null; skipped: boolean }>; total: number; skipped_count: number; is_printing: boolean; }>(`/printers/${printerId}/print/objects`), skipObjects: (printerId: number, objectIds: number[]) => request<{ success: boolean; message: string; skipped_objects: number[] }>( `/printers/${printerId}/print/skip-objects`, { method: 'POST', body: JSON.stringify(objectIds), } ), // AMS Control refreshAmsSlot: (printerId: number, amsId: number, slotId: number) => request<{ success: boolean; message: string }>( `/printers/${printerId}/ams/${amsId}/slot/${slotId}/refresh`, { method: 'POST' } ), // MQTT Debug Logging enableMQTTLogging: (printerId: number) => request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/enable`, { method: 'POST', }), disableMQTTLogging: (printerId: number) => request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/disable`, { method: 'POST', }), getMQTTLogs: (printerId: number) => request(`/printers/${printerId}/logging`), clearMQTTLogs: (printerId: number) => request<{ status: string }>(`/printers/${printerId}/logging`, { method: 'DELETE', }), // Printer File Manager getPrinterFiles: (printerId: number, path = '/') => request<{ path: string; files: Array<{ name: string; is_directory: boolean; size: number; path: string; }>; }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`), getPrinterFileDownloadUrl: (printerId: number, path: string) => `${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`, deletePrinterFile: (printerId: number, path: string) => request<{ status: string; path: string }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`, { method: 'DELETE', }), getPrinterStorage: (printerId: number) => request<{ used_bytes: number | null; free_bytes: number | null }>(`/printers/${printerId}/storage`), // Archives getArchives: (printerId?: number, projectId?: number, limit = 50, offset = 0) => { const params = new URLSearchParams(); if (printerId) params.set('printer_id', String(printerId)); if (projectId) params.set('project_id', String(projectId)); params.set('limit', String(limit)); params.set('offset', String(offset)); return request(`/archives/?${params}`); }, getArchive: (id: number) => request(`/archives/${id}`), searchArchives: (query: string, options?: { printerId?: number; projectId?: number; status?: string; limit?: number; offset?: number; }) => { const params = new URLSearchParams(); params.set('q', query); if (options?.printerId) params.set('printer_id', String(options.printerId)); if (options?.projectId) params.set('project_id', String(options.projectId)); if (options?.status) params.set('status', options.status); if (options?.limit) params.set('limit', String(options.limit)); if (options?.offset) params.set('offset', String(options.offset)); return request(`/archives/search?${params}`); }, rebuildSearchIndex: () => request<{ message: string }>('/archives/search/rebuild-index', { method: 'POST' }), updateArchive: (id: number, data: { printer_id?: number | null; project_id?: number | null; print_name?: string; is_favorite?: boolean; tags?: string; notes?: string; cost?: number; failure_reason?: string | null; status?: string; }) => request(`/archives/${id}`, { method: 'PATCH', body: JSON.stringify(data), }), toggleFavorite: (id: number) => request(`/archives/${id}/favorite`, { method: 'POST' }), deleteArchive: (id: number) => request(`/archives/${id}`, { method: 'DELETE' }), getArchiveStats: () => request('/archives/stats'), getFailureAnalysis: (options?: { days?: number; printerId?: number; projectId?: number }) => { const params = new URLSearchParams(); if (options?.days) params.set('days', String(options.days)); if (options?.printerId) params.set('printer_id', String(options.printerId)); if (options?.projectId) params.set('project_id', String(options.projectId)); return request(`/archives/analysis/failures?${params}`); }, compareArchives: (archiveIds: number[]) => request(`/archives/compare?archive_ids=${archiveIds.join(',')}`), findSimilarArchives: (archiveId: number, limit = 10) => request(`/archives/${archiveId}/similar?limit=${limit}`), exportArchives: async (options?: { format?: 'csv' | 'xlsx'; fields?: string[]; printerId?: number; projectId?: number; status?: string; dateFrom?: string; dateTo?: string; search?: string; }): Promise<{ blob: Blob; filename: string }> => { const params = new URLSearchParams(); if (options?.format) params.set('format', options.format); if (options?.fields) params.set('fields', options.fields.join(',')); if (options?.printerId) params.set('printer_id', String(options.printerId)); if (options?.projectId) params.set('project_id', String(options.projectId)); if (options?.status) params.set('status', options.status); if (options?.dateFrom) params.set('date_from', options.dateFrom); if (options?.dateTo) params.set('date_to', options.dateTo); if (options?.search) params.set('search', options.search); const response = await fetch(`${API_BASE}/archives/export?${params}`); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } const contentDisposition = response.headers.get('Content-Disposition'); let filename = options?.format === 'xlsx' ? 'archives_export.xlsx' : 'archives_export.csv'; if (contentDisposition) { const match = contentDisposition.match(/filename="?([^"]+)"?/); if (match) filename = match[1]; } const blob = await response.blob(); return { blob, filename }; }, exportStats: async (options?: { format?: 'csv' | 'xlsx'; days?: number; printerId?: number; projectId?: number; }): Promise<{ blob: Blob; filename: string }> => { const params = new URLSearchParams(); if (options?.format) params.set('format', options.format); if (options?.days) params.set('days', String(options.days)); if (options?.printerId) params.set('printer_id', String(options.printerId)); if (options?.projectId) params.set('project_id', String(options.projectId)); const response = await fetch(`${API_BASE}/archives/stats/export?${params}`); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } const contentDisposition = response.headers.get('Content-Disposition'); let filename = options?.format === 'xlsx' ? 'stats_export.xlsx' : 'stats_export.csv'; if (contentDisposition) { const match = contentDisposition.match(/filename="?([^"]+)"?/); if (match) filename = match[1]; } const blob = await response.blob(); return { blob, filename }; }, getArchiveDuplicates: (id: number) => request<{ duplicates: ArchiveDuplicate[]; count: number }>(`/archives/${id}/duplicates`), backfillContentHashes: () => request<{ updated: number; errors: Array<{ id: number; error: string }> }>('/archives/backfill-hashes', { method: 'POST', }), getArchiveThumbnail: (id: number) => `${API_BASE}/archives/${id}/thumbnail?v=${Date.now()}`, getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`, getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`, getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse?v=${Date.now()}`, scanArchiveTimelapse: (id: number) => request<{ status: string; message: string; filename?: string; available_files?: Array<{ name: string; path: string; size: number; mtime: string | null }>; }>(`/archives/${id}/timelapse/scan`, { method: 'POST', }), selectArchiveTimelapse: (id: number, filename: string) => request<{ status: string; message: string; filename: string }>( `/archives/${id}/timelapse/select?filename=${encodeURIComponent(filename)}`, { method: 'POST' } ), uploadArchiveTimelapse: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => { const formData = new FormData(); formData.append('file', file); const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/upload`, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); }, // Timelapse Editor getTimelapseInfo: (archiveId: number) => request<{ duration: number; width: number; height: number; fps: number; codec: string; file_size: number; has_audio: boolean; }>(`/archives/${archiveId}/timelapse/info`), getTimelapseThumbnails: (archiveId: number, count: number = 10) => request<{ thumbnails: string[]; timestamps: number[]; }>(`/archives/${archiveId}/timelapse/thumbnails?count=${count}`), processTimelapse: async ( archiveId: number, params: { trimStart?: number; trimEnd?: number; speed?: number; saveMode: 'replace' | 'new'; outputFilename?: string; }, audioFile?: File ): Promise<{ status: string; output_path: string | null; message: string }> => { const formData = new FormData(); formData.append('trim_start', String(params.trimStart ?? 0)); if (params.trimEnd !== undefined) { formData.append('trim_end', String(params.trimEnd)); } formData.append('speed', String(params.speed ?? 1)); formData.append('save_mode', params.saveMode); if (params.outputFilename) { formData.append('output_filename', params.outputFilename); } if (audioFile) { formData.append('audio', audioFile); } const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/process`, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); }, // Photos getArchivePhotoUrl: (archiveId: number, filename: string) => `${API_BASE}/archives/${archiveId}/photos/${encodeURIComponent(filename)}`, uploadArchivePhoto: async (archiveId: number, file: File): Promise<{ status: string; filename: string; photos: string[] }> => { const formData = new FormData(); formData.append('file', file); const response = await fetch(`${API_BASE}/archives/${archiveId}/photos`, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); }, deleteArchivePhoto: (archiveId: number, filename: string) => request<{ status: string; photos: string[] | null }>(`/archives/${archiveId}/photos/${encodeURIComponent(filename)}`, { method: 'DELETE', }), // Source 3MF (original slicer project file) getSource3mfDownloadUrl: (archiveId: number) => `${API_BASE}/archives/${archiveId}/source`, getSource3mfForSlicer: (archiveId: number, filename: string) => `${API_BASE}/archives/${archiveId}/source/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`, uploadSource3mf: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => { const formData = new FormData(); formData.append('file', file); const response = await fetch(`${API_BASE}/archives/${archiveId}/source`, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); }, deleteSource3mf: (archiveId: number) => request<{ status: string }>(`/archives/${archiveId}/source`, { method: 'DELETE', }), // QR Code getArchiveQRCodeUrl: (archiveId: number, size = 200) => `${API_BASE}/archives/${archiveId}/qrcode?size=${size}`, getArchiveCapabilities: (id: number) => request<{ has_model: boolean; has_gcode: boolean; build_volume: { x: number; y: number; z: number }; filament_colors: string[]; }>(`/archives/${id}/capabilities`), // Project Page getArchiveProjectPage: (id: number) => request<{ title: string | null; description: string | null; designer: string | null; designer_user_id: string | null; license: string | null; copyright: string | null; creation_date: string | null; modification_date: string | null; origin: string | null; profile_title: string | null; profile_description: string | null; profile_cover: string | null; profile_user_id: string | null; profile_user_name: string | null; design_model_id: string | null; design_profile_id: string | null; design_region: string | null; model_pictures: Array<{ name: string; path: string; url: string }>; profile_pictures: Array<{ name: string; path: string; url: string }>; thumbnails: Array<{ name: string; path: string; url: string }>; }>(`/archives/${id}/project-page`), updateArchiveProjectPage: (id: number, data: { title?: string; description?: string; designer?: string; license?: string; copyright?: string; profile_title?: string; profile_description?: string; }) => request(`/archives/${id}/project-page`, { method: 'PATCH', body: JSON.stringify(data), }), getArchiveProjectImageUrl: (archiveId: number, imagePath: string) => `${API_BASE}/archives/${archiveId}/project-image/${encodeURIComponent(imagePath)}`, getArchiveForSlicer: (id: number, filename: string) => `${API_BASE}/archives/${id}/file/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`, getArchiveFilamentRequirements: (archiveId: number) => request<{ archive_id: number; filename: string; filaments: Array<{ slot_id: number; type: string; color: string; used_grams: number; used_meters: number; }>; }>(`/archives/${archiveId}/filament-requirements`), reprintArchive: (archiveId: number, printerId: number) => request<{ status: string; printer_id: number; archive_id: number; filename: string }>( `/archives/${archiveId}/reprint?printer_id=${printerId}`, { method: 'POST' } ), uploadArchive: async (file: File, printerId?: number): Promise => { const formData = new FormData(); formData.append('file', file); const url = printerId ? `${API_BASE}/archives/upload?printer_id=${printerId}` : `${API_BASE}/archives/upload`; const response = await fetch(url, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); }, uploadArchivesBulk: async (files: File[], printerId?: number): Promise => { const formData = new FormData(); files.forEach((file) => formData.append('files', file)); const url = printerId ? `${API_BASE}/archives/upload-bulk?printer_id=${printerId}` : `${API_BASE}/archives/upload-bulk`; const response = await fetch(url, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); }, // Settings getSettings: () => request('/settings/'), updateSettings: (data: AppSettingsUpdate) => request('/settings/', { method: 'PUT', body: JSON.stringify(data), }), resetSettings: () => request('/settings/reset', { method: 'POST' }), exportBackup: async (categories?: Record): Promise<{ blob: Blob; filename: string }> => { const params = new URLSearchParams(); if (categories) { if (categories.settings !== undefined) params.set('include_settings', String(categories.settings)); if (categories.notifications !== undefined) params.set('include_notifications', String(categories.notifications)); if (categories.templates !== undefined) params.set('include_templates', String(categories.templates)); if (categories.smart_plugs !== undefined) params.set('include_smart_plugs', String(categories.smart_plugs)); if (categories.external_links !== undefined) params.set('include_external_links', String(categories.external_links)); if (categories.printers !== undefined) params.set('include_printers', String(categories.printers)); if (categories.filaments !== undefined) params.set('include_filaments', String(categories.filaments)); if (categories.maintenance !== undefined) params.set('include_maintenance', String(categories.maintenance)); if (categories.archives !== undefined) params.set('include_archives', String(categories.archives)); if (categories.projects !== undefined) params.set('include_projects', String(categories.projects)); if (categories.access_codes !== undefined) params.set('include_access_codes', String(categories.access_codes)); } const url = `${API_BASE}/settings/backup${params.toString() ? '?' + params.toString() : ''}`; const response = await fetch(url); // Get filename from Content-Disposition header const contentDisposition = response.headers.get('Content-Disposition'); let filename = 'bambuddy-backup.json'; if (contentDisposition) { const match = contentDisposition.match(/filename=([^;]+)/); if (match) filename = match[1].trim(); } const blob = await response.blob(); return { blob, filename }; }, importBackup: async (file: File, overwrite = false) => { const formData = new FormData(); formData.append('file', file); const url = `${API_BASE}/settings/restore${overwrite ? '?overwrite=true' : ''}`; const response = await fetch(url, { method: 'POST', body: formData, }); return response.json() as Promise<{ success: boolean; message: string; restored?: Record; skipped?: Record; skipped_details?: Record; files_restored?: number; total_skipped?: number; }>; }, checkFfmpeg: () => request<{ installed: boolean; path: string | null }>('/settings/check-ffmpeg'), // Cloud getCloudStatus: () => request('/cloud/status'), cloudLogin: (email: string, password: string, region = 'global') => request('/cloud/login', { method: 'POST', body: JSON.stringify({ email, password, region }), }), cloudVerify: (email: string, code: string) => request('/cloud/verify', { method: 'POST', body: JSON.stringify({ email, code }), }), cloudSetToken: (access_token: string) => request('/cloud/token', { method: 'POST', body: JSON.stringify({ access_token }), }), cloudLogout: () => request<{ success: boolean }>('/cloud/logout', { method: 'POST' }), getCloudSettings: (version = '02.04.00.70') => request(`/cloud/settings?version=${version}`), getCloudSettingDetail: (settingId: string) => request(`/cloud/settings/${settingId}`), createCloudSetting: (data: SlicerSettingCreate) => request('/cloud/settings', { method: 'POST', body: JSON.stringify(data), }), updateCloudSetting: (settingId: string, data: SlicerSettingUpdate) => request(`/cloud/settings/${settingId}`, { method: 'PUT', body: JSON.stringify(data), }), deleteCloudSetting: (settingId: string) => request(`/cloud/settings/${settingId}`, { method: 'DELETE', }), getCloudDevices: () => request('/cloud/devices'), getCloudFields: (presetType: 'filament' | 'print' | 'process' | 'printer') => request(`/cloud/fields/${presetType}`), getAllCloudFields: () => request>('/cloud/fields'), getFilamentInfo: (settingIds: string[]) => request>('/cloud/filament-info', { method: 'POST', body: JSON.stringify(settingIds), }), // Smart Plugs getSmartPlugs: () => request('/smart-plugs/'), getSmartPlug: (id: number) => request(`/smart-plugs/${id}`), getSmartPlugByPrinter: (printerId: number) => request(`/smart-plugs/by-printer/${printerId}`), createSmartPlug: (data: SmartPlugCreate) => request('/smart-plugs/', { method: 'POST', body: JSON.stringify(data), }), updateSmartPlug: (id: number, data: SmartPlugUpdate) => request(`/smart-plugs/${id}`, { method: 'PATCH', body: JSON.stringify(data), }), deleteSmartPlug: (id: number) => request(`/smart-plugs/${id}`, { method: 'DELETE' }), controlSmartPlug: (id: number, action: 'on' | 'off' | 'toggle') => request<{ success: boolean; action: string }>(`/smart-plugs/${id}/control`, { method: 'POST', body: JSON.stringify({ action }), }), getSmartPlugStatus: (id: number) => request(`/smart-plugs/${id}/status`), testSmartPlugConnection: (ip_address: string, username?: string | null, password?: string | null) => request('/smart-plugs/test-connection', { method: 'POST', body: JSON.stringify({ ip_address, username, password }), }), // Tasmota Discovery (auto-detects network) startTasmotaScan: () => fetch(`${API_BASE}/smart-plugs/discover/scan`, { method: 'POST' }) .then(res => res.ok ? res.json() : res.json().then(e => { throw new Error(e.detail || `HTTP ${res.status}`); })), getTasmotaScanStatus: () => request('/smart-plugs/discover/status'), stopTasmotaScan: () => fetch(`${API_BASE}/smart-plugs/discover/stop`, { method: 'POST' }) .then(res => res.ok ? res.json() : res.json().then(e => { throw new Error(e.detail || `HTTP ${res.status}`); })), getDiscoveredTasmotaDevices: () => request('/smart-plugs/discover/devices'), // Print Queue getQueue: (printerId?: number, status?: string) => { const params = new URLSearchParams(); if (printerId) params.set('printer_id', String(printerId)); if (status) params.set('status', status); return request(`/queue/?${params}`); }, getQueueItem: (id: number) => request(`/queue/${id}`), addToQueue: (data: PrintQueueItemCreate) => request('/queue/', { method: 'POST', body: JSON.stringify(data), }), updateQueueItem: (id: number, data: PrintQueueItemUpdate) => request(`/queue/${id}`, { method: 'PATCH', body: JSON.stringify(data), }), removeFromQueue: (id: number) => request<{ message: string }>(`/queue/${id}`, { method: 'DELETE' }), reorderQueue: (items: { id: number; position: number }[]) => request<{ message: string }>('/queue/reorder', { method: 'POST', body: JSON.stringify({ items }), }), cancelQueueItem: (id: number) => request<{ message: string }>(`/queue/${id}/cancel`, { method: 'POST' }), stopQueueItem: (id: number) => request<{ message: string }>(`/queue/${id}/stop`, { method: 'POST' }), // K-Profiles getKProfiles: (printerId: number, nozzleDiameter = '0.4') => request(`/printers/${printerId}/kprofiles/?nozzle_diameter=${nozzleDiameter}`), setKProfile: (printerId: number, profile: KProfileCreate) => request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, { method: 'POST', body: JSON.stringify(profile), }), deleteKProfile: (printerId: number, profile: KProfileDelete) => request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, { method: 'DELETE', body: JSON.stringify(profile), }), setKProfilesBatch: (printerId: number, profiles: KProfileCreate[]) => request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/batch`, { method: 'POST', body: JSON.stringify(profiles), }), // K-Profile Notes (stored locally, not on printer) getKProfileNotes: (printerId: number) => request(`/printers/${printerId}/kprofiles/notes`), setKProfileNote: (printerId: number, settingId: string, note: string) => request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/notes`, { method: 'PUT', body: JSON.stringify({ setting_id: settingId, note }), }), deleteKProfileNote: (printerId: number, settingId: string) => request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/notes/${encodeURIComponent(settingId)}`, { method: 'DELETE', }), // Slot Preset Mappings getSlotPresets: (printerId: number) => request>(`/printers/${printerId}/slot-presets`), getSlotPreset: (printerId: number, amsId: number, trayId: number) => request(`/printers/${printerId}/slot-presets/${amsId}/${trayId}`), saveSlotPreset: (printerId: number, amsId: number, trayId: number, presetId: string, presetName: string) => request(`/printers/${printerId}/slot-presets/${amsId}/${trayId}?preset_id=${encodeURIComponent(presetId)}&preset_name=${encodeURIComponent(presetName)}`, { method: 'PUT', }), deleteSlotPreset: (printerId: number, amsId: number, trayId: number) => request<{ success: boolean }>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}`, { method: 'DELETE', }), // Filaments listFilaments: () => request('/filaments/'), getFilament: (id: number) => request(`/filaments/${id}`), getFilamentsByType: (type: string) => request(`/filaments/by-type/${type}`), // Notification Providers getNotificationProviders: () => request('/notifications/'), getNotificationProvider: (id: number) => request(`/notifications/${id}`), createNotificationProvider: (data: NotificationProviderCreate) => request('/notifications/', { method: 'POST', body: JSON.stringify(data), }), updateNotificationProvider: (id: number, data: NotificationProviderUpdate) => request(`/notifications/${id}`, { method: 'PATCH', body: JSON.stringify(data), }), deleteNotificationProvider: (id: number) => request<{ message: string }>(`/notifications/${id}`, { method: 'DELETE' }), testNotificationProvider: (id: number) => request(`/notifications/${id}/test`, { method: 'POST' }), testNotificationConfig: (data: NotificationTestRequest) => request('/notifications/test-config', { method: 'POST', body: JSON.stringify(data), }), testAllNotificationProviders: () => request<{ tested: number; success: number; failed: number; results: Array<{ provider_id: number; provider_name: string; provider_type: string; success: boolean; message: string; }>; }>('/notifications/test-all', { method: 'POST' }), // Notification Templates getNotificationTemplates: () => request('/notification-templates'), getNotificationTemplate: (id: number) => request(`/notification-templates/${id}`), updateNotificationTemplate: (id: number, data: NotificationTemplateUpdate) => request(`/notification-templates/${id}`, { method: 'PUT', body: JSON.stringify(data), }), resetNotificationTemplate: (id: number) => request(`/notification-templates/${id}/reset`, { method: 'POST', }), getTemplateVariables: () => request('/notification-templates/variables'), previewTemplate: (data: TemplatePreviewRequest) => request('/notification-templates/preview', { method: 'POST', body: JSON.stringify(data), }), // Notification Logs getNotificationLogs: (params?: { limit?: number; offset?: number; provider_id?: number; event_type?: string; success?: boolean; days?: number; }) => { const searchParams = new URLSearchParams(); if (params?.limit) searchParams.set('limit', String(params.limit)); if (params?.offset) searchParams.set('offset', String(params.offset)); if (params?.provider_id) searchParams.set('provider_id', String(params.provider_id)); if (params?.event_type) searchParams.set('event_type', params.event_type); if (params?.success !== undefined) searchParams.set('success', String(params.success)); if (params?.days) searchParams.set('days', String(params.days)); return request(`/notifications/logs?${searchParams}`); }, getNotificationLogStats: (days = 7) => request(`/notifications/logs/stats?days=${days}`), clearNotificationLogs: (olderThanDays = 30) => request<{ deleted: number; message: string }>( `/notifications/logs?older_than_days=${olderThanDays}`, { method: 'DELETE' } ), // Spoolman Integration getSpoolmanStatus: () => request('/spoolman/status'), connectSpoolman: () => request<{ success: boolean; message: string }>('/spoolman/connect', { method: 'POST', }), disconnectSpoolman: () => request<{ success: boolean; message: string }>('/spoolman/disconnect', { method: 'POST', }), syncPrinterAms: (printerId: number) => request(`/spoolman/sync/${printerId}`, { method: 'POST', }), syncAllPrintersAms: () => request('/spoolman/sync-all', { method: 'POST', }), getSpoolmanSpools: () => request<{ spools: unknown[] }>('/spoolman/spools'), getSpoolmanFilaments: () => request<{ filaments: unknown[] }>('/spoolman/filaments'), // Updates getVersion: () => request('/updates/version'), checkForUpdates: () => request('/updates/check'), applyUpdate: () => request<{ success: boolean; message: string; status: UpdateStatus }>('/updates/apply', { method: 'POST', }), getUpdateStatus: () => request('/updates/status'), // Maintenance getMaintenanceTypes: () => request('/maintenance/types'), createMaintenanceType: (data: MaintenanceTypeCreate) => request('/maintenance/types', { method: 'POST', body: JSON.stringify(data), }), updateMaintenanceType: (id: number, data: Partial) => request(`/maintenance/types/${id}`, { method: 'PATCH', body: JSON.stringify(data), }), deleteMaintenanceType: (id: number) => request<{ status: string }>(`/maintenance/types/${id}`, { method: 'DELETE' }), getMaintenanceOverview: () => request('/maintenance/overview'), getPrinterMaintenance: (printerId: number) => request(`/maintenance/printers/${printerId}`), updateMaintenanceItem: (itemId: number, data: { custom_interval_hours?: number | null; custom_interval_type?: 'hours' | 'days' | null; enabled?: boolean }) => request(`/maintenance/items/${itemId}`, { method: 'PATCH', body: JSON.stringify(data), }), performMaintenance: (itemId: number, notes?: string) => request(`/maintenance/items/${itemId}/perform`, { method: 'POST', body: JSON.stringify({ notes }), }), getMaintenanceHistory: (itemId: number) => request(`/maintenance/items/${itemId}/history`), getMaintenanceSummary: () => request('/maintenance/summary'), setPrinterHours: (printerId: number, totalHours: number) => request<{ printer_id: number; total_hours: number; archive_hours: number; offset_hours: number }>( `/maintenance/printers/${printerId}/hours?total_hours=${totalHours}`, { method: 'PATCH' } ), assignMaintenanceType: (printerId: number, typeId: number) => request(`/maintenance/printers/${printerId}/assign/${typeId}`, { method: 'POST', }), removeMaintenanceItem: (itemId: number) => request<{ status: string }>(`/maintenance/items/${itemId}`, { method: 'DELETE', }), // Camera getCameraStreamUrl: (printerId: number, fps = 10) => `${API_BASE}/printers/${printerId}/camera/stream?fps=${fps}`, getCameraSnapshotUrl: (printerId: number) => `${API_BASE}/printers/${printerId}/camera/snapshot`, testCameraConnection: (printerId: number) => request<{ success: boolean; message?: string; error?: string }>(`/printers/${printerId}/camera/test`), // External Links getExternalLinks: () => request('/external-links/'), getExternalLink: (id: number) => request(`/external-links/${id}`), createExternalLink: (data: ExternalLinkCreate) => request('/external-links/', { method: 'POST', body: JSON.stringify(data), }), updateExternalLink: (id: number, data: ExternalLinkUpdate) => request(`/external-links/${id}`, { method: 'PATCH', body: JSON.stringify(data), }), deleteExternalLink: (id: number) => request<{ message: string }>(`/external-links/${id}`, { method: 'DELETE' }), reorderExternalLinks: (ids: number[]) => request('/external-links/reorder', { method: 'PUT', body: JSON.stringify({ ids }), }), uploadExternalLinkIcon: async (id: number, file: File): Promise => { const formData = new FormData(); formData.append('file', file); const response = await fetch(`${API_BASE}/external-links/${id}/icon`, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); }, deleteExternalLinkIcon: (id: number) => request(`/external-links/${id}/icon`, { method: 'DELETE' }), getExternalLinkIconUrl: (id: number) => `${API_BASE}/external-links/${id}/icon`, // Projects getProjects: (status?: string) => { const params = new URLSearchParams(); if (status) params.set('status', status); return request(`/projects/?${params}`); }, getProject: (id: number) => request(`/projects/${id}`), createProject: (data: ProjectCreate) => request('/projects/', { method: 'POST', body: JSON.stringify(data), }), updateProject: (id: number, data: ProjectUpdate) => request(`/projects/${id}`, { method: 'PATCH', body: JSON.stringify(data), }), deleteProject: (id: number) => request<{ message: string }>(`/projects/${id}`, { method: 'DELETE' }), getProjectArchives: (id: number, limit = 100, offset = 0) => request(`/projects/${id}/archives?limit=${limit}&offset=${offset}`), addArchivesToProject: (projectId: number, archiveIds: number[]) => request<{ message: string }>(`/projects/${projectId}/add-archives`, { method: 'POST', body: JSON.stringify({ archive_ids: archiveIds }), }), removeArchivesFromProject: (projectId: number, archiveIds: number[]) => request<{ message: string }>(`/projects/${projectId}/remove-archives`, { method: 'POST', body: JSON.stringify({ archive_ids: archiveIds }), }), addQueueItemsToProject: (projectId: number, queueItemIds: number[]) => request<{ message: string }>(`/projects/${projectId}/add-queue`, { method: 'POST', body: JSON.stringify({ queue_item_ids: queueItemIds }), }), // Project Attachments uploadProjectAttachment: async (projectId: number, file: File): Promise<{ status: string; filename: string; original_name: string; attachments: ProjectAttachment[]; }> => { const formData = new FormData(); formData.append('file', file); const response = await fetch(`${API_BASE}/projects/${projectId}/attachments`, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); }, getProjectAttachmentUrl: (projectId: number, filename: string) => `${API_BASE}/projects/${projectId}/attachments/${encodeURIComponent(filename)}`, deleteProjectAttachment: (projectId: number, filename: string) => request<{ status: string; message: string; attachments: ProjectAttachment[] | null }>( `/projects/${projectId}/attachments/${encodeURIComponent(filename)}`, { method: 'DELETE' } ), // BOM (Bill of Materials) getProjectBOM: (projectId: number) => request(`/projects/${projectId}/bom`), createBOMItem: (projectId: number, data: BOMItemCreate) => request(`/projects/${projectId}/bom`, { method: 'POST', body: JSON.stringify(data), }), updateBOMItem: (projectId: number, itemId: number, data: BOMItemUpdate) => request(`/projects/${projectId}/bom/${itemId}`, { method: 'PATCH', body: JSON.stringify(data), }), deleteBOMItem: (projectId: number, itemId: number) => request<{ status: string; message: string }>(`/projects/${projectId}/bom/${itemId}`, { method: 'DELETE', }), // Templates getTemplates: () => request('/projects/templates/'), createTemplateFromProject: (projectId: number) => request(`/projects/${projectId}/create-template`, { method: 'POST' }), createProjectFromTemplate: (templateId: number, name?: string) => request(`/projects/from-template/${templateId}${name ? `?name=${encodeURIComponent(name)}` : ''}`, { method: 'POST', }), // Timeline getProjectTimeline: (projectId: number, limit = 50) => request(`/projects/${projectId}/timeline?limit=${limit}`), // API Keys getAPIKeys: () => request('/api-keys/'), createAPIKey: (data: APIKeyCreate) => request('/api-keys/', { method: 'POST', body: JSON.stringify(data), }), updateAPIKey: (id: number, data: APIKeyUpdate) => request(`/api-keys/${id}`, { method: 'PATCH', body: JSON.stringify(data), }), deleteAPIKey: (id: number) => request<{ message: string }>(`/api-keys/${id}`, { method: 'DELETE' }), // AMS History getAMSHistory: (printerId: number, amsId: number, hours = 24) => request(`/ams-history/${printerId}/${amsId}?hours=${hours}`), // System Info getSystemInfo: () => request('/system/info'), }; // AMS History types export interface AMSHistoryPoint { recorded_at: string; humidity: number | null; humidity_raw: number | null; temperature: number | null; } export interface AMSHistoryResponse { printer_id: number; ams_id: number; data: AMSHistoryPoint[]; min_humidity: number | null; max_humidity: number | null; avg_humidity: number | null; min_temperature: number | null; max_temperature: number | null; avg_temperature: number | null; } // System Info types export interface SystemInfo { app: { version: string; base_dir: string; archive_dir: string; }; database: { archives: number; archives_completed: number; archives_failed: number; archives_printing: number; printers: number; filaments: number; projects: number; smart_plugs: number; total_print_time_seconds: number; total_print_time_formatted: string; total_filament_grams: number; total_filament_kg: number; }; printers: { total: number; connected: number; connected_list: Array<{ id: number; name: string; state: string; model: string; }>; }; storage: { archive_size_bytes: number; archive_size_formatted: string; database_size_bytes: number; database_size_formatted: string; disk_total_bytes: number; disk_total_formatted: string; disk_used_bytes: number; disk_used_formatted: string; disk_free_bytes: number; disk_free_formatted: string; disk_percent_used: number; }; system: { platform: string; platform_release: string; platform_version: string; architecture: string; hostname: string; python_version: string; uptime_seconds: number; uptime_formatted: string; boot_time: string; }; memory: { total_bytes: number; total_formatted: string; available_bytes: number; available_formatted: string; used_bytes: number; used_formatted: string; percent_used: number; }; cpu: { count: number; count_logical: number; percent: number; }; } // Discovery types export interface DiscoveredPrinter { serial: string; name: string; ip_address: string; model: string | null; discovered_at: string | null; } export interface DiscoveryStatus { running: boolean; } export interface DiscoveryInfo { is_docker: boolean; ssdp_running: boolean; scan_running: boolean; } export interface SubnetScanStatus { running: boolean; scanned: number; total: number; } // Discovery API export const discoveryApi = { getInfo: () => request('/discovery/info'), getStatus: () => request('/discovery/status'), startDiscovery: (duration: number = 10) => request(`/discovery/start?duration=${duration}`, { method: 'POST' }), stopDiscovery: () => request('/discovery/stop', { method: 'POST' }), getDiscoveredPrinters: () => request('/discovery/printers'), // Subnet scanning (for Docker environments) startSubnetScan: (subnet: string, timeout: number = 1.0) => request('/discovery/scan', { method: 'POST', body: JSON.stringify({ subnet, timeout }), }), getScanStatus: () => request('/discovery/scan/status'), stopSubnetScan: () => request('/discovery/scan/stop', { method: 'POST' }), }; // Virtual Printer types export interface VirtualPrinterStatus { enabled: boolean; running: boolean; mode: 'immediate' | 'queue'; name: string; serial: string; model: string; pending_files: number; } export interface VirtualPrinterSettings { enabled: boolean; access_code_set: boolean; mode: 'immediate' | 'queue'; status: VirtualPrinterStatus; } export interface PendingUpload { id: number; filename: string; file_size: number; source_ip: string | null; status: string; tags: string | null; notes: string | null; project_id: number | null; uploaded_at: string; } // Virtual Printer API export const virtualPrinterApi = { getSettings: () => request('/settings/virtual-printer'), updateSettings: (data: { enabled?: boolean; access_code?: string; mode?: 'immediate' | 'queue'; }) => { const params = new URLSearchParams(); if (data.enabled !== undefined) params.set('enabled', String(data.enabled)); if (data.access_code !== undefined) params.set('access_code', data.access_code); if (data.mode !== undefined) params.set('mode', data.mode); return request(`/settings/virtual-printer?${params.toString()}`, { method: 'PUT', }); }, }; // Pending Uploads API export const pendingUploadsApi = { list: () => request('/pending-uploads/'), getCount: () => request<{ count: number }>('/pending-uploads/count'), get: (id: number) => request(`/pending-uploads/${id}`), archive: (id: number, data?: { tags?: string; notes?: string; project_id?: number }) => request<{ id: number; print_name: string; filename: string }>(`/pending-uploads/${id}/archive`, { method: 'POST', body: JSON.stringify(data || {}), }), discard: (id: number) => request<{ success: boolean }>(`/pending-uploads/${id}`, { method: 'DELETE' }), archiveAll: () => request<{ archived: number; failed: number }>('/pending-uploads/archive-all', { method: 'POST' }), discardAll: () => request<{ discarded: number }>('/pending-uploads/discard-all', { method: 'DELETE' }), };