diff --git a/.gitea/workflows/nightly.yml b/.gitea/workflows/nightly.yml index 4f8961e..6542cb7 100644 --- a/.gitea/workflows/nightly.yml +++ b/.gitea/workflows/nightly.yml @@ -113,18 +113,23 @@ jobs: | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1) [ -z "$PREV_TAG" ] && PREV_TAG=$(git rev-list --max-parents=0 HEAD) - # Changelog generieren — nur feat/fix, keine ci/release/build-Scopes + # Changelog: NIGHTLY_CHANGELOG.md hat Vorrang (manuell gepflegt), + # sonst auto-generiert aus feat/fix-Commits seit letztem Stable-Tag BODY_FILE=$(mktemp) printf '## KX-Bridge %s — Nightly Build\n\n' "$VERSION" > "$BODY_FILE" printf '[experimental] Untested features, for testers only.\n\n' >> "$BODY_FILE" - printf '### Changes since `%s`\n\n' "$PREV_TAG" >> "$BODY_FILE" - git log "${PREV_TAG}..HEAD" --pretty=format:'%s' --no-merges \ - | grep -E '^(feat|fix)[:(]' \ - | grep -Ev '^(feat|fix)\((ci|release|build|workflow)\)' \ - | sed 's/^/- /' \ - >> "$BODY_FILE" || true - if ! grep -q '^- ' "$BODY_FILE"; then - printf '- No user-facing changes in this build\n' >> "$BODY_FILE" + if [ -s NIGHTLY_CHANGELOG.md ]; then + cat NIGHTLY_CHANGELOG.md >> "$BODY_FILE" + else + printf '### Changes since `%s`\n\n' "$PREV_TAG" >> "$BODY_FILE" + git log "${PREV_TAG}..HEAD" --pretty=format:'%s' --no-merges \ + | grep -E '^(feat|fix)[:(]' \ + | grep -Ev '^(feat|fix)\((ci|release|build|workflow)\)' \ + | sed 's/^/- /' \ + >> "$BODY_FILE" || true + if ! grep -q '^- ' "$BODY_FILE"; then + printf '- No user-facing changes in this build\n' >> "$BODY_FILE" + fi fi printf '\n\n---\n\n### Update Docker image\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" diff --git a/NIGHTLY_CHANGELOG.md b/NIGHTLY_CHANGELOG.md new file mode 100644 index 0000000..f6a628a --- /dev/null +++ b/NIGHTLY_CHANGELOG.md @@ -0,0 +1,6 @@ +## Changes in this build + +- Unified axes control panel: XY and Z merged into one card, shared step size selector (0.1 / 1 / 5 / 10 mm) plus custom mm input field, Home XY/Z buttons placed directly below their respective pads +- Language selector moved from header bar to Settings → Appearance +- Filament mismatch detection: Upload-and-Print is intercepted when GCode material differs from the loaded AMS slot — slot mapper dialog opens automatically to correct the assignment before printing +- Spoolman: assign a spool per AMS slot directly in the AMS status tab (dropdown per slot kachel) and in the Filaments settings tab (dedicated assignment card) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index dc23f57..afe5c5f 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -870,6 +870,7 @@ class KobraXBridge: "print_speed_mode": 2, "connection_error": "", "file_ready": "", + "filament_mismatch": None, "print_start_dialog": getattr(args, "print_start_dialog", 1), "filament_mode": "toolhead", "supplies_usage": 0, @@ -3273,6 +3274,31 @@ class KobraXBridge: self._state["last_upload_size"] = file_size if auto_print: + mismatch = self._check_filament_mismatch(gcode_filaments) + if mismatch: + log.info(f"Upload+Print blockiert — Filament-Mismatch: {mismatch}") + self._state["file_ready"] = remote_filename + self._state["filament_mismatch"] = mismatch + return web.json_response({ + "done": True, + "filament_mismatch": True, + "mismatch_details": mismatch, + "files": { + "local": { + "name": remote_filename, + "origin": "local", + "path": remote_filename, + "refs": { + "download": f"http://{request.host}/api/files/local/{remote_filename}", + "resource": f"http://{request.host}/api/files/local/{remote_filename}", + } + } + }, + "result": { + "item": {"path": remote_filename, "root": "gcodes"}, + "action": "create_file", + } + }, status=201) log.info(f"Upload+Print (print=true): {remote_filename}") self._state["file_ready"] = "" loop = asyncio.get_event_loop() @@ -3301,6 +3327,45 @@ class KobraXBridge: } }, status=201) + def _check_filament_mismatch(self, gcode_filaments: list | None) -> list[dict] | None: + """Vergleicht GCode-Filamente (is_used=True) mit aktuell belegten AMS-Slots. + + Gibt Liste von Mismatch-Einträgen zurück wenn mindestens ein genutzter + GCode-Slot kein passendes Material im AMS hat — sonst None. + Wird nur ausgelöst wenn AMS-Daten vorhanden sind (mindestens 1 belegter Slot).""" + if not gcode_filaments: + return None + slots = self._ams_slots or [] + occupied = {s["global_index"]: s for s in slots if s.get("type") and s.get("status") == 5} + if not occupied: + return None + mismatches = [] + for f in gcode_filaments: + if not f.get("is_used"): + continue + idx = int(f.get("slot_index", -1)) + gcode_mat = (f.get("material") or "").upper().strip() + if not gcode_mat: + continue + slot = occupied.get(idx) + if slot is None: + mismatches.append({ + "slot_index": idx, + "gcode_material": gcode_mat, + "ams_material": None, + "reason": "empty", + }) + else: + ams_mat = (slot.get("type") or "").upper().strip() + if ams_mat and ams_mat != gcode_mat: + mismatches.append({ + "slot_index": idx, + "gcode_material": gcode_mat, + "ams_material": ams_mat, + "reason": "mismatch", + }) + return mismatches if mismatches else None + def _start_print(self, filename: str, url: str = "", md5: str = "", filesize: int = 0, gcode_filaments: list | None = None): self._state["file_ready"] = "" @@ -3492,6 +3557,7 @@ class KobraXBridge: async def handle_api_file_ready_clear(self, request): self._state["file_ready"] = "" + self._state["filament_mismatch"] = None self._thumbnail_b64 = "" self._push_status_update() return web.json_response({"result": "ok"}) diff --git a/web/themes/default/app.js b/web/themes/default/app.js index ad7e02c..e116f34 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -55,6 +55,12 @@ function _loadSpoolmanStatus(){ _slotSpoolMap=d.slot_spools||{}; _updateSpoolmanStatusDot(); _buildSpoolmanSection(); + renderSpoolmanSlotCard(); + if(d.configured){ + fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(sd){ + _spoolmanSpools=sd.spools||[]; + }); + } }).catch(function(){}); } function _updateSpoolmanStatusDot(){ @@ -79,12 +85,8 @@ function _buildSpoolmanSection(){ if(loading)loading.style.display=''; var usedSlots={}; - document.querySelectorAll('#fd-slots select').forEach(function(sel){ - var idx=parseInt(sel.value); - if(idx>=0){ - var slot=(_amsSlots||[]).find(function(s){return s.slot_index===idx;}); - if(slot&&!usedSlots[idx])usedSlots[idx]=slot; - } + (_amsSlots||[]).forEach(function(slot){ + usedSlots[slot.slot_index]=slot; }); fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(d){ @@ -377,12 +379,10 @@ function applyLang(){ setText('d-chart-label',T.panel_temps_chart); // Axis labels setText('ptitle-motion-xy',T.panel_motion_xy); - setText('ptitle-motion-z',T.panel_motion_z); document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z); document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy); document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all); document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors); - document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step); document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':',''))); // Console setText('ptitle-console',T.panel_console_title); @@ -750,7 +750,7 @@ function applyState(){ frb.style.display='none'; if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){ _fdAutoOpenedFile=s.file_ready; - startReadyFileWithSlots(s.file_ready,true); + startReadyFileWithSlots(s.file_ready,true,s.filament_mismatch||null); } } else { frb.style.display='flex'; @@ -963,6 +963,22 @@ function applyState(){ var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':''); vendorBadge='
GCode-Kanal → AMS-Slot zuweisen:
+