feat: Integrationen-Tab + KobraX Full Stack Compose + CI-Workflow
Some checks failed
Docker Publish / build-push (push) Failing after 13s

- Settings-Tab "Integrationen": Spoolman URL/Sync-Rate konfigurierbar,
  Obico Read-only Hinweis auf moonraker-obico.cfg
- docker-compose-KX.yml: Portainer-kompatibler Full Stack (KX-Bridge +
  Obico Self-Hosted + Spoolman + moonraker-obico Plugin)
- moonraker-obico.cfg.example: Verbindungsvorlage für Obico-Integration
- .gitea/workflows/docker-publish.yml: Push auf nightly → :nightly Image,
  v*-Tag → :latest + :<VERSION>
This commit is contained in:
2026-06-24 12:58:46 +02:00
parent 3595cf839c
commit 72f77d92af
12 changed files with 388 additions and 12 deletions

View File

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

210
docker-compose-KX.yml Normal file
View File

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

@@ -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():

View File

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

View File

@@ -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(){

View File

@@ -435,6 +435,7 @@
<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>
@@ -570,6 +571,33 @@
</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

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

View File

@@ -287,5 +287,10 @@
"sf_new": "New",
"ss_date": "↓ Date",
"ss_name": "AZ 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"
}

View File

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

View File

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

View File

@@ -287,5 +287,10 @@
"sf_new": "Nuovo",
"ss_date": "↓ Data",
"ss_name": "Nome AZ",
"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"
}

View File

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