// ── State ── var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0, print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0, curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–', camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80, ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]}}; var tempHistory={n:[],b:[]}; var camOn=false; var currentStep=1; var currentPanel='dashboard'; var aceAutoRefillPrefs=(function(){ try{return JSON.parse(localStorage.getItem('aceAutoRefillPrefs')||'{}')||{};}catch(_){return {};} })(); var aceDryProfiles=(function(){ try{return JSON.parse(localStorage.getItem('aceDryProfiles')||'{}')||{};}catch(_){return {};} })(); var _aceDryDialogAceId=-1; var _aceDryDialogPresetKey=''; var _aceDryDialogPresetOriginals={}; var ACE_DRY_PRESET_DEFAULTS={ pla:{temp:45,duration_sec:4*3600}, pla_plus:{temp:45,duration_sec:4*3600}, petg:{temp:50,duration_sec:4*3600}, tpu:{temp:55,duration_sec:4*3600}, abs_asa:{temp:45,duration_sec:8*3600}, pa_pc:{temp:55,duration_sec:12*3600} }; var ACE_DRY_PRESETS={ pla:{temp:45,duration_sec:4*3600}, pla_plus:{temp:45,duration_sec:4*3600}, petg:{temp:50,duration_sec:4*3600}, tpu:{temp:55,duration_sec:4*3600}, abs_asa:{temp:45,duration_sec:8*3600}, pa_pc:{temp:55,duration_sec:12*3600}, custom_1:{name:'Custom 1',temp:45,duration_sec:4*3600}, custom_2:{name:'Custom 2',temp:45,duration_sec:4*3600}, custom_3:{name:'Custom 3',temp:45,duration_sec:4*3600} }; function _aceAutoRefillGet(aceId){return !!aceAutoRefillPrefs[String(aceId)];} function _aceAutoRefillSet(aceId,on){ aceAutoRefillPrefs[String(aceId)]=!!on; localStorage.setItem('aceAutoRefillPrefs',JSON.stringify(aceAutoRefillPrefs)); } function _aceDryProfileGet(aceId){ var p=aceDryProfiles[String(aceId)]||{}; var temp=parseInt(p.temp,10); var dur=parseInt(p.duration_sec,10); if(!Number.isFinite(temp))temp=45; if(!Number.isFinite(dur))dur=4*3600; temp=Math.max(30,Math.min(80,temp)); dur=Math.max(10*60,Math.min(24*3600,dur)); return {temp:temp,duration_sec:dur,preset:p.preset||''}; } function _aceDryProfileSet(aceId,temp,durationSec,preset){ aceDryProfiles[String(aceId)]={ temp:Math.max(30,Math.min(80,parseInt(temp,10)||45)), duration_sec:Math.max(10*60,Math.min(24*3600,parseInt(durationSec,10)||4*3600)), preset:preset||'' }; localStorage.setItem('aceDryProfiles',JSON.stringify(aceDryProfiles)); } function _aceDryDurationMinFromSec(sec){ var minutes=Math.round((parseInt(sec,10)||0)/60); return Math.max(10,Math.min(1440,minutes)); } function _syncAceDryPresetsFromServer(raw){ if(!raw||typeof raw!=='object')return; Object.keys(ACE_DRY_PRESETS).forEach(function(k){ var p=raw[k]; if(!p||typeof p!=='object')return; var t=parseInt(p.temp,10); var d=parseInt(p.duration_sec,10); if(Number.isFinite(t))ACE_DRY_PRESETS[k].temp=Math.max(30,Math.min(80,t)); if(Number.isFinite(d))ACE_DRY_PRESETS[k].duration_sec=Math.max(10*60,Math.min(24*3600,d)); if(/^custom_[123]$/.test(k)&&typeof p.name==='string'){ var n=p.name.trim(); ACE_DRY_PRESETS[k].name=n||('Custom '+k.slice(-1)); } }); } // ── Theme ── function toggleTheme(){ var h=document.documentElement; h.setAttribute('data-theme',h.getAttribute('data-theme')==='dark'?'light':'dark'); localStorage.setItem('theme',h.getAttribute('data-theme')); } (function(){var t=localStorage.getItem('theme');if(t)document.documentElement.setAttribute('data-theme',t)})(); // ── i18n ── var LANG_DE={ header_status_standby:'Bereit',header_status_printing:'Druckt',header_status_complete:'Fertig',header_status_error:'Fehler', kobra_free:'Bereit',kobra_busy:'Beschäftigt',kobra_printing:'Druckt',kobra_preheating:'Aufheizen',kobra_auto_leveling:'Nivellierung',kobra_checking:'Prüfung',kobra_updated:'Aktualisierung',kobra_init:'Initialisierung',kobra_pausing:'Pausiert...',kobra_paused:'Pausiert',kobra_resuming:'Fortsetzen...',kobra_resumed:'Fortgesetzt',kobra_stopping:'Stoppt...',kobra_stoped:'Gestoppt',kobra_finished:'Abgeschlossen',kobra_failed:'Fehler',kobra_canceled:'Abgebrochen',kobra_offline:'Offline', nav_dashboard:'Dashboard',nav_print:'Druck',nav_temps:'Temperaturen',nav_motion:'Achsen',nav_ams:'AMS',nav_extras:'Licht / Lüfter',nav_console:'Konsole', card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',card_speed:'Druckgeschwindigkeit',card_cam:'Kamera',lbl_elapsed:'Verstrichen:',lbl_remaining:'Restzeit:',lbl_slicer_time:'Slicer-Schätzung:',lbl_layers:'Layer', speed_silent:'🐢 Leise',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport', lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen', card_ace_dry:'ACE Trocknung',ace_dry_dryer:'Trockner',ace_dry_status_off:'Status: Aus',ace_dry_status_on:'Status: Aktiv',ace_dry_status_remaining:'Rest',ace_dry_humidity:'Luftfeuchte',ace_dry_current_temp:'Temperatur',ace_dry_chart:'Verlauf (Temp/Feuchte)',ace_dry_temp:'Temperatur (°C)',ace_dry_duration:'Dauer (Min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto Refill',ace_dry_enable:'Enable Drying',ace_dry_temp_line:'Trocknungstemperatur',ace_dry_time_line:'Trocknungszeit',ace_dry_ui_pending:'(nur UI, Backend folgt)',ace_dry_dialog_title:'Dryer Temp/Time Settings',ace_dry_dialog_temp:'Temperature (30-80°C)',ace_dry_dialog_time:'Rem. Time (h:m:s)',ace_dry_dialog_confirm:'Confirm',ace_dry_dialog_cancel:'Cancel',ace_dry_dialog_save_restart:'Speichern & Neustart',ace_dry_dialog_custom_name:'Custom Name', cam_placeholder:'📷 Kamera nicht gestartet',btn_cam_start:'▶ Kamera',btn_cam_stop:'◼ Kamera', btn_pause:'⏸ Pause',btn_resume:'▶ Weiter',btn_cancel:'✕ Stopp', label_nozzle:'Nozzle',label_bed:'Bett',label_fan:'🌀 Lüfter',label_light:'💡 Licht',label_on_off:'Ein / Aus',label_speed:'Geschwindigkeit', panel_print_title:'Drucksteuerung',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Fortsetzen',panel_print_btn_cancel:'✕ Abbrechen',panel_print_temps_live:'Temperaturen (Live)', label_set:'Setzen',label_off:'Aus', panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heizbett',panel_temps_chart:'Verlauf (letzte 60 Messungen)',label_target_c:'Ziel:', panel_motion_xy:'XY-Achsen',panel_motion_z:'Z-Achse',label_step:'Schrittweite:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motoren aus', panel_ams_title:'Filament',card_ams:'Filament',ams_no_data:'Keine AMS-Daten empfangen',label_slot:'Slot',ams_empty:'Leer', panel_extras_light:'Licht',panel_extras_fan:'Lüfter',panel_extras_camera:'Kamera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop', panel_console_title:'Ereignis-Log', log_light_on:'Licht an',log_light_off:'Licht aus',log_fan:'Lüfter →',log_nozzle:'Nozzle →',log_bed:'Bett →',log_axis:'Achse',log_home:'Home',log_home_all:'Home All',log_cam_start:'Kamera gestartet:',log_cam_stop:'Kamera gestoppt',log_poll_error:'Poll-Fehler:',log_error:'Fehler:', confirm_cancel:'Druck wirklich abbrechen?', settings_title:'Einstellungen',settings_connection:'Verbindung',settings_print:'Druckeinstellungen',settings_poll:'Poll-Intervall',settings_version:'Version', settings_save:'Speichern & Neustart',settings_printer_name:'Drucker-Name',settings_printer_ip:'Drucker-IP',settings_mqtt_port:'MQTT-Port', settings_username:'MQTT-Benutzername',settings_password:'MQTT-Passwort',settings_device_id:'Device-ID',settings_mode_id:'Mode-ID',hint_ip_no_port:'Nur IP-Adresse, kein Port (z.B. 192.168.1.102)', settings_default_slot:'Standard-Slot (Einfarbdruck)',settings_slot_auto:'Auto (alle belegten Slots)',settings_auto_leveling:'Auto-Leveling vor Druck', update_check:'Auf Updates prüfen',update_checking:'Prüfe...',update_available:'verfügbar',update_none:'Bereits aktuell', update_apply:'Jetzt installieren',update_applying:'Lade herunter...',update_restarting:'Starte neu...',update_error:'Fehler', btn_connect:'⚡ Verbinden',btn_disconnect:'✕ Trennen', lbl_conn_error:'Verbindungsfehler:', slot_edit_title:'Slot bearbeiten',slot_edit_color:'Farbe',slot_edit_material:'Material', slot_edit_load:'⬇ Einziehen',slot_edit_unload:'⬆ Ausziehen', slot_edit_save:'💾 Speichern',slot_edit_custom:'z.B. PLA, PETG, ABS…', slot_edit_ok:'AMS Slot', log_dir_all:'Alle', file_ready_btn:'▶ Druck starten', file_slots_btn:'🎨 Slots wählen', file_cancel_btn:'✕ Abbrechen', nav_printers:'Drucker', skip_title:'✂ Objekte überspringen',skip_hint:'Objekte abwählen, die nicht weiter gedruckt werden sollen:', skip_btn_label:'Objekte',skip_no_objects:'Keine Objekte in diesem Druck.', skip_already:'übersprungen',skip_select_at_least_one:'Bitte mindestens ein Objekt wählen.', skip_sending:'Sende …',skip_success:'Objekte werden übersprungen.', fd_objects_hint:'Objekte überspringen (optional):', add_printer:'Drucker hinzufügen',apd_lbl_ip:'Drucker-IP',apd_lbl_name:'Name (optional)', apd_fetching:'Hole Daten vom Drucker…',apd_success:'Drucker hinzugefügt, Bridge startet neu…',apd_err_ip:'Bitte IP-Adresse eingeben', printers_remove:'Drucker entfernen',printers_remove_confirm:'Drucker "{name}" entfernen? Die Bridge startet neu.', printers_active:'● aktiv', printers_switch:'Wechseln →', printers_current:'Aktueller Drucker', printers_loading:'Lade…', printers_none:'Keine Drucker konfiguriert.', printers_empty_hint:'Noch kein Drucker eingerichtet.', nav_browser:'Browser', panel_browser_title:'Datei-Browser', store_empty:'Noch keine Dateien hochgeladen.', store_refresh:'↻ Aktualisieren', store_print:'▶ Drucken', store_delete_confirm:'Datei löschen?', store_print_confirm:'Datei drucken?', store_no_results:'Keine Dateien gefunden.', store_never:'noch nicht gedruckt', sf_all:'Alle',sf_ok:'✓ Erfolgreich',sf_err:'✗ Fehler',sf_new:'Neu', ss_date:'↓ Datum',ss_name:'A–Z Name',ss_dur:'⏱ Druckzeit' }; var LANG_EN={ header_status_standby:'Ready',header_status_printing:'Printing',header_status_complete:'Complete',header_status_error:'Error', kobra_free:'Ready',kobra_busy:'Busy',kobra_printing:'Printing',kobra_preheating:'Preheating',kobra_auto_leveling:'Auto Leveling',kobra_checking:'Checking',kobra_updated:'Updating',kobra_init:'Initializing',kobra_pausing:'Pausing...',kobra_paused:'Paused',kobra_resuming:'Resuming...',kobra_resumed:'Resumed',kobra_stopping:'Stopping...',kobra_stoped:'Stopped',kobra_finished:'Finished',kobra_failed:'Error',kobra_canceled:'Cancelled',kobra_offline:'Offline', nav_dashboard:'Dashboard',nav_print:'Print',nav_temps:'Temperatures',nav_motion:'Motion',nav_ams:'AMS',nav_extras:'Light / Fan',nav_console:'Console', card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera',lbl_elapsed:'Elapsed:',lbl_remaining:'Remaining:',lbl_slicer_time:'Slicer estimate:',lbl_layers:'Layer', speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport', lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload', card_ace_dry:'ACE Drying',ace_dry_dryer:'Dryer',ace_dry_status_off:'Status: Off',ace_dry_status_on:'Status: Active',ace_dry_status_remaining:'Remaining',ace_dry_humidity:'Humidity',ace_dry_current_temp:'Temperature',ace_dry_chart:'History (Temp/Humidity)',ace_dry_temp:'Temperature (°C)',ace_dry_duration:'Duration (min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto Refill',ace_dry_enable:'Enable Drying',ace_dry_temp_line:'Drying Temperature',ace_dry_time_line:'Drying Time',ace_dry_ui_pending:'(UI only, backend next)',ace_dry_dialog_title:'Dryer Temp/Time Settings',ace_dry_dialog_temp:'Temperature (30-80°C)',ace_dry_dialog_time:'Rem. Time (h:m:s)',ace_dry_dialog_confirm:'Confirm',ace_dry_dialog_cancel:'Cancel',ace_dry_dialog_save_restart:'Save & Restart',ace_dry_dialog_custom_name:'Custom Name', cam_placeholder:'📷 Camera not started',btn_cam_start:'▶ Camera',btn_cam_stop:'◼ Camera', btn_pause:'⏸ Pause',btn_resume:'▶ Resume',btn_cancel:'✕ Stop', label_nozzle:'Nozzle',label_bed:'Bed',label_fan:'🌀 Fan',label_light:'💡 Light',label_on_off:'On / Off',label_speed:'Speed', panel_print_title:'Print Control',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Resume',panel_print_btn_cancel:'✕ Cancel',panel_print_temps_live:'Temperatures (Live)', label_set:'Set',label_off:'Off', panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heated Bed',panel_temps_chart:'History (last 60 readings)',label_target_c:'Target:', panel_motion_xy:'XY Axes',panel_motion_z:'Z Axis',label_step:'Step size:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motors Off', panel_ams_title:'Filament',card_ams:'Filament',ams_no_data:'No AMS data received',label_slot:'Slot',ams_empty:'Empty', panel_extras_light:'Light',panel_extras_fan:'Fan',panel_extras_camera:'Camera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop', panel_console_title:'Event Log', log_light_on:'Light on',log_light_off:'Light off',log_fan:'Fan →',log_nozzle:'Nozzle →',log_bed:'Bed →',log_axis:'Axis',log_home:'Home',log_home_all:'Home All',log_cam_start:'Camera started:',log_cam_stop:'Camera stopped',log_poll_error:'Poll error:',log_error:'Error:', confirm_cancel:'Really cancel the print?', settings_title:'Settings',settings_connection:'Connection',settings_print:'Print Settings',settings_poll:'Poll Interval',settings_version:'Version', settings_save:'Save & Restart',settings_printer_name:'Printer Name',settings_printer_ip:'Printer IP',settings_mqtt_port:'MQTT Port', settings_username:'MQTT Username',settings_password:'MQTT Password',settings_device_id:'Device ID',settings_mode_id:'Mode ID',hint_ip_no_port:'IP address only, no port (e.g. 192.168.1.102)', settings_default_slot:'Default Slot (single color)',settings_slot_auto:'Auto (all loaded slots)',settings_auto_leveling:'Auto-Leveling before print', update_check:'Check for Updates',update_checking:'Checking...',update_available:'available',update_none:'Already up to date', update_apply:'Install Now',update_applying:'Downloading...',update_restarting:'Restarting...',update_error:'Error', btn_connect:'⚡ Connect',btn_disconnect:'✕ Disconnect', lbl_conn_error:'Connection error:', slot_edit_title:'Edit Slot',slot_edit_color:'Color',slot_edit_material:'Material', slot_edit_load:'⬇ Load',slot_edit_unload:'⬆ Unload', slot_edit_save:'💾 Save',slot_edit_custom:'e.g. PLA, PETG, ABS…', slot_edit_ok:'AMS Slot', log_dir_all:'All', file_ready_btn:'▶ Start Print', file_slots_btn:'🎨 Select Slots', file_cancel_btn:'✕ Cancel', nav_printers:'Printers', skip_title:'✂ Skip objects',skip_hint:'Uncheck objects you no longer want to print:', skip_btn_label:'Objects',skip_no_objects:'No objects in this print.', skip_already:'skipped',skip_select_at_least_one:'Please pick at least one object.', skip_sending:'Sending …',skip_success:'Objects will be skipped.', fd_objects_hint:'Skip objects (optional):', add_printer:'Add printer',apd_lbl_ip:'Printer IP',apd_lbl_name:'Name (optional)', apd_fetching:'Fetching data from printer…',apd_success:'Printer added, bridge restarting…',apd_err_ip:'Please enter an IP address', printers_remove:'Remove printer',printers_remove_confirm:'Remove printer "{name}"? The bridge will restart.', printers_active:'● active', printers_switch:'Switch →', printers_current:'Current printer', printers_loading:'Loading…', printers_none:'No printers configured.', printers_empty_hint:'No printer set up yet.', nav_browser:'Browser', panel_browser_title:'File Browser', store_empty:'No files uploaded yet.', store_refresh:'↻ Refresh', store_print:'▶ Print', store_delete_confirm:'Delete file?', store_print_confirm:'Print file?', store_no_results:'No files found.', store_never:'never printed', sf_all:'All',sf_ok:'✓ Completed',sf_err:'✗ Failed',sf_new:'New', ss_date:'↓ Date',ss_name:'A–Z Name',ss_dur:'⏱ Print time' }; // Multi-Printer: BASE_URL aus Pathname (/printer2 → andere Bridge-Instanz) var _printers=[]; var _activePrinter=null; (function(){ var path=window.location.pathname.replace(/\/+$/,''); var m=path.match(/^\/printer(\d+)$/); var idx=m?parseInt(m[1]):1; window._printerIndex=idx; })(); function _apiUrl(path){ if(_activePrinter&&_activePrinter.bridge_url){ return _activePrinter.bridge_url.replace(/\/+$/,'')+path; } return path; } function initPrinters(){ fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){ // immer lokale Instanz für Drucker-Liste _printers=d.result||[]; var idx=window._printerIndex||1; _activePrinter=_printers.find(function(p){return String(p.id)===String(idx)})||_printers[0]||null; renderPrinterDropdown(); }).catch(function(){}); } function renderPrinterDropdown(){ var wrap=document.getElementById('printer-dropdown-wrap'); var single=document.getElementById('h-pname-single'); var name=_printers.length===0?'–':(_activePrinter?(_activePrinter.name||'Kobra X'):'Kobra X'); var pname=document.getElementById('h-pname'); if(pname)pname.textContent=name; if(single)single.textContent=name; if(_printers.length>1){ if(wrap)wrap.style.display=''; if(single)single.style.display='none'; var menu=document.getElementById('printer-dropdown-menu'); if(menu){ menu.innerHTML=_printers.map(function(p){ var active=_activePrinter&&String(p.id)===String(_activePrinter.id); var num=p.id; return ''+ (active?'▶ ':'')+p.name+''; }).join(''); } } else { if(wrap)wrap.style.display='none'; if(single)single.style.display=''; } } function togglePrinterDropdown(){ var menu=document.getElementById('printer-dropdown-menu'); if(menu)menu.style.display=menu.style.display==='none'?'block':'none'; } document.addEventListener('click',function(e){ var wrap=document.getElementById('printer-dropdown-wrap'); if(wrap&&!wrap.contains(e.target)){ var menu=document.getElementById('printer-dropdown-menu'); if(menu)menu.style.display='none'; } }); var currentLang='de'; var T=LANG_DE; function toggleLang(){ currentLang=currentLang==='de'?'en':'de'; T=currentLang==='de'?LANG_DE:LANG_EN; localStorage.setItem('lang',currentLang); document.getElementById('lang-btn').textContent=currentLang==='de'?'EN':'DE'; document.documentElement.setAttribute('lang',currentLang); applyLang(); } function applyLang(){ ensureAceDryCards(); // Nav var nb=document.getElementById('nb-dashboard');if(nb)nb.querySelector('.nav-text').textContent=T.nav_dashboard; nb=document.getElementById('nb-console');if(nb)nb.querySelector('.nav-text').textContent=T.nav_console; nb=document.getElementById('nb-printers');if(nb)nb.querySelector('.nav-text').textContent=T.nav_printers; nb=document.getElementById('nb-store');if(nb)nb.querySelector('.nav-text').textContent=T.nav_browser; // Bottom nav var bnb=document.getElementById('bnb-dashboard');if(bnb)bnb.lastChild.textContent=T.nav_dashboard; bnb=document.getElementById('bnb-console');if(bnb)bnb.lastChild.textContent=T.nav_console; bnb=document.getElementById('bnb-printers');if(bnb)bnb.lastChild.textContent=T.nav_printers; bnb=document.getElementById('bnb-store');if(bnb)bnb.lastChild.textContent=T.nav_browser; // Browser panel setText('printers-panel-title','🖨 '+T.nav_printers); setText('add-printer-btn-label',T.add_printer); setText('apd-title',T.add_printer); setText('skip-title',T.skip_title); setText('skip-hint',T.skip_hint); setText('d-btn-skip-label',T.skip_btn_label); setText('fd-objects-hint',T.fd_objects_hint); setText('apd-lbl-ip',T.apd_lbl_ip); setText('apd-lbl-name',T.apd_lbl_name); setText('store-panel-title','🗂 '+T.panel_browser_title); var srb=document.getElementById('store-refresh-btn');if(srb)srb.textContent=T.store_refresh; setText('store-empty',T.store_empty); setText('sf-all',T.sf_all);setText('sf-ok',T.sf_ok);setText('sf-err',T.sf_err);setText('sf-new',T.sf_new); setText('ss-date',T.ss_date);setText('ss-name',T.ss_name);setText('ss-dur',T.ss_dur); // Dashboard card titles setText('d-card-progress',T.card_progress); setText('d-card-temps',T.card_temps); setText('d-card-lightfan',T.card_light_fan); setText('d-card-speed',T.card_speed); setText('d-card-cam',T.card_cam); setText('d-card-ams',T.panel_ams_title); setText('d-lbl-elapsed',T.lbl_elapsed); setText('d-lbl-remain',T.lbl_remaining); setText('d-slicer-label',T.lbl_slicer_time); setText('d-lbl-layers',T.lbl_layers); setText('d-lbl-light',T.lbl_light); setText('d-lbl-bed',T.label_bed); // Dashboard buttons setText('d-btn-pause',T.btn_pause); setText('d-btn-resume',T.btn_resume); setText('d-btn-cancel',T.btn_cancel); setText('cam-toggle-btn',camOn?T.btn_cam_stop:T.btn_cam_start); setText('cam-placeholder-txt',T.cam_placeholder); // Temp labels document.querySelectorAll('.lbl-set').forEach(e=>e.textContent=T.label_set); document.querySelectorAll('.lbl-off').forEach(e=>e.textContent=T.label_off); setText('d-chart-label',T.panel_temps_chart); // Axis labels setText('ptitle-motion-xy',T.panel_motion_xy); setText('ptitle-motion-z',T.panel_motion_z); document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z); document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy); document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all); document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors); document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step); document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':',''))); // Console setText('ptitle-console',T.panel_console_title); // Settings modal setText('modal-title-settings',T.settings_title); setText('modal-sec-connection',T.settings_connection); setText('modal-sec-print',T.settings_print); setText('modal-sec-poll',T.settings_poll); setText('modal-sec-version',T.settings_version); setText('btn-save-settings',T.settings_save); setText('lbl-printer-name',T.settings_printer_name); setText('lbl-printer-ip',T.settings_printer_ip); setText('lbl-mqtt-port',T.settings_mqtt_port); setText('lbl-username',T.settings_username); setText('lbl-password',T.settings_password); setText('lbl-device-id',T.settings_device_id); setText('lbl-mode-id',T.settings_mode_id); setText('lbl-default-slot',T.settings_default_slot); setText('opt-slot-auto',T.settings_slot_auto); setText('lbl-auto-leveling',T.settings_auto_leveling); setText('lbl-update-check',T.update_check); setText('lbl-update-apply',T.update_apply); // Speed buttons setText('d-spd-lbl-1',T.speed_silent.replace(/^\S+\s/,'')); setText('d-spd-lbl-2',T.speed_normal.replace(/^\S+\s/,'')); setText('d-spd-lbl-3',T.speed_sport.replace(/^\S+\s/,'')); // AMS feed/unload document.querySelectorAll('.lbl-feed').forEach(e=>e.textContent=T.lbl_feed); document.querySelectorAll('.lbl-unload').forEach(e=>e.textContent=T.lbl_unload); for(var i=0;i<4;i++){ setText('d-card-ace-dry-'+i,'ACE '+(i+1)+' - '+(T.ace_dry_dryer||'Dryer')); setText('d-ace-auto-refill-label-'+i,T.ace_dry_auto_refill||'Auto Refill'); setText('d-ace-drying-enable-label-'+i,T.ace_dry_enable||'Enable Drying'); setText('d-ace-dry-humidity-label-'+i,(T.ace_dry_humidity||'Humidity')+':'); setText('d-ace-dry-current-temp-label-'+i,(T.ace_dry_current_temp||'Current Temp')+':'); setText('d-ace-dry-target-label-'+i,(T.ace_dry_temp_line||'Drying Temperature')+':'); setText('d-ace-dry-time-label-'+i,(T.ace_dry_time_line||'Drying Time')+':'); setText('d-ace-dry-chart-label-'+i,T.ace_dry_chart||'History (Temp/Humidity)'); var adTemp=document.getElementById('ace-dry-temp-'+i);if(adTemp)adTemp.setAttribute('placeholder',T.ace_dry_temp); var adDur=document.getElementById('ace-dry-duration-'+i);if(adDur)adDur.setAttribute('placeholder',T.ace_dry_duration); } setText('ace-dry-dialog-title',T.ace_dry_dialog_title||'Dryer Temp/Time Settings'); setText('ace-dry-dialog-temp-label',T.ace_dry_dialog_temp||'Temperature (30-80°C)'); setText('ace-dry-dialog-time-label',T.ace_dry_dialog_time||'Rem. Time (h:m:s)'); setText('ace-dry-dialog-custom-name-label',T.ace_dry_dialog_custom_name||'Custom Name'); setText('ace-dry-dialog-cancel',T.ace_dry_dialog_cancel||'Cancel'); setText('ace-dry-dialog-confirm',T.ace_dry_dialog_confirm||'Confirm'); setText('ace-dry-dialog-reset-default',T.ace_dry_dialog_reset_default||'Reset to Default'); setText('ace-dry-dialog-save-preset',T.ace_dry_dialog_save_restart||'Save & Restart'); aceDryDialogSyncCustomButtonNames(); // conn-btn text (nur wenn nicht im Übergangszustand) updateConnBtn(); // Slot-Edit-Dialog setText('lbl-slot-color',T.slot_edit_color); setText('lbl-slot-material',T.slot_edit_material); setText('btn-slot-edit-save',T.slot_edit_save); updateSlotEditFeedButton(); var mi=document.getElementById('slot-edit-mat');if(mi)mi.setAttribute('placeholder',T.slot_edit_custom); setText('logdir-all',T.log_dir_all); setText('file-ready-btn',T.file_ready_btn); setText('file-slots-btn',T.file_slots_btn); setText('file-cancel-btn',T.file_cancel_btn); setText('file-cancel-btn',T.file_cancel_btn); } function setText(id,txt){var el=document.getElementById(id);if(el)el.textContent=txt;} function ensureAceDryCards(){ var grid=document.getElementById('d-ace-dry-grid'); if(!grid||grid.getAttribute('data-init')==='1')return; var html=''; for(var i=0;i<4;i++){ html+=''; } grid.innerHTML=html; grid.setAttribute('data-init','1'); } (function(){ var l=localStorage.getItem('lang')||'de'; currentLang=l;T=l==='de'?LANG_DE:LANG_EN; document.getElementById('lang-btn').textContent=l==='de'?'EN':'DE'; document.documentElement.setAttribute('lang',l); // defer until DOM ready window.addEventListener('DOMContentLoaded',function(){ applyLang(); // Kein Drucker konfiguriert? → direkt in den Drucker-Tab (zeigt "+ Drucker hinzufügen") fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){ if(!d.result||!d.result.length){showPanel('printers');loadPrinterTab();} }).catch(function(){}); }); })(); // ── Panel nav ── function showPanel(id){ document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active')); document.getElementById('panel-'+id).classList.add('active'); document.querySelectorAll('.nav-btn,.bnav-btn').forEach(b=>b.classList.remove('active')); var nb=document.getElementById('nb-'+id);if(nb)nb.classList.add('active'); var bnb=document.getElementById('bnb-'+id);if(bnb)bnb.classList.add('active'); currentPanel=id; } // ── Console log ── var consoleLogs=[]; var logAutoScroll=true; var logBadgeCount=0; var logDirFilter='all'; // 'all'|'rx'|'tx' var logTopicFilter=''; // '' = no topic filter function clog(msg,cls){ cls=cls||'msg-info'; var ts=new Date().toLocaleTimeString('de',{hour:'2-digit',minute:'2-digit',second:'2-digit'}); _appendLog({ts:ts,lvl:'',name:'ui',msg:msg},cls); } function _lvlCls(lvl){ if(lvl==='ERROR'||lvl==='CRITICAL')return'msg-err'; if(lvl==='WARNING')return'msg-warn'; if(lvl==='DEBUG')return'msg-info'; return'msg-ok'; } function _appendLog(entry,forceCls){ var cls=forceCls||_lvlCls(entry.lvl); var label=entry.name?'['+entry.name+'] ':''; consoleLogs.push({ts:entry.ts,msg:label+entry.msg,cls:cls}); if(consoleLogs.length>500)consoleLogs.shift(); // Badge wenn Tab nicht aktiv und Fehler/Warnungen if(currentPanel!=='console'&&(cls==='msg-err'||cls==='msg-warn')){ logBadgeCount++; var bc=logBadgeCount>99?'99+':logBadgeCount; ['log-badge','log-badge-bot'].forEach(function(id){var b=document.getElementById(id);if(b){b.style.display='inline';b.textContent=bc;}}); } renderLog(); } function setLogDir(dir){ logDirFilter=dir; document.querySelectorAll('.log-dir-btn').forEach(function(b){ b.style.background=b.id==='logdir-'+dir?'var(--accent)':'var(--raised)'; b.style.color=b.id==='logdir-'+dir?'#fff':'var(--txt2)'; }); renderLog(); } function setLogTopic(topic){ var inp=document.getElementById('log-filter'); var active=inp.value===topic; inp.value=active?'':topic; document.querySelectorAll('.log-topic-btn').forEach(function(b){ var on=!active&&b.getAttribute('data-topic')===topic; b.style.background=on?'var(--accent)':'var(--raised)'; b.style.color=on?'#fff':'var(--txt2)'; }); renderLog(); } function renderLog(){ var el=document.getElementById('console-log'); if(!el)return; var filter=(document.getElementById('log-filter')||{}).value||''; var fl=filter.toLowerCase(); var rows=consoleLogs.filter(function(l){ var m=l.msg; if(logDirFilter==='rx'&&!/ RX[ (]/.test(m))return false; if(logDirFilter==='tx'&&!/ TX[ (]/.test(m))return false; if(fl&&!m.toLowerCase().includes(fl))return false; return true; }); var savedScroll=logAutoScroll?null:el.scrollTop; el.innerHTML=rows.map(l=>`
${l.ts}${escHtml(l.msg)}
`).join(''); if(logAutoScroll)el.scrollTop=el.scrollHeight; else if(savedScroll!==null)el.scrollTop=savedScroll; } function onLogScroll(){ var el=document.getElementById('console-log'); if(!el)return; var atBottom=el.scrollHeight-el.scrollTop-el.clientHeight<30; if(!atBottom&&logAutoScroll){setAutoScroll(false);} } function toggleAutoScroll(){ setAutoScroll(!logAutoScroll); if(logAutoScroll){var el=document.getElementById('console-log');if(el)el.scrollTop=el.scrollHeight;} } function setAutoScroll(on){ logAutoScroll=on; var btn=document.getElementById('btn-autoscroll'); if(btn){btn.style.background=on?'var(--accent)':'var(--raised)';btn.style.color=on?'#fff':'var(--txt2)';} } function clearLogBadge(){ logBadgeCount=0; ['log-badge','log-badge-bot'].forEach(function(id){var b=document.getElementById(id);if(b)b.style.display='none';}); } function escHtml(s){return s.replace(/&/g,'&').replace(//g,'>');} // SSE server-log stream (function(){ function connect(){ var es=new EventSource('/api/log/stream'); es.onmessage=function(e){try{_appendLog(JSON.parse(e.data));}catch(_){}}; es.onerror=function(){es.close();setTimeout(connect,3000);}; } window.addEventListener('DOMContentLoaded',connect); })(); // ── Helpers ── function fmtTime(s){if(!s||s<0)return'–';var m=Math.floor(s/60),h=Math.floor(m/60);m%=60;return h>0?h+'h '+m+'m':m+'m'} function fmtHmsFromSec(total){ total=Math.max(0,parseInt(total||0,10)); var h=Math.floor(total/3600); var mm=Math.floor((total%3600)/60); var ss=total%60; return h+':'+String(mm).padStart(2,'0')+':'+String(ss).padStart(2,'0'); } function post(url,body){return fetch(_apiUrl(url),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})} function clamp(v,lo,hi){return Math.min(hi,Math.max(lo,v))} // ── Apply state to DOM ── function applyState(){ var s=S; _syncAceDryPresetsFromServer(s.ace_dry_presets); // connection error banner – nur wenn überhaupt ein Drucker konfiguriert ist var banner=document.getElementById('conn-error-banner'); if(banner){if(s.connection_error&&_printers.length>0){banner.textContent='⚠ '+(T.lbl_conn_error||'Connection error:')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}} var frb=document.getElementById('file-ready-banner'); if(frb){ if(s.file_ready&&s.print_state==='standby'){ document.getElementById('file-ready-name').textContent=s.file_ready; frb.style.display='flex'; }else{frb.style.display='none';} } // skip-button (mid-print) – nur sichtbar wenn aktuell gedruckt wird var skipBtn=document.getElementById('d-btn-skip'); if(skipBtn){ var printing=(s.print_state==='printing'||s.print_state==='paused'); skipBtn.style.display=printing?'':'none'; } // header var b=document.getElementById('h-badge'); b.className='hbadge '+s.print_state; document.getElementById('h-state').textContent=T['kobra_'+s.kobra_state]||s.kobra_state||T.header_status_standby; var _pn=_printers.length===0?'–':((_activePrinter&&_activePrinter.name)||s.printer_name); var _el=document.getElementById('h-pname');if(_el)_el.textContent=_pn; var _el2=document.getElementById('h-pname-single');if(_el2)_el2.textContent=_pn; var hv=document.getElementById('h-version');if(hv&&s.version)hv.textContent='v'+s.version; // temps var nt=document.getElementById('d-nt');if(nt)nt.textContent=s.nozzle_temp.toFixed(1); var ntt=document.getElementById('d-nt-t');if(ntt)ntt.textContent=s.nozzle_target.toFixed(0); var bt=document.getElementById('d-bt');if(bt)bt.textContent=s.bed_temp.toFixed(1); var btt=document.getElementById('d-bt-t');if(btt)btt.textContent=s.bed_target.toFixed(0); // temp bars (dashboard) var nb=document.getElementById('d-ntbar');if(nb)nb.style.width=clamp(s.nozzle_temp/300*100,0,100)+'%'; var bb=document.getElementById('d-btbar');if(bb)bb.style.width=clamp(s.bed_temp/120*100,0,100)+'%'; // progress var pct=Math.round(s.progress*100); var dpct=document.getElementById('d-pct');if(dpct)dpct.textContent=pct; var dpbar=document.getElementById('d-pbar');if(dpbar)dpbar.style.width=pct+'%'; var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'–'; var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers; var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration); var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'–'; var dslrow=document.getElementById('d-slicer-row'); var dsltime=document.getElementById('d-slicer-time'); if(dslrow&&dsltime){ if(s.slicer_time>0){dslrow.style.display='';dsltime.textContent=fmtTime(s.slicer_time);} else{dslrow.style.display='none';} } var fn=s.filename||'–'; var dfname=document.getElementById('d-fname');if(dfname){dfname.textContent=fn;dfname.title=fn}; var pfname=document.getElementById('p-fname');if(pfname){pfname.textContent=fn;pfname.title=fn}; var cfo=document.getElementById('cam-fname');if(cfo)cfo.textContent=fn!=='–'?fn:''; // thumbnail var thumb=document.getElementById('d-thumbnail'); if(thumb){ if(s.thumbnail){ thumb.src='data:image/png;base64,'+s.thumbnail; thumb.style.display='block'; } else { thumb.style.display='none'; thumb.src=''; } } // light/fan sync document.getElementById('d-light-toggle').checked=s.light_on; var dfan=document.getElementById('d-fan');if(dfan)dfan.value=s.fan_speed; var dfanval=document.getElementById('d-fan-val');if(dfanval)dfanval.textContent=s.fan_speed; // speed mode buttons var spdWidths={1:25,2:55,3:90}; [1,2,3].forEach(function(m){ var b=document.getElementById('d-spd-'+m); if(b) b.classList.toggle('spd-active', s.print_speed_mode===m); }); var spdBar=document.getElementById('d-spd-bar'); if(spdBar) spdBar.style.width=(spdWidths[s.print_speed_mode]||55)+'%'; var amsTitle=document.getElementById('d-card-ams'); if(amsTitle){ var baseTitle=T.card_ams||'Filament'; var modeMap={toolhead:'Toolhead',ace_direct:'ACE Direct',ace_hub:'ACE Hub'}; var modeTxt=modeMap[s.filament_mode]||''; amsTitle.textContent=modeTxt?(baseTitle+' - '+modeTxt):baseTitle; } ensureAceDryCards(); var dry=s.ace_drying||{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]}; var units=(dry.units||[]); var unitMap={}; units.forEach(function(u){var id=Number(u.id);if(id>=0&&id<=3)unitMap[id]=u;}); var aceMode=s.filament_mode==='ace_direct'||s.filament_mode==='ace_hub'; var detected=(s.ace_units||[]).filter(function(id){return id>=0&&id<=3;}); if(!detected.length){ Object.keys(unitMap).forEach(function(k){detected.push(Number(k));}); } if(!detected.length){ (s.ams_slots||[]).forEach(function(sl){var id=Number(sl.box_id);if(id>=0&&id<=3&&detected.indexOf(id)<0)detected.push(id);}); } detected.sort(function(a,b){return a-b;}); var aceWrap=document.getElementById('d-ace-dry-wrap'); if(aceWrap)aceWrap.style.display=(aceMode&&detected.length)?'contents':'none'; for(var i=0;i<4;i++){ var card=document.getElementById('d-ace-dry-card-'+i); if(!card)continue; var show=aceMode&&detected.indexOf(i)>=0; card.style.display=show?'':'none'; if(!show)continue; var ud=unitMap[i]||dry; var refillToggle=document.getElementById('ace-auto-refill-toggle-'+i); var autoFeedMap=s.ace_auto_feed||{}; if(refillToggle&&!_aceAutoFeedPending[i]){ var afVal=autoFeedMap.hasOwnProperty(String(i))?Number(autoFeedMap[String(i)]):(_aceAutoRefillGet(i)?1:0); refillToggle.checked=afVal===1; } var dryToggle=document.getElementById('ace-dry-enable-toggle-'+i); if(dryToggle)dryToggle.checked=Number(ud.status||0)>0; var hh=document.getElementById('d-ace-dry-humidity-'+i); if(hh){ var hv=(ud.humidity===null||ud.humidity===undefined||ud.humidity==='')?null:Number(ud.humidity); hh.textContent=(hv===null||Number.isNaN(hv))?'-':(Math.round(hv)+'%'); } var ht=document.getElementById('d-ace-dry-current-temp-'+i); if(ht){ var ct=(ud.current_temp===null||ud.current_temp===undefined||ud.current_temp==='')?null:Number(ud.current_temp); ht.textContent=(ct===null||Number.isNaN(ct))?'-':(ct.toFixed(1)+'°C'); } var prof=_aceDryProfileGet(i); var useSec=(Number(ud.status||0)>0&&Number(ud.remain_time)>0) ?Number(ud.remain_time||0)*60 :prof.duration_sec; var showTemp=(Number(ud.status||0)>0&&Number(ud.target_temp)>0)?Number(ud.target_temp):prof.temp; var dryTempEl=document.getElementById('d-ace-dry-target-'+i); if(dryTempEl)dryTempEl.textContent=showTemp+'°C'; var dryTimeEl=document.getElementById('d-ace-dry-time-'+i); if(dryTimeEl)dryTimeEl.textContent=fmtHmsFromSec(useSec); } // AMS if(s.ams_slots&&s.ams_slots.length){ window._amsSlots=s.ams_slots; // Group by box_id (-1=Toolhead, 0=ACE 1, 1=ACE 2, ...) var boxMap={}; s.ams_slots.forEach(function(slot,i){ var bid=slot.box_id!=null?slot.box_id:-1; if(!boxMap[bid])boxMap[bid]=[]; boxMap[bid].push({slot:slot,arrIdx:i}); }); var boxIds=Object.keys(boxMap).map(Number).sort(function(a,b){return a-b}); var acePresent=boxIds.some(function(b){return b>=0;}); var html=''; boxIds.forEach(function(bid){ var entries=boxMap[bid]; var label=bid===-1 ?(acePresent?'Toolhead (Slots 1–3)':'Toolhead') :('ACE '+(bid+1)); html+='
' +'
'+label+'
' +'
'; entries.forEach(function(e){ var slot=e.slot;var i=e.arrIdx; var empty=slot.status!==5; var rgb=empty?[80,80,80]:(Array.isArray(slot.color)?slot.color:[128,128,128]); var col='rgb('+rgb[0]+','+rgb[1]+','+rgb[2]+')'; var globalIdx=slot.global_index!=null?slot.global_index:i; var active=slot.status===1||slot.active; var loaded=(s.ams_loaded_slot!=null&&s.ams_loaded_slot>=0&&globalIdx===s.ams_loaded_slot); var activity=(slot.activity||''); var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'–'); var slotLabel='Slot '+(globalIdx+1); html+='
' +'
' +'
'+(empty?'–':(slot.type||slot.material_type||'–'))+'
' +'
'+slotLabel+'
' +'
'+pct+'
' +'
' +'
'; }); if(bid===-1&&acePresent){ html+='
' +'
ACE
' +'
'; } html+='
'; }); document.getElementById('ams-slots').innerHTML=html; } // camera overlay var co=document.getElementById('cam-overlay'); if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none'; // auto-start camera during print if(s.print_state==='printing'&&!camOn&&s.camera_url){ camStart(); } updateConnBtn(); } function updateConnBtn(){ var btn=document.getElementById('conn-btn'); if(!btn)return; var offline=S.kobra_state==='offline'; if(offline){ btn.className='conn-btn disconnected'; btn.textContent=T.btn_connect||'⚡ Verbinden'; } else { btn.className='conn-btn connected'; btn.textContent=T.btn_disconnect||'✕ Trennen'; } } function toggleConnection(){ var btn=document.getElementById('conn-btn'); var offline=S.kobra_state==='offline'; btn.disabled=true; btn.textContent='…'; var url=offline?'/api/connect':'/api/disconnect'; post(url,{}).then(function(r){return r.json()}).then(function(r){ btn.disabled=false; if(r.error)addLog('Error: '+r.error); }).catch(function(){btn.disabled=false;}); } // ── Temp history + chart ── function updateHistory(){ tempHistory.n.push(S.nozzle_temp); tempHistory.b.push(S.bed_temp); if(tempHistory.n.length>60)tempHistory.n.shift(); if(tempHistory.b.length>60)tempHistory.b.shift(); drawChart('d-chart',tempHistory,[{data:tempHistory.n,color:'#00c8ff',max:300},{data:tempHistory.b,color:'#ff6b35',max:120}]); } function drawChart(id,_,series){ var canvas=document.getElementById(id);if(!canvas)return; var ctx=canvas.getContext('2d'); var W=canvas.offsetWidth*window.devicePixelRatio||canvas.width; var H=canvas.offsetHeight*window.devicePixelRatio||canvas.height; canvas.width=W;canvas.height=H; ctx.clearRect(0,0,W,H); series.forEach(function(s){ var data=s.data;if(!data.length)return; var max=s.max; ctx.beginPath();ctx.strokeStyle=s.color;ctx.lineWidth=2;ctx.lineJoin='round'; data.forEach(function(v,i){ var x=i/(Math.max(data.length-1,1))*(W-4)+2; var y=H-4-(v/max)*(H-8); if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y); }); ctx.stroke(); }); } // ── Settings Modal ── var _updateTag=''; var _updateUrl=''; function openSettings(){ // Titel mit aktivem Drucker-Namen aktualisieren var pname=_activePrinter&&_activePrinter.name?_activePrinter.name:null; var title=document.getElementById('modal-title-settings'); if(title)title.textContent=T.settings_title+(pname?' – '+pname:''); fetch(_apiUrl('/api/settings')).then(function(r){return r.json()}).then(function(d){ document.getElementById('s-printer-name').value=d.printer_name||''; document.getElementById('s-printer-ip').value=d.printer_ip||''; document.getElementById('s-mqtt-port').value=d.mqtt_port||9883; document.getElementById('s-username').value=d.username||''; document.getElementById('s-password').value=d.password||''; document.getElementById('s-device-id').value=d.device_id||''; document.getElementById('s-mode-id').value=d.mode_id||''; document.getElementById('s-default-slot').value=d.default_ams_slot||'auto'; document.getElementById('s-auto-leveling').checked=(d.auto_leveling===undefined?true:!!d.auto_leveling); }); var v=localStorage.getItem('pollInterval')||'2000'; document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')}); var pb=document.getElementById('poll-'+Math.round(parseInt(v)/1000)); if(pb)pb.classList.add('active'); document.getElementById('s-version-label').textContent='v'+('__VERSION__'||'?'); document.getElementById('update-status').textContent=''; document.getElementById('btn-update-apply').style.display='none'; var cl=document.getElementById('update-changelog');if(cl)cl.style.display='none'; _updateTag='';_updateUrl=''; document.getElementById('settings-modal').classList.add('open'); } function closeSettings(){ document.getElementById('settings-modal').classList.remove('open'); } // ── AMS Slot Edit ── var _slotEditIndex=-1; var _slotEditLoaded=false; var _MAT_PRESETS=['PLA','PETG','ABS','ASA','TPU','PA','PC','HIPS']; function updateSlotEditFeedButton(){ var btn=document.getElementById('btn-slot-edit-feed'); if(!btn)return; if(_slotEditIndex<0){ btn.style.display='none'; return; } btn.style.display=''; btn.textContent=_slotEditLoaded?(T.slot_edit_unload||'⬆ Unload'):(T.slot_edit_load||'⬇ Load'); } function openSlotEdit(i){ var slot=(window._amsSlots||[])[i]||{}; var globalIdx=slot.global_index!=null?slot.global_index:(slot.index!=null?slot.index:i); _slotEditIndex=globalIdx; _slotEditLoaded=(S.ams_loaded_slot!=null&&S.ams_loaded_slot===globalIdx); document.getElementById('slot-edit-title').textContent=T.slot_edit_title+' '+(globalIdx+1); var rgb=Array.isArray(slot.color)?slot.color:[128,128,128]; var hex='#'+rgb.map(function(v){return('0'+Math.min(255,v).toString(16)).slice(-2)}).join(''); var ci=document.getElementById('slot-edit-color'); ci.value=hex; document.getElementById('slot-edit-preview').style.background=hex; var mat=(slot.type||'PLA').toUpperCase(); document.getElementById('slot-edit-mat').value=mat; var btns=document.getElementById('slot-mat-btns'); btns.innerHTML=_MAT_PRESETS.map(function(m){ return ''; }).join(''); updateSlotEditFeedButton(); document.getElementById('slot-edit-modal').classList.add('open'); } function closeSlotEdit(){ _slotEditIndex=-1; document.getElementById('slot-edit-modal').classList.remove('open'); } function slotEditFeed(){ if(_slotEditIndex<0)return; var type=_slotEditLoaded?2:1; amsFeed(type,_slotEditIndex) .then(function(){ _slotEditLoaded=!_slotEditLoaded; updateSlotEditFeedButton(); poll(); }) .catch(function(){}); } function startReadyFile(){ var btn=document.getElementById('file-ready-btn'); if(btn){btn.disabled=true;btn.textContent='…';} post('/printer/print/start',{filename:S.file_ready}) .then(function(r){return r.json();}) .then(function(r){ document.getElementById('file-ready-banner').style.display='none'; if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} }) .catch(function(e){ clog((T.log_error||'Error:')+' '+e,'msg-err'); if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} }); } function cancelReadyFile(){ post('/api/file_ready/clear',{}) .then(function(){document.getElementById('file-ready-banner').style.display='none';}); } function selectMatPreset(m){ document.getElementById('slot-edit-mat').value=m; highlightMatBtn(m); } function highlightMatBtn(val){ document.querySelectorAll('.mat-preset-btn').forEach(function(b){ var on=b.getAttribute('data-mat')===val.toUpperCase(); b.style.background=on?'var(--accent)':'var(--raised)'; b.style.color=on?'#fff':'var(--txt2)'; }); } function hexToRgb(hex){ var r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16); return[r,g,b]; } function saveSlotEdit(){ var hex=document.getElementById('slot-edit-color').value; var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA'; var color=hexToRgb(hex); post('/api/ams/set_slot',{index:_slotEditIndex,type:mat,color:color}) .then(function(r){return r.json();}) .then(function(r){ closeSlotEdit(); clog((T.slot_edit_ok||'AMS Slot')+' '+(_slotEditIndex+1)+': '+mat+' '+hex,'msg-ok'); }) .catch(function(e){clog('Fehler: '+e,'msg-err');}); } document.addEventListener('DOMContentLoaded',function(){ document.getElementById('s-printer-ip').addEventListener('input',function(){ var hint=document.getElementById('lbl-ip-hint'); if(this.value.includes(':')){hint.textContent=T.hint_ip_no_port;hint.style.display='block';} else{hint.style.display='none';} }); }); function setPoll(ms){ document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')}); var id='poll-'+Math.round(ms/1000); var pb=document.getElementById(id);if(pb)pb.classList.add('active'); localStorage.setItem('pollInterval',ms); clearInterval(pollTimer); pollTimer=setInterval(poll,ms); } function saveSettings(){ var btn=document.getElementById('btn-save-settings'); btn.disabled=true;btn.textContent='…'; post('/api/settings',{ printer_name: document.getElementById('s-printer-name').value, printer_ip: document.getElementById('s-printer-ip').value, mqtt_port: parseInt(document.getElementById('s-mqtt-port').value)||9883, username: document.getElementById('s-username').value, password: document.getElementById('s-password').value, device_id: document.getElementById('s-device-id').value, mode_id: document.getElementById('s-mode-id').value, default_ams_slot: document.getElementById('s-default-slot').value, auto_leveling: document.getElementById('s-auto-leveling').checked?1:0, }).then(function(){ btn.textContent=T.update_restarting; setTimeout(function(){ btn.disabled=false; setText('btn-save-settings',T.settings_save); closeSettings(); poll(); },4000); }).catch(function(e){ btn.disabled=false;setText('btn-save-settings',T.settings_save); clog('Settings-Fehler: '+e,'msg-err'); }); } function checkUpdate(){ var sb=document.getElementById('update-status'); sb.textContent=T.update_checking; document.getElementById('btn-update-apply').style.display='none'; _updateTag='';_updateUrl=''; fetch(_apiUrl('/api/update/check')).then(function(r){return r.json()}).then(function(d){ if(d.error){sb.textContent=T.update_error+': '+d.error;return;} var cl=document.getElementById('update-changelog'); if(d.changelog&&d.changelog.trim()){cl.textContent=d.changelog;cl.style.display='block';} else{cl.style.display='none';} if(d.update_available){ sb.textContent='v'+d.latest+' '+T.update_available; sb.style.color='var(--ok)'; _updateTag=d.tag;_updateUrl=d.download_url; document.getElementById('btn-update-apply').style.display='inline-block'; } else { sb.textContent=T.update_none; sb.style.color='var(--txt2)'; } }).catch(function(e){sb.textContent=T.update_error+': '+e;}); } function applyUpdate(){ if(!_updateUrl)return; var sb=document.getElementById('update-status'); var btn=document.getElementById('btn-update-apply'); btn.disabled=true;sb.textContent=T.update_applying; post('/api/update/apply',{download_url:_updateUrl,tag:_updateTag}).then(function(){ sb.textContent=T.update_restarting; closeSettings(); setTimeout(function(){poll();},5000); }).catch(function(e){ btn.disabled=false;sb.textContent=T.update_error+': '+e; }); } // ── Poll ── async function poll(){ try{ var r=await fetch(_apiUrl('/api/state')); if(!r.ok)return; var d=await r.json(); Object.assign(S,d); applyState(); updateHistory(); }catch(e){clog(T.log_poll_error+' '+e,'msg-err')} } var pollTimer; (function(){ var ms=parseInt(localStorage.getItem('pollInterval')||'2000'); initPrinters(); poll();pollTimer=setInterval(poll,ms); })(); // ── Print actions ── function printAction(a){ post('/printer/print/'+a,{}).then(function(){clog('Druck: '+a,'msg-ok');poll()}) .catch(function(e){clog('Fehler: '+e,'msg-err')}); } function confirmCancel(){if(confirm('Druck wirklich abbrechen?'))printAction('cancel')} // ── Axis motion ── // axis codes: 0=X, 1=Y, 2=Z // move_type 1=relative, distance positive/negative function getStep(){return currentStep} function setStep(btn,v){ currentStep=v; document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); document.getElementById('step-display').textContent=v; } function move(axis,dir,dist){ // axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z var axisMap={0:1,1:2,2:3}; post('/api/axis',{axis:axisMap[axis],move_type:1,distance:dir*dist}) .then(function(){clog('Achse '+(axis===0?'X':axis===1?'Y':'Z')+' '+(dir>0?'+':'')+dir*dist+'mm','msg-ok')}) .catch(function(e){clog('Achse-Fehler: '+e,'msg-err')}); } function homeAll(){ post('/api/axis',{axis:5,move_type:2,distance:0}) .then(function(){clog('Home All','msg-ok')}) .catch(function(e){clog('Home-Fehler: '+e,'msg-err')}); } function homeXY(){ post('/api/axis',{axis:4,move_type:2,distance:0}) .then(function(){clog('Home XY','msg-ok')}) .catch(function(e){clog('Home-Fehler: '+e,'msg-err')}); } function homeZ(){ post('/api/axis',{axis:3,move_type:2,distance:0}) .then(function(){clog('Home Z','msg-ok')}) .catch(function(e){clog('Home-Fehler: '+e,'msg-err')}); } function disableMotors(){ post('/api/axis',{action:'turnOff'}) .then(function(){clog('Motors Off','msg-ok')}) .catch(function(e){clog('Motors-Fehler: '+e,'msg-err')}); } // ── Temperature ── function setNozzle(){ var v=parseFloat(document.getElementById('p-nozzle-inp').value||0); post('/api/temperature',{nozzle:v,bed:S.bed_target}) .then(function(){clog('Nozzle → '+v+'°C','msg-ok')}) .catch(function(e){clog('Temp-Fehler: '+e,'msg-err')}); } function setBed(){ var v=parseFloat(document.getElementById('p-bed-inp').value||0); post('/api/temperature',{nozzle:S.nozzle_target,bed:v}) .then(function(){clog(T.label_bed+' → '+v+'°C','msg-ok')}) .catch(function(e){clog('Temp-Fehler: '+e,'msg-err')}); } // ── Light ── function setLight(){ var on=document.getElementById('d-light-toggle').checked; post('/api/light',{on:on,brightness:80}) .then(function(){clog('Licht '+(on?'an, '+br+'%':'aus'),'msg-ok')}) .catch(function(e){clog('Licht-Fehler: '+e,'msg-err')}); } // ── Print Speed ── function setSpeed(mode){ S.print_speed_mode=mode; [1,2,3].forEach(function(m){ var b=document.getElementById('d-spd-'+m); if(b) b.classList.toggle('spd-active',m===mode); }); post('/api/speed',{mode:mode}) .catch(function(e){clog('Speed-Fehler: '+e,'msg-err')}); } // ── Fan ── function setFan(){ var v=parseInt(document.getElementById('d-fan').value); document.getElementById('d-fan-val').textContent=v; post('/api/fan',{speed:v}) .then(function(){clog('Lüfter → '+v+'%','msg-ok')}) .catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')}); } function quickFan(v){ document.getElementById('d-fan').value=v; document.getElementById('d-fan-val').textContent=v; post('/api/fan',{speed:v}) .then(function(){clog('Lüfter → '+v+'%','msg-ok')}) .catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')}); } // ── AMS ── function amsFeed(type,slotIndex){ var globalIdx; if(typeof slotIndex==='number'&&slotIndex>=0){ globalIdx=slotIndex; }else{ var i=parseInt(document.getElementById('ams-slot-sel').value); var slot=(window._amsSlots||[])[i]||{}; globalIdx=slot.global_index!=null?slot.global_index:i; } return post('/api/ams/feed',{slot_index:globalIdx,type:type}) .then(function(){clog((type===1?T.lbl_feed:T.lbl_unload)+' Slot '+(globalIdx+1),'msg-ok')}) .catch(function(e){clog('AMS-Fehler: '+e,'msg-err');throw e;}); } // ── Camera ── function camStart(){ var img=document.getElementById('cam-img'); var ph=document.getElementById('cam-placeholder'); var sp=document.getElementById('cam-spinner'); ph.style.display='none'; img.style.display='none'; sp.style.display='block'; post('/api/camera/start',{}).then(function(){ img.onerror=function(){ sp.style.display='none'; img.style.display='none'; ph.style.display='flex'; camOn=false; document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera'; clog((T.log_error||'Fehler:')+' Stream nicht verfügbar','msg-err'); }; img.src='/api/camera/stream?t='+Date.now(); camOn=true; document.getElementById('cam-toggle-btn').textContent=T.btn_cam_stop||'◼ Kamera'; clog((T.log_cam_start||'Kamera gestartet'),'msg-ok'); // MJPEG liefert kein onload – Spinner nach kurzem Timeout ausblenden setTimeout(function(){ sp.style.display='none'; img.style.display='block'; },1200); }).catch(function(e){ sp.style.display='none'; ph.style.display='flex'; clog((T.log_error||'Fehler:')+' '+e,'msg-err'); }); } function camStop(){ var img=document.getElementById('cam-img'); post('/api/camera/stop',{}).catch(function(){}); img.src=''; img.style.display='none'; document.getElementById('cam-placeholder').style.display='flex'; camOn=false; document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera'; clog((T.log_cam_stop||'Kamera gestoppt'),'msg-ok'); } function aceDryStart(aceId){ aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; var prof=_aceDryProfileGet(aceId); var t=parseInt(prof.temp,10); var d=_aceDryDurationMinFromSec(prof.duration_sec); t=Math.max(30,Math.min(80,t)); d=Math.max(10,Math.min(1440,d)); return post('/api/ace/dry',{action:'start',target_temp:t,duration:d,ace_id:aceId}) .then(function(r){return r.json();}) .then(function(r){ if(r.error){throw new Error(r.error);} clog('ACE '+(aceId+1)+' - '+(T.ace_dry_dryer||'Dryer')+': '+(T.ace_dry_start||'start')+' ('+t+'°C, '+d+' min)','msg-ok'); poll(); }) .catch(function(e){clog('ACE-Fehler: '+e,'msg-err');}); } var _aceAutoFeedPending={}; function aceAutoRefillToggle(aceId){ aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; var on=!!((document.getElementById('ace-auto-refill-toggle-'+aceId)||{}).checked); _aceAutoFeedPending[aceId]=true; fetch('/api/ace/auto_feed',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ace_id:aceId,on:on?1:0})}) .then(function(r){return r.json();}) .then(function(d){ delete _aceAutoFeedPending[aceId]; if(d.error){clog('Auto Refill error: '+d.error,'msg-err');var t=document.getElementById('ace-auto-refill-toggle-'+aceId);if(t)t.checked=!on;return;} clog('ACE '+(aceId+1)+' - '+(T.ace_dry_auto_refill||'Auto Refill')+': '+(on?'ON':'OFF'),'msg-ok'); }) .catch(function(e){delete _aceAutoFeedPending[aceId];clog('Auto Refill error: '+e,'msg-err');var t=document.getElementById('ace-auto-refill-toggle-'+aceId);if(t)t.checked=!on;}); } function openAceDryDialog(aceId){ aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; _aceDryDialogAceId=aceId; _syncAceDryPresetsFromServer(S.ace_dry_presets); _aceDryDialogPresetOriginals=JSON.parse(JSON.stringify(ACE_DRY_PRESETS)); aceDryDialogSyncCustomButtonNames(); var hasStored=Object.prototype.hasOwnProperty.call(aceDryProfiles,String(aceId)); var prof=_aceDryProfileGet(aceId); if(hasStored&&prof.preset&&ACE_DRY_PRESETS[prof.preset]){ aceDryDialogPreset(prof.preset); }else if(hasStored){ var sec=prof.duration_sec; document.getElementById('ace-dry-dialog-temp').value=prof.temp; document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600); document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60); document.getElementById('ace-dry-dialog-s').value=sec%60; aceDryDialogHighlightPreset(''); }else{ aceDryDialogPreset('pla'); } aceDryDialogUpdateSaveButton(); aceDryDialogUpdateResetButton(); var sb=document.getElementById('ace-dry-dialog-save-preset'); if(sb){sb.disabled=false;sb.textContent=T.ace_dry_dialog_save_restart||'Save & Restart';} document.getElementById('ace-dry-dialog').classList.add('open'); } function closeAceDryDialog(){ _aceDryDialogAceId=-1; _aceDryDialogPresetOriginals={}; var sb=document.getElementById('ace-dry-dialog-save-preset'); if(sb)sb.style.display='none'; var rb=document.getElementById('ace-dry-dialog-reset-default'); if(rb)rb.style.display='none'; document.getElementById('ace-dry-dialog').classList.remove('open'); } function aceDryDialogIsCustomPreset(key){ return /^custom_[123]$/.test(String(key||'')); } function aceDryDialogSyncCustomButtonNames(){ ['custom_1','custom_2','custom_3'].forEach(function(k){ var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+k+'"]'); if(b)b.textContent=(ACE_DRY_PRESETS[k]&&ACE_DRY_PRESETS[k].name)||('Custom '+k.slice(-1)); }); } function aceDryDialogUpdateCustomNameUi(){ var row=document.getElementById('ace-dry-dialog-custom-name-row'); var input=document.getElementById('ace-dry-dialog-custom-name'); if(!row||!input)return; if(!aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)){ row.style.display='none'; return; } row.style.display='flex'; input.value=(ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||''; } function aceDryDialogCurrentValues(){ var t=parseInt(document.getElementById('ace-dry-dialog-temp').value||45,10); var h=parseInt(document.getElementById('ace-dry-dialog-h').value||0,10); var m=parseInt(document.getElementById('ace-dry-dialog-m').value||0,10); var s=parseInt(document.getElementById('ace-dry-dialog-s').value||0,10); t=Math.max(30,Math.min(80,t)); h=Math.max(0,Math.min(24,h)); m=Math.max(0,Math.min(59,m)); s=Math.max(0,Math.min(59,s)); var totalSec=(h*3600)+(m*60)+s; totalSec=Math.max(10*60,Math.min(24*3600,totalSec)); return {temp:t,duration_sec:totalSec}; } function aceDryDialogUpdateSaveButton(){ var btn=document.getElementById('ace-dry-dialog-save-preset'); if(!btn)return; var key=_aceDryDialogPresetKey||''; if(!key||!ACE_DRY_PRESETS[key]){btn.style.display='none';return;} var p=_aceDryDialogPresetOriginals[key]||ACE_DRY_PRESETS[key]; var cur=aceDryDialogCurrentValues(); var changed=(cur.temp!==Number(p.temp)||cur.duration_sec!==Number(p.duration_sec)); if(aceDryDialogIsCustomPreset(key)){ var nameInp=document.getElementById('ace-dry-dialog-custom-name'); var n=((nameInp&&nameInp.value)||'').trim(); var old=(p&&p.name?String(p.name):('Custom '+key.slice(-1))).trim(); if((n||old)!==old)changed=true; } btn.style.display=changed?'':'none'; } function aceDryDialogUpdateResetButton(){ var btn=document.getElementById('ace-dry-dialog-reset-default'); if(!btn)return; var key=_aceDryDialogPresetKey||''; var d=ACE_DRY_PRESET_DEFAULTS[key]; if(!key||!d){btn.style.display='none';return;} var cur=aceDryDialogCurrentValues(); var changed=(cur.temp!==Number(d.temp)||cur.duration_sec!==Number(d.duration_sec)); btn.style.display=changed?'':'none'; } function aceDryDialogInputsChanged(){ if(aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)){ var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+_aceDryDialogPresetKey+'"]'); var i=document.getElementById('ace-dry-dialog-custom-name'); if(b&&i){ var t=(i.value||'').trim(); b.textContent=t||((ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||('Custom '+_aceDryDialogPresetKey.slice(-1))); } } aceDryDialogUpdateSaveButton(); aceDryDialogUpdateResetButton(); } function aceDryDialogHighlightPreset(presetKey){ _aceDryDialogPresetKey=presetKey||''; document.querySelectorAll('.ace-dry-preset-btn').forEach(function(btn){ var on=(btn.getAttribute('data-preset')===presetKey); btn.style.background=on?'var(--accent)':'var(--raised)'; btn.style.color=on?'#fff':'var(--txt2)'; btn.style.borderColor=on?'var(--accent)':'var(--border)'; }); aceDryDialogUpdateCustomNameUi(); } function aceDryDialogPreset(presetKey){ var p=ACE_DRY_PRESETS[presetKey]; if(!p)return; var sec=p.duration_sec; document.getElementById('ace-dry-dialog-temp').value=p.temp; document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600); document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60); document.getElementById('ace-dry-dialog-s').value=sec%60; aceDryDialogHighlightPreset(presetKey); aceDryDialogSyncCustomButtonNames(); aceDryDialogUpdateSaveButton(); aceDryDialogUpdateResetButton(); } function resetAceDryPresetToDefault(){ var key=_aceDryDialogPresetKey||''; var d=ACE_DRY_PRESET_DEFAULTS[key]; if(!key||!d)return; var sec=Number(d.duration_sec)||0; document.getElementById('ace-dry-dialog-temp').value=Number(d.temp)||45; document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600); document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60); document.getElementById('ace-dry-dialog-s').value=sec%60; aceDryDialogInputsChanged(); } function saveAceDryPresetAndRestart(){ var key=_aceDryDialogPresetKey||''; var btn=document.getElementById('ace-dry-dialog-save-preset'); if(!key||!ACE_DRY_PRESETS[key]||!btn)return; var cur=aceDryDialogCurrentValues(); if(!ACE_DRY_PRESETS[key])ACE_DRY_PRESETS[key]={}; ACE_DRY_PRESETS[key].temp=cur.temp; ACE_DRY_PRESETS[key].duration_sec=cur.duration_sec; if(aceDryDialogIsCustomPreset(key)){ var nameInp=document.getElementById('ace-dry-dialog-custom-name'); var nm=((nameInp&&nameInp.value)||'').trim(); ACE_DRY_PRESETS[key].name=nm||('Custom '+key.slice(-1)); } btn.disabled=true; btn.textContent='…'; fetch(_apiUrl('/api/settings')).then(function(r){return r.json();}).then(function(d){ d.ace_dry_presets={ pla:{temp:ACE_DRY_PRESETS.pla.temp,duration_sec:ACE_DRY_PRESETS.pla.duration_sec}, pla_plus:{temp:ACE_DRY_PRESETS.pla_plus.temp,duration_sec:ACE_DRY_PRESETS.pla_plus.duration_sec}, petg:{temp:ACE_DRY_PRESETS.petg.temp,duration_sec:ACE_DRY_PRESETS.petg.duration_sec}, tpu:{temp:ACE_DRY_PRESETS.tpu.temp,duration_sec:ACE_DRY_PRESETS.tpu.duration_sec}, abs_asa:{temp:ACE_DRY_PRESETS.abs_asa.temp,duration_sec:ACE_DRY_PRESETS.abs_asa.duration_sec}, pa_pc:{temp:ACE_DRY_PRESETS.pa_pc.temp,duration_sec:ACE_DRY_PRESETS.pa_pc.duration_sec}, custom_1:{name:ACE_DRY_PRESETS.custom_1.name,temp:ACE_DRY_PRESETS.custom_1.temp,duration_sec:ACE_DRY_PRESETS.custom_1.duration_sec}, custom_2:{name:ACE_DRY_PRESETS.custom_2.name,temp:ACE_DRY_PRESETS.custom_2.temp,duration_sec:ACE_DRY_PRESETS.custom_2.duration_sec}, custom_3:{name:ACE_DRY_PRESETS.custom_3.name,temp:ACE_DRY_PRESETS.custom_3.temp,duration_sec:ACE_DRY_PRESETS.custom_3.duration_sec} }; return post('/api/settings',d); }).then(function(){ clog('ACE preset '+key+' '+(T.settings_save||'Save & Restart'),'msg-ok'); closeAceDryDialog(); }).catch(function(e){ btn.disabled=false; btn.textContent=T.ace_dry_dialog_save_restart||'Save & Restart'; clog('ACE-Preset Fehler: '+e,'msg-err'); }); } function confirmAceDryDialog(){ if(_aceDryDialogAceId<0)return; var t=parseInt(document.getElementById('ace-dry-dialog-temp').value||45,10); var h=parseInt(document.getElementById('ace-dry-dialog-h').value||0,10); var m=parseInt(document.getElementById('ace-dry-dialog-m').value||0,10); var s=parseInt(document.getElementById('ace-dry-dialog-s').value||0,10); t=Math.max(30,Math.min(80,t)); h=Math.max(0,Math.min(24,h)); m=Math.max(0,Math.min(59,m)); s=Math.max(0,Math.min(59,s)); var totalSec=(h*3600)+(m*60)+s; totalSec=Math.max(10*60,Math.min(24*3600,totalSec)); var preset=_aceDryDialogPresetKey||''; _aceDryProfileSet(_aceDryDialogAceId,t,totalSec,preset); closeAceDryDialog(); applyState(); } function aceDryToggle(aceId,on){ if(on)return aceDryStart(aceId); return aceDryStop(aceId); } function toggleCam(){if(camOn)camStop();else camStart()} function aceDryStop(aceId){ aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; return post('/api/ace/dry',{action:'stop',ace_id:aceId}) .then(function(r){return r.json();}) .then(function(r){ if(r.error){throw new Error(r.error);} clog('ACE '+(aceId+1)+' - '+(T.ace_dry_dryer||'Dryer')+': '+(T.ace_dry_stop||'stop'),'msg-ok'); poll(); }) .catch(function(e){clog('ACE-Fehler: '+e,'msg-err');}); } function loadStore(){ fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){ storeFiles=d.result||[]; renderStore(); }).catch(function(e){clog('Store-Fehler: '+e,'msg-err')}); } function renderStore(){ var grid=document.getElementById('store-grid'); var empty=document.getElementById('store-empty'); // Suche var q=(document.getElementById('store-search')||{value:''}).value.toLowerCase().trim(); // Filter var filter=(document.getElementById('store-filter')||{value:'all'}).value; // Sortierung var sort=(document.getElementById('store-sort')||{value:'date_desc'}).value; var files=storeFiles.filter(function(f){ if(q&&f.filename.toLowerCase().indexOf(q)===-1) return false; if(filter==='completed'&&f.last_print_status!=='completed') return false; if(filter==='failed'&&(f.last_print_status!=='cancelled'&&f.last_print_status!=='failed')) return false; if(filter==='never'&&f.last_print_status) return false; return true; }); files.sort(function(a,b){ if(sort==='name_asc') return a.filename.localeCompare(b.filename); if(sort==='duration_asc'){ var da=a.last_print_duration||a.est_print_time_sec||0; var db=b.last_print_duration||b.est_print_time_sec||0; return da-db; } // date_desc (default) return (b.uploaded_at||'').localeCompare(a.uploaded_at||''); }); if(!storeFiles.length){ empty.textContent=T.store_empty; grid.innerHTML=''; empty.style.display='block'; return; } if(!files.length){ empty.textContent=T.store_no_results; grid.innerHTML=''; empty.style.display='block'; return; } empty.style.display='none'; grid.innerHTML=files.map(function(f){ var thumb=f.thumbnail_b64 ? '' : '
🖨
'; var name=f.filename.length>28?f.filename.slice(0,25)+'…':f.filename; var date=f.uploaded_at?f.uploaded_at.replace('T',' ').slice(0,16):''; var est=f.est_print_time_sec?formatDur(f.est_print_time_sec):'–'; var statusBadge=''; var lastInfo=''; if(f.last_print_status==='completed'){ statusBadge=''; if(f.last_print_duration) lastInfo='
✓ '+formatDur(f.last_print_duration)+'
'; } else if(f.last_print_status==='cancelled'||f.last_print_status==='failed'){ statusBadge=''; lastInfo='
✗ '+f.last_print_status+'
'; } else if(!f.last_print_status){ lastInfo='
'+T.store_never+'
'; } return '
'+ thumb+ '
'+name+statusBadge+'
'+ lastInfo+ '
⏱ Schätzung: '+est+'
'+ '
📅 '+date+'
'+ '
'+ ''+ ''+ '
'+ '
'; }).join(''); } function formatDur(sec){ var h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60); return h?h+'h '+m+'m':m+'m'; } var _storeFileId=null; var _storeFilename=null; var _filamentDialogMode='store'; // 'store' oder 'banner' // GCode-Store-Dateiliste. MUSS deklariert sein – sonst ReferenceError, wenn // "Slots wählen" im Banner geklickt wird, bevor der Browser-Tab je geladen // wurde (Issue #29 / Theme-Auslagerung PR #27). var storeFiles=[]; var _gcodeFilaments=[]; function _setGcodeFilamentsFromFileObj(fileObj){ try{ if(fileObj&&Array.isArray(fileObj.gcode_filaments)){ _gcodeFilaments=fileObj.gcode_filaments; }else if(fileObj&&typeof fileObj.gcode_filaments==='string'&&fileObj.gcode_filaments){ _gcodeFilaments=JSON.parse(fileObj.gcode_filaments); }else{ _gcodeFilaments=[]; } }catch(e){ _gcodeFilaments=[]; } } function storePrint(fileId, filename){ _storeFileId=fileId; _storeFilename=filename; _filamentDialogMode='store'; // GCode-Filamente aus Store-Datei holen (für Vorschau im Dialog) var fileObj=storeFiles.find(function(f){return f.id===fileId;}); _setGcodeFilamentsFromFileObj(fileObj); fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ openFilamentDialog(d.result||[]); }).catch(function(){openFilamentDialog([]);}); } function startReadyFileWithSlots(){ _filamentDialogMode='banner'; _storeFilename=S.file_ready||''; // Banner must never reuse stale store-file context. _storeFileId=null; _gcodeFilaments=[]; function openWithSlots(){ fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ openFilamentDialog(d.result||[]); }).catch(function(){openFilamentDialog([]);}); } var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;}); if(fileObj){ _storeFileId=fileObj.id; _setGcodeFilamentsFromFileObj(fileObj); openWithSlots(); return; } // Fallback: refresh file list, then resolve current file by filename. fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){ storeFiles=d.result||[]; var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;}); if(refreshed){ _storeFileId=refreshed.id; _setGcodeFilamentsFromFileObj(refreshed); } openWithSlots(); }).catch(function(){ openWithSlots(); }); } var _amsSlots=[]; var _printObjects=[]; // [{name, skip}] für aktuell offenen Dialog var _printObjectsSvg=''; // base64-SVG aus DB für Visualisierung // Hilfsfunktionen für farbige Kanal/Slot-Marker (Issue #23) function _contrastText(hex){ // Helle Farbe → dunkler Text, dunkle Farbe → heller Text var c=(hex||'').replace('#',''); if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]; if(c.length<6)return '#fff'; var r=parseInt(c.slice(0,2),16),g=parseInt(c.slice(2,4),16),b=parseInt(c.slice(4,6),16); // YIQ-Helligkeit var y=(r*299 + g*587 + b*114)/1000; return y>=140?'#111':'#fff'; } function _normalizeMaterialKey(material){ var key=(material||'').toUpperCase().replace(/[^A-Z0-9+]/g,''); // Orca often uses PLA for PLA+, while AMS may report PLA+. if(key==='PLA+'||key==='PLAPLUS') return 'PLA'; return key; } function _materialsCompatible(a,b){ return _normalizeMaterialKey(a)===_normalizeMaterialKey(b); } function _updateSlotMarker(sel){ var opt=sel.options[sel.selectedIndex]; var color=opt&&opt.dataset.color?opt.dataset.color:'#888'; var slotIdx=parseInt(opt.value); var paintIdx=sel.dataset.paint; var marker=document.querySelector('.fd-slot-marker[data-for-paint="'+paintIdx+'"]'); if(marker){ marker.style.background=color; marker.style.color=_contrastText(color); marker.textContent=(slotIdx+1); } } function openFilamentDialog(slots){ _amsSlots=slots .filter(function(s){return s.status==='loaded';}) .sort(function(a,b){return (a.slot_index||0)-(b.slot_index||0);}); var dlg=document.getElementById('filament-dialog'); var title=document.getElementById('fd-title'); var body=document.getElementById('fd-slots'); if(title)title.textContent='▶ '+_storeFilename; // Objekt-Liste laden (nur Store-Modus: per File-ID; Banner-Modus hat keine ID) _printObjects=[]; _printObjectsSvg=''; var objSection=document.getElementById('fd-objects-section'); if(objSection)objSection.style.display='none'; if(_filamentDialogMode==='store'&&_storeFileId){ fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects')) .then(function(r){return r.json()}) .then(function(d){ var names=(d.result&&d.result.names)||[]; _printObjectsSvg=(d.result&&d.result.svg_b64)||''; if(names.length>=2){ _printObjects=names.map(function(n){return {name:n,skip:false};}); renderObjectChecklist(); renderObjectSvg(); if(objSection)objSection.style.display='block'; } }).catch(function(){}); } // GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten var channels=_gcodeFilaments.length?_gcodeFilaments:_amsSlots.map(function(s,i){ return {slot_index:i,color_hex:s.color_hex,material:s.material}; }); // Default mapping strategy: // 1) keep order where possible (row i -> nearest compatible slot i) // 2) keep defaults unique while compatible slots are available // 3) use color proximity as tie-breaker function _hexToRgb(hex){ var c=(hex||'').replace('#',''); if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]; if(c.length<6)return [255,255,255]; return [parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16)]; } function _colorDist(a,b){ var ar=_hexToRgb(a), br=_hexToRgb(b); var dr=ar[0]-br[0], dg=ar[1]-br[1], db=ar[2]-br[2]; return (dr*dr + dg*dg + db*db); } var defaultSlotByPaint={}; var usedDefaultSlot={}; channels.forEach(function(gc,i){ var compatible=_amsSlots.filter(function(s){ return _materialsCompatible(gc.material, s.material); }); if(!compatible.length){ defaultSlotByPaint[i]=-1; return; } var ranked=compatible.slice().sort(function(a,b){ var da=Math.abs((a.slot_index||0)-i), db=Math.abs((b.slot_index||0)-i); if(da!==db)return da-db; var ca=_colorDist(gc.color_hex, a.color_hex), cb=_colorDist(gc.color_hex, b.color_hex); if(ca!==cb)return ca-cb; return (a.slot_index||0)-(b.slot_index||0); }); var chosen=ranked.find(function(s){return !usedDefaultSlot[s.slot_index];}) || ranked[0]; defaultSlotByPaint[i]=chosen?chosen.slot_index:-1; if(chosen) usedDefaultSlot[chosen.slot_index]=1; }); if(!_amsSlots.length){ body.innerHTML='

Keine belegten AMS-Slots.
Druck trotzdem starten?

'; } else { body.innerHTML=channels.map(function(gc,i){ var isUsed=(gc&&gc.is_used!==false); // Only allow material-compatible slots. var compatible=_amsSlots.filter(function(s){ return _materialsCompatible(gc.material, s.material); }); var defaultSlotIndex=(defaultSlotByPaint.hasOwnProperty(i)?defaultSlotByPaint[i]:-1); var defaultSlot=compatible.find(function(s){return s.slot_index===defaultSlotIndex;})||null; var opts=compatible.map(function(s){ var sel=(defaultSlot&&s.slot_index===defaultSlot.slot_index)?'selected':''; return ''; }).join(''); if(!compatible.length){ opts=''; } // Kanal-Box (links): farbige Box mit Nummer + auto Kontrast-Text var txt=_contrastText(gc.color_hex); var slotColor=defaultSlot?defaultSlot.color_hex:'#888'; var slotTxt=_contrastText(slotColor); var usedBadge=isUsed ? 'USED' : 'USED'; return '
'+ ''+(i+1)+''+ ''+gc.material+''+ usedBadge+ ''+ ''+(defaultSlot?defaultSlot.slot_index+1:'?')+''+ ''+ '
'; }).join(''); } if(dlg)dlg.classList.add('open'); } function closeFilamentDialog(){ var dlg=document.getElementById('filament-dialog'); if(dlg)dlg.classList.remove('open'); } function confirmFilamentPrint(){ var selects=document.querySelectorAll('#fd-slots select'); var assignments=[]; var missingCompatible=0; selects.forEach(function(sel){ var paintIdx=parseInt(sel.dataset.paint); var paintColor=sel.dataset.paintColor; var isUsed=(sel.dataset.isUsed==='1'); var hasCompatible=(sel.dataset.hasCompatible==='1'); var opt=sel.options[sel.selectedIndex]; var amsIdx=parseInt(opt&&opt.value); if(!hasCompatible || Number.isNaN(amsIdx) || amsIdx < 0){ if(isUsed) missingCompatible += 1; amsIdx = -1; } var amsSlot=_amsSlots.find(function(s){return s.slot_index===amsIdx;})||{}; // Farbe als [R,G,B,255] function hexToRgba(h){ var c=h.replace('#',''); if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]; return [parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16),255]; } assignments.push({ paint_index: paintIdx, is_used: isUsed, slot_index: amsIdx, material: opt.dataset.material||'PLA', paint_color: hexToRgba(paintColor||'#ffffff'), ams_color: hexToRgba(amsSlot.color_hex||'#ffffff'), }); }); if(missingCompatible>0){ clog('Cannot start print: '+missingCompatible+' used paint(s) have no matching material slot','msg-err'); return; } // Pre-Print Skip: Namen der abgehakten Objekte sammeln var excludedObjects=_printObjects.filter(function(o){return o.skip;}).map(function(o){return o.name;}); closeFilamentDialog(); if(_filamentDialogMode==='banner'){ // Banner-Modus: normaler print/start mit Slot-Override var btn=document.getElementById('file-ready-btn'); if(btn){btn.disabled=true;btn.textContent='…';} post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects}) .then(function(r){return r.json();}) .then(function(){ document.getElementById('file-ready-banner').style.display='none'; if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} }) .catch(function(e){ clog((T.log_error||'Error:')+' '+e,'msg-err'); if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} }); } else { // Store-Modus: POST /kx/print fetch(_apiUrl('/kx/print'),{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects}) }).then(function(r){return r.json()}).then(function(d){ if(d.result==='ok'){clog('Druckstart: '+_storeFilename,'msg-ok');showPanel('dashboard');} else{clog('Druckfehler: '+(d.error||'?'),'msg-err');} }).catch(function(e){clog('Druckfehler: '+e,'msg-err');}); } } function renderObjectChecklist(){ var box=document.getElementById('fd-objects'); if(!box)return; box.innerHTML=_printObjects.map(function(o,i){ var label=o.name; // Klipper-Namen sind oft "Datei.stl_id_N_copy_M" → schöner darstellen var m=label.match(/^(.+)\.stl_id_(\d+)_copy_(\d+)$/); if(m)label=m[1]+' #'+(parseInt(m[2])+1)+(m[3]!=='0'?' ('+(parseInt(m[3])+1)+')':''); return ''; }).join(''); } function _toggleObjectSkip(idx,val){ if(_printObjects[idx])_printObjects[idx].skip=!!val; renderObjectSvg(); } function renderObjectSvg(){ var box=document.getElementById('fd-objects-svg'); if(!box)return; if(!_printObjectsSvg||!_printObjects.length){box.style.display='none';box.innerHTML='';return;} box.style.display='block'; var svg=''; try{ svg=atob(_printObjectsSvg);}catch(e){ box.style.display='none'; return; } box.innerHTML=svg; var svgEl=box.querySelector('svg'); if(!svgEl)return; svgEl.style.width='100%'; svgEl.style.maxHeight='200px'; svgEl.style.height='auto'; _printObjects.forEach(function(o,i){ var g=svgEl.querySelector('g[id="'+CSS.escape(o.name)+'"]'); if(!g)return; var path=g.querySelector('path'); g.style.cursor='pointer'; g.setAttribute('opacity', o.skip?'0.8':'0.35'); if(path){ path.setAttribute('fill', o.skip?'#ff5e5b':'#5fa7ff'); path.setAttribute('fill-opacity', o.skip?'0.4':'0.18'); } g.onclick=function(){ _printObjects[i].skip=!_printObjects[i].skip; renderObjectChecklist(); renderObjectSvg(); }; }); } // ── Mid-Print Skip ── var _skipObjects=[]; // [{name, skipped, willSkip}] var _skipSvg=''; function openSkipDialog(){ document.getElementById('skip-status').textContent=''; document.getElementById('skip-confirm').disabled=false; _refreshSkipDialog(); document.getElementById('skip-dialog').classList.add('open'); } function _refreshSkipDialog(){ // Erst aktueller State (mit DB-Objects + svg), dann query_obj für frischen skipped fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ var s=d.result||{}; _skipSvg=s.svg_b64||''; _skipObjects=(s.objects||[]).map(function(n){ return {name:n, skipped:(s.skipped||[]).indexOf(n)>=0, willSkip:false}; }); renderSkipList(); renderSkipSvg(); }); // Frisch nachfragen (skipped-Liste aktualisieren) fetch(_apiUrl('/kx/skip/query'),{method:'POST'}).then(function(r){return r.json()}).then(function(){ setTimeout(function(){ fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ var s=d.result||{}; var skipped=s.skipped||[]; _skipObjects.forEach(function(o){ o.skipped=skipped.indexOf(o.name)>=0; if(o.skipped)o.willSkip=false; }); renderSkipList(); renderSkipSvg(); }); }, 500); }).catch(function(){}); } function closeSkipDialog(){ document.getElementById('skip-dialog').classList.remove('open'); } function _shortLabel(name){ var m=name.match(/^(.+)\.[sS][tT][lL]_id_(\d+)_copy_(\d+)$/); if(!m)return name; return m[1]+' #'+(parseInt(m[2])+1)+(m[3]!=='0'?' ('+(parseInt(m[3])+1)+')':''); } function renderSkipList(){ var box=document.getElementById('skip-list'); if(!box)return; if(!_skipObjects.length){ box.innerHTML='
'+(T.skip_no_objects||'Keine Objekte in diesem Druck.')+'
'; return; } box.innerHTML=_skipObjects.map(function(o,i){ var label=_shortLabel(o.name); var dis=o.skipped?'disabled':''; var note=o.skipped?''+(T.skip_already||'übersprungen')+'':''; return ''; }).join(''); } function renderSkipSvg(){ var box=document.getElementById('skip-svg'); if(!box)return; if(!_skipSvg||!_skipObjects.length){box.style.display='none';box.innerHTML='';return;} box.style.display='block'; // SVG aus base64 dekodieren var svg=''; try{ svg=atob(_skipSvg); }catch(e){ box.style.display='none'; return; } box.innerHTML=svg; // Polygone interaktiv machen: jeder entspricht einem Objekt var svgEl=box.querySelector('svg'); if(!svgEl)return; svgEl.style.width='100%'; svgEl.style.maxHeight='280px'; svgEl.style.height='auto'; _skipObjects.forEach(function(o,i){ var g=svgEl.querySelector('g[id="'+CSS.escape(o.name)+'"]'); if(!g)return; var path=g.querySelector('path'); if(o.skipped){ // bereits übersprungen → ausgegraut, kein Klick g.setAttribute('opacity','0.25'); if(path){path.setAttribute('fill','#888');path.setAttribute('fill-opacity','0.3');} g.style.cursor='not-allowed'; } else { g.style.cursor='pointer'; g.setAttribute('opacity', o.willSkip?'0.8':'0.35'); if(path){ path.setAttribute('fill', o.willSkip?'#ff5e5b':'#5fa7ff'); path.setAttribute('fill-opacity', o.willSkip?'0.4':'0.18'); } g.onclick=function(){ _skipObjects[i].willSkip=!_skipObjects[i].willSkip; renderSkipList(); renderSkipSvg(); }; } }); } function _toggleWillSkip(idx,val){ if(_skipObjects[idx])_skipObjects[idx].willSkip=!!val; renderSkipSvg(); } function confirmSkip(){ var names=_skipObjects.filter(function(o){return o.willSkip;}).map(function(o){return o.name;}); var st=document.getElementById('skip-status'); var btn=document.getElementById('skip-confirm'); if(!names.length){st.textContent=T.skip_select_at_least_one||'Bitte mindestens ein Objekt wählen.';st.style.color='var(--warn)';return;} btn.disabled=true; st.textContent=T.skip_sending||'Sende …'; st.style.color='var(--txt2)'; fetch(_apiUrl('/kx/skip'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({names:names})}) .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) .then(function(res){ if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;} st.textContent=T.skip_success||'Objekte werden übersprungen.';st.style.color='var(--ok)'; // Dialog offen lassen + neu laden damit der "übersprungen"-Status erscheint setTimeout(function(){ _refreshSkipDialog(); btn.disabled=false; st.textContent=''; }, 1500); }) .catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;}); } function storeDelete(fileId){ if(!confirm(T.store_delete_confirm)) return; fetch(_apiUrl('/kx/files/'+fileId),{method:'DELETE'}).then(function(r){ if(r.ok){loadStore();} else{clog('Löschen fehlgeschlagen','msg-err');} }); } // ── Drucker hinzufügen ── function openAddPrinterDialog(){ document.getElementById('apd-ip').value=''; document.getElementById('apd-name').value=''; var st=document.getElementById('apd-status');st.textContent='';st.style.color='var(--txt2)'; document.getElementById('apd-confirm').disabled=false; document.getElementById('add-printer-dialog').classList.add('open'); } function closeAddPrinterDialog(){ document.getElementById('add-printer-dialog').classList.remove('open'); } function confirmAddPrinter(){ var ip=document.getElementById('apd-ip').value.trim(); var name=document.getElementById('apd-name').value.trim(); var st=document.getElementById('apd-status'),btn=document.getElementById('apd-confirm'); if(!ip){st.textContent=T.apd_err_ip;st.style.color='var(--err)';return;} st.textContent=T.apd_fetching;st.style.color='var(--txt2)';btn.disabled=true; fetch('/kx/printers/add',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({printer_ip:ip,name:name})}) .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) .then(function(res){ if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;} st.textContent=T.apd_success;st.style.color='var(--ok)'; setTimeout(function(){location.reload();},2500); }) .catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;}); } function removePrinter(id,name){ if(!confirm(T.printers_remove_confirm.replace('{name}',name)))return; fetch('/kx/printers/'+encodeURIComponent(id),{method:'DELETE'}) .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) .then(function(res){ if(!res.ok){alert((res.j&&res.j.error)||'Fehler');return;} setTimeout(function(){location.href='/printer1';},2000); }) .catch(function(e){alert(''+e);}); } // ── Drucker-Tab ── function loadPrinterTab(){ var grid=document.getElementById('printers-grid'); if(grid)grid.innerHTML='
'+T.printers_loading+'
'; // Drucker-Liste von lokaler Instanz holen fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){ var printers=d.result||[]; if(!printers.length){ if(grid)grid.innerHTML='
'+ '
🖨
'+ '
'+T.printers_empty_hint+'
'+ ''+ '
'; return; } // Status jedes Druckers parallel abrufen var fetches=printers.map(function(p){ var url=(p.bridge_url||'').replace(/\/+$/,''); return fetch(url+'/api/state',{signal:AbortSignal.timeout(3000)}) .then(function(r){return r.json()}) .then(function(s){return {printer:p,state:s,online:true};}) .catch(function(){return {printer:p,state:{},online:false};}); }); Promise.all(fetches).then(function(results){ var activeId=_activePrinter?String(_activePrinter.id):null; if(grid)grid.innerHTML=results.map(function(res){ var p=res.printer,s=res.state,online=res.online; var isActive=String(p.id)===activeId; var url=(p.bridge_url||'').replace(/\/+$/,''); var printerNum=p.id; var ks=online?(s.kobra_state||'free'):'offline'; var stateKey='kobra_'+ks; var stateLabel=T[stateKey]||ks; var stateColor=ks==='free'?'var(--ok)':ks==='printing'?'var(--accent)':ks==='offline'?'var(--txt2)':'var(--warn)'; var progress=online&&s.progress?Math.round(s.progress*100):null; var filename=online&&s.filename?s.filename:''; var nt=online&&s.nozzle_temp?s.nozzle_temp.toFixed(1):'–'; var bt=online&&s.bed_temp?s.bed_temp.toFixed(1):'–'; var border=isActive?'2px solid var(--accent)':'1px solid var(--border)'; var nameEsc=String(p.name).replace(/\\/g,'\\\\').replace(/'/g,"\\'"); return '
'+ '
'+ '🖨 '+p.name+''+ ''+ (isActive?''+T.printers_active+'':'')+ ''+ ''+ '
'+ '
'+ ''+ ''+stateLabel+''+ '
'+ (p.printer_ip?'
🌐 '+p.printer_ip+'
':'')+ (filename?'
📄 '+filename+'
':'')+ (progress!==null?'
':'')+ '
'+ '🌡 '+nt+'°C🛏 '+bt+'°C'+ '
'+ (!isActive?''+T.printers_switch+'':'
'+T.printers_current+'
')+ '
'; }).join(''); }); }).catch(function(e){ if(grid)grid.innerHTML='
Fehler: '+e+'
'; }); }