From 5b4ba567ab8de9f0ad9535ff257104e98475a899 Mon Sep 17 00:00:00 2001 From: Hirnwunde Date: Thu, 21 May 2026 01:50:56 +0200 Subject: [PATCH] bump theming to 0.9.13 --- .gitignore | 2 + kobrax_moonraker_bridge.py | 85 +- web/DOC/THEME-CSS-HOOKS.md | 136 +++ web/DOC/THEME-JS-ID-HOOKS.md | 86 ++ web/themes/default/app.js | 2132 +++++++++++++++++++++++++++++++++ web/themes/default/index.html | 560 +++++++++ web/themes/default/style.css | 293 +++++ 7 files changed, 3291 insertions(+), 3 deletions(-) create mode 100644 web/DOC/THEME-CSS-HOOKS.md create mode 100644 web/DOC/THEME-JS-ID-HOOKS.md create mode 100644 web/themes/default/app.js create mode 100644 web/themes/default/index.html create mode 100644 web/themes/default/style.css diff --git a/.gitignore b/.gitignore index d86da6a..74c2e85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .env +config/config.ini +.dev __pycache__/ *.pyc build/ diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 42d6c5e..1a4fcb0 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -44,6 +44,7 @@ import sys import tempfile import time import threading +import html # Bei PyInstaller-Binary liegt alles neben sys.executable, sonst neben __file__ _BASE = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) else os.path.dirname(os.path.abspath(__file__)) @@ -130,6 +131,14 @@ logging.basicConfig(level=logging.INFO, datefmt="%H:%M:%S") log = logging.getLogger("bridge") +# Web-UI: Unterverzeichnis unter web/themes//index.html +_UI_THEME_NAME_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$") +# Erlaubte statische Theme-Dateien unter /kx/ui/ +_KX_UI_ASSETS: dict[str, str] = { + "style.css": "text/css", + "app.js": "application/javascript", +} + # Ring-Buffer für Browser-Log-Stream (letzte 200 Einträge) import collections as _collections _log_buffer: "_collections.deque[dict]" = _collections.deque(maxlen=500) @@ -550,6 +559,15 @@ class KobraXBridge: # Part-Skip: zuletzt vom Drucker gemeldete Skip-Liste (v0.9.10) self._skip_state: dict = {"objects": [], "skipped": [], "ts": 0} + # Theme-Name prüfen (keine Sonderzeichen oder Umlaute) + raw_theme = (getattr(args, "ui_theme", None) or "default").strip() + if not _UI_THEME_NAME_RE.match(raw_theme): + log.warning("Ungültiger UI-Theme-Name %r – nutze default", raw_theme) + raw_theme = "default" + self._ui_theme = raw_theme + self._index_tpl_cache: str | None = None + self._index_tpl_cache_key: tuple[str, float] | None = None + # Register MQTT push callbacks client.callbacks["tempature/report"] = self._on_temp client.callbacks["print/report"] = self._on_print @@ -1867,6 +1885,30 @@ class KobraXBridge: else: log.warning("Druckstart: keine Antwort vom Drucker") + def _theme_index_path(self) -> str: + return os.path.join(_BASE, "web", "themes", self._ui_theme, "index.html") + + def _load_index_template_cached(self) -> str: + path = self._theme_index_path() + mtime = os.path.getmtime(path) + key = (path, mtime) + if self._index_tpl_cache is not None and self._index_tpl_cache_key == key: + return self._index_tpl_cache + with open(path, "r", encoding="utf-8") as f: + self._index_tpl_cache = f.read() + self._index_tpl_cache_key = key + return self._index_tpl_cache + + def _ui_asset_cache_buster(self) -> str: + base = os.path.join(_BASE, "web", "themes", self._ui_theme) + mt = 0.0 + for fn in ("index.html", "style.css", "app.js"): + try: + mt = max(mt, os.path.getmtime(os.path.join(base, fn))) + except OSError: + pass + return str(int(mt)) if mt else "0" + async def handle_print_start(self, request): try: body = await request.json() @@ -1974,10 +2016,39 @@ class KobraXBridge: "text": "OctoPrint (Kobra X Bridge)", }) + async def handle_kx_ui_asset(self, request): + name = request.match_info.get("name", "") + ctype = _KX_UI_ASSETS.get(name) + if ctype is None: + raise web.HTTPNotFound() + path = os.path.join(_BASE, "web", "themes", self._ui_theme, name) + try: + raw = pathlib.Path(path).read_text(encoding="utf-8") + except OSError: + raise web.HTTPNotFound() + if name == "app.js": + raw = raw.replace("'__VERSION__'", f"'{self._read_version()}'") + return web.Response( + text=raw, + content_type=ctype, + headers={"Cache-Control": "public, max-age=86400"}, + ) + async def handle_index(self, request): - html = INDEX_HTML - version = self._read_version() - html = html.replace("'__VERSION__'", f"'{version}'") + try: + tpl = self._load_index_template_cached() + except OSError: + p = self._theme_index_path() + log.error("Web-UI Theme-Datei fehlt oder nicht lesbar: %s (Theme: %s)", p, self._ui_theme) + return web.Response( + text="
KX-Bridge: index.html nicht gefunden.\nErwartet:\n"
+                + html.escape(p, quote=True)
+                + "
", + status=500, + content_type="text/html; charset=utf-8", + ) + html = tpl.replace("__UI_ASSETS_VER__", self._ui_asset_cache_buster()) + return web.Response(text=html, content_type="text/html", headers={"Cache-Control": "no-store, no-cache, must-revalidate"}) @@ -3243,6 +3314,7 @@ def build_app(bridge: KobraXBridge) -> web.Application: r.add_delete("/kx/files/{file_id}", bridge.handle_kx_file_delete) r.add_get("/kx/filament/slots", bridge.handle_kx_filament_slots) r.add_get("/kx/history", bridge.handle_kx_history) + r.add_get("/kx/ui/{name}", bridge.handle_kx_ui_asset) r.add_get("/kx/files/{id}/objects", bridge.handle_kx_file_objects) r.add_post("/kx/skip", bridge.handle_kx_skip) r.add_post("/kx/skip/query", bridge.handle_kx_skip_query) @@ -3412,6 +3484,13 @@ def main(): help="HTTP/WS-Port (Moonraker-Standard: 7125)") parser.add_argument("--data-dir", default=_default_data_dir(), help="Persistenz-Verzeichnis für GCode-Store und DB") + parser.add_argument( + "--ui-theme", + default=os.environ.get("KX_UI_THEME", "default"), + metavar="NAME", + help="Web-UI-Theme (Ordner web/themes/NAME/, Standard: default). " + "Alternativ: Umgebungsvariable KX_UI_THEME.", + ) args = parser.parse_args() if args.printer_ip and ":" in args.printer_ip: args.printer_ip = args.printer_ip.split(":")[0] diff --git a/web/DOC/THEME-CSS-HOOKS.md b/web/DOC/THEME-CSS-HOOKS.md new file mode 100644 index 0000000..f3e7e48 --- /dev/null +++ b/web/DOC/THEME-CSS-HOOKS.md @@ -0,0 +1,136 @@ +# KX-Bridge Theme – CSS-ID-Hooks + +Referenzliste für CSS-/Layout-Anpassungen. + +| ID | Verwendung | +|---|---| +| `#ace-dry-dialog-custom-name-label` | Hook / Selektor | +| `#ace-dry-dialog-custom-name-row` | Hook / Selektor | +| `#ace-dry-dialog-temp-label` | Hook / Selektor | +| `#ace-dry-dialog-time-label` | Hook / Selektor | +| `#ace-dry-dialog-title` | Hook / Selektor | +| `#add-printer-btn-label` | Hook / Selektor | +| `#ams-no-data` | Hook / Selektor | +| `#apd-ip` | Hook / Selektor | +| `#apd-lbl-ip` | Hook / Selektor | +| `#apd-lbl-name` | Hook / Selektor | +| `#apd-name` | Hook / Selektor | +| `#apd-status` | Hook / Selektor | +| `#apd-title` | Hook / Selektor | +| `#btn-log-dl` | Hook / Selektor | +| `#cam-fname` | Hook / Selektor | +| `#cam-img` | Hook / Selektor | +| `#cam-overlay` | Hook / Selektor | +| `#cam-placeholder` | Hook / Selektor | +| `#cam-placeholder-txt` | Hook / Selektor | +| `#cam-spinner` | Hook / Selektor | +| `#cam-wrap` | Hook / Selektor | +| `#conn-error-banner` | Hook / Selektor | +| `#d-ace-dry-grid` | Hook / Selektor | +| `#d-ace-dry-wrap` | Hook / Selektor | +| `#d-ams-card` | Hook / Selektor | +| `#d-bt-t` | Hook / Selektor | +| `#d-btbar` | Hook / Selektor | +| `#d-btn-skip-label` | Hook / Selektor | +| `#d-card-ams` | Hook / Selektor | +| `#d-card-cam` | Hook / Selektor | +| `#d-card-lightfan` | Hook / Selektor | +| `#d-card-progress` | Hook / Selektor | +| `#d-card-speed` | Hook / Selektor | +| `#d-card-temps` | Hook / Selektor | +| `#d-chart-label` | Hook / Selektor | +| `#d-ctrl-btns` | Hook / Selektor | +| `#d-elapsed` | Hook / Selektor | +| `#d-fname` | Hook / Selektor | +| `#d-layers` | Hook / Selektor | +| `#d-lbl-bed` | Hook / Selektor | +| `#d-lbl-elapsed` | Hook / Selektor | +| `#d-lbl-layers` | Hook / Selektor | +| `#d-lbl-light` | Hook / Selektor | +| `#d-lbl-remain` | Hook / Selektor | +| `#d-nt` | Hook / Selektor | +| `#d-nt-t` | Hook / Selektor | +| `#d-ntbar` | Hook / Selektor | +| `#d-pbar` | Hook / Selektor | +| `#d-pct` | Hook / Selektor | +| `#d-remain` | Hook / Selektor | +| `#d-slicer-label` | Hook / Selektor | +| `#d-slicer-row` | Hook / Selektor | +| `#d-slicer-time` | Hook / Selektor | +| `#d-spd-bar` | Hook / Selektor | +| `#d-spd-lbl-1` | Hook / Selektor | +| `#d-spd-lbl-2` | Hook / Selektor | +| `#d-spd-lbl-3` | Hook / Selektor | +| `#d-thumbnail` | Hook / Selektor | +| `#fd-objects` | Hook / Selektor | +| `#fd-objects-hint` | Hook / Selektor | +| `#fd-objects-section` | Hook / Selektor | +| `#fd-objects-svg` | Hook / Selektor | +| `#fd-slots-hint` | Hook / Selektor | +| `#fd-title` | Hook / Selektor | +| `#file-ready-banner` | Hook / Selektor | +| `#file-ready-name` | Hook / Selektor | +| `#h-badge` | Hook / Selektor | +| `#h-pname` | Hook / Selektor | +| `#h-pname-single` | Hook / Selektor | +| `#h-state` | Hook / Selektor | +| `#h-version` | Hook / Selektor | +| `#lbl-auto-leveling` | Hook / Selektor | +| `#lbl-default-slot` | Hook / Selektor | +| `#lbl-device-id` | Hook / Selektor | +| `#lbl-ip-hint` | Hook / Selektor | +| `#lbl-mode-id` | Hook / Selektor | +| `#lbl-mqtt-port` | Hook / Selektor | +| `#lbl-password` | Hook / Selektor | +| `#lbl-printer-ip` | Hook / Selektor | +| `#lbl-printer-name` | Hook / Selektor | +| `#lbl-slot-color` | Hook / Selektor | +| `#lbl-slot-material` | Hook / Selektor | +| `#lbl-update-apply` | Hook / Selektor | +| `#lbl-update-check` | Hook / Selektor | +| `#lbl-username` | Hook / Selektor | +| `#log-badge` | Hook / Selektor | +| `#log-badge-bot` | Hook / Selektor | +| `#modal-sec-connection` | Hook / Selektor | +| `#modal-sec-poll` | Hook / Selektor | +| `#modal-sec-print` | Hook / Selektor | +| `#modal-sec-version` | Hook / Selektor | +| `#modal-title-settings` | Hook / Selektor | +| `#opt-slot-0` | Hook / Selektor | +| `#opt-slot-1` | Hook / Selektor | +| `#opt-slot-2` | Hook / Selektor | +| `#opt-slot-3` | Hook / Selektor | +| `#opt-slot-auto` | Hook / Selektor | +| `#printer-dropdown-menu` | Hook / Selektor | +| `#printer-dropdown-wrap` | Hook / Selektor | +| `#printers-panel-title` | Hook / Selektor | +| `#ptitle-console` | Hook / Selektor | +| `#ptitle-motion-xy` | Hook / Selektor | +| `#ptitle-motion-z` | Hook / Selektor | +| `#s-auto-leveling` | Hook / Selektor | +| `#s-default-slot` | Hook / Selektor | +| `#s-device-id` | Hook / Selektor | +| `#s-mode-id` | Hook / Selektor | +| `#s-mqtt-port` | Hook / Selektor | +| `#s-password` | Hook / Selektor | +| `#s-printer-name` | Hook / Selektor | +| `#s-username` | Hook / Selektor | +| `#s-version-label` | Hook / Selektor | +| `#sf-all` | Hook / Selektor | +| `#sf-err` | Hook / Selektor | +| `#sf-new` | Hook / Selektor | +| `#sf-ok` | Hook / Selektor | +| `#skip-hint` | Hook / Selektor | +| `#skip-list` | Hook / Selektor | +| `#skip-status` | Hook / Selektor | +| `#skip-svg` | Hook / Selektor | +| `#skip-title` | Hook / Selektor | +| `#slot-edit-title` | Hook / Selektor | +| `#ss-date` | Hook / Selektor | +| `#ss-dur` | Hook / Selektor | +| `#ss-name` | Hook / Selektor | +| `#step-display` | Hook / Selektor | +| `#store-empty` | Hook / Selektor | +| `#store-panel-title` | Hook / Selektor | +| `#update-changelog` | Hook / Selektor | +| `#update-status` | Hook / Selektor | diff --git a/web/DOC/THEME-JS-ID-HOOKS.md b/web/DOC/THEME-JS-ID-HOOKS.md new file mode 100644 index 0000000..a8f199b --- /dev/null +++ b/web/DOC/THEME-JS-ID-HOOKS.md @@ -0,0 +1,86 @@ +# KX-Bridge Theme – JavaScript-ID-Hooks + +Referenzliste für JavaScript-/DOM-Hooks. + +| ID | Verwendung | +|---|---| +| `#ace-dry-dialog` | Hook / Selektor | +| `#ace-dry-dialog-cancel` | Hook / Selektor | +| `#ace-dry-dialog-confirm` | Hook / Selektor | +| `#ace-dry-dialog-custom-name` | Hook / Selektor | +| `#ace-dry-dialog-h` | Hook / Selektor | +| `#ace-dry-dialog-m` | Hook / Selektor | +| `#ace-dry-dialog-reset-default` | Hook / Selektor | +| `#ace-dry-dialog-s` | Hook / Selektor | +| `#ace-dry-dialog-save-preset` | Hook / Selektor | +| `#ace-dry-dialog-temp` | Hook / Selektor | +| `#add-printer-dialog` | Hook / Selektor | +| `#ams-slots` | Hook / Selektor | +| `#apd-confirm` | Hook / Selektor | +| `#bnb-console` | Hook / Selektor | +| `#bnb-dashboard` | Hook / Selektor | +| `#bnb-printers` | Hook / Selektor | +| `#bnb-store` | Hook / Selektor | +| `#btn-autoscroll` | Hook / Selektor | +| `#btn-save-settings` | Hook / Selektor | +| `#btn-slot-edit-feed` | Hook / Selektor | +| `#btn-slot-edit-save` | Hook / Selektor | +| `#btn-update-apply` | Hook / Selektor | +| `#btn-update-check` | Hook / Selektor | +| `#cam-toggle-btn` | Hook / Selektor | +| `#conn-btn` | Hook / Selektor | +| `#console-log` | Hook / Selektor | +| `#d-bt` | Hook / Selektor | +| `#d-btn-cancel` | Hook / Selektor | +| `#d-btn-pause` | Hook / Selektor | +| `#d-btn-resume` | Hook / Selektor | +| `#d-btn-skip` | Hook / Selektor | +| `#d-chart` | Hook / Selektor | +| `#d-fan` | Hook / Selektor | +| `#d-fan-val` | Hook / Selektor | +| `#d-light-toggle` | Hook / Selektor | +| `#d-spd-1` | Hook / Selektor | +| `#d-spd-2` | Hook / Selektor | +| `#d-spd-3` | Hook / Selektor | +| `#fd-cancel` | Hook / Selektor | +| `#fd-print` | Hook / Selektor | +| `#fd-slots` | Hook / Selektor | +| `#filament-dialog` | Hook / Selektor | +| `#file-cancel-btn` | Hook / Selektor | +| `#file-ready-btn` | Hook / Selektor | +| `#file-slots-btn` | Hook / Selektor | +| `#lang-btn` | Hook / Selektor | +| `#log-filter` | Hook / Selektor | +| `#logdir-all` | Hook / Selektor | +| `#logdir-rx` | Hook / Selektor | +| `#logdir-tx` | Hook / Selektor | +| `#nb-console` | Hook / Selektor | +| `#nb-dashboard` | Hook / Selektor | +| `#nb-printers` | Hook / Selektor | +| `#nb-store` | Hook / Selektor | +| `#p-bed-inp` | Hook / Selektor | +| `#p-nozzle-inp` | Hook / Selektor | +| `#panel-console` | Hook / Selektor | +| `#panel-dashboard` | Hook / Selektor | +| `#panel-printers` | Hook / Selektor | +| `#panel-store` | Hook / Selektor | +| `#poll-1` | Hook / Selektor | +| `#poll-2` | Hook / Selektor | +| `#poll-5` | Hook / Selektor | +| `#printer-dropdown-btn` | Hook / Selektor | +| `#printers-grid` | Hook / Selektor | +| `#s-printer-ip` | Hook / Selektor | +| `#settings-btn` | Hook / Selektor | +| `#settings-modal` | Hook / Selektor | +| `#skip-confirm` | Hook / Selektor | +| `#skip-dialog` | Hook / Selektor | +| `#slot-edit-color` | Hook / Selektor | +| `#slot-edit-mat` | Hook / Selektor | +| `#slot-edit-modal` | Hook / Selektor | +| `#slot-edit-preview` | Hook / Selektor | +| `#slot-mat-btns` | Hook / Selektor | +| `#store-filter` | Hook / Selektor | +| `#store-grid` | Hook / Selektor | +| `#store-refresh-btn` | Hook / Selektor | +| `#store-search` | Hook / Selektor | +| `#store-sort` | Hook / Selektor | diff --git a/web/themes/default/app.js b/web/themes/default/app.js new file mode 100644 index 0000000..d661eb0 --- /dev/null +++ b/web/themes/default/app.js @@ -0,0 +1,2132 @@ +// ── State ── +var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0, + print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0, + curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–', + camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80, + ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]}}; +var tempHistory={n:[],b:[]}; +var camOn=false; +var currentStep=1; +var currentPanel='dashboard'; +var aceAutoRefillPrefs=(function(){ + try{return JSON.parse(localStorage.getItem('aceAutoRefillPrefs')||'{}')||{};}catch(_){return {};} +})(); +var aceDryProfiles=(function(){ + try{return JSON.parse(localStorage.getItem('aceDryProfiles')||'{}')||{};}catch(_){return {};} +})(); +var _aceDryDialogAceId=-1; +var _aceDryDialogPresetKey=''; +var _aceDryDialogPresetOriginals={}; +var ACE_DRY_PRESET_DEFAULTS={ + pla:{temp:45,duration_sec:4*3600}, + pla_plus:{temp:45,duration_sec:4*3600}, + petg:{temp:50,duration_sec:4*3600}, + tpu:{temp:55,duration_sec:4*3600}, + abs_asa:{temp:45,duration_sec:8*3600}, + pa_pc:{temp:55,duration_sec:12*3600} +}; +var ACE_DRY_PRESETS={ + pla:{temp:45,duration_sec:4*3600}, + pla_plus:{temp:45,duration_sec:4*3600}, + petg:{temp:50,duration_sec:4*3600}, + tpu:{temp:55,duration_sec:4*3600}, + abs_asa:{temp:45,duration_sec:8*3600}, + pa_pc:{temp:55,duration_sec:12*3600}, + custom_1:{name:'Custom 1',temp:45,duration_sec:4*3600}, + custom_2:{name:'Custom 2',temp:45,duration_sec:4*3600}, + custom_3:{name:'Custom 3',temp:45,duration_sec:4*3600} +}; + +function _aceAutoRefillGet(aceId){return !!aceAutoRefillPrefs[String(aceId)];} +function _aceAutoRefillSet(aceId,on){ + aceAutoRefillPrefs[String(aceId)]=!!on; + localStorage.setItem('aceAutoRefillPrefs',JSON.stringify(aceAutoRefillPrefs)); +} +function _aceDryProfileGet(aceId){ + var p=aceDryProfiles[String(aceId)]||{}; + var temp=parseInt(p.temp,10); + var dur=parseInt(p.duration_sec,10); + if(!Number.isFinite(temp))temp=45; + if(!Number.isFinite(dur))dur=4*3600; + temp=Math.max(30,Math.min(80,temp)); + dur=Math.max(10*60,Math.min(24*3600,dur)); + return {temp:temp,duration_sec:dur,preset:p.preset||''}; +} +function _aceDryProfileSet(aceId,temp,durationSec,preset){ + aceDryProfiles[String(aceId)]={ + temp:Math.max(30,Math.min(80,parseInt(temp,10)||45)), + duration_sec:Math.max(10*60,Math.min(24*3600,parseInt(durationSec,10)||4*3600)), + preset:preset||'' + }; + localStorage.setItem('aceDryProfiles',JSON.stringify(aceDryProfiles)); +} +function _aceDryDurationMinFromSec(sec){ + var minutes=Math.round((parseInt(sec,10)||0)/60); + return Math.max(10,Math.min(1440,minutes)); +} +function _syncAceDryPresetsFromServer(raw){ + if(!raw||typeof raw!=='object')return; + Object.keys(ACE_DRY_PRESETS).forEach(function(k){ + var p=raw[k]; + if(!p||typeof p!=='object')return; + var t=parseInt(p.temp,10); + var d=parseInt(p.duration_sec,10); + if(Number.isFinite(t))ACE_DRY_PRESETS[k].temp=Math.max(30,Math.min(80,t)); + if(Number.isFinite(d))ACE_DRY_PRESETS[k].duration_sec=Math.max(10*60,Math.min(24*3600,d)); + if(/^custom_[123]$/.test(k)&&typeof p.name==='string'){ + var n=p.name.trim(); + ACE_DRY_PRESETS[k].name=n||('Custom '+k.slice(-1)); + } + }); +} + +// ── Theme ── +function toggleTheme(){ + var h=document.documentElement; + h.setAttribute('data-theme',h.getAttribute('data-theme')==='dark'?'light':'dark'); + localStorage.setItem('theme',h.getAttribute('data-theme')); +} +(function(){var t=localStorage.getItem('theme');if(t)document.documentElement.setAttribute('data-theme',t)})(); + +// ── i18n ── +var LANG_DE={ + header_status_standby:'Bereit',header_status_printing:'Druckt',header_status_complete:'Fertig',header_status_error:'Fehler', + kobra_free:'Bereit',kobra_busy:'Beschäftigt',kobra_printing:'Druckt',kobra_preheating:'Aufheizen',kobra_auto_leveling:'Nivellierung',kobra_checking:'Prüfung',kobra_updated:'Aktualisierung',kobra_init:'Initialisierung',kobra_pausing:'Pausiert...',kobra_paused:'Pausiert',kobra_resuming:'Fortsetzen...',kobra_resumed:'Fortgesetzt',kobra_stopping:'Stoppt...',kobra_stoped:'Gestoppt',kobra_finished:'Abgeschlossen',kobra_failed:'Fehler',kobra_canceled:'Abgebrochen',kobra_offline:'Offline', + nav_dashboard:'Dashboard',nav_print:'Druck',nav_temps:'Temperaturen',nav_motion:'Achsen',nav_ams:'AMS',nav_extras:'Licht / Lüfter',nav_console:'Konsole', + card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',card_speed:'Druckgeschwindigkeit',card_cam:'Kamera',lbl_elapsed:'Verstrichen:',lbl_remaining:'Restzeit:',lbl_slicer_time:'Slicer-Schätzung:',lbl_layers:'Layer', + speed_silent:'🐢 Leise',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport', + lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen', + card_ace_dry:'ACE Trocknung',ace_dry_dryer:'Trockner',ace_dry_status_off:'Status: Aus',ace_dry_status_on:'Status: Aktiv',ace_dry_status_remaining:'Rest',ace_dry_humidity:'Luftfeuchte',ace_dry_current_temp:'Temperatur',ace_dry_chart:'Verlauf (Temp/Feuchte)',ace_dry_temp:'Temperatur (°C)',ace_dry_duration:'Dauer (Min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto Refill',ace_dry_enable:'Enable Drying',ace_dry_temp_line:'Trocknungstemperatur',ace_dry_time_line:'Trocknungszeit',ace_dry_ui_pending:'(nur UI, Backend folgt)',ace_dry_dialog_title:'Dryer Temp/Time Settings',ace_dry_dialog_temp:'Temperature (30-80°C)',ace_dry_dialog_time:'Rem. Time (h:m:s)',ace_dry_dialog_confirm:'Confirm',ace_dry_dialog_cancel:'Cancel',ace_dry_dialog_save_restart:'Speichern & Neustart',ace_dry_dialog_custom_name:'Custom Name', + cam_placeholder:'📷 Kamera nicht gestartet',btn_cam_start:'▶ Kamera',btn_cam_stop:'◼ Kamera', + btn_pause:'⏸ Pause',btn_resume:'▶ Weiter',btn_cancel:'✕ Stopp', + label_nozzle:'Nozzle',label_bed:'Bett',label_fan:'🌀 Lüfter',label_light:'💡 Licht',label_on_off:'Ein / Aus',label_speed:'Geschwindigkeit', + panel_print_title:'Drucksteuerung',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Fortsetzen',panel_print_btn_cancel:'✕ Abbrechen',panel_print_temps_live:'Temperaturen (Live)', + label_set:'Setzen',label_off:'Aus', + panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heizbett',panel_temps_chart:'Verlauf (letzte 60 Messungen)',label_target_c:'Ziel:', + panel_motion_xy:'XY-Achsen',panel_motion_z:'Z-Achse',label_step:'Schrittweite:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motoren aus', + panel_ams_title:'Filament',card_ams:'Filament',ams_no_data:'Keine AMS-Daten empfangen',label_slot:'Slot',ams_empty:'Leer', + panel_extras_light:'Licht',panel_extras_fan:'Lüfter',panel_extras_camera:'Kamera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop', + panel_console_title:'Ereignis-Log', + log_light_on:'Licht an',log_light_off:'Licht aus',log_fan:'Lüfter →',log_nozzle:'Nozzle →',log_bed:'Bett →',log_axis:'Achse',log_home:'Home',log_home_all:'Home All',log_cam_start:'Kamera gestartet:',log_cam_stop:'Kamera gestoppt',log_poll_error:'Poll-Fehler:',log_error:'Fehler:', + confirm_cancel:'Druck wirklich abbrechen?', + settings_title:'Einstellungen',settings_connection:'Verbindung',settings_print:'Druckeinstellungen',settings_poll:'Poll-Intervall',settings_version:'Version', + settings_save:'Speichern & Neustart',settings_printer_name:'Drucker-Name',settings_printer_ip:'Drucker-IP',settings_mqtt_port:'MQTT-Port', + settings_username:'MQTT-Benutzername',settings_password:'MQTT-Passwort',settings_device_id:'Device-ID',settings_mode_id:'Mode-ID',hint_ip_no_port:'Nur IP-Adresse, kein Port (z.B. 192.168.1.102)', + settings_default_slot:'Standard-Slot (Einfarbdruck)',settings_slot_auto:'Auto (alle belegten Slots)',settings_auto_leveling:'Auto-Leveling vor Druck', + update_check:'Auf Updates prüfen',update_checking:'Prüfe...',update_available:'verfügbar',update_none:'Bereits aktuell', + update_apply:'Jetzt installieren',update_applying:'Lade herunter...',update_restarting:'Starte neu...',update_error:'Fehler', + btn_connect:'⚡ Verbinden',btn_disconnect:'✕ Trennen', + lbl_conn_error:'Verbindungsfehler:', + slot_edit_title:'Slot bearbeiten',slot_edit_color:'Farbe',slot_edit_material:'Material', + slot_edit_load:'⬇ Einziehen',slot_edit_unload:'⬆ Ausziehen', + slot_edit_save:'💾 Speichern',slot_edit_custom:'z.B. PLA, PETG, ABS…', + slot_edit_ok:'AMS Slot', + log_dir_all:'Alle', + file_ready_btn:'▶ Druck starten', + file_slots_btn:'🎨 Slots wählen', + file_cancel_btn:'✕ Abbrechen', + nav_printers:'Drucker', + skip_title:'✂ Objekte überspringen',skip_hint:'Objekte abwählen, die nicht weiter gedruckt werden sollen:', + skip_btn_label:'Objekte',skip_no_objects:'Keine Objekte in diesem Druck.', + skip_already:'übersprungen',skip_select_at_least_one:'Bitte mindestens ein Objekt wählen.', + skip_sending:'Sende …',skip_success:'Objekte werden übersprungen.', + fd_objects_hint:'Objekte überspringen (optional):', + add_printer:'Drucker hinzufügen',apd_lbl_ip:'Drucker-IP',apd_lbl_name:'Name (optional)', + apd_fetching:'Hole Daten vom Drucker…',apd_success:'Drucker hinzugefügt, Bridge startet neu…',apd_err_ip:'Bitte IP-Adresse eingeben', + printers_remove:'Drucker entfernen',printers_remove_confirm:'Drucker "{name}" entfernen? Die Bridge startet neu.', + printers_active:'● aktiv', + printers_switch:'Wechseln →', + printers_current:'Aktueller Drucker', + printers_loading:'Lade…', + printers_none:'Keine Drucker konfiguriert.', + printers_empty_hint:'Noch kein Drucker eingerichtet.', + nav_browser:'Browser', + panel_browser_title:'Datei-Browser', + store_empty:'Noch keine Dateien hochgeladen.', + store_refresh:'↻ Aktualisieren', + store_print:'▶ Drucken', + store_delete_confirm:'Datei löschen?', + store_print_confirm:'Datei drucken?', + store_no_results:'Keine Dateien gefunden.', + store_never:'noch nicht gedruckt', + sf_all:'Alle',sf_ok:'✓ Erfolgreich',sf_err:'✗ Fehler',sf_new:'Neu', + ss_date:'↓ Datum',ss_name:'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', + card_ace_dry:'ACE Drying',ace_dry_dryer:'Dryer',ace_dry_status_off:'Status: Off',ace_dry_status_on:'Status: Active',ace_dry_status_remaining:'Remaining',ace_dry_humidity:'Humidity',ace_dry_current_temp:'Temperature',ace_dry_chart:'History (Temp/Humidity)',ace_dry_temp:'Temperature (°C)',ace_dry_duration:'Duration (min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto Refill',ace_dry_enable:'Enable Drying',ace_dry_temp_line:'Drying Temperature',ace_dry_time_line:'Drying Time',ace_dry_ui_pending:'(UI only, backend next)',ace_dry_dialog_title:'Dryer Temp/Time Settings',ace_dry_dialog_temp:'Temperature (30-80°C)',ace_dry_dialog_time:'Rem. Time (h:m:s)',ace_dry_dialog_confirm:'Confirm',ace_dry_dialog_cancel:'Cancel',ace_dry_dialog_save_restart:'Save & Restart',ace_dry_dialog_custom_name:'Custom Name', + cam_placeholder:'📷 Camera not started',btn_cam_start:'▶ Camera',btn_cam_stop:'◼ Camera', + btn_pause:'⏸ Pause',btn_resume:'▶ Resume',btn_cancel:'✕ Stop', + label_nozzle:'Nozzle',label_bed:'Bed',label_fan:'🌀 Fan',label_light:'💡 Light',label_on_off:'On / Off',label_speed:'Speed', + panel_print_title:'Print Control',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Resume',panel_print_btn_cancel:'✕ Cancel',panel_print_temps_live:'Temperatures (Live)', + label_set:'Set',label_off:'Off', + panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heated Bed',panel_temps_chart:'History (last 60 readings)',label_target_c:'Target:', + panel_motion_xy:'XY Axes',panel_motion_z:'Z Axis',label_step:'Step size:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motors Off', + panel_ams_title:'Filament',card_ams:'Filament',ams_no_data:'No AMS data received',label_slot:'Slot',ams_empty:'Empty', + panel_extras_light:'Light',panel_extras_fan:'Fan',panel_extras_camera:'Camera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop', + panel_console_title:'Event Log', + log_light_on:'Light on',log_light_off:'Light off',log_fan:'Fan →',log_nozzle:'Nozzle →',log_bed:'Bed →',log_axis:'Axis',log_home:'Home',log_home_all:'Home All',log_cam_start:'Camera started:',log_cam_stop:'Camera stopped',log_poll_error:'Poll error:',log_error:'Error:', + confirm_cancel:'Really cancel the print?', + settings_title:'Settings',settings_connection:'Connection',settings_print:'Print Settings',settings_poll:'Poll Interval',settings_version:'Version', + settings_save:'Save & Restart',settings_printer_name:'Printer Name',settings_printer_ip:'Printer IP',settings_mqtt_port:'MQTT Port', + settings_username:'MQTT Username',settings_password:'MQTT Password',settings_device_id:'Device ID',settings_mode_id:'Mode ID',hint_ip_no_port:'IP address only, no port (e.g. 192.168.1.102)', + settings_default_slot:'Default Slot (single color)',settings_slot_auto:'Auto (all loaded slots)',settings_auto_leveling:'Auto-Leveling before print', + update_check:'Check for Updates',update_checking:'Checking...',update_available:'available',update_none:'Already up to date', + update_apply:'Install Now',update_applying:'Downloading...',update_restarting:'Restarting...',update_error:'Error', + btn_connect:'⚡ Connect',btn_disconnect:'✕ Disconnect', + lbl_conn_error:'Connection error:', + slot_edit_title:'Edit Slot',slot_edit_color:'Color',slot_edit_material:'Material', + slot_edit_load:'⬇ Load',slot_edit_unload:'⬆ Unload', + slot_edit_save:'💾 Save',slot_edit_custom:'e.g. PLA, PETG, ABS…', + slot_edit_ok:'AMS Slot', + log_dir_all:'All', + file_ready_btn:'▶ Start Print', + file_slots_btn:'🎨 Select Slots', + file_cancel_btn:'✕ Cancel', + nav_printers:'Printers', + skip_title:'✂ Skip objects',skip_hint:'Uncheck objects you no longer want to print:', + skip_btn_label:'Objects',skip_no_objects:'No objects in this print.', + skip_already:'skipped',skip_select_at_least_one:'Please pick at least one object.', + skip_sending:'Sending …',skip_success:'Objects will be skipped.', + fd_objects_hint:'Skip objects (optional):', + add_printer:'Add printer',apd_lbl_ip:'Printer IP',apd_lbl_name:'Name (optional)', + apd_fetching:'Fetching data from printer…',apd_success:'Printer added, bridge restarting…',apd_err_ip:'Please enter an IP address', + printers_remove:'Remove printer',printers_remove_confirm:'Remove printer "{name}"? The bridge will restart.', + printers_active:'● active', + printers_switch:'Switch →', + printers_current:'Current printer', + printers_loading:'Loading…', + printers_none:'No printers configured.', + printers_empty_hint:'No printer set up yet.', + nav_browser:'Browser', + panel_browser_title:'File Browser', + store_empty:'No files uploaded yet.', + store_refresh:'↻ Refresh', + store_print:'▶ Print', + store_delete_confirm:'Delete file?', + store_print_confirm:'Print file?', + store_no_results:'No files found.', + store_never:'never printed', + sf_all:'All',sf_ok:'✓ Completed',sf_err:'✗ Failed',sf_new:'New', + ss_date:'↓ Date',ss_name:'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 ''+ + (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'; + } +}); + +var currentLang='de'; +var T=LANG_DE; +function toggleLang(){ + currentLang=currentLang==='de'?'en':'de'; + T=currentLang==='de'?LANG_DE:LANG_EN; + localStorage.setItem('lang',currentLang); + document.getElementById('lang-btn').textContent=currentLang==='de'?'EN':'DE'; + document.documentElement.setAttribute('lang',currentLang); + applyLang(); +} +function applyLang(){ + ensureAceDryCards(); + // Nav + var nb=document.getElementById('nb-dashboard');if(nb)nb.querySelector('.nav-text').textContent=T.nav_dashboard; + nb=document.getElementById('nb-console');if(nb)nb.querySelector('.nav-text').textContent=T.nav_console; + nb=document.getElementById('nb-printers');if(nb)nb.querySelector('.nav-text').textContent=T.nav_printers; + nb=document.getElementById('nb-store');if(nb)nb.querySelector('.nav-text').textContent=T.nav_browser; + // Bottom nav + var bnb=document.getElementById('bnb-dashboard');if(bnb)bnb.lastChild.textContent=T.nav_dashboard; + bnb=document.getElementById('bnb-console');if(bnb)bnb.lastChild.textContent=T.nav_console; + bnb=document.getElementById('bnb-printers');if(bnb)bnb.lastChild.textContent=T.nav_printers; + bnb=document.getElementById('bnb-store');if(bnb)bnb.lastChild.textContent=T.nav_browser; + // Browser panel + setText('printers-panel-title','🖨 '+T.nav_printers); + setText('add-printer-btn-label',T.add_printer); + setText('apd-title',T.add_printer); + setText('skip-title',T.skip_title); + setText('skip-hint',T.skip_hint); + setText('d-btn-skip-label',T.skip_btn_label); + setText('fd-objects-hint',T.fd_objects_hint); + setText('apd-lbl-ip',T.apd_lbl_ip); + setText('apd-lbl-name',T.apd_lbl_name); + setText('store-panel-title','🗂 '+T.panel_browser_title); + var srb=document.getElementById('store-refresh-btn');if(srb)srb.textContent=T.store_refresh; + setText('store-empty',T.store_empty); + setText('sf-all',T.sf_all);setText('sf-ok',T.sf_ok);setText('sf-err',T.sf_err);setText('sf-new',T.sf_new); + setText('ss-date',T.ss_date);setText('ss-name',T.ss_name);setText('ss-dur',T.ss_dur); + // Dashboard card titles + setText('d-card-progress',T.card_progress); + setText('d-card-temps',T.card_temps); + setText('d-card-lightfan',T.card_light_fan); + setText('d-card-speed',T.card_speed); + setText('d-card-cam',T.card_cam); + setText('d-card-ams',T.panel_ams_title); + setText('d-lbl-elapsed',T.lbl_elapsed); + setText('d-lbl-remain',T.lbl_remaining); + setText('d-slicer-label',T.lbl_slicer_time); + setText('d-lbl-layers',T.lbl_layers); + setText('d-lbl-light',T.lbl_light); + setText('d-lbl-bed',T.label_bed); + // Dashboard buttons + setText('d-btn-pause',T.btn_pause); + setText('d-btn-resume',T.btn_resume); + setText('d-btn-cancel',T.btn_cancel); + setText('cam-toggle-btn',camOn?T.btn_cam_stop:T.btn_cam_start); + setText('cam-placeholder-txt',T.cam_placeholder); + // Temp labels + document.querySelectorAll('.lbl-set').forEach(e=>e.textContent=T.label_set); + document.querySelectorAll('.lbl-off').forEach(e=>e.textContent=T.label_off); + setText('d-chart-label',T.panel_temps_chart); + // Axis labels + setText('ptitle-motion-xy',T.panel_motion_xy); + setText('ptitle-motion-z',T.panel_motion_z); + document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z); + document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy); + document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all); + document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors); + document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step); + document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':',''))); + // Console + setText('ptitle-console',T.panel_console_title); + // Settings modal + setText('modal-title-settings',T.settings_title); + setText('modal-sec-connection',T.settings_connection); + setText('modal-sec-print',T.settings_print); + setText('modal-sec-poll',T.settings_poll); + setText('modal-sec-version',T.settings_version); + setText('btn-save-settings',T.settings_save); + setText('lbl-printer-name',T.settings_printer_name); + setText('lbl-printer-ip',T.settings_printer_ip); + setText('lbl-mqtt-port',T.settings_mqtt_port); + setText('lbl-username',T.settings_username); + setText('lbl-password',T.settings_password); + setText('lbl-device-id',T.settings_device_id); + setText('lbl-mode-id',T.settings_mode_id); + setText('lbl-default-slot',T.settings_default_slot); + setText('opt-slot-auto',T.settings_slot_auto); + setText('lbl-auto-leveling',T.settings_auto_leveling); + + setText('lbl-update-check',T.update_check); + setText('lbl-update-apply',T.update_apply); + // Speed buttons + setText('d-spd-lbl-1',T.speed_silent.replace(/^\S+\s/,'')); + setText('d-spd-lbl-2',T.speed_normal.replace(/^\S+\s/,'')); + setText('d-spd-lbl-3',T.speed_sport.replace(/^\S+\s/,'')); + // AMS feed/unload + document.querySelectorAll('.lbl-feed').forEach(e=>e.textContent=T.lbl_feed); + document.querySelectorAll('.lbl-unload').forEach(e=>e.textContent=T.lbl_unload); + for(var i=0;i<4;i++){ + setText('d-card-ace-dry-'+i,'ACE '+(i+1)+' - '+(T.ace_dry_dryer||'Dryer')); + setText('d-ace-auto-refill-label-'+i,T.ace_dry_auto_refill||'Auto Refill'); + setText('d-ace-drying-enable-label-'+i,T.ace_dry_enable||'Enable Drying'); + setText('d-ace-dry-humidity-label-'+i,(T.ace_dry_humidity||'Humidity')+':'); + setText('d-ace-dry-current-temp-label-'+i,(T.ace_dry_current_temp||'Current Temp')+':'); + setText('d-ace-dry-target-label-'+i,(T.ace_dry_temp_line||'Drying Temperature')+':'); + setText('d-ace-dry-time-label-'+i,(T.ace_dry_time_line||'Drying Time')+':'); + setText('d-ace-dry-chart-label-'+i,T.ace_dry_chart||'History (Temp/Humidity)'); + var adTemp=document.getElementById('ace-dry-temp-'+i);if(adTemp)adTemp.setAttribute('placeholder',T.ace_dry_temp); + var adDur=document.getElementById('ace-dry-duration-'+i);if(adDur)adDur.setAttribute('placeholder',T.ace_dry_duration); + } + setText('ace-dry-dialog-title',T.ace_dry_dialog_title||'Dryer Temp/Time Settings'); + setText('ace-dry-dialog-temp-label',T.ace_dry_dialog_temp||'Temperature (30-80°C)'); + setText('ace-dry-dialog-time-label',T.ace_dry_dialog_time||'Rem. Time (h:m:s)'); + setText('ace-dry-dialog-custom-name-label',T.ace_dry_dialog_custom_name||'Custom Name'); + setText('ace-dry-dialog-cancel',T.ace_dry_dialog_cancel||'Cancel'); + setText('ace-dry-dialog-confirm',T.ace_dry_dialog_confirm||'Confirm'); + setText('ace-dry-dialog-reset-default',T.ace_dry_dialog_reset_default||'Reset to Default'); + setText('ace-dry-dialog-save-preset',T.ace_dry_dialog_save_restart||'Save & Restart'); + aceDryDialogSyncCustomButtonNames(); + // conn-btn text (nur wenn nicht im Übergangszustand) + updateConnBtn(); + // Slot-Edit-Dialog + setText('lbl-slot-color',T.slot_edit_color); + setText('lbl-slot-material',T.slot_edit_material); + setText('btn-slot-edit-save',T.slot_edit_save); + updateSlotEditFeedButton(); + var mi=document.getElementById('slot-edit-mat');if(mi)mi.setAttribute('placeholder',T.slot_edit_custom); + setText('logdir-all',T.log_dir_all); + setText('file-ready-btn',T.file_ready_btn); + setText('file-slots-btn',T.file_slots_btn); + setText('file-cancel-btn',T.file_cancel_btn); + setText('file-cancel-btn',T.file_cancel_btn); +} +function setText(id,txt){var el=document.getElementById(id);if(el)el.textContent=txt;} + +function ensureAceDryCards(){ + var grid=document.getElementById('d-ace-dry-grid'); + if(!grid||grid.getAttribute('data-init')==='1')return; + var html=''; + for(var i=0;i<4;i++){ + html+=''; + } + grid.innerHTML=html; + grid.setAttribute('data-init','1'); +} +(function(){ + var l=localStorage.getItem('lang')||'de'; + currentLang=l;T=l==='de'?LANG_DE:LANG_EN; + document.getElementById('lang-btn').textContent=l==='de'?'EN':'DE'; + document.documentElement.setAttribute('lang',l); + // defer until DOM ready + window.addEventListener('DOMContentLoaded',function(){ + applyLang(); + // Kein Drucker konfiguriert? → direkt in den Drucker-Tab (zeigt "+ Drucker hinzufügen") + fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){ + if(!d.result||!d.result.length){showPanel('printers');loadPrinterTab();} + }).catch(function(){}); + }); +})(); + +// ── Panel nav ── +function showPanel(id){ + document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active')); + document.getElementById('panel-'+id).classList.add('active'); + document.querySelectorAll('.nav-btn,.bnav-btn').forEach(b=>b.classList.remove('active')); + var nb=document.getElementById('nb-'+id);if(nb)nb.classList.add('active'); + var bnb=document.getElementById('bnb-'+id);if(bnb)bnb.classList.add('active'); + currentPanel=id; +} + +// ── Console log ── +var consoleLogs=[]; +var logAutoScroll=true; +var logBadgeCount=0; +var logDirFilter='all'; // 'all'|'rx'|'tx' +var 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=>`
${l.ts}${escHtml(l.msg)}
`).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='⚠ '+(T.lbl_conn_error||'Connection error:')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}} + var frb=document.getElementById('file-ready-banner'); + if(frb){ + if(s.file_ready&&s.print_state==='standby'){ + document.getElementById('file-ready-name').textContent=s.file_ready; + frb.style.display='flex'; + }else{frb.style.display='none';} + } + // skip-button (mid-print) – nur sichtbar wenn aktuell gedruckt wird + var skipBtn=document.getElementById('d-btn-skip'); + if(skipBtn){ + var printing=(s.print_state==='printing'||s.print_state==='paused'); + skipBtn.style.display=printing?'':'none'; + } + + // header + var b=document.getElementById('h-badge'); + b.className='hbadge '+s.print_state; + document.getElementById('h-state').textContent=T['kobra_'+s.kobra_state]||s.kobra_state||T.header_status_standby; + var _pn=_printers.length===0?'–':((_activePrinter&&_activePrinter.name)||s.printer_name); + var _el=document.getElementById('h-pname');if(_el)_el.textContent=_pn; + var _el2=document.getElementById('h-pname-single');if(_el2)_el2.textContent=_pn; + var hv=document.getElementById('h-version');if(hv&&s.version)hv.textContent='v'+s.version; + + + // temps + var nt=document.getElementById('d-nt');if(nt)nt.textContent=s.nozzle_temp.toFixed(1); + var ntt=document.getElementById('d-nt-t');if(ntt)ntt.textContent=s.nozzle_target.toFixed(0); + var bt=document.getElementById('d-bt');if(bt)bt.textContent=s.bed_temp.toFixed(1); + var btt=document.getElementById('d-bt-t');if(btt)btt.textContent=s.bed_target.toFixed(0); + + // temp bars (dashboard) + var nb=document.getElementById('d-ntbar');if(nb)nb.style.width=clamp(s.nozzle_temp/300*100,0,100)+'%'; + var bb=document.getElementById('d-btbar');if(bb)bb.style.width=clamp(s.bed_temp/120*100,0,100)+'%'; + + // progress + var pct=Math.round(s.progress*100); + var dpct=document.getElementById('d-pct');if(dpct)dpct.textContent=pct; + var dpbar=document.getElementById('d-pbar');if(dpbar)dpbar.style.width=pct+'%'; + + var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'–'; + var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers; + + var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration); + var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'–'; + var dslrow=document.getElementById('d-slicer-row'); + var dsltime=document.getElementById('d-slicer-time'); + if(dslrow&&dsltime){ + if(s.slicer_time>0){dslrow.style.display='';dsltime.textContent=fmtTime(s.slicer_time);} + else{dslrow.style.display='none';} + } + + var fn=s.filename||'–'; + var dfname=document.getElementById('d-fname');if(dfname){dfname.textContent=fn;dfname.title=fn}; + var pfname=document.getElementById('p-fname');if(pfname){pfname.textContent=fn;pfname.title=fn}; + var cfo=document.getElementById('cam-fname');if(cfo)cfo.textContent=fn!=='–'?fn:''; + + // thumbnail + var thumb=document.getElementById('d-thumbnail'); + if(thumb){ + if(s.thumbnail){ + thumb.src='data:image/png;base64,'+s.thumbnail; + thumb.style.display='block'; + } else { + thumb.style.display='none'; + thumb.src=''; + } + } + + // light/fan sync + document.getElementById('d-light-toggle').checked=s.light_on; + var dfan=document.getElementById('d-fan');if(dfan)dfan.value=s.fan_speed; + var dfanval=document.getElementById('d-fan-val');if(dfanval)dfanval.textContent=s.fan_speed; + + // speed mode buttons + var spdWidths={1:25,2:55,3:90}; + [1,2,3].forEach(function(m){ + var b=document.getElementById('d-spd-'+m); + if(b) b.classList.toggle('spd-active', s.print_speed_mode===m); + }); + var spdBar=document.getElementById('d-spd-bar'); + if(spdBar) spdBar.style.width=(spdWidths[s.print_speed_mode]||55)+'%'; + + var amsTitle=document.getElementById('d-card-ams'); + if(amsTitle){ + var baseTitle=T.card_ams||'Filament'; + var modeMap={toolhead:'Toolhead',ace_direct:'ACE Direct',ace_hub:'ACE Hub'}; + var modeTxt=modeMap[s.filament_mode]||''; + amsTitle.textContent=modeTxt?(baseTitle+' - '+modeTxt):baseTitle; + } + + ensureAceDryCards(); + var dry=s.ace_drying||{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]}; + var units=(dry.units||[]); + var unitMap={}; + units.forEach(function(u){var id=Number(u.id);if(id>=0&&id<=3)unitMap[id]=u;}); + var aceMode=s.filament_mode==='ace_direct'||s.filament_mode==='ace_hub'; + var detected=(s.ace_units||[]).filter(function(id){return id>=0&&id<=3;}); + if(!detected.length){ + Object.keys(unitMap).forEach(function(k){detected.push(Number(k));}); + } + if(!detected.length){ + (s.ams_slots||[]).forEach(function(sl){var id=Number(sl.box_id);if(id>=0&&id<=3&&detected.indexOf(id)<0)detected.push(id);}); + } + detected.sort(function(a,b){return a-b;}); + var aceWrap=document.getElementById('d-ace-dry-wrap'); + if(aceWrap)aceWrap.style.display=(aceMode&&detected.length)?'contents':'none'; + for(var i=0;i<4;i++){ + var card=document.getElementById('d-ace-dry-card-'+i); + if(!card)continue; + var show=aceMode&&detected.indexOf(i)>=0; + card.style.display=show?'':'none'; + if(!show)continue; + var ud=unitMap[i]||dry; + var refillToggle=document.getElementById('ace-auto-refill-toggle-'+i); + var autoFeedMap=s.ace_auto_feed||{}; + if(refillToggle&&!_aceAutoFeedPending[i]){ + var afVal=autoFeedMap.hasOwnProperty(String(i))?Number(autoFeedMap[String(i)]):(_aceAutoRefillGet(i)?1:0); + refillToggle.checked=afVal===1; + } + var dryToggle=document.getElementById('ace-dry-enable-toggle-'+i); + if(dryToggle)dryToggle.checked=Number(ud.status||0)>0; + var hh=document.getElementById('d-ace-dry-humidity-'+i); + if(hh){ + var hv=(ud.humidity===null||ud.humidity===undefined||ud.humidity==='')?null:Number(ud.humidity); + hh.textContent=(hv===null||Number.isNaN(hv))?'-':(Math.round(hv)+'%'); + } + var ht=document.getElementById('d-ace-dry-current-temp-'+i); + if(ht){ + var ct=(ud.current_temp===null||ud.current_temp===undefined||ud.current_temp==='')?null:Number(ud.current_temp); + ht.textContent=(ct===null||Number.isNaN(ct))?'-':(ct.toFixed(1)+'°C'); + } + var prof=_aceDryProfileGet(i); + var useSec=(Number(ud.status||0)>0&&Number(ud.remain_time)>0) + ?Number(ud.remain_time||0)*60 + :prof.duration_sec; + var showTemp=(Number(ud.status||0)>0&&Number(ud.target_temp)>0)?Number(ud.target_temp):prof.temp; + var dryTempEl=document.getElementById('d-ace-dry-target-'+i); + if(dryTempEl)dryTempEl.textContent=showTemp+'°C'; + var dryTimeEl=document.getElementById('d-ace-dry-time-'+i); + if(dryTimeEl)dryTimeEl.textContent=fmtHmsFromSec(useSec); + } + + // AMS + if(s.ams_slots&&s.ams_slots.length){ + window._amsSlots=s.ams_slots; + // Group by box_id (-1=Toolhead, 0=ACE 1, 1=ACE 2, ...) + var boxMap={}; + s.ams_slots.forEach(function(slot,i){ + var bid=slot.box_id!=null?slot.box_id:-1; + if(!boxMap[bid])boxMap[bid]=[]; + boxMap[bid].push({slot:slot,arrIdx:i}); + }); + var boxIds=Object.keys(boxMap).map(Number).sort(function(a,b){return a-b}); + var acePresent=boxIds.some(function(b){return b>=0;}); + var html=''; + boxIds.forEach(function(bid){ + var entries=boxMap[bid]; + var label=bid===-1 + ?(acePresent?'Toolhead (Slots 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='Slot '+(globalIdx+1); + html+='
' + +'
' + +'
'+(empty?'–':(slot.type||slot.material_type||'–'))+'
' + +'
'+slotLabel+'
' + +'
'+pct+'
' + +'
' + +'
'; + }); + if(bid===-1&&acePresent){ + html+='
' + +'
ACE
' + +'
'; + } + html+='
'; + }); + 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 _slotEditLoaded=false; +var _MAT_PRESETS=['PLA','PETG','ABS','ASA','TPU','PA','PC','HIPS']; +function updateSlotEditFeedButton(){ + var btn=document.getElementById('btn-slot-edit-feed'); + if(!btn)return; + if(_slotEditIndex<0){ + btn.style.display='none'; + return; + } + btn.style.display=''; + btn.textContent=_slotEditLoaded?(T.slot_edit_unload||'⬆ Unload'):(T.slot_edit_load||'⬇ Load'); +} +function openSlotEdit(i){ + var slot=(window._amsSlots||[])[i]||{}; + var globalIdx=slot.global_index!=null?slot.global_index:(slot.index!=null?slot.index:i); + _slotEditIndex=globalIdx; + _slotEditLoaded=(S.ams_loaded_slot!=null&&S.ams_loaded_slot===globalIdx); + document.getElementById('slot-edit-title').textContent=T.slot_edit_title+' '+(globalIdx+1); + var rgb=Array.isArray(slot.color)?slot.color:[128,128,128]; + var hex='#'+rgb.map(function(v){return('0'+Math.min(255,v).toString(16)).slice(-2)}).join(''); + var ci=document.getElementById('slot-edit-color'); + ci.value=hex; + document.getElementById('slot-edit-preview').style.background=hex; + var mat=(slot.type||'PLA').toUpperCase(); + document.getElementById('slot-edit-mat').value=mat; + var btns=document.getElementById('slot-mat-btns'); + btns.innerHTML=_MAT_PRESETS.map(function(m){ + return ''; + }).join(''); + updateSlotEditFeedButton(); + document.getElementById('slot-edit-modal').classList.add('open'); +} +function closeSlotEdit(){ + _slotEditIndex=-1; + document.getElementById('slot-edit-modal').classList.remove('open'); +} +function slotEditFeed(){ + if(_slotEditIndex<0)return; + var type=_slotEditLoaded?2:1; + amsFeed(type,_slotEditIndex) + .then(function(){ + _slotEditLoaded=!_slotEditLoaded; + updateSlotEditFeedButton(); + poll(); + }) + .catch(function(){}); +} +function startReadyFile(){ + var btn=document.getElementById('file-ready-btn'); + if(btn){btn.disabled=true;btn.textContent='…';} + post('/printer/print/start',{filename:S.file_ready}) + .then(function(r){return r.json();}) + .then(function(r){ + document.getElementById('file-ready-banner').style.display='none'; + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }) + .catch(function(e){ + clog((T.log_error||'Error:')+' '+e,'msg-err'); + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }); +} +function cancelReadyFile(){ + post('/api/file_ready/clear',{}) + .then(function(){document.getElementById('file-ready-banner').style.display='none';}); +} +function selectMatPreset(m){ + document.getElementById('slot-edit-mat').value=m; + highlightMatBtn(m); +} +function highlightMatBtn(val){ + document.querySelectorAll('.mat-preset-btn').forEach(function(b){ + var on=b.getAttribute('data-mat')===val.toUpperCase(); + b.style.background=on?'var(--accent)':'var(--raised)'; + b.style.color=on?'#fff':'var(--txt2)'; + }); +} +function hexToRgb(hex){ + var r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16); + return[r,g,b]; +} +function saveSlotEdit(){ + var hex=document.getElementById('slot-edit-color').value; + var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA'; + var color=hexToRgb(hex); + post('/api/ams/set_slot',{index:_slotEditIndex,type:mat,color:color}) + .then(function(r){return r.json();}) + .then(function(r){ + closeSlotEdit(); + clog((T.slot_edit_ok||'AMS Slot')+' '+(_slotEditIndex+1)+': '+mat+' '+hex,'msg-ok'); + }) + .catch(function(e){clog('Fehler: '+e,'msg-err');}); +} +document.addEventListener('DOMContentLoaded',function(){ + document.getElementById('s-printer-ip').addEventListener('input',function(){ + var hint=document.getElementById('lbl-ip-hint'); + if(this.value.includes(':')){hint.textContent=T.hint_ip_no_port;hint.style.display='block';} + else{hint.style.display='none';} + }); +}); +function setPoll(ms){ + document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')}); + var id='poll-'+Math.round(ms/1000); + var pb=document.getElementById(id);if(pb)pb.classList.add('active'); + localStorage.setItem('pollInterval',ms); + clearInterval(pollTimer); + pollTimer=setInterval(poll,ms); +} +function saveSettings(){ + var btn=document.getElementById('btn-save-settings'); + btn.disabled=true;btn.textContent='…'; + post('/api/settings',{ + printer_name: document.getElementById('s-printer-name').value, + printer_ip: document.getElementById('s-printer-ip').value, + mqtt_port: parseInt(document.getElementById('s-mqtt-port').value)||9883, + username: document.getElementById('s-username').value, + password: document.getElementById('s-password').value, + device_id: document.getElementById('s-device-id').value, + mode_id: document.getElementById('s-mode-id').value, + default_ams_slot: document.getElementById('s-default-slot').value, + auto_leveling: document.getElementById('s-auto-leveling').checked?1:0, + }).then(function(){ + btn.textContent=T.update_restarting; + setTimeout(function(){ + btn.disabled=false; + setText('btn-save-settings',T.settings_save); + closeSettings(); + poll(); + },4000); + }).catch(function(e){ + btn.disabled=false;setText('btn-save-settings',T.settings_save); + clog('Settings-Fehler: '+e,'msg-err'); + }); +} +function checkUpdate(){ + var sb=document.getElementById('update-status'); + sb.textContent=T.update_checking; + document.getElementById('btn-update-apply').style.display='none'; + _updateTag='';_updateUrl=''; + fetch(_apiUrl('/api/update/check')).then(function(r){return r.json()}).then(function(d){ + if(d.error){sb.textContent=T.update_error+': '+d.error;return;} + var cl=document.getElementById('update-changelog'); + if(d.changelog&&d.changelog.trim()){cl.textContent=d.changelog;cl.style.display='block';} + else{cl.style.display='none';} + if(d.update_available){ + sb.textContent='v'+d.latest+' '+T.update_available; + sb.style.color='var(--ok)'; + _updateTag=d.tag;_updateUrl=d.download_url; + document.getElementById('btn-update-apply').style.display='inline-block'; + } else { + sb.textContent=T.update_none; + sb.style.color='var(--txt2)'; + } + }).catch(function(e){sb.textContent=T.update_error+': '+e;}); +} +function applyUpdate(){ + if(!_updateUrl)return; + var sb=document.getElementById('update-status'); + var btn=document.getElementById('btn-update-apply'); + btn.disabled=true;sb.textContent=T.update_applying; + post('/api/update/apply',{download_url:_updateUrl,tag:_updateTag}).then(function(){ + sb.textContent=T.update_restarting; + closeSettings(); + setTimeout(function(){poll();},5000); + }).catch(function(e){ + btn.disabled=false;sb.textContent=T.update_error+': '+e; + }); +} + +// ── Poll ── +async function poll(){ + try{ + var r=await fetch(_apiUrl('/api/state')); + if(!r.ok)return; + var d=await r.json(); + Object.assign(S,d); + applyState(); + updateHistory(); + }catch(e){clog(T.log_poll_error+' '+e,'msg-err')} +} +var pollTimer; +(function(){ + var ms=parseInt(localStorage.getItem('pollInterval')||'2000'); + initPrinters(); + poll();pollTimer=setInterval(poll,ms); +})(); + +// ── Print actions ── +function printAction(a){ + post('/printer/print/'+a,{}).then(function(){clog('Druck: '+a,'msg-ok');poll()}) + .catch(function(e){clog('Fehler: '+e,'msg-err')}); +} +function confirmCancel(){if(confirm('Druck wirklich abbrechen?'))printAction('cancel')} + +// ── Axis motion ── +// axis codes: 0=X, 1=Y, 2=Z +// move_type 1=relative, distance positive/negative +function getStep(){return currentStep} +function setStep(btn,v){ + currentStep=v; + document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active')); + btn.classList.add('active'); + document.getElementById('step-display').textContent=v; +} +function move(axis,dir,dist){ + // axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z + var axisMap={0:1,1:2,2:3}; + post('/api/axis',{axis:axisMap[axis],move_type:1,distance:dir*dist}) + .then(function(){clog('Achse '+(axis===0?'X':axis===1?'Y':'Z')+' '+(dir>0?'+':'')+dir*dist+'mm','msg-ok')}) + .catch(function(e){clog('Achse-Fehler: '+e,'msg-err')}); +} +function homeAll(){ + post('/api/axis',{axis:5,move_type:2,distance:0}) + .then(function(){clog('Home All','msg-ok')}) + .catch(function(e){clog('Home-Fehler: '+e,'msg-err')}); +} +function homeXY(){ + post('/api/axis',{axis:4,move_type:2,distance:0}) + .then(function(){clog('Home XY','msg-ok')}) + .catch(function(e){clog('Home-Fehler: '+e,'msg-err')}); +} +function homeZ(){ + post('/api/axis',{axis:3,move_type:2,distance:0}) + .then(function(){clog('Home Z','msg-ok')}) + .catch(function(e){clog('Home-Fehler: '+e,'msg-err')}); +} +function disableMotors(){ + post('/api/axis',{action:'turnOff'}) + .then(function(){clog('Motors Off','msg-ok')}) + .catch(function(e){clog('Motors-Fehler: '+e,'msg-err')}); +} + +// ── Temperature ── +function setNozzle(){ + var v=parseFloat(document.getElementById('p-nozzle-inp').value||0); + post('/api/temperature',{nozzle:v,bed:S.bed_target}) + .then(function(){clog('Nozzle → '+v+'°C','msg-ok')}) + .catch(function(e){clog('Temp-Fehler: '+e,'msg-err')}); +} +function setBed(){ + var v=parseFloat(document.getElementById('p-bed-inp').value||0); + post('/api/temperature',{nozzle:S.nozzle_target,bed:v}) + .then(function(){clog(T.label_bed+' → '+v+'°C','msg-ok')}) + .catch(function(e){clog('Temp-Fehler: '+e,'msg-err')}); +} + +// ── Light ── +function setLight(){ + var on=document.getElementById('d-light-toggle').checked; + post('/api/light',{on:on,brightness:80}) + .then(function(){clog('Licht '+(on?'an, '+br+'%':'aus'),'msg-ok')}) + .catch(function(e){clog('Licht-Fehler: '+e,'msg-err')}); +} + +// ── Print Speed ── +function setSpeed(mode){ + S.print_speed_mode=mode; + [1,2,3].forEach(function(m){ + var b=document.getElementById('d-spd-'+m); + if(b) b.classList.toggle('spd-active',m===mode); + }); + post('/api/speed',{mode:mode}) + .catch(function(e){clog('Speed-Fehler: '+e,'msg-err')}); +} + +// ── Fan ── +function setFan(){ + var v=parseInt(document.getElementById('d-fan').value); + document.getElementById('d-fan-val').textContent=v; + post('/api/fan',{speed:v}) + .then(function(){clog('Lüfter → '+v+'%','msg-ok')}) + .catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')}); +} +function quickFan(v){ + document.getElementById('d-fan').value=v; + document.getElementById('d-fan-val').textContent=v; + post('/api/fan',{speed:v}) + .then(function(){clog('Lüfter → '+v+'%','msg-ok')}) + .catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')}); +} + +// ── AMS ── +function amsFeed(type,slotIndex){ + var globalIdx; + if(typeof slotIndex==='number'&&slotIndex>=0){ + globalIdx=slotIndex; + }else{ + var i=parseInt(document.getElementById('ams-slot-sel').value); + var slot=(window._amsSlots||[])[i]||{}; + globalIdx=slot.global_index!=null?slot.global_index:i; + } + return post('/api/ams/feed',{slot_index:globalIdx,type:type}) + .then(function(){clog((type===1?T.lbl_feed:T.lbl_unload)+' Slot '+(globalIdx+1),'msg-ok')}) + .catch(function(e){clog('AMS-Fehler: '+e,'msg-err');throw e;}); +} + +// ── Camera ── +function camStart(){ + var img=document.getElementById('cam-img'); + var ph=document.getElementById('cam-placeholder'); + var sp=document.getElementById('cam-spinner'); + ph.style.display='none'; + img.style.display='none'; + sp.style.display='block'; + post('/api/camera/start',{}).then(function(){ + img.onerror=function(){ + sp.style.display='none'; + img.style.display='none'; + ph.style.display='flex'; + camOn=false; + document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera'; + clog((T.log_error||'Fehler:')+' Stream nicht verfügbar','msg-err'); + }; + img.src='/api/camera/stream?t='+Date.now(); + camOn=true; + document.getElementById('cam-toggle-btn').textContent=T.btn_cam_stop||'◼ Kamera'; + clog((T.log_cam_start||'Kamera gestartet'),'msg-ok'); + // MJPEG liefert kein onload – Spinner nach kurzem Timeout ausblenden + setTimeout(function(){ + sp.style.display='none'; + img.style.display='block'; + },1200); + }).catch(function(e){ + sp.style.display='none'; + ph.style.display='flex'; + clog((T.log_error||'Fehler:')+' '+e,'msg-err'); + }); +} + +function camStop(){ + var img=document.getElementById('cam-img'); + post('/api/camera/stop',{}).catch(function(){}); + img.src=''; + img.style.display='none'; + document.getElementById('cam-placeholder').style.display='flex'; + camOn=false; + document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera'; + clog((T.log_cam_stop||'Kamera gestoppt'),'msg-ok'); +} + +function aceDryStart(aceId){ + aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; + var prof=_aceDryProfileGet(aceId); + var t=parseInt(prof.temp,10); + var d=_aceDryDurationMinFromSec(prof.duration_sec); + t=Math.max(30,Math.min(80,t)); + d=Math.max(10,Math.min(1440,d)); + return post('/api/ace/dry',{action:'start',target_temp:t,duration:d,ace_id:aceId}) + .then(function(r){return r.json();}) + .then(function(r){ + if(r.error){throw new Error(r.error);} + clog('ACE '+(aceId+1)+' - '+(T.ace_dry_dryer||'Dryer')+': '+(T.ace_dry_start||'start')+' ('+t+'°C, '+d+' min)','msg-ok'); + poll(); + }) + .catch(function(e){clog('ACE-Fehler: '+e,'msg-err');}); +} + +var _aceAutoFeedPending={}; +function aceAutoRefillToggle(aceId){ + aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; + var on=!!((document.getElementById('ace-auto-refill-toggle-'+aceId)||{}).checked); + _aceAutoFeedPending[aceId]=true; + fetch('/api/ace/auto_feed',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ace_id:aceId,on:on?1:0})}) + .then(function(r){return r.json();}) + .then(function(d){ + delete _aceAutoFeedPending[aceId]; + if(d.error){clog('Auto Refill error: '+d.error,'msg-err');var t=document.getElementById('ace-auto-refill-toggle-'+aceId);if(t)t.checked=!on;return;} + clog('ACE '+(aceId+1)+' - '+(T.ace_dry_auto_refill||'Auto Refill')+': '+(on?'ON':'OFF'),'msg-ok'); + }) + .catch(function(e){delete _aceAutoFeedPending[aceId];clog('Auto Refill error: '+e,'msg-err');var t=document.getElementById('ace-auto-refill-toggle-'+aceId);if(t)t.checked=!on;}); +} + +function openAceDryDialog(aceId){ + aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; + _aceDryDialogAceId=aceId; + _syncAceDryPresetsFromServer(S.ace_dry_presets); + _aceDryDialogPresetOriginals=JSON.parse(JSON.stringify(ACE_DRY_PRESETS)); + aceDryDialogSyncCustomButtonNames(); + var hasStored=Object.prototype.hasOwnProperty.call(aceDryProfiles,String(aceId)); + var prof=_aceDryProfileGet(aceId); + if(hasStored&&prof.preset&&ACE_DRY_PRESETS[prof.preset]){ + aceDryDialogPreset(prof.preset); + }else if(hasStored){ + var sec=prof.duration_sec; + document.getElementById('ace-dry-dialog-temp').value=prof.temp; + document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600); + document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60); + document.getElementById('ace-dry-dialog-s').value=sec%60; + aceDryDialogHighlightPreset(''); + }else{ + aceDryDialogPreset('pla'); + } + aceDryDialogUpdateSaveButton(); + aceDryDialogUpdateResetButton(); + var sb=document.getElementById('ace-dry-dialog-save-preset'); + if(sb){sb.disabled=false;sb.textContent=T.ace_dry_dialog_save_restart||'Save & Restart';} + document.getElementById('ace-dry-dialog').classList.add('open'); +} + +function closeAceDryDialog(){ + _aceDryDialogAceId=-1; + _aceDryDialogPresetOriginals={}; + var sb=document.getElementById('ace-dry-dialog-save-preset'); + if(sb)sb.style.display='none'; + var rb=document.getElementById('ace-dry-dialog-reset-default'); + if(rb)rb.style.display='none'; + document.getElementById('ace-dry-dialog').classList.remove('open'); +} + +function aceDryDialogIsCustomPreset(key){ + return /^custom_[123]$/.test(String(key||'')); +} + +function aceDryDialogSyncCustomButtonNames(){ + ['custom_1','custom_2','custom_3'].forEach(function(k){ + var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+k+'"]'); + if(b)b.textContent=(ACE_DRY_PRESETS[k]&&ACE_DRY_PRESETS[k].name)||('Custom '+k.slice(-1)); + }); +} + +function aceDryDialogUpdateCustomNameUi(){ + var row=document.getElementById('ace-dry-dialog-custom-name-row'); + var input=document.getElementById('ace-dry-dialog-custom-name'); + if(!row||!input)return; + if(!aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)){ + row.style.display='none'; + return; + } + row.style.display='flex'; + input.value=(ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||''; +} + +function aceDryDialogCurrentValues(){ + var t=parseInt(document.getElementById('ace-dry-dialog-temp').value||45,10); + var h=parseInt(document.getElementById('ace-dry-dialog-h').value||0,10); + var m=parseInt(document.getElementById('ace-dry-dialog-m').value||0,10); + var s=parseInt(document.getElementById('ace-dry-dialog-s').value||0,10); + t=Math.max(30,Math.min(80,t)); + h=Math.max(0,Math.min(24,h)); + m=Math.max(0,Math.min(59,m)); + s=Math.max(0,Math.min(59,s)); + var totalSec=(h*3600)+(m*60)+s; + totalSec=Math.max(10*60,Math.min(24*3600,totalSec)); + return {temp:t,duration_sec:totalSec}; +} + +function aceDryDialogUpdateSaveButton(){ + var btn=document.getElementById('ace-dry-dialog-save-preset'); + if(!btn)return; + var key=_aceDryDialogPresetKey||''; + if(!key||!ACE_DRY_PRESETS[key]){btn.style.display='none';return;} + var p=_aceDryDialogPresetOriginals[key]||ACE_DRY_PRESETS[key]; + var cur=aceDryDialogCurrentValues(); + var changed=(cur.temp!==Number(p.temp)||cur.duration_sec!==Number(p.duration_sec)); + if(aceDryDialogIsCustomPreset(key)){ + var nameInp=document.getElementById('ace-dry-dialog-custom-name'); + var n=((nameInp&&nameInp.value)||'').trim(); + var old=(p&&p.name?String(p.name):('Custom '+key.slice(-1))).trim(); + if((n||old)!==old)changed=true; + } + btn.style.display=changed?'':'none'; +} + +function aceDryDialogUpdateResetButton(){ + var btn=document.getElementById('ace-dry-dialog-reset-default'); + if(!btn)return; + var key=_aceDryDialogPresetKey||''; + var d=ACE_DRY_PRESET_DEFAULTS[key]; + if(!key||!d){btn.style.display='none';return;} + var cur=aceDryDialogCurrentValues(); + var changed=(cur.temp!==Number(d.temp)||cur.duration_sec!==Number(d.duration_sec)); + btn.style.display=changed?'':'none'; +} + +function aceDryDialogInputsChanged(){ + if(aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)){ + var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+_aceDryDialogPresetKey+'"]'); + var i=document.getElementById('ace-dry-dialog-custom-name'); + if(b&&i){ + var t=(i.value||'').trim(); + b.textContent=t||((ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||('Custom '+_aceDryDialogPresetKey.slice(-1))); + } + } + aceDryDialogUpdateSaveButton(); + aceDryDialogUpdateResetButton(); +} + +function aceDryDialogHighlightPreset(presetKey){ + _aceDryDialogPresetKey=presetKey||''; + document.querySelectorAll('.ace-dry-preset-btn').forEach(function(btn){ + var on=(btn.getAttribute('data-preset')===presetKey); + btn.style.background=on?'var(--accent)':'var(--raised)'; + btn.style.color=on?'#fff':'var(--txt2)'; + btn.style.borderColor=on?'var(--accent)':'var(--border)'; + }); + aceDryDialogUpdateCustomNameUi(); +} + +function aceDryDialogPreset(presetKey){ + var p=ACE_DRY_PRESETS[presetKey]; + if(!p)return; + var sec=p.duration_sec; + document.getElementById('ace-dry-dialog-temp').value=p.temp; + document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600); + document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60); + document.getElementById('ace-dry-dialog-s').value=sec%60; + aceDryDialogHighlightPreset(presetKey); + aceDryDialogSyncCustomButtonNames(); + aceDryDialogUpdateSaveButton(); + aceDryDialogUpdateResetButton(); +} + +function resetAceDryPresetToDefault(){ + var key=_aceDryDialogPresetKey||''; + var d=ACE_DRY_PRESET_DEFAULTS[key]; + if(!key||!d)return; + var sec=Number(d.duration_sec)||0; + document.getElementById('ace-dry-dialog-temp').value=Number(d.temp)||45; + document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600); + document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60); + document.getElementById('ace-dry-dialog-s').value=sec%60; + aceDryDialogInputsChanged(); +} + +function saveAceDryPresetAndRestart(){ + var key=_aceDryDialogPresetKey||''; + var btn=document.getElementById('ace-dry-dialog-save-preset'); + if(!key||!ACE_DRY_PRESETS[key]||!btn)return; + var cur=aceDryDialogCurrentValues(); + if(!ACE_DRY_PRESETS[key])ACE_DRY_PRESETS[key]={}; + ACE_DRY_PRESETS[key].temp=cur.temp; + ACE_DRY_PRESETS[key].duration_sec=cur.duration_sec; + if(aceDryDialogIsCustomPreset(key)){ + var nameInp=document.getElementById('ace-dry-dialog-custom-name'); + var nm=((nameInp&&nameInp.value)||'').trim(); + ACE_DRY_PRESETS[key].name=nm||('Custom '+key.slice(-1)); + } + btn.disabled=true; + btn.textContent='…'; + fetch(_apiUrl('/api/settings')).then(function(r){return r.json();}).then(function(d){ + d.ace_dry_presets={ + pla:{temp:ACE_DRY_PRESETS.pla.temp,duration_sec:ACE_DRY_PRESETS.pla.duration_sec}, + pla_plus:{temp:ACE_DRY_PRESETS.pla_plus.temp,duration_sec:ACE_DRY_PRESETS.pla_plus.duration_sec}, + petg:{temp:ACE_DRY_PRESETS.petg.temp,duration_sec:ACE_DRY_PRESETS.petg.duration_sec}, + tpu:{temp:ACE_DRY_PRESETS.tpu.temp,duration_sec:ACE_DRY_PRESETS.tpu.duration_sec}, + abs_asa:{temp:ACE_DRY_PRESETS.abs_asa.temp,duration_sec:ACE_DRY_PRESETS.abs_asa.duration_sec}, + pa_pc:{temp:ACE_DRY_PRESETS.pa_pc.temp,duration_sec:ACE_DRY_PRESETS.pa_pc.duration_sec}, + custom_1:{name:ACE_DRY_PRESETS.custom_1.name,temp:ACE_DRY_PRESETS.custom_1.temp,duration_sec:ACE_DRY_PRESETS.custom_1.duration_sec}, + custom_2:{name:ACE_DRY_PRESETS.custom_2.name,temp:ACE_DRY_PRESETS.custom_2.temp,duration_sec:ACE_DRY_PRESETS.custom_2.duration_sec}, + custom_3:{name:ACE_DRY_PRESETS.custom_3.name,temp:ACE_DRY_PRESETS.custom_3.temp,duration_sec:ACE_DRY_PRESETS.custom_3.duration_sec} + }; + return post('/api/settings',d); + }).then(function(){ + clog('ACE preset '+key+' '+(T.settings_save||'Save & Restart'),'msg-ok'); + closeAceDryDialog(); + }).catch(function(e){ + btn.disabled=false; + btn.textContent=T.ace_dry_dialog_save_restart||'Save & Restart'; + clog('ACE-Preset Fehler: '+e,'msg-err'); + }); +} + +function confirmAceDryDialog(){ + if(_aceDryDialogAceId<0)return; + var t=parseInt(document.getElementById('ace-dry-dialog-temp').value||45,10); + var h=parseInt(document.getElementById('ace-dry-dialog-h').value||0,10); + var m=parseInt(document.getElementById('ace-dry-dialog-m').value||0,10); + var s=parseInt(document.getElementById('ace-dry-dialog-s').value||0,10); + t=Math.max(30,Math.min(80,t)); + h=Math.max(0,Math.min(24,h)); + m=Math.max(0,Math.min(59,m)); + s=Math.max(0,Math.min(59,s)); + var totalSec=(h*3600)+(m*60)+s; + totalSec=Math.max(10*60,Math.min(24*3600,totalSec)); + var preset=_aceDryDialogPresetKey||''; + _aceDryProfileSet(_aceDryDialogAceId,t,totalSec,preset); + closeAceDryDialog(); + applyState(); +} + +function aceDryToggle(aceId,on){ + if(on)return aceDryStart(aceId); + return aceDryStop(aceId); +} + +function toggleCam(){if(camOn)camStop();else camStart()} + +function aceDryStop(aceId){ + aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; + return post('/api/ace/dry',{action:'stop',ace_id:aceId}) + .then(function(r){return r.json();}) + .then(function(r){ + if(r.error){throw new Error(r.error);} + clog('ACE '+(aceId+1)+' - '+(T.ace_dry_dryer||'Dryer')+': '+(T.ace_dry_stop||'stop'),'msg-ok'); + poll(); + }) + .catch(function(e){clog('ACE-Fehler: '+e,'msg-err');}); +} + +function loadStore(){ + fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){ + storeFiles=d.result||[]; + renderStore(); + }).catch(function(e){clog('Store-Fehler: '+e,'msg-err')}); +} + +function renderStore(){ + var grid=document.getElementById('store-grid'); + var empty=document.getElementById('store-empty'); + + // Suche + var q=(document.getElementById('store-search')||{value:''}).value.toLowerCase().trim(); + // Filter + var filter=(document.getElementById('store-filter')||{value:'all'}).value; + // Sortierung + var sort=(document.getElementById('store-sort')||{value:'date_desc'}).value; + + var files=storeFiles.filter(function(f){ + if(q&&f.filename.toLowerCase().indexOf(q)===-1) return false; + if(filter==='completed'&&f.last_print_status!=='completed') return false; + if(filter==='failed'&&(f.last_print_status!=='cancelled'&&f.last_print_status!=='failed')) return false; + if(filter==='never'&&f.last_print_status) return false; + return true; + }); + + files.sort(function(a,b){ + if(sort==='name_asc') return a.filename.localeCompare(b.filename); + if(sort==='duration_asc'){ + var da=a.last_print_duration||a.est_print_time_sec||0; + var db=b.last_print_duration||b.est_print_time_sec||0; + return da-db; + } + // date_desc (default) + return (b.uploaded_at||'').localeCompare(a.uploaded_at||''); + }); + + if(!storeFiles.length){ + empty.textContent=T.store_empty; + grid.innerHTML=''; + empty.style.display='block'; + return; + } + if(!files.length){ + empty.textContent=T.store_no_results; + grid.innerHTML=''; + empty.style.display='block'; + return; + } + empty.style.display='none'; + grid.innerHTML=files.map(function(f){ + var thumb=f.thumbnail_b64 + ? '' + : '
🖨
'; + 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=''; + if(f.last_print_duration) lastInfo='
✓ '+formatDur(f.last_print_duration)+'
'; + } else if(f.last_print_status==='cancelled'||f.last_print_status==='failed'){ + statusBadge=''; + lastInfo='
✗ '+f.last_print_status+'
'; + } else if(!f.last_print_status){ + lastInfo='
'+T.store_never+'
'; + } + return '
'+ + thumb+ + '
'+name+statusBadge+'
'+ + lastInfo+ + '
⏱ Schätzung: '+est+'
'+ + '
📅 '+date+'
'+ + '
'+ + ''+ + ''+ + '
'+ + '
'; + }).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 _setGcodeFilamentsFromFileObj(fileObj){ + try{ + if(fileObj&&Array.isArray(fileObj.gcode_filaments)){ + _gcodeFilaments=fileObj.gcode_filaments; + }else if(fileObj&&typeof fileObj.gcode_filaments==='string'&&fileObj.gcode_filaments){ + _gcodeFilaments=JSON.parse(fileObj.gcode_filaments); + }else{ + _gcodeFilaments=[]; + } + }catch(e){ + _gcodeFilaments=[]; + } +} + +function storePrint(fileId, filename){ + _storeFileId=fileId; + _storeFilename=filename; + _filamentDialogMode='store'; + // GCode-Filamente aus Store-Datei holen (für Vorschau im Dialog) + var fileObj=storeFiles.find(function(f){return f.id===fileId;}); + _setGcodeFilamentsFromFileObj(fileObj); + fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ + openFilamentDialog(d.result||[]); + }).catch(function(){openFilamentDialog([]);}); +} + +function startReadyFileWithSlots(){ + _filamentDialogMode='banner'; + _storeFilename=S.file_ready||''; + // Banner must never reuse stale store-file context. + _storeFileId=null; + _gcodeFilaments=[]; + + function openWithSlots(){ + fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ + openFilamentDialog(d.result||[]); + }).catch(function(){openFilamentDialog([]);}); + } + + var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;}); + if(fileObj){ + _storeFileId=fileObj.id; + _setGcodeFilamentsFromFileObj(fileObj); + openWithSlots(); + return; + } + + // Fallback: refresh file list, then resolve current file by filename. + fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){ + storeFiles=d.result||[]; + var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;}); + if(refreshed){ + _storeFileId=refreshed.id; + _setGcodeFilamentsFromFileObj(refreshed); + } + openWithSlots(); + }).catch(function(){ + openWithSlots(); + }); +} + +var _amsSlots=[]; +var _printObjects=[]; // [{name, skip}] für aktuell offenen Dialog +var _printObjectsSvg=''; // base64-SVG aus DB für Visualisierung + +// Hilfsfunktionen für farbige Kanal/Slot-Marker (Issue #23) +function _contrastText(hex){ + // Helle Farbe → dunkler Text, dunkle Farbe → heller Text + var c=(hex||'').replace('#',''); + if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]; + if(c.length<6)return '#fff'; + var r=parseInt(c.slice(0,2),16),g=parseInt(c.slice(2,4),16),b=parseInt(c.slice(4,6),16); + // YIQ-Helligkeit + var y=(r*299 + g*587 + b*114)/1000; + return y>=140?'#111':'#fff'; +} +function _normalizeMaterialKey(material){ + var key=(material||'').toUpperCase().replace(/[^A-Z0-9+]/g,''); + // Orca often uses PLA for PLA+, while AMS may report PLA+. + if(key==='PLA+'||key==='PLAPLUS') return 'PLA'; + return key; +} +function _materialsCompatible(a,b){ + return _normalizeMaterialKey(a)===_normalizeMaterialKey(b); +} +function _updateSlotMarker(sel){ + var opt=sel.options[sel.selectedIndex]; + var color=opt&&opt.dataset.color?opt.dataset.color:'#888'; + var slotIdx=parseInt(opt.value); + var paintIdx=sel.dataset.paint; + var marker=document.querySelector('.fd-slot-marker[data-for-paint="'+paintIdx+'"]'); + if(marker){ + marker.style.background=color; + marker.style.color=_contrastText(color); + marker.textContent=(slotIdx+1); + } +} + +function openFilamentDialog(slots){ + _amsSlots=slots + .filter(function(s){return s.status==='loaded';}) + .sort(function(a,b){return (a.slot_index||0)-(b.slot_index||0);}); + var dlg=document.getElementById('filament-dialog'); + var title=document.getElementById('fd-title'); + var body=document.getElementById('fd-slots'); + if(title)title.textContent='▶ '+_storeFilename; + // Objekt-Liste laden (nur Store-Modus: per File-ID; Banner-Modus hat keine ID) + _printObjects=[]; + _printObjectsSvg=''; + var objSection=document.getElementById('fd-objects-section'); + if(objSection)objSection.style.display='none'; + if(_filamentDialogMode==='store'&&_storeFileId){ + fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects')) + .then(function(r){return r.json()}) + .then(function(d){ + var names=(d.result&&d.result.names)||[]; + _printObjectsSvg=(d.result&&d.result.svg_b64)||''; + if(names.length>=2){ + _printObjects=names.map(function(n){return {name:n,skip:false};}); + renderObjectChecklist(); renderObjectSvg(); + if(objSection)objSection.style.display='block'; + } + }).catch(function(){}); + } + + // GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten + var channels=_gcodeFilaments.length?_gcodeFilaments:_amsSlots.map(function(s,i){ + return {slot_index:i,color_hex:s.color_hex,material:s.material}; + }); + + // Default mapping strategy: + // 1) keep order where possible (row i -> nearest compatible slot i) + // 2) keep defaults unique while compatible slots are available + // 3) use color proximity as tie-breaker + function _hexToRgb(hex){ + var c=(hex||'').replace('#',''); + if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]; + if(c.length<6)return [255,255,255]; + return [parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16)]; + } + function _colorDist(a,b){ + var ar=_hexToRgb(a), br=_hexToRgb(b); + var dr=ar[0]-br[0], dg=ar[1]-br[1], db=ar[2]-br[2]; + return (dr*dr + dg*dg + db*db); + } + var defaultSlotByPaint={}; + var usedDefaultSlot={}; + channels.forEach(function(gc,i){ + var compatible=_amsSlots.filter(function(s){ + return _materialsCompatible(gc.material, s.material); + }); + if(!compatible.length){ + defaultSlotByPaint[i]=-1; + return; + } + + var ranked=compatible.slice().sort(function(a,b){ + var da=Math.abs((a.slot_index||0)-i), db=Math.abs((b.slot_index||0)-i); + if(da!==db)return da-db; + var ca=_colorDist(gc.color_hex, a.color_hex), cb=_colorDist(gc.color_hex, b.color_hex); + if(ca!==cb)return ca-cb; + return (a.slot_index||0)-(b.slot_index||0); + }); + + var chosen=ranked.find(function(s){return !usedDefaultSlot[s.slot_index];}) || ranked[0]; + defaultSlotByPaint[i]=chosen?chosen.slot_index:-1; + if(chosen) usedDefaultSlot[chosen.slot_index]=1; + }); + + if(!_amsSlots.length){ + body.innerHTML='

Keine belegten AMS-Slots.
Druck trotzdem starten?

'; + } else { + body.innerHTML=channels.map(function(gc,i){ + var isUsed=(gc&&gc.is_used!==false); + // Only allow material-compatible slots. + var compatible=_amsSlots.filter(function(s){ + return _materialsCompatible(gc.material, s.material); + }); + + var defaultSlotIndex=(defaultSlotByPaint.hasOwnProperty(i)?defaultSlotByPaint[i]:-1); + var defaultSlot=compatible.find(function(s){return s.slot_index===defaultSlotIndex;})||null; + var opts=compatible.map(function(s){ + var sel=(defaultSlot&&s.slot_index===defaultSlot.slot_index)?'selected':''; + return ''; + }).join(''); + if(!compatible.length){ + opts=''; + } + // Kanal-Box (links): farbige Box mit Nummer + auto Kontrast-Text + var txt=_contrastText(gc.color_hex); + var slotColor=defaultSlot?defaultSlot.color_hex:'#888'; + var slotTxt=_contrastText(slotColor); + var usedBadge=isUsed + ? 'USED' + : 'USED'; + return '
'+ + ''+(i+1)+''+ + ''+gc.material+''+ + usedBadge+ + ''+ + ''+(defaultSlot?defaultSlot.slot_index+1:'?')+''+ + ''+ + '
'; + }).join(''); + } + if(dlg)dlg.classList.add('open'); +} + +function closeFilamentDialog(){ + var dlg=document.getElementById('filament-dialog'); + if(dlg)dlg.classList.remove('open'); +} + +function confirmFilamentPrint(){ + var selects=document.querySelectorAll('#fd-slots select'); + var assignments=[]; + var missingCompatible=0; + selects.forEach(function(sel){ + var paintIdx=parseInt(sel.dataset.paint); + var paintColor=sel.dataset.paintColor; + var isUsed=(sel.dataset.isUsed==='1'); + var hasCompatible=(sel.dataset.hasCompatible==='1'); + var opt=sel.options[sel.selectedIndex]; + var amsIdx=parseInt(opt&&opt.value); + if(!hasCompatible || Number.isNaN(amsIdx) || amsIdx < 0){ + if(isUsed) missingCompatible += 1; + amsIdx = -1; + } + var amsSlot=_amsSlots.find(function(s){return s.slot_index===amsIdx;})||{}; + // Farbe als [R,G,B,255] + function hexToRgba(h){ + var c=h.replace('#',''); + if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]; + return [parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16),255]; + } + assignments.push({ + paint_index: paintIdx, + is_used: isUsed, + slot_index: amsIdx, + material: opt.dataset.material||'PLA', + paint_color: hexToRgba(paintColor||'#ffffff'), + ams_color: hexToRgba(amsSlot.color_hex||'#ffffff'), + }); + }); + if(missingCompatible>0){ + clog('Cannot start print: '+missingCompatible+' used paint(s) have no matching material slot','msg-err'); + return; + } + // Pre-Print Skip: Namen der abgehakten Objekte sammeln + var excludedObjects=_printObjects.filter(function(o){return o.skip;}).map(function(o){return o.name;}); + closeFilamentDialog(); + if(_filamentDialogMode==='banner'){ + // Banner-Modus: normaler print/start mit Slot-Override + var btn=document.getElementById('file-ready-btn'); + if(btn){btn.disabled=true;btn.textContent='…';} + post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects}) + .then(function(r){return r.json();}) + .then(function(){ + document.getElementById('file-ready-banner').style.display='none'; + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }) + .catch(function(e){ + clog((T.log_error||'Error:')+' '+e,'msg-err'); + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }); + } else { + // Store-Modus: POST /kx/print + fetch(_apiUrl('/kx/print'),{ + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects}) + }).then(function(r){return r.json()}).then(function(d){ + if(d.result==='ok'){clog('Druckstart: '+_storeFilename,'msg-ok');showPanel('dashboard');} + else{clog('Druckfehler: '+(d.error||'?'),'msg-err');} + }).catch(function(e){clog('Druckfehler: '+e,'msg-err');}); + } +} + +function renderObjectChecklist(){ + var box=document.getElementById('fd-objects'); + if(!box)return; + box.innerHTML=_printObjects.map(function(o,i){ + var label=o.name; + // Klipper-Namen sind oft "Datei.stl_id_N_copy_M" → schöner darstellen + var m=label.match(/^(.+)\.stl_id_(\d+)_copy_(\d+)$/); + if(m)label=m[1]+' #'+(parseInt(m[2])+1)+(m[3]!=='0'?' ('+(parseInt(m[3])+1)+')':''); + return ''; + }).join(''); +} +function _toggleObjectSkip(idx,val){ + if(_printObjects[idx])_printObjects[idx].skip=!!val; + renderObjectSvg(); +} +function renderObjectSvg(){ + var box=document.getElementById('fd-objects-svg'); + if(!box)return; + if(!_printObjectsSvg||!_printObjects.length){box.style.display='none';box.innerHTML='';return;} + box.style.display='block'; + var svg=''; try{ svg=atob(_printObjectsSvg);}catch(e){ box.style.display='none'; return; } + box.innerHTML=svg; + var svgEl=box.querySelector('svg'); + if(!svgEl)return; + svgEl.style.width='100%'; svgEl.style.maxHeight='200px'; svgEl.style.height='auto'; + _printObjects.forEach(function(o,i){ + var g=svgEl.querySelector('g[id="'+CSS.escape(o.name)+'"]'); + if(!g)return; + var path=g.querySelector('path'); + g.style.cursor='pointer'; + g.setAttribute('opacity', o.skip?'0.8':'0.35'); + if(path){ + path.setAttribute('fill', o.skip?'#ff5e5b':'#5fa7ff'); + path.setAttribute('fill-opacity', o.skip?'0.4':'0.18'); + } + g.onclick=function(){ + _printObjects[i].skip=!_printObjects[i].skip; + renderObjectChecklist(); renderObjectSvg(); + }; + }); +} + +// ── Mid-Print Skip ── +var _skipObjects=[]; // [{name, skipped, willSkip}] +var _skipSvg=''; +function openSkipDialog(){ + document.getElementById('skip-status').textContent=''; + document.getElementById('skip-confirm').disabled=false; + _refreshSkipDialog(); + document.getElementById('skip-dialog').classList.add('open'); +} +function _refreshSkipDialog(){ + // Erst aktueller State (mit DB-Objects + svg), dann query_obj für frischen skipped + fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ + var s=d.result||{}; + _skipSvg=s.svg_b64||''; + _skipObjects=(s.objects||[]).map(function(n){ + return {name:n, skipped:(s.skipped||[]).indexOf(n)>=0, willSkip:false}; + }); + renderSkipList(); renderSkipSvg(); + }); + // Frisch nachfragen (skipped-Liste aktualisieren) + fetch(_apiUrl('/kx/skip/query'),{method:'POST'}).then(function(r){return r.json()}).then(function(){ + setTimeout(function(){ + fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ + var s=d.result||{}; + var skipped=s.skipped||[]; + _skipObjects.forEach(function(o){ o.skipped=skipped.indexOf(o.name)>=0; if(o.skipped)o.willSkip=false; }); + renderSkipList(); renderSkipSvg(); + }); + }, 500); + }).catch(function(){}); +} +function closeSkipDialog(){ + document.getElementById('skip-dialog').classList.remove('open'); +} +function _shortLabel(name){ + var m=name.match(/^(.+)\.[sS][tT][lL]_id_(\d+)_copy_(\d+)$/); + if(!m)return name; + return m[1]+' #'+(parseInt(m[2])+1)+(m[3]!=='0'?' ('+(parseInt(m[3])+1)+')':''); +} +function renderSkipList(){ + var box=document.getElementById('skip-list'); + if(!box)return; + if(!_skipObjects.length){ + box.innerHTML='
'+(T.skip_no_objects||'Keine Objekte in diesem Druck.')+'
'; + return; + } + box.innerHTML=_skipObjects.map(function(o,i){ + var label=_shortLabel(o.name); + var dis=o.skipped?'disabled':''; + var note=o.skipped?''+(T.skip_already||'übersprungen')+'':''; + return ''; + }).join(''); +} +function renderSkipSvg(){ + var box=document.getElementById('skip-svg'); + if(!box)return; + if(!_skipSvg||!_skipObjects.length){box.style.display='none';box.innerHTML='';return;} + box.style.display='block'; + // SVG aus base64 dekodieren + var svg=''; + try{ svg=atob(_skipSvg); }catch(e){ box.style.display='none'; return; } + box.innerHTML=svg; + // Polygone interaktiv machen: jeder entspricht einem Objekt + var svgEl=box.querySelector('svg'); + if(!svgEl)return; + svgEl.style.width='100%'; svgEl.style.maxHeight='280px'; svgEl.style.height='auto'; + _skipObjects.forEach(function(o,i){ + var g=svgEl.querySelector('g[id="'+CSS.escape(o.name)+'"]'); + if(!g)return; + var path=g.querySelector('path'); + if(o.skipped){ + // bereits übersprungen → ausgegraut, kein Klick + g.setAttribute('opacity','0.25'); + if(path){path.setAttribute('fill','#888');path.setAttribute('fill-opacity','0.3');} + g.style.cursor='not-allowed'; + } else { + g.style.cursor='pointer'; + g.setAttribute('opacity', o.willSkip?'0.8':'0.35'); + if(path){ + path.setAttribute('fill', o.willSkip?'#ff5e5b':'#5fa7ff'); + path.setAttribute('fill-opacity', o.willSkip?'0.4':'0.18'); + } + g.onclick=function(){ + _skipObjects[i].willSkip=!_skipObjects[i].willSkip; + renderSkipList(); renderSkipSvg(); + }; + } + }); +} +function _toggleWillSkip(idx,val){ + if(_skipObjects[idx])_skipObjects[idx].willSkip=!!val; + renderSkipSvg(); +} +function confirmSkip(){ + var names=_skipObjects.filter(function(o){return o.willSkip;}).map(function(o){return o.name;}); + var st=document.getElementById('skip-status'); + var btn=document.getElementById('skip-confirm'); + if(!names.length){st.textContent=T.skip_select_at_least_one||'Bitte mindestens ein Objekt wählen.';st.style.color='var(--warn)';return;} + btn.disabled=true; st.textContent=T.skip_sending||'Sende …'; st.style.color='var(--txt2)'; + fetch(_apiUrl('/kx/skip'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({names:names})}) + .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) + .then(function(res){ + if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;} + st.textContent=T.skip_success||'Objekte werden übersprungen.';st.style.color='var(--ok)'; + // Dialog offen lassen + neu laden damit der "übersprungen"-Status erscheint + setTimeout(function(){ _refreshSkipDialog(); btn.disabled=false; st.textContent=''; }, 1500); + }) + .catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;}); +} + +function storeDelete(fileId){ + if(!confirm(T.store_delete_confirm)) return; + fetch(_apiUrl('/kx/files/'+fileId),{method:'DELETE'}).then(function(r){ + if(r.ok){loadStore();} + else{clog('Löschen fehlgeschlagen','msg-err');} + }); +} + +// ── Drucker hinzufügen ── +function openAddPrinterDialog(){ + document.getElementById('apd-ip').value=''; + document.getElementById('apd-name').value=''; + var st=document.getElementById('apd-status');st.textContent='';st.style.color='var(--txt2)'; + document.getElementById('apd-confirm').disabled=false; + document.getElementById('add-printer-dialog').classList.add('open'); +} +function closeAddPrinterDialog(){ + document.getElementById('add-printer-dialog').classList.remove('open'); +} +function confirmAddPrinter(){ + var ip=document.getElementById('apd-ip').value.trim(); + var name=document.getElementById('apd-name').value.trim(); + var st=document.getElementById('apd-status'),btn=document.getElementById('apd-confirm'); + if(!ip){st.textContent=T.apd_err_ip;st.style.color='var(--err)';return;} + st.textContent=T.apd_fetching;st.style.color='var(--txt2)';btn.disabled=true; + fetch('/kx/printers/add',{method:'POST',headers:{'Content-Type':'application/json'}, + body:JSON.stringify({printer_ip:ip,name:name})}) + .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) + .then(function(res){ + if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;} + st.textContent=T.apd_success;st.style.color='var(--ok)'; + setTimeout(function(){location.reload();},2500); + }) + .catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;}); +} +function removePrinter(id,name){ + if(!confirm(T.printers_remove_confirm.replace('{name}',name)))return; + fetch('/kx/printers/'+encodeURIComponent(id),{method:'DELETE'}) + .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) + .then(function(res){ + if(!res.ok){alert((res.j&&res.j.error)||'Fehler');return;} + setTimeout(function(){location.href='/printer1';},2000); + }) + .catch(function(e){alert(''+e);}); +} + +// ── Drucker-Tab ── +function loadPrinterTab(){ + var grid=document.getElementById('printers-grid'); + if(grid)grid.innerHTML='
'+T.printers_loading+'
'; + // 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='
'+ + '
🖨
'+ + '
'+T.printers_empty_hint+'
'+ + ''+ + '
'; + 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 '
'+ + '
'+ + '🖨 '+p.name+''+ + ''+ + (isActive?''+T.printers_active+'':'')+ + ''+ + ''+ + '
'+ + '
'+ + ''+ + ''+stateLabel+''+ + '
'+ + (p.printer_ip?'
🌐 '+p.printer_ip+'
':'')+ + (filename?'
📄 '+filename+'
':'')+ + (progress!==null?'
':'')+ + '
'+ + '🌡 '+nt+'°C🛏 '+bt+'°C'+ + '
'+ + (!isActive?''+T.printers_switch+'':'
'+T.printers_current+'
')+ + '
'; + }).join(''); + }); + }).catch(function(e){ + if(grid)grid.innerHTML='
Fehler: '+e+'
'; + }); +} diff --git a/web/themes/default/index.html b/web/themes/default/index.html new file mode 100644 index 0000000..f0b5c6d --- /dev/null +++ b/web/themes/default/index.html @@ -0,0 +1,560 @@ + + + + + +KX-Bridge + + + + + + +
+ +
+ +
Anycubic Kobra X
+ +
Standby
+ + + + +
+ + + + + + + +
+ + +
+ +
+
+ +
+
+
📷 Kamera
+
+ 💡 Licht + +
+
+
+
📷 Kamera nicht gestartet
+
+ + + +
+
+ + +
+
Fortschritt
+ +
0%
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + + +
+
+ + +
+
Temperaturen
+
+
+
Nozzle
+
+
+
°C
+
+
0°C
+
+
+
+
+ + + +
+
+
+
Bett
+
+
+
°C
+
+
0°C
+
+
+
+
+ + + +
+
+
+
+
Verlauf (letzte 60 Messungen)
+ +
+
+ + +
+
XY-Achsen
+
+
+ +
+ + + +
+ +
+
+
+ + + + +
+
+ + + + +
+
+
+
Z-Achse
+
+ + +
+
Schrittweite: 1 mm
+
+ + +
+
🏎 Druckgeschwindigkeit
+
+ + + +
+
+
+
+
+ + +
+
🌀 Lüfter
+
+ + 0 +
+
+ + + + + +
+
+ + + + +
+
Filament
+
+
Keine AMS-Daten empfangen
+
+
+
+
+ + + +
+
+
+ 🖨 Drucker +
+ + +
+
+
+
+
+ + +
+
+
+ 🗂 Datei-Browser + +
+
+ + + +
+ +
+
+
+ +
+
+
+ Ereignis-Log + ⬇ Download +
+
+ + + +
+
+ Dir: + + + + Topic: + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + diff --git a/web/themes/default/style.css b/web/themes/default/style.css new file mode 100644 index 0000000..faea4a5 --- /dev/null +++ b/web/themes/default/style.css @@ -0,0 +1,293 @@ +:root{ + --bg:#1a1a1f;--card:#24242c;--raised:#2e2e3a;--border:#3a3a4a; + --txt:#f0f0f5;--txt2:#8888aa;--accent:#00c8ff;--accent2:#ff6b35; + --ok:#4cde80;--err:#ff4d6d;--warn:#ffb020; + --font:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; + --mono:"JetBrains Mono","Fira Code",monospace; +} +[data-theme=light]{ + --bg:#f0f0f5;--card:#fff;--raised:#e8e8f0;--border:#d0d0e0; + --txt:#1a1a2e;--txt2:#666680; +} +*{box-sizing:border-box;margin:0;padding:0} +body{background:var(--bg);color:var(--txt);font-family:var(--font);font-size:14px;min-height:100vh;display:flex;flex-direction:column} +a{color:var(--accent);text-decoration:none} + +/* ── HEADER ── */ +header{background:var(--card);border-bottom:1px solid var(--border); + display:flex;align-items:center;gap:12px;padding:0 20px;height:52px; + position:sticky;top:0;z-index:100} +.logo{font-size:18px;font-weight:700;color:var(--accent);letter-spacing:-.02em} +.hname{font-size:13px;color:var(--txt2)} +.hbadge{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600; + padding:4px 10px;border-radius:20px;background:var(--raised);color:var(--txt2); + text-transform:uppercase;letter-spacing:.04em} +.hbadge.printing{background:#0d2d1a;color:var(--ok)} +.hbadge.complete{background:#0d1f38;color:#60b0ff} +.hbadge.error{background:#2d0d0d;color:var(--err)} +.hbadge .dot{width:7px;height:7px;border-radius:50%;background:currentColor} +.hbadge.printing .dot{animation:pulse 1.4s infinite} +@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}} +.theme-btn{background:none;border:1px solid var(--border);color:var(--txt2); + border-radius:8px;padding:6px 10px;cursor:pointer;font-size:13px;transition:.15s} +.theme-btn:hover{border-color:var(--accent);color:var(--accent)} +.conn-btn{border-radius:8px;padding:6px 12px;cursor:pointer;font-size:13px; + font-weight:600;border:none;transition:.15s} +.conn-btn.disconnected{background:var(--accent);color:#fff} +.conn-btn.disconnected:hover{opacity:.85} +.conn-btn.connected{background:transparent;border:1px solid var(--border);color:var(--txt2)} +.conn-btn.connected:hover{border-color:#e05;color:#e05} + +/* ── LAYOUT ── */ +.layout{display:flex;flex:1;min-height:0} +nav.sidebar{width:200px;background:var(--card);border-right:1px solid var(--border); + display:flex;flex-direction:column;padding:12px 8px;gap:2px;flex-shrink:0} +.nav-btn{background:none;border:none;color:var(--txt2);text-align:left; + padding:9px 12px;border-radius:8px;cursor:pointer;font-size:13px; + display:flex;align-items:center;gap:10px;transition:.12s;width:100%} +.nav-btn:hover{background:var(--raised);color:var(--txt)} +.nav-btn.active{background:var(--raised);color:var(--accent)} +.nav-icon{font-size:16px;width:20px;text-align:center} +main{flex:1;overflow-y:auto;padding:20px} +.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px} + +/* ── CARD ── */ +.card{background:var(--card);border:1px solid var(--border);border-radius:12px; + padding:18px;transition:box-shadow .15s,transform .15s} +.card:hover{box-shadow:0 4px 20px rgba(0,0,0,.3);transform:translateY(-1px)} +.card-title{font-size:11px;text-transform:uppercase;letter-spacing:.1em;color:var(--txt2); + margin-bottom:14px;display:flex;align-items:center;gap:8px} +.card-title span{font-size:14px} + +/* ── HERO ── */ +.hero{grid-column:1/-1;display:grid;grid-template-columns:1fr 320px;gap:16px} +@media(max-width:900px){.hero{grid-template-columns:1fr}} +.cam-wrap{background:#0a0a0e;border-radius:10px;overflow:hidden; + min-height:180px;max-height:320px;display:flex;align-items:center;justify-content:center;position:relative} +.cam-wrap img,.cam-wrap video{width:100%;max-height:320px;height:auto;display:block;object-fit:contain} +.cam-placeholder{color:var(--txt2);font-size:13px;text-align:center;padding:20px} +@keyframes spin{to{transform:rotate(360deg)}} +.cam-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.15); + border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;display:none} +.cam-overlay{position:absolute;bottom:0;left:0;right:0; + background:linear-gradient(transparent,rgba(0,0,0,.75));padding:14px} +.cam-toggle{position:absolute;top:10px;right:10px;background:rgba(0,0,0,.5); + border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:8px; + padding:6px 10px;cursor:pointer;font-size:12px;backdrop-filter:blur(4px)} +.cam-toggle:hover{background:rgba(0,0,0,.7)} + +/* ── PROGRESS ── */ +.hero-info{display:flex;flex-direction:column;gap:12px} +.pct-big{font-size:52px;font-weight:700;line-height:1;color:var(--txt)} +.pct-big small{font-size:20px;font-weight:400;color:var(--txt2)} +.progress-bar{height:8px;background:var(--raised);border-radius:4px;overflow:hidden;margin:4px 0} +.progress-fill{height:100%;background:linear-gradient(90deg,var(--accent),#0080cc); + border-radius:4px;transition:width .6s ease} +.meta-row{display:flex;justify-content:space-between;font-size:12px;color:var(--txt2)} +.layer-badge{background:var(--raised);border-radius:6px;padding:4px 8px; + font-family:var(--mono);font-size:12px;color:var(--txt)} +.fname{font-size:12px;color:var(--txt2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis; + background:var(--raised);border-radius:6px;padding:6px 8px} + +/* ── PRINT CONTROLS ── */ +.ctrl-btns{display:flex;gap:8px;flex-wrap:wrap} +.btn{border:none;border-radius:8px;padding:10px 16px;font-size:13px;font-weight:600; + cursor:pointer;transition:opacity .15s,transform .1s;white-space:nowrap} +.btn:hover{opacity:.85;transform:translateY(-1px)} +.btn:active{transform:translateY(0)} +.btn-start{background:var(--ok);color:#0d2010} +.btn-pause{background:var(--raised);color:var(--txt);border:1px solid var(--border)} +.btn-resume{background:#0d2d1a;color:var(--ok);border:1px solid var(--ok)} +.btn-skip{background:var(--raised);color:var(--warn);border:1px solid var(--warn)} +.btn-cancel{background:#2d0d0d;color:var(--err);border:1px solid var(--err)} +.btn-accent{background:var(--accent);color:#001a24} +.btn-sm{padding:7px 12px;font-size:12px} +.spd-btn{flex:1;border:1.5px solid var(--border);background:var(--raised);color:var(--txt); + border-radius:10px;padding:14px 8px;font-size:13px;font-weight:600;cursor:pointer; + transition:all .15s;display:flex;flex-direction:column;align-items:center;gap:4px} +.spd-btn:hover{border-color:var(--accent);color:var(--accent)} +.spd-btn.spd-active{border-color:var(--accent);background:rgba(0,200,255,.12);color:var(--accent)} +.spd-btn .spd-icon{font-size:22px} +.spd-bar{height:4px;border-radius:2px;background:var(--border);margin-top:10px;overflow:hidden} +.spd-bar-fill{height:100%;border-radius:2px;background:linear-gradient(90deg,var(--accent2),var(--accent));transition:width .3s} + +/* ── TIME CARDS ── */ +.time-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:8px} +.time-block{background:var(--raised);border-radius:10px;padding:10px 12px} +.time-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--txt2);margin-bottom:4px} +.time-val{font-size:20px;font-weight:700;font-family:var(--mono);color:var(--txt)} +/* ── TEMPS ── */ +.temp-pair{display:grid;grid-template-columns:1fr 1fr;gap:12px} +.temp-card-inner{display:grid;grid-template-columns:1fr 1fr;gap:12px} +.temp-block{background:var(--raised);border-radius:10px;padding:14px;position:relative} +.temp-label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--txt2);margin-bottom:6px} +.temp-row{display:flex;align-items:baseline;gap:6px} +.temp-val{font-size:30px;font-weight:700;font-family:var(--mono)} +.temp-unit{font-size:14px;color:var(--txt2)} +.temp-target{font-size:11px;color:var(--txt2);margin-top:2px} +.temp-arc{position:absolute;top:12px;right:12px} +.temp-edit{display:flex;gap:6px;margin-top:10px} +.temp-input{background:var(--bg);border:1px solid var(--border);color:var(--txt); + border-radius:6px;padding:5px 8px;font-size:13px;font-family:var(--mono);width:70px} +.temp-input:focus{outline:none;border-color:var(--accent)} +.chart-wrap{margin-top:12px} +canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:var(--raised)} + +/* ── MOTION ── */ +.joypad{display:grid;grid-template-columns:repeat(3,52px); + grid-template-rows:repeat(3,52px);gap:6px;justify-content:center;margin:8px auto} +.joy{background:var(--raised);border:1px solid var(--border);color:var(--txt); + border-radius:10px;font-size:18px;cursor:pointer;transition:.12s; + display:flex;align-items:center;justify-content:center} +.joy:hover{background:var(--accent);color:#001a24;border-color:var(--accent)} +.joy:active{transform:scale(.93)} +.joy.home{font-size:14px;background:var(--bg)} +.step-btns{display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin-top:10px} +.step-btn{background:var(--raised);border:1px solid var(--border);color:var(--txt2); + border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer;transition:.12s} +.step-btn.active,.step-btn:hover{background:var(--accent);color:#001a24;border-color:var(--accent)} +.home-btns{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px;justify-content:center} + +/* ── AMS ── */ +.ams-slots{display:flex;flex-direction:column;gap:12px} +.ams-box-group{} +.ams-box-label{font-size:11px;font-weight:700;color:var(--txt2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px;padding-left:2px} +.ams-box-slots{display:grid;grid-template-columns:repeat(4,1fr);gap:8px} +.ams-slot{background:var(--raised);border-radius:10px;padding:10px;text-align:center; + border:2px solid transparent;transition:.2s;position:relative} +.ams-slot.active{border-color:var(--slot-color,var(--accent)); + box-shadow:0 0 12px rgba(var(--slot-rgb,0,200,255),.3)} +.ams-slot.loaded{border-color:var(--ok)!important; + box-shadow:0 0 0 2px rgba(64,220,120,.35),0 0 14px rgba(64,220,120,.35)} +.ams-slot.loading{border-color:var(--ok)!important;animation:amsPulseGreen 1s ease-in-out infinite} +.ams-slot.unloading{border-color:var(--err)!important;animation:amsPulseRed 1s ease-in-out infinite} +@keyframes amsPulseGreen{0%{box-shadow:0 0 0 0 rgba(64,220,120,.55)}50%{box-shadow:0 0 0 4px rgba(64,220,120,.25),0 0 18px rgba(64,220,120,.45)}100%{box-shadow:0 0 0 0 rgba(64,220,120,.55)}} +@keyframes amsPulseRed{0%{box-shadow:0 0 0 0 rgba(230,80,80,.55)}50%{box-shadow:0 0 0 4px rgba(230,80,80,.25),0 0 18px rgba(230,80,80,.45)}100%{box-shadow:0 0 0 0 rgba(230,80,80,.55)}} +.ams-slot-bridge{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px; + border:1px dashed var(--border);background:linear-gradient(180deg,rgba(255,255,255,.03),rgba(255,255,255,.01)); + color:var(--txt2);min-height:106px} +.ams-slot-bridge .bridge-chip{width:58px;height:58px;border:1px solid rgba(255,255,255,.14);border-radius:50%; + display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.04);color:var(--txt2); + font-size:13px;font-weight:700;letter-spacing:.04em} +.slot-circle{width:36px;height:36px;border-radius:50%;margin:0 auto 6px;border:2px solid rgba(255,255,255,.15)} +.slot-label{font-size:11px;color:var(--txt2);font-family:var(--mono)} +.slot-material{font-size:12px;font-weight:600;margin-bottom:2px} + +/* ── LIGHT + FAN ── */ +.toggle-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px} +.toggle-label{font-size:13px;font-weight:600} +.toggle{position:relative;width:44px;height:24px;cursor:pointer} +.toggle input{opacity:0;width:0;height:0;position:absolute} +.toggle-track{width:44px;height:24px;background:var(--raised);border-radius:12px; + border:1px solid var(--border);transition:.25s;display:block} +.toggle input:checked+.toggle-track{background:var(--accent)} +.toggle-thumb{position:absolute;top:3px;left:3px;width:18px;height:18px; + background:#fff;border-radius:50%;transition:.25s;pointer-events:none} +.toggle input:checked~.toggle-thumb{transform:translateX(20px)} +.slider-row{display:flex;align-items:center;gap:10px;margin-top:8px} +.slider-label{font-size:12px;color:var(--txt2);width:80px} +.slider{flex:1;-webkit-appearance:none;height:4px;border-radius:2px; + background:var(--raised);outline:none;cursor:pointer} +.slider::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px; + border-radius:50%;background:var(--accent);cursor:pointer;transition:.1s} +.slider::-webkit-slider-thumb:hover{transform:scale(1.2)} +.slider-val{font-family:var(--mono);font-size:12px;color:var(--txt);width:30px;text-align:right} + +/* ── CONSOLE ── */ +.console{background:#0a0a0e;border-radius:8px;padding:10px;font-family:var(--mono); + font-size:11px;color:#8888aa;overflow-y:auto;line-height:1.6} +.console .ts{color:#444;margin-right:6px} +.console .msg-info{color:#8888aa} +.console .msg-ok{color:var(--ok)} +.console .msg-err{color:var(--err)} +.console .msg-warn{color:var(--warn)} + +/* ── PANELS ── */ +.panel{display:none} +.panel.active{display:block} + +/* ── MODAL ── */ +.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6); + z-index:200;align-items:center;justify-content:center;padding:16px} +.modal-overlay.open{display:flex} +.modal-box{background:var(--card);border:1px solid var(--border);border-radius:14px; + width:100%;max-width:480px;max-height:90vh;overflow-y:auto;padding:24px; + display:flex;flex-direction:column;gap:18px} +.modal-header{display:flex;align-items:center;justify-content:space-between} +.modal-title{font-size:15px;font-weight:700;color:var(--txt)} +.modal-close{background:none;border:none;color:var(--txt2);font-size:20px; + cursor:pointer;padding:4px 8px;border-radius:6px} +.modal-close:hover{background:var(--raised);color:var(--txt)} +.modal-section{font-size:10px;text-transform:uppercase;letter-spacing:.1em; + color:var(--txt2);margin-bottom:6px;margin-top:4px} +.modal-field{display:flex;flex-direction:column;gap:4px;margin-bottom:10px} +.modal-field label{font-size:12px;color:var(--txt2)} +.modal-field input{background:var(--raised);border:1px solid var(--border); + border-radius:7px;color:var(--txt);padding:7px 10px;font-size:13px;width:100%} +.modal-field input:focus{outline:none;border-color:var(--accent)} +.poll-btns{display:flex;gap:8px} +.poll-btn{flex:1;padding:7px;background:var(--raised);border:1px solid var(--border); + border-radius:7px;color:var(--txt2);cursor:pointer;font-size:13px;transition:all .15s} +.poll-btn.active{background:var(--accent);border-color:var(--accent);color:#000;font-weight:600} +.update-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap} +.update-status{font-size:12px;color:var(--txt2);flex:1;min-width:0} +.modal-save{width:100%;padding:10px;background:var(--accent);border:none; + border-radius:8px;color:#000;font-weight:700;font-size:14px;cursor:pointer;margin-top:4px} +.modal-save:hover{opacity:.88} + +/* ── BOTTOM NAV (mobile) ── */ +nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0; + background:var(--card);border-top:1px solid var(--border); + justify-content:space-around;padding:8px 0 max(8px,env(safe-area-inset-bottom))} +.bnav-btn{background:none;border:none;color:var(--txt2);display:flex; + flex-direction:column;align-items:center;gap:3px;cursor:pointer;font-size:10px;padding:4px 8px} +.bnav-btn.active{color:var(--accent)} +.bnav-icon{font-size:20px} + +/* ── Tablet (769–1100px): schmale Sidebar ── */ +@media(min-width:769px) and (max-width:1100px){ + nav.sidebar{width:52px;padding:12px 4px} + .nav-btn .nav-text{display:none} + .nav-btn{justify-content:center;padding:10px} + .nav-icon{width:auto} + .grid{grid-template-columns:repeat(2,1fr)} + .hero{grid-template-columns:1fr} +} + +/* ── Mobile (≤768px): Bottom-Nav, 1-Spalte ── */ +@media(max-width:768px){ + nav.sidebar{display:none} + nav.bottom-nav{display:flex} + main{padding:10px;padding-bottom:72px} + + /* Header kompakt */ + header{padding:0 12px;gap:8px} + .hname{display:none} + + /* 1-Spalten-Grid, full-width spans funktionieren weiterhin */ + .grid{grid-template-columns:1fr;gap:12px} + + /* Hero: Kamera über Info */ + .hero{grid-template-columns:1fr} + .cam-wrap{max-height:220px} + + /* Temp-Pair und Temp-Card übereinander */ + .temp-pair{grid-template-columns:1fr} + .temp-card-inner{grid-template-columns:1fr} + + /* AMS: 2 Spalten auf kleinen Screens */ + .ams-box-slots{grid-template-columns:repeat(2,1fr)} + + /* Joypad etwas kleiner */ + .joypad{grid-template-columns:repeat(3,44px);grid-template-rows:repeat(3,44px);gap:5px} + .joy{font-size:16px} + + /* Buttons größere Touch-Targets */ + .btn{padding:10px 14px;font-size:13px} + .btn-sm{padding:8px 12px} + .step-btn{padding:8px 12px;font-size:13px} + + /* Modal vollbreite auf kleinen Screens */ + .modal-box{padding:16px;border-radius:10px} + .poll-btns{gap:6px} +}