diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..94f30e8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,30 @@ +# EditorConfig helps maintain consistent coding styles across all files +# https://editorconfig.org + +root = true + +# Unix-style newlines, UTF-8 encoding for all files +[*] +end_of_line = lf +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +# Python: 4 spaces (PEP 8) +[*.py] +indent_style = space +indent_size = 4 + +# JavaScript/JSON/YAML: 2 spaces (common web standard) +[*.{js,json,yml,yaml}] +indent_style = space +indent_size = 2 + +# HTML/CSS: 2 spaces +[*.{html,css}] +indent_style = space +indent_size = 2 + +# Markdown: preserve formatting +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 07fb09d..d029bb3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ dist/ *.spec releases/ +node_modules/* + !kx-bridge.spec # Laufzeit-Daten und Drucker-Credentials — nie committen diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..fac819d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "jsonRecursiveSort": false, + "jsonSortOrder": "{\"/.*/\": \"lexical\"}", + "plugins": ["prettier-plugin-sort-json"], + "printWidth": 100, + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..df03a7d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,161 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +KX-Bridge is a Python 3.11+ bridge that emulates the Klipper/Moonraker API to enable OrcaSlicer to control Anycubic Kobra X printers in LAN mode — no Klipper, no Raspberry Pi required. It reverse-engineers the Anycubic MQTT protocol to relay commands from slicers to the printer. + +## Running + +**Docker (recommended):** +```bash +docker compose up -d +docker compose logs -f +``` + +**Python directly:** +```bash +pip install -r requirements.txt +python kobrax_moonraker_bridge.py --printer-ip 192.168.x.x +``` + +**Build binary:** +```bash +pyinstaller kx-bridge.spec +``` + +No test suite exists in this repository. + +## Architecture + +The system bridges three parties: + +``` +OrcaSlicer / Browser + ↕ HTTP + WebSocket (port 7125+, Moonraker API) +KX-Bridge (kobrax_moonraker_bridge.py) + ↕ MQTT over TLS (port 9883, Anycubic LAN protocol) +Anycubic Kobra X Printer +``` + +### Core Files + +- [kobrax_moonraker_bridge.py](kobrax_moonraker_bridge.py) — Main server (~5,000 lines). Houses `KobraXBridge` (HTTP/WebSocket handlers, state management), `GCodeStore` (SQLite-backed file/print-history storage), and `CameraCache` (MJPEG stream caching). This is where all Moonraker API endpoints are implemented. +- [kobrax_client.py](kobrax_client.py) — Low-level MQTT client for the Kobra X. Handles raw MQTT 3.1.1 framing, TLS certificate auth, topic subscriptions, and message dispatch. +- [config_loader.py](config_loader.py) — Loads `config/config.ini` (primary); [env_loader.py](env_loader.py) handles `.env` fallback. Also exposes `list_notification_urls()`, `list_printers()`, `list_filament_profiles()`. +- [fetch_credentials.py](fetch_credentials.py) — Standalone tool to pull printer credentials over HTTP from the Kobra X. +- [extract_credentials.py](extract_credentials.py) — Reads credentials from a running AnycubicSlicerNext process (Windows/Linux). + +### Web UI + +Single-page app served by the bridge itself: +- [web/themes/default/index.html](web/themes/default/index.html) — Shell HTML +- [web/themes/default/app.js](web/themes/default/app.js) — All client-side logic (~2,500 lines): WebSocket handling, state rendering, UI events +- [web/themes/default/style.css](web/themes/default/style.css) — Dark/light theme via CSS variables +- [web/translations/](web/translations/) — i18n strings (DE, EN, ES, ZH-CN) + +When adding UI text, all four translation files must be updated. The hint element for notifications uses `innerHTML` (not `textContent`) to support links — follow the same pattern used for `orca_profile_help_html` when translation values contain HTML. + +The theme system is documented in [web/DOC/THEME-CSS-HOOKS.md](web/DOC/THEME-CSS-HOOKS.md) and [web/DOC/THEME-JS-ID-HOOKS.md](web/DOC/THEME-JS-ID-HOOKS.md). + +### Configuration + +Primary config: `config/config.ini` (copy from `config/config.ini.example`). Sections: +- `[connection]` — Printer IP, MQTT credentials, device/mode IDs +- `[printer_N]` — One section per printer for multi-printer setups (ports 7125–7130) +- `[print]` — AMS slots, auto-leveling, camera +- `[filament_profiles]` — Per-slot filament for AMS +- `[bridge]` — Poll interval (1–5 s, default 3 s), printer name +- `[notifications]` — Apprise notification URLs and interval settings (see below) + +### Data Storage + +- **SQLite** at `data/kx-bridge.db` — G-code metadata, print history, thumbnails (base64) +- **`data/orca_filaments.json`** — OrcaSlicer filament database, loaded at startup + +### Key Protocol Details + +- **MQTT auth:** AES-256-CBC encrypted credentials + TLS certificates (`anycubic_slicer.crt/key` bundled via PyInstaller) +- **Moonraker emulation:** Kobra states are mapped to Klipper states via `KOBRA_TO_KLIPPER_STATE` in `kobrax_moonraker_bridge.py` +- **Real-time logs:** Streamed to the browser via Server-Sent Events (SSE), not WebSocket +- **Multi-printer:** Each printer runs its own `KobraXBridge` instance on a separate port + +### PyInstaller Build + +[kx-bridge.spec](kx-bridge.spec) bundles the web UI, filament database, and Anycubic TLS certificates into a single binary. The `web/` tree maps to `static/` inside the binary. Both `pycryptodome` and `apprise` use `collect_all()` to capture dynamically loaded plugins. + +### Notification System (Apprise) + +Push notifications are sent via the [apprise](https://github.com/caronc/apprise) library (supports 60+ services via URL syntax: `discord://`, `telegram://`, `pover://`, `gotify://`, `slack://`, etc.). + +**Config format** (`[notifications]` section): +```ini +url_1 = discord://webhook_id/webhook_token +events_1 = started,finished,failed,cancelled,paused,progress +image_1 = false +url_2 = pover://USERKEY@TOKEN +events_2 = finished,failed +image_2 = true +notify_every_minutes = 10 +notify_every_layers = 0 +``` + +**Key implementation points:** +- `KobraXBridge._notify(event, filename)` — fires on state transitions detected in `_on_print()`. Splits matching URLs into plain and image groups; image group attaches a temp `.jpg` from `camera_cache.latest_jpeg` via `apprise.AppriseAttachment`. +- `KobraXBridge._check_progress_notifications()` — called from `_poll_loop()` every poll tick; fires `progress` event when the time or layer threshold is crossed. Both counters are reset together when either fires, and are also reset when a new print starts. +- `_prev_kobra_state` guards against duplicate notifications when the printer repeatedly reports the same state. +- All notification dispatches run in `threading.Thread(daemon=True)` to avoid blocking the MQTT reader thread or the event loop. +- The test endpoint (`POST /api/notifications/test`) runs apprise synchronously via `run_in_executor`. + +**Supported events:** `started`, `finished`, `failed`, `cancelled`, `paused`, `progress` + +**Settings UI:** Managed through the WebUI settings modal. Each URL entry has per-event checkboxes, a `📷 Image` toggle, and a Test button. Global repeat interval fields (minutes / layers) apply to all URLs subscribed to the `progress` event. + +## Android App + +A companion Android app lives in [android/](android/). It connects to a running KX-Bridge server and provides a mobile printer control panel. + +### Building + +```bash +cd android +./gradlew assembleDebug # APK at app/build/outputs/apk/debug/ +./gradlew assembleRelease # Minified release APK +``` + +- **Min SDK:** 26 (Android 8.0), **Compile/Target SDK:** 34 +- **Language:** Kotlin 2.0.0 + Jetpack Compose (Material3) +- **Java toolchain:** 17 +- No test suite exists. + +### Architecture + +``` +android/app/src/main/java/com/kxbridge/ +├── MainActivity.kt # Entry point; reads SharedPreferences for server URL +├── data/ +│ ├── PrinterRepository.kt # OkHttp3 HTTP client; polls /api/state every 3 s +│ └── model/PrinterState.kt # kotlinx-serialization data model (snake_case JSON) +├── viewmodel/PrinterViewModel.kt # StateFlow (Loading / Success / Error) +└── ui/ + ├── PrinterScreen.kt # Main control UI: temps, progress, pause/resume/cancel + └── SetupScreen.kt # First-run server URL entry form +``` + +`MainActivity` stores the server URL in SharedPreferences (`"kxbridge"` / key `"server_url"`) and switches between `SetupScreen` and `PrinterScreen` based on whether a URL is configured. + +`PrinterViewModel` drives all network interaction: it holds a `PrinterRepository`, exposes a `StateFlow`, and exposes command methods (`pause`, `resume`, `cancel`). `UiState` is a sealed interface. + +### Communication with KX-Bridge + +The app uses plain HTTP (cleartext allowed in the manifest): + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| GET | `/api/state` | Fetch `PrinterState` JSON (polled every 3 s) | +| POST | `/printer/print/pause` | Pause active print | +| POST | `/printer/print/resume` | Resume paused print | +| POST | `/printer/print/cancel` | Cancel print | + +`PrinterState` fields use `@SerialName` for snake_case JSON keys. ProGuard is configured to keep `com.kxbridge.data.model.**` to prevent serialization breakage in release builds. diff --git a/config_loader.py b/config_loader.py index 459942e..57fe9ca 100644 --- a/config_loader.py +++ b/config_loader.py @@ -194,7 +194,6 @@ def list_printers() -> list[dict]: idx += 1 return printers - def _filament_section(printer_id: Optional[str] = None) -> str: """Section name holding a printer's filament-profile mapping. @@ -207,19 +206,14 @@ def _filament_section(printer_id: Optional[str] = None) -> str: if pid and pid != "0": return f"filament_profiles_{pid}" return "filament_profiles" + +def list_filament_profiles() -> dict[int, dict]: + """Liest die [filament_profiles]-Sektion aus config.ini. - -def list_filament_profiles(printer_id: Optional[str] = None) -> dict[int, dict]: - """Reads the [filament_profiles] section from config.ini. - - With ``printer_id`` set, reads the per-printer ``[filament_profiles_]`` - section and falls back to the legacy global ``[filament_profiles]`` while - that printer has no own section yet. - - Format per AMS slot - the primary selector is (vendor, name); the `id` is - looked up from orca_filaments.json on save and carried along - (as a hint for OrcaSlicer; the Orca data model has ~136 profiles sharing - the same filament_id like 'OGFL99', i.e. the ID is not unique): + Format pro AMS-Slot — primärer Selector ist (vendor, name), die `id` wird + aus der orca_filaments.json beim Speichern nachgeschlagen und mitgeführt + (als Hint für OrcaSlicer; das Orca-Datenmodell hat ~136 Profile mit + derselben filament_id wie 'OGFL99', d.h. die ID ist nicht eindeutig): [filament_profiles] slot_0_vendor = Polymaker diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..586ef4f --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,30 @@ +import js from "@eslint/js"; +import globals from "globals"; +import prettier from "eslint-config-prettier"; + +export default [ + { + ignores: ["node_modules/**", "data/**", "releases/**", "android/**", "web/translations/**"], + }, + js.configs.recommended, + { + // Browser-side single-page app served by the bridge. Loaded as a classic + // (non-module) script, so all top-level functions/vars share one global scope. + files: ["web/**/*.js"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "script", + globals: { + ...globals.browser, + }, + }, + rules: { + // Top-level functions/vars are intentional globals invoked from inline + // HTML handlers, so only flag unused *local* bindings, not globals. + "no-unused-vars": ["warn", { vars: "local", args: "none" }], + // Empty catch blocks are used intentionally to swallow non-critical errors. + "no-empty": ["error", { allowEmptyCatch: true }], + }, + }, + prettier, +]; diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index db32088..9edd842 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -1068,6 +1068,27 @@ class KobraXBridge: self._camera_autostarted: bool = False self._camera_user_stopped: bool = False # user manually stopped the camera during a print self.camera_cache: CameraCache = CameraCache() + self._loop: "asyncio.AbstractEventLoop | None" = None + self._prev_kobra_state: str = "" + self._print_active: bool = False + self._notify_every_minutes: int = 0 + self._notify_every_layers: int = 0 + self._notifications_paused: bool = False + self._last_progress_notif_time: float = 0.0 + self._last_progress_notif_layer: int = 0 + try: + import config_loader as _cl2 + self._notification_urls: list[dict] = _cl2.list_notification_urls() + _np = _cl2._find_config_file() + if _np: + import configparser as _cparser + _ncfg = _cparser.ConfigParser() + _ncfg.read(_np, encoding="utf-8") + self._notify_every_minutes = _ncfg.getint("notifications", "notify_every_minutes", fallback=0) + self._notify_every_layers = _ncfg.getint("notifications", "notify_every_layers", fallback=0) + self._notifications_paused = _ncfg.getboolean("notifications", "paused", fallback=False) + except Exception: + self._notification_urls = [] self._thumbnail_b64: str = "" self._ace_dry_presets: dict[str, dict] = self._load_ace_dry_presets_config() @@ -1330,6 +1351,175 @@ class KobraXBridge: out[key]["name"] = name or str(d.get("name", "Custom")) return out + # ------------------------------------------------------------------------- + # Notifications (apprise) + # ------------------------------------------------------------------------- + + async def _capture_notification_image(self, timeout: float = 15.0) -> bytes: + """Liefert einen aktuellen JPEG-Frame für Bild-Benachrichtigungen. + + Falls die Kamera nicht aktiv streamt, wird sie kurz eingeschaltet, ein + Frame abgegriffen und – sofern wir sie selbst gestartet haben – wieder + ausgeschaltet (toggle on → snapshot → toggle off). + """ + loop = asyncio.get_event_loop() + # 1) Bereits ein frischer Frame im Cache (z. B. WebUI schaut zu)? → direkt nutzen. + if self.camera_cache.latest_jpeg and (time.time() - self.camera_cache.latest_jpeg_ts) < 3.0: + return self.camera_cache.latest_jpeg + + started_here = False + try: + # 2) Kamera am Drucker einschalten, falls sie nicht ohnehin läuft. + if not self._camera_autostarted: + try: + await loop.run_in_executor(None, self.client.start_camera) + started_here = True + log.debug("Kamera für Bild-Benachrichtigung temporär gestartet") + except Exception as e: + log.warning(f"Kamera-Start für Benachrichtigung fehlgeschlagen: {e}") + # rtspUrl aktualisieren (kommt im info/report mit, wenn Kamera streamt) + try: + info = await loop.run_in_executor(None, self.client.query_info) + if info: + self._on_info(info) + except Exception: + pass + + url = self._state.get("camera_url", "") + if not url: + return b"" + self.camera_cache.set_url(url) + await self.camera_cache.ensure_running() + + # 3) Auf einen frischen Frame warten (ffmpeg + RTSP-Warmup). + ref_ts = time.time() + deadline = ref_ts + timeout + while time.time() < deadline: + if self.camera_cache.latest_jpeg and self.camera_cache.latest_jpeg_ts >= ref_ts: + return self.camera_cache.latest_jpeg + await asyncio.sleep(0.3) + # Kein frischer Frame – notfalls den letzten vorhandenen nehmen. + return self.camera_cache.latest_jpeg + finally: + # 4) Wieder ausschalten, wenn wir sie nur für den Snapshot gestartet + # haben (nicht während eines laufenden Drucks mit Auto-Kamera). + if started_here and not self._camera_autostarted: + try: + await loop.run_in_executor(None, self.client.stop_camera) + log.debug("Kamera nach Bild-Benachrichtigung wieder gestoppt") + except Exception as e: + log.debug(f"Kamera-Stop nach Benachrichtigung fehlgeschlagen: {e}") + + def _notify(self, event: str, filename: str = ""): + if self._notifications_paused: + return + matching = [e for e in self._notification_urls + if e.get("enabled", True) and event in e.get("events", [])] + if not matching: + return + printer_name = self._state.get("printer_name", "KX-Bridge") + titles = { + "started": "Print Started ▶", + "finished": "Print Finished ✓", + "failed": "Print Failed ✗", + "cancelled": "Print Cancelled", + "paused": "Print Paused ⏸", + "progress": "Print Progress", + } + title = titles.get(event, "KX-Bridge") + if event == "progress": + pct = int(self._state.get("progress", 0) * 100) + curr = self._state.get("curr_layer", 0) + total = self._state.get("total_layers", 0) + rem = self._state.get("remain_time", 0) + rem_str = f"{rem // 3600}h {(rem % 3600) // 60}m" if rem else "" + parts = [f"{pct}%"] + if total: + parts.append(f"Layer {curr}/{total}") + if rem_str: + parts.append(f"Remaining: {rem_str}") + if filename: + parts.append(filename) + body = " • ".join(parts) if parts else printer_name + else: + body = filename if filename else printer_name + if filename and printer_name: + body = f"{filename}\n{printer_name}" + + urls_plain = [e["url"] for e in matching if not e.get("include_image")] + urls_image = [e["url"] for e in matching if e.get("include_image")] + + def _send(): + import apprise, os, tempfile + try: + if urls_plain: + ap = apprise.Apprise() + for u in urls_plain: + ap.add(u) + ap.notify(title=title, body=body) + if urls_image: + ap2 = apprise.Apprise() + for u in urls_image: + ap2.add(u) + attach = None + tmppath = None + jpeg = b"" + if self._loop is not None: + try: + fut = asyncio.run_coroutine_threadsafe( + self._capture_notification_image(timeout=15.0), self._loop) + jpeg = fut.result(timeout=30.0) or b"" + except Exception as e: + log.warning(f"Kamera-Snapshot für Benachrichtigung fehlgeschlagen: {e}") + jpeg = self.camera_cache.latest_jpeg + else: + jpeg = self.camera_cache.latest_jpeg + if jpeg: + try: + with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f: + f.write(jpeg) + tmppath = f.name + attach = apprise.AppriseAttachment() + attach.add(tmppath) + except Exception as e: + log.warning(f"Kamera-Anhang fehlgeschlagen: {e}") + attach = None + else: + log.debug("Bild-Benachrichtigung ohne Anhang (kein Kamera-Frame verfügbar)") + ap2.notify(title=title, body=body, attach=attach) + if tmppath: + try: + os.unlink(tmppath) + except Exception: + pass + log.info(f"Benachrichtigung '{event}' gesendet ({len(matching)} URL(s))") + except Exception as e: + log.warning(f"Benachrichtigung fehlgeschlagen: {e}") + + threading.Thread(target=_send, daemon=True).start() + + def _check_progress_notifications(self): + if self._state.get("kobra_state") != "printing": + return + if not any("progress" in e.get("events", []) for e in self._notification_urls + if e.get("enabled", True)): + return + if self._notify_every_minutes == 0 and self._notify_every_layers == 0: + return + now = time.monotonic() + curr_layer = self._state.get("curr_layer", 0) + fired = False + if self._notify_every_minutes > 0: + if now - self._last_progress_notif_time >= self._notify_every_minutes * 60: + fired = True + if not fired and self._notify_every_layers > 0 and curr_layer > 0: + if curr_layer - self._last_progress_notif_layer >= self._notify_every_layers: + fired = True + if fired: + self._last_progress_notif_time = now + self._last_progress_notif_layer = curr_layer + self._notify("progress", self._state.get("filename", "")) + # ------------------------------------------------------------------------- # MQTT callbacks (called from reader thread) # ------------------------------------------------------------------------- @@ -1491,6 +1681,7 @@ class KobraXBridge: elif kobra_state in ("free", "finished", "stoped", "canceled"): self._camera_autostarted = False self._camera_user_stopped = False # release for the next print + self._print_active = False if project: if "filename" in project: self._state["filename"] = project["filename"] @@ -4135,7 +4326,7 @@ class KobraXBridge: except OSError: raise web.HTTPNotFound() if name == "app.js": - raw = raw.replace("'__VERSION__'", f"'{self._read_version()}'") + raw = raw.replace("__VERSION__", self._read_version()) return web.Response( text=raw, content_type=ctype, @@ -4882,9 +5073,13 @@ class KobraXBridge: "verbose_http_log": getattr(self._args, "verbose_http_log", 0), "filament_profiles": {str(k): v for k, v in self._filament_profiles.items()}, "visible_vendors": self._visible_vendors, - "ace_dry_presets": self._ace_dry_presets, + "ace_dry_presets": self._ace_dry_presets, "spoolman_server": getattr(self._args, "spoolman_server", "") or "", "spoolman_sync_rate": getattr(self._args, "spoolman_sync_rate", 0), + "notifications": self._notification_urls, + "notify_every_minutes": self._notify_every_minutes, + "notify_every_layers": self._notify_every_layers, + "notifications_paused": self._notifications_paused, }) async def handle_api_settings_post(self, request): @@ -4953,6 +5148,42 @@ class KobraXBridge: cfg.set("ace_dry_presets", f"{key}_name", str(val.get("name", key.replace("_", " ").title()))) self._ace_dry_presets = presets + # Benachrichtigungen speichern + if cfg.has_section("notifications"): + cfg.remove_section("notifications") + incoming_notifs = data.get("notifications") + if isinstance(incoming_notifs, list): + valid_events = {"started", "finished", "failed", "cancelled", "paused", "progress"} + entries = [] + for entry in incoming_notifs: + if not isinstance(entry, dict): + continue + url = str(entry.get("url", "")).strip() + if not url: + continue + events = [e for e in entry.get("events", []) if e in valid_events] + include_image = bool(entry.get("include_image", False)) + enabled = bool(entry.get("enabled", True)) + entries.append({"url": url, "events": events, "include_image": include_image, "enabled": enabled}) + cfg.add_section("notifications") + for i, entry in enumerate(entries, 1): + cfg.set("notifications", f"url_{i}", entry["url"]) + cfg.set("notifications", f"events_{i}", ",".join(entry["events"])) + cfg.set("notifications", f"image_{i}", "true" if entry["include_image"] else "false") + cfg.set("notifications", f"enabled_{i}", "true" if entry["enabled"] else "false") + self._notification_urls = entries + elif not cfg.has_section("notifications"): + cfg.add_section("notifications") + notify_every_minutes = max(0, int(data.get("notify_every_minutes") or 0)) + notify_every_layers = max(0, int(data.get("notify_every_layers") or 0)) + cfg.set("notifications", "notify_every_minutes", str(notify_every_minutes)) + cfg.set("notifications", "notify_every_layers", str(notify_every_layers)) + self._notify_every_minutes = notify_every_minutes + self._notify_every_layers = notify_every_layers + if "notifications_paused" in data: + self._notifications_paused = bool(data.get("notifications_paused")) + cfg.set("notifications", "paused", "true" if self._notifications_paused else "false") + with open(config_path, "w", encoding="utf-8") as f: f.write("# KX-Bridge Konfigurationsdatei\n\n") cfg.write(f) @@ -4962,6 +5193,59 @@ class KobraXBridge: asyncio.get_event_loop().call_later(0.3, self._restart_bridge) return response + async def handle_api_notifications_pause(self, request): + """Toggle or set the global notifications-paused flag without restarting.""" + try: + data = await request.json() + except Exception: + data = {} + if isinstance(data, dict) and "paused" in data: + self._notifications_paused = bool(data.get("paused")) + else: + self._notifications_paused = not self._notifications_paused + try: + import configparser + config_path = self._find_config_path() + config_path.parent.mkdir(parents=True, exist_ok=True) + cfg = configparser.ConfigParser() + if config_path.is_file(): + cfg.read(config_path, encoding="utf-8") + if not cfg.has_section("notifications"): + cfg.add_section("notifications") + cfg.set("notifications", "paused", "true" if self._notifications_paused else "false") + with open(config_path, "w", encoding="utf-8") as f: + f.write("# KX-Bridge Konfigurationsdatei\n\n") + cfg.write(f) + except Exception as e: + log.warning(f"Notif-Pause speichern fehlgeschlagen: {e}") + log.info(f"Benachrichtigungen {'pausiert' if self._notifications_paused else 'aktiv'}") + return self._json_cors({"status": "ok", "paused": self._notifications_paused}) + + async def handle_api_notifications_test(self, request): + try: + data = await request.json() + except Exception: + return self._json_cors({"status": "error", "message": "invalid json"}, status=400) + url = str(data.get("url", "")).strip() + if not url: + return self._json_cors({"status": "error", "message": "url required"}, status=400) + printer_name = self._state.get("printer_name", "KX-Bridge") + try: + import apprise + ap = apprise.Apprise() + ap.add(url) + ok = await asyncio.get_event_loop().run_in_executor( + None, + lambda: ap.notify(title="KX-Bridge Test", body=f"Test notification from {printer_name}"), + ) + if ok is False: + return self._json_cors({"status": "error", "message": "Notification failed (check URL)"}) + return self._json_cors({"status": "ok"}) + except ImportError: + return self._json_cors({"status": "error", "message": "apprise not installed"}, status=500) + except Exception as e: + return self._json_cors({"status": "error", "message": str(e)}, status=500) + async def handle_kx_printer_add(self, request): """Adds a printer: fetches credentials via IP, writes [printer_N], restarts.""" try: @@ -5722,6 +6006,7 @@ class KobraXBridge: if now - self._spoolman_last_sync >= self._spoolman.sync_rate: self._spoolman_sync_midprint() self._spoolman_last_sync = now + self._check_progress_notifications() box = self.client.query_multicolor_box() if box: data = box.get("data") or {} @@ -5850,6 +6135,8 @@ def build_app(bridge: KobraXBridge) -> web.Application: r.add_get("/api/state", bridge.handle_api_state) r.add_get("/api/settings", bridge.handle_api_settings_get) r.add_post("/api/settings", bridge.handle_api_settings_post) + r.add_post("/api/notifications/test", bridge.handle_api_notifications_test) + r.add_post("/api/notifications/pause", bridge.handle_api_notifications_pause) r.add_get("/api/update/check", bridge.handle_api_update_check) r.add_post("/api/update/apply", bridge.handle_api_update_apply) r.add_post("/api/file_ready/clear", bridge.handle_api_file_ready_clear) @@ -5964,6 +6251,7 @@ async def run_bridge(args): client, args=per_args, store=store, printer_id=pid, all_bridges=all_bridges, ) + bridge._loop = loop # Adopt printer_name from config.ini if set if p.get("name"): bridge._state["printer_name"] = p["name"] diff --git a/kx-bridge.spec b/kx-bridge.spec index 944d2a6..ef7e24a 100644 --- a/kx-bridge.spec +++ b/kx-bridge.spec @@ -16,6 +16,12 @@ datas += _d binaries += _b hiddenimports += _h +# apprise — alle Notification-Plugins (dynamisch geladen via entry_points) +_d, _b, _h = collect_all("apprise") +datas += _d +binaries += _b +hiddenimports += _h + a = Analysis( ["kobrax_moonraker_bridge.py"], pathex=[], diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dd75b90 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1158 @@ +{ + "name": "kx-bridge", + "version": "0.9.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kx-bridge", + "version": "0.9.0", + "devDependencies": { + "@eslint/js": "^9.0.0", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "globals": "^15.0.0", + "prettier": "^3.0.0", + "prettier-plugin-sort-json": "^4.2.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-sort-json": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-sort-json/-/prettier-plugin-sort-json-4.2.0.tgz", + "integrity": "sha512-jK1w3/7otTvHtv1eoLji2U9mEoOGeyl7QQQ/afLnjht1YtRLSUUk8o0rIIC/HUVXhoGPCFe4SVZbRGYjjUVgvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..90e3219 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "kx-bridge", + "version": "0.9.0", + "description": "Moonraker-compatible bridge for Anycubic Kobra X", + "type": "module", + "devDependencies": { + "@eslint/js": "^9.0.0", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "globals": "^15.0.0", + "prettier": "^3.0.0", + "prettier-plugin-sort-json": "^4.2.0" + }, + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format:js": "prettier --write \"web/**/*.js\" \"*.js\"", + "format:json": "prettier --write \"*.json\" \"web/**/*.json\"", + "format:web": "prettier --write \"web/**/*.{js,json,html,css,yml,yaml,md}\"", + "format": "npm run format:web" + } +} diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..b8483cd --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +# Code formatting +black>=24.1.0 diff --git a/requirements.txt b/requirements.txt index e29debe..84b8c72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ aiohttp>=3.9 imageio-ffmpeg>=0.4.9 requests>=2.30.0 pycryptodome>=3.20.0 +apprise>=1.8 diff --git a/web/DOC/THEME-CSS-HOOKS.md b/web/DOC/THEME-CSS-HOOKS.md index f3e7e48..75e767d 100644 --- a/web/DOC/THEME-CSS-HOOKS.md +++ b/web/DOC/THEME-CSS-HOOKS.md @@ -2,135 +2,135 @@ Referenzliste für CSS-/Layout-Anpassungen. -| ID | Verwendung | -|---|---| +| ID | Verwendung | +| ----------------------------------- | --------------- | | `#ace-dry-dialog-custom-name-label` | Hook / Selektor | -| `#ace-dry-dialog-custom-name-row` | Hook / Selektor | -| `#ace-dry-dialog-temp-label` | Hook / Selektor | -| `#ace-dry-dialog-time-label` | Hook / Selektor | -| `#ace-dry-dialog-title` | Hook / Selektor | -| `#add-printer-btn-label` | Hook / Selektor | -| `#ams-no-data` | Hook / Selektor | -| `#apd-ip` | Hook / Selektor | -| `#apd-lbl-ip` | Hook / Selektor | -| `#apd-lbl-name` | Hook / Selektor | -| `#apd-name` | Hook / Selektor | -| `#apd-status` | Hook / Selektor | -| `#apd-title` | Hook / Selektor | -| `#btn-log-dl` | Hook / Selektor | -| `#cam-fname` | Hook / Selektor | -| `#cam-img` | Hook / Selektor | -| `#cam-overlay` | Hook / Selektor | -| `#cam-placeholder` | Hook / Selektor | -| `#cam-placeholder-txt` | Hook / Selektor | -| `#cam-spinner` | Hook / Selektor | -| `#cam-wrap` | Hook / Selektor | -| `#conn-error-banner` | Hook / Selektor | -| `#d-ace-dry-grid` | Hook / Selektor | -| `#d-ace-dry-wrap` | Hook / Selektor | -| `#d-ams-card` | Hook / Selektor | -| `#d-bt-t` | Hook / Selektor | -| `#d-btbar` | Hook / Selektor | -| `#d-btn-skip-label` | Hook / Selektor | -| `#d-card-ams` | Hook / Selektor | -| `#d-card-cam` | Hook / Selektor | -| `#d-card-lightfan` | Hook / Selektor | -| `#d-card-progress` | Hook / Selektor | -| `#d-card-speed` | Hook / Selektor | -| `#d-card-temps` | Hook / Selektor | -| `#d-chart-label` | Hook / Selektor | -| `#d-ctrl-btns` | Hook / Selektor | -| `#d-elapsed` | Hook / Selektor | -| `#d-fname` | Hook / Selektor | -| `#d-layers` | Hook / Selektor | -| `#d-lbl-bed` | Hook / Selektor | -| `#d-lbl-elapsed` | Hook / Selektor | -| `#d-lbl-layers` | Hook / Selektor | -| `#d-lbl-light` | Hook / Selektor | -| `#d-lbl-remain` | Hook / Selektor | -| `#d-nt` | Hook / Selektor | -| `#d-nt-t` | Hook / Selektor | -| `#d-ntbar` | Hook / Selektor | -| `#d-pbar` | Hook / Selektor | -| `#d-pct` | Hook / Selektor | -| `#d-remain` | Hook / Selektor | -| `#d-slicer-label` | Hook / Selektor | -| `#d-slicer-row` | Hook / Selektor | -| `#d-slicer-time` | Hook / Selektor | -| `#d-spd-bar` | Hook / Selektor | -| `#d-spd-lbl-1` | Hook / Selektor | -| `#d-spd-lbl-2` | Hook / Selektor | -| `#d-spd-lbl-3` | Hook / Selektor | -| `#d-thumbnail` | Hook / Selektor | -| `#fd-objects` | Hook / Selektor | -| `#fd-objects-hint` | Hook / Selektor | -| `#fd-objects-section` | Hook / Selektor | -| `#fd-objects-svg` | Hook / Selektor | -| `#fd-slots-hint` | Hook / Selektor | -| `#fd-title` | Hook / Selektor | -| `#file-ready-banner` | Hook / Selektor | -| `#file-ready-name` | Hook / Selektor | -| `#h-badge` | Hook / Selektor | -| `#h-pname` | Hook / Selektor | -| `#h-pname-single` | Hook / Selektor | -| `#h-state` | Hook / Selektor | -| `#h-version` | Hook / Selektor | -| `#lbl-auto-leveling` | Hook / Selektor | -| `#lbl-default-slot` | Hook / Selektor | -| `#lbl-device-id` | Hook / Selektor | -| `#lbl-ip-hint` | Hook / Selektor | -| `#lbl-mode-id` | Hook / Selektor | -| `#lbl-mqtt-port` | Hook / Selektor | -| `#lbl-password` | Hook / Selektor | -| `#lbl-printer-ip` | Hook / Selektor | -| `#lbl-printer-name` | Hook / Selektor | -| `#lbl-slot-color` | Hook / Selektor | -| `#lbl-slot-material` | Hook / Selektor | -| `#lbl-update-apply` | Hook / Selektor | -| `#lbl-update-check` | Hook / Selektor | -| `#lbl-username` | Hook / Selektor | -| `#log-badge` | Hook / Selektor | -| `#log-badge-bot` | Hook / Selektor | -| `#modal-sec-connection` | Hook / Selektor | -| `#modal-sec-poll` | Hook / Selektor | -| `#modal-sec-print` | Hook / Selektor | -| `#modal-sec-version` | Hook / Selektor | -| `#modal-title-settings` | Hook / Selektor | -| `#opt-slot-0` | Hook / Selektor | -| `#opt-slot-1` | Hook / Selektor | -| `#opt-slot-2` | Hook / Selektor | -| `#opt-slot-3` | Hook / Selektor | -| `#opt-slot-auto` | Hook / Selektor | -| `#printer-dropdown-menu` | Hook / Selektor | -| `#printer-dropdown-wrap` | Hook / Selektor | -| `#printers-panel-title` | Hook / Selektor | -| `#ptitle-console` | Hook / Selektor | -| `#ptitle-motion-xy` | Hook / Selektor | -| `#ptitle-motion-z` | Hook / Selektor | -| `#s-auto-leveling` | Hook / Selektor | -| `#s-default-slot` | Hook / Selektor | -| `#s-device-id` | Hook / Selektor | -| `#s-mode-id` | Hook / Selektor | -| `#s-mqtt-port` | Hook / Selektor | -| `#s-password` | Hook / Selektor | -| `#s-printer-name` | Hook / Selektor | -| `#s-username` | Hook / Selektor | -| `#s-version-label` | Hook / Selektor | -| `#sf-all` | Hook / Selektor | -| `#sf-err` | Hook / Selektor | -| `#sf-new` | Hook / Selektor | -| `#sf-ok` | Hook / Selektor | -| `#skip-hint` | Hook / Selektor | -| `#skip-list` | Hook / Selektor | -| `#skip-status` | Hook / Selektor | -| `#skip-svg` | Hook / Selektor | -| `#skip-title` | Hook / Selektor | -| `#slot-edit-title` | Hook / Selektor | -| `#ss-date` | Hook / Selektor | -| `#ss-dur` | Hook / Selektor | -| `#ss-name` | Hook / Selektor | -| `#step-display` | Hook / Selektor | -| `#store-empty` | Hook / Selektor | -| `#store-panel-title` | Hook / Selektor | -| `#update-changelog` | Hook / Selektor | -| `#update-status` | Hook / Selektor | +| `#ace-dry-dialog-custom-name-row` | Hook / Selektor | +| `#ace-dry-dialog-temp-label` | Hook / Selektor | +| `#ace-dry-dialog-time-label` | Hook / Selektor | +| `#ace-dry-dialog-title` | Hook / Selektor | +| `#add-printer-btn-label` | Hook / Selektor | +| `#ams-no-data` | Hook / Selektor | +| `#apd-ip` | Hook / Selektor | +| `#apd-lbl-ip` | Hook / Selektor | +| `#apd-lbl-name` | Hook / Selektor | +| `#apd-name` | Hook / Selektor | +| `#apd-status` | Hook / Selektor | +| `#apd-title` | Hook / Selektor | +| `#btn-log-dl` | Hook / Selektor | +| `#cam-fname` | Hook / Selektor | +| `#cam-img` | Hook / Selektor | +| `#cam-overlay` | Hook / Selektor | +| `#cam-placeholder` | Hook / Selektor | +| `#cam-placeholder-txt` | Hook / Selektor | +| `#cam-spinner` | Hook / Selektor | +| `#cam-wrap` | Hook / Selektor | +| `#conn-error-banner` | Hook / Selektor | +| `#d-ace-dry-grid` | Hook / Selektor | +| `#d-ace-dry-wrap` | Hook / Selektor | +| `#d-ams-card` | Hook / Selektor | +| `#d-bt-t` | Hook / Selektor | +| `#d-btbar` | Hook / Selektor | +| `#d-btn-skip-label` | Hook / Selektor | +| `#d-card-ams` | Hook / Selektor | +| `#d-card-cam` | Hook / Selektor | +| `#d-card-lightfan` | Hook / Selektor | +| `#d-card-progress` | Hook / Selektor | +| `#d-card-speed` | Hook / Selektor | +| `#d-card-temps` | Hook / Selektor | +| `#d-chart-label` | Hook / Selektor | +| `#d-ctrl-btns` | Hook / Selektor | +| `#d-elapsed` | Hook / Selektor | +| `#d-fname` | Hook / Selektor | +| `#d-layers` | Hook / Selektor | +| `#d-lbl-bed` | Hook / Selektor | +| `#d-lbl-elapsed` | Hook / Selektor | +| `#d-lbl-layers` | Hook / Selektor | +| `#d-lbl-light` | Hook / Selektor | +| `#d-lbl-remain` | Hook / Selektor | +| `#d-nt` | Hook / Selektor | +| `#d-nt-t` | Hook / Selektor | +| `#d-ntbar` | Hook / Selektor | +| `#d-pbar` | Hook / Selektor | +| `#d-pct` | Hook / Selektor | +| `#d-remain` | Hook / Selektor | +| `#d-slicer-label` | Hook / Selektor | +| `#d-slicer-row` | Hook / Selektor | +| `#d-slicer-time` | Hook / Selektor | +| `#d-spd-bar` | Hook / Selektor | +| `#d-spd-lbl-1` | Hook / Selektor | +| `#d-spd-lbl-2` | Hook / Selektor | +| `#d-spd-lbl-3` | Hook / Selektor | +| `#d-thumbnail` | Hook / Selektor | +| `#fd-objects` | Hook / Selektor | +| `#fd-objects-hint` | Hook / Selektor | +| `#fd-objects-section` | Hook / Selektor | +| `#fd-objects-svg` | Hook / Selektor | +| `#fd-slots-hint` | Hook / Selektor | +| `#fd-title` | Hook / Selektor | +| `#file-ready-banner` | Hook / Selektor | +| `#file-ready-name` | Hook / Selektor | +| `#h-badge` | Hook / Selektor | +| `#h-pname` | Hook / Selektor | +| `#h-pname-single` | Hook / Selektor | +| `#h-state` | Hook / Selektor | +| `#h-version` | Hook / Selektor | +| `#lbl-auto-leveling` | Hook / Selektor | +| `#lbl-default-slot` | Hook / Selektor | +| `#lbl-device-id` | Hook / Selektor | +| `#lbl-ip-hint` | Hook / Selektor | +| `#lbl-mode-id` | Hook / Selektor | +| `#lbl-mqtt-port` | Hook / Selektor | +| `#lbl-password` | Hook / Selektor | +| `#lbl-printer-ip` | Hook / Selektor | +| `#lbl-printer-name` | Hook / Selektor | +| `#lbl-slot-color` | Hook / Selektor | +| `#lbl-slot-material` | Hook / Selektor | +| `#lbl-update-apply` | Hook / Selektor | +| `#lbl-update-check` | Hook / Selektor | +| `#lbl-username` | Hook / Selektor | +| `#log-badge` | Hook / Selektor | +| `#log-badge-bot` | Hook / Selektor | +| `#modal-sec-connection` | Hook / Selektor | +| `#modal-sec-poll` | Hook / Selektor | +| `#modal-sec-print` | Hook / Selektor | +| `#modal-sec-version` | Hook / Selektor | +| `#modal-title-settings` | Hook / Selektor | +| `#opt-slot-0` | Hook / Selektor | +| `#opt-slot-1` | Hook / Selektor | +| `#opt-slot-2` | Hook / Selektor | +| `#opt-slot-3` | Hook / Selektor | +| `#opt-slot-auto` | Hook / Selektor | +| `#printer-dropdown-menu` | Hook / Selektor | +| `#printer-dropdown-wrap` | Hook / Selektor | +| `#printers-panel-title` | Hook / Selektor | +| `#ptitle-console` | Hook / Selektor | +| `#ptitle-motion-xy` | Hook / Selektor | +| `#ptitle-motion-z` | Hook / Selektor | +| `#s-auto-leveling` | Hook / Selektor | +| `#s-default-slot` | Hook / Selektor | +| `#s-device-id` | Hook / Selektor | +| `#s-mode-id` | Hook / Selektor | +| `#s-mqtt-port` | Hook / Selektor | +| `#s-password` | Hook / Selektor | +| `#s-printer-name` | Hook / Selektor | +| `#s-username` | Hook / Selektor | +| `#s-version-label` | Hook / Selektor | +| `#sf-all` | Hook / Selektor | +| `#sf-err` | Hook / Selektor | +| `#sf-new` | Hook / Selektor | +| `#sf-ok` | Hook / Selektor | +| `#skip-hint` | Hook / Selektor | +| `#skip-list` | Hook / Selektor | +| `#skip-status` | Hook / Selektor | +| `#skip-svg` | Hook / Selektor | +| `#skip-title` | Hook / Selektor | +| `#slot-edit-title` | Hook / Selektor | +| `#ss-date` | Hook / Selektor | +| `#ss-dur` | Hook / Selektor | +| `#ss-name` | Hook / Selektor | +| `#step-display` | Hook / Selektor | +| `#store-empty` | Hook / Selektor | +| `#store-panel-title` | Hook / Selektor | +| `#update-changelog` | Hook / Selektor | +| `#update-status` | Hook / Selektor | diff --git a/web/DOC/THEME-JS-ID-HOOKS.md b/web/DOC/THEME-JS-ID-HOOKS.md index 6c2ba02..c5e1dca 100644 --- a/web/DOC/THEME-JS-ID-HOOKS.md +++ b/web/DOC/THEME-JS-ID-HOOKS.md @@ -2,89 +2,89 @@ Referenzliste für JavaScript-/DOM-Hooks. -| ID | Verwendung | -|---|---| -| `#ace-dry-dialog` | Hook / Selektor | -| `#ace-dry-dialog-cancel` | Hook / Selektor | -| `#ace-dry-dialog-confirm` | Hook / Selektor | -| `#ace-dry-dialog-custom-name` | Hook / Selektor | -| `#ace-dry-dialog-h` | Hook / Selektor | -| `#ace-dry-dialog-m` | Hook / Selektor | -| `#ace-dry-dialog-reset-default` | Hook / Selektor | -| `#ace-dry-dialog-s` | Hook / Selektor | -| `#ace-dry-dialog-save-preset` | Hook / Selektor | -| `#ace-dry-dialog-temp` | Hook / Selektor | -| `#add-printer-dialog` | Hook / Selektor | -| `#ams-slots` | Hook / Selektor | -| `#apd-confirm` | Hook / Selektor | -| `#bnb-console` | Hook / Selektor | -| `#bnb-dashboard` | Hook / Selektor | -| `#bnb-printers` | Hook / Selektor | -| `#bnb-store` | Hook / Selektor | -| `#btn-autoscroll` | Hook / Selektor | -| `#btn-save-settings` | Hook / Selektor | -| `#btn-slot-edit-feed` | Hook / Selektor | -| `#btn-slot-edit-save` | Hook / Selektor | -| `#btn-update-apply` | Hook / Selektor | -| `#btn-update-check` | Hook / Selektor | -| `#cam-toggle-btn` | Hook / Selektor | -| `#conn-btn` | Hook / Selektor | -| `#console-log` | Hook / Selektor | -| `#d-bt` | Hook / Selektor | -| `#d-btn-cancel` | Hook / Selektor | -| `#d-btn-pause` | Hook / Selektor | -| `#d-btn-resume` | Hook / Selektor | -| `#d-btn-skip` | Hook / Selektor | -| `#d-chart` | Hook / Selektor | -| `#d-fan` | Hook / Selektor | -| `#d-fan-val` | Hook / Selektor | -| `#d-light-toggle` | Hook / Selektor | -| `#d-spd-1` | Hook / Selektor | -| `#d-spd-2` | Hook / Selektor | -| `#d-spd-3` | Hook / Selektor | -| `#fd-cancel` | Hook / Selektor | -| `#fd-print` | Hook / Selektor | -| `#fd-slots` | Hook / Selektor | -| `#filament-dialog` | Hook / Selektor | -| `#file-cancel-btn` | Hook / Selektor | -| `#file-ready-btn` | Hook / Selektor | -| `#file-slots-btn` | Hook / Selektor | -| `#lang-btn` | Hook / Selektor | -| `#log-filter` | Hook / Selektor | -| `#logdir-all` | Hook / Selektor | -| `#logdir-rx` | Hook / Selektor | -| `#logdir-tx` | Hook / Selektor | -| `#log-lbl-level` | i18n-Label "Level:" | -| `#loglvl-all` | onclick `setLogLevel('all')` | -| `#loglvl-err` | onclick `setLogLevel('err')` — nur Fehler | -| `#loglvl-warn` | onclick `setLogLevel('warn')` — Fehler + Warnungen | -| `#nb-console` | Hook / Selektor | -| `#nb-dashboard` | Hook / Selektor | -| `#nb-printers` | Hook / Selektor | -| `#nb-store` | Hook / Selektor | -| `#p-bed-inp` | Hook / Selektor | -| `#p-nozzle-inp` | Hook / Selektor | -| `#panel-console` | Hook / Selektor | -| `#panel-dashboard` | Hook / Selektor | -| `#panel-printers` | Hook / Selektor | -| `#panel-store` | Hook / Selektor | -| `#poll-1` | Hook / Selektor | -| `#poll-2` | Hook / Selektor | -| `#poll-5` | Hook / Selektor | -| `#printer-dropdown-btn` | Hook / Selektor | -| `#printers-grid` | Hook / Selektor | -| `#s-printer-ip` | Hook / Selektor | -| `#settings-btn` | Hook / Selektor | -| `#settings-modal` | Hook / Selektor | -| `#skip-confirm` | Hook / Selektor | -| `#skip-dialog` | Hook / Selektor | -| `#slot-edit-color` | Hook / Selektor | -| `#slot-edit-mat` | Hook / Selektor | -| `#slot-edit-modal` | Hook / Selektor | -| `#slot-edit-preview` | Hook / Selektor | -| `#slot-mat-btns` | Hook / Selektor | -| `#store-filter` | Hook / Selektor | -| `#store-grid` | Hook / Selektor | -| `#store-refresh-btn` | Hook / Selektor | -| `#store-search` | Hook / Selektor | -| `#store-sort` | Hook / Selektor | +| ID | Verwendung | +| ------------------------------- | -------------------------------------------------- | +| `#ace-dry-dialog` | Hook / Selektor | +| `#ace-dry-dialog-cancel` | Hook / Selektor | +| `#ace-dry-dialog-confirm` | Hook / Selektor | +| `#ace-dry-dialog-custom-name` | Hook / Selektor | +| `#ace-dry-dialog-h` | Hook / Selektor | +| `#ace-dry-dialog-m` | Hook / Selektor | +| `#ace-dry-dialog-reset-default` | Hook / Selektor | +| `#ace-dry-dialog-s` | Hook / Selektor | +| `#ace-dry-dialog-save-preset` | Hook / Selektor | +| `#ace-dry-dialog-temp` | Hook / Selektor | +| `#add-printer-dialog` | Hook / Selektor | +| `#ams-slots` | Hook / Selektor | +| `#apd-confirm` | Hook / Selektor | +| `#bnb-console` | Hook / Selektor | +| `#bnb-dashboard` | Hook / Selektor | +| `#bnb-printers` | Hook / Selektor | +| `#bnb-store` | Hook / Selektor | +| `#btn-autoscroll` | Hook / Selektor | +| `#btn-save-settings` | Hook / Selektor | +| `#btn-slot-edit-feed` | Hook / Selektor | +| `#btn-slot-edit-save` | Hook / Selektor | +| `#btn-update-apply` | Hook / Selektor | +| `#btn-update-check` | Hook / Selektor | +| `#cam-toggle-btn` | Hook / Selektor | +| `#conn-btn` | Hook / Selektor | +| `#console-log` | Hook / Selektor | +| `#d-bt` | Hook / Selektor | +| `#d-btn-cancel` | Hook / Selektor | +| `#d-btn-pause` | Hook / Selektor | +| `#d-btn-resume` | Hook / Selektor | +| `#d-btn-skip` | Hook / Selektor | +| `#d-chart` | Hook / Selektor | +| `#d-fan` | Hook / Selektor | +| `#d-fan-val` | Hook / Selektor | +| `#d-light-toggle` | Hook / Selektor | +| `#d-spd-1` | Hook / Selektor | +| `#d-spd-2` | Hook / Selektor | +| `#d-spd-3` | Hook / Selektor | +| `#fd-cancel` | Hook / Selektor | +| `#fd-print` | Hook / Selektor | +| `#fd-slots` | Hook / Selektor | +| `#filament-dialog` | Hook / Selektor | +| `#file-cancel-btn` | Hook / Selektor | +| `#file-ready-btn` | Hook / Selektor | +| `#file-slots-btn` | Hook / Selektor | +| `#lang-btn` | Hook / Selektor | +| `#log-filter` | Hook / Selektor | +| `#logdir-all` | Hook / Selektor | +| `#logdir-rx` | Hook / Selektor | +| `#logdir-tx` | Hook / Selektor | +| `#log-lbl-level` | i18n-Label "Level:" | +| `#loglvl-all` | onclick `setLogLevel('all')` | +| `#loglvl-err` | onclick `setLogLevel('err')` — nur Fehler | +| `#loglvl-warn` | onclick `setLogLevel('warn')` — Fehler + Warnungen | +| `#nb-console` | Hook / Selektor | +| `#nb-dashboard` | Hook / Selektor | +| `#nb-printers` | Hook / Selektor | +| `#nb-store` | Hook / Selektor | +| `#p-bed-inp` | Hook / Selektor | +| `#p-nozzle-inp` | Hook / Selektor | +| `#panel-console` | Hook / Selektor | +| `#panel-dashboard` | Hook / Selektor | +| `#panel-printers` | Hook / Selektor | +| `#panel-store` | Hook / Selektor | +| `#poll-1` | Hook / Selektor | +| `#poll-2` | Hook / Selektor | +| `#poll-5` | Hook / Selektor | +| `#printer-dropdown-btn` | Hook / Selektor | +| `#printers-grid` | Hook / Selektor | +| `#s-printer-ip` | Hook / Selektor | +| `#settings-btn` | Hook / Selektor | +| `#settings-modal` | Hook / Selektor | +| `#skip-confirm` | Hook / Selektor | +| `#skip-dialog` | Hook / Selektor | +| `#slot-edit-color` | Hook / Selektor | +| `#slot-edit-mat` | Hook / Selektor | +| `#slot-edit-modal` | Hook / Selektor | +| `#slot-edit-preview` | Hook / Selektor | +| `#slot-mat-btns` | Hook / Selektor | +| `#store-filter` | Hook / Selektor | +| `#store-grid` | Hook / Selektor | +| `#store-refresh-btn` | Hook / Selektor | +| `#store-search` | Hook / Selektor | +| `#store-sort` | Hook / Selektor | diff --git a/web/themes/default/app.js b/web/themes/default/app.js index aa8d591..9b75615 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -1,47 +1,85 @@ // ── State ── -var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0, - print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0, - curr_layer:0,total_layers:0,z_mm:0,printer_name:'Kobra X',firmware_version:'–', - camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80, - ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]},web_upload_warning:1}; -var tempHistory={n:[],b:[]}; -var camOn=false; -var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print -var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support) -var _lastLoadedFile=null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55) -var _idleCleared=false; // User hat idle-Datei explizit „geleert" → kein Nachladen von s.filename (Issue #57) -var _fdDialogOpen=false; // Dialog ist gerade offen -var _fdAutoOpenedFile=sessionStorage.getItem('fdAutoOpenedFile')||null; -var _fdUserCancelled=sessionStorage.getItem('fdUserCancelled')==='1'; -var currentStep=1; -var currentPanel='dashboard'; -var aceAutoRefillPrefs=(function(){ - try{return JSON.parse(localStorage.getItem('aceAutoRefillPrefs')||'{}')||{};}catch(_){return {};} -})(); -var aceDryProfiles=(function(){ - try{return JSON.parse(localStorage.getItem('aceDryProfiles')||'{}')||{};}catch(_){return {};} -})(); -var _aceDryDialogAceId=-1; -var _aceDryDialogPresetKey=''; -var _aceDryDialogPresetOriginals={}; -var ACE_DRY_PRESET_DEFAULTS={ - pla:{temp:45,duration_sec:4*3600}, - pla_plus:{temp:45,duration_sec:4*3600}, - petg:{temp:50,duration_sec:4*3600}, - tpu:{temp:55,duration_sec:4*3600}, - abs_asa:{temp:45,duration_sec:8*3600}, - pa_pc:{temp:55,duration_sec:12*3600} +var S = { + nozzle_temp: 0, + nozzle_target: 0, + bed_temp: 0, + bed_target: 0, + print_state: "standby", + filename: "", + progress: 0, + print_duration: 0, + remain_time: 0, + curr_layer: 0, + total_layers: 0, + z_mm: 0, + printer_name: "Kobra X", + firmware_version: "–", + camera_url: "", + fan_speed: 0, + print_speed_mode: 2, + light_on: false, + light_brightness: 80, + ams_slots: [], + filament_mode: "toolhead", + ace_units: [], + ace_dry_presets: null, + ace_drying: { + status: 0, + target_temp: 0, + duration: 0, + remain_time: 0, + humidity: null, + current_temp: null, + units: [], + }, + web_upload_warning: 1, }; -var ACE_DRY_PRESETS={ - pla:{temp:45,duration_sec:4*3600}, - pla_plus:{temp:45,duration_sec:4*3600}, - petg:{temp:50,duration_sec:4*3600}, - tpu:{temp:55,duration_sec:4*3600}, - abs_asa:{temp:45,duration_sec:8*3600}, - pa_pc:{temp:55,duration_sec:12*3600}, - custom_1:{name:'Custom 1',temp:45,duration_sec:4*3600}, - custom_2:{name:'Custom 2',temp:45,duration_sec:4*3600}, - custom_3:{name:'Custom 3',temp:45,duration_sec:4*3600} +var tempHistory = { n: [], b: [] }; +var camOn = false; +var camUserStopped = false; // user stopped camera manually — suppress auto-restart for this print +var _camPollInterval = null; // snapshot-polling interval for Android (no MJPEG support) +var _lastLoadedFile = null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55) +var _idleCleared = false; // User hat idle-Datei explizit „geleert" → kein Nachladen von s.filename (Issue #57) +var _fdDialogOpen = false; // Dialog ist gerade offen +var _fdAutoOpenedFile = sessionStorage.getItem("fdAutoOpenedFile") || null; +var _fdUserCancelled = sessionStorage.getItem("fdUserCancelled") === "1"; +var currentStep = 1; +var currentPanel = "dashboard"; +var aceAutoRefillPrefs = (function () { + try { + return JSON.parse(localStorage.getItem("aceAutoRefillPrefs") || "{}") || {}; + } catch (_) { + return {}; + } +})(); +var aceDryProfiles = (function () { + try { + return JSON.parse(localStorage.getItem("aceDryProfiles") || "{}") || {}; + } catch (_) { + return {}; + } +})(); +var _aceDryDialogAceId = -1; +var _aceDryDialogPresetKey = ""; +var _aceDryDialogPresetOriginals = {}; +var ACE_DRY_PRESET_DEFAULTS = { + pla: { temp: 45, duration_sec: 4 * 3600 }, + pla_plus: { temp: 45, duration_sec: 4 * 3600 }, + petg: { temp: 50, duration_sec: 4 * 3600 }, + tpu: { temp: 55, duration_sec: 4 * 3600 }, + abs_asa: { temp: 45, duration_sec: 8 * 3600 }, + pa_pc: { temp: 55, duration_sec: 12 * 3600 }, +}; +var ACE_DRY_PRESETS = { + pla: { temp: 45, duration_sec: 4 * 3600 }, + pla_plus: { temp: 45, duration_sec: 4 * 3600 }, + petg: { temp: 50, duration_sec: 4 * 3600 }, + tpu: { temp: 55, duration_sec: 4 * 3600 }, + abs_asa: { temp: 45, duration_sec: 8 * 3600 }, + pa_pc: { temp: 55, duration_sec: 12 * 3600 }, + custom_1: { name: "Custom 1", temp: 45, duration_sec: 4 * 3600 }, + custom_2: { name: "Custom 2", temp: 45, duration_sec: 4 * 3600 }, + custom_3: { name: "Custom 3", temp: 45, duration_sec: 4 * 3600 }, }; // Spoolman state @@ -49,19 +87,19 @@ var _spoolmanStatus={configured:false,reachable:false,server:'',sync_rate:0,slot var _spoolmanSpools=[]; var _slotSpoolMap={}; // {String(global_index): spoolman_spool_id} — last committed assignment -function _loadSpoolmanStatus(){ - fetch(_apiUrl('/kx/spoolman/status')).then(function(r){return r.json();}).then(function(d){ - _spoolmanStatus=d; - _slotSpoolMap=d.slot_spools||{}; +function _loadSpoolmanStatus() { + fetch(_apiUrl('/kx/spoolman/status')).then(function (r) { return r.json(); }).then(function (d) { + _spoolmanStatus = d; + _slotSpoolMap = d.slot_spools || {}; _updateSpoolmanStatusDot(); _buildSpoolmanSection(); renderSpoolmanSlotCard(); - if(d.configured){ - fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(sd){ - _spoolmanSpools=sd.spools||[]; + if (d.configured) { + fetch(_apiUrl('/kx/spoolman/spools')).then(function (r) { return r.json(); }).then(function (sd) { + _spoolmanSpools = sd.spools || []; }); } - }).catch(function(){}); + }).catch(function () { }); } function _updateSpoolmanStatusDot(){ var dot=document.getElementById('spoolman-status-dot'); @@ -76,19 +114,22 @@ function _updateSpoolmanStatusDot(){ } } -function _buildSpoolmanSection(){ - var sec=document.getElementById('fd-spoolman-section'); - var rows=document.getElementById('fd-spoolman-rows'); - var loading=document.getElementById('fd-spoolman-loading'); - if(!sec||!rows)return; - if(!_spoolmanStatus.configured){sec.style.display='none';return;} - sec.style.display=''; - rows.innerHTML=''; - if(loading)loading.style.display=''; +function _buildSpoolmanSection() { + var sec = document.getElementById("fd-spoolman-section"); + var rows = document.getElementById("fd-spoolman-rows"); + var loading = document.getElementById("fd-spoolman-loading"); + if (!sec || !rows) return; + if (!_spoolmanStatus.configured) { + sec.style.display = "none"; + return; + } + sec.style.display = ""; + rows.innerHTML = ""; + if (loading) loading.style.display = ""; - var usedSlots={}; - (_amsSlots||[]).forEach(function(slot){ - usedSlots[slot.slot_index]=slot; + var usedSlots = {}; + (_amsSlots || []).forEach(function (slot) { + usedSlots[slot.slot_index] = slot; }); fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(d){ @@ -116,168 +157,186 @@ function _buildSpoolmanSection(){ }).catch(function(){if(loading)loading.style.display='none';}); } -function _aceAutoRefillGet(aceId){return !!aceAutoRefillPrefs[String(aceId)];} -function _aceAutoRefillSet(aceId,on){ - aceAutoRefillPrefs[String(aceId)]=!!on; - localStorage.setItem('aceAutoRefillPrefs',JSON.stringify(aceAutoRefillPrefs)); +function _aceAutoRefillGet(aceId) { + return !!aceAutoRefillPrefs[String(aceId)]; } -function _aceDryProfileGet(aceId){ - var p=aceDryProfiles[String(aceId)]||{}; - var temp=parseInt(p.temp,10); - var dur=parseInt(p.duration_sec,10); - if(!Number.isFinite(temp))temp=45; - if(!Number.isFinite(dur))dur=4*3600; - temp=Math.max(30,Math.min(80,temp)); - dur=Math.max(10*60,Math.min(24*3600,dur)); - return {temp:temp,duration_sec:dur,preset:p.preset||''}; +function _aceAutoRefillSet(aceId, on) { + aceAutoRefillPrefs[String(aceId)] = !!on; + localStorage.setItem("aceAutoRefillPrefs", JSON.stringify(aceAutoRefillPrefs)); } -function _aceDryProfileSet(aceId,temp,durationSec,preset){ - aceDryProfiles[String(aceId)]={ - temp:Math.max(30,Math.min(80,parseInt(temp,10)||45)), - duration_sec:Math.max(10*60,Math.min(24*3600,parseInt(durationSec,10)||4*3600)), - preset:preset||'' +function _aceDryProfileGet(aceId) { + var p = aceDryProfiles[String(aceId)] || {}; + var temp = parseInt(p.temp, 10); + var dur = parseInt(p.duration_sec, 10); + if (!Number.isFinite(temp)) temp = 45; + if (!Number.isFinite(dur)) dur = 4 * 3600; + temp = Math.max(30, Math.min(80, temp)); + dur = Math.max(10 * 60, Math.min(24 * 3600, dur)); + return { temp: temp, duration_sec: dur, preset: p.preset || "" }; +} +function _aceDryProfileSet(aceId, temp, durationSec, preset) { + aceDryProfiles[String(aceId)] = { + temp: Math.max(30, Math.min(80, parseInt(temp, 10) || 45)), + duration_sec: Math.max(10 * 60, Math.min(24 * 3600, parseInt(durationSec, 10) || 4 * 3600)), + preset: preset || "", }; - localStorage.setItem('aceDryProfiles',JSON.stringify(aceDryProfiles)); + localStorage.setItem("aceDryProfiles", JSON.stringify(aceDryProfiles)); } -function _aceDryDurationMinFromSec(sec){ - var minutes=Math.round((parseInt(sec,10)||0)/60); - return Math.max(10,Math.min(1440,minutes)); +function _aceDryDurationMinFromSec(sec) { + var minutes = Math.round((parseInt(sec, 10) || 0) / 60); + return Math.max(10, Math.min(1440, minutes)); } -function _syncAceDryPresetsFromServer(raw){ - if(!raw||typeof raw!=='object')return; - Object.keys(ACE_DRY_PRESETS).forEach(function(k){ - var p=raw[k]; - if(!p||typeof p!=='object')return; - var t=parseInt(p.temp,10); - var d=parseInt(p.duration_sec,10); - if(Number.isFinite(t))ACE_DRY_PRESETS[k].temp=Math.max(30,Math.min(80,t)); - if(Number.isFinite(d))ACE_DRY_PRESETS[k].duration_sec=Math.max(10*60,Math.min(24*3600,d)); - if(/^custom_[123]$/.test(k)&&typeof p.name==='string'){ - var n=p.name.trim(); - ACE_DRY_PRESETS[k].name=n||('Custom '+k.slice(-1)); +function _syncAceDryPresetsFromServer(raw) { + if (!raw || typeof raw !== "object") return; + Object.keys(ACE_DRY_PRESETS).forEach(function (k) { + var p = raw[k]; + if (!p || typeof p !== "object") return; + var t = parseInt(p.temp, 10); + var d = parseInt(p.duration_sec, 10); + if (Number.isFinite(t)) ACE_DRY_PRESETS[k].temp = Math.max(30, Math.min(80, t)); + if (Number.isFinite(d)) + ACE_DRY_PRESETS[k].duration_sec = Math.max(10 * 60, Math.min(24 * 3600, d)); + if (/^custom_[123]$/.test(k) && typeof p.name === "string") { + var n = p.name.trim(); + ACE_DRY_PRESETS[k].name = n || "Custom " + k.slice(-1); } }); } // ── Theme ── -function toggleTheme(){ - var h=document.documentElement; - h.setAttribute('data-theme',h.getAttribute('data-theme')==='dark'?'light':'dark'); - localStorage.setItem('theme',h.getAttribute('data-theme')); +function 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)})(); +(function () { + var t = localStorage.getItem("theme"); + if (t) document.documentElement.setAttribute("data-theme", t); +})(); // ── i18n ── -var currentLang='de'; -var T={}; -var _langCache={}; +var currentLang = "de"; +var T = {}; +var _langCache = {}; -function tr(key,fallback){ - var v=T&&T[key]; - return (typeof v==='string'&&v.length)?v:(fallback!==undefined?fallback:''); +function tr(key, fallback) { + var v = T && T[key]; + return typeof v === "string" && v.length ? v : fallback !== undefined ? fallback : ""; } -function _langToggleLabel(lang){ - if(lang==='de')return 'Deutsch'; - if(lang==='en')return 'English'; - if(lang==='fr')return 'Français'; - if(lang==='it')return 'Italiano'; - if(lang==='zh-cn')return '简体中文'; - return 'Espanol'; +function _langToggleLabel(lang) { + if (lang === "de") return "Deutsch"; + if (lang === "en") return "English"; + if (lang === "fr") return "Français"; + if (lang === "it") return "Italiano"; + if (lang === "zh-cn") return "简体中文"; + return "Espanol"; } -function _mapSupportedLang(lang){ - if(!lang)return ''; - var l=String(lang).toLowerCase().replace(/_/g,'-').trim(); - if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='it'||l==='zh-cn')return l; +function _mapSupportedLang(lang) { + if (!lang) return ""; + var l = String(lang).toLowerCase().replace(/_/g, "-").trim(); + if (l === "de" || l === "en" || l === "es" || l === "fr" || l === "it" || l === "zh-cn") return l; - var base=l.split('-')[0]; - if(base==='de'||base==='en'||base==='es'||base==='fr'||base==='it')return base; + var base = l.split("-")[0]; + if (base === "de" || base === "en" || base === "es" || base === "fr" || base === "it") + return base; - if(base==='zh'){ - if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn'; + if (base === "zh") { + if (l.indexOf("cn") >= 0 || l.indexOf("hans") >= 0 || l === "zh") return "zh-cn"; } - return ''; + return ""; } -function _normalizeLang(lang){ - return _mapSupportedLang(lang)||'de'; +function _normalizeLang(lang) { + return _mapSupportedLang(lang) || "de"; } -function _detectBrowserLanguage(){ - var prefs=[]; - if(Array.isArray(navigator.languages)&&navigator.languages.length)prefs=navigator.languages; - else if(navigator.language)prefs=[navigator.language]; - for(var i=0;ie.textContent=T.label_set); - document.querySelectorAll('.lbl-off').forEach(e=>e.textContent=T.label_off); - setText('d-chart-label',T.panel_temps_chart); + document.querySelectorAll(".lbl-set").forEach((e) => (e.textContent = T.label_set)); + document.querySelectorAll(".lbl-off").forEach((e) => (e.textContent = T.label_off)); + setText("d-chart-label", T.panel_temps_chart); // Axis labels - setText('ptitle-motion-xy',T.panel_motion_xy); - document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z); - document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy); - document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all); - document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors); - document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':',''))); + setText('ptitle-motion-xy', T.panel_motion_xy); + document.querySelectorAll('.lbl-home-z').forEach(e => e.textContent = T.btn_home_z); + document.querySelectorAll('.lbl-home-xy').forEach(e => e.textContent = T.btn_home_xy); + document.querySelectorAll('.lbl-home-all').forEach(e => e.textContent = T.btn_home_all); + document.querySelectorAll('.lbl-disable-motors').forEach(e => e.textContent = T.btn_disable_motors); + document.querySelectorAll('.temp-input').forEach(e => e.setAttribute('placeholder', T.label_target_c.replace(':', ''))); // Console - setText('ptitle-console',T.panel_console_title); + setText("ptitle-console", T.panel_console_title); // Settings-Panel - setText('modal-sec-connection',T.settings_connection); - setText('modal-sec-print',T.settings_print); - setText('modal-sec-version',T.settings_version); + setText("modal-sec-connection", T.settings_connection); + setText("modal-sec-print", T.settings_print); + setText("modal-sec-notifications", T.settings_notifications); + var nhEl = document.getElementById("notif-hint"); + if (nhEl && T.settings_notifications_hint) nhEl.innerHTML = T.settings_notifications_hint; + setText("lbl-notif-add", T.settings_notif_add); + notifUpdateToggleBtn(); + setText("lbl-notif-interval", T.settings_notif_interval_lbl); + setText("lbl-notif-min-unit", T.settings_notif_min_unit); + setText("lbl-notif-layers-unit", T.settings_notif_layers_unit); + setText("lbl-notif-zero-off", T.settings_notif_zero_off); + setText("modal-sec-version", T.settings_version); // Nav + Kategorie-Labels (mit Fallback falls i18n-Key noch fehlt) - setText('nav-settings',T.nav_settings||'Einstellungen'); - setText('setcat-lbl-connection',T.settings_connection||'Verbindung'); - setText('setcat-lbl-printer',T.settings_print||'Drucker'); - setText('setcat-lbl-display',T.settings_cat_display||'Darstellung'); - setText('setcat-lbl-display2',T.settings_cat_display||'Darstellung'); - setText('setcat-lbl-filament',T.settings_cat_filament||'Filament'); - setText('setcat-lbl-integrations',T.settings_integrations||'Integrationen'); - setText('modal-sec-spoolman',T.modal_sec_spoolman||'Spoolman'); - setText('lbl-spoolman-url',T.lbl_spoolman_url||'Server-URL'); - setText('lbl-spoolman-sync-rate',T.lbl_spoolman_sync_rate||'Sync-Rate (s, 0=aus)'); - setText('modal-sec-obico',T.modal_sec_obico||'Obico'); - setText('setcat-lbl-system',T.settings_version||'System'); - setText('lbl-set-lang',T.settings_cat_language||'Sprache'); - setText('lbl-set-theme',T.settings_cat_theme||'Hell / Dunkel umschalten'); - setText('lbl-poll-interval',T.settings_poll||'Poll-Intervall (Sekunden)'); + setText("nav-settings", T.nav_settings || "Einstellungen"); + setText("setcat-lbl-connection", T.settings_connection || "Verbindung"); + setText("setcat-lbl-printer", T.settings_print || "Drucker"); + setText("setcat-lbl-display", T.settings_cat_display || "Darstellung"); + setText("setcat-lbl-display2", T.settings_cat_display || "Darstellung"); + setText("setcat-lbl-filament", T.settings_cat_filament || "Filament"); + setText("setcat-lbl-integrations", T.settings_integrations || "Integrationen"); + setText("modal-sec-spoolman", T.modal_sec_spoolman || "Spoolman"); + setText("lbl-spoolman-url", T.lbl_spoolman_url || "Server-URL"); + setText("lbl-spoolman-sync-rate", T.lbl_spoolman_sync_rate || "Sync-Rate (s, 0=aus)"); + setText("modal-sec-obico", T.modal_sec_obico || "Obico"); + var obicoBox = document.getElementById("obico-info-box"); + if (obicoBox) { + var cfgEl = document.getElementById("obico-cfg-path"); + var cfgPath = cfgEl ? cfgEl.textContent : ""; + obicoBox.innerHTML = + (T.obico_info_configured_via || + "Obico wird über den moonraker-obico-Container konfiguriert.") + + "
" + + (T.obico_config_file_label || "Config-Datei:") + + ' ' + + cfgPath + + ""; + } + setText("setcat-lbl-notifications", T.settings_cat_notifications || "Benachrichtigungen"); + setText("setcat-lbl-system", T.settings_cat_system || "System"); + setText("lbl-set-lang", T.settings_cat_language || "Sprache"); + setText("lbl-set-theme", T.settings_cat_theme || "Hell / Dunkel umschalten"); + setText("lbl-poll-interval", T.settings_poll || "Poll-Intervall (Sekunden)"); + var pollHint = document.getElementById("lbl-poll-hint"); + if (pollHint) + pollHint.textContent = + T.settings_poll_interval_hint || "Wie oft die Bridge den Drucker-Status abfragt"; setText('lbl-verbose-http-log',T.settings_verbose_http_log||'Log every HTTP request (verbose)'); var dashLbl=document.getElementById('dash-lbl-edit'); if(dashLbl)dashLbl.textContent=_dashEditing?(T.dash_done||'Done'):(T.dash_edit||'Customize dashboard'); @@ -432,163 +526,246 @@ function applyLang(){ setText('visible-vendors-hint',T.settings_visible_vendors_hint||'Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar.'); setText('lbl-visible-vendors-save',T.settings_visible_vendors_save||'Auswahl speichern'); // Custom-Profile-Import (Issue #41) - setText('modal-sec-orca-profiles',T.orca_profile_section); - setText('orca-profiles-hint',T.orca_profile_hint); - setText('lbl-orca-profiles-import',T.orca_profile_import_btn); - setText('lbl-slot-profile-import',T.orca_profile_import_link); - setText('profile-import-title',T.orca_profile_import_title); - setText('profile-import-dropmsg',T.orca_profile_dropmsg); - setText('profile-import-list-label',T.orca_profile_list_label); + setText("modal-sec-orca-profiles", T.orca_profile_section); + setText("orca-profiles-hint", T.orca_profile_hint); + setText("lbl-orca-profiles-import", T.orca_profile_import_btn); + setText("lbl-slot-profile-import", T.orca_profile_import_link); + setText("profile-import-title", T.orca_profile_import_title); + setText("profile-import-dropmsg", T.orca_profile_dropmsg); + setText("profile-import-list-label", T.orca_profile_list_label); // Hilfe-Text mit Inline-HTML — innerHTML statt setText - var helpEl=document.getElementById('profile-import-help'); - if(helpEl && T.orca_profile_help_html) helpEl.innerHTML=T.orca_profile_help_html; - setText('btn-save-settings',T.settings_save); - setText('lbl-printer-name',T.settings_printer_name); - setText('lbl-printer-ip',T.settings_printer_ip); - setText('lbl-mqtt-port',T.settings_mqtt_port); - setText('lbl-username',T.settings_username); - setText('lbl-password',T.settings_password); - setText('lbl-device-id',T.settings_device_id); - setText('lbl-mode-id',T.settings_mode_id); - setText('lbl-default-slot',T.settings_default_slot); - setText('opt-slot-auto',T.settings_slot_auto); - setText('lbl-auto-leveling',T.settings_auto_leveling); + var helpEl = document.getElementById("profile-import-help"); + if (helpEl && T.orca_profile_help_html) helpEl.innerHTML = T.orca_profile_help_html; + setText("btn-save-settings", T.settings_save); + setText("lbl-printer-name", T.settings_printer_name); + setText("lbl-printer-ip", T.settings_printer_ip); + setText("lbl-mqtt-port", T.settings_mqtt_port); + setText("lbl-username", T.settings_username); + setText("lbl-password", T.settings_password); + setText("lbl-device-id", T.settings_device_id); + setText("lbl-mode-id", T.settings_mode_id); + setText("lbl-default-slot", T.settings_default_slot); + setText("opt-slot-auto", T.settings_slot_auto); + setText("lbl-auto-leveling", T.settings_auto_leveling); setText('lbl-vibration-compensation',T.settings_vibration_compensation); - setText('lbl-file-ready-mode',T.settings_file_ready_mode); - setText('opt-file-ready-dialog',T.settings_file_ready_dialog); - setText('opt-file-ready-banner',T.settings_file_ready_banner); - setText('lbl-camera-on-print',T.settings_camera_on_print); - setText('lbl-web-upload-warning',T.settings_web_upload_warning); - setText('fd-options-title',T.fd_options_title); - setText('fd-lbl-auto-leveling',T.print_auto_leveling); + setText("lbl-file-ready-mode", T.settings_file_ready_mode); + setText("opt-file-ready-dialog", T.settings_file_ready_dialog); + setText("opt-file-ready-banner", T.settings_file_ready_banner); + setText("lbl-camera-on-print", T.settings_camera_on_print); + setText("lbl-web-upload-warning", T.settings_web_upload_warning); + setText("fd-options-title", T.fd_options_title); + setText("fd-lbl-auto-leveling", T.print_auto_leveling); - setText('lbl-update-check',T.update_check); - setText('lbl-update-apply',T.update_apply); + setText("lbl-update-check", T.update_check); + setText("lbl-update-apply", T.update_apply); // Progress-Karten-Aktionen für geladene/idle Datei (Issue #55) - setText('d-idle-print-lbl',T.progress_action_print||'Drucken'); - setText('d-idle-slots-lbl',T.progress_action_slots||'Slots zuordnen'); - setText('d-idle-clear-lbl',T.progress_action_clear||'Leeren'); + setText("d-idle-print-lbl", T.progress_action_print || "Drucken"); + setText("d-idle-slots-lbl", T.progress_action_slots || "Slots zuordnen"); + setText("d-idle-clear-lbl", T.progress_action_clear || "Leeren"); // Speed buttons - setText('d-spd-lbl-1',T.speed_silent.replace(/^\S+\s/,'')); - setText('d-spd-lbl-2',T.speed_normal.replace(/^\S+\s/,'')); - setText('d-spd-lbl-3',T.speed_sport.replace(/^\S+\s/,'')); + setText("d-spd-lbl-1", T.speed_silent.replace(/^\S+\s/, "")); + setText("d-spd-lbl-2", T.speed_normal.replace(/^\S+\s/, "")); + setText("d-spd-lbl-3", T.speed_sport.replace(/^\S+\s/, "")); // AMS feed/unload - document.querySelectorAll('.lbl-feed').forEach(e=>e.textContent=T.lbl_feed); - document.querySelectorAll('.lbl-unload').forEach(e=>e.textContent=T.lbl_unload); - for(var i=0;i<4;i++){ - setText('d-card-ace-dry-'+i,'ACE '+(i+1)+' - '+tr('ace_dry_dryer')); - setText('d-ace-auto-refill-label-'+i,tr('ace_dry_auto_refill')); - setText('d-ace-drying-enable-label-'+i,tr('ace_dry_enable')); - setText('d-ace-dry-humidity-label-'+i,tr('ace_dry_humidity')+':'); - setText('d-ace-dry-current-temp-label-'+i,tr('ace_dry_current_temp')+':'); - setText('d-ace-dry-target-label-'+i,tr('ace_dry_temp_line')+':'); - setText('d-ace-dry-time-label-'+i,tr('ace_dry_time_line')+':'); - setText('d-ace-dry-chart-label-'+i,tr('ace_dry_chart')); - var adTemp=document.getElementById('ace-dry-temp-'+i);if(adTemp)adTemp.setAttribute('placeholder',T.ace_dry_temp); - var adDur=document.getElementById('ace-dry-duration-'+i);if(adDur)adDur.setAttribute('placeholder',T.ace_dry_duration); + document.querySelectorAll(".lbl-feed").forEach((e) => (e.textContent = T.lbl_feed)); + document.querySelectorAll(".lbl-unload").forEach((e) => (e.textContent = T.lbl_unload)); + for (var i = 0; i < 4; i++) { + setText("d-card-ace-dry-" + i, "ACE " + (i + 1) + " - " + tr("ace_dry_dryer")); + setText("d-ace-auto-refill-label-" + i, tr("ace_dry_auto_refill")); + setText("d-ace-drying-enable-label-" + i, tr("ace_dry_enable")); + setText("d-ace-dry-humidity-label-" + i, tr("ace_dry_humidity") + ":"); + setText("d-ace-dry-current-temp-label-" + i, tr("ace_dry_current_temp") + ":"); + setText("d-ace-dry-target-label-" + i, tr("ace_dry_temp_line") + ":"); + setText("d-ace-dry-time-label-" + i, tr("ace_dry_time_line") + ":"); + setText("d-ace-dry-chart-label-" + i, tr("ace_dry_chart")); + var adTemp = document.getElementById("ace-dry-temp-" + i); + if (adTemp) adTemp.setAttribute("placeholder", T.ace_dry_temp); + var adDur = document.getElementById("ace-dry-duration-" + i); + if (adDur) adDur.setAttribute("placeholder", T.ace_dry_duration); } - setText('ace-dry-dialog-title',tr('ace_dry_dialog_title')); - setText('ace-dry-dialog-temp-label',tr('ace_dry_dialog_temp')); - setText('ace-dry-dialog-time-label',tr('ace_dry_dialog_time')); - setText('ace-dry-dialog-custom-name-label',tr('ace_dry_dialog_custom_name')); - setText('ace-dry-dialog-cancel',tr('ace_dry_dialog_cancel')); - setText('ace-dry-dialog-confirm',tr('ace_dry_dialog_confirm')); - setText('ace-dry-dialog-reset-default',tr('ace_dry_dialog_reset_default')); - setText('ace-dry-dialog-save-preset',tr('ace_dry_dialog_save_restart')); + setText("ace-dry-dialog-title", tr("ace_dry_dialog_title")); + setText("ace-dry-dialog-temp-label", tr("ace_dry_dialog_temp")); + setText("ace-dry-dialog-time-label", tr("ace_dry_dialog_time")); + setText("ace-dry-dialog-custom-name-label", tr("ace_dry_dialog_custom_name")); + setText("ace-dry-dialog-cancel", tr("ace_dry_dialog_cancel")); + setText("ace-dry-dialog-confirm", tr("ace_dry_dialog_confirm")); + setText("ace-dry-dialog-reset-default", tr("ace_dry_dialog_reset_default")); + setText("ace-dry-dialog-save-preset", tr("ace_dry_dialog_save_restart")); aceDryDialogSyncCustomButtonNames(); // conn-btn text (nur wenn nicht im Übergangszustand) updateConnBtn(); // Slot-Edit-Dialog - setText('lbl-slot-color',T.slot_edit_color); - setText('lbl-slot-material',T.slot_edit_material); - setText('lbl-slot-profile',T.slot_edit_profile); - setText('slot-profile-hint',T.slot_edit_profile_hint); - var defOpt=document.getElementById('slot-profile-default-opt'); - if(defOpt) defOpt.textContent=T.slot_edit_profile_default; - setText('btn-slot-edit-save',T.slot_edit_save); + setText("lbl-slot-color", T.slot_edit_color); + setText("lbl-slot-material", T.slot_edit_material); + setText("lbl-slot-profile", T.slot_edit_profile); + setText("slot-profile-hint", T.slot_edit_profile_hint); + var defOpt = document.getElementById("slot-profile-default-opt"); + if (defOpt) defOpt.textContent = T.slot_edit_profile_default; + setText("btn-slot-edit-save", T.slot_edit_save); updateSlotEditFeedButton(); - var mi=document.getElementById('slot-edit-mat');if(mi)mi.setAttribute('placeholder',T.slot_edit_custom); - setText('logdir-all',T.log_dir_all); - setText('loglvl-all',T.log_dir_all); - setText('log-lbl-level',T.log_lvl_label); - setText('file-ready-btn',T.file_ready_btn); - setText('file-slots-btn',T.file_slots_btn); - setText('file-cancel-btn',T.file_cancel_btn); + var mi = document.getElementById("slot-edit-mat"); + if (mi) mi.setAttribute("placeholder", T.slot_edit_custom); + setText("logdir-all", T.log_dir_all); + setText("loglvl-all", T.log_dir_all); + setText("log-lbl-level", T.log_lvl_label); + setText("file-ready-btn", T.file_ready_btn); + setText("file-slots-btn", T.file_slots_btn); + setText("file-cancel-btn", T.file_cancel_btn); + // Elements not yet covered by setText above + var settingsBtn = document.getElementById("settings-btn"); + if (settingsBtn) + settingsBtn.title = T.settings_btn_tooltip || T.settings_title || "Einstellungen"; + var snpEl = document.getElementById("s-printer-name"); + if (snpEl) snpEl.placeholder = T.settings_printer_name_placeholder || "z.B. Kobra X Links"; + var sdidEl = document.getElementById("s-device-id"); + if (sdidEl) sdidEl.placeholder = T.settings_device_id_placeholder || "32 Hex-Zeichen"; + setText("d-fan-off", T.label_off || "Aus"); + setText("skip-confirm", T.skip_confirm_btn || "Überspringen"); + setText("ams-no-data", T.ams_no_data || "Keine AMS-Daten empfangen"); + // Dialog: Add Printer + setText("apd-title", T.apd_title || "Drucker hinzufügen"); + setText("apd-cancel", T.apd_cancel || "Abbrechen"); + setText("apd-confirm", T.apd_confirm || "Hinzufügen"); + // Dialog: File Ready (Filament/Slot selection) + setText("fd-options-title", T.fd_options_title || "Druckoptionen"); + setText("fd-cancel", T.fd_cancel || "Abbrechen"); + setText("fd-print", T.fd_print || "▶ Drucken"); + // Dialog: Web Upload Verify + setText("store-web-verify-title", T.store_web_verify_title || "Datei verifizieren"); + setText( + "store-web-verify-msg", + T.store_web_verify_msg || + "Bitte bestätige, dass diese Datei für den Anycubic Kobra X erstellt wurde." + ); + setText("store-web-verify-confirm", T.store_web_verify_confirm || "Bestätigen"); + setText("store-web-verify-abort", T.store_web_verify_abort || "Abbrechen"); // GCode-Browser-Karten: Texte sind via innerHTML eingebacken, // bei Sprachwechsel komplett neu rendern. - if(typeof renderStore==='function' && typeof storeFiles!=='undefined'){ - try{ renderStore(); }catch(e){} + if (typeof renderStore === "function" && typeof storeFiles !== "undefined") { + try { + renderStore(); + } catch (e) { } } } -function setText(id,txt){var el=document.getElementById(id);if(el)el.textContent=txt;} +function setText(id, txt) { + var el = document.getElementById(id); + if (el) el.textContent = txt; +} -function ensureAceDryCards(){ - var grid=document.getElementById('d-ace-dry-grid'); - if(!grid||grid.getAttribute('data-init')==='1')return; - var html=''; - for(var i=0;i<4;i++){ - html+=''; +function ensureAceDryCards() { + var grid = document.getElementById("d-ace-dry-grid"); + if (!grid || grid.getAttribute("data-init") === "1") return; + var html = ""; + for (var i = 0; i < 4; i++) { + html += + '"; } - grid.innerHTML=html; - grid.setAttribute('data-init','1'); + grid.innerHTML = html; + grid.setAttribute("data-init", "1"); } -(function(){ - var l=_resolveInitialLanguage(); - currentLang=_normalizeLang(l); - var langSel=document.getElementById('lang-select'); - if(langSel)langSel.value=currentLang; - document.documentElement.setAttribute('lang',currentLang); +(function () { + var l = _resolveInitialLanguage(); + currentLang = _normalizeLang(l); + var langSel = document.getElementById("lang-select"); + if (langSel) langSel.value = currentLang; + document.documentElement.setAttribute("lang", currentLang); // defer until DOM ready - window.addEventListener('DOMContentLoaded',function(){ - setLanguage(currentLang).catch(function(){}); + window.addEventListener('DOMContentLoaded', function () { + setLanguage(currentLang).catch(function () { }); _loadSpoolmanStatus(); // Kein Drucker konfiguriert? → direkt in den Drucker-Tab (zeigt "+ Drucker hinzufügen") - fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){ - if(!d.result||!d.result.length){showPanel('printers');loadPrinterTab();} - }).catch(function(){}); + fetch("/kx/printers") + .then(function (r) { + return r.json(); + }) + .then(function (d) { + if (!d.result || !d.result.length) { + showPanel("printers"); + loadPrinterTab(); + } + }) + .catch(function () { }); }); })(); // ── Panel nav ── -function showPanel(id){ - document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active')); - document.getElementById('panel-'+id).classList.add('active'); - document.querySelectorAll('.nav-btn,.bnav-btn').forEach(b=>b.classList.remove('active')); - var nb=document.getElementById('nb-'+id);if(nb)nb.classList.add('active'); - var bnb=document.getElementById('bnb-'+id);if(bnb)bnb.classList.add('active'); - currentPanel=id; - if(id==='settings')openSettings(); +function showPanel(id) { + document.querySelectorAll(".panel").forEach((p) => p.classList.remove("active")); + document.getElementById("panel-" + id).classList.add("active"); + document.querySelectorAll(".nav-btn,.bnav-btn").forEach((b) => b.classList.remove("active")); + var nb = document.getElementById("nb-" + id); + if (nb) nb.classList.add("active"); + var bnb = document.getElementById("bnb-" + id); + if (bnb) bnb.classList.add("active"); + currentPanel = id; + if (id === "settings") openSettings(); } // Settings-Kategorie umschalten (Master-Detail) @@ -611,156 +788,219 @@ function showBrowserTab(name){ } // ── Console log ── -var consoleLogs=[]; -var logAutoScroll=true; -var logBadgeCount=0; -var logDirFilter='all'; // 'all'|'rx'|'tx' -var logLevelFilter='all'; // 'all'|'err'|'warn' -var logTopicFilter=''; // '' = no topic filter +var consoleLogs = []; +var logAutoScroll = true; +var logBadgeCount = 0; +var logDirFilter = "all"; // 'all'|'rx'|'tx' +var logLevelFilter = "all"; // 'all'|'err'|'warn' +var logTopicFilter = ""; // '' = no topic filter -function clog(msg,cls){ - cls=cls||'msg-info'; - var ts=new Date().toLocaleTimeString('de',{hour:'2-digit',minute:'2-digit',second:'2-digit'}); - _appendLog({ts:ts,lvl:'',name:'ui',msg:msg},cls); +function 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 _lvlCls(lvl) { + if (lvl === "ERROR" || lvl === "CRITICAL") return "msg-err"; + if (lvl === "WARNING") return "msg-warn"; + if (lvl === "DEBUG") return "msg-info"; + return "msg-ok"; } -function _appendLog(entry,forceCls){ - var cls=forceCls||_lvlCls(entry.lvl); - var label=entry.name?'['+entry.name+'] ':''; - var fullMsg=label+entry.msg; +function _appendLog(entry, forceCls) { + var cls = forceCls || _lvlCls(entry.lvl); + var label = entry.name ? "[" + entry.name + "] " : ""; + var fullMsg = label + entry.msg; // Wiederholungen als Zähler zusammenfassen (×N) statt N identische Zeilen. - var last=consoleLogs[consoleLogs.length-1]; - if(last&&last.msg===fullMsg&&last.cls===cls){ - last.count=(last.count||1)+1; - last.ts=entry.ts; // letzte Sichtung + var last = consoleLogs[consoleLogs.length - 1]; + if (last && last.msg === fullMsg && last.cls === cls) { + last.count = (last.count || 1) + 1; + last.ts = entry.ts; // letzte Sichtung renderLog(); return; } - consoleLogs.push({ts:entry.ts,msg:fullMsg,cls:cls,count:1}); - if(consoleLogs.length>500)consoleLogs.shift(); + consoleLogs.push({ ts: entry.ts, msg: fullMsg, cls: cls, count: 1 }); + if (consoleLogs.length > 500) consoleLogs.shift(); // Badge + Toast wenn Tab nicht aktiv und Fehler/Warnungen - if(currentPanel!=='console'&&(cls==='msg-err'||cls==='msg-warn')){ + 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;}}); + var bc = logBadgeCount > 99 ? "99+" : logBadgeCount; + ["log-badge", "log-badge-bot"].forEach(function (id) { + var b = document.getElementById(id); + if (b) { + b.style.display = "inline"; + b.textContent = bc; + } + }); } - if(cls==='msg-err')showToast(entry.msg.split('\n')[0]); + if (cls === "msg-err") showToast(entry.msg.split("\n")[0]); renderLog(); } // Kurze rote Snackbar bei Fehlern (auch wenn Konsole-Tab nicht offen). -var _toastTimer=null; -function showToast(msg){ - var t=document.getElementById('kx-toast'); - if(!t){ - t=document.createElement('div'); t.id='kx-toast'; - t.style.cssText='position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:var(--err);color:#fff;padding:10px 18px;border-radius:8px;font-size:13px;z-index:9999;max-width:90vw;box-shadow:0 4px 16px rgba(0,0,0,.4);cursor:pointer'; - t.onclick=function(){showPanel('console');t.style.display='none';}; +var _toastTimer = null; +function showToast(msg) { + var t = document.getElementById("kx-toast"); + if (!t) { + t = document.createElement("div"); + t.id = "kx-toast"; + t.style.cssText = + "position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:var(--err);color:#fff;padding:10px 18px;border-radius:8px;font-size:13px;z-index:9999;max-width:90vw;box-shadow:0 4px 16px rgba(0,0,0,.4);cursor:pointer"; + t.onclick = function () { + showPanel("console"); + t.style.display = "none"; + }; document.body.appendChild(t); } - t.textContent='⚠ '+msg; - t.style.display='block'; + t.textContent = "⚠ " + msg; + t.style.display = "block"; clearTimeout(_toastTimer); - _toastTimer=setTimeout(function(){t.style.display='none';},6000); + _toastTimer = setTimeout(function () { + t.style.display = "none"; + }, 6000); } -function setLogDir(dir){ - logDirFilter=dir; - document.querySelectorAll('.log-dir-btn').forEach(function(b){ - b.style.background=b.id==='logdir-'+dir?'var(--accent)':'var(--raised)'; - b.style.color=b.id==='logdir-'+dir?'#fff':'var(--txt2)'; +function setLogDir(dir) { + logDirFilter = dir; + document.querySelectorAll(".log-dir-btn").forEach(function (b) { + b.style.background = b.id === "logdir-" + dir ? "var(--accent)" : "var(--raised)"; + b.style.color = b.id === "logdir-" + dir ? "#fff" : "var(--txt2)"; }); renderLog(); } -function setLogLevel(lvl){ - logLevelFilter=lvl; - document.querySelectorAll('.log-lvl-btn').forEach(function(b){ - b.style.background=b.id==='loglvl-'+lvl?'var(--accent)':'var(--raised)'; - b.style.color=b.id==='loglvl-'+lvl?'#fff':'var(--txt2)'; +function setLogLevel(lvl) { + logLevelFilter = lvl; + document.querySelectorAll(".log-lvl-btn").forEach(function (b) { + b.style.background = b.id === "loglvl-" + lvl ? "var(--accent)" : "var(--raised)"; + b.style.color = b.id === "loglvl-" + lvl ? "#fff" : "var(--txt2)"; }); renderLog(); } -function setLogTopic(topic){ - var inp=document.getElementById('log-filter'); - var active=inp.value===topic; - inp.value=active?'':topic; - document.querySelectorAll('.log-topic-btn').forEach(function(b){ - var on=!active&&b.getAttribute('data-topic')===topic; - b.style.background=on?'var(--accent)':'var(--raised)'; - b.style.color=on?'#fff':'var(--txt2)'; +function setLogTopic(topic) { + var inp = document.getElementById("log-filter"); + var active = inp.value === topic; + inp.value = active ? "" : topic; + document.querySelectorAll(".log-topic-btn").forEach(function (b) { + var on = !active && b.getAttribute("data-topic") === topic; + b.style.background = on ? "var(--accent)" : "var(--raised)"; + b.style.color = on ? "#fff" : "var(--txt2)"; }); renderLog(); } -function renderLog(){ - var el=document.getElementById('console-log'); - if(!el)return; - var filter=(document.getElementById('log-filter')||{}).value||''; - var fl=filter.toLowerCase(); - var rows=consoleLogs.filter(function(l){ - var m=l.msg; - if(logDirFilter==='rx'&&!/ RX[ (]/.test(m))return false; - if(logDirFilter==='tx'&&!/ TX[ (]/.test(m))return false; - if(logLevelFilter==='err'&&l.cls!=='msg-err')return false; - if(logLevelFilter==='warn'&&l.cls!=='msg-err'&&l.cls!=='msg-warn')return false; - if(fl&&!m.toLowerCase().includes(fl))return false; +function renderLog() { + var el = document.getElementById("console-log"); + if (!el) return; + var filter = (document.getElementById("log-filter") || {}).value || ""; + var fl = filter.toLowerCase(); + var rows = consoleLogs.filter(function (l) { + var m = l.msg; + if (logDirFilter === "rx" && !/ RX[ (]/.test(m)) return false; + if (logDirFilter === "tx" && !/ TX[ (]/.test(m)) return false; + if (logLevelFilter === "err" && l.cls !== "msg-err") return false; + if (logLevelFilter === "warn" && l.cls !== "msg-err" && l.cls !== "msg-warn") return false; + if (fl && !m.toLowerCase().includes(fl)) return false; return true; }); - var savedScroll=logAutoScroll?null:el.scrollTop; - el.innerHTML=rows.map(function(l){ - var cnt=(l.count&&l.count>1)?' (×'+l.count+')':''; - return '
'+l.ts+''+escHtml(l.msg)+''+cnt+'
'; - }).join(''); - if(logAutoScroll)el.scrollTop=el.scrollHeight; - else if(savedScroll!==null)el.scrollTop=savedScroll; + var savedScroll = logAutoScroll ? null : el.scrollTop; + el.innerHTML = rows + .map(function (l) { + var cnt = l.count && l.count > 1 ? ' (×' + l.count + ")" : ""; + return ( + '
' + + l.ts + + '' + + escHtml(l.msg) + + "" + + cnt + + "
" + ); + }) + .join(""); + if (logAutoScroll) el.scrollTop = el.scrollHeight; + else if (savedScroll !== null) el.scrollTop = savedScroll; } -function onLogScroll(){ - var el=document.getElementById('console-log'); - if(!el)return; - var atBottom=el.scrollHeight-el.scrollTop-el.clientHeight<30; - if(!atBottom&&logAutoScroll){setAutoScroll(false);} -} -function toggleAutoScroll(){ - setAutoScroll(!logAutoScroll); - if(logAutoScroll){var el=document.getElementById('console-log');if(el)el.scrollTop=el.scrollHeight;} -} -function setAutoScroll(on){ - logAutoScroll=on; - var btn=document.getElementById('btn-autoscroll'); - if(btn){btn.style.background=on?'var(--accent)':'var(--raised)';btn.style.color=on?'#fff':'var(--txt2)';} -} -function clearLogBadge(){ - logBadgeCount=0; - ['log-badge','log-badge-bot'].forEach(function(id){var b=document.getElementById(id);if(b)b.style.display='none';}); -} -function escHtml(s){return s.replace(/&/g,'&').replace(//g,'>');} -// SSE server-log stream -(function(){ - function connect(){ - var es=new EventSource('/api/log/stream'); - es.onmessage=function(e){try{_appendLog(JSON.parse(e.data));}catch(_){}}; - es.onerror=function(){es.close();setTimeout(connect,3000);}; +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); } - window.addEventListener('DOMContentLoaded',connect); +} +function toggleAutoScroll() { + setAutoScroll(!logAutoScroll); + if (logAutoScroll) { + var el = document.getElementById("console-log"); + if (el) el.scrollTop = el.scrollHeight; + } +} +function setAutoScroll(on) { + logAutoScroll = on; + var btn = document.getElementById("btn-autoscroll"); + if (btn) { + btn.style.background = on ? "var(--accent)" : "var(--raised)"; + btn.style.color = on ? "#fff" : "var(--txt2)"; + } +} +function clearLogBadge() { + logBadgeCount = 0; + ["log-badge", "log-badge-bot"].forEach(function (id) { + var b = document.getElementById(id); + if (b) b.style.display = "none"; + }); +} +function escHtml(s) { + return s.replace(/&/g, "&").replace(//g, ">"); +} +// SSE server-log stream +(function () { + function connect() { + var es = new EventSource("/api/log/stream"); + es.onmessage = function (e) { + try { + _appendLog(JSON.parse(e.data)); + } catch (_) { } + }; + es.onerror = function () { + es.close(); + setTimeout(connect, 3000); + }; + } + window.addEventListener("DOMContentLoaded", connect); })(); // ── Helpers ── -function fmtTime(s){if(!s||s<0)return'–';var m=Math.floor(s/60),h=Math.floor(m/60);m%=60;return h>0?h+'h '+m+'m':m+'m'} -function fmtHmsFromSec(total){ - total=Math.max(0,parseInt(total||0,10)); - var h=Math.floor(total/3600); - var mm=Math.floor((total%3600)/60); - var ss=total%60; - return h+':'+String(mm).padStart(2,'0')+':'+String(ss).padStart(2,'0'); +function fmtTime(s) { + if (!s || s < 0) return "–"; + var m = Math.floor(s / 60), + h = Math.floor(m / 60); + m %= 60; + return h > 0 ? h + "h " + m + "m" : m + "m"; +} +function fmtHmsFromSec(total) { + total = Math.max(0, parseInt(total || 0, 10)); + var h = Math.floor(total / 3600); + var mm = Math.floor((total % 3600) / 60); + var ss = total % 60; + return h + ":" + String(mm).padStart(2, "0") + ":" + String(ss).padStart(2, "0"); +} +function post(url, body) { + return fetch(_apiUrl(url), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} +function clamp(v, lo, hi) { + return Math.min(hi, Math.max(lo, v)); } -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; +function applyState() { + var s = S; _syncAceDryPresetsFromServer(s.ace_dry_presets); // connection error banner – nur wenn überhaupt ein Drucker konfiguriert ist var banner=document.getElementById('conn-error-banner'); @@ -783,262 +1023,354 @@ function applyState(){ if(s.file_ready&&s.print_state==='standby'){ document.getElementById('file-ready-name').textContent=s.file_ready; // Neue Datei → Abbruch-Sperre aufheben - if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready){ - _fdUserCancelled=false; - sessionStorage.removeItem('fdUserCancelled'); - sessionStorage.removeItem('fdAutoOpenedFile'); + if (_fdAutoOpenedFile && _fdAutoOpenedFile !== s.file_ready) { + _fdUserCancelled = false; + sessionStorage.removeItem("fdUserCancelled"); + sessionStorage.removeItem("fdAutoOpenedFile"); } - if(shouldAutoOpen){ + if (shouldAutoOpen) { // Dialog-Modus: Banner niemals anzeigen. - frb.style.display='none'; - if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){ - _fdAutoOpenedFile=s.file_ready; - startReadyFileWithSlots(s.file_ready,true,s.filament_mismatch||null); + frb.style.display = 'none'; + if (!_fdDialogOpen && !_fdUserCancelled && _fdAutoOpenedFile !== s.file_ready) { + _fdAutoOpenedFile = s.file_ready; + startReadyFileWithSlots(s.file_ready, true, s.filament_mismatch || null); } } else { - frb.style.display='flex'; - bannerVisible=true; + frb.style.display = "flex"; + bannerVisible = true; } - }else{frb.style.display='none';} + } else { + frb.style.display = "none"; + } } // skip-button (mid-print) – nur sichtbar wenn aktuell gedruckt wird - var printing=(s.print_state==='printing'||s.print_state==='paused'); - var skipBtn=document.getElementById('d-btn-skip'); - if(skipBtn) skipBtn.style.display=printing?'':'none'; + var printing = s.print_state === "printing" || s.print_state === "paused"; + var skipBtn = document.getElementById("d-btn-skip"); + if (skipBtn) skipBtn.style.display = printing ? "" : "none"; // Pause/Stopp-Buttons nur bei aktivem Druck zeigen (sonst verwirrend wenn // der Drucker idle ist). Pause-Button rendert sich passend zum State um. - var ctrlBtns=document.getElementById('d-ctrl-btns'); - if(ctrlBtns) ctrlBtns.style.display=printing?'':'none'; + var ctrlBtns = document.getElementById("d-ctrl-btns"); + if (ctrlBtns) ctrlBtns.style.display = printing ? "" : "none"; updatePauseResumeBtn(); // Zuletzt geladene Datei merken (Issue #55): solange sie über den State // sichtbar ist. Beim Druckende/Abbruch leert die Bridge file_ready+filename // (Issue #29) — die gemerkte Referenz bleibt für die Karten-Aktionen. // Echte ready-Datei oder laufender Druck hebt einen vorherigen „Clear" auf. - if(s.file_ready||printing) _idleCleared=false; - if(s.file_ready) _lastLoadedFile=s.file_ready; - else if(s.filename && !_idleCleared) _lastLoadedFile=s.filename; - else if(_idleCleared) _lastLoadedFile=null; + if (s.file_ready || printing) _idleCleared = false; + if (s.file_ready) _lastLoadedFile = s.file_ready; + else if (s.filename && !_idleCleared) _lastLoadedFile = s.filename; + else if (_idleCleared) _lastLoadedFile = null; // Idle-Aktionen (Drucken/Slots/Leeren) nur wenn nicht gedruckt wird, eine // Datei bekannt ist und der grüne Banner nicht ohnehin schon dieselbe Aktion // anbietet. - var idleBtns=document.getElementById('d-idle-btns'); - if(idleBtns){ - var showIdle=(!printing && _lastLoadedFile && !bannerVisible); - idleBtns.style.display=showIdle?'':'none'; - if(showIdle){ - var dfn=document.getElementById('d-fname'); - if(dfn && (!dfn.textContent || dfn.textContent==='–')){ - dfn.textContent=_lastLoadedFile;dfn.title=_lastLoadedFile; + var idleBtns = document.getElementById("d-idle-btns"); + if (idleBtns) { + var showIdle = !printing && _lastLoadedFile && !bannerVisible; + idleBtns.style.display = showIdle ? "" : "none"; + if (showIdle) { + var dfn = document.getElementById("d-fname"); + if (dfn && (!dfn.textContent || dfn.textContent === "–")) { + dfn.textContent = _lastLoadedFile; + dfn.title = _lastLoadedFile; } } } // header - var b=document.getElementById('h-badge'); - b.className='hbadge '+s.print_state; - document.getElementById('h-state').textContent=T['kobra_'+s.kobra_state]||s.kobra_state||T.header_status_standby; - var _pn=_printers.length===0?'–':((_activePrinter&&_activePrinter.name)||s.printer_name); - var _el=document.getElementById('h-pname');if(_el)_el.textContent=_pn; - var _el2=document.getElementById('h-pname-single');if(_el2)_el2.textContent=_pn; - var hv=document.getElementById('h-version');if(hv&&s.version)hv.textContent='v'+s.version; - + 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); + 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)+'%'; + 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 pct = Math.round(s.progress * 100); + var dpct = document.getElementById("d-pct"); + if (dpct) dpct.textContent = pct; + var dpbar = document.getElementById("d-pbar"); + if (dpbar) dpbar.style.width = pct + "%"; - var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'–'; - var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers; - var dzpos=document.getElementById('d-zpos');if(dzpos)dzpos.textContent=s.z_mm>0?s.z_mm.toFixed(2)+' mm':'–'; + var layers = s.curr_layer && s.total_layers ? "L " + s.curr_layer + " / " + s.total_layers : "–"; + var dlayers = document.getElementById("d-layers"); + if (dlayers) dlayers.textContent = layers; + var dzpos = document.getElementById("d-zpos"); + if (dzpos) dzpos.textContent = s.z_mm > 0 ? s.z_mm.toFixed(2) + " mm" : "–"; - var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration); - var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'–'; - var dslrow=document.getElementById('d-slicer-row'); - var dsltime=document.getElementById('d-slicer-time'); - if(dslrow&&dsltime){ - if(s.slicer_time>0){dslrow.style.display='';dsltime.textContent=fmtTime(s.slicer_time);} - else{dslrow.style.display='none';} + var 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:''; + 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'; + 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=''; + 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; + 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 spdWidths = { 1: 25, 2: 55, 3: 90 }; + [1, 2, 3].forEach(function (m) { + var b = document.getElementById("d-spd-" + m); + if (b) b.classList.toggle("spd-active", s.print_speed_mode === m); }); - var spdBar=document.getElementById('d-spd-bar'); - if(spdBar) spdBar.style.width=(spdWidths[s.print_speed_mode]||55)+'%'; + var spdBar = document.getElementById("d-spd-bar"); + if (spdBar) spdBar.style.width = (spdWidths[s.print_speed_mode] || 55) + "%"; - var amsTitle=document.getElementById('d-card-ams'); - if(amsTitle){ - var baseTitle=tr('card_ams'); - var modeMap={toolhead:'Toolhead',ace_direct:'ACE Direct',ace_hub:'ACE Hub'}; - var modeTxt=modeMap[s.filament_mode]||''; - amsTitle.textContent=modeTxt?(baseTitle+' - '+modeTxt):baseTitle; + var amsTitle = document.getElementById("d-card-ams"); + if (amsTitle) { + var baseTitle = tr("card_ams"); + var modeMap = { + toolhead: "Toolhead", + ace_direct: "ACE Direct", + ace_hub: "ACE Hub", + }; + var modeTxt = modeMap[s.filament_mode] || ""; + amsTitle.textContent = modeTxt ? baseTitle + " - " + modeTxt : baseTitle; } ensureAceDryCards(); - var dry=s.ace_drying||{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]}; - var units=(dry.units||[]); - var unitMap={}; - units.forEach(function(u){var id=Number(u.id);if(id>=0&&id<=3)unitMap[id]=u;}); - var aceMode=s.filament_mode==='ace_direct'||s.filament_mode==='ace_hub'; - var detected=(s.ace_units||[]).filter(function(id){return id>=0&&id<=3;}); - if(!detected.length){ - Object.keys(unitMap).forEach(function(k){detected.push(Number(k));}); + var dry = s.ace_drying || { + status: 0, + target_temp: 0, + duration: 0, + remain_time: 0, + humidity: null, + current_temp: null, + units: [], + }; + var units = dry.units || []; + var unitMap = {}; + units.forEach(function (u) { + var id = Number(u.id); + if (id >= 0 && id <= 3) unitMap[id] = u; + }); + var aceMode = s.filament_mode === "ace_direct" || s.filament_mode === "ace_hub"; + var detected = (s.ace_units || []).filter(function (id) { + return id >= 0 && id <= 3; + }); + if (!detected.length) { + Object.keys(unitMap).forEach(function (k) { + detected.push(Number(k)); + }); } - if(!detected.length){ - (s.ams_slots||[]).forEach(function(sl){var id=Number(sl.box_id);if(id>=0&&id<=3&&detected.indexOf(id)<0)detected.push(id);}); + if (!detected.length) { + (s.ams_slots || []).forEach(function (sl) { + var id = Number(sl.box_id); + if (id >= 0 && id <= 3 && detected.indexOf(id) < 0) detected.push(id); + }); } - detected.sort(function(a,b){return a-b;}); - var aceWrap=document.getElementById('d-ace-dry-wrap'); - if(aceWrap)aceWrap.style.display=(aceMode&&detected.length)?'contents':'none'; - for(var i=0;i<4;i++){ - var card=document.getElementById('d-ace-dry-card-'+i); - if(!card)continue; - var show=aceMode&&detected.indexOf(i)>=0; - card.style.display=show?'':'none'; - if(!show)continue; - var ud=unitMap[i]||dry; - var refillToggle=document.getElementById('ace-auto-refill-toggle-'+i); - var autoFeedMap=s.ace_auto_feed||{}; - if(refillToggle&&!_aceAutoFeedPending[i]){ - var afVal=autoFeedMap.hasOwnProperty(String(i))?Number(autoFeedMap[String(i)]):(_aceAutoRefillGet(i)?1:0); - refillToggle.checked=afVal===1; + detected.sort(function (a, b) { + return a - b; + }); + var aceWrap = document.getElementById("d-ace-dry-wrap"); + if (aceWrap) aceWrap.style.display = aceMode && detected.length ? "contents" : "none"; + for (var i = 0; i < 4; i++) { + var card = document.getElementById("d-ace-dry-card-" + i); + if (!card) continue; + var show = aceMode && detected.indexOf(i) >= 0; + card.style.display = show ? "" : "none"; + if (!show) continue; + var ud = unitMap[i] || dry; + var refillToggle = document.getElementById("ace-auto-refill-toggle-" + i); + var autoFeedMap = s.ace_auto_feed || {}; + if (refillToggle && !_aceAutoFeedPending[i]) { + var afVal = Object.prototype.hasOwnProperty.call(autoFeedMap, String(i)) + ? Number(autoFeedMap[String(i)]) + : _aceAutoRefillGet(i) + ? 1 + : 0; + refillToggle.checked = afVal === 1; } - var dryToggle=document.getElementById('ace-dry-enable-toggle-'+i); - if(dryToggle)dryToggle.checked=Number(ud.status||0)>0; - var hh=document.getElementById('d-ace-dry-humidity-'+i); - if(hh){ - var hv=(ud.humidity===null||ud.humidity===undefined||ud.humidity==='')?null:Number(ud.humidity); - hh.textContent=(hv===null||Number.isNaN(hv))?'-':(Math.round(hv)+'%'); + var dryToggle = document.getElementById("ace-dry-enable-toggle-" + i); + if (dryToggle) dryToggle.checked = Number(ud.status || 0) > 0; + var hh = document.getElementById("d-ace-dry-humidity-" + i); + if (hh) { + var hum = + ud.humidity === null || ud.humidity === undefined || ud.humidity === "" + ? null + : Number(ud.humidity); + hh.textContent = hum === null || Number.isNaN(hum) ? "-" : Math.round(hum) + "%"; } - var ht=document.getElementById('d-ace-dry-current-temp-'+i); - if(ht){ - var ct=(ud.current_temp===null||ud.current_temp===undefined||ud.current_temp==='')?null:Number(ud.current_temp); - ht.textContent=(ct===null||Number.isNaN(ct))?'-':(ct.toFixed(1)+'°C'); + var ht = document.getElementById("d-ace-dry-current-temp-" + i); + if (ht) { + var ct = + ud.current_temp === null || ud.current_temp === undefined || ud.current_temp === "" + ? null + : Number(ud.current_temp); + ht.textContent = ct === null || Number.isNaN(ct) ? "-" : ct.toFixed(1) + "°C"; } - var prof=_aceDryProfileGet(i); - var useSec=(Number(ud.status||0)>0&&Number(ud.remain_time)>0) - ?Number(ud.remain_time||0)*60 - :prof.duration_sec; - var showTemp=(Number(ud.status||0)>0&&Number(ud.target_temp)>0)?Number(ud.target_temp):prof.temp; - var dryTempEl=document.getElementById('d-ace-dry-target-'+i); - if(dryTempEl)dryTempEl.textContent=showTemp+'°C'; - var dryTimeEl=document.getElementById('d-ace-dry-time-'+i); - if(dryTimeEl)dryTimeEl.textContent=fmtHmsFromSec(useSec); + var prof = _aceDryProfileGet(i); + var useSec = + Number(ud.status || 0) > 0 && Number(ud.remain_time) > 0 + ? Number(ud.remain_time || 0) * 60 + : prof.duration_sec; + var showTemp = + Number(ud.status || 0) > 0 && Number(ud.target_temp) > 0 ? Number(ud.target_temp) : prof.temp; + var dryTempEl = document.getElementById("d-ace-dry-target-" + i); + if (dryTempEl) dryTempEl.textContent = showTemp + "°C"; + var dryTimeEl = document.getElementById("d-ace-dry-time-" + i); + if (dryTimeEl) dryTimeEl.textContent = fmtHmsFromSec(useSec); } // AMS - if(s.ams_slots&&s.ams_slots.length){ - window._amsSlots=s.ams_slots; + if (s.ams_slots && s.ams_slots.length) { + window._amsSlots = s.ams_slots; // Group by box_id (-1=Toolhead, 0=ACE 1, 1=ACE 2, ...) - var boxMap={}; - s.ams_slots.forEach(function(slot,i){ - var bid=slot.box_id!=null?slot.box_id:-1; - if(!boxMap[bid])boxMap[bid]=[]; - boxMap[bid].push({slot:slot,arrIdx:i}); + var boxMap = {}; + s.ams_slots.forEach(function (slot, i) { + var bid = slot.box_id != null ? slot.box_id : -1; + if (!boxMap[bid]) boxMap[bid] = []; + boxMap[bid].push({ slot: slot, arrIdx: i }); }); - var boxIds=Object.keys(boxMap).map(Number).sort(function(a,b){return a-b}); - var acePresent=boxIds.some(function(b){return b>=0;}); - var html=''; - boxIds.forEach(function(bid){ - var entries=boxMap[bid]; - var label=bid===-1 - ?(acePresent?'Toolhead (Slots 1–3)':'Toolhead') - :('ACE '+(bid+1)); - html+='
' - +'
'+label+'
' - +'
'; - entries.forEach(function(e){ - var slot=e.slot;var i=e.arrIdx; - var empty=slot.status!==5; - var rgb=empty?[80,80,80]:(Array.isArray(slot.color)?slot.color:[128,128,128]); - var col='rgb('+rgb[0]+','+rgb[1]+','+rgb[2]+')'; - var globalIdx=slot.global_index!=null?slot.global_index:i; - var active=slot.status===1||slot.active; - var loaded=(s.ams_loaded_slot!=null&&s.ams_loaded_slot>=0&&globalIdx===s.ams_loaded_slot); - var activity=(slot.activity||''); - var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'–'); - var slotLabel=T.label_slot+' '+(globalIdx+1); + var boxIds = Object.keys(boxMap) + .map(Number) + .sort(function (a, b) { + return a - b; + }); + var acePresent = boxIds.some(function (b) { + return b >= 0; + }); + var html = ""; + boxIds.forEach(function (bid) { + var entries = boxMap[bid]; + var label = + bid === -1 ? (acePresent ? "Toolhead (Slots 1–3)" : "Toolhead") : "ACE " + (bid + 1); + html += + '
' + + '
' + + label + + "
" + + '
'; + entries.forEach(function (e) { + var slot = e.slot; + var i = e.arrIdx; + var empty = slot.status !== 5; + var rgb = empty ? [80, 80, 80] : Array.isArray(slot.color) ? slot.color : [128, 128, 128]; + var col = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")"; + var globalIdx = slot.global_index != null ? slot.global_index : i; + var active = slot.status === 1 || slot.active; + var loaded = + s.ams_loaded_slot != null && s.ams_loaded_slot >= 0 && globalIdx === s.ams_loaded_slot; + var activity = slot.activity || ""; + var pct = empty + ? T.ams_empty + : slot.consumables_percent != null + ? slot.consumables_percent + "%" + : "–"; + var slotLabel = T.label_slot + " " + (globalIdx + 1); // Gemapptes Profil nur für belegte Slots verwenden — sonst zeigt ein // verwaistes Mapping (Slot wurde geleert) ein „Geister"-Profil (Issue #57). - var profile=empty?null:(window._slotProfileMap||{})[globalIdx]; - var genericType=(slot.type||slot.material_type||'–'); + var profile = empty ? null : (window._slotProfileMap || {})[globalIdx]; + var genericType = slot.type || slot.material_type || "–"; // Material-Label: bei belegtem Slot mit Mapping den konkreten Profilnamen // (z.B. „eSUN PLA+") statt nur des generischen Typs zeigen (Issue #57 Punkt 4). - var materialLabel=empty?'–':((profile&&profile.name)?profile.name:genericType); - var vendorBadge=''; - if(!empty && profile && profile.vendor){ - var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':''); - vendorBadge='
'+profile.vendor+'
'; + var materialLabel = empty ? "–" : profile && profile.name ? profile.name : genericType; + var vendorBadge = ""; + if (!empty && profile && profile.vendor) { + var tt = (profile.name || "") + (profile.id ? " (" + profile.id + ")" : ""); + vendorBadge = + '
' + + profile.vendor + + "
"; } - var spoolSel=''; - if(_spoolmanStatus.configured&&!empty&&_spoolmanSpools.length){ - var curSpool=_slotSpoolMap[String(globalIdx)]||''; - var spoolOpts=''+_spoolmanSpools.map(function(sp){ - var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':''; - var name=sp.filament?sp.filament.name:'#'+sp.id; - var rem=sp.remaining_weight!=null?' '+sp.remaining_weight.toFixed(0)+'g':''; - return ''; + var spoolSel = ''; + if (_spoolmanStatus.configured && !empty && _spoolmanSpools.length) { + var curSpool = _slotSpoolMap[String(globalIdx)] || ''; + var spoolOpts = '' + _spoolmanSpools.map(function (sp) { + var vendor = sp.filament && sp.filament.vendor ? sp.filament.vendor.name + ' ' : ''; + var name = sp.filament ? sp.filament.name : '#' + sp.id; + var rem = sp.remaining_weight != null ? ' ' + sp.remaining_weight.toFixed(0) + 'g' : ''; + return ''; }).join(''); - spoolSel='
' - +'
🧵 Spoolman
' - +'' - +'
'; + spoolSel = '
' + + '
🧵 Spoolman
' + + '' + + '
'; } - html+='
' - +'
' - +'
'+materialLabel+'
' - +vendorBadge - +'
'+slotLabel+'
' - +'
'+pct+'
' - +spoolSel - +'
' - +'
'; + html += '
' + + '
' + + '
' + materialLabel + '
' + + vendorBadge + + '
' + slotLabel + '
' + + '
' + pct + '
' + + spoolSel + + '
' + + '
'; }); - if(bid===-1&&acePresent){ - html+='
' - +'
ACE
' - +'
'; + if (bid === -1 && acePresent) { + html += + '
' + + '
ACE
' + + "
"; } - html+='
'; + html += "
"; }); // Nicht rendern wenn ein Spool-Dropdown gerade offen ist (verhindert Schließen beim Poll) var activeEl=document.activeElement; @@ -1047,130 +1379,400 @@ function applyState(){ } // camera overlay - var co=document.getElementById('cam-overlay'); - if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none'; + var co = document.getElementById("cam-overlay"); + if (co) co.style.display = s.print_state === "printing" && camOn ? "block" : "none"; // auto-start camera during print (unless user explicitly stopped it) - if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped&&s.camera_on_print){ + if ( + s.print_state === "printing" && + !camOn && + s.camera_url && + !camUserStopped && + s.camera_on_print + ) { camStart(); } // reset user-stopped flag when print ends so next print auto-starts again - if(s.print_state!=='printing'){ - camUserStopped=false; + if (s.print_state !== "printing") { + camUserStopped = false; } updateConnBtn(); } -function updateConnBtn(){ - var btn=document.getElementById('conn-btn'); - if(!btn)return; - var offline=S.kobra_state==='offline'; - if(offline){ - btn.className='conn-btn disconnected'; - btn.textContent=tr('btn_connect'); +function updateConnBtn() { + var btn = document.getElementById("conn-btn"); + if (!btn) return; + var offline = S.kobra_state === "offline"; + if (offline) { + btn.className = "conn-btn disconnected"; + btn.textContent = tr("btn_connect"); } else { - btn.className='conn-btn connected'; - btn.textContent=tr('btn_disconnect'); + btn.className = "conn-btn connected"; + btn.textContent = tr("btn_disconnect"); } } -function toggleConnection(){ - var btn=document.getElementById('conn-btn'); - var offline=S.kobra_state==='offline'; - btn.disabled=true; - btn.textContent='…'; - var url=offline?'/api/connect':'/api/disconnect'; - post(url,{}).then(function(r){return r.json()}).then(function(r){ - btn.disabled=false; - if(r.error)addLog('Error: '+r.error); - }).catch(function(){btn.disabled=false;}); +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) _appendLog("Error: " + r.error); + }) + .catch(function () { + btn.disabled = false; + }); } // ── Temp history + chart ── -function updateHistory(){ +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}]); + 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); +function drawChart(id, _, series) { + var canvas = document.getElementById(id); + if (!canvas) return; + var ctx = canvas.getContext("2d"); + var W = canvas.offsetWidth * window.devicePixelRatio || canvas.width; + var H = canvas.offsetHeight * window.devicePixelRatio || canvas.height; + canvas.width = W; + canvas.height = H; + ctx.clearRect(0, 0, W, H); + series.forEach(function (s) { + var data = s.data; + if (!data.length) return; + var max = s.max; + ctx.beginPath(); + ctx.strokeStyle = s.color; + ctx.lineWidth = 2; + ctx.lineJoin = "round"; + data.forEach(function (v, i) { + var x = (i / Math.max(data.length - 1, 1)) * (W - 4) + 2; + var y = H - 4 - (v / max) * (H - 8); + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); }); ctx.stroke(); }); } -// ── Settings Modal ── -var _updateTag=''; -var _updateUrl=''; -function openSettings(){ - fetch(_apiUrl('/api/settings')).then(function(r){return r.json()}).then(function(d){ - document.getElementById('s-printer-name').value=d.printer_name||''; - document.getElementById('s-printer-ip').value=d.printer_ip||''; - document.getElementById('s-mqtt-port').value=d.mqtt_port||9883; - document.getElementById('s-username').value=d.username||''; - document.getElementById('s-password').value=d.password||''; - document.getElementById('s-device-id').value=d.device_id||''; - document.getElementById('s-mode-id').value=d.mode_id||''; - document.getElementById('s-default-slot').value=d.default_ams_slot||'auto'; - document.getElementById('s-auto-leveling').checked=(d.auto_leveling===undefined?true:!!d.auto_leveling); - var vc=document.getElementById('s-vibration-compensation');if(vc)vc.checked=!!d.vibration_compensation; - var cop=document.getElementById('s-camera-on-print');if(cop)cop.checked=!!d.camera_on_print; - var frm=document.getElementById('s-file-ready-mode');if(frm)frm.value=(d.print_start_dialog===undefined?'1':String(d.print_start_dialog?1:0)); - var wuw=document.getElementById('s-web-upload-warning');if(wuw)wuw.checked=(d.web_upload_warning===undefined?true:!!d.web_upload_warning); - // Poll-Intervall (Sekunden) — Backend hat Vorrang vor localStorage - var pi=document.getElementById('s-poll-interval'); - if(pi){ - var sec=d.poll_interval||Math.round((parseInt(localStorage.getItem('pollInterval')||'2000'))/1000)||3; - pi.value=sec; - } - var vhl=document.getElementById('s-verbose-http-log');if(vhl)vhl.checked=!!d.verbose_http_log; - renderFilamentMapping(d.filament_profiles||{}); - renderSpoolmanSlotCard(); - // Spoolman - var su=document.getElementById('s-spoolman-url');if(su)su.value=d.spoolman_server||''; - var sr=document.getElementById('s-spoolman-sync-rate');if(sr)sr.value=(d.spoolman_sync_rate!==undefined?d.spoolman_sync_rate:30); - _updateSpoolmanStatusDot(); +// ── Notifications ── +var _notifRows = []; +var _notifPaused = false; +var _NOTIF_EVENTS = ["started", "finished", "failed", "cancelled", "paused", "progress"]; +function notifUpdateToggleBtn() { + var btn = document.getElementById("btn-notif-toggle"); + var lbl = document.getElementById("lbl-notif-toggle"); + if (!btn) return; + if (_notifPaused) { + if (lbl) lbl.textContent = T.settings_notif_paused || "Paused"; + btn.style.background = "var(--warn, #c08a2d)"; + btn.style.color = "#fff"; + } else { + if (lbl) lbl.textContent = T.settings_notif_active || "Active"; + btn.style.background = "var(--ok, #2e9e4f)"; + btn.style.color = "#fff"; + } +} +function notifTogglePaused() { + var target = !_notifPaused; + var btn = document.getElementById("btn-notif-toggle"); + if (btn) btn.disabled = true; + post("/api/notifications/pause", { paused: target }) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + if (btn) btn.disabled = false; + _notifPaused = d && typeof d.paused === "boolean" ? d.paused : target; + notifUpdateToggleBtn(); + }) + .catch(function () { + if (btn) btn.disabled = false; + }); +} +function notifRenderList(entries) { + _notifRows = entries.map(function (e) { + return { + url: e.url || "", + events: e.events || [], + include_image: !!e.include_image, + enabled: e.enabled === undefined ? true : !!e.enabled, + }; }); + notifRefreshDOM(); +} +function notifRefreshDOM() { + var container = document.getElementById("notif-list"); + if (!container) return; + if (!_notifRows.length) { + container.innerHTML = + '
' + + (T.settings_notif_empty || "No notifications configured.") + + "
"; + return; + } + container.innerHTML = _notifRows + .map(function (row, idx) { + var evChecks = _NOTIF_EVENTS + .map(function (ev) { + var checked = row.events.indexOf(ev) >= 0 ? "checked" : ""; + var lbl = T["settings_notif_ev_" + ev] || ev; + return ( + '" + ); + }) + .join(" "); + var imgCheck = + '"; + return ( + '
' + + '
' + + '" + + '' + + '" + + '' + + "
" + + '
' + + evChecks + + imgCheck + + "
" + + '
' + + "
" + ); + }) + .join(""); +} +function _escHtml(s) { + return (s || "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} +function notifAddRow() { + _notifRows.push({ + url: "", + events: ["finished", "failed"], + include_image: false, + enabled: true, + }); + notifRefreshDOM(); +} +function notifRemoveRow(idx) { + _notifRows.splice(idx, 1); + notifRefreshDOM(); +} +function notifSetUrl(idx, val) { + if (_notifRows[idx]) _notifRows[idx].url = val; +} +function notifToggleEvent(idx, ev) { + if (!_notifRows[idx]) return; + var evs = _notifRows[idx].events; + var pos = evs.indexOf(ev); + if (pos >= 0) evs.splice(pos, 1); + else evs.push(ev); +} +function notifToggleImage(idx) { + if (_notifRows[idx]) _notifRows[idx].include_image = !_notifRows[idx].include_image; +} +function notifToggleEnabled(idx) { + if (!_notifRows[idx]) return; + _notifRows[idx].enabled = !_notifRows[idx].enabled; + notifRefreshDOM(); +} +function notifCollect() { + return _notifRows + .filter(function (r) { + return r.url.trim(); + }) + .map(function (r) { + return { + url: r.url.trim(), + events: r.events, + include_image: !!r.include_image, + enabled: r.enabled === undefined ? true : !!r.enabled, + }; + }); +} +function notifTest(idx) { + var row = _notifRows[idx]; + if (!row || !row.url.trim()) return; + var btn = document.getElementById("notif-test-btn-" + idx); + var status = document.getElementById("notif-test-status-" + idx); + if (btn) btn.disabled = true; + if (status) { + status.textContent = "…"; + status.style.color = "var(--txt2)"; + } + post("/api/notifications/test", { url: row.url.trim() }) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + if (btn) btn.disabled = false; + if (status) { + if (d && d.status === "ok") { + status.textContent = "✓ " + (T.settings_notif_test_ok || "Sent"); + status.style.color = "var(--ok)"; + } else { + status.textContent = + "✗ " + (d && d.message ? d.message : T.settings_notif_test_fail || "Failed"); + status.style.color = "var(--err)"; + } + } + }) + .catch(function (e) { + if (btn) btn.disabled = false; + if (status) { + status.textContent = "✗ " + e; + status.style.color = "var(--err)"; + } + }); +} + +// ── Settings Modal ── +var _updateTag = ""; +var _updateUrl = ""; +function openSettings() { + fetch(_apiUrl("/api/settings")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + document.getElementById("s-printer-name").value = d.printer_name || ""; + document.getElementById("s-printer-ip").value = d.printer_ip || ""; + document.getElementById("s-mqtt-port").value = d.mqtt_port || 9883; + document.getElementById("s-username").value = d.username || ""; + document.getElementById("s-password").value = d.password || ""; + document.getElementById("s-device-id").value = d.device_id || ""; + document.getElementById("s-mode-id").value = d.mode_id || ""; + document.getElementById("s-default-slot").value = d.default_ams_slot || "auto"; + document.getElementById("s-auto-leveling").checked = + d.auto_leveling === undefined ? true : !!d.auto_leveling; + var vc=document.getElementById('s-vibration-compensation');if(vc)vc.checked=!!d.vibration_compensation; + var cop = document.getElementById("s-camera-on-print"); + if (cop) cop.checked = !!d.camera_on_print; + var frm = document.getElementById("s-file-ready-mode"); + if (frm) + frm.value = d.print_start_dialog === undefined ? "1" : String(d.print_start_dialog ? 1 : 0); + var wuw = document.getElementById("s-web-upload-warning"); + if (wuw) wuw.checked = d.web_upload_warning === undefined ? true : !!d.web_upload_warning; + notifRenderList(d.notifications || []); + _notifPaused = !!d.notifications_paused; + notifUpdateToggleBtn(); + var enm = document.getElementById("s-notif-every-min"); + if (enm) enm.value = d.notify_every_minutes || 0; + var enl = document.getElementById("s-notif-every-layers"); + if (enl) enl.value = d.notify_every_layers || 0; + // Poll-Intervall (Sekunden) — Backend hat Vorrang vor localStorage + var pi = document.getElementById("s-poll-interval"); + if (pi) { + var sec = + d.poll_interval || + Math.round(parseInt(localStorage.getItem("pollInterval") || "2000") / 1000) || + 3; + pi.value = sec; + } + var vhl=document.getElementById('s-verbose-http-log');if(vhl)vhl.checked=!!d.verbose_http_log; + renderFilamentMapping(d.filament_profiles || {}); + renderSpoolmanSlotCard(); + // Spoolman + var su = document.getElementById("s-spoolman-url"); + if (su) su.value = d.spoolman_server || ""; + var sr = document.getElementById("s-spoolman-sync-rate"); + if (sr) sr.value = d.spoolman_sync_rate !== undefined ? d.spoolman_sync_rate : 30; + _updateSpoolmanStatusDot(); + }); // Sprach-Select im Settings-Panel mit aktueller Sprache spiegeln - var ls=document.getElementById('s-lang-select'); - if(ls)ls.value=(localStorage.getItem('lang')||document.documentElement.lang||'de'); - document.getElementById('s-version-label').textContent='v'+('__VERSION__'||'?'); - document.getElementById('update-status').textContent=''; - document.getElementById('btn-update-apply').style.display='none'; - var cl=document.getElementById('update-changelog');if(cl)cl.style.display='none'; - _updateTag='';_updateUrl=''; + var ls = document.getElementById("s-lang-select"); + if (ls) ls.value = localStorage.getItem("lang") || document.documentElement.lang || "de"; + var appVersion = "__VERSION__"; + document.getElementById("s-version-label").textContent = "v" + (appVersion || "?"); + document.getElementById("update-status").textContent = ""; + document.getElementById("btn-update-apply").style.display = "none"; + var cl = document.getElementById("update-changelog"); + if (cl) cl.style.display = "none"; + _updateTag = ""; + _updateUrl = ""; // Custom-Profile-Liste laden (Issue #41) refreshUserProfileList(); // Vendor-Sichtbarkeitsfilter (Issue #41 Option A) loadVendorChecklist(); } -function closeSettings(){ +function closeSettings() { // Panel-Variante: zurück zum Dashboard - showPanel('dashboard'); + showPanel("dashboard"); } // Poll-Intervall-Feld → Live-Poll sofort übernehmen (Persistenz erst beim Speichern) -function onPollIntervalInput(){ - var pi=document.getElementById('s-poll-interval'); - if(!pi)return; - var sec=parseInt(pi.value,10); - if(sec>=1&&sec<=60)setPoll(sec*1000); +function onPollIntervalInput() { + var pi = document.getElementById("s-poll-interval"); + if (!pi) return; + var sec = parseInt(pi.value, 10); + if (sec >= 1 && sec <= 60) setPoll(sec * 1000); } // ── Filament-Profil-Mapping pro Slot ([filament_profiles]) ── @@ -1179,277 +1781,393 @@ function onPollIntervalInput(){ // mehr durch manuelle Eingabe brechen (Issue #57 Punkt 1). Optionen werden aus // /kx/filament/profiles geladen, nach Vendor gruppiert, User-Profile zuerst, // mit demselben Vendor-Sichtbarkeitsfilter wie das Slot-Edit-Dropdown. -function renderFilamentMapping(map){ - var el=document.getElementById('filament-mapping-list'); - if(!el)return; - var rows=''; - for(var i=0;i<4;i++){ - var m=map[i]||map[String(i)]||{}; - var idHint=m.id?' ('+m.id+')':''; - rows+=''; +function renderFilamentMapping(map) { + var el = document.getElementById("filament-mapping-list"); + if (!el) return; + var rows = ""; + for (var i = 0; i < 4; i++) { + var m = map[i] || map[String(i)] || {}; + var idHint = m.id + ? ' (' + m.id + ")" + : ""; + rows += + '"; } - el.innerHTML=rows; + el.innerHTML = rows; // Dropdowns befüllen (async, geteilter Profil-Cache + Vendor-Filter) - for(var j=0;j<4;j++){ _fillMappingDropdown(j); } + for (var j = 0; j < 4; j++) { + _fillMappingDropdown(j); + } } -function _fillMappingDropdown(slot){ - var sel=document.getElementById('fmap-'+slot); - if(!sel) return; - var wantKey=_profileKey(sel.dataset.vendor, sel.dataset.name); - _loadOrcaFilaments(function(profiles){ - sel.innerHTML=''; - var userProfs=profiles.filter(function(p){return p.is_user;}); - var systemProfs=profiles.filter(function(p){return !p.is_user;}); - function _opt(g,p){ - var o=document.createElement('option'); - o.value=_profileKey(p.vendor,p.name); - o.dataset.vendor=p.vendor; o.dataset.name=p.name; o.dataset.id=p.id||''; - o.textContent=(p.is_user?'★ ':'')+p.name+(p.vendor?' — '+p.vendor:''); - if(o.value===wantKey)o.selected=true; +function _fillMappingDropdown(slot) { + var sel = document.getElementById("fmap-" + slot); + if (!sel) return; + var wantKey = _profileKey(sel.dataset.vendor, sel.dataset.name); + _loadOrcaFilaments(function (profiles) { + sel.innerHTML = + '"; + var userProfs = profiles.filter(function (p) { + return p.is_user; + }); + var systemProfs = profiles.filter(function (p) { + return !p.is_user; + }); + function _opt(g, p) { + var o = document.createElement("option"); + o.value = _profileKey(p.vendor, p.name); + o.dataset.vendor = p.vendor; + o.dataset.name = p.name; + o.dataset.id = p.id || ""; + o.textContent = (p.is_user ? "★ " : "") + p.name + (p.vendor ? " — " + p.vendor : ""); + if (o.value === wantKey) o.selected = true; g.appendChild(o); } - if(userProfs.length){ - var gUser=document.createElement('optgroup'); - gUser.label='★ '+(tr('orca_profile_user_label')||'Eigene Profile'); - userProfs.forEach(function(p){_opt(gUser,p);}); + if (userProfs.length) { + var gUser = document.createElement("optgroup"); + gUser.label = "★ " + (tr("orca_profile_user_label") || "Eigene Profile"); + userProfs.forEach(function (p) { + _opt(gUser, p); + }); sel.appendChild(gUser); } - _loadVisibleVendors(function(vis){ - var filtered=systemProfs; - if(vis&&vis.length){ - var allow={};vis.forEach(function(v){allow[v]=1;});allow['Generic']=1; - filtered=systemProfs.filter(function(p){return allow[p.vendor];}); + _loadVisibleVendors(function (vis) { + var filtered = systemProfs; + if (vis && vis.length) { + var allow = {}; + vis.forEach(function (v) { + allow[v] = 1; + }); + allow["Generic"] = 1; + filtered = systemProfs.filter(function (p) { + return allow[p.vendor]; + }); } - var byVendor={}; - filtered.forEach(function(p){(byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p);}); - Object.keys(byVendor).sort().forEach(function(v){ - var g=document.createElement('optgroup');g.label=v; - byVendor[v].forEach(function(p){_opt(g,p);}); - sel.appendChild(g); + var byVendor = {}; + filtered.forEach(function (p) { + (byVendor[p.vendor] = byVendor[p.vendor] || []).push(p); }); + Object.keys(byVendor) + .sort() + .forEach(function (v) { + var g = document.createElement("optgroup"); + g.label = v; + byVendor[v].forEach(function (p) { + _opt(g, p); + }); + sel.appendChild(g); + }); }); }); } -function saveFilamentMapping(){ +function saveFilamentMapping() { // Nutzt den per-Slot-Endpoint (vendor,name → ID-Lookup im Backend). // Leere Auswahl ("") = Mapping entfernen. - var chain=Promise.resolve(); - for(var i=0;i<4;i++){ - (function(slot){ - var sel=document.getElementById('fmap-'+slot); - var opt=sel?sel.options[sel.selectedIndex]:null; - var vendor=(opt&&opt.dataset.vendor)||''; - var name=(opt&&opt.dataset.name)||''; - chain=chain.then(function(){ - return fetch(_apiUrl('/kx/filament/slots/'+slot+'/profile'), - {method:'POST',headers:{'Content-Type':'application/json'}, - body:JSON.stringify({vendor:vendor,name:name})}); + var chain = Promise.resolve(); + for (var i = 0; i < 4; i++) { + (function (slot) { + var sel = document.getElementById("fmap-" + slot); + var opt = sel ? sel.options[sel.selectedIndex] : null; + var vendor = (opt && opt.dataset.vendor) || ""; + var name = (opt && opt.dataset.name) || ""; + chain = chain.then(function () { + return fetch(_apiUrl("/kx/filament/slots/" + slot + "/profile"), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ vendor: vendor, name: name }), + }); }); })(i); } - chain.then(function(){ - clog(tr('log_filament_mapping_saved')||'Filament-Mapping gespeichert','msg-ok'); - openSettings(); // neu laden → ID-Hints aktualisieren - }).catch(function(e){clog('Mapping-Fehler: '+e,'msg-err');}); + chain + .then(function () { + clog(tr("log_filament_mapping_saved") || "Filament-Mapping gespeichert", "msg-ok"); + openSettings(); // neu laden → ID-Hints aktualisieren + }) + .catch(function (e) { + clog("Mapping-Fehler: " + e, "msg-err"); + }); } // ── Vendor-Sichtbarkeitsfilter (Issue #41 Option A) ── -var _vendorChecklistSel={}; // {vendor:true} — laufende Auswahl im UI -function loadVendorChecklist(){ +var _vendorChecklistSel = {}; // {vendor:true} — laufende Auswahl im UI +function loadVendorChecklist() { // aktuelle Auswahl aus Backend, dann alle verfügbaren Vendoren rendern - _visibleVendors=null; // Cache invalidieren - _loadVisibleVendors(function(vis){ - _vendorChecklistSel={}; - (vis||[]).forEach(function(v){_vendorChecklistSel[v]=true;}); + _visibleVendors = null; // Cache invalidieren + _loadVisibleVendors(function (vis) { + _vendorChecklistSel = {}; + (vis || []).forEach(function (v) { + _vendorChecklistSel[v] = true; + }); renderVendorChecklist(); }); } -function renderVendorChecklist(){ - var el=document.getElementById('visible-vendors-list'); - if(!el)return; - _loadOrcaFilaments(function(profiles){ +function renderVendorChecklist() { + var el = document.getElementById("visible-vendors-list"); + if (!el) return; + _loadOrcaFilaments(function (profiles) { // alle System-Vendoren (ohne Generic — der ist immer sichtbar) sammeln - var set={}; - profiles.forEach(function(p){ if(!p.is_user && p.vendor && p.vendor!=='Generic') set[p.vendor]=1; }); - var vendors=Object.keys(set).sort(); - var q=((document.getElementById('vendor-filter-search')||{}).value||'').toLowerCase(); - if(q)vendors=vendors.filter(function(v){return v.toLowerCase().indexOf(q)>=0;}); - el.innerHTML=vendors.map(function(v){ - var ck=_vendorChecklistSel[v]?'checked':''; - var safe=v.replace(/"/g,'"'); - return ''; - }).join('')||''; + var set = {}; + profiles.forEach(function (p) { + if (!p.is_user && p.vendor && p.vendor !== "Generic") set[p.vendor] = 1; + }); + var vendors = Object.keys(set).sort(); + var q = ((document.getElementById("vendor-filter-search") || {}).value || "").toLowerCase(); + if (q) + vendors = vendors.filter(function (v) { + return v.toLowerCase().indexOf(q) >= 0; + }); + el.innerHTML = + vendors + .map(function (v) { + var ck = _vendorChecklistSel[v] ? "checked" : ""; + var safe = v.replace(/"/g, """); + return ( + '" + ); + }) + .join("") || ''; }); } -function _vendorCheck(cb){ - var v=cb.getAttribute('data-vendor'); - if(cb.checked)_vendorChecklistSel[v]=true; else delete _vendorChecklistSel[v]; +function _vendorCheck(cb) { + var v = cb.getAttribute("data-vendor"); + if (cb.checked) _vendorChecklistSel[v] = true; + else delete _vendorChecklistSel[v]; } -function renderSpoolmanSlotCard(){ - var card=document.getElementById('spoolman-slot-card'); - var rows=document.getElementById('spoolman-slot-rows'); - if(!card||!rows)return; - if(!_spoolmanStatus.configured){card.style.display='none';return;} - card.style.display=''; +function renderSpoolmanSlotCard() { + var card = document.getElementById('spoolman-slot-card'); + var rows = document.getElementById('spoolman-slot-rows'); + if (!card || !rows) return; + if (!_spoolmanStatus.configured) { card.style.display = 'none'; return; } + card.style.display = ''; Promise.all([ - fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}), - fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}) - ]).then(function(res){ - var spools=res[0].spools||[]; - var slots=(res[1].result||[]).sort(function(a,b){return a.slot_index-b.slot_index;}); - if(!slots.length){rows.innerHTML='Keine AMS-Slots bekannt.';return;} - rows.innerHTML=slots.map(function(slot){ - var idx=parseInt(slot.slot_index); - var col=slot.color_hex||'#888'; - var mat=slot.material||''; - var current=_slotSpoolMap[String(idx)]||''; - var opts=''+spools.map(function(sp){ - var rem=sp.remaining_weight!=null?' ('+sp.remaining_weight.toFixed(0)+'g)':''; - var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':''; - var name=sp.filament?sp.filament.name:'Spool #'+sp.id; - var mat2=sp.filament&&sp.filament.material?' · '+sp.filament.material:''; - return ''; + fetch(_apiUrl('/kx/spoolman/spools')).then(function (r) { return r.json(); }), + fetch(_apiUrl('/kx/filament/slots')).then(function (r) { return r.json(); }) + ]).then(function (res) { + var spools = res[0].spools || []; + var slots = (res[1].result || []).sort(function (a, b) { return a.slot_index - b.slot_index; }); + if (!slots.length) { rows.innerHTML = 'Keine AMS-Slots bekannt.'; return; } + rows.innerHTML = slots.map(function (slot) { + var idx = parseInt(slot.slot_index); + var col = slot.color_hex || '#888'; + var mat = slot.material || ''; + var current = _slotSpoolMap[String(idx)] || ''; + var opts = '' + spools.map(function (sp) { + var rem = sp.remaining_weight != null ? ' (' + sp.remaining_weight.toFixed(0) + 'g)' : ''; + var vendor = sp.filament && sp.filament.vendor ? sp.filament.vendor.name + ' ' : ''; + var name = sp.filament ? sp.filament.name : 'Spool #' + sp.id; + var mat2 = sp.filament && sp.filament.material ? ' · ' + sp.filament.material : ''; + return ''; }).join(''); - return '
'+ - ''+ - 'Slot '+(idx+1)+' '+escHtml(mat)+''+ - '
'; + return '
' + + '' + + 'Slot ' + (idx + 1) + ' ' + escHtml(mat) + '' + + '
'; }).join(''); - }).catch(function(){rows.innerHTML='Spoolman nicht erreichbar';}); + }).catch(function () { rows.innerHTML = 'Spoolman nicht erreichbar'; }); } -function onAmsSpoolChange(sel){ - var idx=sel.getAttribute('data-spool-slot'); - var val=sel.value; - if(val) _slotSpoolMap[String(idx)]=parseInt(val); +function onAmsSpoolChange(sel) { + var idx = sel.getAttribute('data-spool-slot'); + var val = sel.value; + if (val) _slotSpoolMap[String(idx)] = parseInt(val); else delete _slotSpoolMap[String(idx)]; - var mapping={}; - Object.keys(_slotSpoolMap).forEach(function(k){mapping[k]=_slotSpoolMap[k];}); - fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST', - headers:{'Content-Type':'application/json'},body:JSON.stringify({slot_spools:mapping})}); -} - -function saveSpoolmanSlots(){ - var mapping={}; - document.querySelectorAll('#spoolman-slot-rows select[data-spool-slot]').forEach(function(sel){ - var idx=sel.getAttribute('data-spool-slot'); - var val=sel.value; - if(val)mapping[idx]=parseInt(val); - }); - fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST',headers:{'Content-Type':'application/json'}, - body:JSON.stringify({slot_spools:mapping})}).then(function(r){return r.json();}).then(function(d){ - _slotSpoolMap=d.slot_spools||{}; + var mapping = {}; + Object.keys(_slotSpoolMap).forEach(function (k) { mapping[k] = _slotSpoolMap[k]; }); + fetch(_apiUrl('/kx/spoolman/active-spool'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slot_spools: mapping }) }); } -function saveVisibleVendors(){ - var vendors=Object.keys(_vendorChecklistSel); - fetch(_apiUrl('/kx/filament/visible_vendors'),{method:'POST',headers:{'Content-Type':'application/json'}, - body:JSON.stringify({vendors:vendors})}).then(function(r){return r.json();}).then(function(){ - _visibleVendors=vendors.slice(); // Dropdown-Cache aktualisieren - clog(tr('log_visible_vendors_saved')||'Hersteller-Auswahl gespeichert','msg-ok'); - }).catch(function(e){clog('Vendor-Filter-Fehler: '+e,'msg-err');}); +function saveSpoolmanSlots() { + var mapping = {}; + document.querySelectorAll('#spoolman-slot-rows select[data-spool-slot]').forEach(function (sel) { + var idx = sel.getAttribute('data-spool-slot'); + var val = sel.value; + if (val) mapping[idx] = parseInt(val); + }); + fetch(_apiUrl('/kx/spoolman/active-spool'), { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slot_spools: mapping }) + }).then(function (r) { return r.json(); }).then(function (d) { + _slotSpoolMap = d.slot_spools || {}; + }); +} + +function saveVisibleVendors() { + var vendors = Object.keys(_vendorChecklistSel); + fetch(_apiUrl('/kx/filament/visible_vendors'), { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ vendors: vendors }) + }).then(function (r) { return r.json(); }).then(function () { + _visibleVendors = vendors.slice(); // Dropdown-Cache aktualisieren + clog(tr('log_visible_vendors_saved') || 'Hersteller-Auswahl gespeichert', 'msg-ok'); + }).catch(function (e) { clog('Vendor-Filter-Fehler: ' + e, 'msg-err'); }); } // ── Custom Filament Profile Import (Issue #41) ── -function refreshUserProfileList(){ - var listEl=document.getElementById('orca-profiles-list'); - if(!listEl) return; - fetch(_apiUrl('/kx/filament/profiles/user')).then(function(r){return r.json();}).then(function(d){ - var profs=(d && d.result)||[]; - if(!profs.length){ - listEl.innerHTML=''+(tr('orca_profile_user_empty')||'– keine –')+''; - return; - } - listEl.innerHTML=profs.map(function(p){ - var label=p.vendor+' / '+p.name+' ('+p.type+')'; - return '
' - +'★ '+label+'' - +'' - +'
'; - }).join(''); - }).catch(function(){}); +function refreshUserProfileList() { + var listEl = document.getElementById("orca-profiles-list"); + if (!listEl) return; + fetch(_apiUrl("/kx/filament/profiles/user")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + var profs = (d && d.result) || []; + if (!profs.length) { + listEl.innerHTML = + '' + (tr("orca_profile_user_empty") || "– keine –") + ""; + return; + } + listEl.innerHTML = profs + .map(function (p) { + var label = p.vendor + " / " + p.name + " (" + p.type + ")"; + return ( + '
' + + "★ " + + label + + "" + + "' + + "
" + ); + }) + .join(""); + }) + .catch(function () { }); } -function deleteUserProfile(vendor, name){ - fetch(_apiUrl('/kx/filament/profiles/user?vendor='+vendor+'&name='+name), {method:'DELETE'}) - .then(function(r){return r.json();}) - .then(function(){ - _orcaFilamentCache=null; +function deleteUserProfile(vendor, name) { + fetch(_apiUrl("/kx/filament/profiles/user?vendor=" + vendor + "&name=" + name), { + method: "DELETE", + }) + .then(function (r) { + return r.json(); + }) + .then(function () { + _orcaFilamentCache = null; refreshUserProfileList(); // Falls Import-Dialog offen ist, dort auch refreshen refreshImportDialogList(); }); } -function openProfileImport(){ - document.getElementById('profile-import-status').textContent=''; +function openProfileImport() { + document.getElementById("profile-import-status").textContent = ""; refreshImportDialogList(); - document.getElementById('profile-import-modal').classList.add('open'); + document.getElementById("profile-import-modal").classList.add("open"); } -function closeProfileImport(){ - document.getElementById('profile-import-modal').classList.remove('open'); +function closeProfileImport() { + document.getElementById("profile-import-modal").classList.remove("open"); } -function refreshImportDialogList(){ - var el=document.getElementById('profile-import-list'); - if(!el) return; - fetch(_apiUrl('/kx/filament/profiles/user')).then(function(r){return r.json();}).then(function(d){ - var profs=(d && d.result)||[]; - if(!profs.length){ - el.innerHTML=''+(tr('orca_profile_user_empty')||'– keine –')+''; - return; - } - el.innerHTML=profs.map(function(p){ - var label=p.vendor+' / '+p.name+' ('+p.type+')'; - return '
' - +'★ '+label+'' - +'' - +'
'; - }).join(''); - }).catch(function(){}); +function refreshImportDialogList() { + var el = document.getElementById("profile-import-list"); + if (!el) return; + fetch(_apiUrl("/kx/filament/profiles/user")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + var profs = (d && d.result) || []; + if (!profs.length) { + el.innerHTML = + '' + (tr("orca_profile_user_empty") || "– keine –") + ""; + return; + } + el.innerHTML = profs + .map(function (p) { + var label = p.vendor + " / " + p.name + " (" + p.type + ")"; + return ( + '
' + + "★ " + + label + + "" + + "' + + "
" + ); + }) + .join(""); + }) + .catch(function () { }); } -function doProfileImportUpload(files){ - if(!files || !files.length) return; - var status=document.getElementById('profile-import-status'); - status.textContent=(tr('orca_profile_uploading')||'Lade hoch…'); - status.style.color='var(--txt2)'; - var done=0, totalAdded=0, totalSkipped=0; - function _one(idx){ - if(idx>=files.length){ - status.textContent=(tr('orca_profile_done')||'Importiert')+': '+totalAdded - +(totalSkipped?' / '+totalSkipped+' '+(tr('orca_profile_skipped')||'übersprungen'):''); - status.style.color='var(--ok)'; - _orcaFilamentCache=null; +function doProfileImportUpload(files) { + if (!files || !files.length) return; + var status = document.getElementById("profile-import-status"); + status.textContent = tr("orca_profile_uploading") || "Lade hoch…"; + status.style.color = "var(--txt2)"; + var done = 0, + totalAdded = 0, + totalSkipped = 0; + function _one(idx) { + if (idx >= files.length) { + status.textContent = + (tr("orca_profile_done") || "Importiert") + + ": " + + totalAdded + + (totalSkipped + ? " / " + totalSkipped + " " + (tr("orca_profile_skipped") || "übersprungen") + : ""); + status.style.color = "var(--ok)"; + _orcaFilamentCache = null; refreshImportDialogList(); refreshUserProfileList(); // Vendor-Checkliste neu aufbauen — ein Import kann einen bisher // unbekannten System-Vendor mitbringen (Issue #41). - if(document.getElementById('visible-vendors-list')) renderVendorChecklist(); + if (document.getElementById("visible-vendors-list")) renderVendorChecklist(); // Wenn Slot-Edit offen ist, Dropdown gleich neu befüllen - var mat=document.getElementById('slot-edit-mat'); - if(mat && document.getElementById('slot-edit-modal').classList.contains('open')){ - _fillSlotProfileDropdown(mat.value, '', ''); + var mat = document.getElementById("slot-edit-mat"); + if (mat && document.getElementById("slot-edit-modal").classList.contains("open")) { + _fillSlotProfileDropdown(mat.value, "", ""); } return; } - var fd=new FormData(); - fd.append('file', files[idx]); - fetch(_apiUrl('/kx/filament/profiles/user'), {method:'POST', body:fd}) - .then(function(r){return r.json();}) - .then(function(d){ - totalAdded += (d.added||0); - totalSkipped += (d.skipped||0); - done++; - _one(idx+1); + var fd = new FormData(); + fd.append("file", files[idx]); + fetch(_apiUrl("/kx/filament/profiles/user"), { method: "POST", body: fd }) + .then(function (r) { + return r.json(); }) - .catch(function(e){ - status.textContent='Fehler: '+e; - status.style.color='var(--err)'; + .then(function (d) { + totalAdded += d.added || 0; + totalSkipped += d.skipped || 0; + done++; + _one(idx + 1); + }) + .catch(function (e) { + status.textContent = tr("log_error", "Fehler:") + " " + e; + status.style.color = "var(--err)"; }); } _one(0); @@ -1474,36 +2192,57 @@ function updateSlotEditFeedButton(){ btn.style.display='none'; return; } - btn.style.display=''; - btn.textContent=_slotEditLoaded?tr('slot_edit_unload'):tr('slot_edit_load'); + btn.style.display = ""; + btn.textContent = _slotEditLoaded ? tr("slot_edit_unload") : tr("slot_edit_load"); } -var _orcaFilamentCache=null; // [{id,name,vendor,type,color}, …] -var _visibleVendors=null; // Vendor-Sichtbarkeitsfilter (Issue #41); [] = alle -function _loadVisibleVendors(cb){ - if(_visibleVendors!==null){ cb(_visibleVendors); return; } - fetch(_apiUrl('/kx/filament/visible_vendors')).then(function(r){return r.json();}).then(function(d){ - _visibleVendors=d.result||[]; +var _orcaFilamentCache = null; // [{id,name,vendor,type,color}, …] +var _visibleVendors = null; // Vendor-Sichtbarkeitsfilter (Issue #41); [] = alle +function _loadVisibleVendors(cb) { + if (_visibleVendors !== null) { cb(_visibleVendors); - }).catch(function(){ _visibleVendors=[]; cb([]); }); + return; + } + fetch(_apiUrl("/kx/filament/visible_vendors")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + _visibleVendors = d.result || []; + cb(_visibleVendors); + }) + .catch(function () { + _visibleVendors = []; + cb([]); + }); } -function _loadOrcaFilaments(cb){ - if(_orcaFilamentCache){ cb(_orcaFilamentCache); return; } - fetch(_apiUrl('/kx/filament/profiles')).then(function(r){return r.json();}).then(function(d){ - _orcaFilamentCache=d.result||[]; +function _loadOrcaFilaments(cb) { + if (_orcaFilamentCache) { cb(_orcaFilamentCache); - }).catch(function(){ cb([]); }); + return; + } + fetch(_apiUrl("/kx/filament/profiles")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + _orcaFilamentCache = d.result || []; + cb(_orcaFilamentCache); + }) + .catch(function () { + cb([]); + }); } -function _profileKey(vendor, name){ +function _profileKey(vendor, name) { // Eindeutiger Selector: (vendor, name). IDs aus orca_filaments.json sind // NICHT eindeutig (z.B. 136 Profile mit OGFL99). Wir kodieren beide in den // '; + sel.innerHTML = '"; // User-Profile (is_user) zuerst — eigene Optgroup '★ Eigene' an erster Stelle. - var userProfs=matched.filter(function(p){return p.is_user;}); - var systemProfs=matched.filter(function(p){return !p.is_user;}); - function _appendOption(g, p){ - var o=document.createElement('option'); - o.value=_profileKey(p.vendor, p.name); - o.dataset.vendor=p.vendor; - o.dataset.name=p.name; - o.dataset.id=p.id || ''; - o.textContent=(p.is_user?'★ ':'')+p.name; - if(o.value===wantKey) o.selected=true; + var userProfs = matched.filter(function (p) { + return p.is_user; + }); + var systemProfs = matched.filter(function (p) { + return !p.is_user; + }); + function _appendOption(g, p) { + var o = document.createElement("option"); + o.value = _profileKey(p.vendor, p.name); + o.dataset.vendor = p.vendor; + o.dataset.name = p.name; + o.dataset.id = p.id || ""; + o.textContent = (p.is_user ? "★ " : "") + p.name; + if (o.value === wantKey) o.selected = true; g.appendChild(o); } - if(userProfs.length){ - var gUser=document.createElement('optgroup'); - gUser.label='★ '+(tr('orca_profile_user_label')||'Eigene Profile'); - userProfs.forEach(function(p){ _appendOption(gUser, p); }); + if (userProfs.length) { + var gUser = document.createElement("optgroup"); + gUser.label = "★ " + (tr("orca_profile_user_label") || "Eigene Profile"); + userProfs.forEach(function (p) { + _appendOption(gUser, p); + }); sel.appendChild(gUser); } // Vendor-Sichtbarkeitsfilter (Issue #41 Option A): nur gewählte Vendoren + // Generic. Leere Liste = alle (rückwärtskompatibel). Eigene Profile (is_user) // sind oben bereits unkonditional drin. - _loadVisibleVendors(function(vis){ - var filtered=systemProfs; - if(vis&&vis.length){ - var allow={};vis.forEach(function(v){allow[v]=1;}); - allow['Generic']=1; - filtered=systemProfs.filter(function(p){return allow[p.vendor];}); + _loadVisibleVendors(function (vis) { + var filtered = systemProfs; + if (vis && vis.length) { + var allow = {}; + vis.forEach(function (v) { + allow[v] = 1; + }); + allow["Generic"] = 1; + filtered = systemProfs.filter(function (p) { + return allow[p.vendor]; + }); } - var byVendor={}; - filtered.forEach(function(p){ (byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p); }); - Object.keys(byVendor).sort().forEach(function(v){ - var g=document.createElement('optgroup'); g.label=v; - byVendor[v].forEach(function(p){ _appendOption(g, p); }); - sel.appendChild(g); + var byVendor = {}; + filtered.forEach(function (p) { + (byVendor[p.vendor] = byVendor[p.vendor] || []).push(p); }); + Object.keys(byVendor) + .sort() + .forEach(function (v) { + var g = document.createElement("optgroup"); + g.label = v; + byVendor[v].forEach(function (p) { + _appendOption(g, p); + }); + sel.appendChild(g); + }); }); }); } @@ -1693,117 +2450,164 @@ function openSlotEdit(i){ }).join(''); // OrcaSlicer-Profil-Dropdown: aktuellen User-Override für diesen Slot // aus /kx/filament/slots holen (enthält vendor+name+id). - fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}).then(function(d){ - var arr=d.result||[]; - var entry=arr.find(function(x){return x.slot_index===globalIdx;})||{}; - window._slotProfileMap=window._slotProfileMap||{}; - window._slotProfileMap[globalIdx]={ - id: entry.filament_id||'', - vendor:entry.filament_vendor||'', - name: entry.filament_name||'', - }; - _fillSlotProfileDropdown(mat, entry.filament_vendor||'', entry.filament_name||''); - }).catch(function(){ _fillSlotProfileDropdown(mat,'',''); }); + fetch(_apiUrl("/kx/filament/slots")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + var arr = d.result || []; + var entry = + arr.find(function (x) { + return x.slot_index === globalIdx; + }) || {}; + window._slotProfileMap = window._slotProfileMap || {}; + window._slotProfileMap[globalIdx] = { + id: entry.filament_id || "", + vendor: entry.filament_vendor || "", + name: entry.filament_name || "", + }; + _fillSlotProfileDropdown(mat, entry.filament_vendor || "", entry.filament_name || ""); + }) + .catch(function () { + _fillSlotProfileDropdown(mat, "", ""); + }); updateSlotEditFeedButton(); - document.getElementById('slot-edit-modal').classList.add('open'); + document.getElementById("slot-edit-modal").classList.add("open"); } -function closeSlotEdit(){ - _slotEditIndex=-1; - document.getElementById('slot-edit-modal').classList.remove('open'); +function closeSlotEdit() { + _slotEditIndex = -1; + document.getElementById("slot-edit-modal").classList.remove("open"); } -function slotEditFeed(){ - if(_slotEditIndex<0)return; - var type=_slotEditLoaded?2:1; - amsFeed(type,_slotEditIndex) - .then(function(){ - _slotEditLoaded=!_slotEditLoaded; +function slotEditFeed() { + if (_slotEditIndex < 0) return; + var type = _slotEditLoaded ? 2 : 1; + amsFeed(type, _slotEditIndex) + .then(function () { + _slotEditLoaded = !_slotEditLoaded; updateSlotEditFeedButton(); poll(); }) - .catch(function(){}); + .catch(function () { }); } -function startReadyFile(filename){ - var fn=filename||S.file_ready; - function _doStartReadyFile(){ - var btn=document.getElementById('file-ready-btn'); - if(btn){btn.disabled=true;btn.textContent='…';} - post('/printer/print/start',{filename:fn}) - .then(function(r){return r.json();}) - .then(function(){ - document.getElementById('file-ready-banner').style.display='none'; - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} +function startReadyFile(filename) { + var fn = filename || S.file_ready; + function _doStartReadyFile() { + var btn = document.getElementById("file-ready-btn"); + if (btn) { + btn.disabled = true; + btn.textContent = "…"; + } + post("/printer/print/start", { filename: fn }) + .then(function (r) { + return r.json(); }) - .catch(function(e){ - clog(tr('log_error')+' '+e,'msg-err'); - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + .then(function () { + document.getElementById("file-ready-banner").style.display = "none"; + if (btn) { + btn.disabled = false; + setText("file-ready-btn", T.file_ready_btn); + } + }) + .catch(function (e) { + clog(tr("log_error", "Fehler:") + " " + e, "msg-err"); + if (btn) { + btn.disabled = false; + setText("file-ready-btn", T.file_ready_btn); + } }); } - function _gateAndStart(fileObj){ - if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){ - maybeGateWebUpload(fileObj, function(){ startReadyFile(fn); }); + function _gateAndStart(fileObj) { + if (fileObj && fileObj.web_unverified && webUploadWarningEnabled()) { + maybeGateWebUpload(fileObj, function () { + startReadyFile(fn); + }); return; } _doStartReadyFile(); } - var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;}); - if(currentFile){ + var currentFile = (storeFiles || []).find(function (f) { + return f.filename === fn; + }); + if (currentFile) { _gateAndStart(currentFile); return; } - fetch(_apiUrl('/kx/files')).then(function(r){return r.json();}).then(function(d){ - storeFiles=d.result||[]; - var refreshed=(storeFiles||[]).find(function(f){return f.filename===fn;})||null; - _gateAndStart(refreshed); - }).catch(function(){ - _doStartReadyFile(); - }); + fetch(_apiUrl("/kx/files")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + storeFiles = d.result || []; + var refreshed = + (storeFiles || []).find(function (f) { + return f.filename === fn; + }) || null; + _gateAndStart(refreshed); + }) + .catch(function () { + _doStartReadyFile(); + }); } -function cancelReadyFile(){ - post('/api/file_ready/clear',{}) - .then(function(){document.getElementById('file-ready-banner').style.display='none';}); +function cancelReadyFile() { + post("/api/file_ready/clear", {}).then(function () { + document.getElementById("file-ready-banner").style.display = "none"; + }); } // ── Aktionen für geladene/idle Datei in der Progress-Karte (Issue #55) ── -function startIdleFile(){ - if(_lastLoadedFile) startReadyFile(_lastLoadedFile); +function startIdleFile() { + if (_lastLoadedFile) startReadyFile(_lastLoadedFile); } -function startIdleFileWithSlots(){ - if(_lastLoadedFile) startReadyFileWithSlots(_lastLoadedFile); +function startIdleFileWithSlots() { + if (_lastLoadedFile) startReadyFileWithSlots(_lastLoadedFile); } -function clearIdleFile(){ - _lastLoadedFile=null; - _idleCleared=true; // verhindert Nachladen von s.filename im nächsten poll() (Issue #57) - _fdAutoOpenedFile=null; // nächster Upload derselben Datei soll Dialog wieder öffnen - _fdUserCancelled=false; - _fdDialogOpen=false; - sessionStorage.removeItem('fdAutoOpenedFile'); - sessionStorage.removeItem('fdUserCancelled'); - sessionStorage.removeItem('webVerifyCancelledFileId'); - S.file_ready=''; S.filename=''; S.thumbnail=''; // sofort lokal leeren, kein Warten auf nächsten Poll - var ib=document.getElementById('d-idle-btns');if(ib)ib.style.display='none'; - var fn=document.getElementById('d-fname');if(fn){fn.textContent='–';fn.title='';} - var thumb=document.getElementById('d-thumbnail');if(thumb){thumb.style.display='none';thumb.src='';} - post('/api/file_ready/clear',{}).catch(function(){}); +function clearIdleFile() { + _lastLoadedFile = null; + _idleCleared = true; // verhindert Nachladen von s.filename im nächsten poll() (Issue #57) + _fdAutoOpenedFile = null; // nächster Upload derselben Datei soll Dialog wieder öffnen + _fdUserCancelled = false; + _fdDialogOpen = false; + sessionStorage.removeItem("fdAutoOpenedFile"); + sessionStorage.removeItem("fdUserCancelled"); + sessionStorage.removeItem("webVerifyCancelledFileId"); + S.file_ready = ""; + S.filename = ""; + S.thumbnail = ""; // sofort lokal leeren, kein Warten auf nächsten Poll + var ib = document.getElementById("d-idle-btns"); + if (ib) ib.style.display = "none"; + var fn = document.getElementById("d-fname"); + if (fn) { + fn.textContent = "–"; + fn.title = ""; + } + var thumb = document.getElementById("d-thumbnail"); + if (thumb) { + thumb.style.display = "none"; + thumb.src = ""; + } + post("/api/file_ready/clear", {}).catch(function () { }); } -function selectMatPreset(m){ - document.getElementById('slot-edit-mat').value=m; +function selectMatPreset(m) { + document.getElementById("slot-edit-mat").value = m; highlightMatBtn(m); // Filament-Profile-Dropdown an neues Material anpassen // (vorherige Selektion zurücksetzen — andere Material-Profile passen nicht) - _fillSlotProfileDropdown(m, '', ''); + _fillSlotProfileDropdown(m, "", ""); } -function highlightMatBtn(val){ - document.querySelectorAll('.mat-preset-btn').forEach(function(b){ - var on=b.getAttribute('data-mat')===val.toUpperCase(); - b.style.background=on?'var(--accent)':'var(--raised)'; - b.style.color=on?'#fff':'var(--txt2)'; +function highlightMatBtn(val) { + document.querySelectorAll(".mat-preset-btn").forEach(function (b) { + var on = b.getAttribute("data-mat") === val.toUpperCase(); + b.style.background = on ? "var(--accent)" : "var(--raised)"; + b.style.color = on ? "#fff" : "var(--txt2)"; }); // Auch bei manueller Eingabe ins Material-Textfeld: Dropdown refreshen. - if(val) _fillSlotProfileDropdown(val, '', ''); + if (val) _fillSlotProfileDropdown(val, "", ""); } -function hexToRgb(hex){ - var r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16); - return[r,g,b]; +function 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; @@ -1816,182 +2620,232 @@ function saveSlotEdit(){ // Primärer Selector: (vendor, name). id ist nur Hint (aus dem JSON-data-attr // mitgegeben — Backend lookt sie nochmal selber nach um veraltete Hints zu // korrigieren). - var newProfVendor=sel?(sel.dataset.vendor||''):''; - var newProfName =sel?(sel.dataset.name ||''):''; - var newProfId =sel?(sel.dataset.id ||''):''; + var newProfVendor = sel ? sel.dataset.vendor || "" : ""; + var newProfName = sel ? sel.dataset.name || "" : ""; + var newProfId = sel ? sel.dataset.id || "" : ""; // Sequenziell speichern: erst Profil-Override (config.ini), dann Material/Farbe // (MQTT zum Drucker). Sonst können beide Pfade sich überholen und der Slot-State // ist beim nächsten Re-Open inkonsistent. - fetch(_apiUrl('/kx/filament/slots/'+slotIdx+'/profile'),{ - method:'POST', - headers:{'Content-Type':'application/json'}, - body:JSON.stringify({vendor:newProfVendor, name:newProfName, id:newProfId}) + fetch(_apiUrl("/kx/filament/slots/" + slotIdx + "/profile"), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + vendor: newProfVendor, + name: newProfName, + id: newProfId, + }), }) - .then(function(r){return r.json();}) - .then(function(){ - window._slotProfileMap=window._slotProfileMap||{}; - if(newProfVendor && newProfName){ - window._slotProfileMap[slotIdx]={id:newProfId, vendor:newProfVendor, name:newProfName}; - } else { - delete window._slotProfileMap[slotIdx]; - } - return post('/api/ams/set_slot',{index:slotIdx,type:mat,color:color}); - }) - .then(function(r){return r?r.json():null;}) - .then(function(){ - // Slot-Map refreshen damit die Karte sofort den Vendor zeigt. - return fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}); - }) - .then(function(d){ - var arr=(d && d.result)||[]; - window._slotProfileMap={}; - arr.forEach(function(e){ - if(e.filament_vendor && e.filament_name){ - window._slotProfileMap[e.slot_index]={ - id: e.filament_id||'', - vendor:e.filament_vendor, - name: e.filament_name, + .then(function (r) { + return r.json(); + }) + .then(function () { + window._slotProfileMap = window._slotProfileMap || {}; + if (newProfVendor && newProfName) { + window._slotProfileMap[slotIdx] = { + id: newProfId, + vendor: newProfVendor, + name: newProfName, }; + } else { + delete window._slotProfileMap[slotIdx]; } + return post("/api/ams/set_slot", { + index: slotIdx, + type: mat, + color: color, + }); + }) + .then(function (r) { + return r ? r.json() : null; + }) + .then(function () { + // Slot-Map refreshen damit die Karte sofort den Vendor zeigt. + return fetch(_apiUrl("/kx/filament/slots")).then(function (r) { + return r.json(); + }); + }) + .then(function (d) { + var arr = (d && d.result) || []; + window._slotProfileMap = {}; + arr.forEach(function (e) { + if (e.filament_vendor && e.filament_name) { + window._slotProfileMap[e.slot_index] = { + id: e.filament_id || "", + vendor: e.filament_vendor, + name: e.filament_name, + }; + } + }); + closeSlotEdit(); + var profSuffix = newProfName ? " [" + newProfVendor + " " + newProfName + "]" : ""; + clog( + tr("slot_edit_ok") + " " + (slotIdx + 1) + ": " + mat + " " + hex + profSuffix, + "msg-ok" + ); + // Sofortiges Re-Render mit aktuellem _slotProfileMap (poll() ist async + // und re-rendert beim nächsten Tick — wir wollen aber dass die Vendor- + // Badge JETZT direkt sichtbar wird). + if (typeof applyState === "function") applyState(); + if (typeof poll === "function") poll(); + }) + .catch(function (e) { + clog(tr("log_error", "Fehler:") + " " + e, "msg-err"); }); - closeSlotEdit(); - var profSuffix=newProfName?(' ['+newProfVendor+' '+newProfName+']'):''; - clog(tr('slot_edit_ok')+' '+(slotIdx+1)+': '+mat+' '+hex+profSuffix,'msg-ok'); - // Sofortiges Re-Render mit aktuellem _slotProfileMap (poll() ist async - // und re-rendert beim nächsten Tick — wir wollen aber dass die Vendor- - // Badge JETZT direkt sichtbar wird). - if(typeof applyState==='function') applyState(); - if(typeof poll==='function') poll(); - }) - .catch(function(e){clog('Fehler: '+e,'msg-err');}); } -document.addEventListener('DOMContentLoaded',function(){ - document.getElementById('s-printer-ip').addEventListener('input',function(){ - var hint=document.getElementById('lbl-ip-hint'); - if(this.value.includes(':')){hint.textContent=T.hint_ip_no_port;hint.style.display='block';} - else{hint.style.display='none';} +document.addEventListener("DOMContentLoaded", function () { + document.getElementById("s-printer-ip").addEventListener("input", function () { + var hint = document.getElementById("lbl-ip-hint"); + if (this.value.includes(":")) { + hint.textContent = T.hint_ip_no_port; + hint.style.display = "block"; + } else { + hint.style.display = "none"; + } }); }); -function setPoll(ms){ - localStorage.setItem('pollInterval',ms); +function setPoll(ms) { + localStorage.setItem("pollInterval", ms); clearInterval(pollTimer); - pollTimer=setInterval(poll,ms); + pollTimer = setInterval(poll, ms); } -function saveSettings(){ - var btn=document.getElementById('btn-save-settings'); - btn.disabled=true;btn.textContent='…'; - var webUploadWarning=(document.getElementById('s-web-upload-warning')||{}).checked?1:0; - S.web_upload_warning=webUploadWarning; +function saveSettings() { + var btn = document.getElementById("btn-save-settings"); + btn.disabled = true; + btn.textContent = "…"; + var webUploadWarning = (document.getElementById("s-web-upload-warning") || {}).checked ? 1 : 0; + S.web_upload_warning = webUploadWarning; // Start-Print-Behavior-Wechsel könnte den Auto-Open sonst dauerhaft blockieren // (alter _fdUserCancelled bei gleicher file_ready) → Dialog-State zurücksetzen (Issue #57). - _fdUserCancelled=false;_fdAutoOpenedFile=null; - sessionStorage.removeItem('fdUserCancelled');sessionStorage.removeItem('fdAutoOpenedFile'); - sessionStorage.removeItem('webVerifyCancelledFileId'); - post('/api/settings',{ - printer_name: document.getElementById('s-printer-name').value, - printer_ip: document.getElementById('s-printer-ip').value, - mqtt_port: parseInt(document.getElementById('s-mqtt-port').value)||9883, - username: document.getElementById('s-username').value, - password: document.getElementById('s-password').value, - device_id: document.getElementById('s-device-id').value, - mode_id: document.getElementById('s-mode-id').value, - default_ams_slot: document.getElementById('s-default-slot').value, - auto_leveling: document.getElementById('s-auto-leveling').checked?1:0, + _fdUserCancelled = false; + _fdAutoOpenedFile = null; + sessionStorage.removeItem("fdUserCancelled"); + sessionStorage.removeItem("fdAutoOpenedFile"); + sessionStorage.removeItem("webVerifyCancelledFileId"); + post("/api/settings", { + printer_name: document.getElementById("s-printer-name").value, + printer_ip: document.getElementById("s-printer-ip").value, + mqtt_port: parseInt(document.getElementById("s-mqtt-port").value) || 9883, + username: document.getElementById("s-username").value, + password: document.getElementById("s-password").value, + device_id: document.getElementById("s-device-id").value, + mode_id: document.getElementById("s-mode-id").value, + default_ams_slot: document.getElementById("s-default-slot").value, + auto_leveling: document.getElementById("s-auto-leveling").checked ? 1 : 0, vibration_compensation: (document.getElementById('s-vibration-compensation')||{}).checked?1:0, - camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0, - print_start_dialog: parseInt((document.getElementById('s-file-ready-mode')||{}).value||'1',10), - web_upload_warning:webUploadWarning, - poll_interval: Math.min(60,Math.max(1,parseInt((document.getElementById('s-poll-interval')||{}).value,10)||3)), + camera_on_print: (document.getElementById("s-camera-on-print") || {}).checked ? 1 : 0, + print_start_dialog: parseInt( + (document.getElementById("s-file-ready-mode") || {}).value || "1", + 10 + ), + web_upload_warning: webUploadWarning, + poll_interval: Math.min( + 60, + Math.max(1, parseInt((document.getElementById("s-poll-interval") || {}).value, 10) || 3) + ), verbose_http_log: (document.getElementById('s-verbose-http-log')||{}).checked?1:0, - spoolman_server: (document.getElementById('s-spoolman-url')||{}).value||'', - spoolman_sync_rate: Math.max(0,parseInt((document.getElementById('s-spoolman-sync-rate')||{}).value||'30',10)), - }).then(function(){ - btn.textContent=T.update_restarting; - setTimeout(function(){ - btn.disabled=false; - setText('btn-save-settings',T.settings_save); - closeSettings(); - _loadSpoolmanStatus(); - poll(); - },4000); - }).catch(function(e){ - btn.disabled=false;setText('btn-save-settings',T.settings_save); - clog('Settings-Fehler: '+e,'msg-err'); - }); + notifications: notifCollect(), + notifications_paused: _notifPaused, + notify_every_minutes: + parseInt((document.getElementById("s-notif-every-min") || {}).value || "0", 10) || 0, + notify_every_layers: + parseInt((document.getElementById("s-notif-every-layers") || {}).value || "0", 10) || 0, + spoolman_server: ((document.getElementById("s-spoolman-url") || {}).value || "").trim(), + spoolman_sync_rate: Math.min( + 3600, + Math.max(0, parseInt((document.getElementById("s-spoolman-sync-rate") || {}).value, 10) || 0) + ), + }) + .then(function () { + btn.textContent = T.update_restarting; + setTimeout(function () { + btn.disabled = false; + setText("btn-save-settings", T.settings_save); + closeSettings(); + _loadSpoolmanStatus(); + poll(); + }, 4000); + }) + .catch(function (e) { + btn.disabled = false; + setText("btn-save-settings", T.settings_save); + clog(T.settings_title + " " + tr("log_error", "Fehler:") + " " + e, "msg-err"); + }); } -var _updateDockerOnly=false; -function checkUpdate(){ - var sb=document.getElementById('update-status'); - sb.textContent=T.update_checking; - document.getElementById('btn-update-apply').style.display='none'; - _updateTag='';_updateUrl='';_updateDockerOnly=false; - fetch(_apiUrl('/api/update/check')).then(function(r){return r.json()}).then(function(d){ - if(d.error){sb.textContent=T.update_error+': '+d.error;return;} - var cl=document.getElementById('update-changelog'); - if(d.changelog&&d.changelog.trim()){ +var _updateDockerOnly = false; +function checkUpdate() { + var sb = document.getElementById('update-status'); + sb.textContent = T.update_checking; + document.getElementById('btn-update-apply').style.display = 'none'; + _updateTag = ''; _updateUrl = ''; _updateDockerOnly = false; + fetch(_apiUrl('/api/update/check')).then(function (r) { return r.json() }).then(function (d) { + if (d.error) { sb.textContent = T.update_error + ': ' + d.error; return; } + var cl = document.getElementById('update-changelog'); + if (d.changelog && d.changelog.trim()) { // Changelog als Markdown-Text (pre-formatiert) anzeigen - cl.textContent=d.changelog;cl.style.display='block'; - } else {cl.style.display='none';} - _updateDockerOnly=!!d.docker_only; - if(d.update_available){ - sb.textContent=d.latest+' '+T.update_available; - sb.style.color='var(--ok)'; - _updateTag=d.tag;_updateUrl=d.download_url||''; - var btn=document.getElementById('btn-update-apply'); - if(_updateDockerOnly){ - btn.textContent=T.update_docker||'docker compose pull'; - btn.title='docker compose pull && docker compose up -d'; + cl.textContent = d.changelog; cl.style.display = 'block'; + } else { cl.style.display = 'none'; } + _updateDockerOnly = !!d.docker_only; + if (d.update_available) { + sb.textContent = d.latest + ' ' + T.update_available; + sb.style.color = 'var(--ok)'; + _updateTag = d.tag; _updateUrl = d.download_url || ''; + var btn = document.getElementById('btn-update-apply'); + if (_updateDockerOnly) { + btn.textContent = T.update_docker || 'docker compose pull'; + btn.title = 'docker compose pull && docker compose up -d'; } else { - btn.textContent=T.update_apply; - btn.title=''; + btn.textContent = T.update_apply; + btn.title = ''; } - btn.style.display='inline-block'; + btn.style.display = 'inline-block'; } else { - sb.textContent=T.update_none; - sb.style.color='var(--txt2)'; + sb.textContent = T.update_none; + sb.style.color = 'var(--txt2)'; } - }).catch(function(e){sb.textContent=T.update_error+': '+e;}); + }).catch(function (e) { sb.textContent = T.update_error + ': ' + e; }); } -function applyUpdate(){ - var sb=document.getElementById('update-status'); - var btn=document.getElementById('btn-update-apply'); - if(_updateDockerOnly){ +function applyUpdate() { + var sb = document.getElementById('update-status'); + var btn = document.getElementById('btn-update-apply'); + if (_updateDockerOnly) { // Nightly: kein Self-Update, Docker-Befehl in Zwischenablage kopieren - var cmd='docker compose pull && docker compose up -d'; - if(navigator.clipboard){ - navigator.clipboard.writeText(cmd).then(function(){ - sb.textContent=T.update_docker_copied||'Befehl kopiert: '+cmd; + var cmd = 'docker compose pull && docker compose up -d'; + if (navigator.clipboard) { + navigator.clipboard.writeText(cmd).then(function () { + sb.textContent = T.update_docker_copied || 'Befehl kopiert: ' + cmd; }); } else { - sb.textContent=cmd; + sb.textContent = cmd; } return; } - if(!_updateUrl)return; - btn.disabled=true;sb.textContent=T.update_applying; - post('/api/update/apply',{download_url:_updateUrl,tag:_updateTag}).then(function(){ - sb.textContent=T.update_restarting; + if (!_updateUrl) return; + btn.disabled = true; sb.textContent = T.update_applying; + post('/api/update/apply', { download_url: _updateUrl, tag: _updateTag }).then(function () { + sb.textContent = T.update_restarting; closeSettings(); - setTimeout(function(){poll();},5000); - }).catch(function(e){ - btn.disabled=false;sb.textContent=T.update_error+': '+e; + 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); +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')} + } catch (e) { + clog(T.log_poll_error + " " + e, "msg-err"); + } } var pollTimer; -(function(){ - var ms=parseInt(localStorage.getItem('pollInterval')||'2000'); +(function () { + var ms = parseInt(localStorage.getItem("pollInterval") || "2000"); initPrinters(); // Slot-Profile-Map initial laden, sonst zeigen die Karten beim ersten // Render keine Vendor-Badge obwohl in der config.ini ein Override steht. @@ -2318,458 +3172,631 @@ function applyDashPreset(name){ // , so the DOM is ready. initDashGrid(); // ── 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 printAction(a) { + post("/printer/print/" + a, {}) + .then(function () { + clog(tr("log_print_action", "Druck:") + " " + a, "msg-ok"); + poll(); + }) + .catch(function (e) { + clog(tr("log_error", "Fehler:") + " " + e, "msg-err"); + }); } -function togglePauseResume(){ +function togglePauseResume() { // Druckt → pause; Pausiert → resume. Status kommt aus dem zuletzt gepollten // print_state in S; bei Unklarheit (kein State) Pause als Default. - var state=(S && S.print_state)||''; - if(state==='paused') printAction('resume'); - else printAction('pause'); + var state = (S && S.print_state) || ""; + if (state === "paused") printAction("resume"); + else printAction("pause"); } -function updatePauseResumeBtn(){ - var btn=document.getElementById('d-btn-pause'); - if(!btn) return; - var state=(S && S.print_state)||''; - if(state==='paused'){ - btn.textContent=T.btn_resume||'▶ Weiter'; - btn.classList.add('btn-resume'); - btn.classList.remove('btn-pause'); +function updatePauseResumeBtn() { + var btn = document.getElementById("d-btn-pause"); + if (!btn) return; + var state = (S && S.print_state) || ""; + if (state === "paused") { + btn.textContent = T.btn_resume || "▶ Weiter"; + btn.classList.add("btn-resume"); + btn.classList.remove("btn-pause"); } else { - btn.textContent=T.btn_pause||'⏸ Pause'; - btn.classList.add('btn-pause'); - btn.classList.remove('btn-resume'); + btn.textContent = T.btn_pause || "⏸ Pause"; + btn.classList.add("btn-pause"); + btn.classList.remove("btn-resume"); } } -function confirmCancel(){if(confirm(T.confirm_cancel||'Druck wirklich abbrechen?'))printAction('cancel')} +function confirmCancel() { + if (confirm(T.confirm_cancel || "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')); +function getStep() { return currentStep } +function setStep(btn, v) { + currentStep = v; + document.querySelectorAll('.step-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); - var ci=document.getElementById('step-custom'); - if(ci)ci.value=''; + var ci = document.getElementById('step-custom'); + if (ci) ci.value = ''; } -function setStepCustom(inp){ - var v=parseFloat(inp.value); - if(!isNaN(v)&&v>0){ - currentStep=v; - document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active')); +function setStepCustom(inp) { + var v = parseFloat(inp.value); + if (!isNaN(v) && v > 0) { + currentStep = v; + document.querySelectorAll('.step-btn').forEach(b => b.classList.remove('active')); } } -function move(axis,dir,dist){ +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')}); + var axisMap = { 0: 1, 1: 2, 2: 3 }; + post("/api/axis", { axis: axisMap[axis], move_type: 1, distance: dir * dist }) + .then(function () { + clog( + tr("log_axis", "Achse") + + " " + + (axis === 0 ? "X" : axis === 1 ? "Y" : "Z") + + " " + + (dir > 0 ? "+" : "") + + dir * dist + + "mm", + "msg-ok" + ); + }) + .catch(function (e) { + clog(tr("log_axis", "Achse") + "-" + tr("log_error", "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 homeAll() { + post("/api/axis", { axis: 5, move_type: 2, distance: 0 }) + .then(function () { + clog(tr("log_home_all", "Home All"), "msg-ok"); + }) + .catch(function (e) { + clog(tr("log_home_all", "Home All") + " " + tr("log_error", "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 homeXY() { + post("/api/axis", { axis: 4, move_type: 2, distance: 0 }) + .then(function () { + clog(tr("btn_home_xy", "Home XY"), "msg-ok"); + }) + .catch(function (e) { + clog(tr("btn_home_xy", "Home XY") + " " + tr("log_error", "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 homeZ() { + post("/api/axis", { axis: 3, move_type: 2, distance: 0 }) + .then(function () { + clog(tr("btn_home_z", "Home Z"), "msg-ok"); + }) + .catch(function (e) { + clog(tr("btn_home_z", "Home Z") + " " + tr("log_error", "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')}); +function disableMotors() { + post("/api/axis", { action: "turnOff" }) + .then(function () { + clog(tr("btn_disable_motors", "Motors Off"), "msg-ok"); + }) + .catch(function (e) { + clog( + tr("btn_disable_motors", "Motors Off") + " " + tr("log_error", "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 setNozzle() { + var v = parseFloat(document.getElementById("p-nozzle-inp").value || 0); + post("/api/temperature", { nozzle: v, bed: S.bed_target }) + .then(function () { + clog(tr("log_nozzle", "Nozzle → ") + v + "°C", "msg-ok"); + }) + .catch(function (e) { + clog(tr("label_nozzle", "Düse") + " " + tr("log_error", "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')}); +function setBed() { + var v = parseFloat(document.getElementById("p-bed-inp").value || 0); + post("/api/temperature", { nozzle: S.nozzle_target, bed: v }) + .then(function () { + clog(tr("log_bed", "Bett → ") + v + "°C", "msg-ok"); + }) + .catch(function (e) { + clog(tr("label_bed", "Bett") + " " + tr("log_error", "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, 80%':'aus'),'msg-ok')}) +function setLight() { + var on = document.getElementById("d-light-toggle").checked; + post("/api/light", { on: on, brightness: 80 }) + .then(function () { + clog("Licht " + (on ? "an, 80%" : "aus"), "msg-ok"); + }) - .catch(function(e){clog('Licht-Fehler: '+e,'msg-err')}); + .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); +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( + tr("label_speed", "Geschwindigkeit") + " " + tr("log_error", "Fehler:") + " " + e, + "msg-err" + ); }); - 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 setFan() { + var v = parseInt(document.getElementById("d-fan").value); + document.getElementById("d-fan-val").textContent = v; + post("/api/fan", { speed: v }) + .then(function () { + clog(tr("log_fan", "Lüfter → ") + v + "%", "msg-ok"); + }) + .catch(function (e) { + clog(tr("log_fan", "Lüfter → ") + tr("log_error", "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')}); +function quickFan(v) { + document.getElementById("d-fan").value = v; + document.getElementById("d-fan-val").textContent = v; + post("/api/fan", { speed: v }) + .then(function () { + clog(tr("log_fan", "Lüfter → ") + v + "%", "msg-ok"); + }) + .catch(function (e) { + clog(tr("log_fan", "Lüfter → ") + tr("log_error", "Fehler:") + " " + e, "msg-err"); + }); } // ── AMS ── -function amsFeed(type,slotIndex){ +function amsFeed(type, slotIndex) { var globalIdx; - if(typeof slotIndex==='number'&&slotIndex>=0){ - globalIdx=slotIndex; - }else{ - var i=parseInt(document.getElementById('ams-slot-sel').value); - var slot=(window._amsSlots||[])[i]||{}; - globalIdx=slot.global_index!=null?slot.global_index:i; + if (typeof slotIndex === "number" && slotIndex >= 0) { + globalIdx = slotIndex; + } else { + var i = parseInt(document.getElementById("ams-slot-sel").value); + var slot = (window._amsSlots || [])[i] || {}; + globalIdx = slot.global_index != null ? slot.global_index : i; } - return post('/api/ams/feed',{slot_index:globalIdx,type:type}) - .then(function(){clog((type===1?T.lbl_feed:T.lbl_unload)+' Slot '+(globalIdx+1),'msg-ok')}) - .catch(function(e){clog('AMS-Fehler: '+e,'msg-err');throw e;}); + return post("/api/ams/feed", { slot_index: globalIdx, type: type }) + .then(function () { + clog((type === 1 ? T.lbl_feed : T.lbl_unload) + " Slot " + (globalIdx + 1), "msg-ok"); + }) + .catch(function (e) { + clog("AMS " + tr("log_error", "Fehler:") + " " + e, "msg-err"); + throw e; + }); } // ── Camera ── -function camStart(){ - var img=document.getElementById('cam-img'); - var ph=document.getElementById('cam-placeholder'); - var sp=document.getElementById('cam-spinner'); - ph.style.display='none'; - img.style.display='none'; - sp.style.display='block'; - post('/api/camera/start',{}).then(function(){ - camOn=true; - document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_stop'); - var rb=document.getElementById('cam-reset-btn');if(rb)rb.style.display=''; - clog(tr('log_cam_start'),'msg-ok'); - setTimeout(function(){ sp.style.display='none'; img.style.display='block'; },1200); - if(/Android/i.test(navigator.userAgent)){ +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 () { + camOn = true; + document.getElementById('cam-toggle-btn').textContent = tr('btn_cam_stop'); + var rb = document.getElementById('cam-reset-btn'); if (rb) rb.style.display = ''; + clog(tr('log_cam_start'), 'msg-ok'); + setTimeout(function () { sp.style.display = 'none'; img.style.display = 'block'; }, 1200); + if (/Android/i.test(navigator.userAgent)) { // Android browsers don't support multipart/x-mixed-replace (MJPEG) — poll snapshots instead - _camPollInterval=setInterval(function(){ img.src='/api/camera/snapshot?t='+Date.now(); },200); + _camPollInterval = setInterval(function () { img.src = '/api/camera/snapshot?t=' + Date.now(); }, 200); } else { - img.onerror=function(){ - sp.style.display='none'; - img.style.display='none'; - ph.style.display='flex'; - camOn=false; - document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start'); - clog(tr('log_error')+' '+tr('cam_stream_unavailable'),'msg-err'); + img.onerror = function () { + sp.style.display = 'none'; + img.style.display = 'none'; + ph.style.display = 'flex'; + camOn = false; + document.getElementById('cam-toggle-btn').textContent = tr('btn_cam_start'); + clog(tr('log_error') + ' ' + tr('cam_stream_unavailable'), 'msg-err'); }; - img.src='/api/camera/stream?t='+Date.now(); + img.src = '/api/camera/stream?t=' + Date.now(); } - }).catch(function(e){ - sp.style.display='none'; - ph.style.display='flex'; - clog(tr('log_error')+' '+e,'msg-err'); + }).catch(function (e) { + sp.style.display = 'none'; + ph.style.display = 'flex'; + clog(tr('log_error') + ' ' + e, 'msg-err'); }); } -function camStop(){ - var img=document.getElementById('cam-img'); - if(_camPollInterval){clearInterval(_camPollInterval);_camPollInterval=null;} - img.onerror=null; // deregister error handler before clearing src to avoid spurious error toast - post('/api/camera/stop',{}).catch(function(){}); - img.src=''; - img.style.display='none'; - document.getElementById('cam-placeholder').style.display='flex'; - camOn=false; - camUserStopped=true; // suppress auto-restart for remainder of this print - document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start'); - var rb=document.getElementById('cam-reset-btn');if(rb)rb.style.display='none'; - clog(tr('log_cam_stop'),'msg-ok'); +function camStop() { + var img = document.getElementById('cam-img'); + if (_camPollInterval) { clearInterval(_camPollInterval); _camPollInterval = null; } + img.onerror = null; // deregister error handler before clearing src to avoid spurious error toast + post('/api/camera/stop', {}).catch(function () { }); + img.src = ''; + img.style.display = 'none'; + document.getElementById('cam-placeholder').style.display = 'flex'; + camOn = false; + camUserStopped = true; // suppress auto-restart for remainder of this print + document.getElementById('cam-toggle-btn').textContent = tr('btn_cam_start'); + var rb = document.getElementById('cam-reset-btn'); if (rb) rb.style.display = 'none'; + clog(tr('log_cam_stop'), 'msg-ok'); } -function aceDryStart(aceId){ - aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; - var prof=_aceDryProfileGet(aceId); - var t=parseInt(prof.temp,10); - var d=_aceDryDurationMinFromSec(prof.duration_sec); - t=Math.max(30,Math.min(80,t)); - d=Math.max(10,Math.min(1440,d)); - return post('/api/ace/dry',{action:'start',target_temp:t,duration:d,ace_id:aceId}) - .then(function(r){return r.json();}) - .then(function(r){ - if(r.error){throw new Error(r.error);} - clog('ACE '+(aceId+1)+' - '+tr('ace_dry_dryer')+': '+tr('ace_dry_start')+' ('+t+'°C, '+d+' min)','msg-ok'); +function aceDryStart(aceId) { + aceId = typeof aceId === "number" && aceId >= 0 ? aceId : 0; + var prof = _aceDryProfileGet(aceId); + var t = parseInt(prof.temp, 10); + var d = _aceDryDurationMinFromSec(prof.duration_sec); + t = Math.max(30, Math.min(80, t)); + d = Math.max(10, Math.min(1440, d)); + return post("/api/ace/dry", { + action: "start", + target_temp: t, + duration: d, + ace_id: aceId, + }) + .then(function (r) { + return r.json(); + }) + .then(function (r) { + if (r.error) { + throw new Error(r.error); + } + clog( + "ACE " + + (aceId + 1) + + " - " + + tr("ace_dry_dryer") + + ": " + + tr("ace_dry_start") + + " (" + + t + + "°C, " + + d + + " min)", + "msg-ok" + ); poll(); }) - .catch(function(e){clog('ACE-Fehler: '+e,'msg-err');}); + .catch(function (e) { + clog("ACE " + tr("log_error", "Fehler:") + " " + e, "msg-err"); + }); } -var _aceAutoFeedPending={}; -function aceAutoRefillToggle(aceId){ - aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; - var on=!!((document.getElementById('ace-auto-refill-toggle-'+aceId)||{}).checked); - _aceAutoFeedPending[aceId]=true; - fetch('/api/ace/auto_feed',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ace_id:aceId,on:on?1:0})}) - .then(function(r){return r.json();}) - .then(function(d){ - delete _aceAutoFeedPending[aceId]; - if(d.error){clog('Auto Refill error: '+d.error,'msg-err');var t=document.getElementById('ace-auto-refill-toggle-'+aceId);if(t)t.checked=!on;return;} - clog('ACE '+(aceId+1)+' - '+tr('ace_dry_auto_refill')+': '+(on?'ON':'OFF'),'msg-ok'); +var _aceAutoFeedPending = {}; +function aceAutoRefillToggle(aceId) { + aceId = typeof aceId === "number" && aceId >= 0 ? aceId : 0; + var on = !!(document.getElementById("ace-auto-refill-toggle-" + aceId) || {}).checked; + _aceAutoFeedPending[aceId] = true; + fetch("/api/ace/auto_feed", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ace_id: aceId, on: on ? 1 : 0 }), + }) + .then(function (r) { + return r.json(); }) - .catch(function(e){delete _aceAutoFeedPending[aceId];clog('Auto Refill error: '+e,'msg-err');var t=document.getElementById('ace-auto-refill-toggle-'+aceId);if(t)t.checked=!on;}); + .then(function (d) { + delete _aceAutoFeedPending[aceId]; + if (d.error) { + clog("Auto Refill error: " + d.error, "msg-err"); + var t = document.getElementById("ace-auto-refill-toggle-" + aceId); + if (t) t.checked = !on; + return; + } + clog( + "ACE " + (aceId + 1) + " - " + tr("ace_dry_auto_refill") + ": " + (on ? "ON" : "OFF"), + "msg-ok" + ); + }) + .catch(function (e) { + delete _aceAutoFeedPending[aceId]; + clog("Auto Refill error: " + e, "msg-err"); + var t = document.getElementById("ace-auto-refill-toggle-" + aceId); + if (t) t.checked = !on; + }); } -function openAceDryDialog(aceId){ - aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; - _aceDryDialogAceId=aceId; +function openAceDryDialog(aceId) { + aceId = typeof aceId === "number" && aceId >= 0 ? aceId : 0; + _aceDryDialogAceId = aceId; _syncAceDryPresetsFromServer(S.ace_dry_presets); - _aceDryDialogPresetOriginals=JSON.parse(JSON.stringify(ACE_DRY_PRESETS)); + _aceDryDialogPresetOriginals = JSON.parse(JSON.stringify(ACE_DRY_PRESETS)); aceDryDialogSyncCustomButtonNames(); - var hasStored=Object.prototype.hasOwnProperty.call(aceDryProfiles,String(aceId)); - var prof=_aceDryProfileGet(aceId); - if(hasStored&&prof.preset&&ACE_DRY_PRESETS[prof.preset]){ + var hasStored = Object.prototype.hasOwnProperty.call(aceDryProfiles, String(aceId)); + var prof = _aceDryProfileGet(aceId); + if (hasStored && prof.preset && ACE_DRY_PRESETS[prof.preset]) { aceDryDialogPreset(prof.preset); - }else if(hasStored){ - var sec=prof.duration_sec; - document.getElementById('ace-dry-dialog-temp').value=prof.temp; - document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600); - document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60); - document.getElementById('ace-dry-dialog-s').value=sec%60; - aceDryDialogHighlightPreset(''); - }else{ - aceDryDialogPreset('pla'); + } else if (hasStored) { + var sec = prof.duration_sec; + document.getElementById("ace-dry-dialog-temp").value = prof.temp; + document.getElementById("ace-dry-dialog-h").value = Math.floor(sec / 3600); + document.getElementById("ace-dry-dialog-m").value = Math.floor((sec % 3600) / 60); + document.getElementById("ace-dry-dialog-s").value = sec % 60; + aceDryDialogHighlightPreset(""); + } else { + aceDryDialogPreset("pla"); } aceDryDialogUpdateSaveButton(); aceDryDialogUpdateResetButton(); - var sb=document.getElementById('ace-dry-dialog-save-preset'); - if(sb){sb.disabled=false;sb.textContent=tr('ace_dry_dialog_save_restart');} - document.getElementById('ace-dry-dialog').classList.add('open'); + var sb = document.getElementById("ace-dry-dialog-save-preset"); + if (sb) { + sb.disabled = false; + sb.textContent = tr("ace_dry_dialog_save_restart"); + } + document.getElementById("ace-dry-dialog").classList.add("open"); } -function closeAceDryDialog(){ - _aceDryDialogAceId=-1; - _aceDryDialogPresetOriginals={}; - var sb=document.getElementById('ace-dry-dialog-save-preset'); - if(sb)sb.style.display='none'; - var rb=document.getElementById('ace-dry-dialog-reset-default'); - if(rb)rb.style.display='none'; - document.getElementById('ace-dry-dialog').classList.remove('open'); +function closeAceDryDialog() { + _aceDryDialogAceId = -1; + _aceDryDialogPresetOriginals = {}; + var sb = document.getElementById("ace-dry-dialog-save-preset"); + if (sb) sb.style.display = "none"; + var rb = document.getElementById("ace-dry-dialog-reset-default"); + if (rb) rb.style.display = "none"; + document.getElementById("ace-dry-dialog").classList.remove("open"); } -function aceDryDialogIsCustomPreset(key){ - return /^custom_[123]$/.test(String(key||'')); +function aceDryDialogIsCustomPreset(key) { + return /^custom_[123]$/.test(String(key || "")); } -function aceDryDialogSyncCustomButtonNames(){ - ['custom_1','custom_2','custom_3'].forEach(function(k){ - var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+k+'"]'); - if(b)b.textContent=(ACE_DRY_PRESETS[k]&&ACE_DRY_PRESETS[k].name)||('Custom '+k.slice(-1)); +function aceDryDialogSyncCustomButtonNames() { + ["custom_1", "custom_2", "custom_3"].forEach(function (k) { + var b = document.querySelector('.ace-dry-preset-btn[data-preset="' + k + '"]'); + if (b) + b.textContent = (ACE_DRY_PRESETS[k] && ACE_DRY_PRESETS[k].name) || "Custom " + k.slice(-1); }); } -function aceDryDialogUpdateCustomNameUi(){ - var row=document.getElementById('ace-dry-dialog-custom-name-row'); - var input=document.getElementById('ace-dry-dialog-custom-name'); - if(!row||!input)return; - if(!aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)){ - row.style.display='none'; +function aceDryDialogUpdateCustomNameUi() { + var row = document.getElementById("ace-dry-dialog-custom-name-row"); + var input = document.getElementById("ace-dry-dialog-custom-name"); + if (!row || !input) return; + if (!aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)) { + row.style.display = "none"; return; } - row.style.display='flex'; - input.value=(ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||''; + row.style.display = "flex"; + input.value = + (ACE_DRY_PRESETS[_aceDryDialogPresetKey] && ACE_DRY_PRESETS[_aceDryDialogPresetKey].name) || ""; } -function aceDryDialogCurrentValues(){ - var t=parseInt(document.getElementById('ace-dry-dialog-temp').value||45,10); - var h=parseInt(document.getElementById('ace-dry-dialog-h').value||0,10); - var m=parseInt(document.getElementById('ace-dry-dialog-m').value||0,10); - var s=parseInt(document.getElementById('ace-dry-dialog-s').value||0,10); - t=Math.max(30,Math.min(80,t)); - h=Math.max(0,Math.min(24,h)); - m=Math.max(0,Math.min(59,m)); - s=Math.max(0,Math.min(59,s)); - var totalSec=(h*3600)+(m*60)+s; - totalSec=Math.max(10*60,Math.min(24*3600,totalSec)); - return {temp:t,duration_sec:totalSec}; +function aceDryDialogCurrentValues() { + var t = parseInt(document.getElementById("ace-dry-dialog-temp").value || 45, 10); + var h = parseInt(document.getElementById("ace-dry-dialog-h").value || 0, 10); + var m = parseInt(document.getElementById("ace-dry-dialog-m").value || 0, 10); + var s = parseInt(document.getElementById("ace-dry-dialog-s").value || 0, 10); + t = Math.max(30, Math.min(80, t)); + h = Math.max(0, Math.min(24, h)); + m = Math.max(0, Math.min(59, m)); + s = Math.max(0, Math.min(59, s)); + var totalSec = h * 3600 + m * 60 + s; + totalSec = Math.max(10 * 60, Math.min(24 * 3600, totalSec)); + return { temp: t, duration_sec: totalSec }; } -function aceDryDialogUpdateSaveButton(){ - var btn=document.getElementById('ace-dry-dialog-save-preset'); - if(!btn)return; - var key=_aceDryDialogPresetKey||''; - if(!key||!ACE_DRY_PRESETS[key]){btn.style.display='none';return;} - var p=_aceDryDialogPresetOriginals[key]||ACE_DRY_PRESETS[key]; - var cur=aceDryDialogCurrentValues(); - var changed=(cur.temp!==Number(p.temp)||cur.duration_sec!==Number(p.duration_sec)); - if(aceDryDialogIsCustomPreset(key)){ - var nameInp=document.getElementById('ace-dry-dialog-custom-name'); - var n=((nameInp&&nameInp.value)||'').trim(); - var old=(p&&p.name?String(p.name):('Custom '+key.slice(-1))).trim(); - if((n||old)!==old)changed=true; +function aceDryDialogUpdateSaveButton() { + var btn = document.getElementById("ace-dry-dialog-save-preset"); + if (!btn) return; + var key = _aceDryDialogPresetKey || ""; + if (!key || !ACE_DRY_PRESETS[key]) { + btn.style.display = "none"; + return; } - btn.style.display=changed?'':'none'; + var p = _aceDryDialogPresetOriginals[key] || ACE_DRY_PRESETS[key]; + var cur = aceDryDialogCurrentValues(); + var changed = cur.temp !== Number(p.temp) || cur.duration_sec !== Number(p.duration_sec); + if (aceDryDialogIsCustomPreset(key)) { + var nameInp = document.getElementById("ace-dry-dialog-custom-name"); + var n = ((nameInp && nameInp.value) || "").trim(); + var old = (p && p.name ? String(p.name) : "Custom " + key.slice(-1)).trim(); + if ((n || old) !== old) changed = true; + } + btn.style.display = changed ? "" : "none"; } -function aceDryDialogUpdateResetButton(){ - var btn=document.getElementById('ace-dry-dialog-reset-default'); - if(!btn)return; - var key=_aceDryDialogPresetKey||''; - var d=ACE_DRY_PRESET_DEFAULTS[key]; - if(!key||!d){btn.style.display='none';return;} - var cur=aceDryDialogCurrentValues(); - var changed=(cur.temp!==Number(d.temp)||cur.duration_sec!==Number(d.duration_sec)); - btn.style.display=changed?'':'none'; +function aceDryDialogUpdateResetButton() { + var btn = document.getElementById("ace-dry-dialog-reset-default"); + if (!btn) return; + var key = _aceDryDialogPresetKey || ""; + var d = ACE_DRY_PRESET_DEFAULTS[key]; + if (!key || !d) { + btn.style.display = "none"; + return; + } + var cur = aceDryDialogCurrentValues(); + var changed = cur.temp !== Number(d.temp) || cur.duration_sec !== Number(d.duration_sec); + btn.style.display = changed ? "" : "none"; } -function aceDryDialogInputsChanged(){ - if(aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)){ - var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+_aceDryDialogPresetKey+'"]'); - var i=document.getElementById('ace-dry-dialog-custom-name'); - if(b&&i){ - var t=(i.value||'').trim(); - b.textContent=t||((ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||('Custom '+_aceDryDialogPresetKey.slice(-1))); +function aceDryDialogInputsChanged() { + if (aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)) { + var b = document.querySelector( + '.ace-dry-preset-btn[data-preset="' + _aceDryDialogPresetKey + '"]' + ); + var i = document.getElementById("ace-dry-dialog-custom-name"); + if (b && i) { + var t = (i.value || "").trim(); + b.textContent = + t || + (ACE_DRY_PRESETS[_aceDryDialogPresetKey] && ACE_DRY_PRESETS[_aceDryDialogPresetKey].name) || + "Custom " + _aceDryDialogPresetKey.slice(-1); } } aceDryDialogUpdateSaveButton(); aceDryDialogUpdateResetButton(); } -function aceDryDialogHighlightPreset(presetKey){ - _aceDryDialogPresetKey=presetKey||''; - document.querySelectorAll('.ace-dry-preset-btn').forEach(function(btn){ - var on=(btn.getAttribute('data-preset')===presetKey); - btn.style.background=on?'var(--accent)':'var(--raised)'; - btn.style.color=on?'#fff':'var(--txt2)'; - btn.style.borderColor=on?'var(--accent)':'var(--border)'; +function aceDryDialogHighlightPreset(presetKey) { + _aceDryDialogPresetKey = presetKey || ""; + document.querySelectorAll(".ace-dry-preset-btn").forEach(function (btn) { + var on = btn.getAttribute("data-preset") === presetKey; + btn.style.background = on ? "var(--accent)" : "var(--raised)"; + btn.style.color = on ? "#fff" : "var(--txt2)"; + btn.style.borderColor = on ? "var(--accent)" : "var(--border)"; }); aceDryDialogUpdateCustomNameUi(); } -function aceDryDialogPreset(presetKey){ - var p=ACE_DRY_PRESETS[presetKey]; - if(!p)return; - var sec=p.duration_sec; - document.getElementById('ace-dry-dialog-temp').value=p.temp; - document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600); - document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60); - document.getElementById('ace-dry-dialog-s').value=sec%60; +function aceDryDialogPreset(presetKey) { + var p = ACE_DRY_PRESETS[presetKey]; + if (!p) return; + var sec = p.duration_sec; + document.getElementById("ace-dry-dialog-temp").value = p.temp; + document.getElementById("ace-dry-dialog-h").value = Math.floor(sec / 3600); + document.getElementById("ace-dry-dialog-m").value = Math.floor((sec % 3600) / 60); + document.getElementById("ace-dry-dialog-s").value = sec % 60; aceDryDialogHighlightPreset(presetKey); aceDryDialogSyncCustomButtonNames(); aceDryDialogUpdateSaveButton(); aceDryDialogUpdateResetButton(); } -function resetAceDryPresetToDefault(){ - var key=_aceDryDialogPresetKey||''; - var d=ACE_DRY_PRESET_DEFAULTS[key]; - if(!key||!d)return; - var sec=Number(d.duration_sec)||0; - document.getElementById('ace-dry-dialog-temp').value=Number(d.temp)||45; - document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600); - document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60); - document.getElementById('ace-dry-dialog-s').value=sec%60; +function resetAceDryPresetToDefault() { + var key = _aceDryDialogPresetKey || ""; + var d = ACE_DRY_PRESET_DEFAULTS[key]; + if (!key || !d) return; + var sec = Number(d.duration_sec) || 0; + document.getElementById("ace-dry-dialog-temp").value = Number(d.temp) || 45; + document.getElementById("ace-dry-dialog-h").value = Math.floor(sec / 3600); + document.getElementById("ace-dry-dialog-m").value = Math.floor((sec % 3600) / 60); + document.getElementById("ace-dry-dialog-s").value = sec % 60; aceDryDialogInputsChanged(); } -function saveAceDryPresetAndRestart(){ - var key=_aceDryDialogPresetKey||''; - var btn=document.getElementById('ace-dry-dialog-save-preset'); - if(!key||!ACE_DRY_PRESETS[key]||!btn)return; - var cur=aceDryDialogCurrentValues(); - if(!ACE_DRY_PRESETS[key])ACE_DRY_PRESETS[key]={}; - ACE_DRY_PRESETS[key].temp=cur.temp; - ACE_DRY_PRESETS[key].duration_sec=cur.duration_sec; - if(aceDryDialogIsCustomPreset(key)){ - var nameInp=document.getElementById('ace-dry-dialog-custom-name'); - var nm=((nameInp&&nameInp.value)||'').trim(); - ACE_DRY_PRESETS[key].name=nm||('Custom '+key.slice(-1)); +function saveAceDryPresetAndRestart() { + var key = _aceDryDialogPresetKey || ""; + var btn = document.getElementById("ace-dry-dialog-save-preset"); + if (!key || !ACE_DRY_PRESETS[key] || !btn) return; + var cur = aceDryDialogCurrentValues(); + if (!ACE_DRY_PRESETS[key]) ACE_DRY_PRESETS[key] = {}; + ACE_DRY_PRESETS[key].temp = cur.temp; + ACE_DRY_PRESETS[key].duration_sec = cur.duration_sec; + if (aceDryDialogIsCustomPreset(key)) { + var nameInp = document.getElementById("ace-dry-dialog-custom-name"); + var nm = ((nameInp && nameInp.value) || "").trim(); + ACE_DRY_PRESETS[key].name = nm || "Custom " + key.slice(-1); } - btn.disabled=true; - btn.textContent='…'; - fetch(_apiUrl('/api/settings')).then(function(r){return r.json();}).then(function(d){ - d.ace_dry_presets={ - pla:{temp:ACE_DRY_PRESETS.pla.temp,duration_sec:ACE_DRY_PRESETS.pla.duration_sec}, - pla_plus:{temp:ACE_DRY_PRESETS.pla_plus.temp,duration_sec:ACE_DRY_PRESETS.pla_plus.duration_sec}, - petg:{temp:ACE_DRY_PRESETS.petg.temp,duration_sec:ACE_DRY_PRESETS.petg.duration_sec}, - tpu:{temp:ACE_DRY_PRESETS.tpu.temp,duration_sec:ACE_DRY_PRESETS.tpu.duration_sec}, - abs_asa:{temp:ACE_DRY_PRESETS.abs_asa.temp,duration_sec:ACE_DRY_PRESETS.abs_asa.duration_sec}, - pa_pc:{temp:ACE_DRY_PRESETS.pa_pc.temp,duration_sec:ACE_DRY_PRESETS.pa_pc.duration_sec}, - custom_1:{name:ACE_DRY_PRESETS.custom_1.name,temp:ACE_DRY_PRESETS.custom_1.temp,duration_sec:ACE_DRY_PRESETS.custom_1.duration_sec}, - custom_2:{name:ACE_DRY_PRESETS.custom_2.name,temp:ACE_DRY_PRESETS.custom_2.temp,duration_sec:ACE_DRY_PRESETS.custom_2.duration_sec}, - custom_3:{name:ACE_DRY_PRESETS.custom_3.name,temp:ACE_DRY_PRESETS.custom_3.temp,duration_sec:ACE_DRY_PRESETS.custom_3.duration_sec} - }; - return post('/api/settings',d); - }).then(function(){ - clog('ACE preset '+key+' '+tr('settings_save'),'msg-ok'); - closeAceDryDialog(); - }).catch(function(e){ - btn.disabled=false; - btn.textContent=tr('ace_dry_dialog_save_restart'); - clog('ACE-Preset Fehler: '+e,'msg-err'); - }); + btn.disabled = true; + btn.textContent = "…"; + fetch(_apiUrl("/api/settings")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + d.ace_dry_presets = { + pla: { + temp: ACE_DRY_PRESETS.pla.temp, + duration_sec: ACE_DRY_PRESETS.pla.duration_sec, + }, + pla_plus: { + temp: ACE_DRY_PRESETS.pla_plus.temp, + duration_sec: ACE_DRY_PRESETS.pla_plus.duration_sec, + }, + petg: { + temp: ACE_DRY_PRESETS.petg.temp, + duration_sec: ACE_DRY_PRESETS.petg.duration_sec, + }, + tpu: { + temp: ACE_DRY_PRESETS.tpu.temp, + duration_sec: ACE_DRY_PRESETS.tpu.duration_sec, + }, + abs_asa: { + temp: ACE_DRY_PRESETS.abs_asa.temp, + duration_sec: ACE_DRY_PRESETS.abs_asa.duration_sec, + }, + pa_pc: { + temp: ACE_DRY_PRESETS.pa_pc.temp, + duration_sec: ACE_DRY_PRESETS.pa_pc.duration_sec, + }, + custom_1: { + name: ACE_DRY_PRESETS.custom_1.name, + temp: ACE_DRY_PRESETS.custom_1.temp, + duration_sec: ACE_DRY_PRESETS.custom_1.duration_sec, + }, + custom_2: { + name: ACE_DRY_PRESETS.custom_2.name, + temp: ACE_DRY_PRESETS.custom_2.temp, + duration_sec: ACE_DRY_PRESETS.custom_2.duration_sec, + }, + custom_3: { + name: ACE_DRY_PRESETS.custom_3.name, + temp: ACE_DRY_PRESETS.custom_3.temp, + duration_sec: ACE_DRY_PRESETS.custom_3.duration_sec, + }, + }; + return post("/api/settings", d); + }) + .then(function () { + clog("ACE preset " + key + " " + tr("settings_save"), "msg-ok"); + closeAceDryDialog(); + }) + .catch(function (e) { + btn.disabled = false; + btn.textContent = tr("ace_dry_dialog_save_restart"); + clog("ACE preset " + tr("log_error", "Fehler:") + " " + e, "msg-err"); + }); } -function confirmAceDryDialog(){ - if(_aceDryDialogAceId<0)return; - var t=parseInt(document.getElementById('ace-dry-dialog-temp').value||45,10); - var h=parseInt(document.getElementById('ace-dry-dialog-h').value||0,10); - var m=parseInt(document.getElementById('ace-dry-dialog-m').value||0,10); - var s=parseInt(document.getElementById('ace-dry-dialog-s').value||0,10); - t=Math.max(30,Math.min(80,t)); - h=Math.max(0,Math.min(24,h)); - m=Math.max(0,Math.min(59,m)); - s=Math.max(0,Math.min(59,s)); - var totalSec=(h*3600)+(m*60)+s; - totalSec=Math.max(10*60,Math.min(24*3600,totalSec)); - var preset=_aceDryDialogPresetKey||''; - _aceDryProfileSet(_aceDryDialogAceId,t,totalSec,preset); +function confirmAceDryDialog() { + if (_aceDryDialogAceId < 0) return; + var t = parseInt(document.getElementById("ace-dry-dialog-temp").value || 45, 10); + var h = parseInt(document.getElementById("ace-dry-dialog-h").value || 0, 10); + var m = parseInt(document.getElementById("ace-dry-dialog-m").value || 0, 10); + var s = parseInt(document.getElementById("ace-dry-dialog-s").value || 0, 10); + t = Math.max(30, Math.min(80, t)); + h = Math.max(0, Math.min(24, h)); + m = Math.max(0, Math.min(59, m)); + s = Math.max(0, Math.min(59, s)); + var totalSec = h * 3600 + m * 60 + s; + totalSec = Math.max(10 * 60, Math.min(24 * 3600, totalSec)); + var preset = _aceDryDialogPresetKey || ""; + _aceDryProfileSet(_aceDryDialogAceId, t, totalSec, preset); closeAceDryDialog(); applyState(); } -function aceDryToggle(aceId,on){ - if(on)return aceDryStart(aceId); +function aceDryToggle(aceId, on) { + if (on) return aceDryStart(aceId); return aceDryStop(aceId); } -function toggleCam(){if(camOn)camStop();else camStart()} +function toggleCam() { if (camOn) camStop(); else camStart() } -function resetCamera(){ - post('/api/camera/reset',{}).then(function(){ - var btn=document.getElementById('cam-reset-btn'); - if(btn){btn.textContent='↻';setTimeout(function(){btn.textContent='↺';},1000);} - }).catch(function(){}); +function resetCamera() { + post('/api/camera/reset', {}).then(function () { + var btn = document.getElementById('cam-reset-btn'); + if (btn) { btn.textContent = '↻'; setTimeout(function () { btn.textContent = '↺'; }, 1000); } + }).catch(function () { }); } -function aceDryStop(aceId){ - aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0; - return post('/api/ace/dry',{action:'stop',ace_id:aceId}) - .then(function(r){return r.json();}) - .then(function(r){ - if(r.error){throw new Error(r.error);} - clog('ACE '+(aceId+1)+' - '+tr('ace_dry_dryer')+': '+tr('ace_dry_stop'),'msg-ok'); +function aceDryStop(aceId) { + aceId = typeof aceId === "number" && aceId >= 0 ? aceId : 0; + return post("/api/ace/dry", { action: "stop", ace_id: aceId }) + .then(function (r) { + return r.json(); + }) + .then(function (r) { + if (r.error) { + throw new Error(r.error); + } + clog( + "ACE " + (aceId + 1) + " - " + tr("ace_dry_dryer") + ": " + tr("ace_dry_stop"), + "msg-ok" + ); poll(); }) - .catch(function(e){clog('ACE-Fehler: '+e,'msg-err');}); + .catch(function (e) { + clog("ACE " + tr("log_error", "Fehler:") + " " + e, "msg-err"); + }); } function loadStore(){ @@ -2811,6 +3838,35 @@ function _storeFilteredFiles(){ return files; } +// Applies the store's search/filter/sort controls to storeFiles. Shared by +// renderStore() and storeToggleSelectAll() so "Select All" only selects what +// the user can currently see, not the entire (possibly filtered-out) list. +function _storeFilteredFiles(){ + var q=(document.getElementById('store-search')||{value:''}).value.toLowerCase().trim(); + var filter=(document.getElementById('store-filter')||{value:'all'}).value; + 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||''); + }); + return files; +} + function uploadGcode(file){ if(!file) return; var zone=document.getElementById('store-upload-zone'); @@ -2818,37 +3874,57 @@ function uploadGcode(file){ var label=document.getElementById('store-upload-label'); // Nur druckbare Dateien zulassen (Issue #59) — Drag&Drop umgeht das // accept-Attribut, daher hier explizit prüfen. - var _fn=(file.name||'').toLowerCase(); - if(!/\.(gcode|gcode\.3mf|3mf|bgcode)$/.test(_fn)){ - if(status){ status.textContent=T.store_upload_only_gcode||'Only GCode files allowed'; status.style.display=''; status.className='upload-status-err'; } - clog('Upload abgelehnt (kein GCode): '+file.name,'msg-err'); + var _fn = (file.name || "").toLowerCase(); + if (!/\.(gcode|gcode\.3mf|3mf|bgcode)$/.test(_fn)) { + if (status) { + status.textContent = T.store_upload_only_gcode || "Only GCode files allowed"; + status.style.display = ""; + status.className = "upload-status-err"; + } + clog("Upload abgelehnt (kein GCode): " + file.name, "msg-err"); return; } - if(status) { status.textContent=T.store_upload_busy; status.style.display=''; status.className='upload-status-busy'; } - if(label) label.style.display='none'; - if(zone) zone.style.pointerEvents='none'; - var fd=new FormData(); - fd.append('file', file); - fd.append('web_upload', 'true'); - fetch(_apiUrl('/api/files/local'),{method:'POST',body:fd}) - .then(function(r){ - if(!r.ok) return r.text().then(function(t){throw new Error(r.status+': '+t);}); + if (status) { + status.textContent = T.store_upload_busy; + status.style.display = ""; + status.className = "upload-status-busy"; + } + if (label) label.style.display = "none"; + if (zone) zone.style.pointerEvents = "none"; + var fd = new FormData(); + fd.append("file", file); + fd.append("web_upload", "true"); + fetch(_apiUrl("/api/files/local"), { method: "POST", body: fd }) + .then(function (r) { + if (!r.ok) + return r.text().then(function (t) { + throw new Error(r.status + ": " + t); + }); return r.json(); }) - .then(function(){ - if(status){ status.textContent=T.store_upload_success.replace('{file}',file.name); status.className='upload-status-ok'; } + .then(function () { + if (status) { + status.textContent = T.store_upload_success.replace("{file}", file.name); + status.className = "upload-status-ok"; + } loadStore(); - setTimeout(function(){ - if(status){status.style.display='none'; status.className='';} - if(label) label.style.display=''; - if(zone) zone.style.pointerEvents=''; + setTimeout(function () { + if (status) { + status.style.display = "none"; + status.className = ""; + } + if (label) label.style.display = ""; + if (zone) zone.style.pointerEvents = ""; }, 3000); }) - .catch(function(e){ - if(status){ status.textContent=T.store_upload_error.replace('{error}',e.message); status.className='upload-status-err'; } - if(label) label.style.display=''; - if(zone) zone.style.pointerEvents=''; - clog('Upload-Fehler: '+e,'msg-err'); + .catch(function (e) { + if (status) { + status.textContent = T.store_upload_error.replace("{error}", e.message); + status.className = "upload-status-err"; + } + if (label) label.style.display = ""; + if (zone) zone.style.pointerEvents = ""; + clog(tr("log_error", "Fehler:") + " " + e, "msg-err"); }); } @@ -2857,16 +3933,16 @@ function renderStore(){ var empty=document.getElementById('store-empty'); var files=_storeFilteredFiles(); - if(!storeFiles.length){ - empty.textContent=T.store_empty; - grid.innerHTML=''; - empty.style.display='block'; + 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'; + if (!files.length) { + empty.textContent = T.store_no_results; + grid.innerHTML = ""; + empty.style.display = "block"; return; } empty.style.display='none'; @@ -2940,18 +4016,19 @@ function _storeUpdateSelectBar(files){ delBtn.disabled=n===0; } -function formatDur(sec){ - var h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60); - return h?h+'h '+m+'m':m+'m'; +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 _pendingWebVerifyFileId=null; -var _pendingWebVerifyFilename=''; -var _pendingWebVerifyAction=null; -var _pendingWebVerifyAutoOpen=false; +var _storeFileId = null; +var _storeFilename = null; +var _filamentDialogMode = "store"; // 'store' oder 'banner' +var _pendingWebVerifyFileId = null; +var _pendingWebVerifyFilename = ""; +var _pendingWebVerifyAction = null; +var _pendingWebVerifyAutoOpen = false; // GCode-Store-Dateiliste. MUSS deklariert sein – sonst ReferenceError, wenn // "Slots wählen" im Banner geklickt wird, bevor der Browser-Tab je geladen // wurde (Issue #29 / Theme-Auslagerung PR #27). @@ -3196,753 +4273,1132 @@ function _setGcodeFilamentsFromFileObj(fileObj){ }else{ _gcodeFilaments=[]; } - }catch(e){ - _gcodeFilaments=[]; + } catch (e) { + _gcodeFilaments = []; } } -function storePrint(fileId, filename){ - _storeFileId=fileId; - _storeFilename=filename; - _filamentDialogMode='store'; - var fileObj=storeFiles.find(function(f){return f.id===fileId;}); +function storePrint(fileId, filename) { + _storeFileId = fileId; + _storeFilename = filename; + _filamentDialogMode = "store"; + var fileObj = storeFiles.find(function (f) { + return f.id === fileId; + }); openStorePrintDialog(fileId, filename, fileObj); } -function openStorePrintDialog(fileId, filename, fileObj){ - _storeFileId=fileId; - _storeFilename=filename; - _filamentDialogMode='store'; - maybeGateWebUpload(fileObj, function(){ +function openStorePrintDialog(fileId, filename, fileObj) { + _storeFileId = fileId; + _storeFilename = filename; + _filamentDialogMode = "store"; + maybeGateWebUpload(fileObj, function () { // GCode-Filamente aus Store-Datei holen (für Vorschau im Dialog) _setGcodeFilamentsFromFileObj(fileObj); - fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ - openFilamentDialog(d.result||[]); - }).catch(function(){openFilamentDialog([]);}); + fetch(_apiUrl("/kx/filament/slots")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + openFilamentDialog(d.result || []); + }) + .catch(function () { + openFilamentDialog([]); + }); }); } -function webUploadWarningEnabled(){ - return S.web_upload_warning===undefined ? true : !!S.web_upload_warning; +function webUploadWarningEnabled() { + return S.web_upload_warning === undefined ? true : !!S.web_upload_warning; } -function clearWebUploadWarningFlag(fileId, onDone){ - if(!fileId){ - if(onDone) onDone(); +function clearWebUploadWarningFlag(fileId, onDone) { + if (!fileId) { + if (onDone) onDone(); return; } - fetch(_apiUrl('/kx/files/'+encodeURIComponent(fileId)+'/verify'), {method:'POST'}) - .then(function(r){ - if(!r.ok) return r.text().then(function(t){throw new Error(r.status+': '+t);}); + fetch(_apiUrl("/kx/files/" + encodeURIComponent(fileId) + "/verify"), { + method: "POST", + }) + .then(function (r) { + if (!r.ok) + return r.text().then(function (t) { + throw new Error(r.status + ": " + t); + }); return r.json(); }) - .then(function(){ - var fileObj=(storeFiles||[]).find(function(f){return f.id===fileId;}); - if(fileObj){fileObj.web_unverified=false;} - if(onDone) onDone(); + .then(function () { + var fileObj = (storeFiles || []).find(function (f) { + return f.id === fileId; + }); + if (fileObj) { + fileObj.web_unverified = false; + } + if (onDone) onDone(); loadStore(); }) - .catch(function(e){ - clog('Verifizierungs-Fehler: '+e,'msg-err'); + .catch(function (e) { + clog(tr("log_error", "Fehler:") + " " + e, "msg-err"); }); } -function maybeGateWebUpload(fileObj, onContinue, opts){ - opts=opts||{}; - if(!fileObj || !fileObj.web_unverified){ - if(onContinue) onContinue(); +function maybeGateWebUpload(fileObj, onContinue, opts) { + opts = opts || {}; + if (!fileObj || !fileObj.web_unverified) { + if (onContinue) onContinue(); return; } - if(!webUploadWarningEnabled()){ - if(onContinue) onContinue(); + if (!webUploadWarningEnabled()) { + if (onContinue) onContinue(); return; } - var cancelledId=sessionStorage.getItem('webVerifyCancelledFileId')||''; - if(opts.autoOpen && cancelledId && cancelledId===String(fileObj.id||'')){ + var cancelledId = sessionStorage.getItem("webVerifyCancelledFileId") || ""; + if (opts.autoOpen && cancelledId && cancelledId === String(fileObj.id || "")) { return; } - openWebVerifyDialog(fileObj.id, fileObj.filename, function(){ - clearWebUploadWarningFlag(fileObj.id, onContinue); - }, !!opts.autoOpen); + openWebVerifyDialog( + fileObj.id, + fileObj.filename, + function () { + clearWebUploadWarningFlag(fileObj.id, onContinue); + }, + !!opts.autoOpen + ); } -function openWebVerifyDialog(fileId, filename, onConfirm, autoOpen){ - _pendingWebVerifyFileId=fileId; - _pendingWebVerifyFilename=filename; - _pendingWebVerifyAction=onConfirm||null; - _pendingWebVerifyAutoOpen=!!autoOpen; - var status=document.getElementById('store-web-verify-status'); - if(status){status.textContent='';} +function openWebVerifyDialog(fileId, filename, onConfirm, autoOpen) { + _pendingWebVerifyFileId = fileId; + _pendingWebVerifyFilename = filename; + _pendingWebVerifyAction = onConfirm || null; + _pendingWebVerifyAutoOpen = !!autoOpen; + var status = document.getElementById("store-web-verify-status"); + if (status) { + status.textContent = ""; + } openStoreWebVerifyDialog(); } -function openStoreWebVerifyDialog(){ - var modal=document.getElementById('store-web-verify-dialog'); - if(modal){modal.classList.add('open');} -} - -function closeStoreWebVerifyDialog(){ - var modal=document.getElementById('store-web-verify-dialog'); - if(modal){modal.classList.remove('open');} - if(_pendingWebVerifyAutoOpen && _pendingWebVerifyFileId){ - sessionStorage.setItem('webVerifyCancelledFileId', String(_pendingWebVerifyFileId)); +function openStoreWebVerifyDialog() { + var modal = document.getElementById("store-web-verify-dialog"); + if (modal) { + modal.classList.add("open"); } - _pendingWebVerifyFileId=null; - _pendingWebVerifyFilename=''; - _pendingWebVerifyAction=null; - _pendingWebVerifyAutoOpen=false; } -function confirmStoreWebVerify(){ - if(!_pendingWebVerifyFileId||!_pendingWebVerifyFilename){ +function closeStoreWebVerifyDialog() { + var modal = document.getElementById("store-web-verify-dialog"); + if (modal) { + modal.classList.remove("open"); + } + if (_pendingWebVerifyAutoOpen && _pendingWebVerifyFileId) { + sessionStorage.setItem("webVerifyCancelledFileId", String(_pendingWebVerifyFileId)); + } + _pendingWebVerifyFileId = null; + _pendingWebVerifyFilename = ""; + _pendingWebVerifyAction = null; + _pendingWebVerifyAutoOpen = false; +} + +function confirmStoreWebVerify() { + if (!_pendingWebVerifyFileId || !_pendingWebVerifyFilename) { closeStoreWebVerifyDialog(); return; } - var fileId=_pendingWebVerifyFileId; - var action=_pendingWebVerifyAction; - var status=document.getElementById('store-web-verify-status'); - if(status){status.textContent='…';} - fetch(_apiUrl('/kx/files/'+encodeURIComponent(fileId)+'/verify'), {method:'POST'}) - .then(function(r){ - if(!r.ok) return r.text().then(function(t){throw new Error(r.status+': '+t);}); + var fileId = _pendingWebVerifyFileId; + var action = _pendingWebVerifyAction; + var status = document.getElementById("store-web-verify-status"); + if (status) { + status.textContent = "…"; + } + fetch(_apiUrl("/kx/files/" + encodeURIComponent(fileId) + "/verify"), { + method: "POST", + }) + .then(function (r) { + if (!r.ok) + return r.text().then(function (t) { + throw new Error(r.status + ": " + t); + }); return r.json(); }) - .then(function(){ - var fileObj=(storeFiles||[]).find(function(f){return f.id===fileId;}); - if(fileObj){fileObj.web_unverified=false;} - sessionStorage.removeItem('webVerifyCancelledFileId'); - _pendingWebVerifyFileId=null; - _pendingWebVerifyFilename=''; - _pendingWebVerifyAction=null; - _pendingWebVerifyAutoOpen=false; + .then(function () { + var fileObj = (storeFiles || []).find(function (f) { + return f.id === fileId; + }); + if (fileObj) { + fileObj.web_unverified = false; + } + sessionStorage.removeItem("webVerifyCancelledFileId"); + _pendingWebVerifyFileId = null; + _pendingWebVerifyFilename = ""; + _pendingWebVerifyAction = null; + _pendingWebVerifyAutoOpen = false; closeStoreWebVerifyDialog(); loadStore(); - if(typeof action==='function') action(); + if (typeof action === "function") action(); }) - .catch(function(e){ - if(status){status.textContent='✗ '+e.message;} - clog('Verifizierungs-Fehler: '+e,'msg-err'); + .catch(function (e) { + if (status) { + status.textContent = "✗ " + e.message; + } + clog(tr("log_error", "Fehler:") + " " + e, "msg-err"); }); } -function _showMismatchWarn(mismatches){ - var el=document.getElementById('fd-mismatch-warn'); - if(!el)return; - if(!mismatches||!mismatches.length){el.style.display='none';el.innerHTML='';return;} - var lines=mismatches.map(function(m){ - var slot='Slot '+(m.slot_index+1); - if(m.reason==='empty') - return '⚠ '+slot+': GCode needs '+m.gcode_material+' — slot is empty'; - return '⚠ '+slot+': GCode needs '+m.gcode_material+', loaded: '+(m.ams_material||'?'); +function _showMismatchWarn(mismatches) { + var el = document.getElementById('fd-mismatch-warn'); + if (!el) return; + if (!mismatches || !mismatches.length) { el.style.display = 'none'; el.innerHTML = ''; return; } + var lines = mismatches.map(function (m) { + var slot = 'Slot ' + (m.slot_index + 1); + if (m.reason === 'empty') + return '⚠ ' + slot + ': GCode needs ' + m.gcode_material + ' — slot is empty'; + return '⚠ ' + slot + ': GCode needs ' + m.gcode_material + ', loaded: ' + (m.ams_material || '?'); }); - el.innerHTML='Filament mismatch detected
'+lines.join('
'); - el.style.display=''; + el.innerHTML = 'Filament mismatch detected
' + lines.join('
'); + el.style.display = ''; } -function startReadyFileWithSlots(filename,_autoOpen,_mismatch){ - if(!_autoOpen) _fdAutoOpenedFile=null; // manueller Aufruf → Auto-Open-Sperre aufheben - var fn=filename||S.file_ready; - var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;}); - if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){ - maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen}); +function startReadyFileWithSlots(filename, _autoOpen, _mismatch) { + if (!_autoOpen) _fdAutoOpenedFile = null; // manueller Aufruf → Auto-Open-Sperre aufheben + var fn = filename || S.file_ready; + var currentFile = (storeFiles || []).find(function (f) { return f.filename === fn; }); + if (currentFile && currentFile.web_unverified && webUploadWarningEnabled()) { + maybeGateWebUpload(currentFile, function () { startReadyFileWithSlots(fn, _autoOpen); }, { autoOpen: !!_autoOpen }); return; } - _filamentDialogMode='banner'; - _storeFilename=fn||''; + _filamentDialogMode = "banner"; + _storeFilename = fn || ""; // Banner must never reuse stale store-file context. - _storeFileId=null; - _gcodeFilaments=[]; + _storeFileId = null; + _gcodeFilaments = []; - var _autoOpenFile=_autoOpen?fn:null; - if(_autoOpen) _fdDialogOpen=true; // bereits während Fetch sperren - function openWithSlots(){ - fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ - if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;} - openFilamentDialog(d.result||[]); - _showMismatchWarn(_mismatch||null); - }).catch(function(){ - if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;} + var _autoOpenFile = _autoOpen ? fn : null; + if (_autoOpen) _fdDialogOpen = true; // bereits während Fetch sperren + function openWithSlots() { + fetch(_apiUrl('/kx/filament/slots')).then(function (r) { return r.json() }).then(function (d) { + if (_autoOpenFile && _fdUserCancelled) { _fdDialogOpen = false; return; } + openFilamentDialog(d.result || []); + _showMismatchWarn(_mismatch || null); + }).catch(function () { + if (_autoOpenFile && _fdUserCancelled) { _fdDialogOpen = false; return; } openFilamentDialog([]); - _showMismatchWarn(_mismatch||null); + _showMismatchWarn(_mismatch || null); }); } - function _proceedWithFileObj(fileObj){ - if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){ + function _proceedWithFileObj(fileObj) { + if (fileObj && fileObj.web_unverified && webUploadWarningEnabled()) { // Verify-Gate war beim ersten Lookup noch nicht aktiv (storeFiles leer) — jetzt prüfen. - if(_autoOpen){_fdDialogOpen=false;} - maybeGateWebUpload(fileObj, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen}); + if (_autoOpen) { + _fdDialogOpen = false; + } + maybeGateWebUpload( + fileObj, + function () { + startReadyFileWithSlots(fn, _autoOpen); + }, + { autoOpen: !!_autoOpen } + ); return; } - if(fileObj){ - _storeFileId=fileObj.id; + if (fileObj) { + _storeFileId = fileObj.id; _setGcodeFilamentsFromFileObj(fileObj); } openWithSlots(); } - var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;}); - if(fileObj){ + var fileObj = (storeFiles || []).find(function (f) { + return f.filename === _storeFilename; + }); + if (fileObj) { _proceedWithFileObj(fileObj); return; } // Fallback: refresh file list, then resolve current file by filename. - fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){ - storeFiles=d.result||[]; - var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;})||null; - _proceedWithFileObj(refreshed); - }).catch(function(){ - openWithSlots(); - }); + fetch(_apiUrl("/kx/files")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + storeFiles = d.result || []; + var refreshed = + (storeFiles || []).find(function (f) { + return f.filename === _storeFilename; + }) || null; + _proceedWithFileObj(refreshed); + }) + .catch(function () { + openWithSlots(); + }); } -var _amsSlots=[]; -var _printObjects=[]; // [{name, skip}] für aktuell offenen Dialog -var _printObjectsSvg=''; // base64-SVG aus DB für Visualisierung +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){ +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); + 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'; + var y = (r * 299 + g * 587 + b * 114) / 1000; + return y >= 140 ? "#111" : "#fff"; } -function _normalizeMaterialKey(material){ - var key=(material||'').toUpperCase().replace(/[^A-Z0-9+]/g,''); +function _normalizeMaterialKey(material) { + var key = (material || "").toUpperCase().replace(/[^A-Z0-9+]/g, ""); // Orca often uses PLA for PLA+, while AMS may report PLA+. - if(key==='PLA+'||key==='PLAPLUS') return 'PLA'; + if (key === "PLA+" || key === "PLAPLUS") return "PLA"; // Handle modifier+base patterns in either order: "Matte PLA", "Silk PETG", // "PLA Silk", "PLA Matte". OrcaSlicer always writes the base type in GCode // (filament_type = PLA), but users label slots with the full product-style name. - var trimmed=(material||'').trim(); - if(trimmed.indexOf(' ')>=0){ - var words=trimmed.toUpperCase().split(/\s+/); - for(var i=0;i=0) return w; + var trimmed = (material || "").trim(); + if (trimmed.indexOf(" ") >= 0) { + var words = trimmed.toUpperCase().split(/\s+/); + for (var i = 0; i < words.length; i++) { + var w = words[i].replace(/[^A-Z0-9+]/g, ""); + if (_BASE_MATERIAL_TYPES.indexOf(w) >= 0) return w; } } return key; } -function _materialsCompatible(a,b){ - return _normalizeMaterialKey(a)===_normalizeMaterialKey(b); +function _materialsCompatible(a, b) { + return _normalizeMaterialKey(a) === _normalizeMaterialKey(b); } // Issue #57 Punkt 4: konkreter Profilname (User-Override) statt generischem Typ. // Fällt auf den Material-Typ zurück wenn kein Profil gemappt ist. -function _slotProfileLabel(slot){ - if(!slot)return ''; - if(slot.filament_name){ - return slot.filament_name+(slot.filament_vendor?' — '+slot.filament_vendor:''); +function _slotProfileLabel(slot) { + if (!slot) return ""; + if (slot.filament_name) { + return slot.filament_name + (slot.filament_vendor ? " — " + slot.filament_vendor : ""); } - return slot.material||''; + return slot.material || ""; } -function _escAttr(s){ - return String(s||'').replace(/&/g,'&').replace(/"/g,'"').replace(//g,'>'); +function _escAttr(s) { + return String(s || "") + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(//g, ">"); } -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); - marker.title=(opt&&opt.dataset.profile)?opt.dataset.profile:''; +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; + marker.title = opt && opt.dataset.profile ? opt.dataset.profile : ""; } } -function openFilamentDialog(slots){ - _amsSlots=slots - .filter(function(s){return s.status==='loaded';}) - .sort(function(a,b){return (a.slot_index||0)-(b.slot_index||0);}); +function openFilamentDialog(slots) { + _amsSlots = slots + .filter(function (s) { + return s.status === "loaded"; + }) + .sort(function (a, b) { + return (a.slot_index || 0) - (b.slot_index || 0); + }); _loadSpoolmanStatus(); - var dlg=document.getElementById('filament-dialog'); - var title=document.getElementById('fd-title'); - var body=document.getElementById('fd-slots'); - if(title)title.textContent='▶ '+_storeFilename; + var dlg = document.getElementById("filament-dialog"); + var title = document.getElementById("fd-title"); + var body = document.getElementById("fd-slots"); + if (title) title.textContent = "▶ " + _storeFilename; // Auto-Leveling-Checkbox mit globalem Default vorbelegen - var fdAl=document.getElementById('fd-auto-leveling'); - if(fdAl) fdAl.checked=(S.auto_leveling===undefined?true:!!S.auto_leveling); + var fdAl = document.getElementById("fd-auto-leveling"); + if (fdAl) fdAl.checked = S.auto_leveling === undefined ? true : !!S.auto_leveling; // Objekt-Liste laden — sobald eine File-ID auflösbar ist (Issue #57 Punkt 3: // Skip-Parität auch im Banner-/Upload-Modus, nicht nur im Store-Modus). // startReadyFileWithSlots() setzt _storeFileId auch im Banner-Modus per // filename→fileObj-Lookup, daher reicht hier die _storeFileId-Prüfung. - _printObjects=[]; - _printObjectsSvg=''; - var objSection=document.getElementById('fd-objects-section'); - var objBody=document.getElementById('fd-objects-body'); - var objArrow=document.getElementById('fd-objects-arrow'); - if(objSection)objSection.style.display='none'; - if(objBody)objBody.style.display='none'; // immer eingeklappt starten - if(objArrow)objArrow.style.transform=''; - if(_storeFileId){ + _printObjects = []; + _printObjectsSvg = ""; + var objSection = document.getElementById("fd-objects-section"); + var objBody = document.getElementById("fd-objects-body"); + var objArrow = document.getElementById("fd-objects-arrow"); + if (objSection) objSection.style.display = "none"; + if (objBody) objBody.style.display = "none"; // immer eingeklappt starten + if (objArrow) objArrow.style.transform = ""; + if (_storeFileId) { // Bei frischem Orca-/Web-Upload liefert der Drucker die Objektliste // (objects_skip_parts) erst per fileDetails nach → ist im Store kurz leer. // Daher mehrfach nachfragen, bis Objekte da sind (Issue #57 Skip-Parität). - var _objFid=_storeFileId; - var _objTries=0; - (function _loadObjects(){ - if(_objFid!==_storeFileId) return; // Dialog wechselte Datei → abbrechen - fetch(_apiUrl('/kx/files/'+encodeURIComponent(_objFid)+'/objects')) - .then(function(r){return r.json()}) - .then(function(d){ - var names=(d.result&&d.result.names)||[]; - var svg=(d.result&&d.result.svg_b64)||''; - if(names.length>=2){ - _printObjectsSvg=svg; - _printObjects=names.map(function(n){return {name:n,skip:false};}); - renderObjectChecklist(); renderObjectSvg(); - var cnt=document.getElementById('fd-objects-count'); - if(cnt)cnt.textContent='('+names.length+')'; - if(objSection)objSection.style.display='block'; - } else if(_objTries++ < 6){ + var _objFid = _storeFileId; + var _objTries = 0; + (function _loadObjects() { + if (_objFid !== _storeFileId) return; // Dialog wechselte Datei → abbrechen + fetch(_apiUrl("/kx/files/" + encodeURIComponent(_objFid) + "/objects")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + var names = (d.result && d.result.names) || []; + var svg = (d.result && d.result.svg_b64) || ""; + if (names.length >= 2) { + _printObjectsSvg = svg; + _printObjects = names.map(function (n) { + return { name: n, skip: false }; + }); + renderObjectChecklist(); + renderObjectSvg(); + var cnt = document.getElementById("fd-objects-count"); + if (cnt) cnt.textContent = "(" + names.length + ")"; + if (objSection) objSection.style.display = "block"; + } else if (_objTries++ < 6) { setTimeout(_loadObjects, 1000); // bis ~6s auf fileDetails warten } - }).catch(function(){}); + }) + .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}; - }); + var channels = _gcodeFilaments.length + ? _gcodeFilaments + : _amsSlots.map(function (s, i) { + return { slot_index: i, color_hex: s.color_hex, material: s.material }; + }); // Default mapping strategy: // 1) keep order where possible (row i -> nearest compatible slot i) // 2) keep defaults unique while compatible slots are available // 3) use color proximity as tie-breaker - function _hexToRgb(hex){ - var c=(hex||'').replace('#',''); - if(c.length===3)c=c[0]+c[0]+c[1]+c[1]+c[2]+c[2]; - if(c.length<6)return [255,255,255]; - return [parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16)]; + function _hexToRgb(hex) { + var c = (hex || "").replace("#", ""); + if (c.length === 3) c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2]; + if (c.length < 6) return [255, 255, 255]; + return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)]; } - function _colorDist(a,b){ - var ar=_hexToRgb(a), br=_hexToRgb(b); - var dr=ar[0]-br[0], dg=ar[1]-br[1], db=ar[2]-br[2]; - return (dr*dr + dg*dg + db*db); + function _colorDist(a, b) { + var ar = _hexToRgb(a), + br = _hexToRgb(b); + var dr = ar[0] - br[0], + dg = ar[1] - br[1], + db = ar[2] - br[2]; + return dr * dr + dg * dg + db * db; } - var defaultSlotByPaint={}; - var usedDefaultSlot={}; - channels.forEach(function(gc,i){ - var compatible=_amsSlots.filter(function(s){ + var defaultSlotByPaint = {}; + var usedDefaultSlot = {}; + channels.forEach(function (gc, i) { + var compatible = _amsSlots.filter(function (s) { return _materialsCompatible(gc.material, s.material); }); - if(!compatible.length){ - defaultSlotByPaint[i]=-1; + if (!compatible.length) { + defaultSlotByPaint[i] = -1; return; } - var ranked=compatible.slice().sort(function(a,b){ - var da=Math.abs((a.slot_index||0)-i), db=Math.abs((b.slot_index||0)-i); - if(da!==db)return da-db; - var ca=_colorDist(gc.color_hex, a.color_hex), cb=_colorDist(gc.color_hex, b.color_hex); - if(ca!==cb)return ca-cb; - return (a.slot_index||0)-(b.slot_index||0); + var ranked = compatible.slice().sort(function (a, b) { + var da = Math.abs((a.slot_index || 0) - i), + db = Math.abs((b.slot_index || 0) - i); + if (da !== db) return da - db; + var ca = _colorDist(gc.color_hex, a.color_hex), + cb = _colorDist(gc.color_hex, b.color_hex); + if (ca !== cb) return ca - cb; + return (a.slot_index || 0) - (b.slot_index || 0); }); - var chosen=ranked.find(function(s){return !usedDefaultSlot[s.slot_index];}) || ranked[0]; - defaultSlotByPaint[i]=chosen?chosen.slot_index:-1; - if(chosen) usedDefaultSlot[chosen.slot_index]=1; + var chosen = + ranked.find(function (s) { + return !usedDefaultSlot[s.slot_index]; + }) || ranked[0]; + defaultSlotByPaint[i] = chosen ? chosen.slot_index : -1; + if (chosen) usedDefaultSlot[chosen.slot_index] = 1; }); - if(!_amsSlots.length){ - body.innerHTML='

'+T.fd_no_slots_msg.replace('{br}','
')+'

'; + if (!_amsSlots.length) { + body.innerHTML = + '

' + + T.fd_no_slots_msg.replace("{br}", "
") + + "

"; } else { - body.innerHTML=channels.map(function(gc,i){ - var isUsed=(gc&&gc.is_used!==false); - // Only allow material-compatible slots. - var compatible=_amsSlots.filter(function(s){ - return _materialsCompatible(gc.material, s.material); - }); + body.innerHTML = channels + .map(function (gc, i) { + var isUsed = gc && gc.is_used !== false; + // Only allow material-compatible slots. + var compatible = _amsSlots.filter(function (s) { + return _materialsCompatible(gc.material, s.material); + }); - var defaultSlotIndex=(defaultSlotByPaint.hasOwnProperty(i)?defaultSlotByPaint[i]:-1); - var defaultSlot=compatible.find(function(s){return s.slot_index===defaultSlotIndex;})||null; - var opts=compatible.map(function(s){ - var sel=(defaultSlot&&s.slot_index===defaultSlot.slot_index)?'selected':''; - return ''; - }).join(''); - if(!compatible.length){ - opts=''; - } - // Kanal-Box (links): farbige Box mit Nummer + auto Kontrast-Text - var txt=_contrastText(gc.color_hex); - var slotColor=defaultSlot?defaultSlot.color_hex:'#888'; - var slotTxt=_contrastText(slotColor); - var usedBadge=isUsed - ? ''+T.fd_used+'' - : ''+T.fd_used+''; - return '
'+ - ''+(i+1)+''+ - ''+gc.material+''+ - usedBadge+ - ''+ - ''+(defaultSlot?defaultSlot.slot_index+1:'?')+''+ - ''+ - '
'; - }).join(''); + var defaultSlotIndex = Object.prototype.hasOwnProperty.call(defaultSlotByPaint, i) + ? defaultSlotByPaint[i] + : -1; + var defaultSlot = + compatible.find(function (s) { + return s.slot_index === defaultSlotIndex; + }) || null; + var opts = compatible + .map(function (s) { + var sel = defaultSlot && s.slot_index === defaultSlot.slot_index ? "selected" : ""; + return ( + '" + ); + }) + .join(""); + if (!compatible.length) { + opts = + '"; + } + // Kanal-Box (links): farbige Box mit Nummer + auto Kontrast-Text + var txt = _contrastText(gc.color_hex); + var slotColor = defaultSlot ? defaultSlot.color_hex : "#888"; + var slotTxt = _contrastText(slotColor); + var usedBadge = isUsed + ? '' + + T.fd_used + + "" + : '' + + T.fd_used + + ""; + return ( + '
' + + '' + + (i + 1) + + "" + + '' + + gc.material + + "" + + usedBadge + + '' + + '' + + (defaultSlot ? defaultSlot.slot_index + 1 : "?") + + "" + + '" + + "
" + ); + }) + .join(""); } - if(dlg)dlg.classList.add('open'); + if (dlg) dlg.classList.add("open"); // Spoolman-Section nach Slot-Render aufbauen (braucht die selects im DOM) setTimeout(_buildSpoolmanSection, 0); } -function closeFilamentDialog(){ - var dlg=document.getElementById('filament-dialog'); - if(dlg)dlg.classList.remove('open'); +function closeFilamentDialog() { + var dlg = document.getElementById('filament-dialog'); + if (dlg) dlg.classList.remove('open'); _showMismatchWarn(null); - _fdDialogOpen=false; - if(_fdAutoOpenedFile){ - _fdUserCancelled=true; - sessionStorage.setItem('fdUserCancelled','1'); - sessionStorage.setItem('fdAutoOpenedFile',_fdAutoOpenedFile); + _fdDialogOpen = false; + if (_fdAutoOpenedFile) { + _fdUserCancelled = true; + sessionStorage.setItem('fdUserCancelled', '1'); + sessionStorage.setItem('fdAutoOpenedFile', _fdAutoOpenedFile); } } -function confirmFilamentPrint(){ - var selects=document.querySelectorAll('#fd-slots select'); - var assignments=[]; - var missingCompatible=0; - selects.forEach(function(sel){ - var paintIdx=parseInt(sel.dataset.paint); - var paintColor=sel.dataset.paintColor; - var isUsed=(sel.dataset.isUsed==='1'); - var hasCompatible=(sel.dataset.hasCompatible==='1'); - var opt=sel.options[sel.selectedIndex]; - var amsIdx=parseInt(opt&&opt.value); - if(!hasCompatible || Number.isNaN(amsIdx) || amsIdx < 0){ - if(isUsed) missingCompatible += 1; +function confirmFilamentPrint() { + var selects = document.querySelectorAll("#fd-slots select"); + var assignments = []; + var missingCompatible = 0; + selects.forEach(function (sel) { + var paintIdx = parseInt(sel.dataset.paint); + var paintColor = sel.dataset.paintColor; + var isUsed = sel.dataset.isUsed === "1"; + var hasCompatible = sel.dataset.hasCompatible === "1"; + var opt = sel.options[sel.selectedIndex]; + var amsIdx = parseInt(opt && opt.value); + if (!hasCompatible || Number.isNaN(amsIdx) || amsIdx < 0) { + if (isUsed) missingCompatible += 1; amsIdx = -1; } - var amsSlot=_amsSlots.find(function(s){return s.slot_index===amsIdx;})||{}; + 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]; + function hexToRgba(h) { + var c = h.replace("#", ""); + if (c.length === 3) c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2]; + return [ + parseInt(c.slice(0, 2), 16), + parseInt(c.slice(2, 4), 16), + parseInt(c.slice(4, 6), 16), + 255, + ]; } assignments.push({ - paint_index: paintIdx, - is_used: isUsed, - slot_index: amsIdx, - material: opt.dataset.material||'PLA', - paint_color: hexToRgba(paintColor||'#ffffff'), - ams_color: hexToRgba(amsSlot.color_hex||'#ffffff'), + paint_index: paintIdx, + is_used: isUsed, + slot_index: amsIdx, + material: opt.dataset.material || "PLA", + paint_color: hexToRgba(paintColor || "#ffffff"), + ams_color: hexToRgba(amsSlot.color_hex || "#ffffff"), }); }); - if(missingCompatible>0){ - clog('Cannot start print: '+missingCompatible+' used paint(s) have no matching material slot','msg-err'); + if (missingCompatible > 0) { + clog( + "Cannot start print: " + missingCompatible + " used paint(s) have no matching material slot", + "msg-err" + ); return; } // Pre-Print Skip: Namen der abgehakten Objekte sammeln - var excludedObjects=_printObjects.filter(function(o){return o.skip;}).map(function(o){return o.name;}); - var fdAlEl=document.getElementById('fd-auto-leveling'); - var fdAutoLeveling=fdAlEl?( fdAlEl.checked?1:0):(S.auto_leveling===undefined?1:S.auto_leveling?1:0); - // Spoolman: Slot→Spool-Mapping aus Dialog sammeln und senden - if(_spoolmanStatus.configured){ - var slotMap={}; - document.querySelectorAll('[data-spool-slot]').forEach(function(sel){ - var idx=sel.dataset.spoolSlot; - var val=parseInt(sel.value); - if(val>0)slotMap[idx]=val; + var excludedObjects = _printObjects + .filter(function (o) { + return o.skip; + }) + .map(function (o) { + return o.name; }); - _slotSpoolMap=slotMap; - post('/kx/spoolman/active-spool',{slot_map:slotMap}).catch(function(){}); + var fdAlEl = document.getElementById("fd-auto-leveling"); + var fdAutoLeveling = fdAlEl + ? fdAlEl.checked + ? 1 + : 0 + : S.auto_leveling === undefined + ? 1 + : S.auto_leveling + ? 1 + : 0; + // Spoolman: Slot→Spool-Mapping aus Dialog sammeln und senden + if (_spoolmanStatus.configured) { + var slotMap = {}; + document.querySelectorAll("[data-spool-slot]").forEach(function (sel) { + var idx = sel.dataset.spoolSlot; + var val = parseInt(sel.value); + if (val > 0) slotMap[idx] = val; + }); + _slotSpoolMap = slotMap; + post("/kx/spoolman/active-spool", { slot_map: slotMap }).catch(function () { }); } closeFilamentDialog(); - if(_filamentDialogMode==='banner'){ + if (_filamentDialogMode === "banner") { // Banner-Modus: /kx/print bevorzugen wenn _storeFileId bekannt (gleicher Pfad wie File-Browser). - var btn=document.getElementById('file-ready-btn'); - if(btn){btn.disabled=true;btn.textContent='…';} + var btn = document.getElementById("file-ready-btn"); + if (btn) { + btn.disabled = true; + btn.textContent = "…"; + } var startPromise; - if(_storeFileId){ - startPromise=fetch(_apiUrl('/kx/print'),{ - method:'POST', - headers:{'Content-Type':'application/json'}, - body:JSON.stringify({ - file_id:_storeFileId, - filament_assignments:assignments, - excluded_objects:excludedObjects, - auto_leveling:fdAutoLeveling - }) + if (_storeFileId) { + startPromise = fetch(_apiUrl("/kx/print"), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + file_id: _storeFileId, + filament_assignments: assignments, + excluded_objects: excludedObjects, + auto_leveling: fdAutoLeveling, + }), }); - }else{ - startPromise=post('/printer/print/start',{ - filename:S.file_ready||_storeFilename, - filament_assignments:assignments, - excluded_objects:excludedObjects, - auto_leveling:fdAutoLeveling + } else { + startPromise = post("/printer/print/start", { + filename: S.file_ready || _storeFilename, + filament_assignments: assignments, + excluded_objects: excludedObjects, + auto_leveling: fdAutoLeveling, }); } - startPromise.then(function(r){ - if(!r.ok){return r.text().then(function(t){throw new Error(t||('HTTP '+r.status));});} - return r.json(); - }).then(function(d){ - if(d&&d.error) throw new Error(d.error); - if(d&&d.result&&d.result!=='ok') throw new Error(String(d.result)); - document.getElementById('file-ready-banner').style.display='none'; - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} - }).catch(function(e){ - clog(tr('log_error')+' '+e,'msg-err'); - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} - }); + startPromise + .then(function (r) { + if (!r.ok) { + return r.text().then(function (t) { + throw new Error(t || "HTTP " + r.status); + }); + } + return r.json(); + }) + .then(function (d) { + if (d && d.error) throw new Error(d.error); + if (d && d.result && d.result !== "ok") throw new Error(String(d.result)); + document.getElementById("file-ready-banner").style.display = "none"; + if (btn) { + btn.disabled = false; + setText("file-ready-btn", T.file_ready_btn); + } + }) + .catch(function (e) { + clog(tr("log_error", "Fehler:") + " " + 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,auto_leveling:fdAutoLeveling}) - }).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');}); + fetch(_apiUrl("/kx/print"), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + file_id: _storeFileId, + filament_assignments: assignments, + excluded_objects: excludedObjects, + auto_leveling: fdAutoLeveling, + }), + }) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + if (d.result === "ok") { + clog(tr("log_print_start", "Druckstart:") + " " + _storeFilename, "msg-ok"); + showPanel("dashboard"); + } else { + clog(tr("log_error", "Fehler:") + " " + (d.error || "?"), "msg-err"); + } + }) + .catch(function (e) { + clog(tr("log_error", "Fehler:") + " " + 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 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; +function _toggleObjectSkip(idx, val) { + if (_printObjects[idx]) _printObjects[idx].skip = !!val; renderObjectSvg(); } // Issue #57 Punkt 3: Skip-Objekte-Bereich ein-/ausklappen -function toggleFdObjects(){ - var body=document.getElementById('fd-objects-body'); - var arrow=document.getElementById('fd-objects-arrow'); - if(!body)return; - var open=body.style.display!=='none'; - body.style.display=open?'none':'block'; - if(arrow)arrow.style.transform=open?'':'rotate(90deg)'; +function toggleFdObjects() { + var body = document.getElementById("fd-objects-body"); + var arrow = document.getElementById("fd-objects-arrow"); + if (!body) return; + var open = body.style.display !== "none"; + body.style.display = open ? "none" : "block"; + if (arrow) arrow.style.transform = open ? "" : "rotate(90deg)"; } -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'); +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(); + g.onclick = function () { + _printObjects[i].skip = !_printObjects[i].skip; + renderObjectChecklist(); + renderObjectSvg(); }; }); } // ── Mid-Print Skip ── -var _skipObjects=[]; // [{name, skipped, willSkip}] -var _skipSvg=''; -var _skipPollTimer=null; -function _applySkipDialogState(s){ - s=s||{}; - _skipSvg=s.svg_b64||''; - var skipped=s.skipped||[]; +var _skipObjects = []; // [{name, skipped, willSkip}] +var _skipSvg = ""; +var _skipPollTimer = null; +function _applySkipDialogState(s) { + s = s || {}; + _skipSvg = s.svg_b64 || ""; + var skipped = s.skipped || []; // Pending-Auswahl (willSkip) beim Refresh erhalten. - var prevWillSkip={}; - (_skipObjects||[]).forEach(function(o){if(o&&o.name)prevWillSkip[o.name]=!!o.willSkip;}); - _skipObjects=(s.objects||[]).map(function(n){ - var isSkipped=(skipped.indexOf(n)>=0); - return {name:n, skipped:isSkipped, willSkip:isSkipped?false:!!prevWillSkip[n]}; + var prevWillSkip = {}; + (_skipObjects || []).forEach(function (o) { + if (o && o.name) prevWillSkip[o.name] = !!o.willSkip; }); - renderSkipList(); renderSkipSvg(); + _skipObjects = (s.objects || []).map(function (n) { + var isSkipped = skipped.indexOf(n) >= 0; + return { + name: n, + skipped: isSkipped, + willSkip: isSkipped ? false : !!prevWillSkip[n], + }; + }); + renderSkipList(); + renderSkipSvg(); } -function openSkipDialog(){ - document.getElementById('skip-status').textContent=''; - document.getElementById('skip-confirm').disabled=false; +function openSkipDialog() { + document.getElementById("skip-status").textContent = ""; + document.getElementById("skip-confirm").disabled = false; _refreshSkipDialog(); - if(_skipPollTimer)clearInterval(_skipPollTimer); - _skipPollTimer=setInterval(function(){ - var dlg=document.getElementById('skip-dialog'); - if(!(dlg&&dlg.classList.contains('open')))return; + if (_skipPollTimer) clearInterval(_skipPollTimer); + _skipPollTimer = setInterval(function () { + var dlg = document.getElementById("skip-dialog"); + if (!(dlg && dlg.classList.contains("open"))) return; _refreshSkipDialog(); - },2000); - document.getElementById('skip-dialog').classList.add('open'); + }, 2000); + document.getElementById("skip-dialog").classList.add("open"); } -function _refreshSkipDialog(){ +function _refreshSkipDialog() { // query-Endpoint wartet intern auf frischen skip/report (bis 1.5s). - fetch(_apiUrl('/kx/skip/query'),{method:'POST'}) - .then(function(r){return r.json();}) - .then(function(d){_applySkipDialogState(d.result||{});}) - .catch(function(){ - fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json();}).then(function(d){ - _applySkipDialogState(d.result||{}); - }).catch(function(){}); + fetch(_apiUrl("/kx/skip/query"), { method: "POST" }) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + _applySkipDialogState(d.result || {}); + }) + .catch(function () { + fetch(_apiUrl("/kx/skip/state")) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + _applySkipDialogState(d.result || {}); + }) + .catch(function () { }); }); } -function closeSkipDialog(){ - if(_skipPollTimer){clearInterval(_skipPollTimer);_skipPollTimer=null;} - document.getElementById('skip-dialog').classList.remove('open'); +function closeSkipDialog() { + if (_skipPollTimer) { + clearInterval(_skipPollTimer); + _skipPollTimer = null; + } + 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 _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='
'+tr('skip_no_objects')+'
'; +function renderSkipList() { + var box = document.getElementById("skip-list"); + if (!box) return; + if (!_skipObjects.length) { + box.innerHTML = + '
' + + tr("skip_no_objects") + + "
"; return; } - box.innerHTML=_skipObjects.map(function(o,i){ - var label=_shortLabel(o.name); - var dis=o.skipped?'disabled':''; - var note=o.skipped?''+tr('skip_already')+'':''; - return ''; - }).join(''); + box.innerHTML = _skipObjects + .map(function (o, i) { + var label = _shortLabel(o.name); + var dis = o.skipped ? "disabled" : ""; + var note = o.skipped + ? '' + + tr("skip_already") + + "" + : ""; + 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'; +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; + 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){ + 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.setAttribute("opacity", "0.25"); + if (path) { + path.setAttribute("fill", "#888"); + path.setAttribute("fill-opacity", "0.3"); } - g.onclick=function(){ - _skipObjects[i].willSkip=!_skipObjects[i].willSkip; - renderSkipList(); renderSkipSvg(); + 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; +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=tr('skip_select_at_least_one');st.style.color='var(--warn)';return;} - btn.disabled=true; st.textContent=tr('skip_sending'); 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=tr('skip_success');st.style.color='var(--ok)'; - // Dialog offen lassen + neu laden damit der "übersprungen"-Status erscheint - setTimeout(function(){ _refreshSkipDialog(); btn.disabled=false; st.textContent=''; }, 1500); +function confirmSkip() { + var names = _skipObjects + .filter(function (o) { + return o.willSkip; }) - .catch(function(e){st.textContent=''+e;st.style.color='var(--err)';btn.disabled=false;}); + .map(function (o) { + return o.name; + }); + var st = document.getElementById("skip-status"); + var btn = document.getElementById("skip-confirm"); + if (!names.length) { + st.textContent = tr("skip_select_at_least_one"); + st.style.color = "var(--warn)"; + return; + } + btn.disabled = true; + st.textContent = tr("skip_sending"); + 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) || tr("log_error", "Fehler:"); + st.style.color = "var(--err)"; + btn.disabled = false; + return; + } + st.textContent = tr("skip_success"); + 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');} +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(tr("log_delete_failed", "Löschen fehlgeschlagen"), "msg-err"); + } }); } -function storeDownload(fileId){ - var a=document.createElement('a'); - a.href=_apiUrl('/kx/files/'+encodeURIComponent(fileId)+'/download'); - a.style.display='none'; +function storeDownload(fileId) { + var a = document.createElement("a"); + a.href = _apiUrl("/kx/files/" + encodeURIComponent(fileId) + "/download"); + a.style.display = "none"; document.body.appendChild(a); a.click(); a.remove(); } // ── 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 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 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); +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 }; + }); }) - .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); + .then(function (res) { + if (!res.ok) { + st.textContent = (res.j && res.j.error) || tr("log_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){alert(''+e);}); + .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) || tr("log_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+'
'; +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||[]; diff --git a/web/themes/default/index.html b/web/themes/default/index.html index 30836fa..50ffb0e 100644 --- a/web/themes/default/index.html +++ b/web/themes/default/index.html @@ -1,4 +1,4 @@ - + @@ -96,46 +96,153 @@ - -