From 34b3c1601a1c76db5395902c637fb6612b8c81e4 Mon Sep 17 00:00:00 2001 From: Gangoke Date: Tue, 16 Jun 2026 23:06:12 -1000 Subject: [PATCH 1/2] fix: print start dialog across all print start methods and file uploads fix: object skip when selected on print start --- kobrax_moonraker_bridge.py | 84 ++++++++++++++++++++++++------ web/themes/default/app.js | 103 +++++++++++++++++++++++++++---------- 2 files changed, 143 insertions(+), 44 deletions(-) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 774e7e5..80ff444 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -1077,6 +1077,25 @@ 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 = [] @@ -1123,7 +1142,10 @@ class KobraXBridge: return False for i in range(max(1, int(retries))): try: - if self._state.get("kobra_state") != "printing": + # kobra_state ist waehrend Start oft nicht exakt "printing" + # (z.B. busy/preheating). Auf print_state pruefen, sonst wird + # skip/start nie abgeschickt. + if self._state.get("print_state") not in ("printing", "paused"): time.sleep(max(0.1, float(delay_s))) continue resp = self.client.skip_objects(wanted) @@ -2482,26 +2504,29 @@ class KobraXBridge: async def handle_kx_skip_query(self, request): """Druck-Objektliste vom Drucker neu abfragen. - POST /kx/skip/query → triggert skip/query_obj, gibt zuletzt bekannten - Stand zurück (skip/report kommt async, Frontend pollt /kx/skip/state). + POST /kx/skip/query → triggert skip/query_obj, wartet kurz auf den + async skip/report und gibt den zusammengeführten Skip-State zurück. """ + 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) - return self._json_cors({"result": self._skip_state}) - async def handle_kx_skip_state(self, request): - """Aktueller Skip-State. + # skip/report kommt asynchron; kurz warten, damit der Rückgabewert + # möglichst den frisch bestätigten Druckerzustand enthält. + deadline = time.time() + 1.5 + while time.time() < deadline: + cur_ts = int(self._skip_state.get("ts", 0) or 0) + if cur_ts > prev_ts: + break + await asyncio.sleep(0.1) - Kombiniert: - - Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell - laufenden filename (file/report beim Druckstart hat die Liste gefüllt). - skip/query_obj liefert nämlich NUR die bereits geskippten zurück, - nicht die Gesamtliste. - - Geskippt: aus self._skip_state (von skip/report aktualisiert). - """ + return self._json_cors({"result": self._build_skip_state_result()}) + + 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 = "" @@ -2513,13 +2538,25 @@ class KobraXBridge: svg = f.get("svg_image") or "" except Exception as e: log.warning(f"skip_state lookup failed: {e}") - result = { + 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_state(self, request): + """Aktueller Skip-State. + + Kombiniert: + - Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell + laufenden filename (file/report beim Druckstart hat die Liste gefüllt). + skip/query_obj liefert nämlich NUR die bereits geskippten zurück, + nicht die Gesamtliste. + - Geskippt: aus self._skip_state (von skip/report aktualisiert). + """ + result = self._build_skip_state_result() return self._json_cors({"result": result}) async def handle_kx_printers(self, request): @@ -2615,8 +2652,8 @@ class KobraXBridge: }, } - # Pre-Print-Skip sofort im UI-Status spiegeln - self._skip_state = {"skipped": list(excluded_objects), "ts": int(time.time())} + # UI erst nach echter Drucker-Bestaetigung 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 @@ -3139,7 +3176,7 @@ class KobraXBridge: ams_box_mapping = self._build_auto_ams_box_mapping() use_ams = len(ams_box_mapping) > 0 - auto_leveling = getattr(self._args, "auto_leveling", 1) + auto_leveling = int(body.get("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", "") @@ -3169,6 +3206,15 @@ class KobraXBridge: }, } + # UI erst nach echter Drucker-Bestaetigung 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}" @@ -3181,6 +3227,9 @@ 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): @@ -3815,6 +3864,7 @@ 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), "web_upload_warning": getattr(self._args, "web_upload_warning", 1), "light_on": s["light_on"], "light_brightness": s["light_brightness"], diff --git a/web/themes/default/app.js b/web/themes/default/app.js index 02e0de0..c6e813a 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -665,9 +665,13 @@ function applyState(){ document.getElementById('file-ready-name').textContent=s.file_ready; // Neue Datei → Abbruch-Sperre aufheben 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); + if(shouldAutoOpen){ + // Dialog-Modus: nur Dialog verwenden, den Banner niemals anzeigen. + frb.style.display='none'; + if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){ + _fdAutoOpenedFile=s.file_ready; + startReadyFileWithSlots(s.file_ready,true); + } } else { frb.style.display='flex'; bannerVisible=true; @@ -2270,7 +2274,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); }); + maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn,_autoOpen); }); return; } _filamentDialogMode='banner'; @@ -2535,12 +2539,38 @@ 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: normaler print/start mit Slot-Override + // Banner-Modus: identisches Start-Verhalten wie im File-Browser bevorzugen. var btn=document.getElementById('file-ready-btn'); if(btn){btn.disabled=true;btn.textContent='…';} - 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(){ + 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 + }) + }); + }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);} }) @@ -2618,35 +2648,54 @@ 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||[]; + // Keep local pending selections while live state refreshes in background. + 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(){ - // 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(){ + // Query-Endpoint liefert bereits den frischen, zusammengeführten Skip-State. + 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){ - 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(){}); + _applySkipDialogState(d.result||{}); + }).catch(function(){}); + }); } function closeSkipDialog(){ + if(_skipPollTimer){clearInterval(_skipPollTimer);_skipPollTimer=null;} document.getElementById('skip-dialog').classList.remove('open'); } function _shortLabel(name){ -- 2.49.1 From 755b82137cfeefcdca2b9ef125327dbc890f9dc3 Mon Sep 17 00:00:00 2001 From: Gangoke Date: Wed, 17 Jun 2026 00:24:19 -1000 Subject: [PATCH 2/2] cleanup odd behavior on refresh after exiting certain dialogs. remove unintended bypass for files needing to be verified before printing --- web/themes/default/app.js | 108 +++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 31 deletions(-) diff --git a/web/themes/default/app.js b/web/themes/default/app.js index c6e813a..b8fd63c 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -10,8 +10,8 @@ var camUserStopped=false; // user stopped camera manually — suppress auto-rest 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 _fdDialogOpen=false; // Dialog ist gerade offen -var _fdAutoOpenedFile=null; // Dateiname für den der Dialog auto-geöffnet wurde -var _fdUserCancelled=false; // User hat den Auto-Open-Dialog abgebrochen +var _fdAutoOpenedFile=sessionStorage.getItem('fdAutoOpenedFile')||null; +var _fdUserCancelled=sessionStorage.getItem('fdUserCancelled')==='1'; var currentStep=1; var currentPanel='dashboard'; var aceAutoRefillPrefs=(function(){ @@ -664,7 +664,11 @@ 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; + if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready){ + _fdUserCancelled=false; + sessionStorage.removeItem('fdUserCancelled'); + sessionStorage.removeItem('fdAutoOpenedFile'); + } if(shouldAutoOpen){ // Dialog-Modus: nur Dialog verwenden, den Banner niemals anzeigen. frb.style.display='none'; @@ -1333,23 +1337,40 @@ 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(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 _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 && currentFile.web_unverified && webUploadWarningEnabled()){ - maybeGateWebUpload(currentFile, function(){ startReadyFile(fn); }); + if(currentFile){ + _gateAndStart(currentFile); return; } - 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);} - }); + // Fresh page loads may not have storeFiles populated yet. + 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(); + }); } function cancelReadyFile(){ post('/api/file_ready/clear',{}) @@ -2136,6 +2157,7 @@ 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). @@ -2203,7 +2225,8 @@ function clearWebUploadWarningFlag(fileId, onDone){ }); } -function maybeGateWebUpload(fileObj, onContinue){ +function maybeGateWebUpload(fileObj, onContinue, opts){ + opts=opts||{}; if(!fileObj || !fileObj.web_unverified){ if(onContinue) onContinue(); return; @@ -2212,15 +2235,20 @@ function maybeGateWebUpload(fileObj, onContinue){ 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){ +function openWebVerifyDialog(fileId, filename, onConfirm, autoOpen){ _pendingWebVerifyFileId=fileId; _pendingWebVerifyFilename=filename; _pendingWebVerifyAction=onConfirm||null; + _pendingWebVerifyAutoOpen=!!autoOpen; var status=document.getElementById('store-web-verify-status'); if(status){status.textContent='';} openStoreWebVerifyDialog(); @@ -2234,9 +2262,13 @@ 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(){ @@ -2256,9 +2288,11 @@ 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(); @@ -2274,7 +2308,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); }); + maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen}); return; } _filamentDialogMode='banner'; @@ -2295,23 +2329,31 @@ function startReadyFileWithSlots(filename,_autoOpen){ }); } + function _proceedWithFileObj(fileObj){ + if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){ + // Verify gate was bypassed (storeFiles was empty on page load) — re-gate now. + 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){ - _storeFileId=fileObj.id; - _setGcodeFilamentsFromFileObj(fileObj); - openWithSlots(); + _proceedWithFileObj(fileObj); 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;}); - if(refreshed){ - _storeFileId=refreshed.id; - _setGcodeFilamentsFromFileObj(refreshed); - } - openWithSlots(); + var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;})||null; + _proceedWithFileObj(refreshed); }).catch(function(){ openWithSlots(); }); @@ -2495,7 +2537,11 @@ function closeFilamentDialog(){ var dlg=document.getElementById('filament-dialog'); if(dlg)dlg.classList.remove('open'); _fdDialogOpen=false; - if(_fdAutoOpenedFile) _fdUserCancelled=true; + if(_fdAutoOpenedFile){ + _fdUserCancelled=true; + sessionStorage.setItem('fdUserCancelled','1'); + sessionStorage.setItem('fdAutoOpenedFile',_fdAutoOpenedFile); + } } function confirmFilamentPrint(){ -- 2.49.1