diff --git a/Dockerfile b/Dockerfile index 276bc25..d1682c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,6 @@ COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY kobrax_moonraker_bridge.py . -COPY web ./web COPY config_loader.py . COPY env_loader.py . COPY kobrax_client.py . diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 599f57c..9cab173 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -11,7 +11,6 @@ OrcaSlicer-Konfiguration: """ import argparse -import html import sqlite3 import uuid try: @@ -115,14 +114,6 @@ 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) @@ -444,14 +435,6 @@ class KobraXBridge: self._thumbnail_b64: str = "" - 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 @@ -1332,62 +1315,2047 @@ class KobraXBridge: "text": "OctoPrint (Kobra X Bridge)", }) - 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_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): - 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()) + html = r""" + + + + +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 +
+
+ + + + + +
+
+ + +
+
AMS / Filamentbox
+
+
Keine AMS-Daten empfangen
+
+
+
Slot auswählen
+
+ + Slot 1 +
+
+ + +
+
+
+
+
+ + + +
+
+
+ 🖨 Drucker +
+ + +
+
+
+
+
+ + +
+
+
+ 🗂 Datei-Browser + +
+
+ + + +
+ +
+
+
+ +
+
+
+ Ereignis-Log + ⬇ Download +
+
+ + + +
+
+ Dir: + + + + Topic: + + + + +
+
+
+
+
+
+ + + + + + + + + +
+ © ViewIT 2026 +
+ +""" + version = self._read_version() + html = html.replace("'__VERSION__'", f"'{version}'") return web.Response(text=html, content_type="text/html", headers={"Cache-Control": "no-store, no-cache, must-revalidate"}) @@ -2467,7 +4435,6 @@ 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_route("OPTIONS", "/kx/{path:.*}", bridge.handle_kx_options) # Root + Printer-Routen (Single-Page, JS liest Pathname) @@ -2633,13 +4600,6 @@ 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/themes/THEME-ID-CHECKLIST.md b/web/themes/THEME-ID-CHECKLIST.md deleted file mode 100644 index 1fbcd29..0000000 --- a/web/themes/THEME-ID-CHECKLIST.md +++ /dev/null @@ -1,338 +0,0 @@ -# KX-Bridge Theme – Element-ID-Checkliste - -Alle IDs, die `default/app.js` erwartet oder nutzt. Beim Umbau eines Themes (`new1`, …) müssen diese **IDs beibehalten** werden, sonst brechen Features still (null-Check) oder mit Fehler (ohne null-Check). - -**Legende** - -| Symbol | Bedeutung | -|--------|-----------| -| **Pflicht** | Fehlt → Laufzeitfehler oder zentraler Ablauf bricht | -| **Empfohlen** | Fehlt → Feature funktioniert nicht (meist mit `if(el)` abgesichert) | -| **Optional** | Nur i18n, Dekoration oder rein HTML-seitig | - -**Quelle:** automatisch aus `default/app.js` extrahiert + Abgleich mit `default/index.html` - ---- - -## Navigation & Panels - -| ID | Pflicht | Verwendung in app.js | -|----|---------|----------------------| -| `panel-dashboard` | **Pflicht** | `showPanel('dashboard')` | -| `panel-printers` | **Pflicht** | `showPanel('printers')` | -| `panel-store` | **Pflicht** | `showPanel('store')` | -| `panel-console` | **Pflicht** | `showPanel('console')` | -| `nb-dashboard` | Empfohlen | Sidebar active + i18n `.nav-text` | -| `nb-printers` | Empfohlen | Sidebar active + i18n | -| `nb-store` | Empfohlen | Sidebar active + i18n | -| `nb-console` | Empfohlen | Sidebar active + i18n + `#log-badge` | -| `bnb-dashboard` | Empfohlen | Bottom-Nav active + i18n | -| `bnb-printers` | Empfohlen | Bottom-Nav | -| `bnb-store` | Empfohlen | Bottom-Nav | -| `bnb-console` | Empfohlen | Bottom-Nav + `#log-badge-bot` | -| `log-badge` | Empfohlen | Fehler-Badge Sidebar-Konsole | -| `log-badge-bot` | Empfohlen | Fehler-Badge Bottom-Nav | - -**CSS-Klassen:** `.panel`, `.nav-btn`, `.bnav-btn`, `.nav-text` (in Sidebar-Buttons) - ---- - -## Header & Banner - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `lang-btn` | **Pflicht** | Sprachumschalter (ohne null-Check) | -| `conn-btn` | **Pflicht** | Verbinden/Trennen, `updateConnBtn()` | -| `settings-btn` | Optional | Nur HTML – `openSettings()` per onclick | -| `printer-dropdown-wrap` | Empfohlen | Multi-Drucker UI | -| `printer-dropdown-btn` | Optional | Nur HTML-onclick | -| `printer-dropdown-menu` | Empfohlen | Dropdown-Inhalt | -| `h-pname` | Empfohlen | Druckername (Multi-Modus) | -| `h-pname-single` | Empfohlen | Druckername (Einzel-Modus) | -| `h-version` | Empfohlen | Bridge-Version aus `/api/state` | -| `h-badge` | Empfohlen | Status-Badge CSS-Klasse | -| `h-state` | Empfohlen | Status-Text | -| `conn-error-banner` | Empfohlen | MQTT-Verbindungsfehler | -| `file-ready-banner` | Empfohlen | Slicer-Upload bereit | -| `file-ready-name` | Empfohlen | Dateiname im Banner | -| `file-ready-btn` | Empfohlen | Druck starten | -| `file-slots-btn` | Empfohlen | Slots wählen | -| `file-cancel-btn` | Empfohlen | Banner abbrechen | - ---- - -## Dashboard – Kamera - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `d-light-toggle` | **Pflicht** | Licht sync in `applyState()` (`.checked`) | -| `cam-toggle-btn` | Empfohlen | Start/Stop-Label | -| `cam-img` | Empfohlen | MJPEG-Stream | -| `cam-placeholder` | Empfohlen | Platzhalter sichtbar/unsichtbar | -| `cam-placeholder-txt` | Empfohlen | i18n Platzhaltertext | -| `cam-spinner` | Empfohlen | Lade-Animation | -| `cam-overlay` | Empfohlen | Dateiname über Video | -| `cam-fname` | Empfohlen | Dateiname in Overlay | -| `cam-wrap` | Optional | Nur HTML/CSS | - ---- - -## Dashboard – Fortschritt & Druck - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `d-pct` | Empfohlen | Prozent-Anzeige | -| `d-pbar` | Empfohlen | Fortschrittsbalken | -| `d-layers` | Empfohlen | Layer X / Y | -| `d-elapsed` | Empfohlen | Verstrichene Zeit | -| `d-remain` | Empfohlen | Restzeit | -| `d-slicer-row` | Empfohlen | Slicer-Schätzung ein/aus | -| `d-slicer-time` | Empfohlen | Slicer-Zeitwert | -| `d-fname` | Empfohlen | Aktueller Dateiname | -| `d-thumbnail` | Empfohlen | GCode-Vorschaubild | -| `d-btn-pause` | Empfohlen | i18n + onclick | -| `d-btn-resume` | Empfohlen | i18n + onclick | -| `d-btn-cancel` | Empfohlen | i18n + onclick | -| `d-ctrl-btns` | Optional | Nur Layout | -| `p-fname` | Optional | Legacy/Print-Panel (falls vorhanden) | -| `d-card-progress` | Optional | i18n Kartentitel | -| `d-lbl-elapsed` | Optional | i18n | -| `d-lbl-remain` | Optional | i18n | -| `d-lbl-layers` | Optional | i18n | -| `d-slicer-label` | Optional | i18n | - ---- - -## Dashboard – Temperaturen - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `d-nt` | Empfohlen | Ist-Temperatur Düse | -| `d-nt-t` | Empfohlen | Soll-Temperatur Düse | -| `d-bt` | Empfohlen | Ist-Temperatur Bett | -| `d-bt-t` | Empfohlen | Soll-Temperatur Bett | -| `d-ntbar` | Empfohlen | Fortschrittsbalken Düse | -| `d-btbar` | Empfohlen | Fortschrittsbalken Bett | -| `p-nozzle-inp` | Empfohlen | Eingabe Ziel Düse (`setNozzle`) | -| `p-bed-inp` | Empfohlen | Eingabe Ziel Bett (`setBed`) | -| `d-chart` | Empfohlen | Canvas Temperatur-Verlauf | -| `d-chart-label` | Optional | i18n | -| `d-card-temps` | Optional | i18n | -| `d-lbl-bed` | Optional | i18n | - -**CSS-Klassen:** `.lbl-set`, `.lbl-off`, `.temp-input` - ---- - -## Dashboard – Achsen & Schrittweite - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `step-display` | Empfohlen | Anzeige aktuelle Schrittweite | -| `ptitle-motion-xy` | Optional | i18n | -| `ptitle-motion-z` | Optional | i18n | - -**CSS-Klassen:** `.step-btn`, `.joy`, `.lbl-home-z`, `.lbl-home-xy`, `.lbl-home-all`, `.lbl-disable-motors`, `.lbl-step` - ---- - -## Dashboard – Geschwindigkeit & Lüfter - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `d-spd-1` | Empfohlen | Speed-Modus Leise | -| `d-spd-2` | Empfohlen | Speed-Modus Normal | -| `d-spd-3` | Empfohlen | Speed-Modus Sport | -| `d-spd-bar` | Empfohlen | Speed-Balken | -| `d-fan` | Empfohlen | Lüfter-Slider | -| `d-fan-val` | Empfohlen | Lüfter-Prozent-Anzeige | -| `d-card-speed` | Optional | i18n | -| `d-card-lightfan` | Optional | i18n | -| `d-spd-lbl-1` | Optional | i18n | -| `d-spd-lbl-2` | Optional | i18n | -| `d-spd-lbl-3` | Optional | i18n | -| `d-lbl-light` | Optional | i18n | - -**CSS-Klassen:** `.lbl-feed`, `.lbl-unload` (AMS-Bereich) - ---- - -## Dashboard – AMS - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `ams-slots` | **Pflicht** | Container für dynamische Slot-Kacheln | -| `ams-slot-sel` | Empfohlen | Slot-Auswahl für Feed/Unload | -| `ams-slot-label` | Optional | Nur HTML-oninput Label | -| `ams-no-data` | Optional | Platzhalter (wird durch JS ersetzt) | -| `btn-unload` | Optional | Nur HTML | -| `d-card-ams` | Optional | i18n | -| `d-card-cam` | Optional | i18n | - ---- - -## Einstellungen (`#settings-modal`) - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `settings-modal` | **Pflicht** | Modal open/close (`.classList`) | -| `s-printer-name` | Empfohlen | Config-Feld | -| `s-printer-ip` | **Pflicht** | Config + IP-Hinweis-Listener | -| `s-mqtt-port` | Empfohlen | Config | -| `s-username` | Empfohlen | Config | -| `s-password` | Empfohlen | Config | -| `s-device-id` | Empfohlen | Config | -| `s-mode-id` | Empfohlen | Config | -| `s-default-slot` | Empfohlen | AMS-Standard-Slot | -| `s-auto-leveling` | Empfohlen | Checkbox Auto-Leveling | -| `poll-1` | Empfohlen | Poll 1 s | -| `poll-2` | Empfohlen | Poll 2 s | -| `poll-5` | Empfohlen | Poll 5 s | -| `s-version-label` | Empfohlen | Versionsanzeige | -| `update-status` | Empfohlen | Update-Status-Text | -| `btn-update-apply` | Empfohlen | Update installieren | -| `update-changelog` | Empfohlen | Changelog-Anzeige | -| `btn-save-settings` | Empfohlen | Speichern | -| `btn-update-check` | Optional | Nur HTML-onclick | -| `lbl-ip-hint` | Empfohlen | IP-Format-Warnung | -| `modal-title-settings` | Optional | i18n | -| `modal-sec-connection` | Optional | i18n | -| `modal-sec-print` | Optional | i18n | -| `modal-sec-poll` | Optional | i18n | -| `modal-sec-version` | Optional | i18n | -| `lbl-printer-name` … `lbl-mode-id` | Optional | i18n Labels | -| `lbl-default-slot` | Optional | i18n | -| `lbl-auto-leveling` | Optional | i18n | -| `opt-slot-auto` | Optional | i18n | -| `opt-slot-0` … `opt-slot-3` | Optional | Nur HTML | -| `lbl-update-check` | Optional | i18n | -| `lbl-update-apply` | Optional | i18n | - -**CSS-Klasse:** `.poll-btn` - ---- - -## AMS-Slot-Dialog (`#slot-edit-modal`) - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `slot-edit-modal` | **Pflicht** | Modal open/close | -| `slot-edit-title` | Empfohlen | Dialogtitel | -| `slot-edit-color` | Empfohlen | Farbwähler | -| `slot-edit-preview` | Empfohlen | Farbvorschau | -| `slot-edit-mat` | Empfohlen | Material-Text | -| `slot-mat-btns` | Empfohlen | Preset-Buttons (dynamisch) | -| `btn-slot-edit-save` | Empfohlen | Speichern | -| `lbl-slot-color` | Optional | i18n | -| `lbl-slot-material` | Optional | i18n | - -**CSS-Klasse:** `.mat-preset-btn` (dynamisch) - ---- - -## Filament-Druck-Dialog (`#filament-dialog`) - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `filament-dialog` | **Pflicht** | Modal open/close | -| `fd-title` | Empfohlen | Dateiname im Titel | -| `fd-slots` | Empfohlen | Kanal→Slot-Zuordnung (dynamisch) | - ---- - -## Drucker hinzufügen (`#add-printer-dialog`) - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `add-printer-dialog` | **Pflicht** | Modal open/close | -| `apd-ip` | Empfohlen | IP-Eingabe | -| `apd-name` | Empfohlen | Name optional | -| `apd-status` | Empfohlen | Status/Fehlermeldung | -| `apd-confirm` | Empfohlen | Bestätigen | -| `apd-title` | Optional | i18n | -| `apd-lbl-ip` | Optional | i18n | -| `apd-lbl-name` | Optional | i18n | -| `add-printer-btn-label` | Optional | i18n | - ---- - -## Drucker-Tab - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `printers-grid` | Empfohlen | Container Drucker-Karten | -| `printers-panel-title` | Optional | i18n | - ---- - -## Datei-Browser - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `store-grid` | Empfohlen | Datei-Karten (dynamisch) | -| `store-empty` | Empfohlen | Leer-/Kein-Treffer-Hinweis | -| `store-search` | Empfohlen | Suche | -| `store-filter` | Empfohlen | Filter Select | -| `store-sort` | Empfohlen | Sortierung Select | -| `store-refresh-btn` | Optional | i18n Button-Text | -| `store-panel-title` | Optional | i18n | -| `sf-all`, `sf-ok`, `sf-err`, `sf-new` | Optional | i18n Optionen | -| `ss-date`, `ss-name`, `ss-dur` | Optional | i18n Optionen | - ---- - -## Konsole - -| ID | Pflicht | Verwendung | -|----|---------|------------| -| `console-log` | Empfohlen | Log-Ausgabe | -| `log-filter` | Empfohlen | Textfilter | -| `btn-autoscroll` | Empfohlen | Auto-Scroll Toggle | -| `logdir-all` | Empfohlen | Filter Alle | -| `logdir-rx` | Empfohlen | Filter RX | -| `logdir-tx` | Empfohlen | Filter TX | -| `ptitle-console` | Optional | i18n | -| `btn-log-dl` | Optional | Nur HTML – Download-Link `/api/log/download` | - -**CSS-Klassen:** `.log-dir-btn`, `.log-topic-btn` (Topic-Buttons nutzen `data-topic`, keine festen IDs) - ---- - -## IDs nur in HTML (nicht in app.js) - -Diese IDs sind im Default-Theme vorhanden, werden von `app.js` aber nicht per `getElementById` angesprochen: - -| ID | Anmerkung | -|----|-----------| -| `settings-btn` | Nur `onclick="openSettings()"` | -| `printer-dropdown-btn` | Nur onclick | -| `btn-update-check` | Nur onclick | -| `opt-slot-0` … `opt-slot-3` | Option-Labels statisch | -| `cam-wrap` | Layout | -| `d-ctrl-btns` | Layout | -| `ams-no-data` | Initial-Platzhalter, wird von JS überschrieben | -| `ams-slot-label` | Nur inline oninput | -| `btn-unload` | Nur HTML | -| `btn-log-dl` | Statischer Download-Link | - ---- - -## Schnell-Check vor Theme-Release - -- [ ] Alle vier `panel-*` vorhanden und in `.panel` gewrappt -- [ ] `lang-btn`, `conn-btn`, `d-light-toggle`, `settings-modal`, `ams-slots` vorhanden -- [ ] Alle Modals: `settings-modal`, `slot-edit-modal`, `filament-dialog`, `add-printer-dialog` -- [ ] Navigation: `nb-*` / `bnb-*` für `dashboard`, `printers`, `store`, `console` -- [ ] Dashboard-Kern: Temps, Progress, Fan, Speed, AMS-Container -- [ ] Store: `store-grid`, `store-search`, `store-filter`, `store-sort` -- [ ] Konsole: `console-log`, `log-filter` -- [ ] `index.html` bindet `/kx/ui/style.css` und `/kx/ui/app.js` ein -- [ ] Alle `onclick`-Handler aus [THEME-JS-HOOKS.md](./THEME-JS-HOOKS.md) gesetzt oder ersetzt - ---- - -## Siehe auch - -- [THEME-JS-HOOKS.md](./THEME-JS-HOOKS.md) – alle JS-Funktionsaufrufe mit Kontext -- Theme-Ordner: `web/themes//` mit `index.html`, `style.css`, `app.js` -- Aktivierung: `--ui-theme ` oder `KX_UI_THEME=` diff --git a/web/themes/THEME-JS-HOOKS.md b/web/themes/THEME-JS-HOOKS.md deleted file mode 100644 index 0eb11e2..0000000 --- a/web/themes/THEME-JS-HOOKS.md +++ /dev/null @@ -1,290 +0,0 @@ -# KX-Bridge Theme – JavaScript-Hooks - -Referenz für eigenes Theme-Layout (`index.html` + gemeinsames `app.js`). - -**Quelle:** `default/index.html` und `default/app.js` -**Stand:** Theme-Struktur mit getrennten Dateien (`index.html`, `style.css`, `app.js`) - ---- - -## Hinweise für Theme-Entwickler - -- Alle `onclick`-Handler rufen **globale Funktionen** aus `app.js` auf – Namen beibehalten oder in eigenem JS neu binden. -- `app.js` wird unter `/kx/ui/app.js` geladen und ist pro Theme austauschbar; die Standard-Implementierung erwartet die Hooks unten. -- Dynamisch erzeugtes HTML (AMS-Slots, Datei-Karten, Drucker-Karten) setzt zusätzliche `onclick`-Handler – siehe Abschnitt **Dynamisch aus app.js**. - ---- - -## Globale / Header - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `startReadyFile()` | `#file-ready-btn` | GCode vom Slicer bereit → sofort drucken | -| `startReadyFileWithSlots()` | `#file-slots-btn` | Druck mit AMS-Slot-Zuordnung | -| `cancelReadyFile()` | `#file-cancel-btn` | Upload-Banner schließen | -| `togglePrinterDropdown()` | `#printer-dropdown-btn` | Multi-Drucker-Dropdown | -| `toggleTheme()` | Theme-Button | Hell/Dunkel (`data-theme`, localStorage) | -| `toggleLang()` | `#lang-btn` | DE/EN umschalten | -| `openSettings()` | `#settings-btn` | Einstellungs-Modal | -| `toggleConnection()` | `#conn-btn` | MQTT verbinden / trennen | - ---- - -## Einstellungs-Modal (`#settings-modal`) - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `closeSettings()` | Overlay (Backdrop), Schließen-Button | Modal schließen | -| `setPoll(1000)` | `#poll-1` | Status-Poll 1 s | -| `setPoll(2000)` | `#poll-2` | Status-Poll 2 s | -| `setPoll(5000)` | `#poll-5` | Status-Poll 5 s | -| `checkUpdate()` | `#btn-update-check` | Update prüfen | -| `applyUpdate()` | `#btn-update-apply` | Update installieren | -| `saveSettings()` | `#btn-save-settings` | Config speichern → Neustart | - -**Ohne `onclick` in HTML:** IP-Hinweis für `#s-printer-ip` wird in `app.js` per `addEventListener('input', …)` gebunden. - ---- - -## AMS-Slot bearbeiten (`#slot-edit-modal`) - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `closeSlotEdit()` | Overlay, ✕ | Dialog schließen | -| `highlightMatBtn(this.value)` | `#slot-edit-mat` `oninput` | Material-Presets hervorheben | -| `saveSlotEdit()` | `#btn-slot-edit-save` | Farbe/Material speichern | - -**Inline (keine benannte Funktion):** - -| Code | Element | Zweck | -|------|---------|--------| -| `document.getElementById('slot-edit-preview').style.background=this.value` | `#slot-edit-color` | Farbvorschau | - -**Dynamisch aus `app.js`:** - -| Aufruf | Kontext | -|--------|---------| -| `openSlotEdit(i)` | Klick auf AMS-Kachel in `#ams-slots` | -| `selectMatPreset('PLA'…)` | Material-Preset-Buttons in `#slot-mat-btns` | - ---- - -## Navigation (Sidebar + Bottom-Nav) - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `showPanel('dashboard')` | `#nb-dashboard`, `#bnb-dashboard` | Dashboard | -| `showPanel('printers'); loadPrinterTab()` | `#nb-printers`, `#bnb-printers` | Drucker-Tab | -| `showPanel('store'); loadStore()` | `#nb-store`, `#bnb-store` | Datei-Browser | -| `showPanel('console'); clearLogBadge()` | `#nb-console`, `#bnb-console` | Konsole, Badge leeren | - -**Erforderliche Panel-IDs:** `panel-dashboard`, `panel-printers`, `panel-store`, `panel-console` -**Nav-IDs:** `nb-*` und `bnb-*` mit gleichem Suffix (`dashboard`, `printers`, `store`, `console`) - ---- - -## Dashboard – Kamera - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `setLight()` | `#d-light-toggle` `onchange` | Drucker-Licht | -| `toggleCam()` | `#cam-toggle-btn` | Stream starten/stoppen | - -Intern: `toggleCam()` → `camStart()` / `camStop()` (nur in `app.js`). - ---- - -## Dashboard – Drucksteuerung - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `printAction('pause')` | `#d-btn-pause` | Pausieren | -| `printAction('resume')` | `#d-btn-resume` | Fortsetzen | -| `confirmCancel()` | `#d-btn-cancel` | Bestätigung → `printAction('cancel')` | - ---- - -## Dashboard – Temperaturen - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `setNozzle()` | Set-Button Nozzle | Zieltemperatur Düse (`#p-nozzle-inp`) | -| `setBed()` | Set-Button Bett | Zieltemperatur Bett (`#p-bed-inp`) | - -**Inline:** Aus-Button setzt Input auf `0`, dann `setNozzle()` bzw. `setBed()`. - ---- - -## Dashboard – Achsensteuerung - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `move(1, 1, getStep())` | Joypad ▲ | Y+ | -| `move(1, -1, getStep())` | Joypad ▼ | Y− | -| `move(0, -1, getStep())` | Joypad ◀ | X− | -| `move(0, 1, getStep())` | Joypad ▶ | X+ | -| `move(2, 1, getStep())` | Z ▲ | Z+ | -| `move(2, -1, getStep())` | Z ▼ | Z− | -| `homeAll()` | Joypad ⌂, Home-All | Referenzfahrt alle Achsen | -| `homeZ()` | Home Z | Nur Z | -| `homeXY()` | Home XY | Nur X/Y | -| `disableMotors()` | Motors Off | Motoren aus | -| `setStep(this, 0.1\|1\|5\|10)` | Schrittweiten-Buttons | Jog-Schrittweite | -| `getStep()` | in allen `move(…)` | Aktuelle Schrittweite (Hilfsfunktion) | - ---- - -## Dashboard – Druckgeschwindigkeit - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `setSpeed(1)` | `#d-spd-1` | Leise | -| `setSpeed(2)` | `#d-spd-2` | Normal | -| `setSpeed(3)` | `#d-spd-3` | Sport | - ---- - -## Dashboard – Lüfter - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `setFan()` | `#d-fan` `onchange` | Slider-Wert senden | -| `quickFan(0\|25\|50\|75\|100)` | Schnell-Buttons | Vordefinierte Stufen | - -**Inline:** `#d-fan` `oninput` → `#d-fan-val` aktualisieren. - ---- - -## Dashboard – AMS / Filament - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `amsFeed(1)` | Einziehen | Filament laden | -| `amsFeed(2)` | Ausziehen | Filament entladen | - -**Inline:** `#ams-slot-sel` `oninput` → `#ams-slot-label` („Slot 1…4“). - ---- - -## Drucker-Tab (`#panel-printers`) - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `openAddPrinterDialog()` | „+ Drucker hinzufügen“ | Dialog öffnen | -| `loadPrinterTab()` | ↻-Button | Liste neu laden | - -**Dynamisch:** `removePrinter(id, name)` auf Drucker-Karten in `#printers-grid`. - ---- - -## Datei-Browser (`#panel-store`) - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `loadStore()` | `#store-refresh-btn` | Dateiliste laden | -| `renderStore()` | `#store-search`, `#store-filter`, `#store-sort` | Filtern/Sortieren | - -**Dynamisch:** `storePrint(fileId, filename)`, `storeDelete(fileId)` auf Karten in `#store-grid`. - ---- - -## Konsole (`#panel-console`) - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `renderLog()` | `#log-filter` `oninput`, Clear-Button | Filtern / leeren | -| `toggleAutoScroll()` | `#btn-autoscroll` | Auto-Scroll | -| `setLogDir('all'\|'rx'\|'tx')` | `#logdir-all`, `#logdir-rx`, `#logdir-tx` | Richtungsfilter | -| `setLogTopic('multiColorBox'\|'print'\|'info'\|'status')` | Topic-Buttons | MQTT-Topic | -| `onLogScroll()` | `#console-log` `onscroll` | Auto-Scroll bei manuellem Scroll deaktivieren | - -**Inline Clear:** `consoleLogs=[]; renderLog()` (keine eigene Funktion). - ---- - -## Filament-Dialog (`#filament-dialog`) - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `closeFilamentDialog()` | Overlay, Abbrechen, ✕ | Schließen | -| `confirmFilamentPrint()` | „▶ Drucken“ | Slot-Zuordnung bestätigen | - -Inhalt von `#fd-slots` wird von `openFilamentDialog()` befüllt (nach `startReadyFileWithSlots()` / `storePrint()`). - ---- - -## Drucker-hinzufügen-Dialog (`#add-printer-dialog`) - -| Aufruf | HTML-Element | Kontext | -|--------|--------------|---------| -| `closeAddPrinterDialog()` | Overlay, Abbrechen, ✕ | Schließen | -| `confirmAddPrinter()` | `#apd-confirm` | IP/Name senden → Neustart | - ---- - -## Modal-Backdrop-Muster - -Mehrere Overlays nutzen: - -```html -onclick="if(event.target===this)closeSettings()" -``` - -| Funktion | Modal-ID | -|----------|----------| -| `closeSettings()` | `#settings-modal` | -| `closeSlotEdit()` | `#slot-edit-modal` | -| `closeFilamentDialog()` | `#filament-dialog` | -| `closeAddPrinterDialog()` | `#add-printer-dialog` | - ---- - -## Funktionen ohne HTML-Hook (automatisch) - -| Funktion | Wann / wofür | -|----------|----------------| -| `poll()` | Periodisch `/api/state` – Dashboard-Update | -| `applyState()` | Poll-Daten ins DOM schreiben | -| `initPrinters()` | Multi-Drucker-Liste beim Start | -| `renderPrinterDropdown()` | Header-Dropdown | -| `applyLang()` | Übersetzungen (Start + nach `toggleLang()`) | -| `updateConnBtn()` | Verbindungs-Button | -| `updateHistory()` / `drawChart()` | Temperatur-Verlauf `#d-chart` | -| `connect()` (SSE) | Live-Server-Log | -| `openFilamentDialog()` | Multi-Farben-Druck | -| `openSlotEdit()` | AMS-Kacheln | -| `storePrint()` / `storeDelete()` | Datei-Karten | -| `removePrinter()` | Drucker-Karten | -| `camStart()` / `camStop()` | Kamera (auch auto bei Druckstart) | - ---- - -## CSS-Klassen (keine IDs, aber von app.js genutzt) - -| Klasse | Verwendung | -|--------|------------| -| `.panel` | Tab-Panels (`showPanel`) | -| `.nav-btn`, `.bnav-btn` | Navigation active-State | -| `.nav-text` | Sidebar-Label (i18n) | -| `.lbl-set`, `.lbl-off` | Temp-Button-Texte | -| `.lbl-home-z`, `.lbl-home-xy`, `.lbl-home-all`, `.lbl-disable-motors` | Home-Buttons | -| `.lbl-step` | Schrittweiten-Label | -| `.lbl-feed`, `.lbl-unload` | AMS-Buttons | -| `.temp-input` | Placeholder i18n | -| `.step-btn` | Schrittweiten-Auswahl | -| `.poll-btn` | Poll-Intervall | -| `.log-dir-btn`, `.log-topic-btn` | Log-Filter | -| `.mat-preset-btn` | Slot-Material (dynamisch) | -| `.ams-slot` | AMS-Kacheln (dynamisch) | - ---- - -## Theme aktivieren - -```bash -python kobrax_moonraker_bridge.py … --ui-theme default -# oder -KX_UI_THEME=new1 python kobrax_moonraker_bridge.py … -``` - -Pro Theme-Ordner: `index.html`, `style.css`, `app.js` unter `web/themes//`. diff --git a/web/themes/default/app.js b/web/themes/default/app.js deleted file mode 100644 index 3fcd946..0000000 --- a/web/themes/default/app.js +++ /dev/null @@ -1,1267 +0,0 @@ -// ── State ── -var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0, - print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0, - curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–', - camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80,ams_slots:[]}; -var tempHistory={n:[],b:[]}; -var camOn=false; -var currentStep=1; -var currentPanel='dashboard'; - -// ── Theme ── -function toggleTheme(){ - var h=document.documentElement; - h.setAttribute('data-theme',h.getAttribute('data-theme')==='dark'?'light':'dark'); - localStorage.setItem('theme',h.getAttribute('data-theme')); -} -(function(){var t=localStorage.getItem('theme');if(t)document.documentElement.setAttribute('data-theme',t)})(); - -// ── i18n ── -var LANG_DE={ - header_status_standby:'Bereit',header_status_printing:'Druckt',header_status_complete:'Fertig',header_status_error:'Fehler', - kobra_free:'Bereit',kobra_busy:'Beschäftigt',kobra_printing:'Druckt',kobra_preheating:'Aufheizen',kobra_auto_leveling:'Nivellierung',kobra_checking:'Prüfung',kobra_updated:'Aktualisierung',kobra_init:'Initialisierung',kobra_pausing:'Pausiert...',kobra_paused:'Pausiert',kobra_resuming:'Fortsetzen...',kobra_resumed:'Fortgesetzt',kobra_stopping:'Stoppt...',kobra_stoped:'Gestoppt',kobra_finished:'Abgeschlossen',kobra_failed:'Fehler',kobra_canceled:'Abgebrochen',kobra_offline:'Offline', - nav_dashboard:'Dashboard',nav_print:'Druck',nav_temps:'Temperaturen',nav_motion:'Achsen',nav_ams:'AMS',nav_extras:'Licht / Lüfter',nav_console:'Konsole', - card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',card_speed:'Druckgeschwindigkeit',card_cam:'Kamera',lbl_elapsed:'Verstrichen:',lbl_remaining:'Restzeit:',lbl_slicer_time:'Slicer-Schätzung:',lbl_layers:'Layer', - speed_silent:'🐢 Leise',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport', - lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen', - cam_placeholder:'📷 Kamera nicht gestartet',btn_cam_start:'▶ Kamera',btn_cam_stop:'◼ Kamera', - btn_pause:'⏸ Pause',btn_resume:'▶ Weiter',btn_cancel:'✕ Stopp', - label_nozzle:'Nozzle',label_bed:'Bett',label_fan:'🌀 Lüfter',label_light:'💡 Licht',label_on_off:'Ein / Aus',label_speed:'Geschwindigkeit', - panel_print_title:'Drucksteuerung',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Fortsetzen',panel_print_btn_cancel:'✕ Abbrechen',panel_print_temps_live:'Temperaturen (Live)', - label_set:'Setzen',label_off:'Aus', - panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heizbett',panel_temps_chart:'Verlauf (letzte 60 Messungen)',label_target_c:'Ziel:', - panel_motion_xy:'XY-Achsen',panel_motion_z:'Z-Achse',label_step:'Schrittweite:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motoren aus', - panel_ams_title:'AMS / Filamentbox',ams_no_data:'Keine AMS-Daten empfangen',label_slot:'Slot',ams_empty:'Leer', - panel_extras_light:'Licht',panel_extras_fan:'Lüfter',panel_extras_camera:'Kamera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop', - panel_console_title:'Ereignis-Log', - log_light_on:'Licht an',log_light_off:'Licht aus',log_fan:'Lüfter →',log_nozzle:'Nozzle →',log_bed:'Bett →',log_axis:'Achse',log_home:'Home',log_home_all:'Home All',log_cam_start:'Kamera gestartet:',log_cam_stop:'Kamera gestoppt',log_poll_error:'Poll-Fehler:',log_error:'Fehler:', - confirm_cancel:'Druck wirklich abbrechen?', - settings_title:'Einstellungen',settings_connection:'Verbindung',settings_print:'Druckeinstellungen',settings_poll:'Poll-Intervall',settings_version:'Version', - settings_save:'Speichern & Neustart',settings_printer_name:'Drucker-Name',settings_printer_ip:'Drucker-IP',settings_mqtt_port:'MQTT-Port', - settings_username:'MQTT-Benutzername',settings_password:'MQTT-Passwort',settings_device_id:'Device-ID',settings_mode_id:'Mode-ID',hint_ip_no_port:'Nur IP-Adresse, kein Port (z.B. 192.168.1.102)', - settings_default_slot:'Standard-Slot (Einfarbdruck)',settings_slot_auto:'Auto (alle belegten Slots)',settings_auto_leveling:'Auto-Leveling vor Druck', - update_check:'Auf Updates prüfen',update_checking:'Prüfe...',update_available:'verfügbar',update_none:'Bereits aktuell', - update_apply:'Jetzt installieren',update_applying:'Lade herunter...',update_restarting:'Starte neu...',update_error:'Fehler', - btn_connect:'⚡ Verbinden',btn_disconnect:'✕ Trennen', - lbl_conn_error:'Verbindungsfehler:', - slot_edit_title:'Slot bearbeiten',slot_edit_color:'Farbe',slot_edit_material:'Material', - slot_edit_save:'💾 Speichern',slot_edit_custom:'z.B. PLA, PETG, ABS…', - slot_edit_ok:'AMS Slot', - log_dir_all:'Alle', - file_ready_btn:'▶ Druck starten', - file_slots_btn:'🎨 Slots wählen', - file_cancel_btn:'✕ Abbrechen', - nav_printers:'Drucker', - add_printer:'Drucker hinzufügen',apd_lbl_ip:'Drucker-IP',apd_lbl_name:'Name (optional)', - apd_fetching:'Hole Daten vom Drucker…',apd_success:'Drucker hinzugefügt, Bridge startet neu…',apd_err_ip:'Bitte IP-Adresse eingeben', - printers_remove:'Drucker entfernen',printers_remove_confirm:'Drucker "{name}" entfernen? Die Bridge startet neu.', - printers_active:'● aktiv', - printers_switch:'Wechseln →', - printers_current:'Aktueller Drucker', - printers_loading:'Lade…', - printers_none:'Keine Drucker konfiguriert.', - printers_empty_hint:'Noch kein Drucker eingerichtet.', - nav_browser:'Browser', - panel_browser_title:'Datei-Browser', - store_empty:'Noch keine Dateien hochgeladen.', - store_refresh:'↻ Aktualisieren', - store_print:'▶ Drucken', - store_delete_confirm:'Datei löschen?', - store_print_confirm:'Datei drucken?', - store_no_results:'Keine Dateien gefunden.', - store_never:'noch nicht gedruckt', - sf_all:'Alle',sf_ok:'✓ Erfolgreich',sf_err:'✗ Fehler',sf_new:'Neu', - ss_date:'↓ Datum',ss_name:'A–Z Name',ss_dur:'⏱ Druckzeit' -}; -var LANG_EN={ - header_status_standby:'Ready',header_status_printing:'Printing',header_status_complete:'Complete',header_status_error:'Error', - kobra_free:'Ready',kobra_busy:'Busy',kobra_printing:'Printing',kobra_preheating:'Preheating',kobra_auto_leveling:'Auto Leveling',kobra_checking:'Checking',kobra_updated:'Updating',kobra_init:'Initializing',kobra_pausing:'Pausing...',kobra_paused:'Paused',kobra_resuming:'Resuming...',kobra_resumed:'Resumed',kobra_stopping:'Stopping...',kobra_stoped:'Stopped',kobra_finished:'Finished',kobra_failed:'Error',kobra_canceled:'Cancelled',kobra_offline:'Offline', - nav_dashboard:'Dashboard',nav_print:'Print',nav_temps:'Temperatures',nav_motion:'Motion',nav_ams:'AMS',nav_extras:'Light / Fan',nav_console:'Console', - card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera',lbl_elapsed:'Elapsed:',lbl_remaining:'Remaining:',lbl_slicer_time:'Slicer estimate:',lbl_layers:'Layer', - speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport', - lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload', - cam_placeholder:'📷 Camera not started',btn_cam_start:'▶ Camera',btn_cam_stop:'◼ Camera', - btn_pause:'⏸ Pause',btn_resume:'▶ Resume',btn_cancel:'✕ Stop', - label_nozzle:'Nozzle',label_bed:'Bed',label_fan:'🌀 Fan',label_light:'💡 Light',label_on_off:'On / Off',label_speed:'Speed', - panel_print_title:'Print Control',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Resume',panel_print_btn_cancel:'✕ Cancel',panel_print_temps_live:'Temperatures (Live)', - label_set:'Set',label_off:'Off', - panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heated Bed',panel_temps_chart:'History (last 60 readings)',label_target_c:'Target:', - panel_motion_xy:'XY Axes',panel_motion_z:'Z Axis',label_step:'Step size:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motors Off', - panel_ams_title:'AMS / Filament Box',ams_no_data:'No AMS data received',label_slot:'Slot',ams_empty:'Empty', - panel_extras_light:'Light',panel_extras_fan:'Fan',panel_extras_camera:'Camera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop', - panel_console_title:'Event Log', - log_light_on:'Light on',log_light_off:'Light off',log_fan:'Fan →',log_nozzle:'Nozzle →',log_bed:'Bed →',log_axis:'Axis',log_home:'Home',log_home_all:'Home All',log_cam_start:'Camera started:',log_cam_stop:'Camera stopped',log_poll_error:'Poll error:',log_error:'Error:', - confirm_cancel:'Really cancel the print?', - settings_title:'Settings',settings_connection:'Connection',settings_print:'Print Settings',settings_poll:'Poll Interval',settings_version:'Version', - settings_save:'Save & Restart',settings_printer_name:'Printer Name',settings_printer_ip:'Printer IP',settings_mqtt_port:'MQTT Port', - settings_username:'MQTT Username',settings_password:'MQTT Password',settings_device_id:'Device ID',settings_mode_id:'Mode ID',hint_ip_no_port:'IP address only, no port (e.g. 192.168.1.102)', - settings_default_slot:'Default Slot (single color)',settings_slot_auto:'Auto (all loaded slots)',settings_auto_leveling:'Auto-Leveling before print', - update_check:'Check for Updates',update_checking:'Checking...',update_available:'available',update_none:'Already up to date', - update_apply:'Install Now',update_applying:'Downloading...',update_restarting:'Restarting...',update_error:'Error', - btn_connect:'⚡ Connect',btn_disconnect:'✕ Disconnect', - lbl_conn_error:'Connection error:', - slot_edit_title:'Edit Slot',slot_edit_color:'Color',slot_edit_material:'Material', - slot_edit_save:'💾 Save',slot_edit_custom:'e.g. PLA, PETG, ABS…', - slot_edit_ok:'AMS Slot', - log_dir_all:'All', - file_ready_btn:'▶ Start Print', - file_slots_btn:'🎨 Select Slots', - file_cancel_btn:'✕ Cancel', - nav_printers:'Printers', - add_printer:'Add printer',apd_lbl_ip:'Printer IP',apd_lbl_name:'Name (optional)', - apd_fetching:'Fetching data from printer…',apd_success:'Printer added, bridge restarting…',apd_err_ip:'Please enter an IP address', - printers_remove:'Remove printer',printers_remove_confirm:'Remove printer "{name}"? The bridge will restart.', - printers_active:'● active', - printers_switch:'Switch →', - printers_current:'Current printer', - printers_loading:'Loading…', - printers_none:'No printers configured.', - printers_empty_hint:'No printer set up yet.', - nav_browser:'Browser', - panel_browser_title:'File Browser', - store_empty:'No files uploaded yet.', - store_refresh:'↻ Refresh', - store_print:'▶ Print', - store_delete_confirm:'Delete file?', - store_print_confirm:'Print file?', - store_no_results:'No files found.', - store_never:'never printed', - sf_all:'All',sf_ok:'✓ Completed',sf_err:'✗ Failed',sf_new:'New', - ss_date:'↓ Date',ss_name:'A–Z Name',ss_dur:'⏱ Print time' -}; -// Multi-Printer: BASE_URL aus Pathname (/printer2 → andere Bridge-Instanz) -var _printers=[]; -var _activePrinter=null; -(function(){ - var path=window.location.pathname.replace(/\/+$/,''); - var m=path.match(/^\/printer(\d+)$/); - var idx=m?parseInt(m[1]):1; - window._printerIndex=idx; -})(); -function _apiUrl(path){ - if(_activePrinter&&_activePrinter.bridge_url){ - return _activePrinter.bridge_url.replace(/\/+$/,'')+path; - } - return path; -} -function initPrinters(){ - fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){ // immer lokale Instanz für Drucker-Liste - _printers=d.result||[]; - var idx=window._printerIndex||1; - _activePrinter=_printers.find(function(p){return String(p.id)===String(idx)})||_printers[0]||null; - renderPrinterDropdown(); - }).catch(function(){}); -} -function renderPrinterDropdown(){ - var wrap=document.getElementById('printer-dropdown-wrap'); - var single=document.getElementById('h-pname-single'); - var name=_printers.length===0?'–':(_activePrinter?(_activePrinter.name||'Kobra X'):'Kobra X'); - var pname=document.getElementById('h-pname'); - if(pname)pname.textContent=name; - if(single)single.textContent=name; - if(_printers.length>1){ - if(wrap)wrap.style.display=''; - if(single)single.style.display='none'; - var menu=document.getElementById('printer-dropdown-menu'); - if(menu){ - menu.innerHTML=_printers.map(function(p){ - var active=_activePrinter&&String(p.id)===String(_activePrinter.id); - var num=p.id; - return ''+ - (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(){ - // Nav - var nb=document.getElementById('nb-dashboard');if(nb)nb.querySelector('.nav-text').textContent=T.nav_dashboard; - nb=document.getElementById('nb-console');if(nb)nb.querySelector('.nav-text').textContent=T.nav_console; - nb=document.getElementById('nb-printers');if(nb)nb.querySelector('.nav-text').textContent=T.nav_printers; - nb=document.getElementById('nb-store');if(nb)nb.querySelector('.nav-text').textContent=T.nav_browser; - // Bottom nav - var bnb=document.getElementById('bnb-dashboard');if(bnb)bnb.lastChild.textContent=T.nav_dashboard; - bnb=document.getElementById('bnb-console');if(bnb)bnb.lastChild.textContent=T.nav_console; - bnb=document.getElementById('bnb-printers');if(bnb)bnb.lastChild.textContent=T.nav_printers; - bnb=document.getElementById('bnb-store');if(bnb)bnb.lastChild.textContent=T.nav_browser; - // Browser panel - setText('printers-panel-title','🖨 '+T.nav_printers); - setText('add-printer-btn-label',T.add_printer); - setText('apd-title',T.add_printer); - setText('apd-lbl-ip',T.apd_lbl_ip); - setText('apd-lbl-name',T.apd_lbl_name); - setText('store-panel-title','🗂 '+T.panel_browser_title); - var srb=document.getElementById('store-refresh-btn');if(srb)srb.textContent=T.store_refresh; - setText('store-empty',T.store_empty); - setText('sf-all',T.sf_all);setText('sf-ok',T.sf_ok);setText('sf-err',T.sf_err);setText('sf-new',T.sf_new); - setText('ss-date',T.ss_date);setText('ss-name',T.ss_name);setText('ss-dur',T.ss_dur); - // Dashboard card titles - setText('d-card-progress',T.card_progress); - setText('d-card-temps',T.card_temps); - setText('d-card-lightfan',T.card_light_fan); - setText('d-card-speed',T.card_speed); - setText('d-card-cam',T.card_cam); - setText('d-card-ams',T.panel_ams_title); - setText('d-lbl-elapsed',T.lbl_elapsed); - setText('d-lbl-remain',T.lbl_remaining); - setText('d-slicer-label',T.lbl_slicer_time); - setText('d-lbl-layers',T.lbl_layers); - setText('d-lbl-light',T.lbl_light); - setText('d-lbl-bed',T.label_bed); - // Dashboard buttons - setText('d-btn-pause',T.btn_pause); - setText('d-btn-resume',T.btn_resume); - setText('d-btn-cancel',T.btn_cancel); - setText('cam-toggle-btn',camOn?T.btn_cam_stop:T.btn_cam_start); - setText('cam-placeholder-txt',T.cam_placeholder); - // Temp labels - document.querySelectorAll('.lbl-set').forEach(e=>e.textContent=T.label_set); - document.querySelectorAll('.lbl-off').forEach(e=>e.textContent=T.label_off); - setText('d-chart-label',T.panel_temps_chart); - // Axis labels - setText('ptitle-motion-xy',T.panel_motion_xy); - setText('ptitle-motion-z',T.panel_motion_z); - document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z); - document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy); - document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all); - document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors); - document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step); - document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':',''))); - // Console - setText('ptitle-console',T.panel_console_title); - // Settings modal - setText('modal-title-settings',T.settings_title); - setText('modal-sec-connection',T.settings_connection); - setText('modal-sec-print',T.settings_print); - setText('modal-sec-poll',T.settings_poll); - setText('modal-sec-version',T.settings_version); - setText('btn-save-settings',T.settings_save); - setText('lbl-printer-name',T.settings_printer_name); - setText('lbl-printer-ip',T.settings_printer_ip); - setText('lbl-mqtt-port',T.settings_mqtt_port); - setText('lbl-username',T.settings_username); - setText('lbl-password',T.settings_password); - setText('lbl-device-id',T.settings_device_id); - setText('lbl-mode-id',T.settings_mode_id); - setText('lbl-default-slot',T.settings_default_slot); - setText('opt-slot-auto',T.settings_slot_auto); - setText('lbl-auto-leveling',T.settings_auto_leveling); - - setText('lbl-update-check',T.update_check); - setText('lbl-update-apply',T.update_apply); - // Speed buttons - setText('d-spd-lbl-1',T.speed_silent.replace(/^\S+\s/,'')); - setText('d-spd-lbl-2',T.speed_normal.replace(/^\S+\s/,'')); - setText('d-spd-lbl-3',T.speed_sport.replace(/^\S+\s/,'')); - // AMS feed/unload - document.querySelectorAll('.lbl-feed').forEach(e=>e.textContent=T.lbl_feed); - document.querySelectorAll('.lbl-unload').forEach(e=>e.textContent=T.lbl_unload); - // conn-btn text (nur wenn nicht im Übergangszustand) - updateConnBtn(); - // Slot-Edit-Dialog - setText('lbl-slot-color',T.slot_edit_color); - setText('lbl-slot-material',T.slot_edit_material); - setText('btn-slot-edit-save',T.slot_edit_save); - var mi=document.getElementById('slot-edit-mat');if(mi)mi.setAttribute('placeholder',T.slot_edit_custom); - setText('logdir-all',T.log_dir_all); - setText('file-ready-btn',T.file_ready_btn); - setText('file-slots-btn',T.file_slots_btn); - setText('file-cancel-btn',T.file_cancel_btn); - setText('file-cancel-btn',T.file_cancel_btn); -} -function setText(id,txt){var el=document.getElementById(id);if(el)el.textContent=txt;} -(function(){ - var l=localStorage.getItem('lang')||'de'; - currentLang=l;T=l==='de'?LANG_DE:LANG_EN; - document.getElementById('lang-btn').textContent=l==='de'?'EN':'DE'; - document.documentElement.setAttribute('lang',l); - // defer until DOM ready - window.addEventListener('DOMContentLoaded',function(){ - applyLang(); - // Kein Drucker konfiguriert? → direkt in den Drucker-Tab (zeigt "+ Drucker hinzufügen") - fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){ - if(!d.result||!d.result.length){showPanel('printers');loadPrinterTab();} - }).catch(function(){}); - }); -})(); - -// ── Panel nav ── -function showPanel(id){ - document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active')); - document.getElementById('panel-'+id).classList.add('active'); - document.querySelectorAll('.nav-btn,.bnav-btn').forEach(b=>b.classList.remove('active')); - var nb=document.getElementById('nb-'+id);if(nb)nb.classList.add('active'); - var bnb=document.getElementById('bnb-'+id);if(bnb)bnb.classList.add('active'); - currentPanel=id; -} - -// ── Console log ── -var consoleLogs=[]; -var logAutoScroll=true; -var logBadgeCount=0; -var logDirFilter='all'; // 'all'|'rx'|'tx' -var logTopicFilter=''; // '' = no topic filter - -function clog(msg,cls){ - cls=cls||'msg-info'; - var ts=new Date().toLocaleTimeString('de',{hour:'2-digit',minute:'2-digit',second:'2-digit'}); - _appendLog({ts:ts,lvl:'',name:'ui',msg:msg},cls); -} -function _lvlCls(lvl){ - if(lvl==='ERROR'||lvl==='CRITICAL')return'msg-err'; - if(lvl==='WARNING')return'msg-warn'; - if(lvl==='DEBUG')return'msg-info'; - return'msg-ok'; -} -function _appendLog(entry,forceCls){ - var cls=forceCls||_lvlCls(entry.lvl); - var label=entry.name?'['+entry.name+'] ':''; - consoleLogs.push({ts:entry.ts,msg:label+entry.msg,cls:cls}); - if(consoleLogs.length>500)consoleLogs.shift(); - // Badge wenn Tab nicht aktiv und Fehler/Warnungen - if(currentPanel!=='console'&&(cls==='msg-err'||cls==='msg-warn')){ - logBadgeCount++; - var bc=logBadgeCount>99?'99+':logBadgeCount; - ['log-badge','log-badge-bot'].forEach(function(id){var b=document.getElementById(id);if(b){b.style.display='inline';b.textContent=bc;}}); - } - renderLog(); -} -function setLogDir(dir){ - logDirFilter=dir; - document.querySelectorAll('.log-dir-btn').forEach(function(b){ - b.style.background=b.id==='logdir-'+dir?'var(--accent)':'var(--raised)'; - b.style.color=b.id==='logdir-'+dir?'#fff':'var(--txt2)'; - }); - renderLog(); -} -function setLogTopic(topic){ - var inp=document.getElementById('log-filter'); - var active=inp.value===topic; - inp.value=active?'':topic; - document.querySelectorAll('.log-topic-btn').forEach(function(b){ - var on=!active&&b.getAttribute('data-topic')===topic; - b.style.background=on?'var(--accent)':'var(--raised)'; - b.style.color=on?'#fff':'var(--txt2)'; - }); - renderLog(); -} -function renderLog(){ - var el=document.getElementById('console-log'); - if(!el)return; - var filter=(document.getElementById('log-filter')||{}).value||''; - var fl=filter.toLowerCase(); - var rows=consoleLogs.filter(function(l){ - var m=l.msg; - if(logDirFilter==='rx'&&!/ RX[ (]/.test(m))return false; - if(logDirFilter==='tx'&&!/ TX[ (]/.test(m))return false; - if(fl&&!m.toLowerCase().includes(fl))return false; - return true; - }); - var savedScroll=logAutoScroll?null:el.scrollTop; - el.innerHTML=rows.map(l=>`
${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 post(url,body){return fetch(_apiUrl(url),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})} -function clamp(v,lo,hi){return Math.min(hi,Math.max(lo,v))} - -// ── Apply state to DOM ── -function applyState(){ - var s=S; - // connection error banner – nur wenn überhaupt ein Drucker konfiguriert ist - var banner=document.getElementById('conn-error-banner'); - if(banner){if(s.connection_error&&_printers.length>0){banner.textContent='⚠ '+(T.lbl_conn_error||'Connection error:')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}} - var frb=document.getElementById('file-ready-banner'); - if(frb){ - if(s.file_ready&&s.print_state==='standby'){ - document.getElementById('file-ready-name').textContent=s.file_ready; - frb.style.display='flex'; - }else{frb.style.display='none';} - } - // header - var b=document.getElementById('h-badge'); - b.className='hbadge '+s.print_state; - document.getElementById('h-state').textContent=T['kobra_'+s.kobra_state]||s.kobra_state||T.header_status_standby; - var _pn=_printers.length===0?'–':((_activePrinter&&_activePrinter.name)||s.printer_name); - var _el=document.getElementById('h-pname');if(_el)_el.textContent=_pn; - var _el2=document.getElementById('h-pname-single');if(_el2)_el2.textContent=_pn; - var hv=document.getElementById('h-version');if(hv&&s.version)hv.textContent='v'+s.version; - - - // temps - var nt=document.getElementById('d-nt');if(nt)nt.textContent=s.nozzle_temp.toFixed(1); - var ntt=document.getElementById('d-nt-t');if(ntt)ntt.textContent=s.nozzle_target.toFixed(0); - var bt=document.getElementById('d-bt');if(bt)bt.textContent=s.bed_temp.toFixed(1); - var btt=document.getElementById('d-bt-t');if(btt)btt.textContent=s.bed_target.toFixed(0); - - // temp bars (dashboard) - var nb=document.getElementById('d-ntbar');if(nb)nb.style.width=clamp(s.nozzle_temp/300*100,0,100)+'%'; - var bb=document.getElementById('d-btbar');if(bb)bb.style.width=clamp(s.bed_temp/120*100,0,100)+'%'; - - // progress - var pct=Math.round(s.progress*100); - var dpct=document.getElementById('d-pct');if(dpct)dpct.textContent=pct; - var dpbar=document.getElementById('d-pbar');if(dpbar)dpbar.style.width=pct+'%'; - - var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'–'; - var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers; - - var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration); - var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'–'; - var dslrow=document.getElementById('d-slicer-row'); - var dsltime=document.getElementById('d-slicer-time'); - if(dslrow&&dsltime){ - if(s.slicer_time>0){dslrow.style.display='';dsltime.textContent=fmtTime(s.slicer_time);} - else{dslrow.style.display='none';} - } - - var fn=s.filename||'–'; - var dfname=document.getElementById('d-fname');if(dfname){dfname.textContent=fn;dfname.title=fn}; - var pfname=document.getElementById('p-fname');if(pfname){pfname.textContent=fn;pfname.title=fn}; - var cfo=document.getElementById('cam-fname');if(cfo)cfo.textContent=fn!=='–'?fn:''; - - // thumbnail - var thumb=document.getElementById('d-thumbnail'); - if(thumb){ - if(s.thumbnail){ - thumb.src='data:image/png;base64,'+s.thumbnail; - thumb.style.display='block'; - } else { - thumb.style.display='none'; - thumb.src=''; - } - } - - // light/fan sync - document.getElementById('d-light-toggle').checked=s.light_on; - var dfan=document.getElementById('d-fan');if(dfan)dfan.value=s.fan_speed; - var dfanval=document.getElementById('d-fan-val');if(dfanval)dfanval.textContent=s.fan_speed; - - // speed mode buttons - var spdWidths={1:25,2:55,3:90}; - [1,2,3].forEach(function(m){ - var b=document.getElementById('d-spd-'+m); - if(b) b.classList.toggle('spd-active', s.print_speed_mode===m); - }); - var spdBar=document.getElementById('d-spd-bar'); - if(spdBar) spdBar.style.width=(spdWidths[s.print_speed_mode]||55)+'%'; - - // AMS - if(s.ams_slots&&s.ams_slots.length){ - window._amsSlots=s.ams_slots; - var html=''; - s.ams_slots.forEach(function(slot,i){ - var empty=slot.status!==5; - var rgb=empty?[80,80,80]:(Array.isArray(slot.color)?slot.color:[128,128,128]); - var col='rgb('+rgb[0]+','+rgb[1]+','+rgb[2]+')'; - var active=slot.status===1||slot.active; - var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'–'); - var idx=slot.index!=null?slot.index:i; - html+='
' - +'
' - +'
'+(empty?'–':(slot.type||slot.material_type||'–'))+'
' - +'
Slot '+(idx+1)+'
' - +'
'+pct+'
' - +'
' - +'
'; - }); - document.getElementById('ams-slots').innerHTML=html; - } - - // camera overlay - var co=document.getElementById('cam-overlay'); - if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none'; - - // auto-start camera during print - if(s.print_state==='printing'&&!camOn&&s.camera_url){ - camStart(); - } - - updateConnBtn(); -} - -function updateConnBtn(){ - var btn=document.getElementById('conn-btn'); - if(!btn)return; - var offline=S.kobra_state==='offline'; - if(offline){ - btn.className='conn-btn disconnected'; - btn.textContent=T.btn_connect||'⚡ Verbinden'; - } else { - btn.className='conn-btn connected'; - btn.textContent=T.btn_disconnect||'✕ Trennen'; - } -} - -function toggleConnection(){ - var btn=document.getElementById('conn-btn'); - var offline=S.kobra_state==='offline'; - btn.disabled=true; - btn.textContent='…'; - var url=offline?'/api/connect':'/api/disconnect'; - post(url,{}).then(function(r){return r.json()}).then(function(r){ - btn.disabled=false; - if(r.error)addLog('Error: '+r.error); - }).catch(function(){btn.disabled=false;}); -} - -// ── Temp history + chart ── -function updateHistory(){ - tempHistory.n.push(S.nozzle_temp); - tempHistory.b.push(S.bed_temp); - if(tempHistory.n.length>60)tempHistory.n.shift(); - if(tempHistory.b.length>60)tempHistory.b.shift(); - drawChart('d-chart',tempHistory,[{data:tempHistory.n,color:'#00c8ff',max:300},{data:tempHistory.b,color:'#ff6b35',max:120}]); -} -function drawChart(id,_,series){ - var canvas=document.getElementById(id);if(!canvas)return; - var ctx=canvas.getContext('2d'); - var W=canvas.offsetWidth*window.devicePixelRatio||canvas.width; - var H=canvas.offsetHeight*window.devicePixelRatio||canvas.height; - canvas.width=W;canvas.height=H; - ctx.clearRect(0,0,W,H); - series.forEach(function(s){ - var data=s.data;if(!data.length)return; - var max=s.max; - ctx.beginPath();ctx.strokeStyle=s.color;ctx.lineWidth=2;ctx.lineJoin='round'; - data.forEach(function(v,i){ - var x=i/(Math.max(data.length-1,1))*(W-4)+2; - var y=H-4-(v/max)*(H-8); - if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y); - }); - ctx.stroke(); - }); -} - -// ── Settings Modal ── -var _updateTag=''; -var _updateUrl=''; -function openSettings(){ - // Titel mit aktivem Drucker-Namen aktualisieren - var pname=_activePrinter&&_activePrinter.name?_activePrinter.name:null; - var title=document.getElementById('modal-title-settings'); - if(title)title.textContent=T.settings_title+(pname?' – '+pname:''); - fetch(_apiUrl('/api/settings')).then(function(r){return r.json()}).then(function(d){ - document.getElementById('s-printer-name').value=d.printer_name||''; - document.getElementById('s-printer-ip').value=d.printer_ip||''; - document.getElementById('s-mqtt-port').value=d.mqtt_port||9883; - document.getElementById('s-username').value=d.username||''; - document.getElementById('s-password').value=d.password||''; - document.getElementById('s-device-id').value=d.device_id||''; - document.getElementById('s-mode-id').value=d.mode_id||''; - document.getElementById('s-default-slot').value=d.default_ams_slot||'auto'; - document.getElementById('s-auto-leveling').checked=(d.auto_leveling===undefined?true:!!d.auto_leveling); - }); - var v=localStorage.getItem('pollInterval')||'2000'; - document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')}); - var pb=document.getElementById('poll-'+Math.round(parseInt(v)/1000)); - if(pb)pb.classList.add('active'); - document.getElementById('s-version-label').textContent='v'+('__VERSION__'||'?'); - document.getElementById('update-status').textContent=''; - document.getElementById('btn-update-apply').style.display='none'; - var cl=document.getElementById('update-changelog');if(cl)cl.style.display='none'; - _updateTag='';_updateUrl=''; - document.getElementById('settings-modal').classList.add('open'); -} -function closeSettings(){ - document.getElementById('settings-modal').classList.remove('open'); -} - -// ── AMS Slot Edit ── -var _slotEditIndex=-1; -var _MAT_PRESETS=['PLA','PETG','ABS','ASA','TPU','PA','PC','HIPS']; -function openSlotEdit(i){ - var slot=(window._amsSlots||[])[i]||{}; - var index=slot.index!=null?slot.index:i; - _slotEditIndex=index; - document.getElementById('slot-edit-title').textContent=T.slot_edit_title+' '+(index+1); - var rgb=Array.isArray(slot.color)?slot.color:[128,128,128]; - var hex='#'+rgb.map(function(v){return('0'+Math.min(255,v).toString(16)).slice(-2)}).join(''); - var ci=document.getElementById('slot-edit-color'); - ci.value=hex; - document.getElementById('slot-edit-preview').style.background=hex; - var mat=(slot.type||'PLA').toUpperCase(); - document.getElementById('slot-edit-mat').value=mat; - var btns=document.getElementById('slot-mat-btns'); - btns.innerHTML=_MAT_PRESETS.map(function(m){ - return ''; - }).join(''); - document.getElementById('slot-edit-modal').classList.add('open'); -} -function closeSlotEdit(){ - document.getElementById('slot-edit-modal').classList.remove('open'); -} -function startReadyFile(){ - var btn=document.getElementById('file-ready-btn'); - if(btn){btn.disabled=true;btn.textContent='…';} - post('/printer/print/start',{filename:S.file_ready}) - .then(function(r){return r.json();}) - .then(function(r){ - document.getElementById('file-ready-banner').style.display='none'; - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} - }) - .catch(function(e){ - clog((T.log_error||'Error:')+' '+e,'msg-err'); - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} - }); -} -function cancelReadyFile(){ - post('/api/file_ready/clear',{}) - .then(function(){document.getElementById('file-ready-banner').style.display='none';}); -} -function selectMatPreset(m){ - document.getElementById('slot-edit-mat').value=m; - highlightMatBtn(m); -} -function highlightMatBtn(val){ - document.querySelectorAll('.mat-preset-btn').forEach(function(b){ - var on=b.getAttribute('data-mat')===val.toUpperCase(); - b.style.background=on?'var(--accent)':'var(--raised)'; - b.style.color=on?'#fff':'var(--txt2)'; - }); -} -function hexToRgb(hex){ - var r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16); - return[r,g,b]; -} -function saveSlotEdit(){ - var hex=document.getElementById('slot-edit-color').value; - var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA'; - var color=hexToRgb(hex); - post('/api/ams/set_slot',{index:_slotEditIndex,type:mat,color:color}) - .then(function(r){return r.json();}) - .then(function(r){ - closeSlotEdit(); - clog((T.slot_edit_ok||'AMS Slot')+' '+(_slotEditIndex+1)+': '+mat+' '+hex,'msg-ok'); - }) - .catch(function(e){clog('Fehler: '+e,'msg-err');}); -} -document.addEventListener('DOMContentLoaded',function(){ - document.getElementById('s-printer-ip').addEventListener('input',function(){ - var hint=document.getElementById('lbl-ip-hint'); - if(this.value.includes(':')){hint.textContent=T.hint_ip_no_port;hint.style.display='block';} - else{hint.style.display='none';} - }); -}); -function setPoll(ms){ - document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')}); - var id='poll-'+Math.round(ms/1000); - var pb=document.getElementById(id);if(pb)pb.classList.add('active'); - localStorage.setItem('pollInterval',ms); - clearInterval(pollTimer); - pollTimer=setInterval(poll,ms); -} -function saveSettings(){ - var btn=document.getElementById('btn-save-settings'); - btn.disabled=true;btn.textContent='…'; - post('/api/settings',{ - printer_name: document.getElementById('s-printer-name').value, - printer_ip: document.getElementById('s-printer-ip').value, - mqtt_port: parseInt(document.getElementById('s-mqtt-port').value)||9883, - username: document.getElementById('s-username').value, - password: document.getElementById('s-password').value, - device_id: document.getElementById('s-device-id').value, - mode_id: document.getElementById('s-mode-id').value, - default_ams_slot: document.getElementById('s-default-slot').value, - auto_leveling: document.getElementById('s-auto-leveling').checked?1:0, - }).then(function(){ - btn.textContent=T.update_restarting; - setTimeout(function(){ - btn.disabled=false; - setText('btn-save-settings',T.settings_save); - closeSettings(); - poll(); - },4000); - }).catch(function(e){ - btn.disabled=false;setText('btn-save-settings',T.settings_save); - clog('Settings-Fehler: '+e,'msg-err'); - }); -} -function checkUpdate(){ - var sb=document.getElementById('update-status'); - sb.textContent=T.update_checking; - document.getElementById('btn-update-apply').style.display='none'; - _updateTag='';_updateUrl=''; - fetch(_apiUrl('/api/update/check')).then(function(r){return r.json()}).then(function(d){ - if(d.error){sb.textContent=T.update_error+': '+d.error;return;} - var cl=document.getElementById('update-changelog'); - if(d.changelog&&d.changelog.trim()){cl.textContent=d.changelog;cl.style.display='block';} - else{cl.style.display='none';} - if(d.update_available){ - sb.textContent='v'+d.latest+' '+T.update_available; - sb.style.color='var(--ok)'; - _updateTag=d.tag;_updateUrl=d.download_url; - document.getElementById('btn-update-apply').style.display='inline-block'; - } else { - sb.textContent=T.update_none; - sb.style.color='var(--txt2)'; - } - }).catch(function(e){sb.textContent=T.update_error+': '+e;}); -} -function applyUpdate(){ - if(!_updateUrl)return; - var sb=document.getElementById('update-status'); - var btn=document.getElementById('btn-update-apply'); - btn.disabled=true;sb.textContent=T.update_applying; - post('/api/update/apply',{download_url:_updateUrl,tag:_updateTag}).then(function(){ - sb.textContent=T.update_restarting; - closeSettings(); - setTimeout(function(){poll();},5000); - }).catch(function(e){ - btn.disabled=false;sb.textContent=T.update_error+': '+e; - }); -} - -// ── Poll ── -async function poll(){ - try{ - var r=await fetch(_apiUrl('/api/state')); - if(!r.ok)return; - var d=await r.json(); - Object.assign(S,d); - applyState(); - updateHistory(); - }catch(e){clog(T.log_poll_error+' '+e,'msg-err')} -} -var pollTimer; -(function(){ - var ms=parseInt(localStorage.getItem('pollInterval')||'2000'); - initPrinters(); - poll();pollTimer=setInterval(poll,ms); -})(); - -// ── Print actions ── -function printAction(a){ - post('/printer/print/'+a,{}).then(function(){clog('Druck: '+a,'msg-ok');poll()}) - .catch(function(e){clog('Fehler: '+e,'msg-err')}); -} -function confirmCancel(){if(confirm('Druck wirklich abbrechen?'))printAction('cancel')} - -// ── Axis motion ── -// axis codes: 0=X, 1=Y, 2=Z -// move_type 1=relative, distance positive/negative -function getStep(){return currentStep} -function setStep(btn,v){ - currentStep=v; - document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active')); - btn.classList.add('active'); - document.getElementById('step-display').textContent=v; -} -function move(axis,dir,dist){ - // axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z - var axisMap={0:1,1:2,2:3}; - post('/api/axis',{axis:axisMap[axis],move_type:1,distance:dir*dist}) - .then(function(){clog('Achse '+(axis===0?'X':axis===1?'Y':'Z')+' '+(dir>0?'+':'')+dir*dist+'mm','msg-ok')}) - .catch(function(e){clog('Achse-Fehler: '+e,'msg-err')}); -} -function homeAll(){ - post('/api/axis',{axis:5,move_type:2,distance:0}) - .then(function(){clog('Home All','msg-ok')}) - .catch(function(e){clog('Home-Fehler: '+e,'msg-err')}); -} -function homeXY(){ - post('/api/axis',{axis:4,move_type:2,distance:0}) - .then(function(){clog('Home XY','msg-ok')}) - .catch(function(e){clog('Home-Fehler: '+e,'msg-err')}); -} -function homeZ(){ - post('/api/axis',{axis:3,move_type:2,distance:0}) - .then(function(){clog('Home Z','msg-ok')}) - .catch(function(e){clog('Home-Fehler: '+e,'msg-err')}); -} -function disableMotors(){ - post('/api/axis',{action:'turnOff'}) - .then(function(){clog('Motors Off','msg-ok')}) - .catch(function(e){clog('Motors-Fehler: '+e,'msg-err')}); -} - -// ── Temperature ── -function setNozzle(){ - var v=parseFloat(document.getElementById('p-nozzle-inp').value||0); - post('/api/temperature',{nozzle:v,bed:S.bed_target}) - .then(function(){clog('Nozzle → '+v+'°C','msg-ok')}) - .catch(function(e){clog('Temp-Fehler: '+e,'msg-err')}); -} -function setBed(){ - var v=parseFloat(document.getElementById('p-bed-inp').value||0); - post('/api/temperature',{nozzle:S.nozzle_target,bed:v}) - .then(function(){clog(T.label_bed+' → '+v+'°C','msg-ok')}) - .catch(function(e){clog('Temp-Fehler: '+e,'msg-err')}); -} - -// ── Light ── -function setLight(){ - var on=document.getElementById('d-light-toggle').checked; - post('/api/light',{on:on,brightness:80}) - .then(function(){clog('Licht '+(on?'an, '+br+'%':'aus'),'msg-ok')}) - .catch(function(e){clog('Licht-Fehler: '+e,'msg-err')}); -} - -// ── Print Speed ── -function setSpeed(mode){ - S.print_speed_mode=mode; - [1,2,3].forEach(function(m){ - var b=document.getElementById('d-spd-'+m); - if(b) b.classList.toggle('spd-active',m===mode); - }); - post('/api/speed',{mode:mode}) - .catch(function(e){clog('Speed-Fehler: '+e,'msg-err')}); -} - -// ── Fan ── -function setFan(){ - var v=parseInt(document.getElementById('d-fan').value); - document.getElementById('d-fan-val').textContent=v; - post('/api/fan',{speed:v}) - .then(function(){clog('Lüfter → '+v+'%','msg-ok')}) - .catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')}); -} -function quickFan(v){ - document.getElementById('d-fan').value=v; - document.getElementById('d-fan-val').textContent=v; - post('/api/fan',{speed:v}) - .then(function(){clog('Lüfter → '+v+'%','msg-ok')}) - .catch(function(e){clog('Lüfter-Fehler: '+e,'msg-err')}); -} - -// ── AMS ── -function amsFeed(type){ - var slot=parseInt(document.getElementById('ams-slot-sel').value); - post('/api/ams/feed',{slot_index:slot,type:type}) - .then(function(){clog((type===1?T.lbl_feed:T.lbl_unload)+' Slot '+(slot+1),'msg-ok')}) - .catch(function(e){clog('AMS-Fehler: '+e,'msg-err')}); -} - -// ── Camera ── -function camStart(){ - var img=document.getElementById('cam-img'); - var ph=document.getElementById('cam-placeholder'); - var sp=document.getElementById('cam-spinner'); - ph.style.display='none'; - img.style.display='none'; - sp.style.display='block'; - post('/api/camera/start',{}).then(function(){ - img.onerror=function(){ - sp.style.display='none'; - img.style.display='none'; - ph.style.display='flex'; - camOn=false; - document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera'; - clog((T.log_error||'Fehler:')+' Stream nicht verfügbar','msg-err'); - }; - img.src='/api/camera/stream?t='+Date.now(); - camOn=true; - document.getElementById('cam-toggle-btn').textContent=T.btn_cam_stop||'◼ Kamera'; - clog((T.log_cam_start||'Kamera gestartet'),'msg-ok'); - // MJPEG liefert kein onload – Spinner nach kurzem Timeout ausblenden - setTimeout(function(){ - sp.style.display='none'; - img.style.display='block'; - },1200); - }).catch(function(e){ - sp.style.display='none'; - ph.style.display='flex'; - clog((T.log_error||'Fehler:')+' '+e,'msg-err'); - }); -} -function camStop(){ - post('/api/camera/stop',{}).then(function(){ - var img=document.getElementById('cam-img'); - img.src=''; - img.style.display='none'; - document.getElementById('cam-spinner').style.display='none'; - document.getElementById('cam-placeholder').style.display='flex'; - camOn=false; - document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera'; - clog(T.log_cam_stop||'Kamera gestoppt','msg-ok'); - }).catch(function(e){clog((T.log_error||'Fehler:')+' '+e,'msg-err')}); -} -function toggleCam(){if(camOn)camStop();else camStart()} - -// ── GCode Store ── -var storeFiles=[]; - -function loadStore(){ - fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){ - storeFiles=d.result||[]; - renderStore(); - }).catch(function(e){clog('Store-Fehler: '+e,'msg-err')}); -} - -function renderStore(){ - var grid=document.getElementById('store-grid'); - var empty=document.getElementById('store-empty'); - - // Suche - var q=(document.getElementById('store-search')||{value:''}).value.toLowerCase().trim(); - // Filter - var filter=(document.getElementById('store-filter')||{value:'all'}).value; - // Sortierung - var sort=(document.getElementById('store-sort')||{value:'date_desc'}).value; - - var files=storeFiles.filter(function(f){ - if(q&&f.filename.toLowerCase().indexOf(q)===-1) return false; - if(filter==='completed'&&f.last_print_status!=='completed') return false; - if(filter==='failed'&&(f.last_print_status!=='cancelled'&&f.last_print_status!=='failed')) return false; - if(filter==='never'&&f.last_print_status) return false; - return true; - }); - - files.sort(function(a,b){ - if(sort==='name_asc') return a.filename.localeCompare(b.filename); - if(sort==='duration_asc'){ - var da=a.last_print_duration||a.est_print_time_sec||0; - var db=b.last_print_duration||b.est_print_time_sec||0; - return da-db; - } - // date_desc (default) - return (b.uploaded_at||'').localeCompare(a.uploaded_at||''); - }); - - if(!storeFiles.length){ - empty.textContent=T.store_empty; - grid.innerHTML=''; - empty.style.display='block'; - return; - } - if(!files.length){ - empty.textContent=T.store_no_results; - grid.innerHTML=''; - empty.style.display='block'; - return; - } - empty.style.display='none'; - grid.innerHTML=files.map(function(f){ - var thumb=f.thumbnail_b64 - ? '' - : '
🖨
'; - 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 storePrint(fileId, filename){ - _storeFileId=fileId; - _storeFilename=filename; - _filamentDialogMode='store'; - // GCode-Filamente aus Store-Datei holen (für Vorschau im Dialog) - var fileObj=storeFiles.find(function(f){return f.id===fileId;}); - try{ _gcodeFilaments=fileObj&&fileObj.gcode_filaments?JSON.parse(fileObj.gcode_filaments):[]; } - catch(e){ _gcodeFilaments=[]; } - fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ - openFilamentDialog(d.result||[]); - }).catch(function(){openFilamentDialog([]);}); -} - -function startReadyFileWithSlots(){ - _filamentDialogMode='banner'; - _storeFilename=S.file_ready||''; - fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ - openFilamentDialog(d.result||[]); - }).catch(function(){openFilamentDialog([]);}); -} - -var _amsSlots=[]; - -function openFilamentDialog(slots){ - _amsSlots=slots.filter(function(s){return s.status==='loaded';}); - var dlg=document.getElementById('filament-dialog'); - var title=document.getElementById('fd-title'); - var body=document.getElementById('fd-slots'); - if(title)title.textContent='▶ '+_storeFilename; - - // GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten - var channels=_gcodeFilaments.length?_gcodeFilaments:_amsSlots.map(function(s,i){ - return {slot_index:i,color_hex:s.color_hex,material:s.material}; - }); - - if(!_amsSlots.length){ - body.innerHTML='

Keine belegten AMS-Slots.
Druck trotzdem starten?

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

Keine belegten AMS-Slots.
Druck trotzdem starten?

'; - } else { - body.innerHTML=channels.map(function(gc,i){ - // Passende Slots: gleicher Materialtyp - var compatible=_amsSlots.filter(function(s){return s.material.toUpperCase()===gc.material.toUpperCase();}); - if(!compatible.length) compatible=_amsSlots; // Fallback: alle - // Standard-Auswahl: Slot mit gleichem Index oder erster kompatibler - var defaultSlot=compatible.find(function(s){return s.slot_index===gc.slot_index;})||compatible[0]; - var opts=compatible.map(function(s){ - var sel=(s.slot_index===defaultSlot.slot_index)?'selected':''; - return ''; - }).join(''); - return '
'+ - ''+ - 'Kanal '+(i+1)+''+ - ''+gc.material+''+ - ''+ - ''+ - '
'; - }).join(''); - } - if(dlg)dlg.classList.add('open'); -} - -function closeFilamentDialog(){ - var dlg=document.getElementById('filament-dialog'); - if(dlg)dlg.classList.remove('open'); -} - -function confirmFilamentPrint(){ - var selects=document.querySelectorAll('#fd-slots select'); - var assignments=[]; - selects.forEach(function(sel){ - var paintIdx=parseInt(sel.dataset.paint); - var paintColor=sel.dataset.paintColor; - var opt=sel.options[sel.selectedIndex]; - var amsIdx=parseInt(opt.value); - var amsSlot=_amsSlots.find(function(s){return s.slot_index===amsIdx;})||{}; - // Farbe als [R,G,B,255] - function hexToRgba(h){ - var c=h.replace('#',''); - if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]; - return [parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16),255]; - } - assignments.push({ - paint_index: paintIdx, - slot_index: amsIdx, - material: opt.dataset.material||'PLA', - paint_color: hexToRgba(paintColor||'#ffffff'), - ams_color: hexToRgba(amsSlot.color_hex||'#ffffff'), - }); - }); - closeFilamentDialog(); - if(_filamentDialogMode==='banner'){ - // Banner-Modus: normaler print/start mit Slot-Override - var btn=document.getElementById('file-ready-btn'); - if(btn){btn.disabled=true;btn.textContent='…';} - post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments}) - .then(function(r){return r.json();}) - .then(function(){ - document.getElementById('file-ready-banner').style.display='none'; - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} - }) - .catch(function(e){ - clog((T.log_error||'Error:')+' '+e,'msg-err'); - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} - }); - } else { - // Store-Modus: POST /kx/print - fetch(_apiUrl('/kx/print'),{ - method:'POST', - headers:{'Content-Type':'application/json'}, - body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments}) - }).then(function(r){return r.json()}).then(function(d){ - if(d.result==='ok'){clog('Druckstart: '+_storeFilename,'msg-ok');showPanel('dashboard');} - else{clog('Druckfehler: '+(d.error||'?'),'msg-err');} - }).catch(function(e){clog('Druckfehler: '+e,'msg-err');}); - } -} - -function storeDelete(fileId){ - if(!confirm(T.store_delete_confirm)) return; - fetch(_apiUrl('/kx/files/'+fileId),{method:'DELETE'}).then(function(r){ - if(r.ok){loadStore();} - else{clog('Löschen fehlgeschlagen','msg-err');} - }); -} - -// ── Drucker hinzufügen ── -function openAddPrinterDialog(){ - document.getElementById('apd-ip').value=''; - document.getElementById('apd-name').value=''; - var st=document.getElementById('apd-status');st.textContent='';st.style.color='var(--txt2)'; - document.getElementById('apd-confirm').disabled=false; - document.getElementById('add-printer-dialog').classList.add('open'); -} -function closeAddPrinterDialog(){ - document.getElementById('add-printer-dialog').classList.remove('open'); -} -function confirmAddPrinter(){ - var ip=document.getElementById('apd-ip').value.trim(); - var name=document.getElementById('apd-name').value.trim(); - var st=document.getElementById('apd-status'),btn=document.getElementById('apd-confirm'); - if(!ip){st.textContent=T.apd_err_ip;st.style.color='var(--err)';return;} - st.textContent=T.apd_fetching;st.style.color='var(--txt2)';btn.disabled=true; - fetch('/kx/printers/add',{method:'POST',headers:{'Content-Type':'application/json'}, - body:JSON.stringify({printer_ip:ip,name:name})}) - .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) - .then(function(res){ - if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;} - st.textContent=T.apd_success;st.style.color='var(--ok)'; - setTimeout(function(){location.reload();},2500); - }) - .catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;}); -} -function removePrinter(id,name){ - if(!confirm(T.printers_remove_confirm.replace('{name}',name)))return; - fetch('/kx/printers/'+encodeURIComponent(id),{method:'DELETE'}) - .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) - .then(function(res){ - if(!res.ok){alert((res.j&&res.j.error)||'Fehler');return;} - setTimeout(function(){location.href='/printer1';},2000); - }) - .catch(function(e){alert(''+e);}); -} - -// ── Drucker-Tab ── -function loadPrinterTab(){ - var grid=document.getElementById('printers-grid'); - if(grid)grid.innerHTML='
'+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/new1/index.html b/web/themes/new1/index.html deleted file mode 100644 index ce6c6e7..0000000 --- a/web/themes/new1/index.html +++ /dev/null @@ -1,492 +0,0 @@ - - - - - -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 -
-
- - - - - -
-
- - -
-
AMS / Filamentbox
-
-
Keine AMS-Daten empfangen
-
-
-
Slot auswählen
-
- - Slot 1 -
-
- - -
-
-
-
-
- - - -
-
-
- 🖨 Drucker -
- - -
-
-
-
-
- - -
-
-
- 🗂 Datei-Browser - -
-
- - - -
- -
-
-
- -
-
-
- Ereignis-Log - ⬇ Download -
-
- - - -
-
- Dir: - - - - Topic: - - - - -
-
-
-
-
-
- - - - - - - - -
- © ViewIT 2026 -
- - - \ No newline at end of file diff --git a/web/themes/new1/style.css b/web/themes/new1/style.css deleted file mode 100644 index 020ca7a..0000000 --- a/web/themes/new1/style.css +++ /dev/null @@ -1,277 +0,0 @@ -: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-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: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)} -.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 */ - .ams-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} -}