diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml new file mode 100644 index 0000000..98589d5 --- /dev/null +++ b/.gitea/workflows/docker-publish.yml @@ -0,0 +1,56 @@ +name: Docker Publish + +on: + push: + tags: + - 'v*' + branches: + - nightly + workflow_dispatch: + +jobs: + build-push: + runs-on: self-hosted + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + clean: true + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea registry + uses: docker/login-action@v3 + with: + registry: gitea.it-drui.de + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Derive tags + id: meta + run: | + REF="${GITHUB_REF}" + if [[ "$REF" == refs/tags/v* ]]; then + VER=$(cat VERSION) + TAGS="gitea.it-drui.de/viewit/kx-bridge:latest" + TAGS="$TAGS,gitea.it-drui.de/viewit/kx-bridge:$VER" + else + # Push auf nightly-Branch oder workflow_dispatch + TAGS="gitea.it-drui.de/viewit/kx-bridge:nightly" + fi + echo "tags=$TAGS" >>"$GITHUB_OUTPUT" + echo "Pushing tags: $TAGS" + + - name: Build & push (amd64 + arm64) + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + provenance: false + no-cache: true + tags: ${{ steps.meta.outputs.tags }} diff --git a/docker-compose-KX.yml b/docker-compose-KX.yml new file mode 100644 index 0000000..12752d0 --- /dev/null +++ b/docker-compose-KX.yml @@ -0,0 +1,210 @@ +# 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// +# +# 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=' + # → 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 diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 358a99a..b70d1f4 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -4218,6 +4218,8 @@ 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): @@ -4232,7 +4234,7 @@ class KobraXBridge: cfg.read(config_path, encoding="utf-8") # Sections sicherstellen - for section in ("connection", "print", "bridge", "ace_dry_presets"): + for section in ("connection", "print", "bridge", "ace_dry_presets", "spoolman"): if not cfg.has_section(section): cfg.add_section(section) @@ -4262,6 +4264,16 @@ 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(): diff --git a/moonraker-obico.cfg.example b/moonraker-obico.cfg.example new file mode 100644 index 0000000..e166677 --- /dev/null +++ b/moonraker-obico.cfg.example @@ -0,0 +1,18 @@ +[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 diff --git a/web/themes/default/app.js b/web/themes/default/app.js index 8d51f74..103b8db 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -53,8 +53,19 @@ 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'); @@ -385,6 +396,11 @@ 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'); @@ -1058,6 +1074,10 @@ 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'); @@ -1626,6 +1646,8 @@ 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(){ diff --git a/web/themes/default/index.html b/web/themes/default/index.html index d677621..7462845 100644 --- a/web/themes/default/index.html +++ b/web/themes/default/index.html @@ -435,6 +435,7 @@ + @@ -570,6 +571,33 @@ + +
+ +
+
🧵 Spoolman
+
+ + +
+
+ + +
+
+ +
+
+ +
+
🕵 Obico
+
+ Obico wird über den moonraker-obico-Container konfiguriert.
+ Config-Datei: /mnt/dockerdata/KobraXStack/moonraker-obico/moonraker-obico.cfg +
+
+
+
diff --git a/web/translations/de.json b/web/translations/de.json index c5011df..35ecc76 100644 --- a/web/translations/de.json +++ b/web/translations/de.json @@ -287,5 +287,10 @@ "log_clear": "✕ Leeren", "log_filter_placeholder": "Filtern…", "skip_cancel": "Abbrechen", - "skip_confirm": "Überspringen" -} + "skip_confirm": "Überspringen", + "settings_integrations": "Integrationen", + "modal_sec_spoolman": "Spoolman", + "lbl_spoolman_url": "Server-URL", + "lbl_spoolman_sync_rate": "Sync-Rate (s, 0=aus)", + "modal_sec_obico": "Obico" +} \ No newline at end of file diff --git a/web/translations/en.json b/web/translations/en.json index 61c5c86..7359609 100644 --- a/web/translations/en.json +++ b/web/translations/en.json @@ -287,5 +287,10 @@ "sf_new": "New", "ss_date": "↓ Date", "ss_name": "A–Z Name", - "ss_dur": "⏱ Print time" -} + "ss_dur": "⏱ Print time", + "settings_integrations": "Integrations", + "modal_sec_spoolman": "Spoolman", + "lbl_spoolman_url": "Server URL", + "lbl_spoolman_sync_rate": "Sync rate (s, 0=off)", + "modal_sec_obico": "Obico" +} \ No newline at end of file diff --git a/web/translations/es.json b/web/translations/es.json index 822cf4b..dfca2a5 100644 --- a/web/translations/es.json +++ b/web/translations/es.json @@ -287,5 +287,10 @@ "log_clear": "✕ Limpiar", "log_filter_placeholder": "Filtrar…", "skip_cancel": "Cancelar", - "skip_confirm": "Omitir" -} + "skip_confirm": "Omitir", + "settings_integrations": "Integraciones", + "modal_sec_spoolman": "Spoolman", + "lbl_spoolman_url": "URL del servidor", + "lbl_spoolman_sync_rate": "Tasa de sincronización (s, 0=desact.)", + "modal_sec_obico": "Obico" +} \ No newline at end of file diff --git a/web/translations/fr.json b/web/translations/fr.json index 01fc6f6..bf4f0ba 100644 --- a/web/translations/fr.json +++ b/web/translations/fr.json @@ -287,5 +287,10 @@ "log_clear": "✕ Effacer", "log_filter_placeholder": "Filtrer…", "skip_cancel": "Annuler", - "skip_confirm": "Ignorer" -} + "skip_confirm": "Ignorer", + "settings_integrations": "Intégrations", + "modal_sec_spoolman": "Spoolman", + "lbl_spoolman_url": "URL du serveur", + "lbl_spoolman_sync_rate": "Taux de sync. (s, 0=désact.)", + "modal_sec_obico": "Obico" +} \ No newline at end of file diff --git a/web/translations/it.json b/web/translations/it.json index 56ed376..4a9b504 100644 --- a/web/translations/it.json +++ b/web/translations/it.json @@ -287,5 +287,10 @@ "sf_new": "Nuovo", "ss_date": "↓ Data", "ss_name": "Nome A–Z", - "ss_dur": "⏱ Tempo di stampa" + "ss_dur": "⏱ Tempo di stampa", + "settings_integrations": "Integrazioni", + "modal_sec_spoolman": "Spoolman", + "lbl_spoolman_url": "URL server", + "lbl_spoolman_sync_rate": "Frequenza sync (s, 0=disatt.)", + "modal_sec_obico": "Obico" } \ No newline at end of file diff --git a/web/translations/zh-cn.json b/web/translations/zh-cn.json index 8225ee0..d376c21 100644 --- a/web/translations/zh-cn.json +++ b/web/translations/zh-cn.json @@ -287,5 +287,10 @@ "log_clear": "✕ 清空", "log_filter_placeholder": "筛选…", "skip_cancel": "取消", - "skip_confirm": "跳过" -} + "skip_confirm": "跳过", + "settings_integrations": "集成", + "modal_sec_spoolman": "Spoolman", + "lbl_spoolman_url": "服务器地址", + "lbl_spoolman_sync_rate": "同步频率(秒,0=关闭)", + "modal_sec_obico": "Obico" +} \ No newline at end of file