Compare commits

...

4 Commits

Author SHA1 Message Date
44383fabec fix(docker): gcc + python3-dev für pycryptodome arm/v7 Kompilierung
All checks were successful
Nightly Build / build (push) Successful in 12m26s
2026-06-30 14:30:10 +02:00
48bec55611 feat(ci): linux/arm/v7 Platform zu Docker-Build hinzugefügt (Raspberry Pi 2/3 32-bit)
Some checks failed
Nightly Build / build (push) Failing after 4m53s
2026-06-30 12:21:20 +02:00
ab44e234be fix(ui): AMS-Spool-Dropdown bleibt offen während Poll-Tick (kein innerHTML-Reset bei fokussiertem Select)
Some checks failed
Nightly Build / build (push) Has been cancelled
2026-06-30 12:17:03 +02:00
74fc2ddab0 feat: color picker, unified UI styling, filament mismatch detection, Spoolman slot assignment
All checks were successful
Nightly Build / build (push) Successful in 4m27s
- Slot color editor: Pickr HSV color picker (offline, served from lib/),
  recent swatches (up to 16, localStorage), copy color from other slot
- Unified axes control panel: XY+Z merged, shared step size + custom mm input
- Language selector moved from header to Settings → Appearance
- Filament mismatch detection blocks Upload-and-Print on material mismatch,
  slot mapper opens automatically
