Compare commits

..

4 Commits

Author SHA1 Message Date
Phil Merricks
d7f4983d0d fix: slot assignment dialog now matches "PLA Silk", "Matte PLA" etc. to PLA slots
_normalizeMaterialKey now scans all space-separated words in a slot label and
returns the first known base material type found. This handles both "modifier
first" ("Matte PLA") and "modifier last" ("PLA Silk", "PLA Matte") patterns,
which arise when users label slots with full product-style names while OrcaSlicer
writes only the base type (PLA, PETG, …) in GCode comments.

Dash-suffix composites ("PLA-CF", "PETG-CF") contain no space and are unchanged,
preserving correct incompatibility with their base types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 20:47:45 +01:00
891b679230 .gitea/ISSUE_TEMPLATE/bug_report.yml hinzugefügt 2026-06-18 15:01:05 +02:00
8ae218321f .gitea/ISSUE_TEMPLATE/bug_report.ym hinzugefügt 2026-06-18 14:56:56 +02:00
ada027bc04 .gitea/ISSUE_TEMPLATE/bug_report.yml 2026-06-18 14:54:02 +02:00
11 changed files with 368 additions and 681 deletions

View File

@@ -0,0 +1,82 @@
name: "Bug Report"
about: "Report a problem or error"
title: "[BUG] "
labels:
- bug
body:
- type: dropdown
id: os
attributes:
label: "Operating System"
options:
- "Windows 10"
- "Windows 11"
- "Ubuntu / Debian"
- "Fedora / RHEL"
- "Arch Linux"
- "macOS"
- "Other"
validations:
required: true
- type: dropdown
id: install_type
attributes:
label: "Installation Type"
options:
- "Docker / Docker Compose"
- "EXE / Binary (Windows)"
- "Binary (Linux)"
validations:
required: true
- type: dropdown
id: slicer
attributes:
label: "Slicer"
options:
- "OrcaSlicer (KX-Patch)"
- "OrcaSlicer (Standard)"
- "BambuStudio"
- "PrusaSlicer"
- "Other"
validations:
required: true
- type: input
id: slicer_version
attributes:
label: "Slicer Version"
placeholder: "e.g. 2.4.0-dev-kx1"
validations:
required: true
- type: input
id: bridge_version
attributes:
label: "KX-Bridge Version"
placeholder: "e.g. v0.9.20"
description: "Found in the web interface or in the logs at startup."
validations:
required: true
- type: textarea
id: description
attributes:
label: "Problem Description"
description: "What is happening? What did you expect to happen?"
placeholder: |
What happened:
What was expected:
validations:
required: true
- type: textarea
id: logs
attributes:
label: "Logs / Error Message"
description: "Relevant output from the KX-Bridge logs. Docker: `docker logs <container>`"
render: text
validations:
required: false

View File

@@ -0,0 +1,85 @@
name: Bug Report
about: File a bug report
title: "[Bug]: "
body:
- type: markdown
attributes:
value: |
Please fill out all required fields to help us resolve your issue faster.
- type: dropdown
id: os
attributes:
label: Operating System
options:
- Windows 10
- Windows 11
- Ubuntu / Debian
- Fedora / RHEL
- Arch Linux
- macOS
- Other
validations:
required: true
- type: dropdown
id: install_type
attributes:
label: Installation Type
options:
- Docker / Docker Compose
- EXE / Binary (Windows)
- Binary (Linux)
validations:
required: true
- type: dropdown
id: slicer
attributes:
label: Slicer
options:
- OrcaSlicer (KX-Patch)
- OrcaSlicer (Standard)
- BambuStudio
- PrusaSlicer
- Other
validations:
required: true
- type: input
id: slicer_version
attributes:
label: Slicer Version
placeholder: "e.g. 2.4.0-dev-kx1"
validations:
required: true
- type: input
id: bridge_version
attributes:
label: KX-Bridge Version
description: "Found in the web interface or in the logs at startup."
placeholder: "e.g. v0.9.20"
validations:
required: true
- type: textarea
id: description
attributes:
label: Problem Description
description: What is happening? What did you expect to happen?
placeholder: |
What happened:
What was expected:
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs / Error Message
description: "Relevant output from the KX-Bridge logs. Docker: `docker logs <container>`"
render: shell
validations:
required: false

View File

