1268 lines
61 KiB
JavaScript
1268 lines
61 KiB
JavaScript
// ── State ──
|
||
var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
|
||
print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0,
|
||
curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–',
|
||
camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80,ams_slots:[]};
|
||
var tempHistory={n:[],b:[]};
|
||
var camOn=false;
|
||
var currentStep=1;
|
||
var currentPanel='dashboard';
|
||
|
||
// ── Theme ──
|
||
function toggleTheme(){
|
||
var h=document.documentElement;
|
||
h.setAttribute('data-theme',h.getAttribute('data-theme')==='dark'?'light':'dark');
|
||
localStorage.setItem('theme',h.getAttribute('data-theme'));
|
||
}
|
||
(function(){var t=localStorage.getItem('theme');if(t)document.documentElement.setAttribute('data-theme',t)})();
|
||
|
||
// ── i18n ──
|
||
var LANG_DE={
|
||
header_status_standby:'Bereit',header_status_printing:'Druckt',header_status_complete:'Fertig',header_status_error:'Fehler',
|
||
kobra_free:'Bereit',kobra_busy:'Beschäftigt',kobra_printing:'Druckt',kobra_preheating:'Aufheizen',kobra_auto_leveling:'Nivellierung',kobra_checking:'Prüfung',kobra_updated:'Aktualisierung',kobra_init:'Initialisierung',kobra_pausing:'Pausiert...',kobra_paused:'Pausiert',kobra_resuming:'Fortsetzen...',kobra_resumed:'Fortgesetzt',kobra_stopping:'Stoppt...',kobra_stoped:'Gestoppt',kobra_finished:'Abgeschlossen',kobra_failed:'Fehler',kobra_canceled:'Abgebrochen',kobra_offline:'Offline',
|
||
nav_dashboard:'Dashboard',nav_print:'Druck',nav_temps:'Temperaturen',nav_motion:'Achsen',nav_ams:'AMS',nav_extras:'Licht / Lüfter',nav_console:'Konsole',
|
||
card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',card_speed:'Druckgeschwindigkeit',card_cam:'Kamera',lbl_elapsed:'Verstrichen:',lbl_remaining:'Restzeit:',lbl_slicer_time:'Slicer-Schätzung:',lbl_layers:'Layer',
|
||
speed_silent:'🐢 Leise',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
|
||
lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen',
|
||
cam_placeholder:'📷 Kamera nicht gestartet',btn_cam_start:'▶ Kamera',btn_cam_stop:'◼ Kamera',
|
||
btn_pause:'⏸ Pause',btn_resume:'▶ Weiter',btn_cancel:'✕ Stopp',
|
||
label_nozzle:'Nozzle',label_bed:'Bett',label_fan:'🌀 Lüfter',label_light:'💡 Licht',label_on_off:'Ein / Aus',label_speed:'Geschwindigkeit',
|
||
panel_print_title:'Drucksteuerung',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Fortsetzen',panel_print_btn_cancel:'✕ Abbrechen',panel_print_temps_live:'Temperaturen (Live)',
|
||
label_set:'Setzen',label_off:'Aus',
|
||
panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heizbett',panel_temps_chart:'Verlauf (letzte 60 Messungen)',label_target_c:'Ziel:',
|
||
panel_motion_xy:'XY-Achsen',panel_motion_z:'Z-Achse',label_step:'Schrittweite:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motoren aus',
|
||
panel_ams_title:'AMS / Filamentbox',ams_no_data:'Keine AMS-Daten empfangen',label_slot:'Slot',ams_empty:'Leer',
|
||
panel_extras_light:'Licht',panel_extras_fan:'Lüfter',panel_extras_camera:'Kamera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop',
|
||
panel_console_title:'Ereignis-Log',
|
||
log_light_on:'Licht an',log_light_off:'Licht aus',log_fan:'Lüfter →',log_nozzle:'Nozzle →',log_bed:'Bett →',log_axis:'Achse',log_home:'Home',log_home_all:'Home All',log_cam_start:'Kamera gestartet:',log_cam_stop:'Kamera gestoppt',log_poll_error:'Poll-Fehler:',log_error:'Fehler:',
|
||
confirm_cancel:'Druck wirklich abbrechen?',
|
||
settings_title:'Einstellungen',settings_connection:'Verbindung',settings_print:'Druckeinstellungen',settings_poll:'Poll-Intervall',settings_version:'Version',
|
||
settings_save:'Speichern & Neustart',settings_printer_name:'Drucker-Name',settings_printer_ip:'Drucker-IP',settings_mqtt_port:'MQTT-Port',
|
||
settings_username:'MQTT-Benutzername',settings_password:'MQTT-Passwort',settings_device_id:'Device-ID',settings_mode_id:'Mode-ID',hint_ip_no_port:'Nur IP-Adresse, kein Port (z.B. 192.168.1.102)',
|
||
settings_default_slot:'Standard-Slot (Einfarbdruck)',settings_slot_auto:'Auto (alle belegten Slots)',settings_auto_leveling:'Auto-Leveling vor Druck',
|
||
update_check:'Auf Updates prüfen',update_checking:'Prüfe...',update_available:'verfügbar',update_none:'Bereits aktuell',
|
||
update_apply:'Jetzt installieren',update_applying:'Lade herunter...',update_restarting:'Starte neu...',update_error:'Fehler',
|
||
btn_connect:'⚡ Verbinden',btn_disconnect:'✕ Trennen',
|
||
lbl_conn_error:'Verbindungsfehler:',
|
||
slot_edit_title:'Slot bearbeiten',slot_edit_color:'Farbe',slot_edit_material:'Material',
|
||
slot_edit_save:'💾 Speichern',slot_edit_custom:'z.B. PLA, PETG, ABS…',
|
||
slot_edit_ok:'AMS Slot',
|
||
log_dir_all:'Alle',
|
||
file_ready_btn:'▶ Druck starten',
|
||
file_slots_btn:'🎨 Slots wählen',
|
||
file_cancel_btn:'✕ Abbrechen',
|
||
nav_printers:'Drucker',
|
||
add_printer:'Drucker hinzufügen',apd_lbl_ip:'Drucker-IP',apd_lbl_name:'Name (optional)',
|
||
apd_fetching:'Hole Daten vom Drucker…',apd_success:'Drucker hinzugefügt, Bridge startet neu…',apd_err_ip:'Bitte IP-Adresse eingeben',
|
||
printers_remove:'Drucker entfernen',printers_remove_confirm:'Drucker "{name}" entfernen? Die Bridge startet neu.',
|
||
printers_active:'● aktiv',
|
||
printers_switch:'Wechseln →',
|
||
printers_current:'Aktueller Drucker',
|
||
printers_loading:'Lade…',
|
||
printers_none:'Keine Drucker konfiguriert.',
|
||
printers_empty_hint:'Noch kein Drucker eingerichtet.',
|
||
nav_browser:'Browser',
|
||
panel_browser_title:'Datei-Browser',
|
||
store_empty:'Noch keine Dateien hochgeladen.',
|
||
store_refresh:'↻ Aktualisieren',
|
||
store_print:'▶ Drucken',
|
||
store_delete_confirm:'Datei löschen?',
|
||
store_print_confirm:'Datei drucken?',
|
||
store_no_results:'Keine Dateien gefunden.',
|
||
store_never:'noch nicht gedruckt',
|
||
sf_all:'Alle',sf_ok:'✓ Erfolgreich',sf_err:'✗ Fehler',sf_new:'Neu',
|
||
ss_date:'↓ Datum',ss_name:'A–Z Name',ss_dur:'⏱ Druckzeit'
|
||
};
|
||
var LANG_EN={
|
||
header_status_standby:'Ready',header_status_printing:'Printing',header_status_complete:'Complete',header_status_error:'Error',
|
||
kobra_free:'Ready',kobra_busy:'Busy',kobra_printing:'Printing',kobra_preheating:'Preheating',kobra_auto_leveling:'Auto Leveling',kobra_checking:'Checking',kobra_updated:'Updating',kobra_init:'Initializing',kobra_pausing:'Pausing...',kobra_paused:'Paused',kobra_resuming:'Resuming...',kobra_resumed:'Resumed',kobra_stopping:'Stopping...',kobra_stoped:'Stopped',kobra_finished:'Finished',kobra_failed:'Error',kobra_canceled:'Cancelled',kobra_offline:'Offline',
|
||
nav_dashboard:'Dashboard',nav_print:'Print',nav_temps:'Temperatures',nav_motion:'Motion',nav_ams:'AMS',nav_extras:'Light / Fan',nav_console:'Console',
|
||
card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera',lbl_elapsed:'Elapsed:',lbl_remaining:'Remaining:',lbl_slicer_time:'Slicer estimate:',lbl_layers:'Layer',
|
||
speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
|
||
lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload',
|
||
cam_placeholder:'📷 Camera not started',btn_cam_start:'▶ Camera',btn_cam_stop:'◼ Camera',
|
||
btn_pause:'⏸ Pause',btn_resume:'▶ Resume',btn_cancel:'✕ Stop',
|
||
label_nozzle:'Nozzle',label_bed:'Bed',label_fan:'🌀 Fan',label_light:'💡 Light',label_on_off:'On / Off',label_speed:'Speed',
|
||
panel_print_title:'Print Control',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Resume',panel_print_btn_cancel:'✕ Cancel',panel_print_temps_live:'Temperatures (Live)',
|
||
label_set:'Set',label_off:'Off',
|
||
panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heated Bed',panel_temps_chart:'History (last 60 readings)',label_target_c:'Target:',
|
||
panel_motion_xy:'XY Axes',panel_motion_z:'Z Axis',label_step:'Step size:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motors Off',
|
||
panel_ams_title:'AMS / Filament Box',ams_no_data:'No AMS data received',label_slot:'Slot',ams_empty:'Empty',
|
||
panel_extras_light:'Light',panel_extras_fan:'Fan',panel_extras_camera:'Camera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop',
|
||
panel_console_title:'Event Log',
|
||
log_light_on:'Light on',log_light_off:'Light off',log_fan:'Fan →',log_nozzle:'Nozzle →',log_bed:'Bed →',log_axis:'Axis',log_home:'Home',log_home_all:'Home All',log_cam_start:'Camera started:',log_cam_stop:'Camera stopped',log_poll_error:'Poll error:',log_error:'Error:',
|
||
confirm_cancel:'Really cancel the print?',
|
||
settings_title:'Settings',settings_connection:'Connection',settings_print:'Print Settings',settings_poll:'Poll Interval',settings_version:'Version',
|
||
settings_save:'Save & Restart',settings_printer_name:'Printer Name',settings_printer_ip:'Printer IP',settings_mqtt_port:'MQTT Port',
|
||
settings_username:'MQTT Username',settings_password:'MQTT Password',settings_device_id:'Device ID',settings_mode_id:'Mode ID',hint_ip_no_port:'IP address only, no port (e.g. 192.168.1.102)',
|
||
settings_default_slot:'Default Slot (single color)',settings_slot_auto:'Auto (all loaded slots)',settings_auto_leveling:'Auto-Leveling before print',
|
||
update_check:'Check for Updates',update_checking:'Checking...',update_available:'available',update_none:'Already up to date',
|
||
update_apply:'Install Now',update_applying:'Downloading...',update_restarting:'Restarting...',update_error:'Error',
|
||
btn_connect:'⚡ Connect',btn_disconnect:'✕ Disconnect',
|
||
lbl_conn_error:'Connection error:',
|
||
slot_edit_title:'Edit Slot',slot_edit_color:'Color',slot_edit_material:'Material',
|
||
slot_edit_save:'💾 Save',slot_edit_custom:'e.g. PLA, PETG, ABS…',
|
||
slot_edit_ok:'AMS Slot',
|
||
log_dir_all:'All',
|
||
file_ready_btn:'▶ Start Print',
|
||
file_slots_btn:'🎨 Select Slots',
|
||
file_cancel_btn:'✕ Cancel',
|
||
nav_printers:'Printers',
|
||
add_printer:'Add printer',apd_lbl_ip:'Printer IP',apd_lbl_name:'Name (optional)',
|
||
apd_fetching:'Fetching data from printer…',apd_success:'Printer added, bridge restarting…',apd_err_ip:'Please enter an IP address',
|
||
printers_remove:'Remove printer',printers_remove_confirm:'Remove printer "{name}"? The bridge will restart.',
|
||
printers_active:'● active',
|
||
printers_switch:'Switch →',
|
||
printers_current:'Current printer',
|
||
printers_loading:'Loading…',
|
||
printers_none:'No printers configured.',
|
||
printers_empty_hint:'No printer set up yet.',
|
||
nav_browser:'Browser',
|
||
panel_browser_title:'File Browser',
|
||
store_empty:'No files uploaded yet.',
|
||
store_refresh:'↻ Refresh',
|
||
store_print:'▶ Print',
|
||
store_delete_confirm:'Delete file?',
|
||
store_print_confirm:'Print file?',
|
||
store_no_results:'No files found.',
|
||
store_never:'never printed',
|
||
sf_all:'All',sf_ok:'✓ Completed',sf_err:'✗ Failed',sf_new:'New',
|
||
ss_date:'↓ Date',ss_name:'A–Z Name',ss_dur:'⏱ Print time'
|
||
};
|
||
// Multi-Printer: BASE_URL aus Pathname (/printer2 → andere Bridge-Instanz)
|
||
var _printers=[];
|
||
var _activePrinter=null;
|
||
(function(){
|
||
var path=window.location.pathname.replace(/\/+$/,'');
|
||
var m=path.match(/^\/printer(\d+)$/);
|
||
var idx=m?parseInt(m[1]):1;
|
||
window._printerIndex=idx;
|
||
})();
|
||
function _apiUrl(path){
|
||
if(_activePrinter&&_activePrinter.bridge_url){
|
||
return _activePrinter.bridge_url.replace(/\/+$/,'')+path;
|
||
}
|
||
return path;
|
||
}
|
||
function initPrinters(){
|
||
fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){ // immer lokale Instanz für Drucker-Liste
|
||
_printers=d.result||[];
|
||
var idx=window._printerIndex||1;
|
||
_activePrinter=_printers.find(function(p){return String(p.id)===String(idx)})||_printers[0]||null;
|
||
renderPrinterDropdown();
|
||
}).catch(function(){});
|
||
}
|
||
function renderPrinterDropdown(){
|
||
var wrap=document.getElementById('printer-dropdown-wrap');
|
||
var single=document.getElementById('h-pname-single');
|
||
var name=_printers.length===0?'–':(_activePrinter?(_activePrinter.name||'Kobra X'):'Kobra X');
|
||
var pname=document.getElementById('h-pname');
|
||
if(pname)pname.textContent=name;
|
||
if(single)single.textContent=name;
|
||
if(_printers.length>1){
|
||
if(wrap)wrap.style.display='';
|
||
if(single)single.style.display='none';
|
||
var menu=document.getElementById('printer-dropdown-menu');
|
||
if(menu){
|
||
menu.innerHTML=_printers.map(function(p){
|
||
var active=_activePrinter&&String(p.id)===String(_activePrinter.id);
|
||
var num=p.id;
|
||
return '<a href="/printer'+num+'" style="display:block;padding:10px 14px;color:'+(active?'var(--accent)':'var(--txt)')+';text-decoration:none;font-size:13px;border-bottom:1px solid var(--border)" '+(active?'style="font-weight:600"':'')+'>'+
|
||
(active?'▶ ':'')+p.name+'</a>';
|
||
}).join('');
|
||
}
|
||
} else {
|
||
if(wrap)wrap.style.display='none';
|
||
if(single)single.style.display='';
|
||
}
|
||
}
|
||
function togglePrinterDropdown(){
|
||
var menu=document.getElementById('printer-dropdown-menu');
|
||
if(menu)menu.style.display=menu.style.display==='none'?'block':'none';
|
||
}
|
||
document.addEventListener('click',function(e){
|
||
var wrap=document.getElementById('printer-dropdown-wrap');
|
||
if(wrap&&!wrap.contains(e.target)){
|
||
var menu=document.getElementById('printer-dropdown-menu');
|
||
if(menu)menu.style.display='none';
|
||
}
|
||
});
|
||
|
||
var currentLang='de';
|
||
var T=LANG_DE;
|
||
function toggleLang(){
|
||
currentLang=currentLang==='de'?'en':'de';
|
||
T=currentLang==='de'?LANG_DE:LANG_EN;
|
||
localStorage.setItem('lang',currentLang);
|
||
document.getElementById('lang-btn').textContent=currentLang==='de'?'EN':'DE';
|
||
document.documentElement.setAttribute('lang',currentLang);
|
||
applyLang();
|
||
}
|
||
function applyLang(){
|
||
// Nav
|
||
var nb=document.getElementById('nb-dashboard');if(nb)nb.querySelector('.nav-text').textContent=T.nav_dashboard;
|
||
nb=document.getElementById('nb-console');if(nb)nb.querySelector('.nav-text').textContent=T.nav_console;
|
||
nb=document.getElementById('nb-printers');if(nb)nb.querySelector('.nav-text').textContent=T.nav_printers;
|
||
nb=document.getElementById('nb-store');if(nb)nb.querySelector('.nav-text').textContent=T.nav_browser;
|
||
// Bottom nav
|
||
var bnb=document.getElementById('bnb-dashboard');if(bnb)bnb.lastChild.textContent=T.nav_dashboard;
|
||
bnb=document.getElementById('bnb-console');if(bnb)bnb.lastChild.textContent=T.nav_console;
|
||
bnb=document.getElementById('bnb-printers');if(bnb)bnb.lastChild.textContent=T.nav_printers;
|
||
bnb=document.getElementById('bnb-store');if(bnb)bnb.lastChild.textContent=T.nav_browser;
|
||
// Browser panel
|
||
setText('printers-panel-title','🖨 '+T.nav_printers);
|
||
setText('add-printer-btn-label',T.add_printer);
|
||
setText('apd-title',T.add_printer);
|
||
setText('apd-lbl-ip',T.apd_lbl_ip);
|
||
setText('apd-lbl-name',T.apd_lbl_name);
|
||
setText('store-panel-title','🗂 '+T.panel_browser_title);
|
||
var srb=document.getElementById('store-refresh-btn');if(srb)srb.textContent=T.store_refresh;
|
||
setText('store-empty',T.store_empty);
|
||
setText('sf-all',T.sf_all);setText('sf-ok',T.sf_ok);setText('sf-err',T.sf_err);setText('sf-new',T.sf_new);
|
||
setText('ss-date',T.ss_date);setText('ss-name',T.ss_name);setText('ss-dur',T.ss_dur);
|
||
// Dashboard card titles
|
||
setText('d-card-progress',T.card_progress);
|
||
setText('d-card-temps',T.card_temps);
|
||
setText('d-card-lightfan',T.card_light_fan);
|
||
setText('d-card-speed',T.card_speed);
|
||
setText('d-card-cam',T.card_cam);
|
||
setText('d-card-ams',T.panel_ams_title);
|
||
setText('d-lbl-elapsed',T.lbl_elapsed);
|
||
setText('d-lbl-remain',T.lbl_remaining);
|
||
setText('d-slicer-label',T.lbl_slicer_time);
|
||
setText('d-lbl-layers',T.lbl_layers);
|
||
setText('d-lbl-light',T.lbl_light);
|
||
setText('d-lbl-bed',T.label_bed);
|
||
// Dashboard buttons
|
||
setText('d-btn-pause',T.btn_pause);
|
||
setText('d-btn-resume',T.btn_resume);
|
||
setText('d-btn-cancel',T.btn_cancel);
|
||
setText('cam-toggle-btn',camOn?T.btn_cam_stop:T.btn_cam_start);
|
||
setText('cam-placeholder-txt',T.cam_placeholder);
|
||
// Temp labels
|
||
document.querySelectorAll('.lbl-set').forEach(e=>e.textContent=T.label_set);
|
||
document.querySelectorAll('.lbl-off').forEach(e=>e.textContent=T.label_off);
|
||
setText('d-chart-label',T.panel_temps_chart);
|
||
// Axis labels
|
||
setText('ptitle-motion-xy',T.panel_motion_xy);
|
||
setText('ptitle-motion-z',T.panel_motion_z);
|
||
document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z);
|
||
document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy);
|
||
document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all);
|
||
document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors);
|
||
document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step);
|
||
document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':','')));
|
||
// Console
|
||
setText('ptitle-console',T.panel_console_title);
|
||
// Settings modal
|
||
setText('modal-title-settings',T.settings_title);
|
||
setText('modal-sec-connection',T.settings_connection);
|
||
setText('modal-sec-print',T.settings_print);
|
||
setText('modal-sec-poll',T.settings_poll);
|
||
setText('modal-sec-version',T.settings_version);
|
||
setText('btn-save-settings',T.settings_save);
|
||
setText('lbl-printer-name',T.settings_printer_name);
|
||
setText('lbl-printer-ip',T.settings_printer_ip);
|
||
setText('lbl-mqtt-port',T.settings_mqtt_port);
|
||
setText('lbl-username',T.settings_username);
|
||
setText('lbl-password',T.settings_password);
|
||
setText('lbl-device-id',T.settings_device_id);
|
||
setText('lbl-mode-id',T.settings_mode_id);
|
||
setText('lbl-default-slot',T.settings_default_slot);
|
||
setText('opt-slot-auto',T.settings_slot_auto);
|
||
setText('lbl-auto-leveling',T.settings_auto_leveling);
|
||
|
||
setText('lbl-update-check',T.update_check);
|
||
setText('lbl-update-apply',T.update_apply);
|
||
// Speed buttons
|
||
setText('d-spd-lbl-1',T.speed_silent.replace(/^\S+\s/,''));
|
||
setText('d-spd-lbl-2',T.speed_normal.replace(/^\S+\s/,''));
|
||
setText('d-spd-lbl-3',T.speed_sport.replace(/^\S+\s/,''));
|
||
// AMS feed/unload
|
||
document.querySelectorAll('.lbl-feed').forEach(e=>e.textContent=T.lbl_feed);
|
||
document.querySelectorAll('.lbl-unload').forEach(e=>e.textContent=T.lbl_unload);
|
||
// conn-btn text (nur wenn nicht im Übergangszustand)
|
||
updateConnBtn();
|
||
// Slot-Edit-Dialog
|
||
setText('lbl-slot-color',T.slot_edit_color);
|
||
setText('lbl-slot-material',T.slot_edit_material);
|
||
setText('btn-slot-edit-save',T.slot_edit_save);
|
||
var mi=document.getElementById('slot-edit-mat');if(mi)mi.setAttribute('placeholder',T.slot_edit_custom);
|
||
setText('logdir-all',T.log_dir_all);
|
||
setText('file-ready-btn',T.file_ready_btn);
|
||
setText('file-slots-btn',T.file_slots_btn);
|
||
setText('file-cancel-btn',T.file_cancel_btn);
|
||
setText('file-cancel-btn',T.file_cancel_btn);
|
||
}
|
||
function setText(id,txt){var el=document.getElementById(id);if(el)el.textContent=txt;}
|
||
(function(){
|
||
var l=localStorage.getItem('lang')||'de';
|
||
currentLang=l;T=l==='de'?LANG_DE:LANG_EN;
|
||
document.getElementById('lang-btn').textContent=l==='de'?'EN':'DE';
|
||
document.documentElement.setAttribute('lang',l);
|
||
// defer until DOM ready
|
||
window.addEventListener('DOMContentLoaded',function(){
|
||
applyLang();
|
||
// Kein Drucker konfiguriert? → direkt in den Drucker-Tab (zeigt "+ Drucker hinzufügen")
|
||
fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){
|
||
if(!d.result||!d.result.length){showPanel('printers');loadPrinterTab();}
|
||
}).catch(function(){});
|
||
});
|
||
})();
|
||
|
||
// ── Panel nav ──
|
||
function showPanel(id){
|
||
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
|
||
document.getElementById('panel-'+id).classList.add('active');
|
||
document.querySelectorAll('.nav-btn,.bnav-btn').forEach(b=>b.classList.remove('active'));
|
||
var nb=document.getElementById('nb-'+id);if(nb)nb.classList.add('active');
|
||
var bnb=document.getElementById('bnb-'+id);if(bnb)bnb.classList.add('active');
|
||
currentPanel=id;
|
||
}
|
||
|
||
// ── Console log ──
|
||
var consoleLogs=[];
|
||
var logAutoScroll=true;
|
||
var logBadgeCount=0;
|
||
var logDirFilter='all'; // 'all'|'rx'|'tx'
|
||
var logTopicFilter=''; // '' = no topic filter
|
||
|
||
function clog(msg,cls){
|
||
cls=cls||'msg-info';
|
||
var ts=new Date().toLocaleTimeString('de',{hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
||
_appendLog({ts:ts,lvl:'',name:'ui',msg:msg},cls);
|
||
}
|
||
function _lvlCls(lvl){
|
||
if(lvl==='ERROR'||lvl==='CRITICAL')return'msg-err';
|
||
if(lvl==='WARNING')return'msg-warn';
|
||
if(lvl==='DEBUG')return'msg-info';
|
||
return'msg-ok';
|
||
}
|
||
function _appendLog(entry,forceCls){
|
||
var cls=forceCls||_lvlCls(entry.lvl);
|
||
var label=entry.name?'['+entry.name+'] ':'';
|
||
consoleLogs.push({ts:entry.ts,msg:label+entry.msg,cls:cls});
|
||
if(consoleLogs.length>500)consoleLogs.shift();
|
||
// Badge wenn Tab nicht aktiv und Fehler/Warnungen
|
||
if(currentPanel!=='console'&&(cls==='msg-err'||cls==='msg-warn')){
|
||
logBadgeCount++;
|
||
var bc=logBadgeCount>99?'99+':logBadgeCount;
|
||
['log-badge','log-badge-bot'].forEach(function(id){var b=document.getElementById(id);if(b){b.style.display='inline';b.textContent=bc;}});
|
||
}
|
||
renderLog();
|
||
}
|
||
function setLogDir(dir){
|
||
logDirFilter=dir;
|
||
document.querySelectorAll('.log-dir-btn').forEach(function(b){
|
||
b.style.background=b.id==='logdir-'+dir?'var(--accent)':'var(--raised)';
|
||
b.style.color=b.id==='logdir-'+dir?'#fff':'var(--txt2)';
|
||
});
|
||
renderLog();
|
||
}
|
||
function setLogTopic(topic){
|
||
var inp=document.getElementById('log-filter');
|
||
var active=inp.value===topic;
|
||
inp.value=active?'':topic;
|
||
document.querySelectorAll('.log-topic-btn').forEach(function(b){
|
||
var on=!active&&b.getAttribute('data-topic')===topic;
|
||
b.style.background=on?'var(--accent)':'var(--raised)';
|
||
b.style.color=on?'#fff':'var(--txt2)';
|
||
});
|
||
renderLog();
|
||
}
|
||
function renderLog(){
|
||
var el=document.getElementById('console-log');
|
||
if(!el)return;
|
||
var filter=(document.getElementById('log-filter')||{}).value||'';
|
||
var fl=filter.toLowerCase();
|
||
var rows=consoleLogs.filter(function(l){
|
||
var m=l.msg;
|
||
if(logDirFilter==='rx'&&!/ RX[ (]/.test(m))return false;
|
||
if(logDirFilter==='tx'&&!/ TX[ (]/.test(m))return false;
|
||
if(fl&&!m.toLowerCase().includes(fl))return false;
|
||
return true;
|
||
});
|
||
var savedScroll=logAutoScroll?null:el.scrollTop;
|
||
el.innerHTML=rows.map(l=>`<div><span class="ts">${l.ts}</span><span class="${l.cls}">${escHtml(l.msg)}</span></div>`).join('');
|
||
if(logAutoScroll)el.scrollTop=el.scrollHeight;
|
||
else if(savedScroll!==null)el.scrollTop=savedScroll;
|
||
}
|
||
function onLogScroll(){
|
||
var el=document.getElementById('console-log');
|
||
if(!el)return;
|
||
var atBottom=el.scrollHeight-el.scrollTop-el.clientHeight<30;
|
||
if(!atBottom&&logAutoScroll){setAutoScroll(false);}
|
||
}
|
||
function toggleAutoScroll(){
|
||
setAutoScroll(!logAutoScroll);
|
||
if(logAutoScroll){var el=document.getElementById('console-log');if(el)el.scrollTop=el.scrollHeight;}
|
||
}
|
||
function setAutoScroll(on){
|
||
logAutoScroll=on;
|
||
var btn=document.getElementById('btn-autoscroll');
|
||
if(btn){btn.style.background=on?'var(--accent)':'var(--raised)';btn.style.color=on?'#fff':'var(--txt2)';}
|
||
}
|
||
function clearLogBadge(){
|
||
logBadgeCount=0;
|
||
['log-badge','log-badge-bot'].forEach(function(id){var b=document.getElementById(id);if(b)b.style.display='none';});
|
||
}
|
||
function escHtml(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||
// SSE server-log stream
|
||
(function(){
|
||
function connect(){
|
||
var es=new EventSource('/api/log/stream');
|
||
es.onmessage=function(e){try{_appendLog(JSON.parse(e.data));}catch(_){}};
|
||
es.onerror=function(){es.close();setTimeout(connect,3000);};
|
||
}
|
||
window.addEventListener('DOMContentLoaded',connect);
|
||
})();
|
||
|
||
// ── Helpers ──
|
||
function fmtTime(s){if(!s||s<0)return'–';var m=Math.floor(s/60),h=Math.floor(m/60);m%=60;return h>0?h+'h '+m+'m':m+'m'}
|
||
function post(url,body){return fetch(_apiUrl(url),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})}
|
||
function clamp(v,lo,hi){return Math.min(hi,Math.max(lo,v))}
|
||
|
||
// ── Apply state to DOM ──
|
||
function applyState(){
|
||
var s=S;
|
||
// connection error banner – nur wenn überhaupt ein Drucker konfiguriert ist
|
||
var banner=document.getElementById('conn-error-banner');
|
||
if(banner){if(s.connection_error&&_printers.length>0){banner.textContent='⚠ '+(T.lbl_conn_error||'Connection error:')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}}
|
||
var frb=document.getElementById('file-ready-banner');
|
||
if(frb){
|
||
if(s.file_ready&&s.print_state==='standby'){
|
||
document.getElementById('file-ready-name').textContent=s.file_ready;
|
||
frb.style.display='flex';
|
||
}else{frb.style.display='none';}
|
||
}
|
||
// header
|
||
var b=document.getElementById('h-badge');
|
||
b.className='hbadge '+s.print_state;
|
||
document.getElementById('h-state').textContent=T['kobra_'+s.kobra_state]||s.kobra_state||T.header_status_standby;
|
||
var _pn=_printers.length===0?'–':((_activePrinter&&_activePrinter.name)||s.printer_name);
|
||
var _el=document.getElementById('h-pname');if(_el)_el.textContent=_pn;
|
||
var _el2=document.getElementById('h-pname-single');if(_el2)_el2.textContent=_pn;
|
||
var hv=document.getElementById('h-version');if(hv&&s.version)hv.textContent='v'+s.version;
|
||
|
||
|
||
// temps
|
||
var nt=document.getElementById('d-nt');if(nt)nt.textContent=s.nozzle_temp.toFixed(1);
|
||
var ntt=document.getElementById('d-nt-t');if(ntt)ntt.textContent=s.nozzle_target.toFixed(0);
|
||
var bt=document.getElementById('d-bt');if(bt)bt.textContent=s.bed_temp.toFixed(1);
|
||
var btt=document.getElementById('d-bt-t');if(btt)btt.textContent=s.bed_target.toFixed(0);
|
||
|
||
// temp bars (dashboard)
|
||
var nb=document.getElementById('d-ntbar');if(nb)nb.style.width=clamp(s.nozzle_temp/300*100,0,100)+'%';
|
||
var bb=document.getElementById('d-btbar');if(bb)bb.style.width=clamp(s.bed_temp/120*100,0,100)+'%';
|
||
|
||
// progress
|
||
var pct=Math.round(s.progress*100);
|
||
var dpct=document.getElementById('d-pct');if(dpct)dpct.textContent=pct;
|
||
var dpbar=document.getElementById('d-pbar');if(dpbar)dpbar.style.width=pct+'%';
|
||
|
||
var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'–';
|
||
var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers;
|
||
|
||
var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration);
|
||
var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'–';
|
||
var dslrow=document.getElementById('d-slicer-row');
|
||
var dsltime=document.getElementById('d-slicer-time');
|
||
if(dslrow&&dsltime){
|
||
if(s.slicer_time>0){dslrow.style.display='';dsltime.textContent=fmtTime(s.slicer_time);}
|
||
else{dslrow.style.display='none';}
|
||
}
|
||
|
||
var fn=s.filename||'–';
|
||
var dfname=document.getElementById('d-fname');if(dfname){dfname.textContent=fn;dfname.title=fn};
|
||
var pfname=document.getElementById('p-fname');if(pfname){pfname.textContent=fn;pfname.title=fn};
|
||
var cfo=document.getElementById('cam-fname');if(cfo)cfo.textContent=fn!=='–'?fn:'';
|
||
|
||
// thumbnail
|
||
var thumb=document.getElementById('d-thumbnail');
|
||
if(thumb){
|
||
if(s.thumbnail){
|
||
thumb.src='data:image/png;base64,'+s.thumbnail;
|
||
thumb.style.display='block';
|
||
} else {
|
||
thumb.style.display='none';
|
||
thumb.src='';
|
||
}
|
||
}
|
||
|
||
// light/fan sync
|
||
document.getElementById('d-light-toggle').checked=s.light_on;
|
||
var dfan=document.getElementById('d-fan');if(dfan)dfan.value=s.fan_speed;
|
||
var dfanval=document.getElementById('d-fan-val');if(dfanval)dfanval.textContent=s.fan_speed;
|
||
|
||
// speed mode buttons
|
||
var spdWidths={1:25,2:55,3:90};
|
||
[1,2,3].forEach(function(m){
|
||
var b=document.getElementById('d-spd-'+m);
|
||
if(b) b.classList.toggle('spd-active', s.print_speed_mode===m);
|
||
});
|
||
var spdBar=document.getElementById('d-spd-bar');
|
||
if(spdBar) spdBar.style.width=(spdWidths[s.print_speed_mode]||55)+'%';
|
||
|
||
// AMS
|
||
if(s.ams_slots&&s.ams_slots.length){
|
||
window._amsSlots=s.ams_slots;
|
||
var html='';
|
||
s.ams_slots.forEach(function(slot,i){
|
||
var empty=slot.status!==5;
|
||
var rgb=empty?[80,80,80]:(Array.isArray(slot.color)?slot.color:[128,128,128]);
|
||
var col='rgb('+rgb[0]+','+rgb[1]+','+rgb[2]+')';
|
||
var active=slot.status===1||slot.active;
|
||
var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'–');
|
||
var idx=slot.index!=null?slot.index:i;
|
||
html+='<div class="ams-slot'+(active?' active':'')+(empty?' empty':'')+ '" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">'
|
||
+'<div class="slot-circle" style="background:'+col+'"></div>'
|
||
+'<div class="slot-material">'+(empty?'–':(slot.type||slot.material_type||'–'))+'</div>'
|
||
+'<div class="slot-label">Slot '+(idx+1)+'</div>'
|
||
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
|
||
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
|
||
+'</div>';
|
||
});
|
||
document.getElementById('ams-slots').innerHTML=html;
|
||
}
|
||
|
||
// camera overlay
|
||
var co=document.getElementById('cam-overlay');
|
||
if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none';
|
||
|
||
// auto-start camera during print
|
||
if(s.print_state==='printing'&&!camOn&&s.camera_url){
|
||
camStart();
|
||
}
|
||
|
||
updateConnBtn();
|
||
}
|
||
|
||
function updateConnBtn(){
|
||
var btn=document.getElementById('conn-btn');
|
||
if(!btn)return;
|
||
var offline=S.kobra_state==='offline';
|
||
if(offline){
|
||
btn.className='conn-btn disconnected';
|
||
btn.textContent=T.btn_connect||'⚡ Verbinden';
|
||
} else {
|
||
btn.className='conn-btn connected';
|
||
btn.textContent=T.btn_disconnect||'✕ Trennen';
|
||
}
|
||
}
|
||
|
||
function toggleConnection(){
|
||
var btn=document.getElementById('conn-btn');
|
||
var offline=S.kobra_state==='offline';
|
||
btn.disabled=true;
|
||
btn.textContent='…';
|
||
var url=offline?'/api/connect':'/api/disconnect';
|
||
post(url,{}).then(function(r){return r.json()}).then(function(r){
|
||
btn.disabled=false;
|
||
if(r.error)addLog('Error: '+r.error);
|
||
}).catch(function(){btn.disabled=false;});
|
||
}
|
||
|
||
// ── Temp history + chart ──
|
||
function updateHistory(){
|
||
tempHistory.n.push(S.nozzle_temp);
|
||
tempHistory.b.push(S.bed_temp);
|
||
if(tempHistory.n.length>60)tempHistory.n.shift();
|
||
if(tempHistory.b.length>60)tempHistory.b.shift();
|
||
drawChart('d-chart',tempHistory,[{data:tempHistory.n,color:'#00c8ff',max:300},{data:tempHistory.b,color:'#ff6b35',max:120}]);
|
||
}
|
||
function drawChart(id,_,series){
|
||
var canvas=document.getElementById(id);if(!canvas)return;
|
||
var ctx=canvas.getContext('2d');
|
||
var W=canvas.offsetWidth*window.devicePixelRatio||canvas.width;
|
||
var H=canvas.offsetHeight*window.devicePixelRatio||canvas.height;
|
||
canvas.width=W;canvas.height=H;
|
||
ctx.clearRect(0,0,W,H);
|
||
series.forEach(function(s){
|
||
var data=s.data;if(!data.length)return;
|
||
var max=s.max;
|
||
ctx.beginPath();ctx.strokeStyle=s.color;ctx.lineWidth=2;ctx.lineJoin='round';
|
||
data.forEach(function(v,i){
|
||
var x=i/(Math.max(data.length-1,1))*(W-4)+2;
|
||
var y=H-4-(v/max)*(H-8);
|
||
if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
|
||
});
|
||
ctx.stroke();
|
||
});
|
||
}
|
||
|
||
// ── Settings Modal ──
|
||
var _updateTag='';
|
||
var _updateUrl='';
|
||
function openSettings(){
|
||
// Titel mit aktivem Drucker-Namen aktualisieren
|
||
var pname=_activePrinter&&_activePrinter.name?_activePrinter.name:null;
|
||
var title=document.getElementById('modal-title-settings');
|
||
if(title)title.textContent=T.settings_title+(pname?' – '+pname:'');
|
||
fetch(_apiUrl('/api/settings')).then(function(r){return r.json()}).then(function(d){
|
||
document.getElementById('s-printer-name').value=d.printer_name||'';
|
||
document.getElementById('s-printer-ip').value=d.printer_ip||'';
|
||
document.getElementById('s-mqtt-port').value=d.mqtt_port||9883;
|
||
document.getElementById('s-username').value=d.username||'';
|
||
document.getElementById('s-password').value=d.password||'';
|
||
document.getElementById('s-device-id').value=d.device_id||'';
|
||
document.getElementById('s-mode-id').value=d.mode_id||'';
|
||
document.getElementById('s-default-slot').value=d.default_ams_slot||'auto';
|
||
document.getElementById('s-auto-leveling').checked=(d.auto_leveling===undefined?true:!!d.auto_leveling);
|
||
});
|
||
var v=localStorage.getItem('pollInterval')||'2000';
|
||
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
|
||
var pb=document.getElementById('poll-'+Math.round(parseInt(v)/1000));
|
||
if(pb)pb.classList.add('active');
|
||
document.getElementById('s-version-label').textContent='v'+('__VERSION__'||'?');
|
||
document.getElementById('update-status').textContent='';
|
||
document.getElementById('btn-update-apply').style.display='none';
|
||
var cl=document.getElementById('update-changelog');if(cl)cl.style.display='none';
|
||
_updateTag='';_updateUrl='';
|
||
document.getElementById('settings-modal').classList.add('open');
|
||
}
|
||
function closeSettings(){
|
||
document.getElementById('settings-modal').classList.remove('open');
|
||
}
|
||
|
||
// ── AMS Slot Edit ──
|
||
var _slotEditIndex=-1;
|
||
var _MAT_PRESETS=['PLA','PETG','ABS','ASA','TPU','PA','PC','HIPS'];
|
||
function openSlotEdit(i){
|
||
var slot=(window._amsSlots||[])[i]||{};
|
||
var index=slot.index!=null?slot.index:i;
|
||
_slotEditIndex=index;
|
||
document.getElementById('slot-edit-title').textContent=T.slot_edit_title+' '+(index+1);
|
||
var rgb=Array.isArray(slot.color)?slot.color:[128,128,128];
|
||
var hex='#'+rgb.map(function(v){return('0'+Math.min(255,v).toString(16)).slice(-2)}).join('');
|
||
var ci=document.getElementById('slot-edit-color');
|
||
ci.value=hex;
|
||
document.getElementById('slot-edit-preview').style.background=hex;
|
||
var mat=(slot.type||'PLA').toUpperCase();
|
||
document.getElementById('slot-edit-mat').value=mat;
|
||
var btns=document.getElementById('slot-mat-btns');
|
||
btns.innerHTML=_MAT_PRESETS.map(function(m){
|
||
return '<button class="mat-preset-btn" data-mat="'+m+'" onclick="selectMatPreset(\''+m+'\')" '
|
||
+'style="padding:4px 10px;border-radius:6px;border:1px solid var(--border);cursor:pointer;font-size:12px;'
|
||
+(m===mat?'background:var(--accent);color:#fff':'background:var(--raised);color:var(--txt2)')+'">'+m+'</button>';
|
||
}).join('');
|
||
document.getElementById('slot-edit-modal').classList.add('open');
|
||
}
|
||
function closeSlotEdit(){
|
||
document.getElementById('slot-edit-modal').classList.remove('open');
|
||
}
|
||
function startReadyFile(){
|
||
var btn=document.getElementById('file-ready-btn');
|
||
if(btn){btn.disabled=true;btn.textContent='…';}
|
||
post('/printer/print/start',{filename:S.file_ready})
|
||
.then(function(r){return r.json();})
|
||
.then(function(r){
|
||
document.getElementById('file-ready-banner').style.display='none';
|
||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||
})
|
||
.catch(function(e){
|
||
clog((T.log_error||'Error:')+' '+e,'msg-err');
|
||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||
});
|
||
}
|
||
function cancelReadyFile(){
|
||
post('/api/file_ready/clear',{})
|
||
.then(function(){document.getElementById('file-ready-banner').style.display='none';});
|
||
}
|
||
function selectMatPreset(m){
|
||
document.getElementById('slot-edit-mat').value=m;
|
||
highlightMatBtn(m);
|
||
}
|
||
function highlightMatBtn(val){
|
||
document.querySelectorAll('.mat-preset-btn').forEach(function(b){
|
||
var on=b.getAttribute('data-mat')===val.toUpperCase();
|
||
b.style.background=on?'var(--accent)':'var(--raised)';
|
||
b.style.color=on?'#fff':'var(--txt2)';
|
||
});
|
||
}
|
||
function hexToRgb(hex){
|
||
var r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16);
|
||
return[r,g,b];
|
||
}
|
||
function saveSlotEdit(){
|
||
var hex=document.getElementById('slot-edit-color').value;
|
||
var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA';
|
||
var color=hexToRgb(hex);
|
||
post('/api/ams/set_slot',{index:_slotEditIndex,type:mat,color:color})
|
||
.then(function(r){return r.json();})
|
||
.then(function(r){
|
||
closeSlotEdit();
|
||
clog((T.slot_edit_ok||'AMS Slot')+' '+(_slotEditIndex+1)+': '+mat+' '+hex,'msg-ok');
|
||
})
|
||
.catch(function(e){clog('Fehler: '+e,'msg-err');});
|
||
}
|
||
document.addEventListener('DOMContentLoaded',function(){
|
||
document.getElementById('s-printer-ip').addEventListener('input',function(){
|
||
var hint=document.getElementById('lbl-ip-hint');
|
||
if(this.value.includes(':')){hint.textContent=T.hint_ip_no_port;hint.style.display='block';}
|
||
else{hint.style.display='none';}
|
||
});
|
||
});
|
||
function setPoll(ms){
|
||
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
|
||
var id='poll-'+Math.round(ms/1000);
|
||
var pb=document.getElementById(id);if(pb)pb.classList.add('active');
|
||
localStorage.setItem('pollInterval',ms);
|
||
clearInterval(pollTimer);
|
||
pollTimer=setInterval(poll,ms);
|
||
}
|
||
function saveSettings(){
|
||
var btn=document.getElementById('btn-save-settings');
|
||
btn.disabled=true;btn.textContent='…';
|
||
post('/api/settings',{
|
||
printer_name: document.getElementById('s-printer-name').value,
|
||
printer_ip: document.getElementById('s-printer-ip').value,
|
||
mqtt_port: parseInt(document.getElementById('s-mqtt-port').value)||9883,
|
||
username: document.getElementById('s-username').value,
|
||
password: document.getElementById('s-password').value,
|
||
device_id: document.getElementById('s-device-id').value,
|
||
mode_id: document.getElementById('s-mode-id').value,
|
||
default_ams_slot: document.getElementById('s-default-slot').value,
|
||
auto_leveling: document.getElementById('s-auto-leveling').checked?1:0,
|
||
}).then(function(){
|
||
btn.textContent=T.update_restarting;
|
||
setTimeout(function(){
|
||
btn.disabled=false;
|
||
setText('btn-save-settings',T.settings_save);
|
||
closeSettings();
|
||
poll();
|
||
},4000);
|
||
}).catch(function(e){
|
||
btn.disabled=false;setText('btn-save-settings',T.settings_save);
|
||
clog('Settings-Fehler: '+e,'msg-err');
|
||
});
|
||
}
|
||
function checkUpdate(){
|
||
var sb=document.getElementById('update-status');
|
||
sb.textContent=T.update_checking;
|
||
document.getElementById('btn-update-apply').style.display='none';
|
||
_updateTag='';_updateUrl='';
|
||
fetch(_apiUrl('/api/update/check')).then(function(r){return r.json()}).then(function(d){
|
||
if(d.error){sb.textContent=T.update_error+': '+d.error;return;}
|
||
var cl=document.getElementById('update-changelog');
|
||
if(d.changelog&&d.changelog.trim()){cl.textContent=d.changelog;cl.style.display='block';}
|
||
else{cl.style.display='none';}
|
||
if(d.update_available){
|
||
sb.textContent='v'+d.latest+' '+T.update_available;
|
||
sb.style.color='var(--ok)';
|
||
_updateTag=d.tag;_updateUrl=d.download_url;
|
||
document.getElementById('btn-update-apply').style.display='inline-block';
|
||
} else {
|
||
sb.textContent=T.update_none;
|
||
sb.style.color='var(--txt2)';
|
||
}
|
||
}).catch(function(e){sb.textContent=T.update_error+': '+e;});
|
||
}
|
||
function applyUpdate(){
|
||
if(!_updateUrl)return;
|
||
var sb=document.getElementById('update-status');
|
||
var btn=document.getElementById('btn-update-apply');
|
||
btn.disabled=true;sb.textContent=T.update_applying;
|
||
post('/api/update/apply',{download_url:_updateUrl,tag:_updateTag}).then(function(){
|
||
sb.textContent=T.update_restarting;
|
||
closeSettings();
|
||
setTimeout(function(){poll();},5000);
|
||
}).catch(function(e){
|
||
btn.disabled=false;sb.textContent=T.update_error+': '+e;
|
||
});
|
||
}
|
||
|
||
// ── Poll ──
|
||
async function poll(){
|
||
try{
|
||
var r=await fetch(_apiUrl('/api/state'));
|
||
if(!r.ok)return;
|
||
var d=await r.json();
|
||
Object.assign(S,d);
|
||
applyState();
|
||
updateHistory();
|
||
}catch(e){clog(T.log_poll_error+' '+e,'msg-err')}
|
||
}
|
||
var pollTimer;
|
||
(function(){
|
||
var ms=parseInt(localStorage.getItem('pollInterval')||'2000');
|
||
initPrinters();
|
||
poll();pollTimer=setInterval(poll,ms);
|
||
})();
|
||
|
||
// ── Print actions ──
|
||
function printAction(a){
|
||
post('/printer/print/'+a,{}).then(function(){clog('Druck: '+a,'msg-ok');poll()})
|
||
.catch(function(e){clog('Fehler: '+e,'msg-err')});
|
||
}
|
||
function confirmCancel(){if(confirm('Druck wirklich abbrechen?'))printAction('cancel')}
|
||
|
||
// ── Axis motion ──
|
||
// axis codes: 0=X, 1=Y, 2=Z
|
||
// move_type 1=relative, distance positive/negative
|
||
function getStep(){return currentStep}
|
||
function setStep(btn,v){
|
||
currentStep=v;
|
||
document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.getElementById('step-display').textContent=v;
|
||
}
|
||
function move(axis,dir,dist){
|
||
// axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z
|
||
var axisMap={0:1,1:2,2:3};
|
||
post('/api/axis',{axis:axisMap[axis],move_type:1,distance:dir*dist})
|
||
.then(function(){clog('Achse '+(axis===0?'X':axis===1?'Y':'Z')+' '+(dir>0?'+':'')+dir*dist+'mm','msg-ok')})
|
||
.catch(function(e){clog('Achse-Fehler: '+e,'msg-err')});
|
||
}
|
||
function homeAll(){
|
||
post('/api/axis',{axis:5,move_type:2,distance:0})
|
||
.then(function(){clog('Home All','msg-ok')})
|
||
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
|
||
}
|
||
function homeXY(){
|
||
post('/api/axis',{axis:4,move_type:2,distance:0})
|
||
.then(function(){clog('Home XY','msg-ok')})
|
||
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
|
||
}
|
||
function homeZ(){
|
||
post('/api/axis',{axis:3,move_type:2,distance:0})
|
||
.then(function(){clog('Home Z','msg-ok')})
|
||
.catch(function(e){clog('Home-Fehler: '+e,'msg-err')});
|
||
}
|
||
function disableMotors(){
|
||
post('/api/axis',{action:'turnOff'})
|
||
.then(function(){clog('Motors Off','msg-ok')})
|
||
.catch(function(e){clog('Motors-Fehler: '+e,'msg-err')});
|
||
}
|
||
|
||
// ── Temperature ──
|
||
function setNozzle(){
|
||
var v=parseFloat(document.getElementById('p-nozzle-inp').value||0);
|
||
post('/api/temperature',{nozzle:v,bed:S.bed_target})
|
||
.then(function(){clog('Nozzle → '+v+'°C','msg-ok')})
|
||
.catch(function(e){clog('Temp-Fehler: '+e,'msg-err')});
|
||
}
|
||
function setBed(){
|
||
var v=parseFloat(document.getElementById('p-bed-inp').value||0);
|
||
post('/api/temperature',{nozzle:S.nozzle_target,bed:v})
|
||
.then(function(){clog(T.label_bed+' → '+v+'°C','msg-ok')})
|
||
.catch(function(e){clog('Temp-Fehler: '+e,'msg-err')});
|
||
}
|
||
|
||
// ── Light ──
|
||
function setLight(){
|
||
var on=document.getElementById('d-light-toggle').checked;
|
||
post('/api/light',{on:on,brightness:80})
|
||
.then(function(){clog('Licht '+(on?'an, '+br+'%':'aus'),'msg-ok')})
|
||
.catch(function(e){clog('Licht-Fehler: '+e,'msg-err')});
|
||
}
|
||
|
||
// ── Print Speed ──
|
||
function setSpeed(mode){
|
||
S.print_speed_mode=mode;
|
||
[1,2,3].forEach(function(m){
|
||
var b=document.getElementById('d-spd-'+m);
|
||
if(b) b.classList.toggle('spd-active',m===mode);
|
||
});
|
||
post('/api/speed',{mode:mode})
|
||
.catch(function(e){clog('Speed-Fehler: '+e,'msg-err')});
|
||
}
|
||
|
||
// ── Fan ──
|
||
function setFan(){
|
||
var v=parseInt(document.getElementById('d-fan').value);
|
||
document.getElementById('d-fan-val').textContent=v;
|
||
post('/api/fan',{speed:v})
|
||
.then(function(){clog('Lüfter → '+v+'%','msg-ok')})
|
||
.catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')});
|
||
}
|
||
function quickFan(v){
|
||
document.getElementById('d-fan').value=v;
|
||
document.getElementById('d-fan-val').textContent=v;
|
||
post('/api/fan',{speed:v})
|
||
.then(function(){clog('Lüfter → '+v+'%','msg-ok')})
|
||
.catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')});
|
||
}
|
||
|
||
// ── AMS ──
|
||
function amsFeed(type){
|
||
var slot=parseInt(document.getElementById('ams-slot-sel').value);
|
||
post('/api/ams/feed',{slot_index:slot,type:type})
|
||
.then(function(){clog((type===1?T.lbl_feed:T.lbl_unload)+' Slot '+(slot+1),'msg-ok')})
|
||
.catch(function(e){clog('AMS-Fehler: '+e,'msg-err')});
|
||
}
|
||
|
||
// ── Camera ──
|
||
function camStart(){
|
||
var img=document.getElementById('cam-img');
|
||
var ph=document.getElementById('cam-placeholder');
|
||
var sp=document.getElementById('cam-spinner');
|
||
ph.style.display='none';
|
||
img.style.display='none';
|
||
sp.style.display='block';
|
||
post('/api/camera/start',{}).then(function(){
|
||
img.onerror=function(){
|
||
sp.style.display='none';
|
||
img.style.display='none';
|
||
ph.style.display='flex';
|
||
camOn=false;
|
||
document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera';
|
||
clog((T.log_error||'Fehler:')+' Stream nicht verfügbar','msg-err');
|
||
};
|
||
img.src='/api/camera/stream?t='+Date.now();
|
||
camOn=true;
|
||
document.getElementById('cam-toggle-btn').textContent=T.btn_cam_stop||'◼ Kamera';
|
||
clog((T.log_cam_start||'Kamera gestartet'),'msg-ok');
|
||
// MJPEG liefert kein onload – Spinner nach kurzem Timeout ausblenden
|
||
setTimeout(function(){
|
||
sp.style.display='none';
|
||
img.style.display='block';
|
||
},1200);
|
||
}).catch(function(e){
|
||
sp.style.display='none';
|
||
ph.style.display='flex';
|
||
clog((T.log_error||'Fehler:')+' '+e,'msg-err');
|
||
});
|
||
}
|
||
function camStop(){
|
||
post('/api/camera/stop',{}).then(function(){
|
||
var img=document.getElementById('cam-img');
|
||
img.src='';
|
||
img.style.display='none';
|
||
document.getElementById('cam-spinner').style.display='none';
|
||
document.getElementById('cam-placeholder').style.display='flex';
|
||
camOn=false;
|
||
document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera';
|
||
clog(T.log_cam_stop||'Kamera gestoppt','msg-ok');
|
||
}).catch(function(e){clog((T.log_error||'Fehler:')+' '+e,'msg-err')});
|
||
}
|
||
function toggleCam(){if(camOn)camStop();else camStart()}
|
||
|
||
// ── GCode Store ──
|
||
var storeFiles=[];
|
||
|
||
function loadStore(){
|
||
fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){
|
||
storeFiles=d.result||[];
|
||
renderStore();
|
||
}).catch(function(e){clog('Store-Fehler: '+e,'msg-err')});
|
||
}
|
||
|
||
function renderStore(){
|
||
var grid=document.getElementById('store-grid');
|
||
var empty=document.getElementById('store-empty');
|
||
|
||
// Suche
|
||
var q=(document.getElementById('store-search')||{value:''}).value.toLowerCase().trim();
|
||
// Filter
|
||
var filter=(document.getElementById('store-filter')||{value:'all'}).value;
|
||
// Sortierung
|
||
var sort=(document.getElementById('store-sort')||{value:'date_desc'}).value;
|
||
|
||
var files=storeFiles.filter(function(f){
|
||
if(q&&f.filename.toLowerCase().indexOf(q)===-1) return false;
|
||
if(filter==='completed'&&f.last_print_status!=='completed') return false;
|
||
if(filter==='failed'&&(f.last_print_status!=='cancelled'&&f.last_print_status!=='failed')) return false;
|
||
if(filter==='never'&&f.last_print_status) return false;
|
||
return true;
|
||
});
|
||
|
||
files.sort(function(a,b){
|
||
if(sort==='name_asc') return a.filename.localeCompare(b.filename);
|
||
if(sort==='duration_asc'){
|
||
var da=a.last_print_duration||a.est_print_time_sec||0;
|
||
var db=b.last_print_duration||b.est_print_time_sec||0;
|
||
return da-db;
|
||
}
|
||
// date_desc (default)
|
||
return (b.uploaded_at||'').localeCompare(a.uploaded_at||'');
|
||
});
|
||
|
||
if(!storeFiles.length){
|
||
empty.textContent=T.store_empty;
|
||
grid.innerHTML='';
|
||
empty.style.display='block';
|
||
return;
|
||
}
|
||
if(!files.length){
|
||
empty.textContent=T.store_no_results;
|
||
grid.innerHTML='';
|
||
empty.style.display='block';
|
||
return;
|
||
}
|
||
empty.style.display='none';
|
||
grid.innerHTML=files.map(function(f){
|
||
var thumb=f.thumbnail_b64
|
||
? '<img src="data:image/png;base64,'+f.thumbnail_b64+'" style="width:100%;height:130px;object-fit:cover;border-radius:6px;display:block;margin-bottom:8px">'
|
||
: '<div style="width:100%;height:130px;background:var(--raised);border-radius:6px;display:flex;align-items:center;justify-content:center;margin-bottom:8px;font-size:32px">🖨</div>';
|
||
var name=f.filename.length>28?f.filename.slice(0,25)+'…':f.filename;
|
||
var date=f.uploaded_at?f.uploaded_at.replace('T',' ').slice(0,16):'';
|
||
var est=f.est_print_time_sec?formatDur(f.est_print_time_sec):'–';
|
||
var statusBadge='';
|
||
var lastInfo='';
|
||
if(f.last_print_status==='completed'){
|
||
statusBadge='<span style="background:#2a7a3b;color:#fff;border-radius:4px;padding:1px 6px;font-size:10px;margin-left:4px">✓</span>';
|
||
if(f.last_print_duration) lastInfo='<div style="font-size:11px;color:var(--ok);margin-bottom:2px">✓ '+formatDur(f.last_print_duration)+'</div>';
|
||
} else if(f.last_print_status==='cancelled'||f.last_print_status==='failed'){
|
||
statusBadge='<span style="background:#a04020;color:#fff;border-radius:4px;padding:1px 6px;font-size:10px;margin-left:4px">✗</span>';
|
||
lastInfo='<div style="font-size:11px;color:var(--err);margin-bottom:2px">✗ '+f.last_print_status+'</div>';
|
||
} else if(!f.last_print_status){
|
||
lastInfo='<div style="font-size:11px;color:var(--txt2);margin-bottom:2px;opacity:.6">'+T.store_never+'</div>';
|
||
}
|
||
return '<div style="background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:10px;display:flex;flex-direction:column">'+
|
||
thumb+
|
||
'<div title="'+f.filename+'" style="font-size:12px;font-weight:600;margin-bottom:4px;color:var(--txt)">'+name+statusBadge+'</div>'+
|
||
lastInfo+
|
||
'<div style="font-size:11px;color:var(--txt2);margin-bottom:2px">⏱ Schätzung: '+est+'</div>'+
|
||
'<div style="font-size:11px;color:var(--txt2);margin-bottom:8px">📅 '+date+'</div>'+
|
||
'<div style="display:flex;gap:6px;margin-top:auto">'+
|
||
'<button onclick="storePrint(\''+f.id+'\',\''+f.filename.replace(/'/g,"\\'")+'\')" '+
|
||
'style="flex:1;font-size:12px;padding:5px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer">'+T.store_print+'</button>'+
|
||
'<button onclick="storeDelete(\''+f.id+'\')" '+
|
||
'style="font-size:12px;padding:5px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer">🗑</button>'+
|
||
'</div>'+
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
|
||
function formatDur(sec){
|
||
var h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60);
|
||
return h?h+'h '+m+'m':m+'m';
|
||
}
|
||
|
||
var _storeFileId=null;
|
||
var _storeFilename=null;
|
||
var _filamentDialogMode='store'; // 'store' oder 'banner'
|
||
|
||
var _gcodeFilaments=[];
|
||
|
||
function storePrint(fileId, filename){
|
||
_storeFileId=fileId;
|
||
_storeFilename=filename;
|
||
_filamentDialogMode='store';
|
||
// GCode-Filamente aus Store-Datei holen (für Vorschau im Dialog)
|
||
var fileObj=storeFiles.find(function(f){return f.id===fileId;});
|
||
try{ _gcodeFilaments=fileObj&&fileObj.gcode_filaments?JSON.parse(fileObj.gcode_filaments):[]; }
|
||
catch(e){ _gcodeFilaments=[]; }
|
||
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){
|
||
openFilamentDialog(d.result||[]);
|
||
}).catch(function(){openFilamentDialog([]);});
|
||
}
|
||
|
||
function startReadyFileWithSlots(){
|
||
_filamentDialogMode='banner';
|
||
_storeFilename=S.file_ready||'';
|
||
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){
|
||
openFilamentDialog(d.result||[]);
|
||
}).catch(function(){openFilamentDialog([]);});
|
||
}
|
||
|
||
var _amsSlots=[];
|
||
|
||
function openFilamentDialog(slots){
|
||
_amsSlots=slots.filter(function(s){return s.status==='loaded';});
|
||
var dlg=document.getElementById('filament-dialog');
|
||
var title=document.getElementById('fd-title');
|
||
var body=document.getElementById('fd-slots');
|
||
if(title)title.textContent='▶ '+_storeFilename;
|
||
|
||
// GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten
|
||
var channels=_gcodeFilaments.length?_gcodeFilaments:_amsSlots.map(function(s,i){
|
||
return {slot_index:i,color_hex:s.color_hex,material:s.material};
|
||
});
|
||
|
||
if(!_amsSlots.length){
|
||
body.innerHTML='<p style="color:var(--txt2);font-size:13px;text-align:center;padding:16px 0">Keine belegten AMS-Slots.<br>Druck trotzdem starten?</p>';
|
||
} else {
|
||
body.innerHTML=channels.map(function(gc,i){
|
||
// Passende Slots: gleicher Materialtyp
|
||
var compatible=_amsSlots.filter(function(s){return s.material.toUpperCase()===gc.material.toUpperCase();});
|
||
if(!compatible.length) compatible=_amsSlots; // Fallback: alle
|
||
// Standard-Auswahl: Slot mit gleichem Index oder erster kompatibler
|
||
var defaultSlot=compatible.find(function(s){return s.slot_index===gc.slot_index;})||compatible[0];
|
||
var opts=compatible.map(function(s){
|
||
var sel=(s.slot_index===defaultSlot.slot_index)?'selected':'';
|
||
return '<option value="'+s.slot_index+'" data-color="'+s.color_hex+'" data-material="'+s.material+'" '+sel+'>'+
|
||
'Slot '+(s.slot_index+1)+' · '+s.material+'</option>';
|
||
}).join('');
|
||
return '<div style="display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;background:var(--raised);border:1px solid var(--border)">'+
|
||
'<span style="display:inline-block;width:16px;height:16px;border-radius:50%;background:'+gc.color_hex+';border:1px solid var(--border);flex-shrink:0"></span>'+
|
||
'<span style="font-size:13px;font-weight:600;min-width:70px">Kanal '+(i+1)+'</span>'+
|
||
'<span style="font-size:11px;color:var(--txt2);min-width:32px">'+gc.material+'</span>'+
|
||
'<span style="font-size:18px;color:var(--txt2)">→</span>'+
|
||
'<select data-paint="'+i+'" data-paint-color="'+gc.color_hex+'" style="flex:1;padding:4px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:13px">'+
|
||
opts+'</select>'+
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
if(dlg)dlg.classList.add('open');
|
||
}
|
||
|
||
function closeFilamentDialog(){
|
||
var dlg=document.getElementById('filament-dialog');
|
||
if(dlg)dlg.classList.remove('open');
|
||
}
|
||
|
||
function confirmFilamentPrint(){
|
||
var selects=document.querySelectorAll('#fd-slots select');
|
||
var assignments=[];
|
||
selects.forEach(function(sel){
|
||
var paintIdx=parseInt(sel.dataset.paint);
|
||
var paintColor=sel.dataset.paintColor;
|
||
var opt=sel.options[sel.selectedIndex];
|
||
var amsIdx=parseInt(opt.value);
|
||
var amsSlot=_amsSlots.find(function(s){return s.slot_index===amsIdx;})||{};
|
||
// Farbe als [R,G,B,255]
|
||
function hexToRgba(h){
|
||
var c=h.replace('#','');
|
||
if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2];
|
||
return [parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16),255];
|
||
}
|
||
assignments.push({
|
||
paint_index: paintIdx,
|
||
slot_index: amsIdx,
|
||
material: opt.dataset.material||'PLA',
|
||
paint_color: hexToRgba(paintColor||'#ffffff'),
|
||
ams_color: hexToRgba(amsSlot.color_hex||'#ffffff'),
|
||
});
|
||
});
|
||
closeFilamentDialog();
|
||
if(_filamentDialogMode==='banner'){
|
||
// Banner-Modus: normaler print/start mit Slot-Override
|
||
var btn=document.getElementById('file-ready-btn');
|
||
if(btn){btn.disabled=true;btn.textContent='…';}
|
||
post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments})
|
||
.then(function(r){return r.json();})
|
||
.then(function(){
|
||
document.getElementById('file-ready-banner').style.display='none';
|
||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||
})
|
||
.catch(function(e){
|
||
clog((T.log_error||'Error:')+' '+e,'msg-err');
|
||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||
});
|
||
} else {
|
||
// Store-Modus: POST /kx/print
|
||
fetch(_apiUrl('/kx/print'),{
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments})
|
||
}).then(function(r){return r.json()}).then(function(d){
|
||
if(d.result==='ok'){clog('Druckstart: '+_storeFilename,'msg-ok');showPanel('dashboard');}
|
||
else{clog('Druckfehler: '+(d.error||'?'),'msg-err');}
|
||
}).catch(function(e){clog('Druckfehler: '+e,'msg-err');});
|
||
}
|
||
}
|
||
|
||
function storeDelete(fileId){
|
||
if(!confirm(T.store_delete_confirm)) return;
|
||
fetch(_apiUrl('/kx/files/'+fileId),{method:'DELETE'}).then(function(r){
|
||
if(r.ok){loadStore();}
|
||
else{clog('Löschen fehlgeschlagen','msg-err');}
|
||
});
|
||
}
|
||
|
||
// ── Drucker hinzufügen ──
|
||
function openAddPrinterDialog(){
|
||
document.getElementById('apd-ip').value='';
|
||
document.getElementById('apd-name').value='';
|
||
var st=document.getElementById('apd-status');st.textContent='';st.style.color='var(--txt2)';
|
||
document.getElementById('apd-confirm').disabled=false;
|
||
document.getElementById('add-printer-dialog').classList.add('open');
|
||
}
|
||
function closeAddPrinterDialog(){
|
||
document.getElementById('add-printer-dialog').classList.remove('open');
|
||
}
|
||
function confirmAddPrinter(){
|
||
var ip=document.getElementById('apd-ip').value.trim();
|
||
var name=document.getElementById('apd-name').value.trim();
|
||
var st=document.getElementById('apd-status'),btn=document.getElementById('apd-confirm');
|
||
if(!ip){st.textContent=T.apd_err_ip;st.style.color='var(--err)';return;}
|
||
st.textContent=T.apd_fetching;st.style.color='var(--txt2)';btn.disabled=true;
|
||
fetch('/kx/printers/add',{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({printer_ip:ip,name:name})})
|
||
.then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});})
|
||
.then(function(res){
|
||
if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;}
|
||
st.textContent=T.apd_success;st.style.color='var(--ok)';
|
||
setTimeout(function(){location.reload();},2500);
|
||
})
|
||
.catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;});
|
||
}
|
||
function removePrinter(id,name){
|
||
if(!confirm(T.printers_remove_confirm.replace('{name}',name)))return;
|
||
fetch('/kx/printers/'+encodeURIComponent(id),{method:'DELETE'})
|
||
.then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});})
|
||
.then(function(res){
|
||
if(!res.ok){alert((res.j&&res.j.error)||'Fehler');return;}
|
||
setTimeout(function(){location.href='/printer1';},2000);
|
||
})
|
||
.catch(function(e){alert(''+e);});
|
||
}
|
||
|
||
// ── Drucker-Tab ──
|
||
function loadPrinterTab(){
|
||
var grid=document.getElementById('printers-grid');
|
||
if(grid)grid.innerHTML='<div style="color:var(--txt2);font-size:13px;padding:20px">'+T.printers_loading+'</div>';
|
||
// Drucker-Liste von lokaler Instanz holen
|
||
fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){
|
||
var printers=d.result||[];
|
||
if(!printers.length){
|
||
if(grid)grid.innerHTML='<div style="grid-column:1/-1;text-align:center;padding:40px 20px;color:var(--txt2)">'+
|
||
'<div style="font-size:32px;margin-bottom:8px">🖨</div>'+
|
||
'<div style="font-size:14px;margin-bottom:14px">'+T.printers_empty_hint+'</div>'+
|
||
'<button onclick="openAddPrinterDialog()" style="font-size:13px;padding:8px 18px;background:var(--accent);border:none;border-radius:8px;color:#fff;cursor:pointer;font-weight:600">+ '+T.add_printer+'</button>'+
|
||
'</div>';
|
||
return;
|
||
}
|
||
// Status jedes Druckers parallel abrufen
|
||
var fetches=printers.map(function(p){
|
||
var url=(p.bridge_url||'').replace(/\/+$/,'');
|
||
return fetch(url+'/api/state',{signal:AbortSignal.timeout(3000)})
|
||
.then(function(r){return r.json()})
|
||
.then(function(s){return {printer:p,state:s,online:true};})
|
||
.catch(function(){return {printer:p,state:{},online:false};});
|
||
});
|
||
Promise.all(fetches).then(function(results){
|
||
var activeId=_activePrinter?String(_activePrinter.id):null;
|
||
if(grid)grid.innerHTML=results.map(function(res){
|
||
var p=res.printer,s=res.state,online=res.online;
|
||
var isActive=String(p.id)===activeId;
|
||
var url=(p.bridge_url||'').replace(/\/+$/,'');
|
||
var printerNum=p.id;
|
||
var ks=online?(s.kobra_state||'free'):'offline';
|
||
var stateKey='kobra_'+ks;
|
||
var stateLabel=T[stateKey]||ks;
|
||
var stateColor=ks==='free'?'var(--ok)':ks==='printing'?'var(--accent)':ks==='offline'?'var(--txt2)':'var(--warn)';
|
||
var progress=online&&s.progress?Math.round(s.progress*100):null;
|
||
var filename=online&&s.filename?s.filename:'';
|
||
var nt=online&&s.nozzle_temp?s.nozzle_temp.toFixed(1):'–';
|
||
var bt=online&&s.bed_temp?s.bed_temp.toFixed(1):'–';
|
||
var border=isActive?'2px solid var(--accent)':'1px solid var(--border)';
|
||
var nameEsc=String(p.name).replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||
return '<div style="background:var(--raised);border:'+border+';border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:8px">'+
|
||
'<div style="display:flex;align-items:center;justify-content:space-between;gap:8px">'+
|
||
'<span style="font-weight:700;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">🖨 '+p.name+'</span>'+
|
||
'<span style="display:flex;align-items:center;gap:8px;flex-shrink:0">'+
|
||
(isActive?'<span style="font-size:11px;color:var(--accent);font-weight:600">'+T.printers_active+'</span>':'')+
|
||
'<button onclick="removePrinter(\''+printerNum+'\',\''+nameEsc+'\')" title="'+T.printers_remove+'" style="background:none;border:none;color:var(--txt2);font-size:16px;cursor:pointer;line-height:1;padding:0">✕</button>'+
|
||
'</span>'+
|
||
'</div>'+
|
||
'<div style="display:flex;align-items:center;gap:6px">'+
|
||
'<span style="width:8px;height:8px;border-radius:50%;background:'+stateColor+';display:inline-block"></span>'+
|
||
'<span style="font-size:13px;color:var(--txt2)">'+stateLabel+'</span>'+
|
||
'</div>'+
|
||
(p.printer_ip?'<div style="font-size:12px;color:var(--txt2)">🌐 '+p.printer_ip+'</div>':'')+
|
||
(filename?'<div style="font-size:12px;color:var(--txt2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="'+filename+'">📄 '+filename+'</div>':'')+
|
||
(progress!==null?'<div style="background:var(--border);border-radius:4px;height:6px;overflow:hidden"><div style="background:var(--accent);height:100%;width:'+progress+'%"></div></div>':'')+
|
||
'<div style="font-size:12px;color:var(--txt2);display:flex;gap:12px">'+
|
||
'<span>🌡 '+nt+'°C</span><span>🛏 '+bt+'°C</span>'+
|
||
'</div>'+
|
||
(!isActive?'<a href="/printer'+printerNum+'" style="display:block;text-align:center;padding:7px;background:var(--accent);color:#fff;border-radius:7px;font-size:13px;font-weight:600;text-decoration:none;margin-top:4px">'+T.printers_switch+'</a>':'<div style="text-align:center;padding:7px;font-size:12px;color:var(--txt2)">'+T.printers_current+'</div>')+
|
||
'</div>';
|
||
}).join('');
|
||
});
|
||
}).catch(function(e){
|
||
if(grid)grid.innerHTML='<div style="color:var(--err);font-size:13px;padding:20px">Fehler: '+e+'</div>';
|
||
});
|
||
}
|