add more statistics

This commit is contained in:
2026-07-11 16:44:45 +02:00
parent 8b89b59f9c
commit 55172d747e
6 changed files with 54 additions and 16 deletions
+11 -5
View File
@@ -52,17 +52,23 @@ let cache={loading:true,scannedAt:null,sessions:[],error:null};
async function scan(){ cache.loading=true; try { const list=await files(SESSIONS); const sessions=[]; for(let i=0;i<list.length;i+=12) sessions.push(...await Promise.all(list.slice(i,i+12).map(parse))); cache={loading:false,scannedAt:new Date().toISOString(),sessions,error:null}; } catch(e){cache={...cache,loading:false,error:e.message};} }
function stats(url){
const from=url.searchParams.get('from'), to=url.searchParams.get('to'), modelFilter=url.searchParams.get('model'), projectFilter=url.searchParams.get('project');
const sessions=cache.sessions.filter(s=>(!from||s.started>=from)&&(!to||s.started<to+'T23:59:59.999Z')&&(!projectFilter||s.project===projectFilter));
const events=sessions.flatMap(s=>s.events.map(e=>({...e,session:s.id,project:s.project,source:s.source}))).filter(e=>!modelFilter||e.model===modelFilter);
const from=url.searchParams.get('from'), to=url.searchParams.get('to'), modelFilter=url.searchParams.get('model'), projectFilter=url.searchParams.get('project'), sourceFilter=url.searchParams.get('source');
// Usage belongs to the time of the model call, not the time the chat began.
// A Codex session can remain active across several days, so filtering sessions
// by `started` silently drops later usage from long-running chats.
const allSessions=cache.sessions.filter(s=>!projectFilter||s.project===projectFilter);
const events=allSessions.flatMap(s=>s.events.map(e=>({...e,session:s.id,project:s.project,source:s.source}))).filter(e=>(!from||e.at>=from)&&(!to||e.at<to+'T23:59:59.999Z')&&(!modelFilter||e.model===modelFilter)&&(!sourceFilter||e.source===sourceFilter));
const matchedIds=new Set(events.map(e=>e.session));
const sessions=allSessions.filter(s=>matchedIds.has(s.id));
const total=empty(), byDay={}, byModel={}, byProject={}, dayModels={};
for(const e of events){ add(total,e); const day=(e.at||'unknown').slice(0,10); add(byDay[day]??=empty(),e); add(byModel[e.model]??=empty(),e); add(byProject[e.project]??=empty(),e); add((dayModels[day]??={})[e.model]??=empty(),e); }
for(const s of sessions){ if(modelFilter && !s.models.includes(modelFilter)) continue; total.turns+=s.usage.turns; total.duration+=s.usage.duration; (byProject[s.project]??=empty()).turns+=s.usage.turns; (byProject[s.project]).duration+=s.usage.duration; }
const rows=o=>Object.entries(o).map(([name,value])=>({name,...value})).filter(x=>x.total>0).sort((a,b)=>b.total-a.total);
const sessionRows=sessions.map(s=>({...s,models:undefined,events:undefined,model:s.models.join(', '),usage:undefined,...s.usage})).sort((a,b)=>b.total-a.total);
const bySession={}; for(const e of events){ const row=bySession[e.session]??={id:e.session,started:e.at,project:e.project,source:e.source,models:new Set(),...empty()}; row.models.add(e.model); add(row,e); if(e.at<row.started)row.started=e.at; }
const sessionRows=Object.values(bySession).map(s=>({...s,models:undefined,model:[...s.models].join(', ')})).sort((a,b)=>b.total-a.total);
const days=rows(byDay).sort((a,b)=>a.name.localeCompare(b.name)).map(d=>({...d,models:Object.fromEntries(Object.entries(dayModels[d.name]||{}).map(([m,u])=>[m,u.total]))}));
const modelRows=rows(byModel);
return {meta:{schemaVersion:2,scannedAt:cache.scannedAt,files:cache.sessions.length,matchedSessions:sessions.length,events:events.length,pricingSource:'https://developers.openai.com/api/docs/pricing'},total,days,models:modelRows,projects:rows(byProject),sessions:sessionRows.slice(0,100),filters:{models:modelRows.map(x=>x.name).sort(),projects:[...new Set(cache.sessions.filter(s=>s.usage.total>0).map(s=>s.project))].sort()},prices:Object.fromEntries(Object.entries(prices).map(([k,v])=>[k,{input:v[0],cached:v[1],output:v[2]}]))};
return {meta:{schemaVersion:2,scannedAt:cache.scannedAt,files:cache.sessions.length,matchedSessions:sessions.length,events:events.length,pricingSource:'https://developers.openai.com/api/docs/pricing'},total,days,models:modelRows,projects:rows(byProject),sessions:sessionRows.slice(0,100),filters:{models:modelRows.map(x=>x.name).sort(),projects:[...new Set(cache.sessions.filter(s=>s.usage.total>0).map(s=>s.project))].sort(),sources:[...new Set(cache.sessions.map(s=>s.source))].sort()},prices:Object.fromEntries(Object.entries(prices).map(([k,v])=>[k,{input:v[0],cached:v[1],output:v[2]}]))};
}
function send(res,code,body,type='application/json'){res.writeHead(code,{'content-type':type,'cache-control':'no-store'});res.end(type==='application/json'?JSON.stringify(body):body)}