@@ -1,21 +1,5 @@
# Changelog
## [0.9.26] 2026-06-21
### Neu
- **Italienische Sprachunterstützung** (PR #66, @Alex_M). Die Bridge-UI ist jetzt vollständig auf Italienisch verfügbar.
### Behoben
- **Kamera startete immer beim Druckbeginn** (Issue #50). `camera_on_print` fehlte in der `/api/state`-Antwort — JavaScript las `undefined` und startete die Kamera unabhängig vom Setting. Jetzt korrekt im State enthalten.
- **Auto-Leveling-Setting wurde im Moonraker-Druckpfad ignoriert** (Issue #57). `handle_print_start` las den Wert nur aus den Bridge-Args, nicht aus dem Request-Body — Dialog-Checkbox und Per-Print-Override hatten keine Wirkung. Verhält sich jetzt identisch zum direkten Druckpfad.
- **Filament-Mapping: Freitext-Felder durch Dropdowns ersetzt** (Issue #57). Falsch getippte Vendor/Name-Kombination brach das Profil-Matching ohne Fehlermeldung; Felder sind jetzt Dropdowns (Vendor → Profil, vendor-gefiltert), sodass nur gültige Kombinationen gespeichert werden können.
- **Dashboard zeigte generischen Materialtyp statt Profilname** (Issue #57). AMS-Slot-Karten zeigen jetzt den gemappten Profilnamen (z.B. „eSUN PLA-Basic") statt nur „PLA". Fallback auf generischen Typ wenn kein Profil gemappt ist.
- **Ghost-Profil auf leerem Slot** (Issue #57). Verwaiste Mappings für leere Slots wurden weiterhin angezeigt; leere Slots zeigen jetzt korrekt „–".
- **Skip-Objects-Panel fehlte im Orca-Upload-Flow** (Issue #57). Panel erscheint jetzt in allen Druckflows; bei frischem Upload fragt die Bridge `fileDetails` beim Drucker nach und pollt die Objektliste bis zu 6 Sekunden nach.
- **Banner und Dialog erschienen gleichzeitig** (Issue #57). Settings-Save setzt jetzt den Dialog-Cancel-State zurück, sodass der Slot-Mapper nach Wechsel des Start-Print-Verhaltens zuverlässig öffnet.
- **„Leeren" lud idle-Datei beim nächsten Poll nach** (Issue #57). Leeren setzt jetzt den lokalen State sofort zurück (`file_ready`, `filename`, `thumbnail`) und löscht alle Dialog-Sperren — Vorschaubild und Aktions-Buttons verschwinden sofort und kommen nicht zurück.
- **Material-Matching für „PLA Silk", „Matte PLA" etc.** (PR #64, @p2l). Modifier+Basis-Muster in beliebiger Wortreihenfolge werden jetzt auf den Basis-Typ normalisiert; Dash-Varianten (PLA-CF) bleiben weiterhin korrekt inkompatibel mit ihrem Basis-Typ.
## [0.9.25] 2026-06-17
### Behoben

View File

@@ -1,40 +1,5 @@
# Changelog
## [0.9.26] 2026-06-21
### New
- **Italian language support** (PR #66, @Alex_M). The bridge UI is now fully
available in Italian.
### Fixed
- **Camera always started at print begin** (issue #50). `camera_on_print` was
missing from the `/api/state` response — JavaScript read `undefined` and started
the camera regardless of the setting. Now correctly exposed in state.
- **Auto-leveling setting ignored in Moonraker print path** (issue #57).
`handle_print_start` read the value only from bridge args, not from the request
body, so the dialog checkbox and the per-print override had no effect. Now
behaves identically to the direct print path.
- **Filament mapping free-text fields replaced by dropdowns** (issue #57). A
mistyped vendor/name broke profile matching silently; fields are now dropdowns
(vendor → profile, vendor-filtered) so only valid combinations can be saved.
- **Dashboard showed generic material type instead of profile name** (issue #57).
AMS slot cards now display the mapped profile name (e.g. "eSUN PLA-Basic")
instead of just "PLA". Falls back to the generic type when no profile is mapped.
- **Ghost profile shown on empty slot** (issue #57). Stale mappings for empty
slots were still rendered; empty slots now correctly show "".
- **Skip-Objects panel missing in Orca upload flow** (issue #57). Panel now
appears in all print flows; on fresh upload the bridge requests `fileDetails`
from the printer and retries the object list for up to 6 s.
- **Banner and dialog appeared simultaneously** (issue #57). Settings save now
resets the dialog cancel state so the slot mapper reliably opens after toggling
Start Print Behavior.
- **"Clear" reloaded idle file on next poll** (issue #57). Clear now immediately
resets local state (`file_ready`, `filename`, `thumbnail`) and clears all dialog
locks — the preview and action buttons disappear instantly and do not return.
- **Material matching for "PLA Silk", "Matte PLA" etc.** (PR #64, @p2l).
Modifier+base patterns in any word order are now normalised to the base type;
dash-suffix variants (PLA-CF) remain correctly incompatible with their base.
## [0.9.25] 2026-06-17
### Fixed

View File

@@ -1 +1 @@
0.9.26
0.9.25

82
bug_report.yml Normal file
View File

@@ -0,0 +1,82 @@
name: "Bug Report"
about: "Problem oder Fehler melden"
title: "[BUG] "
labels:
- bug
body:
- type: dropdown
id: os
attributes:
label: "Betriebssystem"
options:
- "Windows 10"
- "Windows 11"
- "Ubuntu / Debian"
- "Fedora / RHEL"
- "Arch Linux"
- "macOS"
- "Anderes"
validations:
required: true
- type: dropdown
id: install_type
attributes:
label: "Installationsart"
options:
- "Docker / Docker Compose"
- "EXE / Binary (Windows)"
- "Binary (Linux)"
validations:
required: true
- type: dropdown
id: slicer
attributes:
label: "Slicer"
options:
- "OrcaSlicer (KX-Patch)"
- "OrcaSlicer (Standard)"
- "BambuStudio"
- "PrusaSlicer"
- "Anderer"
validations:
required: true
- type: input
id: slicer_version
attributes:
label: "Slicer Version"
placeholder: "z.B. 2.4.0-dev-kx1"
validations:
required: true
- type: input
id: bridge_version
attributes:
label: "KX-Bridge Version"
placeholder: "z.B. v0.9.20"
description: "Zu finden im Webinterface oder in den Logs beim Start."
validations:
required: true
- type: textarea
id: description
attributes:
label: "Problembeschreibung"
description: "Was passiert? Was hast du erwartet?"
placeholder: |
Was ist passiert:
Was wurde erwartet:
validations:
required: true
- type: textarea
id: logs
attributes:
label: "Logs / Fehlermeldung"
description: "Relevante Ausgabe aus den KX-Bridge Logs. Docker: `docker logs <container>`"
render: text
validations:
required: false

View File

@@ -48,6 +48,4 @@ MODE_ID = get("MODE_ID", "")
DEVICE_ID = get("DEVICE_ID", "")
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT", "0"))
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))

View File

@@ -1077,25 +1077,6 @@ class KobraXBridge:
if (not skipped and self._pending_preprint_skip
and now <= self._pending_preprint_skip_deadline):
return
# Während eines aktiven Drucks sind Skip-Zustände effektiv monoton.
# Manche Firmware-Reports kommen zwischenzeitlich leer/teilweise zurück;
# diese dürfen bereits bestätigte Skip-Objekte nicht aus der UI löschen.
existing_skipped = [str(n) for n in (self._skip_state.get("skipped") or []) if n]
existing_set = set(existing_skipped)
incoming_skipped = [str(n) for n in (skipped or []) if n]
incoming_set = set(incoming_skipped)
active_print = self._state.get("print_state") in ("printing", "paused")
if active_print and existing_set:
if not incoming_set:
skipped = list(existing_skipped)
elif not incoming_set.issuperset(existing_set):
merged = list(existing_skipped)
for n in incoming_skipped:
if n not in existing_set:
merged.append(n)
skipped = merged
# Pending-Lock aufheben sobald Drucker die gewünschten Objekte bestätigt
if self._pending_preprint_skip and set(skipped) >= set(self._pending_preprint_skip):
self._pending_preprint_skip = []
@@ -1142,7 +1123,7 @@ class KobraXBridge:
return False
for i in range(max(1, int(retries))):
try:
if self._state.get("print_state") not in ("printing", "paused"):
if self._state.get("kobra_state") != "printing":
time.sleep(max(0.1, float(delay_s)))
continue
resp = self.client.skip_objects(wanted)
@@ -2472,18 +2453,6 @@ class KobraXBridge:
names = json.loads(f.get("objects_skip_parts") or "[]")
except Exception:
names = []
# Noch keine Objekte im Store (frischer Orca-/Web-Upload): einmal aktiv
# file/fileDetails beim Drucker anfragen. _on_file() füllt den Store nach,
# das Frontend pollt diesen Endpoint und bekommt die Liste beim nächsten
# Versuch (Issue #57 — Skip-Parität auch außerhalb des File-Browsers).
if not names:
fn = f.get("filename") or ""
if fn:
try:
self.client.publish("file", "fileDetails",
{"root": "local", "filename": fn}, timeout=0)
except Exception as e:
log.debug(f"fileDetails-Nachfrage fehlgeschlagen: {e}")
return self._json_cors({
"result": {
"names": names,
@@ -2510,47 +2479,18 @@ class KobraXBridge:
return self._json_cors({"error": str(e)}, status=502)
return self._json_cors({"result": "ok", "names": names})
def _build_skip_state_result(self) -> dict:
"""Baut den kombinierten Skip-State für UI-Endpunkte."""
filename = self._state.get("filename", "")
all_objects: list[str] = []
svg = ""
if filename:
try:
f = self._store.get_file_by_name(filename)
if f:
all_objects = json.loads(f.get("objects_skip_parts") or "[]")
svg = f.get("svg_image") or ""
except Exception as e:
log.warning(f"skip_state lookup failed: {e}")
return {
"objects": all_objects,
"skipped": list(self._skip_state.get("skipped", [])),
"svg_b64": svg,
"ts": self._skip_state.get("ts", 0),
"filename": filename,
}
async def handle_kx_skip_query(self, request):
"""Druck-Objektliste vom Drucker neu abfragen.
POST /kx/skip/query → triggert skip/query_obj, wartet kurz auf den
async skip/report und gibt den zusammengeführten Skip-State zurück.
POST /kx/skip/query → triggert skip/query_obj, gibt zuletzt bekannten
Stand zurück (skip/report kommt async, Frontend pollt /kx/skip/state).
"""
prev_ts = int(self._skip_state.get("ts", 0) or 0)
try:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, lambda: self.client.query_skip_objects())
except Exception as e:
return self._json_cors({"error": str(e)}, status=502)
deadline = time.time() + 1.5
while time.time() < deadline:
if int(self._skip_state.get("ts", 0) or 0) > prev_ts:
break
await asyncio.sleep(0.1)
return self._json_cors({"result": self._build_skip_state_result()})
return self._json_cors({"result": self._skip_state})
async def handle_kx_skip_state(self, request):
"""Aktueller Skip-State.
@@ -2562,7 +2502,25 @@ class KobraXBridge:
nicht die Gesamtliste.
- Geskippt: aus self._skip_state (von skip/report aktualisiert).
"""
return self._json_cors({"result": self._build_skip_state_result()})
filename = self._state.get("filename", "")
all_objects: list[str] = []
svg = ""
if filename:
try:
f = self._store.get_file_by_name(filename)
if f:
all_objects = json.loads(f.get("objects_skip_parts") or "[]")
svg = f.get("svg_image") or ""
except Exception as e:
log.warning(f"skip_state lookup failed: {e}")
result = {
"objects": all_objects,
"skipped": list(self._skip_state.get("skipped", [])),
"svg_b64": svg,
"ts": self._skip_state.get("ts", 0),
"filename": filename,
}
return self._json_cors({"result": result})
async def handle_kx_printers(self, request):
# Aktive Drucker (mit IP) sammeln
@@ -2657,8 +2615,8 @@ class KobraXBridge:
},
}
# UI erst nach echter Drucker-Bestätigung als "geskippt" markieren.
self._skip_state = {"skipped": [], "ts": int(time.time())}
# Pre-Print-Skip sofort im UI-Status spiegeln
self._skip_state = {"skipped": list(excluded_objects), "ts": int(time.time())}
if excluded_objects:
self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n]
self._pending_preprint_skip_deadline = time.time() + 12.0
@@ -3181,8 +3139,7 @@ class KobraXBridge:
ams_box_mapping = self._build_auto_ams_box_mapping()
use_ams = len(ams_box_mapping) > 0
# Dialog-Checkbox (body) hat Vorrang, sonst Setting-Default (wie handle_kx_print).
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
auto_leveling = getattr(self._args, "auto_leveling", 1)
url = self._state.get("last_upload_url", "")
filesize = self._state.get("last_upload_size", 0)
md5 = self._state.get("last_upload_md5", "")
@@ -3212,15 +3169,6 @@ class KobraXBridge:
},
}
# UI erst nach echter Drucker-Bestätigung als "geskippt" markieren.
self._skip_state = {"skipped": [], "ts": int(time.time())}
if excluded_objects:
self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n]
self._pending_preprint_skip_deadline = time.time() + 12.0
else:
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
log.info(
f"print/start api=1 mode={self._filament_mode} "
f"ams={len(ams_box_mapping)} slots assignments={filament_assignments is not None}"
@@ -3233,9 +3181,6 @@ class KobraXBridge:
if result is None:
return web.json_response({"error": "Keine Antwort vom Drucker"}, status=504)
if excluded_objects:
loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects))
return web.json_response({"result": "ok"})
async def handle_print_pause(self, request):
@@ -3870,8 +3815,6 @@ class KobraXBridge:
"camera_url": s["camera_url"],
"fan_speed": s["fan_speed"],
"print_speed_mode": s["print_speed_mode"],
"auto_leveling": getattr(self._args, "auto_leveling", 1),
"camera_on_print": getattr(self._args, "camera_on_print", 0),
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
"light_on": s["light_on"],
"light_brightness": s["light_brightness"],

View File

@@ -9,10 +9,9 @@ var camOn=false;
var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print
var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support)
var _lastLoadedFile=null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55)
var _idleCleared=false; // User hat idle-Datei explizit „geleert" → kein Nachladen von s.filename (Issue #57)
var _fdDialogOpen=false; // Dialog ist gerade offen
var _fdAutoOpenedFile=sessionStorage.getItem('fdAutoOpenedFile')||null;
var _fdUserCancelled=sessionStorage.getItem('fdUserCancelled')==='1';
var _fdAutoOpenedFile=null; // Dateiname für den der Dialog auto-geöffnet wurde
var _fdUserCancelled=false; // User hat den Auto-Open-Dialog abgebrochen
var currentStep=1;
var currentPanel='dashboard';
var aceAutoRefillPrefs=(function(){
@@ -109,7 +108,6 @@ function _langToggleLabel(lang){
if(lang==='de')return 'Deutsch';
if(lang==='en')return 'English';
if(lang==='fr')return 'Français';
if(lang==='it')return 'Italiano';
if(lang==='zh-cn')return '简体中文';
return 'Espanol';
}
@@ -117,10 +115,10 @@ function _langToggleLabel(lang){
function _mapSupportedLang(lang){
if(!lang)return '';
var l=String(lang).toLowerCase().replace(/_/g,'-').trim();
if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='it'||l==='zh-cn')return l;
if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='zh-cn')return l;
var base=l.split('-')[0];
if(base==='de'||base==='en'||base==='es'||base==='fr'||base==='it')return base;
if(base==='de'||base==='en'||base==='es'||base==='fr')return base;
if(base==='zh'){
if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn';
@@ -666,18 +664,10 @@ function applyState(){
if(s.file_ready&&s.print_state==='standby'){
document.getElementById('file-ready-name').textContent=s.file_ready;
// Neue Datei → Abbruch-Sperre aufheben
if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready){
_fdUserCancelled=false;
sessionStorage.removeItem('fdUserCancelled');
sessionStorage.removeItem('fdAutoOpenedFile');
}
if(shouldAutoOpen){
// Dialog-Modus: Banner niemals anzeigen.
frb.style.display='none';
if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){
_fdAutoOpenedFile=s.file_ready;
startReadyFileWithSlots(s.file_ready,true);
}
if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready) _fdUserCancelled=false;
if(shouldAutoOpen&&!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){
_fdAutoOpenedFile=s.file_ready;
startReadyFileWithSlots(s.file_ready,true);
} else {
frb.style.display='flex';
bannerVisible=true;
@@ -696,11 +686,8 @@ function applyState(){
// Zuletzt geladene Datei merken (Issue #55): solange sie über den State
// sichtbar ist. Beim Druckende/Abbruch leert die Bridge file_ready+filename
// (Issue #29) — die gemerkte Referenz bleibt für die Karten-Aktionen.
// Echte ready-Datei oder laufender Druck hebt einen vorherigen „Clear" auf.
if(s.file_ready||printing) _idleCleared=false;
if(s.file_ready) _lastLoadedFile=s.file_ready;
else if(s.filename && !_idleCleared) _lastLoadedFile=s.filename;
else if(_idleCleared) _lastLoadedFile=null;
else if(s.filename) _lastLoadedFile=s.filename;
// Idle-Aktionen (Drucken/Slots/Leeren) nur wenn nicht gedruckt wird, eine
// Datei bekannt ist und der grüne Banner nicht ohnehin schon dieselbe Aktion
// anbietet.
@@ -877,13 +864,7 @@ function applyState(){
var activity=(slot.activity||'');
var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'');
var slotLabel=T.label_slot+' '+(globalIdx+1);
// Gemapptes Profil nur für belegte Slots verwenden — sonst zeigt ein
// verwaistes Mapping (Slot wurde geleert) ein „Geister"-Profil (Issue #57).
var profile=empty?null:(window._slotProfileMap||{})[globalIdx];
var genericType=(slot.type||slot.material_type||'');
// Material-Label: bei belegtem Slot mit Mapping den konkreten Profilnamen
// (z.B. „eSUN PLA+") statt nur des generischen Typs zeigen (Issue #57 Punkt 4).
var materialLabel=empty?'':((profile&&profile.name)?profile.name:genericType);
var profile=(window._slotProfileMap||{})[globalIdx];
var vendorBadge='';
if(!empty && profile && profile.vendor){
var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':'');
@@ -892,7 +873,7 @@ function applyState(){
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>'
+'<div class="slot-material" title="'+(empty?'':genericType)+'">'+materialLabel+'</div>'
+'<div class="slot-material">'+(empty?'':(slot.type||slot.material_type||''))+'</div>'
+vendorBadge
+'<div class="slot-label">'+slotLabel+'</div>'
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
@@ -914,7 +895,7 @@ function applyState(){
if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none';
// auto-start camera during print (unless user explicitly stopped it)
if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped&&s.camera_on_print){
if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped){
camStart();
}
// reset user-stopped flag when print ends so next print auto-starts again
@@ -1030,11 +1011,6 @@ function onPollIntervalInput(){
}
// ── Filament-Profil-Mapping pro Slot ([filament_profiles]) ──
// Pro Slot ein einzelnes Profil-Dropdown (vendor+name gemeinsam, gekeyt per
// _profileKey). Kein Freitext mehr → das (vendor,name)→id-Matching kann nicht
// mehr durch manuelle Eingabe brechen (Issue #57 Punkt 1). Optionen werden aus
// /kx/filament/profiles geladen, nach Vendor gruppiert, User-Profile zuerst,
// mit demselben Vendor-Sichtbarkeitsfilter wie das Slot-Edit-Dropdown.
function renderFilamentMapping(map){
var el=document.getElementById('filament-mapping-list');
if(!el)return;
@@ -1044,61 +1020,21 @@ function renderFilamentMapping(map){
var idHint=m.id?' <span style="color:var(--txt2);font-size:11px">('+m.id+')</span>':'';
rows+='<div class="modal-field" style="margin-bottom:8px">'
+'<label>Slot '+(i+1)+idHint+'</label>'
+'<select id="fmap-'+i+'" data-vendor="'+(m.vendor||'')+'" data-name="'+(m.name||'')+'" style="width:100%"></select>'
+'</div>';
+'<div style="display:flex;gap:6px;flex-wrap:wrap">'
+'<input type="text" id="fmap-'+i+'-vendor" placeholder="Vendor (z.B. Polymaker)" value="'+(m.vendor||'')+'" style="flex:1;min-width:120px">'
+'<input type="text" id="fmap-'+i+'-name" placeholder="Name (z.B. PolyTerra PLA)" value="'+(m.name||'')+'" style="flex:1;min-width:140px">'
+'</div></div>';
}
el.innerHTML=rows;
// Dropdowns befüllen (async, geteilter Profil-Cache + Vendor-Filter)
for(var j=0;j<4;j++){ _fillMappingDropdown(j); }
}
function _fillMappingDropdown(slot){
var sel=document.getElementById('fmap-'+slot);
if(!sel) return;
var wantKey=_profileKey(sel.dataset.vendor, sel.dataset.name);
_loadOrcaFilaments(function(profiles){
sel.innerHTML='<option value="">'+(tr('slot_edit_profile_default')||'Generic (Standard)')+'</option>';
var userProfs=profiles.filter(function(p){return p.is_user;});
var systemProfs=profiles.filter(function(p){return !p.is_user;});
function _opt(g,p){
var o=document.createElement('option');
o.value=_profileKey(p.vendor,p.name);
o.dataset.vendor=p.vendor; o.dataset.name=p.name; o.dataset.id=p.id||'';
o.textContent=(p.is_user?'★ ':'')+p.name+(p.vendor?' — '+p.vendor:'');
if(o.value===wantKey)o.selected=true;
g.appendChild(o);
}
if(userProfs.length){
var gUser=document.createElement('optgroup');
gUser.label='★ '+(tr('orca_profile_user_label')||'Eigene Profile');
userProfs.forEach(function(p){_opt(gUser,p);});
sel.appendChild(gUser);
}
_loadVisibleVendors(function(vis){
var filtered=systemProfs;
if(vis&&vis.length){
var allow={};vis.forEach(function(v){allow[v]=1;});allow['Generic']=1;
filtered=systemProfs.filter(function(p){return allow[p.vendor];});
}
var byVendor={};
filtered.forEach(function(p){(byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p);});
Object.keys(byVendor).sort().forEach(function(v){
var g=document.createElement('optgroup');g.label=v;
byVendor[v].forEach(function(p){_opt(g,p);});
sel.appendChild(g);
});
});
});
}
function saveFilamentMapping(){
// Nutzt den per-Slot-Endpoint (vendor,name → ID-Lookup im Backend).
// Leere Auswahl ("") = Mapping entfernen.
// Leere Felder = Mapping entfernen.
var chain=Promise.resolve();
for(var i=0;i<4;i++){
(function(slot){
var sel=document.getElementById('fmap-'+slot);
var opt=sel?sel.options[sel.selectedIndex]:null;
var vendor=(opt&&opt.dataset.vendor)||'';
var name=(opt&&opt.dataset.name)||'';
var vendor=((document.getElementById('fmap-'+slot+'-vendor')||{}).value||'').trim();
var name=((document.getElementById('fmap-'+slot+'-name')||{}).value||'').trim();
chain=chain.then(function(){
return fetch(_apiUrl('/kx/filament/slots/'+slot+'/profile'),
{method:'POST',headers:{'Content-Type':'application/json'},
@@ -1394,39 +1330,23 @@ function slotEditFeed(){
}
function startReadyFile(filename){
var fn=filename||S.file_ready;
function _doStartReadyFile(){
var btn=document.getElementById('file-ready-btn');
if(btn){btn.disabled=true;btn.textContent='…';}
post('/printer/print/start',{filename:fn})
.then(function(r){return r.json();})
.then(function(){
document.getElementById('file-ready-banner').style.display='none';
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
})
.catch(function(e){
clog(tr('log_error')+' '+e,'msg-err');
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
});
}
function _gateAndStart(fileObj){
if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){
maybeGateWebUpload(fileObj, function(){ startReadyFile(fn); });
return;
}
_doStartReadyFile();
}
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
if(currentFile){
_gateAndStart(currentFile);
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
maybeGateWebUpload(currentFile, function(){ startReadyFile(fn); });
return;
}
fetch(_apiUrl('/kx/files')).then(function(r){return r.json();}).then(function(d){
storeFiles=d.result||[];
var refreshed=(storeFiles||[]).find(function(f){return f.filename===fn;})||null;
_gateAndStart(refreshed);
}).catch(function(){
_doStartReadyFile();
});
var btn=document.getElementById('file-ready-btn');
if(btn){btn.disabled=true;btn.textContent='…';}
post('/printer/print/start',{filename:fn})
.then(function(r){return r.json();})
.then(function(r){
document.getElementById('file-ready-banner').style.display='none';
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
})
.catch(function(e){
clog(tr('log_error')+' '+e,'msg-err');
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
});
}
function cancelReadyFile(){
post('/api/file_ready/clear',{})
@@ -1442,17 +1362,8 @@ function startIdleFileWithSlots(){
}
function clearIdleFile(){
_lastLoadedFile=null;
_idleCleared=true; // verhindert Nachladen von s.filename im nächsten poll() (Issue #57)
_fdAutoOpenedFile=null; // nächster Upload derselben Datei soll Dialog wieder öffnen
_fdUserCancelled=false;
_fdDialogOpen=false;
sessionStorage.removeItem('fdAutoOpenedFile');
sessionStorage.removeItem('fdUserCancelled');
sessionStorage.removeItem('webVerifyCancelledFileId');
S.file_ready=''; S.filename=''; S.thumbnail=''; // sofort lokal leeren, kein Warten auf nächsten Poll
var ib=document.getElementById('d-idle-btns');if(ib)ib.style.display='none';
var fn=document.getElementById('d-fname');if(fn){fn.textContent='';fn.title='';}
var thumb=document.getElementById('d-thumbnail');if(thumb){thumb.style.display='none';thumb.src='';}
post('/api/file_ready/clear',{}).catch(function(){});
}
function selectMatPreset(m){
@@ -1551,11 +1462,6 @@ function saveSettings(){
btn.disabled=true;btn.textContent='…';
var webUploadWarning=(document.getElementById('s-web-upload-warning')||{}).checked?1:0;
S.web_upload_warning=webUploadWarning;
// Start-Print-Behavior-Wechsel könnte den Auto-Open sonst dauerhaft blockieren
// (alter _fdUserCancelled bei gleicher file_ready) → Dialog-State zurücksetzen (Issue #57).
_fdUserCancelled=false;_fdAutoOpenedFile=null;
sessionStorage.removeItem('fdUserCancelled');sessionStorage.removeItem('fdAutoOpenedFile');
sessionStorage.removeItem('webVerifyCancelledFileId');
post('/api/settings',{
printer_name: document.getElementById('s-printer-name').value,
printer_ip: document.getElementById('s-printer-ip').value,
@@ -2227,7 +2133,6 @@ var _filamentDialogMode='store'; // 'store' oder 'banner'
var _pendingWebVerifyFileId=null;
var _pendingWebVerifyFilename='';
var _pendingWebVerifyAction=null;
var _pendingWebVerifyAutoOpen=false;
// GCode-Store-Dateiliste. MUSS deklariert sein sonst ReferenceError, wenn
// "Slots wählen" im Banner geklickt wird, bevor der Browser-Tab je geladen
// wurde (Issue #29 / Theme-Auslagerung PR #27).
@@ -2295,8 +2200,7 @@ function clearWebUploadWarningFlag(fileId, onDone){
});
}
function maybeGateWebUpload(fileObj, onContinue, opts){
opts=opts||{};
function maybeGateWebUpload(fileObj, onContinue){
if(!fileObj || !fileObj.web_unverified){
if(onContinue) onContinue();
return;
@@ -2305,20 +2209,15 @@ function maybeGateWebUpload(fileObj, onContinue, opts){
if(onContinue) onContinue();
return;
}
var cancelledId=sessionStorage.getItem('webVerifyCancelledFileId')||'';
if(opts.autoOpen && cancelledId && cancelledId===String(fileObj.id||'')){
return;
}
openWebVerifyDialog(fileObj.id, fileObj.filename, function(){
clearWebUploadWarningFlag(fileObj.id, onContinue);
}, !!opts.autoOpen);
});
}
function openWebVerifyDialog(fileId, filename, onConfirm, autoOpen){
function openWebVerifyDialog(fileId, filename, onConfirm){
_pendingWebVerifyFileId=fileId;
_pendingWebVerifyFilename=filename;
_pendingWebVerifyAction=onConfirm||null;
_pendingWebVerifyAutoOpen=!!autoOpen;
var status=document.getElementById('store-web-verify-status');
if(status){status.textContent='';}
openStoreWebVerifyDialog();
@@ -2332,13 +2231,9 @@ function openStoreWebVerifyDialog(){
function closeStoreWebVerifyDialog(){
var modal=document.getElementById('store-web-verify-dialog');
if(modal){modal.classList.remove('open');}
if(_pendingWebVerifyAutoOpen && _pendingWebVerifyFileId){
sessionStorage.setItem('webVerifyCancelledFileId', String(_pendingWebVerifyFileId));
}
_pendingWebVerifyFileId=null;
_pendingWebVerifyFilename='';
_pendingWebVerifyAction=null;
_pendingWebVerifyAutoOpen=false;
}
function confirmStoreWebVerify(){
@@ -2358,11 +2253,9 @@ function confirmStoreWebVerify(){
.then(function(){
var fileObj=(storeFiles||[]).find(function(f){return f.id===fileId;});
if(fileObj){fileObj.web_unverified=false;}
sessionStorage.removeItem('webVerifyCancelledFileId');
_pendingWebVerifyFileId=null;
_pendingWebVerifyFilename='';
_pendingWebVerifyAction=null;
_pendingWebVerifyAutoOpen=false;
closeStoreWebVerifyDialog();
loadStore();
if(typeof action==='function') action();
@@ -2378,7 +2271,7 @@ function startReadyFileWithSlots(filename,_autoOpen){
var fn=filename||S.file_ready;
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen});
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn); });
return;
}
_filamentDialogMode='banner';
@@ -2399,31 +2292,23 @@ function startReadyFileWithSlots(filename,_autoOpen){
});
}
function _proceedWithFileObj(fileObj){
if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){
// Verify-Gate war beim ersten Lookup noch nicht aktiv (storeFiles leer) — jetzt prüfen.
if(_autoOpen){_fdDialogOpen=false;}
maybeGateWebUpload(fileObj, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen});
return;
}
if(fileObj){
_storeFileId=fileObj.id;
_setGcodeFilamentsFromFileObj(fileObj);
}
openWithSlots();
}
var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
if(fileObj){
_proceedWithFileObj(fileObj);
_storeFileId=fileObj.id;
_setGcodeFilamentsFromFileObj(fileObj);
openWithSlots();
return;
}
// Fallback: refresh file list, then resolve current file by filename.
fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){
storeFiles=d.result||[];
var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;})||null;
_proceedWithFileObj(refreshed);
var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
if(refreshed){
_storeFileId=refreshed.id;
_setGcodeFilamentsFromFileObj(refreshed);
}
openWithSlots();
}).catch(function(){
openWithSlots();
});
@@ -2451,6 +2336,9 @@ function _normalizeMaterialKey(material){
// Handle modifier+base patterns in either order: "Matte PLA", "Silk PETG",
// "PLA Silk", "PLA Matte". OrcaSlicer always writes the base type in GCode
// (filament_type = PLA), but users label slots with the full product-style name.
// Scan each space-separated word; return the first one that is a known base material.
// Dash-suffix variants ("PLA-CF", "PETG-CF") contain no space and fall through
// unchanged, preserving correct incompatibility with their base types.
var trimmed=(material||'').trim();
if(trimmed.indexOf(' ')>=0){
var words=trimmed.toUpperCase().split(/\s+/);
@@ -2514,30 +2402,19 @@ function openFilamentDialog(slots){
if(objBody)objBody.style.display='none'; // immer eingeklappt starten
if(objArrow)objArrow.style.transform='';
if(_storeFileId){
// Bei frischem Orca-/Web-Upload liefert der Drucker die Objektliste
// (objects_skip_parts) erst per fileDetails nach → ist im Store kurz leer.
// Daher mehrfach nachfragen, bis Objekte da sind (Issue #57 Skip-Parität).
var _objFid=_storeFileId;
var _objTries=0;
(function _loadObjects(){
if(_objFid!==_storeFileId) return; // Dialog wechselte Datei → abbrechen
fetch(_apiUrl('/kx/files/'+encodeURIComponent(_objFid)+'/objects'))
.then(function(r){return r.json()})
.then(function(d){
var names=(d.result&&d.result.names)||[];
var svg=(d.result&&d.result.svg_b64)||'';
if(names.length>=2){
_printObjectsSvg=svg;
_printObjects=names.map(function(n){return {name:n,skip:false};});
renderObjectChecklist(); renderObjectSvg();
var cnt=document.getElementById('fd-objects-count');
if(cnt)cnt.textContent='('+names.length+')';
if(objSection)objSection.style.display='block';
} else if(_objTries++ < 6){
setTimeout(_loadObjects, 1000); // bis ~6s auf fileDetails warten
}
}).catch(function(){});
})();
fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects'))
.then(function(r){return r.json()})
.then(function(d){
var names=(d.result&&d.result.names)||[];
_printObjectsSvg=(d.result&&d.result.svg_b64)||'';
if(names.length>=2){
_printObjects=names.map(function(n){return {name:n,skip:false};});
renderObjectChecklist(); renderObjectSvg();
var cnt=document.getElementById('fd-objects-count');
if(cnt)cnt.textContent='('+names.length+')';
if(objSection)objSection.style.display='block';
}
}).catch(function(){});
}
// GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten
@@ -2629,11 +2506,7 @@ function closeFilamentDialog(){
var dlg=document.getElementById('filament-dialog');
if(dlg)dlg.classList.remove('open');
_fdDialogOpen=false;
if(_fdAutoOpenedFile){
_fdUserCancelled=true;
sessionStorage.setItem('fdUserCancelled','1');
sessionStorage.setItem('fdAutoOpenedFile',_fdAutoOpenedFile);
}
if(_fdAutoOpenedFile) _fdUserCancelled=true;
}
function confirmFilamentPrint(){
@@ -2677,41 +2550,19 @@ function confirmFilamentPrint(){
var fdAutoLeveling=fdAlEl?( fdAlEl.checked?1:0):(S.auto_leveling===undefined?1:S.auto_leveling?1:0);
closeFilamentDialog();
if(_filamentDialogMode==='banner'){
// Banner-Modus: /kx/print bevorzugen wenn _storeFileId bekannt (gleicher Pfad wie File-Browser).
// Banner-Modus: normaler print/start mit Slot-Override
var btn=document.getElementById('file-ready-btn');
if(btn){btn.disabled=true;btn.textContent='…';}
var startPromise;
if(_storeFileId){
startPromise=fetch(_apiUrl('/kx/print'),{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({
file_id:_storeFileId,
filament_assignments:assignments,
excluded_objects:excludedObjects,
auto_leveling:fdAutoLeveling
})
post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:fdAutoLeveling})
.then(function(r){return r.json();})
.then(function(){
document.getElementById('file-ready-banner').style.display='none';
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
})
.catch(function(e){
clog(tr('log_error')+' '+e,'msg-err');
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
});
}else{
startPromise=post('/printer/print/start',{
filename:S.file_ready||_storeFilename,
filament_assignments:assignments,
excluded_objects:excludedObjects,
auto_leveling:fdAutoLeveling
});
}
startPromise.then(function(r){
if(!r.ok){return r.text().then(function(t){throw new Error(t||('HTTP '+r.status));});}
return r.json();
}).then(function(d){
if(d&&d.error) throw new Error(d.error);
if(d&&d.result&&d.result!=='ok') throw new Error(String(d.result));
document.getElementById('file-ready-banner').style.display='none';
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
}).catch(function(e){
clog(tr('log_error')+' '+e,'msg-err');
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
});
} else {
// Store-Modus: POST /kx/print
fetch(_apiUrl('/kx/print'),{
@@ -2782,45 +2633,35 @@ function renderObjectSvg(){
// ── Mid-Print Skip ──
var _skipObjects=[]; // [{name, skipped, willSkip}]
var _skipSvg='';
var _skipPollTimer=null;
function _applySkipDialogState(s){
s=s||{};
_skipSvg=s.svg_b64||'';
var skipped=s.skipped||[];
// Pending-Auswahl (willSkip) beim Refresh erhalten.
var prevWillSkip={};
(_skipObjects||[]).forEach(function(o){if(o&&o.name)prevWillSkip[o.name]=!!o.willSkip;});
_skipObjects=(s.objects||[]).map(function(n){
var isSkipped=(skipped.indexOf(n)>=0);
return {name:n, skipped:isSkipped, willSkip:isSkipped?false:!!prevWillSkip[n]};
});
renderSkipList(); renderSkipSvg();
}
function openSkipDialog(){
document.getElementById('skip-status').textContent='';
document.getElementById('skip-confirm').disabled=false;
_refreshSkipDialog();
if(_skipPollTimer)clearInterval(_skipPollTimer);
_skipPollTimer=setInterval(function(){
var dlg=document.getElementById('skip-dialog');
if(!(dlg&&dlg.classList.contains('open')))return;
_refreshSkipDialog();
},2000);
document.getElementById('skip-dialog').classList.add('open');
}
function _refreshSkipDialog(){
// query-Endpoint wartet intern auf frischen skip/report (bis 1.5s).
fetch(_apiUrl('/kx/skip/query'),{method:'POST'})
.then(function(r){return r.json();})
.then(function(d){_applySkipDialogState(d.result||{});})
.catch(function(){
fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json();}).then(function(d){
_applySkipDialogState(d.result||{});
}).catch(function(){});
// Erst aktueller State (mit DB-Objects + svg), dann query_obj für frischen skipped
fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){
var s=d.result||{};
_skipSvg=s.svg_b64||'';
_skipObjects=(s.objects||[]).map(function(n){
return {name:n, skipped:(s.skipped||[]).indexOf(n)>=0, willSkip:false};
});
renderSkipList(); renderSkipSvg();
});
// Frisch nachfragen (skipped-Liste aktualisieren)
fetch(_apiUrl('/kx/skip/query'),{method:'POST'}).then(function(r){return r.json()}).then(function(){
setTimeout(function(){
fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){
var s=d.result||{};
var skipped=s.skipped||[];
_skipObjects.forEach(function(o){ o.skipped=skipped.indexOf(o.name)>=0; if(o.skipped)o.willSkip=false; });
renderSkipList(); renderSkipSvg();
});
}, 500);
}).catch(function(){});
}
function closeSkipDialog(){
if(_skipPollTimer){clearInterval(_skipPollTimer);_skipPollTimer=null;}
document.getElementById('skip-dialog').classList.remove('open');
}
function _shortLabel(name){

View File

@@ -39,7 +39,6 @@
<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>
@@ -522,7 +521,6 @@
<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>

View File

@@ -1,291 +0,0 @@
{
"header_status_standby": "Pronto",
"header_status_printing": "In stampa",
"header_status_complete": "Completato",
"header_status_error": "Errore",
"kobra_free": "Pronto",
"kobra_busy": "Occupato",
"kobra_printing": "In stampa",
"kobra_preheating": "Preriscaldamento",
"kobra_auto_leveling": "Livellamento automatico",
"kobra_checking": "Verifica",
"kobra_updated": "Aggiornamento",
"kobra_init": "Inizializzazione",
"kobra_pausing": "Pausa in corso...",
"kobra_paused": "In pausa",
"kobra_resuming": "Ripresa...",
"kobra_resumed": "Ripreso",
"kobra_stopping": "Arresto...",
"kobra_stoped": "Arrestato",
"kobra_finished": "Finito",
"kobra_failed": "Errore",
"kobra_canceled": "Annullato",
"kobra_offline": "Offline",
"nav_dashboard": "Dashboard",
"nav_print": "Stampa",
"nav_temps": "Temperature",
"nav_motion": "Movimento",
"nav_ams": "AMS",
"nav_extras": "Luce / Ventola",
"nav_console": "Console",
"card_progress": "Avanzamento",
"card_temps": "Temperature",
"card_light_fan": "Ventola",
"card_speed": "Velocità di stampa",
"card_cam": "Camera",
"lbl_elapsed": "Trascorso:",
"lbl_remaining": "Rimanente:",
"lbl_slicer_time": "Stima slicer:",
"lbl_layers": "Layer",
"lbl_zpos": "Z (mm)",
"speed_silent": "🐢 Silenzioso",
"speed_normal": "⚡ Normale",
"speed_sport": "🚀 Sport",
"lbl_light": "💡 Luce",
"lbl_feed": "Carica",
"lbl_unload": "Rimuovi",
"card_ace_dry": "Essiccazione ACE",
"ace_dry_dryer": "Essiccatore",
"ace_dry_status_off": "Stato: Spento",
"ace_dry_status_on": "Stato: Attivo",
"ace_dry_status_remaining": "Rimanente",
"ace_dry_humidity": "Umidità",
"ace_dry_current_temp": "Temperatura",
"ace_dry_chart": "Cronologia (Temp/Umidità)",
"ace_dry_temp": "Temperatura (°C)",
"ace_dry_duration": "Durata (min)",
"ace_dry_start": "▶ Avvia",
"ace_dry_stop": "■ Ferma",
"ace_dry_auto_refill": "Ricarica automatica",
"ace_dry_enable": "Abilita essiccazione",
"ace_dry_temp_line": "Temperatura di essiccazione",
"ace_dry_time_line": "Tempo di essiccazione",
"ace_dry_ui_pending": "(Solo interfaccia, backend a seguire)",
"ace_dry_dialog_title": "Impostazioni Temp/Tempo essiccatore",
"ace_dry_dialog_temp": "Temperatura (30-80°C)",
"ace_dry_dialog_time": "Tempo rim. (h:m:s)",
"ace_dry_dialog_confirm": "Conferma",
"ace_dry_dialog_cancel": "Annulla",
"ace_dry_dialog_save_restart": "Salva e riavvia",
"ace_dry_dialog_custom_name": "Nome personalizzato",
"ace_dry_dialog_reset_default": "Ripristina predefiniti",
"ace_dry_preset_pla": "PLA",
"ace_dry_preset_pla_plus": "PLA+",
"ace_dry_preset_petg": "PETG",
"ace_dry_preset_tpu": "TPU",
"ace_dry_preset_abs_asa": "ABS / ASA",
"ace_dry_preset_pa_pc": "PA / PC",
"ace_dry_preset_custom": "Personalizzato",
"cam_placeholder": "📷 Camera non avviata",
"cam_stream_unavailable": "Flusso video non disponibile",
"btn_cam_start": "▶ Camera",
"btn_cam_stop": "◼ Camera",
"btn_pause": "⏸ Pausa",
"btn_resume": "▶ Riprendi",
"btn_cancel": "✕ Stop",
"label_nozzle": "Ugello",
"label_bed": "Piatto",
"label_fan": "🌀 Ventola",
"label_light": "💡 Luce",
"label_on_off": "On / Off",
"label_speed": "Velocità",
"panel_print_title": "Controllo stampa",
"panel_print_btn_pause": "⏸ Pausa",
"panel_print_btn_resume": "▶ Riprendi",
"panel_print_btn_cancel": "✕ Annulla",
"panel_print_temps_live": "Temperature (In tempo reale)",
"label_set": "Imposta",
"label_off": "Off",
"panel_temps_nozzle": "Ugello",
"panel_temps_bed": "Piatto riscaldato",
"panel_temps_chart": "Cronologia (ultime 60 letture)",
"label_target_c": "Target:",
"panel_motion_xy": "Assi XY",
"panel_motion_z": "Asse Z",
"label_step": "Ampiezza passo:",
"btn_home_z": "Home Z",
"btn_home_xy": "Home XY",
"btn_home_all": "Home generale",
"btn_disable_motors": "Spegni motori",
"panel_ams_title": "Filamento",
"card_ams": "Filamento",
"ams_no_data": "Nessun dato ricevuto dall' AMS",
"label_slot": "Slot",
"ams_empty": "Vuoto",
"panel_extras_light": "Luce",
"panel_extras_fan": "Ventola",
"panel_extras_camera": "Camera",
"btn_cam_start2": "▶ Avvia",
"btn_cam_stop2": "◼ Ferma",
"panel_console_title": "Registro eventi",
"log_light_on": "Luce accesa",
"log_light_off": "Luce spenta",
"log_fan": "Ventola →",
"log_nozzle": "Ugello →",
"log_bed": "Piatto →",
"log_axis": "Asse",
"log_home": "Home",
"log_home_all": "Home generale",
"log_cam_start": "Camera avviata:",
"log_cam_stop": "Camera arrestata",
"log_poll_error": "Errore di sincronizzazione:",
"log_error": "Errore:",
"confirm_cancel": "Annullare davvero la stampa?",
"settings_title": "Impostazioni",
"settings_connection": "Connessione",
"settings_print": "Impostazioni di stampa",
"settings_poll": "Intervallo di sincronizzazione (secondi)",
"nav_settings": "Impostazioni",
"settings_cat_display": "Aspetto",
"settings_cat_filament": "Filamento",
"settings_cat_language": "Lingua",
"settings_cat_theme": "Alterna chiaro / scuro",
"settings_filament_mapping": "Mappatura profilo filamento (per slot)",
"settings_filament_mapping_save": "Salva mappatura",
"settings_visible_vendors": "Produttori visibili (menu del profilo)",
"settings_visible_vendors_hint": "Solo questi produttori appariranno nel menu del profilo dello slot. Se non selezioni nulla = mostra tutti. I profili \"Generici\" e i tuoi personali sono sempre visibili.",
"settings_visible_vendors_save": "Salva selezione",
"progress_action_print": "Stampa",
"progress_action_slots": "Mappa slot",
"progress_action_clear": "Cancella",
"settings_version": "Versione",
"settings_save": "Salva e riavvia",
"settings_printer_name": "Nome stampante",
"settings_printer_ip": "IP stampante",
"settings_mqtt_port": "Porta MQTT",
"settings_username": "Nome utente MQTT",
"settings_password": "Password MQTT",
"settings_device_id": "ID dispositivo",
"settings_mode_id": "ID modalità",
"hint_ip_no_port": "Solo indirizzo IP, senza porta (es. 192.168.1.102)",
"settings_default_slot": "Slot predefinito (colore singolo)",
"settings_slot_auto": "Auto (tutti gli slot caricati)",
"settings_auto_leveling": "Livellamento automatico predefinito",
"fd_options_title": "Opzioni di stampa",
"print_auto_leveling": "Livellamento automatico",
"settings_file_ready_mode": "Comportamento all'avvio stampa",
"settings_file_ready_banner": "Barra di stampa",
"settings_file_ready_dialog": "Finestra di dialogo di stampa",
"settings_camera_on_print": "Attiva la camera all'avvio della stampa",
"settings_web_upload_warning": "Mostra un avviso quando si stampano caricamenti web",
"update_check": "Controlla aggiornamenti",
"update_checking": "Verifica in corso...",
"update_available": "disponibile",
"update_none": "Già aggiornato",
"update_apply": "Installa ora",
"update_applying": "Download in corso...",
"update_restarting": "Riavvio in corso...",
"update_error": "Errore",
"btn_connect": "⚡ Connetti",
"btn_disconnect": "✕ Disconnetti",
"lbl_conn_error": "Errore di connessione:",
"slot_edit_title": "Modifica slot",
"slot_edit_color": "Colore",
"slot_edit_material": "Materiale",
"slot_edit_load": "⬇ Carica",
"slot_edit_unload": "⬆ Rimuovi",
"slot_edit_save": "💾 Salva",
"slot_edit_custom": "es. PLA, PETG, ABS…",
"slot_edit_ok": "Slot AMS",
"slot_edit_profile": "Profilo OrcaSlicer",
"slot_edit_profile_hint": "Inviato durante la sincronizzazione con OrcaSlicer come marchio specifico invece di un semplice \"Generico\"",
"slot_edit_profile_default": "— Generico (predefinito) —",
"orca_profile_section": "Profili OrcaSlicer",
"orca_profile_hint": "Importa i tuoi profili di filamento OrcaSlicer (apri la cartella utente tramite Aiuto → Mostra cartella di configurazione)",
"orca_profile_import_btn": "Importa profili",
"orca_profile_import_link": "★ Importa i tuoi profili…",
"orca_profile_import_title": "Importa i tuoi profili OrcaSlicer",
"orca_profile_help_html": "Carica un file <b>ZIP</b> della tua cartella filamenti di OrcaSlicer o file singoli <b>.json</b>.<br>In OrcaSlicer: <i>Aiuto → Mostra cartella di configurazione → user/&lt;id&gt;/filament/</i>",
"orca_profile_dropmsg": "Trascina qui o fai clic",
"orca_profile_list_label": "Attualmente importati",
"orca_profile_user_label": "Profili personali",
"orca_profile_user_empty": " nessuno ",
"orca_profile_uploading": "Caricamento in corso…",
"orca_profile_done": "Importato",
"orca_profile_skipped": "saltato",
"log_dir_all": "Tutti",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_dir_label": "Dir:",
"log_lvl_label": "Livello:",
"log_lvl_err": "⛔ Errori",
"log_lvl_warn": "⚠ Avvisi",
"log_topic_label": "Argomento:",
"log_topic_ams": "AMS",
"log_topic_print": "Stampa",
"log_topic_info": "Info",
"log_topic_status": "Stato",
"log_download": "⬇ Scarica",
"log_auto": "⬇ Auto",
"log_clear": "✕ Cancella",
"log_filter_placeholder": "Filtra…",
"file_ready_btn": "▶ Avvia stampa",
"file_slots_btn": "🎨 Seleziona slot",
"file_cancel_btn": "✕ Annulla",
"nav_printers": "Stampanti",
"skip_title": "✂ Salta oggetti",
"skip_hint": "Deseleziona gli oggetti che non vuoi più stampare:",
"skip_btn_label": "Oggetti",
"skip_no_objects": "Nessun oggetto in questa stampa.",
"skip_already": "saltato",
"skip_cancel": "Annulla",
"skip_confirm": "Salta",
"skip_select_at_least_one": "Seleziona almeno un oggetto.",
"skip_sending": "Invio in corso …",
"skip_success": "Gli oggetti verranno saltati.",
"fd_objects_hint": "Salta oggetti (opzionale):",
"fd_objects_toggle": "Salta oggetti",
"fd_slots_hint": "Assegna il canale GCode allo slot AMS:",
"fd_cancel": "Annulla",
"fd_print": "▶ Stampa",
"fd_no_slots_msg": "Nessuno slot AMS caricato.{br}Avviare comunque la stampa?",
"fd_slot": "Slot",
"fd_no_matching_material": "Nessun materiale corrispondente",
"fd_used": "USATO",
"add_printer": "Aggiungi stampante",
"apd_lbl_ip": "IP stampante",
"apd_lbl_name": "Nome (opzionale)",
"apd_placeholder_name": "es. Kobra X Soggiorno",
"apd_cancel": "Annulla",
"apd_confirm": "Aggiungi",
"apd_fetching": "Recupero dati dalla stampante…",
"apd_success": "Stampante aggiunta, riavvio del bridge in corso…",
"apd_err_ip": "Inserisci un indirizzo IP",
"printers_remove": "Rimuovi stampante",
"printers_remove_confirm": "Rimuovere la stampante \"{name}\"? Il bridge si riavvierà.",
"printers_active": "● attiva",
"printers_switch": "Cambia →",
"printers_current": "Stampante corrente",
"printers_loading": "Caricamento in corso…",
"printers_none": "Nessuna stampante configurata.",
"printers_empty_hint": "Nessuna stampante ancora configurata.",
"nav_browser": "Browser",
"panel_browser_title": "Browser dei file",
"store_search_placeholder": "🔍 Cerca…",
"store_empty": "Nessun file caricato.",
"store_refresh": "↻ Aggiorna",
"store_print": "▶ Stampa",
"store_download": "⬇ Scarica",
"store_delete_confirm": "Eliminare il file?",
"store_print_confirm": "Stampare il file?",
"store_web_verify_title": "Verifica file",
"store_web_verify_msg": "Verifica che questo file sia stato creato per Anycubic Kobra X.",
"store_web_verify_confirm": "Conferma",
"store_web_verify_abort": "Interrompi",
"store_no_results": "Nessun file trovato.",
"store_never": "mai stampato",
"store_estimate": "Stima",
"store_upload_label_prefix": "Trascina il GCode qui o ",
"store_upload_label_browse": "sfoglia",
"store_upload_busy": "⏳ Caricamento in corso…",
"store_upload_success": "✓ {file}",
"store_upload_error": "✗ {error}",
"store_upload_only_gcode": "✗ Sono consentiti solo file GCode (.gcode, .3mf, .bgcode)",
"sf_all": "Tutti",
"sf_ok": "✓ Completato",
"sf_err": "✗ Fallito",
"sf_new": "Nuovo",
"ss_date": "↓ Data",
"ss_name": "Nome AZ",
"ss_dur": "⏱ Tempo di stampa"
}