Skip to content

fix(dashboard): don't collapse open UI on the 15s auto-refresh#3

Merged
jmlago merged 1 commit into
mainfrom
fix/dashboard-autorefresh-collapse
Jun 22, 2026
Merged

fix(dashboard): don't collapse open UI on the 15s auto-refresh#3
jmlago merged 1 commit into
mainfrom
fix/dashboard-autorefresh-collapse

Conversation

@jmlago

@jmlago jmlago commented Jun 22, 2026

Copy link
Copy Markdown
Member

Problem

The dashboard auto-refreshes every 15s (setInterval re-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

  • Bug Fixes
    • Improved auto-refresh behavior to prevent UI disruptions while users interact with the dashboard. Auto-refresh now pauses when the controls drawer, account forms, or activity details are actively being viewed or edited.

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>
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

In auth_proxy.py's embedded dashboard HTML/JS, tab navigation switches from a delegated container click handler to direct per-tab onclick assignments. The 15-second auto-refresh loop gains early-return guards that skip the refresh when the consumer controls drawer is open, add-provider/add-codex cards are visible, or an activity detail row is expanded.

Changes

Dashboard JS: Tab Navigation and Auto-Refresh Guards

Layer / File(s) Summary
Tab onclick wiring and auto-refresh guard conditions
auth_proxy.py
Replaces the delegated nav-container click handler with direct per-tab onclick assignments. Adds early-return conditions to the 15-second auto-refresh loop to skip refresh when the consumer controls drawer is open, add-provider/add-codex cards are shown, or a recent activity detail row is expanded.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~5 minutes

Poem

A rabbit tabs through menus bright,
No sudden refresh mid-delight!
The drawer stays open, forms stand tall,
Expanded rows won't slip or fall.
🐇 onclick by onclick, all is right! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main fix: preventing the auto-refresh from collapsing open UI elements, which is the core change in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dashboard-autorefresh-collapse

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72f2d604-fa2c-4c8c-93a9-3ea0cb8aa891

📥 Commits

Reviewing files that changed from the base of the PR and between 0ff3bb2 and a4da1c0.

📒 Files selected for processing (1)
  • auth_proxy.py

Comment thread auth_proxy.py
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
$('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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

@jmlago jmlago merged commit 996d607 into main Jun 22, 2026
1 check passed
jmlago added a commit that referenced this pull request Jun 22, 2026
…lapse

fix(dashboard): don't collapse open UI on the 15s auto-refresh
jmlago added a commit that referenced this pull request Jun 29, 2026
…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.
jmlago added a commit that referenced this pull request Jun 29, 2026
… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant