Compare commits

...

3 Commits

Author SHA1 Message Date
1b05362c2b fix(ci): JSON-Serialisierung via python3 statt awk-Pipeline (SIGPIPE/exit-141-Fix) 2026-06-28 23:24:49 +02:00
cfe70430d3 feat: unified axes panel, language selector cleanup, filament mismatch detection, Spoolman slot assignment
Some checks failed
Nightly Build / build (push) Failing after 5m11s
- Merged XY + Z axis cards into single Axes Control panel with shared step
  size row and free-text mm input; Home XY/Z placed below respective pads;
  Home All + Motors Off centred at bottom
- Language selector removed from header bar, now only in Settings → Appearance
- Upload-and-Print blocked on GCode/AMS material mismatch; slot mapper dialog
  opens automatically with mismatch warning highlighting affected slots
- Spoolman spool-per-slot assignment: dropdown in AMS status tab per kachel,
  dedicated card in Filaments settings tab with save button
- nightly.yml: NIGHTLY_CHANGELOG.md takes precedence over auto-generated
  commit log when present; CI uses it as release body verbatim
2026-06-28 23:17:58 +02:00
2c8a62f130 fix(ci): Changelog nur feat/fix-Commits seit letztem Stable-Tag
Some checks failed
Nightly Build / build (push) Failing after 5m3s
2026-06-28 21:59:38 +02:00
11 changed files with 275 additions and 83 deletions

View File

