client.ts 69 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205
  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. location: string | null; // Group/location name
  28. nozzle_count: number; // 1 or 2, auto-detected from MQTT
  29. is_active: boolean;
  30. auto_archive: boolean;
  31. created_at: string;
  32. updated_at: string;
  33. }
  34. export interface HMSError {
  35. code: string;
  36. attr: number; // Attribute value for constructing wiki URL
  37. module: number;
  38. severity: number; // 1=fatal, 2=serious, 3=common, 4=info
  39. }
  40. export interface AMSTray {
  41. id: number;
  42. tray_color: string | null;
  43. tray_type: string | null;
  44. tray_sub_brands: string | null; // Full name like "PLA Basic", "PETG HF"
  45. tray_id_name: string | null; // Bambu filament ID like "A00-Y2" (can decode to color)
  46. tray_info_idx: string | null; // Filament preset ID like "GFA00" - maps to cloud setting_id
  47. remain: number;
  48. k: number | null; // Pressure advance value
  49. tag_uid: string | null; // RFID tag UID (any tag)
  50. tray_uuid: string | null; // Bambu Lab spool UUID (32-char hex, only valid for Bambu Lab spools)
  51. nozzle_temp_min: number | null; // Min nozzle temperature
  52. nozzle_temp_max: number | null; // Max nozzle temperature
  53. }
  54. export interface AMSUnit {
  55. id: number;
  56. humidity: number | null;
  57. temp: number | null;
  58. is_ams_ht: boolean; // True for AMS-HT (single spool), False for regular AMS (4 spools)
  59. tray: AMSTray[];
  60. }
  61. export interface NozzleInfo {
  62. nozzle_type: string; // "stainless_steel" or "hardened_steel"
  63. nozzle_diameter: string; // e.g., "0.4"
  64. }
  65. export interface PrintOptions {
  66. // Core AI detectors
  67. spaghetti_detector: boolean;
  68. print_halt: boolean;
  69. halt_print_sensitivity: string; // "low", "medium", "high" - spaghetti sensitivity
  70. first_layer_inspector: boolean;
  71. printing_monitor: boolean;
  72. buildplate_marker_detector: boolean;
  73. allow_skip_parts: boolean;
  74. // Additional AI detectors (decoded from cfg bitmask)
  75. nozzle_clumping_detector: boolean;
  76. nozzle_clumping_sensitivity: string; // "low", "medium", "high"
  77. pileup_detector: boolean;
  78. pileup_sensitivity: string; // "low", "medium", "high"
  79. airprint_detector: boolean;
  80. airprint_sensitivity: string; // "low", "medium", "high"
  81. auto_recovery_step_loss: boolean;
  82. filament_tangle_detect: boolean;
  83. }
  84. export interface PrinterStatus {
  85. id: number;
  86. name: string;
  87. connected: boolean;
  88. state: string | null;
  89. current_print: string | null;
  90. subtask_name: string | null;
  91. gcode_file: string | null;
  92. progress: number | null;
  93. remaining_time: number | null;
  94. layer_num: number | null;
  95. total_layers: number | null;
  96. temperatures: {
  97. bed?: number;
  98. bed_target?: number;
  99. bed_heating?: boolean; // Actual heater state from MQTT
  100. nozzle?: number;
  101. nozzle_target?: number;
  102. nozzle_heating?: boolean; // Actual heater state from MQTT
  103. nozzle_2?: number; // Second nozzle for H2 series (dual nozzle)
  104. nozzle_2_target?: number;
  105. nozzle_2_heating?: boolean; // Actual heater state from MQTT
  106. chamber?: number;
  107. chamber_target?: number;
  108. chamber_heating?: boolean; // Actual heater state from MQTT
  109. } | null;
  110. cover_url: string | null;
  111. hms_errors: HMSError[];
  112. ams: AMSUnit[];
  113. ams_exists: boolean;
  114. vt_tray: AMSTray | null; // Virtual tray / external spool
  115. sdcard: boolean; // SD card inserted
  116. store_to_sdcard: boolean; // Store sent files on SD card
  117. timelapse: boolean; // Timelapse recording active
  118. ipcam: boolean; // Live view enabled
  119. wifi_signal: number | null; // WiFi signal strength in dBm
  120. nozzles: NozzleInfo[]; // Nozzle hardware info (index 0=left/primary, 1=right)
  121. print_options: PrintOptions | null; // AI detection and print options
  122. // Calibration stage tracking
  123. stg_cur: number; // Current stage number (-1 = not calibrating)
  124. stg_cur_name: string | null; // Human-readable current stage name
  125. stg: number[]; // List of stage numbers in calibration sequence
  126. // Air conditioning mode (0=cooling, 1=heating)
  127. airduct_mode: number;
  128. // Print speed level (1=silent, 2=standard, 3=sport, 4=ludicrous)
  129. speed_level: number;
  130. // Chamber light on/off
  131. chamber_light: boolean;
  132. // Active extruder for dual nozzle (0=right, 1=left)
  133. active_extruder: number;
  134. // AMS mapping - which AMS is connected to which nozzle
  135. // Format: [ams_id_for_nozzle0, ams_id_for_nozzle1, ...] where -1 means no AMS
  136. ams_mapping: number[];
  137. // Per-AMS extruder mapping - extracted from each AMS unit's info field
  138. // Format: {ams_id: extruder_id} where extruder 0=right, 1=left
  139. // Note: JSON keys are always strings
  140. ams_extruder_map: Record<string, number>;
  141. // Currently loaded tray (global tray ID, 255 = no filament loaded, 254 = external spool)
  142. tray_now: number;
  143. // AMS status for filament change tracking (0=idle, 1=filament_change, 2=rfid_identifying, 3=assist, 4=calibration)
  144. ams_status_main: number;
  145. // AMS sub-status for filament change step (when main=1): 4=retraction, 6=load verification, 7=purge
  146. ams_status_sub: number;
  147. // mc_print_sub_stage - filament change step indicator used by OrcaSlicer/BambuStudio
  148. mc_print_sub_stage: number;
  149. // Timestamp of last AMS data update (for RFID refresh detection)
  150. last_ams_update: number;
  151. }
  152. export interface PrinterCreate {
  153. name: string;
  154. serial_number: string;
  155. ip_address: string;
  156. access_code: string;
  157. model?: string;
  158. location?: string;
  159. auto_archive?: boolean;
  160. }
  161. // Archive types
  162. export interface ArchiveDuplicate {
  163. id: number;
  164. print_name: string | null;
  165. created_at: string;
  166. match_type: 'exact' | 'similar'; // 'exact' = hash match, 'similar' = name match
  167. }
  168. export interface Archive {
  169. id: number;
  170. printer_id: number | null;
  171. project_id: number | null;
  172. project_name: string | null;
  173. filename: string;
  174. file_path: string;
  175. file_size: number;
  176. content_hash: string | null;
  177. thumbnail_path: string | null;
  178. timelapse_path: string | null;
  179. source_3mf_path: string | null;
  180. duplicates: ArchiveDuplicate[] | null;
  181. duplicate_count: number;
  182. print_name: string | null;
  183. print_time_seconds: number | null;
  184. actual_time_seconds: number | null; // Computed from started_at/completed_at
  185. time_accuracy: number | null; // Percentage: 100 = perfect, >100 = faster than estimated
  186. filament_used_grams: number | null;
  187. filament_type: string | null;
  188. filament_color: string | null;
  189. layer_height: number | null;
  190. total_layers: number | null;
  191. nozzle_diameter: number | null;
  192. bed_temperature: number | null;
  193. nozzle_temperature: number | null;
  194. status: string;
  195. started_at: string | null;
  196. completed_at: string | null;
  197. extra_data: Record<string, unknown> | null;
  198. makerworld_url: string | null;
  199. designer: string | null;
  200. is_favorite: boolean;
  201. tags: string | null;
  202. notes: string | null;
  203. cost: number | null;
  204. photos: string[] | null;
  205. failure_reason: string | null;
  206. energy_kwh: number | null;
  207. energy_cost: number | null;
  208. created_at: string;
  209. }
  210. export interface ArchiveStats {
  211. total_prints: number;
  212. successful_prints: number;
  213. failed_prints: number;
  214. total_print_time_hours: number;
  215. total_filament_grams: number;
  216. total_cost: number;
  217. prints_by_filament_type: Record<string, number>;
  218. prints_by_printer: Record<string, number>;
  219. average_time_accuracy: number | null;
  220. time_accuracy_by_printer: Record<string, number> | null;
  221. total_energy_kwh: number;
  222. total_energy_cost: number;
  223. }
  224. export interface FailureAnalysis {
  225. period_days: number;
  226. total_prints: number;
  227. failed_prints: number;
  228. failure_rate: number;
  229. failures_by_reason: Record<string, number>;
  230. failures_by_filament: Record<string, number>;
  231. failures_by_printer: Record<string, number>;
  232. failures_by_hour: Record<number, number>;
  233. recent_failures: Array<{
  234. id: number;
  235. print_name: string;
  236. failure_reason: string | null;
  237. filament_type: string | null;
  238. printer_id: number | null;
  239. created_at: string | null;
  240. }>;
  241. trend: Array<{
  242. week_start: string;
  243. total_prints: number;
  244. failed_prints: number;
  245. failure_rate: number;
  246. }>;
  247. }
  248. export interface BulkUploadResult {
  249. uploaded: number;
  250. failed: number;
  251. results: Array<{ filename: string; id: number; status: string }>;
  252. errors: Array<{ filename: string; error: string }>;
  253. }
  254. // Archive Comparison types
  255. export interface ComparisonArchiveInfo {
  256. id: number;
  257. print_name: string;
  258. status: string;
  259. created_at: string | null;
  260. printer_id: number | null;
  261. project_name: string | null;
  262. }
  263. export interface ComparisonField {
  264. field: string;
  265. label: string;
  266. unit: string | null;
  267. values: (string | number | null)[];
  268. raw_values: (string | number | null)[];
  269. has_difference: boolean;
  270. }
  271. export interface SuccessCorrelationInsight {
  272. field: string;
  273. label: string;
  274. insight: string;
  275. success_avg?: number;
  276. failed_avg?: number;
  277. success_values?: string[];
  278. failed_values?: string[];
  279. }
  280. export interface SuccessCorrelation {
  281. has_both_outcomes: boolean;
  282. message?: string;
  283. successful_count?: number;
  284. failed_count?: number;
  285. insights?: SuccessCorrelationInsight[];
  286. }
  287. export interface ArchiveComparison {
  288. archives: ComparisonArchiveInfo[];
  289. comparison: ComparisonField[];
  290. differences: ComparisonField[];
  291. success_correlation: SuccessCorrelation;
  292. }
  293. export interface SimilarArchive {
  294. archive: {
  295. id: number;
  296. print_name: string;
  297. status: string;
  298. created_at: string | null;
  299. };
  300. match_reason: string;
  301. match_score: number;
  302. }
  303. // Project types
  304. export interface ProjectStats {
  305. total_archives: number;
  306. completed_prints: number;
  307. failed_prints: number;
  308. queued_prints: number;
  309. in_progress_prints: number;
  310. total_print_time_hours: number;
  311. total_filament_grams: number;
  312. progress_percent: number | null;
  313. estimated_cost: number;
  314. total_energy_kwh: number;
  315. total_energy_cost: number;
  316. remaining_prints: number | null;
  317. bom_total_items: number;
  318. bom_completed_items: number;
  319. }
  320. export interface ProjectChildPreview {
  321. id: number;
  322. name: string;
  323. color: string | null;
  324. status: string;
  325. progress_percent: number | null;
  326. }
  327. export interface Project {
  328. id: number;
  329. name: string;
  330. description: string | null;
  331. color: string | null;
  332. status: string; // active, completed, archived
  333. target_count: number | null;
  334. notes: string | null;
  335. attachments: ProjectAttachment[] | null;
  336. tags: string | null;
  337. due_date: string | null;
  338. priority: string; // low, normal, high, urgent
  339. budget: number | null;
  340. is_template: boolean;
  341. template_source_id: number | null;
  342. parent_id: number | null;
  343. parent_name: string | null;
  344. children: ProjectChildPreview[];
  345. created_at: string;
  346. updated_at: string;
  347. stats?: ProjectStats;
  348. }
  349. export interface ProjectAttachment {
  350. filename: string;
  351. original_name: string;
  352. size: number;
  353. uploaded_at: string;
  354. }
  355. export interface ArchivePreview {
  356. id: number;
  357. print_name: string | null;
  358. thumbnail_path: string | null;
  359. status: string;
  360. }
  361. export interface ProjectListItem {
  362. id: number;
  363. name: string;
  364. description: string | null;
  365. color: string | null;
  366. status: string;
  367. target_count: number | null;
  368. created_at: string;
  369. archive_count: number;
  370. queue_count: number;
  371. progress_percent: number | null;
  372. archives: ArchivePreview[];
  373. }
  374. export interface ProjectCreate {
  375. name: string;
  376. description?: string;
  377. color?: string;
  378. target_count?: number;
  379. notes?: string;
  380. tags?: string;
  381. due_date?: string;
  382. priority?: string;
  383. budget?: number;
  384. parent_id?: number;
  385. }
  386. export interface ProjectUpdate {
  387. name?: string;
  388. description?: string;
  389. color?: string;
  390. status?: string;
  391. target_count?: number;
  392. notes?: string;
  393. tags?: string;
  394. due_date?: string;
  395. priority?: string;
  396. budget?: number;
  397. parent_id?: number;
  398. }
  399. // BOM Types
  400. export interface BOMItem {
  401. id: number;
  402. project_id: number;
  403. name: string;
  404. quantity_needed: number;
  405. quantity_printed: number;
  406. archive_id: number | null;
  407. archive_name: string | null;
  408. stl_filename: string | null;
  409. notes: string | null;
  410. sort_order: number;
  411. is_complete: boolean;
  412. created_at: string;
  413. updated_at: string;
  414. }
  415. export interface BOMItemCreate {
  416. name: string;
  417. quantity_needed?: number;
  418. archive_id?: number;
  419. stl_filename?: string;
  420. notes?: string;
  421. }
  422. export interface BOMItemUpdate {
  423. name?: string;
  424. quantity_needed?: number;
  425. quantity_printed?: number;
  426. archive_id?: number;
  427. stl_filename?: string;
  428. notes?: string;
  429. }
  430. // Timeline Types
  431. export interface TimelineEvent {
  432. event_type: string;
  433. timestamp: string;
  434. title: string;
  435. description: string | null;
  436. metadata: Record<string, unknown> | null;
  437. }
  438. // API Key types
  439. export interface APIKey {
  440. id: number;
  441. name: string;
  442. key_prefix: string;
  443. can_queue: boolean;
  444. can_control_printer: boolean;
  445. can_read_status: boolean;
  446. printer_ids: number[] | null;
  447. enabled: boolean;
  448. last_used: string | null;
  449. created_at: string;
  450. expires_at: string | null;
  451. }
  452. export interface APIKeyCreate {
  453. name: string;
  454. can_queue?: boolean;
  455. can_control_printer?: boolean;
  456. can_read_status?: boolean;
  457. printer_ids?: number[] | null;
  458. expires_at?: string | null;
  459. }
  460. export interface APIKeyCreateResponse extends APIKey {
  461. key: string; // Full key, only shown on creation
  462. }
  463. export interface APIKeyUpdate {
  464. name?: string;
  465. can_queue?: boolean;
  466. can_control_printer?: boolean;
  467. can_read_status?: boolean;
  468. printer_ids?: number[] | null;
  469. enabled?: boolean;
  470. expires_at?: string | null;
  471. }
  472. // Settings types
  473. export interface AppSettings {
  474. auto_archive: boolean;
  475. save_thumbnails: boolean;
  476. capture_finish_photo: boolean;
  477. default_filament_cost: number;
  478. currency: string;
  479. energy_cost_per_kwh: number;
  480. energy_tracking_mode: 'print' | 'total';
  481. check_updates: boolean;
  482. notification_language: string;
  483. // AMS threshold settings
  484. ams_humidity_good: number; // <= this is green
  485. ams_humidity_fair: number; // <= this is orange, > is red
  486. ams_temp_good: number; // <= this is green/blue
  487. ams_temp_fair: number; // <= this is orange, > is red
  488. ams_history_retention_days: number; // days to keep AMS sensor history
  489. // Date/time format settings
  490. date_format: 'system' | 'us' | 'eu' | 'iso';
  491. time_format: 'system' | '12h' | '24h';
  492. // Default printer
  493. default_printer_id: number | null;
  494. // Telemetry
  495. telemetry_enabled: boolean;
  496. }
  497. export type AppSettingsUpdate = Partial<AppSettings>;
  498. // Cloud types
  499. export interface CloudAuthStatus {
  500. is_authenticated: boolean;
  501. email: string | null;
  502. }
  503. export interface CloudLoginResponse {
  504. success: boolean;
  505. needs_verification: boolean;
  506. message: string;
  507. }
  508. export interface SlicerSetting {
  509. setting_id: string;
  510. name: string;
  511. type: string;
  512. version: string | null;
  513. user_id: string | null;
  514. updated_time: string | null;
  515. }
  516. export interface SlicerSettingsResponse {
  517. filament: SlicerSetting[];
  518. printer: SlicerSetting[];
  519. process: SlicerSetting[];
  520. }
  521. export interface SlicerSettingDetail {
  522. message?: string | null;
  523. code?: string | null;
  524. error?: string | null;
  525. public: boolean;
  526. version?: string | null;
  527. type: string;
  528. name: string;
  529. update_time?: string | null;
  530. nickname?: string | null;
  531. base_id?: string | null;
  532. setting: Record<string, unknown>;
  533. filament_id?: string | null;
  534. setting_id?: string | null;
  535. }
  536. export interface SlicerSettingCreate {
  537. type: string; // 'filament', 'print', or 'printer'
  538. name: string;
  539. base_id: string;
  540. setting: Record<string, unknown>;
  541. }
  542. export interface SlicerSettingUpdate {
  543. name?: string;
  544. setting?: Record<string, unknown>;
  545. }
  546. export interface SlicerSettingDeleteResponse {
  547. success: boolean;
  548. message: string;
  549. }
  550. export interface FieldOption {
  551. value: string;
  552. label: string;
  553. }
  554. export interface FieldDefinition {
  555. key: string;
  556. label: string;
  557. type: 'text' | 'number' | 'boolean' | 'select';
  558. category: string;
  559. description?: string;
  560. options?: FieldOption[];
  561. unit?: string;
  562. min?: number;
  563. max?: number;
  564. step?: number;
  565. }
  566. export interface FieldDefinitionsResponse {
  567. version: string;
  568. description: string;
  569. fields: FieldDefinition[];
  570. }
  571. export interface CloudDevice {
  572. dev_id: string;
  573. name: string;
  574. dev_model_name: string | null;
  575. dev_product_name: string | null;
  576. online: boolean;
  577. }
  578. // Smart Plug types
  579. export interface SmartPlug {
  580. id: number;
  581. name: string;
  582. ip_address: string;
  583. printer_id: number | null;
  584. enabled: boolean;
  585. auto_on: boolean;
  586. auto_off: boolean;
  587. off_delay_mode: 'time' | 'temperature';
  588. off_delay_minutes: number;
  589. off_temp_threshold: number;
  590. username: string | null;
  591. password: string | null;
  592. // Power alerts
  593. power_alert_enabled: boolean;
  594. power_alert_high: number | null;
  595. power_alert_low: number | null;
  596. power_alert_last_triggered: string | null;
  597. // Schedule
  598. schedule_enabled: boolean;
  599. schedule_on_time: string | null;
  600. schedule_off_time: string | null;
  601. // Status
  602. last_state: string | null;
  603. last_checked: string | null;
  604. auto_off_executed: boolean; // True when auto-off was triggered after print
  605. created_at: string;
  606. updated_at: string;
  607. }
  608. export interface SmartPlugCreate {
  609. name: string;
  610. ip_address: string;
  611. printer_id?: number | null;
  612. enabled?: boolean;
  613. auto_on?: boolean;
  614. auto_off?: boolean;
  615. off_delay_mode?: 'time' | 'temperature';
  616. off_delay_minutes?: number;
  617. off_temp_threshold?: number;
  618. username?: string | null;
  619. password?: string | null;
  620. // Power alerts
  621. power_alert_enabled?: boolean;
  622. power_alert_high?: number | null;
  623. power_alert_low?: number | null;
  624. // Schedule
  625. schedule_enabled?: boolean;
  626. schedule_on_time?: string | null;
  627. schedule_off_time?: string | null;
  628. }
  629. export interface SmartPlugUpdate {
  630. name?: string;
  631. ip_address?: string;
  632. printer_id?: number | null;
  633. enabled?: boolean;
  634. auto_on?: boolean;
  635. auto_off?: boolean;
  636. off_delay_mode?: 'time' | 'temperature';
  637. off_delay_minutes?: number;
  638. off_temp_threshold?: number;
  639. username?: string | null;
  640. password?: string | null;
  641. // Power alerts
  642. power_alert_enabled?: boolean;
  643. power_alert_high?: number | null;
  644. power_alert_low?: number | null;
  645. // Schedule
  646. schedule_enabled?: boolean;
  647. schedule_on_time?: string | null;
  648. schedule_off_time?: string | null;
  649. }
  650. export interface SmartPlugEnergy {
  651. power: number | null; // Current watts
  652. voltage: number | null; // Volts
  653. current: number | null; // Amps
  654. today: number | null; // kWh used today
  655. yesterday: number | null; // kWh used yesterday
  656. total: number | null; // Total kWh
  657. factor: number | null; // Power factor (0-1)
  658. apparent_power: number | null; // VA
  659. reactive_power: number | null; // VAr
  660. }
  661. export interface SmartPlugStatus {
  662. state: string | null;
  663. reachable: boolean;
  664. device_name: string | null;
  665. energy: SmartPlugEnergy | null;
  666. }
  667. export interface SmartPlugTestResult {
  668. success: boolean;
  669. state: string | null;
  670. device_name: string | null;
  671. }
  672. // Print Queue types
  673. export interface PrintQueueItem {
  674. id: number;
  675. printer_id: number;
  676. archive_id: number;
  677. position: number;
  678. scheduled_time: string | null;
  679. require_previous_success: boolean;
  680. auto_off_after: boolean;
  681. status: 'pending' | 'printing' | 'completed' | 'failed' | 'skipped' | 'cancelled';
  682. started_at: string | null;
  683. completed_at: string | null;
  684. error_message: string | null;
  685. created_at: string;
  686. archive_name?: string | null;
  687. archive_thumbnail?: string | null;
  688. printer_name?: string | null;
  689. print_time_seconds?: number | null; // Estimated print time from archive
  690. }
  691. export interface PrintQueueItemCreate {
  692. printer_id: number;
  693. archive_id: number;
  694. scheduled_time?: string | null;
  695. require_previous_success?: boolean;
  696. auto_off_after?: boolean;
  697. }
  698. export interface PrintQueueItemUpdate {
  699. printer_id?: number;
  700. position?: number;
  701. scheduled_time?: string | null;
  702. require_previous_success?: boolean;
  703. auto_off_after?: boolean;
  704. }
  705. // MQTT Logging types
  706. export interface MQTTLogEntry {
  707. timestamp: string;
  708. topic: string;
  709. direction: 'in' | 'out';
  710. payload: Record<string, unknown>;
  711. }
  712. export interface MQTTLogsResponse {
  713. logging_enabled: boolean;
  714. logs: MQTTLogEntry[];
  715. }
  716. // K-Profile types
  717. export interface KProfile {
  718. slot_id: number;
  719. extruder_id: number;
  720. nozzle_id: string;
  721. nozzle_diameter: string;
  722. filament_id: string;
  723. name: string;
  724. k_value: string;
  725. n_coef: string;
  726. ams_id: number;
  727. tray_id: number;
  728. setting_id: string | null;
  729. }
  730. export interface KProfileCreate {
  731. slot_id?: number; // Storage slot, 0 for new profiles
  732. extruder_id?: number;
  733. nozzle_id: string;
  734. nozzle_diameter: string;
  735. filament_id: string;
  736. name: string;
  737. k_value: string;
  738. n_coef?: string;
  739. ams_id?: number;
  740. tray_id?: number;
  741. setting_id?: string | null;
  742. }
  743. export interface KProfileDelete {
  744. slot_id: number; // cali_idx - calibration index to delete
  745. extruder_id: number;
  746. nozzle_id: string; // e.g., "HH00-0.4"
  747. nozzle_diameter: string; // e.g., "0.4"
  748. filament_id: string; // Bambu filament identifier
  749. setting_id?: string | null; // Setting ID (for X1C series)
  750. }
  751. export interface KProfilesResponse {
  752. profiles: KProfile[];
  753. nozzle_diameter: string;
  754. }
  755. export interface KProfileNote {
  756. setting_id: string;
  757. note: string;
  758. }
  759. export interface KProfileNotesResponse {
  760. notes: Record<string, string>; // setting_id -> note
  761. }
  762. // Slot Preset Mapping
  763. export interface SlotPresetMapping {
  764. ams_id: number;
  765. tray_id: number;
  766. preset_id: string;
  767. preset_name: string;
  768. }
  769. // Filament types
  770. export interface Filament {
  771. id: number;
  772. name: string;
  773. type: string; // PLA, PETG, ABS, etc.
  774. brand: string | null;
  775. color: string | null;
  776. color_hex: string | null;
  777. cost_per_kg: number;
  778. spool_weight_g: number;
  779. currency: string;
  780. density: number | null;
  781. print_temp_min: number | null;
  782. print_temp_max: number | null;
  783. bed_temp_min: number | null;
  784. bed_temp_max: number | null;
  785. created_at: string;
  786. updated_at: string;
  787. }
  788. // Notification Provider types
  789. export type ProviderType = 'callmebot' | 'ntfy' | 'pushover' | 'telegram' | 'email' | 'discord' | 'webhook';
  790. export interface NotificationProvider {
  791. id: number;
  792. name: string;
  793. provider_type: ProviderType;
  794. enabled: boolean;
  795. config: Record<string, unknown>;
  796. // Print lifecycle events
  797. on_print_start: boolean;
  798. on_print_complete: boolean;
  799. on_print_failed: boolean;
  800. on_print_stopped: boolean;
  801. on_print_progress: boolean;
  802. // Printer status events
  803. on_printer_offline: boolean;
  804. on_printer_error: boolean;
  805. on_filament_low: boolean;
  806. on_maintenance_due: boolean;
  807. // AMS environmental alarms (regular AMS)
  808. on_ams_humidity_high: boolean;
  809. on_ams_temperature_high: boolean;
  810. // AMS-HT environmental alarms
  811. on_ams_ht_humidity_high: boolean;
  812. on_ams_ht_temperature_high: boolean;
  813. // Quiet hours
  814. quiet_hours_enabled: boolean;
  815. quiet_hours_start: string | null;
  816. quiet_hours_end: string | null;
  817. // Daily digest
  818. daily_digest_enabled: boolean;
  819. daily_digest_time: string | null;
  820. // Printer filter
  821. printer_id: number | null;
  822. // Status tracking
  823. last_success: string | null;
  824. last_error: string | null;
  825. last_error_at: string | null;
  826. // Timestamps
  827. created_at: string;
  828. updated_at: string;
  829. }
  830. export interface NotificationProviderCreate {
  831. name: string;
  832. provider_type: ProviderType;
  833. enabled?: boolean;
  834. config: Record<string, unknown>;
  835. // Print lifecycle events
  836. on_print_start?: boolean;
  837. on_print_complete?: boolean;
  838. on_print_failed?: boolean;
  839. on_print_stopped?: boolean;
  840. on_print_progress?: boolean;
  841. // Printer status events
  842. on_printer_offline?: boolean;
  843. on_printer_error?: boolean;
  844. on_filament_low?: boolean;
  845. on_maintenance_due?: boolean;
  846. // AMS environmental alarms (regular AMS)
  847. on_ams_humidity_high?: boolean;
  848. on_ams_temperature_high?: boolean;
  849. // AMS-HT environmental alarms
  850. on_ams_ht_humidity_high?: boolean;
  851. on_ams_ht_temperature_high?: boolean;
  852. // Quiet hours
  853. quiet_hours_enabled?: boolean;
  854. quiet_hours_start?: string | null;
  855. quiet_hours_end?: string | null;
  856. // Daily digest
  857. daily_digest_enabled?: boolean;
  858. daily_digest_time?: string | null;
  859. // Printer filter
  860. printer_id?: number | null;
  861. }
  862. export interface NotificationProviderUpdate {
  863. name?: string;
  864. provider_type?: ProviderType;
  865. enabled?: boolean;
  866. config?: Record<string, unknown>;
  867. // Print lifecycle events
  868. on_print_start?: boolean;
  869. on_print_complete?: boolean;
  870. on_print_failed?: boolean;
  871. on_print_stopped?: boolean;
  872. on_print_progress?: boolean;
  873. // Printer status events
  874. on_printer_offline?: boolean;
  875. on_printer_error?: boolean;
  876. on_filament_low?: boolean;
  877. on_maintenance_due?: boolean;
  878. // AMS environmental alarms (regular AMS)
  879. on_ams_humidity_high?: boolean;
  880. on_ams_temperature_high?: boolean;
  881. // AMS-HT environmental alarms
  882. on_ams_ht_humidity_high?: boolean;
  883. on_ams_ht_temperature_high?: boolean;
  884. // Quiet hours
  885. quiet_hours_enabled?: boolean;
  886. quiet_hours_start?: string | null;
  887. quiet_hours_end?: string | null;
  888. // Daily digest
  889. daily_digest_enabled?: boolean;
  890. daily_digest_time?: string | null;
  891. // Printer filter
  892. printer_id?: number | null;
  893. }
  894. export interface NotificationTestRequest {
  895. provider_type: ProviderType;
  896. config: Record<string, unknown>;
  897. }
  898. export interface NotificationTestResponse {
  899. success: boolean;
  900. message: string;
  901. }
  902. // Provider-specific config types for reference
  903. export interface CallMeBotConfig {
  904. phone: string;
  905. apikey: string;
  906. }
  907. export interface NtfyConfig {
  908. server?: string;
  909. topic: string;
  910. auth_token?: string | null;
  911. }
  912. export interface PushoverConfig {
  913. user_key: string;
  914. app_token: string;
  915. priority?: number;
  916. }
  917. export interface TelegramConfig {
  918. bot_token: string;
  919. chat_id: string;
  920. }
  921. export interface EmailConfig {
  922. smtp_server: string;
  923. smtp_port?: number;
  924. username: string;
  925. password: string;
  926. from_email: string;
  927. to_email: string;
  928. use_tls?: boolean;
  929. }
  930. // Notification Template types
  931. export interface NotificationTemplate {
  932. id: number;
  933. event_type: string;
  934. name: string;
  935. title_template: string;
  936. body_template: string;
  937. is_default: boolean;
  938. created_at: string;
  939. updated_at: string;
  940. }
  941. export interface NotificationTemplateUpdate {
  942. title_template?: string;
  943. body_template?: string;
  944. }
  945. export interface EventVariablesResponse {
  946. event_type: string;
  947. event_name: string;
  948. variables: string[];
  949. }
  950. export interface TemplatePreviewRequest {
  951. event_type: string;
  952. title_template: string;
  953. body_template: string;
  954. }
  955. export interface TemplatePreviewResponse {
  956. title: string;
  957. body: string;
  958. }
  959. // Notification Log types
  960. export interface NotificationLogEntry {
  961. id: number;
  962. provider_id: number;
  963. provider_name: string | null;
  964. provider_type: string | null;
  965. event_type: string;
  966. title: string;
  967. message: string;
  968. success: boolean;
  969. error_message: string | null;
  970. printer_id: number | null;
  971. printer_name: string | null;
  972. created_at: string;
  973. }
  974. export interface NotificationLogStats {
  975. total: number;
  976. success_count: number;
  977. failure_count: number;
  978. by_event_type: Record<string, number>;
  979. by_provider: Record<string, number>;
  980. }
  981. // Spoolman types
  982. export interface SpoolmanStatus {
  983. enabled: boolean;
  984. connected: boolean;
  985. url: string | null;
  986. }
  987. export interface SpoolmanSyncResult {
  988. success: boolean;
  989. synced_count: number;
  990. errors: string[];
  991. }
  992. // Update types
  993. export interface VersionInfo {
  994. version: string;
  995. repo: string;
  996. }
  997. export interface UpdateCheckResult {
  998. update_available: boolean;
  999. current_version: string;
  1000. latest_version: string | null;
  1001. release_name?: string;
  1002. release_notes?: string;
  1003. release_url?: string;
  1004. published_at?: string;
  1005. error?: string;
  1006. message?: string;
  1007. }
  1008. export interface UpdateStatus {
  1009. status: 'idle' | 'checking' | 'downloading' | 'installing' | 'complete' | 'error';
  1010. progress: number;
  1011. message: string;
  1012. error: string | null;
  1013. }
  1014. // Maintenance types
  1015. export interface MaintenanceType {
  1016. id: number;
  1017. name: string;
  1018. description: string | null;
  1019. default_interval_hours: number;
  1020. interval_type: 'hours' | 'days'; // "hours" = print hours, "days" = calendar days
  1021. icon: string | null;
  1022. is_system: boolean;
  1023. created_at: string;
  1024. }
  1025. export interface MaintenanceTypeCreate {
  1026. name: string;
  1027. description?: string | null;
  1028. default_interval_hours?: number;
  1029. interval_type?: 'hours' | 'days';
  1030. icon?: string | null;
  1031. }
  1032. export interface MaintenanceStatus {
  1033. id: number;
  1034. printer_id: number;
  1035. printer_name: string;
  1036. maintenance_type_id: number;
  1037. maintenance_type_name: string;
  1038. maintenance_type_icon: string | null;
  1039. enabled: boolean;
  1040. interval_hours: number; // For hours type: print hours; for days type: number of days
  1041. interval_type: 'hours' | 'days';
  1042. current_hours: number;
  1043. hours_since_maintenance: number;
  1044. hours_until_due: number;
  1045. days_since_maintenance: number | null; // For days type
  1046. days_until_due: number | null; // For days type
  1047. is_due: boolean;
  1048. is_warning: boolean;
  1049. last_performed_at: string | null;
  1050. }
  1051. export interface PrinterMaintenanceOverview {
  1052. printer_id: number;
  1053. printer_name: string;
  1054. total_print_hours: number;
  1055. maintenance_items: MaintenanceStatus[];
  1056. due_count: number;
  1057. warning_count: number;
  1058. }
  1059. export interface MaintenanceHistory {
  1060. id: number;
  1061. printer_maintenance_id: number;
  1062. performed_at: string;
  1063. hours_at_maintenance: number;
  1064. notes: string | null;
  1065. }
  1066. export interface MaintenanceSummary {
  1067. total_due: number;
  1068. total_warning: number;
  1069. printers_with_issues: Array<{
  1070. printer_id: number;
  1071. printer_name: string;
  1072. due_count: number;
  1073. warning_count: number;
  1074. }>;
  1075. }
  1076. // External Links (sidebar)
  1077. export interface ExternalLink {
  1078. id: number;
  1079. name: string;
  1080. url: string;
  1081. icon: string;
  1082. custom_icon: string | null;
  1083. sort_order: number;
  1084. created_at: string;
  1085. updated_at: string;
  1086. }
  1087. export interface ExternalLinkCreate {
  1088. name: string;
  1089. url: string;
  1090. icon: string;
  1091. }
  1092. export interface ExternalLinkUpdate {
  1093. name?: string;
  1094. url?: string;
  1095. icon?: string;
  1096. }
  1097. // API functions
  1098. export const api = {
  1099. // Printers
  1100. getPrinters: () => request<Printer[]>('/printers/'),
  1101. getPrinter: (id: number) => request<Printer>(`/printers/${id}`),
  1102. createPrinter: (data: PrinterCreate) =>
  1103. request<Printer>('/printers/', {
  1104. method: 'POST',
  1105. body: JSON.stringify(data),
  1106. }),
  1107. updatePrinter: (id: number, data: Partial<PrinterCreate>) =>
  1108. request<Printer>(`/printers/${id}`, {
  1109. method: 'PATCH',
  1110. body: JSON.stringify(data),
  1111. }),
  1112. deletePrinter: (id: number, deleteArchives: boolean = true) =>
  1113. request<{ status: string; archives_deleted: boolean }>(
  1114. `/printers/${id}?delete_archives=${deleteArchives}`,
  1115. { method: 'DELETE' }
  1116. ),
  1117. getPrinterStatus: (id: number) =>
  1118. request<PrinterStatus>(`/printers/${id}/status`),
  1119. connectPrinter: (id: number) =>
  1120. request<{ connected: boolean }>(`/printers/${id}/connect`, {
  1121. method: 'POST',
  1122. }),
  1123. disconnectPrinter: (id: number) =>
  1124. request<{ connected: boolean }>(`/printers/${id}/disconnect`, {
  1125. method: 'POST',
  1126. }),
  1127. // MQTT Debug Logging
  1128. enableMQTTLogging: (printerId: number) =>
  1129. request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/enable`, {
  1130. method: 'POST',
  1131. }),
  1132. disableMQTTLogging: (printerId: number) =>
  1133. request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/disable`, {
  1134. method: 'POST',
  1135. }),
  1136. getMQTTLogs: (printerId: number) =>
  1137. request<MQTTLogsResponse>(`/printers/${printerId}/logging`),
  1138. clearMQTTLogs: (printerId: number) =>
  1139. request<{ status: string }>(`/printers/${printerId}/logging`, {
  1140. method: 'DELETE',
  1141. }),
  1142. // Printer File Manager
  1143. getPrinterFiles: (printerId: number, path = '/') =>
  1144. request<{
  1145. path: string;
  1146. files: Array<{
  1147. name: string;
  1148. is_directory: boolean;
  1149. size: number;
  1150. path: string;
  1151. }>;
  1152. }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`),
  1153. getPrinterFileDownloadUrl: (printerId: number, path: string) =>
  1154. `${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`,
  1155. deletePrinterFile: (printerId: number, path: string) =>
  1156. request<{ status: string; path: string }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`, {
  1157. method: 'DELETE',
  1158. }),
  1159. getPrinterStorage: (printerId: number) =>
  1160. request<{ used_bytes: number | null; free_bytes: number | null }>(`/printers/${printerId}/storage`),
  1161. // Archives
  1162. getArchives: (printerId?: number, projectId?: number, limit = 50, offset = 0) => {
  1163. const params = new URLSearchParams();
  1164. if (printerId) params.set('printer_id', String(printerId));
  1165. if (projectId) params.set('project_id', String(projectId));
  1166. params.set('limit', String(limit));
  1167. params.set('offset', String(offset));
  1168. return request<Archive[]>(`/archives/?${params}`);
  1169. },
  1170. getArchive: (id: number) => request<Archive>(`/archives/${id}`),
  1171. searchArchives: (query: string, options?: {
  1172. printerId?: number;
  1173. projectId?: number;
  1174. status?: string;
  1175. limit?: number;
  1176. offset?: number;
  1177. }) => {
  1178. const params = new URLSearchParams();
  1179. params.set('q', query);
  1180. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1181. if (options?.projectId) params.set('project_id', String(options.projectId));
  1182. if (options?.status) params.set('status', options.status);
  1183. if (options?.limit) params.set('limit', String(options.limit));
  1184. if (options?.offset) params.set('offset', String(options.offset));
  1185. return request<Archive[]>(`/archives/search?${params}`);
  1186. },
  1187. rebuildSearchIndex: () => request<{ message: string }>('/archives/search/rebuild-index', { method: 'POST' }),
  1188. updateArchive: (id: number, data: {
  1189. printer_id?: number | null;
  1190. project_id?: number | null;
  1191. print_name?: string;
  1192. is_favorite?: boolean;
  1193. tags?: string;
  1194. notes?: string;
  1195. cost?: number;
  1196. failure_reason?: string | null;
  1197. status?: string;
  1198. }) =>
  1199. request<Archive>(`/archives/${id}`, {
  1200. method: 'PATCH',
  1201. body: JSON.stringify(data),
  1202. }),
  1203. toggleFavorite: (id: number) =>
  1204. request<Archive>(`/archives/${id}/favorite`, { method: 'POST' }),
  1205. deleteArchive: (id: number) =>
  1206. request<void>(`/archives/${id}`, { method: 'DELETE' }),
  1207. getArchiveStats: () => request<ArchiveStats>('/archives/stats'),
  1208. getFailureAnalysis: (options?: { days?: number; printerId?: number; projectId?: number }) => {
  1209. const params = new URLSearchParams();
  1210. if (options?.days) params.set('days', String(options.days));
  1211. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1212. if (options?.projectId) params.set('project_id', String(options.projectId));
  1213. return request<FailureAnalysis>(`/archives/analysis/failures?${params}`);
  1214. },
  1215. compareArchives: (archiveIds: number[]) =>
  1216. request<ArchiveComparison>(`/archives/compare?archive_ids=${archiveIds.join(',')}`),
  1217. findSimilarArchives: (archiveId: number, limit = 10) =>
  1218. request<SimilarArchive[]>(`/archives/${archiveId}/similar?limit=${limit}`),
  1219. exportArchives: async (options?: {
  1220. format?: 'csv' | 'xlsx';
  1221. fields?: string[];
  1222. printerId?: number;
  1223. projectId?: number;
  1224. status?: string;
  1225. dateFrom?: string;
  1226. dateTo?: string;
  1227. search?: string;
  1228. }): Promise<{ blob: Blob; filename: string }> => {
  1229. const params = new URLSearchParams();
  1230. if (options?.format) params.set('format', options.format);
  1231. if (options?.fields) params.set('fields', options.fields.join(','));
  1232. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1233. if (options?.projectId) params.set('project_id', String(options.projectId));
  1234. if (options?.status) params.set('status', options.status);
  1235. if (options?.dateFrom) params.set('date_from', options.dateFrom);
  1236. if (options?.dateTo) params.set('date_to', options.dateTo);
  1237. if (options?.search) params.set('search', options.search);
  1238. const response = await fetch(`${API_BASE}/archives/export?${params}`);
  1239. if (!response.ok) {
  1240. const error = await response.json().catch(() => ({}));
  1241. throw new Error(error.detail || `HTTP ${response.status}`);
  1242. }
  1243. const contentDisposition = response.headers.get('Content-Disposition');
  1244. let filename = options?.format === 'xlsx' ? 'archives_export.xlsx' : 'archives_export.csv';
  1245. if (contentDisposition) {
  1246. const match = contentDisposition.match(/filename="?([^"]+)"?/);
  1247. if (match) filename = match[1];
  1248. }
  1249. const blob = await response.blob();
  1250. return { blob, filename };
  1251. },
  1252. exportStats: async (options?: {
  1253. format?: 'csv' | 'xlsx';
  1254. days?: number;
  1255. printerId?: number;
  1256. projectId?: number;
  1257. }): Promise<{ blob: Blob; filename: string }> => {
  1258. const params = new URLSearchParams();
  1259. if (options?.format) params.set('format', options.format);
  1260. if (options?.days) params.set('days', String(options.days));
  1261. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1262. if (options?.projectId) params.set('project_id', String(options.projectId));
  1263. const response = await fetch(`${API_BASE}/archives/stats/export?${params}`);
  1264. if (!response.ok) {
  1265. const error = await response.json().catch(() => ({}));
  1266. throw new Error(error.detail || `HTTP ${response.status}`);
  1267. }
  1268. const contentDisposition = response.headers.get('Content-Disposition');
  1269. let filename = options?.format === 'xlsx' ? 'stats_export.xlsx' : 'stats_export.csv';
  1270. if (contentDisposition) {
  1271. const match = contentDisposition.match(/filename="?([^"]+)"?/);
  1272. if (match) filename = match[1];
  1273. }
  1274. const blob = await response.blob();
  1275. return { blob, filename };
  1276. },
  1277. getArchiveDuplicates: (id: number) =>
  1278. request<{ duplicates: ArchiveDuplicate[]; count: number }>(`/archives/${id}/duplicates`),
  1279. backfillContentHashes: () =>
  1280. request<{ updated: number; errors: Array<{ id: number; error: string }> }>('/archives/backfill-hashes', {
  1281. method: 'POST',
  1282. }),
  1283. getArchiveThumbnail: (id: number) => `${API_BASE}/archives/${id}/thumbnail`,
  1284. getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`,
  1285. getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`,
  1286. getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse`,
  1287. scanArchiveTimelapse: (id: number) =>
  1288. request<{
  1289. status: string;
  1290. message: string;
  1291. filename?: string;
  1292. available_files?: Array<{ name: string; path: string; size: number; mtime: string | null }>;
  1293. }>(`/archives/${id}/timelapse/scan`, {
  1294. method: 'POST',
  1295. }),
  1296. selectArchiveTimelapse: (id: number, filename: string) =>
  1297. request<{ status: string; message: string; filename: string }>(
  1298. `/archives/${id}/timelapse/select?filename=${encodeURIComponent(filename)}`,
  1299. { method: 'POST' }
  1300. ),
  1301. uploadArchiveTimelapse: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
  1302. const formData = new FormData();
  1303. formData.append('file', file);
  1304. const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/upload`, {
  1305. method: 'POST',
  1306. body: formData,
  1307. });
  1308. if (!response.ok) {
  1309. const error = await response.json().catch(() => ({}));
  1310. throw new Error(error.detail || `HTTP ${response.status}`);
  1311. }
  1312. return response.json();
  1313. },
  1314. // Photos
  1315. getArchivePhotoUrl: (archiveId: number, filename: string) =>
  1316. `${API_BASE}/archives/${archiveId}/photos/${encodeURIComponent(filename)}`,
  1317. uploadArchivePhoto: async (archiveId: number, file: File): Promise<{ status: string; filename: string; photos: string[] }> => {
  1318. const formData = new FormData();
  1319. formData.append('file', file);
  1320. const response = await fetch(`${API_BASE}/archives/${archiveId}/photos`, {
  1321. method: 'POST',
  1322. body: formData,
  1323. });
  1324. if (!response.ok) {
  1325. const error = await response.json().catch(() => ({}));
  1326. throw new Error(error.detail || `HTTP ${response.status}`);
  1327. }
  1328. return response.json();
  1329. },
  1330. deleteArchivePhoto: (archiveId: number, filename: string) =>
  1331. request<{ status: string; photos: string[] | null }>(`/archives/${archiveId}/photos/${encodeURIComponent(filename)}`, {
  1332. method: 'DELETE',
  1333. }),
  1334. // Source 3MF (original slicer project file)
  1335. getSource3mfDownloadUrl: (archiveId: number) =>
  1336. `${API_BASE}/archives/${archiveId}/source`,
  1337. getSource3mfForSlicer: (archiveId: number, filename: string) =>
  1338. `${API_BASE}/archives/${archiveId}/source/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
  1339. uploadSource3mf: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
  1340. const formData = new FormData();
  1341. formData.append('file', file);
  1342. const response = await fetch(`${API_BASE}/archives/${archiveId}/source`, {
  1343. method: 'POST',
  1344. body: formData,
  1345. });
  1346. if (!response.ok) {
  1347. const error = await response.json().catch(() => ({}));
  1348. throw new Error(error.detail || `HTTP ${response.status}`);
  1349. }
  1350. return response.json();
  1351. },
  1352. deleteSource3mf: (archiveId: number) =>
  1353. request<{ status: string }>(`/archives/${archiveId}/source`, {
  1354. method: 'DELETE',
  1355. }),
  1356. // QR Code
  1357. getArchiveQRCodeUrl: (archiveId: number, size = 200) =>
  1358. `${API_BASE}/archives/${archiveId}/qrcode?size=${size}`,
  1359. getArchiveCapabilities: (id: number) =>
  1360. request<{
  1361. has_model: boolean;
  1362. has_gcode: boolean;
  1363. build_volume: { x: number; y: number; z: number };
  1364. filament_colors: string[];
  1365. }>(`/archives/${id}/capabilities`),
  1366. // Project Page
  1367. getArchiveProjectPage: (id: number) =>
  1368. request<{
  1369. title: string | null;
  1370. description: string | null;
  1371. designer: string | null;
  1372. designer_user_id: string | null;
  1373. license: string | null;
  1374. copyright: string | null;
  1375. creation_date: string | null;
  1376. modification_date: string | null;
  1377. origin: string | null;
  1378. profile_title: string | null;
  1379. profile_description: string | null;
  1380. profile_cover: string | null;
  1381. profile_user_id: string | null;
  1382. profile_user_name: string | null;
  1383. design_model_id: string | null;
  1384. design_profile_id: string | null;
  1385. design_region: string | null;
  1386. model_pictures: Array<{ name: string; path: string; url: string }>;
  1387. profile_pictures: Array<{ name: string; path: string; url: string }>;
  1388. thumbnails: Array<{ name: string; path: string; url: string }>;
  1389. }>(`/archives/${id}/project-page`),
  1390. updateArchiveProjectPage: (id: number, data: {
  1391. title?: string;
  1392. description?: string;
  1393. designer?: string;
  1394. license?: string;
  1395. copyright?: string;
  1396. profile_title?: string;
  1397. profile_description?: string;
  1398. }) =>
  1399. request(`/archives/${id}/project-page`, {
  1400. method: 'PATCH',
  1401. body: JSON.stringify(data),
  1402. }),
  1403. getArchiveProjectImageUrl: (archiveId: number, imagePath: string) =>
  1404. `${API_BASE}/archives/${archiveId}/project-image/${encodeURIComponent(imagePath)}`,
  1405. getArchiveForSlicer: (id: number, filename: string) =>
  1406. `${API_BASE}/archives/${id}/file/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
  1407. reprintArchive: (archiveId: number, printerId: number) =>
  1408. request<{ status: string; printer_id: number; archive_id: number; filename: string }>(
  1409. `/archives/${archiveId}/reprint?printer_id=${printerId}`,
  1410. { method: 'POST' }
  1411. ),
  1412. uploadArchive: async (file: File, printerId?: number): Promise<Archive> => {
  1413. const formData = new FormData();
  1414. formData.append('file', file);
  1415. const url = printerId
  1416. ? `${API_BASE}/archives/upload?printer_id=${printerId}`
  1417. : `${API_BASE}/archives/upload`;
  1418. const response = await fetch(url, {
  1419. method: 'POST',
  1420. body: formData,
  1421. });
  1422. if (!response.ok) {
  1423. const error = await response.json().catch(() => ({}));
  1424. throw new Error(error.detail || `HTTP ${response.status}`);
  1425. }
  1426. return response.json();
  1427. },
  1428. uploadArchivesBulk: async (files: File[], printerId?: number): Promise<BulkUploadResult> => {
  1429. const formData = new FormData();
  1430. files.forEach((file) => formData.append('files', file));
  1431. const url = printerId
  1432. ? `${API_BASE}/archives/upload-bulk?printer_id=${printerId}`
  1433. : `${API_BASE}/archives/upload-bulk`;
  1434. const response = await fetch(url, {
  1435. method: 'POST',
  1436. body: formData,
  1437. });
  1438. if (!response.ok) {
  1439. const error = await response.json().catch(() => ({}));
  1440. throw new Error(error.detail || `HTTP ${response.status}`);
  1441. }
  1442. return response.json();
  1443. },
  1444. // Settings
  1445. getSettings: () => request<AppSettings>('/settings/'),
  1446. updateSettings: (data: AppSettingsUpdate) =>
  1447. request<AppSettings>('/settings/', {
  1448. method: 'PUT',
  1449. body: JSON.stringify(data),
  1450. }),
  1451. resetSettings: () =>
  1452. request<AppSettings>('/settings/reset', { method: 'POST' }),
  1453. exportBackup: async (categories?: Record<string, boolean>): Promise<{ blob: Blob; filename: string }> => {
  1454. const params = new URLSearchParams();
  1455. if (categories) {
  1456. if (categories.settings !== undefined) params.set('include_settings', String(categories.settings));
  1457. if (categories.notifications !== undefined) params.set('include_notifications', String(categories.notifications));
  1458. if (categories.templates !== undefined) params.set('include_templates', String(categories.templates));
  1459. if (categories.smart_plugs !== undefined) params.set('include_smart_plugs', String(categories.smart_plugs));
  1460. if (categories.printers !== undefined) params.set('include_printers', String(categories.printers));
  1461. if (categories.filaments !== undefined) params.set('include_filaments', String(categories.filaments));
  1462. if (categories.maintenance !== undefined) params.set('include_maintenance', String(categories.maintenance));
  1463. if (categories.archives !== undefined) params.set('include_archives', String(categories.archives));
  1464. if (categories.access_codes !== undefined) params.set('include_access_codes', String(categories.access_codes));
  1465. }
  1466. const url = `${API_BASE}/settings/backup${params.toString() ? '?' + params.toString() : ''}`;
  1467. const response = await fetch(url);
  1468. // Get filename from Content-Disposition header
  1469. const contentDisposition = response.headers.get('Content-Disposition');
  1470. let filename = 'bambuddy-backup.json';
  1471. if (contentDisposition) {
  1472. const match = contentDisposition.match(/filename=([^;]+)/);
  1473. if (match) filename = match[1].trim();
  1474. }
  1475. const blob = await response.blob();
  1476. return { blob, filename };
  1477. },
  1478. importBackup: async (file: File, overwrite = false) => {
  1479. const formData = new FormData();
  1480. formData.append('file', file);
  1481. const url = `${API_BASE}/settings/restore${overwrite ? '?overwrite=true' : ''}`;
  1482. const response = await fetch(url, {
  1483. method: 'POST',
  1484. body: formData,
  1485. });
  1486. return response.json() as Promise<{
  1487. success: boolean;
  1488. message: string;
  1489. restored?: Record<string, number>;
  1490. skipped?: Record<string, number>;
  1491. skipped_details?: Record<string, string[]>;
  1492. files_restored?: number;
  1493. total_skipped?: number;
  1494. }>;
  1495. },
  1496. checkFfmpeg: () =>
  1497. request<{ installed: boolean; path: string | null }>('/settings/check-ffmpeg'),
  1498. // Cloud
  1499. getCloudStatus: () => request<CloudAuthStatus>('/cloud/status'),
  1500. cloudLogin: (email: string, password: string, region = 'global') =>
  1501. request<CloudLoginResponse>('/cloud/login', {
  1502. method: 'POST',
  1503. body: JSON.stringify({ email, password, region }),
  1504. }),
  1505. cloudVerify: (email: string, code: string) =>
  1506. request<CloudLoginResponse>('/cloud/verify', {
  1507. method: 'POST',
  1508. body: JSON.stringify({ email, code }),
  1509. }),
  1510. cloudSetToken: (access_token: string) =>
  1511. request<CloudAuthStatus>('/cloud/token', {
  1512. method: 'POST',
  1513. body: JSON.stringify({ access_token }),
  1514. }),
  1515. cloudLogout: () =>
  1516. request<{ success: boolean }>('/cloud/logout', { method: 'POST' }),
  1517. getCloudSettings: (version = '02.04.00.70') =>
  1518. request<SlicerSettingsResponse>(`/cloud/settings?version=${version}`),
  1519. getCloudSettingDetail: (settingId: string) =>
  1520. request<SlicerSettingDetail>(`/cloud/settings/${settingId}`),
  1521. createCloudSetting: (data: SlicerSettingCreate) =>
  1522. request<SlicerSettingDetail>('/cloud/settings', {
  1523. method: 'POST',
  1524. body: JSON.stringify(data),
  1525. }),
  1526. updateCloudSetting: (settingId: string, data: SlicerSettingUpdate) =>
  1527. request<SlicerSettingDetail>(`/cloud/settings/${settingId}`, {
  1528. method: 'PUT',
  1529. body: JSON.stringify(data),
  1530. }),
  1531. deleteCloudSetting: (settingId: string) =>
  1532. request<SlicerSettingDeleteResponse>(`/cloud/settings/${settingId}`, {
  1533. method: 'DELETE',
  1534. }),
  1535. getCloudDevices: () => request<CloudDevice[]>('/cloud/devices'),
  1536. getCloudFields: (presetType: 'filament' | 'print' | 'process' | 'printer') =>
  1537. request<FieldDefinitionsResponse>(`/cloud/fields/${presetType}`),
  1538. getAllCloudFields: () =>
  1539. request<Record<string, FieldDefinitionsResponse>>('/cloud/fields'),
  1540. // Smart Plugs
  1541. getSmartPlugs: () => request<SmartPlug[]>('/smart-plugs/'),
  1542. getSmartPlug: (id: number) => request<SmartPlug>(`/smart-plugs/${id}`),
  1543. getSmartPlugByPrinter: (printerId: number) => request<SmartPlug | null>(`/smart-plugs/by-printer/${printerId}`),
  1544. createSmartPlug: (data: SmartPlugCreate) =>
  1545. request<SmartPlug>('/smart-plugs/', {
  1546. method: 'POST',
  1547. body: JSON.stringify(data),
  1548. }),
  1549. updateSmartPlug: (id: number, data: SmartPlugUpdate) =>
  1550. request<SmartPlug>(`/smart-plugs/${id}`, {
  1551. method: 'PATCH',
  1552. body: JSON.stringify(data),
  1553. }),
  1554. deleteSmartPlug: (id: number) =>
  1555. request<void>(`/smart-plugs/${id}`, { method: 'DELETE' }),
  1556. controlSmartPlug: (id: number, action: 'on' | 'off' | 'toggle') =>
  1557. request<{ success: boolean; action: string }>(`/smart-plugs/${id}/control`, {
  1558. method: 'POST',
  1559. body: JSON.stringify({ action }),
  1560. }),
  1561. getSmartPlugStatus: (id: number) =>
  1562. request<SmartPlugStatus>(`/smart-plugs/${id}/status`),
  1563. testSmartPlugConnection: (ip_address: string, username?: string | null, password?: string | null) =>
  1564. request<SmartPlugTestResult>('/smart-plugs/test-connection', {
  1565. method: 'POST',
  1566. body: JSON.stringify({ ip_address, username, password }),
  1567. }),
  1568. // Print Queue
  1569. getQueue: (printerId?: number, status?: string) => {
  1570. const params = new URLSearchParams();
  1571. if (printerId) params.set('printer_id', String(printerId));
  1572. if (status) params.set('status', status);
  1573. return request<PrintQueueItem[]>(`/queue/?${params}`);
  1574. },
  1575. getQueueItem: (id: number) => request<PrintQueueItem>(`/queue/${id}`),
  1576. addToQueue: (data: PrintQueueItemCreate) =>
  1577. request<PrintQueueItem>('/queue/', {
  1578. method: 'POST',
  1579. body: JSON.stringify(data),
  1580. }),
  1581. updateQueueItem: (id: number, data: PrintQueueItemUpdate) =>
  1582. request<PrintQueueItem>(`/queue/${id}`, {
  1583. method: 'PATCH',
  1584. body: JSON.stringify(data),
  1585. }),
  1586. removeFromQueue: (id: number) =>
  1587. request<{ message: string }>(`/queue/${id}`, { method: 'DELETE' }),
  1588. reorderQueue: (items: { id: number; position: number }[]) =>
  1589. request<{ message: string }>('/queue/reorder', {
  1590. method: 'POST',
  1591. body: JSON.stringify({ items }),
  1592. }),
  1593. cancelQueueItem: (id: number) =>
  1594. request<{ message: string }>(`/queue/${id}/cancel`, { method: 'POST' }),
  1595. stopQueueItem: (id: number) =>
  1596. request<{ message: string }>(`/queue/${id}/stop`, { method: 'POST' }),
  1597. // K-Profiles
  1598. getKProfiles: (printerId: number, nozzleDiameter = '0.4') =>
  1599. request<KProfilesResponse>(`/printers/${printerId}/kprofiles/?nozzle_diameter=${nozzleDiameter}`),
  1600. setKProfile: (printerId: number, profile: KProfileCreate) =>
  1601. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
  1602. method: 'POST',
  1603. body: JSON.stringify(profile),
  1604. }),
  1605. deleteKProfile: (printerId: number, profile: KProfileDelete) =>
  1606. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
  1607. method: 'DELETE',
  1608. body: JSON.stringify(profile),
  1609. }),
  1610. setKProfilesBatch: (printerId: number, profiles: KProfileCreate[]) =>
  1611. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/batch`, {
  1612. method: 'POST',
  1613. body: JSON.stringify(profiles),
  1614. }),
  1615. // K-Profile Notes (stored locally, not on printer)
  1616. getKProfileNotes: (printerId: number) =>
  1617. request<KProfileNotesResponse>(`/printers/${printerId}/kprofiles/notes`),
  1618. setKProfileNote: (printerId: number, settingId: string, note: string) =>
  1619. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/notes`, {
  1620. method: 'PUT',
  1621. body: JSON.stringify({ setting_id: settingId, note }),
  1622. }),
  1623. deleteKProfileNote: (printerId: number, settingId: string) =>
  1624. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/notes/${encodeURIComponent(settingId)}`, {
  1625. method: 'DELETE',
  1626. }),
  1627. // Slot Preset Mappings
  1628. getSlotPresets: (printerId: number) =>
  1629. request<Record<number, SlotPresetMapping>>(`/printers/${printerId}/slot-presets`),
  1630. getSlotPreset: (printerId: number, amsId: number, trayId: number) =>
  1631. request<SlotPresetMapping | null>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}`),
  1632. saveSlotPreset: (printerId: number, amsId: number, trayId: number, presetId: string, presetName: string) =>
  1633. request<SlotPresetMapping>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}?preset_id=${encodeURIComponent(presetId)}&preset_name=${encodeURIComponent(presetName)}`, {
  1634. method: 'PUT',
  1635. }),
  1636. deleteSlotPreset: (printerId: number, amsId: number, trayId: number) =>
  1637. request<{ success: boolean }>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}`, {
  1638. method: 'DELETE',
  1639. }),
  1640. // Filaments
  1641. listFilaments: () => request<Filament[]>('/filaments/'),
  1642. getFilament: (id: number) => request<Filament>(`/filaments/${id}`),
  1643. getFilamentsByType: (type: string) => request<Filament[]>(`/filaments/by-type/${type}`),
  1644. // Notification Providers
  1645. getNotificationProviders: () => request<NotificationProvider[]>('/notifications/'),
  1646. getNotificationProvider: (id: number) => request<NotificationProvider>(`/notifications/${id}`),
  1647. createNotificationProvider: (data: NotificationProviderCreate) =>
  1648. request<NotificationProvider>('/notifications/', {
  1649. method: 'POST',
  1650. body: JSON.stringify(data),
  1651. }),
  1652. updateNotificationProvider: (id: number, data: NotificationProviderUpdate) =>
  1653. request<NotificationProvider>(`/notifications/${id}`, {
  1654. method: 'PATCH',
  1655. body: JSON.stringify(data),
  1656. }),
  1657. deleteNotificationProvider: (id: number) =>
  1658. request<{ message: string }>(`/notifications/${id}`, { method: 'DELETE' }),
  1659. testNotificationProvider: (id: number) =>
  1660. request<NotificationTestResponse>(`/notifications/${id}/test`, { method: 'POST' }),
  1661. testNotificationConfig: (data: NotificationTestRequest) =>
  1662. request<NotificationTestResponse>('/notifications/test-config', {
  1663. method: 'POST',
  1664. body: JSON.stringify(data),
  1665. }),
  1666. testAllNotificationProviders: () =>
  1667. request<{
  1668. tested: number;
  1669. success: number;
  1670. failed: number;
  1671. results: Array<{
  1672. provider_id: number;
  1673. provider_name: string;
  1674. provider_type: string;
  1675. success: boolean;
  1676. message: string;
  1677. }>;
  1678. }>('/notifications/test-all', { method: 'POST' }),
  1679. // Notification Templates
  1680. getNotificationTemplates: () => request<NotificationTemplate[]>('/notification-templates'),
  1681. getNotificationTemplate: (id: number) => request<NotificationTemplate>(`/notification-templates/${id}`),
  1682. updateNotificationTemplate: (id: number, data: NotificationTemplateUpdate) =>
  1683. request<NotificationTemplate>(`/notification-templates/${id}`, {
  1684. method: 'PUT',
  1685. body: JSON.stringify(data),
  1686. }),
  1687. resetNotificationTemplate: (id: number) =>
  1688. request<NotificationTemplate>(`/notification-templates/${id}/reset`, {
  1689. method: 'POST',
  1690. }),
  1691. getTemplateVariables: () => request<EventVariablesResponse[]>('/notification-templates/variables'),
  1692. previewTemplate: (data: TemplatePreviewRequest) =>
  1693. request<TemplatePreviewResponse>('/notification-templates/preview', {
  1694. method: 'POST',
  1695. body: JSON.stringify(data),
  1696. }),
  1697. // Notification Logs
  1698. getNotificationLogs: (params?: {
  1699. limit?: number;
  1700. offset?: number;
  1701. provider_id?: number;
  1702. event_type?: string;
  1703. success?: boolean;
  1704. days?: number;
  1705. }) => {
  1706. const searchParams = new URLSearchParams();
  1707. if (params?.limit) searchParams.set('limit', String(params.limit));
  1708. if (params?.offset) searchParams.set('offset', String(params.offset));
  1709. if (params?.provider_id) searchParams.set('provider_id', String(params.provider_id));
  1710. if (params?.event_type) searchParams.set('event_type', params.event_type);
  1711. if (params?.success !== undefined) searchParams.set('success', String(params.success));
  1712. if (params?.days) searchParams.set('days', String(params.days));
  1713. return request<NotificationLogEntry[]>(`/notifications/logs?${searchParams}`);
  1714. },
  1715. getNotificationLogStats: (days = 7) =>
  1716. request<NotificationLogStats>(`/notifications/logs/stats?days=${days}`),
  1717. clearNotificationLogs: (olderThanDays = 30) =>
  1718. request<{ deleted: number; message: string }>(
  1719. `/notifications/logs?older_than_days=${olderThanDays}`,
  1720. { method: 'DELETE' }
  1721. ),
  1722. // Spoolman Integration
  1723. getSpoolmanStatus: () => request<SpoolmanStatus>('/spoolman/status'),
  1724. connectSpoolman: () =>
  1725. request<{ success: boolean; message: string }>('/spoolman/connect', {
  1726. method: 'POST',
  1727. }),
  1728. disconnectSpoolman: () =>
  1729. request<{ success: boolean; message: string }>('/spoolman/disconnect', {
  1730. method: 'POST',
  1731. }),
  1732. syncPrinterAms: (printerId: number) =>
  1733. request<SpoolmanSyncResult>(`/spoolman/sync/${printerId}`, {
  1734. method: 'POST',
  1735. }),
  1736. syncAllPrintersAms: () =>
  1737. request<SpoolmanSyncResult>('/spoolman/sync-all', {
  1738. method: 'POST',
  1739. }),
  1740. getSpoolmanSpools: () =>
  1741. request<{ spools: unknown[] }>('/spoolman/spools'),
  1742. getSpoolmanFilaments: () =>
  1743. request<{ filaments: unknown[] }>('/spoolman/filaments'),
  1744. // Updates
  1745. getVersion: () => request<VersionInfo>('/updates/version'),
  1746. checkForUpdates: () => request<UpdateCheckResult>('/updates/check'),
  1747. applyUpdate: () =>
  1748. request<{ success: boolean; message: string; status: UpdateStatus }>('/updates/apply', {
  1749. method: 'POST',
  1750. }),
  1751. getUpdateStatus: () => request<UpdateStatus>('/updates/status'),
  1752. // Maintenance
  1753. getMaintenanceTypes: () => request<MaintenanceType[]>('/maintenance/types'),
  1754. createMaintenanceType: (data: MaintenanceTypeCreate) =>
  1755. request<MaintenanceType>('/maintenance/types', {
  1756. method: 'POST',
  1757. body: JSON.stringify(data),
  1758. }),
  1759. updateMaintenanceType: (id: number, data: Partial<MaintenanceTypeCreate>) =>
  1760. request<MaintenanceType>(`/maintenance/types/${id}`, {
  1761. method: 'PATCH',
  1762. body: JSON.stringify(data),
  1763. }),
  1764. deleteMaintenanceType: (id: number) =>
  1765. request<{ status: string }>(`/maintenance/types/${id}`, { method: 'DELETE' }),
  1766. getMaintenanceOverview: () => request<PrinterMaintenanceOverview[]>('/maintenance/overview'),
  1767. getPrinterMaintenance: (printerId: number) =>
  1768. request<PrinterMaintenanceOverview>(`/maintenance/printers/${printerId}`),
  1769. updateMaintenanceItem: (itemId: number, data: { custom_interval_hours?: number | null; custom_interval_type?: 'hours' | 'days' | null; enabled?: boolean }) =>
  1770. request<MaintenanceStatus>(`/maintenance/items/${itemId}`, {
  1771. method: 'PATCH',
  1772. body: JSON.stringify(data),
  1773. }),
  1774. performMaintenance: (itemId: number, notes?: string) =>
  1775. request<MaintenanceStatus>(`/maintenance/items/${itemId}/perform`, {
  1776. method: 'POST',
  1777. body: JSON.stringify({ notes }),
  1778. }),
  1779. getMaintenanceHistory: (itemId: number) =>
  1780. request<MaintenanceHistory[]>(`/maintenance/items/${itemId}/history`),
  1781. getMaintenanceSummary: () => request<MaintenanceSummary>('/maintenance/summary'),
  1782. setPrinterHours: (printerId: number, totalHours: number) =>
  1783. request<{ printer_id: number; total_hours: number; archive_hours: number; offset_hours: number }>(
  1784. `/maintenance/printers/${printerId}/hours?total_hours=${totalHours}`,
  1785. { method: 'PATCH' }
  1786. ),
  1787. assignMaintenanceType: (printerId: number, typeId: number) =>
  1788. request<MaintenanceStatus>(`/maintenance/printers/${printerId}/assign/${typeId}`, {
  1789. method: 'POST',
  1790. }),
  1791. removeMaintenanceItem: (itemId: number) =>
  1792. request<{ status: string }>(`/maintenance/items/${itemId}`, {
  1793. method: 'DELETE',
  1794. }),
  1795. // Camera
  1796. getCameraStreamUrl: (printerId: number, fps = 10) =>
  1797. `${API_BASE}/printers/${printerId}/camera/stream?fps=${fps}`,
  1798. getCameraSnapshotUrl: (printerId: number) =>
  1799. `${API_BASE}/printers/${printerId}/camera/snapshot`,
  1800. testCameraConnection: (printerId: number) =>
  1801. request<{ success: boolean; message?: string; error?: string }>(`/printers/${printerId}/camera/test`),
  1802. // External Links
  1803. getExternalLinks: () => request<ExternalLink[]>('/external-links/'),
  1804. getExternalLink: (id: number) => request<ExternalLink>(`/external-links/${id}`),
  1805. createExternalLink: (data: ExternalLinkCreate) =>
  1806. request<ExternalLink>('/external-links/', {
  1807. method: 'POST',
  1808. body: JSON.stringify(data),
  1809. }),
  1810. updateExternalLink: (id: number, data: ExternalLinkUpdate) =>
  1811. request<ExternalLink>(`/external-links/${id}`, {
  1812. method: 'PATCH',
  1813. body: JSON.stringify(data),
  1814. }),
  1815. deleteExternalLink: (id: number) =>
  1816. request<{ message: string }>(`/external-links/${id}`, { method: 'DELETE' }),
  1817. reorderExternalLinks: (ids: number[]) =>
  1818. request<ExternalLink[]>('/external-links/reorder', {
  1819. method: 'PUT',
  1820. body: JSON.stringify({ ids }),
  1821. }),
  1822. uploadExternalLinkIcon: async (id: number, file: File): Promise<ExternalLink> => {
  1823. const formData = new FormData();
  1824. formData.append('file', file);
  1825. const response = await fetch(`${API_BASE}/external-links/${id}/icon`, {
  1826. method: 'POST',
  1827. body: formData,
  1828. });
  1829. if (!response.ok) {
  1830. const error = await response.json().catch(() => ({}));
  1831. throw new Error(error.detail || `HTTP ${response.status}`);
  1832. }
  1833. return response.json();
  1834. },
  1835. deleteExternalLinkIcon: (id: number) =>
  1836. request<ExternalLink>(`/external-links/${id}/icon`, { method: 'DELETE' }),
  1837. getExternalLinkIconUrl: (id: number) => `${API_BASE}/external-links/${id}/icon`,
  1838. // Projects
  1839. getProjects: (status?: string) => {
  1840. const params = new URLSearchParams();
  1841. if (status) params.set('status', status);
  1842. return request<ProjectListItem[]>(`/projects/?${params}`);
  1843. },
  1844. getProject: (id: number) => request<Project>(`/projects/${id}`),
  1845. createProject: (data: ProjectCreate) =>
  1846. request<Project>('/projects/', {
  1847. method: 'POST',
  1848. body: JSON.stringify(data),
  1849. }),
  1850. updateProject: (id: number, data: ProjectUpdate) =>
  1851. request<Project>(`/projects/${id}`, {
  1852. method: 'PATCH',
  1853. body: JSON.stringify(data),
  1854. }),
  1855. deleteProject: (id: number) =>
  1856. request<{ message: string }>(`/projects/${id}`, { method: 'DELETE' }),
  1857. getProjectArchives: (id: number, limit = 100, offset = 0) =>
  1858. request<Archive[]>(`/projects/${id}/archives?limit=${limit}&offset=${offset}`),
  1859. addArchivesToProject: (projectId: number, archiveIds: number[]) =>
  1860. request<{ message: string }>(`/projects/${projectId}/add-archives`, {
  1861. method: 'POST',
  1862. body: JSON.stringify({ archive_ids: archiveIds }),
  1863. }),
  1864. removeArchivesFromProject: (projectId: number, archiveIds: number[]) =>
  1865. request<{ message: string }>(`/projects/${projectId}/remove-archives`, {
  1866. method: 'POST',
  1867. body: JSON.stringify({ archive_ids: archiveIds }),
  1868. }),
  1869. addQueueItemsToProject: (projectId: number, queueItemIds: number[]) =>
  1870. request<{ message: string }>(`/projects/${projectId}/add-queue`, {
  1871. method: 'POST',
  1872. body: JSON.stringify({ queue_item_ids: queueItemIds }),
  1873. }),
  1874. // Project Attachments
  1875. uploadProjectAttachment: async (projectId: number, file: File): Promise<{
  1876. status: string;
  1877. filename: string;
  1878. original_name: string;
  1879. attachments: ProjectAttachment[];
  1880. }> => {
  1881. const formData = new FormData();
  1882. formData.append('file', file);
  1883. const response = await fetch(`${API_BASE}/projects/${projectId}/attachments`, {
  1884. method: 'POST',
  1885. body: formData,
  1886. });
  1887. if (!response.ok) {
  1888. const error = await response.json().catch(() => ({}));
  1889. throw new Error(error.detail || `HTTP ${response.status}`);
  1890. }
  1891. return response.json();
  1892. },
  1893. getProjectAttachmentUrl: (projectId: number, filename: string) =>
  1894. `${API_BASE}/projects/${projectId}/attachments/${encodeURIComponent(filename)}`,
  1895. deleteProjectAttachment: (projectId: number, filename: string) =>
  1896. request<{ status: string; message: string; attachments: ProjectAttachment[] | null }>(
  1897. `/projects/${projectId}/attachments/${encodeURIComponent(filename)}`,
  1898. { method: 'DELETE' }
  1899. ),
  1900. // BOM (Bill of Materials)
  1901. getProjectBOM: (projectId: number) =>
  1902. request<BOMItem[]>(`/projects/${projectId}/bom`),
  1903. createBOMItem: (projectId: number, data: BOMItemCreate) =>
  1904. request<BOMItem>(`/projects/${projectId}/bom`, {
  1905. method: 'POST',
  1906. body: JSON.stringify(data),
  1907. }),
  1908. updateBOMItem: (projectId: number, itemId: number, data: BOMItemUpdate) =>
  1909. request<BOMItem>(`/projects/${projectId}/bom/${itemId}`, {
  1910. method: 'PATCH',
  1911. body: JSON.stringify(data),
  1912. }),
  1913. deleteBOMItem: (projectId: number, itemId: number) =>
  1914. request<{ status: string; message: string }>(`/projects/${projectId}/bom/${itemId}`, {
  1915. method: 'DELETE',
  1916. }),
  1917. // Templates
  1918. getTemplates: () => request<ProjectListItem[]>('/projects/templates/'),
  1919. createTemplateFromProject: (projectId: number) =>
  1920. request<Project>(`/projects/${projectId}/create-template`, { method: 'POST' }),
  1921. createProjectFromTemplate: (templateId: number, name?: string) =>
  1922. request<Project>(`/projects/from-template/${templateId}${name ? `?name=${encodeURIComponent(name)}` : ''}`, {
  1923. method: 'POST',
  1924. }),
  1925. // Timeline
  1926. getProjectTimeline: (projectId: number, limit = 50) =>
  1927. request<TimelineEvent[]>(`/projects/${projectId}/timeline?limit=${limit}`),
  1928. // API Keys
  1929. getAPIKeys: () => request<APIKey[]>('/api-keys/'),
  1930. createAPIKey: (data: APIKeyCreate) =>
  1931. request<APIKeyCreateResponse>('/api-keys/', {
  1932. method: 'POST',
  1933. body: JSON.stringify(data),
  1934. }),
  1935. updateAPIKey: (id: number, data: APIKeyUpdate) =>
  1936. request<APIKey>(`/api-keys/${id}`, {
  1937. method: 'PATCH',
  1938. body: JSON.stringify(data),
  1939. }),
  1940. deleteAPIKey: (id: number) =>
  1941. request<{ message: string }>(`/api-keys/${id}`, { method: 'DELETE' }),
  1942. // AMS History
  1943. getAMSHistory: (printerId: number, amsId: number, hours = 24) =>
  1944. request<AMSHistoryResponse>(`/ams-history/${printerId}/${amsId}?hours=${hours}`),
  1945. // System Info
  1946. getSystemInfo: () => request<SystemInfo>('/system/info'),
  1947. };
  1948. // AMS History types
  1949. export interface AMSHistoryPoint {
  1950. recorded_at: string;
  1951. humidity: number | null;
  1952. humidity_raw: number | null;
  1953. temperature: number | null;
  1954. }
  1955. export interface AMSHistoryResponse {
  1956. printer_id: number;
  1957. ams_id: number;
  1958. data: AMSHistoryPoint[];
  1959. min_humidity: number | null;
  1960. max_humidity: number | null;
  1961. avg_humidity: number | null;
  1962. min_temperature: number | null;
  1963. max_temperature: number | null;
  1964. avg_temperature: number | null;
  1965. }
  1966. // System Info types
  1967. export interface SystemInfo {
  1968. app: {
  1969. version: string;
  1970. base_dir: string;
  1971. archive_dir: string;
  1972. };
  1973. database: {
  1974. archives: number;
  1975. archives_completed: number;
  1976. archives_failed: number;
  1977. archives_printing: number;
  1978. printers: number;
  1979. filaments: number;
  1980. projects: number;
  1981. smart_plugs: number;
  1982. total_print_time_seconds: number;
  1983. total_print_time_formatted: string;
  1984. total_filament_grams: number;
  1985. total_filament_kg: number;
  1986. };
  1987. printers: {
  1988. total: number;
  1989. connected: number;
  1990. connected_list: Array<{
  1991. id: number;
  1992. name: string;
  1993. state: string;
  1994. model: string;
  1995. }>;
  1996. };
  1997. storage: {
  1998. archive_size_bytes: number;
  1999. archive_size_formatted: string;
  2000. database_size_bytes: number;
  2001. database_size_formatted: string;
  2002. disk_total_bytes: number;
  2003. disk_total_formatted: string;
  2004. disk_used_bytes: number;
  2005. disk_used_formatted: string;
  2006. disk_free_bytes: number;
  2007. disk_free_formatted: string;
  2008. disk_percent_used: number;
  2009. };
  2010. system: {
  2011. platform: string;
  2012. platform_release: string;
  2013. platform_version: string;
  2014. architecture: string;
  2015. hostname: string;
  2016. python_version: string;
  2017. uptime_seconds: number;
  2018. uptime_formatted: string;
  2019. boot_time: string;
  2020. };
  2021. memory: {
  2022. total_bytes: number;
  2023. total_formatted: string;
  2024. available_bytes: number;
  2025. available_formatted: string;
  2026. used_bytes: number;
  2027. used_formatted: string;
  2028. percent_used: number;
  2029. };
  2030. cpu: {
  2031. count: number;
  2032. count_logical: number;
  2033. percent: number;
  2034. };
  2035. }
  2036. // Discovery types
  2037. export interface DiscoveredPrinter {
  2038. serial: string;
  2039. name: string;
  2040. ip_address: string;
  2041. model: string | null;
  2042. discovered_at: string | null;
  2043. }
  2044. export interface DiscoveryStatus {
  2045. running: boolean;
  2046. }
  2047. // Discovery API
  2048. export const discoveryApi = {
  2049. getStatus: () => request<DiscoveryStatus>('/discovery/status'),
  2050. startDiscovery: (duration: number = 10) =>
  2051. request<DiscoveryStatus>(`/discovery/start?duration=${duration}`, { method: 'POST' }),
  2052. stopDiscovery: () =>
  2053. request<DiscoveryStatus>('/discovery/stop', { method: 'POST' }),
  2054. getDiscoveredPrinters: () =>
  2055. request<DiscoveredPrinter[]>('/discovery/printers'),
  2056. };