fix(dashboard): don't collapse open UI on the 15s auto-refresh#3
Conversation
The dashboard's 15s auto-poll re-ran the active tab's loader, rebuilding the Activity table (and other views) from scratch — so an expanded Activity row, an open consumer drawer, or the add-provider/add-codex card would close by itself without the user closing it. Pause the auto-refresh while any of those is open; it resumes on the next tick once the user closes them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughIn ChangesDashboard JS: Tab Navigation and Auto-Refresh Guards
Estimated code review effort🎯 2 (Simple) | ⏱️ ~5 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@auth_proxy.py`:
- Line 3948: The auto-refresh guard inside the setInterval function is not
correctly detecting when the add provider and add codex cards are open. The
condition checks `ap.style.display && ap.style.display !== 'none'` but when the
cards are toggled open, their style.display is set to an empty string (''),
which is falsy, causing the guard to fail. Fix this by removing the falsy check
on ap.style.display and ac.style.display and instead only check that the display
value is not equal to 'none', so the guard properly pauses polling when either
card is visible. Apply this same fix to both the addProviderCard and
addCodexCard conditions in the setInterval function.
- Line 3948: Tab elements are receiving duplicate setTab calls because
individual onclick handlers (like the ones assigned to tabOverview,
tabConsumers, tabProviderKeys, etc.) are firing in addition to the delegated
click handler on the .nav element. Remove all the direct onclick assignments for
tab buttons (the lines with $('tabOverview').onclick, $('tabConsumers').onclick,
etc.) and instead ensure each tab button has a data-tab attribute set to the
appropriate tab name so that the delegated .nav click handler on the element
with [data-tab] selector will be the sole handler managing tab switches.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| async function postConfig(updates){try{const r=await fetch('/dashboard/api/config',{method:'POST',headers:{'content-type':'application/json'},credentials:'same-origin',body:JSON.stringify({updates})});if(r.status===401){showLogin();return}const d=await r.json();if(!r.ok)throw new Error(d.error?.message||('config '+r.status));toast(d.applied_live?'Saved · live':(d.note?'Saved · '+d.note:'Saved'));renderConfig(d.knobs||[])}catch(e){showErr(e.message)}} | ||
| async function deleteCodexAccount(name){if(!confirm('Delete codex account '+name+'?'))return;try{const r=await fetch('/dashboard/api/codex/accounts/'+encodeURIComponent(name),{method:'DELETE',credentials:'same-origin'});if(r.status===401){showLogin();return}const d=await r.json();if(!r.ok)throw new Error(d.error?.message||`delete ${r.status}`);toast('Codex account deleted');loadCodexAccounts()}catch(e){showErr(e.message)}} | ||
| $('loginBtn').onclick=login;$('apiKeyLoginBtn').onclick=apiKeyLogin;$('password').addEventListener('keydown',e=>{if(e.key==='Enter')login()});$('apiKeyLogin').addEventListener('keydown',e=>{if(e.key==='Enter')apiKeyLogin()});$('logout').onclick=logout;$('tabOverview').onclick=()=>setTab('overview');$('tabConsumers').onclick=()=>setTab('consumers');$('tabProviderKeys').onclick=()=>setTab('providerKeys');$('tabKeyUsage').onclick=()=>setTab('keyUsage');$('tabMarket').onclick=()=>setTab('market');$('tabBuilder').onclick=()=>setTab('builder');$('tabActivity').onclick=()=>setTab('activity');$('recent').addEventListener('click',e=>{const cp=e.target.closest('[data-copyterm]');if(cp){navigator.clipboard.writeText(cp.dataset.copyterm).then(()=>toast('Policy term copied'));return}const row=e.target.closest('.actRow');if(!row)return;const det=$('recent').querySelector('.actDetail[data-d="'+row.dataset.i+'"]');if(!det)return;det.classList.toggle('hidden');const tog=row.querySelector('.actToggle');if(tog)tog.textContent=det.classList.contains('hidden')?'▸':'▾'});$('bReview').onclick=bReview;$('bDownload').onclick=bDownload;$('bTestBtn').onclick=bTest;$('bEx1').onclick=()=>bLoadExample('ex1');$('bEx2').onclick=()=>bLoadExample('ex2');$('bAddCond').onclick=()=>{bSync();bFilters.push({field:'latency_ms',rel:'le',val:''});bRender()};$('bAddOr').onclick=()=>{bSync();bFilters.push({kind:'or',subs:[{field:'latency_ms',rel:'le',val:''}]});bRender()};$('bAddScore').onclick=()=>{bSync();bScores.push({field:'field:price_in',w:'0.5',norm:true,inv:true});bRender()};$('b_selector').onchange=()=>{$('bTempWrap').style.display=$('b_selector').value==='sample'?'':'none'};document.querySelectorAll('#bModeSeg button').forEach(b=>b.onclick=()=>bSetMode(b.dataset.mode));$('bStructured').addEventListener('change',e=>{if(e.target.classList.contains('bF-field'))bSyncRender()});$('bStructured').addEventListener('click',e=>{const b=e.target.closest('[data-act]');if(!b)return;bSync();const i=+b.dataset.i,j=+b.dataset.j,act=b.dataset.act;if(act==='del')bFilters.splice(i,1);else if(act==='addsub')bFilters[i].subs.push({field:'latency_ms',rel:'le',val:''});else if(act==='delsub')bFilters[i].subs.splice(j,1);else if(act==='delscore')bScores.splice(i,1);bRender()});bRender();document.querySelector('.nav').addEventListener('click',e=>{const b=e.target.closest('[data-tab]');if(b){e.preventDefault();setTab(b.dataset.tab)}});$('refresh').onclick=()=>{if(activeTab==='policies')loadPolicies();else if(activeTab==='market')loadMarket();else if(activeTab==='keyUsage')loadKeyUsage();else load()};$('market').addEventListener('click',e=>{const h=e.target.closest('[data-fam]');if(!h)return;const fam=h.dataset.fam;if(marketOpen.has(fam))marketOpen.delete(fam);else marketOpen.add(fam);if(lastMarket)renderMarket(lastMarket)});$('marketSearch').oninput=()=>{if(lastMarket)renderMarket(lastMarket)};$('tradableOnly').checked=localStorage.getItem('tradableOnly')==='1';$('tradableOnly').onchange=()=>{localStorage.setItem('tradableOnly',$('tradableOnly').checked?'1':'0');if(lastMarket)renderMarket(lastMarket)};$('marketCopy').onclick=()=>{if(!lastMarket){showErr('No catalog data loaded yet');return}navigator.clipboard.writeText(JSON.stringify(lastMarket,null,2)).then(()=>toast('Catalog copied to clipboard')).catch(e=>showErr(e.message))};$('marketSkill').onclick=async()=>{try{const r=await fetch('/dashboard/api/skill',{credentials:'same-origin'});if(r.status===401){showLogin();return}if(!r.ok)throw new Error('skill '+r.status);const text=await r.text();const blob=new Blob([text],{type:'text/markdown'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='SKILL.md';a.click();URL.revokeObjectURL(a.href);toast('SKILL.md downloaded — load it into any assistant to author policies')}catch(e){showErr(e.message)}};$('toggleAddProvider').onclick=()=>{const c=$('addProviderCard');c.style.display=c.style.display==='none'?'':'none'};$('addProvCancel').onclick=()=>{$('addProviderCard').style.display='none'};$('addProvSubmit').onclick=addProvider;$('toggleAddCodex').onclick=()=>{const c=$('addCodexCard');c.style.display=c.style.display==='none'?'':'none'};$('addCodexCancel').onclick=()=>{$('addCodexCard').style.display='none'};$('addCodexSubmit').onclick=addCodexAccount;$('addProvId').addEventListener('blur',()=>{if(!$('addProvEnv').value.trim()&&$('addProvId').value.trim())$('addProvEnv').value=$('addProvId').value.trim().toUpperCase().replace(/[^A-Z0-9]+/g,'_')+'_API_KEY'});$('loadKeyUsage').onclick=loadKeyUsage;$('consumer').onchange=load;$('timeframe').onchange=load;$('consumerSearch').oninput=()=>renderConsumers(lastStats.keys||[]);document.querySelectorAll('#consumerStatusSeg button').forEach(b=>b.onclick=()=>{document.querySelectorAll('#consumerStatusSeg button').forEach(x=>x.classList.remove('active'));b.classList.add('active');consumerFilterStatus=b.dataset.status;renderConsumers(lastStats.keys||[])});document.querySelectorAll('#activitySeg button').forEach(b=>b.onclick=()=>{document.querySelectorAll('#activitySeg button').forEach(x=>x.classList.remove('active'));b.classList.add('active');activityKind=b.dataset.kind;render(lastStats)});$('newConsumerKey').onclick=()=>openDrawer('', 'create');$('closeDrawer').onclick=closeDrawer;$('drawerShade').addEventListener('click',e=>{if(e.target===$('drawerShade'))closeDrawer()});$('revealKeys').onclick=revealKeys;$('copyRevealKey').onclick=()=>navigator.clipboard.writeText($('revealKeyValue').value).then(()=>toast('Copied'));$('createKey').onclick=createKey;$('copyKey').onclick=()=>navigator.clipboard.writeText($('newKeyValue').value).then(()=>toast('Key copied'));$('copyKeyHandoff').onclick=()=>navigator.clipboard.writeText($('newKeyHandoffValue').value).then(()=>toast('Setup blurb copied'));$('saveConsumerSettings').onclick=saveConsumerSettings;$('revokeKey').onclick=revokeKey;$('anProvider').onchange=load;$('anModel').onchange=load;setTab(tabFromLocation(),{silent:true});setInterval(()=>{if(activeTab==='policies')loadPolicies();else if(activeTab==='market')loadMarket();else load()},15000); | ||
| $('loginBtn').onclick=login;$('apiKeyLoginBtn').onclick=apiKeyLogin;$('password').addEventListener('keydown',e=>{if(e.key==='Enter')login()});$('apiKeyLogin').addEventListener('keydown',e=>{if(e.key==='Enter')apiKeyLogin()});$('logout').onclick=logout;$('tabOverview').onclick=()=>setTab('overview');$('tabConsumers').onclick=()=>setTab('consumers');$('tabProviderKeys').onclick=()=>setTab('providerKeys');$('tabKeyUsage').onclick=()=>setTab('keyUsage');$('tabMarket').onclick=()=>setTab('market');$('tabBuilder').onclick=()=>setTab('builder');$('tabActivity').onclick=()=>setTab('activity');$('recent').addEventListener('click',e=>{const cp=e.target.closest('[data-copyterm]');if(cp){navigator.clipboard.writeText(cp.dataset.copyterm).then(()=>toast('Policy term copied'));return}const row=e.target.closest('.actRow');if(!row)return;const det=$('recent').querySelector('.actDetail[data-d="'+row.dataset.i+'"]');if(!det)return;det.classList.toggle('hidden');const tog=row.querySelector('.actToggle');if(tog)tog.textContent=det.classList.contains('hidden')?'▸':'▾'});$('bReview').onclick=bReview;$('bDownload').onclick=bDownload;$('bTestBtn').onclick=bTest;$('bEx1').onclick=()=>bLoadExample('ex1');$('bEx2').onclick=()=>bLoadExample('ex2');$('bAddCond').onclick=()=>{bSync();bFilters.push({field:'latency_ms',rel:'le',val:''});bRender()};$('bAddOr').onclick=()=>{bSync();bFilters.push({kind:'or',subs:[{field:'latency_ms',rel:'le',val:''}]});bRender()};$('bAddScore').onclick=()=>{bSync();bScores.push({field:'field:price_in',w:'0.5',norm:true,inv:true});bRender()};$('b_selector').onchange=()=>{$('bTempWrap').style.display=$('b_selector').value==='sample'?'':'none'};document.querySelectorAll('#bModeSeg button').forEach(b=>b.onclick=()=>bSetMode(b.dataset.mode));$('bStructured').addEventListener('change',e=>{if(e.target.classList.contains('bF-field'))bSyncRender()});$('bStructured').addEventListener('click',e=>{const b=e.target.closest('[data-act]');if(!b)return;bSync();const i=+b.dataset.i,j=+b.dataset.j,act=b.dataset.act;if(act==='del')bFilters.splice(i,1);else if(act==='addsub')bFilters[i].subs.push({field:'latency_ms',rel:'le',val:''});else if(act==='delsub')bFilters[i].subs.splice(j,1);else if(act==='delscore')bScores.splice(i,1);bRender()});bRender();document.querySelector('.nav').addEventListener('click',e=>{const b=e.target.closest('[data-tab]');if(b){e.preventDefault();setTab(b.dataset.tab)}});$('refresh').onclick=()=>{if(activeTab==='policies')loadPolicies();else if(activeTab==='market')loadMarket();else if(activeTab==='keyUsage')loadKeyUsage();else load()};$('market').addEventListener('click',e=>{const h=e.target.closest('[data-fam]');if(!h)return;const fam=h.dataset.fam;if(marketOpen.has(fam))marketOpen.delete(fam);else marketOpen.add(fam);if(lastMarket)renderMarket(lastMarket)});$('marketSearch').oninput=()=>{if(lastMarket)renderMarket(lastMarket)};$('tradableOnly').checked=localStorage.getItem('tradableOnly')==='1';$('tradableOnly').onchange=()=>{localStorage.setItem('tradableOnly',$('tradableOnly').checked?'1':'0');if(lastMarket)renderMarket(lastMarket)};$('marketCopy').onclick=()=>{if(!lastMarket){showErr('No catalog data loaded yet');return}navigator.clipboard.writeText(JSON.stringify(lastMarket,null,2)).then(()=>toast('Catalog copied to clipboard')).catch(e=>showErr(e.message))};$('marketSkill').onclick=async()=>{try{const r=await fetch('/dashboard/api/skill',{credentials:'same-origin'});if(r.status===401){showLogin();return}if(!r.ok)throw new Error('skill '+r.status);const text=await r.text();const blob=new Blob([text],{type:'text/markdown'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='SKILL.md';a.click();URL.revokeObjectURL(a.href);toast('SKILL.md downloaded — load it into any assistant to author policies')}catch(e){showErr(e.message)}};$('toggleAddProvider').onclick=()=>{const c=$('addProviderCard');c.style.display=c.style.display==='none'?'':'none'};$('addProvCancel').onclick=()=>{$('addProviderCard').style.display='none'};$('addProvSubmit').onclick=addProvider;$('toggleAddCodex').onclick=()=>{const c=$('addCodexCard');c.style.display=c.style.display==='none'?'':'none'};$('addCodexCancel').onclick=()=>{$('addCodexCard').style.display='none'};$('addCodexSubmit').onclick=addCodexAccount;$('addProvId').addEventListener('blur',()=>{if(!$('addProvEnv').value.trim()&&$('addProvId').value.trim())$('addProvEnv').value=$('addProvId').value.trim().toUpperCase().replace(/[^A-Z0-9]+/g,'_')+'_API_KEY'});$('loadKeyUsage').onclick=loadKeyUsage;$('consumer').onchange=load;$('timeframe').onchange=load;$('consumerSearch').oninput=()=>renderConsumers(lastStats.keys||[]);document.querySelectorAll('#consumerStatusSeg button').forEach(b=>b.onclick=()=>{document.querySelectorAll('#consumerStatusSeg button').forEach(x=>x.classList.remove('active'));b.classList.add('active');consumerFilterStatus=b.dataset.status;renderConsumers(lastStats.keys||[])});document.querySelectorAll('#activitySeg button').forEach(b=>b.onclick=()=>{document.querySelectorAll('#activitySeg button').forEach(x=>x.classList.remove('active'));b.classList.add('active');activityKind=b.dataset.kind;render(lastStats)});$('newConsumerKey').onclick=()=>openDrawer('', 'create');$('closeDrawer').onclick=closeDrawer;$('drawerShade').addEventListener('click',e=>{if(e.target===$('drawerShade'))closeDrawer()});$('revealKeys').onclick=revealKeys;$('copyRevealKey').onclick=()=>navigator.clipboard.writeText($('revealKeyValue').value).then(()=>toast('Copied'));$('createKey').onclick=createKey;$('copyKey').onclick=()=>navigator.clipboard.writeText($('newKeyValue').value).then(()=>toast('Key copied'));$('copyKeyHandoff').onclick=()=>navigator.clipboard.writeText($('newKeyHandoffValue').value).then(()=>toast('Setup blurb copied'));$('saveConsumerSettings').onclick=saveConsumerSettings;$('revokeKey').onclick=revokeKey;$('anProvider').onchange=load;$('anModel').onchange=load;setTab(tabFromLocation(),{silent:true});setInterval(()=>{const ds=$('drawerShade');if(ds&&ds.classList.contains('open'))return;const ap=$('addProviderCard'),ac=$('addCodexCard');if((ap&&ap.style.display&&ap.style.display!=='none')||(ac&&ac.style.display&&ac.style.display!=='none'))return;if(document.querySelector('#recent .actDetail:not(.hidden)'))return;if(activeTab==='policies')loadPolicies();else if(activeTab==='market')loadMarket();else load()},15000); |
There was a problem hiding this comment.
Auto-refresh guard does not pause when add cards are open.
On Line 3948, the guard uses ap.style.display && ap.style.display !== 'none' (same for ac).
When those cards are opened in this file, style.display is set to '', so this condition is false and polling continues.
Suggested fix
- const ap=$('addProviderCard'),ac=$('addCodexCard');if((ap&&ap.style.display&&ap.style.display!=='none')||(ac&&ac.style.display&&ac.style.display!=='none'))return;
+ const ap=$('addProviderCard'),ac=$('addCodexCard');
+ if ((ap && getComputedStyle(ap).display !== 'none') || (ac && getComputedStyle(ac).display !== 'none')) return;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $('loginBtn').onclick=login;$('apiKeyLoginBtn').onclick=apiKeyLogin;$('password').addEventListener('keydown',e=>{if(e.key==='Enter')login()});$('apiKeyLogin').addEventListener('keydown',e=>{if(e.key==='Enter')apiKeyLogin()});$('logout').onclick=logout;$('tabOverview').onclick=()=>setTab('overview');$('tabConsumers').onclick=()=>setTab('consumers');$('tabProviderKeys').onclick=()=>setTab('providerKeys');$('tabKeyUsage').onclick=()=>setTab('keyUsage');$('tabMarket').onclick=()=>setTab('market');$('tabBuilder').onclick=()=>setTab('builder');$('tabActivity').onclick=()=>setTab('activity');$('recent').addEventListener('click',e=>{const cp=e.target.closest('[data-copyterm]');if(cp){navigator.clipboard.writeText(cp.dataset.copyterm).then(()=>toast('Policy term copied'));return}const row=e.target.closest('.actRow');if(!row)return;const det=$('recent').querySelector('.actDetail[data-d="'+row.dataset.i+'"]');if(!det)return;det.classList.toggle('hidden');const tog=row.querySelector('.actToggle');if(tog)tog.textContent=det.classList.contains('hidden')?'▸':'▾'});$('bReview').onclick=bReview;$('bDownload').onclick=bDownload;$('bTestBtn').onclick=bTest;$('bEx1').onclick=()=>bLoadExample('ex1');$('bEx2').onclick=()=>bLoadExample('ex2');$('bAddCond').onclick=()=>{bSync();bFilters.push({field:'latency_ms',rel:'le',val:''});bRender()};$('bAddOr').onclick=()=>{bSync();bFilters.push({kind:'or',subs:[{field:'latency_ms',rel:'le',val:''}]});bRender()};$('bAddScore').onclick=()=>{bSync();bScores.push({field:'field:price_in',w:'0.5',norm:true,inv:true});bRender()};$('b_selector').onchange=()=>{$('bTempWrap').style.display=$('b_selector').value==='sample'?'':'none'};document.querySelectorAll('#bModeSeg button').forEach(b=>b.onclick=()=>bSetMode(b.dataset.mode));$('bStructured').addEventListener('change',e=>{if(e.target.classList.contains('bF-field'))bSyncRender()});$('bStructured').addEventListener('click',e=>{const b=e.target.closest('[data-act]');if(!b)return;bSync();const i=+b.dataset.i,j=+b.dataset.j,act=b.dataset.act;if(act==='del')bFilters.splice(i,1);else if(act==='addsub')bFilters[i].subs.push({field:'latency_ms',rel:'le',val:''});else if(act==='delsub')bFilters[i].subs.splice(j,1);else if(act==='delscore')bScores.splice(i,1);bRender()});bRender();document.querySelector('.nav').addEventListener('click',e=>{const b=e.target.closest('[data-tab]');if(b){e.preventDefault();setTab(b.dataset.tab)}});$('refresh').onclick=()=>{if(activeTab==='policies')loadPolicies();else if(activeTab==='market')loadMarket();else if(activeTab==='keyUsage')loadKeyUsage();else load()};$('market').addEventListener('click',e=>{const h=e.target.closest('[data-fam]');if(!h)return;const fam=h.dataset.fam;if(marketOpen.has(fam))marketOpen.delete(fam);else marketOpen.add(fam);if(lastMarket)renderMarket(lastMarket)});$('marketSearch').oninput=()=>{if(lastMarket)renderMarket(lastMarket)};$('tradableOnly').checked=localStorage.getItem('tradableOnly')==='1';$('tradableOnly').onchange=()=>{localStorage.setItem('tradableOnly',$('tradableOnly').checked?'1':'0');if(lastMarket)renderMarket(lastMarket)};$('marketCopy').onclick=()=>{if(!lastMarket){showErr('No catalog data loaded yet');return}navigator.clipboard.writeText(JSON.stringify(lastMarket,null,2)).then(()=>toast('Catalog copied to clipboard')).catch(e=>showErr(e.message))};$('marketSkill').onclick=async()=>{try{const r=await fetch('/dashboard/api/skill',{credentials:'same-origin'});if(r.status===401){showLogin();return}if(!r.ok)throw new Error('skill '+r.status);const text=await r.text();const blob=new Blob([text],{type:'text/markdown'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='SKILL.md';a.click();URL.revokeObjectURL(a.href);toast('SKILL.md downloaded — load it into any assistant to author policies')}catch(e){showErr(e.message)}};$('toggleAddProvider').onclick=()=>{const c=$('addProviderCard');c.style.display=c.style.display==='none'?'':'none'};$('addProvCancel').onclick=()=>{$('addProviderCard').style.display='none'};$('addProvSubmit').onclick=addProvider;$('toggleAddCodex').onclick=()=>{const c=$('addCodexCard');c.style.display=c.style.display==='none'?'':'none'};$('addCodexCancel').onclick=()=>{$('addCodexCard').style.display='none'};$('addCodexSubmit').onclick=addCodexAccount;$('addProvId').addEventListener('blur',()=>{if(!$('addProvEnv').value.trim()&&$('addProvId').value.trim())$('addProvEnv').value=$('addProvId').value.trim().toUpperCase().replace(/[^A-Z0-9]+/g,'_')+'_API_KEY'});$('loadKeyUsage').onclick=loadKeyUsage;$('consumer').onchange=load;$('timeframe').onchange=load;$('consumerSearch').oninput=()=>renderConsumers(lastStats.keys||[]);document.querySelectorAll('#consumerStatusSeg button').forEach(b=>b.onclick=()=>{document.querySelectorAll('#consumerStatusSeg button').forEach(x=>x.classList.remove('active'));b.classList.add('active');consumerFilterStatus=b.dataset.status;renderConsumers(lastStats.keys||[])});document.querySelectorAll('#activitySeg button').forEach(b=>b.onclick=()=>{document.querySelectorAll('#activitySeg button').forEach(x=>x.classList.remove('active'));b.classList.add('active');activityKind=b.dataset.kind;render(lastStats)});$('newConsumerKey').onclick=()=>openDrawer('', 'create');$('closeDrawer').onclick=closeDrawer;$('drawerShade').addEventListener('click',e=>{if(e.target===$('drawerShade'))closeDrawer()});$('revealKeys').onclick=revealKeys;$('copyRevealKey').onclick=()=>navigator.clipboard.writeText($('revealKeyValue').value).then(()=>toast('Copied'));$('createKey').onclick=createKey;$('copyKey').onclick=()=>navigator.clipboard.writeText($('newKeyValue').value).then(()=>toast('Key copied'));$('copyKeyHandoff').onclick=()=>navigator.clipboard.writeText($('newKeyHandoffValue').value).then(()=>toast('Setup blurb copied'));$('saveConsumerSettings').onclick=saveConsumerSettings;$('revokeKey').onclick=revokeKey;$('anProvider').onchange=load;$('anModel').onchange=load;setTab(tabFromLocation(),{silent:true});setInterval(()=>{const ds=$('drawerShade');if(ds&&ds.classList.contains('open'))return;const ap=$('addProviderCard'),ac=$('addCodexCard');if((ap&&ap.style.display&&ap.style.display!=='none')||(ac&&ac.style.display&&ac.style.display!=='none'))return;if(document.querySelector('#recent .actDetail:not(.hidden)'))return;if(activeTab==='policies')loadPolicies();else if(activeTab==='market')loadMarket();else load()},15000); | |
| $('loginBtn').onclick=login;$('apiKeyLoginBtn').onclick=apiKeyLogin;$('password').addEventListener('keydown',e=>{if(e.key==='Enter')login()});$('apiKeyLogin').addEventListener('keydown',e=>{if(e.key==='Enter')apiKeyLogin()});$('logout').onclick=logout;$('tabOverview').onclick=()=>setTab('overview');$('tabConsumers').onclick=()=>setTab('consumers');$('tabProviderKeys').onclick=()=>setTab('providerKeys');$('tabKeyUsage').onclick=()=>setTab('keyUsage');$('tabMarket').onclick=()=>setTab('market');$('tabBuilder').onclick=()=>setTab('builder');$('tabActivity').onclick=()=>setTab('activity');$('recent').addEventListener('click',e=>{const cp=e.target.closest('[data-copyterm]');if(cp){navigator.clipboard.writeText(cp.dataset.copyterm).then(()=>toast('Policy term copied'));return}const row=e.target.closest('.actRow');if(!row)return;const det=$('recent').querySelector('.actDetail[data-d="'+row.dataset.i+'"]');if(!det)return;det.classList.toggle('hidden');const tog=row.querySelector('.actToggle');if(tog)tog.textContent=det.classList.contains('hidden')?'▸':'▾'});$('bReview').onclick=bReview;$('bDownload').onclick=bDownload;$('bTestBtn').onclick=bTest;$('bEx1').onclick=()=>bLoadExample('ex1');$('bEx2').onclick=()=>bLoadExample('ex2');$('bAddCond').onclick=()=>{bSync();bFilters.push({field:'latency_ms',rel:'le',val:''});bRender()};$('bAddOr').onclick=()=>{bSync();bFilters.push({kind:'or',subs:[{field:'latency_ms',rel:'le',val:''}]});bRender()};$('bAddScore').onclick=()=>{bSync();bScores.push({field:'field:price_in',w:'0.5',norm:true,inv:true});bRender()};$('b_selector').onchange=()=>{$('bTempWrap').style.display=$('b_selector').value==='sample'?'':'none'};document.querySelectorAll('`#bModeSeg` button').forEach(b=>b.onclick=()=>bSetMode(b.dataset.mode));$('bStructured').addEventListener('change',e=>{if(e.target.classList.contains('bF-field'))bSyncRender()});$('bStructured').addEventListener('click',e=>{const b=e.target.closest('[data-act]');if(!b)return;bSync();const i=+b.dataset.i,j=+b.dataset.j,act=b.dataset.act;if(act==='del')bFilters.splice(i,1);else if(act==='addsub')bFilters[i].subs.push({field:'latency_ms',rel:'le',val:''});else if(act==='delsub')bFilters[i].subs.splice(j,1);else if(act==='delscore')bScores.splice(i,1);bRender()});bRender();document.querySelector('.nav').addEventListener('click',e=>{const b=e.target.closest('[data-tab]');if(b){e.preventDefault();setTab(b.dataset.tab)}});$('refresh').onclick=()=>{if(activeTab==='policies')loadPolicies();else if(activeTab==='market')loadMarket();else if(activeTab==='keyUsage')loadKeyUsage();else load()};$('market').addEventListener('click',e=>{const h=e.target.closest('[data-fam]');if(!h)return;const fam=h.dataset.fam;if(marketOpen.has(fam))marketOpen.delete(fam);else marketOpen.add(fam);if(lastMarket)renderMarket(lastMarket)});$('marketSearch').oninput=()=>{if(lastMarket)renderMarket(lastMarket)};$('tradableOnly').checked=localStorage.getItem('tradableOnly')==='1';$('tradableOnly').onchange=()=>{localStorage.setItem('tradableOnly',$('tradableOnly').checked?'1':'0');if(lastMarket)renderMarket(lastMarket)};$('marketCopy').onclick=()=>{if(!lastMarket){showErr('No catalog data loaded yet');return}navigator.clipboard.writeText(JSON.stringify(lastMarket,null,2)).then(()=>toast('Catalog copied to clipboard')).catch(e=>showErr(e.message))};$('marketSkill').onclick=async()=>{try{const r=await fetch('/dashboard/api/skill',{credentials:'same-origin'});if(r.status===401){showLogin();return}if(!r.ok)throw new Error('skill '+r.status);const text=await r.text();const blob=new Blob([text],{type:'text/markdown'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='SKILL.md';a.click();URL.revokeObjectURL(a.href);toast('SKILL.md downloaded — load it into any assistant to author policies')}catch(e){showErr(e.message)}};$('toggleAddProvider').onclick=()=>{const c=$('addProviderCard');c.style.display=c.style.display==='none'?'':'none'};$('addProvCancel').onclick=()=>{$('addProviderCard').style.display='none'};$('addProvSubmit').onclick=addProvider;$('toggleAddCodex').onclick=()=>{const c=$('addCodexCard');c.style.display=c.style.display==='none'?'':'none'};$('addCodexCancel').onclick=()=>{$('addCodexCard').style.display='none'};$('addCodexSubmit').onclick=addCodexAccount;$('addProvId').addEventListener('blur',()=>{if(!$('addProvEnv').value.trim()&&$('addProvId').value.trim())$('addProvEnv').value=$('addProvId').value.trim().toUpperCase().replace(/[^A-Z0-9]+/g,'_')+'_API_KEY'});$('loadKeyUsage').onclick=loadKeyUsage;$('consumer').onchange=load;$('timeframe').onchange=load;$('consumerSearch').oninput=()=>renderConsumers(lastStats.keys||[]);document.querySelectorAll('`#consumerStatusSeg` button').forEach(b=>b.onclick=()=>{document.querySelectorAll('`#consumerStatusSeg` button').forEach(x=>x.classList.remove('active'));b.classList.add('active');consumerFilterStatus=b.dataset.status;renderConsumers(lastStats.keys||[])});document.querySelectorAll('`#activitySeg` button').forEach(b=>b.onclick=()=>{document.querySelectorAll('`#activitySeg` button').forEach(x=>x.classList.remove('active'));b.classList.add('active');activityKind=b.dataset.kind;render(lastStats)});$('newConsumerKey').onclick=()=>openDrawer('', 'create');$('closeDrawer').onclick=closeDrawer;$('drawerShade').addEventListener('click',e=>{if(e.target===$('drawerShade'))closeDrawer()});$('revealKeys').onclick=revealKeys;$('copyRevealKey').onclick=()=>navigator.clipboard.writeText($('revealKeyValue').value).then(()=>toast('Copied'));$('createKey').onclick=createKey;$('copyKey').onclick=()=>navigator.clipboard.writeText($('newKeyValue').value).then(()=>toast('Key copied'));$('copyKeyHandoff').onclick=()=>navigator.clipboard.writeText($('newKeyHandoffValue').value).then(()=>toast('Setup blurb copied'));$('saveConsumerSettings').onclick=saveConsumerSettings;$('revokeKey').onclick=revokeKey;$('anProvider').onchange=load;$('anModel').onchange=load;setTab(tabFromLocation(),{silent:true});setInterval(()=>{const ds=$('drawerShade');if(ds&&ds.classList.contains('open'))return;const ap=$('addProviderCard'),ac=$('addCodexCard'); | |
| if ((ap && getComputedStyle(ap).display !== 'none') || (ac && getComputedStyle(ac).display !== 'none')) return;if(document.querySelector('`#recent` .actDetail:not(.hidden)'))return;if(activeTab==='policies')loadPolicies();else if(activeTab==='market')loadMarket();else load()},15000); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@auth_proxy.py` at line 3948, The auto-refresh guard inside the setInterval
function is not correctly detecting when the add provider and add codex cards
are open. The condition checks `ap.style.display && ap.style.display !== 'none'`
but when the cards are toggled open, their style.display is set to an empty
string (''), which is falsy, causing the guard to fail. Fix this by removing the
falsy check on ap.style.display and ac.style.display and instead only check that
the display value is not equal to 'none', so the guard properly pauses polling
when either card is visible. Apply this same fix to both the addProviderCard and
addCodexCard conditions in the setInterval function.
Tab clicks currently trigger setTab twice.
On Line 3948, tabs now have direct onclick handlers, but the delegated .nav click handler still calls setTab(b.dataset.tab). A tab click bubbles and executes both, duplicating state updates/network loads.
Suggested fix
- document.querySelector('.nav').addEventListener('click',e=>{const b=e.target.closest('[data-tab]');if(b){e.preventDefault();setTab(b.dataset.tab)}});
+ // Removed delegated tab handler; per-tab onclick handlers already cover navigation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@auth_proxy.py` at line 3948, Tab elements are receiving duplicate setTab
calls because individual onclick handlers (like the ones assigned to
tabOverview, tabConsumers, tabProviderKeys, etc.) are firing in addition to the
delegated click handler on the .nav element. Remove all the direct onclick
assignments for tab buttons (the lines with $('tabOverview').onclick,
$('tabConsumers').onclick, etc.) and instead ensure each tab button has a
data-tab attribute set to the appropriate tab name so that the delegated .nav
click handler on the element with [data-tab] selector will be the sole handler
managing tab switches.
…lapse fix(dashboard): don't collapse open UI on the 15s auto-refresh
…bump) #3 of the operational-store migration: enrich the `calls` fact table with the two raw per-call facts the #4 route/analytics views will derive from — the executed route identity and the cache-token breakdown. Prerequisite for keying per-route stats off the ledger. - Submodule bump core 97d0333 -> 537e204 (unhardcoded-engine #23): the engine's `chosen` now carries `served_by` — the marketplace peer that served the call, or the provider itself for a direct route (never nil). Host suite green on it. - host_store.py: `calls` gains `served_by TEXT` + `tokens_cached BIGINT`, applied to existing tables via idempotent `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` (CREATE TABLE IF NOT EXISTS never alters an existing table — the store gains its first in-place migration). insert_call maps both. route_key is left unchanged: deriving a peer-granular route key from served_by is #4's job; this commit only captures the raw fact. - shim.py: `_build_x_router` surfaces `served_by` from `chosen` (tokens_cached was already there). - auth_proxy.py: the ingress threads served_by + tokens_cached off x_router into the recorded call (both stream and unary paths) -> insert_call. ttft was intentionally NOT added: nothing measures it yet, so the column would be idle (Axis 3). error_type was already a column. Verification: full suite 411 passed, 2 skipped, 0 failed against the compose Postgres; the ALTER migration applies in place on boot; a live chat records served_by + tokens_cached in `calls` end to end against engine #23.
… the filesystem (#38) * feat(host-store): peer_offers — antseed market book off the filesystem Move the antseed marketplace book from market.json (a file on a shared volume, unioned by hand in merge-market.js) into the Postgres host store — the next slice of the JSON/in-process migration after #36. Form delta: - Definition: a new `peer_offers` table holds one RAW row per (peer, service) — the seller's announced prices/cap/reputation as columns, not interpreted. The antseed sidecar is the sole writer (it runs `antseed network browse`); sources/antseed._load_market is the sole reader. The 15-min sliding window that merge-market.js unioned by hand is now a read-time filter on observed_at (WHERE observed_at >= now - window); the sidecar prunes rows past the window. - Invariants: store raw, derive by query — no scoring host-side; the negative / cached>input / reputation gates stay in offers_sync. Fail-soft: a DB error degrades to "no antseed candidates" exactly as a missing dump did. Behaviour preserved: offers_sync / market_book unchanged. - Irreversible: peer_offers is new DB state; market.json is retired. Changes: - host_store.py: peer_offers schema (PK (peer_id, service) + observed_at index) and a window-filtered peer_offers() reader; truncate hook updated. - antseed/write-market.js: replaces merge-market.js — flattens the browse dump to (peer, service) rows, UPSERTs into peer_offers (type-cleaning at the write, mirroring the old Python coercion), prunes past the window. - sources/antseed.py: _load_market reads host_store.peer_offers(); the file / staleness / flatten code and the now-dead coercion helpers are removed. - Dockerfile.antseed: pin pg@8.16.3 + NODE_PATH so the writer can require it. - compose.yml: DATABASE_URL + postgres dependency for the antseed service (it already shares the llm-router-internal network with postgres). - tests: seed peer_offers (shared conftest helper) instead of market.json; new host_store peer_offers round-trip + window tests. Sovereignty (Axis 4): pg is the boring standard Postgres client, pinned, and lives only in the sidecar; no new Python dependency (psycopg is from #36). Verification: full suite 409 passed, 2 skipped, 0 failed against the compose Postgres; the real Node writer -> Postgres -> Python reader round-trip, non-dump validation and window prune checked; the full stack boots healthy and /x/market surfaces a seeded antseed peer end to end. * feat(host-store): buyer_status — antseed buyer status off the filesystem Twin of the peer_offers move: the antseed buyer's status (session pin + escrow + wallet) goes from status-<id>.json on the shared volume to the Postgres host store. With both off the filesystem, sources/antseed.py no longer touches disk and the antseed-market volume is removed entirely. Form delta: - Definition: a new `buyer_status` table holds one row per buyer pid — the raw buyer-reported fields (pinned_peer_id, deposits_available/_reserved, wallet_address, connection_state) as columns. The antseed sidecar writes it (write-status.js on the poll loop + control.js after a wallet op); sources/antseed reads it (_pinned_peer + balances). - Invariants: store raw — deposits stay the strings the buyer reports and are coerced on read, exactly as the JSON status was. Fail-soft: a missing row / store error degrades to "no pin, no balance" as a missing status file did. Behaviour preserved: _pinned_peer / balances unchanged but for the source. - Irreversible: buyer_status is new DB state; status-<id>.json is retired and the antseed-market volume (+ both mounts) is dropped. Changes: - host_store.py: buyer_status schema + a buyer_status(pid) reader; truncate hook updated. - antseed/store.js: shared buyer_status row shape + UPSERT, used by both writers so they can't drift. - antseed/write-status.js: replaces the inline node -e + atomic_write; reads `buyer status --json`, UPSERTs buyer_status, validates (non-status -> no write). - antseed/control.js: refreshStatus UPSERTs buyer_status via a pg pool instead of writing the file; still returns the fresh status for the HTTP response. - antseed/entrypoint.sh: write_status calls write-status.js; the now-dead atomic_write helper is removed; comments updated. - sources/antseed.py: _pinned_peer + balances read host_store.buyer_status; the file / json / Path / market_dir machinery is removed (no disk access). - Dockerfile.antseed: COPY store.js + write-status.js. - compose.yml: drop the antseed-market volume and its router/antseed mounts. - tests: seed buyer_status (shared conftest helper) instead of status files; new host_store buyer_status round-trip/absent test. Verification: full suite 410 passed, 2 skipped, 0 failed against the compose Postgres; the real write-status.js -> Postgres -> Python reader round-trip and non-status validation checked; all four sidecar JS files pass node --check; the full stack boots healthy and creates buyer_status on boot. * feat(host-store): calls carries served_by + tokens_cached (engine #23 bump) #3 of the operational-store migration: enrich the `calls` fact table with the two raw per-call facts the #4 route/analytics views will derive from — the executed route identity and the cache-token breakdown. Prerequisite for keying per-route stats off the ledger. - Submodule bump core 97d0333 -> 537e204 (unhardcoded-engine #23): the engine's `chosen` now carries `served_by` — the marketplace peer that served the call, or the provider itself for a direct route (never nil). Host suite green on it. - host_store.py: `calls` gains `served_by TEXT` + `tokens_cached BIGINT`, applied to existing tables via idempotent `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` (CREATE TABLE IF NOT EXISTS never alters an existing table — the store gains its first in-place migration). insert_call maps both. route_key is left unchanged: deriving a peer-granular route key from served_by is #4's job; this commit only captures the raw fact. - shim.py: `_build_x_router` surfaces `served_by` from `chosen` (tokens_cached was already there). - auth_proxy.py: the ingress threads served_by + tokens_cached off x_router into the recorded call (both stream and unary paths) -> insert_call. ttft was intentionally NOT added: nothing measures it yet, so the column would be idle (Axis 3). error_type was already a column. Verification: full suite 411 passed, 2 skipped, 0 failed against the compose Postgres; the ALTER migration applies in place on boot; a live chat records served_by + tokens_cached in `calls` end to end against engine #23. * test(host-store): guard the peer_offers/buyer_status cross-language column contract peer_offers and buyer_status are CREATEd by the Python host store but WRITTEN by the Node antseed sidecar (write-market.js, antseed/store.js) and seeded by Python test mimics (conftest). Three places must agree on the column set and nothing at runtime makes them: the readers are fail-soft, so a renamed/added/dropped column degrades antseed to "no candidates" silently -- and the unit suite can't see it, because it seeds via the Python mimic, not the real Node writer (green proves the reader works, not that Node and Python agree). Add a static contract test that parses the column list out of all three sources and asserts it matches per table. Pure text parsing: no DB, no node runtime, runs in the ordinary unit suite; red on any drift (verified by injecting a rename). The live behave e2e stays the only thing exercising the real Node writer; this guards the part that drifts. * fix(antseed): guard a non-hex ANTSEED_IDENTITY_HEX in the entrypoint Prod runs the sidecar as the image now (not the inline node command), so the entrypoint must keep the inline's safety: a CHANGE_ME / unset-secret placeholder is not a valid identity and the CLI would reject it. Unset it when it isn't a 64-hex string so the buyer falls back to a generated key on the data volume (matching the previous inline behaviour); the prod secret is a real hot-wallet.
Problem
The dashboard auto-refreshes every 15s (
setIntervalre-running the active tab's loader). For the Activity tab that rebuilds the recent-requests table from scratch, so an expanded Activity row collapses by itself every 15s. Same class of issue closes an open consumer drawer or the add-provider / add-codex card.Fix
One-line guard on the auto-poll: skip the refresh while the user has something open — a drawer (
drawerShade.open), the add-provider/add-codex card, or an expanded Activity detail (#recent .actDetail:not(.hidden)). The next tick refreshes normally once they close it.Verification
Covered by a real-browser (headless chromium) regression test in the BDD suite PR: expand an Activity row, wait 17s (past the auto-poll), assert it's still expanded.
🤖 Generated with Claude Code
Summary by CodeRabbit