@@ -108,19 +108,29 @@ jobs:
. /tmp/nightly_version.env
TAG="nightly-${VERSION}"
# Letzten nightly-Tag als Changelog-Basis ermitteln
PREV_TAG=$(git tag --list 'nightly-*' --sort=-version:refname | head -1)
[ -z "$PREV_TAG" ] && PREV_TAG="$LAST_STABLE"
# Letzten Stable-Tag als Changelog-Basis (nur echte vX.Y.Z-Tags)
PREV_TAG=$(git tag --list 'v*' --sort=-version:refname \
| 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
# 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 \
--invert-grep --grep='^chore: nightly' \
>> "$BODY_FILE" || true
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"
@@ -142,13 +152,21 @@ jobs:
"https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases/tags/${TAG}" \
2>/dev/null || true
# Release erstellen
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
# Release erstellen — JSON via python3 serialisieren (kein SIGPIPE-Risiko)
python3 - "$TAG" "$VERSION" "$BODY_FILE" <<'PYEOF'
import sys, json
tag, version, body_file = sys.argv[1], sys.argv[2], sys.argv[3]
body = open(body_file).read()
payload = json.dumps({
"tag_name": tag,
"name": f"KX-Bridge {version} Nightly",
"body": body,
"draft": False,
"prerelease": True
})
with open("/tmp/release_body.json", "w") as f:
f.write(payload)
PYEOF
curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \

6
NIGHTLY_CHANGELOG.md Normal file
View File

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

View File

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

View File

@@ -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='<div class="slot-label" style="font-size:9px;color:var(--accent);font-weight:600;margin-top:1px" title="'+tt+'">'+profile.vendor+'</div>';
}
var spoolSel='';
if(_spoolmanStatus.configured&&!empty&&_spoolmanSpools.length){
var curSpool=_slotSpoolMap[String(globalIdx)]||'';
var spoolOpts='<option value=""></option>'+_spoolmanSpools.map(function(sp){
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':'';
var name=sp.filament?sp.filament.name:'#'+sp.id;
var rem=sp.remaining_weight!=null?' '+sp.remaining_weight.toFixed(0)+'g':'';
return '<option value="'+sp.id+'"'+(String(sp.id)==String(curSpool)?' selected':'')+'>'+
escHtml(vendor+name+rem)+'</option>';
}).join('');
spoolSel='<div onclick="event.stopPropagation()" style="margin-top:6px;border-top:1px solid rgba(255,255,255,.07);padding-top:5px">'
+'<div style="font-size:8px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:.06em;margin-bottom:3px">🧵 Spoolman</div>'
+'<select data-spool-slot="'+globalIdx+'" onchange="onAmsSpoolChange(this)" '
+'style="width:100%;padding:3px 5px;font-size:10px;border-radius:5px;border:1px solid var(--border);background:var(--card);color:var(--txt);cursor:pointer">'+spoolOpts+'</select>'
+'</div>';
}
html+='<div class="ams-slot'+(active?' active':'')+(loaded?' loaded':'')+(activity?' '+activity:'')+(empty?' empty':'')
+'" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">'
+'<div class="slot-circle" style="background:'+col+'"></div>'
@@ -970,6 +986,7 @@ function applyState(){
+vendorBadge
+'<div class="slot-label">'+slotLabel+'</div>'
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
+spoolSel
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
+'</div>';
});
@@ -1076,6 +1093,7 @@ function openSettings(){
pi.value=sec;
}
renderFilamentMapping(d.filament_profiles||{});
renderSpoolmanSlotCard();
// Spoolman
var su=document.getElementById('s-spoolman-url');if(su)su.value=d.spoolman_server||'';
var sr=document.getElementById('s-spoolman-sync-rate');if(sr)sr.value=(d.spoolman_sync_rate!==undefined?d.spoolman_sync_rate:30);
@@ -1223,6 +1241,64 @@ function _vendorCheck(cb){
var v=cb.getAttribute('data-vendor');
if(cb.checked)_vendorChecklistSel[v]=true; else delete _vendorChecklistSel[v];
}
function renderSpoolmanSlotCard(){
var card=document.getElementById('spoolman-slot-card');
var rows=document.getElementById('spoolman-slot-rows');
if(!card||!rows)return;
if(!_spoolmanStatus.configured){card.style.display='none';return;}
card.style.display='';
Promise.all([
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}),
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();})
]).then(function(res){
var spools=res[0].spools||[];
var slots=(res[1].result||[]).sort(function(a,b){return a.slot_index-b.slot_index;});
if(!slots.length){rows.innerHTML='<span style="font-size:11px;color:var(--txt2)">Keine AMS-Slots bekannt.</span>';return;}
rows.innerHTML=slots.map(function(slot){
var idx=parseInt(slot.slot_index);
var col=slot.color_hex||'#888';
var mat=slot.material||'';
var current=_slotSpoolMap[String(idx)]||'';
var opts='<option value=""></option>'+spools.map(function(sp){
var rem=sp.remaining_weight!=null?' ('+sp.remaining_weight.toFixed(0)+'g)':'';
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':'';
var name=sp.filament?sp.filament.name:'Spool #'+sp.id;
var mat2=sp.filament&&sp.filament.material?' · '+sp.filament.material:'';
return '<option value="'+sp.id+'"'+(String(sp.id)==String(current)?' selected':'')+'>'+
escHtml('#'+sp.id+' '+vendor+name+mat2+rem)+'</option>';
}).join('');
return '<div style="display:flex;align-items:center;gap:8px;font-size:12px">'+
'<span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:'+col+';border:1px solid var(--border);flex-shrink:0"></span>'+
'<span style="color:var(--txt2);min-width:60px">Slot '+(idx+1)+' <span style="color:var(--txt2);font-size:10px">'+escHtml(mat)+'</span></span>'+
'<select data-spool-slot="'+idx+'" style="flex:1;padding:3px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">'+opts+'</select></div>';
}).join('');
}).catch(function(){rows.innerHTML='<span style="font-size:11px;color:var(--err)">Spoolman nicht erreichbar</span>';});
}
function onAmsSpoolChange(sel){
var idx=sel.getAttribute('data-spool-slot');
var val=sel.value;
if(val) _slotSpoolMap[String(idx)]=parseInt(val);
else delete _slotSpoolMap[String(idx)];
var mapping={};
Object.keys(_slotSpoolMap).forEach(function(k){mapping[k]=_slotSpoolMap[k];});
fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST',
headers:{'Content-Type':'application/json'},body:JSON.stringify({slot_spools:mapping})});
}
function saveSpoolmanSlots(){
var mapping={};
document.querySelectorAll('#spoolman-slot-rows select[data-spool-slot]').forEach(function(sel){
var idx=sel.getAttribute('data-spool-slot');
var val=sel.value;
if(val)mapping[idx]=parseInt(val);
});
fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({slot_spools:mapping})}).then(function(r){return r.json();}).then(function(d){
_slotSpoolMap=d.slot_spools||{};
});
}
function saveVisibleVendors(){
var vendors=Object.keys(_vendorChecklistSel);
fetch(_apiUrl('/kx/filament/visible_vendors'),{method:'POST',headers:{'Content-Type':'application/json'},
@@ -1792,7 +1868,15 @@ function setStep(btn,v){
currentStep=v;
document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('step-display').textContent=v;
var ci=document.getElementById('step-custom');
if(ci)ci.value='';
}
function setStepCustom(inp){
var v=parseFloat(inp.value);
if(!isNaN(v)&&v>0){
currentStep=v;
document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active'));
}
}
function move(axis,dir,dist){
// axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z
@@ -2487,7 +2571,20 @@ function confirmStoreWebVerify(){
});
}
function startReadyFileWithSlots(filename,_autoOpen){
function _showMismatchWarn(mismatches){
var el=document.getElementById('fd-mismatch-warn');
if(!el)return;
if(!mismatches||!mismatches.length){el.style.display='none';el.innerHTML='';return;}
var lines=mismatches.map(function(m){
var slot='Slot '+(m.slot_index+1);
if(m.reason==='empty')
return '⚠ '+slot+': GCode needs '+m.gcode_material+' — slot is empty';
return '⚠ '+slot+': GCode needs '+m.gcode_material+', loaded: '+(m.ams_material||'?');
});
el.innerHTML='<strong>Filament mismatch detected</strong><br>'+lines.join('<br>');
el.style.display='';
}
function startReadyFileWithSlots(filename,_autoOpen,_mismatch){
if(!_autoOpen) _fdAutoOpenedFile=null; // manueller Aufruf → Auto-Open-Sperre aufheben
var fn=filename||S.file_ready;
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
@@ -2507,9 +2604,11 @@ function startReadyFileWithSlots(filename,_autoOpen){
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){
if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;}
openFilamentDialog(d.result||[]);
_showMismatchWarn(_mismatch||null);
}).catch(function(){
if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;}
openFilamentDialog([]);
_showMismatchWarn(_mismatch||null);
});
}
@@ -2745,6 +2844,7 @@ function openFilamentDialog(slots){
function closeFilamentDialog(){
var dlg=document.getElementById('filament-dialog');
if(dlg)dlg.classList.remove('open');
_showMismatchWarn(null);
_fdDialogOpen=false;
if(_fdAutoOpenedFile){
_fdUserCancelled=true;

View File

@@ -32,17 +32,6 @@
<span id="h-version" style="font-size:11px;opacity:.5;margin-left:6px"></span>
<div class="hbadge" id="h-badge"><span class="dot"></span><span id="h-state">Standby</span></div>
<button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button>
<div style="display:flex;align-items:center;gap:6px">
<span aria-hidden="true" style="font-size:15px;line-height:1;opacity:.85">🌐</span>
<select class="theme-btn" id="lang-select" onchange="setLanguageFromSelect()" style="padding:6px 10px">
<option value="de">Deutsch</option>
<option value="en">English</option>
<option value="es">Espanol</option>
<option value="fr">Français</option>
<option value="it">Italiano</option>
<option value="zh-cn">中文(简体)</option>
</select>
</div>
<button class="theme-btn" onclick="showPanel('settings')" id="settings-btn" title="Einstellungen"></button>
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
</header>
@@ -252,39 +241,51 @@
<!-- Achsensteuerung -->
<div class="card">
<div class="card-title"><span></span> <span id="ptitle-motion-xy">XY-Achsen</span></div>
<div class="joypad">
<div></div>
<button class="joy" onclick="move(1,1,getStep())" title="Y+"></button>
<div></div>
<button class="joy" onclick="move(0,-1,getStep())" title="X"></button>
<button class="joy home" onclick="homeAll()" title="Home All"></button>
<button class="joy" onclick="move(0,1,getStep())" title="X+"></button>
<div></div>
<button class="joy" onclick="move(1,-1,getStep())" title="Y"></button>
<div></div>
<div class="card-title"><span></span> <span id="ptitle-motion-xy">Achsensteuerung</span></div>
<div style="display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap">
<!-- XY -->
<div style="display:flex;flex-direction:column;align-items:center;gap:6px">
<div class="joypad">
<div></div>
<button class="joy" onclick="move(1,1,getStep())" title="Y+"></button>
<div></div>
<button class="joy" onclick="move(0,-1,getStep())" title="X"></button>
<div></div>
<button class="joy" onclick="move(0,1,getStep())" title="X+"></button>
<div></div>
<button class="joy" onclick="move(1,-1,getStep())" title="Y"></button>
<div></div>
</div>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt);width:100%" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
</div>
<!-- Z -->
<div style="display:flex;flex-direction:column;align-items:center;gap:6px">
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
<button class="joy" onclick="move(2,1,getStep())" title="Z+"></button>
<button class="joy" onclick="move(2,-1,getStep())" title="Z"></button>
</div>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt);width:100%" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
</div>
</div>
<div class="step-btns">
<button class="step-btn" onclick="setStep(this,0.1)">0.1</button>
<button class="step-btn active" onclick="setStep(this,1)">1</button>
<button class="step-btn" onclick="setStep(this,5)">5</button>
<button class="step-btn" onclick="setStep(this,10)">10 mm</button>
<!-- Einheitliche Step-Size -->
<div style="display:flex;align-items:center;gap:6px;margin-top:10px;flex-wrap:wrap">
<div class="step-btns" style="margin-top:0">
<button class="step-btn" onclick="setStep(this,0.1)">0.1</button>
<button class="step-btn active" onclick="setStep(this,1)">1</button>
<button class="step-btn" onclick="setStep(this,5)">5</button>
<button class="step-btn" onclick="setStep(this,10)">10</button>
</div>
<input id="step-custom" type="number" min="0.1" max="260" step="0.1"
placeholder="mm"
style="width:60px;padding:4px 6px;font-size:12px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);text-align:center"
oninput="setStepCustom(this)">
</div>
<div class="home-btns">
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
<!-- Globale Befehle zentriert -->
<div style="display:flex;justify-content:center;gap:8px;margin-top:10px">
<button class="btn btn-sm btn-accent" onclick="homeAll()"><span class="lbl-home-all">Home All</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="disableMotors()"><span class="lbl-disable-motors">Motors Off</span></button>
</div>
</div>
<div class="card">
<div class="card-title"><span></span> <span id="ptitle-motion-z">Z-Achse</span></div>
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
<button class="joy" onclick="move(2,1,getStep())" title="Z+"></button>
<button class="joy" onclick="move(2,-1,getStep())" title="Z"></button>
</div>
<div style="text-align:center;margin-top:8px;font-size:12px;color:var(--txt2)"><span class="lbl-step">Schrittweite:</span> <span id="step-display">1</span> mm</div>
</div>
<!-- Print Speed -->
<div class="card">
@@ -570,6 +571,12 @@
<div id="visible-vendors-list" style="max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px"></div>
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveVisibleVendors()"><span id="lbl-visible-vendors-save">Auswahl speichern</span></button>
</div>
<div class="card" id="spoolman-slot-card" style="display:none">
<div class="card-title"><span>🧵</span> <span id="lbl-spoolman-slot-assign">Spoolman — Slot-Zuordnung</span></div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:10px" id="lbl-spoolman-slot-hint">Spoolman-Spool pro AMS-Slot zuweisen. Der Verbrauch wird beim Druck automatisch gemeldet.</div>
<div id="spoolman-slot-rows" style="display:flex;flex-direction:column;gap:8px"></div>
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:10px" onclick="saveSpoolmanSlots()"><span id="lbl-spoolman-slot-save">Zuordnung speichern</span></button>
</div>
</div>
<!-- Integrationen -->
@@ -655,6 +662,7 @@
<button onclick="closeFilamentDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)"></button>
</div>
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
<div id="fd-mismatch-warn" style="display:none;margin-bottom:10px;padding:8px 10px;background:rgba(255,160,0,.12);border:1px solid rgba(255,160,0,.4);border-radius:8px;font-size:11px;color:#ffa000"></div>
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
<button type="button" id="fd-objects-toggle" onclick="toggleFdObjects()"

View File

@@ -189,8 +189,7 @@
"panel_extras_camera": "Kamera",
"panel_extras_fan": "Lüfter",
"panel_extras_light": "Licht",
"panel_motion_xy": "XY-Achsen",
"panel_motion_z": "Z-Achse",
"panel_motion_xy": "Achsensteuerung",
"panel_print_btn_cancel": "✕ Abbrechen",
"panel_print_btn_pause": "⏸ Pause",
"panel_print_btn_resume": "▶ Fortsetzen",
@@ -326,4 +325,4 @@
"update_error": "Fehler",
"update_none": "Bereits aktuell",
"update_restarting": "Starte neu..."
}
}

