// ── 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+=''; } 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