client.ts 79 KB

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