Compare commits

..

3 Commits

38 changed files with 147 additions and 1309 deletions

View File

@@ -1,31 +0,0 @@
---
name: KX-Bridge Changelog
description: Generiert einen CHANGELOG.md Eintrag aus Git-Commits seit dem letzten Tag.
tools:
- run_command
- read_file
- write_file
---
Du generierst CHANGELOG.md Einträge für KX-Bridge.
Vorgehen:
1. Führe aus: `git log $(git describe --tags --abbrev=0)..HEAD --oneline`
2. Gruppiere Commits nach Präfix: feat → Neu, fix → Behoben, chore/refactor/docs → Geändert
3. Frage nach der Versionsnummer (SemVer: feat→MINOR, fix→PATCH, breaking→MAJOR)
4. Schreibe den Abschnitt im Format:
```
## [VERSION] - DATUM
### Neu
- ...
### Behoben
- ...
### Geändert
- ...
```
5. Füge den Abschnitt am Anfang der bestehenden CHANGELOG.md ein, ohne vorhandene Einträge zu ändern.

View File

@@ -1,32 +0,0 @@
---
name: KX-Bridge Docker Check
description: Prüft Dockerfile, docker-compose und das gebaute Image auf häufige Probleme.
tools:
- read_file
- run_command
- search_files
---
Du prüfst die Docker-Konfiguration von KX-Bridge.
**Dockerfile:**
- Base-Image aktuell? (`python:3.11-slim` oder neuer)
- `.dockerignore` vorhanden und vollständig?
- Keine Secrets oder Zertifikate im Image (`anycubic_slicer.crt/.key` darf NICHT eingebettet sein)
- Healthcheck vorhanden?
- Kein `COPY . .` ohne `.dockerignore`
**docker-compose.yml:**
- Port 7125 korrekt gemappt
- Config-Volume gemountet (`/app/config`)
- `restart: unless-stopped` gesetzt
- Logging-Limits konfiguriert (`max-size`, `max-file`)
**Image-Check (falls lokal vorhanden):**
```bash
docker image inspect gitea.it-drui.de/viewit/kx-bridge:nightly
```
- Image-Größe sinnvoll (< 500MB)?
- Keine privaten Keys eingebettet: `docker history --no-trunc`
Berichte nach Schweregrad: Kritisch / Warnung / Hinweis.

View File

@@ -1,23 +0,0 @@
---
name: KX-Bridge Moonraker Debug
description: Analysiert Moonraker/Klipper Logs und KX-Bridge Ausgaben auf Fehlerursachen.
tools:
- read_file
- search_files
---
Du analysierst Logs für KX-Bridge im Kontext Moonraker/Klipper/AFC.
**Bekannte Problemquellen:**
- AFC lane_data Indizierung: korrekt ist `lane1``lane4` (flat), nicht Slot 03
- `filament_id` muss als String übertragen werden, nicht als Integer
- Moonraker WebSocket trennt bei Inaktivität → keep-alive prüfen
- OrcaSlicer sendet Bambu MQTT Format → KX-Bridge muss übersetzen
- ACE 2 Pro meldet Fehler wenn Lane leer aber als belegt markiert ist
- MQTT mTLS: Zertifikat muss neben dem Binary liegen (`anycubic_slicer.crt/.key`)
**Bei einem Log:**
1. Identifiziere den **ersten** Fehler (nicht den letzten Symptom)
2. Zeige den relevanten Log-Kontext (±10 Zeilen um den Fehler)
3. Nenne die wahrscheinliche Ursache
4. Schlage einen konkreten Fix vor (Datei + Funktion wenn möglich)

View File

@@ -1,25 +0,0 @@
---
name: KX-Bridge Nightly Prep
description: Bereitet den PR von nightly nach main vor. Prüft ob alle Voraussetzungen für ein Stable Release erfüllt sind.
tools:
- run_command
- read_file
---
Du bereitest einen nightly → main Merge für KX-Bridge vor.
Führe folgende Checks aus und berichte:
1. `git log main..nightly --oneline` → alle Commits die noch nicht in main sind
2. `git diff main..nightly -- CHANGELOG.md` → ist CHANGELOG.md für alle Änderungen aktualisiert?
3. Prüfe ob `tests/` alle geänderten Module abdeckt
4. Prüfe ob Dockerfile ein aktuelles Base-Image verwendet
5. Schlage eine SemVer-Versionsnummer vor:
- `feat:` Commits → MINOR erhöhen
- `fix:` Commits → PATCH erhöhen
- Breaking Change im Commit-Body → MAJOR erhöhen
Abschlussbericht:
- ✅ Bereit für Release
- ⚠️ Offen: [Liste]
- ❌ Blockiert durch: [Grund]

View File

@@ -1,24 +0,0 @@
---
name: KX-Bridge Reviewer
description: Reviewt geänderte Dateien vor einem PR auf nightly. Prüft Logik, Fehlerbehandlung, Moonraker-Kompatibilität und Stil.
tools:
- read_file
- list_directory
- search_files
---
Du bist Code-Reviewer für KX-Bridge — eine Python-Bridge zwischen OrcaSlicer und Moonraker/Klipper für den Anycubic Kobra X.
Beim Review prüfst du:
- Korrekte Fehlerbehandlung bei Moonraker HTTP/MQTT Calls (keine unbehandelten Exceptions)
- Keine hardcodierten IPs oder Ports (müssen aus config.ini kommen)
- Thread-Sicherheit bei parallelen Moonraker-Abfragen (asyncio korrekt verwendet)
- AFC lane_data Struktur: flache Indizierung lane1lane4, kein Slot-Mapping 03
- Kein `print()` statt `logging` (außer in CLI-Hilfsfunktionen)
- Typ-Annotationen vorhanden, Python 3.8+ kompatibel (kein `X | Y` Syntax)
- Tests für neue öffentliche Funktionen vorhanden
Ausgabeformat:
1. **Kritische Fehler** — blockieren den Merge
2. **Warnungen** — sollten vor Merge behoben werden
3. **Hinweise** — optional, für zukünftige Verbesserungen

View File

@@ -1,26 +0,0 @@
---
name: KX-Bridge Test Writer
description: Leitet pytest-Tests aus geänderten oder neuen Python-Dateien ab.
tools:
- read_file
- write_file
- list_directory
- search_files
---
Du schreibst pytest-Tests für KX-Bridge.
Kontext:
- Moonraker API läuft auf Port 7125 (HTTP + WebSocket)
- AFC lane_data: flache Indizierung lane1lane4
- Externe HTTP-Calls zu Moonraker werden mit `unittest.mock` gemockt
- Python 3.8+ Kompatibilität (kein `X | Y` Union-Syntax)
Für jede zu testende Funktion schreibst du:
1. Happy Path (Normalfall mit validen Eingaben)
2. Fehlerfall (Moonraker nicht erreichbar, Timeout, falsche Antwort)
3. Grenzwerte (leere lane_data, ungültige filament_id, None-Werte)
Dateiname: `tests/test_<modulname>.py`
Verwende pytest-Fixtures für Moonraker-Mock-Responses.
Keine echten Netzwerkaufrufe in Tests.

View File

@@ -1,12 +0,0 @@
{
"project": "KX-Bridge",
"language": "de",
"defaultAgent": "reviewer",
"context": {
"repoBase": "gitea.it-drui.de/viewit/KX-Bridge-Release",
"defaultBranch": "nightly",
"stableBranch": "main",
"registry": "gitea.it-drui.de/viewit/kx-bridge",
"moonrakerPort": 7125
}
}

30
.editorconfig Normal file
View File

@@ -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

View File

@@ -1,29 +0,0 @@
---
name: Bug Report
about: Report a bug in KX-Bridge
labels: bug
---
## Description
<!-- What is happening? -->
## Steps to Reproduce
1.
2.
3.
## Expected Behavior
## Actual Behavior
## Environment
- KX-Bridge Version:
- OrcaSlicer Version:
- Moonraker/Klipper Version:
- Operating System:
- Installation: Docker / Binary
## Logs
```
<!-- docker logs kx-bridge --tail 50 -->
```

View File

@@ -1,14 +0,0 @@
---
name: Feature Request
about: Suggest a new feature or improvement
labels: enhancement
---
## Description
<!-- What should be added or improved? -->
## Motivation
<!-- Why is this useful? What problem does it solve? -->
## Proposed Implementation
<!-- Optional: How could this be implemented technically? -->

View File

@@ -1,21 +0,0 @@
## Description
<!-- What does this PR change? -->
## Related Issue
Closes #
## Type
- [ ] Bug fix
- [ ] Feature
- [ ] Documentation
- [ ] Refactoring
## Tested with
- OrcaSlicer Version:
- Printer:
- Moonraker/Klipper Version:
## Checklist
- [ ] Tests added/updated
- [ ] CHANGELOG.md updated
- [ ] No debug code included

