diff --git a/.gitignore b/.gitignore index d86da6a..d3d0f8c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist/ releases/*/kx-bridge releases/*/extract_credentials releases/*/extract_credentials.exe +config/config.json diff --git a/Dockerfile b/Dockerfile index d1682c3..276bc25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ 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 3a9c73a..a9366b7 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -11,6 +11,7 @@ OrcaSlicer-Konfiguration: """ import argparse +import html import sqlite3 import uuid try: @@ -114,6 +115,14 @@ logging.basicConfig(level=logging.INFO, datefmt="%H:%M:%S") log = logging.getLogger("bridge") +# Web-UI: Unterverzeichnis unter web/themes//index.html +_UI_THEME_NAME_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$") +# Erlaubte statische Theme-Dateien unter /kx/ui/ +_KX_UI_ASSETS: dict[str, str] = { + "style.css": "text/css", + "app.js": "application/javascript", +} + # Ring-Buffer für Browser-Log-Stream (letzte 200 Einträge) import collections as _collections _log_buffer: "_collections.deque[dict]" = _collections.deque(maxlen=500) @@ -459,6 +468,15 @@ class KobraXBridge: # Part-Skip: zuletzt vom Drucker gemeldete Skip-Liste (v0.9.10) self._skip_state: dict = {"objects": [], "skipped": [], "ts": 0} + # Theme-Name prüfen (keine Sonderzeichen oder Umlaute) + raw_theme = (getattr(args, "ui_theme", None) or "default").strip() + if not _UI_THEME_NAME_RE.match(raw_theme): + log.warning("Ungültiger UI-Theme-Name %r – nutze default", raw_theme) + raw_theme = "default" + self._ui_theme = raw_theme + self._index_tpl_cache: str | None = None + self._index_tpl_cache_key: tuple[str, float] | None = None + # Register MQTT push callbacks client.callbacks["tempature/report"] = self._on_temp client.callbacks["print/report"] = self._on_print @@ -1461,2300 +1479,63 @@ 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): - 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}'") + try: + tpl = self._load_index_template_cached() + except OSError: + p = self._theme_index_path() + log.error("Web-UI Theme-Datei fehlt oder nicht lesbar: %s (Theme: %s)", p, self._ui_theme) + return web.Response( + text="
KX-Bridge: index.html nicht gefunden.\nErwartet:\n"
+                + html.escape(p, quote=True)
+                + "
", + status=500, + content_type="text/html; charset=utf-8", + ) + html = tpl.replace("__UI_ASSETS_VER__", self._ui_asset_cache_buster()) return web.Response(text=html, content_type="text/html", headers={"Cache-Control": "no-store, no-cache, must-revalidate"}) @@ -4834,6 +2615,7 @@ def build_app(bridge: KobraXBridge) -> web.Application: r.add_delete("/kx/files/{file_id}", bridge.handle_kx_file_delete) r.add_get("/kx/filament/slots", bridge.handle_kx_filament_slots) r.add_get("/kx/history", bridge.handle_kx_history) + r.add_get("/kx/ui/{name}", bridge.handle_kx_ui_asset) r.add_get("/kx/files/{id}/objects", bridge.handle_kx_file_objects) r.add_post("/kx/skip", bridge.handle_kx_skip) r.add_post("/kx/skip/query", bridge.handle_kx_skip_query) @@ -5003,6 +2785,13 @@ def main(): help="HTTP/WS-Port (Moonraker-Standard: 7125)") parser.add_argument("--data-dir", default=_default_data_dir(), help="Persistenz-Verzeichnis für GCode-Store und DB") + parser.add_argument( + "--ui-theme", + default=os.environ.get("KX_UI_THEME", "default"), + metavar="NAME", + help="Web-UI-Theme (Ordner web/themes/NAME/, Standard: default). " + "Alternativ: Umgebungsvariable KX_UI_THEME.", + ) args = parser.parse_args() if args.printer_ip and ":" in args.printer_ip: args.printer_ip = args.printer_ip.split(":")[0] diff --git a/web/themes/THEME-ID-CHECKLIST.md b/web/themes/THEME-ID-CHECKLIST.md new file mode 100644 index 0000000..1fbcd29 --- /dev/null +++ b/web/themes/THEME-ID-CHECKLIST.md @@ -0,0 +1,338 @@ +# 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 new file mode 100644 index 0000000..0eb11e2 --- /dev/null +++ b/web/themes/THEME-JS-HOOKS.md @@ -0,0 +1,290 @@ +# 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 new file mode 100644 index 0000000..5849baf --- /dev/null +++ b/web/themes/default/app.js @@ -0,0 +1,1494 @@ +// ── 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', + skip_title:'✂ Objekte überspringen',skip_hint:'Objekte abwählen, die nicht weiter gedruckt werden sollen:', + skip_btn_label:'Objekte',skip_no_objects:'Keine Objekte in diesem Druck.', + skip_already:'übersprungen',skip_select_at_least_one:'Bitte mindestens ein Objekt wählen.', + skip_sending:'Sende …',skip_success:'Objekte werden übersprungen.', + fd_objects_hint:'Objekte überspringen (optional):', + add_printer:'Drucker hinzufügen',apd_lbl_ip:'Drucker-IP',apd_lbl_name:'Name (optional)', + apd_fetching:'Hole Daten vom Drucker…',apd_success:'Drucker hinzugefügt, Bridge startet neu…',apd_err_ip:'Bitte IP-Adresse eingeben', + printers_remove:'Drucker entfernen',printers_remove_confirm:'Drucker "{name}" entfernen? Die Bridge startet neu.', + printers_active:'● aktiv', + printers_switch:'Wechseln →', + printers_current:'Aktueller Drucker', + printers_loading:'Lade…', + printers_none:'Keine Drucker konfiguriert.', + printers_empty_hint:'Noch kein Drucker eingerichtet.', + nav_browser:'Browser', + panel_browser_title:'Datei-Browser', + store_empty:'Noch keine Dateien hochgeladen.', + store_refresh:'↻ Aktualisieren', + store_print:'▶ Drucken', + store_delete_confirm:'Datei löschen?', + store_print_confirm:'Datei drucken?', + store_no_results:'Keine Dateien gefunden.', + store_never:'noch nicht gedruckt', + sf_all:'Alle',sf_ok:'✓ Erfolgreich',sf_err:'✗ Fehler',sf_new:'Neu', + ss_date:'↓ Datum',ss_name:'A–Z Name',ss_dur:'⏱ Druckzeit' +}; +var LANG_EN={ + header_status_standby:'Ready',header_status_printing:'Printing',header_status_complete:'Complete',header_status_error:'Error', + kobra_free:'Ready',kobra_busy:'Busy',kobra_printing:'Printing',kobra_preheating:'Preheating',kobra_auto_leveling:'Auto Leveling',kobra_checking:'Checking',kobra_updated:'Updating',kobra_init:'Initializing',kobra_pausing:'Pausing...',kobra_paused:'Paused',kobra_resuming:'Resuming...',kobra_resumed:'Resumed',kobra_stopping:'Stopping...',kobra_stoped:'Stopped',kobra_finished:'Finished',kobra_failed:'Error',kobra_canceled:'Cancelled',kobra_offline:'Offline', + nav_dashboard:'Dashboard',nav_print:'Print',nav_temps:'Temperatures',nav_motion:'Motion',nav_ams:'AMS',nav_extras:'Light / Fan',nav_console:'Console', + card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera',lbl_elapsed:'Elapsed:',lbl_remaining:'Remaining:',lbl_slicer_time:'Slicer estimate:',lbl_layers:'Layer', + speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport', + lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload', + 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', + skip_title:'✂ Skip objects',skip_hint:'Uncheck objects you no longer want to print:', + skip_btn_label:'Objects',skip_no_objects:'No objects in this print.', + skip_already:'skipped',skip_select_at_least_one:'Please pick at least one object.', + skip_sending:'Sending …',skip_success:'Objects will be skipped.', + fd_objects_hint:'Skip objects (optional):', + add_printer:'Add printer',apd_lbl_ip:'Printer IP',apd_lbl_name:'Name (optional)', + apd_fetching:'Fetching data from printer…',apd_success:'Printer added, bridge restarting…',apd_err_ip:'Please enter an IP address', + printers_remove:'Remove printer',printers_remove_confirm:'Remove printer "{name}"? The bridge will restart.', + printers_active:'● active', + printers_switch:'Switch →', + printers_current:'Current printer', + printers_loading:'Loading…', + printers_none:'No printers configured.', + printers_empty_hint:'No printer set up yet.', + nav_browser:'Browser', + panel_browser_title:'File Browser', + store_empty:'No files uploaded yet.', + store_refresh:'↻ Refresh', + store_print:'▶ Print', + store_delete_confirm:'Delete file?', + store_print_confirm:'Print file?', + store_no_results:'No files found.', + store_never:'never printed', + sf_all:'All',sf_ok:'✓ Completed',sf_err:'✗ Failed',sf_new:'New', + ss_date:'↓ Date',ss_name:'A–Z Name',ss_dur:'⏱ Print time' +}; +// Multi-Printer: BASE_URL aus Pathname (/printer2 → andere Bridge-Instanz) +var _printers=[]; +var _activePrinter=null; +(function(){ + var path=window.location.pathname.replace(/\/+$/,''); + var m=path.match(/^\/printer(\d+)$/); + var idx=m?parseInt(m[1]):1; + window._printerIndex=idx; +})(); +function _apiUrl(path){ + if(_activePrinter&&_activePrinter.bridge_url){ + return _activePrinter.bridge_url.replace(/\/+$/,'')+path; + } + return path; +} +function initPrinters(){ + fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){ // immer lokale Instanz für Drucker-Liste + _printers=d.result||[]; + var idx=window._printerIndex||1; + _activePrinter=_printers.find(function(p){return String(p.id)===String(idx)})||_printers[0]||null; + renderPrinterDropdown(); + }).catch(function(){}); +} +function renderPrinterDropdown(){ + var wrap=document.getElementById('printer-dropdown-wrap'); + var single=document.getElementById('h-pname-single'); + var name=_printers.length===0?'–':(_activePrinter?(_activePrinter.name||'Kobra X'):'Kobra X'); + var pname=document.getElementById('h-pname'); + if(pname)pname.textContent=name; + if(single)single.textContent=name; + if(_printers.length>1){ + if(wrap)wrap.style.display=''; + if(single)single.style.display='none'; + var menu=document.getElementById('printer-dropdown-menu'); + if(menu){ + menu.innerHTML=_printers.map(function(p){ + var active=_activePrinter&&String(p.id)===String(_activePrinter.id); + var num=p.id; + return ''+ + (active?'▶ ':'')+p.name+''; + }).join(''); + } + } else { + if(wrap)wrap.style.display='none'; + if(single)single.style.display=''; + } +} +function togglePrinterDropdown(){ + var menu=document.getElementById('printer-dropdown-menu'); + if(menu)menu.style.display=menu.style.display==='none'?'block':'none'; +} +document.addEventListener('click',function(e){ + var wrap=document.getElementById('printer-dropdown-wrap'); + if(wrap&&!wrap.contains(e.target)){ + var menu=document.getElementById('printer-dropdown-menu'); + if(menu)menu.style.display='none'; + } +}); + +var currentLang='de'; +var T=LANG_DE; +function toggleLang(){ + currentLang=currentLang==='de'?'en':'de'; + T=currentLang==='de'?LANG_DE:LANG_EN; + localStorage.setItem('lang',currentLang); + document.getElementById('lang-btn').textContent=currentLang==='de'?'EN':'DE'; + document.documentElement.setAttribute('lang',currentLang); + applyLang(); +} +function applyLang(){ + // Nav + var nb=document.getElementById('nb-dashboard');if(nb)nb.querySelector('.nav-text').textContent=T.nav_dashboard; + nb=document.getElementById('nb-console');if(nb)nb.querySelector('.nav-text').textContent=T.nav_console; + nb=document.getElementById('nb-printers');if(nb)nb.querySelector('.nav-text').textContent=T.nav_printers; + nb=document.getElementById('nb-store');if(nb)nb.querySelector('.nav-text').textContent=T.nav_browser; + // Bottom nav + var bnb=document.getElementById('bnb-dashboard');if(bnb)bnb.lastChild.textContent=T.nav_dashboard; + bnb=document.getElementById('bnb-console');if(bnb)bnb.lastChild.textContent=T.nav_console; + bnb=document.getElementById('bnb-printers');if(bnb)bnb.lastChild.textContent=T.nav_printers; + bnb=document.getElementById('bnb-store');if(bnb)bnb.lastChild.textContent=T.nav_browser; + // Browser panel + setText('printers-panel-title','🖨 '+T.nav_printers); + setText('add-printer-btn-label',T.add_printer); + setText('apd-title',T.add_printer); + setText('skip-title',T.skip_title); + setText('skip-hint',T.skip_hint); + setText('d-btn-skip-label',T.skip_btn_label); + setText('fd-objects-hint',T.fd_objects_hint); + setText('apd-lbl-ip',T.apd_lbl_ip); + setText('apd-lbl-name',T.apd_lbl_name); + setText('store-panel-title','🗂 '+T.panel_browser_title); + var srb=document.getElementById('store-refresh-btn');if(srb)srb.textContent=T.store_refresh; + setText('store-empty',T.store_empty); + setText('sf-all',T.sf_all);setText('sf-ok',T.sf_ok);setText('sf-err',T.sf_err);setText('sf-new',T.sf_new); + setText('ss-date',T.ss_date);setText('ss-name',T.ss_name);setText('ss-dur',T.ss_dur); + // Dashboard card titles + setText('d-card-progress',T.card_progress); + setText('d-card-temps',T.card_temps); + setText('d-card-lightfan',T.card_light_fan); + setText('d-card-speed',T.card_speed); + setText('d-card-cam',T.card_cam); + setText('d-card-ams',T.panel_ams_title); + setText('d-lbl-elapsed',T.lbl_elapsed); + setText('d-lbl-remain',T.lbl_remaining); + setText('d-slicer-label',T.lbl_slicer_time); + setText('d-lbl-layers',T.lbl_layers); + setText('d-lbl-light',T.lbl_light); + setText('d-lbl-bed',T.label_bed); + // Dashboard buttons + setText('d-btn-pause',T.btn_pause); + setText('d-btn-resume',T.btn_resume); + setText('d-btn-cancel',T.btn_cancel); + setText('cam-toggle-btn',camOn?T.btn_cam_stop:T.btn_cam_start); + setText('cam-placeholder-txt',T.cam_placeholder); + // Temp labels + document.querySelectorAll('.lbl-set').forEach(e=>e.textContent=T.label_set); + document.querySelectorAll('.lbl-off').forEach(e=>e.textContent=T.label_off); + setText('d-chart-label',T.panel_temps_chart); + // Axis labels + setText('ptitle-motion-xy',T.panel_motion_xy); + setText('ptitle-motion-z',T.panel_motion_z); + document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z); + document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy); + document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all); + document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors); + document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step); + document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':',''))); + // Console + setText('ptitle-console',T.panel_console_title); + // Settings modal + setText('modal-title-settings',T.settings_title); + setText('modal-sec-connection',T.settings_connection); + setText('modal-sec-print',T.settings_print); + setText('modal-sec-poll',T.settings_poll); + setText('modal-sec-version',T.settings_version); + setText('btn-save-settings',T.settings_save); + setText('lbl-printer-name',T.settings_printer_name); + setText('lbl-printer-ip',T.settings_printer_ip); + setText('lbl-mqtt-port',T.settings_mqtt_port); + setText('lbl-username',T.settings_username); + setText('lbl-password',T.settings_password); + setText('lbl-device-id',T.settings_device_id); + setText('lbl-mode-id',T.settings_mode_id); + setText('lbl-default-slot',T.settings_default_slot); + setText('opt-slot-auto',T.settings_slot_auto); + setText('lbl-auto-leveling',T.settings_auto_leveling); + + setText('lbl-update-check',T.update_check); + setText('lbl-update-apply',T.update_apply); + // Speed buttons + setText('d-spd-lbl-1',T.speed_silent.replace(/^\S+\s/,'')); + setText('d-spd-lbl-2',T.speed_normal.replace(/^\S+\s/,'')); + setText('d-spd-lbl-3',T.speed_sport.replace(/^\S+\s/,'')); + // AMS feed/unload + document.querySelectorAll('.lbl-feed').forEach(e=>e.textContent=T.lbl_feed); + document.querySelectorAll('.lbl-unload').forEach(e=>e.textContent=T.lbl_unload); + // 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';} + } + // skip-button (mid-print) – nur sichtbar wenn aktuell gedruckt wird + var skipBtn=document.getElementById('d-btn-skip'); + if(skipBtn){ + var printing=(s.print_state==='printing'||s.print_state==='paused'); + skipBtn.style.display=printing?'':'none'; + } + + // header + var b=document.getElementById('h-badge'); + b.className='hbadge '+s.print_state; + document.getElementById('h-state').textContent=T['kobra_'+s.kobra_state]||s.kobra_state||T.header_status_standby; + var _pn=_printers.length===0?'–':((_activePrinter&&_activePrinter.name)||s.printer_name); + var _el=document.getElementById('h-pname');if(_el)_el.textContent=_pn; + var _el2=document.getElementById('h-pname-single');if(_el2)_el2.textContent=_pn; + var hv=document.getElementById('h-version');if(hv&&s.version)hv.textContent='v'+s.version; + + + // temps + var nt=document.getElementById('d-nt');if(nt)nt.textContent=s.nozzle_temp.toFixed(1); + var ntt=document.getElementById('d-nt-t');if(ntt)ntt.textContent=s.nozzle_target.toFixed(0); + var bt=document.getElementById('d-bt');if(bt)bt.textContent=s.bed_temp.toFixed(1); + var btt=document.getElementById('d-bt-t');if(btt)btt.textContent=s.bed_target.toFixed(0); + + // temp bars (dashboard) + var nb=document.getElementById('d-ntbar');if(nb)nb.style.width=clamp(s.nozzle_temp/300*100,0,100)+'%'; + var bb=document.getElementById('d-btbar');if(bb)bb.style.width=clamp(s.bed_temp/120*100,0,100)+'%'; + + // progress + var pct=Math.round(s.progress*100); + var dpct=document.getElementById('d-pct');if(dpct)dpct.textContent=pct; + var dpbar=document.getElementById('d-pbar');if(dpbar)dpbar.style.width=pct+'%'; + + var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'–'; + var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers; + + var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration); + var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'–'; + var dslrow=document.getElementById('d-slicer-row'); + var dsltime=document.getElementById('d-slicer-time'); + if(dslrow&&dsltime){ + if(s.slicer_time>0){dslrow.style.display='';dsltime.textContent=fmtTime(s.slicer_time);} + else{dslrow.style.display='none';} + } + + var fn=s.filename||'–'; + var dfname=document.getElementById('d-fname');if(dfname){dfname.textContent=fn;dfname.title=fn}; + var pfname=document.getElementById('p-fname');if(pfname){pfname.textContent=fn;pfname.title=fn}; + var cfo=document.getElementById('cam-fname');if(cfo)cfo.textContent=fn!=='–'?fn:''; + + // thumbnail + var thumb=document.getElementById('d-thumbnail'); + if(thumb){ + if(s.thumbnail){ + thumb.src='data:image/png;base64,'+s.thumbnail; + thumb.style.display='block'; + } else { + thumb.style.display='none'; + thumb.src=''; + } + } + + // light/fan sync + document.getElementById('d-light-toggle').checked=s.light_on; + var dfan=document.getElementById('d-fan');if(dfan)dfan.value=s.fan_speed; + var dfanval=document.getElementById('d-fan-val');if(dfanval)dfanval.textContent=s.fan_speed; + + // speed mode buttons + var spdWidths={1:25,2:55,3:90}; + [1,2,3].forEach(function(m){ + var b=document.getElementById('d-spd-'+m); + if(b) b.classList.toggle('spd-active', s.print_speed_mode===m); + }); + var spdBar=document.getElementById('d-spd-bar'); + if(spdBar) spdBar.style.width=(spdWidths[s.print_speed_mode]||55)+'%'; + + // 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=[]; +var _printObjects=[]; // [{name, skip}] für aktuell offenen Dialog +var _printObjectsSvg=''; // base64-SVG aus DB für Visualisierung +// Hilfsfunktionen für farbige Kanal/Slot-Marker (Issue #23) +function _contrastText(hex){ + // Helle Farbe → dunkler Text, dunkle Farbe → heller Text + var c=(hex||'').replace('#',''); + if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]; + if(c.length<6)return '#fff'; + var r=parseInt(c.slice(0,2),16),g=parseInt(c.slice(2,4),16),b=parseInt(c.slice(4,6),16); + // YIQ-Helligkeit + var y=(r*299 + g*587 + b*114)/1000; + return y>=140?'#111':'#fff'; +} +function _updateSlotMarker(sel){ + var opt=sel.options[sel.selectedIndex]; + var color=opt&&opt.dataset.color?opt.dataset.color:'#888'; + var slotIdx=parseInt(opt.value); + var paintIdx=sel.dataset.paint; + var marker=document.querySelector('.fd-slot-marker[data-for-paint="'+paintIdx+'"]'); + if(marker){ + marker.style.background=color; + marker.style.color=_contrastText(color); + marker.textContent=(slotIdx+1); + } +} + +function openFilamentDialog(slots){ + _amsSlots=slots.filter(function(s){return s.status==='loaded';}); + var dlg=document.getElementById('filament-dialog'); + var title=document.getElementById('fd-title'); + var body=document.getElementById('fd-slots'); + if(title)title.textContent='▶ '+_storeFilename; + // Objekt-Liste laden (nur Store-Modus: per File-ID; Banner-Modus hat keine ID) + _printObjects=[]; + _printObjectsSvg=''; + var objSection=document.getElementById('fd-objects-section'); + if(objSection)objSection.style.display='none'; + if(_storeFileId){ + fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects')) + .then(function(r){return r.json()}) + .then(function(d){ + var names=(d.result&&d.result.names)||[]; + _printObjectsSvg=(d.result&&d.result.svg_b64)||''; + if(names.length>=2){ + _printObjects=names.map(function(n){return {name:n,skip:false};}); + renderObjectChecklist(); renderObjectSvg(); + if(objSection)objSection.style.display='block'; + } + }).catch(function(){}); + } + + // GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten + var channels=_gcodeFilaments.length?_gcodeFilaments:_amsSlots.map(function(s,i){ + return {slot_index:i,color_hex:s.color_hex,material:s.material}; + }); + + 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(''); + // Kanal-Box (links): farbige Box mit Nummer + auto Kontrast-Text + var txt=_contrastText(gc.color_hex); + var slotColor=defaultSlot?defaultSlot.color_hex:'#888'; + var slotTxt=_contrastText(slotColor); + return '
'+ + ''+(i+1)+''+ + ''+gc.material+''+ + ''+ + ''+(defaultSlot?defaultSlot.slot_index+1:'?')+''+ + ''+ + '
'; + }).join(''); + } + if(dlg)dlg.classList.add('open'); +} + +function closeFilamentDialog(){ + var dlg=document.getElementById('filament-dialog'); + if(dlg)dlg.classList.remove('open'); +} + +function confirmFilamentPrint(){ + var selects=document.querySelectorAll('#fd-slots select'); + var assignments=[]; + 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'), + }); + }); + // Pre-Print Skip: Namen der abgehakten Objekte sammeln + var excludedObjects=_printObjects.filter(function(o){return o.skip;}).map(function(o){return o.name;}); + closeFilamentDialog(); + if(_filamentDialogMode==='banner'){ + // Banner-Modus: normaler print/start mit Slot-Override + var btn=document.getElementById('file-ready-btn'); + if(btn){btn.disabled=true;btn.textContent='…';} + post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects}) + .then(function(r){return r.json();}) + .then(function(){ + document.getElementById('file-ready-banner').style.display='none'; + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }) + .catch(function(e){ + clog((T.log_error||'Error:')+' '+e,'msg-err'); + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }); + } else { + // Store-Modus: POST /kx/print + fetch(_apiUrl('/kx/print'),{ + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects}) + }).then(function(r){return r.json()}).then(function(d){ + if(d.result==='ok'){clog('Druckstart: '+_storeFilename,'msg-ok');showPanel('dashboard');} + else{clog('Druckfehler: '+(d.error||'?'),'msg-err');} + }).catch(function(e){clog('Druckfehler: '+e,'msg-err');}); + } +} + +function renderObjectChecklist(){ + var box=document.getElementById('fd-objects'); + if(!box)return; + box.innerHTML=_printObjects.map(function(o,i){ + var label=o.name; + // Klipper-Namen sind oft "Datei.stl_id_N_copy_M" → schöner darstellen + var m=label.match(/^(.+)\.stl_id_(\d+)_copy_(\d+)$/); + if(m)label=m[1]+' #'+(parseInt(m[2])+1)+(m[3]!=='0'?' ('+(parseInt(m[3])+1)+')':''); + return ''; + }).join(''); +} +function _toggleObjectSkip(idx,val){ + if(_printObjects[idx])_printObjects[idx].skip=!!val; + renderObjectSvg(); +} +function renderObjectSvg(){ + var box=document.getElementById('fd-objects-svg'); + if(!box)return; + if(!_printObjectsSvg||!_printObjects.length){box.style.display='none';box.innerHTML='';return;} + box.style.display='block'; + var svg=''; try{ svg=atob(_printObjectsSvg);}catch(e){ box.style.display='none'; return; } + box.innerHTML=svg; + var svgEl=box.querySelector('svg'); + if(!svgEl)return; + svgEl.style.width='100%'; svgEl.style.maxHeight='200px'; svgEl.style.height='auto'; + _printObjects.forEach(function(o,i){ + var g=svgEl.querySelector('g[id="'+CSS.escape(o.name)+'"]'); + if(!g)return; + var path=g.querySelector('path'); + g.style.cursor='pointer'; + g.setAttribute('opacity', o.skip?'0.8':'0.35'); + if(path){ + path.setAttribute('fill', o.skip?'#ff5e5b':'#5fa7ff'); + path.setAttribute('fill-opacity', o.skip?'0.4':'0.18'); + } + g.onclick=function(){ + _printObjects[i].skip=!_printObjects[i].skip; + renderObjectChecklist(); renderObjectSvg(); + }; + }); +} +// ── Mid-Print Skip ── +var _skipObjects=[]; // [{name, skipped, willSkip}] +var _skipSvg=''; +function openSkipDialog(){ + document.getElementById('skip-status').textContent=''; + document.getElementById('skip-confirm').disabled=false; + _refreshSkipDialog(); + document.getElementById('skip-dialog').classList.add('open'); +} +function _refreshSkipDialog(){ + // Erst aktueller State (mit DB-Objects + svg), dann query_obj für frischen skipped + fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ + var s=d.result||{}; + _skipSvg=s.svg_b64||''; + _skipObjects=(s.objects||[]).map(function(n){ + return {name:n, skipped:(s.skipped||[]).indexOf(n)>=0, willSkip:false}; + }); + renderSkipList(); renderSkipSvg(); + }); + // Frisch nachfragen (skipped-Liste aktualisieren) + fetch(_apiUrl('/kx/skip/query'),{method:'POST'}).then(function(r){return r.json()}).then(function(){ + setTimeout(function(){ + fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ + var s=d.result||{}; + var skipped=s.skipped||[]; + _skipObjects.forEach(function(o){ o.skipped=skipped.indexOf(o.name)>=0; if(o.skipped)o.willSkip=false; }); + renderSkipList(); renderSkipSvg(); + }); + }, 500); + }).catch(function(){}); +} +function closeSkipDialog(){ + document.getElementById('skip-dialog').classList.remove('open'); +} +function _shortLabel(name){ + var m=name.match(/^(.+)\.[sS][tT][lL]_id_(\d+)_copy_(\d+)$/); + if(!m)return name; + return m[1]+' #'+(parseInt(m[2])+1)+(m[3]!=='0'?' ('+(parseInt(m[3])+1)+')':''); +} +function renderSkipList(){ + var box=document.getElementById('skip-list'); + if(!box)return; + if(!_skipObjects.length){ + box.innerHTML='
'+(T.skip_no_objects||'Keine Objekte in diesem Druck.')+'
'; + return; + } + box.innerHTML=_skipObjects.map(function(o,i){ + var label=_shortLabel(o.name); + var dis=o.skipped?'disabled':''; + var note=o.skipped?''+(T.skip_already||'übersprungen')+'':''; + return ''; + }).join(''); +} +function renderSkipSvg(){ + var box=document.getElementById('skip-svg'); + if(!box)return; + if(!_skipSvg||!_skipObjects.length){box.style.display='none';box.innerHTML='';return;} + box.style.display='block'; + // SVG aus base64 dekodieren + var svg=''; + try{ svg=atob(_skipSvg); }catch(e){ box.style.display='none'; return; } + box.innerHTML=svg; + // Polygone interaktiv machen: jeder entspricht einem Objekt + var svgEl=box.querySelector('svg'); + if(!svgEl)return; + svgEl.style.width='100%'; svgEl.style.maxHeight='280px'; svgEl.style.height='auto'; + _skipObjects.forEach(function(o,i){ + var g=svgEl.querySelector('g[id="'+CSS.escape(o.name)+'"]'); + if(!g)return; + var path=g.querySelector('path'); + if(o.skipped){ + // bereits übersprungen → ausgegraut, kein Klick + g.setAttribute('opacity','0.25'); + if(path){path.setAttribute('fill','#888');path.setAttribute('fill-opacity','0.3');} + g.style.cursor='not-allowed'; + } else { + g.style.cursor='pointer'; + g.setAttribute('opacity', o.willSkip?'0.8':'0.35'); + if(path){ + path.setAttribute('fill', o.willSkip?'#ff5e5b':'#5fa7ff'); + path.setAttribute('fill-opacity', o.willSkip?'0.4':'0.18'); + } + g.onclick=function(){ + _skipObjects[i].willSkip=!_skipObjects[i].willSkip; + renderSkipList(); renderSkipSvg(); + }; + } + }); +} +function _toggleWillSkip(idx,val){ + if(_skipObjects[idx])_skipObjects[idx].willSkip=!!val; + renderSkipSvg(); +} +function confirmSkip(){ + var names=_skipObjects.filter(function(o){return o.willSkip;}).map(function(o){return o.name;}); + var st=document.getElementById('skip-status'); + var btn=document.getElementById('skip-confirm'); + if(!names.length){st.textContent=T.skip_select_at_least_one||'Bitte mindestens ein Objekt wählen.';st.style.color='var(--warn)';return;} + btn.disabled=true; st.textContent=T.skip_sending||'Sende …'; st.style.color='var(--txt2)'; + fetch(_apiUrl('/kx/skip'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({names:names})}) + .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) + .then(function(res){ + if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;} + st.textContent=T.skip_success||'Objekte werden übersprungen.';st.style.color='var(--ok)'; + // Dialog offen lassen + neu laden damit der "übersprungen"-Status erscheint + setTimeout(function(){ _refreshSkipDialog(); btn.disabled=false; st.textContent=''; }, 1500); + }) + .catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;}); +} + +function storeDelete(fileId){ + if(!confirm(T.store_delete_confirm)) return; + fetch(_apiUrl('/kx/files/'+fileId),{method:'DELETE'}).then(function(r){ + if(r.ok){loadStore();} + else{clog('Löschen fehlgeschlagen','msg-err');} + }); +} + +// ── Drucker hinzufügen ── +function openAddPrinterDialog(){ + document.getElementById('apd-ip').value=''; + document.getElementById('apd-name').value=''; + var st=document.getElementById('apd-status');st.textContent='';st.style.color='var(--txt2)'; + document.getElementById('apd-confirm').disabled=false; + document.getElementById('add-printer-dialog').classList.add('open'); +} +function closeAddPrinterDialog(){ + document.getElementById('add-printer-dialog').classList.remove('open'); +} +function confirmAddPrinter(){ + var ip=document.getElementById('apd-ip').value.trim(); + var name=document.getElementById('apd-name').value.trim(); + var st=document.getElementById('apd-status'),btn=document.getElementById('apd-confirm'); + if(!ip){st.textContent=T.apd_err_ip;st.style.color='var(--err)';return;} + st.textContent=T.apd_fetching;st.style.color='var(--txt2)';btn.disabled=true; + fetch('/kx/printers/add',{method:'POST',headers:{'Content-Type':'application/json'}, + body:JSON.stringify({printer_ip:ip,name:name})}) + .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) + .then(function(res){ + if(!res.ok){st.textContent=(res.j&&res.j.error)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;} + st.textContent=T.apd_success;st.style.color='var(--ok)'; + setTimeout(function(){location.reload();},2500); + }) + .catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;}); +} +function removePrinter(id,name){ + if(!confirm(T.printers_remove_confirm.replace('{name}',name)))return; + fetch('/kx/printers/'+encodeURIComponent(id),{method:'DELETE'}) + .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});}) + .then(function(res){ + if(!res.ok){alert((res.j&&res.j.error)||'Fehler');return;} + setTimeout(function(){location.href='/printer1';},2000); + }) + .catch(function(e){alert(''+e);}); +} + +// ── Drucker-Tab ── +function loadPrinterTab(){ + var grid=document.getElementById('printers-grid'); + if(grid)grid.innerHTML='
'+T.printers_loading+'
'; + // Drucker-Liste von lokaler Instanz holen + fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){ + var printers=d.result||[]; + if(!printers.length){ + if(grid)grid.innerHTML='
'+ + '
🖨
'+ + '
'+T.printers_empty_hint+'
'+ + ''+ + '
'; + return; + } + // Status jedes Druckers parallel abrufen + var fetches=printers.map(function(p){ + var url=(p.bridge_url||'').replace(/\/+$/,''); + return fetch(url+'/api/state',{signal:AbortSignal.timeout(3000)}) + .then(function(r){return r.json()}) + .then(function(s){return {printer:p,state:s,online:true};}) + .catch(function(){return {printer:p,state:{},online:false};}); + }); + Promise.all(fetches).then(function(results){ + var activeId=_activePrinter?String(_activePrinter.id):null; + if(grid)grid.innerHTML=results.map(function(res){ + var p=res.printer,s=res.state,online=res.online; + var isActive=String(p.id)===activeId; + var url=(p.bridge_url||'').replace(/\/+$/,''); + var printerNum=p.id; + var ks=online?(s.kobra_state||'free'):'offline'; + var stateKey='kobra_'+ks; + var stateLabel=T[stateKey]||ks; + var stateColor=ks==='free'?'var(--ok)':ks==='printing'?'var(--accent)':ks==='offline'?'var(--txt2)':'var(--warn)'; + var progress=online&&s.progress?Math.round(s.progress*100):null; + var filename=online&&s.filename?s.filename:''; + var nt=online&&s.nozzle_temp?s.nozzle_temp.toFixed(1):'–'; + var bt=online&&s.bed_temp?s.bed_temp.toFixed(1):'–'; + var border=isActive?'2px solid var(--accent)':'1px solid var(--border)'; + var nameEsc=String(p.name).replace(/\\/g,'\\\\').replace(/'/g,"\\'"); + return '
'+ + '
'+ + '🖨 '+p.name+''+ + ''+ + (isActive?''+T.printers_active+'':'')+ + ''+ + ''+ + '
'+ + '
'+ + ''+ + ''+stateLabel+''+ + '
'+ + (p.printer_ip?'
🌐 '+p.printer_ip+'
':'')+ + (filename?'
📄 '+filename+'
':'')+ + (progress!==null?'
':'')+ + '
'+ + '🌡 '+nt+'°C🛏 '+bt+'°C'+ + '
'+ + (!isActive?''+T.printers_switch+'':'
'+T.printers_current+'
')+ + '
'; + }).join(''); + }); + }).catch(function(e){ + if(grid)grid.innerHTML='
Fehler: '+e+'
'; + }); +} diff --git a/web/themes/default/index.html b/web/themes/default/index.html new file mode 100644 index 0000000..2c12d13 --- /dev/null +++ b/web/themes/default/index.html @@ -0,0 +1,515 @@ + + + + + +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 new file mode 100644 index 0000000..020ca7a --- /dev/null +++ b/web/themes/default/style.css @@ -0,0 +1,277 @@ +: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 new file mode 100644 index 0000000..3fcd946 --- /dev/null +++ b/web/themes/new1/app.js @@ -0,0 +1,1267 @@ +// ── 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 new file mode 100644 index 0000000..ce6c6e7 --- /dev/null +++ b/web/themes/new1/index.html @@ -0,0 +1,492 @@ + + + + + +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 new file mode 100644 index 0000000..020ca7a --- /dev/null +++ b/web/themes/new1/style.css @@ -0,0 +1,277 @@ +: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} +}