client.ts 97 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050
  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. // Fan speeds (0-100 percentage, null if not available for this model)
  159. cooling_fan_speed: number | null; // Part cooling fan
  160. big_fan1_speed: number | null; // Auxiliary fan
  161. big_fan2_speed: number | null; // Chamber/exhaust fan
  162. heatbreak_fan_speed: number | null; // Hotend heatbreak fan
  163. }
  164. export interface PrinterCreate {
  165. name: string;
  166. serial_number: string;
  167. ip_address: string;
  168. access_code: string;
  169. model?: string;
  170. location?: string;
  171. auto_archive?: boolean;
  172. }
  173. // Archive types
  174. export interface ArchiveDuplicate {
  175. id: number;
  176. print_name: string | null;
  177. created_at: string;
  178. match_type: 'exact' | 'similar'; // 'exact' = hash match, 'similar' = name match
  179. }
  180. export interface Archive {
  181. id: number;
  182. printer_id: number | null;
  183. project_id: number | null;
  184. project_name: string | null;
  185. filename: string;
  186. file_path: string;
  187. file_size: number;
  188. content_hash: string | null;
  189. thumbnail_path: string | null;
  190. timelapse_path: string | null;
  191. source_3mf_path: string | null;
  192. f3d_path: string | null;
  193. duplicates: ArchiveDuplicate[] | null;
  194. duplicate_count: number;
  195. object_count: number | null;
  196. print_name: string | null;
  197. print_time_seconds: number | null;
  198. actual_time_seconds: number | null; // Computed from started_at/completed_at
  199. time_accuracy: number | null; // Percentage: 100 = perfect, >100 = faster than estimated
  200. filament_used_grams: number | null;
  201. filament_type: string | null;
  202. filament_color: string | null;
  203. layer_height: number | null;
  204. total_layers: number | null;
  205. nozzle_diameter: number | null;
  206. bed_temperature: number | null;
  207. nozzle_temperature: number | null;
  208. status: string;
  209. started_at: string | null;
  210. completed_at: string | null;
  211. extra_data: Record<string, unknown> | null;
  212. makerworld_url: string | null;
  213. designer: string | null;
  214. is_favorite: boolean;
  215. tags: string | null;
  216. notes: string | null;
  217. cost: number | null;
  218. photos: string[] | null;
  219. failure_reason: string | null;
  220. quantity: number;
  221. energy_kwh: number | null;
  222. energy_cost: number | null;
  223. created_at: string;
  224. }
  225. export interface ArchiveStats {
  226. total_prints: number;
  227. successful_prints: number;
  228. failed_prints: number;
  229. total_print_time_hours: number;
  230. total_filament_grams: number;
  231. total_cost: number;
  232. prints_by_filament_type: Record<string, number>;
  233. prints_by_printer: Record<string, number>;
  234. average_time_accuracy: number | null;
  235. time_accuracy_by_printer: Record<string, number> | null;
  236. total_energy_kwh: number;
  237. total_energy_cost: number;
  238. }
  239. export interface FailureAnalysis {
  240. period_days: number;
  241. total_prints: number;
  242. failed_prints: number;
  243. failure_rate: number;
  244. failures_by_reason: Record<string, number>;
  245. failures_by_filament: Record<string, number>;
  246. failures_by_printer: Record<string, number>;
  247. failures_by_hour: Record<number, number>;
  248. recent_failures: Array<{
  249. id: number;
  250. print_name: string;
  251. failure_reason: string | null;
  252. filament_type: string | null;
  253. printer_id: number | null;
  254. created_at: string | null;
  255. }>;
  256. trend: Array<{
  257. week_start: string;
  258. total_prints: number;
  259. failed_prints: number;
  260. failure_rate: number;
  261. }>;
  262. }
  263. export interface BulkUploadResult {
  264. uploaded: number;
  265. failed: number;
  266. results: Array<{ filename: string; id: number; status: string }>;
  267. errors: Array<{ filename: string; error: string }>;
  268. }
  269. // Archive Comparison types
  270. export interface ComparisonArchiveInfo {
  271. id: number;
  272. print_name: string;
  273. status: string;
  274. created_at: string | null;
  275. printer_id: number | null;
  276. project_name: string | null;
  277. }
  278. export interface ComparisonField {
  279. field: string;
  280. label: string;
  281. unit: string | null;
  282. values: (string | number | null)[];
  283. raw_values: (string | number | null)[];
  284. has_difference: boolean;
  285. }
  286. export interface SuccessCorrelationInsight {
  287. field: string;
  288. label: string;
  289. insight: string;
  290. success_avg?: number;
  291. failed_avg?: number;
  292. success_values?: string[];
  293. failed_values?: string[];
  294. }
  295. export interface SuccessCorrelation {
  296. has_both_outcomes: boolean;
  297. message?: string;
  298. successful_count?: number;
  299. failed_count?: number;
  300. insights?: SuccessCorrelationInsight[];
  301. }
  302. export interface ArchiveComparison {
  303. archives: ComparisonArchiveInfo[];
  304. comparison: ComparisonField[];
  305. differences: ComparisonField[];
  306. success_correlation: SuccessCorrelation;
  307. }
  308. export interface SimilarArchive {
  309. archive: {
  310. id: number;
  311. print_name: string;
  312. status: string;
  313. created_at: string | null;
  314. };
  315. match_reason: string;
  316. match_score: number;
  317. }
  318. // Project types
  319. export interface ProjectStats {
  320. total_archives: number;
  321. total_items: number; // Sum of quantities (total items printed)
  322. completed_prints: number; // Sum of quantities for completed prints (parts)
  323. failed_prints: number;
  324. queued_prints: number;
  325. in_progress_prints: number;
  326. total_print_time_hours: number;
  327. total_filament_grams: number;
  328. progress_percent: number | null; // Plates progress (total_archives / target_count)
  329. parts_progress_percent: number | null; // Parts progress (completed_prints / target_parts_count)
  330. estimated_cost: number;
  331. total_energy_kwh: number;
  332. total_energy_cost: number;
  333. remaining_prints: number | null; // Remaining plates
  334. remaining_parts: number | null; // Remaining parts
  335. bom_total_items: number;
  336. bom_completed_items: number;
  337. }
  338. export interface ProjectChildPreview {
  339. id: number;
  340. name: string;
  341. color: string | null;
  342. status: string;
  343. progress_percent: number | null;
  344. }
  345. export interface Project {
  346. id: number;
  347. name: string;
  348. description: string | null;
  349. color: string | null;
  350. status: string; // active, completed, archived
  351. target_count: number | null; // Target number of plates/print jobs
  352. target_parts_count: number | null; // Target number of parts/objects
  353. notes: string | null;
  354. attachments: ProjectAttachment[] | null;
  355. tags: string | null;
  356. due_date: string | null;
  357. priority: string; // low, normal, high, urgent
  358. budget: number | null;
  359. is_template: boolean;
  360. template_source_id: number | null;
  361. parent_id: number | null;
  362. parent_name: string | null;
  363. children: ProjectChildPreview[];
  364. created_at: string;
  365. updated_at: string;
  366. stats?: ProjectStats;
  367. }
  368. export interface ProjectAttachment {
  369. filename: string;
  370. original_name: string;
  371. size: number;
  372. uploaded_at: string;
  373. }
  374. export interface ArchivePreview {
  375. id: number;
  376. print_name: string | null;
  377. thumbnail_path: string | null;
  378. status: string;
  379. filament_type: string | null;
  380. filament_color: string | null;
  381. }
  382. export interface ProjectListItem {
  383. id: number;
  384. name: string;
  385. description: string | null;
  386. color: string | null;
  387. status: string;
  388. target_count: number | null; // Target number of plates/print jobs
  389. target_parts_count: number | null; // Target number of parts/objects
  390. created_at: string;
  391. archive_count: number; // Number of print jobs (plates)
  392. total_items: number; // Sum of quantities (total items printed, including failed)
  393. completed_count: number; // Sum of quantities for completed prints only (parts)
  394. failed_count: number; // Sum of quantities for failed prints
  395. queue_count: number;
  396. progress_percent: number | null; // Plates progress
  397. archives: ArchivePreview[];
  398. }
  399. export interface ProjectCreate {
  400. name: string;
  401. description?: string;
  402. color?: string;
  403. target_count?: number;
  404. target_parts_count?: number;
  405. notes?: string;
  406. tags?: string;
  407. due_date?: string;
  408. priority?: string;
  409. budget?: number;
  410. parent_id?: number;
  411. }
  412. export interface ProjectUpdate {
  413. name?: string;
  414. description?: string;
  415. color?: string;
  416. status?: string;
  417. target_count?: number;
  418. target_parts_count?: number;
  419. notes?: string;
  420. tags?: string;
  421. due_date?: string;
  422. priority?: string;
  423. budget?: number;
  424. parent_id?: number;
  425. }
  426. // BOM Types - Tracks sourced/purchased parts (hardware, electronics, etc.)
  427. export interface BOMItem {
  428. id: number;
  429. project_id: number;
  430. name: string;
  431. quantity_needed: number;
  432. quantity_acquired: number;
  433. unit_price: number | null;
  434. sourcing_url: string | null;
  435. archive_id: number | null;
  436. archive_name: string | null;
  437. stl_filename: string | null;
  438. remarks: string | null;
  439. sort_order: number;
  440. is_complete: boolean;
  441. created_at: string;
  442. updated_at: string;
  443. }
  444. export interface BOMItemCreate {
  445. name: string;
  446. quantity_needed?: number;
  447. unit_price?: number;
  448. sourcing_url?: string;
  449. archive_id?: number;
  450. stl_filename?: string;
  451. remarks?: string;
  452. }
  453. export interface BOMItemUpdate {
  454. name?: string;
  455. quantity_needed?: number;
  456. quantity_acquired?: number;
  457. unit_price?: number;
  458. sourcing_url?: string;
  459. archive_id?: number;
  460. stl_filename?: string;
  461. remarks?: string;
  462. }
  463. // Timeline Types
  464. export interface TimelineEvent {
  465. event_type: string;
  466. timestamp: string;
  467. title: string;
  468. description: string | null;
  469. metadata: Record<string, unknown> | null;
  470. }
  471. // API Key types
  472. export interface APIKey {
  473. id: number;
  474. name: string;
  475. key_prefix: string;
  476. can_queue: boolean;
  477. can_control_printer: boolean;
  478. can_read_status: boolean;
  479. printer_ids: number[] | null;
  480. enabled: boolean;
  481. last_used: string | null;
  482. created_at: string;
  483. expires_at: string | null;
  484. }
  485. export interface APIKeyCreate {
  486. name: string;
  487. can_queue?: boolean;
  488. can_control_printer?: boolean;
  489. can_read_status?: boolean;
  490. printer_ids?: number[] | null;
  491. expires_at?: string | null;
  492. }
  493. export interface APIKeyCreateResponse extends APIKey {
  494. key: string; // Full key, only shown on creation
  495. }
  496. export interface APIKeyUpdate {
  497. name?: string;
  498. can_queue?: boolean;
  499. can_control_printer?: boolean;
  500. can_read_status?: boolean;
  501. printer_ids?: number[] | null;
  502. enabled?: boolean;
  503. expires_at?: string | null;
  504. }
  505. // Settings types
  506. export interface AppSettings {
  507. auto_archive: boolean;
  508. save_thumbnails: boolean;
  509. capture_finish_photo: boolean;
  510. default_filament_cost: number;
  511. currency: string;
  512. energy_cost_per_kwh: number;
  513. energy_tracking_mode: 'print' | 'total';
  514. check_updates: boolean;
  515. notification_language: string;
  516. // AMS threshold settings
  517. ams_humidity_good: number; // <= this is green
  518. ams_humidity_fair: number; // <= this is orange, > is red
  519. ams_temp_good: number; // <= this is green/blue
  520. ams_temp_fair: number; // <= this is orange, > is red
  521. ams_history_retention_days: number; // days to keep AMS sensor history
  522. // Date/time format settings
  523. date_format: 'system' | 'us' | 'eu' | 'iso';
  524. time_format: 'system' | '12h' | '24h';
  525. // Default printer
  526. default_printer_id: number | null;
  527. // Telemetry
  528. telemetry_enabled: boolean;
  529. // Dark mode theme settings
  530. dark_style: 'classic' | 'glow' | 'vibrant';
  531. dark_background: 'neutral' | 'warm' | 'cool' | 'oled' | 'slate' | 'forest';
  532. dark_accent: 'green' | 'teal' | 'blue' | 'orange' | 'purple' | 'red';
  533. // Light mode theme settings
  534. light_style: 'classic' | 'glow' | 'vibrant';
  535. light_background: 'neutral' | 'warm' | 'cool';
  536. light_accent: 'green' | 'teal' | 'blue' | 'orange' | 'purple' | 'red';
  537. // FTP retry settings
  538. ftp_retry_enabled: boolean;
  539. ftp_retry_count: number;
  540. ftp_retry_delay: number;
  541. ftp_timeout: number;
  542. // MQTT relay settings
  543. mqtt_enabled: boolean;
  544. mqtt_broker: string;
  545. mqtt_port: number;
  546. mqtt_username: string;
  547. mqtt_password: string;
  548. mqtt_topic_prefix: string;
  549. mqtt_use_tls: boolean;
  550. // Home Assistant integration
  551. ha_enabled: boolean;
  552. ha_url: string;
  553. ha_token: string;
  554. // File Manager / Library settings
  555. library_archive_mode: 'always' | 'never' | 'ask';
  556. library_disk_warning_gb: number;
  557. }
  558. export type AppSettingsUpdate = Partial<AppSettings>;
  559. // MQTT relay status
  560. export interface MQTTStatus {
  561. enabled: boolean;
  562. connected: boolean;
  563. broker: string;
  564. port: number;
  565. topic_prefix: string;
  566. }
  567. // Cloud types
  568. export interface CloudAuthStatus {
  569. is_authenticated: boolean;
  570. email: string | null;
  571. }
  572. export interface CloudLoginResponse {
  573. success: boolean;
  574. needs_verification: boolean;
  575. message: string;
  576. }
  577. export interface SlicerSetting {
  578. setting_id: string;
  579. name: string;
  580. type: string;
  581. version: string | null;
  582. user_id: string | null;
  583. updated_time: string | null;
  584. }
  585. export interface SlicerSettingsResponse {
  586. filament: SlicerSetting[];
  587. printer: SlicerSetting[];
  588. process: SlicerSetting[];
  589. }
  590. export interface SlicerSettingDetail {
  591. message?: string | null;
  592. code?: string | null;
  593. error?: string | null;
  594. public: boolean;
  595. version?: string | null;
  596. type: string;
  597. name: string;
  598. update_time?: string | null;
  599. nickname?: string | null;
  600. base_id?: string | null;
  601. setting: Record<string, unknown>;
  602. filament_id?: string | null;
  603. setting_id?: string | null;
  604. }
  605. export interface SlicerSettingCreate {
  606. type: string; // 'filament', 'print', or 'printer'
  607. name: string;
  608. base_id: string;
  609. setting: Record<string, unknown>;
  610. }
  611. export interface SlicerSettingUpdate {
  612. name?: string;
  613. setting?: Record<string, unknown>;
  614. }
  615. export interface SlicerSettingDeleteResponse {
  616. success: boolean;
  617. message: string;
  618. }
  619. export interface FieldOption {
  620. value: string;
  621. label: string;
  622. }
  623. export interface FieldDefinition {
  624. key: string;
  625. label: string;
  626. type: 'text' | 'number' | 'boolean' | 'select';
  627. category: string;
  628. description?: string;
  629. options?: FieldOption[];
  630. unit?: string;
  631. min?: number;
  632. max?: number;
  633. step?: number;
  634. }
  635. export interface FieldDefinitionsResponse {
  636. version: string;
  637. description: string;
  638. fields: FieldDefinition[];
  639. }
  640. export interface CloudDevice {
  641. dev_id: string;
  642. name: string;
  643. dev_model_name: string | null;
  644. dev_product_name: string | null;
  645. online: boolean;
  646. }
  647. // Smart Plug types
  648. export interface SmartPlug {
  649. id: number;
  650. name: string;
  651. plug_type: 'tasmota' | 'homeassistant';
  652. ip_address: string | null; // Required for Tasmota
  653. ha_entity_id: string | null; // Required for Home Assistant (e.g., "switch.printer_plug")
  654. printer_id: number | null;
  655. enabled: boolean;
  656. auto_on: boolean;
  657. auto_off: boolean;
  658. off_delay_mode: 'time' | 'temperature';
  659. off_delay_minutes: number;
  660. off_temp_threshold: number;
  661. username: string | null;
  662. password: string | null;
  663. // Power alerts
  664. power_alert_enabled: boolean;
  665. power_alert_high: number | null;
  666. power_alert_low: number | null;
  667. power_alert_last_triggered: string | null;
  668. // Schedule
  669. schedule_enabled: boolean;
  670. schedule_on_time: string | null;
  671. schedule_off_time: string | null;
  672. // Switchbar visibility
  673. show_in_switchbar: boolean;
  674. // Status
  675. last_state: string | null;
  676. last_checked: string | null;
  677. auto_off_executed: boolean; // True when auto-off was triggered after print
  678. created_at: string;
  679. updated_at: string;
  680. }
  681. export interface SmartPlugCreate {
  682. name: string;
  683. plug_type?: 'tasmota' | 'homeassistant';
  684. ip_address?: string | null; // Required for Tasmota
  685. ha_entity_id?: string | null; // Required for Home Assistant
  686. printer_id?: number | null;
  687. enabled?: boolean;
  688. auto_on?: boolean;
  689. auto_off?: boolean;
  690. off_delay_mode?: 'time' | 'temperature';
  691. off_delay_minutes?: number;
  692. off_temp_threshold?: number;
  693. username?: string | null;
  694. password?: string | null;
  695. // Power alerts
  696. power_alert_enabled?: boolean;
  697. power_alert_high?: number | null;
  698. power_alert_low?: number | null;
  699. // Schedule
  700. schedule_enabled?: boolean;
  701. schedule_on_time?: string | null;
  702. schedule_off_time?: string | null;
  703. // Switchbar visibility
  704. show_in_switchbar?: boolean;
  705. }
  706. export interface SmartPlugUpdate {
  707. name?: string;
  708. plug_type?: 'tasmota' | 'homeassistant';
  709. ip_address?: string | null;
  710. ha_entity_id?: string | null;
  711. printer_id?: number | null;
  712. enabled?: boolean;
  713. auto_on?: boolean;
  714. auto_off?: boolean;
  715. off_delay_mode?: 'time' | 'temperature';
  716. off_delay_minutes?: number;
  717. off_temp_threshold?: number;
  718. username?: string | null;
  719. password?: string | null;
  720. // Power alerts
  721. power_alert_enabled?: boolean;
  722. power_alert_high?: number | null;
  723. power_alert_low?: number | null;
  724. // Schedule
  725. schedule_enabled?: boolean;
  726. schedule_on_time?: string | null;
  727. schedule_off_time?: string | null;
  728. // Switchbar visibility
  729. show_in_switchbar?: boolean;
  730. }
  731. // Home Assistant entity for smart plug selection
  732. export interface HAEntity {
  733. entity_id: string;
  734. friendly_name: string;
  735. state: string | null;
  736. domain: string; // "switch", "light", "input_boolean"
  737. }
  738. export interface HATestConnectionResult {
  739. success: boolean;
  740. message: string | null;
  741. error: string | null;
  742. }
  743. export interface SmartPlugEnergy {
  744. power: number | null; // Current watts
  745. voltage: number | null; // Volts
  746. current: number | null; // Amps
  747. today: number | null; // kWh used today
  748. yesterday: number | null; // kWh used yesterday
  749. total: number | null; // Total kWh
  750. factor: number | null; // Power factor (0-1)
  751. apparent_power: number | null; // VA
  752. reactive_power: number | null; // VAr
  753. }
  754. export interface SmartPlugStatus {
  755. state: string | null;
  756. reachable: boolean;
  757. device_name: string | null;
  758. energy: SmartPlugEnergy | null;
  759. }
  760. export interface SmartPlugTestResult {
  761. success: boolean;
  762. state: string | null;
  763. device_name: string | null;
  764. }
  765. // Tasmota Discovery types
  766. export interface TasmotaScanStatus {
  767. running: boolean;
  768. scanned: number;
  769. total: number;
  770. }
  771. export interface DiscoveredTasmotaDevice {
  772. ip_address: string;
  773. name: string;
  774. module: number | null;
  775. state: string | null;
  776. discovered_at: string | null;
  777. }
  778. // Print Queue types
  779. export interface PrintQueueItem {
  780. id: number;
  781. printer_id: number | null; // null = unassigned
  782. archive_id: number;
  783. position: number;
  784. scheduled_time: string | null;
  785. require_previous_success: boolean;
  786. auto_off_after: boolean;
  787. manual_start: boolean; // Requires manual trigger to start (staged)
  788. ams_mapping: number[] | null; // AMS slot mapping for multi-color prints
  789. plate_id: number | null; // Plate ID for multi-plate 3MF files
  790. // Print options
  791. bed_levelling: boolean;
  792. flow_cali: boolean;
  793. vibration_cali: boolean;
  794. layer_inspect: boolean;
  795. timelapse: boolean;
  796. use_ams: boolean;
  797. status: 'pending' | 'printing' | 'completed' | 'failed' | 'skipped' | 'cancelled';
  798. started_at: string | null;
  799. completed_at: string | null;
  800. error_message: string | null;
  801. created_at: string;
  802. archive_name?: string | null;
  803. archive_thumbnail?: string | null;
  804. printer_name?: string | null;
  805. print_time_seconds?: number | null; // Estimated print time from archive
  806. }
  807. export interface PrintQueueItemCreate {
  808. printer_id?: number | null; // null = unassigned
  809. archive_id: number;
  810. scheduled_time?: string | null;
  811. require_previous_success?: boolean;
  812. auto_off_after?: boolean;
  813. manual_start?: boolean; // Requires manual trigger to start (staged)
  814. ams_mapping?: number[] | null; // AMS slot mapping for multi-color prints
  815. plate_id?: number | null; // Plate ID for multi-plate 3MF files
  816. // Print options
  817. bed_levelling?: boolean;
  818. flow_cali?: boolean;
  819. vibration_cali?: boolean;
  820. layer_inspect?: boolean;
  821. timelapse?: boolean;
  822. use_ams?: boolean;
  823. }
  824. export interface PrintQueueItemUpdate {
  825. printer_id?: number | null; // null = unassign
  826. position?: number;
  827. scheduled_time?: string | null;
  828. require_previous_success?: boolean;
  829. auto_off_after?: boolean;
  830. manual_start?: boolean;
  831. ams_mapping?: number[];
  832. plate_id?: number | null; // Plate ID for multi-plate 3MF files
  833. // Print options
  834. bed_levelling?: boolean;
  835. flow_cali?: boolean;
  836. vibration_cali?: boolean;
  837. layer_inspect?: boolean;
  838. timelapse?: boolean;
  839. use_ams?: boolean;
  840. }
  841. // MQTT Logging types
  842. export interface MQTTLogEntry {
  843. timestamp: string;
  844. topic: string;
  845. direction: 'in' | 'out';
  846. payload: Record<string, unknown>;
  847. }
  848. export interface MQTTLogsResponse {
  849. logging_enabled: boolean;
  850. logs: MQTTLogEntry[];
  851. }
  852. // K-Profile types
  853. export interface KProfile {
  854. slot_id: number;
  855. extruder_id: number;
  856. nozzle_id: string;
  857. nozzle_diameter: string;
  858. filament_id: string;
  859. name: string;
  860. k_value: string;
  861. n_coef: string;
  862. ams_id: number;
  863. tray_id: number;
  864. setting_id: string | null;
  865. }
  866. export interface KProfileCreate {
  867. slot_id?: number; // Storage slot, 0 for new profiles
  868. extruder_id?: number;
  869. nozzle_id: string;
  870. nozzle_diameter: string;
  871. filament_id: string;
  872. name: string;
  873. k_value: string;
  874. n_coef?: string;
  875. ams_id?: number;
  876. tray_id?: number;
  877. setting_id?: string | null;
  878. }
  879. export interface KProfileDelete {
  880. slot_id: number; // cali_idx - calibration index to delete
  881. extruder_id: number;
  882. nozzle_id: string; // e.g., "HH00-0.4"
  883. nozzle_diameter: string; // e.g., "0.4"
  884. filament_id: string; // Bambu filament identifier
  885. setting_id?: string | null; // Setting ID (for X1C series)
  886. }
  887. export interface KProfilesResponse {
  888. profiles: KProfile[];
  889. nozzle_diameter: string;
  890. }
  891. export interface KProfileNote {
  892. setting_id: string;
  893. note: string;
  894. }
  895. export interface KProfileNotesResponse {
  896. notes: Record<string, string>; // setting_id -> note
  897. }
  898. // Slot Preset Mapping
  899. export interface SlotPresetMapping {
  900. ams_id: number;
  901. tray_id: number;
  902. preset_id: string;
  903. preset_name: string;
  904. }
  905. // Filament types
  906. export interface Filament {
  907. id: number;
  908. name: string;
  909. type: string; // PLA, PETG, ABS, etc.
  910. brand: string | null;
  911. color: string | null;
  912. color_hex: string | null;
  913. cost_per_kg: number;
  914. spool_weight_g: number;
  915. currency: string;
  916. density: number | null;
  917. print_temp_min: number | null;
  918. print_temp_max: number | null;
  919. bed_temp_min: number | null;
  920. bed_temp_max: number | null;
  921. created_at: string;
  922. updated_at: string;
  923. }
  924. // Notification Provider types
  925. export type ProviderType = 'callmebot' | 'ntfy' | 'pushover' | 'telegram' | 'email' | 'discord' | 'webhook';
  926. export interface NotificationProvider {
  927. id: number;
  928. name: string;
  929. provider_type: ProviderType;
  930. enabled: boolean;
  931. config: Record<string, unknown>;
  932. // Print lifecycle events
  933. on_print_start: boolean;
  934. on_print_complete: boolean;
  935. on_print_failed: boolean;
  936. on_print_stopped: boolean;
  937. on_print_progress: boolean;
  938. // Printer status events
  939. on_printer_offline: boolean;
  940. on_printer_error: boolean;
  941. on_filament_low: boolean;
  942. on_maintenance_due: boolean;
  943. // AMS environmental alarms (regular AMS)
  944. on_ams_humidity_high: boolean;
  945. on_ams_temperature_high: boolean;
  946. // AMS-HT environmental alarms
  947. on_ams_ht_humidity_high: boolean;
  948. on_ams_ht_temperature_high: boolean;
  949. // Quiet hours
  950. quiet_hours_enabled: boolean;
  951. quiet_hours_start: string | null;
  952. quiet_hours_end: string | null;
  953. // Daily digest
  954. daily_digest_enabled: boolean;
  955. daily_digest_time: string | null;
  956. // Printer filter
  957. printer_id: number | null;
  958. // Status tracking
  959. last_success: string | null;
  960. last_error: string | null;
  961. last_error_at: string | null;
  962. // Timestamps
  963. created_at: string;
  964. updated_at: string;
  965. }
  966. export interface NotificationProviderCreate {
  967. name: string;
  968. provider_type: ProviderType;
  969. enabled?: boolean;
  970. config: Record<string, unknown>;
  971. // Print lifecycle events
  972. on_print_start?: boolean;
  973. on_print_complete?: boolean;
  974. on_print_failed?: boolean;
  975. on_print_stopped?: boolean;
  976. on_print_progress?: boolean;
  977. // Printer status events
  978. on_printer_offline?: boolean;
  979. on_printer_error?: boolean;
  980. on_filament_low?: boolean;
  981. on_maintenance_due?: boolean;
  982. // AMS environmental alarms (regular AMS)
  983. on_ams_humidity_high?: boolean;
  984. on_ams_temperature_high?: boolean;
  985. // AMS-HT environmental alarms
  986. on_ams_ht_humidity_high?: boolean;
  987. on_ams_ht_temperature_high?: boolean;
  988. // Quiet hours
  989. quiet_hours_enabled?: boolean;
  990. quiet_hours_start?: string | null;
  991. quiet_hours_end?: string | null;
  992. // Daily digest
  993. daily_digest_enabled?: boolean;
  994. daily_digest_time?: string | null;
  995. // Printer filter
  996. printer_id?: number | null;
  997. }
  998. export interface NotificationProviderUpdate {
  999. name?: string;
  1000. provider_type?: ProviderType;
  1001. enabled?: boolean;
  1002. config?: Record<string, unknown>;
  1003. // Print lifecycle events
  1004. on_print_start?: boolean;
  1005. on_print_complete?: boolean;
  1006. on_print_failed?: boolean;
  1007. on_print_stopped?: boolean;
  1008. on_print_progress?: boolean;
  1009. // Printer status events
  1010. on_printer_offline?: boolean;
  1011. on_printer_error?: boolean;
  1012. on_filament_low?: boolean;
  1013. on_maintenance_due?: boolean;
  1014. // AMS environmental alarms (regular AMS)
  1015. on_ams_humidity_high?: boolean;
  1016. on_ams_temperature_high?: boolean;
  1017. // AMS-HT environmental alarms
  1018. on_ams_ht_humidity_high?: boolean;
  1019. on_ams_ht_temperature_high?: boolean;
  1020. // Quiet hours
  1021. quiet_hours_enabled?: boolean;
  1022. quiet_hours_start?: string | null;
  1023. quiet_hours_end?: string | null;
  1024. // Daily digest
  1025. daily_digest_enabled?: boolean;
  1026. daily_digest_time?: string | null;
  1027. // Printer filter
  1028. printer_id?: number | null;
  1029. }
  1030. export interface NotificationTestRequest {
  1031. provider_type: ProviderType;
  1032. config: Record<string, unknown>;
  1033. }
  1034. export interface NotificationTestResponse {
  1035. success: boolean;
  1036. message: string;
  1037. }
  1038. // Provider-specific config types for reference
  1039. export interface CallMeBotConfig {
  1040. phone: string;
  1041. apikey: string;
  1042. }
  1043. export interface NtfyConfig {
  1044. server?: string;
  1045. topic: string;
  1046. auth_token?: string | null;
  1047. }
  1048. export interface PushoverConfig {
  1049. user_key: string;
  1050. app_token: string;
  1051. priority?: number;
  1052. }
  1053. export interface TelegramConfig {
  1054. bot_token: string;
  1055. chat_id: string;
  1056. }
  1057. export interface EmailConfig {
  1058. smtp_server: string;
  1059. smtp_port?: number;
  1060. username: string;
  1061. password: string;
  1062. from_email: string;
  1063. to_email: string;
  1064. use_tls?: boolean;
  1065. }
  1066. // Notification Template types
  1067. export interface NotificationTemplate {
  1068. id: number;
  1069. event_type: string;
  1070. name: string;
  1071. title_template: string;
  1072. body_template: string;
  1073. is_default: boolean;
  1074. created_at: string;
  1075. updated_at: string;
  1076. }
  1077. export interface NotificationTemplateUpdate {
  1078. title_template?: string;
  1079. body_template?: string;
  1080. }
  1081. export interface EventVariablesResponse {
  1082. event_type: string;
  1083. event_name: string;
  1084. variables: string[];
  1085. }
  1086. export interface TemplatePreviewRequest {
  1087. event_type: string;
  1088. title_template: string;
  1089. body_template: string;
  1090. }
  1091. export interface TemplatePreviewResponse {
  1092. title: string;
  1093. body: string;
  1094. }
  1095. // Notification Log types
  1096. export interface NotificationLogEntry {
  1097. id: number;
  1098. provider_id: number;
  1099. provider_name: string | null;
  1100. provider_type: string | null;
  1101. event_type: string;
  1102. title: string;
  1103. message: string;
  1104. success: boolean;
  1105. error_message: string | null;
  1106. printer_id: number | null;
  1107. printer_name: string | null;
  1108. created_at: string;
  1109. }
  1110. export interface NotificationLogStats {
  1111. total: number;
  1112. success_count: number;
  1113. failure_count: number;
  1114. by_event_type: Record<string, number>;
  1115. by_provider: Record<string, number>;
  1116. }
  1117. // Spoolman types
  1118. export interface SpoolmanStatus {
  1119. enabled: boolean;
  1120. connected: boolean;
  1121. url: string | null;
  1122. }
  1123. export interface SkippedSpool {
  1124. location: string;
  1125. reason: string;
  1126. filament_type: string | null;
  1127. color: string | null;
  1128. }
  1129. export interface SpoolmanSyncResult {
  1130. success: boolean;
  1131. synced_count: number;
  1132. skipped_count: number;
  1133. skipped: SkippedSpool[];
  1134. errors: string[];
  1135. }
  1136. export interface UnlinkedSpool {
  1137. id: number;
  1138. filament_name: string | null;
  1139. filament_material: string | null;
  1140. filament_color_hex: string | null;
  1141. remaining_weight: number | null;
  1142. location: string | null;
  1143. }
  1144. // Update types
  1145. export interface VersionInfo {
  1146. version: string;
  1147. repo: string;
  1148. }
  1149. export interface UpdateCheckResult {
  1150. update_available: boolean;
  1151. current_version: string;
  1152. latest_version: string | null;
  1153. release_name?: string;
  1154. release_notes?: string;
  1155. release_url?: string;
  1156. published_at?: string;
  1157. error?: string;
  1158. message?: string;
  1159. is_docker?: boolean;
  1160. update_method?: 'docker' | 'git';
  1161. }
  1162. export interface UpdateStatus {
  1163. status: 'idle' | 'checking' | 'downloading' | 'installing' | 'complete' | 'error';
  1164. progress: number;
  1165. message: string;
  1166. error: string | null;
  1167. }
  1168. // Maintenance types
  1169. export interface MaintenanceType {
  1170. id: number;
  1171. name: string;
  1172. description: string | null;
  1173. default_interval_hours: number;
  1174. interval_type: 'hours' | 'days'; // "hours" = print hours, "days" = calendar days
  1175. icon: string | null;
  1176. wiki_url: string | null; // Documentation link
  1177. is_system: boolean;
  1178. created_at: string;
  1179. }
  1180. export interface MaintenanceTypeCreate {
  1181. name: string;
  1182. description?: string | null;
  1183. default_interval_hours?: number;
  1184. interval_type?: 'hours' | 'days';
  1185. icon?: string | null;
  1186. wiki_url?: string | null;
  1187. }
  1188. export interface MaintenanceStatus {
  1189. id: number;
  1190. printer_id: number;
  1191. printer_name: string;
  1192. printer_model: string | null;
  1193. maintenance_type_id: number;
  1194. maintenance_type_name: string;
  1195. maintenance_type_icon: string | null;
  1196. maintenance_type_wiki_url: string | null; // Custom wiki URL from type
  1197. enabled: boolean;
  1198. interval_hours: number; // For hours type: print hours; for days type: number of days
  1199. interval_type: 'hours' | 'days';
  1200. current_hours: number;
  1201. hours_since_maintenance: number;
  1202. hours_until_due: number;
  1203. days_since_maintenance: number | null; // For days type
  1204. days_until_due: number | null; // For days type
  1205. is_due: boolean;
  1206. is_warning: boolean;
  1207. last_performed_at: string | null;
  1208. }
  1209. export interface PrinterMaintenanceOverview {
  1210. printer_id: number;
  1211. printer_name: string;
  1212. printer_model: string | null;
  1213. total_print_hours: number;
  1214. maintenance_items: MaintenanceStatus[];
  1215. due_count: number;
  1216. warning_count: number;
  1217. }
  1218. export interface MaintenanceHistory {
  1219. id: number;
  1220. printer_maintenance_id: number;
  1221. performed_at: string;
  1222. hours_at_maintenance: number;
  1223. notes: string | null;
  1224. }
  1225. export interface MaintenanceSummary {
  1226. total_due: number;
  1227. total_warning: number;
  1228. printers_with_issues: Array<{
  1229. printer_id: number;
  1230. printer_name: string;
  1231. due_count: number;
  1232. warning_count: number;
  1233. }>;
  1234. }
  1235. // External Links (sidebar)
  1236. export interface ExternalLink {
  1237. id: number;
  1238. name: string;
  1239. url: string;
  1240. icon: string;
  1241. custom_icon: string | null;
  1242. sort_order: number;
  1243. created_at: string;
  1244. updated_at: string;
  1245. }
  1246. export interface ExternalLinkCreate {
  1247. name: string;
  1248. url: string;
  1249. icon: string;
  1250. }
  1251. export interface ExternalLinkUpdate {
  1252. name?: string;
  1253. url?: string;
  1254. icon?: string;
  1255. }
  1256. // API functions
  1257. export const api = {
  1258. // Printers
  1259. getPrinters: () => request<Printer[]>('/printers/'),
  1260. getPrinter: (id: number) => request<Printer>(`/printers/${id}`),
  1261. createPrinter: (data: PrinterCreate) =>
  1262. request<Printer>('/printers/', {
  1263. method: 'POST',
  1264. body: JSON.stringify(data),
  1265. }),
  1266. updatePrinter: (id: number, data: Partial<PrinterCreate>) =>
  1267. request<Printer>(`/printers/${id}`, {
  1268. method: 'PATCH',
  1269. body: JSON.stringify(data),
  1270. }),
  1271. deletePrinter: (id: number, deleteArchives: boolean = true) =>
  1272. request<{ status: string; archives_deleted: boolean }>(
  1273. `/printers/${id}?delete_archives=${deleteArchives}`,
  1274. { method: 'DELETE' }
  1275. ),
  1276. getPrinterStatus: (id: number) =>
  1277. request<PrinterStatus>(`/printers/${id}/status`),
  1278. refreshPrinterStatus: (id: number) =>
  1279. request<{ status: string }>(`/printers/${id}/refresh-status`, {
  1280. method: 'POST',
  1281. }),
  1282. connectPrinter: (id: number) =>
  1283. request<{ connected: boolean }>(`/printers/${id}/connect`, {
  1284. method: 'POST',
  1285. }),
  1286. disconnectPrinter: (id: number) =>
  1287. request<{ connected: boolean }>(`/printers/${id}/disconnect`, {
  1288. method: 'POST',
  1289. }),
  1290. // Print Control
  1291. stopPrint: (printerId: number) =>
  1292. request<{ success: boolean; message: string }>(`/printers/${printerId}/print/stop`, {
  1293. method: 'POST',
  1294. }),
  1295. pausePrint: (printerId: number) =>
  1296. request<{ success: boolean; message: string }>(`/printers/${printerId}/print/pause`, {
  1297. method: 'POST',
  1298. }),
  1299. resumePrint: (printerId: number) =>
  1300. request<{ success: boolean; message: string }>(`/printers/${printerId}/print/resume`, {
  1301. method: 'POST',
  1302. }),
  1303. // Chamber Light Control
  1304. setChamberLight: (printerId: number, on: boolean) =>
  1305. request<{ success: boolean; message: string }>(`/printers/${printerId}/chamber-light?on=${on}`, {
  1306. method: 'POST',
  1307. }),
  1308. // Skip Objects
  1309. getPrintableObjects: (printerId: number) =>
  1310. request<{
  1311. objects: Array<{ id: number; name: string; x: number | null; y: number | null; skipped: boolean }>;
  1312. total: number;
  1313. skipped_count: number;
  1314. is_printing: boolean;
  1315. bbox_all: [number, number, number, number] | null;
  1316. }>(`/printers/${printerId}/print/objects`),
  1317. skipObjects: (printerId: number, objectIds: number[]) =>
  1318. request<{ success: boolean; message: string; skipped_objects: number[] }>(
  1319. `/printers/${printerId}/print/skip-objects`,
  1320. {
  1321. method: 'POST',
  1322. body: JSON.stringify(objectIds),
  1323. }
  1324. ),
  1325. // AMS Control
  1326. refreshAmsSlot: (printerId: number, amsId: number, slotId: number) =>
  1327. request<{ success: boolean; message: string }>(
  1328. `/printers/${printerId}/ams/${amsId}/slot/${slotId}/refresh`,
  1329. { method: 'POST' }
  1330. ),
  1331. // MQTT Debug Logging
  1332. enableMQTTLogging: (printerId: number) =>
  1333. request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/enable`, {
  1334. method: 'POST',
  1335. }),
  1336. disableMQTTLogging: (printerId: number) =>
  1337. request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/disable`, {
  1338. method: 'POST',
  1339. }),
  1340. getMQTTLogs: (printerId: number) =>
  1341. request<MQTTLogsResponse>(`/printers/${printerId}/logging`),
  1342. clearMQTTLogs: (printerId: number) =>
  1343. request<{ status: string }>(`/printers/${printerId}/logging`, {
  1344. method: 'DELETE',
  1345. }),
  1346. // Printer File Manager
  1347. getPrinterFiles: (printerId: number, path = '/') =>
  1348. request<{
  1349. path: string;
  1350. files: Array<{
  1351. name: string;
  1352. is_directory: boolean;
  1353. size: number;
  1354. path: string;
  1355. }>;
  1356. }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`),
  1357. getPrinterFileDownloadUrl: (printerId: number, path: string) =>
  1358. `${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`,
  1359. deletePrinterFile: (printerId: number, path: string) =>
  1360. request<{ status: string; path: string }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`, {
  1361. method: 'DELETE',
  1362. }),
  1363. getPrinterStorage: (printerId: number) =>
  1364. request<{ used_bytes: number | null; free_bytes: number | null }>(`/printers/${printerId}/storage`),
  1365. // Archives
  1366. getArchives: (printerId?: number, projectId?: number, limit = 50, offset = 0) => {
  1367. const params = new URLSearchParams();
  1368. if (printerId) params.set('printer_id', String(printerId));
  1369. if (projectId) params.set('project_id', String(projectId));
  1370. params.set('limit', String(limit));
  1371. params.set('offset', String(offset));
  1372. return request<Archive[]>(`/archives/?${params}`);
  1373. },
  1374. getArchive: (id: number) => request<Archive>(`/archives/${id}`),
  1375. searchArchives: (query: string, options?: {
  1376. printerId?: number;
  1377. projectId?: number;
  1378. status?: string;
  1379. limit?: number;
  1380. offset?: number;
  1381. }) => {
  1382. const params = new URLSearchParams();
  1383. params.set('q', query);
  1384. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1385. if (options?.projectId) params.set('project_id', String(options.projectId));
  1386. if (options?.status) params.set('status', options.status);
  1387. if (options?.limit) params.set('limit', String(options.limit));
  1388. if (options?.offset) params.set('offset', String(options.offset));
  1389. return request<Archive[]>(`/archives/search?${params}`);
  1390. },
  1391. rebuildSearchIndex: () => request<{ message: string }>('/archives/search/rebuild-index', { method: 'POST' }),
  1392. updateArchive: (id: number, data: {
  1393. printer_id?: number | null;
  1394. project_id?: number | null;
  1395. print_name?: string;
  1396. is_favorite?: boolean;
  1397. tags?: string;
  1398. notes?: string;
  1399. cost?: number;
  1400. failure_reason?: string | null;
  1401. status?: string;
  1402. quantity?: number;
  1403. }) =>
  1404. request<Archive>(`/archives/${id}`, {
  1405. method: 'PATCH',
  1406. body: JSON.stringify(data),
  1407. }),
  1408. toggleFavorite: (id: number) =>
  1409. request<Archive>(`/archives/${id}/favorite`, { method: 'POST' }),
  1410. deleteArchive: (id: number) =>
  1411. request<void>(`/archives/${id}`, { method: 'DELETE' }),
  1412. getArchiveStats: () => request<ArchiveStats>('/archives/stats'),
  1413. getFailureAnalysis: (options?: { days?: number; printerId?: number; projectId?: number }) => {
  1414. const params = new URLSearchParams();
  1415. if (options?.days) params.set('days', String(options.days));
  1416. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1417. if (options?.projectId) params.set('project_id', String(options.projectId));
  1418. return request<FailureAnalysis>(`/archives/analysis/failures?${params}`);
  1419. },
  1420. compareArchives: (archiveIds: number[]) =>
  1421. request<ArchiveComparison>(`/archives/compare?archive_ids=${archiveIds.join(',')}`),
  1422. findSimilarArchives: (archiveId: number, limit = 10) =>
  1423. request<SimilarArchive[]>(`/archives/${archiveId}/similar?limit=${limit}`),
  1424. exportArchives: async (options?: {
  1425. format?: 'csv' | 'xlsx';
  1426. fields?: string[];
  1427. printerId?: number;
  1428. projectId?: number;
  1429. status?: string;
  1430. dateFrom?: string;
  1431. dateTo?: string;
  1432. search?: string;
  1433. }): Promise<{ blob: Blob; filename: string }> => {
  1434. const params = new URLSearchParams();
  1435. if (options?.format) params.set('format', options.format);
  1436. if (options?.fields) params.set('fields', options.fields.join(','));
  1437. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1438. if (options?.projectId) params.set('project_id', String(options.projectId));
  1439. if (options?.status) params.set('status', options.status);
  1440. if (options?.dateFrom) params.set('date_from', options.dateFrom);
  1441. if (options?.dateTo) params.set('date_to', options.dateTo);
  1442. if (options?.search) params.set('search', options.search);
  1443. const response = await fetch(`${API_BASE}/archives/export?${params}`);
  1444. if (!response.ok) {
  1445. const error = await response.json().catch(() => ({}));
  1446. throw new Error(error.detail || `HTTP ${response.status}`);
  1447. }
  1448. const contentDisposition = response.headers.get('Content-Disposition');
  1449. let filename = options?.format === 'xlsx' ? 'archives_export.xlsx' : 'archives_export.csv';
  1450. if (contentDisposition) {
  1451. const match = contentDisposition.match(/filename="?([^"]+)"?/);
  1452. if (match) filename = match[1];
  1453. }
  1454. const blob = await response.blob();
  1455. return { blob, filename };
  1456. },
  1457. exportStats: async (options?: {
  1458. format?: 'csv' | 'xlsx';
  1459. days?: number;
  1460. printerId?: number;
  1461. projectId?: number;
  1462. }): Promise<{ blob: Blob; filename: string }> => {
  1463. const params = new URLSearchParams();
  1464. if (options?.format) params.set('format', options.format);
  1465. if (options?.days) params.set('days', String(options.days));
  1466. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1467. if (options?.projectId) params.set('project_id', String(options.projectId));
  1468. const response = await fetch(`${API_BASE}/archives/stats/export?${params}`);
  1469. if (!response.ok) {
  1470. const error = await response.json().catch(() => ({}));
  1471. throw new Error(error.detail || `HTTP ${response.status}`);
  1472. }
  1473. const contentDisposition = response.headers.get('Content-Disposition');
  1474. let filename = options?.format === 'xlsx' ? 'stats_export.xlsx' : 'stats_export.csv';
  1475. if (contentDisposition) {
  1476. const match = contentDisposition.match(/filename="?([^"]+)"?/);
  1477. if (match) filename = match[1];
  1478. }
  1479. const blob = await response.blob();
  1480. return { blob, filename };
  1481. },
  1482. getArchiveDuplicates: (id: number) =>
  1483. request<{ duplicates: ArchiveDuplicate[]; count: number }>(`/archives/${id}/duplicates`),
  1484. backfillContentHashes: () =>
  1485. request<{ updated: number; errors: Array<{ id: number; error: string }> }>('/archives/backfill-hashes', {
  1486. method: 'POST',
  1487. }),
  1488. getArchiveThumbnail: (id: number) => `${API_BASE}/archives/${id}/thumbnail?v=${Date.now()}`,
  1489. getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`,
  1490. getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`,
  1491. getArchivePlatePreview: (id: number) => `${API_BASE}/archives/${id}/plate-preview`,
  1492. getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse?v=${Date.now()}`,
  1493. scanArchiveTimelapse: (id: number) =>
  1494. request<{
  1495. status: string;
  1496. message: string;
  1497. filename?: string;
  1498. available_files?: Array<{ name: string; path: string; size: number; mtime: string | null }>;
  1499. }>(`/archives/${id}/timelapse/scan`, {
  1500. method: 'POST',
  1501. }),
  1502. selectArchiveTimelapse: (id: number, filename: string) =>
  1503. request<{ status: string; message: string; filename: string }>(
  1504. `/archives/${id}/timelapse/select?filename=${encodeURIComponent(filename)}`,
  1505. { method: 'POST' }
  1506. ),
  1507. uploadArchiveTimelapse: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
  1508. const formData = new FormData();
  1509. formData.append('file', file);
  1510. const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/upload`, {
  1511. method: 'POST',
  1512. body: formData,
  1513. });
  1514. if (!response.ok) {
  1515. const error = await response.json().catch(() => ({}));
  1516. throw new Error(error.detail || `HTTP ${response.status}`);
  1517. }
  1518. return response.json();
  1519. },
  1520. // Timelapse Editor
  1521. getTimelapseInfo: (archiveId: number) =>
  1522. request<{
  1523. duration: number;
  1524. width: number;
  1525. height: number;
  1526. fps: number;
  1527. codec: string;
  1528. file_size: number;
  1529. has_audio: boolean;
  1530. }>(`/archives/${archiveId}/timelapse/info`),
  1531. getTimelapseThumbnails: (archiveId: number, count: number = 10) =>
  1532. request<{
  1533. thumbnails: string[];
  1534. timestamps: number[];
  1535. }>(`/archives/${archiveId}/timelapse/thumbnails?count=${count}`),
  1536. processTimelapse: async (
  1537. archiveId: number,
  1538. params: {
  1539. trimStart?: number;
  1540. trimEnd?: number;
  1541. speed?: number;
  1542. saveMode: 'replace' | 'new';
  1543. outputFilename?: string;
  1544. },
  1545. audioFile?: File
  1546. ): Promise<{ status: string; output_path: string | null; message: string }> => {
  1547. const formData = new FormData();
  1548. formData.append('trim_start', String(params.trimStart ?? 0));
  1549. if (params.trimEnd !== undefined) {
  1550. formData.append('trim_end', String(params.trimEnd));
  1551. }
  1552. formData.append('speed', String(params.speed ?? 1));
  1553. formData.append('save_mode', params.saveMode);
  1554. if (params.outputFilename) {
  1555. formData.append('output_filename', params.outputFilename);
  1556. }
  1557. if (audioFile) {
  1558. formData.append('audio', audioFile);
  1559. }
  1560. const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/process`, {
  1561. method: 'POST',
  1562. body: formData,
  1563. });
  1564. if (!response.ok) {
  1565. const error = await response.json().catch(() => ({}));
  1566. throw new Error(error.detail || `HTTP ${response.status}`);
  1567. }
  1568. return response.json();
  1569. },
  1570. // Photos
  1571. getArchivePhotoUrl: (archiveId: number, filename: string) =>
  1572. `${API_BASE}/archives/${archiveId}/photos/${encodeURIComponent(filename)}`,
  1573. uploadArchivePhoto: async (archiveId: number, file: File): Promise<{ status: string; filename: string; photos: string[] }> => {
  1574. const formData = new FormData();
  1575. formData.append('file', file);
  1576. const response = await fetch(`${API_BASE}/archives/${archiveId}/photos`, {
  1577. method: 'POST',
  1578. body: formData,
  1579. });
  1580. if (!response.ok) {
  1581. const error = await response.json().catch(() => ({}));
  1582. throw new Error(error.detail || `HTTP ${response.status}`);
  1583. }
  1584. return response.json();
  1585. },
  1586. deleteArchivePhoto: (archiveId: number, filename: string) =>
  1587. request<{ status: string; photos: string[] | null }>(`/archives/${archiveId}/photos/${encodeURIComponent(filename)}`, {
  1588. method: 'DELETE',
  1589. }),
  1590. // Source 3MF (original slicer project file)
  1591. getSource3mfDownloadUrl: (archiveId: number) =>
  1592. `${API_BASE}/archives/${archiveId}/source`,
  1593. getSource3mfForSlicer: (archiveId: number, filename: string) =>
  1594. `${API_BASE}/archives/${archiveId}/source/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
  1595. uploadSource3mf: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
  1596. const formData = new FormData();
  1597. formData.append('file', file);
  1598. const response = await fetch(`${API_BASE}/archives/${archiveId}/source`, {
  1599. method: 'POST',
  1600. body: formData,
  1601. });
  1602. if (!response.ok) {
  1603. const error = await response.json().catch(() => ({}));
  1604. throw new Error(error.detail || `HTTP ${response.status}`);
  1605. }
  1606. return response.json();
  1607. },
  1608. deleteSource3mf: (archiveId: number) =>
  1609. request<{ status: string }>(`/archives/${archiveId}/source`, {
  1610. method: 'DELETE',
  1611. }),
  1612. // F3D (Fusion 360 design file)
  1613. getF3dDownloadUrl: (archiveId: number) =>
  1614. `${API_BASE}/archives/${archiveId}/f3d`,
  1615. uploadF3d: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
  1616. const formData = new FormData();
  1617. formData.append('file', file);
  1618. const response = await fetch(`${API_BASE}/archives/${archiveId}/f3d`, {
  1619. method: 'POST',
  1620. body: formData,
  1621. });
  1622. if (!response.ok) {
  1623. const error = await response.json().catch(() => ({}));
  1624. throw new Error(error.detail || `HTTP ${response.status}`);
  1625. }
  1626. return response.json();
  1627. },
  1628. deleteF3d: (archiveId: number) =>
  1629. request<{ status: string }>(`/archives/${archiveId}/f3d`, {
  1630. method: 'DELETE',
  1631. }),
  1632. // QR Code
  1633. getArchiveQRCodeUrl: (archiveId: number, size = 200) =>
  1634. `${API_BASE}/archives/${archiveId}/qrcode?size=${size}`,
  1635. getArchiveCapabilities: (id: number) =>
  1636. request<{
  1637. has_model: boolean;
  1638. has_gcode: boolean;
  1639. has_source: boolean;
  1640. build_volume: { x: number; y: number; z: number };
  1641. filament_colors: string[];
  1642. }>(`/archives/${id}/capabilities`),
  1643. // Project Page
  1644. getArchiveProjectPage: (id: number) =>
  1645. request<{
  1646. title: string | null;
  1647. description: string | null;
  1648. designer: string | null;
  1649. designer_user_id: string | null;
  1650. license: string | null;
  1651. copyright: string | null;
  1652. creation_date: string | null;
  1653. modification_date: string | null;
  1654. origin: string | null;
  1655. profile_title: string | null;
  1656. profile_description: string | null;
  1657. profile_cover: string | null;
  1658. profile_user_id: string | null;
  1659. profile_user_name: string | null;
  1660. design_model_id: string | null;
  1661. design_profile_id: string | null;
  1662. design_region: string | null;
  1663. model_pictures: Array<{ name: string; path: string; url: string }>;
  1664. profile_pictures: Array<{ name: string; path: string; url: string }>;
  1665. thumbnails: Array<{ name: string; path: string; url: string }>;
  1666. }>(`/archives/${id}/project-page`),
  1667. updateArchiveProjectPage: (id: number, data: {
  1668. title?: string;
  1669. description?: string;
  1670. designer?: string;
  1671. license?: string;
  1672. copyright?: string;
  1673. profile_title?: string;
  1674. profile_description?: string;
  1675. }) =>
  1676. request(`/archives/${id}/project-page`, {
  1677. method: 'PATCH',
  1678. body: JSON.stringify(data),
  1679. }),
  1680. getArchiveProjectImageUrl: (archiveId: number, imagePath: string) =>
  1681. `${API_BASE}/archives/${archiveId}/project-image/${encodeURIComponent(imagePath)}`,
  1682. getArchiveForSlicer: (id: number, filename: string) =>
  1683. `${API_BASE}/archives/${id}/file/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
  1684. getArchivePlates: (archiveId: number) =>
  1685. request<{
  1686. archive_id: number;
  1687. filename: string;
  1688. plates: Array<{
  1689. index: number;
  1690. name: string | null;
  1691. objects: string[];
  1692. has_thumbnail: boolean;
  1693. thumbnail_url: string | null;
  1694. print_time_seconds: number | null;
  1695. filament_used_grams: number | null;
  1696. filaments: Array<{
  1697. slot_id: number;
  1698. type: string;
  1699. color: string;
  1700. used_grams: number;
  1701. used_meters: number;
  1702. }>;
  1703. }>;
  1704. is_multi_plate: boolean;
  1705. }>(`/archives/${archiveId}/plates`),
  1706. getArchiveFilamentRequirements: (archiveId: number, plateId?: number) =>
  1707. request<{
  1708. archive_id: number;
  1709. filename: string;
  1710. plate_id: number | null;
  1711. filaments: Array<{
  1712. slot_id: number;
  1713. type: string;
  1714. color: string;
  1715. used_grams: number;
  1716. used_meters: number;
  1717. }>;
  1718. }>(`/archives/${archiveId}/filament-requirements${plateId !== undefined ? `?plate_id=${plateId}` : ''}`),
  1719. reprintArchive: (
  1720. archiveId: number,
  1721. printerId: number,
  1722. options?: {
  1723. plate_id?: number;
  1724. ams_mapping?: number[];
  1725. timelapse?: boolean;
  1726. bed_levelling?: boolean;
  1727. flow_cali?: boolean;
  1728. vibration_cali?: boolean;
  1729. layer_inspect?: boolean;
  1730. use_ams?: boolean;
  1731. }
  1732. ) =>
  1733. request<{ status: string; printer_id: number; archive_id: number; filename: string }>(
  1734. `/archives/${archiveId}/reprint?printer_id=${printerId}`,
  1735. {
  1736. method: 'POST',
  1737. headers: options ? { 'Content-Type': 'application/json' } : undefined,
  1738. body: options ? JSON.stringify(options) : undefined,
  1739. }
  1740. ),
  1741. uploadArchive: async (file: File, printerId?: number): Promise<Archive> => {
  1742. const formData = new FormData();
  1743. formData.append('file', file);
  1744. const url = printerId
  1745. ? `${API_BASE}/archives/upload?printer_id=${printerId}`
  1746. : `${API_BASE}/archives/upload`;
  1747. const response = await fetch(url, {
  1748. method: 'POST',
  1749. body: formData,
  1750. });
  1751. if (!response.ok) {
  1752. const error = await response.json().catch(() => ({}));
  1753. throw new Error(error.detail || `HTTP ${response.status}`);
  1754. }
  1755. return response.json();
  1756. },
  1757. uploadArchivesBulk: async (files: File[], printerId?: number): Promise<BulkUploadResult> => {
  1758. const formData = new FormData();
  1759. files.forEach((file) => formData.append('files', file));
  1760. const url = printerId
  1761. ? `${API_BASE}/archives/upload-bulk?printer_id=${printerId}`
  1762. : `${API_BASE}/archives/upload-bulk`;
  1763. const response = await fetch(url, {
  1764. method: 'POST',
  1765. body: formData,
  1766. });
  1767. if (!response.ok) {
  1768. const error = await response.json().catch(() => ({}));
  1769. throw new Error(error.detail || `HTTP ${response.status}`);
  1770. }
  1771. return response.json();
  1772. },
  1773. // Settings
  1774. getSettings: () => request<AppSettings>('/settings/'),
  1775. updateSettings: (data: AppSettingsUpdate) =>
  1776. request<AppSettings>('/settings/', {
  1777. method: 'PUT',
  1778. body: JSON.stringify(data),
  1779. }),
  1780. getMQTTStatus: () => request<MQTTStatus>('/settings/mqtt/status'),
  1781. resetSettings: () =>
  1782. request<AppSettings>('/settings/reset', { method: 'POST' }),
  1783. exportBackup: async (categories?: Record<string, boolean>): Promise<{ blob: Blob; filename: string }> => {
  1784. const params = new URLSearchParams();
  1785. if (categories) {
  1786. if (categories.settings !== undefined) params.set('include_settings', String(categories.settings));
  1787. if (categories.notifications !== undefined) params.set('include_notifications', String(categories.notifications));
  1788. if (categories.templates !== undefined) params.set('include_templates', String(categories.templates));
  1789. if (categories.smart_plugs !== undefined) params.set('include_smart_plugs', String(categories.smart_plugs));
  1790. if (categories.external_links !== undefined) params.set('include_external_links', String(categories.external_links));
  1791. if (categories.printers !== undefined) params.set('include_printers', String(categories.printers));
  1792. if (categories.filaments !== undefined) params.set('include_filaments', String(categories.filaments));
  1793. if (categories.maintenance !== undefined) params.set('include_maintenance', String(categories.maintenance));
  1794. if (categories.archives !== undefined) params.set('include_archives', String(categories.archives));
  1795. if (categories.projects !== undefined) params.set('include_projects', String(categories.projects));
  1796. if (categories.pending_uploads !== undefined) params.set('include_pending_uploads', String(categories.pending_uploads));
  1797. if (categories.access_codes !== undefined) params.set('include_access_codes', String(categories.access_codes));
  1798. if (categories.api_keys !== undefined) params.set('include_api_keys', String(categories.api_keys));
  1799. }
  1800. const url = `${API_BASE}/settings/backup${params.toString() ? '?' + params.toString() : ''}`;
  1801. const response = await fetch(url);
  1802. // Check for errors
  1803. if (!response.ok) {
  1804. const errorText = await response.text();
  1805. throw new Error(errorText || `Backup failed with status ${response.status}`);
  1806. }
  1807. // Get filename from Content-Disposition header
  1808. const contentDisposition = response.headers.get('Content-Disposition');
  1809. let filename = 'bambuddy-backup.json';
  1810. if (contentDisposition) {
  1811. const match = contentDisposition.match(/filename=([^;]+)/);
  1812. if (match) filename = match[1].trim();
  1813. }
  1814. const blob = await response.blob();
  1815. return { blob, filename };
  1816. },
  1817. importBackup: async (file: File, overwrite = false) => {
  1818. const formData = new FormData();
  1819. formData.append('file', file);
  1820. const url = `${API_BASE}/settings/restore${overwrite ? '?overwrite=true' : ''}`;
  1821. const response = await fetch(url, {
  1822. method: 'POST',
  1823. body: formData,
  1824. });
  1825. return response.json() as Promise<{
  1826. success: boolean;
  1827. message: string;
  1828. restored?: Record<string, number>;
  1829. skipped?: Record<string, number>;
  1830. skipped_details?: Record<string, string[]>;
  1831. files_restored?: number;
  1832. total_skipped?: number;
  1833. }>;
  1834. },
  1835. checkFfmpeg: () =>
  1836. request<{ installed: boolean; path: string | null }>('/settings/check-ffmpeg'),
  1837. // Cloud
  1838. getCloudStatus: () => request<CloudAuthStatus>('/cloud/status'),
  1839. cloudLogin: (email: string, password: string, region = 'global') =>
  1840. request<CloudLoginResponse>('/cloud/login', {
  1841. method: 'POST',
  1842. body: JSON.stringify({ email, password, region }),
  1843. }),
  1844. cloudVerify: (email: string, code: string) =>
  1845. request<CloudLoginResponse>('/cloud/verify', {
  1846. method: 'POST',
  1847. body: JSON.stringify({ email, code }),
  1848. }),
  1849. cloudSetToken: (access_token: string) =>
  1850. request<CloudAuthStatus>('/cloud/token', {
  1851. method: 'POST',
  1852. body: JSON.stringify({ access_token }),
  1853. }),
  1854. cloudLogout: () =>
  1855. request<{ success: boolean }>('/cloud/logout', { method: 'POST' }),
  1856. getCloudSettings: (version = '02.04.00.70') =>
  1857. request<SlicerSettingsResponse>(`/cloud/settings?version=${version}`),
  1858. getCloudSettingDetail: (settingId: string) =>
  1859. request<SlicerSettingDetail>(`/cloud/settings/${settingId}`),
  1860. createCloudSetting: (data: SlicerSettingCreate) =>
  1861. request<SlicerSettingDetail>('/cloud/settings', {
  1862. method: 'POST',
  1863. body: JSON.stringify(data),
  1864. }),
  1865. updateCloudSetting: (settingId: string, data: SlicerSettingUpdate) =>
  1866. request<SlicerSettingDetail>(`/cloud/settings/${settingId}`, {
  1867. method: 'PUT',
  1868. body: JSON.stringify(data),
  1869. }),
  1870. deleteCloudSetting: (settingId: string) =>
  1871. request<SlicerSettingDeleteResponse>(`/cloud/settings/${settingId}`, {
  1872. method: 'DELETE',
  1873. }),
  1874. getCloudDevices: () => request<CloudDevice[]>('/cloud/devices'),
  1875. getCloudFields: (presetType: 'filament' | 'print' | 'process' | 'printer') =>
  1876. request<FieldDefinitionsResponse>(`/cloud/fields/${presetType}`),
  1877. getAllCloudFields: () =>
  1878. request<Record<string, FieldDefinitionsResponse>>('/cloud/fields'),
  1879. getFilamentInfo: (settingIds: string[]) =>
  1880. request<Record<string, { name: string; k: number | null }>>('/cloud/filament-info', {
  1881. method: 'POST',
  1882. body: JSON.stringify(settingIds),
  1883. }),
  1884. // Smart Plugs
  1885. getSmartPlugs: () => request<SmartPlug[]>('/smart-plugs/'),
  1886. getSmartPlug: (id: number) => request<SmartPlug>(`/smart-plugs/${id}`),
  1887. getSmartPlugByPrinter: (printerId: number) => request<SmartPlug | null>(`/smart-plugs/by-printer/${printerId}`),
  1888. createSmartPlug: (data: SmartPlugCreate) =>
  1889. request<SmartPlug>('/smart-plugs/', {
  1890. method: 'POST',
  1891. body: JSON.stringify(data),
  1892. }),
  1893. updateSmartPlug: (id: number, data: SmartPlugUpdate) =>
  1894. request<SmartPlug>(`/smart-plugs/${id}`, {
  1895. method: 'PATCH',
  1896. body: JSON.stringify(data),
  1897. }),
  1898. deleteSmartPlug: (id: number) =>
  1899. request<void>(`/smart-plugs/${id}`, { method: 'DELETE' }),
  1900. controlSmartPlug: (id: number, action: 'on' | 'off' | 'toggle') =>
  1901. request<{ success: boolean; action: string }>(`/smart-plugs/${id}/control`, {
  1902. method: 'POST',
  1903. body: JSON.stringify({ action }),
  1904. }),
  1905. getSmartPlugStatus: (id: number) =>
  1906. request<SmartPlugStatus>(`/smart-plugs/${id}/status`),
  1907. testSmartPlugConnection: (ip_address: string, username?: string | null, password?: string | null) =>
  1908. request<SmartPlugTestResult>('/smart-plugs/test-connection', {
  1909. method: 'POST',
  1910. body: JSON.stringify({ ip_address, username, password }),
  1911. }),
  1912. // Tasmota Discovery (auto-detects network)
  1913. startTasmotaScan: () =>
  1914. fetch(`${API_BASE}/smart-plugs/discover/scan`, { method: 'POST' })
  1915. .then(res => res.ok ? res.json() : res.json().then(e => { throw new Error(e.detail || `HTTP ${res.status}`); })),
  1916. getTasmotaScanStatus: () =>
  1917. request<TasmotaScanStatus>('/smart-plugs/discover/status'),
  1918. stopTasmotaScan: () =>
  1919. fetch(`${API_BASE}/smart-plugs/discover/stop`, { method: 'POST' })
  1920. .then(res => res.ok ? res.json() : res.json().then(e => { throw new Error(e.detail || `HTTP ${res.status}`); })),
  1921. getDiscoveredTasmotaDevices: () =>
  1922. request<DiscoveredTasmotaDevice[]>('/smart-plugs/discover/devices'),
  1923. // Home Assistant Integration
  1924. testHAConnection: (url: string, token: string) =>
  1925. request<HATestConnectionResult>('/smart-plugs/ha/test-connection', {
  1926. method: 'POST',
  1927. body: JSON.stringify({ url, token }),
  1928. }),
  1929. getHAEntities: () =>
  1930. request<HAEntity[]>('/smart-plugs/ha/entities'),
  1931. // Print Queue
  1932. getQueue: (printerId?: number, status?: string) => {
  1933. const params = new URLSearchParams();
  1934. if (printerId) params.set('printer_id', String(printerId));
  1935. if (status) params.set('status', status);
  1936. return request<PrintQueueItem[]>(`/queue/?${params}`);
  1937. },
  1938. getQueueItem: (id: number) => request<PrintQueueItem>(`/queue/${id}`),
  1939. addToQueue: (data: PrintQueueItemCreate) =>
  1940. request<PrintQueueItem>('/queue/', {
  1941. method: 'POST',
  1942. body: JSON.stringify(data),
  1943. }),
  1944. updateQueueItem: (id: number, data: PrintQueueItemUpdate) =>
  1945. request<PrintQueueItem>(`/queue/${id}`, {
  1946. method: 'PATCH',
  1947. body: JSON.stringify(data),
  1948. }),
  1949. removeFromQueue: (id: number) =>
  1950. request<{ message: string }>(`/queue/${id}`, { method: 'DELETE' }),
  1951. reorderQueue: (items: { id: number; position: number }[]) =>
  1952. request<{ message: string }>('/queue/reorder', {
  1953. method: 'POST',
  1954. body: JSON.stringify({ items }),
  1955. }),
  1956. cancelQueueItem: (id: number) =>
  1957. request<{ message: string }>(`/queue/${id}/cancel`, { method: 'POST' }),
  1958. stopQueueItem: (id: number) =>
  1959. request<{ message: string }>(`/queue/${id}/stop`, { method: 'POST' }),
  1960. startQueueItem: (id: number) =>
  1961. request<PrintQueueItem>(`/queue/${id}/start`, { method: 'POST' }),
  1962. // K-Profiles
  1963. getKProfiles: (printerId: number, nozzleDiameter = '0.4') =>
  1964. request<KProfilesResponse>(`/printers/${printerId}/kprofiles/?nozzle_diameter=${nozzleDiameter}`),
  1965. setKProfile: (printerId: number, profile: KProfileCreate) =>
  1966. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
  1967. method: 'POST',
  1968. body: JSON.stringify(profile),
  1969. }),
  1970. deleteKProfile: (printerId: number, profile: KProfileDelete) =>
  1971. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
  1972. method: 'DELETE',
  1973. body: JSON.stringify(profile),
  1974. }),
  1975. setKProfilesBatch: (printerId: number, profiles: KProfileCreate[]) =>
  1976. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/batch`, {
  1977. method: 'POST',
  1978. body: JSON.stringify(profiles),
  1979. }),
  1980. // K-Profile Notes (stored locally, not on printer)
  1981. getKProfileNotes: (printerId: number) =>
  1982. request<KProfileNotesResponse>(`/printers/${printerId}/kprofiles/notes`),
  1983. setKProfileNote: (printerId: number, settingId: string, note: string) =>
  1984. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/notes`, {
  1985. method: 'PUT',
  1986. body: JSON.stringify({ setting_id: settingId, note }),
  1987. }),
  1988. deleteKProfileNote: (printerId: number, settingId: string) =>
  1989. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/notes/${encodeURIComponent(settingId)}`, {
  1990. method: 'DELETE',
  1991. }),
  1992. // Slot Preset Mappings
  1993. getSlotPresets: (printerId: number) =>
  1994. request<Record<number, SlotPresetMapping>>(`/printers/${printerId}/slot-presets`),
  1995. getSlotPreset: (printerId: number, amsId: number, trayId: number) =>
  1996. request<SlotPresetMapping | null>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}`),
  1997. saveSlotPreset: (printerId: number, amsId: number, trayId: number, presetId: string, presetName: string) =>
  1998. request<SlotPresetMapping>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}?preset_id=${encodeURIComponent(presetId)}&preset_name=${encodeURIComponent(presetName)}`, {
  1999. method: 'PUT',
  2000. }),
  2001. deleteSlotPreset: (printerId: number, amsId: number, trayId: number) =>
  2002. request<{ success: boolean }>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}`, {
  2003. method: 'DELETE',
  2004. }),
  2005. configureAmsSlot: (
  2006. printerId: number,
  2007. amsId: number,
  2008. trayId: number,
  2009. config: {
  2010. tray_info_idx: string;
  2011. tray_type: string;
  2012. tray_sub_brands: string;
  2013. tray_color: string;
  2014. nozzle_temp_min: number;
  2015. nozzle_temp_max: number;
  2016. cali_idx: number;
  2017. nozzle_diameter: string;
  2018. setting_id?: string;
  2019. kprofile_filament_id?: string;
  2020. kprofile_setting_id?: string;
  2021. k_value?: number;
  2022. }
  2023. ) => {
  2024. const params = new URLSearchParams({
  2025. tray_info_idx: config.tray_info_idx,
  2026. tray_type: config.tray_type,
  2027. tray_sub_brands: config.tray_sub_brands,
  2028. tray_color: config.tray_color,
  2029. nozzle_temp_min: config.nozzle_temp_min.toString(),
  2030. nozzle_temp_max: config.nozzle_temp_max.toString(),
  2031. cali_idx: config.cali_idx.toString(),
  2032. nozzle_diameter: config.nozzle_diameter,
  2033. });
  2034. if (config.setting_id) {
  2035. params.set('setting_id', config.setting_id);
  2036. }
  2037. if (config.kprofile_filament_id) {
  2038. params.set('kprofile_filament_id', config.kprofile_filament_id);
  2039. }
  2040. if (config.kprofile_setting_id) {
  2041. params.set('kprofile_setting_id', config.kprofile_setting_id);
  2042. }
  2043. if (config.k_value !== undefined && config.k_value > 0) {
  2044. params.set('k_value', config.k_value.toString());
  2045. }
  2046. return request<{ success: boolean; message: string }>(
  2047. `/printers/${printerId}/slots/${amsId}/${trayId}/configure?${params}`,
  2048. { method: 'POST' }
  2049. );
  2050. },
  2051. resetAmsSlot: (printerId: number, amsId: number, trayId: number) =>
  2052. request<{ success: boolean; message: string }>(
  2053. `/printers/${printerId}/ams/${amsId}/tray/${trayId}/reset`,
  2054. { method: 'POST' }
  2055. ),
  2056. // Filaments
  2057. listFilaments: () => request<Filament[]>('/filaments/'),
  2058. getFilament: (id: number) => request<Filament>(`/filaments/${id}`),
  2059. getFilamentsByType: (type: string) => request<Filament[]>(`/filaments/by-type/${type}`),
  2060. // Notification Providers
  2061. getNotificationProviders: () => request<NotificationProvider[]>('/notifications/'),
  2062. getNotificationProvider: (id: number) => request<NotificationProvider>(`/notifications/${id}`),
  2063. createNotificationProvider: (data: NotificationProviderCreate) =>
  2064. request<NotificationProvider>('/notifications/', {
  2065. method: 'POST',
  2066. body: JSON.stringify(data),
  2067. }),
  2068. updateNotificationProvider: (id: number, data: NotificationProviderUpdate) =>
  2069. request<NotificationProvider>(`/notifications/${id}`, {
  2070. method: 'PATCH',
  2071. body: JSON.stringify(data),
  2072. }),
  2073. deleteNotificationProvider: (id: number) =>
  2074. request<{ message: string }>(`/notifications/${id}`, { method: 'DELETE' }),
  2075. testNotificationProvider: (id: number) =>
  2076. request<NotificationTestResponse>(`/notifications/${id}/test`, { method: 'POST' }),
  2077. testNotificationConfig: (data: NotificationTestRequest) =>
  2078. request<NotificationTestResponse>('/notifications/test-config', {
  2079. method: 'POST',
  2080. body: JSON.stringify(data),
  2081. }),
  2082. testAllNotificationProviders: () =>
  2083. request<{
  2084. tested: number;
  2085. success: number;
  2086. failed: number;
  2087. results: Array<{
  2088. provider_id: number;
  2089. provider_name: string;
  2090. provider_type: string;
  2091. success: boolean;
  2092. message: string;
  2093. }>;
  2094. }>('/notifications/test-all', { method: 'POST' }),
  2095. // Notification Templates
  2096. getNotificationTemplates: () => request<NotificationTemplate[]>('/notification-templates'),
  2097. getNotificationTemplate: (id: number) => request<NotificationTemplate>(`/notification-templates/${id}`),
  2098. updateNotificationTemplate: (id: number, data: NotificationTemplateUpdate) =>
  2099. request<NotificationTemplate>(`/notification-templates/${id}`, {
  2100. method: 'PUT',
  2101. body: JSON.stringify(data),
  2102. }),
  2103. resetNotificationTemplate: (id: number) =>
  2104. request<NotificationTemplate>(`/notification-templates/${id}/reset`, {
  2105. method: 'POST',
  2106. }),
  2107. getTemplateVariables: () => request<EventVariablesResponse[]>('/notification-templates/variables'),
  2108. previewTemplate: (data: TemplatePreviewRequest) =>
  2109. request<TemplatePreviewResponse>('/notification-templates/preview', {
  2110. method: 'POST',
  2111. body: JSON.stringify(data),
  2112. }),
  2113. // Notification Logs
  2114. getNotificationLogs: (params?: {
  2115. limit?: number;
  2116. offset?: number;
  2117. provider_id?: number;
  2118. event_type?: string;
  2119. success?: boolean;
  2120. days?: number;
  2121. }) => {
  2122. const searchParams = new URLSearchParams();
  2123. if (params?.limit) searchParams.set('limit', String(params.limit));
  2124. if (params?.offset) searchParams.set('offset', String(params.offset));
  2125. if (params?.provider_id) searchParams.set('provider_id', String(params.provider_id));
  2126. if (params?.event_type) searchParams.set('event_type', params.event_type);
  2127. if (params?.success !== undefined) searchParams.set('success', String(params.success));
  2128. if (params?.days) searchParams.set('days', String(params.days));
  2129. return request<NotificationLogEntry[]>(`/notifications/logs?${searchParams}`);
  2130. },
  2131. getNotificationLogStats: (days = 7) =>
  2132. request<NotificationLogStats>(`/notifications/logs/stats?days=${days}`),
  2133. clearNotificationLogs: (olderThanDays = 30) =>
  2134. request<{ deleted: number; message: string }>(
  2135. `/notifications/logs?older_than_days=${olderThanDays}`,
  2136. { method: 'DELETE' }
  2137. ),
  2138. // Spoolman Integration
  2139. getSpoolmanStatus: () => request<SpoolmanStatus>('/spoolman/status'),
  2140. connectSpoolman: () =>
  2141. request<{ success: boolean; message: string }>('/spoolman/connect', {
  2142. method: 'POST',
  2143. }),
  2144. disconnectSpoolman: () =>
  2145. request<{ success: boolean; message: string }>('/spoolman/disconnect', {
  2146. method: 'POST',
  2147. }),
  2148. syncPrinterAms: (printerId: number) =>
  2149. request<SpoolmanSyncResult>(`/spoolman/sync/${printerId}`, {
  2150. method: 'POST',
  2151. }),
  2152. syncAllPrintersAms: () =>
  2153. request<SpoolmanSyncResult>('/spoolman/sync-all', {
  2154. method: 'POST',
  2155. }),
  2156. getSpoolmanSpools: () =>
  2157. request<{ spools: unknown[] }>('/spoolman/spools'),
  2158. getSpoolmanFilaments: () =>
  2159. request<{ filaments: unknown[] }>('/spoolman/filaments'),
  2160. getUnlinkedSpools: () =>
  2161. request<UnlinkedSpool[]>('/spoolman/spools/unlinked'),
  2162. linkSpool: (spoolId: number, trayUuid: string) =>
  2163. request<{ success: boolean; message: string }>(`/spoolman/spools/${spoolId}/link`, {
  2164. method: 'POST',
  2165. body: JSON.stringify({ tray_uuid: trayUuid }),
  2166. }),
  2167. // Updates
  2168. getVersion: () => request<VersionInfo>('/updates/version'),
  2169. checkForUpdates: () => request<UpdateCheckResult>('/updates/check'),
  2170. applyUpdate: () =>
  2171. request<{ success: boolean; message: string; status?: UpdateStatus; is_docker?: boolean }>('/updates/apply', {
  2172. method: 'POST',
  2173. }),
  2174. getUpdateStatus: () => request<UpdateStatus>('/updates/status'),
  2175. // Maintenance
  2176. getMaintenanceTypes: () => request<MaintenanceType[]>('/maintenance/types'),
  2177. createMaintenanceType: (data: MaintenanceTypeCreate) =>
  2178. request<MaintenanceType>('/maintenance/types', {
  2179. method: 'POST',
  2180. body: JSON.stringify(data),
  2181. }),
  2182. updateMaintenanceType: (id: number, data: Partial<MaintenanceTypeCreate>) =>
  2183. request<MaintenanceType>(`/maintenance/types/${id}`, {
  2184. method: 'PATCH',
  2185. body: JSON.stringify(data),
  2186. }),
  2187. deleteMaintenanceType: (id: number) =>
  2188. request<{ status: string }>(`/maintenance/types/${id}`, { method: 'DELETE' }),
  2189. getMaintenanceOverview: () => request<PrinterMaintenanceOverview[]>('/maintenance/overview'),
  2190. getPrinterMaintenance: (printerId: number) =>
  2191. request<PrinterMaintenanceOverview>(`/maintenance/printers/${printerId}`),
  2192. updateMaintenanceItem: (itemId: number, data: { custom_interval_hours?: number | null; custom_interval_type?: 'hours' | 'days' | null; enabled?: boolean }) =>
  2193. request<MaintenanceStatus>(`/maintenance/items/${itemId}`, {
  2194. method: 'PATCH',
  2195. body: JSON.stringify(data),
  2196. }),
  2197. performMaintenance: (itemId: number, notes?: string) =>
  2198. request<MaintenanceStatus>(`/maintenance/items/${itemId}/perform`, {
  2199. method: 'POST',
  2200. body: JSON.stringify({ notes }),
  2201. }),
  2202. getMaintenanceHistory: (itemId: number) =>
  2203. request<MaintenanceHistory[]>(`/maintenance/items/${itemId}/history`),
  2204. getMaintenanceSummary: () => request<MaintenanceSummary>('/maintenance/summary'),
  2205. setPrinterHours: (printerId: number, totalHours: number) =>
  2206. request<{ printer_id: number; total_hours: number; archive_hours: number; offset_hours: number }>(
  2207. `/maintenance/printers/${printerId}/hours?total_hours=${totalHours}`,
  2208. { method: 'PATCH' }
  2209. ),
  2210. assignMaintenanceType: (printerId: number, typeId: number) =>
  2211. request<MaintenanceStatus>(`/maintenance/printers/${printerId}/assign/${typeId}`, {
  2212. method: 'POST',
  2213. }),
  2214. removeMaintenanceItem: (itemId: number) =>
  2215. request<{ status: string }>(`/maintenance/items/${itemId}`, {
  2216. method: 'DELETE',
  2217. }),
  2218. // Camera
  2219. getCameraStreamUrl: (printerId: number, fps = 10) =>
  2220. `${API_BASE}/printers/${printerId}/camera/stream?fps=${fps}`,
  2221. getCameraSnapshotUrl: (printerId: number) =>
  2222. `${API_BASE}/printers/${printerId}/camera/snapshot`,
  2223. testCameraConnection: (printerId: number) =>
  2224. request<{ success: boolean; message?: string; error?: string }>(`/printers/${printerId}/camera/test`),
  2225. // External Links
  2226. getExternalLinks: () => request<ExternalLink[]>('/external-links/'),
  2227. getExternalLink: (id: number) => request<ExternalLink>(`/external-links/${id}`),
  2228. createExternalLink: (data: ExternalLinkCreate) =>
  2229. request<ExternalLink>('/external-links/', {
  2230. method: 'POST',
  2231. body: JSON.stringify(data),
  2232. }),
  2233. updateExternalLink: (id: number, data: ExternalLinkUpdate) =>
  2234. request<ExternalLink>(`/external-links/${id}`, {
  2235. method: 'PATCH',
  2236. body: JSON.stringify(data),
  2237. }),
  2238. deleteExternalLink: (id: number) =>
  2239. request<{ message: string }>(`/external-links/${id}`, { method: 'DELETE' }),
  2240. reorderExternalLinks: (ids: number[]) =>
  2241. request<ExternalLink[]>('/external-links/reorder', {
  2242. method: 'PUT',
  2243. body: JSON.stringify({ ids }),
  2244. }),
  2245. uploadExternalLinkIcon: async (id: number, file: File): Promise<ExternalLink> => {
  2246. const formData = new FormData();
  2247. formData.append('file', file);
  2248. const response = await fetch(`${API_BASE}/external-links/${id}/icon`, {
  2249. method: 'POST',
  2250. body: formData,
  2251. });
  2252. if (!response.ok) {
  2253. const error = await response.json().catch(() => ({}));
  2254. throw new Error(error.detail || `HTTP ${response.status}`);
  2255. }
  2256. return response.json();
  2257. },
  2258. deleteExternalLinkIcon: (id: number) =>
  2259. request<ExternalLink>(`/external-links/${id}/icon`, { method: 'DELETE' }),
  2260. getExternalLinkIconUrl: (id: number) => `${API_BASE}/external-links/${id}/icon`,
  2261. // Projects
  2262. getProjects: (status?: string) => {
  2263. const params = new URLSearchParams();
  2264. if (status) params.set('status', status);
  2265. return request<ProjectListItem[]>(`/projects/?${params}`);
  2266. },
  2267. getProject: (id: number) => request<Project>(`/projects/${id}`),
  2268. createProject: (data: ProjectCreate) =>
  2269. request<Project>('/projects/', {
  2270. method: 'POST',
  2271. body: JSON.stringify(data),
  2272. }),
  2273. updateProject: (id: number, data: ProjectUpdate) =>
  2274. request<Project>(`/projects/${id}`, {
  2275. method: 'PATCH',
  2276. body: JSON.stringify(data),
  2277. }),
  2278. deleteProject: (id: number) =>
  2279. request<{ message: string }>(`/projects/${id}`, { method: 'DELETE' }),
  2280. getProjectArchives: (id: number, limit = 100, offset = 0) =>
  2281. request<Archive[]>(`/projects/${id}/archives?limit=${limit}&offset=${offset}`),
  2282. addArchivesToProject: (projectId: number, archiveIds: number[]) =>
  2283. request<{ message: string }>(`/projects/${projectId}/add-archives`, {
  2284. method: 'POST',
  2285. body: JSON.stringify({ archive_ids: archiveIds }),
  2286. }),
  2287. removeArchivesFromProject: (projectId: number, archiveIds: number[]) =>
  2288. request<{ message: string }>(`/projects/${projectId}/remove-archives`, {
  2289. method: 'POST',
  2290. body: JSON.stringify({ archive_ids: archiveIds }),
  2291. }),
  2292. addQueueItemsToProject: (projectId: number, queueItemIds: number[]) =>
  2293. request<{ message: string }>(`/projects/${projectId}/add-queue`, {
  2294. method: 'POST',
  2295. body: JSON.stringify({ queue_item_ids: queueItemIds }),
  2296. }),
  2297. // Project Attachments
  2298. uploadProjectAttachment: async (projectId: number, file: File): Promise<{
  2299. status: string;
  2300. filename: string;
  2301. original_name: string;
  2302. attachments: ProjectAttachment[];
  2303. }> => {
  2304. const formData = new FormData();
  2305. formData.append('file', file);
  2306. const response = await fetch(`${API_BASE}/projects/${projectId}/attachments`, {
  2307. method: 'POST',
  2308. body: formData,
  2309. });
  2310. if (!response.ok) {
  2311. const error = await response.json().catch(() => ({}));
  2312. throw new Error(error.detail || `HTTP ${response.status}`);
  2313. }
  2314. return response.json();
  2315. },
  2316. getProjectAttachmentUrl: (projectId: number, filename: string) =>
  2317. `${API_BASE}/projects/${projectId}/attachments/${encodeURIComponent(filename)}`,
  2318. deleteProjectAttachment: (projectId: number, filename: string) =>
  2319. request<{ status: string; message: string; attachments: ProjectAttachment[] | null }>(
  2320. `/projects/${projectId}/attachments/${encodeURIComponent(filename)}`,
  2321. { method: 'DELETE' }
  2322. ),
  2323. // BOM (Bill of Materials)
  2324. getProjectBOM: (projectId: number) =>
  2325. request<BOMItem[]>(`/projects/${projectId}/bom`),
  2326. createBOMItem: (projectId: number, data: BOMItemCreate) =>
  2327. request<BOMItem>(`/projects/${projectId}/bom`, {
  2328. method: 'POST',
  2329. body: JSON.stringify(data),
  2330. }),
  2331. updateBOMItem: (projectId: number, itemId: number, data: BOMItemUpdate) =>
  2332. request<BOMItem>(`/projects/${projectId}/bom/${itemId}`, {
  2333. method: 'PATCH',
  2334. body: JSON.stringify(data),
  2335. }),
  2336. deleteBOMItem: (projectId: number, itemId: number) =>
  2337. request<{ status: string; message: string }>(`/projects/${projectId}/bom/${itemId}`, {
  2338. method: 'DELETE',
  2339. }),
  2340. // Templates
  2341. getTemplates: () => request<ProjectListItem[]>('/projects/templates/'),
  2342. createTemplateFromProject: (projectId: number) =>
  2343. request<Project>(`/projects/${projectId}/create-template`, { method: 'POST' }),
  2344. createProjectFromTemplate: (templateId: number, name?: string) =>
  2345. request<Project>(`/projects/from-template/${templateId}${name ? `?name=${encodeURIComponent(name)}` : ''}`, {
  2346. method: 'POST',
  2347. }),
  2348. // Timeline
  2349. getProjectTimeline: (projectId: number, limit = 50) =>
  2350. request<TimelineEvent[]>(`/projects/${projectId}/timeline?limit=${limit}`),
  2351. // API Keys
  2352. getAPIKeys: () => request<APIKey[]>('/api-keys/'),
  2353. createAPIKey: (data: APIKeyCreate) =>
  2354. request<APIKeyCreateResponse>('/api-keys/', {
  2355. method: 'POST',
  2356. body: JSON.stringify(data),
  2357. }),
  2358. updateAPIKey: (id: number, data: APIKeyUpdate) =>
  2359. request<APIKey>(`/api-keys/${id}`, {
  2360. method: 'PATCH',
  2361. body: JSON.stringify(data),
  2362. }),
  2363. deleteAPIKey: (id: number) =>
  2364. request<{ message: string }>(`/api-keys/${id}`, { method: 'DELETE' }),
  2365. // AMS History
  2366. getAMSHistory: (printerId: number, amsId: number, hours = 24) =>
  2367. request<AMSHistoryResponse>(`/ams-history/${printerId}/${amsId}?hours=${hours}`),
  2368. // System Info
  2369. getSystemInfo: () => request<SystemInfo>('/system/info'),
  2370. // Library (File Manager)
  2371. getLibraryFolders: () => request<LibraryFolderTree[]>('/library/folders'),
  2372. createLibraryFolder: (data: LibraryFolderCreate) =>
  2373. request<LibraryFolder>('/library/folders', {
  2374. method: 'POST',
  2375. body: JSON.stringify(data),
  2376. }),
  2377. updateLibraryFolder: (id: number, data: LibraryFolderUpdate) =>
  2378. request<LibraryFolder>(`/library/folders/${id}`, {
  2379. method: 'PUT',
  2380. body: JSON.stringify(data),
  2381. }),
  2382. deleteLibraryFolder: (id: number) =>
  2383. request<{ status: string; message: string }>(`/library/folders/${id}`, { method: 'DELETE' }),
  2384. getLibraryFoldersByProject: (projectId: number) =>
  2385. request<LibraryFolder[]>(`/library/folders/by-project/${projectId}`),
  2386. getLibraryFoldersByArchive: (archiveId: number) =>
  2387. request<LibraryFolder[]>(`/library/folders/by-archive/${archiveId}`),
  2388. getLibraryFiles: (folderId?: number | null, includeRoot = true) => {
  2389. const params = new URLSearchParams();
  2390. if (folderId !== undefined && folderId !== null) {
  2391. params.set('folder_id', String(folderId));
  2392. }
  2393. params.set('include_root', String(includeRoot));
  2394. return request<LibraryFileListItem[]>(`/library/files?${params}`);
  2395. },
  2396. getLibraryFile: (id: number) => request<LibraryFile>(`/library/files/${id}`),
  2397. uploadLibraryFile: async (file: File, folderId?: number | null): Promise<LibraryFileUploadResponse> => {
  2398. const formData = new FormData();
  2399. formData.append('file', file);
  2400. const params = folderId ? `?folder_id=${folderId}` : '';
  2401. const response = await fetch(`${API_BASE}/library/files${params}`, {
  2402. method: 'POST',
  2403. body: formData,
  2404. });
  2405. if (!response.ok) {
  2406. const error = await response.json().catch(() => ({}));
  2407. throw new Error(error.detail || `HTTP ${response.status}`);
  2408. }
  2409. return response.json();
  2410. },
  2411. updateLibraryFile: (id: number, data: LibraryFileUpdate) =>
  2412. request<LibraryFile>(`/library/files/${id}`, {
  2413. method: 'PUT',
  2414. body: JSON.stringify(data),
  2415. }),
  2416. deleteLibraryFile: (id: number) =>
  2417. request<{ status: string; message: string }>(`/library/files/${id}`, { method: 'DELETE' }),
  2418. getLibraryFileDownloadUrl: (id: number) => `${API_BASE}/library/files/${id}/download`,
  2419. getLibraryFileThumbnailUrl: (id: number) => `${API_BASE}/library/files/${id}/thumbnail`,
  2420. getLibraryFileGcodeUrl: (id: number) => `${API_BASE}/library/files/${id}/gcode`,
  2421. moveLibraryFiles: (fileIds: number[], folderId: number | null) =>
  2422. request<{ status: string; moved: number }>('/library/files/move', {
  2423. method: 'POST',
  2424. body: JSON.stringify({ file_ids: fileIds, folder_id: folderId }),
  2425. }),
  2426. bulkDeleteLibrary: (fileIds: number[], folderIds: number[]) =>
  2427. request<{ deleted_files: number; deleted_folders: number }>('/library/bulk-delete', {
  2428. method: 'POST',
  2429. body: JSON.stringify({ file_ids: fileIds, folder_ids: folderIds }),
  2430. }),
  2431. getLibraryStats: () => request<LibraryStats>('/library/stats'),
  2432. addLibraryFilesToQueue: (fileIds: number[]) =>
  2433. request<AddToQueueResponse>('/library/files/add-to-queue', {
  2434. method: 'POST',
  2435. body: JSON.stringify({ file_ids: fileIds }),
  2436. }),
  2437. };
  2438. // AMS History types
  2439. export interface AMSHistoryPoint {
  2440. recorded_at: string;
  2441. humidity: number | null;
  2442. humidity_raw: number | null;
  2443. temperature: number | null;
  2444. }
  2445. export interface AMSHistoryResponse {
  2446. printer_id: number;
  2447. ams_id: number;
  2448. data: AMSHistoryPoint[];
  2449. min_humidity: number | null;
  2450. max_humidity: number | null;
  2451. avg_humidity: number | null;
  2452. min_temperature: number | null;
  2453. max_temperature: number | null;
  2454. avg_temperature: number | null;
  2455. }
  2456. // System Info types
  2457. export interface SystemInfo {
  2458. app: {
  2459. version: string;
  2460. base_dir: string;
  2461. archive_dir: string;
  2462. };
  2463. database: {
  2464. archives: number;
  2465. archives_completed: number;
  2466. archives_failed: number;
  2467. archives_printing: number;
  2468. printers: number;
  2469. filaments: number;
  2470. projects: number;
  2471. smart_plugs: number;
  2472. total_print_time_seconds: number;
  2473. total_print_time_formatted: string;
  2474. total_filament_grams: number;
  2475. total_filament_kg: number;
  2476. };
  2477. printers: {
  2478. total: number;
  2479. connected: number;
  2480. connected_list: Array<{
  2481. id: number;
  2482. name: string;
  2483. state: string;
  2484. model: string;
  2485. }>;
  2486. };
  2487. storage: {
  2488. archive_size_bytes: number;
  2489. archive_size_formatted: string;
  2490. database_size_bytes: number;
  2491. database_size_formatted: string;
  2492. disk_total_bytes: number;
  2493. disk_total_formatted: string;
  2494. disk_used_bytes: number;
  2495. disk_used_formatted: string;
  2496. disk_free_bytes: number;
  2497. disk_free_formatted: string;
  2498. disk_percent_used: number;
  2499. };
  2500. system: {
  2501. platform: string;
  2502. platform_release: string;
  2503. platform_version: string;
  2504. architecture: string;
  2505. hostname: string;
  2506. python_version: string;
  2507. uptime_seconds: number;
  2508. uptime_formatted: string;
  2509. boot_time: string;
  2510. };
  2511. memory: {
  2512. total_bytes: number;
  2513. total_formatted: string;
  2514. available_bytes: number;
  2515. available_formatted: string;
  2516. used_bytes: number;
  2517. used_formatted: string;
  2518. percent_used: number;
  2519. };
  2520. cpu: {
  2521. count: number;
  2522. count_logical: number;
  2523. percent: number;
  2524. };
  2525. }
  2526. // Library (File Manager) types
  2527. export interface LibraryFolderTree {
  2528. id: number;
  2529. name: string;
  2530. parent_id: number | null;
  2531. project_id: number | null;
  2532. archive_id: number | null;
  2533. project_name: string | null;
  2534. archive_name: string | null;
  2535. file_count: number;
  2536. children: LibraryFolderTree[];
  2537. }
  2538. export interface LibraryFolder {
  2539. id: number;
  2540. name: string;
  2541. parent_id: number | null;
  2542. project_id: number | null;
  2543. archive_id: number | null;
  2544. project_name: string | null;
  2545. archive_name: string | null;
  2546. file_count: number;
  2547. created_at: string;
  2548. updated_at: string;
  2549. }
  2550. export interface LibraryFolderCreate {
  2551. name: string;
  2552. parent_id?: number | null;
  2553. project_id?: number | null;
  2554. archive_id?: number | null;
  2555. }
  2556. export interface LibraryFolderUpdate {
  2557. name?: string;
  2558. parent_id?: number | null;
  2559. project_id?: number | null; // 0 to unlink
  2560. archive_id?: number | null; // 0 to unlink
  2561. }
  2562. export interface LibraryFileDuplicate {
  2563. id: number;
  2564. filename: string;
  2565. folder_id: number | null;
  2566. folder_name: string | null;
  2567. created_at: string;
  2568. }
  2569. export interface LibraryFile {
  2570. id: number;
  2571. folder_id: number | null;
  2572. folder_name: string | null;
  2573. project_id: number | null;
  2574. project_name: string | null;
  2575. filename: string;
  2576. file_path: string;
  2577. file_type: string;
  2578. file_size: number;
  2579. file_hash: string | null;
  2580. thumbnail_path: string | null;
  2581. metadata: Record<string, unknown> | null;
  2582. print_count: number;
  2583. last_printed_at: string | null;
  2584. notes: string | null;
  2585. duplicates: LibraryFileDuplicate[] | null;
  2586. duplicate_count: number;
  2587. created_at: string;
  2588. updated_at: string;
  2589. }
  2590. export interface LibraryFileListItem {
  2591. id: number;
  2592. folder_id: number | null;
  2593. filename: string;
  2594. file_type: string;
  2595. file_size: number;
  2596. thumbnail_path: string | null;
  2597. print_count: number;
  2598. duplicate_count: number;
  2599. created_at: string;
  2600. print_name: string | null;
  2601. print_time_seconds: number | null;
  2602. filament_used_grams: number | null;
  2603. }
  2604. export interface LibraryFileUpdate {
  2605. folder_id?: number | null;
  2606. project_id?: number | null;
  2607. notes?: string | null;
  2608. }
  2609. export interface LibraryFileUploadResponse {
  2610. id: number;
  2611. filename: string;
  2612. file_type: string;
  2613. file_size: number;
  2614. thumbnail_path: string | null;
  2615. duplicate_of: number | null;
  2616. metadata: Record<string, unknown> | null;
  2617. }
  2618. export interface LibraryStats {
  2619. total_files: number;
  2620. total_folders: number;
  2621. total_size_bytes: number;
  2622. files_by_type: Record<string, number>;
  2623. total_prints: number;
  2624. disk_free_bytes: number;
  2625. disk_total_bytes: number;
  2626. disk_used_bytes: number;
  2627. }
  2628. // Library Queue types
  2629. export interface AddToQueueResult {
  2630. file_id: number;
  2631. filename: string;
  2632. queue_item_id: number;
  2633. archive_id: number;
  2634. }
  2635. export interface AddToQueueError {
  2636. file_id: number;
  2637. filename: string;
  2638. error: string;
  2639. }
  2640. export interface AddToQueueResponse {
  2641. added: AddToQueueResult[];
  2642. errors: AddToQueueError[];
  2643. }
  2644. // Discovery types
  2645. export interface DiscoveredPrinter {
  2646. serial: string;
  2647. name: string;
  2648. ip_address: string;
  2649. model: string | null;
  2650. discovered_at: string | null;
  2651. }
  2652. export interface DiscoveryStatus {
  2653. running: boolean;
  2654. }
  2655. export interface DiscoveryInfo {
  2656. is_docker: boolean;
  2657. ssdp_running: boolean;
  2658. scan_running: boolean;
  2659. }
  2660. export interface SubnetScanStatus {
  2661. running: boolean;
  2662. scanned: number;
  2663. total: number;
  2664. }
  2665. // Discovery API
  2666. export const discoveryApi = {
  2667. getInfo: () => request<DiscoveryInfo>('/discovery/info'),
  2668. getStatus: () => request<DiscoveryStatus>('/discovery/status'),
  2669. startDiscovery: (duration: number = 10) =>
  2670. request<DiscoveryStatus>(`/discovery/start?duration=${duration}`, { method: 'POST' }),
  2671. stopDiscovery: () =>
  2672. request<DiscoveryStatus>('/discovery/stop', { method: 'POST' }),
  2673. getDiscoveredPrinters: () =>
  2674. request<DiscoveredPrinter[]>('/discovery/printers'),
  2675. // Subnet scanning (for Docker environments)
  2676. startSubnetScan: (subnet: string, timeout: number = 1.0) =>
  2677. request<SubnetScanStatus>('/discovery/scan', {
  2678. method: 'POST',
  2679. body: JSON.stringify({ subnet, timeout }),
  2680. }),
  2681. getScanStatus: () => request<SubnetScanStatus>('/discovery/scan/status'),
  2682. stopSubnetScan: () =>
  2683. request<SubnetScanStatus>('/discovery/scan/stop', { method: 'POST' }),
  2684. };
  2685. // Virtual Printer types
  2686. export interface VirtualPrinterStatus {
  2687. enabled: boolean;
  2688. running: boolean;
  2689. mode: 'immediate' | 'queue' | 'review' | 'print_queue'; // 'queue' is legacy, normalized to 'review'
  2690. name: string;
  2691. serial: string;
  2692. model: string;
  2693. model_name: string;
  2694. pending_files: number;
  2695. }
  2696. export interface VirtualPrinterSettings {
  2697. enabled: boolean;
  2698. access_code_set: boolean;
  2699. mode: 'immediate' | 'queue' | 'review' | 'print_queue'; // 'queue' is legacy, normalized to 'review'
  2700. model: string;
  2701. status: VirtualPrinterStatus;
  2702. }
  2703. export interface VirtualPrinterModels {
  2704. models: Record<string, string>; // SSDP code -> display name
  2705. default: string;
  2706. }
  2707. export interface PendingUpload {
  2708. id: number;
  2709. filename: string;
  2710. file_size: number;
  2711. source_ip: string | null;
  2712. status: string;
  2713. tags: string | null;
  2714. notes: string | null;
  2715. project_id: number | null;
  2716. uploaded_at: string;
  2717. }
  2718. // Virtual Printer API
  2719. export const virtualPrinterApi = {
  2720. getSettings: () => request<VirtualPrinterSettings>('/settings/virtual-printer'),
  2721. getModels: () => request<VirtualPrinterModels>('/settings/virtual-printer/models'),
  2722. updateSettings: (data: {
  2723. enabled?: boolean;
  2724. access_code?: string;
  2725. mode?: 'immediate' | 'review' | 'print_queue';
  2726. model?: string;
  2727. }) => {
  2728. const params = new URLSearchParams();
  2729. if (data.enabled !== undefined) params.set('enabled', String(data.enabled));
  2730. if (data.access_code !== undefined) params.set('access_code', data.access_code);
  2731. if (data.mode !== undefined) params.set('mode', data.mode);
  2732. if (data.model !== undefined) params.set('model', data.model);
  2733. return request<VirtualPrinterSettings>(`/settings/virtual-printer?${params.toString()}`, {
  2734. method: 'PUT',
  2735. });
  2736. },
  2737. };
  2738. // Pending Uploads API
  2739. export const pendingUploadsApi = {
  2740. list: () => request<PendingUpload[]>('/pending-uploads/'),
  2741. getCount: () => request<{ count: number }>('/pending-uploads/count'),
  2742. get: (id: number) => request<PendingUpload>(`/pending-uploads/${id}`),
  2743. archive: (id: number, data?: { tags?: string; notes?: string; project_id?: number }) =>
  2744. request<{ id: number; print_name: string; filename: string }>(`/pending-uploads/${id}/archive`, {
  2745. method: 'POST',
  2746. body: JSON.stringify(data || {}),
  2747. }),
  2748. discard: (id: number) =>
  2749. request<{ success: boolean }>(`/pending-uploads/${id}`, { method: 'DELETE' }),
  2750. archiveAll: () =>
  2751. request<{ archived: number; failed: number }>('/pending-uploads/archive-all', { method: 'POST' }),
  2752. discardAll: () =>
  2753. request<{ discarded: number }>('/pending-uploads/discard-all', { method: 'DELETE' }),
  2754. };
  2755. // Firmware API Types
  2756. export interface FirmwareUpdateInfo {
  2757. printer_id: number;
  2758. printer_name: string;
  2759. model: string | null;
  2760. current_version: string | null;
  2761. latest_version: string | null;
  2762. update_available: boolean;
  2763. download_url: string | null;
  2764. release_notes: string | null;
  2765. }
  2766. export interface FirmwareUploadPrepare {
  2767. can_proceed: boolean;
  2768. sd_card_present: boolean;
  2769. sd_card_free_space: number;
  2770. firmware_size: number;
  2771. space_sufficient: boolean;
  2772. update_available: boolean;
  2773. current_version: string | null;
  2774. latest_version: string | null;
  2775. firmware_filename: string | null;
  2776. errors: string[];
  2777. }
  2778. export interface FirmwareUploadStatus {
  2779. status: 'idle' | 'preparing' | 'downloading' | 'uploading' | 'complete' | 'error';
  2780. progress: number;
  2781. message: string;
  2782. error: string | null;
  2783. firmware_filename: string | null;
  2784. firmware_version: string | null;
  2785. }
  2786. // Firmware API
  2787. export const firmwareApi = {
  2788. checkUpdates: () =>
  2789. request<{ updates: FirmwareUpdateInfo[]; updates_available: number }>('/firmware/updates'),
  2790. checkPrinterUpdate: (printerId: number) =>
  2791. request<FirmwareUpdateInfo>(`/firmware/updates/${printerId}`),
  2792. prepareUpload: (printerId: number) =>
  2793. request<FirmwareUploadPrepare>(`/firmware/updates/${printerId}/prepare`),
  2794. startUpload: (printerId: number) =>
  2795. request<{ started: boolean; message: string }>(`/firmware/updates/${printerId}/upload`, {
  2796. method: 'POST',
  2797. }),
  2798. getUploadStatus: (printerId: number) =>
  2799. request<FirmwareUploadStatus>(`/firmware/updates/${printerId}/upload/status`),
  2800. };
  2801. // Support types
  2802. export interface DebugLoggingState {
  2803. enabled: boolean;
  2804. enabled_at: string | null;
  2805. duration_seconds: number | null;
  2806. }
  2807. // Support API
  2808. export const supportApi = {
  2809. getDebugLoggingState: () =>
  2810. request<DebugLoggingState>('/support/debug-logging'),
  2811. setDebugLogging: (enabled: boolean) =>
  2812. request<DebugLoggingState>('/support/debug-logging', {
  2813. method: 'POST',
  2814. body: JSON.stringify({ enabled }),
  2815. }),
  2816. downloadSupportBundle: async () => {
  2817. const response = await fetch(`${API_BASE}/support/bundle`);
  2818. if (!response.ok) {
  2819. const error = await response.json().catch(() => ({}));
  2820. throw new Error(error.detail || `HTTP ${response.status}`);
  2821. }
  2822. // Get filename from Content-Disposition header or use default
  2823. const disposition = response.headers.get('Content-Disposition');
  2824. const filenameMatch = disposition?.match(/filename=(.+)/);
  2825. const filename = filenameMatch ? filenameMatch[1] : 'bambuddy-support.zip';
  2826. // Download the blob
  2827. const blob = await response.blob();
  2828. const url = window.URL.createObjectURL(blob);
  2829. const a = document.createElement('a');
  2830. a.href = url;
  2831. a.download = filename;
  2832. document.body.appendChild(a);
  2833. a.click();
  2834. document.body.removeChild(a);
  2835. window.URL.revokeObjectURL(url);
  2836. },
  2837. };