// ── 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,z_mm: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:[]},web_upload_warning:1};
var tempHistory={n:[],b:[]};
var camOn=false;
var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print
var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support)
var _lastLoadedFile=null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55)
var _idleCleared=false; // User hat idle-Datei explizit „geleert" → kein Nachladen von s.filename (Issue #57)
var _fdDialogOpen=false; // Dialog ist gerade offen
var _fdAutoOpenedFile=sessionStorage.getItem('fdAutoOpenedFile')||null;
var _fdUserCancelled=sessionStorage.getItem('fdUserCancelled')==='1';
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}
};
// Spoolman state
var _spoolmanStatus={configured:false,reachable:false,server:'',sync_rate:0,slot_spools:{}};
var _spoolmanSpools=[];
var _slotSpoolMap={}; // {String(global_index): spoolman_spool_id} — last committed assignment
function _loadSpoolmanStatus(){
fetch(_apiUrl('/kx/spoolman/status')).then(function(r){return r.json();}).then(function(d){
_spoolmanStatus=d;
_slotSpoolMap=d.slot_spools||{};
_updateSpoolmanStatusDot();
_buildSpoolmanSection();
renderSpoolmanSlotCard();
if(d.configured){
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(sd){
_spoolmanSpools=sd.spools||[];
});
}
}).catch(function(){});
}
function _updateSpoolmanStatusDot(){
var dot=document.getElementById('spoolman-status-dot');
var lbl=document.getElementById('spoolman-status-lbl');
if(!dot||!lbl)return;
if(!_spoolmanStatus.configured){
dot.style.color='var(--txt2)';lbl.textContent='nicht konfiguriert';
} else if(_spoolmanStatus.reachable){
dot.style.color='var(--ok)';lbl.textContent=_spoolmanStatus.server||'verbunden';
} else {
dot.style.color='var(--err)';lbl.textContent=(_spoolmanStatus.server||'')+' (nicht erreichbar)';
}
}
function _buildSpoolmanSection(){
var sec=document.getElementById('fd-spoolman-section');
var rows=document.getElementById('fd-spoolman-rows');
var loading=document.getElementById('fd-spoolman-loading');
if(!sec||!rows)return;
if(!_spoolmanStatus.configured){sec.style.display='none';return;}
sec.style.display='';
rows.innerHTML='';
if(loading)loading.style.display='';
var usedSlots={};
(_amsSlots||[]).forEach(function(slot){
usedSlots[slot.slot_index]=slot;
});
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(d){
if(loading)loading.style.display='none';
_spoolmanSpools=d.spools||[];
var slotKeys=Object.keys(usedSlots).map(Number).sort(function(a,b){return a-b;});
if(!slotKeys.length){rows.innerHTML='–';return;}
rows.innerHTML=slotKeys.map(function(idx){
var slot=usedSlots[idx];
var col=(slot.color_hex||'#888');
var currentSpool=_slotSpoolMap[String(idx)]||'';
var opts=''+_spoolmanSpools.map(function(sp){
var rem=sp.remaining_weight!=null?' ('+sp.remaining_weight.toFixed(0)+'g)':'';
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':'';
var name=sp.filament&&sp.filament.name?sp.filament.name:'Spool';
return '';
}).join('');
return '
'+
''+
'Slot '+(idx+1)+''+
'
';
}).join('');
}).catch(function(){if(loading)loading.style.display='none';});
}
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 currentLang='de';
var T={};
var _langCache={};
function tr(key,fallback){
var v=T&&T[key];
return (typeof v==='string'&&v.length)?v:(fallback!==undefined?fallback:'');
}
function _langToggleLabel(lang){
if(lang==='de')return 'Deutsch';
if(lang==='en')return 'English';
if(lang==='fr')return 'Français';
if(lang==='it')return 'Italiano';
if(lang==='zh-cn')return '简体中文';
return 'Espanol';
}
function _mapSupportedLang(lang){
if(!lang)return '';
var l=String(lang).toLowerCase().replace(/_/g,'-').trim();
if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='it'||l==='zh-cn')return l;
var base=l.split('-')[0];
if(base==='de'||base==='en'||base==='es'||base==='fr'||base==='it')return base;
if(base==='zh'){
if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn';
}
return '';
}
function _normalizeLang(lang){
return _mapSupportedLang(lang)||'de';
}
function _detectBrowserLanguage(){
var prefs=[];
if(Array.isArray(navigator.languages)&&navigator.languages.length)prefs=navigator.languages;
else if(navigator.language)prefs=[navigator.language];
for(var i=0;i1){
if(wrap)wrap.style.display='';
if(single)single.style.display='none';
var menu=document.getElementById('printer-dropdown-menu');
if(menu){
menu.innerHTML=_printers.map(function(p){
var active=_activePrinter&&String(p.id)===String(_activePrinter.id);
var num=p.id;
return ''+
(active?'▶ ':'')+p.name+'';
}).join('');
}
} else {
if(wrap)wrap.style.display='none';
if(single)single.style.display='';
}
}
function togglePrinterDropdown(){
var menu=document.getElementById('printer-dropdown-menu');
if(menu)menu.style.display=menu.style.display==='none'?'block':'none';
}
document.addEventListener('click',function(e){
var wrap=document.getElementById('printer-dropdown-wrap');
if(wrap&&!wrap.contains(e.target)){
var menu=document.getElementById('printer-dropdown-menu');
if(menu)menu.style.display='none';
}
});
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-toggle-lbl',T.fd_objects_toggle);
setText('apd-lbl-ip',T.apd_lbl_ip);
setText('apd-lbl-name',T.apd_lbl_name);
var apn=document.getElementById('apd-name');if(apn)apn.setAttribute('placeholder',T.apd_placeholder_name);
setText('apd-cancel',T.apd_cancel);
setText('apd-confirm',T.apd_confirm);
setText('fd-slots-hint',T.fd_slots_hint);
setText('fd-cancel',T.fd_cancel);
setText('fd-print',T.fd_print);
setText('store-panel-title','🗂 '+T.panel_browser_title);
var srb=document.getElementById('store-refresh-btn');if(srb)srb.textContent=T.store_refresh;
var ssp=document.getElementById('store-search');if(ssp)ssp.setAttribute('placeholder',T.store_search_placeholder);
setText('store-upload-label-prefix',T.store_upload_label_prefix);
setText('store-upload-label-browse',T.store_upload_label_browse);
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);
setText('store-web-verify-title',T.store_web_verify_title);
setText('store-web-verify-msg',T.store_web_verify_msg);
setText('store-web-verify-confirm',T.store_web_verify_confirm);
setText('store-web-verify-abort',T.store_web_verify_abort);
setText('store-lbl-select-all',T.store_select_all||'Select All');
setText('store-lbl-delete-selected',T.store_delete_selected||'Delete Selected');
setText('store-lbl-exit-select',T.store_exit_select||'Cancel');
setText('btab-lbl-uploaded',T.browser_tab_uploaded||'Uploaded');
setText('btab-lbl-printer',T.browser_tab_printer||'On Printer');
setText('printer-store-lbl-select-all',T.store_select_all||'Select All');
setText('printer-store-lbl-delete-selected',T.store_delete_selected||'Delete Selected');
setText('printer-store-lbl-exit-select',T.store_exit_select||'Cancel');
// 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-zpos',T.lbl_zpos);
setText('d-lbl-light',T.lbl_light);
setText('d-lbl-nozzle',T.label_nozzle);
setText('d-lbl-bed',T.label_bed);
// Dashboard buttons — Pause-Button wird zur Toggle-Action; Resume-Beschriftung
// wird in updatePauseResumeBtn() je nach Druckerzustand gesetzt.
updatePauseResumeBtn();
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);
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('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':','')));
// Console
setText('ptitle-console',T.panel_console_title);
// Settings-Panel
setText('modal-sec-connection',T.settings_connection);
setText('modal-sec-print',T.settings_print);
setText('modal-sec-version',T.settings_version);
// Nav + Kategorie-Labels (mit Fallback falls i18n-Key noch fehlt)
setText('nav-settings',T.nav_settings||'Einstellungen');
setText('setcat-lbl-connection',T.settings_connection||'Verbindung');
setText('setcat-lbl-printer',T.settings_print||'Drucker');
setText('setcat-lbl-display',T.settings_cat_display||'Darstellung');
setText('setcat-lbl-display2',T.settings_cat_display||'Darstellung');
setText('setcat-lbl-filament',T.settings_cat_filament||'Filament');
setText('setcat-lbl-integrations',T.settings_integrations||'Integrationen');
setText('modal-sec-spoolman',T.modal_sec_spoolman||'Spoolman');
setText('lbl-spoolman-url',T.lbl_spoolman_url||'Server-URL');
setText('lbl-spoolman-sync-rate',T.lbl_spoolman_sync_rate||'Sync-Rate (s, 0=aus)');
setText('modal-sec-obico',T.modal_sec_obico||'Obico');
setText('setcat-lbl-system',T.settings_version||'System');
setText('lbl-set-lang',T.settings_cat_language||'Sprache');
setText('lbl-set-theme',T.settings_cat_theme||'Hell / Dunkel umschalten');
setText('lbl-poll-interval',T.settings_poll||'Poll-Intervall (Sekunden)');
setText('lbl-verbose-http-log',T.settings_verbose_http_log||'Log every HTTP request (verbose)');
var dashLbl=document.getElementById('dash-lbl-edit');
if(dashLbl)dashLbl.textContent=_dashEditing?(T.dash_done||'Done'):(T.dash_edit||'Customize dashboard');
setText('dash-lbl-reset',T.dash_reset||'Reset');
setText('dash-lbl-save-preset',T.dash_save_preset||'Save as preset');
var dashPresetStd=document.getElementById('dash-preset-standard');
if(dashPresetStd)dashPresetStd.textContent=T.dash_preset_standard||'Standard';
var dashPresetWide=document.getElementById('dash-preset-wide89');
if(dashPresetWide)dashPresetWide.textContent=T.dash_preset_wide89||'Wide desktop';
if(document.getElementById('dash-hidden-bar'))_dashRenderHiddenBar();
setText('lbl-filament-mapping',T.settings_filament_mapping||'Filament-Profil-Mapping (pro Slot)');
setText('lbl-filament-mapping-save',T.settings_filament_mapping_save||'Mapping speichern');
setText('lbl-visible-vendors',T.settings_visible_vendors||'Sichtbare Hersteller (Profil-Dropdown)');
setText('visible-vendors-hint',T.settings_visible_vendors_hint||'Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar.');
setText('lbl-visible-vendors-save',T.settings_visible_vendors_save||'Auswahl speichern');
// Custom-Profile-Import (Issue #41)
setText('modal-sec-orca-profiles',T.orca_profile_section);
setText('orca-profiles-hint',T.orca_profile_hint);
setText('lbl-orca-profiles-import',T.orca_profile_import_btn);
setText('lbl-slot-profile-import',T.orca_profile_import_link);
setText('profile-import-title',T.orca_profile_import_title);
setText('profile-import-dropmsg',T.orca_profile_dropmsg);
setText('profile-import-list-label',T.orca_profile_list_label);
// Hilfe-Text mit Inline-HTML — innerHTML statt setText
var helpEl=document.getElementById('profile-import-help');
if(helpEl && T.orca_profile_help_html) helpEl.innerHTML=T.orca_profile_help_html;
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-vibration-compensation',T.settings_vibration_compensation);
setText('lbl-file-ready-mode',T.settings_file_ready_mode);
setText('opt-file-ready-dialog',T.settings_file_ready_dialog);
setText('opt-file-ready-banner',T.settings_file_ready_banner);
setText('lbl-camera-on-print',T.settings_camera_on_print);
setText('lbl-web-upload-warning',T.settings_web_upload_warning);
setText('fd-options-title',T.fd_options_title);
setText('fd-lbl-auto-leveling',T.print_auto_leveling);
setText('lbl-update-check',T.update_check);
setText('lbl-update-apply',T.update_apply);
// Progress-Karten-Aktionen für geladene/idle Datei (Issue #55)
setText('d-idle-print-lbl',T.progress_action_print||'Drucken');
setText('d-idle-slots-lbl',T.progress_action_slots||'Slots zuordnen');
setText('d-idle-clear-lbl',T.progress_action_clear||'Leeren');
// 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)+' - '+tr('ace_dry_dryer'));
setText('d-ace-auto-refill-label-'+i,tr('ace_dry_auto_refill'));
setText('d-ace-drying-enable-label-'+i,tr('ace_dry_enable'));
setText('d-ace-dry-humidity-label-'+i,tr('ace_dry_humidity')+':');
setText('d-ace-dry-current-temp-label-'+i,tr('ace_dry_current_temp')+':');
setText('d-ace-dry-target-label-'+i,tr('ace_dry_temp_line')+':');
setText('d-ace-dry-time-label-'+i,tr('ace_dry_time_line')+':');
setText('d-ace-dry-chart-label-'+i,tr('ace_dry_chart'));
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',tr('ace_dry_dialog_title'));
setText('ace-dry-dialog-temp-label',tr('ace_dry_dialog_temp'));
setText('ace-dry-dialog-time-label',tr('ace_dry_dialog_time'));
setText('ace-dry-dialog-custom-name-label',tr('ace_dry_dialog_custom_name'));
setText('ace-dry-dialog-cancel',tr('ace_dry_dialog_cancel'));
setText('ace-dry-dialog-confirm',tr('ace_dry_dialog_confirm'));
setText('ace-dry-dialog-reset-default',tr('ace_dry_dialog_reset_default'));
setText('ace-dry-dialog-save-preset',tr('ace_dry_dialog_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('lbl-slot-profile',T.slot_edit_profile);
setText('slot-profile-hint',T.slot_edit_profile_hint);
var defOpt=document.getElementById('slot-profile-default-opt');
if(defOpt) defOpt.textContent=T.slot_edit_profile_default;
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);
// GCode-Browser-Karten: Texte sind via innerHTML eingebacken,
// bei Sprachwechsel komplett neu rendern.
if(typeof renderStore==='function' && typeof storeFiles!=='undefined'){
try{ renderStore(); }catch(e){}
}
}
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+='
'
+'
♨ACE '+(i+1)+' - Dryer
'
+'
'
+'Temperature:-'
+'Humidity:-'
+'
'
+''
+'
'
+'Drying Temperature:-'
+'Drying Time:-'
+'
'
+'
'
+''
+'
'
+''
+'
'
+'Auto Refill'
+''
+'
'
+'
'
+'Enable Drying'
+''
+'
'
+'
';
}
grid.innerHTML=html;
grid.setAttribute('data-init','1');
}
(function(){
var l=_resolveInitialLanguage();
currentLang=_normalizeLang(l);
var langSel=document.getElementById('lang-select');
if(langSel)langSel.value=currentLang;
document.documentElement.setAttribute('lang',currentLang);
// defer until DOM ready
window.addEventListener('DOMContentLoaded',function(){
setLanguage(currentLang).catch(function(){});
_loadSpoolmanStatus();
// 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;
if(id==='settings')openSettings();
}
// Settings-Kategorie umschalten (Master-Detail)
function showSettingsCat(name){
document.querySelectorAll('.set-group').forEach(g=>g.classList.remove('active'));
document.querySelectorAll('.set-cat').forEach(b=>b.classList.remove('active'));
var g=document.getElementById('setgrp-'+name);if(g)g.classList.add('active');
var c=document.getElementById('setcat-'+name);if(c)c.classList.add('active');
}
// Browser-Sub-Tab umschalten: hochgeladene Dateien (Bridge-Store) vs. Dateien
// auf dem Drucker selbst (interner Speicher, via listLocal MQTT).
var _printerFilesLoaded=false;
function showBrowserTab(name){
document.querySelectorAll('.browser-group').forEach(g=>g.classList.remove('active'));
document.querySelectorAll('.browser-tab').forEach(b=>b.classList.remove('active'));
var g=document.getElementById('browser-group-'+name);if(g)g.classList.add('active');
var t=document.getElementById('btab-'+name);if(t)t.classList.add('active');
if(name==='printer'&&!_printerFilesLoaded)loadPrinterFiles();
}
// ── 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)?' (×'+l.count+')':'';
return '
'+l.ts+''+escHtml(l.msg)+''+cnt+'
';
}).join('');
if(logAutoScroll)el.scrollTop=el.scrollHeight;
else if(savedScroll!==null)el.scrollTop=savedScroll;
}
function onLogScroll(){
var el=document.getElementById('console-log');
if(!el)return;
var atBottom=el.scrollHeight-el.scrollTop-el.clientHeight<30;
if(!atBottom&&logAutoScroll){setAutoScroll(false);}
}
function toggleAutoScroll(){
setAutoScroll(!logAutoScroll);
if(logAutoScroll){var el=document.getElementById('console-log');if(el)el.scrollTop=el.scrollHeight;}
}
function setAutoScroll(on){
logAutoScroll=on;
var btn=document.getElementById('btn-autoscroll');
if(btn){btn.style.background=on?'var(--accent)':'var(--raised)';btn.style.color=on?'#fff':'var(--txt2)';}
}
function clearLogBadge(){
logBadgeCount=0;
['log-badge','log-badge-bot'].forEach(function(id){var b=document.getElementById(id);if(b)b.style.display='none';});
}
function escHtml(s){return s.replace(/&/g,'&').replace(//g,'>');}
// SSE server-log stream
(function(){
function connect(){
var es=new EventSource('/api/log/stream');
es.onmessage=function(e){try{_appendLog(JSON.parse(e.data));}catch(_){}};
es.onerror=function(){es.close();setTimeout(connect,3000);};
}
window.addEventListener('DOMContentLoaded',connect);
})();
// ── Helpers ──
function fmtTime(s){if(!s||s<0)return'–';var m=Math.floor(s/60),h=Math.floor(m/60);m%=60;return h>0?h+'h '+m+'m':m+'m'}
function fmtHmsFromSec(total){
total=Math.max(0,parseInt(total||0,10));
var h=Math.floor(total/3600);
var mm=Math.floor((total%3600)/60);
var ss=total%60;
return h+':'+String(mm).padStart(2,'0')+':'+String(ss).padStart(2,'0');
}
function post(url,body){return fetch(_apiUrl(url),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})}
function clamp(v,lo,hi){return Math.min(hi,Math.max(lo,v))}
// ── Apply state to DOM ──
function applyState(){
var s=S;
_syncAceDryPresetsFromServer(s.ace_dry_presets);
// connection error banner – nur wenn überhaupt ein Drucker konfiguriert ist
var banner=document.getElementById('conn-error-banner');
if(banner){if(s.connection_error&&_printers.length>0){banner.textContent='⚠ '+tr('lbl_conn_error')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}}
var pauseBanner=document.getElementById('pause-msg-banner');
if(pauseBanner){
if(s.pause_msg && s.print_state==='paused'){
var codePart = (s.error_code==0) ? ' ' :
' ['+s.error_code+'] ';
pauseBanner.innerHTML='⏸ '+tr('lbl_pause_reason')+codePart+s.pause_msg;
pauseBanner.style.display='block';
}else{
pauseBanner.style.display='none';
}
}
var bannerVisible=false;
var frb=document.getElementById('file-ready-banner');
if(frb){
var shouldAutoOpen=(s.print_start_dialog===undefined?true:!!s.print_start_dialog);
if(s.file_ready&&s.print_state==='standby'){
document.getElementById('file-ready-name').textContent=s.file_ready;
// Neue Datei → Abbruch-Sperre aufheben
if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready){
_fdUserCancelled=false;
sessionStorage.removeItem('fdUserCancelled');
sessionStorage.removeItem('fdAutoOpenedFile');
}
if(shouldAutoOpen){
// Dialog-Modus: Banner niemals anzeigen.
frb.style.display='none';
if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){
_fdAutoOpenedFile=s.file_ready;
startReadyFileWithSlots(s.file_ready,true,s.filament_mismatch||null);
}
} else {
frb.style.display='flex';
bannerVisible=true;
}
}else{frb.style.display='none';}
}
// skip-button (mid-print) – nur sichtbar wenn aktuell gedruckt wird
var printing=(s.print_state==='printing'||s.print_state==='paused');
var skipBtn=document.getElementById('d-btn-skip');
if(skipBtn) skipBtn.style.display=printing?'':'none';
// Pause/Stopp-Buttons nur bei aktivem Druck zeigen (sonst verwirrend wenn
// der Drucker idle ist). Pause-Button rendert sich passend zum State um.
var ctrlBtns=document.getElementById('d-ctrl-btns');
if(ctrlBtns) ctrlBtns.style.display=printing?'':'none';
updatePauseResumeBtn();
// Zuletzt geladene Datei merken (Issue #55): solange sie über den State
// sichtbar ist. Beim Druckende/Abbruch leert die Bridge file_ready+filename
// (Issue #29) — die gemerkte Referenz bleibt für die Karten-Aktionen.
// Echte ready-Datei oder laufender Druck hebt einen vorherigen „Clear" auf.
if(s.file_ready||printing) _idleCleared=false;
if(s.file_ready) _lastLoadedFile=s.file_ready;
else if(s.filename && !_idleCleared) _lastLoadedFile=s.filename;
else if(_idleCleared) _lastLoadedFile=null;
// Idle-Aktionen (Drucken/Slots/Leeren) nur wenn nicht gedruckt wird, eine
// Datei bekannt ist und der grüne Banner nicht ohnehin schon dieselbe Aktion
// anbietet.
var idleBtns=document.getElementById('d-idle-btns');
if(idleBtns){
var showIdle=(!printing && _lastLoadedFile && !bannerVisible);
idleBtns.style.display=showIdle?'':'none';
if(showIdle){
var dfn=document.getElementById('d-fname');
if(dfn && (!dfn.textContent || dfn.textContent==='–')){
dfn.textContent=_lastLoadedFile;dfn.title=_lastLoadedFile;
}
}
}
// 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 dzpos=document.getElementById('d-zpos');if(dzpos)dzpos.textContent=s.z_mm>0?s.z_mm.toFixed(2)+' mm':'–';
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=tr('card_ams');
var modeMap={toolhead:'Toolhead',ace_direct:'ACE Direct',ace_hub:'ACE Hub'};
var modeTxt=modeMap[s.filament_mode]||'';
amsTitle.textContent=modeTxt?(baseTitle+' - '+modeTxt):baseTitle;
}
ensureAceDryCards();
var dry=s.ace_drying||{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]};
var units=(dry.units||[]);
var unitMap={};
units.forEach(function(u){var id=Number(u.id);if(id>=0&&id<=3)unitMap[id]=u;});
var aceMode=s.filament_mode==='ace_direct'||s.filament_mode==='ace_hub';
var detected=(s.ace_units||[]).filter(function(id){return id>=0&&id<=3;});
if(!detected.length){
Object.keys(unitMap).forEach(function(k){detected.push(Number(k));});
}
if(!detected.length){
(s.ams_slots||[]).forEach(function(sl){var id=Number(sl.box_id);if(id>=0&&id<=3&&detected.indexOf(id)<0)detected.push(id);});
}
detected.sort(function(a,b){return a-b;});
var aceWrap=document.getElementById('d-ace-dry-wrap');
if(aceWrap)aceWrap.style.display=(aceMode&&detected.length)?'contents':'none';
for(var i=0;i<4;i++){
var card=document.getElementById('d-ace-dry-card-'+i);
if(!card)continue;
var show=aceMode&&detected.indexOf(i)>=0;
card.style.display=show?'':'none';
if(!show)continue;
var ud=unitMap[i]||dry;
var refillToggle=document.getElementById('ace-auto-refill-toggle-'+i);
var autoFeedMap=s.ace_auto_feed||{};
if(refillToggle&&!_aceAutoFeedPending[i]){
var afVal=autoFeedMap.hasOwnProperty(String(i))?Number(autoFeedMap[String(i)]):(_aceAutoRefillGet(i)?1:0);
refillToggle.checked=afVal===1;
}
var dryToggle=document.getElementById('ace-dry-enable-toggle-'+i);
if(dryToggle)dryToggle.checked=Number(ud.status||0)>0;
var hh=document.getElementById('d-ace-dry-humidity-'+i);
if(hh){
var hv=(ud.humidity===null||ud.humidity===undefined||ud.humidity==='')?null:Number(ud.humidity);
hh.textContent=(hv===null||Number.isNaN(hv))?'-':(Math.round(hv)+'%');
}
var ht=document.getElementById('d-ace-dry-current-temp-'+i);
if(ht){
var ct=(ud.current_temp===null||ud.current_temp===undefined||ud.current_temp==='')?null:Number(ud.current_temp);
ht.textContent=(ct===null||Number.isNaN(ct))?'-':(ct.toFixed(1)+'°C');
}
var prof=_aceDryProfileGet(i);
var useSec=(Number(ud.status||0)>0&&Number(ud.remain_time)>0)
?Number(ud.remain_time||0)*60
:prof.duration_sec;
var showTemp=(Number(ud.status||0)>0&&Number(ud.target_temp)>0)?Number(ud.target_temp):prof.temp;
var dryTempEl=document.getElementById('d-ace-dry-target-'+i);
if(dryTempEl)dryTempEl.textContent=showTemp+'°C';
var dryTimeEl=document.getElementById('d-ace-dry-time-'+i);
if(dryTimeEl)dryTimeEl.textContent=fmtHmsFromSec(useSec);
}
// AMS
if(s.ams_slots&&s.ams_slots.length){
window._amsSlots=s.ams_slots;
// Group by box_id (-1=Toolhead, 0=ACE 1, 1=ACE 2, ...)
var boxMap={};
s.ams_slots.forEach(function(slot,i){
var bid=slot.box_id!=null?slot.box_id:-1;
if(!boxMap[bid])boxMap[bid]=[];
boxMap[bid].push({slot:slot,arrIdx:i});
});
var boxIds=Object.keys(boxMap).map(Number).sort(function(a,b){return a-b});
var acePresent=boxIds.some(function(b){return b>=0;});
var html='';
boxIds.forEach(function(bid){
var entries=boxMap[bid];
var label=bid===-1
?(acePresent?'Toolhead (Slots 1–3)':'Toolhead')
:('ACE '+(bid+1));
html+='
'
+'
'+label+'
'
+'
';
entries.forEach(function(e){
var slot=e.slot;var i=e.arrIdx;
var empty=slot.status!==5;
var rgb=empty?[80,80,80]:(Array.isArray(slot.color)?slot.color:[128,128,128]);
var col='rgb('+rgb[0]+','+rgb[1]+','+rgb[2]+')';
var globalIdx=slot.global_index!=null?slot.global_index:i;
var active=slot.status===1||slot.active;
var loaded=(s.ams_loaded_slot!=null&&s.ams_loaded_slot>=0&&globalIdx===s.ams_loaded_slot);
var activity=(slot.activity||'');
var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'–');
var slotLabel=T.label_slot+' '+(globalIdx+1);
// Gemapptes Profil nur für belegte Slots verwenden — sonst zeigt ein
// verwaistes Mapping (Slot wurde geleert) ein „Geister"-Profil (Issue #57).
var profile=empty?null:(window._slotProfileMap||{})[globalIdx];
var genericType=(slot.type||slot.material_type||'–');
// Material-Label: bei belegtem Slot mit Mapping den konkreten Profilnamen
// (z.B. „eSUN PLA+") statt nur des generischen Typs zeigen (Issue #57 Punkt 4).
var materialLabel=empty?'–':((profile&&profile.name)?profile.name:genericType);
var vendorBadge='';
if(!empty && profile && profile.vendor){
var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':'');
vendorBadge='
'+profile.vendor+'
';
}
var spoolSel='';
if(_spoolmanStatus.configured&&!empty&&_spoolmanSpools.length){
var curSpool=_slotSpoolMap[String(globalIdx)]||'';
var spoolOpts=''+_spoolmanSpools.map(function(sp){
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':'';
var name=sp.filament?sp.filament.name:'#'+sp.id;
var rem=sp.remaining_weight!=null?' '+sp.remaining_weight.toFixed(0)+'g':'';
return '';
}).join('');
spoolSel='
'
+'
🧵 Spoolman
'
+''
+'
';
}
html+='
'
+''
+'
'+materialLabel+'
'
+vendorBadge
+'
'+slotLabel+'
'
+'
'+pct+'
'
+spoolSel
+'
✏
'
+'
';
});
if(bid===-1&&acePresent){
html+='
'
+'
ACE
'
+'
';
}
html+='
';
});
// Nicht rendern wenn ein Spool-Dropdown gerade offen ist (verhindert Schließen beim Poll)
var activeEl=document.activeElement;
var spoolOpen=activeEl&&activeEl.tagName==='SELECT'&&activeEl.dataset.spoolSlot!=null;
if(!spoolOpen) 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 (unless user explicitly stopped it)
if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped&&s.camera_on_print){
camStart();
}
// reset user-stopped flag when print ends so next print auto-starts again
if(s.print_state!=='printing'){
camUserStopped=false;
}
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=tr('btn_connect');
} else {
btn.className='conn-btn connected';
btn.textContent=tr('btn_disconnect');
}
}
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(){
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 vc=document.getElementById('s-vibration-compensation');if(vc)vc.checked=!!d.vibration_compensation;
var cop=document.getElementById('s-camera-on-print');if(cop)cop.checked=!!d.camera_on_print;
var frm=document.getElementById('s-file-ready-mode');if(frm)frm.value=(d.print_start_dialog===undefined?'1':String(d.print_start_dialog?1:0));
var wuw=document.getElementById('s-web-upload-warning');if(wuw)wuw.checked=(d.web_upload_warning===undefined?true:!!d.web_upload_warning);
// Poll-Intervall (Sekunden) — Backend hat Vorrang vor localStorage
var pi=document.getElementById('s-poll-interval');
if(pi){
var sec=d.poll_interval||Math.round((parseInt(localStorage.getItem('pollInterval')||'2000'))/1000)||3;
pi.value=sec;
}
var vhl=document.getElementById('s-verbose-http-log');if(vhl)vhl.checked=!!d.verbose_http_log;
renderFilamentMapping(d.filament_profiles||{});
renderSpoolmanSlotCard();
// Spoolman
var su=document.getElementById('s-spoolman-url');if(su)su.value=d.spoolman_server||'';
var sr=document.getElementById('s-spoolman-sync-rate');if(sr)sr.value=(d.spoolman_sync_rate!==undefined?d.spoolman_sync_rate:30);
_updateSpoolmanStatusDot();
});
// Sprach-Select im Settings-Panel mit aktueller Sprache spiegeln
var ls=document.getElementById('s-lang-select');
if(ls)ls.value=(localStorage.getItem('lang')||document.documentElement.lang||'de');
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='';
// Custom-Profile-Liste laden (Issue #41)
refreshUserProfileList();
// Vendor-Sichtbarkeitsfilter (Issue #41 Option A)
loadVendorChecklist();
}
function closeSettings(){
// Panel-Variante: zurück zum Dashboard
showPanel('dashboard');
}
// Poll-Intervall-Feld → Live-Poll sofort übernehmen (Persistenz erst beim Speichern)
function onPollIntervalInput(){
var pi=document.getElementById('s-poll-interval');
if(!pi)return;
var sec=parseInt(pi.value,10);
if(sec>=1&&sec<=60)setPoll(sec*1000);
}
// ── Filament-Profil-Mapping pro Slot ([filament_profiles]) ──
// Pro Slot ein einzelnes Profil-Dropdown (vendor+name gemeinsam, gekeyt per
// _profileKey). Kein Freitext mehr → das (vendor,name)→id-Matching kann nicht
// mehr durch manuelle Eingabe brechen (Issue #57 Punkt 1). Optionen werden aus
// /kx/filament/profiles geladen, nach Vendor gruppiert, User-Profile zuerst,
// mit demselben Vendor-Sichtbarkeitsfilter wie das Slot-Edit-Dropdown.
function renderFilamentMapping(map){
var el=document.getElementById('filament-mapping-list');
if(!el)return;
var rows='';
for(var i=0;i<4;i++){
var m=map[i]||map[String(i)]||{};
var idHint=m.id?' ('+m.id+')':'';
rows+='
'
+''
+''
+'
';
}
el.innerHTML=rows;
// Dropdowns befüllen (async, geteilter Profil-Cache + Vendor-Filter)
for(var j=0;j<4;j++){ _fillMappingDropdown(j); }
}
function _fillMappingDropdown(slot){
var sel=document.getElementById('fmap-'+slot);
if(!sel) return;
var wantKey=_profileKey(sel.dataset.vendor, sel.dataset.name);
_loadOrcaFilaments(function(profiles){
sel.innerHTML='';
var userProfs=profiles.filter(function(p){return p.is_user;});
var systemProfs=profiles.filter(function(p){return !p.is_user;});
function _opt(g,p){
var o=document.createElement('option');
o.value=_profileKey(p.vendor,p.name);
o.dataset.vendor=p.vendor; o.dataset.name=p.name; o.dataset.id=p.id||'';
o.textContent=(p.is_user?'★ ':'')+p.name+(p.vendor?' — '+p.vendor:'');
if(o.value===wantKey)o.selected=true;
g.appendChild(o);
}
if(userProfs.length){
var gUser=document.createElement('optgroup');
gUser.label='★ '+(tr('orca_profile_user_label')||'Eigene Profile');
userProfs.forEach(function(p){_opt(gUser,p);});
sel.appendChild(gUser);
}
_loadVisibleVendors(function(vis){
var filtered=systemProfs;
if(vis&&vis.length){
var allow={};vis.forEach(function(v){allow[v]=1;});allow['Generic']=1;
filtered=systemProfs.filter(function(p){return allow[p.vendor];});
}
var byVendor={};
filtered.forEach(function(p){(byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p);});
Object.keys(byVendor).sort().forEach(function(v){
var g=document.createElement('optgroup');g.label=v;
byVendor[v].forEach(function(p){_opt(g,p);});
sel.appendChild(g);
});
});
});
}
function saveFilamentMapping(){
// Nutzt den per-Slot-Endpoint (vendor,name → ID-Lookup im Backend).
// Leere Auswahl ("") = Mapping entfernen.
var chain=Promise.resolve();
for(var i=0;i<4;i++){
(function(slot){
var sel=document.getElementById('fmap-'+slot);
var opt=sel?sel.options[sel.selectedIndex]:null;
var vendor=(opt&&opt.dataset.vendor)||'';
var name=(opt&&opt.dataset.name)||'';
chain=chain.then(function(){
return fetch(_apiUrl('/kx/filament/slots/'+slot+'/profile'),
{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({vendor:vendor,name:name})});
});
})(i);
}
chain.then(function(){
clog(tr('log_filament_mapping_saved')||'Filament-Mapping gespeichert','msg-ok');
openSettings(); // neu laden → ID-Hints aktualisieren
}).catch(function(e){clog('Mapping-Fehler: '+e,'msg-err');});
}
// ── Vendor-Sichtbarkeitsfilter (Issue #41 Option A) ──
var _vendorChecklistSel={}; // {vendor:true} — laufende Auswahl im UI
function loadVendorChecklist(){
// aktuelle Auswahl aus Backend, dann alle verfügbaren Vendoren rendern
_visibleVendors=null; // Cache invalidieren
_loadVisibleVendors(function(vis){
_vendorChecklistSel={};
(vis||[]).forEach(function(v){_vendorChecklistSel[v]=true;});
renderVendorChecklist();
});
}
function renderVendorChecklist(){
var el=document.getElementById('visible-vendors-list');
if(!el)return;
_loadOrcaFilaments(function(profiles){
// alle System-Vendoren (ohne Generic — der ist immer sichtbar) sammeln
var set={};
profiles.forEach(function(p){ if(!p.is_user && p.vendor && p.vendor!=='Generic') set[p.vendor]=1; });
var vendors=Object.keys(set).sort();
var q=((document.getElementById('vendor-filter-search')||{}).value||'').toLowerCase();
if(q)vendors=vendors.filter(function(v){return v.toLowerCase().indexOf(q)>=0;});
el.innerHTML=vendors.map(function(v){
var ck=_vendorChecklistSel[v]?'checked':'';
var safe=v.replace(/"/g,'"');
return '';
}).join('')||'–';
});
}
function _vendorCheck(cb){
var v=cb.getAttribute('data-vendor');
if(cb.checked)_vendorChecklistSel[v]=true; else delete _vendorChecklistSel[v];
}
function renderSpoolmanSlotCard(){
var card=document.getElementById('spoolman-slot-card');
var rows=document.getElementById('spoolman-slot-rows');
if(!card||!rows)return;
if(!_spoolmanStatus.configured){card.style.display='none';return;}
card.style.display='';
Promise.all([
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}),
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();})
]).then(function(res){
var spools=res[0].spools||[];
var slots=(res[1].result||[]).sort(function(a,b){return a.slot_index-b.slot_index;});
if(!slots.length){rows.innerHTML='Keine AMS-Slots bekannt.';return;}
rows.innerHTML=slots.map(function(slot){
var idx=parseInt(slot.slot_index);
var col=slot.color_hex||'#888';
var mat=slot.material||'';
var current=_slotSpoolMap[String(idx)]||'';
var opts=''+spools.map(function(sp){
var rem=sp.remaining_weight!=null?' ('+sp.remaining_weight.toFixed(0)+'g)':'';
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':'';
var name=sp.filament?sp.filament.name:'Spool #'+sp.id;
var mat2=sp.filament&&sp.filament.material?' · '+sp.filament.material:'';
return '';
}).join('');
return '
'+
''+
'Slot '+(idx+1)+' '+escHtml(mat)+''+
'
';
}).join('');
}).catch(function(){rows.innerHTML='Spoolman nicht erreichbar';});
}
function onAmsSpoolChange(sel){
var idx=sel.getAttribute('data-spool-slot');
var val=sel.value;
if(val) _slotSpoolMap[String(idx)]=parseInt(val);
else delete _slotSpoolMap[String(idx)];
var mapping={};
Object.keys(_slotSpoolMap).forEach(function(k){mapping[k]=_slotSpoolMap[k];});
fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST',
headers:{'Content-Type':'application/json'},body:JSON.stringify({slot_spools:mapping})});
}
function saveSpoolmanSlots(){
var mapping={};
document.querySelectorAll('#spoolman-slot-rows select[data-spool-slot]').forEach(function(sel){
var idx=sel.getAttribute('data-spool-slot');
var val=sel.value;
if(val)mapping[idx]=parseInt(val);
});
fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({slot_spools:mapping})}).then(function(r){return r.json();}).then(function(d){
_slotSpoolMap=d.slot_spools||{};
});
}
function saveVisibleVendors(){
var vendors=Object.keys(_vendorChecklistSel);
fetch(_apiUrl('/kx/filament/visible_vendors'),{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({vendors:vendors})}).then(function(r){return r.json();}).then(function(){
_visibleVendors=vendors.slice(); // Dropdown-Cache aktualisieren
clog(tr('log_visible_vendors_saved')||'Hersteller-Auswahl gespeichert','msg-ok');
}).catch(function(e){clog('Vendor-Filter-Fehler: '+e,'msg-err');});
}
// ── Custom Filament Profile Import (Issue #41) ──
function refreshUserProfileList(){
var listEl=document.getElementById('orca-profiles-list');
if(!listEl) return;
fetch(_apiUrl('/kx/filament/profiles/user')).then(function(r){return r.json();}).then(function(d){
var profs=(d && d.result)||[];
if(!profs.length){
listEl.innerHTML=''+(tr('orca_profile_user_empty')||'– keine –')+'';
return;
}
listEl.innerHTML=profs.map(function(p){
var label=p.vendor+' / '+p.name+' ('+p.type+')';
return '
'
+'★ '+label+''
+''
+'
';
}).join('');
}).catch(function(){});
}
function deleteUserProfile(vendor, name){
fetch(_apiUrl('/kx/filament/profiles/user?vendor='+vendor+'&name='+name), {method:'DELETE'})
.then(function(r){return r.json();})
.then(function(){
_orcaFilamentCache=null;
refreshUserProfileList();
// Falls Import-Dialog offen ist, dort auch refreshen
refreshImportDialogList();
});
}
function openProfileImport(){
document.getElementById('profile-import-status').textContent='';
refreshImportDialogList();
document.getElementById('profile-import-modal').classList.add('open');
}
function closeProfileImport(){
document.getElementById('profile-import-modal').classList.remove('open');
}
function refreshImportDialogList(){
var el=document.getElementById('profile-import-list');
if(!el) return;
fetch(_apiUrl('/kx/filament/profiles/user')).then(function(r){return r.json();}).then(function(d){
var profs=(d && d.result)||[];
if(!profs.length){
el.innerHTML=''+(tr('orca_profile_user_empty')||'– keine –')+'';
return;
}
el.innerHTML=profs.map(function(p){
var label=p.vendor+' / '+p.name+' ('+p.type+')';
return '
'
+'★ '+label+''
+''
+'
';
}).join('');
}).catch(function(){});
}
function doProfileImportUpload(files){
if(!files || !files.length) return;
var status=document.getElementById('profile-import-status');
status.textContent=(tr('orca_profile_uploading')||'Lade hoch…');
status.style.color='var(--txt2)';
var done=0, totalAdded=0, totalSkipped=0;
function _one(idx){
if(idx>=files.length){
status.textContent=(tr('orca_profile_done')||'Importiert')+': '+totalAdded
+(totalSkipped?' / '+totalSkipped+' '+(tr('orca_profile_skipped')||'übersprungen'):'');
status.style.color='var(--ok)';
_orcaFilamentCache=null;
refreshImportDialogList();
refreshUserProfileList();
// Vendor-Checkliste neu aufbauen — ein Import kann einen bisher
// unbekannten System-Vendor mitbringen (Issue #41).
if(document.getElementById('visible-vendors-list')) renderVendorChecklist();
// Wenn Slot-Edit offen ist, Dropdown gleich neu befüllen
var mat=document.getElementById('slot-edit-mat');
if(mat && document.getElementById('slot-edit-modal').classList.contains('open')){
_fillSlotProfileDropdown(mat.value, '', '');
}
return;
}
var fd=new FormData();
fd.append('file', files[idx]);
fetch(_apiUrl('/kx/filament/profiles/user'), {method:'POST', body:fd})
.then(function(r){return r.json();})
.then(function(d){
totalAdded += (d.added||0);
totalSkipped += (d.skipped||0);
done++;
_one(idx+1);
})
.catch(function(e){
status.textContent='Fehler: '+e;
status.style.color='var(--err)';
});
}
_one(0);
}
// ── AMS Slot Edit ──
var _slotEditIndex=-1;
var _slotEditLoaded=false;
var _MAT_PRESETS=['PLA','PLA+','PLA SILK','PLA MATTE','PETG','ABS','ASA','TPU','PA','PC','HIPS'];
function _normalizeMat(m){
var s=m.toUpperCase().trim().replace(/-/g,' ').replace(/_/g,' ');
var aliases={'PLAPLUS':'PLA+','PLA PLUS':'PLA+','SILK PLA':'PLA SILK','PLASILK':'PLA SILK',
'PLA MATTE':'PLA','PLA MARBLE':'PLA','PLA WOOD':'PLA','TPE':'TPU',
'PETG PLUS':'PETG+','PA6':'PA','PA12':'PA','PA66':'PA'};
return aliases[s]||s;
}
var _BASE_MATERIAL_TYPES=['PLA','PETG','ABS','ASA','TPU','TPE','PA','PC','HIPS','PEI','PEEK'];
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?tr('slot_edit_unload'):tr('slot_edit_load');
}
var _orcaFilamentCache=null; // [{id,name,vendor,type,color}, …]
var _visibleVendors=null; // Vendor-Sichtbarkeitsfilter (Issue #41); [] = alle
function _loadVisibleVendors(cb){
if(_visibleVendors!==null){ cb(_visibleVendors); return; }
fetch(_apiUrl('/kx/filament/visible_vendors')).then(function(r){return r.json();}).then(function(d){
_visibleVendors=d.result||[];
cb(_visibleVendors);
}).catch(function(){ _visibleVendors=[]; cb([]); });
}
function _loadOrcaFilaments(cb){
if(_orcaFilamentCache){ cb(_orcaFilamentCache); return; }
fetch(_apiUrl('/kx/filament/profiles')).then(function(r){return r.json();}).then(function(d){
_orcaFilamentCache=d.result||[];
cb(_orcaFilamentCache);
}).catch(function(){ cb([]); });
}
function _profileKey(vendor, name){
// Eindeutiger Selector: (vendor, name). IDs aus orca_filaments.json sind
// NICHT eindeutig (z.B. 136 Profile mit OGFL99). Wir kodieren beide in den
// ';
// User-Profile (is_user) zuerst — eigene Optgroup '★ Eigene' an erster Stelle.
var userProfs=matched.filter(function(p){return p.is_user;});
var systemProfs=matched.filter(function(p){return !p.is_user;});
function _appendOption(g, p){
var o=document.createElement('option');
o.value=_profileKey(p.vendor, p.name);
o.dataset.vendor=p.vendor;
o.dataset.name=p.name;
o.dataset.id=p.id || '';
o.textContent=(p.is_user?'★ ':'')+p.name;
if(o.value===wantKey) o.selected=true;
g.appendChild(o);
}
if(userProfs.length){
var gUser=document.createElement('optgroup');
gUser.label='★ '+(tr('orca_profile_user_label')||'Eigene Profile');
userProfs.forEach(function(p){ _appendOption(gUser, p); });
sel.appendChild(gUser);
}
// Vendor-Sichtbarkeitsfilter (Issue #41 Option A): nur gewählte Vendoren +
// Generic. Leere Liste = alle (rückwärtskompatibel). Eigene Profile (is_user)
// sind oben bereits unkonditional drin.
_loadVisibleVendors(function(vis){
var filtered=systemProfs;
if(vis&&vis.length){
var allow={};vis.forEach(function(v){allow[v]=1;});
allow['Generic']=1;
filtered=systemProfs.filter(function(p){return allow[p.vendor];});
}
var byVendor={};
filtered.forEach(function(p){ (byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p); });
Object.keys(byVendor).sort().forEach(function(v){
var g=document.createElement('optgroup'); g.label=v;
byVendor[v].forEach(function(p){ _appendOption(g, p); });
sel.appendChild(g);
});
});
});
}
// ── Pickr color picker ──────────────────────────────────────────────────────
var _pickr=null;
function _initPickr(hex){
// destroy previous instance if exists
if(_pickr){ try{ _pickr.destroyAndRemove(); }catch(e){} _pickr=null; }
var anchor=document.getElementById('slot-pickr-anchor');
if(!anchor||typeof Pickr==='undefined') return;
// fresh button element so Pickr can mount
anchor.innerHTML='';
_pickr=Pickr.create({
el:'#slot-pickr-btn',
theme:'nano',
default: hex||'#808080',
inline: true,
showAlways: true,
components:{
preview:true, opacity:false, hue:true,
interaction:{ hex:true, rgba:false, input:true, save:false, clear:false }
}
});
_pickr.on('change',function(color){
var h=color.toHEXA().toString().slice(0,7);
document.getElementById('slot-edit-color').value=h;
document.getElementById('slot-edit-preview').style.background=h;
});
// Theme anpassen: Pickr benutzt eigene CSS-Variablen, wir überschreiben via style
requestAnimationFrame(function(){
var el=anchor.querySelector('.pickr');
if(el) el.style.cssText='width:100%';
var app=anchor.querySelector('.pcr-app');
if(app){
app.style.cssText='position:relative;width:100%;box-shadow:none;background:transparent';
var btn=app.querySelector('.pcr-result');
if(btn) btn.style.cssText='background:var(--raised);border:1px solid var(--border);color:var(--txt);border-radius:6px;font-size:12px';
}
});
}
// ── Color swatches (localStorage, max 16) ──────────────────────────────────
var _SWATCH_KEY='kxb_color_swatches';
var _SWATCH_MAX=16;
function _loadSwatches(){
try{ return JSON.parse(localStorage.getItem(_SWATCH_KEY)||'[]'); }catch(e){ return []; }
}
function _saveSwatches(arr){ try{ localStorage.setItem(_SWATCH_KEY, JSON.stringify(arr)); }catch(e){} }
function _addSwatch(hex){
var arr=_loadSwatches().filter(function(c){ return c.toLowerCase()!==hex.toLowerCase(); });
arr.unshift(hex);
if(arr.length>_SWATCH_MAX) arr=arr.slice(0,_SWATCH_MAX);
_saveSwatches(arr);
}
function _renderSwatches(){
var el=document.getElementById('slot-color-swatches');
if(!el) return;
var arr=_loadSwatches();
if(!arr.length){ el.style.display='none'; return; }
el.style.display='flex';
el.innerHTML=arr.map(function(c){
return '';
}).join('');
}
function slotPickSwatch(hex){
if(_pickr){ _pickr.setColor(hex); }
var ci=document.getElementById('slot-edit-color');
if(ci) ci.value=hex;
document.getElementById('slot-edit-preview').style.background=hex;
}
// ── Copy color from other slot ──────────────────────────────────────────────
function _renderCopyFromSlot(currentGlobalIdx){
var slots=(window._amsSlots||[]).filter(function(s){
return s.global_index!==currentGlobalIdx && s.status==5 && Array.isArray(s.color);
});
var row=document.getElementById('slot-copy-row');
var sel=document.getElementById('slot-copy-select');
if(!row||!sel) return;
if(!slots.length){ row.style.display='none'; return; }
row.style.display='';
var ph=document.getElementById('lbl-slot-copy-from');
var phTxt=ph?ph.textContent:(T.slot_copy_from||'Copy color from slot…');
sel.innerHTML=''+slots.map(function(s){
var rgb=s.color;
var hex='#'+rgb.map(function(v){return('0'+Math.min(255,v).toString(16)).slice(-2)}).join('');
return '';
}).join('');
}
function slotCopyColor(sel){
if(!sel.value) return;
var ci=document.getElementById('slot-edit-color');
if(!ci) return;
ci.value=sel.value;
document.getElementById('slot-edit-preview').style.background=sel.value;
sel.selectedIndex=0;
}
// ───────────────────────────────────────────────────────────────────────────
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;
_initPickr(hex);
_renderSwatches();
_renderCopyFromSlot(globalIdx);
var mat=(slot.type||'PLA').toUpperCase();
document.getElementById('slot-edit-mat').value=mat;
// Normalisieren für Button-Highlighting: PLA-Varianten auf nächsten Preset mappen
var matNorm=_normalizeMat(mat);
var btns=document.getElementById('slot-mat-btns');
btns.innerHTML=_MAT_PRESETS.map(function(m){
var active=m===mat||m===matNorm;
return '';
}).join('');
// OrcaSlicer-Profil-Dropdown: aktuellen User-Override für diesen Slot
// aus /kx/filament/slots holen (enthält vendor+name+id).
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}).then(function(d){
var arr=d.result||[];
var entry=arr.find(function(x){return x.slot_index===globalIdx;})||{};
window._slotProfileMap=window._slotProfileMap||{};
window._slotProfileMap[globalIdx]={
id: entry.filament_id||'',
vendor:entry.filament_vendor||'',
name: entry.filament_name||'',
};
_fillSlotProfileDropdown(mat, entry.filament_vendor||'', entry.filament_name||'');
}).catch(function(){ _fillSlotProfileDropdown(mat,'',''); });
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(filename){
var fn=filename||S.file_ready;
function _doStartReadyFile(){
var btn=document.getElementById('file-ready-btn');
if(btn){btn.disabled=true;btn.textContent='…';}
post('/printer/print/start',{filename:fn})
.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(tr('log_error')+' '+e,'msg-err');
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
});
}
function _gateAndStart(fileObj){
if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){
maybeGateWebUpload(fileObj, function(){ startReadyFile(fn); });
return;
}
_doStartReadyFile();
}
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
if(currentFile){
_gateAndStart(currentFile);
return;
}
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===fn;})||null;
_gateAndStart(refreshed);
}).catch(function(){
_doStartReadyFile();
});
}
function cancelReadyFile(){
post('/api/file_ready/clear',{})
.then(function(){document.getElementById('file-ready-banner').style.display='none';});
}
// ── Aktionen für geladene/idle Datei in der Progress-Karte (Issue #55) ──
function startIdleFile(){
if(_lastLoadedFile) startReadyFile(_lastLoadedFile);
}
function startIdleFileWithSlots(){
if(_lastLoadedFile) startReadyFileWithSlots(_lastLoadedFile);
}
function clearIdleFile(){
_lastLoadedFile=null;
_idleCleared=true; // verhindert Nachladen von s.filename im nächsten poll() (Issue #57)
_fdAutoOpenedFile=null; // nächster Upload derselben Datei soll Dialog wieder öffnen
_fdUserCancelled=false;
_fdDialogOpen=false;
sessionStorage.removeItem('fdAutoOpenedFile');
sessionStorage.removeItem('fdUserCancelled');
sessionStorage.removeItem('webVerifyCancelledFileId');
S.file_ready=''; S.filename=''; S.thumbnail=''; // sofort lokal leeren, kein Warten auf nächsten Poll
var ib=document.getElementById('d-idle-btns');if(ib)ib.style.display='none';
var fn=document.getElementById('d-fname');if(fn){fn.textContent='–';fn.title='';}
var thumb=document.getElementById('d-thumbnail');if(thumb){thumb.style.display='none';thumb.src='';}
post('/api/file_ready/clear',{}).catch(function(){});
}
function selectMatPreset(m){
document.getElementById('slot-edit-mat').value=m;
highlightMatBtn(m);
// Filament-Profile-Dropdown an neues Material anpassen
// (vorherige Selektion zurücksetzen — andere Material-Profile passen nicht)
_fillSlotProfileDropdown(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)';
});
// Auch bei manueller Eingabe ins Material-Textfeld: Dropdown refreshen.
if(val) _fillSlotProfileDropdown(val, '', '');
}
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;
_addSwatch(hex);
var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA';
var color=hexToRgb(hex);
var slotIdx=_slotEditIndex;
var profSel=document.getElementById('slot-edit-profile');
var sel=profSel && profSel.selectedOptions && profSel.selectedOptions[0];
// Primärer Selector: (vendor, name). id ist nur Hint (aus dem JSON-data-attr
// mitgegeben — Backend lookt sie nochmal selber nach um veraltete Hints zu
// korrigieren).
var newProfVendor=sel?(sel.dataset.vendor||''):'';
var newProfName =sel?(sel.dataset.name ||''):'';
var newProfId =sel?(sel.dataset.id ||''):'';
// Sequenziell speichern: erst Profil-Override (config.ini), dann Material/Farbe
// (MQTT zum Drucker). Sonst können beide Pfade sich überholen und der Slot-State
// ist beim nächsten Re-Open inkonsistent.
fetch(_apiUrl('/kx/filament/slots/'+slotIdx+'/profile'),{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({vendor:newProfVendor, name:newProfName, id:newProfId})
})
.then(function(r){return r.json();})
.then(function(){
window._slotProfileMap=window._slotProfileMap||{};
if(newProfVendor && newProfName){
window._slotProfileMap[slotIdx]={id:newProfId, vendor:newProfVendor, name:newProfName};
} else {
delete window._slotProfileMap[slotIdx];
}
return post('/api/ams/set_slot',{index:slotIdx,type:mat,color:color});
})
.then(function(r){return r?r.json():null;})
.then(function(){
// Slot-Map refreshen damit die Karte sofort den Vendor zeigt.
return fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();});
})
.then(function(d){
var arr=(d && d.result)||[];
window._slotProfileMap={};
arr.forEach(function(e){
if(e.filament_vendor && e.filament_name){
window._slotProfileMap[e.slot_index]={
id: e.filament_id||'',
vendor:e.filament_vendor,
name: e.filament_name,
};
}
});
closeSlotEdit();
var profSuffix=newProfName?(' ['+newProfVendor+' '+newProfName+']'):'';
clog(tr('slot_edit_ok')+' '+(slotIdx+1)+': '+mat+' '+hex+profSuffix,'msg-ok');
// Sofortiges Re-Render mit aktuellem _slotProfileMap (poll() ist async
// und re-rendert beim nächsten Tick — wir wollen aber dass die Vendor-
// Badge JETZT direkt sichtbar wird).
if(typeof applyState==='function') applyState();
if(typeof poll==='function') poll();
})
.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){
localStorage.setItem('pollInterval',ms);
clearInterval(pollTimer);
pollTimer=setInterval(poll,ms);
}
function saveSettings(){
var btn=document.getElementById('btn-save-settings');
btn.disabled=true;btn.textContent='…';
var webUploadWarning=(document.getElementById('s-web-upload-warning')||{}).checked?1:0;
S.web_upload_warning=webUploadWarning;
// Start-Print-Behavior-Wechsel könnte den Auto-Open sonst dauerhaft blockieren
// (alter _fdUserCancelled bei gleicher file_ready) → Dialog-State zurücksetzen (Issue #57).
_fdUserCancelled=false;_fdAutoOpenedFile=null;
sessionStorage.removeItem('fdUserCancelled');sessionStorage.removeItem('fdAutoOpenedFile');
sessionStorage.removeItem('webVerifyCancelledFileId');
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,
vibration_compensation: (document.getElementById('s-vibration-compensation')||{}).checked?1:0,
camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0,
print_start_dialog: parseInt((document.getElementById('s-file-ready-mode')||{}).value||'1',10),
web_upload_warning:webUploadWarning,
poll_interval: Math.min(60,Math.max(1,parseInt((document.getElementById('s-poll-interval')||{}).value,10)||3)),
verbose_http_log: (document.getElementById('s-verbose-http-log')||{}).checked?1:0,
spoolman_server: (document.getElementById('s-spoolman-url')||{}).value||'',
spoolman_sync_rate: Math.max(0,parseInt((document.getElementById('s-spoolman-sync-rate')||{}).value||'30',10)),
}).then(function(){
btn.textContent=T.update_restarting;
setTimeout(function(){
btn.disabled=false;
setText('btn-save-settings',T.settings_save);
closeSettings();
_loadSpoolmanStatus();
poll();
},4000);
}).catch(function(e){
btn.disabled=false;setText('btn-save-settings',T.settings_save);
clog('Settings-Fehler: '+e,'msg-err');
});
}
var _updateDockerOnly=false;
function checkUpdate(){
var sb=document.getElementById('update-status');
sb.textContent=T.update_checking;
document.getElementById('btn-update-apply').style.display='none';
_updateTag='';_updateUrl='';_updateDockerOnly=false;
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()){
// Changelog als Markdown-Text (pre-formatiert) anzeigen
cl.textContent=d.changelog;cl.style.display='block';
} else {cl.style.display='none';}
_updateDockerOnly=!!d.docker_only;
if(d.update_available){
sb.textContent=d.latest+' '+T.update_available;
sb.style.color='var(--ok)';
_updateTag=d.tag;_updateUrl=d.download_url||'';
var btn=document.getElementById('btn-update-apply');
if(_updateDockerOnly){
btn.textContent=T.update_docker||'docker compose pull';
btn.title='docker compose pull && docker compose up -d';
} else {
btn.textContent=T.update_apply;
btn.title='';
}
btn.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(){
var sb=document.getElementById('update-status');
var btn=document.getElementById('btn-update-apply');
if(_updateDockerOnly){
// Nightly: kein Self-Update, Docker-Befehl in Zwischenablage kopieren
var cmd='docker compose pull && docker compose up -d';
if(navigator.clipboard){
navigator.clipboard.writeText(cmd).then(function(){
sb.textContent=T.update_docker_copied||'Befehl kopiert: '+cmd;
});
} else {
sb.textContent=cmd;
}
return;
}
if(!_updateUrl)return;
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();
// Slot-Profile-Map initial laden, sonst zeigen die Karten beim ersten
// Render keine Vendor-Badge obwohl in der config.ini ein Override steht.
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}).then(function(d){
var arr=(d && d.result)||[];
window._slotProfileMap={};
arr.forEach(function(e){
if(e.filament_vendor && e.filament_name){
window._slotProfileMap[e.slot_index]={
id: e.filament_id||'',
vendor:e.filament_vendor,
name: e.filament_name,
};
}
});
}).catch(function(){});
poll();pollTimer=setInterval(poll,ms);
setInterval(_loadSpoolmanStatus,30000);
// initDashGrid() is called at the very end of the file, after all the
// DASH_* var declarations below have executed (they are hoisted but not yet
// assigned at this point in the IIFE).
})();
// ── Dashboard Free Grid (Issue #89) ────────────────────────────────────────
// User-customizable dashboard powered by GridStack (v10, docs: gridstack.js).
// Cards move + resize on a 12-column snap grid; layout persisted per browser
// in localStorage. Built on documented APIs only:
// - float:false (default) -> compact packing, no holes between cards
// - staticGrid:true -> locked by default, setStatic(false) in edit mode
// - grid.save(false)/load(layout,false) -> serialize/restore by gs-id
// - draggable.cancel -> inputs/buttons/img excluded from drag start
// - columnOpts.breakpoints -> 1-column below 700px GRID width (sidebar-aware)
var DASH_STORAGE_KEY='dashLayout';
var DASH_LAYOUT_VERSION=3; // v1/v2 = older editors; discard those states
var DASH_CARD_KEYS=['camera','progress','temps','motion','speed','fan','ams'];
// Layout arrays in GridStack save()/load() format. cellHeight=60px.
var DASH_DEFAULT_LAYOUT=[
{id:'camera', x:0,y:0, w:12,h:7},
{id:'progress',x:0,y:7, w:12,h:6},
{id:'temps', x:0,y:13,w:6, h:7},
{id:'motion', x:6,y:13,w:6, h:7},
{id:'speed', x:0,y:20,w:6, h:3},
{id:'fan', x:6,y:20,w:6, h:3},
{id:'ams', x:0,y:23,w:12,h:4},
];
// "Wide desktop" preset (Blaim, Issue #89): big camera left, settings center,
// motion right.
var DASH_PRESET_WIDE=[
{id:'progress',x:0, y:0, w:12,h:4},
{id:'camera', x:0, y:4, w:6, h:11},
{id:'temps', x:6, y:4, w:3, h:7},
{id:'speed', x:6, y:11,w:3, h:2},
{id:'fan', x:6, y:13,w:3, h:2},
{id:'motion', x:9, y:4, w:3, h:11},
{id:'ams', x:0, y:15,w:12,h:4},
];
var _dashGrid=null;
var _dashEditing=false;
var _dashHidden=[]; // [{id,x,y,w,h}] cards currently hidden (position kept for re-show)
// GridStack requires .grid-stack-item > .grid-stack-item-content around each
// card — wrap the existing .card elements in place (IDs/event handlers survive,
// appendChild moves nodes without recreating them).
function _dashWrapCards(){
var grid=document.getElementById('dash-grid');
if(!grid)return;
DASH_CARD_KEYS.forEach(function(key){
var card=grid.querySelector('[data-card="'+key+'"]');
if(!card||card.parentNode.classList.contains('grid-stack-item-content'))return;
var item=document.createElement('div');
item.className='grid-stack-item';
item.setAttribute('gs-id',key);
var content=document.createElement('div');
content.className='grid-stack-item-content';
grid.insertBefore(item,card);
item.appendChild(content);
content.appendChild(card);
});
}
function _dashItem(key){
return document.querySelector('#dash-grid .grid-stack-item[gs-id="'+key+'"]');
}
function _dashLoadState(){
try{
var raw=localStorage.getItem(DASH_STORAGE_KEY);
if(!raw)return null;
var s=JSON.parse(raw);
if(!s||s.version!==DASH_LAYOUT_VERSION||!Array.isArray(s.layout))return null;
return s;
}catch(e){return null;}
}
function _dashSaveState(){
if(!_dashGrid)return;
localStorage.setItem(DASH_STORAGE_KEY,JSON.stringify({
version:DASH_LAYOUT_VERSION,
layout:_dashGrid.save(false), // [{id,x,y,w,h},…] — visible widgets only
hidden:_dashHidden.slice(),
}));
}
function initDashGrid(){
var el=document.getElementById('dash-grid');
if(!el||typeof GridStack==='undefined')return;
_dashWrapCards();
_dashGrid=GridStack.init({
cellHeight:60,
margin:8,
staticGrid:true, // locked by default; setStatic(false) enables drag+resize
columnOpts:{breakpoints:[{w:700,c:1}]}, // grid-width based (sidebar-aware)
draggable:{cancel:'input,textarea,button,select,option,img,.slider'},
},el);
var state=_dashLoadState();
if(state){
_dashHidden=Array.isArray(state.hidden)?state.hidden:[];
_dashGrid.load(state.layout,false); // update matching ids, no add/remove
_dashHidden.forEach(function(n){
var item=_dashItem(n.id);
if(item){_dashGrid.removeWidget(item,false);item.style.display='none';}
});
}else{
_dashGrid.load(DASH_DEFAULT_LAYOUT,false);
}
_dashGrid.on('change',function(){if(_dashEditing)_dashSaveState();});
_dashRefreshPresetDropdown();
}
function toggleDashEdit(){
if(!_dashGrid)return;
_dashEditing=!_dashEditing;
_dashGrid.setStatic(!_dashEditing);
document.getElementById('dash-grid').classList.toggle('editing',_dashEditing);
document.getElementById('dash-lbl-edit').textContent=_dashEditing?(T.dash_done||'Done'):(T.dash_edit||'Customize dashboard');
document.getElementById('dash-reset-btn').style.display=_dashEditing?'':'none';
document.getElementById('dash-preset').style.display=_dashEditing?'':'none';
document.getElementById('dash-preset-save-btn').style.display=_dashEditing?'':'none';
_dashUpdatePresetDeleteBtn();
if(_dashEditing)_dashInjectControls(); else _dashRemoveControls();
_dashRenderHiddenBar();
if(!_dashEditing)_dashSaveState();
}
// ── Custom presets (user-saved layouts) ────────────────────────────────────
var DASH_CUSTOM_PRESETS_KEY='dashCustomPresets';
var DASH_BUILTIN_PRESET_IDS=['standard','wide89'];
function _dashLoadCustomPresets(){
try{
var raw=localStorage.getItem(DASH_CUSTOM_PRESETS_KEY);
var obj=raw?JSON.parse(raw):{};
return (obj&&typeof obj==='object')?obj:{};
}catch(e){return {};}
}
function _dashSaveCustomPresets(presets){
localStorage.setItem(DASH_CUSTOM_PRESETS_KEY,JSON.stringify(presets));
}
function _dashRefreshPresetDropdown(){
var sel=document.getElementById('dash-preset');
if(!sel)return;
var current=sel.value;
var customs=_dashLoadCustomPresets();
// Remove previously-rendered custom