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(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); } // Printer types export interface Printer { id: number; name: string; serial_number: string; ip_address: string; access_code: string; model: string | null; is_active: boolean; auto_archive: boolean; created_at: string; updated_at: string; } export interface HMSError { code: string; module: number; severity: number; // 1=fatal, 2=serious, 3=common, 4=info } 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; nozzle?: number; nozzle_target?: number; chamber?: number; } | null; cover_url: string | null; hms_errors: HMSError[]; } export interface PrinterCreate { name: string; serial_number: string; ip_address: string; access_code: string; model?: 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; filename: string; file_path: string; file_size: number; content_hash: string | null; thumbnail_path: string | null; timelapse_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 BulkUploadResult { uploaded: number; failed: number; results: Array<{ filename: string; id: number; status: string }>; errors: Array<{ filename: string; error: string }>; } // 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; } 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 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; last_state: string | null; last_checked: string | null; 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; } 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; } 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; } // 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; } 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[]; } // 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) => request(`/printers/${id}`, { method: 'DELETE' }), getPrinterStatus: (id: number) => request(`/printers/${id}/status`), connectPrinter: (id: number) => request<{ connected: boolean }>(`/printers/${id}/connect`, { method: 'POST', }), disconnectPrinter: (id: number) => request<{ connected: boolean }>(`/printers/${id}/disconnect`, { 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, limit = 50, offset = 0) => { const params = new URLSearchParams(); if (printerId) params.set('printer_id', String(printerId)); params.set('limit', String(limit)); params.set('offset', String(offset)); return request(`/archives/?${params}`); }, getArchive: (id: number) => request(`/archives/${id}`), updateArchive: (id: number, data: { printer_id?: number | null; print_name?: string; is_favorite?: boolean; tags?: string; notes?: string; cost?: number; failure_reason?: string | null; }) => 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'), 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`, getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`, getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`, getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse`, scanArchiveTimelapse: (id: number) => request<{ status: string; message: string; filename?: string }>(`/archives/${id}/timelapse/scan`, { 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(); }, // 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', }), // 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 }; }>(`/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')}`, 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' }), 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 = '01.09.00.00') => request(`/cloud/settings?version=${version}`), getCloudSettingDetail: (settingId: string) => request>(`/cloud/settings/${settingId}`), getCloudDevices: () => request('/cloud/devices'), // 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 }), }), // 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' }), };