client.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  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. is_active: boolean;
  28. auto_archive: boolean;
  29. created_at: string;
  30. updated_at: string;
  31. }
  32. export interface HMSError {
  33. code: string;
  34. module: number;
  35. severity: number; // 1=fatal, 2=serious, 3=common, 4=info
  36. }
  37. export interface PrinterStatus {
  38. id: number;
  39. name: string;
  40. connected: boolean;
  41. state: string | null;
  42. current_print: string | null;
  43. subtask_name: string | null;
  44. gcode_file: string | null;
  45. progress: number | null;
  46. remaining_time: number | null;
  47. layer_num: number | null;
  48. total_layers: number | null;
  49. temperatures: {
  50. bed?: number;
  51. bed_target?: number;
  52. nozzle?: number;
  53. nozzle_target?: number;
  54. chamber?: number;
  55. } | null;
  56. cover_url: string | null;
  57. hms_errors: HMSError[];
  58. }
  59. export interface PrinterCreate {
  60. name: string;
  61. serial_number: string;
  62. ip_address: string;
  63. access_code: string;
  64. model?: string;
  65. auto_archive?: boolean;
  66. }
  67. // Archive types
  68. export interface ArchiveDuplicate {
  69. id: number;
  70. print_name: string | null;
  71. created_at: string;
  72. match_type: 'exact' | 'similar'; // 'exact' = hash match, 'similar' = name match
  73. }
  74. export interface Archive {
  75. id: number;
  76. printer_id: number | null;
  77. filename: string;
  78. file_path: string;
  79. file_size: number;
  80. content_hash: string | null;
  81. thumbnail_path: string | null;
  82. timelapse_path: string | null;
  83. duplicates: ArchiveDuplicate[] | null;
  84. duplicate_count: number;
  85. print_name: string | null;
  86. print_time_seconds: number | null;
  87. actual_time_seconds: number | null; // Computed from started_at/completed_at
  88. time_accuracy: number | null; // Percentage: 100 = perfect, >100 = faster than estimated
  89. filament_used_grams: number | null;
  90. filament_type: string | null;
  91. filament_color: string | null;
  92. layer_height: number | null;
  93. total_layers: number | null;
  94. nozzle_diameter: number | null;
  95. bed_temperature: number | null;
  96. nozzle_temperature: number | null;
  97. status: string;
  98. started_at: string | null;
  99. completed_at: string | null;
  100. extra_data: Record<string, unknown> | null;
  101. makerworld_url: string | null;
  102. designer: string | null;
  103. is_favorite: boolean;
  104. tags: string | null;
  105. notes: string | null;
  106. cost: number | null;
  107. photos: string[] | null;
  108. failure_reason: string | null;
  109. energy_kwh: number | null;
  110. energy_cost: number | null;
  111. created_at: string;
  112. }
  113. export interface ArchiveStats {
  114. total_prints: number;
  115. successful_prints: number;
  116. failed_prints: number;
  117. total_print_time_hours: number;
  118. total_filament_grams: number;
  119. total_cost: number;
  120. prints_by_filament_type: Record<string, number>;
  121. prints_by_printer: Record<string, number>;
  122. average_time_accuracy: number | null;
  123. time_accuracy_by_printer: Record<string, number> | null;
  124. total_energy_kwh: number;
  125. total_energy_cost: number;
  126. }
  127. export interface BulkUploadResult {
  128. uploaded: number;
  129. failed: number;
  130. results: Array<{ filename: string; id: number; status: string }>;
  131. errors: Array<{ filename: string; error: string }>;
  132. }
  133. // Settings types
  134. export interface AppSettings {
  135. auto_archive: boolean;
  136. save_thumbnails: boolean;
  137. capture_finish_photo: boolean;
  138. default_filament_cost: number;
  139. currency: string;
  140. energy_cost_per_kwh: number;
  141. }
  142. export type AppSettingsUpdate = Partial<AppSettings>;
  143. // Cloud types
  144. export interface CloudAuthStatus {
  145. is_authenticated: boolean;
  146. email: string | null;
  147. }
  148. export interface CloudLoginResponse {
  149. success: boolean;
  150. needs_verification: boolean;
  151. message: string;
  152. }
  153. export interface SlicerSetting {
  154. setting_id: string;
  155. name: string;
  156. type: string;
  157. version: string | null;
  158. user_id: string | null;
  159. updated_time: string | null;
  160. }
  161. export interface SlicerSettingsResponse {
  162. filament: SlicerSetting[];
  163. printer: SlicerSetting[];
  164. process: SlicerSetting[];
  165. }
  166. export interface CloudDevice {
  167. dev_id: string;
  168. name: string;
  169. dev_model_name: string | null;
  170. dev_product_name: string | null;
  171. online: boolean;
  172. }
  173. // Smart Plug types
  174. export interface SmartPlug {
  175. id: number;
  176. name: string;
  177. ip_address: string;
  178. printer_id: number | null;
  179. enabled: boolean;
  180. auto_on: boolean;
  181. auto_off: boolean;
  182. off_delay_mode: 'time' | 'temperature';
  183. off_delay_minutes: number;
  184. off_temp_threshold: number;
  185. username: string | null;
  186. password: string | null;
  187. last_state: string | null;
  188. last_checked: string | null;
  189. created_at: string;
  190. updated_at: string;
  191. }
  192. export interface SmartPlugCreate {
  193. name: string;
  194. ip_address: string;
  195. printer_id?: number | null;
  196. enabled?: boolean;
  197. auto_on?: boolean;
  198. auto_off?: boolean;
  199. off_delay_mode?: 'time' | 'temperature';
  200. off_delay_minutes?: number;
  201. off_temp_threshold?: number;
  202. username?: string | null;
  203. password?: string | null;
  204. }
  205. export interface SmartPlugUpdate {
  206. name?: string;
  207. ip_address?: string;
  208. printer_id?: number | null;
  209. enabled?: boolean;
  210. auto_on?: boolean;
  211. auto_off?: boolean;
  212. off_delay_mode?: 'time' | 'temperature';
  213. off_delay_minutes?: number;
  214. off_temp_threshold?: number;
  215. username?: string | null;
  216. password?: string | null;
  217. }
  218. export interface SmartPlugEnergy {
  219. power: number | null; // Current watts
  220. voltage: number | null; // Volts
  221. current: number | null; // Amps
  222. today: number | null; // kWh used today
  223. yesterday: number | null; // kWh used yesterday
  224. total: number | null; // Total kWh
  225. factor: number | null; // Power factor (0-1)
  226. apparent_power: number | null; // VA
  227. reactive_power: number | null; // VAr
  228. }
  229. export interface SmartPlugStatus {
  230. state: string | null;
  231. reachable: boolean;
  232. device_name: string | null;
  233. energy: SmartPlugEnergy | null;
  234. }
  235. export interface SmartPlugTestResult {
  236. success: boolean;
  237. state: string | null;
  238. device_name: string | null;
  239. }
  240. // Print Queue types
  241. export interface PrintQueueItem {
  242. id: number;
  243. printer_id: number;
  244. archive_id: number;
  245. position: number;
  246. scheduled_time: string | null;
  247. require_previous_success: boolean;
  248. auto_off_after: boolean;
  249. status: 'pending' | 'printing' | 'completed' | 'failed' | 'skipped' | 'cancelled';
  250. started_at: string | null;
  251. completed_at: string | null;
  252. error_message: string | null;
  253. created_at: string;
  254. archive_name?: string | null;
  255. archive_thumbnail?: string | null;
  256. printer_name?: string | null;
  257. }
  258. export interface PrintQueueItemCreate {
  259. printer_id: number;
  260. archive_id: number;
  261. scheduled_time?: string | null;
  262. require_previous_success?: boolean;
  263. auto_off_after?: boolean;
  264. }
  265. export interface PrintQueueItemUpdate {
  266. printer_id?: number;
  267. position?: number;
  268. scheduled_time?: string | null;
  269. require_previous_success?: boolean;
  270. auto_off_after?: boolean;
  271. }
  272. // MQTT Logging types
  273. export interface MQTTLogEntry {
  274. timestamp: string;
  275. topic: string;
  276. direction: 'in' | 'out';
  277. payload: Record<string, unknown>;
  278. }
  279. export interface MQTTLogsResponse {
  280. logging_enabled: boolean;
  281. logs: MQTTLogEntry[];
  282. }
  283. // API functions
  284. export const api = {
  285. // Printers
  286. getPrinters: () => request<Printer[]>('/printers/'),
  287. getPrinter: (id: number) => request<Printer>(`/printers/${id}`),
  288. createPrinter: (data: PrinterCreate) =>
  289. request<Printer>('/printers/', {
  290. method: 'POST',
  291. body: JSON.stringify(data),
  292. }),
  293. updatePrinter: (id: number, data: Partial<PrinterCreate>) =>
  294. request<Printer>(`/printers/${id}`, {
  295. method: 'PATCH',
  296. body: JSON.stringify(data),
  297. }),
  298. deletePrinter: (id: number) =>
  299. request<void>(`/printers/${id}`, { method: 'DELETE' }),
  300. getPrinterStatus: (id: number) =>
  301. request<PrinterStatus>(`/printers/${id}/status`),
  302. connectPrinter: (id: number) =>
  303. request<{ connected: boolean }>(`/printers/${id}/connect`, {
  304. method: 'POST',
  305. }),
  306. disconnectPrinter: (id: number) =>
  307. request<{ connected: boolean }>(`/printers/${id}/disconnect`, {
  308. method: 'POST',
  309. }),
  310. // MQTT Debug Logging
  311. enableMQTTLogging: (printerId: number) =>
  312. request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/enable`, {
  313. method: 'POST',
  314. }),
  315. disableMQTTLogging: (printerId: number) =>
  316. request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/disable`, {
  317. method: 'POST',
  318. }),
  319. getMQTTLogs: (printerId: number) =>
  320. request<MQTTLogsResponse>(`/printers/${printerId}/logging`),
  321. clearMQTTLogs: (printerId: number) =>
  322. request<{ status: string }>(`/printers/${printerId}/logging`, {
  323. method: 'DELETE',
  324. }),
  325. // Printer File Manager
  326. getPrinterFiles: (printerId: number, path = '/') =>
  327. request<{
  328. path: string;
  329. files: Array<{
  330. name: string;
  331. is_directory: boolean;
  332. size: number;
  333. path: string;
  334. }>;
  335. }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`),
  336. getPrinterFileDownloadUrl: (printerId: number, path: string) =>
  337. `${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`,
  338. deletePrinterFile: (printerId: number, path: string) =>
  339. request<{ status: string; path: string }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`, {
  340. method: 'DELETE',
  341. }),
  342. getPrinterStorage: (printerId: number) =>
  343. request<{ used_bytes: number | null; free_bytes: number | null }>(`/printers/${printerId}/storage`),
  344. // Archives
  345. getArchives: (printerId?: number, limit = 50, offset = 0) => {
  346. const params = new URLSearchParams();
  347. if (printerId) params.set('printer_id', String(printerId));
  348. params.set('limit', String(limit));
  349. params.set('offset', String(offset));
  350. return request<Archive[]>(`/archives/?${params}`);
  351. },
  352. getArchive: (id: number) => request<Archive>(`/archives/${id}`),
  353. updateArchive: (id: number, data: {
  354. printer_id?: number | null;
  355. print_name?: string;
  356. is_favorite?: boolean;
  357. tags?: string;
  358. notes?: string;
  359. cost?: number;
  360. failure_reason?: string | null;
  361. }) =>
  362. request<Archive>(`/archives/${id}`, {
  363. method: 'PATCH',
  364. body: JSON.stringify(data),
  365. }),
  366. toggleFavorite: (id: number) =>
  367. request<Archive>(`/archives/${id}/favorite`, { method: 'POST' }),
  368. deleteArchive: (id: number) =>
  369. request<void>(`/archives/${id}`, { method: 'DELETE' }),
  370. getArchiveStats: () => request<ArchiveStats>('/archives/stats'),
  371. getArchiveDuplicates: (id: number) =>
  372. request<{ duplicates: ArchiveDuplicate[]; count: number }>(`/archives/${id}/duplicates`),
  373. backfillContentHashes: () =>
  374. request<{ updated: number; errors: Array<{ id: number; error: string }> }>('/archives/backfill-hashes', {
  375. method: 'POST',
  376. }),
  377. getArchiveThumbnail: (id: number) => `${API_BASE}/archives/${id}/thumbnail`,
  378. getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`,
  379. getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`,
  380. getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse`,
  381. scanArchiveTimelapse: (id: number) =>
  382. request<{ status: string; message: string; filename?: string }>(`/archives/${id}/timelapse/scan`, {
  383. method: 'POST',
  384. }),
  385. uploadArchiveTimelapse: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
  386. const formData = new FormData();
  387. formData.append('file', file);
  388. const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/upload`, {
  389. method: 'POST',
  390. body: formData,
  391. });
  392. if (!response.ok) {
  393. const error = await response.json().catch(() => ({}));
  394. throw new Error(error.detail || `HTTP ${response.status}`);
  395. }
  396. return response.json();
  397. },
  398. // Photos
  399. getArchivePhotoUrl: (archiveId: number, filename: string) =>
  400. `${API_BASE}/archives/${archiveId}/photos/${encodeURIComponent(filename)}`,
  401. uploadArchivePhoto: async (archiveId: number, file: File): Promise<{ status: string; filename: string; photos: string[] }> => {
  402. const formData = new FormData();
  403. formData.append('file', file);
  404. const response = await fetch(`${API_BASE}/archives/${archiveId}/photos`, {
  405. method: 'POST',
  406. body: formData,
  407. });
  408. if (!response.ok) {
  409. const error = await response.json().catch(() => ({}));
  410. throw new Error(error.detail || `HTTP ${response.status}`);
  411. }
  412. return response.json();
  413. },
  414. deleteArchivePhoto: (archiveId: number, filename: string) =>
  415. request<{ status: string; photos: string[] | null }>(`/archives/${archiveId}/photos/${encodeURIComponent(filename)}`, {
  416. method: 'DELETE',
  417. }),
  418. // QR Code
  419. getArchiveQRCodeUrl: (archiveId: number, size = 200) =>
  420. `${API_BASE}/archives/${archiveId}/qrcode?size=${size}`,
  421. getArchiveCapabilities: (id: number) =>
  422. request<{
  423. has_model: boolean;
  424. has_gcode: boolean;
  425. build_volume: { x: number; y: number; z: number };
  426. }>(`/archives/${id}/capabilities`),
  427. // Project Page
  428. getArchiveProjectPage: (id: number) =>
  429. request<{
  430. title: string | null;
  431. description: string | null;
  432. designer: string | null;
  433. designer_user_id: string | null;
  434. license: string | null;
  435. copyright: string | null;
  436. creation_date: string | null;
  437. modification_date: string | null;
  438. origin: string | null;
  439. profile_title: string | null;
  440. profile_description: string | null;
  441. profile_cover: string | null;
  442. profile_user_id: string | null;
  443. profile_user_name: string | null;
  444. design_model_id: string | null;
  445. design_profile_id: string | null;
  446. design_region: string | null;
  447. model_pictures: Array<{ name: string; path: string; url: string }>;
  448. profile_pictures: Array<{ name: string; path: string; url: string }>;
  449. thumbnails: Array<{ name: string; path: string; url: string }>;
  450. }>(`/archives/${id}/project-page`),
  451. updateArchiveProjectPage: (id: number, data: {
  452. title?: string;
  453. description?: string;
  454. designer?: string;
  455. license?: string;
  456. copyright?: string;
  457. profile_title?: string;
  458. profile_description?: string;
  459. }) =>
  460. request(`/archives/${id}/project-page`, {
  461. method: 'PATCH',
  462. body: JSON.stringify(data),
  463. }),
  464. getArchiveProjectImageUrl: (archiveId: number, imagePath: string) =>
  465. `${API_BASE}/archives/${archiveId}/project-image/${encodeURIComponent(imagePath)}`,
  466. getArchiveForSlicer: (id: number, filename: string) =>
  467. `${API_BASE}/archives/${id}/file/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
  468. reprintArchive: (archiveId: number, printerId: number) =>
  469. request<{ status: string; printer_id: number; archive_id: number; filename: string }>(
  470. `/archives/${archiveId}/reprint?printer_id=${printerId}`,
  471. { method: 'POST' }
  472. ),
  473. uploadArchive: async (file: File, printerId?: number): Promise<Archive> => {
  474. const formData = new FormData();
  475. formData.append('file', file);
  476. const url = printerId
  477. ? `${API_BASE}/archives/upload?printer_id=${printerId}`
  478. : `${API_BASE}/archives/upload`;
  479. const response = await fetch(url, {
  480. method: 'POST',
  481. body: formData,
  482. });
  483. if (!response.ok) {
  484. const error = await response.json().catch(() => ({}));
  485. throw new Error(error.detail || `HTTP ${response.status}`);
  486. }
  487. return response.json();
  488. },
  489. uploadArchivesBulk: async (files: File[], printerId?: number): Promise<BulkUploadResult> => {
  490. const formData = new FormData();
  491. files.forEach((file) => formData.append('files', file));
  492. const url = printerId
  493. ? `${API_BASE}/archives/upload-bulk?printer_id=${printerId}`
  494. : `${API_BASE}/archives/upload-bulk`;
  495. const response = await fetch(url, {
  496. method: 'POST',
  497. body: formData,
  498. });
  499. if (!response.ok) {
  500. const error = await response.json().catch(() => ({}));
  501. throw new Error(error.detail || `HTTP ${response.status}`);
  502. }
  503. return response.json();
  504. },
  505. // Settings
  506. getSettings: () => request<AppSettings>('/settings/'),
  507. updateSettings: (data: AppSettingsUpdate) =>
  508. request<AppSettings>('/settings/', {
  509. method: 'PUT',
  510. body: JSON.stringify(data),
  511. }),
  512. resetSettings: () =>
  513. request<AppSettings>('/settings/reset', { method: 'POST' }),
  514. checkFfmpeg: () =>
  515. request<{ installed: boolean; path: string | null }>('/settings/check-ffmpeg'),
  516. // Cloud
  517. getCloudStatus: () => request<CloudAuthStatus>('/cloud/status'),
  518. cloudLogin: (email: string, password: string, region = 'global') =>
  519. request<CloudLoginResponse>('/cloud/login', {
  520. method: 'POST',
  521. body: JSON.stringify({ email, password, region }),
  522. }),
  523. cloudVerify: (email: string, code: string) =>
  524. request<CloudLoginResponse>('/cloud/verify', {
  525. method: 'POST',
  526. body: JSON.stringify({ email, code }),
  527. }),
  528. cloudSetToken: (access_token: string) =>
  529. request<CloudAuthStatus>('/cloud/token', {
  530. method: 'POST',
  531. body: JSON.stringify({ access_token }),
  532. }),
  533. cloudLogout: () =>
  534. request<{ success: boolean }>('/cloud/logout', { method: 'POST' }),
  535. getCloudSettings: (version = '01.09.00.00') =>
  536. request<SlicerSettingsResponse>(`/cloud/settings?version=${version}`),
  537. getCloudSettingDetail: (settingId: string) =>
  538. request<Record<string, unknown>>(`/cloud/settings/${settingId}`),
  539. getCloudDevices: () => request<CloudDevice[]>('/cloud/devices'),
  540. // Smart Plugs
  541. getSmartPlugs: () => request<SmartPlug[]>('/smart-plugs/'),
  542. getSmartPlug: (id: number) => request<SmartPlug>(`/smart-plugs/${id}`),
  543. getSmartPlugByPrinter: (printerId: number) => request<SmartPlug | null>(`/smart-plugs/by-printer/${printerId}`),
  544. createSmartPlug: (data: SmartPlugCreate) =>
  545. request<SmartPlug>('/smart-plugs/', {
  546. method: 'POST',
  547. body: JSON.stringify(data),
  548. }),
  549. updateSmartPlug: (id: number, data: SmartPlugUpdate) =>
  550. request<SmartPlug>(`/smart-plugs/${id}`, {
  551. method: 'PATCH',
  552. body: JSON.stringify(data),
  553. }),
  554. deleteSmartPlug: (id: number) =>
  555. request<void>(`/smart-plugs/${id}`, { method: 'DELETE' }),
  556. controlSmartPlug: (id: number, action: 'on' | 'off' | 'toggle') =>
  557. request<{ success: boolean; action: string }>(`/smart-plugs/${id}/control`, {
  558. method: 'POST',
  559. body: JSON.stringify({ action }),
  560. }),
  561. getSmartPlugStatus: (id: number) =>
  562. request<SmartPlugStatus>(`/smart-plugs/${id}/status`),
  563. testSmartPlugConnection: (ip_address: string, username?: string | null, password?: string | null) =>
  564. request<SmartPlugTestResult>('/smart-plugs/test-connection', {
  565. method: 'POST',
  566. body: JSON.stringify({ ip_address, username, password }),
  567. }),
  568. // Print Queue
  569. getQueue: (printerId?: number, status?: string) => {
  570. const params = new URLSearchParams();
  571. if (printerId) params.set('printer_id', String(printerId));
  572. if (status) params.set('status', status);
  573. return request<PrintQueueItem[]>(`/queue/?${params}`);
  574. },
  575. getQueueItem: (id: number) => request<PrintQueueItem>(`/queue/${id}`),
  576. addToQueue: (data: PrintQueueItemCreate) =>
  577. request<PrintQueueItem>('/queue/', {
  578. method: 'POST',
  579. body: JSON.stringify(data),
  580. }),
  581. updateQueueItem: (id: number, data: PrintQueueItemUpdate) =>
  582. request<PrintQueueItem>(`/queue/${id}`, {
  583. method: 'PATCH',
  584. body: JSON.stringify(data),
  585. }),
  586. removeFromQueue: (id: number) =>
  587. request<{ message: string }>(`/queue/${id}`, { method: 'DELETE' }),
  588. reorderQueue: (items: { id: number; position: number }[]) =>
  589. request<{ message: string }>('/queue/reorder', {
  590. method: 'POST',
  591. body: JSON.stringify({ items }),
  592. }),
  593. cancelQueueItem: (id: number) =>
  594. request<{ message: string }>(`/queue/${id}/cancel`, { method: 'POST' }),
  595. stopQueueItem: (id: number) =>
  596. request<{ message: string }>(`/queue/${id}/stop`, { method: 'POST' }),
  597. };