Files
KX-Bridge-Release/web/themes/default/app.js
2026-05-22 11:26:16 +02:00

2181 lines
103 KiB
JavaScript
Raw 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:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]}};
var tempHistory={n:[],b:[]};
var camOn=false;
var currentStep=1;
var currentPanel='dashboard';
var aceAutoRefillPrefs=(function(){
try{return JSON.parse(localStorage.getItem('aceAutoRefillPrefs')||'{}')||{};}catch(_){return {};}
})();
var aceDryProfiles=(function(){
try{return JSON.parse(localStorage.getItem('aceDryProfiles')||'{}')||{};}catch(_){return {};}
})();
var _aceDryDialogAceId=-1;
var _aceDryDialogPresetKey='';
var _aceDryDialogPresetOriginals={};
var ACE_DRY_PRESET_DEFAULTS={
pla:{temp:45,duration_sec:4*3600},
pla_plus:{temp:45,duration_sec:4*3600},
petg:{temp:50,duration_sec:4*3600},
tpu:{temp:55,duration_sec:4*3600},
abs_asa:{temp:45,duration_sec:8*3600},
pa_pc:{temp:55,duration_sec:12*3600}
};
var ACE_DRY_PRESETS={
pla:{temp:45,duration_sec:4*3600},
pla_plus:{temp:45,duration_sec:4*3600},
petg:{temp:50,duration_sec:4*3600},
tpu:{temp:55,duration_sec:4*3600},
abs_asa:{temp:45,duration_sec:8*3600},
pa_pc:{temp:55,duration_sec:12*3600},
custom_1:{name:'Custom 1',temp:45,duration_sec:4*3600},
custom_2:{name:'Custom 2',temp:45,duration_sec:4*3600},
custom_3:{name:'Custom 3',temp:45,duration_sec:4*3600}
};
function _aceAutoRefillGet(aceId){return !!aceAutoRefillPrefs[String(aceId)];}
function _aceAutoRefillSet(aceId,on){
aceAutoRefillPrefs[String(aceId)]=!!on;
localStorage.setItem('aceAutoRefillPrefs',JSON.stringify(aceAutoRefillPrefs));
}
function _aceDryProfileGet(aceId){
var p=aceDryProfiles[String(aceId)]||{};
var temp=parseInt(p.temp,10);
var dur=parseInt(p.duration_sec,10);
if(!Number.isFinite(temp))temp=45;
if(!Number.isFinite(dur))dur=4*3600;
temp=Math.max(30,Math.min(80,temp));
dur=Math.max(10*60,Math.min(24*3600,dur));
return {temp:temp,duration_sec:dur,preset:p.preset||''};
}
function _aceDryProfileSet(aceId,temp,durationSec,preset){
aceDryProfiles[String(aceId)]={
temp:Math.max(30,Math.min(80,parseInt(temp,10)||45)),
duration_sec:Math.max(10*60,Math.min(24*3600,parseInt(durationSec,10)||4*3600)),
preset:preset||''
};
localStorage.setItem('aceDryProfiles',JSON.stringify(aceDryProfiles));
}
function _aceDryDurationMinFromSec(sec){
var minutes=Math.round((parseInt(sec,10)||0)/60);
return Math.max(10,Math.min(1440,minutes));
}
function _syncAceDryPresetsFromServer(raw){
if(!raw||typeof raw!=='object')return;
Object.keys(ACE_DRY_PRESETS).forEach(function(k){
var p=raw[k];
if(!p||typeof p!=='object')return;
var t=parseInt(p.temp,10);
var d=parseInt(p.duration_sec,10);
if(Number.isFinite(t))ACE_DRY_PRESETS[k].temp=Math.max(30,Math.min(80,t));
if(Number.isFinite(d))ACE_DRY_PRESETS[k].duration_sec=Math.max(10*60,Math.min(24*3600,d));
if(/^custom_[123]$/.test(k)&&typeof p.name==='string'){
var n=p.name.trim();
ACE_DRY_PRESETS[k].name=n||('Custom '+k.slice(-1));
}
});
}
// ── Theme ──
function toggleTheme(){
var h=document.documentElement;
h.setAttribute('data-theme',h.getAttribute('data-theme')==='dark'?'light':'dark');
localStorage.setItem('theme',h.getAttribute('data-theme'));
}
(function(){var t=localStorage.getItem('theme');if(t)document.documentElement.setAttribute('data-theme',t)})();
// ── i18n ──
var LANG_DE={
header_status_standby:'Bereit',header_status_printing:'Druckt',header_status_complete:'Fertig',header_status_error:'Fehler',
kobra_free:'Bereit',kobra_busy:'Beschäftigt',kobra_printing:'Druckt',kobra_preheating:'Aufheizen',kobra_auto_leveling:'Nivellierung',kobra_checking:'Prüfung',kobra_updated:'Aktualisierung',kobra_init:'Initialisierung',kobra_pausing:'Pausiert...',kobra_paused:'Pausiert',kobra_resuming:'Fortsetzen...',kobra_resumed:'Fortgesetzt',kobra_stopping:'Stoppt...',kobra_stoped:'Gestoppt',kobra_finished:'Abgeschlossen',kobra_failed:'Fehler',kobra_canceled:'Abgebrochen',kobra_offline:'Offline',
nav_dashboard:'Dashboard',nav_print:'Druck',nav_temps:'Temperaturen',nav_motion:'Achsen',nav_ams:'AMS',nav_extras:'Licht / Lüfter',nav_console:'Konsole',
card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',card_speed:'Druckgeschwindigkeit',card_cam:'Kamera',lbl_elapsed:'Verstrichen:',lbl_remaining:'Restzeit:',lbl_slicer_time:'Slicer-Schätzung:',lbl_layers:'Layer',
speed_silent:'🐢 Leise',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen',
card_ace_dry:'ACE Trocknung',ace_dry_dryer:'Trockner',ace_dry_status_off:'Status: Aus',ace_dry_status_on:'Status: Aktiv',ace_dry_status_remaining:'Rest',ace_dry_humidity:'Luftfeuchte',ace_dry_current_temp:'Temperatur',ace_dry_chart:'Verlauf (Temp/Feuchte)',ace_dry_temp:'Temperatur (°C)',ace_dry_duration:'Dauer (Min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto-Nachschub',ace_dry_enable:'Trocknung aktivieren',ace_dry_temp_line:'Trocknungstemperatur',ace_dry_time_line:'Trocknungszeit',ace_dry_ui_pending:'(nur UI, Backend folgt)',ace_dry_dialog_title:'Trockner Temp/Zeit-Einstellungen',ace_dry_dialog_temp:'Temperatur (30-80°C)',ace_dry_dialog_time:'Restzeit (h:m:s)',ace_dry_dialog_confirm:'Bestätigen',ace_dry_dialog_cancel:'Abbrechen',ace_dry_dialog_save_restart:'Speichern & Neustart',ace_dry_dialog_custom_name:'Eigener Name',ace_dry_dialog_reset_default:'Auf Standard zurücksetzen',
cam_placeholder:'📷 Kamera nicht gestartet',btn_cam_start:'▶ Kamera',btn_cam_stop:'◼ Kamera',
btn_pause:'⏸ Pause',btn_resume:'▶ Weiter',btn_cancel:'✕ Stopp',
label_nozzle:'Nozzle',label_bed:'Bett',label_fan:'🌀 Lüfter',label_light:'💡 Licht',label_on_off:'Ein / Aus',label_speed:'Geschwindigkeit',
panel_print_title:'Drucksteuerung',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Fortsetzen',panel_print_btn_cancel:'✕ Abbrechen',panel_print_temps_live:'Temperaturen (Live)',
label_set:'Setzen',label_off:'Aus',
panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heizbett',panel_temps_chart:'Verlauf (letzte 60 Messungen)',label_target_c:'Ziel:',
panel_motion_xy:'XY-Achsen',panel_motion_z:'Z-Achse',label_step:'Schrittweite:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motoren aus',
panel_ams_title:'Filament',card_ams:'Filament',ams_no_data:'Keine AMS-Daten empfangen',label_slot:'Slot',ams_empty:'Leer',
panel_extras_light:'Licht',panel_extras_fan:'Lüfter',panel_extras_camera:'Kamera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop',
panel_console_title:'Ereignis-Log',
log_light_on:'Licht an',log_light_off:'Licht aus',log_fan:'Lüfter →',log_nozzle:'Nozzle →',log_bed:'Bett →',log_axis:'Achse',log_home:'Home',log_home_all:'Home All',log_cam_start:'Kamera gestartet:',log_cam_stop:'Kamera gestoppt',log_poll_error:'Poll-Fehler:',log_error:'Fehler:',
confirm_cancel:'Druck wirklich abbrechen?',
settings_title:'Einstellungen',settings_connection:'Verbindung',settings_print:'Druckeinstellungen',settings_poll:'Poll-Intervall',settings_version:'Version',
settings_save:'Speichern & Neustart',settings_printer_name:'Drucker-Name',settings_printer_ip:'Drucker-IP',settings_mqtt_port:'MQTT-Port',
settings_username:'MQTT-Benutzername',settings_password:'MQTT-Passwort',settings_device_id:'Device-ID',settings_mode_id:'Mode-ID',hint_ip_no_port:'Nur IP-Adresse, kein Port (z.B. 192.168.1.102)',
settings_default_slot:'Standard-Slot (Einfarbdruck)',settings_slot_auto:'Auto (alle belegten Slots)',settings_auto_leveling:'Auto-Leveling vor Druck',settings_camera_on_print:'Kamera bei Druckstart einschalten',
update_check:'Auf Updates prüfen',update_checking:'Prüfe...',update_available:'verfügbar',update_none:'Bereits aktuell',
update_apply:'Jetzt installieren',update_applying:'Lade herunter...',update_restarting:'Starte neu...',update_error:'Fehler',
btn_connect:'⚡ Verbinden',btn_disconnect:'✕ Trennen',
lbl_conn_error:'Verbindungsfehler:',
slot_edit_title:'Slot bearbeiten',slot_edit_color:'Farbe',slot_edit_material:'Material',
slot_edit_load:'⬇ Einziehen',slot_edit_unload:'⬆ Ausziehen',
slot_edit_save:'💾 Speichern',slot_edit_custom:'z.B. PLA, PETG, ABS…',
slot_edit_ok:'AMS Slot',
log_dir_all:'Alle',log_lvl_label:'Level:',
file_ready_btn:'▶ Druck starten',
file_slots_btn:'🎨 Slots wählen',
file_cancel_btn:'✕ Abbrechen',
nav_printers:'Drucker',
skip_title:'✂ Objekte überspringen',skip_hint:'Objekte abwählen, die nicht weiter gedruckt werden sollen:',
skip_btn_label:'Objekte',skip_no_objects:'Keine Objekte in diesem Druck.',
skip_already:'übersprungen',skip_select_at_least_one:'Bitte mindestens ein Objekt wählen.',
skip_sending:'Sende …',skip_success:'Objekte werden übersprungen.',
fd_objects_hint:'Objekte überspringen (optional):',
add_printer:'Drucker hinzufügen',apd_lbl_ip:'Drucker-IP',apd_lbl_name:'Name (optional)',
apd_fetching:'Hole Daten vom Drucker…',apd_success:'Drucker hinzugefügt, Bridge startet neu…',apd_err_ip:'Bitte IP-Adresse eingeben',
printers_remove:'Drucker entfernen',printers_remove_confirm:'Drucker "{name}" entfernen? Die Bridge startet neu.',
printers_active:'● aktiv',
printers_switch:'Wechseln →',
printers_current:'Aktueller Drucker',
printers_loading:'Lade…',
printers_none:'Keine Drucker konfiguriert.',
printers_empty_hint:'Noch kein Drucker eingerichtet.',
nav_browser:'Browser',
panel_browser_title:'Datei-Browser',
store_empty:'Noch keine Dateien hochgeladen.',
store_refresh:'↻ Aktualisieren',
store_print:'▶ Drucken',
store_delete_confirm:'Datei löschen?',
store_print_confirm:'Datei drucken?',
store_no_results:'Keine Dateien gefunden.',
store_never:'noch nicht gedruckt',
sf_all:'Alle',sf_ok:'✓ Erfolgreich',sf_err:'✗ Fehler',sf_new:'Neu',
ss_date:'↓ Datum',ss_name:'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',
card_ace_dry:'ACE Drying',ace_dry_dryer:'Dryer',ace_dry_status_off:'Status: Off',ace_dry_status_on:'Status: Active',ace_dry_status_remaining:'Remaining',ace_dry_humidity:'Humidity',ace_dry_current_temp:'Temperature',ace_dry_chart:'History (Temp/Humidity)',ace_dry_temp:'Temperature (°C)',ace_dry_duration:'Duration (min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto Refill',ace_dry_enable:'Enable Drying',ace_dry_temp_line:'Drying Temperature',ace_dry_time_line:'Drying Time',ace_dry_ui_pending:'(UI only, backend next)',ace_dry_dialog_title:'Dryer Temp/Time Settings',ace_dry_dialog_temp:'Temperature (30-80°C)',ace_dry_dialog_time:'Rem. Time (h:m:s)',ace_dry_dialog_confirm:'Confirm',ace_dry_dialog_cancel:'Cancel',ace_dry_dialog_save_restart:'Save & Restart',ace_dry_dialog_custom_name:'Custom Name',ace_dry_dialog_reset_default:'Reset to Default',
cam_placeholder:'📷 Camera not started',btn_cam_start:'▶ Camera',btn_cam_stop:'◼ Camera',
btn_pause:'⏸ Pause',btn_resume:'▶ Resume',btn_cancel:'✕ Stop',
label_nozzle:'Nozzle',label_bed:'Bed',label_fan:'🌀 Fan',label_light:'💡 Light',label_on_off:'On / Off',label_speed:'Speed',
panel_print_title:'Print Control',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Resume',panel_print_btn_cancel:'✕ Cancel',panel_print_temps_live:'Temperatures (Live)',
label_set:'Set',label_off:'Off',
panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heated Bed',panel_temps_chart:'History (last 60 readings)',label_target_c:'Target:',
panel_motion_xy:'XY Axes',panel_motion_z:'Z Axis',label_step:'Step size:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motors Off',
panel_ams_title:'Filament',card_ams:'Filament',ams_no_data:'No AMS data received',label_slot:'Slot',ams_empty:'Empty',
panel_extras_light:'Light',panel_extras_fan:'Fan',panel_extras_camera:'Camera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop',
panel_console_title:'Event Log',
log_light_on:'Light on',log_light_off:'Light off',log_fan:'Fan →',log_nozzle:'Nozzle →',log_bed:'Bed →',log_axis:'Axis',log_home:'Home',log_home_all:'Home All',log_cam_start:'Camera started:',log_cam_stop:'Camera stopped',log_poll_error:'Poll error:',log_error:'Error:',
confirm_cancel:'Really cancel the print?',
settings_title:'Settings',settings_connection:'Connection',settings_print:'Print Settings',settings_poll:'Poll Interval',settings_version:'Version',
settings_save:'Save & Restart',settings_printer_name:'Printer Name',settings_printer_ip:'Printer IP',settings_mqtt_port:'MQTT Port',
settings_username:'MQTT Username',settings_password:'MQTT Password',settings_device_id:'Device ID',settings_mode_id:'Mode ID',hint_ip_no_port:'IP address only, no port (e.g. 192.168.1.102)',
settings_default_slot:'Default Slot (single color)',settings_slot_auto:'Auto (all loaded slots)',settings_auto_leveling:'Auto-Leveling before print',settings_camera_on_print:'Turn camera on at print start',
update_check:'Check for Updates',update_checking:'Checking...',update_available:'available',update_none:'Already up to date',
update_apply:'Install Now',update_applying:'Downloading...',update_restarting:'Restarting...',update_error:'Error',
btn_connect:'⚡ Connect',btn_disconnect:'✕ Disconnect',
lbl_conn_error:'Connection error:',
slot_edit_title:'Edit Slot',slot_edit_color:'Color',slot_edit_material:'Material',
slot_edit_load:'⬇ Load',slot_edit_unload:'⬆ Unload',
slot_edit_save:'💾 Save',slot_edit_custom:'e.g. PLA, PETG, ABS…',
slot_edit_ok:'AMS Slot',
log_dir_all:'All',log_lvl_label:'Level:',
file_ready_btn:'▶ Start Print',
file_slots_btn:'🎨 Select Slots',
file_cancel_btn:'✕ Cancel',
nav_printers:'Printers',
skip_title:'✂ Skip objects',skip_hint:'Uncheck objects you no longer want to print:',
skip_btn_label:'Objects',skip_no_objects:'No objects in this print.',
skip_already:'skipped',skip_select_at_least_one:'Please pick at least one object.',
skip_sending:'Sending …',skip_success:'Objects will be skipped.',
fd_objects_hint:'Skip objects (optional):',
add_printer:'Add printer',apd_lbl_ip:'Printer IP',apd_lbl_name:'Name (optional)',
apd_fetching:'Fetching data from printer…',apd_success:'Printer added, bridge restarting…',apd_err_ip:'Please enter an IP address',
printers_remove:'Remove printer',printers_remove_confirm:'Remove printer "{name}"? The bridge will restart.',
printers_active:'● active',
printers_switch:'Switch →',
printers_current:'Current printer',
printers_loading:'Loading…',
printers_none:'No printers configured.',
printers_empty_hint:'No printer set up yet.',
nav_browser:'Browser',
panel_browser_title:'File Browser',
store_empty:'No files uploaded yet.',
store_refresh:'↻ Refresh',
store_print:'▶ Print',
store_delete_confirm:'Delete file?',
store_print_confirm:'Print file?',
store_no_results:'No files found.',
store_never:'never printed',
sf_all:'All',sf_ok:'✓ Completed',sf_err:'✗ Failed',sf_new:'New',
ss_date:'↓ Date',ss_name:'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(){
ensureAceDryCards();
// Nav
var nb=document.getElementById('nb-dashboard');if(nb)nb.querySelector('.nav-text').textContent=T.nav_dashboard;
nb=document.getElementById('nb-console');if(nb)nb.querySelector('.nav-text').textContent=T.nav_console;
nb=document.getElementById('nb-printers');if(nb)nb.querySelector('.nav-text').textContent=T.nav_printers;
nb=document.getElementById('nb-store');if(nb)nb.querySelector('.nav-text').textContent=T.nav_browser;
// Bottom nav
var bnb=document.getElementById('bnb-dashboard');if(bnb)bnb.lastChild.textContent=T.nav_dashboard;
bnb=document.getElementById('bnb-console');if(bnb)bnb.lastChild.textContent=T.nav_console;
bnb=document.getElementById('bnb-printers');if(bnb)bnb.lastChild.textContent=T.nav_printers;
bnb=document.getElementById('bnb-store');if(bnb)bnb.lastChild.textContent=T.nav_browser;
// Browser panel
setText('printers-panel-title','🖨 '+T.nav_printers);
setText('add-printer-btn-label',T.add_printer);
setText('apd-title',T.add_printer);
setText('skip-title',T.skip_title);
setText('skip-hint',T.skip_hint);
setText('d-btn-skip-label',T.skip_btn_label);
setText('fd-objects-hint',T.fd_objects_hint);
setText('apd-lbl-ip',T.apd_lbl_ip);
setText('apd-lbl-name',T.apd_lbl_name);
setText('store-panel-title','🗂 '+T.panel_browser_title);
var srb=document.getElementById('store-refresh-btn');if(srb)srb.textContent=T.store_refresh;
setText('store-empty',T.store_empty);
setText('sf-all',T.sf_all);setText('sf-ok',T.sf_ok);setText('sf-err',T.sf_err);setText('sf-new',T.sf_new);
setText('ss-date',T.ss_date);setText('ss-name',T.ss_name);setText('ss-dur',T.ss_dur);
// Dashboard card titles
setText('d-card-progress',T.card_progress);
setText('d-card-temps',T.card_temps);
setText('d-card-lightfan',T.card_light_fan);
setText('d-card-speed',T.card_speed);
setText('d-card-cam',T.card_cam);
setText('d-card-ams',T.panel_ams_title);
setText('d-lbl-elapsed',T.lbl_elapsed);
setText('d-lbl-remain',T.lbl_remaining);
setText('d-slicer-label',T.lbl_slicer_time);
setText('d-lbl-layers',T.lbl_layers);
setText('d-lbl-light',T.lbl_light);
setText('d-lbl-bed',T.label_bed);
// Dashboard buttons
setText('d-btn-pause',T.btn_pause);
setText('d-btn-resume',T.btn_resume);
setText('d-btn-cancel',T.btn_cancel);
setText('cam-toggle-btn',camOn?T.btn_cam_stop:T.btn_cam_start);
setText('cam-placeholder-txt',T.cam_placeholder);
// Temp labels
document.querySelectorAll('.lbl-set').forEach(e=>e.textContent=T.label_set);
document.querySelectorAll('.lbl-off').forEach(e=>e.textContent=T.label_off);
setText('d-chart-label',T.panel_temps_chart);
// Axis labels
setText('ptitle-motion-xy',T.panel_motion_xy);
setText('ptitle-motion-z',T.panel_motion_z);
document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z);
document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy);
document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all);
document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors);
document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step);
document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':','')));
// Console
setText('ptitle-console',T.panel_console_title);
// Settings modal
setText('modal-title-settings',T.settings_title);
setText('modal-sec-connection',T.settings_connection);
setText('modal-sec-print',T.settings_print);
setText('modal-sec-poll',T.settings_poll);
setText('modal-sec-version',T.settings_version);
setText('btn-save-settings',T.settings_save);
setText('lbl-printer-name',T.settings_printer_name);
setText('lbl-printer-ip',T.settings_printer_ip);
setText('lbl-mqtt-port',T.settings_mqtt_port);
setText('lbl-username',T.settings_username);
setText('lbl-password',T.settings_password);
setText('lbl-device-id',T.settings_device_id);
setText('lbl-mode-id',T.settings_mode_id);
setText('lbl-default-slot',T.settings_default_slot);
setText('opt-slot-auto',T.settings_slot_auto);
setText('lbl-auto-leveling',T.settings_auto_leveling);
setText('lbl-camera-on-print',T.settings_camera_on_print);
setText('lbl-update-check',T.update_check);
setText('lbl-update-apply',T.update_apply);
// Speed buttons
setText('d-spd-lbl-1',T.speed_silent.replace(/^\S+\s/,''));
setText('d-spd-lbl-2',T.speed_normal.replace(/^\S+\s/,''));
setText('d-spd-lbl-3',T.speed_sport.replace(/^\S+\s/,''));
// AMS feed/unload
document.querySelectorAll('.lbl-feed').forEach(e=>e.textContent=T.lbl_feed);
document.querySelectorAll('.lbl-unload').forEach(e=>e.textContent=T.lbl_unload);
for(var i=0;i<4;i++){
setText('d-card-ace-dry-'+i,'ACE '+(i+1)+' - '+(T.ace_dry_dryer||'Dryer'));
setText('d-ace-auto-refill-label-'+i,T.ace_dry_auto_refill||'Auto Refill');
setText('d-ace-drying-enable-label-'+i,T.ace_dry_enable||'Enable Drying');
setText('d-ace-dry-humidity-label-'+i,(T.ace_dry_humidity||'Humidity')+':');
setText('d-ace-dry-current-temp-label-'+i,(T.ace_dry_current_temp||'Current Temp')+':');
setText('d-ace-dry-target-label-'+i,(T.ace_dry_temp_line||'Drying Temperature')+':');
setText('d-ace-dry-time-label-'+i,(T.ace_dry_time_line||'Drying Time')+':');
setText('d-ace-dry-chart-label-'+i,T.ace_dry_chart||'History (Temp/Humidity)');
var adTemp=document.getElementById('ace-dry-temp-'+i);if(adTemp)adTemp.setAttribute('placeholder',T.ace_dry_temp);
var adDur=document.getElementById('ace-dry-duration-'+i);if(adDur)adDur.setAttribute('placeholder',T.ace_dry_duration);
}
setText('ace-dry-dialog-title',T.ace_dry_dialog_title||'Dryer Temp/Time Settings');
setText('ace-dry-dialog-temp-label',T.ace_dry_dialog_temp||'Temperature (30-80°C)');
setText('ace-dry-dialog-time-label',T.ace_dry_dialog_time||'Rem. Time (h:m:s)');
setText('ace-dry-dialog-custom-name-label',T.ace_dry_dialog_custom_name||'Custom Name');
setText('ace-dry-dialog-cancel',T.ace_dry_dialog_cancel||'Cancel');
setText('ace-dry-dialog-confirm',T.ace_dry_dialog_confirm||'Confirm');
setText('ace-dry-dialog-reset-default',T.ace_dry_dialog_reset_default||'Reset to Default');
setText('ace-dry-dialog-save-preset',T.ace_dry_dialog_save_restart||'Save & Restart');
aceDryDialogSyncCustomButtonNames();
// conn-btn text (nur wenn nicht im Übergangszustand)
updateConnBtn();
// Slot-Edit-Dialog
setText('lbl-slot-color',T.slot_edit_color);
setText('lbl-slot-material',T.slot_edit_material);
setText('btn-slot-edit-save',T.slot_edit_save);
updateSlotEditFeedButton();
var mi=document.getElementById('slot-edit-mat');if(mi)mi.setAttribute('placeholder',T.slot_edit_custom);
setText('logdir-all',T.log_dir_all);
setText('loglvl-all',T.log_dir_all);
setText('log-lbl-level',T.log_lvl_label);
setText('file-ready-btn',T.file_ready_btn);
setText('file-slots-btn',T.file_slots_btn);
setText('file-cancel-btn',T.file_cancel_btn);
setText('file-cancel-btn',T.file_cancel_btn);
}
function setText(id,txt){var el=document.getElementById(id);if(el)el.textContent=txt;}
function ensureAceDryCards(){
var grid=document.getElementById('d-ace-dry-grid');
if(!grid||grid.getAttribute('data-init')==='1')return;
var html='';
for(var i=0;i<4;i++){
html+='<div class="card" id="d-ace-dry-card-'+i+'" style="display:none">'
+'<div class="card-title"><span>♨</span> <span id="d-card-ace-dry-'+i+'">ACE '+(i+1)+' - Dryer</span></div>'
+'<div style="display:flex;justify-content:space-between;gap:10px;font-size:14px;color:var(--txt2);margin-bottom:10px">'
+'<span><span id="d-ace-dry-current-temp-label-'+i+'">Temperature:</span> <span id="d-ace-dry-current-temp-'+i+'" style="font-size:30px;font-weight:700;color:#ff8c2f;line-height:1">-</span></span>'
+'<span><span id="d-ace-dry-humidity-label-'+i+'">Humidity:</span> <span id="d-ace-dry-humidity-'+i+'" style="font-size:30px;font-weight:700;color:#3aa8ff;line-height:1">-</span></span>'
+'</div>'
+'<div style="height:1px;background:var(--border);margin-bottom:10px"></div>'
+'<div style="display:flex;justify-content:space-between;gap:10px;font-size:14px;color:var(--txt2);margin-bottom:10px">'
+'<span><span id="d-ace-dry-target-label-'+i+'">Drying Temperature:</span> <span id="d-ace-dry-target-'+i+'" style="font-size:30px;font-weight:700;color:#ff8c2f;line-height:1">-</span></span>'
+'<span><span id="d-ace-dry-time-label-'+i+'">Drying Time:</span> <span id="d-ace-dry-time-'+i+'" style="font-size:30px;font-weight:700;color:#fff;line-height:1">-</span></span>'
+'</div>'
+'<div style="margin-bottom:10px">'
+'<button onclick="openAceDryDialog('+i+')" title="Edit Dryer Temp/Time Settings" style="width:100%;padding:8px 10px;border-radius:8px;border:1px solid var(--accent);background:var(--accent);color:#000;cursor:pointer;font-size:13px;font-weight:600;line-height:1.2">Set Temp/Time</button>'
+'</div>'
+'<div style="height:1px;background:var(--border);margin-bottom:10px"></div>'
+'<div class="toggle-row" style="margin-bottom:10px">'
+'<span class="toggle-label" id="d-ace-auto-refill-label-'+i+'">Auto Refill</span>'
+'<label class="toggle">'
+'<input type="checkbox" id="ace-auto-refill-toggle-'+i+'" onchange="aceAutoRefillToggle('+i+')">'
+'<span class="toggle-track"></span>'
+'<span class="toggle-thumb"></span>'
+'</label>'
+'</div>'
+'<div class="toggle-row" style="margin-bottom:8px">'
+'<span class="toggle-label" id="d-ace-drying-enable-label-'+i+'">Enable Drying</span>'
+'<label class="toggle">'
+'<input type="checkbox" id="ace-dry-enable-toggle-'+i+'" onchange="aceDryToggle('+i+',this.checked)">'
+'<span class="toggle-track"></span>'
+'<span class="toggle-thumb"></span>'
+'</label>'
+'</div>'
+'</div>';
}
grid.innerHTML=html;
grid.setAttribute('data-init','1');
}
(function(){
var l=localStorage.getItem('lang')||'de';
currentLang=l;T=l==='de'?LANG_DE:LANG_EN;
document.getElementById('lang-btn').textContent=l==='de'?'EN':'DE';
document.documentElement.setAttribute('lang',l);
// defer until DOM ready
window.addEventListener('DOMContentLoaded',function(){
applyLang();
// Kein Drucker konfiguriert? → direkt in den Drucker-Tab (zeigt "+ Drucker hinzufügen")
fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){
if(!d.result||!d.result.length){showPanel('printers');loadPrinterTab();}
}).catch(function(){});
});
})();
// ── Panel nav ──
function showPanel(id){
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
document.getElementById('panel-'+id).classList.add('active');
document.querySelectorAll('.nav-btn,.bnav-btn').forEach(b=>b.classList.remove('active'));
var nb=document.getElementById('nb-'+id);if(nb)nb.classList.add('active');
var bnb=document.getElementById('bnb-'+id);if(bnb)bnb.classList.add('active');
currentPanel=id;
}
// ── Console log ──
var consoleLogs=[];
var logAutoScroll=true;
var logBadgeCount=0;
var logDirFilter='all'; // 'all'|'rx'|'tx'
var logLevelFilter='all'; // 'all'|'err'|'warn'
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+'] ':'';
var fullMsg=label+entry.msg;
// Wiederholungen als Zähler zusammenfassen (×N) statt N identische Zeilen.
var last=consoleLogs[consoleLogs.length-1];
if(last&&last.msg===fullMsg&&last.cls===cls){
last.count=(last.count||1)+1;
last.ts=entry.ts; // letzte Sichtung
renderLog();
return;
}
consoleLogs.push({ts:entry.ts,msg:fullMsg,cls:cls,count:1});
if(consoleLogs.length>500)consoleLogs.shift();
// Badge + Toast 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;}});
}
if(cls==='msg-err')showToast(entry.msg.split('\n')[0]);
renderLog();
}
// Kurze rote Snackbar bei Fehlern (auch wenn Konsole-Tab nicht offen).
var _toastTimer=null;
function showToast(msg){
var t=document.getElementById('kx-toast');
if(!t){
t=document.createElement('div'); t.id='kx-toast';
t.style.cssText='position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:var(--err);color:#fff;padding:10px 18px;border-radius:8px;font-size:13px;z-index:9999;max-width:90vw;box-shadow:0 4px 16px rgba(0,0,0,.4);cursor:pointer';
t.onclick=function(){showPanel('console');t.style.display='none';};
document.body.appendChild(t);
}
t.textContent='⚠ '+msg;
t.style.display='block';
clearTimeout(_toastTimer);
_toastTimer=setTimeout(function(){t.style.display='none';},6000);
}
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 setLogLevel(lvl){
logLevelFilter=lvl;
document.querySelectorAll('.log-lvl-btn').forEach(function(b){
b.style.background=b.id==='loglvl-'+lvl?'var(--accent)':'var(--raised)';
b.style.color=b.id==='loglvl-'+lvl?'#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(logLevelFilter==='err'&&l.cls!=='msg-err')return false;
if(logLevelFilter==='warn'&&l.cls!=='msg-err'&&l.cls!=='msg-warn')return false;
if(fl&&!m.toLowerCase().includes(fl))return false;
return true;
});
var savedScroll=logAutoScroll?null:el.scrollTop;
el.innerHTML=rows.map(function(l){
var cnt=(l.count&&l.count>1)?' <span style="opacity:.7">(×'+l.count+')</span>':'';
return '<div><span class="ts">'+l.ts+'</span><span class="'+l.cls+'">'+escHtml(l.msg)+'</span>'+cnt+'</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 fmtHmsFromSec(total){
total=Math.max(0,parseInt(total||0,10));
var h=Math.floor(total/3600);
var mm=Math.floor((total%3600)/60);
var ss=total%60;
return h+':'+String(mm).padStart(2,'0')+':'+String(ss).padStart(2,'0');
}
function post(url,body){return fetch(_apiUrl(url),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})}
function clamp(v,lo,hi){return Math.min(hi,Math.max(lo,v))}
// ── Apply state to DOM ──
function applyState(){
var s=S;
_syncAceDryPresetsFromServer(s.ace_dry_presets);
// connection error banner nur wenn überhaupt ein Drucker konfiguriert ist
var banner=document.getElementById('conn-error-banner');
if(banner){if(s.connection_error&&_printers.length>0){banner.textContent='⚠ '+(T.lbl_conn_error||'Connection error:')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}}
var frb=document.getElementById('file-ready-banner');
if(frb){
if(s.file_ready&&s.print_state==='standby'){
document.getElementById('file-ready-name').textContent=s.file_ready;
frb.style.display='flex';
}else{frb.style.display='none';}
}
// skip-button (mid-print) nur sichtbar wenn aktuell gedruckt wird
var skipBtn=document.getElementById('d-btn-skip');
if(skipBtn){
var printing=(s.print_state==='printing'||s.print_state==='paused');
skipBtn.style.display=printing?'':'none';
}
// header
var b=document.getElementById('h-badge');
b.className='hbadge '+s.print_state;
document.getElementById('h-state').textContent=T['kobra_'+s.kobra_state]||s.kobra_state||T.header_status_standby;
var _pn=_printers.length===0?'':((_activePrinter&&_activePrinter.name)||s.printer_name);
var _el=document.getElementById('h-pname');if(_el)_el.textContent=_pn;
var _el2=document.getElementById('h-pname-single');if(_el2)_el2.textContent=_pn;
var hv=document.getElementById('h-version');if(hv&&s.version)hv.textContent='v'+s.version;
// temps
var nt=document.getElementById('d-nt');if(nt)nt.textContent=s.nozzle_temp.toFixed(1);
var ntt=document.getElementById('d-nt-t');if(ntt)ntt.textContent=s.nozzle_target.toFixed(0);
var bt=document.getElementById('d-bt');if(bt)bt.textContent=s.bed_temp.toFixed(1);
var btt=document.getElementById('d-bt-t');if(btt)btt.textContent=s.bed_target.toFixed(0);
// temp bars (dashboard)
var nb=document.getElementById('d-ntbar');if(nb)nb.style.width=clamp(s.nozzle_temp/300*100,0,100)+'%';
var bb=document.getElementById('d-btbar');if(bb)bb.style.width=clamp(s.bed_temp/120*100,0,100)+'%';
// progress
var pct=Math.round(s.progress*100);
var dpct=document.getElementById('d-pct');if(dpct)dpct.textContent=pct;
var dpbar=document.getElementById('d-pbar');if(dpbar)dpbar.style.width=pct+'%';
var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'';
var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers;
var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration);
var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'';
var dslrow=document.getElementById('d-slicer-row');
var dsltime=document.getElementById('d-slicer-time');
if(dslrow&&dsltime){
if(s.slicer_time>0){dslrow.style.display='';dsltime.textContent=fmtTime(s.slicer_time);}
else{dslrow.style.display='none';}
}
var fn=s.filename||'';
var dfname=document.getElementById('d-fname');if(dfname){dfname.textContent=fn;dfname.title=fn};
var pfname=document.getElementById('p-fname');if(pfname){pfname.textContent=fn;pfname.title=fn};
var cfo=document.getElementById('cam-fname');if(cfo)cfo.textContent=fn!==''?fn:'';
// thumbnail
var thumb=document.getElementById('d-thumbnail');
if(thumb){
if(s.thumbnail){
thumb.src='data:image/png;base64,'+s.thumbnail;
thumb.style.display='block';
} else {
thumb.style.display='none';
thumb.src='';
}
}
// light/fan sync
document.getElementById('d-light-toggle').checked=s.light_on;
var dfan=document.getElementById('d-fan');if(dfan)dfan.value=s.fan_speed;
var dfanval=document.getElementById('d-fan-val');if(dfanval)dfanval.textContent=s.fan_speed;
// speed mode buttons
var spdWidths={1:25,2:55,3:90};
[1,2,3].forEach(function(m){
var b=document.getElementById('d-spd-'+m);
if(b) b.classList.toggle('spd-active', s.print_speed_mode===m);
});
var spdBar=document.getElementById('d-spd-bar');
if(spdBar) spdBar.style.width=(spdWidths[s.print_speed_mode]||55)+'%';
var amsTitle=document.getElementById('d-card-ams');
if(amsTitle){
var baseTitle=T.card_ams||'Filament';
var modeMap={toolhead:'Toolhead',ace_direct:'ACE Direct',ace_hub:'ACE Hub'};
var modeTxt=modeMap[s.filament_mode]||'';
amsTitle.textContent=modeTxt?(baseTitle+' - '+modeTxt):baseTitle;
}
ensureAceDryCards();
var dry=s.ace_drying||{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]};
var units=(dry.units||[]);
var unitMap={};
units.forEach(function(u){var id=Number(u.id);if(id>=0&&id<=3)unitMap[id]=u;});
var aceMode=s.filament_mode==='ace_direct'||s.filament_mode==='ace_hub';
var detected=(s.ace_units||[]).filter(function(id){return id>=0&&id<=3;});
if(!detected.length){
Object.keys(unitMap).forEach(function(k){detected.push(Number(k));});
}
if(!detected.length){
(s.ams_slots||[]).forEach(function(sl){var id=Number(sl.box_id);if(id>=0&&id<=3&&detected.indexOf(id)<0)detected.push(id);});
}
detected.sort(function(a,b){return a-b;});
var aceWrap=document.getElementById('d-ace-dry-wrap');
if(aceWrap)aceWrap.style.display=(aceMode&&detected.length)?'contents':'none';
for(var i=0;i<4;i++){
var card=document.getElementById('d-ace-dry-card-'+i);
if(!card)continue;
var show=aceMode&&detected.indexOf(i)>=0;
card.style.display=show?'':'none';
if(!show)continue;
var ud=unitMap[i]||dry;
var refillToggle=document.getElementById('ace-auto-refill-toggle-'+i);
var autoFeedMap=s.ace_auto_feed||{};
if(refillToggle&&!_aceAutoFeedPending[i]){
var afVal=autoFeedMap.hasOwnProperty(String(i))?Number(autoFeedMap[String(i)]):(_aceAutoRefillGet(i)?1:0);
refillToggle.checked=afVal===1;
}
var dryToggle=document.getElementById('ace-dry-enable-toggle-'+i);
if(dryToggle)dryToggle.checked=Number(ud.status||0)>0;
var hh=document.getElementById('d-ace-dry-humidity-'+i);
if(hh){
var hv=(ud.humidity===null||ud.humidity===undefined||ud.humidity==='')?null:Number(ud.humidity);
hh.textContent=(hv===null||Number.isNaN(hv))?'-':(Math.round(hv)+'%');
}
var ht=document.getElementById('d-ace-dry-current-temp-'+i);
if(ht){
var ct=(ud.current_temp===null||ud.current_temp===undefined||ud.current_temp==='')?null:Number(ud.current_temp);
ht.textContent=(ct===null||Number.isNaN(ct))?'-':(ct.toFixed(1)+'°C');
}
var prof=_aceDryProfileGet(i);
var useSec=(Number(ud.status||0)>0&&Number(ud.remain_time)>0)
?Number(ud.remain_time||0)*60
:prof.duration_sec;
var showTemp=(Number(ud.status||0)>0&&Number(ud.target_temp)>0)?Number(ud.target_temp):prof.temp;
var dryTempEl=document.getElementById('d-ace-dry-target-'+i);
if(dryTempEl)dryTempEl.textContent=showTemp+'°C';
var dryTimeEl=document.getElementById('d-ace-dry-time-'+i);
if(dryTimeEl)dryTimeEl.textContent=fmtHmsFromSec(useSec);
}
// AMS
if(s.ams_slots&&s.ams_slots.length){
window._amsSlots=s.ams_slots;
// Group by box_id (-1=Toolhead, 0=ACE 1, 1=ACE 2, ...)
var boxMap={};
s.ams_slots.forEach(function(slot,i){
var bid=slot.box_id!=null?slot.box_id:-1;
if(!boxMap[bid])boxMap[bid]=[];
boxMap[bid].push({slot:slot,arrIdx:i});
});
var boxIds=Object.keys(boxMap).map(Number).sort(function(a,b){return a-b});
var acePresent=boxIds.some(function(b){return b>=0;});
var html='';
boxIds.forEach(function(bid){
var entries=boxMap[bid];
var label=bid===-1
?(acePresent?'Toolhead (Slots 13)':'Toolhead')
:('ACE '+(bid+1));
html+='<div class="ams-box-group">'
+'<div class="ams-box-label">'+label+'</div>'
+'<div class="ams-box-slots">';
entries.forEach(function(e){
var slot=e.slot;var i=e.arrIdx;
var empty=slot.status!==5;
var rgb=empty?[80,80,80]:(Array.isArray(slot.color)?slot.color:[128,128,128]);
var col='rgb('+rgb[0]+','+rgb[1]+','+rgb[2]+')';
var globalIdx=slot.global_index!=null?slot.global_index:i;
var active=slot.status===1||slot.active;
var loaded=(s.ams_loaded_slot!=null&&s.ams_loaded_slot>=0&&globalIdx===s.ams_loaded_slot);
var activity=(slot.activity||'');
var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'');
var slotLabel='Slot '+(globalIdx+1);
html+='<div class="ams-slot'+(active?' active':'')+(loaded?' loaded':'')+(activity?' '+activity:'')+(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">'+slotLabel+'</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>';
});
if(bid===-1&&acePresent){
html+='<div class="ams-slot ams-slot-bridge" aria-label="Slot 4 connected to ACE">'
+'<div class="bridge-chip">ACE</div>'
+'</div>';
}
html+='</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 cop=document.getElementById('s-camera-on-print');if(cop)cop.checked=!!d.camera_on_print;
});
var v=localStorage.getItem('pollInterval')||'2000';
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
var pb=document.getElementById('poll-'+Math.round(parseInt(v)/1000));
if(pb)pb.classList.add('active');
document.getElementById('s-version-label').textContent='v'+('__VERSION__'||'?');
document.getElementById('update-status').textContent='';
document.getElementById('btn-update-apply').style.display='none';
var cl=document.getElementById('update-changelog');if(cl)cl.style.display='none';
_updateTag='';_updateUrl='';
document.getElementById('settings-modal').classList.add('open');
}
function closeSettings(){
document.getElementById('settings-modal').classList.remove('open');
}
// ── AMS Slot Edit ──
var _slotEditIndex=-1;
var _slotEditLoaded=false;
var _MAT_PRESETS=['PLA','PETG','ABS','ASA','TPU','PA','PC','HIPS'];
function updateSlotEditFeedButton(){
var btn=document.getElementById('btn-slot-edit-feed');
if(!btn)return;
if(_slotEditIndex<0){
btn.style.display='none';
return;
}
btn.style.display='';
btn.textContent=_slotEditLoaded?(T.slot_edit_unload||'⬆ Unload'):(T.slot_edit_load||'⬇ Load');
}
function openSlotEdit(i){
var slot=(window._amsSlots||[])[i]||{};
var globalIdx=slot.global_index!=null?slot.global_index:(slot.index!=null?slot.index:i);
_slotEditIndex=globalIdx;
_slotEditLoaded=(S.ams_loaded_slot!=null&&S.ams_loaded_slot===globalIdx);
document.getElementById('slot-edit-title').textContent=T.slot_edit_title+' '+(globalIdx+1);
var rgb=Array.isArray(slot.color)?slot.color:[128,128,128];
var hex='#'+rgb.map(function(v){return('0'+Math.min(255,v).toString(16)).slice(-2)}).join('');
var ci=document.getElementById('slot-edit-color');
ci.value=hex;
document.getElementById('slot-edit-preview').style.background=hex;
var mat=(slot.type||'PLA').toUpperCase();
document.getElementById('slot-edit-mat').value=mat;
var btns=document.getElementById('slot-mat-btns');
btns.innerHTML=_MAT_PRESETS.map(function(m){
return '<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('');
updateSlotEditFeedButton();
document.getElementById('slot-edit-modal').classList.add('open');
}
function closeSlotEdit(){
_slotEditIndex=-1;
document.getElementById('slot-edit-modal').classList.remove('open');
}
function slotEditFeed(){
if(_slotEditIndex<0)return;
var type=_slotEditLoaded?2:1;
amsFeed(type,_slotEditIndex)
.then(function(){
_slotEditLoaded=!_slotEditLoaded;
updateSlotEditFeedButton();
poll();
})
.catch(function(){});
}
function startReadyFile(){
var btn=document.getElementById('file-ready-btn');
if(btn){btn.disabled=true;btn.textContent='…';}
post('/printer/print/start',{filename:S.file_ready})
.then(function(r){return r.json();})
.then(function(r){
document.getElementById('file-ready-banner').style.display='none';
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
})
.catch(function(e){
clog((T.log_error||'Error:')+' '+e,'msg-err');
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
});
}
function cancelReadyFile(){
post('/api/file_ready/clear',{})
.then(function(){document.getElementById('file-ready-banner').style.display='none';});
}
function selectMatPreset(m){
document.getElementById('slot-edit-mat').value=m;
highlightMatBtn(m);
}
function highlightMatBtn(val){
document.querySelectorAll('.mat-preset-btn').forEach(function(b){
var on=b.getAttribute('data-mat')===val.toUpperCase();
b.style.background=on?'var(--accent)':'var(--raised)';
b.style.color=on?'#fff':'var(--txt2)';
});
}
function hexToRgb(hex){
var r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16);
return[r,g,b];
}
function saveSlotEdit(){
var hex=document.getElementById('slot-edit-color').value;
var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA';
var color=hexToRgb(hex);
post('/api/ams/set_slot',{index:_slotEditIndex,type:mat,color:color})
.then(function(r){return r.json();})
.then(function(r){
closeSlotEdit();
clog((T.slot_edit_ok||'AMS Slot')+' '+(_slotEditIndex+1)+': '+mat+' '+hex,'msg-ok');
})
.catch(function(e){clog('Fehler: '+e,'msg-err');});
}
document.addEventListener('DOMContentLoaded',function(){
document.getElementById('s-printer-ip').addEventListener('input',function(){
var hint=document.getElementById('lbl-ip-hint');
if(this.value.includes(':')){hint.textContent=T.hint_ip_no_port;hint.style.display='block';}
else{hint.style.display='none';}
});
});
function setPoll(ms){
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
var id='poll-'+Math.round(ms/1000);
var pb=document.getElementById(id);if(pb)pb.classList.add('active');
localStorage.setItem('pollInterval',ms);
clearInterval(pollTimer);
pollTimer=setInterval(poll,ms);
}
function saveSettings(){
var btn=document.getElementById('btn-save-settings');
btn.disabled=true;btn.textContent='…';
post('/api/settings',{
printer_name: document.getElementById('s-printer-name').value,
printer_ip: document.getElementById('s-printer-ip').value,
mqtt_port: parseInt(document.getElementById('s-mqtt-port').value)||9883,
username: document.getElementById('s-username').value,
password: document.getElementById('s-password').value,
device_id: document.getElementById('s-device-id').value,
mode_id: document.getElementById('s-mode-id').value,
default_ams_slot: document.getElementById('s-default-slot').value,
auto_leveling: document.getElementById('s-auto-leveling').checked?1:0,
camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0,
}).then(function(){
btn.textContent=T.update_restarting;
setTimeout(function(){
btn.disabled=false;
setText('btn-save-settings',T.settings_save);
closeSettings();
poll();
},4000);
}).catch(function(e){
btn.disabled=false;setText('btn-save-settings',T.settings_save);
clog('Settings-Fehler: '+e,'msg-err');
});
}
function checkUpdate(){
var sb=document.getElementById('update-status');
sb.textContent=T.update_checking;
document.getElementById('btn-update-apply').style.display='none';
_updateTag='';_updateUrl='';
fetch(_apiUrl('/api/update/check')).then(function(r){return r.json()}).then(function(d){
if(d.error){sb.textContent=T.update_error+': '+d.error;return;}
var cl=document.getElementById('update-changelog');
if(d.changelog&&d.changelog.trim()){cl.textContent=d.changelog;cl.style.display='block';}
else{cl.style.display='none';}
if(d.update_available){
sb.textContent='v'+d.latest+' '+T.update_available;
sb.style.color='var(--ok)';
_updateTag=d.tag;_updateUrl=d.download_url;
document.getElementById('btn-update-apply').style.display='inline-block';
} else {
sb.textContent=T.update_none;
sb.style.color='var(--txt2)';
}
}).catch(function(e){sb.textContent=T.update_error+': '+e;});
}
function applyUpdate(){
if(!_updateUrl)return;
var sb=document.getElementById('update-status');
var btn=document.getElementById('btn-update-apply');
btn.disabled=true;sb.textContent=T.update_applying;
post('/api/update/apply',{download_url:_updateUrl,tag:_updateTag}).then(function(){
sb.textContent=T.update_restarting;
closeSettings();
setTimeout(function(){poll();},5000);
}).catch(function(e){
btn.disabled=false;sb.textContent=T.update_error+': '+e;
});
}
// ── Poll ──
async function poll(){
try{
var r=await fetch(_apiUrl('/api/state'));
if(!r.ok)return;
var d=await r.json();
Object.assign(S,d);
applyState();
updateHistory();
}catch(e){clog(T.log_poll_error+' '+e,'msg-err')}
}
var pollTimer;
(function(){
var ms=parseInt(localStorage.getItem('pollInterval')||'2000');
initPrinters();
poll();pollTimer=setInterval(poll,ms);
})();
// ── Print actions ──
function printAction(a){
post('/printer/print/'+a,{}).then(function(){clog('Druck: '+a,'msg-ok');poll()})
.catch(function(e){clog('Fehler: '+e,'msg-err')});
}
function confirmCancel(){if(confirm('Druck wirklich abbrechen?'))printAction('cancel')}
// ── Axis motion ──
// axis codes: 0=X, 1=Y, 2=Z
// move_type 1=relative, distance positive/negative
function getStep(){return currentStep}
function setStep(btn,v){
currentStep=v;
document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('step-display').textContent=v;
}
function move(axis,dir,dist){
// axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z
var axisMap={0:1,1:2,2:3};
post('/api/axis',{axis:axisMap[axis],move_type:1,distance:dir*dist})
.then(function(){clog('Achse '+(axis===0?'X':axis===1?'Y':'Z')+' '+(dir>0?'+':'')+dir*dist+'mm','msg-ok')})
.catch(function(e){clog('Achse-Fehler: '+e,'msg-err')});
}
function homeAll(){
post('/api/axis',{axis:5,move_type:2,distance:0})
.then(function(){clog('Home All','msg-ok')})
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
}
function homeXY(){
post('/api/axis',{axis:4,move_type:2,distance:0})
.then(function(){clog('Home XY','msg-ok')})
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
}
function homeZ(){
post('/api/axis',{axis:3,move_type:2,distance:0})
.then(function(){clog('Home Z','msg-ok')})
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
}
function disableMotors(){
post('/api/axis',{action:'turnOff'})
.then(function(){clog('Motors Off','msg-ok')})
.catch(function(e){clog('Motors-Fehler: '+e,'msg-err')});
}
// ── Temperature ──
function setNozzle(){
var v=parseFloat(document.getElementById('p-nozzle-inp').value||0);
post('/api/temperature',{nozzle:v,bed:S.bed_target})
.then(function(){clog('Nozzle → '+v+'°C','msg-ok')})
.catch(function(e){clog('Temp-Fehler: '+e,'msg-err')});
}
function setBed(){
var v=parseFloat(document.getElementById('p-bed-inp').value||0);
post('/api/temperature',{nozzle:S.nozzle_target,bed:v})
.then(function(){clog(T.label_bed+' → '+v+'°C','msg-ok')})
.catch(function(e){clog('Temp-Fehler: '+e,'msg-err')});
}
// ── Light ──
function setLight(){
var on=document.getElementById('d-light-toggle').checked;
post('/api/light',{on:on,brightness:80})
.then(function(){clog('Licht '+(on?'an, '+br+'%':'aus'),'msg-ok')})
.catch(function(e){clog('Licht-Fehler: '+e,'msg-err')});
}
// ── Print Speed ──
function setSpeed(mode){
S.print_speed_mode=mode;
[1,2,3].forEach(function(m){
var b=document.getElementById('d-spd-'+m);
if(b) b.classList.toggle('spd-active',m===mode);
});
post('/api/speed',{mode:mode})
.catch(function(e){clog('Speed-Fehler: '+e,'msg-err')});
}
// ── Fan ──
function setFan(){
var v=parseInt(document.getElementById('d-fan').value);
document.getElementById('d-fan-val').textContent=v;
post('/api/fan',{speed:v})
.then(function(){clog('Lüfter → '+v+'%','msg-ok')})
.catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')});
}
function quickFan(v){
document.getElementById('d-fan').value=v;
document.getElementById('d-fan-val').textContent=v;
post('/api/fan',{speed:v})
.then(function(){clog('Lüfter → '+v+'%','msg-ok')})
.catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')});
}
// ── AMS ──
function amsFeed(type,slotIndex){
var globalIdx;
if(typeof slotIndex==='number'&&slotIndex>=0){
globalIdx=slotIndex;
}else{
var i=parseInt(document.getElementById('ams-slot-sel').value);
var slot=(window._amsSlots||[])[i]||{};
globalIdx=slot.global_index!=null?slot.global_index:i;
}
return post('/api/ams/feed',{slot_index:globalIdx,type:type})
.then(function(){clog((type===1?T.lbl_feed:T.lbl_unload)+' Slot '+(globalIdx+1),'msg-ok')})
.catch(function(e){clog('AMS-Fehler: '+e,'msg-err');throw e;});
}
// ── Camera ──
function camStart(){
var img=document.getElementById('cam-img');
var ph=document.getElementById('cam-placeholder');
var sp=document.getElementById('cam-spinner');
ph.style.display='none';
img.style.display='none';
sp.style.display='block';
post('/api/camera/start',{}).then(function(){
img.onerror=function(){
sp.style.display='none';
img.style.display='none';
ph.style.display='flex';
camOn=false;
document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera';
clog((T.log_error||'Fehler:')+' Stream nicht verfügbar','msg-err');
};
img.src='/api/camera/stream?t='+Date.now();
camOn=true;
document.getElementById('cam-toggle-btn').textContent=T.btn_cam_stop||'◼ Kamera';
clog((T.log_cam_start||'Kamera gestartet'),'msg-ok');
// MJPEG liefert kein onload Spinner nach kurzem Timeout ausblenden
setTimeout(function(){
sp.style.display='none';
img.style.display='block';
},1200);
}).catch(function(e){
sp.style.display='none';
ph.style.display='flex';
clog((T.log_error||'Fehler:')+' '+e,'msg-err');
});
}
function camStop(){
var img=document.getElementById('cam-img');
post('/api/camera/stop',{}).catch(function(){});
img.src='';
img.style.display='none';
document.getElementById('cam-placeholder').style.display='flex';
camOn=false;
document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera';
clog((T.log_cam_stop||'Kamera gestoppt'),'msg-ok');
}
function aceDryStart(aceId){
aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0;
var prof=_aceDryProfileGet(aceId);
var t=parseInt(prof.temp,10);
var d=_aceDryDurationMinFromSec(prof.duration_sec);
t=Math.max(30,Math.min(80,t));
d=Math.max(10,Math.min(1440,d));
return post('/api/ace/dry',{action:'start',target_temp:t,duration:d,ace_id:aceId})
.then(function(r){return r.json();})
.then(function(r){
if(r.error){throw new Error(r.error);}
clog('ACE '+(aceId+1)+' - '+(T.ace_dry_dryer||'Dryer')+': '+(T.ace_dry_start||'start')+' ('+t+'°C, '+d+' min)','msg-ok');
poll();
})
.catch(function(e){clog('ACE-Fehler: '+e,'msg-err');});
}
var _aceAutoFeedPending={};
function aceAutoRefillToggle(aceId){
aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0;
var on=!!((document.getElementById('ace-auto-refill-toggle-'+aceId)||{}).checked);
_aceAutoFeedPending[aceId]=true;
fetch('/api/ace/auto_feed',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ace_id:aceId,on:on?1:0})})
.then(function(r){return r.json();})
.then(function(d){
delete _aceAutoFeedPending[aceId];
if(d.error){clog('Auto Refill error: '+d.error,'msg-err');var t=document.getElementById('ace-auto-refill-toggle-'+aceId);if(t)t.checked=!on;return;}
clog('ACE '+(aceId+1)+' - '+(T.ace_dry_auto_refill||'Auto Refill')+': '+(on?'ON':'OFF'),'msg-ok');
})
.catch(function(e){delete _aceAutoFeedPending[aceId];clog('Auto Refill error: '+e,'msg-err');var t=document.getElementById('ace-auto-refill-toggle-'+aceId);if(t)t.checked=!on;});
}
function openAceDryDialog(aceId){
aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0;
_aceDryDialogAceId=aceId;
_syncAceDryPresetsFromServer(S.ace_dry_presets);
_aceDryDialogPresetOriginals=JSON.parse(JSON.stringify(ACE_DRY_PRESETS));
aceDryDialogSyncCustomButtonNames();
var hasStored=Object.prototype.hasOwnProperty.call(aceDryProfiles,String(aceId));
var prof=_aceDryProfileGet(aceId);
if(hasStored&&prof.preset&&ACE_DRY_PRESETS[prof.preset]){
aceDryDialogPreset(prof.preset);
}else if(hasStored){
var sec=prof.duration_sec;
document.getElementById('ace-dry-dialog-temp').value=prof.temp;
document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600);
document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60);
document.getElementById('ace-dry-dialog-s').value=sec%60;
aceDryDialogHighlightPreset('');
}else{
aceDryDialogPreset('pla');
}
aceDryDialogUpdateSaveButton();
aceDryDialogUpdateResetButton();
var sb=document.getElementById('ace-dry-dialog-save-preset');
if(sb){sb.disabled=false;sb.textContent=T.ace_dry_dialog_save_restart||'Save & Restart';}
document.getElementById('ace-dry-dialog').classList.add('open');
}
function closeAceDryDialog(){
_aceDryDialogAceId=-1;
_aceDryDialogPresetOriginals={};
var sb=document.getElementById('ace-dry-dialog-save-preset');
if(sb)sb.style.display='none';
var rb=document.getElementById('ace-dry-dialog-reset-default');
if(rb)rb.style.display='none';
document.getElementById('ace-dry-dialog').classList.remove('open');
}
function aceDryDialogIsCustomPreset(key){
return /^custom_[123]$/.test(String(key||''));
}
function aceDryDialogSyncCustomButtonNames(){
['custom_1','custom_2','custom_3'].forEach(function(k){
var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+k+'"]');
if(b)b.textContent=(ACE_DRY_PRESETS[k]&&ACE_DRY_PRESETS[k].name)||('Custom '+k.slice(-1));
});
}
function aceDryDialogUpdateCustomNameUi(){
var row=document.getElementById('ace-dry-dialog-custom-name-row');
var input=document.getElementById('ace-dry-dialog-custom-name');
if(!row||!input)return;
if(!aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)){
row.style.display='none';
return;
}
row.style.display='flex';
input.value=(ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||'';
}
function aceDryDialogCurrentValues(){
var t=parseInt(document.getElementById('ace-dry-dialog-temp').value||45,10);
var h=parseInt(document.getElementById('ace-dry-dialog-h').value||0,10);
var m=parseInt(document.getElementById('ace-dry-dialog-m').value||0,10);
var s=parseInt(document.getElementById('ace-dry-dialog-s').value||0,10);
t=Math.max(30,Math.min(80,t));
h=Math.max(0,Math.min(24,h));
m=Math.max(0,Math.min(59,m));
s=Math.max(0,Math.min(59,s));
var totalSec=(h*3600)+(m*60)+s;
totalSec=Math.max(10*60,Math.min(24*3600,totalSec));
return {temp:t,duration_sec:totalSec};
}
function aceDryDialogUpdateSaveButton(){
var btn=document.getElementById('ace-dry-dialog-save-preset');
if(!btn)return;
var key=_aceDryDialogPresetKey||'';
if(!key||!ACE_DRY_PRESETS[key]){btn.style.display='none';return;}
var p=_aceDryDialogPresetOriginals[key]||ACE_DRY_PRESETS[key];
var cur=aceDryDialogCurrentValues();
var changed=(cur.temp!==Number(p.temp)||cur.duration_sec!==Number(p.duration_sec));
if(aceDryDialogIsCustomPreset(key)){
var nameInp=document.getElementById('ace-dry-dialog-custom-name');
var n=((nameInp&&nameInp.value)||'').trim();
var old=(p&&p.name?String(p.name):('Custom '+key.slice(-1))).trim();
if((n||old)!==old)changed=true;
}
btn.style.display=changed?'':'none';
}
function aceDryDialogUpdateResetButton(){
var btn=document.getElementById('ace-dry-dialog-reset-default');
if(!btn)return;
var key=_aceDryDialogPresetKey||'';
var d=ACE_DRY_PRESET_DEFAULTS[key];
if(!key||!d){btn.style.display='none';return;}
var cur=aceDryDialogCurrentValues();
var changed=(cur.temp!==Number(d.temp)||cur.duration_sec!==Number(d.duration_sec));
btn.style.display=changed?'':'none';
}
function aceDryDialogInputsChanged(){
if(aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)){
var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+_aceDryDialogPresetKey+'"]');
var i=document.getElementById('ace-dry-dialog-custom-name');
if(b&&i){
var t=(i.value||'').trim();
b.textContent=t||((ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||('Custom '+_aceDryDialogPresetKey.slice(-1)));
}
}
aceDryDialogUpdateSaveButton();
aceDryDialogUpdateResetButton();
}
function aceDryDialogHighlightPreset(presetKey){
_aceDryDialogPresetKey=presetKey||'';
document.querySelectorAll('.ace-dry-preset-btn').forEach(function(btn){
var on=(btn.getAttribute('data-preset')===presetKey);
btn.style.background=on?'var(--accent)':'var(--raised)';
btn.style.color=on?'#fff':'var(--txt2)';
btn.style.borderColor=on?'var(--accent)':'var(--border)';
});
aceDryDialogUpdateCustomNameUi();
}
function aceDryDialogPreset(presetKey){
var p=ACE_DRY_PRESETS[presetKey];
if(!p)return;
var sec=p.duration_sec;
document.getElementById('ace-dry-dialog-temp').value=p.temp;
document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600);
document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60);
document.getElementById('ace-dry-dialog-s').value=sec%60;
aceDryDialogHighlightPreset(presetKey);
aceDryDialogSyncCustomButtonNames();
aceDryDialogUpdateSaveButton();
aceDryDialogUpdateResetButton();
}
function resetAceDryPresetToDefault(){
var key=_aceDryDialogPresetKey||'';
var d=ACE_DRY_PRESET_DEFAULTS[key];
if(!key||!d)return;
var sec=Number(d.duration_sec)||0;
document.getElementById('ace-dry-dialog-temp').value=Number(d.temp)||45;
document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600);
document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60);
document.getElementById('ace-dry-dialog-s').value=sec%60;
aceDryDialogInputsChanged();
}
function saveAceDryPresetAndRestart(){
var key=_aceDryDialogPresetKey||'';
var btn=document.getElementById('ace-dry-dialog-save-preset');
if(!key||!ACE_DRY_PRESETS[key]||!btn)return;
var cur=aceDryDialogCurrentValues();
if(!ACE_DRY_PRESETS[key])ACE_DRY_PRESETS[key]={};
ACE_DRY_PRESETS[key].temp=cur.temp;
ACE_DRY_PRESETS[key].duration_sec=cur.duration_sec;
if(aceDryDialogIsCustomPreset(key)){
var nameInp=document.getElementById('ace-dry-dialog-custom-name');
var nm=((nameInp&&nameInp.value)||'').trim();
ACE_DRY_PRESETS[key].name=nm||('Custom '+key.slice(-1));
}
btn.disabled=true;
btn.textContent='…';
fetch(_apiUrl('/api/settings')).then(function(r){return r.json();}).then(function(d){
d.ace_dry_presets={
pla:{temp:ACE_DRY_PRESETS.pla.temp,duration_sec:ACE_DRY_PRESETS.pla.duration_sec},
pla_plus:{temp:ACE_DRY_PRESETS.pla_plus.temp,duration_sec:ACE_DRY_PRESETS.pla_plus.duration_sec},
petg:{temp:ACE_DRY_PRESETS.petg.temp,duration_sec:ACE_DRY_PRESETS.petg.duration_sec},
tpu:{temp:ACE_DRY_PRESETS.tpu.temp,duration_sec:ACE_DRY_PRESETS.tpu.duration_sec},
abs_asa:{temp:ACE_DRY_PRESETS.abs_asa.temp,duration_sec:ACE_DRY_PRESETS.abs_asa.duration_sec},
pa_pc:{temp:ACE_DRY_PRESETS.pa_pc.temp,duration_sec:ACE_DRY_PRESETS.pa_pc.duration_sec},
custom_1:{name:ACE_DRY_PRESETS.custom_1.name,temp:ACE_DRY_PRESETS.custom_1.temp,duration_sec:ACE_DRY_PRESETS.custom_1.duration_sec},
custom_2:{name:ACE_DRY_PRESETS.custom_2.name,temp:ACE_DRY_PRESETS.custom_2.temp,duration_sec:ACE_DRY_PRESETS.custom_2.duration_sec},
custom_3:{name:ACE_DRY_PRESETS.custom_3.name,temp:ACE_DRY_PRESETS.custom_3.temp,duration_sec:ACE_DRY_PRESETS.custom_3.duration_sec}
};
return post('/api/settings',d);
}).then(function(){
clog('ACE preset '+key+' '+(T.settings_save||'Save & Restart'),'msg-ok');
closeAceDryDialog();
}).catch(function(e){
btn.disabled=false;
btn.textContent=T.ace_dry_dialog_save_restart||'Save & Restart';
clog('ACE-Preset Fehler: '+e,'msg-err');
});
}
function confirmAceDryDialog(){
if(_aceDryDialogAceId<0)return;
var t=parseInt(document.getElementById('ace-dry-dialog-temp').value||45,10);
var h=parseInt(document.getElementById('ace-dry-dialog-h').value||0,10);
var m=parseInt(document.getElementById('ace-dry-dialog-m').value||0,10);
var s=parseInt(document.getElementById('ace-dry-dialog-s').value||0,10);
t=Math.max(30,Math.min(80,t));
h=Math.max(0,Math.min(24,h));
m=Math.max(0,Math.min(59,m));
s=Math.max(0,Math.min(59,s));
var totalSec=(h*3600)+(m*60)+s;
totalSec=Math.max(10*60,Math.min(24*3600,totalSec));
var preset=_aceDryDialogPresetKey||'';
_aceDryProfileSet(_aceDryDialogAceId,t,totalSec,preset);
closeAceDryDialog();
applyState();
}
function aceDryToggle(aceId,on){
if(on)return aceDryStart(aceId);
return aceDryStop(aceId);
}
function toggleCam(){if(camOn)camStop();else camStart()}
function aceDryStop(aceId){
aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0;
return post('/api/ace/dry',{action:'stop',ace_id:aceId})
.then(function(r){return r.json();})
.then(function(r){
if(r.error){throw new Error(r.error);}
clog('ACE '+(aceId+1)+' - '+(T.ace_dry_dryer||'Dryer')+': '+(T.ace_dry_stop||'stop'),'msg-ok');
poll();
})
.catch(function(e){clog('ACE-Fehler: '+e,'msg-err');});
}
function loadStore(){
fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){
storeFiles=d.result||[];
renderStore();
}).catch(function(e){clog('Store-Fehler: '+e,'msg-err')});
}
function renderStore(){
var grid=document.getElementById('store-grid');
var empty=document.getElementById('store-empty');
// Suche
var q=(document.getElementById('store-search')||{value:''}).value.toLowerCase().trim();
// Filter
var filter=(document.getElementById('store-filter')||{value:'all'}).value;
// Sortierung
var sort=(document.getElementById('store-sort')||{value:'date_desc'}).value;
var files=storeFiles.filter(function(f){
if(q&&f.filename.toLowerCase().indexOf(q)===-1) return false;
if(filter==='completed'&&f.last_print_status!=='completed') return false;
if(filter==='failed'&&(f.last_print_status!=='cancelled'&&f.last_print_status!=='failed')) return false;
if(filter==='never'&&f.last_print_status) return false;
return true;
});
files.sort(function(a,b){
if(sort==='name_asc') return a.filename.localeCompare(b.filename);
if(sort==='duration_asc'){
var da=a.last_print_duration||a.est_print_time_sec||0;
var db=b.last_print_duration||b.est_print_time_sec||0;
return da-db;
}
// date_desc (default)
return (b.uploaded_at||'').localeCompare(a.uploaded_at||'');
});
if(!storeFiles.length){
empty.textContent=T.store_empty;
grid.innerHTML='';
empty.style.display='block';
return;
}
if(!files.length){
empty.textContent=T.store_no_results;
grid.innerHTML='';
empty.style.display='block';
return;
}
empty.style.display='none';
grid.innerHTML=files.map(function(f){
var thumb=f.thumbnail_b64
? '<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'
// GCode-Store-Dateiliste. MUSS deklariert sein sonst ReferenceError, wenn
// "Slots wählen" im Banner geklickt wird, bevor der Browser-Tab je geladen
// wurde (Issue #29 / Theme-Auslagerung PR #27).
var storeFiles=[];
var _gcodeFilaments=[];
function _setGcodeFilamentsFromFileObj(fileObj){
try{
if(fileObj&&Array.isArray(fileObj.gcode_filaments)){
_gcodeFilaments=fileObj.gcode_filaments;
}else if(fileObj&&typeof fileObj.gcode_filaments==='string'&&fileObj.gcode_filaments){
_gcodeFilaments=JSON.parse(fileObj.gcode_filaments);
}else{
_gcodeFilaments=[];
}
}catch(e){
_gcodeFilaments=[];
}
}
function storePrint(fileId, filename){
_storeFileId=fileId;
_storeFilename=filename;
_filamentDialogMode='store';
// GCode-Filamente aus Store-Datei holen (für Vorschau im Dialog)
var fileObj=storeFiles.find(function(f){return f.id===fileId;});
_setGcodeFilamentsFromFileObj(fileObj);
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){
openFilamentDialog(d.result||[]);
}).catch(function(){openFilamentDialog([]);});
}
function startReadyFileWithSlots(){
_filamentDialogMode='banner';
_storeFilename=S.file_ready||'';
// Banner must never reuse stale store-file context.
_storeFileId=null;
_gcodeFilaments=[];
function openWithSlots(){
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){
openFilamentDialog(d.result||[]);
}).catch(function(){openFilamentDialog([]);});
}
var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
if(fileObj){
_storeFileId=fileObj.id;
_setGcodeFilamentsFromFileObj(fileObj);
openWithSlots();
return;
}
// Fallback: refresh file list, then resolve current file by filename.
fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){
storeFiles=d.result||[];
var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
if(refreshed){
_storeFileId=refreshed.id;
_setGcodeFilamentsFromFileObj(refreshed);
}
openWithSlots();
}).catch(function(){
openWithSlots();
});
}
var _amsSlots=[];
var _printObjects=[]; // [{name, skip}] für aktuell offenen Dialog
var _printObjectsSvg=''; // base64-SVG aus DB für Visualisierung
// Hilfsfunktionen für farbige Kanal/Slot-Marker (Issue #23)
function _contrastText(hex){
// Helle Farbe → dunkler Text, dunkle Farbe → heller Text
var c=(hex||'').replace('#','');
if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2];
if(c.length<6)return '#fff';
var r=parseInt(c.slice(0,2),16),g=parseInt(c.slice(2,4),16),b=parseInt(c.slice(4,6),16);
// YIQ-Helligkeit
var y=(r*299 + g*587 + b*114)/1000;
return y>=140?'#111':'#fff';
}
function _normalizeMaterialKey(material){
var key=(material||'').toUpperCase().replace(/[^A-Z0-9+]/g,'');
// Orca often uses PLA for PLA+, while AMS may report PLA+.
if(key==='PLA+'||key==='PLAPLUS') return 'PLA';
return key;
}
function _materialsCompatible(a,b){
return _normalizeMaterialKey(a)===_normalizeMaterialKey(b);
}
function _updateSlotMarker(sel){
var opt=sel.options[sel.selectedIndex];
var color=opt&&opt.dataset.color?opt.dataset.color:'#888';
var slotIdx=parseInt(opt.value);
var paintIdx=sel.dataset.paint;
var marker=document.querySelector('.fd-slot-marker[data-for-paint="'+paintIdx+'"]');
if(marker){
marker.style.background=color;
marker.style.color=_contrastText(color);
marker.textContent=(slotIdx+1);
}
}
function openFilamentDialog(slots){
_amsSlots=slots
.filter(function(s){return s.status==='loaded';})
.sort(function(a,b){return (a.slot_index||0)-(b.slot_index||0);});
var dlg=document.getElementById('filament-dialog');
var title=document.getElementById('fd-title');
var body=document.getElementById('fd-slots');
if(title)title.textContent='▶ '+_storeFilename;
// Objekt-Liste laden (nur Store-Modus: per File-ID; Banner-Modus hat keine ID)
_printObjects=[];
_printObjectsSvg='';
var objSection=document.getElementById('fd-objects-section');
if(objSection)objSection.style.display='none';
if(_filamentDialogMode==='store'&&_storeFileId){
fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects'))
.then(function(r){return r.json()})
.then(function(d){
var names=(d.result&&d.result.names)||[];
_printObjectsSvg=(d.result&&d.result.svg_b64)||'';
if(names.length>=2){
_printObjects=names.map(function(n){return {name:n,skip:false};});
renderObjectChecklist(); renderObjectSvg();
if(objSection)objSection.style.display='block';
}
}).catch(function(){});
}
// GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten
var channels=_gcodeFilaments.length?_gcodeFilaments:_amsSlots.map(function(s,i){
return {slot_index:i,color_hex:s.color_hex,material:s.material};
});
// Default mapping strategy:
// 1) keep order where possible (row i -> nearest compatible slot i)
// 2) keep defaults unique while compatible slots are available
// 3) use color proximity as tie-breaker
function _hexToRgb(hex){
var c=(hex||'').replace('#','');
if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2];
if(c.length<6)return [255,255,255];
return [parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16)];
}
function _colorDist(a,b){
var ar=_hexToRgb(a), br=_hexToRgb(b);
var dr=ar[0]-br[0], dg=ar[1]-br[1], db=ar[2]-br[2];
return (dr*dr + dg*dg + db*db);
}
var defaultSlotByPaint={};
var usedDefaultSlot={};
channels.forEach(function(gc,i){
var compatible=_amsSlots.filter(function(s){
return _materialsCompatible(gc.material, s.material);
});
if(!compatible.length){
defaultSlotByPaint[i]=-1;
return;
}
var ranked=compatible.slice().sort(function(a,b){
var da=Math.abs((a.slot_index||0)-i), db=Math.abs((b.slot_index||0)-i);
if(da!==db)return da-db;
var ca=_colorDist(gc.color_hex, a.color_hex), cb=_colorDist(gc.color_hex, b.color_hex);
if(ca!==cb)return ca-cb;
return (a.slot_index||0)-(b.slot_index||0);
});
var chosen=ranked.find(function(s){return !usedDefaultSlot[s.slot_index];}) || ranked[0];
defaultSlotByPaint[i]=chosen?chosen.slot_index:-1;
if(chosen) usedDefaultSlot[chosen.slot_index]=1;
});
if(!_amsSlots.length){
body.innerHTML='<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){
var isUsed=(gc&&gc.is_used!==false);
// Only allow material-compatible slots.
var compatible=_amsSlots.filter(function(s){
return _materialsCompatible(gc.material, s.material);
});
var defaultSlotIndex=(defaultSlotByPaint.hasOwnProperty(i)?defaultSlotByPaint[i]:-1);
var defaultSlot=compatible.find(function(s){return s.slot_index===defaultSlotIndex;})||null;
var opts=compatible.map(function(s){
var sel=(defaultSlot&&s.slot_index===defaultSlot.slot_index)?'selected':'';
return '<option value="'+s.slot_index+'" data-color="'+s.color_hex+'" data-material="'+s.material+'" '+sel+'>'+
'● Slot '+(s.slot_index+1)+' · '+s.material+'</option>';
}).join('');
if(!compatible.length){
opts='<option value="-1" data-color="#888888" data-material="" selected>⚠ No matching material</option>';
}
// Kanal-Box (links): farbige Box mit Nummer + auto Kontrast-Text
var txt=_contrastText(gc.color_hex);
var slotColor=defaultSlot?defaultSlot.color_hex:'#888';
var slotTxt=_contrastText(slotColor);
var usedBadge=isUsed
? '<span style="font-size:10px;color:var(--ok);font-weight:700;min-width:32px">USED</span>'
: '<span style="font-size:10px;color:var(--txt2);font-weight:700;min-width:32px;opacity:.75">USED</span>';
return '<div style="display:flex;align-items:center;gap:8px;padding:8px;border-radius:6px;background:var(--raised);border:1px solid var(--border)">'+
'<span style="display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;background:'+gc.color_hex+';color:'+txt+';font-weight:700;font-size:13px;border:1px solid var(--border);flex-shrink:0">'+(i+1)+'</span>'+
'<span style="font-size:11px;color:var(--txt2);min-width:36px">'+gc.material+'</span>'+
usedBadge+
'<span style="font-size:16px;color:var(--txt2)">→</span>'+
'<span class="fd-slot-marker" data-for-paint="'+i+'" style="display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:5px;background:'+slotColor+';color:'+slotTxt+';font-weight:700;font-size:12px;border:1px solid var(--border);flex-shrink:0">'+(defaultSlot?defaultSlot.slot_index+1:'?')+'</span>'+
'<select data-paint="'+i+'" data-paint-color="'+gc.color_hex+'" data-is-used="'+(isUsed?'1':'0')+'" data-has-compatible="'+(compatible.length?'1':'0')+'" '+(compatible.length?'':'disabled')+' onchange="_updateSlotMarker(this)" style="flex:1;min-width:0;padding:4px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">'+
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=[];
var missingCompatible=0;
selects.forEach(function(sel){
var paintIdx=parseInt(sel.dataset.paint);
var paintColor=sel.dataset.paintColor;
var isUsed=(sel.dataset.isUsed==='1');
var hasCompatible=(sel.dataset.hasCompatible==='1');
var opt=sel.options[sel.selectedIndex];
var amsIdx=parseInt(opt&&opt.value);
if(!hasCompatible || Number.isNaN(amsIdx) || amsIdx < 0){
if(isUsed) missingCompatible += 1;
amsIdx = -1;
}
var amsSlot=_amsSlots.find(function(s){return s.slot_index===amsIdx;})||{};
// Farbe als [R,G,B,255]
function hexToRgba(h){
var c=h.replace('#','');
if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2];
return [parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16),255];
}
assignments.push({
paint_index: paintIdx,
is_used: isUsed,
slot_index: amsIdx,
material: opt.dataset.material||'PLA',
paint_color: hexToRgba(paintColor||'#ffffff'),
ams_color: hexToRgba(amsSlot.color_hex||'#ffffff'),
});
});
if(missingCompatible>0){
clog('Cannot start print: '+missingCompatible+' used paint(s) have no matching material slot','msg-err');
return;
}
// Pre-Print Skip: Namen der abgehakten Objekte sammeln
var excludedObjects=_printObjects.filter(function(o){return o.skip;}).map(function(o){return o.name;});
closeFilamentDialog();
if(_filamentDialogMode==='banner'){
// Banner-Modus: normaler print/start mit Slot-Override
var btn=document.getElementById('file-ready-btn');
if(btn){btn.disabled=true;btn.textContent='…';}
post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects})
.then(function(r){return r.json();})
.then(function(){
document.getElementById('file-ready-banner').style.display='none';
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
})
.catch(function(e){
clog((T.log_error||'Error:')+' '+e,'msg-err');
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
});
} else {
// Store-Modus: POST /kx/print
fetch(_apiUrl('/kx/print'),{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects})
}).then(function(r){return r.json()}).then(function(d){
if(d.result==='ok'){clog('Druckstart: '+_storeFilename,'msg-ok');showPanel('dashboard');}
else{clog('Druckfehler: '+(d.error||'?'),'msg-err');}
}).catch(function(e){clog('Druckfehler: '+e,'msg-err');});
}
}
function renderObjectChecklist(){
var box=document.getElementById('fd-objects');
if(!box)return;
box.innerHTML=_printObjects.map(function(o,i){
var label=o.name;
// Klipper-Namen sind oft "Datei.stl_id_N_copy_M" → schöner darstellen
var m=label.match(/^(.+)\.stl_id_(\d+)_copy_(\d+)$/);
if(m)label=m[1]+' #'+(parseInt(m[2])+1)+(m[3]!=='0'?' ('+(parseInt(m[3])+1)+')':'');
return '<label style="display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:var(--raised);border:1px solid var(--border);cursor:pointer;font-size:12px">'+
'<input type="checkbox" data-idx="'+i+'" '+(o.skip?'checked':'')+' onchange="_toggleObjectSkip('+i+',this.checked)">'+
'<span style="word-break:break-all">'+label+'</span>'+
'</label>';
}).join('');
}
function _toggleObjectSkip(idx,val){
if(_printObjects[idx])_printObjects[idx].skip=!!val;
renderObjectSvg();
}
function renderObjectSvg(){
var box=document.getElementById('fd-objects-svg');
if(!box)return;
if(!_printObjectsSvg||!_printObjects.length){box.style.display='none';box.innerHTML='';return;}
box.style.display='block';
var svg=''; try{ svg=atob(_printObjectsSvg);}catch(e){ box.style.display='none'; return; }
box.innerHTML=svg;
var svgEl=box.querySelector('svg');
if(!svgEl)return;
svgEl.style.width='100%'; svgEl.style.maxHeight='200px'; svgEl.style.height='auto';
_printObjects.forEach(function(o,i){
var g=svgEl.querySelector('g[id="'+CSS.escape(o.name)+'"]');
if(!g)return;
var path=g.querySelector('path');
g.style.cursor='pointer';
g.setAttribute('opacity', o.skip?'0.8':'0.35');
if(path){
path.setAttribute('fill', o.skip?'#ff5e5b':'#5fa7ff');
path.setAttribute('fill-opacity', o.skip?'0.4':'0.18');
}
g.onclick=function(){
_printObjects[i].skip=!_printObjects[i].skip;
renderObjectChecklist(); renderObjectSvg();
};
});
}
// ── Mid-Print Skip ──
var _skipObjects=[]; // [{name, skipped, willSkip}]
var _skipSvg='';
function openSkipDialog(){
document.getElementById('skip-status').textContent='';
document.getElementById('skip-confirm').disabled=false;
_refreshSkipDialog();
document.getElementById('skip-dialog').classList.add('open');
}
function _refreshSkipDialog(){
// Erst aktueller State (mit DB-Objects + svg), dann query_obj für frischen skipped
fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){
var s=d.result||{};
_skipSvg=s.svg_b64||'';
_skipObjects=(s.objects||[]).map(function(n){
return {name:n, skipped:(s.skipped||[]).indexOf(n)>=0, willSkip:false};
});
renderSkipList(); renderSkipSvg();
});
// Frisch nachfragen (skipped-Liste aktualisieren)
fetch(_apiUrl('/kx/skip/query'),{method:'POST'}).then(function(r){return r.json()}).then(function(){
setTimeout(function(){
fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){
var s=d.result||{};
var skipped=s.skipped||[];
_skipObjects.forEach(function(o){ o.skipped=skipped.indexOf(o.name)>=0; if(o.skipped)o.willSkip=false; });
renderSkipList(); renderSkipSvg();
});
}, 500);
}).catch(function(){});
}
function closeSkipDialog(){
document.getElementById('skip-dialog').classList.remove('open');
}
function _shortLabel(name){
var m=name.match(/^(.+)\.[sS][tT][lL]_id_(\d+)_copy_(\d+)$/);
if(!m)return name;
return m[1]+' #'+(parseInt(m[2])+1)+(m[3]!=='0'?' ('+(parseInt(m[3])+1)+')':'');
}
function renderSkipList(){
var box=document.getElementById('skip-list');
if(!box)return;
if(!_skipObjects.length){
box.innerHTML='<div style="color:var(--txt2);font-size:12px;padding:12px;text-align:center">'+(T.skip_no_objects||'Keine Objekte in diesem Druck.')+'</div>';
return;
}
box.innerHTML=_skipObjects.map(function(o,i){
var label=_shortLabel(o.name);
var dis=o.skipped?'disabled':'';
var note=o.skipped?'<span style="font-size:11px;color:var(--warn);margin-left:auto">'+(T.skip_already||'übersprungen')+'</span>':'';
return '<label style="display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:var(--raised);border:1px solid var(--border);font-size:12px;'+(o.skipped?'opacity:0.5':'')+'">'+
'<input type="checkbox" data-idx="'+i+'" '+(o.willSkip?'checked':'')+' '+dis+' onchange="_toggleWillSkip('+i+',this.checked)">'+
'<span style="word-break:break-all">'+label+'</span>'+note+
'</label>';
}).join('');
}
function renderSkipSvg(){
var box=document.getElementById('skip-svg');
if(!box)return;
if(!_skipSvg||!_skipObjects.length){box.style.display='none';box.innerHTML='';return;}
box.style.display='block';
// SVG aus base64 dekodieren
var svg='';
try{ svg=atob(_skipSvg); }catch(e){ box.style.display='none'; return; }
box.innerHTML=svg;
// Polygone interaktiv machen: jeder <g id="..."> entspricht einem Objekt
var svgEl=box.querySelector('svg');
if(!svgEl)return;
svgEl.style.width='100%'; svgEl.style.maxHeight='280px'; svgEl.style.height='auto';
_skipObjects.forEach(function(o,i){
var g=svgEl.querySelector('g[id="'+CSS.escape(o.name)+'"]');
if(!g)return;
var path=g.querySelector('path');
if(o.skipped){
// bereits übersprungen → ausgegraut, kein Klick
g.setAttribute('opacity','0.25');
if(path){path.setAttribute('fill','#888');path.setAttribute('fill-opacity','0.3');}
g.style.cursor='not-allowed';
} else {
g.style.cursor='pointer';
g.setAttribute('opacity', o.willSkip?'0.8':'0.35');
if(path){
path.setAttribute('fill', o.willSkip?'#ff5e5b':'#5fa7ff');
path.setAttribute('fill-opacity', o.willSkip?'0.4':'0.18');
}
g.onclick=function(){
_skipObjects[i].willSkip=!_skipObjects[i].willSkip;
renderSkipList(); renderSkipSvg();
};
}
});
}
function _toggleWillSkip(idx,val){
if(_skipObjects[idx])_skipObjects[idx].willSkip=!!val;
renderSkipSvg();
}
function confirmSkip(){
var names=_skipObjects.filter(function(o){return o.willSkip;}).map(function(o){return o.name;});
var st=document.getElementById('skip-status');
var btn=document.getElementById('skip-confirm');
if(!names.length){st.textContent=T.skip_select_at_least_one||'Bitte mindestens ein Objekt wählen.';st.style.color='var(--warn)';return;}
btn.disabled=true; st.textContent=T.skip_sending||'Sende …'; st.style.color='var(--txt2)';
fetch(_apiUrl('/kx/skip'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({names:names})})
.then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});})
.then(function(res){
if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;}
st.textContent=T.skip_success||'Objekte werden übersprungen.';st.style.color='var(--ok)';
// Dialog offen lassen + neu laden damit der "übersprungen"-Status erscheint
setTimeout(function(){ _refreshSkipDialog(); btn.disabled=false; st.textContent=''; }, 1500);
})
.catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;});
}
function storeDelete(fileId){
if(!confirm(T.store_delete_confirm)) return;
fetch(_apiUrl('/kx/files/'+fileId),{method:'DELETE'}).then(function(r){
if(r.ok){loadStore();}
else{clog('Löschen fehlgeschlagen','msg-err');}
});
}
// ── Drucker hinzufügen ──
function openAddPrinterDialog(){
document.getElementById('apd-ip').value='';
document.getElementById('apd-name').value='';
var st=document.getElementById('apd-status');st.textContent='';st.style.color='var(--txt2)';
document.getElementById('apd-confirm').disabled=false;
document.getElementById('add-printer-dialog').classList.add('open');
}
function closeAddPrinterDialog(){
document.getElementById('add-printer-dialog').classList.remove('open');
}
function confirmAddPrinter(){
var ip=document.getElementById('apd-ip').value.trim();
var name=document.getElementById('apd-name').value.trim();
var st=document.getElementById('apd-status'),btn=document.getElementById('apd-confirm');
if(!ip){st.textContent=T.apd_err_ip;st.style.color='var(--err)';return;}
st.textContent=T.apd_fetching;st.style.color='var(--txt2)';btn.disabled=true;
fetch('/kx/printers/add',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({printer_ip:ip,name:name})})
.then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});})
.then(function(res){
if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;}
st.textContent=T.apd_success;st.style.color='var(--ok)';
setTimeout(function(){location.reload();},2500);
})
.catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;});
}
function removePrinter(id,name){
if(!confirm(T.printers_remove_confirm.replace('{name}',name)))return;
fetch('/kx/printers/'+encodeURIComponent(id),{method:'DELETE'})
.then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});})
.then(function(res){
if(!res.ok){alert((res.j&&res.j.error)||'Fehler');return;}
setTimeout(function(){location.href='/printer1';},2000);
})
.catch(function(e){alert(''+e);});
}
// ── Drucker-Tab ──
function loadPrinterTab(){
var grid=document.getElementById('printers-grid');
if(grid)grid.innerHTML='<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>';
});
}