// ── 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:[]}; var tempHistory={n:[],b:[]}; var camOn=false; var currentStep=1; var currentPanel='dashboard'; // ── 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', 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:'AMS / Filamentbox',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_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', 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', 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:'AMS / Filament Box',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_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', 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(){ // 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('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); // 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); 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(){ 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 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; // 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';} } // 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)+'%'; // AMS if(s.ams_slots&&s.ams_slots.length){ window._amsSlots=s.ams_slots; var html=''; s.ams_slots.forEach(function(slot,i){ 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 active=slot.status===1||slot.active; var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'–'); var idx=slot.index!=null?slot.index:i; html+='
' +'
' +'
'+(empty?'–':(slot.type||slot.material_type||'–'))+'
' +'
Slot '+(idx+1)+'
' +'
'+pct+'
' +'
' +'
'; }); 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 _MAT_PRESETS=['PLA','PETG','ABS','ASA','TPU','PA','PC','HIPS']; function openSlotEdit(i){ var slot=(window._amsSlots||[])[i]||{}; var index=slot.index!=null?slot.index:i; _slotEditIndex=index; document.getElementById('slot-edit-title').textContent=T.slot_edit_title+' '+(index+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(''); document.getElementById('slot-edit-modal').classList.add('open'); } function closeSlotEdit(){ document.getElementById('slot-edit-modal').classList.remove('open'); } 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){ var slot=parseInt(document.getElementById('ams-slot-sel').value); post('/api/ams/feed',{slot_index:slot,type:type}) .then(function(){clog((type===1?T.lbl_feed:T.lbl_unload)+' Slot '+(slot+1),'msg-ok')}) .catch(function(e){clog('AMS-Fehler: '+e,'msg-err')}); } // ── 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(){ post('/api/camera/stop',{}).then(function(){ var img=document.getElementById('cam-img'); img.src=''; img.style.display='none'; document.getElementById('cam-spinner').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'); }).catch(function(e){clog((T.log_error||'Fehler:')+' '+e,'msg-err')}); } function toggleCam(){if(camOn)camStop();else camStart()} // ── GCode Store ── var storeFiles=[]; 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' var _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;}); try{ _gcodeFilaments=fileObj&&fileObj.gcode_filaments?JSON.parse(fileObj.gcode_filaments):[]; } catch(e){ _gcodeFilaments=[]; } 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||''; fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ openFilamentDialog(d.result||[]); }).catch(function(){openFilamentDialog([]);}); } var _amsSlots=[]; function openFilamentDialog(slots){ _amsSlots=slots.filter(function(s){return s.status==='loaded';}); var dlg=document.getElementById('filament-dialog'); var title=document.getElementById('fd-title'); var body=document.getElementById('fd-slots'); if(title)title.textContent='▶ '+_storeFilename; // 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}; }); if(!_amsSlots.length){ body.innerHTML='

Keine belegten AMS-Slots.
Druck trotzdem starten?

'; } else { body.innerHTML=channels.map(function(gc,i){ // Passende Slots: gleicher Materialtyp var compatible=_amsSlots.filter(function(s){return s.material.toUpperCase()===gc.material.toUpperCase();}); if(!compatible.length) compatible=_amsSlots; // Fallback: alle // Standard-Auswahl: Slot mit gleichem Index oder erster kompatibler var defaultSlot=compatible.find(function(s){return s.slot_index===gc.slot_index;})||compatible[0]; var opts=compatible.map(function(s){ var sel=(s.slot_index===defaultSlot.slot_index)?'selected':''; return ''; }).join(''); return '
'+ ''+ 'Kanal '+(i+1)+''+ ''+gc.material+''+ ''+ ''+ '
'; }).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=[]; selects.forEach(function(sel){ var paintIdx=parseInt(sel.dataset.paint); var paintColor=sel.dataset.paintColor; var opt=sel.options[sel.selectedIndex]; var amsIdx=parseInt(opt.value); 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, slot_index: amsIdx, material: opt.dataset.material||'PLA', paint_color: hexToRgba(paintColor||'#ffffff'), ams_color: hexToRgba(amsSlot.color_hex||'#ffffff'), }); }); 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}) .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}) }).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 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+'
'; }); }