Files
KX-Bridge-Release/web/themes/new1/app.js
2026-05-18 11:54:31 +02:00

1268 lines
61 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── 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:'AZ 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:'AZ 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 '<a href="/printer'+num+'" style="display:block;padding:10px 14px;color:'+(active?'var(--accent)':'var(--txt)')+';text-decoration:none;font-size:13px;border-bottom:1px solid var(--border)" '+(active?'style="font-weight:600"':'')+'>'+
(active?'▶ ':'')+p.name+'</a>';
}).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=>`<div><span class="ts">${l.ts}</span><span class="${l.cls}">${escHtml(l.msg)}</span></div>`).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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
// 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+='<div class="ams-slot'+(active?' active':'')+(empty?' empty':'')+ '" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">'
+'<div class="slot-circle" style="background:'+col+'"></div>'
+'<div class="slot-material">'+(empty?'':(slot.type||slot.material_type||''))+'</div>'
+'<div class="slot-label">Slot '+(idx+1)+'</div>'
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
+'</div>';
});
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 '<button class="mat-preset-btn" data-mat="'+m+'" onclick="selectMatPreset(\''+m+'\')" '
+'style="padding:4px 10px;border-radius:6px;border:1px solid var(--border);cursor:pointer;font-size:12px;'
+(m===mat?'background:var(--accent);color:#fff':'background:var(--raised);color:var(--txt2)')+'">'+m+'</button>';
}).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
? '<img src="data:image/png;base64,'+f.thumbnail_b64+'" style="width:100%;height:130px;object-fit:cover;border-radius:6px;display:block;margin-bottom:8px">'
: '<div style="width:100%;height:130px;background:var(--raised);border-radius:6px;display:flex;align-items:center;justify-content:center;margin-bottom:8px;font-size:32px">🖨</div>';
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='<span style="background:#2a7a3b;color:#fff;border-radius:4px;padding:1px 6px;font-size:10px;margin-left:4px">✓</span>';
if(f.last_print_duration) lastInfo='<div style="font-size:11px;color:var(--ok);margin-bottom:2px">✓ '+formatDur(f.last_print_duration)+'</div>';
} else if(f.last_print_status==='cancelled'||f.last_print_status==='failed'){
statusBadge='<span style="background:#a04020;color:#fff;border-radius:4px;padding:1px 6px;font-size:10px;margin-left:4px">✗</span>';
lastInfo='<div style="font-size:11px;color:var(--err);margin-bottom:2px">✗ '+f.last_print_status+'</div>';
} else if(!f.last_print_status){
lastInfo='<div style="font-size:11px;color:var(--txt2);margin-bottom:2px;opacity:.6">'+T.store_never+'</div>';
}
return '<div style="background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:10px;display:flex;flex-direction:column">'+
thumb+
'<div title="'+f.filename+'" style="font-size:12px;font-weight:600;margin-bottom:4px;color:var(--txt)">'+name+statusBadge+'</div>'+
lastInfo+
'<div style="font-size:11px;color:var(--txt2);margin-bottom:2px">⏱ Schätzung: '+est+'</div>'+
'<div style="font-size:11px;color:var(--txt2);margin-bottom:8px">📅 '+date+'</div>'+
'<div style="display:flex;gap:6px;margin-top:auto">'+
'<button onclick="storePrint(\''+f.id+'\',\''+f.filename.replace(/'/g,"\\'")+'\')" '+
'style="flex:1;font-size:12px;padding:5px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer">'+T.store_print+'</button>'+
'<button onclick="storeDelete(\''+f.id+'\')" '+
'style="font-size:12px;padding:5px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer">🗑</button>'+
'</div>'+
'</div>';
}).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='<p style="color:var(--txt2);font-size:13px;text-align:center;padding:16px 0">Keine belegten AMS-Slots.<br>Druck trotzdem starten?</p>';
} 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 '<option value="'+s.slot_index+'" data-color="'+s.color_hex+'" data-material="'+s.material+'" '+sel+'>'+
'Slot '+(s.slot_index+1)+' · '+s.material+'</option>';
}).join('');
return '<div style="display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;background:var(--raised);border:1px solid var(--border)">'+
'<span style="display:inline-block;width:16px;height:16px;border-radius:50%;background:'+gc.color_hex+';border:1px solid var(--border);flex-shrink:0"></span>'+
'<span style="font-size:13px;font-weight:600;min-width:70px">Kanal '+(i+1)+'</span>'+
'<span style="font-size:11px;color:var(--txt2);min-width:32px">'+gc.material+'</span>'+
'<span style="font-size:18px;color:var(--txt2)">→</span>'+
'<select data-paint="'+i+'" data-paint-color="'+gc.color_hex+'" style="flex:1;padding:4px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:13px">'+
opts+'</select>'+
'</div>';
}).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='<div style="color:var(--txt2);font-size:13px;padding:20px">'+T.printers_loading+'</div>';
// 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='<div style="grid-column:1/-1;text-align:center;padding:40px 20px;color:var(--txt2)">'+
'<div style="font-size:32px;margin-bottom:8px">🖨</div>'+
'<div style="font-size:14px;margin-bottom:14px">'+T.printers_empty_hint+'</div>'+
'<button onclick="openAddPrinterDialog()" style="font-size:13px;padding:8px 18px;background:var(--accent);border:none;border-radius:8px;color:#fff;cursor:pointer;font-weight:600">+ '+T.add_printer+'</button>'+
'</div>';
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 '<div style="background:var(--raised);border:'+border+';border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:8px">'+
'<div style="display:flex;align-items:center;justify-content:space-between;gap:8px">'+
'<span style="font-weight:700;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">🖨 '+p.name+'</span>'+
'<span style="display:flex;align-items:center;gap:8px;flex-shrink:0">'+
(isActive?'<span style="font-size:11px;color:var(--accent);font-weight:600">'+T.printers_active+'</span>':'')+
'<button onclick="removePrinter(\''+printerNum+'\',\''+nameEsc+'\')" title="'+T.printers_remove+'" style="background:none;border:none;color:var(--txt2);font-size:16px;cursor:pointer;line-height:1;padding:0">✕</button>'+
'</span>'+
'</div>'+
'<div style="display:flex;align-items:center;gap:6px">'+
'<span style="width:8px;height:8px;border-radius:50%;background:'+stateColor+';display:inline-block"></span>'+
'<span style="font-size:13px;color:var(--txt2)">'+stateLabel+'</span>'+
'</div>'+
(p.printer_ip?'<div style="font-size:12px;color:var(--txt2)">🌐 '+p.printer_ip+'</div>':'')+
(filename?'<div style="font-size:12px;color:var(--txt2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="'+filename+'">📄 '+filename+'</div>':'')+
(progress!==null?'<div style="background:var(--border);border-radius:4px;height:6px;overflow:hidden"><div style="background:var(--accent);height:100%;width:'+progress+'%"></div></div>':'')+
'<div style="font-size:12px;color:var(--txt2);display:flex;gap:12px">'+
'<span>🌡 '+nt+'°C</span><span>🛏 '+bt+'°C</span>'+
'</div>'+
(!isActive?'<a href="/printer'+printerNum+'" style="display:block;text-align:center;padding:7px;background:var(--accent);color:#fff;border-radius:7px;font-size:13px;font-weight:600;text-decoration:none;margin-top:4px">'+T.printers_switch+'</a>':'<div style="text-align:center;padding:7px;font-size:12px;color:var(--txt2)">'+T.printers_current+'</div>')+
'</div>';
}).join('');
});
}).catch(function(e){
if(grid)grid.innerHTML='<div style="color:var(--err);font-size:13px;padding:20px">Fehler: '+e+'</div>';
});
}