- Spoolman spool-per-slot assignment in AMS status tab and Filaments settings
- Fix: Spoolman sync rate label — 0=end of print, not disabled (Issue #76)
- Fix: lib/ assets served by bridge static handler for offline use
- UI: global unified select + input styling, set-row labels match modal-field
2026-06-30 11:13:34 +02:00
16 changed files with 218 additions and 28 deletions

View File

@@ -94,7 +94,7 @@ jobs:
# VERSION-Datei im Arbeitsverzeichnis für den Docker-Build setzen (kein Commit)
echo "$VERSION" > VERSION
docker buildx build \
--platform linux/amd64,linux/arm64 \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--push \
--provenance=false \
--no-cache \

View File

@@ -61,7 +61,7 @@ jobs:
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--push \
--provenance=false \
--no-cache \

View File

@@ -2,10 +2,11 @@ FROM python:3.11-slim-bookworm
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg gcc python3-dev && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir -r requirements.txt && \
apt-get purge -y gcc python3-dev && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
COPY kobrax_moonraker_bridge.py .
COPY web/ ./web/

View File

@@ -3,4 +3,9 @@
- 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)
- Spoolman: assign a spool per AMS slot directly in the AMS status tab (dropdown per slot tile) and in the Filaments settings tab (dedicated assignment card)
- Fix: filament profiles now isolated per printer in multi-printer setups — configuring one printer no longer overwrites the other (PR #75 by @walterioo)
- Fix: printer dropdown and switch link now navigate to each printer's own bridge URL (same-origin, no cross-instance profile bleed)
- Fix: Spoolman sync rate label corrected — 0 means sync at end of print, not disabled (Issue #76)
- Slot color editor: Pickr color picker (HSV wheel + hex input), recent color swatches (up to 16, saved in browser), and "Copy color from slot" dropdown for identical backup spool setup (Issue #73)
- UI: unified dropdown and input field styling across all settings panels

View File

@@ -142,6 +142,11 @@ _KX_UI_ASSETS: dict[str, str] = {
"style.css": "text/css",
"app.js": "application/javascript",
}
# Dateien aus lib/ werden anhand der Extension ausgeliefert (kein Whitelist-Eintrag nötig)
_KX_UI_LIB_TYPES: dict[str, str] = {
".js": "application/javascript",
".css": "text/css",
}
_KX_UI_TRANSLATION_RE = re.compile(r"^translations/([a-z]{2}(?:-[a-z]{2})?)\.json$")
# Ring-Buffer für Browser-Log-Stream (letzte 200 Einträge)
@@ -3576,6 +3581,12 @@ class KobraXBridge:
if ctype is not None:
path = os.path.join(_WEB_BASE, "web", "themes", self._ui_theme, name)
elif name.startswith("lib/"):
ext = os.path.splitext(name)[1].lower()
ctype = _KX_UI_LIB_TYPES.get(ext)
if not ctype:
raise web.HTTPNotFound()
path = os.path.join(_WEB_BASE, "web", "themes", self._ui_theme, name)
else:
m = _KX_UI_TRANSLATION_RE.match(name)
if not m:

View File

@@ -997,7 +997,10 @@ function applyState(){
}
html+='</div></div>';
});
document.getElementById('ams-slots').innerHTML=html;
// Nicht rendern wenn ein Spool-Dropdown gerade offen ist (verhindert Schließen beim Poll)
var activeEl=document.activeElement;
var spoolOpen=activeEl&&activeEl.tagName==='SELECT'&&activeEl.dataset.spoolSlot!=null;
if(!spoolOpen) document.getElementById('ams-slots').innerHTML=html;
}
// camera overlay
@@ -1496,6 +1499,110 @@ function _fillSlotProfileDropdown(material, currentVendor, currentName){
});
});
}
// ── Pickr color picker ──────────────────────────────────────────────────────
var _pickr=null;
function _initPickr(hex){
// destroy previous instance if exists
if(_pickr){ try{ _pickr.destroyAndRemove(); }catch(e){} _pickr=null; }
var anchor=document.getElementById('slot-pickr-anchor');
if(!anchor||typeof Pickr==='undefined') return;
// fresh button element so Pickr can mount
anchor.innerHTML='<div id="slot-pickr-btn"></div>';
_pickr=Pickr.create({
el:'#slot-pickr-btn',
theme:'nano',
default: hex||'#808080',
inline: true,
showAlways: true,
components:{
preview:true, opacity:false, hue:true,
interaction:{ hex:true, rgba:false, input:true, save:false, clear:false }
}
});
_pickr.on('change',function(color){
var h=color.toHEXA().toString().slice(0,7);
document.getElementById('slot-edit-color').value=h;
document.getElementById('slot-edit-preview').style.background=h;
});
// Theme anpassen: Pickr benutzt eigene CSS-Variablen, wir überschreiben via style
requestAnimationFrame(function(){
var el=anchor.querySelector('.pickr');
if(el) el.style.cssText='width:100%';
var app=anchor.querySelector('.pcr-app');
if(app){
app.style.cssText='position:relative;width:100%;box-shadow:none;background:transparent';
var btn=app.querySelector('.pcr-result');
if(btn) btn.style.cssText='background:var(--raised);border:1px solid var(--border);color:var(--txt);border-radius:6px;font-size:12px';
}
});
}
// ── Color swatches (localStorage, max 16) ──────────────────────────────────
var _SWATCH_KEY='kxb_color_swatches';
var _SWATCH_MAX=16;
function _loadSwatches(){
try{ return JSON.parse(localStorage.getItem(_SWATCH_KEY)||'[]'); }catch(e){ return []; }
}
function _saveSwatches(arr){ try{ localStorage.setItem(_SWATCH_KEY, JSON.stringify(arr)); }catch(e){} }
function _addSwatch(hex){
var arr=_loadSwatches().filter(function(c){ return c.toLowerCase()!==hex.toLowerCase(); });
arr.unshift(hex);
if(arr.length>_SWATCH_MAX) arr=arr.slice(0,_SWATCH_MAX);
_saveSwatches(arr);
}
function _renderSwatches(){
var el=document.getElementById('slot-color-swatches');
if(!el) return;
var arr=_loadSwatches();
if(!arr.length){ el.style.display='none'; return; }
el.style.display='flex';
el.innerHTML=arr.map(function(c){
return '<div title="'+c+'" onclick="slotPickSwatch(\''+c+'\')" style="width:22px;height:22px;border-radius:4px;background:'+c+
';border:2px solid rgba(255,255,255,.2);cursor:pointer;flex-shrink:0"></div>';
}).join('');
}
function slotPickSwatch(hex){
if(_pickr){ _pickr.setColor(hex); }
var ci=document.getElementById('slot-edit-color');
if(ci) ci.value=hex;
document.getElementById('slot-edit-preview').style.background=hex;
}
// ── Copy color from other slot ──────────────────────────────────────────────
function _renderCopyFromSlot(currentGlobalIdx){
var slots=(window._amsSlots||[]).filter(function(s){
return s.global_index!==currentGlobalIdx && s.status==5 && Array.isArray(s.color);
});
var row=document.getElementById('slot-copy-row');
var sel=document.getElementById('slot-copy-select');
if(!row||!sel) return;
if(!slots.length){ row.style.display='none'; return; }
row.style.display='';
var ph=document.getElementById('lbl-slot-copy-from');
var phTxt=ph?ph.textContent:(T.slot_copy_from||'Copy color from slot…');
sel.innerHTML='<option value="">'+phTxt+'</option>'+slots.map(function(s){
var rgb=s.color;
var hex='#'+rgb.map(function(v){return('0'+Math.min(255,v).toString(16)).slice(-2)}).join('');
return '<option value="'+hex+'">Slot '+(s.global_index+1)+' — '+(s.type||'?')+' '+hex+'</option>';
}).join('');
}
function slotCopyColor(sel){
if(!sel.value) return;
var ci=document.getElementById('slot-edit-color');
if(!ci) return;
ci.value=sel.value;
document.getElementById('slot-edit-preview').style.background=sel.value;
sel.selectedIndex=0;
}
// ───────────────────────────────────────────────────────────────────────────
function openSlotEdit(i){
var slot=(window._amsSlots||[])[i]||{};
var globalIdx=slot.global_index!=null?slot.global_index:(slot.index!=null?slot.index:i);
@@ -1507,6 +1614,9 @@ function openSlotEdit(i){
var ci=document.getElementById('slot-edit-color');
ci.value=hex;
document.getElementById('slot-edit-preview').style.background=hex;
_initPickr(hex);
_renderSwatches();
_renderCopyFromSlot(globalIdx);
var mat=(slot.type||'PLA').toUpperCase();
document.getElementById('slot-edit-mat').value=mat;
var btns=document.getElementById('slot-mat-btns');
@@ -1631,6 +1741,7 @@ function hexToRgb(hex){
}
function saveSlotEdit(){
var hex=document.getElementById('slot-edit-color').value;
_addSwatch(hex);
var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA';
var color=hexToRgb(hex);
var slotIdx=_slotEditIndex;

View File

@@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>KX-Bridge</title>
<link rel="stylesheet" href="/kx/ui/style.css">
<link rel="stylesheet" href="/kx/ui/lib/pickr-nano.min.css">
<script src="/kx/ui/lib/pickr.min.js"></script>
<body>
<div id="conn-error-banner" style="display:none;background:#c0392b;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:999;"></div>
@@ -46,15 +48,26 @@
<span class="modal-title" id="slot-edit-title"></span>
<button onclick="closeSlotEdit()" style="background:none;border:none;color:var(--txt2);font-size:20px;cursor:pointer;line-height:1"></button>
</div>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
<div id="slot-edit-preview" style="width:56px;height:56px;border-radius:50%;border:3px solid rgba(255,255,255,.2);flex-shrink:0"></div>
<div style="flex:1">
<div style="font-size:11px;color:var(--txt2);margin-bottom:4px" id="lbl-slot-color"></div>
<input type="color" id="slot-edit-color"
oninput="document.getElementById('slot-edit-preview').style.background=this.value"
style="width:100%;height:36px;border:1px solid var(--border);border-radius:6px;background:var(--raised);cursor:pointer;padding:2px">
<div style="display:flex;align-items:flex-start;gap:16px;margin-bottom:12px">
<div id="slot-edit-preview" style="width:56px;height:56px;border-radius:50%;border:3px solid rgba(255,255,255,.2);flex-shrink:0;margin-top:4px"></div>
<div style="flex:1;min-width:0">
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-color"></div>
<!-- Pickr anchor — JS mounts the picker here -->
<div id="slot-pickr-anchor"></div>
<!-- hidden input keeps the hex value for saveSlotEdit() -->
<input type="hidden" id="slot-edit-color">
</div>
</div>
<!-- Recent color swatches (max 16, localStorage) -->
<div id="slot-color-swatches" style="display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px"></div>
<!-- Copy from slot -->
<div id="slot-copy-row" style="display:none;margin-bottom:16px">
<select id="slot-copy-select"
style="width:100%;padding:5px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px;box-sizing:border-box"
onchange="slotCopyColor(this)">
<option value="" id="lbl-slot-copy-from">Copy color from slot…</option>
</select>
</div>
<div style="margin-bottom:20px">
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-material"></div>
<div style="display:flex;flex-wrap:wrap;gap:6px" id="slot-mat-btns">
@@ -589,7 +602,7 @@
<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>
<label id="lbl-spoolman-sync-rate">Sync-Rate (s, 0=Druckende)</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)">

File diff suppressed because one or more lines are too long

3
web/themes/default/lib/pickr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -15,10 +15,46 @@
body{background:var(--bg);color:var(--txt);font-family:var(--font);font-size:14px;min-height:100vh;display:flex;flex-direction:column}
a{color:var(--accent);text-decoration:none}
/* select/option-Farben explizit setzen — OrcaSlicers Device-Tab-Webview erbt
sie sonst nicht und rendert weiße Schrift auf weißem Grund (Issue #29). */
select{background:var(--raised)!important;color:var(--txt)!important}
sie sonst nicht und rendert weiße Schrift auf weißem Grund (Issue #29).
Einheitliches Styling für alle Dropdowns im gesamten UI. */
select{
background:var(--raised)!important;
color:var(--txt)!important;
border:1px solid var(--border)!important;
border-radius:8px!important;
padding:6px 10px!important;
font-size:13px!important;
appearance:none!important;
-webkit-appearance:none!important;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23888' d='M6 8L0 0h12z'/%3E%3C/svg%3E")!important;
background-repeat:no-repeat!important;
background-position:right 10px center!important;
padding-right:28px!important;
cursor:pointer!important;
outline:none!important;
box-sizing:border-box!important;
}
select:focus{border-color:var(--accent)!important;box-shadow:0 0 0 2px rgba(0,200,255,0.18)!important}
select option{background:var(--card)!important;color:var(--txt)!important}
/* Einheitliches Styling für Text/Number-Inputs */
input[type=text],input[type=number],input[type=url],input[type=password],input[type=email],input[type=search]{
background:var(--raised);
color:var(--txt);
border:1px solid var(--border);
border-radius:8px;
padding:6px 10px;
font-size:13px;
outline:none;
box-sizing:border-box;
}
input[type=text]:focus,input[type=number]:focus,input[type=url]:focus,
input[type=password]:focus,input[type=email]:focus,input[type=search]:focus{
border-color:var(--accent);
box-shadow:0 0 0 2px rgba(0,200,255,0.18);
}
input::placeholder{color:var(--txt2);opacity:1}
/* ── HEADER ── */
header{background:var(--card);border-bottom:1px solid var(--border);
display:flex;align-items:center;gap:12px;padding:0 20px;height:52px;
@@ -265,6 +301,8 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:
.modal-field input{background:var(--raised);border:1px solid var(--border);
border-radius:7px;color:var(--txt);padding:7px 10px;font-size:13px;width:100%}
.modal-field input:focus{outline:none;border-color:var(--accent)}
.set-row{display:flex;flex-direction:column;gap:4px;margin-bottom:10px}
.set-row label{font-size:12px;color:var(--txt2)}
.poll-btns{display:flex;gap:8px}
.poll-btn{flex:1;padding:7px;background:var(--raised);border:1px solid var(--border);
border-radius:7px;color:var(--txt2);cursor:pointer;font-size:13px;transition:all .15s}

View File

@@ -123,7 +123,7 @@
"lbl_light": "💡 Licht",
"lbl_remaining": "Restzeit:",
"lbl_slicer_time": "Slicer-Schätzung:",
"lbl_spoolman_sync_rate": "Sync-Rate (s, 0=aus)",
"lbl_spoolman_sync_rate": "Sync-Rate (s, 0=Druckende)",
"lbl_spoolman_url": "Server-URL",
"lbl_unload": "Ausziehen",
"lbl_zpos": "Z (mm)",
@@ -324,5 +324,6 @@
"update_docker_copied": "Kopiert! Ausführen: docker compose pull && docker compose up -d",
"update_error": "Fehler",
"update_none": "Bereits aktuell",
"update_restarting": "Starte neu..."
"update_restarting": "Starte neu...",
"slot_copy_from": "Farbe von Slot kopieren…"
}

View File

@@ -123,7 +123,7 @@
"lbl_light": "💡 Light",
"lbl_remaining": "Remaining:",
"lbl_slicer_time": "Slicer estimate:",
"lbl_spoolman_sync_rate": "Sync rate (s, 0=off)",
"lbl_spoolman_sync_rate": "Sync rate (s, 0=end of print)",
"lbl_spoolman_url": "Server URL",
"lbl_unload": "Unload",
"lbl_zpos": "Z (mm)",
@@ -324,5 +324,6 @@
"update_docker_copied": "Copied! Run: docker compose pull && docker compose up -d",
"update_error": "Error",
"update_none": "Already up to date",
"update_restarting": "Restarting..."
"update_restarting": "Restarting...",
"slot_copy_from": "Copy color from slot…"
}

View File

@@ -123,7 +123,7 @@
"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_sync_rate": "Tasa de sincronización (s, 0=fin impresión)",
"lbl_spoolman_url": "URL del servidor",
"lbl_unload": "Descargar",
"lbl_zpos": "Z (mm)",
@@ -324,5 +324,6 @@
"update_docker_copied": "Copiado. Ejecutar: docker compose pull && docker compose up -d",
"update_error": "Error",
"update_none": "Ya actualizado",
"update_restarting": "Reiniciando..."
"update_restarting": "Reiniciando...",
"slot_copy_from": "Copiar color del slot…"
}

View File

@@ -123,7 +123,7 @@
"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_sync_rate": "Taux de sync. (s, 0=fin impression)",
"lbl_spoolman_url": "URL du serveur",
"lbl_unload": "Décharger",
"lbl_zpos": "Z (mm)",
@@ -324,5 +324,6 @@
"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…"
"update_restarting": "Redémarrage…",
"slot_copy_from": "Copier la couleur du slot…"
}