View File

@@ -1,132 +0,0 @@
name: Nightly Build
on:
push:
branches:
- nightly
paths:
- '**.py'
- 'Dockerfile'
- 'requirements.txt'
- 'web/**'
- 'data/**'
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
build:
runs-on: server-runner
steps:
- name: Checkout
run: |
if [ -d .git ]; then
git fetch origin nightly
git reset --hard origin/nightly
git clean -fd
else
git clone --depth=1 --branch nightly https://gitea.it-drui.de/viewit/KX-Bridge-Release.git .
fi
- name: Install Docker CLI
run: |
if ! command -v docker >/dev/null 2>&1; then
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
DARCH="x86_64"
BARCH="amd64"
else
DARCH="aarch64"
BARCH="arm64"
fi
wget -qO- "https://download.docker.com/linux/static/stable/${DARCH}/docker-27.5.1.tgz" \
| tar xz --strip-components=1 -C /usr/local/bin docker/docker
chmod +x /usr/local/bin/docker
mkdir -p /usr/local/lib/docker/cli-plugins
wget -qO /usr/local/lib/docker/cli-plugins/docker-buildx \
"https://github.com/docker/buildx/releases/download/v0.23.0/buildx-v0.23.0.linux-${BARCH}"
chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
fi
docker version --format '{{.Client.Version}}'
- name: Set up QEMU
run: |
docker run --rm --privileged tonistiigi/binfmt:latest --install all
- name: Set up buildx
run: |
docker buildx inspect kxbuilder 2>/dev/null || \
docker buildx create --name kxbuilder --use
docker buildx use kxbuilder
- name: Login to Gitea registry
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | \
docker login gitea.it-drui.de -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Build & push (amd64 + arm64)
run: |
VERSION=$(cat VERSION)
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
--provenance=false \
--no-cache \
-t "gitea.it-drui.de/viewit/kx-bridge:nightly" \
-t "gitea.it-drui.de/viewit/kx-bridge:nightly-${VERSION}" \
.
- name: Create Gitea Nightly Release
env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
VERSION=$(cat VERSION)
TAG="nightly-${VERSION}"
git config user.name "gitea-actions"
git config user.email "actions@it-drui.de"
# Changelog aus CHANGES.md lesen (wird von release.sh aus dem Dev-Repo generiert)
BODY_FILE=$(mktemp)
if [ -f CHANGES.md ]; then
cat CHANGES.md > "$BODY_FILE"
else
# Fallback falls CHANGES.md fehlt
printf '## KX-Bridge %s -- Nightly Build\n\n' "$VERSION" > "$BODY_FILE"
printf '[experimentell] Ungetestete Features, nur fuer Tester geeignet.\n\n' >> "$BODY_FILE"
printf '- Automatischer Nightly-Build\n\n---\n\n' >> "$BODY_FILE"
printf '### Docker-Image aktualisieren\n\n```bash\ndocker compose pull && docker compose up -d\n```\n\n' >> "$BODY_FILE"
printf 'Image-Tag: `gitea.it-drui.de/viewit/kx-bridge:nightly`\n' >> "$BODY_FILE"
fi
# Tag setzen
git tag -f "$TAG"
git push https://gitea-actions:${GITEA_TOKEN}@gitea.it-drui.de/viewit/KX-Bridge-Release.git "$TAG" --force
# curl installieren (BusyBox wget kann kein DELETE/POST mit Headers)
if ! command -v curl >/dev/null 2>&1; then
if ! apk add --no-cache curl 2>/dev/null; then
wget -qO /usr/local/bin/curl \
"https://github.com/moparisthebest/static-curl/releases/download/v8.6.0/curl-amd64"
chmod +x /usr/local/bin/curl
fi
fi
# Altes Release loeschen falls vorhanden
curl -s -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases/tags/${TAG}" \
2>/dev/null || true
# Release erstellen (JSON-Body via awk escapen)
BODY_JSON=$(awk '{
gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); gsub(/\t/, "\\t");
printf "%s\\n", $0
}' "$BODY_FILE" | awk 'BEGIN{printf "\""} {printf "%s", $0} END{printf "\""}')
JSON_PAYLOAD="{\"tag_name\":\"${TAG}\",\"name\":\"KX-Bridge ${VERSION} Nightly\",\"body\":${BODY_JSON},\"draft\":false,\"prerelease\":true}"
printf '%s' "$JSON_PAYLOAD" > /tmp/release_body.json
curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary @/tmp/release_body.json \
"https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases"
rm -f "$BODY_FILE" /tmp/release_body.json

View File

@@ -1,34 +0,0 @@
name: PR Check
on:
pull_request:
branches:
- nightly
jobs:
lint-and-test:
runs-on: server-runner
steps:
- name: Checkout
run: |
if [ -d .git ]; then
git fetch origin
git reset --hard origin/nightly
git clean -fd
else
git clone --depth=1 --branch nightly https://gitea.it-drui.de/viewit/KX-Bridge-Release.git .
fi
- name: Dependencies installieren
run: pip3 install -r requirements.txt
- name: Lint
run: |
pip3 install flake8
flake8 *.py --max-line-length=120 --extend-ignore=E501
- name: Tests
run: |
pip3 install pytest
pytest tests/ -v
if: ${{ hashFiles('tests/') != '' }}

View File

@@ -1,74 +0,0 @@
name: Stable Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: server-runner
steps:
- name: Checkout
run: |
TAG="${GITHUB_REF#refs/tags/}"
if [ -d .git ]; then
git fetch --tags origin
git checkout "$TAG"
git clean -fd
else
git clone --depth=1 --branch "$TAG" https://gitea.it-drui.de/viewit/KX-Bridge-Release.git .
fi
- name: Install Docker CLI
run: |
if ! command -v docker >/dev/null 2>&1; then
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
DARCH="x86_64"
BARCH="amd64"
else
DARCH="aarch64"
BARCH="arm64"
fi
wget -qO- "https://download.docker.com/linux/static/stable/${DARCH}/docker-27.5.1.tgz" \
| tar xz --strip-components=1 -C /usr/local/bin docker/docker
chmod +x /usr/local/bin/docker
mkdir -p /usr/local/lib/docker/cli-plugins
wget -qO /usr/local/lib/docker/cli-plugins/docker-buildx \
"https://github.com/docker/buildx/releases/download/v0.23.0/buildx-v0.23.0.linux-${BARCH}"
chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
fi
docker version --format '{{.Client.Version}}'
- name: Set up QEMU
run: |
docker run --rm --privileged tonistiigi/binfmt:latest --install all
- name: Set up buildx
run: |
docker buildx inspect kxbuilder 2>/dev/null || \
docker buildx create --name kxbuilder --use
docker buildx use kxbuilder
- name: Login to Gitea registry
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | \
docker login gitea.it-drui.de -u "${{ secrets.REGISTRY_USER }}" --password-stdin
# Strip fuehrendes 'v' fuer den Image-Tag (VERSION-Datei hat kein 'v').
- name: Build & push (amd64 + arm64)
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
--provenance=false \
--no-cache \
-t "gitea.it-drui.de/viewit/kx-bridge:latest" \
-t "gitea.it-drui.de/viewit/kx-bridge:${VERSION}" \
.
# Hinweis: Das Gitea-Release (inkl. englischem Auto-Changelog + Binaries als
# Assets) erstellt release.sh synchron, da es die lokal via CodeBuilder
# gebauten Binaries direkt hochlaedt. Dieser Workflow baut nur das Docker-Image.

3
.gitignore vendored
View File

