client.ts 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046
  1. const API_BASE = '/api/v1';
  2. async function request<T>(
  3. endpoint: string,
  4. options: RequestInit = {}
  5. ): Promise<T> {
  6. const response = await fetch(`${API_BASE}${endpoint}`, {
  7. ...options,
  8. headers: {
  9. 'Content-Type': 'application/json',
  10. ...options.headers,
  11. },
  12. });
  13. if (!response.ok) {
  14. const error = await response.json().catch(() => ({}));
  15. throw new Error(error.detail || `HTTP ${response.status}`);
  16. }
  17. return response.json();
  18. }
  19. // Printer types
  20. export interface Printer {
  21. id: number;
  22. name: string;
  23. serial_number: string;
  24. ip_address: string;
  25. access_code: string;
  26. model: string | null;
  27. nozzle_count: number; // 1 or 2, auto-detected from MQTT
  28. is_active: boolean;
  29. auto_archive: boolean;
  30. created_at: string;
  31. updated_at: string;
  32. }
  33. export interface HMSError {
  34. code: string;
  35. module: number;
  36. severity: number; // 1=fatal, 2=serious, 3=common, 4=info
  37. }
  38. export interface PrinterStatus {
  39. id: number;
  40. name: string;
  41. connected: boolean;
  42. state: string | null;
  43. current_print: string | null;
  44. subtask_name: string | null;
  45. gcode_file: string | null;
  46. progress: number | null;
  47. remaining_time: number | null;
  48. layer_num: number | null;
  49. total_layers: number | null;
  50. temperatures: {
  51. bed?: number;
  52. bed_target?: number;
  53. nozzle?: number;
  54. nozzle_target?: number;
  55. chamber?: number;
  56. } | null;
  57. cover_url: string | null;
  58. hms_errors: HMSError[];
  59. }
  60. export interface PrinterCreate {
  61. name: string;
  62. serial_number: string;
  63. ip_address: string;
  64. access_code: string;
  65. model?: string;
  66. auto_archive?: boolean;
  67. }
  68. // Archive types
  69. export interface ArchiveDuplicate {
  70. id: number;
  71. print_name: string | null;
  72. created_at: string;
  73. match_type: 'exact' | 'similar'; // 'exact' = hash match, 'similar' = name match
  74. }
  75. export interface Archive {
  76. id: number;
  77. printer_id: number | null;
  78. filename: string;
  79. file_path: string;
  80. file_size: number;
  81. content_hash: string | null;
  82. thumbnail_path: string | null;
  83. timelapse_path: string | null;
  84. source_3mf_path: string | null;
  85. duplicates: ArchiveDuplicate[] | null;
  86. duplicate_count: number;
  87. print_name: string | null;
  88. print_time_seconds: number | null;
  89. actual_time_seconds: number | null; // Computed from started_at/completed_at
  90. time_accuracy: number | null; // Percentage: 100 = perfect, >100 = faster than estimated
  91. filament_used_grams: number | null;
  92. filament_type: string | null;
  93. filament_color: string | null;
  94. layer_height: number | null;
  95. total_layers: number | null;
  96. nozzle_diameter: number | null;
  97. bed_temperature: number | null;
  98. nozzle_temperature: number | null;
  99. status: string;
  100. started_at: string | null;
  101. completed_at: string | null;
  102. extra_data: Record<string, unknown> | null;
  103. makerworld_url: string | null;
  104. designer: string | null;
  105. is_favorite: boolean;
  106. tags: string | null;
  107. notes: string | null;
  108. cost: number | null;
  109. photos: string[] | null;
  110. failure_reason: string | null;
  111. energy_kwh: number | null;
  112. energy_cost: number | null;
  113. created_at: string;
  114. }
  115. export interface ArchiveStats {
  116. total_prints: number;
  117. successful_prints: number;
  118. failed_prints: number;
  119. total_print_time_hours: number;
  120. total_filament_grams: number;
  121. total_cost: number;
  122. prints_by_filament_type: Record<string, number>;
  123. prints_by_printer: Record<string, number>;
  124. average_time_accuracy: number | null;
  125. time_accuracy_by_printer: Record<string, number> | null;
  126. total_energy_kwh: number;
  127. total_energy_cost: number;
  128. }
  129. export interface BulkUploadResult {
  130. uploaded: number;
  131. failed: number;
  132. results: Array<{ filename: string; id: number; status: string }>;
  133. errors: Array<{ filename: string; error: string }>;
  134. }
  135. // Settings types
  136. export interface AppSettings {
  137. auto_archive: boolean;
  138. save_thumbnails: boolean;
  139. capture_finish_photo: boolean;
  140. default_filament_cost: number;
  141. currency: string;
  142. energy_cost_per_kwh: number;
  143. energy_tracking_mode: 'print' | 'total';
  144. check_updates: boolean;
  145. }
  146. export type AppSettingsUpdate = Partial<AppSettings>;
  147. // Cloud types
  148. export interface CloudAuthStatus {
  149. is_authenticated: boolean;
  150. email: string | null;
  151. }
  152. export interface CloudLoginResponse {
  153. success: boolean;
  154. needs_verification: boolean;
  155. message: string;
  156. }
  157. export interface SlicerSetting {
  158. setting_id: string;
  159. name: string;
  160. type: string;
  161. version: string | null;
  162. user_id: string | null;
  163. updated_time: string | null;
  164. }
  165. export interface SlicerSettingsResponse {
  166. filament: SlicerSetting[];
  167. printer: SlicerSetting[];
  168. process: SlicerSetting[];
  169. }
  170. export interface CloudDevice {
  171. dev_id: string;
  172. name: string;
  173. dev_model_name: string | null;
  174. dev_product_name: string | null;
  175. online: boolean;
  176. }
  177. // Smart Plug types
  178. export interface SmartPlug {
  179. id: number;
  180. name: string;
  181. ip_address: string;
  182. printer_id: number | null;
  183. enabled: boolean;
  184. auto_on: boolean;
  185. auto_off: boolean;
  186. off_delay_mode: 'time' | 'temperature';
  187. off_delay_minutes: number;
  188. off_temp_threshold: number;
  189. username: string | null;
  190. password: string | null;
  191. last_state: string | null;
  192. last_checked: string | null;
  193. auto_off_executed: boolean; // True when auto-off was triggered after print
  194. created_at: string;
  195. updated_at: string;
  196. }
  197. export interface SmartPlugCreate {
  198. name: string;
  199. ip_address: string;
  200. printer_id?: number | null;
  201. enabled?: boolean;
  202. auto_on?: boolean;
  203. auto_off?: boolean;
  204. off_delay_mode?: 'time' | 'temperature';
  205. off_delay_minutes?: number;
  206. off_temp_threshold?: number;
  207. username?: string | null;
  208. password?: string | null;
  209. }
  210. export interface SmartPlugUpdate {
  211. name?: string;
  212. ip_address?: string;
  213. printer_id?: number | null;
  214. enabled?: boolean;
  215. auto_on?: boolean;
  216. auto_off?: boolean;
  217. off_delay_mode?: 'time' | 'temperature';
  218. off_delay_minutes?: number;
  219. off_temp_threshold?: number;
  220. username?: string | null;
  221. password?: string | null;
  222. }
  223. export interface SmartPlugEnergy {
  224. power: number | null; // Current watts
  225. voltage: number | null; // Volts
  226. current: number | null; // Amps
  227. today: number | null; // kWh used today
  228. yesterday: number | null; // kWh used yesterday
  229. total: number | null; // Total kWh
  230. factor: number | null; // Power factor (0-1)
  231. apparent_power: number | null; // VA
  232. reactive_power: number | null; // VAr
  233. }
  234. export interface SmartPlugStatus {
  235. state: string | null;
  236. reachable: boolean;
  237. device_name: string | null;
  238. energy: SmartPlugEnergy | null;
  239. }
  240. export interface SmartPlugTestResult {
  241. success: boolean;
  242. state: string | null;
  243. device_name: string | null;
  244. }
  245. // Print Queue types
  246. export interface PrintQueueItem {
  247. id: number;
  248. printer_id: number;
  249. archive_id: number;
  250. position: number;
  251. scheduled_time: string | null;
  252. require_previous_success: boolean;
  253. auto_off_after: boolean;
  254. status: 'pending' | 'printing' | 'completed' | 'failed' | 'skipped' | 'cancelled';
  255. started_at: string | null;
  256. completed_at: string | null;
  257. error_message: string | null;
  258. created_at: string;
  259. archive_name?: string | null;
  260. archive_thumbnail?: string | null;
  261. printer_name?: string | null;
  262. }
  263. export interface PrintQueueItemCreate {
  264. printer_id: number;
  265. archive_id: number;
  266. scheduled_time?: string | null;
  267. require_previous_success?: boolean;
  268. auto_off_after?: boolean;
  269. }
  270. export interface PrintQueueItemUpdate {
  271. printer_id?: number;
  272. position?: number;
  273. scheduled_time?: string | null;
  274. require_previous_success?: boolean;
  275. auto_off_after?: boolean;
  276. }
  277. // MQTT Logging types
  278. export interface MQTTLogEntry {
  279. timestamp: string;
  280. topic: string;
  281. direction: 'in' | 'out';
  282. payload: Record<string, unknown>;
  283. }
  284. export interface MQTTLogsResponse {
  285. logging_enabled: boolean;
  286. logs: MQTTLogEntry[];
  287. }
  288. // K-Profile types
  289. export interface KProfile {
  290. slot_id: number;
  291. extruder_id: number;
  292. nozzle_id: string;
  293. nozzle_diameter: string;
  294. filament_id: string;
  295. name: string;
  296. k_value: string;
  297. n_coef: string;
  298. ams_id: number;
  299. tray_id: number;
  300. setting_id: string | null;
  301. }
  302. export interface KProfileCreate {
  303. slot_id?: number; // Storage slot, 0 for new profiles
  304. extruder_id?: number;
  305. nozzle_id: string;
  306. nozzle_diameter: string;
  307. filament_id: string;
  308. name: string;
  309. k_value: string;
  310. n_coef?: string;
  311. ams_id?: number;
  312. tray_id?: number;
  313. setting_id?: string | null;
  314. }
  315. export interface KProfileDelete {
  316. slot_id: number; // cali_idx - calibration index to delete
  317. extruder_id: number;
  318. nozzle_id: string; // e.g., "HH00-0.4"
  319. nozzle_diameter: string; // e.g., "0.4"
  320. filament_id: string; // Bambu filament identifier
  321. }
  322. export interface KProfilesResponse {
  323. profiles: KProfile[];
  324. nozzle_diameter: string;
  325. }
  326. // Notification Provider types
  327. export type ProviderType = 'callmebot' | 'ntfy' | 'pushover' | 'telegram' | 'email';
  328. export interface NotificationProvider {
  329. id: number;
  330. name: string;
  331. provider_type: ProviderType;
  332. enabled: boolean;
  333. config: Record<string, unknown>;
  334. // Print lifecycle events
  335. on_print_start: boolean;
  336. on_print_complete: boolean;
  337. on_print_failed: boolean;
  338. on_print_stopped: boolean;
  339. on_print_progress: boolean;
  340. // Printer status events
  341. on_printer_offline: boolean;
  342. on_printer_error: boolean;
  343. on_filament_low: boolean;
  344. on_maintenance_due: boolean;
  345. // Quiet hours
  346. quiet_hours_enabled: boolean;
  347. quiet_hours_start: string | null;
  348. quiet_hours_end: string | null;
  349. // Printer filter
  350. printer_id: number | null;
  351. // Status tracking
  352. last_success: string | null;
  353. last_error: string | null;
  354. last_error_at: string | null;
  355. // Timestamps
  356. created_at: string;
  357. updated_at: string;
  358. }
  359. export interface NotificationProviderCreate {
  360. name: string;
  361. provider_type: ProviderType;
  362. enabled?: boolean;
  363. config: Record<string, unknown>;
  364. // Print lifecycle events
  365. on_print_start?: boolean;
  366. on_print_complete?: boolean;
  367. on_print_failed?: boolean;
  368. on_print_stopped?: boolean;
  369. on_print_progress?: boolean;
  370. // Printer status events
  371. on_printer_offline?: boolean;
  372. on_printer_error?: boolean;
  373. on_filament_low?: boolean;
  374. on_maintenance_due?: boolean;
  375. // Quiet hours
  376. quiet_hours_enabled?: boolean;
  377. quiet_hours_start?: string | null;
  378. quiet_hours_end?: string | null;
  379. // Printer filter
  380. printer_id?: number | null;
  381. }
  382. export interface NotificationProviderUpdate {
  383. name?: string;
  384. provider_type?: ProviderType;
  385. enabled?: boolean;
  386. config?: Record<string, unknown>;
  387. // Print lifecycle events
  388. on_print_start?: boolean;
  389. on_print_complete?: boolean;
  390. on_print_failed?: boolean;
  391. on_print_stopped?: boolean;
  392. on_print_progress?: boolean;
  393. // Printer status events
  394. on_printer_offline?: boolean;
  395. on_printer_error?: boolean;
  396. on_filament_low?: boolean;
  397. on_maintenance_due?: boolean;
  398. // Quiet hours
  399. quiet_hours_enabled?: boolean;
  400. quiet_hours_start?: string | null;
  401. quiet_hours_end?: string | null;
  402. // Printer filter
  403. printer_id?: number | null;
  404. }
  405. export interface NotificationTestRequest {
  406. provider_type: ProviderType;
  407. config: Record<string, unknown>;
  408. }
  409. export interface NotificationTestResponse {
  410. success: boolean;
  411. message: string;
  412. }
  413. // Provider-specific config types for reference
  414. export interface CallMeBotConfig {
  415. phone: string;
  416. apikey: string;
  417. }
  418. export interface NtfyConfig {
  419. server?: string;
  420. topic: string;
  421. auth_token?: string | null;
  422. }
  423. export interface PushoverConfig {
  424. user_key: string;
  425. app_token: string;
  426. priority?: number;
  427. }
  428. export interface TelegramConfig {
  429. bot_token: string;
  430. chat_id: string;
  431. }
  432. export interface EmailConfig {
  433. smtp_server: string;
  434. smtp_port?: number;
  435. username: string;
  436. password: string;
  437. from_email: string;
  438. to_email: string;
  439. use_tls?: boolean;
  440. }
  441. // Spoolman types
  442. export interface SpoolmanStatus {
  443. enabled: boolean;
  444. connected: boolean;
  445. url: string | null;
  446. }
  447. export interface SpoolmanSyncResult {
  448. success: boolean;
  449. synced_count: number;
  450. errors: string[];
  451. }
  452. // Update types
  453. export interface VersionInfo {
  454. version: string;
  455. repo: string;
  456. }
  457. export interface UpdateCheckResult {
  458. update_available: boolean;
  459. current_version: string;
  460. latest_version: string | null;
  461. release_name?: string;
  462. release_notes?: string;
  463. release_url?: string;
  464. published_at?: string;
  465. error?: string;
  466. message?: string;
  467. }
  468. export interface UpdateStatus {
  469. status: 'idle' | 'checking' | 'downloading' | 'installing' | 'complete' | 'error';
  470. progress: number;
  471. message: string;
  472. error: string | null;
  473. }
  474. // Maintenance types
  475. export interface MaintenanceType {
  476. id: number;
  477. name: string;
  478. description: string | null;
  479. default_interval_hours: number;
  480. icon: string | null;
  481. is_system: boolean;
  482. created_at: string;
  483. }
  484. export interface MaintenanceTypeCreate {
  485. name: string;
  486. description?: string | null;
  487. default_interval_hours?: number;
  488. icon?: string | null;
  489. }
  490. export interface MaintenanceStatus {
  491. id: number;
  492. printer_id: number;
  493. printer_name: string;
  494. maintenance_type_id: number;
  495. maintenance_type_name: string;
  496. maintenance_type_icon: string | null;
  497. enabled: boolean;
  498. interval_hours: number;
  499. current_hours: number;
  500. hours_since_maintenance: number;
  501. hours_until_due: number;
  502. is_due: boolean;
  503. is_warning: boolean;
  504. last_performed_at: string | null;
  505. }
  506. export interface PrinterMaintenanceOverview {
  507. printer_id: number;
  508. printer_name: string;
  509. total_print_hours: number;
  510. maintenance_items: MaintenanceStatus[];
  511. due_count: number;
  512. warning_count: number;
  513. }
  514. export interface MaintenanceHistory {
  515. id: number;
  516. printer_maintenance_id: number;
  517. performed_at: string;
  518. hours_at_maintenance: number;
  519. notes: string | null;
  520. }
  521. export interface MaintenanceSummary {
  522. total_due: number;
  523. total_warning: number;
  524. printers_with_issues: Array<{
  525. printer_id: number;
  526. printer_name: string;
  527. due_count: number;
  528. warning_count: number;
  529. }>;
  530. }
  531. // API functions
  532. export const api = {
  533. // Printers
  534. getPrinters: () => request<Printer[]>('/printers/'),
  535. getPrinter: (id: number) => request<Printer>(`/printers/${id}`),
  536. createPrinter: (data: PrinterCreate) =>
  537. request<Printer>('/printers/', {
  538. method: 'POST',
  539. body: JSON.stringify(data),
  540. }),
  541. updatePrinter: (id: number, data: Partial<PrinterCreate>) =>
  542. request<Printer>(`/printers/${id}`, {
  543. method: 'PATCH',
  544. body: JSON.stringify(data),
  545. }),
  546. deletePrinter: (id: number) =>
  547. request<void>(`/printers/${id}`, { method: 'DELETE' }),
  548. getPrinterStatus: (id: number) =>
  549. request<PrinterStatus>(`/printers/${id}/status`),
  550. connectPrinter: (id: number) =>
  551. request<{ connected: boolean }>(`/printers/${id}/connect`, {
  552. method: 'POST',
  553. }),
  554. disconnectPrinter: (id: number) =>
  555. request<{ connected: boolean }>(`/printers/${id}/disconnect`, {
  556. method: 'POST',
  557. }),
  558. // MQTT Debug Logging
  559. enableMQTTLogging: (printerId: number) =>
  560. request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/enable`, {
  561. method: 'POST',
  562. }),
  563. disableMQTTLogging: (printerId: number) =>
  564. request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/disable`, {
  565. method: 'POST',
  566. }),
  567. getMQTTLogs: (printerId: number) =>
  568. request<MQTTLogsResponse>(`/printers/${printerId}/logging`),
  569. clearMQTTLogs: (printerId: number) =>
  570. request<{ status: string }>(`/printers/${printerId}/logging`, {
  571. method: 'DELETE',
  572. }),
  573. // Printer File Manager
  574. getPrinterFiles: (printerId: number, path = '/') =>
  575. request<{
  576. path: string;
  577. files: Array<{
  578. name: string;
  579. is_directory: boolean;
  580. size: number;
  581. path: string;
  582. }>;
  583. }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`),
  584. getPrinterFileDownloadUrl: (printerId: number, path: string) =>
  585. `${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`,
  586. deletePrinterFile: (printerId: number, path: string) =>
  587. request<{ status: string; path: string }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`, {
  588. method: 'DELETE',
  589. }),
  590. getPrinterStorage: (printerId: number) =>
  591. request<{ used_bytes: number | null; free_bytes: number | null }>(`/printers/${printerId}/storage`),
  592. // Archives
  593. getArchives: (printerId?: number, limit = 50, offset = 0) => {
  594. const params = new URLSearchParams();
  595. if (printerId) params.set('printer_id', String(printerId));
  596. params.set('limit', String(limit));
  597. params.set('offset', String(offset));
  598. return request<Archive[]>(`/archives/?${params}`);
  599. },
  600. getArchive: (id: number) => request<Archive>(`/archives/${id}`),
  601. updateArchive: (id: number, data: {
  602. printer_id?: number | null;
  603. print_name?: string;
  604. is_favorite?: boolean;
  605. tags?: string;
  606. notes?: string;
  607. cost?: number;
  608. failure_reason?: string | null;
  609. }) =>
  610. request<Archive>(`/archives/${id}`, {
  611. method: 'PATCH',
  612. body: JSON.stringify(data),
  613. }),
  614. toggleFavorite: (id: number) =>
  615. request<Archive>(`/archives/${id}/favorite`, { method: 'POST' }),
  616. deleteArchive: (id: number) =>
  617. request<void>(`/archives/${id}`, { method: 'DELETE' }),
  618. getArchiveStats: () => request<ArchiveStats>('/archives/stats'),
  619. getArchiveDuplicates: (id: number) =>
  620. request<{ duplicates: ArchiveDuplicate[]; count: number }>(`/archives/${id}/duplicates`),
  621. backfillContentHashes: () =>
  622. request<{ updated: number; errors: Array<{ id: number; error: string }> }>('/archives/backfill-hashes', {
  623. method: 'POST',
  624. }),
  625. getArchiveThumbnail: (id: number) => `${API_BASE}/archives/${id}/thumbnail`,
  626. getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`,
  627. getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`,
  628. getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse`,
  629. scanArchiveTimelapse: (id: number) =>
  630. request<{
  631. status: string;
  632. message: string;
  633. filename?: string;
  634. available_files?: Array<{ name: string; path: string; size: number; mtime: string | null }>;
  635. }>(`/archives/${id}/timelapse/scan`, {
  636. method: 'POST',
  637. }),
  638. selectArchiveTimelapse: (id: number, filename: string) =>
  639. request<{ status: string; message: string; filename: string }>(
  640. `/archives/${id}/timelapse/select?filename=${encodeURIComponent(filename)}`,
  641. { method: 'POST' }
  642. ),
  643. uploadArchiveTimelapse: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
  644. const formData = new FormData();
  645. formData.append('file', file);
  646. const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/upload`, {
  647. method: 'POST',
  648. body: formData,
  649. });
  650. if (!response.ok) {
  651. const error = await response.json().catch(() => ({}));
  652. throw new Error(error.detail || `HTTP ${response.status}`);
  653. }
  654. return response.json();
  655. },
  656. // Photos
  657. getArchivePhotoUrl: (archiveId: number, filename: string) =>
  658. `${API_BASE}/archives/${archiveId}/photos/${encodeURIComponent(filename)}`,
  659. uploadArchivePhoto: async (archiveId: number, file: File): Promise<{ status: string; filename: string; photos: string[] }> => {
  660. const formData = new FormData();
  661. formData.append('file', file);
  662. const response = await fetch(`${API_BASE}/archives/${archiveId}/photos`, {
  663. method: 'POST',
  664. body: formData,
  665. });
  666. if (!response.ok) {
  667. const error = await response.json().catch(() => ({}));
  668. throw new Error(error.detail || `HTTP ${response.status}`);
  669. }
  670. return response.json();
  671. },
  672. deleteArchivePhoto: (archiveId: number, filename: string) =>
  673. request<{ status: string; photos: string[] | null }>(`/archives/${archiveId}/photos/${encodeURIComponent(filename)}`, {
  674. method: 'DELETE',
  675. }),
  676. // Source 3MF (original slicer project file)
  677. getSource3mfDownloadUrl: (archiveId: number) =>
  678. `${API_BASE}/archives/${archiveId}/source`,
  679. getSource3mfForSlicer: (archiveId: number, filename: string) =>
  680. `${API_BASE}/archives/${archiveId}/source/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
  681. uploadSource3mf: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
  682. const formData = new FormData();
  683. formData.append('file', file);
  684. const response = await fetch(`${API_BASE}/archives/${archiveId}/source`, {
  685. method: 'POST',
  686. body: formData,
  687. });
  688. if (!response.ok) {
  689. const error = await response.json().catch(() => ({}));
  690. throw new Error(error.detail || `HTTP ${response.status}`);
  691. }
  692. return response.json();
  693. },
  694. deleteSource3mf: (archiveId: number) =>
  695. request<{ status: string }>(`/archives/${archiveId}/source`, {
  696. method: 'DELETE',
  697. }),
  698. // QR Code
  699. getArchiveQRCodeUrl: (archiveId: number, size = 200) =>
  700. `${API_BASE}/archives/${archiveId}/qrcode?size=${size}`,
  701. getArchiveCapabilities: (id: number) =>
  702. request<{
  703. has_model: boolean;
  704. has_gcode: boolean;
  705. build_volume: { x: number; y: number; z: number };
  706. }>(`/archives/${id}/capabilities`),
  707. // Project Page
  708. getArchiveProjectPage: (id: number) =>
  709. request<{
  710. title: string | null;
  711. description: string | null;
  712. designer: string | null;
  713. designer_user_id: string | null;
  714. license: string | null;
  715. copyright: string | null;
  716. creation_date: string | null;
  717. modification_date: string | null;
  718. origin: string | null;
  719. profile_title: string | null;
  720. profile_description: string | null;
  721. profile_cover: string | null;
  722. profile_user_id: string | null;
  723. profile_user_name: string | null;
  724. design_model_id: string | null;
  725. design_profile_id: string | null;
  726. design_region: string | null;
  727. model_pictures: Array<{ name: string; path: string; url: string }>;
  728. profile_pictures: Array<{ name: string; path: string; url: string }>;
  729. thumbnails: Array<{ name: string; path: string; url: string }>;
  730. }>(`/archives/${id}/project-page`),
  731. updateArchiveProjectPage: (id: number, data: {
  732. title?: string;
  733. description?: string;
  734. designer?: string;
  735. license?: string;
  736. copyright?: string;
  737. profile_title?: string;
  738. profile_description?: string;
  739. }) =>
  740. request(`/archives/${id}/project-page`, {
  741. method: 'PATCH',
  742. body: JSON.stringify(data),
  743. }),
  744. getArchiveProjectImageUrl: (archiveId: number, imagePath: string) =>
  745. `${API_BASE}/archives/${archiveId}/project-image/${encodeURIComponent(imagePath)}`,
  746. getArchiveForSlicer: (id: number, filename: string) =>
  747. `${API_BASE}/archives/${id}/file/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
  748. reprintArchive: (archiveId: number, printerId: number) =>
  749. request<{ status: string; printer_id: number; archive_id: number; filename: string }>(
  750. `/archives/${archiveId}/reprint?printer_id=${printerId}`,
  751. { method: 'POST' }
  752. ),
  753. uploadArchive: async (file: File, printerId?: number): Promise<Archive> => {
  754. const formData = new FormData();
  755. formData.append('file', file);
  756. const url = printerId
  757. ? `${API_BASE}/archives/upload?printer_id=${printerId}`
  758. : `${API_BASE}/archives/upload`;
  759. const response = await fetch(url, {
  760. method: 'POST',
  761. body: formData,
  762. });
  763. if (!response.ok) {
  764. const error = await response.json().catch(() => ({}));
  765. throw new Error(error.detail || `HTTP ${response.status}`);
  766. }
  767. return response.json();
  768. },
  769. uploadArchivesBulk: async (files: File[], printerId?: number): Promise<BulkUploadResult> => {
  770. const formData = new FormData();
  771. files.forEach((file) => formData.append('files', file));
  772. const url = printerId
  773. ? `${API_BASE}/archives/upload-bulk?printer_id=${printerId}`
  774. : `${API_BASE}/archives/upload-bulk`;
  775. const response = await fetch(url, {
  776. method: 'POST',
  777. body: formData,
  778. });
  779. if (!response.ok) {
  780. const error = await response.json().catch(() => ({}));
  781. throw new Error(error.detail || `HTTP ${response.status}`);
  782. }
  783. return response.json();
  784. },
  785. // Settings
  786. getSettings: () => request<AppSettings>('/settings/'),
  787. updateSettings: (data: AppSettingsUpdate) =>
  788. request<AppSettings>('/settings/', {
  789. method: 'PUT',
  790. body: JSON.stringify(data),
  791. }),
  792. resetSettings: () =>
  793. request<AppSettings>('/settings/reset', { method: 'POST' }),
  794. checkFfmpeg: () =>
  795. request<{ installed: boolean; path: string | null }>('/settings/check-ffmpeg'),
  796. // Cloud
  797. getCloudStatus: () => request<CloudAuthStatus>('/cloud/status'),
  798. cloudLogin: (email: string, password: string, region = 'global') =>
  799. request<CloudLoginResponse>('/cloud/login', {
  800. method: 'POST',
  801. body: JSON.stringify({ email, password, region }),
  802. }),
  803. cloudVerify: (email: string, code: string) =>
  804. request<CloudLoginResponse>('/cloud/verify', {
  805. method: 'POST',
  806. body: JSON.stringify({ email, code }),
  807. }),
  808. cloudSetToken: (access_token: string) =>
  809. request<CloudAuthStatus>('/cloud/token', {
  810. method: 'POST',
  811. body: JSON.stringify({ access_token }),
  812. }),
  813. cloudLogout: () =>
  814. request<{ success: boolean }>('/cloud/logout', { method: 'POST' }),
  815. getCloudSettings: (version = '01.09.00.00') =>
  816. request<SlicerSettingsResponse>(`/cloud/settings?version=${version}`),
  817. getCloudSettingDetail: (settingId: string) =>
  818. request<Record<string, unknown>>(`/cloud/settings/${settingId}`),
  819. getCloudDevices: () => request<CloudDevice[]>('/cloud/devices'),
  820. // Smart Plugs
  821. getSmartPlugs: () => request<SmartPlug[]>('/smart-plugs/'),
  822. getSmartPlug: (id: number) => request<SmartPlug>(`/smart-plugs/${id}`),
  823. getSmartPlugByPrinter: (printerId: number) => request<SmartPlug | null>(`/smart-plugs/by-printer/${printerId}`),
  824. createSmartPlug: (data: SmartPlugCreate) =>
  825. request<SmartPlug>('/smart-plugs/', {
  826. method: 'POST',
  827. body: JSON.stringify(data),
  828. }),
  829. updateSmartPlug: (id: number, data: SmartPlugUpdate) =>
  830. request<SmartPlug>(`/smart-plugs/${id}`, {
  831. method: 'PATCH',
  832. body: JSON.stringify(data),
  833. }),
  834. deleteSmartPlug: (id: number) =>
  835. request<void>(`/smart-plugs/${id}`, { method: 'DELETE' }),
  836. controlSmartPlug: (id: number, action: 'on' | 'off' | 'toggle') =>
  837. request<{ success: boolean; action: string }>(`/smart-plugs/${id}/control`, {
  838. method: 'POST',
  839. body: JSON.stringify({ action }),
  840. }),
  841. getSmartPlugStatus: (id: number) =>
  842. request<SmartPlugStatus>(`/smart-plugs/${id}/status`),
  843. testSmartPlugConnection: (ip_address: string, username?: string | null, password?: string | null) =>
  844. request<SmartPlugTestResult>('/smart-plugs/test-connection', {
  845. method: 'POST',
  846. body: JSON.stringify({ ip_address, username, password }),
  847. }),
  848. // Print Queue
  849. getQueue: (printerId?: number, status?: string) => {
  850. const params = new URLSearchParams();
  851. if (printerId) params.set('printer_id', String(printerId));
  852. if (status) params.set('status', status);
  853. return request<PrintQueueItem[]>(`/queue/?${params}`);
  854. },
  855. getQueueItem: (id: number) => request<PrintQueueItem>(`/queue/${id}`),
  856. addToQueue: (data: PrintQueueItemCreate) =>
  857. request<PrintQueueItem>('/queue/', {
  858. method: 'POST',
  859. body: JSON.stringify(data),
  860. }),
  861. updateQueueItem: (id: number, data: PrintQueueItemUpdate) =>
  862. request<PrintQueueItem>(`/queue/${id}`, {
  863. method: 'PATCH',
  864. body: JSON.stringify(data),
  865. }),
  866. removeFromQueue: (id: number) =>
  867. request<{ message: string }>(`/queue/${id}`, { method: 'DELETE' }),
  868. reorderQueue: (items: { id: number; position: number }[]) =>
  869. request<{ message: string }>('/queue/reorder', {
  870. method: 'POST',
  871. body: JSON.stringify({ items }),
  872. }),
  873. cancelQueueItem: (id: number) =>
  874. request<{ message: string }>(`/queue/${id}/cancel`, { method: 'POST' }),
  875. stopQueueItem: (id: number) =>
  876. request<{ message: string }>(`/queue/${id}/stop`, { method: 'POST' }),
  877. // K-Profiles
  878. getKProfiles: (printerId: number, nozzleDiameter = '0.4') =>
  879. request<KProfilesResponse>(`/printers/${printerId}/kprofiles/?nozzle_diameter=${nozzleDiameter}`),
  880. setKProfile: (printerId: number, profile: KProfileCreate) =>
  881. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
  882. method: 'POST',
  883. body: JSON.stringify(profile),
  884. }),
  885. deleteKProfile: (printerId: number, profile: KProfileDelete) =>
  886. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
  887. method: 'DELETE',
  888. body: JSON.stringify(profile),
  889. }),
  890. // Notification Providers
  891. getNotificationProviders: () => request<NotificationProvider[]>('/notifications/'),
  892. getNotificationProvider: (id: number) => request<NotificationProvider>(`/notifications/${id}`),
  893. createNotificationProvider: (data: NotificationProviderCreate) =>
  894. request<NotificationProvider>('/notifications/', {
  895. method: 'POST',
  896. body: JSON.stringify(data),
  897. }),
  898. updateNotificationProvider: (id: number, data: NotificationProviderUpdate) =>
  899. request<NotificationProvider>(`/notifications/${id}`, {
  900. method: 'PATCH',
  901. body: JSON.stringify(data),
  902. }),
  903. deleteNotificationProvider: (id: number) =>
  904. request<{ message: string }>(`/notifications/${id}`, { method: 'DELETE' }),
  905. testNotificationProvider: (id: number) =>
  906. request<NotificationTestResponse>(`/notifications/${id}/test`, { method: 'POST' }),
  907. testNotificationConfig: (data: NotificationTestRequest) =>
  908. request<NotificationTestResponse>('/notifications/test-config', {
  909. method: 'POST',
  910. body: JSON.stringify(data),
  911. }),
  912. // Spoolman Integration
  913. getSpoolmanStatus: () => request<SpoolmanStatus>('/spoolman/status'),
  914. connectSpoolman: () =>
  915. request<{ success: boolean; message: string }>('/spoolman/connect', {
  916. method: 'POST',
  917. }),
  918. disconnectSpoolman: () =>
  919. request<{ success: boolean; message: string }>('/spoolman/disconnect', {
  920. method: 'POST',
  921. }),
  922. syncPrinterAms: (printerId: number) =>
  923. request<SpoolmanSyncResult>(`/spoolman/sync/${printerId}`, {
  924. method: 'POST',
  925. }),
  926. syncAllPrintersAms: () =>
  927. request<SpoolmanSyncResult>('/spoolman/sync-all', {
  928. method: 'POST',
  929. }),
  930. getSpoolmanSpools: () =>
  931. request<{ spools: unknown[] }>('/spoolman/spools'),
  932. getSpoolmanFilaments: () =>
  933. request<{ filaments: unknown[] }>('/spoolman/filaments'),
  934. // Updates
  935. getVersion: () => request<VersionInfo>('/updates/version'),
  936. checkForUpdates: () => request<UpdateCheckResult>('/updates/check'),
  937. applyUpdate: () =>
  938. request<{ success: boolean; message: string; status: UpdateStatus }>('/updates/apply', {
  939. method: 'POST',
  940. }),
  941. getUpdateStatus: () => request<UpdateStatus>('/updates/status'),
  942. // Maintenance
  943. getMaintenanceTypes: () => request<MaintenanceType[]>('/maintenance/types'),
  944. createMaintenanceType: (data: MaintenanceTypeCreate) =>
  945. request<MaintenanceType>('/maintenance/types', {
  946. method: 'POST',
  947. body: JSON.stringify(data),
  948. }),
  949. updateMaintenanceType: (id: number, data: Partial<MaintenanceTypeCreate>) =>
  950. request<MaintenanceType>(`/maintenance/types/${id}`, {
  951. method: 'PATCH',
  952. body: JSON.stringify(data),
  953. }),
  954. deleteMaintenanceType: (id: number) =>
  955. request<{ status: string }>(`/maintenance/types/${id}`, { method: 'DELETE' }),
  956. getMaintenanceOverview: () => request<PrinterMaintenanceOverview[]>('/maintenance/overview'),
  957. getPrinterMaintenance: (printerId: number) =>
  958. request<PrinterMaintenanceOverview>(`/maintenance/printers/${printerId}`),
  959. updateMaintenanceItem: (itemId: number, data: { custom_interval_hours?: number | null; enabled?: boolean }) =>
  960. request<MaintenanceStatus>(`/maintenance/items/${itemId}`, {
  961. method: 'PATCH',
  962. body: JSON.stringify(data),
  963. }),
  964. performMaintenance: (itemId: number, notes?: string) =>
  965. request<MaintenanceStatus>(`/maintenance/items/${itemId}/perform`, {
  966. method: 'POST',
  967. body: JSON.stringify({ notes }),
  968. }),
  969. getMaintenanceHistory: (itemId: number) =>
  970. request<MaintenanceHistory[]>(`/maintenance/items/${itemId}/history`),
  971. getMaintenanceSummary: () => request<MaintenanceSummary>('/maintenance/summary'),
  972. setPrinterHours: (printerId: number, totalHours: number) =>
  973. request<{ printer_id: number; total_hours: number; archive_hours: number; offset_hours: number }>(
  974. `/maintenance/printers/${printerId}/hours?total_hours=${totalHours}`,
  975. { method: 'PATCH' }
  976. ),
  977. };