View File

@@ -123,7 +123,7 @@
"lbl_light": "💡 Luce",
"lbl_remaining": "Rimanente:",
"lbl_slicer_time": "Stima slicer:",
"lbl_spoolman_sync_rate": "Frequenza sync (s, 0=disatt.)",
"lbl_spoolman_sync_rate": "Frequenza sync (s, 0=fine stampa)",
"lbl_spoolman_url": "URL server",
"lbl_unload": "Rimuovi",
"lbl_zpos": "Z (mm)",
@@ -324,5 +324,6 @@
"update_docker_copied": "Copiato! Eseguire: docker compose pull && docker compose up -d",
"update_error": "Errore",
"update_none": "Già aggiornato",
"update_restarting": "Riavvio in corso..."
"update_restarting": "Riavvio in corso...",
"slot_copy_from": "Copia colore dallo slot…"
}

View File

@@ -123,7 +123,7 @@
"lbl_light": "💡 灯光",
"lbl_remaining": "剩余时间:",
"lbl_slicer_time": "切片预估:",
"lbl_spoolman_sync_rate": "同步频率0=关闭",
"lbl_spoolman_sync_rate": "同步频率0=打印结束",
"lbl_spoolman_url": "服务器地址",
"lbl_unload": "退料",
"lbl_zpos": "Z (mm)",
@@ -324,5 +324,6 @@
"update_docker_copied": "已复制执行docker compose pull && docker compose up -d",
"update_error": "错误",
"update_none": "已是最新版本",
"update_restarting": "重启中..."
"update_restarting": "重启中...",
"slot_copy_from": "从插槽复制颜色…"
}