client.ts 117 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743
  1. const API_BASE = '/api/v1';
  2. // Auth token storage
  3. let authToken: string | null = localStorage.getItem('auth_token');
  4. export function setAuthToken(token: string | null) {
  5. authToken = token;
  6. if (token) {
  7. localStorage.setItem('auth_token', token);
  8. } else {
  9. localStorage.removeItem('auth_token');
  10. }
  11. }
  12. export function getAuthToken(): string | null {
  13. return authToken;
  14. }
  15. async function request<T>(
  16. endpoint: string,
  17. options: RequestInit = {}
  18. ): Promise<T> {
  19. const headers: Record<string, string> = {
  20. 'Content-Type': 'application/json',
  21. ...options.headers as Record<string, string>,
  22. };
  23. // Add auth token if available
  24. if (authToken) {
  25. headers['Authorization'] = `Bearer ${authToken}`;
  26. }
  27. const response = await fetch(`${API_BASE}${endpoint}`, {
  28. ...options,
  29. cache: 'no-store', // Prevent browser caching of API responses
  30. headers,
  31. });
  32. if (!response.ok) {
  33. // Handle 401 Unauthorized - clear token and redirect to login
  34. if (response.status === 401) {
  35. setAuthToken(null);
  36. // Don't throw here - let the auth context handle redirect
  37. }
  38. const error = await response.json().catch(() => ({}));
  39. const detail = error.detail;
  40. const message = typeof detail === 'string'
  41. ? detail
  42. : (detail ? JSON.stringify(detail) : `HTTP ${response.status}`);
  43. throw new Error(message);
  44. }
  45. return await response.json();
  46. }
  47. // Printer types
  48. export interface Printer {
  49. id: number;
  50. name: string;
  51. serial_number: string;
  52. ip_address: string;
  53. access_code: string;
  54. model: string | null;
  55. location: string | null; // Group/location name
  56. nozzle_count: number; // 1 or 2, auto-detected from MQTT
  57. is_active: boolean;
  58. auto_archive: boolean;
  59. external_camera_url: string | null;
  60. external_camera_type: string | null; // "mjpeg", "rtsp", "snapshot"
  61. external_camera_enabled: boolean;
  62. plate_detection_enabled: boolean; // Check plate before print
  63. plate_detection_roi?: PlateDetectionROI; // ROI for plate detection
  64. created_at: string;
  65. updated_at: string;
  66. }
  67. export interface HMSError {
  68. code: string;
  69. attr: number; // Attribute value for constructing wiki URL
  70. module: number;
  71. severity: number; // 1=fatal, 2=serious, 3=common, 4=info
  72. }
  73. export interface AMSTray {
  74. id: number;
  75. tray_color: string | null;
  76. tray_type: string | null;
  77. tray_sub_brands: string | null; // Full name like "PLA Basic", "PETG HF"
  78. tray_id_name: string | null; // Bambu filament ID like "A00-Y2" (can decode to color)
  79. tray_info_idx: string | null; // Filament preset ID like "GFA00" - maps to cloud setting_id
  80. remain: number;
  81. k: number | null; // Pressure advance value (from tray or K-profile lookup)
  82. cali_idx: number | null; // Calibration index for K-profile lookup
  83. tag_uid: string | null; // RFID tag UID (any tag)
  84. tray_uuid: string | null; // Bambu Lab spool UUID (32-char hex, only valid for Bambu Lab spools)
  85. nozzle_temp_min: number | null; // Min nozzle temperature
  86. nozzle_temp_max: number | null; // Max nozzle temperature
  87. }
  88. export interface AMSUnit {
  89. id: number;
  90. humidity: number | null;
  91. temp: number | null;
  92. is_ams_ht: boolean; // True for AMS-HT (single spool), False for regular AMS (4 spools)
  93. tray: AMSTray[];
  94. }
  95. export interface NozzleInfo {
  96. nozzle_type: string; // "stainless_steel" or "hardened_steel"
  97. nozzle_diameter: string; // e.g., "0.4"
  98. }
  99. export interface PrintOptions {
  100. // Core AI detectors
  101. spaghetti_detector: boolean;
  102. print_halt: boolean;
  103. halt_print_sensitivity: string; // "low", "medium", "high" - spaghetti sensitivity
  104. first_layer_inspector: boolean;
  105. printing_monitor: boolean;
  106. buildplate_marker_detector: boolean;
  107. allow_skip_parts: boolean;
  108. // Additional AI detectors (decoded from cfg bitmask)
  109. nozzle_clumping_detector: boolean;
  110. nozzle_clumping_sensitivity: string; // "low", "medium", "high"
  111. pileup_detector: boolean;
  112. pileup_sensitivity: string; // "low", "medium", "high"
  113. airprint_detector: boolean;
  114. airprint_sensitivity: string; // "low", "medium", "high"
  115. auto_recovery_step_loss: boolean;
  116. filament_tangle_detect: boolean;
  117. }
  118. export interface PrinterStatus {
  119. id: number;
  120. name: string;
  121. connected: boolean;
  122. state: string | null;
  123. current_print: string | null;
  124. subtask_name: string | null;
  125. gcode_file: string | null;
  126. progress: number | null;
  127. remaining_time: number | null;
  128. layer_num: number | null;
  129. total_layers: number | null;
  130. temperatures: {
  131. bed?: number;
  132. bed_target?: number;
  133. bed_heating?: boolean; // Actual heater state from MQTT
  134. nozzle?: number;
  135. nozzle_target?: number;
  136. nozzle_heating?: boolean; // Actual heater state from MQTT
  137. nozzle_2?: number; // Second nozzle for H2 series (dual nozzle)
  138. nozzle_2_target?: number;
  139. nozzle_2_heating?: boolean; // Actual heater state from MQTT
  140. chamber?: number;
  141. chamber_target?: number;
  142. chamber_heating?: boolean; // Actual heater state from MQTT
  143. } | null;
  144. cover_url: string | null;
  145. hms_errors: HMSError[];
  146. ams: AMSUnit[];
  147. ams_exists: boolean;
  148. vt_tray: AMSTray | null; // Virtual tray / external spool
  149. sdcard: boolean; // SD card inserted
  150. store_to_sdcard: boolean; // Store sent files on SD card
  151. timelapse: boolean; // Timelapse recording active
  152. ipcam: boolean; // Live view enabled
  153. wifi_signal: number | null; // WiFi signal strength in dBm
  154. nozzles: NozzleInfo[]; // Nozzle hardware info (index 0=left/primary, 1=right)
  155. print_options: PrintOptions | null; // AI detection and print options
  156. // Calibration stage tracking
  157. stg_cur: number; // Current stage number (-1 = not calibrating)
  158. stg_cur_name: string | null; // Human-readable current stage name
  159. stg: number[]; // List of stage numbers in calibration sequence
  160. // Air conditioning mode (0=cooling, 1=heating)
  161. airduct_mode: number;
  162. // Print speed level (1=silent, 2=standard, 3=sport, 4=ludicrous)
  163. speed_level: number;
  164. // Chamber light on/off
  165. chamber_light: boolean;
  166. // Active extruder for dual nozzle (0=right, 1=left)
  167. active_extruder: number;
  168. // AMS mapping - which AMS is connected to which nozzle
  169. // Format: [ams_id_for_nozzle0, ams_id_for_nozzle1, ...] where -1 means no AMS
  170. ams_mapping: number[];
  171. // Per-AMS extruder mapping - extracted from each AMS unit's info field
  172. // Format: {ams_id: extruder_id} where extruder 0=right, 1=left
  173. // Note: JSON keys are always strings
  174. ams_extruder_map: Record<string, number>;
  175. // Currently loaded tray (global tray ID, 255 = no filament loaded, 254 = external spool)
  176. tray_now: number;
  177. // AMS status for filament change tracking (0=idle, 1=filament_change, 2=rfid_identifying, 3=assist, 4=calibration)
  178. ams_status_main: number;
  179. // AMS sub-status for filament change step (when main=1): 4=retraction, 6=load verification, 7=purge
  180. ams_status_sub: number;
  181. // mc_print_sub_stage - filament change step indicator used by OrcaSlicer/BambuStudio
  182. mc_print_sub_stage: number;
  183. // Timestamp of last AMS data update (for RFID refresh detection)
  184. last_ams_update: number;
  185. // Number of printable objects in current print (for skip objects feature)
  186. printable_objects_count: number;
  187. // Fan speeds (0-100 percentage, null if not available for this model)
  188. cooling_fan_speed: number | null; // Part cooling fan
  189. big_fan1_speed: number | null; // Auxiliary fan
  190. big_fan2_speed: number | null; // Chamber/exhaust fan
  191. heatbreak_fan_speed: number | null; // Hotend heatbreak fan
  192. }
  193. export interface PrinterCreate {
  194. name: string;
  195. serial_number: string;
  196. ip_address: string;
  197. access_code: string;
  198. model?: string;
  199. location?: string;
  200. auto_archive?: boolean;
  201. external_camera_url?: string | null;
  202. external_camera_type?: string | null;
  203. external_camera_enabled?: boolean;
  204. plate_detection_enabled?: boolean;
  205. plate_detection_roi?: PlateDetectionROI;
  206. }
  207. // Plate Detection
  208. export interface PlateDetectionROI {
  209. x: number; // X start % (0.0-1.0)
  210. y: number; // Y start % (0.0-1.0)
  211. w: number; // Width % (0.0-1.0)
  212. h: number; // Height % (0.0-1.0)
  213. }
  214. export interface PlateDetectionResult {
  215. is_empty: boolean;
  216. confidence: number;
  217. difference_percent: number;
  218. message: string;
  219. has_debug_image: boolean;
  220. debug_image_url?: string;
  221. needs_calibration: boolean;
  222. light_warning?: boolean;
  223. reference_count?: number;
  224. max_references?: number;
  225. roi?: PlateDetectionROI;
  226. }
  227. export interface PlateDetectionStatus {
  228. available: boolean;
  229. calibrated: boolean;
  230. reference_count: number;
  231. max_references: number;
  232. message: string;
  233. }
  234. export interface CalibrationResult {
  235. success: boolean;
  236. message: string;
  237. }
  238. export interface PlateReference {
  239. index: number;
  240. label: string;
  241. timestamp: string;
  242. has_image: boolean;
  243. thumbnail_url: string;
  244. }
  245. // Archive types
  246. export interface ArchiveDuplicate {
  247. id: number;
  248. print_name: string | null;
  249. created_at: string;
  250. match_type: 'exact' | 'similar'; // 'exact' = hash match, 'similar' = name match
  251. }
  252. export interface Archive {
  253. id: number;
  254. printer_id: number | null;
  255. project_id: number | null;
  256. project_name: string | null;
  257. filename: string;
  258. file_path: string;
  259. file_size: number;
  260. content_hash: string | null;
  261. thumbnail_path: string | null;
  262. timelapse_path: string | null;
  263. source_3mf_path: string | null;
  264. f3d_path: string | null;
  265. duplicates: ArchiveDuplicate[] | null;
  266. duplicate_count: number;
  267. object_count: number | null;
  268. print_name: string | null;
  269. print_time_seconds: number | null;
  270. actual_time_seconds: number | null; // Computed from started_at/completed_at
  271. time_accuracy: number | null; // Percentage: 100 = perfect, >100 = faster than estimated
  272. filament_used_grams: number | null;
  273. filament_type: string | null;
  274. filament_color: string | null;
  275. layer_height: number | null;
  276. total_layers: number | null;
  277. nozzle_diameter: number | null;
  278. bed_temperature: number | null;
  279. nozzle_temperature: number | null;
  280. sliced_for_model: string | null; // Printer model this file was sliced for
  281. status: string;
  282. started_at: string | null;
  283. completed_at: string | null;
  284. extra_data: Record<string, unknown> | null;
  285. makerworld_url: string | null;
  286. designer: string | null;
  287. external_url: string | null;
  288. is_favorite: boolean;
  289. tags: string | null;
  290. notes: string | null;
  291. cost: number | null;
  292. photos: string[] | null;
  293. failure_reason: string | null;
  294. quantity: number;
  295. energy_kwh: number | null;
  296. energy_cost: number | null;
  297. created_at: string;
  298. }
  299. export interface ArchiveStats {
  300. total_prints: number;
  301. successful_prints: number;
  302. failed_prints: number;
  303. total_print_time_hours: number;
  304. total_filament_grams: number;
  305. total_cost: number;
  306. prints_by_filament_type: Record<string, number>;
  307. prints_by_printer: Record<string, number>;
  308. average_time_accuracy: number | null;
  309. time_accuracy_by_printer: Record<string, number> | null;
  310. total_energy_kwh: number;
  311. total_energy_cost: number;
  312. }
  313. export interface TagInfo {
  314. name: string;
  315. count: number;
  316. }
  317. export interface FailureAnalysis {
  318. period_days: number;
  319. total_prints: number;
  320. failed_prints: number;
  321. failure_rate: number;
  322. failures_by_reason: Record<string, number>;
  323. failures_by_filament: Record<string, number>;
  324. failures_by_printer: Record<string, number>;
  325. failures_by_hour: Record<number, number>;
  326. recent_failures: Array<{
  327. id: number;
  328. print_name: string;
  329. failure_reason: string | null;
  330. filament_type: string | null;
  331. printer_id: number | null;
  332. created_at: string | null;
  333. }>;
  334. trend: Array<{
  335. week_start: string;
  336. total_prints: number;
  337. failed_prints: number;
  338. failure_rate: number;
  339. }>;
  340. }
  341. export interface BulkUploadResult {
  342. uploaded: number;
  343. failed: number;
  344. results: Array<{ filename: string; id: number; status: string }>;
  345. errors: Array<{ filename: string; error: string }>;
  346. }
  347. // Archive Comparison types
  348. export interface ComparisonArchiveInfo {
  349. id: number;
  350. print_name: string;
  351. status: string;
  352. created_at: string | null;
  353. printer_id: number | null;
  354. project_name: string | null;
  355. }
  356. export interface ComparisonField {
  357. field: string;
  358. label: string;
  359. unit: string | null;
  360. values: (string | number | null)[];
  361. raw_values: (string | number | null)[];
  362. has_difference: boolean;
  363. }
  364. export interface SuccessCorrelationInsight {
  365. field: string;
  366. label: string;
  367. insight: string;
  368. success_avg?: number;
  369. failed_avg?: number;
  370. success_values?: string[];
  371. failed_values?: string[];
  372. }
  373. export interface SuccessCorrelation {
  374. has_both_outcomes: boolean;
  375. message?: string;
  376. successful_count?: number;
  377. failed_count?: number;
  378. insights?: SuccessCorrelationInsight[];
  379. }
  380. export interface ArchiveComparison {
  381. archives: ComparisonArchiveInfo[];
  382. comparison: ComparisonField[];
  383. differences: ComparisonField[];
  384. success_correlation: SuccessCorrelation;
  385. }
  386. export interface SimilarArchive {
  387. archive: {
  388. id: number;
  389. print_name: string;
  390. status: string;
  391. created_at: string | null;
  392. };
  393. match_reason: string;
  394. match_score: number;
  395. }
  396. // Project types
  397. export interface ProjectStats {
  398. total_archives: number;
  399. total_items: number; // Sum of quantities (total items printed)
  400. completed_prints: number; // Sum of quantities for completed prints (parts)
  401. failed_prints: number;
  402. queued_prints: number;
  403. in_progress_prints: number;
  404. total_print_time_hours: number;
  405. total_filament_grams: number;
  406. progress_percent: number | null; // Plates progress (total_archives / target_count)
  407. parts_progress_percent: number | null; // Parts progress (completed_prints / target_parts_count)
  408. estimated_cost: number;
  409. total_energy_kwh: number;
  410. total_energy_cost: number;
  411. remaining_prints: number | null; // Remaining plates
  412. remaining_parts: number | null; // Remaining parts
  413. bom_total_items: number;
  414. bom_completed_items: number;
  415. }
  416. export interface ProjectChildPreview {
  417. id: number;
  418. name: string;
  419. color: string | null;
  420. status: string;
  421. progress_percent: number | null;
  422. }
  423. export interface Project {
  424. id: number;
  425. name: string;
  426. description: string | null;
  427. color: string | null;
  428. status: string; // active, completed, archived
  429. target_count: number | null; // Target number of plates/print jobs
  430. target_parts_count: number | null; // Target number of parts/objects
  431. notes: string | null;
  432. attachments: ProjectAttachment[] | null;
  433. tags: string | null;
  434. due_date: string | null;
  435. priority: string; // low, normal, high, urgent
  436. budget: number | null;
  437. is_template: boolean;
  438. template_source_id: number | null;
  439. parent_id: number | null;
  440. parent_name: string | null;
  441. children: ProjectChildPreview[];
  442. created_at: string;
  443. updated_at: string;
  444. stats?: ProjectStats;
  445. }
  446. export interface ProjectAttachment {
  447. filename: string;
  448. original_name: string;
  449. size: number;
  450. uploaded_at: string;
  451. }
  452. export interface ArchivePreview {
  453. id: number;
  454. print_name: string | null;
  455. thumbnail_path: string | null;
  456. status: string;
  457. filament_type: string | null;
  458. filament_color: string | null;
  459. }
  460. export interface ProjectListItem {
  461. id: number;
  462. name: string;
  463. description: string | null;
  464. color: string | null;
  465. status: string;
  466. target_count: number | null; // Target number of plates/print jobs
  467. target_parts_count: number | null; // Target number of parts/objects
  468. created_at: string;
  469. archive_count: number; // Number of print jobs (plates)
  470. total_items: number; // Sum of quantities (total items printed, including failed)
  471. completed_count: number; // Sum of quantities for completed prints only (parts)
  472. failed_count: number; // Sum of quantities for failed prints
  473. queue_count: number;
  474. progress_percent: number | null; // Plates progress
  475. archives: ArchivePreview[];
  476. }
  477. export interface ProjectCreate {
  478. name: string;
  479. description?: string;
  480. color?: string;
  481. target_count?: number;
  482. target_parts_count?: number;
  483. notes?: string;
  484. tags?: string;
  485. due_date?: string;
  486. priority?: string;
  487. budget?: number;
  488. parent_id?: number;
  489. }
  490. export interface ProjectUpdate {
  491. name?: string;
  492. description?: string;
  493. color?: string;
  494. status?: string;
  495. target_count?: number;
  496. target_parts_count?: number;
  497. notes?: string;
  498. tags?: string;
  499. due_date?: string;
  500. priority?: string;
  501. budget?: number;
  502. parent_id?: number;
  503. }
  504. // BOM Types - Tracks sourced/purchased parts (hardware, electronics, etc.)
  505. export interface BOMItem {
  506. id: number;
  507. project_id: number;
  508. name: string;
  509. quantity_needed: number;
  510. quantity_acquired: number;
  511. unit_price: number | null;
  512. sourcing_url: string | null;
  513. archive_id: number | null;
  514. archive_name: string | null;
  515. stl_filename: string | null;
  516. remarks: string | null;
  517. sort_order: number;
  518. is_complete: boolean;
  519. created_at: string;
  520. updated_at: string;
  521. }
  522. export interface BOMItemCreate {
  523. name: string;
  524. quantity_needed?: number;
  525. unit_price?: number;
  526. sourcing_url?: string;
  527. archive_id?: number;
  528. stl_filename?: string;
  529. remarks?: string;
  530. }
  531. export interface BOMItemUpdate {
  532. name?: string;
  533. quantity_needed?: number;
  534. quantity_acquired?: number;
  535. unit_price?: number;
  536. sourcing_url?: string;
  537. archive_id?: number;
  538. stl_filename?: string;
  539. remarks?: string;
  540. }
  541. // Project Export/Import Types
  542. export interface BOMItemExport {
  543. name: string;
  544. quantity_needed: number;
  545. quantity_acquired: number;
  546. unit_price: number | null;
  547. sourcing_url: string | null;
  548. stl_filename: string | null;
  549. remarks: string | null;
  550. }
  551. export interface LinkedFolderExport {
  552. name: string;
  553. }
  554. export interface ProjectExport {
  555. name: string;
  556. description: string | null;
  557. color: string | null;
  558. status: string;
  559. target_count: number | null;
  560. target_parts_count: number | null;
  561. notes: string | null;
  562. tags: string | null;
  563. due_date: string | null;
  564. priority: string;
  565. budget: number | null;
  566. bom_items: BOMItemExport[];
  567. linked_folders: LinkedFolderExport[];
  568. }
  569. export interface ProjectImport {
  570. name: string;
  571. description?: string;
  572. color?: string;
  573. status?: string;
  574. target_count?: number;
  575. target_parts_count?: number;
  576. notes?: string;
  577. tags?: string;
  578. due_date?: string;
  579. priority?: string;
  580. budget?: number;
  581. bom_items?: BOMItemExport[];
  582. linked_folders?: LinkedFolderExport[];
  583. }
  584. // Timeline Types
  585. export interface TimelineEvent {
  586. event_type: string;
  587. timestamp: string;
  588. title: string;
  589. description: string | null;
  590. metadata: Record<string, unknown> | null;
  591. }
  592. // API Key types
  593. export interface APIKey {
  594. id: number;
  595. name: string;
  596. key_prefix: string;
  597. can_queue: boolean;
  598. can_control_printer: boolean;
  599. can_read_status: boolean;
  600. printer_ids: number[] | null;
  601. enabled: boolean;
  602. last_used: string | null;
  603. created_at: string;
  604. expires_at: string | null;
  605. }
  606. export interface APIKeyCreate {
  607. name: string;
  608. can_queue?: boolean;
  609. can_control_printer?: boolean;
  610. can_read_status?: boolean;
  611. printer_ids?: number[] | null;
  612. expires_at?: string | null;
  613. }
  614. export interface APIKeyCreateResponse extends APIKey {
  615. key: string; // Full key, only shown on creation
  616. }
  617. export interface APIKeyUpdate {
  618. name?: string;
  619. can_queue?: boolean;
  620. can_control_printer?: boolean;
  621. can_read_status?: boolean;
  622. printer_ids?: number[] | null;
  623. enabled?: boolean;
  624. expires_at?: string | null;
  625. }
  626. // Settings types
  627. export interface AppSettings {
  628. auto_archive: boolean;
  629. save_thumbnails: boolean;
  630. capture_finish_photo: boolean;
  631. default_filament_cost: number;
  632. currency: string;
  633. energy_cost_per_kwh: number;
  634. energy_tracking_mode: 'print' | 'total';
  635. check_updates: boolean;
  636. check_printer_firmware: boolean;
  637. notification_language: string;
  638. // AMS threshold settings
  639. ams_humidity_good: number; // <= this is green
  640. ams_humidity_fair: number; // <= this is orange, > is red
  641. ams_temp_good: number; // <= this is green/blue
  642. ams_temp_fair: number; // <= this is orange, > is red
  643. ams_history_retention_days: number; // days to keep AMS sensor history
  644. // Print modal settings
  645. per_printer_mapping_expanded: boolean; // Whether custom mapping is expanded by default in print modal
  646. // Date/time format settings
  647. date_format: 'system' | 'us' | 'eu' | 'iso';
  648. time_format: 'system' | '12h' | '24h';
  649. // Default printer
  650. default_printer_id: number | null;
  651. // Dark mode theme settings
  652. dark_style: 'classic' | 'glow' | 'vibrant';
  653. dark_background: 'neutral' | 'warm' | 'cool' | 'oled' | 'slate' | 'forest';
  654. dark_accent: 'green' | 'teal' | 'blue' | 'orange' | 'purple' | 'red';
  655. // Light mode theme settings
  656. light_style: 'classic' | 'glow' | 'vibrant';
  657. light_background: 'neutral' | 'warm' | 'cool';
  658. light_accent: 'green' | 'teal' | 'blue' | 'orange' | 'purple' | 'red';
  659. // FTP retry settings
  660. ftp_retry_enabled: boolean;
  661. ftp_retry_count: number;
  662. ftp_retry_delay: number;
  663. ftp_timeout: number;
  664. // MQTT relay settings
  665. mqtt_enabled: boolean;
  666. mqtt_broker: string;
  667. mqtt_port: number;
  668. mqtt_username: string;
  669. mqtt_password: string;
  670. mqtt_topic_prefix: string;
  671. mqtt_use_tls: boolean;
  672. // External URL for notifications
  673. external_url: string;
  674. // Home Assistant integration
  675. ha_enabled: boolean;
  676. ha_url: string;
  677. ha_token: string;
  678. // File Manager / Library settings
  679. library_archive_mode: 'always' | 'never' | 'ask';
  680. library_disk_warning_gb: number;
  681. // Camera view settings
  682. camera_view_mode: 'window' | 'embedded';
  683. // Prometheus metrics
  684. prometheus_enabled: boolean;
  685. prometheus_token: string;
  686. }
  687. export type AppSettingsUpdate = Partial<AppSettings>;
  688. // MQTT relay status
  689. export interface MQTTStatus {
  690. enabled: boolean;
  691. connected: boolean;
  692. broker: string;
  693. port: number;
  694. topic_prefix: string;
  695. }
  696. // Cloud types
  697. export interface CloudAuthStatus {
  698. is_authenticated: boolean;
  699. email: string | null;
  700. }
  701. export interface CloudLoginResponse {
  702. success: boolean;
  703. needs_verification: boolean;
  704. message: string;
  705. }
  706. export interface SlicerSetting {
  707. setting_id: string;
  708. name: string;
  709. type: string;
  710. version: string | null;
  711. user_id: string | null;
  712. updated_time: string | null;
  713. }
  714. export interface SlicerSettingsResponse {
  715. filament: SlicerSetting[];
  716. printer: SlicerSetting[];
  717. process: SlicerSetting[];
  718. }
  719. export interface SlicerSettingDetail {
  720. message?: string | null;
  721. code?: string | null;
  722. error?: string | null;
  723. public: boolean;
  724. version?: string | null;
  725. type: string;
  726. name: string;
  727. update_time?: string | null;
  728. nickname?: string | null;
  729. base_id?: string | null;
  730. setting: Record<string, unknown>;
  731. filament_id?: string | null;
  732. setting_id?: string | null;
  733. }
  734. export interface SlicerSettingCreate {
  735. type: string; // 'filament', 'print', or 'printer'
  736. name: string;
  737. base_id: string;
  738. setting: Record<string, unknown>;
  739. }
  740. export interface SlicerSettingUpdate {
  741. name?: string;
  742. setting?: Record<string, unknown>;
  743. }
  744. export interface SlicerSettingDeleteResponse {
  745. success: boolean;
  746. message: string;
  747. }
  748. export interface FieldOption {
  749. value: string;
  750. label: string;
  751. }
  752. export interface FieldDefinition {
  753. key: string;
  754. label: string;
  755. type: 'text' | 'number' | 'boolean' | 'select';
  756. category: string;
  757. description?: string;
  758. options?: FieldOption[];
  759. unit?: string;
  760. min?: number;
  761. max?: number;
  762. step?: number;
  763. }
  764. export interface FieldDefinitionsResponse {
  765. version: string;
  766. description: string;
  767. fields: FieldDefinition[];
  768. }
  769. export interface CloudDevice {
  770. dev_id: string;
  771. name: string;
  772. dev_model_name: string | null;
  773. dev_product_name: string | null;
  774. online: boolean;
  775. }
  776. // Smart Plug types
  777. export interface SmartPlug {
  778. id: number;
  779. name: string;
  780. plug_type: 'tasmota' | 'homeassistant';
  781. ip_address: string | null; // Required for Tasmota
  782. ha_entity_id: string | null; // Required for Home Assistant (e.g., "switch.printer_plug", "script.turn_on_printer")
  783. // Home Assistant energy sensor entities (optional)
  784. ha_power_entity: string | null;
  785. ha_energy_today_entity: string | null;
  786. ha_energy_total_entity: string | null;
  787. printer_id: number | null;
  788. enabled: boolean;
  789. auto_on: boolean;
  790. auto_off: boolean;
  791. off_delay_mode: 'time' | 'temperature';
  792. off_delay_minutes: number;
  793. off_temp_threshold: number;
  794. username: string | null;
  795. password: string | null;
  796. // Power alerts
  797. power_alert_enabled: boolean;
  798. power_alert_high: number | null;
  799. power_alert_low: number | null;
  800. power_alert_last_triggered: string | null;
  801. // Schedule
  802. schedule_enabled: boolean;
  803. schedule_on_time: string | null;
  804. schedule_off_time: string | null;
  805. // Visibility options
  806. show_in_switchbar: boolean;
  807. show_on_printer_card: boolean; // For scripts: show on printer card
  808. // Status
  809. last_state: string | null;
  810. last_checked: string | null;
  811. auto_off_executed: boolean; // True when auto-off was triggered after print
  812. created_at: string;
  813. updated_at: string;
  814. }
  815. export interface SmartPlugCreate {
  816. name: string;
  817. plug_type?: 'tasmota' | 'homeassistant';
  818. ip_address?: string | null; // Required for Tasmota
  819. ha_entity_id?: string | null; // Required for Home Assistant
  820. // Home Assistant energy sensor entities (optional)
  821. ha_power_entity?: string | null;
  822. ha_energy_today_entity?: string | null;
  823. ha_energy_total_entity?: string | null;
  824. printer_id?: number | null;
  825. enabled?: boolean;
  826. auto_on?: boolean;
  827. auto_off?: boolean;
  828. off_delay_mode?: 'time' | 'temperature';
  829. off_delay_minutes?: number;
  830. off_temp_threshold?: number;
  831. username?: string | null;
  832. password?: string | null;
  833. // Power alerts
  834. power_alert_enabled?: boolean;
  835. power_alert_high?: number | null;
  836. power_alert_low?: number | null;
  837. // Schedule
  838. schedule_enabled?: boolean;
  839. schedule_on_time?: string | null;
  840. schedule_off_time?: string | null;
  841. // Visibility options
  842. show_in_switchbar?: boolean;
  843. show_on_printer_card?: boolean;
  844. }
  845. export interface SmartPlugUpdate {
  846. name?: string;
  847. plug_type?: 'tasmota' | 'homeassistant';
  848. ip_address?: string | null;
  849. ha_entity_id?: string | null;
  850. // Home Assistant energy sensor entities (optional)
  851. ha_power_entity?: string | null;
  852. ha_energy_today_entity?: string | null;
  853. ha_energy_total_entity?: string | null;
  854. printer_id?: number | null;
  855. enabled?: boolean;
  856. auto_on?: boolean;
  857. auto_off?: boolean;
  858. off_delay_mode?: 'time' | 'temperature';
  859. off_delay_minutes?: number;
  860. off_temp_threshold?: number;
  861. username?: string | null;
  862. password?: string | null;
  863. // Power alerts
  864. power_alert_enabled?: boolean;
  865. power_alert_high?: number | null;
  866. power_alert_low?: number | null;
  867. // Schedule
  868. schedule_enabled?: boolean;
  869. schedule_on_time?: string | null;
  870. schedule_off_time?: string | null;
  871. // Visibility options
  872. show_in_switchbar?: boolean;
  873. show_on_printer_card?: boolean;
  874. }
  875. // Home Assistant entity for smart plug selection
  876. export interface HAEntity {
  877. entity_id: string;
  878. friendly_name: string;
  879. state: string | null;
  880. domain: string; // "switch", "light", "input_boolean", "script"
  881. }
  882. // Home Assistant sensor entity for energy monitoring
  883. export interface HASensorEntity {
  884. entity_id: string;
  885. friendly_name: string;
  886. state: string | null;
  887. unit_of_measurement: string | null; // "W", "kW", "kWh", "Wh"
  888. }
  889. export interface HATestConnectionResult {
  890. success: boolean;
  891. message: string | null;
  892. error: string | null;
  893. }
  894. export interface SmartPlugEnergy {
  895. power: number | null; // Current watts
  896. voltage: number | null; // Volts
  897. current: number | null; // Amps
  898. today: number | null; // kWh used today
  899. yesterday: number | null; // kWh used yesterday
  900. total: number | null; // Total kWh
  901. factor: number | null; // Power factor (0-1)
  902. apparent_power: number | null; // VA
  903. reactive_power: number | null; // VAr
  904. }
  905. export interface SmartPlugStatus {
  906. state: string | null;
  907. reachable: boolean;
  908. device_name: string | null;
  909. energy: SmartPlugEnergy | null;
  910. }
  911. export interface SmartPlugTestResult {
  912. success: boolean;
  913. state: string | null;
  914. device_name: string | null;
  915. }
  916. // Tasmota Discovery types
  917. export interface TasmotaScanStatus {
  918. running: boolean;
  919. scanned: number;
  920. total: number;
  921. }
  922. export interface DiscoveredTasmotaDevice {
  923. ip_address: string;
  924. name: string;
  925. module: number | null;
  926. state: string | null;
  927. discovered_at: string | null;
  928. }
  929. // Print Queue types
  930. export interface PrintQueueItem {
  931. id: number;
  932. printer_id: number | null; // null = unassigned
  933. target_model: string | null; // Target printer model for model-based assignment
  934. required_filament_types: string[] | null; // Required filament types for model-based assignment
  935. waiting_reason: string | null; // Why a model-based job hasn't started yet
  936. // Either archive_id OR library_file_id must be set (archive created at print start)
  937. archive_id: number | null;
  938. library_file_id: number | null;
  939. position: number;
  940. scheduled_time: string | null;
  941. require_previous_success: boolean;
  942. auto_off_after: boolean;
  943. manual_start: boolean; // Requires manual trigger to start (staged)
  944. ams_mapping: number[] | null; // AMS slot mapping for multi-color prints
  945. plate_id: number | null; // Plate ID for multi-plate 3MF files
  946. // Print options
  947. bed_levelling: boolean;
  948. flow_cali: boolean;
  949. vibration_cali: boolean;
  950. layer_inspect: boolean;
  951. timelapse: boolean;
  952. use_ams: boolean;
  953. status: 'pending' | 'printing' | 'completed' | 'failed' | 'skipped' | 'cancelled';
  954. started_at: string | null;
  955. completed_at: string | null;
  956. error_message: string | null;
  957. created_at: string;
  958. archive_name?: string | null;
  959. archive_thumbnail?: string | null;
  960. library_file_name?: string | null;
  961. library_file_thumbnail?: string | null;
  962. printer_name?: string | null;
  963. print_time_seconds?: number | null; // Estimated print time from archive or library file
  964. }
  965. export interface PrintQueueItemCreate {
  966. printer_id?: number | null; // null = unassigned
  967. target_model?: string | null; // Target printer model (mutually exclusive with printer_id)
  968. // Either archive_id OR library_file_id must be provided
  969. archive_id?: number | null;
  970. library_file_id?: number | null;
  971. scheduled_time?: string | null;
  972. require_previous_success?: boolean;
  973. auto_off_after?: boolean;
  974. manual_start?: boolean; // Requires manual trigger to start (staged)
  975. ams_mapping?: number[] | null; // AMS slot mapping for multi-color prints
  976. plate_id?: number | null; // Plate ID for multi-plate 3MF files
  977. // Print options
  978. bed_levelling?: boolean;
  979. flow_cali?: boolean;
  980. vibration_cali?: boolean;
  981. layer_inspect?: boolean;
  982. timelapse?: boolean;
  983. use_ams?: boolean;
  984. }
  985. export interface PrintQueueItemUpdate {
  986. printer_id?: number | null; // null = unassign
  987. target_model?: string | null; // Target printer model (mutually exclusive with printer_id)
  988. position?: number;
  989. scheduled_time?: string | null;
  990. require_previous_success?: boolean;
  991. auto_off_after?: boolean;
  992. manual_start?: boolean;
  993. ams_mapping?: number[];
  994. plate_id?: number | null; // Plate ID for multi-plate 3MF files
  995. // Print options
  996. bed_levelling?: boolean;
  997. flow_cali?: boolean;
  998. vibration_cali?: boolean;
  999. layer_inspect?: boolean;
  1000. timelapse?: boolean;
  1001. use_ams?: boolean;
  1002. }
  1003. export interface PrintQueueBulkUpdate {
  1004. item_ids: number[];
  1005. printer_id?: number | null;
  1006. scheduled_time?: string | null;
  1007. require_previous_success?: boolean;
  1008. auto_off_after?: boolean;
  1009. manual_start?: boolean;
  1010. // Print options
  1011. bed_levelling?: boolean;
  1012. flow_cali?: boolean;
  1013. vibration_cali?: boolean;
  1014. layer_inspect?: boolean;
  1015. timelapse?: boolean;
  1016. use_ams?: boolean;
  1017. }
  1018. export interface PrintQueueBulkUpdateResponse {
  1019. updated_count: number;
  1020. skipped_count: number;
  1021. message: string;
  1022. }
  1023. // MQTT Logging types
  1024. export interface MQTTLogEntry {
  1025. timestamp: string;
  1026. topic: string;
  1027. direction: 'in' | 'out';
  1028. payload: Record<string, unknown>;
  1029. }
  1030. export interface MQTTLogsResponse {
  1031. logging_enabled: boolean;
  1032. logs: MQTTLogEntry[];
  1033. }
  1034. // K-Profile types
  1035. export interface KProfile {
  1036. slot_id: number;
  1037. extruder_id: number;
  1038. nozzle_id: string;
  1039. nozzle_diameter: string;
  1040. filament_id: string;
  1041. name: string;
  1042. k_value: string;
  1043. n_coef: string;
  1044. ams_id: number;
  1045. tray_id: number;
  1046. setting_id: string | null;
  1047. }
  1048. export interface KProfileCreate {
  1049. slot_id?: number; // Storage slot, 0 for new profiles
  1050. extruder_id?: number;
  1051. nozzle_id: string;
  1052. nozzle_diameter: string;
  1053. filament_id: string;
  1054. name: string;
  1055. k_value: string;
  1056. n_coef?: string;
  1057. ams_id?: number;
  1058. tray_id?: number;
  1059. setting_id?: string | null;
  1060. }
  1061. export interface KProfileDelete {
  1062. slot_id: number; // cali_idx - calibration index to delete
  1063. extruder_id: number;
  1064. nozzle_id: string; // e.g., "HH00-0.4"
  1065. nozzle_diameter: string; // e.g., "0.4"
  1066. filament_id: string; // Bambu filament identifier
  1067. setting_id?: string | null; // Setting ID (for X1C series)
  1068. }
  1069. export interface KProfilesResponse {
  1070. profiles: KProfile[];
  1071. nozzle_diameter: string;
  1072. }
  1073. export interface KProfileNote {
  1074. setting_id: string;
  1075. note: string;
  1076. }
  1077. export interface KProfileNotesResponse {
  1078. notes: Record<string, string>; // setting_id -> note
  1079. }
  1080. // Slot Preset Mapping
  1081. export interface SlotPresetMapping {
  1082. ams_id: number;
  1083. tray_id: number;
  1084. preset_id: string;
  1085. preset_name: string;
  1086. }
  1087. // Filament types
  1088. export interface Filament {
  1089. id: number;
  1090. name: string;
  1091. type: string; // PLA, PETG, ABS, etc.
  1092. brand: string | null;
  1093. color: string | null;
  1094. color_hex: string | null;
  1095. cost_per_kg: number;
  1096. spool_weight_g: number;
  1097. currency: string;
  1098. density: number | null;
  1099. print_temp_min: number | null;
  1100. print_temp_max: number | null;
  1101. bed_temp_min: number | null;
  1102. bed_temp_max: number | null;
  1103. created_at: string;
  1104. updated_at: string;
  1105. }
  1106. // Notification Provider types
  1107. export type ProviderType = 'callmebot' | 'ntfy' | 'pushover' | 'telegram' | 'email' | 'discord' | 'webhook';
  1108. export interface NotificationProvider {
  1109. id: number;
  1110. name: string;
  1111. provider_type: ProviderType;
  1112. enabled: boolean;
  1113. config: Record<string, unknown>;
  1114. // Print lifecycle events
  1115. on_print_start: boolean;
  1116. on_print_complete: boolean;
  1117. on_print_failed: boolean;
  1118. on_print_stopped: boolean;
  1119. on_print_progress: boolean;
  1120. // Printer status events
  1121. on_printer_offline: boolean;
  1122. on_printer_error: boolean;
  1123. on_filament_low: boolean;
  1124. on_maintenance_due: boolean;
  1125. // AMS environmental alarms (regular AMS)
  1126. on_ams_humidity_high: boolean;
  1127. on_ams_temperature_high: boolean;
  1128. // AMS-HT environmental alarms
  1129. on_ams_ht_humidity_high: boolean;
  1130. on_ams_ht_temperature_high: boolean;
  1131. // Build plate detection
  1132. on_plate_not_empty: boolean;
  1133. // Print queue events
  1134. on_queue_job_added: boolean;
  1135. on_queue_job_assigned: boolean;
  1136. on_queue_job_started: boolean;
  1137. on_queue_job_waiting: boolean;
  1138. on_queue_job_skipped: boolean;
  1139. on_queue_job_failed: boolean;
  1140. on_queue_completed: boolean;
  1141. // Quiet hours
  1142. quiet_hours_enabled: boolean;
  1143. quiet_hours_start: string | null;
  1144. quiet_hours_end: string | null;
  1145. // Daily digest
  1146. daily_digest_enabled: boolean;
  1147. daily_digest_time: string | null;
  1148. // Printer filter
  1149. printer_id: number | null;
  1150. // Status tracking
  1151. last_success: string | null;
  1152. last_error: string | null;
  1153. last_error_at: string | null;
  1154. // Timestamps
  1155. created_at: string;
  1156. updated_at: string;
  1157. }
  1158. export interface NotificationProviderCreate {
  1159. name: string;
  1160. provider_type: ProviderType;
  1161. enabled?: boolean;
  1162. config: Record<string, unknown>;
  1163. // Print lifecycle events
  1164. on_print_start?: boolean;
  1165. on_print_complete?: boolean;
  1166. on_print_failed?: boolean;
  1167. on_print_stopped?: boolean;
  1168. on_print_progress?: boolean;
  1169. // Printer status events
  1170. on_printer_offline?: boolean;
  1171. on_printer_error?: boolean;
  1172. on_filament_low?: boolean;
  1173. on_maintenance_due?: boolean;
  1174. // AMS environmental alarms (regular AMS)
  1175. on_ams_humidity_high?: boolean;
  1176. on_ams_temperature_high?: boolean;
  1177. // AMS-HT environmental alarms
  1178. on_ams_ht_humidity_high?: boolean;
  1179. on_ams_ht_temperature_high?: boolean;
  1180. // Build plate detection
  1181. on_plate_not_empty?: boolean;
  1182. // Print queue events
  1183. on_queue_job_added?: boolean;
  1184. on_queue_job_assigned?: boolean;
  1185. on_queue_job_started?: boolean;
  1186. on_queue_job_waiting?: boolean;
  1187. on_queue_job_skipped?: boolean;
  1188. on_queue_job_failed?: boolean;
  1189. on_queue_completed?: boolean;
  1190. // Quiet hours
  1191. quiet_hours_enabled?: boolean;
  1192. quiet_hours_start?: string | null;
  1193. quiet_hours_end?: string | null;
  1194. // Daily digest
  1195. daily_digest_enabled?: boolean;
  1196. daily_digest_time?: string | null;
  1197. // Printer filter
  1198. printer_id?: number | null;
  1199. }
  1200. export interface NotificationProviderUpdate {
  1201. name?: string;
  1202. provider_type?: ProviderType;
  1203. enabled?: boolean;
  1204. config?: Record<string, unknown>;
  1205. // Print lifecycle events
  1206. on_print_start?: boolean;
  1207. on_print_complete?: boolean;
  1208. on_print_failed?: boolean;
  1209. on_print_stopped?: boolean;
  1210. on_print_progress?: boolean;
  1211. // Printer status events
  1212. on_printer_offline?: boolean;
  1213. on_printer_error?: boolean;
  1214. on_filament_low?: boolean;
  1215. on_maintenance_due?: boolean;
  1216. // AMS environmental alarms (regular AMS)
  1217. on_ams_humidity_high?: boolean;
  1218. on_ams_temperature_high?: boolean;
  1219. // AMS-HT environmental alarms
  1220. on_ams_ht_humidity_high?: boolean;
  1221. on_ams_ht_temperature_high?: boolean;
  1222. // Build plate detection
  1223. on_plate_not_empty?: boolean;
  1224. // Print queue events
  1225. on_queue_job_added?: boolean;
  1226. on_queue_job_assigned?: boolean;
  1227. on_queue_job_started?: boolean;
  1228. on_queue_job_waiting?: boolean;
  1229. on_queue_job_skipped?: boolean;
  1230. on_queue_job_failed?: boolean;
  1231. on_queue_completed?: boolean;
  1232. // Quiet hours
  1233. quiet_hours_enabled?: boolean;
  1234. quiet_hours_start?: string | null;
  1235. quiet_hours_end?: string | null;
  1236. // Daily digest
  1237. daily_digest_enabled?: boolean;
  1238. daily_digest_time?: string | null;
  1239. // Printer filter
  1240. printer_id?: number | null;
  1241. }
  1242. // GitHub Backup types
  1243. export type ScheduleType = 'hourly' | 'daily' | 'weekly';
  1244. export interface GitHubBackupConfig {
  1245. id: number;
  1246. repository_url: string;
  1247. has_token: boolean;
  1248. branch: string;
  1249. schedule_enabled: boolean;
  1250. schedule_type: ScheduleType;
  1251. backup_kprofiles: boolean;
  1252. backup_cloud_profiles: boolean;
  1253. backup_settings: boolean;
  1254. enabled: boolean;
  1255. last_backup_at: string | null;
  1256. last_backup_status: string | null;
  1257. last_backup_message: string | null;
  1258. last_backup_commit_sha: string | null;
  1259. next_scheduled_run: string | null;
  1260. created_at: string;
  1261. updated_at: string;
  1262. }
  1263. export interface GitHubBackupConfigCreate {
  1264. repository_url: string;
  1265. access_token: string;
  1266. branch?: string;
  1267. schedule_enabled?: boolean;
  1268. schedule_type?: ScheduleType;
  1269. backup_kprofiles?: boolean;
  1270. backup_cloud_profiles?: boolean;
  1271. backup_settings?: boolean;
  1272. enabled?: boolean;
  1273. }
  1274. export interface GitHubBackupLog {
  1275. id: number;
  1276. config_id: number;
  1277. started_at: string;
  1278. completed_at: string | null;
  1279. status: string;
  1280. trigger: string;
  1281. commit_sha: string | null;
  1282. files_changed: number;
  1283. error_message: string | null;
  1284. }
  1285. export interface GitHubBackupStatus {
  1286. configured: boolean;
  1287. enabled: boolean;
  1288. is_running: boolean;
  1289. progress: string | null;
  1290. last_backup_at: string | null;
  1291. last_backup_status: string | null;
  1292. next_scheduled_run: string | null;
  1293. }
  1294. export interface GitHubTestConnectionResponse {
  1295. success: boolean;
  1296. message: string;
  1297. repo_name: string | null;
  1298. permissions: Record<string, boolean> | null;
  1299. }
  1300. export interface GitHubBackupTriggerResponse {
  1301. success: boolean;
  1302. message: string;
  1303. log_id: number | null;
  1304. commit_sha: string | null;
  1305. files_changed: number;
  1306. }
  1307. export interface NotificationTestRequest {
  1308. provider_type: ProviderType;
  1309. config: Record<string, unknown>;
  1310. }
  1311. export interface NotificationTestResponse {
  1312. success: boolean;
  1313. message: string;
  1314. }
  1315. // Provider-specific config types for reference
  1316. export interface CallMeBotConfig {
  1317. phone: string;
  1318. apikey: string;
  1319. }
  1320. export interface NtfyConfig {
  1321. server?: string;
  1322. topic: string;
  1323. auth_token?: string | null;
  1324. }
  1325. export interface PushoverConfig {
  1326. user_key: string;
  1327. app_token: string;
  1328. priority?: number;
  1329. }
  1330. export interface TelegramConfig {
  1331. bot_token: string;
  1332. chat_id: string;
  1333. }
  1334. export interface EmailConfig {
  1335. smtp_server: string;
  1336. smtp_port?: number;
  1337. username: string;
  1338. password: string;
  1339. from_email: string;
  1340. to_email: string;
  1341. use_tls?: boolean;
  1342. }
  1343. // Notification Template types
  1344. export interface NotificationTemplate {
  1345. id: number;
  1346. event_type: string;
  1347. name: string;
  1348. title_template: string;
  1349. body_template: string;
  1350. is_default: boolean;
  1351. created_at: string;
  1352. updated_at: string;
  1353. }
  1354. export interface NotificationTemplateUpdate {
  1355. title_template?: string;
  1356. body_template?: string;
  1357. }
  1358. export interface EventVariablesResponse {
  1359. event_type: string;
  1360. event_name: string;
  1361. variables: string[];
  1362. }
  1363. export interface TemplatePreviewRequest {
  1364. event_type: string;
  1365. title_template: string;
  1366. body_template: string;
  1367. }
  1368. export interface TemplatePreviewResponse {
  1369. title: string;
  1370. body: string;
  1371. }
  1372. // Notification Log types
  1373. export interface NotificationLogEntry {
  1374. id: number;
  1375. provider_id: number;
  1376. provider_name: string | null;
  1377. provider_type: string | null;
  1378. event_type: string;
  1379. title: string;
  1380. message: string;
  1381. success: boolean;
  1382. error_message: string | null;
  1383. printer_id: number | null;
  1384. printer_name: string | null;
  1385. created_at: string;
  1386. }
  1387. export interface NotificationLogStats {
  1388. total: number;
  1389. success_count: number;
  1390. failure_count: number;
  1391. by_event_type: Record<string, number>;
  1392. by_provider: Record<string, number>;
  1393. }
  1394. // Spoolman types
  1395. export interface SpoolmanStatus {
  1396. enabled: boolean;
  1397. connected: boolean;
  1398. url: string | null;
  1399. }
  1400. export interface SkippedSpool {
  1401. location: string;
  1402. reason: string;
  1403. filament_type: string | null;
  1404. color: string | null;
  1405. }
  1406. export interface SpoolmanSyncResult {
  1407. success: boolean;
  1408. synced_count: number;
  1409. skipped_count: number;
  1410. skipped: SkippedSpool[];
  1411. errors: string[];
  1412. }
  1413. export interface UnlinkedSpool {
  1414. id: number;
  1415. filament_name: string | null;
  1416. filament_material: string | null;
  1417. filament_color_hex: string | null;
  1418. remaining_weight: number | null;
  1419. location: string | null;
  1420. }
  1421. // Update types
  1422. export interface VersionInfo {
  1423. version: string;
  1424. repo: string;
  1425. }
  1426. export interface UpdateCheckResult {
  1427. update_available: boolean;
  1428. current_version: string;
  1429. latest_version: string | null;
  1430. release_name?: string;
  1431. release_notes?: string;
  1432. release_url?: string;
  1433. published_at?: string;
  1434. error?: string;
  1435. message?: string;
  1436. is_docker?: boolean;
  1437. update_method?: 'docker' | 'git';
  1438. }
  1439. export interface UpdateStatus {
  1440. status: 'idle' | 'checking' | 'downloading' | 'installing' | 'complete' | 'error';
  1441. progress: number;
  1442. message: string;
  1443. error: string | null;
  1444. }
  1445. // Maintenance types
  1446. export interface MaintenanceType {
  1447. id: number;
  1448. name: string;
  1449. description: string | null;
  1450. default_interval_hours: number;
  1451. interval_type: 'hours' | 'days'; // "hours" = print hours, "days" = calendar days
  1452. icon: string | null;
  1453. wiki_url: string | null; // Documentation link
  1454. is_system: boolean;
  1455. created_at: string;
  1456. }
  1457. export interface MaintenanceTypeCreate {
  1458. name: string;
  1459. description?: string | null;
  1460. default_interval_hours?: number;
  1461. interval_type?: 'hours' | 'days';
  1462. icon?: string | null;
  1463. wiki_url?: string | null;
  1464. }
  1465. export interface MaintenanceStatus {
  1466. id: number;
  1467. printer_id: number;
  1468. printer_name: string;
  1469. printer_model: string | null;
  1470. maintenance_type_id: number;
  1471. maintenance_type_name: string;
  1472. maintenance_type_icon: string | null;
  1473. maintenance_type_wiki_url: string | null; // Custom wiki URL from type
  1474. enabled: boolean;
  1475. interval_hours: number; // For hours type: print hours; for days type: number of days
  1476. interval_type: 'hours' | 'days';
  1477. current_hours: number;
  1478. hours_since_maintenance: number;
  1479. hours_until_due: number;
  1480. days_since_maintenance: number | null; // For days type
  1481. days_until_due: number | null; // For days type
  1482. is_due: boolean;
  1483. is_warning: boolean;
  1484. last_performed_at: string | null;
  1485. }
  1486. export interface PrinterMaintenanceOverview {
  1487. printer_id: number;
  1488. printer_name: string;
  1489. printer_model: string | null;
  1490. total_print_hours: number;
  1491. maintenance_items: MaintenanceStatus[];
  1492. due_count: number;
  1493. warning_count: number;
  1494. }
  1495. export interface MaintenanceHistory {
  1496. id: number;
  1497. printer_maintenance_id: number;
  1498. performed_at: string;
  1499. hours_at_maintenance: number;
  1500. notes: string | null;
  1501. }
  1502. export interface MaintenanceSummary {
  1503. total_due: number;
  1504. total_warning: number;
  1505. printers_with_issues: Array<{
  1506. printer_id: number;
  1507. printer_name: string;
  1508. due_count: number;
  1509. warning_count: number;
  1510. }>;
  1511. }
  1512. // External Links (sidebar)
  1513. export interface ExternalLink {
  1514. id: number;
  1515. name: string;
  1516. url: string;
  1517. icon: string;
  1518. custom_icon: string | null;
  1519. sort_order: number;
  1520. created_at: string;
  1521. updated_at: string;
  1522. }
  1523. export interface ExternalLinkCreate {
  1524. name: string;
  1525. url: string;
  1526. icon: string;
  1527. }
  1528. export interface ExternalLinkUpdate {
  1529. name?: string;
  1530. url?: string;
  1531. icon?: string;
  1532. }
  1533. // Auth types
  1534. export interface LoginRequest {
  1535. username: string;
  1536. password: string;
  1537. }
  1538. export interface LoginResponse {
  1539. access_token: string;
  1540. token_type: string;
  1541. user: UserResponse;
  1542. }
  1543. export interface UserResponse {
  1544. id: number;
  1545. username: string;
  1546. role: string;
  1547. is_active: boolean;
  1548. created_at: string;
  1549. }
  1550. export interface UserCreate {
  1551. username: string;
  1552. password: string;
  1553. role: string;
  1554. }
  1555. export interface UserUpdate {
  1556. username?: string;
  1557. password?: string;
  1558. role?: string;
  1559. is_active?: boolean;
  1560. }
  1561. export interface SetupRequest {
  1562. auth_enabled: boolean;
  1563. admin_username?: string;
  1564. admin_password?: string;
  1565. }
  1566. export interface SetupResponse {
  1567. auth_enabled: boolean;
  1568. admin_created?: boolean;
  1569. }
  1570. export interface AuthStatus {
  1571. auth_enabled: boolean;
  1572. requires_setup: boolean;
  1573. }
  1574. // API functions
  1575. export const api = {
  1576. // Authentication
  1577. getAuthStatus: () => request<AuthStatus>('/auth/status'),
  1578. setupAuth: (data: SetupRequest) =>
  1579. request<SetupResponse>('/auth/setup', {
  1580. method: 'POST',
  1581. body: JSON.stringify(data),
  1582. }),
  1583. login: (data: LoginRequest) =>
  1584. request<LoginResponse>('/auth/login', {
  1585. method: 'POST',
  1586. body: JSON.stringify(data),
  1587. }),
  1588. logout: () =>
  1589. request<{ message: string }>('/auth/logout', {
  1590. method: 'POST',
  1591. }),
  1592. getCurrentUser: () => request<UserResponse>('/auth/me'),
  1593. disableAuth: () =>
  1594. request<{ message: string; auth_enabled: boolean }>('/auth/disable', {
  1595. method: 'POST',
  1596. }),
  1597. // Users (admin only)
  1598. getUsers: () => request<UserResponse[]>('/users/'),
  1599. getUser: (id: number) => request<UserResponse>(`/users/${id}`),
  1600. createUser: (data: UserCreate) =>
  1601. request<UserResponse>('/users/', {
  1602. method: 'POST',
  1603. body: JSON.stringify(data),
  1604. }),
  1605. updateUser: (id: number, data: UserUpdate) =>
  1606. request<UserResponse>(`/users/${id}`, {
  1607. method: 'PATCH',
  1608. body: JSON.stringify(data),
  1609. }),
  1610. deleteUser: (id: number) =>
  1611. request<void>(`/users/${id}`, {
  1612. method: 'DELETE',
  1613. }),
  1614. // Printers
  1615. getPrinters: () => request<Printer[]>('/printers/'),
  1616. getPrinter: (id: number) => request<Printer>(`/printers/${id}`),
  1617. createPrinter: (data: PrinterCreate) =>
  1618. request<Printer>('/printers/', {
  1619. method: 'POST',
  1620. body: JSON.stringify(data),
  1621. }),
  1622. updatePrinter: (id: number, data: Partial<PrinterCreate>) =>
  1623. request<Printer>(`/printers/${id}`, {
  1624. method: 'PATCH',
  1625. body: JSON.stringify(data),
  1626. }),
  1627. deletePrinter: (id: number, deleteArchives: boolean = true) =>
  1628. request<{ status: string; archives_deleted: boolean }>(
  1629. `/printers/${id}?delete_archives=${deleteArchives}`,
  1630. { method: 'DELETE' }
  1631. ),
  1632. getPrinterStatus: (id: number) =>
  1633. request<PrinterStatus>(`/printers/${id}/status`),
  1634. refreshPrinterStatus: (id: number) =>
  1635. request<{ status: string }>(`/printers/${id}/refresh-status`, {
  1636. method: 'POST',
  1637. }),
  1638. connectPrinter: (id: number) =>
  1639. request<{ connected: boolean }>(`/printers/${id}/connect`, {
  1640. method: 'POST',
  1641. }),
  1642. disconnectPrinter: (id: number) =>
  1643. request<{ connected: boolean }>(`/printers/${id}/disconnect`, {
  1644. method: 'POST',
  1645. }),
  1646. testExternalCamera: (printerId: number, url: string, cameraType: string) =>
  1647. request<{ success: boolean; error?: string; resolution?: string }>(
  1648. `/printers/${printerId}/camera/external/test?url=${encodeURIComponent(url)}&camera_type=${encodeURIComponent(cameraType)}`,
  1649. { method: 'POST' }
  1650. ),
  1651. // Print Control
  1652. stopPrint: (printerId: number) =>
  1653. request<{ success: boolean; message: string }>(`/printers/${printerId}/print/stop`, {
  1654. method: 'POST',
  1655. }),
  1656. pausePrint: (printerId: number) =>
  1657. request<{ success: boolean; message: string }>(`/printers/${printerId}/print/pause`, {
  1658. method: 'POST',
  1659. }),
  1660. resumePrint: (printerId: number) =>
  1661. request<{ success: boolean; message: string }>(`/printers/${printerId}/print/resume`, {
  1662. method: 'POST',
  1663. }),
  1664. // Chamber Light Control
  1665. setChamberLight: (printerId: number, on: boolean) =>
  1666. request<{ success: boolean; message: string }>(`/printers/${printerId}/chamber-light?on=${on}`, {
  1667. method: 'POST',
  1668. }),
  1669. // Skip Objects
  1670. getPrintableObjects: (printerId: number) =>
  1671. request<{
  1672. objects: Array<{ id: number; name: string; x: number | null; y: number | null; skipped: boolean }>;
  1673. total: number;
  1674. skipped_count: number;
  1675. is_printing: boolean;
  1676. bbox_all: [number, number, number, number] | null;
  1677. }>(`/printers/${printerId}/print/objects`),
  1678. skipObjects: (printerId: number, objectIds: number[]) =>
  1679. request<{ success: boolean; message: string; skipped_objects: number[] }>(
  1680. `/printers/${printerId}/print/skip-objects`,
  1681. {
  1682. method: 'POST',
  1683. body: JSON.stringify(objectIds),
  1684. }
  1685. ),
  1686. // AMS Control
  1687. refreshAmsSlot: (printerId: number, amsId: number, slotId: number) =>
  1688. request<{ success: boolean; message: string }>(
  1689. `/printers/${printerId}/ams/${amsId}/slot/${slotId}/refresh`,
  1690. { method: 'POST' }
  1691. ),
  1692. // MQTT Debug Logging
  1693. enableMQTTLogging: (printerId: number) =>
  1694. request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/enable`, {
  1695. method: 'POST',
  1696. }),
  1697. disableMQTTLogging: (printerId: number) =>
  1698. request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/disable`, {
  1699. method: 'POST',
  1700. }),
  1701. getMQTTLogs: (printerId: number) =>
  1702. request<MQTTLogsResponse>(`/printers/${printerId}/logging`),
  1703. clearMQTTLogs: (printerId: number) =>
  1704. request<{ status: string }>(`/printers/${printerId}/logging`, {
  1705. method: 'DELETE',
  1706. }),
  1707. // Printer File Manager
  1708. getPrinterFiles: (printerId: number, path = '/') =>
  1709. request<{
  1710. path: string;
  1711. files: Array<{
  1712. name: string;
  1713. is_directory: boolean;
  1714. size: number;
  1715. path: string;
  1716. mtime?: string;
  1717. }>;
  1718. }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`),
  1719. getPrinterFileDownloadUrl: (printerId: number, path: string) =>
  1720. `${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`,
  1721. downloadPrinterFilesAsZip: async (printerId: number, paths: string[]): Promise<Blob> => {
  1722. const response = await fetch(`${API_BASE}/printers/${printerId}/files/download-zip`, {
  1723. method: 'POST',
  1724. headers: { 'Content-Type': 'application/json' },
  1725. body: JSON.stringify({ paths }),
  1726. });
  1727. if (!response.ok) {
  1728. const error = await response.json().catch(() => ({}));
  1729. throw new Error(error.detail || `HTTP ${response.status}`);
  1730. }
  1731. return response.blob();
  1732. },
  1733. deletePrinterFile: (printerId: number, path: string) =>
  1734. request<{ status: string; path: string }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`, {
  1735. method: 'DELETE',
  1736. }),
  1737. getPrinterStorage: (printerId: number) =>
  1738. request<{ used_bytes: number | null; free_bytes: number | null }>(`/printers/${printerId}/storage`),
  1739. // Archives
  1740. getArchives: (printerId?: number, projectId?: number, limit = 50, offset = 0) => {
  1741. const params = new URLSearchParams();
  1742. if (printerId) params.set('printer_id', String(printerId));
  1743. if (projectId) params.set('project_id', String(projectId));
  1744. params.set('limit', String(limit));
  1745. params.set('offset', String(offset));
  1746. return request<Archive[]>(`/archives/?${params}`);
  1747. },
  1748. getArchive: (id: number) => request<Archive>(`/archives/${id}`),
  1749. searchArchives: (query: string, options?: {
  1750. printerId?: number;
  1751. projectId?: number;
  1752. status?: string;
  1753. limit?: number;
  1754. offset?: number;
  1755. }) => {
  1756. const params = new URLSearchParams();
  1757. params.set('q', query);
  1758. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1759. if (options?.projectId) params.set('project_id', String(options.projectId));
  1760. if (options?.status) params.set('status', options.status);
  1761. if (options?.limit) params.set('limit', String(options.limit));
  1762. if (options?.offset) params.set('offset', String(options.offset));
  1763. return request<Archive[]>(`/archives/search?${params}`);
  1764. },
  1765. rebuildSearchIndex: () => request<{ message: string }>('/archives/search/rebuild-index', { method: 'POST' }),
  1766. updateArchive: (id: number, data: {
  1767. printer_id?: number | null;
  1768. project_id?: number | null;
  1769. print_name?: string;
  1770. is_favorite?: boolean;
  1771. tags?: string;
  1772. notes?: string;
  1773. cost?: number;
  1774. failure_reason?: string | null;
  1775. status?: string;
  1776. quantity?: number;
  1777. external_url?: string | null;
  1778. }) =>
  1779. request<Archive>(`/archives/${id}`, {
  1780. method: 'PATCH',
  1781. body: JSON.stringify(data),
  1782. }),
  1783. toggleFavorite: (id: number) =>
  1784. request<Archive>(`/archives/${id}/favorite`, { method: 'POST' }),
  1785. deleteArchive: (id: number) =>
  1786. request<void>(`/archives/${id}`, { method: 'DELETE' }),
  1787. getArchiveStats: () => request<ArchiveStats>('/archives/stats'),
  1788. // Tag management
  1789. getTags: () => request<TagInfo[]>('/archives/tags'),
  1790. renameTag: (oldName: string, newName: string) =>
  1791. request<{ affected: number }>(`/archives/tags/${encodeURIComponent(oldName)}`, {
  1792. method: 'PUT',
  1793. body: JSON.stringify({ new_name: newName }),
  1794. }),
  1795. deleteTag: (name: string) =>
  1796. request<{ affected: number }>(`/archives/tags/${encodeURIComponent(name)}`, {
  1797. method: 'DELETE',
  1798. }),
  1799. recalculateCosts: () =>
  1800. request<{ message: string; updated: number }>('/archives/recalculate-costs', { method: 'POST' }),
  1801. getFailureAnalysis: (options?: { days?: number; printerId?: number; projectId?: number }) => {
  1802. const params = new URLSearchParams();
  1803. if (options?.days) params.set('days', String(options.days));
  1804. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1805. if (options?.projectId) params.set('project_id', String(options.projectId));
  1806. return request<FailureAnalysis>(`/archives/analysis/failures?${params}`);
  1807. },
  1808. compareArchives: (archiveIds: number[]) =>
  1809. request<ArchiveComparison>(`/archives/compare?archive_ids=${archiveIds.join(',')}`),
  1810. findSimilarArchives: (archiveId: number, limit = 10) =>
  1811. request<SimilarArchive[]>(`/archives/${archiveId}/similar?limit=${limit}`),
  1812. exportArchives: async (options?: {
  1813. format?: 'csv' | 'xlsx';
  1814. fields?: string[];
  1815. printerId?: number;
  1816. projectId?: number;
  1817. status?: string;
  1818. dateFrom?: string;
  1819. dateTo?: string;
  1820. search?: string;
  1821. }): Promise<{ blob: Blob; filename: string }> => {
  1822. const params = new URLSearchParams();
  1823. if (options?.format) params.set('format', options.format);
  1824. if (options?.fields) params.set('fields', options.fields.join(','));
  1825. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1826. if (options?.projectId) params.set('project_id', String(options.projectId));
  1827. if (options?.status) params.set('status', options.status);
  1828. if (options?.dateFrom) params.set('date_from', options.dateFrom);
  1829. if (options?.dateTo) params.set('date_to', options.dateTo);
  1830. if (options?.search) params.set('search', options.search);
  1831. const response = await fetch(`${API_BASE}/archives/export?${params}`);
  1832. if (!response.ok) {
  1833. const error = await response.json().catch(() => ({}));
  1834. throw new Error(error.detail || `HTTP ${response.status}`);
  1835. }
  1836. const contentDisposition = response.headers.get('Content-Disposition');
  1837. let filename = options?.format === 'xlsx' ? 'archives_export.xlsx' : 'archives_export.csv';
  1838. if (contentDisposition) {
  1839. const match = contentDisposition.match(/filename="?([^"]+)"?/);
  1840. if (match) filename = match[1];
  1841. }
  1842. const blob = await response.blob();
  1843. return { blob, filename };
  1844. },
  1845. exportStats: async (options?: {
  1846. format?: 'csv' | 'xlsx';
  1847. days?: number;
  1848. printerId?: number;
  1849. projectId?: number;
  1850. }): Promise<{ blob: Blob; filename: string }> => {
  1851. const params = new URLSearchParams();
  1852. if (options?.format) params.set('format', options.format);
  1853. if (options?.days) params.set('days', String(options.days));
  1854. if (options?.printerId) params.set('printer_id', String(options.printerId));
  1855. if (options?.projectId) params.set('project_id', String(options.projectId));
  1856. const response = await fetch(`${API_BASE}/archives/stats/export?${params}`);
  1857. if (!response.ok) {
  1858. const error = await response.json().catch(() => ({}));
  1859. throw new Error(error.detail || `HTTP ${response.status}`);
  1860. }
  1861. const contentDisposition = response.headers.get('Content-Disposition');
  1862. let filename = options?.format === 'xlsx' ? 'stats_export.xlsx' : 'stats_export.csv';
  1863. if (contentDisposition) {
  1864. const match = contentDisposition.match(/filename="?([^"]+)"?/);
  1865. if (match) filename = match[1];
  1866. }
  1867. const blob = await response.blob();
  1868. return { blob, filename };
  1869. },
  1870. getArchiveDuplicates: (id: number) =>
  1871. request<{ duplicates: ArchiveDuplicate[]; count: number }>(`/archives/${id}/duplicates`),
  1872. backfillContentHashes: () =>
  1873. request<{ updated: number; errors: Array<{ id: number; error: string }> }>('/archives/backfill-hashes', {
  1874. method: 'POST',
  1875. }),
  1876. getArchiveThumbnail: (id: number) => `${API_BASE}/archives/${id}/thumbnail?v=${Date.now()}`,
  1877. getArchivePlateThumbnail: (id: number, plateIndex: number) =>
  1878. `${API_BASE}/archives/${id}/plate-thumbnail/${plateIndex}`,
  1879. getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`,
  1880. getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`,
  1881. getArchivePlatePreview: (id: number) => `${API_BASE}/archives/${id}/plate-preview`,
  1882. getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse?v=${Date.now()}`,
  1883. scanArchiveTimelapse: (id: number) =>
  1884. request<{
  1885. status: string;
  1886. message: string;
  1887. filename?: string;
  1888. available_files?: Array<{ name: string; path: string; size: number; mtime: string | null }>;
  1889. }>(`/archives/${id}/timelapse/scan`, {
  1890. method: 'POST',
  1891. }),
  1892. selectArchiveTimelapse: (id: number, filename: string) =>
  1893. request<{ status: string; message: string; filename: string }>(
  1894. `/archives/${id}/timelapse/select?filename=${encodeURIComponent(filename)}`,
  1895. { method: 'POST' }
  1896. ),
  1897. uploadArchiveTimelapse: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
  1898. const formData = new FormData();
  1899. formData.append('file', file);
  1900. const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/upload`, {
  1901. method: 'POST',
  1902. body: formData,
  1903. });
  1904. if (!response.ok) {
  1905. const error = await response.json().catch(() => ({}));
  1906. throw new Error(error.detail || `HTTP ${response.status}`);
  1907. }
  1908. return response.json();
  1909. },
  1910. // Timelapse Editor
  1911. getTimelapseInfo: (archiveId: number) =>
  1912. request<{
  1913. duration: number;
  1914. width: number;
  1915. height: number;
  1916. fps: number;
  1917. codec: string;
  1918. file_size: number;
  1919. has_audio: boolean;
  1920. }>(`/archives/${archiveId}/timelapse/info`),
  1921. getTimelapseThumbnails: (archiveId: number, count: number = 10) =>
  1922. request<{
  1923. thumbnails: string[];
  1924. timestamps: number[];
  1925. }>(`/archives/${archiveId}/timelapse/thumbnails?count=${count}`),
  1926. processTimelapse: async (
  1927. archiveId: number,
  1928. params: {
  1929. trimStart?: number;
  1930. trimEnd?: number;
  1931. speed?: number;
  1932. saveMode: 'replace' | 'new';
  1933. outputFilename?: string;
  1934. },
  1935. audioFile?: File
  1936. ): Promise<{ status: string; output_path: string | null; message: string }> => {
  1937. const formData = new FormData();
  1938. formData.append('trim_start', String(params.trimStart ?? 0));
  1939. if (params.trimEnd !== undefined) {
  1940. formData.append('trim_end', String(params.trimEnd));
  1941. }
  1942. formData.append('speed', String(params.speed ?? 1));
  1943. formData.append('save_mode', params.saveMode);
  1944. if (params.outputFilename) {
  1945. formData.append('output_filename', params.outputFilename);
  1946. }
  1947. if (audioFile) {
  1948. formData.append('audio', audioFile);
  1949. }
  1950. const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/process`, {
  1951. method: 'POST',
  1952. body: formData,
  1953. });
  1954. if (!response.ok) {
  1955. const error = await response.json().catch(() => ({}));
  1956. throw new Error(error.detail || `HTTP ${response.status}`);
  1957. }
  1958. return response.json();
  1959. },
  1960. // Photos
  1961. getArchivePhotoUrl: (archiveId: number, filename: string) =>
  1962. `${API_BASE}/archives/${archiveId}/photos/${encodeURIComponent(filename)}`,
  1963. uploadArchivePhoto: async (archiveId: number, file: File): Promise<{ status: string; filename: string; photos: string[] }> => {
  1964. const formData = new FormData();
  1965. formData.append('file', file);
  1966. const response = await fetch(`${API_BASE}/archives/${archiveId}/photos`, {
  1967. method: 'POST',
  1968. body: formData,
  1969. });
  1970. if (!response.ok) {
  1971. const error = await response.json().catch(() => ({}));
  1972. throw new Error(error.detail || `HTTP ${response.status}`);
  1973. }
  1974. return response.json();
  1975. },
  1976. deleteArchivePhoto: (archiveId: number, filename: string) =>
  1977. request<{ status: string; photos: string[] | null }>(`/archives/${archiveId}/photos/${encodeURIComponent(filename)}`, {
  1978. method: 'DELETE',
  1979. }),
  1980. // Source 3MF (original slicer project file)
  1981. getSource3mfDownloadUrl: (archiveId: number) =>
  1982. `${API_BASE}/archives/${archiveId}/source`,
  1983. getSource3mfForSlicer: (archiveId: number, filename: string) =>
  1984. `${API_BASE}/archives/${archiveId}/source/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
  1985. uploadSource3mf: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
  1986. const formData = new FormData();
  1987. formData.append('file', file);
  1988. const response = await fetch(`${API_BASE}/archives/${archiveId}/source`, {
  1989. method: 'POST',
  1990. body: formData,
  1991. });
  1992. if (!response.ok) {
  1993. const error = await response.json().catch(() => ({}));
  1994. throw new Error(error.detail || `HTTP ${response.status}`);
  1995. }
  1996. return response.json();
  1997. },
  1998. deleteSource3mf: (archiveId: number) =>
  1999. request<{ status: string }>(`/archives/${archiveId}/source`, {
  2000. method: 'DELETE',
  2001. }),
  2002. // F3D (Fusion 360 design file)
  2003. getF3dDownloadUrl: (archiveId: number) =>
  2004. `${API_BASE}/archives/${archiveId}/f3d`,
  2005. uploadF3d: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
  2006. const formData = new FormData();
  2007. formData.append('file', file);
  2008. const response = await fetch(`${API_BASE}/archives/${archiveId}/f3d`, {
  2009. method: 'POST',
  2010. body: formData,
  2011. });
  2012. if (!response.ok) {
  2013. const error = await response.json().catch(() => ({}));
  2014. throw new Error(error.detail || `HTTP ${response.status}`);
  2015. }
  2016. return response.json();
  2017. },
  2018. deleteF3d: (archiveId: number) =>
  2019. request<{ status: string }>(`/archives/${archiveId}/f3d`, {
  2020. method: 'DELETE',
  2021. }),
  2022. // QR Code
  2023. getArchiveQRCodeUrl: (archiveId: number, size = 200) =>
  2024. `${API_BASE}/archives/${archiveId}/qrcode?size=${size}`,
  2025. getArchiveCapabilities: (id: number) =>
  2026. request<{
  2027. has_model: boolean;
  2028. has_gcode: boolean;
  2029. has_source: boolean;
  2030. build_volume: { x: number; y: number; z: number };
  2031. filament_colors: string[];
  2032. }>(`/archives/${id}/capabilities`),
  2033. // Project Page
  2034. getArchiveProjectPage: (id: number) =>
  2035. request<{
  2036. title: string | null;
  2037. description: string | null;
  2038. designer: string | null;
  2039. designer_user_id: string | null;
  2040. license: string | null;
  2041. copyright: string | null;
  2042. creation_date: string | null;
  2043. modification_date: string | null;
  2044. origin: string | null;
  2045. profile_title: string | null;
  2046. profile_description: string | null;
  2047. profile_cover: string | null;
  2048. profile_user_id: string | null;
  2049. profile_user_name: string | null;
  2050. design_model_id: string | null;
  2051. design_profile_id: string | null;
  2052. design_region: string | null;
  2053. model_pictures: Array<{ name: string; path: string; url: string }>;
  2054. profile_pictures: Array<{ name: string; path: string; url: string }>;
  2055. thumbnails: Array<{ name: string; path: string; url: string }>;
  2056. }>(`/archives/${id}/project-page`),
  2057. updateArchiveProjectPage: (id: number, data: {
  2058. title?: string;
  2059. description?: string;
  2060. designer?: string;
  2061. license?: string;
  2062. copyright?: string;
  2063. profile_title?: string;
  2064. profile_description?: string;
  2065. }) =>
  2066. request(`/archives/${id}/project-page`, {
  2067. method: 'PATCH',
  2068. body: JSON.stringify(data),
  2069. }),
  2070. getArchiveProjectImageUrl: (archiveId: number, imagePath: string) =>
  2071. `${API_BASE}/archives/${archiveId}/project-image/${encodeURIComponent(imagePath)}`,
  2072. getArchiveForSlicer: (id: number, filename: string) =>
  2073. `${API_BASE}/archives/${id}/file/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
  2074. getArchivePlates: (archiveId: number) =>
  2075. request<{
  2076. archive_id: number;
  2077. filename: string;
  2078. plates: Array<{
  2079. index: number;
  2080. name: string | null;
  2081. objects: string[];
  2082. has_thumbnail: boolean;
  2083. thumbnail_url: string | null;
  2084. print_time_seconds: number | null;
  2085. filament_used_grams: number | null;
  2086. filaments: Array<{
  2087. slot_id: number;
  2088. type: string;
  2089. color: string;
  2090. used_grams: number;
  2091. used_meters: number;
  2092. }>;
  2093. }>;
  2094. is_multi_plate: boolean;
  2095. }>(`/archives/${archiveId}/plates`),
  2096. getArchiveFilamentRequirements: (archiveId: number, plateId?: number) =>
  2097. request<{
  2098. archive_id: number;
  2099. filename: string;
  2100. plate_id: number | null;
  2101. filaments: Array<{
  2102. slot_id: number;
  2103. type: string;
  2104. color: string;
  2105. used_grams: number;
  2106. used_meters: number;
  2107. }>;
  2108. }>(`/archives/${archiveId}/filament-requirements${plateId !== undefined ? `?plate_id=${plateId}` : ''}`),
  2109. reprintArchive: (
  2110. archiveId: number,
  2111. printerId: number,
  2112. options?: {
  2113. plate_id?: number;
  2114. ams_mapping?: number[];
  2115. timelapse?: boolean;
  2116. bed_levelling?: boolean;
  2117. flow_cali?: boolean;
  2118. vibration_cali?: boolean;
  2119. layer_inspect?: boolean;
  2120. use_ams?: boolean;
  2121. }
  2122. ) =>
  2123. request<{ status: string; printer_id: number; archive_id: number; filename: string }>(
  2124. `/archives/${archiveId}/reprint?printer_id=${printerId}`,
  2125. {
  2126. method: 'POST',
  2127. headers: options ? { 'Content-Type': 'application/json' } : undefined,
  2128. body: options ? JSON.stringify(options) : undefined,
  2129. }
  2130. ),
  2131. uploadArchive: async (file: File, printerId?: number): Promise<Archive> => {
  2132. const formData = new FormData();
  2133. formData.append('file', file);
  2134. const url = printerId
  2135. ? `${API_BASE}/archives/upload?printer_id=${printerId}`
  2136. : `${API_BASE}/archives/upload`;
  2137. const response = await fetch(url, {
  2138. method: 'POST',
  2139. body: formData,
  2140. });
  2141. if (!response.ok) {
  2142. const error = await response.json().catch(() => ({}));
  2143. throw new Error(error.detail || `HTTP ${response.status}`);
  2144. }
  2145. return response.json();
  2146. },
  2147. uploadArchivesBulk: async (files: File[], printerId?: number): Promise<BulkUploadResult> => {
  2148. const formData = new FormData();
  2149. files.forEach((file) => formData.append('files', file));
  2150. const url = printerId
  2151. ? `${API_BASE}/archives/upload-bulk?printer_id=${printerId}`
  2152. : `${API_BASE}/archives/upload-bulk`;
  2153. const response = await fetch(url, {
  2154. method: 'POST',
  2155. body: formData,
  2156. });
  2157. if (!response.ok) {
  2158. const error = await response.json().catch(() => ({}));
  2159. throw new Error(error.detail || `HTTP ${response.status}`);
  2160. }
  2161. return response.json();
  2162. },
  2163. // Settings
  2164. getSettings: () => request<AppSettings>('/settings/'),
  2165. updateSettings: (data: AppSettingsUpdate) =>
  2166. request<AppSettings>('/settings/', {
  2167. method: 'PUT',
  2168. body: JSON.stringify(data),
  2169. }),
  2170. getMQTTStatus: () => request<MQTTStatus>('/settings/mqtt/status'),
  2171. resetSettings: () =>
  2172. request<AppSettings>('/settings/reset', { method: 'POST' }),
  2173. exportBackup: async (categories?: Record<string, boolean>): Promise<{ blob: Blob; filename: string }> => {
  2174. const params = new URLSearchParams();
  2175. if (categories) {
  2176. if (categories.settings !== undefined) params.set('include_settings', String(categories.settings));
  2177. if (categories.notifications !== undefined) params.set('include_notifications', String(categories.notifications));
  2178. if (categories.templates !== undefined) params.set('include_templates', String(categories.templates));
  2179. if (categories.smart_plugs !== undefined) params.set('include_smart_plugs', String(categories.smart_plugs));
  2180. if (categories.external_links !== undefined) params.set('include_external_links', String(categories.external_links));
  2181. if (categories.printers !== undefined) params.set('include_printers', String(categories.printers));
  2182. if (categories.plate_calibration !== undefined) params.set('include_plate_calibration', String(categories.plate_calibration));
  2183. if (categories.filaments !== undefined) params.set('include_filaments', String(categories.filaments));
  2184. if (categories.maintenance !== undefined) params.set('include_maintenance', String(categories.maintenance));
  2185. if (categories.archives !== undefined) params.set('include_archives', String(categories.archives));
  2186. if (categories.projects !== undefined) params.set('include_projects', String(categories.projects));
  2187. if (categories.pending_uploads !== undefined) params.set('include_pending_uploads', String(categories.pending_uploads));
  2188. if (categories.access_codes !== undefined) params.set('include_access_codes', String(categories.access_codes));
  2189. if (categories.api_keys !== undefined) params.set('include_api_keys', String(categories.api_keys));
  2190. }
  2191. const url = `${API_BASE}/settings/backup${params.toString() ? '?' + params.toString() : ''}`;
  2192. const response = await fetch(url);
  2193. // Check for errors
  2194. if (!response.ok) {
  2195. const errorText = await response.text();
  2196. throw new Error(errorText || `Backup failed with status ${response.status}`);
  2197. }
  2198. // Get filename from Content-Disposition header
  2199. const contentDisposition = response.headers.get('Content-Disposition');
  2200. let filename = 'bambuddy-backup.json';
  2201. if (contentDisposition) {
  2202. const match = contentDisposition.match(/filename=([^;]+)/);
  2203. if (match) filename = match[1].trim();
  2204. }
  2205. const blob = await response.blob();
  2206. return { blob, filename };
  2207. },
  2208. importBackup: async (file: File, overwrite = false) => {
  2209. const formData = new FormData();
  2210. formData.append('file', file);
  2211. const url = `${API_BASE}/settings/restore${overwrite ? '?overwrite=true' : ''}`;
  2212. const response = await fetch(url, {
  2213. method: 'POST',
  2214. body: formData,
  2215. });
  2216. return response.json() as Promise<{
  2217. success: boolean;
  2218. message: string;
  2219. restored?: Record<string, number>;
  2220. skipped?: Record<string, number>;
  2221. skipped_details?: Record<string, string[]>;
  2222. files_restored?: number;
  2223. total_skipped?: number;
  2224. }>;
  2225. },
  2226. checkFfmpeg: () =>
  2227. request<{ installed: boolean; path: string | null }>('/settings/check-ffmpeg'),
  2228. // Cloud
  2229. getCloudStatus: () => request<CloudAuthStatus>('/cloud/status'),
  2230. cloudLogin: (email: string, password: string, region = 'global') =>
  2231. request<CloudLoginResponse>('/cloud/login', {
  2232. method: 'POST',
  2233. body: JSON.stringify({ email, password, region }),
  2234. }),
  2235. cloudVerify: (email: string, code: string) =>
  2236. request<CloudLoginResponse>('/cloud/verify', {
  2237. method: 'POST',
  2238. body: JSON.stringify({ email, code }),
  2239. }),
  2240. cloudSetToken: (access_token: string) =>
  2241. request<CloudAuthStatus>('/cloud/token', {
  2242. method: 'POST',
  2243. body: JSON.stringify({ access_token }),
  2244. }),
  2245. cloudLogout: () =>
  2246. request<{ success: boolean }>('/cloud/logout', { method: 'POST' }),
  2247. getCloudSettings: (version = '02.04.00.70') =>
  2248. request<SlicerSettingsResponse>(`/cloud/settings?version=${version}`),
  2249. getCloudSettingDetail: (settingId: string) =>
  2250. request<SlicerSettingDetail>(`/cloud/settings/${settingId}`),
  2251. createCloudSetting: (data: SlicerSettingCreate) =>
  2252. request<SlicerSettingDetail>('/cloud/settings', {
  2253. method: 'POST',
  2254. body: JSON.stringify(data),
  2255. }),
  2256. updateCloudSetting: (settingId: string, data: SlicerSettingUpdate) =>
  2257. request<SlicerSettingDetail>(`/cloud/settings/${settingId}`, {
  2258. method: 'PUT',
  2259. body: JSON.stringify(data),
  2260. }),
  2261. deleteCloudSetting: (settingId: string) =>
  2262. request<SlicerSettingDeleteResponse>(`/cloud/settings/${settingId}`, {
  2263. method: 'DELETE',
  2264. }),
  2265. getCloudDevices: () => request<CloudDevice[]>('/cloud/devices'),
  2266. getCloudFields: (presetType: 'filament' | 'print' | 'process' | 'printer') =>
  2267. request<FieldDefinitionsResponse>(`/cloud/fields/${presetType}`),
  2268. getAllCloudFields: () =>
  2269. request<Record<string, FieldDefinitionsResponse>>('/cloud/fields'),
  2270. getFilamentInfo: (settingIds: string[]) =>
  2271. request<Record<string, { name: string; k: number | null }>>('/cloud/filament-info', {
  2272. method: 'POST',
  2273. body: JSON.stringify(settingIds),
  2274. }),
  2275. // Smart Plugs
  2276. getSmartPlugs: () => request<SmartPlug[]>('/smart-plugs/'),
  2277. getSmartPlug: (id: number) => request<SmartPlug>(`/smart-plugs/${id}`),
  2278. getSmartPlugByPrinter: (printerId: number) => request<SmartPlug | null>(`/smart-plugs/by-printer/${printerId}`),
  2279. getScriptPlugsByPrinter: (printerId: number) => request<SmartPlug[]>(`/smart-plugs/by-printer/${printerId}/scripts`),
  2280. createSmartPlug: (data: SmartPlugCreate) =>
  2281. request<SmartPlug>('/smart-plugs/', {
  2282. method: 'POST',
  2283. body: JSON.stringify(data),
  2284. }),
  2285. updateSmartPlug: (id: number, data: SmartPlugUpdate) =>
  2286. request<SmartPlug>(`/smart-plugs/${id}`, {
  2287. method: 'PATCH',
  2288. body: JSON.stringify(data),
  2289. }),
  2290. deleteSmartPlug: (id: number) =>
  2291. request<void>(`/smart-plugs/${id}`, { method: 'DELETE' }),
  2292. controlSmartPlug: (id: number, action: 'on' | 'off' | 'toggle') =>
  2293. request<{ success: boolean; action: string }>(`/smart-plugs/${id}/control`, {
  2294. method: 'POST',
  2295. body: JSON.stringify({ action }),
  2296. }),
  2297. getSmartPlugStatus: (id: number) =>
  2298. request<SmartPlugStatus>(`/smart-plugs/${id}/status`),
  2299. testSmartPlugConnection: (ip_address: string, username?: string | null, password?: string | null) =>
  2300. request<SmartPlugTestResult>('/smart-plugs/test-connection', {
  2301. method: 'POST',
  2302. body: JSON.stringify({ ip_address, username, password }),
  2303. }),
  2304. // Tasmota Discovery (auto-detects network)
  2305. startTasmotaScan: () =>
  2306. fetch(`${API_BASE}/smart-plugs/discover/scan`, { method: 'POST' })
  2307. .then(res => res.ok ? res.json() : res.json().then(e => { throw new Error(e.detail || `HTTP ${res.status}`); })),
  2308. getTasmotaScanStatus: () =>
  2309. request<TasmotaScanStatus>('/smart-plugs/discover/status'),
  2310. stopTasmotaScan: () =>
  2311. fetch(`${API_BASE}/smart-plugs/discover/stop`, { method: 'POST' })
  2312. .then(res => res.ok ? res.json() : res.json().then(e => { throw new Error(e.detail || `HTTP ${res.status}`); })),
  2313. getDiscoveredTasmotaDevices: () =>
  2314. request<DiscoveredTasmotaDevice[]>('/smart-plugs/discover/devices'),
  2315. // Home Assistant Integration
  2316. testHAConnection: (url: string, token: string) =>
  2317. request<HATestConnectionResult>('/smart-plugs/ha/test-connection', {
  2318. method: 'POST',
  2319. body: JSON.stringify({ url, token }),
  2320. }),
  2321. getHAEntities: (search?: string) => {
  2322. const params = search ? `?search=${encodeURIComponent(search)}` : '';
  2323. return request<HAEntity[]>(`/smart-plugs/ha/entities${params}`);
  2324. },
  2325. getHASensorEntities: () =>
  2326. request<HASensorEntity[]>('/smart-plugs/ha/sensors'),
  2327. // Print Queue
  2328. getQueue: (printerId?: number, status?: string) => {
  2329. const params = new URLSearchParams();
  2330. if (printerId) params.set('printer_id', String(printerId));
  2331. if (status) params.set('status', status);
  2332. return request<PrintQueueItem[]>(`/queue/?${params}`);
  2333. },
  2334. getQueueItem: (id: number) => request<PrintQueueItem>(`/queue/${id}`),
  2335. addToQueue: (data: PrintQueueItemCreate) =>
  2336. request<PrintQueueItem>('/queue/', {
  2337. method: 'POST',
  2338. body: JSON.stringify(data),
  2339. }),
  2340. updateQueueItem: (id: number, data: PrintQueueItemUpdate) =>
  2341. request<PrintQueueItem>(`/queue/${id}`, {
  2342. method: 'PATCH',
  2343. body: JSON.stringify(data),
  2344. }),
  2345. removeFromQueue: (id: number) =>
  2346. request<{ message: string }>(`/queue/${id}`, { method: 'DELETE' }),
  2347. reorderQueue: (items: { id: number; position: number }[]) =>
  2348. request<{ message: string }>('/queue/reorder', {
  2349. method: 'POST',
  2350. body: JSON.stringify({ items }),
  2351. }),
  2352. cancelQueueItem: (id: number) =>
  2353. request<{ message: string }>(`/queue/${id}/cancel`, { method: 'POST' }),
  2354. stopQueueItem: (id: number) =>
  2355. request<{ message: string }>(`/queue/${id}/stop`, { method: 'POST' }),
  2356. startQueueItem: (id: number) =>
  2357. request<PrintQueueItem>(`/queue/${id}/start`, { method: 'POST' }),
  2358. bulkUpdateQueue: (data: PrintQueueBulkUpdate) =>
  2359. request<PrintQueueBulkUpdateResponse>('/queue/bulk', {
  2360. method: 'PATCH',
  2361. body: JSON.stringify(data),
  2362. }),
  2363. // K-Profiles
  2364. getKProfiles: (printerId: number, nozzleDiameter = '0.4') =>
  2365. request<KProfilesResponse>(`/printers/${printerId}/kprofiles/?nozzle_diameter=${nozzleDiameter}`),
  2366. setKProfile: (printerId: number, profile: KProfileCreate) =>
  2367. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
  2368. method: 'POST',
  2369. body: JSON.stringify(profile),
  2370. }),
  2371. deleteKProfile: (printerId: number, profile: KProfileDelete) =>
  2372. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
  2373. method: 'DELETE',
  2374. body: JSON.stringify(profile),
  2375. }),
  2376. setKProfilesBatch: (printerId: number, profiles: KProfileCreate[]) =>
  2377. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/batch`, {
  2378. method: 'POST',
  2379. body: JSON.stringify(profiles),
  2380. }),
  2381. // K-Profile Notes (stored locally, not on printer)
  2382. getKProfileNotes: (printerId: number) =>
  2383. request<KProfileNotesResponse>(`/printers/${printerId}/kprofiles/notes`),
  2384. setKProfileNote: (printerId: number, settingId: string, note: string) =>
  2385. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/notes`, {
  2386. method: 'PUT',
  2387. body: JSON.stringify({ setting_id: settingId, note }),
  2388. }),
  2389. deleteKProfileNote: (printerId: number, settingId: string) =>
  2390. request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/notes/${encodeURIComponent(settingId)}`, {
  2391. method: 'DELETE',
  2392. }),
  2393. // Slot Preset Mappings
  2394. getSlotPresets: (printerId: number) =>
  2395. request<Record<number, SlotPresetMapping>>(`/printers/${printerId}/slot-presets`),
  2396. getSlotPreset: (printerId: number, amsId: number, trayId: number) =>
  2397. request<SlotPresetMapping | null>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}`),
  2398. saveSlotPreset: (printerId: number, amsId: number, trayId: number, presetId: string, presetName: string) =>
  2399. request<SlotPresetMapping>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}?preset_id=${encodeURIComponent(presetId)}&preset_name=${encodeURIComponent(presetName)}`, {
  2400. method: 'PUT',
  2401. }),
  2402. deleteSlotPreset: (printerId: number, amsId: number, trayId: number) =>
  2403. request<{ success: boolean }>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}`, {
  2404. method: 'DELETE',
  2405. }),
  2406. configureAmsSlot: (
  2407. printerId: number,
  2408. amsId: number,
  2409. trayId: number,
  2410. config: {
  2411. tray_info_idx: string;
  2412. tray_type: string;
  2413. tray_sub_brands: string;
  2414. tray_color: string;
  2415. nozzle_temp_min: number;
  2416. nozzle_temp_max: number;
  2417. cali_idx: number;
  2418. nozzle_diameter: string;
  2419. setting_id?: string;
  2420. kprofile_filament_id?: string;
  2421. kprofile_setting_id?: string;
  2422. k_value?: number;
  2423. }
  2424. ) => {
  2425. const params = new URLSearchParams({
  2426. tray_info_idx: config.tray_info_idx,
  2427. tray_type: config.tray_type,
  2428. tray_sub_brands: config.tray_sub_brands,
  2429. tray_color: config.tray_color,
  2430. nozzle_temp_min: config.nozzle_temp_min.toString(),
  2431. nozzle_temp_max: config.nozzle_temp_max.toString(),
  2432. cali_idx: config.cali_idx.toString(),
  2433. nozzle_diameter: config.nozzle_diameter,
  2434. });
  2435. if (config.setting_id) {
  2436. params.set('setting_id', config.setting_id);
  2437. }
  2438. if (config.kprofile_filament_id) {
  2439. params.set('kprofile_filament_id', config.kprofile_filament_id);
  2440. }
  2441. if (config.kprofile_setting_id) {
  2442. params.set('kprofile_setting_id', config.kprofile_setting_id);
  2443. }
  2444. if (config.k_value !== undefined && config.k_value > 0) {
  2445. params.set('k_value', config.k_value.toString());
  2446. }
  2447. return request<{ success: boolean; message: string }>(
  2448. `/printers/${printerId}/slots/${amsId}/${trayId}/configure?${params}`,
  2449. { method: 'POST' }
  2450. );
  2451. },
  2452. resetAmsSlot: (printerId: number, amsId: number, trayId: number) =>
  2453. request<{ success: boolean; message: string }>(
  2454. `/printers/${printerId}/ams/${amsId}/tray/${trayId}/reset`,
  2455. { method: 'POST' }
  2456. ),
  2457. // Filaments
  2458. listFilaments: () => request<Filament[]>('/filaments/'),
  2459. getFilament: (id: number) => request<Filament>(`/filaments/${id}`),
  2460. getFilamentsByType: (type: string) => request<Filament[]>(`/filaments/by-type/${type}`),
  2461. // Notification Providers
  2462. getNotificationProviders: () => request<NotificationProvider[]>('/notifications/'),
  2463. getNotificationProvider: (id: number) => request<NotificationProvider>(`/notifications/${id}`),
  2464. createNotificationProvider: (data: NotificationProviderCreate) =>
  2465. request<NotificationProvider>('/notifications/', {
  2466. method: 'POST',
  2467. body: JSON.stringify(data),
  2468. }),
  2469. updateNotificationProvider: (id: number, data: NotificationProviderUpdate) =>
  2470. request<NotificationProvider>(`/notifications/${id}`, {
  2471. method: 'PATCH',
  2472. body: JSON.stringify(data),
  2473. }),
  2474. deleteNotificationProvider: (id: number) =>
  2475. request<{ message: string }>(`/notifications/${id}`, { method: 'DELETE' }),
  2476. testNotificationProvider: (id: number) =>
  2477. request<NotificationTestResponse>(`/notifications/${id}/test`, { method: 'POST' }),
  2478. testNotificationConfig: (data: NotificationTestRequest) =>
  2479. request<NotificationTestResponse>('/notifications/test-config', {
  2480. method: 'POST',
  2481. body: JSON.stringify(data),
  2482. }),
  2483. testAllNotificationProviders: () =>
  2484. request<{
  2485. tested: number;
  2486. success: number;
  2487. failed: number;
  2488. results: Array<{
  2489. provider_id: number;
  2490. provider_name: string;
  2491. provider_type: string;
  2492. success: boolean;
  2493. message: string;
  2494. }>;
  2495. }>('/notifications/test-all', { method: 'POST' }),
  2496. // Notification Templates
  2497. getNotificationTemplates: () => request<NotificationTemplate[]>('/notification-templates'),
  2498. getNotificationTemplate: (id: number) => request<NotificationTemplate>(`/notification-templates/${id}`),
  2499. updateNotificationTemplate: (id: number, data: NotificationTemplateUpdate) =>
  2500. request<NotificationTemplate>(`/notification-templates/${id}`, {
  2501. method: 'PUT',
  2502. body: JSON.stringify(data),
  2503. }),
  2504. resetNotificationTemplate: (id: number) =>
  2505. request<NotificationTemplate>(`/notification-templates/${id}/reset`, {
  2506. method: 'POST',
  2507. }),
  2508. getTemplateVariables: () => request<EventVariablesResponse[]>('/notification-templates/variables'),
  2509. previewTemplate: (data: TemplatePreviewRequest) =>
  2510. request<TemplatePreviewResponse>('/notification-templates/preview', {
  2511. method: 'POST',
  2512. body: JSON.stringify(data),
  2513. }),
  2514. // Notification Logs
  2515. getNotificationLogs: (params?: {
  2516. limit?: number;
  2517. offset?: number;
  2518. provider_id?: number;
  2519. event_type?: string;
  2520. success?: boolean;
  2521. days?: number;
  2522. }) => {
  2523. const searchParams = new URLSearchParams();
  2524. if (params?.limit) searchParams.set('limit', String(params.limit));
  2525. if (params?.offset) searchParams.set('offset', String(params.offset));
  2526. if (params?.provider_id) searchParams.set('provider_id', String(params.provider_id));
  2527. if (params?.event_type) searchParams.set('event_type', params.event_type);
  2528. if (params?.success !== undefined) searchParams.set('success', String(params.success));
  2529. if (params?.days) searchParams.set('days', String(params.days));
  2530. return request<NotificationLogEntry[]>(`/notifications/logs?${searchParams}`);
  2531. },
  2532. getNotificationLogStats: (days = 7) =>
  2533. request<NotificationLogStats>(`/notifications/logs/stats?days=${days}`),
  2534. clearNotificationLogs: (olderThanDays = 30) =>
  2535. request<{ deleted: number; message: string }>(
  2536. `/notifications/logs?older_than_days=${olderThanDays}`,
  2537. { method: 'DELETE' }
  2538. ),
  2539. // Spoolman Integration
  2540. getSpoolmanStatus: () => request<SpoolmanStatus>('/spoolman/status'),
  2541. connectSpoolman: () =>
  2542. request<{ success: boolean; message: string }>('/spoolman/connect', {
  2543. method: 'POST',
  2544. }),
  2545. disconnectSpoolman: () =>
  2546. request<{ success: boolean; message: string }>('/spoolman/disconnect', {
  2547. method: 'POST',
  2548. }),
  2549. syncPrinterAms: (printerId: number) =>
  2550. request<SpoolmanSyncResult>(`/spoolman/sync/${printerId}`, {
  2551. method: 'POST',
  2552. }),
  2553. syncAllPrintersAms: () =>
  2554. request<SpoolmanSyncResult>('/spoolman/sync-all', {
  2555. method: 'POST',
  2556. }),
  2557. getSpoolmanSpools: () =>
  2558. request<{ spools: unknown[] }>('/spoolman/spools'),
  2559. getSpoolmanFilaments: () =>
  2560. request<{ filaments: unknown[] }>('/spoolman/filaments'),
  2561. getUnlinkedSpools: () =>
  2562. request<UnlinkedSpool[]>('/spoolman/spools/unlinked'),
  2563. linkSpool: (spoolId: number, trayUuid: string) =>
  2564. request<{ success: boolean; message: string }>(`/spoolman/spools/${spoolId}/link`, {
  2565. method: 'POST',
  2566. body: JSON.stringify({ tray_uuid: trayUuid }),
  2567. }),
  2568. // Updates
  2569. getVersion: () => request<VersionInfo>('/updates/version'),
  2570. checkForUpdates: () => request<UpdateCheckResult>('/updates/check'),
  2571. applyUpdate: () =>
  2572. request<{ success: boolean; message: string; status?: UpdateStatus; is_docker?: boolean }>('/updates/apply', {
  2573. method: 'POST',
  2574. }),
  2575. getUpdateStatus: () => request<UpdateStatus>('/updates/status'),
  2576. // Maintenance
  2577. getMaintenanceTypes: () => request<MaintenanceType[]>('/maintenance/types'),
  2578. createMaintenanceType: (data: MaintenanceTypeCreate) =>
  2579. request<MaintenanceType>('/maintenance/types', {
  2580. method: 'POST',
  2581. body: JSON.stringify(data),
  2582. }),
  2583. updateMaintenanceType: (id: number, data: Partial<MaintenanceTypeCreate>) =>
  2584. request<MaintenanceType>(`/maintenance/types/${id}`, {
  2585. method: 'PATCH',
  2586. body: JSON.stringify(data),
  2587. }),
  2588. deleteMaintenanceType: (id: number) =>
  2589. request<{ status: string }>(`/maintenance/types/${id}`, { method: 'DELETE' }),
  2590. getMaintenanceOverview: () => request<PrinterMaintenanceOverview[]>('/maintenance/overview'),
  2591. getPrinterMaintenance: (printerId: number) =>
  2592. request<PrinterMaintenanceOverview>(`/maintenance/printers/${printerId}`),
  2593. updateMaintenanceItem: (itemId: number, data: { custom_interval_hours?: number | null; custom_interval_type?: 'hours' | 'days' | null; enabled?: boolean }) =>
  2594. request<MaintenanceStatus>(`/maintenance/items/${itemId}`, {
  2595. method: 'PATCH',
  2596. body: JSON.stringify(data),
  2597. }),
  2598. performMaintenance: (itemId: number, notes?: string) =>
  2599. request<MaintenanceStatus>(`/maintenance/items/${itemId}/perform`, {
  2600. method: 'POST',
  2601. body: JSON.stringify({ notes }),
  2602. }),
  2603. getMaintenanceHistory: (itemId: number) =>
  2604. request<MaintenanceHistory[]>(`/maintenance/items/${itemId}/history`),
  2605. getMaintenanceSummary: () => request<MaintenanceSummary>('/maintenance/summary'),
  2606. setPrinterHours: (printerId: number, totalHours: number) =>
  2607. request<{ printer_id: number; total_hours: number; archive_hours: number; offset_hours: number }>(
  2608. `/maintenance/printers/${printerId}/hours?total_hours=${totalHours}`,
  2609. { method: 'PATCH' }
  2610. ),
  2611. assignMaintenanceType: (printerId: number, typeId: number) =>
  2612. request<MaintenanceStatus>(`/maintenance/printers/${printerId}/assign/${typeId}`, {
  2613. method: 'POST',
  2614. }),
  2615. removeMaintenanceItem: (itemId: number) =>
  2616. request<{ status: string }>(`/maintenance/items/${itemId}`, {
  2617. method: 'DELETE',
  2618. }),
  2619. // Camera
  2620. getCameraStreamUrl: (printerId: number, fps = 10) =>
  2621. `${API_BASE}/printers/${printerId}/camera/stream?fps=${fps}`,
  2622. getCameraSnapshotUrl: (printerId: number) =>
  2623. `${API_BASE}/printers/${printerId}/camera/snapshot`,
  2624. testCameraConnection: (printerId: number) =>
  2625. request<{ success: boolean; message?: string; error?: string }>(`/printers/${printerId}/camera/test`),
  2626. // Plate Detection - Multi-reference calibration (stores up to 5 references per printer)
  2627. checkPlateEmpty: (printerId: number, options?: { useExternal?: boolean; includeDebugImage?: boolean }) => {
  2628. const params = new URLSearchParams();
  2629. params.set('use_external', String(options?.useExternal ?? false));
  2630. params.set('include_debug_image', String(options?.includeDebugImage ?? false));
  2631. return request<PlateDetectionResult>(
  2632. `/printers/${printerId}/camera/check-plate?${params.toString()}`
  2633. );
  2634. },
  2635. getPlateDetectionStatus: (printerId: number) => {
  2636. return request<PlateDetectionStatus & { chamber_light?: boolean }>(
  2637. `/printers/${printerId}/camera/plate-detection/status`
  2638. );
  2639. },
  2640. calibratePlateDetection: (printerId: number, options?: { label?: string; useExternal?: boolean }) => {
  2641. const params = new URLSearchParams();
  2642. if (options?.label) params.set('label', options.label);
  2643. params.set('use_external', String(options?.useExternal ?? false));
  2644. return request<CalibrationResult & { index: number }>(
  2645. `/printers/${printerId}/camera/plate-detection/calibrate?${params.toString()}`,
  2646. { method: 'POST' }
  2647. );
  2648. },
  2649. deletePlateCalibration: (printerId: number) => {
  2650. return request<CalibrationResult>(
  2651. `/printers/${printerId}/camera/plate-detection/calibrate`,
  2652. { method: 'DELETE' }
  2653. );
  2654. },
  2655. getPlateReferences: (printerId: number) => {
  2656. return request<{
  2657. references: PlateReference[];
  2658. max_references: number;
  2659. }>(`/printers/${printerId}/camera/plate-detection/references`);
  2660. },
  2661. getPlateReferenceThumbnailUrl: (printerId: number, index: number) => {
  2662. return `${API_BASE}/printers/${printerId}/camera/plate-detection/references/${index}/thumbnail`;
  2663. },
  2664. updatePlateReferenceLabel: (printerId: number, index: number, label: string) => {
  2665. const params = new URLSearchParams();
  2666. params.set('label', label);
  2667. return request<{ success: boolean; index: number; label: string }>(
  2668. `/printers/${printerId}/camera/plate-detection/references/${index}?${params.toString()}`,
  2669. { method: 'PUT' }
  2670. );
  2671. },
  2672. deletePlateReference: (printerId: number, index: number) => {
  2673. return request<{ success: boolean; message: string }>(
  2674. `/printers/${printerId}/camera/plate-detection/references/${index}`,
  2675. { method: 'DELETE' }
  2676. );
  2677. },
  2678. // External Links
  2679. getExternalLinks: () => request<ExternalLink[]>('/external-links/'),
  2680. getExternalLink: (id: number) => request<ExternalLink>(`/external-links/${id}`),
  2681. createExternalLink: (data: ExternalLinkCreate) =>
  2682. request<ExternalLink>('/external-links/', {
  2683. method: 'POST',
  2684. body: JSON.stringify(data),
  2685. }),
  2686. updateExternalLink: (id: number, data: ExternalLinkUpdate) =>
  2687. request<ExternalLink>(`/external-links/${id}`, {
  2688. method: 'PATCH',
  2689. body: JSON.stringify(data),
  2690. }),
  2691. deleteExternalLink: (id: number) =>
  2692. request<{ message: string }>(`/external-links/${id}`, { method: 'DELETE' }),
  2693. reorderExternalLinks: (ids: number[]) =>
  2694. request<ExternalLink[]>('/external-links/reorder', {
  2695. method: 'PUT',
  2696. body: JSON.stringify({ ids }),
  2697. }),
  2698. uploadExternalLinkIcon: async (id: number, file: File): Promise<ExternalLink> => {
  2699. const formData = new FormData();
  2700. formData.append('file', file);
  2701. const response = await fetch(`${API_BASE}/external-links/${id}/icon`, {
  2702. method: 'POST',
  2703. body: formData,
  2704. });
  2705. if (!response.ok) {
  2706. const error = await response.json().catch(() => ({}));
  2707. throw new Error(error.detail || `HTTP ${response.status}`);
  2708. }
  2709. return response.json();
  2710. },
  2711. deleteExternalLinkIcon: (id: number) =>
  2712. request<ExternalLink>(`/external-links/${id}/icon`, { method: 'DELETE' }),
  2713. getExternalLinkIconUrl: (id: number) => `${API_BASE}/external-links/${id}/icon`,
  2714. // Projects
  2715. getProjects: (status?: string) => {
  2716. const params = new URLSearchParams();
  2717. if (status) params.set('status', status);
  2718. return request<ProjectListItem[]>(`/projects/?${params}`);
  2719. },
  2720. getProject: (id: number) => request<Project>(`/projects/${id}`),
  2721. createProject: (data: ProjectCreate) =>
  2722. request<Project>('/projects/', {
  2723. method: 'POST',
  2724. body: JSON.stringify(data),
  2725. }),
  2726. updateProject: (id: number, data: ProjectUpdate) =>
  2727. request<Project>(`/projects/${id}`, {
  2728. method: 'PATCH',
  2729. body: JSON.stringify(data),
  2730. }),
  2731. deleteProject: (id: number) =>
  2732. request<{ message: string }>(`/projects/${id}`, { method: 'DELETE' }),
  2733. getProjectArchives: (id: number, limit = 100, offset = 0) =>
  2734. request<Archive[]>(`/projects/${id}/archives?limit=${limit}&offset=${offset}`),
  2735. addArchivesToProject: (projectId: number, archiveIds: number[]) =>
  2736. request<{ message: string }>(`/projects/${projectId}/add-archives`, {
  2737. method: 'POST',
  2738. body: JSON.stringify({ archive_ids: archiveIds }),
  2739. }),
  2740. removeArchivesFromProject: (projectId: number, archiveIds: number[]) =>
  2741. request<{ message: string }>(`/projects/${projectId}/remove-archives`, {
  2742. method: 'POST',
  2743. body: JSON.stringify({ archive_ids: archiveIds }),
  2744. }),
  2745. addQueueItemsToProject: (projectId: number, queueItemIds: number[]) =>
  2746. request<{ message: string }>(`/projects/${projectId}/add-queue`, {
  2747. method: 'POST',
  2748. body: JSON.stringify({ queue_item_ids: queueItemIds }),
  2749. }),
  2750. // Project Attachments
  2751. uploadProjectAttachment: async (projectId: number, file: File): Promise<{
  2752. status: string;
  2753. filename: string;
  2754. original_name: string;
  2755. attachments: ProjectAttachment[];
  2756. }> => {
  2757. const formData = new FormData();
  2758. formData.append('file', file);
  2759. const response = await fetch(`${API_BASE}/projects/${projectId}/attachments`, {
  2760. method: 'POST',
  2761. body: formData,
  2762. });
  2763. if (!response.ok) {
  2764. const error = await response.json().catch(() => ({}));
  2765. throw new Error(error.detail || `HTTP ${response.status}`);
  2766. }
  2767. return response.json();
  2768. },
  2769. getProjectAttachmentUrl: (projectId: number, filename: string) =>
  2770. `${API_BASE}/projects/${projectId}/attachments/${encodeURIComponent(filename)}`,
  2771. deleteProjectAttachment: (projectId: number, filename: string) =>
  2772. request<{ status: string; message: string; attachments: ProjectAttachment[] | null }>(
  2773. `/projects/${projectId}/attachments/${encodeURIComponent(filename)}`,
  2774. { method: 'DELETE' }
  2775. ),
  2776. // BOM (Bill of Materials)
  2777. getProjectBOM: (projectId: number) =>
  2778. request<BOMItem[]>(`/projects/${projectId}/bom`),
  2779. createBOMItem: (projectId: number, data: BOMItemCreate) =>
  2780. request<BOMItem>(`/projects/${projectId}/bom`, {
  2781. method: 'POST',
  2782. body: JSON.stringify(data),
  2783. }),
  2784. updateBOMItem: (projectId: number, itemId: number, data: BOMItemUpdate) =>
  2785. request<BOMItem>(`/projects/${projectId}/bom/${itemId}`, {
  2786. method: 'PATCH',
  2787. body: JSON.stringify(data),
  2788. }),
  2789. deleteBOMItem: (projectId: number, itemId: number) =>
  2790. request<{ status: string; message: string }>(`/projects/${projectId}/bom/${itemId}`, {
  2791. method: 'DELETE',
  2792. }),
  2793. // Templates
  2794. getTemplates: () => request<ProjectListItem[]>('/projects/templates/'),
  2795. createTemplateFromProject: (projectId: number) =>
  2796. request<Project>(`/projects/${projectId}/create-template`, { method: 'POST' }),
  2797. createProjectFromTemplate: (templateId: number, name?: string) =>
  2798. request<Project>(`/projects/from-template/${templateId}${name ? `?name=${encodeURIComponent(name)}` : ''}`, {
  2799. method: 'POST',
  2800. }),
  2801. // Timeline
  2802. getProjectTimeline: (projectId: number, limit = 50) =>
  2803. request<TimelineEvent[]>(`/projects/${projectId}/timeline?limit=${limit}`),
  2804. // Project Export/Import
  2805. exportProjectJson: (projectId: number) =>
  2806. request<ProjectExport>(`/projects/${projectId}/export?format=json`),
  2807. importProject: (data: ProjectImport) =>
  2808. request<Project>('/projects/import', {
  2809. method: 'POST',
  2810. body: JSON.stringify(data),
  2811. }),
  2812. // API Keys
  2813. getAPIKeys: () => request<APIKey[]>('/api-keys/'),
  2814. createAPIKey: (data: APIKeyCreate) =>
  2815. request<APIKeyCreateResponse>('/api-keys/', {
  2816. method: 'POST',
  2817. body: JSON.stringify(data),
  2818. }),
  2819. updateAPIKey: (id: number, data: APIKeyUpdate) =>
  2820. request<APIKey>(`/api-keys/${id}`, {
  2821. method: 'PATCH',
  2822. body: JSON.stringify(data),
  2823. }),
  2824. deleteAPIKey: (id: number) =>
  2825. request<{ message: string }>(`/api-keys/${id}`, { method: 'DELETE' }),
  2826. // AMS History
  2827. getAMSHistory: (printerId: number, amsId: number, hours = 24) =>
  2828. request<AMSHistoryResponse>(`/ams-history/${printerId}/${amsId}?hours=${hours}`),
  2829. // System Info
  2830. getSystemInfo: () => request<SystemInfo>('/system/info'),
  2831. // Library (File Manager)
  2832. getLibraryFolders: () => request<LibraryFolderTree[]>('/library/folders'),
  2833. createLibraryFolder: (data: LibraryFolderCreate) =>
  2834. request<LibraryFolder>('/library/folders', {
  2835. method: 'POST',
  2836. body: JSON.stringify(data),
  2837. }),
  2838. updateLibraryFolder: (id: number, data: LibraryFolderUpdate) =>
  2839. request<LibraryFolder>(`/library/folders/${id}`, {
  2840. method: 'PUT',
  2841. body: JSON.stringify(data),
  2842. }),
  2843. deleteLibraryFolder: (id: number) =>
  2844. request<{ status: string; message: string }>(`/library/folders/${id}`, { method: 'DELETE' }),
  2845. getLibraryFoldersByProject: (projectId: number) =>
  2846. request<LibraryFolder[]>(`/library/folders/by-project/${projectId}`),
  2847. getLibraryFoldersByArchive: (archiveId: number) =>
  2848. request<LibraryFolder[]>(`/library/folders/by-archive/${archiveId}`),
  2849. getLibraryFiles: (folderId?: number | null, includeRoot = true) => {
  2850. const params = new URLSearchParams();
  2851. if (folderId !== undefined && folderId !== null) {
  2852. params.set('folder_id', String(folderId));
  2853. }
  2854. params.set('include_root', String(includeRoot));
  2855. return request<LibraryFileListItem[]>(`/library/files?${params}`);
  2856. },
  2857. getLibraryFile: (id: number) => request<LibraryFile>(`/library/files/${id}`),
  2858. uploadLibraryFile: async (
  2859. file: File,
  2860. folderId?: number | null,
  2861. generateStlThumbnails: boolean = true
  2862. ): Promise<LibraryFileUploadResponse> => {
  2863. const formData = new FormData();
  2864. formData.append('file', file);
  2865. const params = new URLSearchParams();
  2866. if (folderId) params.set('folder_id', String(folderId));
  2867. params.set('generate_stl_thumbnails', String(generateStlThumbnails));
  2868. const response = await fetch(`${API_BASE}/library/files?${params}`, {
  2869. method: 'POST',
  2870. body: formData,
  2871. });
  2872. if (!response.ok) {
  2873. const error = await response.json().catch(() => ({}));
  2874. throw new Error(error.detail || `HTTP ${response.status}`);
  2875. }
  2876. return response.json();
  2877. },
  2878. extractZipFile: async (
  2879. file: File,
  2880. folderId?: number | null,
  2881. preserveStructure: boolean = true,
  2882. createFolderFromZip: boolean = false,
  2883. generateStlThumbnails: boolean = true
  2884. ): Promise<ZipExtractResponse> => {
  2885. const formData = new FormData();
  2886. formData.append('file', file);
  2887. const params = new URLSearchParams();
  2888. if (folderId) params.set('folder_id', String(folderId));
  2889. params.set('preserve_structure', String(preserveStructure));
  2890. params.set('create_folder_from_zip', String(createFolderFromZip));
  2891. params.set('generate_stl_thumbnails', String(generateStlThumbnails));
  2892. const response = await fetch(`${API_BASE}/library/files/extract-zip?${params}`, {
  2893. method: 'POST',
  2894. body: formData,
  2895. });
  2896. if (!response.ok) {
  2897. const error = await response.json().catch(() => ({}));
  2898. throw new Error(error.detail || `HTTP ${response.status}`);
  2899. }
  2900. return response.json();
  2901. },
  2902. updateLibraryFile: (id: number, data: LibraryFileUpdate) =>
  2903. request<LibraryFile>(`/library/files/${id}`, {
  2904. method: 'PUT',
  2905. body: JSON.stringify(data),
  2906. }),
  2907. deleteLibraryFile: (id: number) =>
  2908. request<{ status: string; message: string }>(`/library/files/${id}`, { method: 'DELETE' }),
  2909. getLibraryFileDownloadUrl: (id: number) => `${API_BASE}/library/files/${id}/download`,
  2910. getLibraryFileThumbnailUrl: (id: number) => `${API_BASE}/library/files/${id}/thumbnail`,
  2911. getLibraryFilePlateThumbnail: (id: number, plateIndex: number) =>
  2912. `${API_BASE}/library/files/${id}/plate-thumbnail/${plateIndex}`,
  2913. getLibraryFileGcodeUrl: (id: number) => `${API_BASE}/library/files/${id}/gcode`,
  2914. moveLibraryFiles: (fileIds: number[], folderId: number | null) =>
  2915. request<{ status: string; moved: number }>('/library/files/move', {
  2916. method: 'POST',
  2917. body: JSON.stringify({ file_ids: fileIds, folder_id: folderId }),
  2918. }),
  2919. bulkDeleteLibrary: (fileIds: number[], folderIds: number[]) =>
  2920. request<{ deleted_files: number; deleted_folders: number }>('/library/bulk-delete', {
  2921. method: 'POST',
  2922. body: JSON.stringify({ file_ids: fileIds, folder_ids: folderIds }),
  2923. }),
  2924. getLibraryStats: () => request<LibraryStats>('/library/stats'),
  2925. batchGenerateStlThumbnails: (options: {
  2926. file_ids?: number[];
  2927. folder_id?: number;
  2928. all_missing?: boolean;
  2929. }) =>
  2930. request<BatchThumbnailResponse>('/library/generate-stl-thumbnails', {
  2931. method: 'POST',
  2932. body: JSON.stringify(options),
  2933. }),
  2934. addLibraryFilesToQueue: (fileIds: number[]) =>
  2935. request<AddToQueueResponse>('/library/files/add-to-queue', {
  2936. method: 'POST',
  2937. body: JSON.stringify({ file_ids: fileIds }),
  2938. }),
  2939. printLibraryFile: (
  2940. fileId: number,
  2941. printerId: number,
  2942. options?: {
  2943. plate_id?: number;
  2944. ams_mapping?: number[];
  2945. bed_levelling?: boolean;
  2946. flow_cali?: boolean;
  2947. vibration_cali?: boolean;
  2948. layer_inspect?: boolean;
  2949. timelapse?: boolean;
  2950. use_ams?: boolean;
  2951. }
  2952. ) =>
  2953. request<{ status: string; printer_id: number; archive_id: number; filename: string }>(
  2954. `/library/files/${fileId}/print?printer_id=${printerId}`,
  2955. {
  2956. method: 'POST',
  2957. body: options ? JSON.stringify(options) : undefined,
  2958. }
  2959. ),
  2960. getLibraryFilePlates: (fileId: number) =>
  2961. request<{
  2962. file_id: number;
  2963. filename: string;
  2964. plates: Array<{
  2965. index: number;
  2966. name: string | null;
  2967. objects: string[];
  2968. has_thumbnail: boolean;
  2969. thumbnail_url: string | null;
  2970. print_time_seconds: number | null;
  2971. filament_used_grams: number | null;
  2972. filaments: Array<{
  2973. slot_id: number;
  2974. type: string;
  2975. color: string;
  2976. used_grams: number;
  2977. used_meters: number;
  2978. }>;
  2979. }>;
  2980. is_multi_plate: boolean;
  2981. }>(`/library/files/${fileId}/plates`),
  2982. getLibraryFileFilamentRequirements: (fileId: number, plateId?: number) =>
  2983. request<{
  2984. file_id: number;
  2985. filename: string;
  2986. filaments: Array<{
  2987. slot_id: number;
  2988. type: string;
  2989. color: string;
  2990. used_grams: number;
  2991. used_meters: number;
  2992. }>;
  2993. }>(`/library/files/${fileId}/filament-requirements${plateId !== undefined ? `?plate_id=${plateId}` : ''}`),
  2994. // GitHub Backup
  2995. getGitHubBackupConfig: () =>
  2996. request<GitHubBackupConfig | null>('/github-backup/config'),
  2997. saveGitHubBackupConfig: (config: GitHubBackupConfigCreate) =>
  2998. request<GitHubBackupConfig>('/github-backup/config', {
  2999. method: 'POST',
  3000. body: JSON.stringify(config),
  3001. }),
  3002. updateGitHubBackupConfig: (config: Partial<GitHubBackupConfigCreate>) =>
  3003. request<GitHubBackupConfig>('/github-backup/config', {
  3004. method: 'PATCH',
  3005. body: JSON.stringify(config),
  3006. }),
  3007. deleteGitHubBackupConfig: () =>
  3008. request<{ message: string }>('/github-backup/config', { method: 'DELETE' }),
  3009. testGitHubConnection: (repoUrl: string, token: string) =>
  3010. request<GitHubTestConnectionResponse>(
  3011. `/github-backup/test?repo_url=${encodeURIComponent(repoUrl)}&token=${encodeURIComponent(token)}`,
  3012. { method: 'POST' }
  3013. ),
  3014. testGitHubStoredConnection: () =>
  3015. request<GitHubTestConnectionResponse>('/github-backup/test-stored', { method: 'POST' }),
  3016. triggerGitHubBackup: () =>
  3017. request<GitHubBackupTriggerResponse>('/github-backup/run', { method: 'POST' }),
  3018. getGitHubBackupStatus: () =>
  3019. request<GitHubBackupStatus>('/github-backup/status'),
  3020. getGitHubBackupLogs: (limit: number = 50) =>
  3021. request<GitHubBackupLog[]>(`/github-backup/logs?limit=${limit}`),
  3022. clearGitHubBackupLogs: (keepLast: number = 10) =>
  3023. request<{ deleted: number; message: string }>(`/github-backup/logs?keep_last=${keepLast}`, { method: 'DELETE' }),
  3024. };
  3025. // AMS History types
  3026. export interface AMSHistoryPoint {
  3027. recorded_at: string;
  3028. humidity: number | null;
  3029. humidity_raw: number | null;
  3030. temperature: number | null;
  3031. }
  3032. export interface AMSHistoryResponse {
  3033. printer_id: number;
  3034. ams_id: number;
  3035. data: AMSHistoryPoint[];
  3036. min_humidity: number | null;
  3037. max_humidity: number | null;
  3038. avg_humidity: number | null;
  3039. min_temperature: number | null;
  3040. max_temperature: number | null;
  3041. avg_temperature: number | null;
  3042. }
  3043. // System Info types
  3044. export interface SystemInfo {
  3045. app: {
  3046. version: string;
  3047. base_dir: string;
  3048. archive_dir: string;
  3049. };
  3050. database: {
  3051. archives: number;
  3052. archives_completed: number;
  3053. archives_failed: number;
  3054. archives_printing: number;
  3055. printers: number;
  3056. filaments: number;
  3057. projects: number;
  3058. smart_plugs: number;
  3059. total_print_time_seconds: number;
  3060. total_print_time_formatted: string;
  3061. total_filament_grams: number;
  3062. total_filament_kg: number;
  3063. };
  3064. printers: {
  3065. total: number;
  3066. connected: number;
  3067. connected_list: Array<{
  3068. id: number;
  3069. name: string;
  3070. state: string;
  3071. model: string;
  3072. }>;
  3073. };
  3074. storage: {
  3075. archive_size_bytes: number;
  3076. archive_size_formatted: string;
  3077. database_size_bytes: number;
  3078. database_size_formatted: string;
  3079. disk_total_bytes: number;
  3080. disk_total_formatted: string;
  3081. disk_used_bytes: number;
  3082. disk_used_formatted: string;
  3083. disk_free_bytes: number;
  3084. disk_free_formatted: string;
  3085. disk_percent_used: number;
  3086. };
  3087. system: {
  3088. platform: string;
  3089. platform_release: string;
  3090. platform_version: string;
  3091. architecture: string;
  3092. hostname: string;
  3093. python_version: string;
  3094. uptime_seconds: number;
  3095. uptime_formatted: string;
  3096. boot_time: string;
  3097. };
  3098. memory: {
  3099. total_bytes: number;
  3100. total_formatted: string;
  3101. available_bytes: number;
  3102. available_formatted: string;
  3103. used_bytes: number;
  3104. used_formatted: string;
  3105. percent_used: number;
  3106. };
  3107. cpu: {
  3108. count: number;
  3109. count_logical: number;
  3110. percent: number;
  3111. };
  3112. }
  3113. // Library (File Manager) types
  3114. export interface LibraryFolderTree {
  3115. id: number;
  3116. name: string;
  3117. parent_id: number | null;
  3118. project_id: number | null;
  3119. archive_id: number | null;
  3120. project_name: string | null;
  3121. archive_name: string | null;
  3122. file_count: number;
  3123. children: LibraryFolderTree[];
  3124. }
  3125. export interface LibraryFolder {
  3126. id: number;
  3127. name: string;
  3128. parent_id: number | null;
  3129. project_id: number | null;
  3130. archive_id: number | null;
  3131. project_name: string | null;
  3132. archive_name: string | null;
  3133. file_count: number;
  3134. created_at: string;
  3135. updated_at: string;
  3136. }
  3137. export interface LibraryFolderCreate {
  3138. name: string;
  3139. parent_id?: number | null;
  3140. project_id?: number | null;
  3141. archive_id?: number | null;
  3142. }
  3143. export interface LibraryFolderUpdate {
  3144. name?: string;
  3145. parent_id?: number | null;
  3146. project_id?: number | null; // 0 to unlink
  3147. archive_id?: number | null; // 0 to unlink
  3148. }
  3149. export interface LibraryFileDuplicate {
  3150. id: number;
  3151. filename: string;
  3152. folder_id: number | null;
  3153. folder_name: string | null;
  3154. created_at: string;
  3155. }
  3156. export interface LibraryFile {
  3157. id: number;
  3158. folder_id: number | null;
  3159. folder_name: string | null;
  3160. project_id: number | null;
  3161. project_name: string | null;
  3162. filename: string;
  3163. file_path: string;
  3164. file_type: string;
  3165. file_size: number;
  3166. file_hash: string | null;
  3167. thumbnail_path: string | null;
  3168. metadata: Record<string, unknown> | null;
  3169. print_count: number;
  3170. last_printed_at: string | null;
  3171. notes: string | null;
  3172. duplicates: LibraryFileDuplicate[] | null;
  3173. duplicate_count: number;
  3174. created_at: string;
  3175. updated_at: string;
  3176. }
  3177. export interface LibraryFileListItem {
  3178. id: number;
  3179. folder_id: number | null;
  3180. filename: string;
  3181. file_type: string;
  3182. file_size: number;
  3183. thumbnail_path: string | null;
  3184. print_count: number;
  3185. duplicate_count: number;
  3186. created_at: string;
  3187. print_name: string | null;
  3188. print_time_seconds: number | null;
  3189. filament_used_grams: number | null;
  3190. }
  3191. export interface LibraryFileUpdate {
  3192. filename?: string;
  3193. folder_id?: number | null;
  3194. project_id?: number | null;
  3195. notes?: string | null;
  3196. }
  3197. export interface LibraryFileUploadResponse {
  3198. id: number;
  3199. filename: string;
  3200. file_type: string;
  3201. file_size: number;
  3202. thumbnail_path: string | null;
  3203. duplicate_of: number | null;
  3204. metadata: Record<string, unknown> | null;
  3205. }
  3206. export interface LibraryStats {
  3207. total_files: number;
  3208. total_folders: number;
  3209. total_size_bytes: number;
  3210. files_by_type: Record<string, number>;
  3211. total_prints: number;
  3212. disk_free_bytes: number;
  3213. disk_total_bytes: number;
  3214. disk_used_bytes: number;
  3215. }
  3216. export interface ZipExtractResult {
  3217. filename: string;
  3218. file_id: number;
  3219. folder_id: number | null;
  3220. }
  3221. export interface ZipExtractError {
  3222. filename: string;
  3223. error: string;
  3224. }
  3225. export interface ZipExtractResponse {
  3226. extracted: number;
  3227. folders_created: number;
  3228. files: ZipExtractResult[];
  3229. errors: ZipExtractError[];
  3230. }
  3231. // STL Thumbnail Generation types
  3232. export interface BatchThumbnailResult {
  3233. file_id: number;
  3234. filename: string;
  3235. success: boolean;
  3236. error?: string | null;
  3237. }
  3238. export interface BatchThumbnailResponse {
  3239. processed: number;
  3240. succeeded: number;
  3241. failed: number;
  3242. results: BatchThumbnailResult[];
  3243. }
  3244. // Library Queue types
  3245. export interface AddToQueueResult {
  3246. file_id: number;
  3247. filename: string;
  3248. queue_item_id: number;
  3249. archive_id: number;
  3250. }
  3251. export interface AddToQueueError {
  3252. file_id: number;
  3253. filename: string;
  3254. error: string;
  3255. }
  3256. export interface AddToQueueResponse {
  3257. added: AddToQueueResult[];
  3258. errors: AddToQueueError[];
  3259. }
  3260. // Discovery types
  3261. export interface DiscoveredPrinter {
  3262. serial: string;
  3263. name: string;
  3264. ip_address: string;
  3265. model: string | null;
  3266. discovered_at: string | null;
  3267. }
  3268. export interface DiscoveryStatus {
  3269. running: boolean;
  3270. }
  3271. export interface DiscoveryInfo {
  3272. is_docker: boolean;
  3273. ssdp_running: boolean;
  3274. scan_running: boolean;
  3275. }
  3276. export interface SubnetScanStatus {
  3277. running: boolean;
  3278. scanned: number;
  3279. total: number;
  3280. }
  3281. // Discovery API
  3282. export const discoveryApi = {
  3283. getInfo: () => request<DiscoveryInfo>('/discovery/info'),
  3284. getStatus: () => request<DiscoveryStatus>('/discovery/status'),
  3285. startDiscovery: (duration: number = 10) =>
  3286. request<DiscoveryStatus>(`/discovery/start?duration=${duration}`, { method: 'POST' }),
  3287. stopDiscovery: () =>
  3288. request<DiscoveryStatus>('/discovery/stop', { method: 'POST' }),
  3289. getDiscoveredPrinters: () =>
  3290. request<DiscoveredPrinter[]>('/discovery/printers'),
  3291. // Subnet scanning (for Docker environments)
  3292. startSubnetScan: (subnet: string, timeout: number = 1.0) =>
  3293. request<SubnetScanStatus>('/discovery/scan', {
  3294. method: 'POST',
  3295. body: JSON.stringify({ subnet, timeout }),
  3296. }),
  3297. getScanStatus: () => request<SubnetScanStatus>('/discovery/scan/status'),
  3298. stopSubnetScan: () =>
  3299. request<SubnetScanStatus>('/discovery/scan/stop', { method: 'POST' }),
  3300. };
  3301. // Virtual Printer types
  3302. export interface VirtualPrinterStatus {
  3303. enabled: boolean;
  3304. running: boolean;
  3305. mode: 'immediate' | 'queue' | 'review' | 'print_queue'; // 'queue' is legacy, normalized to 'review'
  3306. name: string;
  3307. serial: string;
  3308. model: string;
  3309. model_name: string;
  3310. pending_files: number;
  3311. }
  3312. export interface VirtualPrinterSettings {
  3313. enabled: boolean;
  3314. access_code_set: boolean;
  3315. mode: 'immediate' | 'queue' | 'review' | 'print_queue'; // 'queue' is legacy, normalized to 'review'
  3316. model: string;
  3317. status: VirtualPrinterStatus;
  3318. }
  3319. export interface VirtualPrinterModels {
  3320. models: Record<string, string>; // SSDP code -> display name
  3321. default: string;
  3322. }
  3323. export interface PendingUpload {
  3324. id: number;
  3325. filename: string;
  3326. file_size: number;
  3327. source_ip: string | null;
  3328. status: string;
  3329. tags: string | null;
  3330. notes: string | null;
  3331. project_id: number | null;
  3332. uploaded_at: string;
  3333. }
  3334. // Virtual Printer API
  3335. export const virtualPrinterApi = {
  3336. getSettings: () => request<VirtualPrinterSettings>('/settings/virtual-printer'),
  3337. getModels: () => request<VirtualPrinterModels>('/settings/virtual-printer/models'),
  3338. updateSettings: (data: {
  3339. enabled?: boolean;
  3340. access_code?: string;
  3341. mode?: 'immediate' | 'review' | 'print_queue';
  3342. model?: string;
  3343. }) => {
  3344. const params = new URLSearchParams();
  3345. if (data.enabled !== undefined) params.set('enabled', String(data.enabled));
  3346. if (data.access_code !== undefined) params.set('access_code', data.access_code);
  3347. if (data.mode !== undefined) params.set('mode', data.mode);
  3348. if (data.model !== undefined) params.set('model', data.model);
  3349. return request<VirtualPrinterSettings>(`/settings/virtual-printer?${params.toString()}`, {
  3350. method: 'PUT',
  3351. });
  3352. },
  3353. };
  3354. // Pending Uploads API
  3355. export const pendingUploadsApi = {
  3356. list: () => request<PendingUpload[]>('/pending-uploads/'),
  3357. getCount: () => request<{ count: number }>('/pending-uploads/count'),
  3358. get: (id: number) => request<PendingUpload>(`/pending-uploads/${id}`),
  3359. archive: (id: number, data?: { tags?: string; notes?: string; project_id?: number }) =>
  3360. request<{ id: number; print_name: string; filename: string }>(`/pending-uploads/${id}/archive`, {
  3361. method: 'POST',
  3362. body: JSON.stringify(data || {}),
  3363. }),
  3364. discard: (id: number) =>
  3365. request<{ success: boolean }>(`/pending-uploads/${id}`, { method: 'DELETE' }),
  3366. archiveAll: () =>
  3367. request<{ archived: number; failed: number }>('/pending-uploads/archive-all', { method: 'POST' }),
  3368. discardAll: () =>
  3369. request<{ discarded: number }>('/pending-uploads/discard-all', { method: 'DELETE' }),
  3370. };
  3371. // Firmware API Types
  3372. export interface FirmwareUpdateInfo {
  3373. printer_id: number;
  3374. printer_name: string;
  3375. model: string | null;
  3376. current_version: string | null;
  3377. latest_version: string | null;
  3378. update_available: boolean;
  3379. download_url: string | null;
  3380. release_notes: string | null;
  3381. }
  3382. export interface FirmwareUploadPrepare {
  3383. can_proceed: boolean;
  3384. sd_card_present: boolean;
  3385. sd_card_free_space: number;
  3386. firmware_size: number;
  3387. space_sufficient: boolean;
  3388. update_available: boolean;
  3389. current_version: string | null;
  3390. latest_version: string | null;
  3391. firmware_filename: string | null;
  3392. errors: string[];
  3393. }
  3394. export interface FirmwareUploadStatus {
  3395. status: 'idle' | 'preparing' | 'downloading' | 'uploading' | 'complete' | 'error';
  3396. progress: number;
  3397. message: string;
  3398. error: string | null;
  3399. firmware_filename: string | null;
  3400. firmware_version: string | null;
  3401. }
  3402. // Firmware API
  3403. export const firmwareApi = {
  3404. checkUpdates: () =>
  3405. request<{ updates: FirmwareUpdateInfo[]; updates_available: number }>('/firmware/updates'),
  3406. checkPrinterUpdate: (printerId: number) =>
  3407. request<FirmwareUpdateInfo>(`/firmware/updates/${printerId}`),
  3408. prepareUpload: (printerId: number) =>
  3409. request<FirmwareUploadPrepare>(`/firmware/updates/${printerId}/prepare`),
  3410. startUpload: (printerId: number) =>
  3411. request<{ started: boolean; message: string }>(`/firmware/updates/${printerId}/upload`, {
  3412. method: 'POST',
  3413. }),
  3414. getUploadStatus: (printerId: number) =>
  3415. request<FirmwareUploadStatus>(`/firmware/updates/${printerId}/upload/status`),
  3416. };
  3417. // Support types
  3418. export interface DebugLoggingState {
  3419. enabled: boolean;
  3420. enabled_at: string | null;
  3421. duration_seconds: number | null;
  3422. }
  3423. export interface LogEntry {
  3424. timestamp: string;
  3425. level: string;
  3426. logger_name: string;
  3427. message: string;
  3428. }
  3429. export interface LogsResponse {
  3430. entries: LogEntry[];
  3431. total_in_file: number;
  3432. filtered_count: number;
  3433. }
  3434. // Support API
  3435. export const supportApi = {
  3436. getDebugLoggingState: () =>
  3437. request<DebugLoggingState>('/support/debug-logging'),
  3438. setDebugLogging: (enabled: boolean) =>
  3439. request<DebugLoggingState>('/support/debug-logging', {
  3440. method: 'POST',
  3441. body: JSON.stringify({ enabled }),
  3442. }),
  3443. downloadSupportBundle: async () => {
  3444. const response = await fetch(`${API_BASE}/support/bundle`);
  3445. if (!response.ok) {
  3446. const error = await response.json().catch(() => ({}));
  3447. throw new Error(error.detail || `HTTP ${response.status}`);
  3448. }
  3449. // Get filename from Content-Disposition header or use default
  3450. const disposition = response.headers.get('Content-Disposition');
  3451. const filenameMatch = disposition?.match(/filename=(.+)/);
  3452. const filename = filenameMatch ? filenameMatch[1] : 'bambuddy-support.zip';
  3453. // Download the blob
  3454. const blob = await response.blob();
  3455. const url = window.URL.createObjectURL(blob);
  3456. const a = document.createElement('a');
  3457. a.href = url;
  3458. a.download = filename;
  3459. document.body.appendChild(a);
  3460. a.click();
  3461. document.body.removeChild(a);
  3462. window.URL.revokeObjectURL(url);
  3463. },
  3464. getLogs: (params?: { limit?: number; level?: string; search?: string }) => {
  3465. const searchParams = new URLSearchParams();
  3466. if (params?.limit) searchParams.set('limit', params.limit.toString());
  3467. if (params?.level) searchParams.set('level', params.level);
  3468. if (params?.search) searchParams.set('search', params.search);
  3469. const query = searchParams.toString();
  3470. return request<LogsResponse>(`/support/logs${query ? `?${query}` : ''}`);
  3471. },
  3472. clearLogs: () =>
  3473. request<{ message: string }>('/support/logs', { method: 'DELETE' }),
  3474. };