View File

@@ -189,8 +189,7 @@
"panel_extras_camera": "Camera",
"panel_extras_fan": "Fan",
"panel_extras_light": "Light",
"panel_motion_xy": "XY Axes",
"panel_motion_z": "Z Axis",
"panel_motion_xy": "Axes Control",
"panel_print_btn_cancel": "✕ Cancel",
"panel_print_btn_pause": "⏸ Pause",
"panel_print_btn_resume": "▶ Resume",
@@ -326,4 +325,4 @@
"update_error": "Error",
"update_none": "Already up to date",
"update_restarting": "Restarting..."
}
}

View File

@@ -189,8 +189,7 @@
"panel_extras_camera": "Cámara",
"panel_extras_fan": "Ventilador",
"panel_extras_light": "Luz",
"panel_motion_xy": "Ejes XY",
"panel_motion_z": "Eje Z",
"panel_motion_xy": "Control de Ejes",
"panel_print_btn_cancel": "✕ Cancelar",
"panel_print_btn_pause": "⏸ Pausa",
"panel_print_btn_resume": "▶ Reanudar",
@@ -326,4 +325,4 @@
"update_error": "Error",
"update_none": "Ya actualizado",
"update_restarting": "Reiniciando..."
}
}

View File

@@ -189,8 +189,7 @@
"panel_extras_camera": "Caméra",
"panel_extras_fan": "Ventilateur",
"panel_extras_light": "Lumière",
"panel_motion_xy": "Axes XY",
"panel_motion_z": "Axe Z",
"panel_motion_xy": "Contrôle des Axes",
"panel_print_btn_cancel": "✕ Annuler",
"panel_print_btn_pause": "⏸ Pause",
"panel_print_btn_resume": "▶ Reprendre",
@@ -326,4 +325,4 @@
"update_error": "Erreur",
"update_none": "Déjà à jour",
"update_restarting": "Redémarrage…"
}
}

View File

@@ -189,8 +189,7 @@
"panel_extras_camera": "Camera",
"panel_extras_fan": "Ventola",
"panel_extras_light": "Luce",
"panel_motion_xy": "Assi XY",
"panel_motion_z": "Asse Z",
"panel_motion_xy": "Controllo Assi",
"panel_print_btn_cancel": "✕ Annulla",
"panel_print_btn_pause": "⏸ Pausa",
"panel_print_btn_resume": "▶ Riprendi",
@@ -326,4 +325,4 @@
"update_error": "Errore",
"update_none": "Già aggiornato",
"update_restarting": "Riavvio in corso..."
}
}

View File

@@ -189,8 +189,7 @@
"panel_extras_camera": "相机",
"panel_extras_fan": "风扇",
"panel_extras_light": "灯光",
"panel_motion_xy": "XY 轴",
"panel_motion_z": "Z 轴",
"panel_motion_xy": "轴控制",
"panel_print_btn_cancel": "✕ 取消",
"panel_print_btn_pause": "⏸ 暂停",
"panel_print_btn_resume": "▶ 继续",
@@ -326,4 +325,4 @@
"update_error": "错误",
"update_none": "已是最新版本",
"update_restarting": "重启中..."
}
}