Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dashboard/js/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const DELIVERY_EVIDENCE_FILTERS=['all','comparable','spend_only','git_only','una
let deliveryRange=localStorage.getItem('tm_delivery_range')||'30';
if(!DELIVERY_RANGES.includes(deliveryRange)){deliveryRange='30';localStorage.setItem('tm_delivery_range',deliveryRange);}
let deliveryProject=localStorage.getItem('tm_delivery_project')||'';
let deliveryPayload=null,deliveryRequest=0,deliverySelectedDay='',deliveryLastLoadedAt=0;
let deliveryPayload=null,deliveryRequest=0,deliveryLoading=false,deliverySelectedDay='',deliveryLastLoadedAt=0;
let deliverySort={key:'covered_cost',direction:'desc'};
let deliveryModelSort={key:'covered_cost',direction:'desc'};
let deliveryEvidenceFilter='all';
Expand Down Expand Up @@ -130,7 +130,7 @@ const countWord=(n,one,many=one+'s')=>Number(n)===1?one:many;
const speedFmt=n=>{n=Number(n)||0;if(n<=0)return'--';if(n>=100)return n.toFixed(0);if(n>=10)return n.toFixed(1);return n.toFixed(2);};
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
// Work page state is declared early: the initial hash route can run before later definitions.
let WORK=null,workRequest=0,workLastLoadedAt=0,workMonths='6',workRuntime='',workProject='';
let WORK=null,workRequest=0,workLoading=false,workLastLoadedAt=0,workMonths='6',workRuntime='',workProject='';
let workSupported=null;
// Declared before the first route runs: a deep link schedules its scroll during startup.
let pendingSettingsScroll=null;
Expand Down
4 changes: 2 additions & 2 deletions dashboard/js/live.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ function applyLiveState(state){
if($('view-capabilities').classList.contains('on'))renderCapabilities(LATEST.xsession?.capabilities);
if($('view-efficiency').classList.contains('on'))renderActiveEfficiency();
if(['view-daily','view-efficiency','view-git','view-work'].some(id=>$(id).classList.contains('on'))&&Date.now()-goalsLoadedAt>30000)loadGoals(true);
if($('view-git').classList.contains('on')&&Date.now()-deliveryLastLoadedAt>60000)loadDelivery();
if($('view-work').classList.contains('on')&&Date.now()-workLastLoadedAt>15000)loadWork();
if($('view-git').classList.contains('on')&&!deliveryLoading&&Date.now()-deliveryLastLoadedAt>60000)loadDelivery();
if($('view-work').classList.contains('on')&&!workLoading&&Date.now()-workLastLoadedAt>15000)loadWork();
if($('view-settings').classList.contains('on'))renderSettings(LATEST.xsession);
if($('view-learn').classList.contains('on'))renderLearn(LATEST);
if($('welcome-dialog').open)renderWelcome(LATEST);
Expand Down
6 changes: 4 additions & 2 deletions dashboard/js/work-insights.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ function renderWork(payload){
}
async function loadWork(){
const request=++workRequest,query=new URLSearchParams({months:workMonths});
workLoading=true;
if(workRuntime)query.set('runtime',workRuntime);if(workProject)query.set('project',workProject);
try{
const response=await fetch(`/work?${query}`,{cache:'no-store'});
Expand All @@ -228,7 +229,7 @@ async function loadWork(){
if(request!==workRequest)return;
$('w-state').dataset.state='storage_error';$('w-state').textContent='Error';
$('w-status-title').textContent='Work insights are unavailable';$('w-status-detail').textContent='Token Meter will try again shortly.';
}
}finally{if(request===workRequest)workLoading=false;}
}
async function postWorkAction(path,payload){
const token=agentActionToken();
Expand Down Expand Up @@ -354,6 +355,7 @@ async function applyWorkSessionFilter(params){
}
async function loadDelivery(){
const request=++deliveryRequest,query=new URLSearchParams({range:deliveryRange});if(deliveryProject)query.set('project',deliveryProject);
deliveryLoading=true;
try{
const response=await fetch(`/git-delivery?${query}`,{cache:'no-store'});if(!response.ok)throw new Error('Git data unavailable');
const payload=await response.json();if(request!==deliveryRequest)return;
Expand All @@ -370,7 +372,7 @@ async function loadDelivery(){
$('d-shape').innerHTML='<div class="deliveryShapeEmpty emptyState">Git evidence unavailable.</div>';
$('d-evidence-spend').textContent='--';setDeliveryEvidenceRatio('Git evidence unavailable.','Git evidence unavailable. Ratios require overlapping spend and Git evidence.');$('d-evidence-freshness').textContent='Git scan unavailable.';
['comparable','spend','git','unavailable'].forEach(key=>{$(`d-evidence-count-${key}`).textContent='--';});
}
}finally{if(request===deliveryRequest)deliveryLoading=false;}
}
$('d-coverage').addEventListener('click',()=>setDeliveryEvidencePanel($('d-evidence-panel').hidden));
$('d-evidence-close').addEventListener('click',()=>setDeliveryEvidencePanel(false));
Expand Down
38 changes: 38 additions & 0 deletions tests/test_dashboard_filter_refresh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import json
import shutil
import subprocess
import unittest
from pathlib import Path


@unittest.skipUnless(shutil.which("node"), "Node.js is required")
class DashboardFilterRefreshTests(unittest.TestCase):
def test_slow_filter_requests_survive_live_refresh_and_new_selections_win(self):
root = Path(__file__).resolve().parents[1]
script = r"""
const fs=require('fs'),root=process.argv[1];
const source=fs.readFileSync(root+'/dashboard/js/work-insights.js','utf8');
const live=fs.readFileSync(root+'/dashboard/js/live.js','utf8');
const part=(s,a,b)=>s.slice(s.indexOf(a),s.indexOf(b,s.indexOf(a)));
let workRequest=0,workLastLoadedAt=0,workMonths='6',workRuntime='',workProject='';
let deliveryRequest=0,deliveryLastLoadedAt=0,deliveryRange='30',deliveryProject='',deliveryLoadRetries=0,deliveryRetryTimer=0;
let workLoading=false,deliveryLoading=false,active='work',renders=[],pending=[];
let RUNTIME_CATALOG={},defaultSessionBudget=0,LATEST=null,pinned=null,currentPanel='',sessionScope='',goalsLoadedAt=Date.now(),workSupported=true;
const no=()=>{},adoptFreshDiscounts=no,applyWorkSupport=no,renderMenubarSettings=no,renderCurrentSessions=no,maybeAutoShowWelcome=no;
const nodes={};function $(id){return nodes[id]||(nodes[id]={classList:{contains:()=>id==='view-'+active},open:false});}
function fetch(url){return new Promise(resolve=>pending.push({url,resolve}));}
function renderWork(p){renders.push('work:'+p.value);} function renderDelivery(p){renders.push('git:'+p.value);}
function answer(index,value){pending[index].resolve({ok:true,json:async()=>({ok:true,value,coverage:{last_checked:true}})});}
const setTimeout=no,clearTimeout=no;
eval(part(source,'async function loadWork(){','async function postWorkAction(')+part(source,'async function loadDelivery(){',"$('d-coverage').addEventListener")+part(live,'function applyLiveState(state){','async function refreshLiveState(){')+`
(async()=>{
active='work';let old=loadWork();workMonths='1d';let chosen=loadWork();applyLiveState({});applyLiveState({});
let workCount=pending.length;answer(0,'old');answer(1,'chosen');await Promise.all([old,chosen]);
active='git';let before=pending.length;let oldGit=loadDelivery();deliveryRange='7';let chosenGit=loadDelivery();applyLiveState({});applyLiveState({});
let gitCount=pending.length-before;answer(before,'old');answer(before+1,'chosen');await Promise.all([oldGit,chosenGit]);
console.log(JSON.stringify({workCount,gitCount,renders}));
})().catch(e=>{console.error(e);process.exitCode=1;});`);
"""
result = subprocess.run(["node", "-e", script, str(root)], text=True, capture_output=True, check=True)
self.assertEqual(json.loads(result.stdout), {"workCount": 2, "gitCount": 2,
"renders": ["work:chosen", "git:chosen"]})
Loading