from pathlib import Path folder=Path(__file__).resolve().parent;p=folder/'payload/assets/ui/console-app.js' s=p.read_text(encoding='utf-8') helper='''// Keep in-progress values separate from background snapshots. They are cleared // only when the backend echoes the same value, never just because focus moved. const fieldDrafts = new Map(); const pageScroll = new Map(); const pendingEdits = new Map(); const fieldSources = { "ai-name":["ai","name"], "auto-poll":["automation","poll"], "auto-batch":["automation","batch"], "auto-delay":["automation","delay"], "auto-idle":["automation","idle"], "auto-context":["automation","context"], "auto-draft-autosend":["automation","draftAutosend"], "sys-language":["system","language"], "sys-scale":["system","scale"], "sys-retention":["system","retention"], "sys-server-url":["system","serverUrl"], "archive-search":[null,"archiveQuery"], "event-search":[null,"eventQuery"], "wechat-db-search":["wechatDb","query"], "archive-date":[null,"archiveDate"] }; function recordDraft(element){ if (!element || !fieldSources[element.id]) return; fieldDrafts.set(element.id, {view:STATE.view,value:element.value}); } function acknowledgeDrafts(data){ for (const [id,draft] of fieldDrafts){ const [section,key]=fieldSources[id]; const source=section ? data[section] : data; if (source && source[key] != null && String(source[key]) === draft.value) fieldDrafts.delete(id); } } function restoreDrafts(view){ for (const [id,draft] of fieldDrafts){ const element=document.getElementById(id); if (draft.view===view && element && element.value!==draft.value) element.value=draft.value; } if (view==='ai' && fieldDrafts.has('ai-name')) markAiDirty(); } function cancelEdit(key){ const edit=pendingEdits.get(key); if (edit) clearTimeout(edit.timer); pendingEdits.delete(key); } function sendEdit(key){ const edit=pendingEdits.get(key); if (!edit) return; cancelEdit(key); callBridge(edit.method,edit.payload); } function scheduleEdit(key,method,payload,delay){ cancelEdit(key); pendingEdits.set(key,{method,payload,timer:setTimeout(()=>sendEdit(key),delay)}); } function flushEdits(){ for (const key of [...pendingEdits.keys()]) sendEdit(key); } let composing=false, pointerDown=false, deferredRefresh=0; document.addEventListener('pointerdown',()=>{pointerDown=true;},true); document.addEventListener('pointerup',()=>{pointerDown=false;},true); document.addEventListener('pointercancel',()=>{pointerDown=false;},true); document.addEventListener('compositionstart',()=>{composing=true;}); document.addEventListener('compositionend',event=>{ composing=false; event.target.dispatchEvent(new Event('input',{bubbles:true})); }); document.addEventListener('focusout',()=>{ clearTimeout(deferredRefresh); deferredRefresh=setTimeout(()=>{ const active=document.activeElement; if (!pointerDown && !composing && window.__deferredView===STATE.view && !(active && /^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName))){ window.__deferredView=null; render(STATE.view); } },100); }); ''' assert s.count('const _render = render;')==1;s=s.replace('const _render = render;',helper+'const _render = render;') s=s.replace(''' const sameView = STATE.view === name; STATE.view = name;''',''' const sameView = STATE.view === name; const scrollHost = document.getElementById("main"); if (scrollHost) pageScroll.set(STATE.view,scrollHost.scrollTop); STATE.view = name;''',1) s=s.replace(''' main.innerHTML = html; normalizeReferenceView();''',''' main.innerHTML = html; restoreDrafts(name); normalizeReferenceView(); main.scrollTop = pageScroll.get(name) || 0;''',1) point=''' const prev = STATE.view; Object.assign(STATE, data);''' new=''' if (window.__pendingNavigation && data.view && data.view!==window.__pendingNavigation) return; if (data.view===window.__pendingNavigation) window.__pendingNavigation=null; acknowledgeDrafts(data); const prev = STATE.view; const previousScale=(STATE.system || {}).scale; Object.assign(STATE, data); if (data.system && data.system.scale!==previousScale && typeof fit==='function') fit();''' assert s.count(point)==1;s=s.replace(point,new) s=s.replace(''' if (typing && (STATE.view || prev) === prev && ["ai","automation","system","archive","logs"].includes(prev)) { return; }''',''' if ((typing || composing) && (STATE.view || prev)===prev) { window.__deferredView=prev; return; }''') old=''' if (name !== STATE.view) render(name); callBridge("navigate", name);''' new=''' flushEdits(); window.__pendingNavigation=name; if (name !== STATE.view) render(name); const serial=window.__navigationSerial=(window.__navigationSerial || 0)+1; requestAnimationFrame(()=>requestAnimationFrame(()=>{ if (serial===window.__navigationSerial) callBridge("navigate",name); }));''' assert s.count(old)==1;s=s.replace(old,new) # Separate debounces, capture values now (before a navigation replaces the DOM). s=s.replace('let searchTimer = 0;\n','') s=s.replace(''' const id = e.target.id || ""; const value = e.target.value || "";''',''' const id = e.target.id || ""; const value = e.target.value || ""; recordDraft(e.target); if (e.isComposing || composing) return;''',1) old=''' clearTimeout(searchTimer); searchTimer = setTimeout(() => { if (id === "archive-search") callBridge("setArchiveQuery", value); else if (id === "event-search") callBridge("setEventQuery", value); else callBridge("setWeChatDatabaseQuery", value); }, 280);''' new=''' const method={"archive-search":"setArchiveQuery","event-search":"setEventQuery","wechat-db-search":"setWeChatDatabaseQuery"}[id]; scheduleEdit(id,method,value,280);''' assert old in s;s=s.replace(old,new) s=s.replace('id === "auto-context") {','id === "auto-context" || id === "auto-draft-autosend") {') old=''' clearTimeout(searchTimer); searchTimer = setTimeout(() => { callBridge("saveAutomationSettings", collectAutomationPayload()); }, 400);''' new=''' scheduleEdit("automation","saveAutomationSettings",collectAutomationPayload(),400);''' assert old in s;s=s.replace(old,new) old=''' clearTimeout(searchTimer); searchTimer = setTimeout(() => { callBridge("setSystemField", collectSystemPayload()); }, 280);''' new=''' if (id === "sys-retention") scheduleEdit("system","setSystemField",collectSystemPayload(),280);''' assert old in s;s=s.replace(old,new) old=''' if (id === "sys-language" || id === "sys-scale") { callBridge("setSystemField", collectSystemPayload()); }''' new=''' if (id === "sys-language" || id === "sys-scale") { recordDraft(e.target); cancelEdit("system"); callBridge("setSystemField",collectSystemPayload()); }''' assert old in s;s=s.replace(old,new) # Cancel the pending autosave when clicking the explicit Save button. point='''document.addEventListener("click", e => { const nav =''' new='''document.addEventListener("click", e => { const saveAction=e.target.closest('[data-action="save-system"],[data-action="save-automation"]'); if (saveAction) cancelEdit(saveAction.dataset.action==='save-system'?'system':'automation'); const nav =''' assert point in s;s=s.replace(point,new) p.write_text(s,encoding='utf-8',newline='\n') print('Prepared independent saves, draft protection and navigation coalescing')