@@ -8,6 +8,8 @@ releases/*/kx-bridge
releases/*/extract_credentials
releases/*/extract_credentials.exe
node_modules/*
!kx-bridge.spec
# Laufzeit-Daten und Drucker-Credentials — nie committen
@@ -17,4 +19,3 @@ config/*.ini
data/
!data/orca_filaments.json
.runner-token

View File

@@ -1,33 +0,0 @@
## KX-Bridge 0.9.27-nightly9 — Nightly Build
[experimentell] Ungetestete Features, nur für Tester geeignet.
### Änderungen seit `v0.9.26`
- fix(spoolman): Status-Dot beim Seitenload initialisieren
- chore: CHANGES.md mit echtem Changelog aus Dev-Repo ins Release-Repo schreiben
- fix(spoolman): SPOOLMAN_* env-Cache bei Restart leeren, Status-Dot nach Save aktualisieren
- chore: README.es.md + CONTRIBUTING.md in release.sh-Sync aufnehmen
- docs: Nightly-Sektion, Wartungshinweis + CONTRIBUTING.md, FR/IT-Sprachen, Downloads-Badge
- fix(update): Nightly-Vergleich auf Versions-String umstellen (statt Datum)
- feat(update): Nightly-Track vom Stable-Track trennen
- fix(release): nightly immer auf nightly-Branch pushen, kein master-Push
- fix(camera): exponentielles Backoff bei ffmpeg-Fehler + /api/camera/reset + ↺-Button
- fix(release): Nightly-Release vom nightly-Branch erlauben
- feat(i18n): fehlende UI-Übersetzungen ergänzt + Keys alphabetisch sortiert (PR #70 @fenopy)
- fix: config/config.ini.example beim Release-Sync mitübertragen (Issue #72)
- feat(ui): Integrationen-Tab in Settings (Spoolman + Obico-Hinweis)
- feat(stack): KobraX Full Stack Compose für Portainer (KX-Bridge + Obico + Spoolman)
- feat(release): Nightly/Stable Release-Workflow mit eigenem Docker-Tag
- feat(spoolman): optionale Spoolman-Filamentverbrauch-Integration (PR #65, @p2l)
- fix(release): Artifact-Download per HTTP statt lokalem Dateipfad
---
### Docker-Image aktualisieren
```bash
docker compose pull && docker compose up -d
```
Image-Tag: `gitea.it-drui.de/viewit/kx-bridge:nightly`

View File

@@ -1,102 +0,0 @@
# Contributing to KX-Bridge
Thanks for taking the time to contribute! Here's everything you need to know.
---
## How to report a bug or request a feature
Use the issue tracker:
- **Bug:** [New Bug Report](https://gitea.it-drui.de/viewit/KX-Bridge-Release/issues/new?template=bug_report.md)
- **Feature:** [New Feature Request](https://gitea.it-drui.de/viewit/KX-Bridge-Release/issues/new?template=feature_request.md)
Please fill in the template — especially the **KX-Bridge version** and **logs**.
Issues without version info are hard to debug.
---
## How to submit a Pull Request
### 1. Fork the repository
Click **Fork** at the top of this page.
You now have your own copy at `gitea.it-drui.de/your-username/KX-Bridge-Release`.
### 2. Clone your fork
```bash
git clone https://gitea.it-drui.de/your-username/KX-Bridge-Release.git
cd KX-Bridge-Release
```
### 3. Create a branch
Always branch off `nightly`:
```bash
git checkout nightly
git checkout -b feature/my-feature # or fix/my-fix
```
### 4. Make your changes
- Test your changes locally with Docker:
```bash
docker build -t kx-bridge:dev .
docker run -p 7125:7125 -v ./config:/app/config kx-bridge:dev
```
- No debug `print()` statements — use `logging`
- Keep commits focused; one thing per commit
### 5. Push and open a PR
```bash
git push origin feature/my-feature
```
Gitea will show a banner — click **"Create Pull Request"**.
The PR template will be pre-filled. Set the target branch to **`nightly`**.
---
## Branch model
```
master ← stable releases only (merged by maintainer)
nightly ← integration branch — PRs go here
feature/* ← your feature branch (in your fork)
fix/* ← your bugfix branch (in your fork)
```
Your PR always targets `nightly`. The maintainer periodically merges `nightly → master` for a new stable release.
---
## Commit style
Use conventional commit prefixes:
| Prefix | When |
|---|---|
| `feat:` | new feature |
| `fix:` | bug fix |
| `docs:` | documentation only |
| `chore:` | maintenance, dependencies |
| `refactor:` | code change without new feature or fix |
Example: `fix: prevent crash when printer is offline during startup`
---
## Language
- **Code and comments:** English
- **Issue comments:** match the language of the issue (if someone writes in German, reply in German)
- **Commit messages:** English
---
## Questions?
Open a [Discussion](https://gitea.it-drui.de/viewit/KX-Bridge-Release/issues) or leave a comment on the relevant issue.

View File

@@ -21,7 +21,7 @@ Feedback willkommen.</sub>
&nbsp;
[![Releases](https://img.shields.io/badge/Download-Releases-2EA043?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
&nbsp;
[![Downloads](https://img.shields.io/badge/Downloads-3.1k%2B-8957E5?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
[![Downloads](https://img.shields.io/badge/Downloads-800%2B-8957E5?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
&nbsp;
[![Video](https://img.shields.io/badge/YouTube-Tutorial-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=1Ql4wfH27fM)
@@ -29,11 +29,6 @@ Feedback willkommen.</sub>
</div>
> [!CAUTION]
> **Laufende Wartungsarbeiten** — Wir strukturieren das Repository um (Branch-Modell, CI-Workflows, Beitragsprozess). Es kann zu Änderungen bei Branch-Namen, PR-Templates und der Art der Veröffentlichungen kommen. Wir entschuldigen uns für etwaige Unannehmlichkeiten. Handhabung, Workflow und langfristige Wartbarkeit werden dadurch deutlich verbessert.
>
> 👉 Möchtest du beitragen? Bitte zuerst [CONTRIBUTING.md](CONTRIBUTING.md) lesen.
---
## ✨ Was kann KX-Bridge?
@@ -50,7 +45,7 @@ Feedback willkommen.</sub>
| 🧩 | **Multi-Printer** — mehrere Drucker in **einer** Bridge-Instanz, Umschalten per Dropdown |
| | **Drucker hinzufügen per Klick** — nur die IP eingeben, Zugangsdaten werden automatisch importiert |
| 🔁 | **Robuster MQTT-Reconnect** — Bridge überlebt nächtlichen Drucker-Reboot ohne manuellen Neustart |
| 🌐 | **Mehrsprachiges UI** — DE / EN / ES / FR / IT / 中文, Browser-Sprache automatisch erkannt |
| 🌐 | **Mehrsprachiges UI** — DE / EN / ES / 中文, Browser-Sprache automatisch erkannt |
| 🔄 | **Self-Update** — neue Versionen direkt im Browser installieren |
| 🧠 | **OrcaSlicer** — volles Moonraker-Protokoll (HTTP + WebSocket); für korrekten Vendor-Match pro Slot den [OrcaSlicer-KX-Build](#-empfohlener-slicer) nutzen |
@@ -189,24 +184,6 @@ docker compose up -d --build # lokal selber bauen (statt zu pullen)
---
## 🌙 Nightly-Builds
Nightly-Builds enthalten die neuesten unveröffentlichten Features und werden automatisch bei jedem Entwicklungs-Push gebaut.
Sie können instabil sein — für Tests oder frühen Zugriff auf neue Funktionen geeignet.
```bash
docker compose -f docker-compose.yml -f docker-compose.nightly.yml pull
docker compose -f docker-compose.yml -f docker-compose.nightly.yml up -d
```
Zurück zum stabilen Release:
```bash
docker compose pull && docker compose up -d
```
---
## 🩹 Troubleshooting
<details>

View File

@@ -20,7 +20,7 @@ ninguna está oficialmente probada ni soportada. Se agradece el feedback.</sub>
&nbsp;
[![Releases](https://img.shields.io/badge/Descargar-Lanzamientos-2EA043?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
&nbsp;
[![Downloads](https://img.shields.io/badge/Descargas-3.1k%2B-8957E5?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
[![Downloads](https://img.shields.io/badge/Descargas-800%2B-8957E5?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
&nbsp;
[![Video](https://img.shields.io/badge/YouTube-Tutorial-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=1Ql4wfH27fM)
@@ -28,11 +28,6 @@ ninguna está oficialmente probada ni soportada. Se agradece el feedback.</sub>
</div>
> [!CAUTION]
> **Trabajos de mantenimiento en curso** — Estamos reestructurando el repositorio (modelo de ramas, flujos CI, proceso de contribución). Es posible que notes cambios en los nombres de ramas, plantillas de PR y la forma en que se publican las versiones. Pedimos disculpas por las molestias. El manejo, el flujo de trabajo y la mantenibilidad a largo plazo mejorarán considerablemente.
>
> 👉 ¿Quieres contribuir? Por favor lee [CONTRIBUTING.md](CONTRIBUTING.md) primero.
---
## ✨ Características
@@ -49,7 +44,7 @@ ninguna está oficialmente probada ni soportada. Se agradece el feedback.</sub>
| 🧩 | **Multi-impresora** — múltiples impresoras en **una** instancia del puente, cambia mediante un menú desplegable |
| | **Añade una impresora con un clic** — solo introduce la IP, las credenciales se importan automáticamente |
| 🔁 | **Reconexión MQTT robusta** — el puente sobrevive a reinicios nocturnos de la impresora sin reinicio manual |
| 🌐 | **Interfaz multilingüe** — DE / EN / ES / FR / IT / 中文, detecta automáticamente el idioma del navegador |
| 🌐 | **Interfaz multilingüe** — DE / EN / ES / 中文, detecta automáticamente el idioma del navegador |
| 🔄 | **Actualización automática** — instala nuevas versiones directamente desde el navegador |
| 🧠 | **OrcaSlicer** — protocolo Moonraker completo (HTTP + WebSocket); usa el [build OrcaSlicer-KX](#-slicer-recomendado) para emparejamiento correcto de vendor por ranura |
@@ -71,17 +66,43 @@ docker compose up -d
**Binario Linux (sin Docker):**
```bash
chmod +x kx-bridge && ./kx-bridge
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
```
**EXE Windows (sin Docker):**
```
kx-bridge.exe
```
> `config\` y `data\` se crean junto al EXE — instalación portátil.
> Con los binarios de Linux y Windows, `config/` y `data/` (configuración, SQLite, almacén de GCode)
> viven junto al programa. Copia toda la carpeta para mover la instalación.
> ⚠️ **Certificados TLS necesarios para el binario standalone**
>
> El bridge habla con el MQTT de la impresora vía mTLS y necesita dos
> ficheros de certificado **junto al binario**:
>
> - `anycubic_slicer.crt`
> - `anycubic_slicer.key`
>
> Ambos vienen en **`anycubic-certs.zip`** en la misma página de release.
> Descárgalo y extrae los dos ficheros en el mismo directorio que
> `kx-bridge-linux-amd64` o `kx-bridge.exe`. Sin ellos verás
> `Verbindung fehlgeschlagen: TLS-Zertifikate fehlen …` (0.9.19.1+) o
> `[Errno 2] No such file or directory` (versiones anteriores).
>
> Estructura correcta:
> ```
> ~/kx-bridge/
> ├── kx-bridge-linux-amd64 (o kx-bridge.exe)
> ├── anycubic_slicer.crt ← de anycubic-certs.zip
> ├── anycubic_slicer.key ← de anycubic-certs.zip
> └── config/ (se crea en el primer arranque)
> ```
>
> Los usuarios de Docker no necesitan hacer esto — los certificados
> están incluidos en la imagen.
> Con los binarios de Linux y Windows, `config/` y `data/` (configuración,
> SQLite, almacén de GCode) viven junto al programa. Copia toda la carpeta
> para mover la instalación.
**Python directamente:**
```bash
@@ -187,24 +208,6 @@ docker compose up -d --build # recompilar localmente (en lugar de desc
---
## 🌙 Builds nocturnos (Nightly)
Los builds nocturnos contienen las últimas funciones no publicadas y se generan automáticamente en cada push de desarrollo.
Pueden ser inestables — úsalos para pruebas o acceso anticipado a nuevas funciones.
```bash
docker compose -f docker-compose.yml -f docker-compose.nightly.yml pull
docker compose -f docker-compose.yml -f docker-compose.nightly.yml up -d
```
Volver a la versión estable:
```bash
docker compose pull && docker compose up -d
```
---
## 🩹 Solución de problemas
<details>

View File

@@ -20,7 +20,7 @@ officially tested or supported. Feedback welcome.</sub>
&nbsp;
[![Releases](https://img.shields.io/badge/Download-Releases-2EA043?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
&nbsp;
[![Downloads](https://img.shields.io/badge/Downloads-3.1k%2B-8957E5?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
[![Downloads](https://img.shields.io/badge/Downloads-800%2B-8957E5?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
&nbsp;
[![Video](https://img.shields.io/badge/YouTube-Tutorial-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=1Ql4wfH27fM)
@@ -28,11 +28,6 @@ officially tested or supported. Feedback welcome.</sub>
</div>
> [!CAUTION]
> **Ongoing maintenance work** — We are restructuring the repository (branch model, CI workflows, contribution process). You may notice changes to branch names, PR templates, and how releases are published. We apologise for any inconvenience. Handling, workflow, and long-term maintainability will be significantly improved as a result.
>
> 👉 Want to contribute? Please read [CONTRIBUTING.md](CONTRIBUTING.md) first.
---
## ✨ Features
@@ -49,7 +44,7 @@ officially tested or supported. Feedback welcome.</sub>
| 🧩 | **Multi-printer** — multiple printers in **one** bridge instance, switch via dropdown |
| | **Add a printer with one click** — just enter the IP, credentials are imported automatically |
| 🔁 | **Robust MQTT reconnect** — bridge survives overnight printer reboots without manual restart |
| 🌐 | **Multi-language UI** — DE / EN / ES / FR / IT / 中文, auto-detect browser locale |
| 🌐 | **Multi-language UI** — DE / EN / ES / 中文, auto-detect browser locale |
| 🔄 | **Self-update** — install new versions directly in the browser |
| 🧠 | **OrcaSlicer** — full Moonraker protocol (HTTP + WebSocket); pair with the [OrcaSlicer-KX build](#-recommended-slicer) for proper per-slot vendor matching |
@@ -187,24 +182,6 @@ docker compose up -d --build # rebuild locally (instead of pulling)
---
## 🌙 Nightly Builds
Nightly builds contain the latest unreleased features and are built automatically on every development push.
They may be unstable — use them for testing or early access to new functionality.
```bash
docker compose -f docker-compose.yml -f docker-compose.nightly.yml pull
docker compose -f docker-compose.yml -f docker-compose.nightly.yml up -d
```
To go back to the stable release:
```bash
docker compose pull && docker compose up -d
```
---
## 🩹 Troubleshooting
<details>

View File

@@ -1 +1 @@
0.9.27-nightly9
0.9.27-nightly1

View File

@@ -1,31 +0,0 @@
# KX-Bridge Claude Agents
## Available Agents
| Agent | File | When to use |
|---|---|---|
| Reviewer | `.claude/agents/reviewer.md` | Before every PR — checks logic, error handling, Moonraker compatibility |
| Changelog | `.claude/agents/changelog.md` | After merge to nightly — generates CHANGELOG.md entry from commits |
| Test Writer | `.claude/agents/test-writer.md` | When adding new functions — derives pytest tests |
| Nightly Prep | `.claude/agents/nightly-prep.md` | Before a release — checks readiness of nightly → main merge |
| Docker Check | `.claude/agents/docker-check.md` | Before image push — validates Dockerfile and compose config |
| Moonraker Debug | `.claude/agents/moonraker-debug.md` | On runtime errors — analyzes Moonraker/Klipper logs |
## Usage
In VS Code with Claude Code extension:
```
@reviewer → code review of current changes
@changelog → generate CHANGELOG entry
@test-writer → write tests for changed files
@nightly-prep → check release readiness
@docker-check → validate Docker config
@moonraker-debug → analyze logs
```
## Context
- Moonraker API: Port 7125
- AFC lane_data: flat indexing lane1lane4
- Registry: `gitea.it-drui.de/viewit/kx-bridge`
- Default PR target: `nightly`

View File

@@ -31,88 +31,6 @@ default_ams_slot = auto
# Auto-Leveling vor jedem Druck (1 = an, 0 = aus)
auto_leveling = 1
# Kamera-Stream bei Druckstart automatisch einschalten (1 = an, 0 = aus)
camera_on_print = 0
# Warnung vor Druck von Web-Uploads (1 = an, 0 = aus)
web_upload_warning = 1
# Nach Upload: Filament/Color-Selector automatisch öffnen (1 = an, 0 = aus)
print_start_dialog = 1
# ─── Filament-Profile pro AMS-Slot (optional) ────────────────────────────────
# Beim Slicer-Sync nimmt OrcaSlicer per Default immer "Generic PLA/PETG/...".
# Mit diesen Mappings sendet die Bridge die konkrete Orca-Filament-ID +
# Vendor mit (Anzeige im Slicer dann z.B. "PolyTerra PLA — Polymaker" statt
# nur "Generic PLA"). Mapping wird über die Web-UI gepflegt.
# Beispiel:
# [filament_profiles]
# slot_0_id = OGFL01
# slot_0_vendor = Polymaker
# slot_1_id = OGFG23
# slot_1_vendor = Polymaker
[bridge]
# Poll-Intervall in Sekunden
poll_interval = 3
# ─── Multi-Printer (optional) ──────────────────────────────────────────────────
# Mehrere Drucker können als [printer_1], [printer_2], … definiert werden.
# Jede Bridge-Instanz verbindet sich mit einem Drucker (je eigener Port).
# bridge_url zeigt auf die jeweilige Bridge-Instanz (für den /kx/printers-Endpunkt).
# Die [connection]-Sektion wird weiterhin als Fallback für diese Instanz verwendet.
#
# Beispiel:
# [printer_1]
# name = Kobra X Links
# bridge_url = http://192.168.178.95:7125
# printer_ip = 192.168.178.95
# mqtt_port = 9883
# username = userXXXXXXXXXX
# password = XXXXXXXXXXXXXXX
# device_id = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# mode_id = 20030
#
# [printer_2]
# name = Kobra X Rechts
# bridge_url = http://192.168.178.96:7125
# printer_ip = 192.168.178.96
# mqtt_port = 9883
# username = userYYYYYYYYYY
# password = YYYYYYYYYYYYYYY
# device_id = yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
# mode_id = 20030
[ace_dry_presets]
# Vordefinierte Dry-Set Presets (Temp in °C, Dauer in Sekunden)
pla_temp = 45
pla_duration_sec = 14400
pla_plus_temp = 45
pla_plus_duration_sec = 14400
petg_temp = 50
petg_duration_sec = 14400
tpu_temp = 55
tpu_duration_sec = 14400
abs_asa_temp = 45
abs_asa_duration_sec = 28800
pa_pc_temp = 55
pa_pc_duration_sec = 43200
# Custom Presets (Name + Temp + Dauer)
custom_1_name = Custom 1
custom_1_temp = 45
custom_1_duration_sec = 14400
custom_2_name = Custom 2
custom_2_temp = 45
custom_2_duration_sec = 14400
custom_3_name = Custom 3
custom_3_temp = 45
custom_3_duration_sec = 14400
[spoolman]
# URL der Spoolman-Instanz (leer lassen um Spoolman zu deaktivieren)
# server = http://192.168.x.x:7912
# Wie oft (Sekunden) der Filamentverbrauch während des Drucks gemeldet wird
# (0 = nur beim Druckende)
# sync_rate = 0

View File

@@ -1,210 +0,0 @@
# KobraX Full Stack — KX-Bridge + Obico Self-Hosted + Spoolman
#
# Für Portainer: Stack → Add Stack → Upload → diese Datei wählen
#
# Voraussetzung: Obico-Images einmalig in Gitea-Registry pushen:
# docker tag obico-server-web:latest gitea.it-drui.de/viewit/obico-web:latest
# docker tag obico-server-ml_api:latest gitea.it-drui.de/viewit/obico-ml:latest
# docker tag obico-server-tasks:latest gitea.it-drui.de/viewit/obico-tasks:latest
# docker push gitea.it-drui.de/viewit/obico-web:latest
# docker push gitea.it-drui.de/viewit/obico-ml:latest
# docker push gitea.it-drui.de/viewit/obico-tasks:latest
#
# Persistente Daten: /mnt/dockerdata/KobraXStack/<service>/
#
# Ports:
# 7125 — KX-Bridge (Moonraker-API)
# 3334 — Obico (Web-UI)
# 7912 — Spoolman (Web-UI)
#
# Obico Admin-Account nach dem ersten Start:
# docker exec obico-web python manage.py createsuperuser
x-obico-base: &obico-base
restart: unless-stopped
volumes:
- /mnt/dockerdata/KobraXStack/obico/data:/data
- /mnt/dockerdata/KobraXStack/obico/frontend:/frontend
depends_on:
- obico-redis
environment:
DEBUG: "False"
REDIS_URL: "redis://obico-redis:6379"
DATABASE_URL: "sqlite:////data/db.sqlite3"
INTERNAL_MEDIA_HOST: "http://obico-web:3334"
ML_API_HOST: "http://obico-ml:3333"
ACCOUNT_ALLOW_SIGN_UP: "False"
SITE_USES_HTTPS: "False"
SITE_IS_PUBLIC: "False"
DJANGO_SECRET_KEY: "change-me-to-a-random-secret-key-before-use"
WEBPACK_LOADER_ENABLED: "False"
networks:
- kobrax-stack
services:
# ── KX-Bridge ───────────────────────────────────────────────
kx-bridge:
image: gitea.it-drui.de/viewit/kx-bridge:latest
container_name: kx-bridge
restart: unless-stopped
ports:
- "7125:7125"
volumes:
- /mnt/dockerdata/KobraXStack/kx-bridge/config:/app/config
- /mnt/dockerdata/KobraXStack/kx-bridge/data:/app/data
networks:
- kobrax-stack
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# ── Spoolman ────────────────────────────────────────────────
spoolman:
image: ghcr.io/donkie/spoolman:latest
container_name: spoolman
restart: unless-stopped
ports:
- "7912:8000"
volumes:
- /mnt/dockerdata/KobraXStack/spoolman:/home/app/.local/share/spoolman
networks:
- kobrax-stack
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# ── Obico Redis ─────────────────────────────────────────────
obico-redis:
image: redis:7.2-alpine
container_name: obico-redis
restart: unless-stopped
volumes:
- /mnt/dockerdata/KobraXStack/obico/redis:/data
networks:
- kobrax-stack
healthcheck:
test: ["CMD", "redis-cli", "ping"]
start_period: 10s
interval: 15s
timeout: 5s
retries: 10
logging:
driver: json-file
options:
max-size: "5m"
max-file: "2"
# ── Obico ML API ────────────────────────────────────────────
obico-ml:
image: gitea.it-drui.de/viewit/obico-ml:latest
container_name: obico-ml
restart: unless-stopped
command: bash -c "gunicorn --bind 0.0.0.0:3333 --workers 1 wsgi"
working_dir: /app
environment:
DEBUG: "False"
FLASK_APP: "server.py"
networks:
- kobrax-stack
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3333/hc/ || exit 1"]
start_period: 30s
interval: 30s
timeout: 10s
retries: 3
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# ── Obico Web ───────────────────────────────────────────────
obico-web:
<<: *obico-base
image: gitea.it-drui.de/viewit/obico-web:latest
container_name: obico-web
ports:
- "3334:3334"
depends_on:
- obico-ml
- obico-redis
command: >
sh -c 'python manage.py migrate &&
python manage.py shell -c "from django.contrib.sites.models import Site; s=Site.objects.first(); s.domain=\"192.168.178.204:3334\"; s.name=\"Obico\"; s.save()" &&
python manage.py collectstatic --noinput &&
daphne -b 0.0.0.0 -p 3334 config.routing:application'
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3334/hc/ || exit 1"]
start_period: 60s
interval: 90s
timeout: 20s
retries: 3
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# ── Obico Tasks (Celery) ────────────────────────────────────
obico-tasks:
<<: *obico-base
image: gitea.it-drui.de/viewit/obico-tasks:latest
container_name: obico-tasks
command: sh -c "celery -A config worker --beat -l info -c 2 -Q realtime,celery"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# ── moonraker-obico Plugin ──────────────────────────────────
# Verbindet KX-Bridge mit dem Obico-Server (Spaghetti-Detektion, Remote-UI)
# Voraussetzung: /mnt/dockerdata/KobraXStack/moonraker-obico/moonraker-obico.cfg
# muss existieren und einen gültigen auth_token enthalten.
#
# Token holen (nach erstem obico-web Start):
# docker exec obico-web python manage.py shell -c "
# from app.models import OneTimeVerificationCode, User
# from django.utils import timezone; from datetime import timedelta; import random
# u = User.objects.first()
# c = OneTimeVerificationCode.objects.create(user=u, code='%06d' % random.randint(100000,999999), expired_at=timezone.now()+timedelta(hours=2))
# print('CODE:', c.code)"
# curl -X POST 'http://localhost:3334/api/v1/octo/verify/?code=<CODE>'
# → printer.auth_token aus der Antwort in die cfg eintragen
moonraker-obico:
image: gitea.it-drui.de/viewit/moonraker-obico:latest
container_name: moonraker-obico
restart: unless-stopped
network_mode: host
volumes:
- /mnt/dockerdata/KobraXStack/moonraker-obico:/opt/printer_data/config
- /mnt/dockerdata/KobraXStack/moonraker-obico/logs:/opt/printer_data/logs
command: ["-c", "/opt/printer_data/config/moonraker-obico.cfg"]
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
kobrax-stack:
driver: bridge
# Verzeichnisse müssen auf dem Host existieren:
# mkdir -p /mnt/dockerdata/KobraXStack/kx-bridge/config \
# /mnt/dockerdata/KobraXStack/kx-bridge/data \
# /mnt/dockerdata/KobraXStack/spoolman \
# /mnt/dockerdata/KobraXStack/obico/data \
# /mnt/dockerdata/KobraXStack/obico/frontend \
# /mnt/dockerdata/KobraXStack/obico/redis \
# /mnt/dockerdata/KobraXStack/moonraker-obico/logs
# Spoolman benötigt UID/GID 1000:
# sudo chown -R 1000:1000 /mnt/dockerdata/KobraXStack/spoolman
#
# moonraker-obico Config anlegen (auth_token nach Obico-Setup eintragen):
# cp /path/to/moonraker-obico.cfg.example /mnt/dockerdata/KobraXStack/moonraker-obico/moonraker-obico.cfg

View File

@@ -602,23 +602,10 @@ class CameraCache:
self._task_jpeg: "asyncio.Task | None" = None
self._task_h264: "asyncio.Task | None" = None
self._lock = asyncio.Lock()
self._fail_count_jpeg: int = 0
self._fail_count_h264: int = 0
def set_url(self, url: str):
self._url = url
def reset(self):
"""Backoff-Zähler zurücksetzen und laufende ffmpeg-Prozesse killen."""
self._fail_count_jpeg = 0
self._fail_count_h264 = 0
for proc in (self._proc_jpeg, self._proc_h264):
if proc is not None:
try:
proc.kill()
except Exception:
pass
async def ensure_running(self):
if self._proc_jpeg is None or self._proc_jpeg.returncode is not None:
self._task_jpeg = asyncio.create_task(self._run_jpeg_loop())
@@ -642,13 +629,13 @@ class CameraCache:
continue
try:
self._proc_jpeg = await asyncio.create_subprocess_exec(
_find_ffmpeg(), "-loglevel", "warning",
_find_ffmpeg(), "-loglevel", "quiet",
*self._input_args(url), "-i", url,
"-vf", "fps=2",
"-f", "image2pipe", "-vcodec", "mjpeg", "-q:v", "3",
"-flush_packets", "1", "pipe:1",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)
except Exception as e:
log.warning(f"CameraCache: ffmpeg-jpeg start fehlgeschlagen: {e}")
@@ -656,7 +643,6 @@ class CameraCache:
continue
buf = b""
rc = None
try:
while True:
chunk = await self._proc_jpeg.stdout.read(self.TS_CHUNK)
@@ -690,23 +676,8 @@ class CameraCache:
await self._proc_jpeg.wait()
except Exception:
pass
rc = self._proc_jpeg.returncode
if rc:
try:
err = await self._proc_jpeg.stderr.read(500)
if err:
log.warning(f"CameraCache: ffmpeg-jpeg stderr: {err.decode(errors='replace').strip()}")
except Exception:
pass
self._proc_jpeg = None
if rc:
self._fail_count_jpeg += 1
delay = min(2.0 * (2 ** self._fail_count_jpeg), 300.0)
log.warning(f"CameraCache: ffmpeg-jpeg exit {rc}, retry in {delay:.0f}s (Versuch {self._fail_count_jpeg})")
await asyncio.sleep(delay)
else:
self._fail_count_jpeg = 0
await asyncio.sleep(2.0)
await asyncio.sleep(2.0) # restart delay
async def _run_h264_loop(self):
"""Hält einen ffmpeg-Prozess am Leben der MPEG-TS an alle Subscriber fanoutet."""
@@ -717,19 +688,18 @@ class CameraCache:
continue
try:
self._proc_h264 = await asyncio.create_subprocess_exec(
_find_ffmpeg(), "-loglevel", "warning",
_find_ffmpeg(), "-loglevel", "quiet",
*self._input_args(url), "-i", url,
"-c:v", "copy", "-an",
"-f", "mpegts", "pipe:1",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)
except Exception as e:
log.warning(f"CameraCache: ffmpeg-h264 start fehlgeschlagen: {e}")
await asyncio.sleep(3.0)
continue
rc = None
try:
while True:
chunk = await self._proc_h264.stdout.read(self.TS_CHUNK)
@@ -759,23 +729,8 @@ class CameraCache:
await self._proc_h264.wait()
except Exception:
pass
rc = self._proc_h264.returncode
if rc:
try:
err = await self._proc_h264.stderr.read(500)
if err:
log.warning(f"CameraCache: ffmpeg-h264 stderr: {err.decode(errors='replace').strip()}")
except Exception:
pass
self._proc_h264 = None
if rc:
self._fail_count_h264 += 1
delay = min(2.0 * (2 ** self._fail_count_h264), 300.0)
log.warning(f"CameraCache: ffmpeg-h264 exit {rc}, retry in {delay:.0f}s (Versuch {self._fail_count_h264})")
await asyncio.sleep(delay)
else:
self._fail_count_h264 = 0
await asyncio.sleep(2.0)
await asyncio.sleep(2.0)
class SpoolmanClient:
@@ -3908,16 +3863,6 @@ class KobraXBridge:
self._camera_user_stopped = True
return web.json_response({"result": "ok"})
async def handle_api_camera_reset(self, request):
"""Backoff-Zähler zurücksetzen und ffmpeg sofort neu starten.
Nützlich nach 429-Sperre (Retry-After abgelaufen) oder nach Drucker-Neustart."""
self.camera_cache.reset()
url = self._state.get("camera_url", "")
if url:
self.camera_cache.set_url(url)
await self.camera_cache.ensure_running()
return web.json_response({"result": "ok"})
async def handle_api_camera_snapshot(self, request):
"""Letzter JPEG-Frame aus dem CameraCache — instant aus dem RAM,
keine eigene ffmpeg-Instanz mehr (verhindert Single-Client-429 am
@@ -4273,8 +4218,6 @@ class KobraXBridge:
"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,
"spoolman_server": getattr(self._args, "spoolman_server", "") or "",
"spoolman_sync_rate": getattr(self._args, "spoolman_sync_rate", 0),
})
async def handle_api_settings_post(self, request):
@@ -4289,7 +4232,7 @@ class KobraXBridge:
cfg.read(config_path, encoding="utf-8")
# Sections sicherstellen
for section in ("connection", "print", "bridge", "ace_dry_presets", "spoolman"):
for section in ("connection", "print", "bridge", "ace_dry_presets"):
if not cfg.has_section(section):
cfg.add_section(section)
@@ -4319,16 +4262,6 @@ class KobraXBridge:
elif cfg.has_option("bridge", "printer_name"):
cfg.remove_option("bridge", "printer_name")
# Spoolman
if "spoolman_server" in data:
cfg.set("spoolman", "server", str(data["spoolman_server"]).strip())
if "spoolman_sync_rate" in data:
try:
sr = max(0, int(data["spoolman_sync_rate"]))
except (TypeError, ValueError):
sr = 30
cfg.set("spoolman", "sync_rate", str(sr))
incoming_presets = data.get("ace_dry_presets") if isinstance(data, dict) else None
presets = self._sanitize_ace_dry_presets(incoming_presets if isinstance(incoming_presets, dict) else self._ace_dry_presets)
for key, val in presets.items():
@@ -4488,8 +4421,7 @@ class KobraXBridge:
for _k in ("PRINTER_IP", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD",
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
"CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "PRINT_START_DIALOG",
"FILE_READY_DIALOG", "BRIDGE_PRINTER_NAME",
"SPOOLMAN_SERVER", "SPOOLMAN_SYNC_RATE"):
"FILE_READY_DIALOG", "BRIDGE_PRINTER_NAME"):
os.environ.pop(_k, None)
in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER")
@@ -4525,10 +4457,9 @@ class KobraXBridge:
# ─── Update ──────────────────────────────────────────────────────────────
STABLE_RELEASE_API = "https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases?limit=1"
NIGHTLY_RELEASE_API = "https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases?limit=5&pre-release=true"
DEV_RELEASE_API = "https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases?limit=10&pre-release=true"
GITEA_RAW_BASE = "https://gitea.it-drui.de/viewit/KX-Bridge-Release/raw/tag"
STABLE_RELEASE_API = "https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases?limit=1"
DEV_RELEASE_API = "https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases?limit=10&pre-release=true"
GITEA_RAW_BASE = "https://gitea.it-drui.de/viewit/KX-Bridge-Release/raw/tag"
def _read_version(self) -> str:
# PyInstaller-Onefile entpackt VERSION (per kx-bridge.spec datas) nach
@@ -4603,14 +4534,8 @@ class KobraXBridge:
async def handle_api_update_check(self, request):
current = self._read_version()
is_nightly = "nightly" in current
is_dev = "-dev+" in current
if is_nightly:
api_url = self.NIGHTLY_RELEASE_API
elif is_dev:
api_url = self.DEV_RELEASE_API
else:
api_url = self.STABLE_RELEASE_API
api_url = self.DEV_RELEASE_API if is_dev else self.STABLE_RELEASE_API
try:
async with aiohttp.ClientSession() as session:
async with session.get(api_url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
@@ -4619,37 +4544,14 @@ class KobraXBridge:
releases = await resp.json(content_type=None)
if not releases:
return web.json_response({"error": "Keine Releases gefunden"}, status=404)
if is_nightly:
# Neuestes Prerelease mit nightly-Tag suchen
nightly_releases = [r for r in releases if r.get("prerelease") and "nightly" in r.get("tag_name", "")]
if not nightly_releases:
return web.json_response({"error": "Keine Nightly-Releases gefunden"}, status=404)
data = nightly_releases[0]
tag = data.get("tag_name", "")
# Tag-Format: "nightly-0.9.27-nightly4", current: "0.9.27-nightly4"
tag_version = tag[len("nightly-"):] if tag.startswith("nightly-") else tag
update_available = tag_version != current
latest = tag
return web.json_response({
"current": current,
"latest": latest,
"update_available": update_available,
"tag": tag,
"docker_only": True,
"changelog": data.get("body", ""),
})
elif is_dev:
# Dev: neuestes Release mit "-dev+" im Tag suchen
if is_dev:
dev_releases = [r for r in releases if "-dev+" in r.get("tag_name", "")]
if not dev_releases:
return web.json_response({"error": "Keine Dev-Releases gefunden"}, status=404)
data = dev_releases[0]
else:
# Stable: nur non-prerelease nehmen
stable_releases = [r for r in releases if not r.get("prerelease")]
if not stable_releases:
return web.json_response({"error": "Keine Stable-Releases gefunden"}, status=404)
data = stable_releases[0]
data = releases[0]
tag = data.get("tag_name", "")
latest = tag.lstrip("v")
if is_dev:
@@ -4663,7 +4565,6 @@ class KobraXBridge:
"update_available": update_available,
"tag": tag,
"download_url": download_url,
"docker_only": False,
"changelog": data.get("body", ""),
})
except Exception as e:
@@ -4684,10 +4585,6 @@ class KobraXBridge:
async def handle_api_update_apply(self, request):
data = await request.json()
new_tag = data.get("tag", "")
if "nightly" in self._read_version():
return web.json_response(
{"error": "Nightly-Updates laufen über Docker: "
"docker compose pull && docker compose up -d"}, status=400)
if getattr(sys, "frozen", False):
return web.json_response(
{"error": "Self-Update wird im Binary-Modus nicht unterstützt "
@@ -5223,7 +5120,6 @@ def build_app(bridge: KobraXBridge) -> web.Application:
r.add_get("/api/camera/snapshot", bridge.handle_api_camera_snapshot)
r.add_post("/api/camera/start", bridge.handle_api_camera_start)
r.add_post("/api/camera/stop", bridge.handle_api_camera_stop)
r.add_post("/api/camera/reset", bridge.handle_api_camera_reset)
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)

View File

@@ -1,18 +0,0 @@
[server]
url = http://127.0.0.1:3334
auth_token = REPLACE_ME
sentry_opt = out
[moonraker]
host = 127.0.0.1
port = 7125
[webcam]
disable_video_streaming = False
snapshot_url = http://127.0.0.1:7125/api/camera/snapshot
stream_url = http://127.0.0.1:7125/api/camera/stream
target_fps = 5
[logging]
path = /opt/printer_data/logs/moonraker-obico.log
level = INFO

31
package-lock.json generated Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "kx-bridge",
"version": "0.9.27",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kx-bridge",
"version": "0.9.27",
"devDependencies": {
"prettier": "^3.0.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"
}
}
}
}

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "kx-bridge",
"version": "0.9.27",
"description": "Moonraker-compatible bridge for Anycubic Kobra X",
"type": "module",
"devDependencies": {
"prettier": "^3.0.0"
},
"scripts": {
"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"
},
"prettier": {
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}
}

View File

@@ -1,21 +0,0 @@
## Description
<!-- What does this PR change? -->
## Related Issue
Closes #
## Type
- [ ] Bug fix
- [ ] Feature
- [ ] Documentation
- [ ] Refactoring
## Tested with
- OrcaSlicer Version:
- Printer:
- Moonraker/Klipper Version:
## Checklist
- [ ] Tests added/updated
- [ ] CHANGELOG.md updated
- [ ] No debug code included

2
requirements-dev.txt Normal file
View File

@@ -0,0 +1,2 @@
# Code formatting
black>=24.1.0

View File

@@ -53,19 +53,8 @@ function _loadSpoolmanStatus(){
fetch(_apiUrl('/kx/spoolman/status')).then(function(r){return r.json();}).then(function(d){
_spoolmanStatus=d;
_slotSpoolMap=d.slot_spools||{};
_updateSpoolmanStatusDot();
}).catch(function(){});
}
function _updateSpoolmanStatusDot(){
var dot=document.getElementById('spoolman-status-dot');
var lbl=document.getElementById('spoolman-status-lbl');
if(!dot||!lbl)return;
if(_spoolmanStatus.configured){
dot.style.color='var(--ok)';lbl.textContent=_spoolmanStatus.server||'verbunden';
} else {
dot.style.color='var(--txt2)';lbl.textContent='nicht konfiguriert';
}
}
function _buildSpoolmanSection(){
var sec=document.getElementById('fd-spoolman-section');
@@ -396,11 +385,6 @@ function applyLang(){
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');
@@ -550,7 +534,6 @@ function ensureAceDryCards(){
// defer until DOM ready
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();}
@@ -1075,10 +1058,6 @@ function openSettings(){
pi.value=sec;
}
renderFilamentMapping(d.filament_profiles||{});
// 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');
@@ -1647,15 +1626,12 @@ function saveSettings(){
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)),
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){
@@ -1663,33 +1639,21 @@ function saveSettings(){
clog('Settings-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;
_updateTag='';_updateUrl='';
fetch(_apiUrl('/api/update/check')).then(function(r){return r.json()}).then(function(d){
if(d.error){sb.textContent=T.update_error+': '+d.error;return;}
var cl=document.getElementById('update-changelog');
if(d.changelog&&d.changelog.trim()){
// 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.changelog&&d.changelog.trim()){cl.textContent=d.changelog;cl.style.display='block';}
else{cl.style.display='none';}
if(d.update_available){
sb.textContent=d.latest+' '+T.update_available;
sb.textContent='v'+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.style.display='inline-block';
_updateTag=d.tag;_updateUrl=d.download_url;
document.getElementById('btn-update-apply').style.display='inline-block';
} else {
sb.textContent=T.update_none;
sb.style.color='var(--txt2)';
@@ -1697,21 +1661,9 @@ function checkUpdate(){
}).catch(function(e){sb.textContent=T.update_error+': '+e;});
}
function applyUpdate(){
if(!_updateUrl)return;
var sb=document.getElementById('update-status');
var btn=document.getElementById('btn-update-apply');
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;
});
} else {
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;
@@ -1897,7 +1849,6 @@ function camStart(){
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)){
@@ -1932,7 +1883,6 @@ function camStop(){
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');
}
@@ -2183,13 +2133,6 @@ function aceDryToggle(aceId,on){
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 aceDryStop(aceId){
aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0;
return post('/api/ace/dry',{action:'stop',ace_id:aceId})

View File

@@ -157,7 +157,6 @@
<div style="font-size:12px;color:#fff" id="cam-fname"></div>
</div>
<button class="cam-toggle" onclick="toggleCam()" id="cam-toggle-btn">▶ Kamera</button>
<button class="cam-toggle" onclick="resetCamera()" id="cam-reset-btn" style="display:none;margin-left:4px" title="Kamera-Stream neu verbinden (nach 429-Sperre)"></button>
</div>
</div>
@@ -436,7 +435,6 @@
<button class="set-cat" id="setcat-printer" onclick="showSettingsCat('printer')"><span>🖨</span> <span id="setcat-lbl-printer">Drucker</span></button>
<button class="set-cat" id="setcat-display" onclick="showSettingsCat('display')"><span>🎨</span> <span id="setcat-lbl-display">Darstellung</span></button>
<button class="set-cat" id="setcat-filament" onclick="showSettingsCat('filament')"><span>🧵</span> <span id="setcat-lbl-filament">Filament</span></button>
<button class="set-cat" id="setcat-integrations" onclick="showSettingsCat('integrations')"><span></span> <span id="setcat-lbl-integrations">Integrationen</span></button>
<button class="set-cat" id="setcat-system" onclick="showSettingsCat('system')"><span></span> <span id="setcat-lbl-system">System</span></button>
</div>
@@ -572,33 +570,6 @@
</div>
</div>
<!-- Integrationen -->
<div class="set-group" id="setgrp-integrations">
<!-- Spoolman -->
<div class="card">
<div class="card-title"><span>🧵</span> <span id="modal-sec-spoolman">Spoolman</span></div>
<div class="set-row">
<label id="lbl-spoolman-url">Server-URL</label>
<input type="text" id="s-spoolman-url" placeholder="http://spoolman:7912" style="width:200px">
</div>
<div class="set-row">
<label id="lbl-spoolman-sync-rate">Sync-Rate (s, 0=aus)</label>
<input type="number" id="s-spoolman-sync-rate" min="0" max="3600" value="30" style="width:80px">
</div>
<div id="spoolman-status-row" style="margin-top:6px;font-size:12px;color:var(--txt2)">
<span id="spoolman-status-dot"></span> <span id="spoolman-status-lbl"></span>
</div>
</div>
<!-- Obico -->
<div class="card" style="margin-top:10px">
<div class="card-title"><span>🕵</span> <span id="modal-sec-obico">Obico</span></div>
<div style="font-size:12px;color:var(--txt2);line-height:1.6" id="obico-info-box">
Obico wird über den <code>moonraker-obico</code>-Container konfiguriert.<br>
Config-Datei: <code id="obico-cfg-path">/mnt/dockerdata/KobraXStack/moonraker-obico/moonraker-obico.cfg</code>
</div>
</div>
</div>
<!-- System -->
<div class="set-group" id="setgrp-system">
<div class="card">

View File

@@ -123,8 +123,6 @@
"lbl_light": "💡 Licht",
"lbl_remaining": "Restzeit:",
"lbl_slicer_time": "Slicer-Schätzung:",
"lbl_spoolman_sync_rate": "Sync-Rate (s, 0=aus)",
"lbl_spoolman_url": "Server-URL",
"lbl_unload": "Ausziehen",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ Auto",
@@ -158,8 +156,6 @@
"log_topic_label": "Thema:",
"log_topic_print": "Druck",
"log_topic_status": "Status",
"modal_sec_obico": "Obico",
"modal_sec_spoolman": "Spoolman",
"nav_ams": "AMS",
"nav_browser": "Browser",
"nav_console": "Konsole",
@@ -235,7 +231,6 @@
"settings_file_ready_banner": "Druckleiste",
"settings_file_ready_dialog": "Druckdialog",
"settings_file_ready_mode": "Nach Upload: Druckstart-Verhalten",
"settings_integrations": "Integrationen",
"settings_language": "Sprache",
"settings_mode_id": "Mode-ID",
"settings_mode_id_placeholder": "20030",
@@ -321,8 +316,6 @@
"update_available": "verfügbar",
"update_check": "Auf Updates prüfen",
"update_checking": "Prüfe...",
"update_docker": "Befehl kopieren",
"update_docker_copied": "Kopiert! Ausführen: docker compose pull && docker compose up -d",
"update_error": "Fehler",
"update_none": "Bereits aktuell",
"update_restarting": "Starte neu..."

View File

@@ -123,8 +123,6 @@
"lbl_light": "💡 Light",
"lbl_remaining": "Remaining:",
"lbl_slicer_time": "Slicer estimate:",
"lbl_spoolman_sync_rate": "Sync rate (s, 0=off)",
"lbl_spoolman_url": "Server URL",
"lbl_unload": "Unload",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ Auto",
@@ -158,8 +156,6 @@
"log_topic_label": "Topic:",
"log_topic_print": "Print",
"log_topic_status": "Status",
"modal_sec_obico": "Obico",
"modal_sec_spoolman": "Spoolman",
"nav_ams": "AMS",
"nav_browser": "Browser",
"nav_console": "Console",
@@ -235,7 +231,6 @@
"settings_file_ready_banner": "Print bar",
"settings_file_ready_dialog": "Print dialog",
"settings_file_ready_mode": "After upload: Start print behavior",
"settings_integrations": "Integrations",
"settings_language": "Language",
"settings_mode_id": "Mode ID",
"settings_mode_id_placeholder": "20030",
@@ -321,8 +316,6 @@
"update_available": "available",
"update_check": "Check for Updates",
"update_checking": "Checking...",
"update_docker": "Copy Command",
"update_docker_copied": "Copied! Run: docker compose pull && docker compose up -d",
"update_error": "Error",
"update_none": "Already up to date",
"update_restarting": "Restarting..."

View File

@@ -123,8 +123,6 @@
"lbl_light": "💡 Luz",
"lbl_remaining": "Restante:",
"lbl_slicer_time": "Estimación del slicer:",
"lbl_spoolman_sync_rate": "Tasa de sincronización (s, 0=desact.)",
"lbl_spoolman_url": "URL del servidor",
"lbl_unload": "Descargar",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ Auto",
@@ -158,8 +156,6 @@
"log_topic_label": "Tema:",
"log_topic_print": "Impresión",
"log_topic_status": "Estado",
"modal_sec_obico": "Obico",
"modal_sec_spoolman": "Spoolman",
"nav_ams": "AMS",
"nav_browser": "Explorador",
"nav_console": "Consola",
@@ -235,7 +231,6 @@
"settings_file_ready_banner": "Barra de impresión",
"settings_file_ready_dialog": "Diálogo de impresión",
"settings_file_ready_mode": "Después de carga: Comportamiento de inicio de impresión",
"settings_integrations": "Integraciones",
"settings_language": "Idioma",
"settings_mode_id": "ID de modo",
"settings_mode_id_placeholder": "20030",
@@ -321,8 +316,6 @@
"update_available": "disponible",
"update_check": "Buscar actualizaciones",
"update_checking": "Comprobando...",
"update_docker": "Copiar comando",
"update_docker_copied": "Copiado. Ejecutar: docker compose pull && docker compose up -d",
"update_error": "Error",
"update_none": "Ya actualizado",
"update_restarting": "Reiniciando..."

View File

@@ -123,8 +123,6 @@
"lbl_light": "💡 Lumière",
"lbl_remaining": "Restant :",
"lbl_slicer_time": "Estimation slicer :",
"lbl_spoolman_sync_rate": "Taux de sync. (s, 0=désact.)",
"lbl_spoolman_url": "URL du serveur",
"lbl_unload": "Décharger",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ Auto",
@@ -158,8 +156,6 @@
"log_topic_label": "Sujet :",
"log_topic_print": "Impression",
"log_topic_status": "Statut",
"modal_sec_obico": "Obico",
"modal_sec_spoolman": "Spoolman",
"nav_ams": "AMS",
"nav_browser": "Navigateur",
"nav_console": "Console",
@@ -235,7 +231,6 @@
"settings_file_ready_banner": "Barre d'impression",
"settings_file_ready_dialog": "Dialogue d'impression",
"settings_file_ready_mode": "Après téléchargement : Comportement de démarrage d'impression",
"settings_integrations": "Intégrations",
"settings_language": "Langue",
"settings_mode_id": "ID du mode",
"settings_mode_id_placeholder": "20030",
@@ -321,8 +316,6 @@
"update_available": "disponible",
"update_check": "Vérifier les mises à jour",
"update_checking": "Vérification…",
"update_docker": "Copier la commande",
"update_docker_copied": "Copié ! Exécuter : docker compose pull && docker compose up -d",
"update_error": "Erreur",
"update_none": "Déjà à jour",
"update_restarting": "Redémarrage…"

View File

@@ -123,8 +123,6 @@
"lbl_light": "💡 Luce",
"lbl_remaining": "Rimanente:",
"lbl_slicer_time": "Stima slicer:",
"lbl_spoolman_sync_rate": "Frequenza sync (s, 0=disatt.)",
"lbl_spoolman_url": "URL server",
"lbl_unload": "Rimuovi",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ Auto",
@@ -158,8 +156,6 @@
"log_topic_label": "Argomento:",
"log_topic_print": "Stampa",
"log_topic_status": "Stato",
"modal_sec_obico": "Obico",
"modal_sec_spoolman": "Spoolman",
"nav_ams": "AMS",
"nav_browser": "Browser",
"nav_console": "Console",
@@ -235,7 +231,6 @@
"settings_file_ready_banner": "Barra di stampa",
"settings_file_ready_dialog": "Finestra di dialogo stampa",
"settings_file_ready_mode": "Dopo il caricamento: Comportamento di avvio stampa",
"settings_integrations": "Integrazioni",
"settings_language": "Lingua",
"settings_mode_id": "ID modalità",
"settings_mode_id_placeholder": "20030",
@@ -321,8 +316,6 @@
"update_available": "disponibile",
"update_check": "Controlla aggiornamenti",
"update_checking": "Verifica in corso...",
"update_docker": "Copia comando",
"update_docker_copied": "Copiato! Eseguire: docker compose pull && docker compose up -d",
"update_error": "Errore",
"update_none": "Già aggiornato",
"update_restarting": "Riavvio in corso..."

View File

@@ -123,8 +123,6 @@
"lbl_light": "💡 灯光",
"lbl_remaining": "剩余时间:",
"lbl_slicer_time": "切片预估:",
"lbl_spoolman_sync_rate": "同步频率0=关闭)",
"lbl_spoolman_url": "服务器地址",
"lbl_unload": "退料",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ 自动",
@@ -158,8 +156,6 @@
"log_topic_label": "主题:",
"log_topic_print": "打印",
"log_topic_status": "状态",
"modal_sec_obico": "Obico",
"modal_sec_spoolman": "Spoolman",
"nav_ams": "AMS",
"nav_browser": "浏览器",
"nav_console": "控制台",
@@ -235,7 +231,6 @@
"settings_file_ready_banner": "打印栏",
"settings_file_ready_dialog": "打印对话框",
"settings_file_ready_mode": "上传后:开始打印行为",
"settings_integrations": "集成",
"settings_language": "语言",
"settings_mode_id": "模式 ID",
"settings_mode_id_placeholder": "20030",
@@ -321,8 +316,6 @@
"update_available": "可用",
"update_check": "检查更新",
"update_checking": "检查中...",
"update_docker": "复制命令",
"update_docker_copied": "已复制执行docker compose pull && docker compose up -d",
"update_error": "错误",
"update_none": "已是最新版本",
"update_restarting": "重启中..."