|
|
@@ -102,6 +102,42 @@
|
|
|
color: var(--dim); margin: 6px 0; }
|
|
|
.callout b, .callout code { color: var(--term-fg); }
|
|
|
|
|
|
+ /* ——— day pills + session gantt ——— */
|
|
|
+ .days { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
|
|
|
+ .dpill { flex: 1; min-width: 84px; max-width: 140px; background: none;
|
|
|
+ border: 1px solid var(--subtle); border-radius: 4px;
|
|
|
+ padding: 9px 6px; font: inherit; color: var(--dim);
|
|
|
+ cursor: pointer; text-align: center; }
|
|
|
+ .dpill:hover { border-color: var(--dim); background: var(--hover); }
|
|
|
+ .dpill .dow { font-size: 10px; color: var(--subtle); display: block; }
|
|
|
+ .dpill .date { font-size: 11px; color: var(--term-fg); font-weight: 500;
|
|
|
+ display: block; margin: 2px 0 4px; }
|
|
|
+ .dpill .pct { font-size: 16px; font-weight: 700; color: var(--term-fg); display: block; }
|
|
|
+ .dpill .ns { font-size: 10px; color: var(--subtle); display: block; margin-top: 2px; }
|
|
|
+ .dpill.heaviest .pct { color: var(--clay); }
|
|
|
+ .dpill.sel { border-color: var(--clay); background: rgba(217,119,87,0.10); }
|
|
|
+ .gantt-hd { display: flex; justify-content: space-between; align-items: baseline;
|
|
|
+ margin-bottom: 6px; }
|
|
|
+ .gantt-hd .day { color: var(--term-fg); font-weight: 500; }
|
|
|
+ .gantt-hd .stats { font-size: 11px; color: var(--dim); }
|
|
|
+ .gantt-hd .stats b { color: var(--clay); }
|
|
|
+ .gantt { position: relative; border-top: 1px solid var(--outline);
|
|
|
+ border-bottom: 1px solid var(--outline); min-height: 32px; }
|
|
|
+ .lane { position: relative; height: 16px;
|
|
|
+ border-bottom: 1px dashed rgba(255,255,255,0.04); }
|
|
|
+ .seg { position: absolute; top: 2px; height: 12px; border-radius: 2px;
|
|
|
+ opacity: .85; cursor: crosshair; }
|
|
|
+ .seg:hover { opacity: 1; outline: 1px solid var(--term-fg); z-index: 2; }
|
|
|
+ .gantt-rule { position: absolute; top: 0; bottom: 0; width: 0;
|
|
|
+ border-left: 1px dashed var(--subtle); opacity: .4;
|
|
|
+ pointer-events: none; }
|
|
|
+ .gantt-axis { display: flex; justify-content: space-between;
|
|
|
+ font-size: 10px; color: var(--subtle); padding: 4px 0; }
|
|
|
+ .gantt-leg { font-size: 10px; color: var(--subtle); margin-top: 8px;
|
|
|
+ display: flex; gap: 14px; flex-wrap: wrap; }
|
|
|
+ .gantt-leg .sw { display: inline-block; width: 14px; height: 10px;
|
|
|
+ border-radius: 2px; vertical-align: middle; margin-right: 4px; }
|
|
|
+
|
|
|
/* ——— block-char bars ——— */
|
|
|
.bar { display: grid; grid-template-columns: 26ch 1fr 8ch; gap: 14px;
|
|
|
padding: 2px 0; align-items: center; }
|
|
|
@@ -231,6 +267,21 @@
|
|
|
<div class="section-body" id="project-bars"></div>
|
|
|
</section>
|
|
|
|
|
|
+ <section id="timeline-section">
|
|
|
+ <div class="hr"></div>
|
|
|
+ <h2>session timeline by day<span class="hint">click a day · ←/→ to navigate</span></h2>
|
|
|
+ <div class="section-body">
|
|
|
+ <div class="days" id="day-pills"></div>
|
|
|
+ <div class="gantt-hd">
|
|
|
+ <span class="day" id="g-day">—</span>
|
|
|
+ <span class="stats" id="g-stats"></span>
|
|
|
+ </div>
|
|
|
+ <div class="gantt-axis"><span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>24:00</span></div>
|
|
|
+ <div class="gantt" id="gantt"></div>
|
|
|
+ <div class="gantt-leg" id="gantt-leg"></div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
<section>
|
|
|
<div class="hr"></div>
|
|
|
<h2>most expensive prompts<span class="hint">click to expand context</span></h2>
|
|
|
@@ -335,6 +386,65 @@
|
|
|
`<div class="val">${typeof v==='number'&&v>=1e4?fmt(v):v}</div>`+
|
|
|
(d?`<div class="detail">${d}</div>`:'')+`</div>`).join('');
|
|
|
|
|
|
+ // session timeline by day
|
|
|
+ (function() {
|
|
|
+ const days = (DATA.by_day||[]).slice(-14);
|
|
|
+ if (!days.length) { $('timeline-section').style.display='none'; return; }
|
|
|
+ const PCOL = ['rgb(177,185,249)','rgb(78,186,101)','#D97757','rgb(255,193,7)',
|
|
|
+ 'rgb(255,107,128)','#9b8cff','#6ec1d6','#c792ea'];
|
|
|
+ const dayTotal = days.reduce((a,d)=>a+d.tokens,0) || 1;
|
|
|
+ const tokMax = Math.max(...days.map(d=>d.tokens));
|
|
|
+ const projects = [...new Set(days.flatMap(d=>d.sessions.map(s=>s.project)))];
|
|
|
+ const colorOf = p => PCOL[projects.indexOf(p)%PCOL.length];
|
|
|
+ const hhmm = m => (m>=1440?`+${Math.floor(m/1440)}d `:'') +
|
|
|
+ `${String(Math.floor(m/60)%24).padStart(2,'0')}:${String(m%60).padStart(2,'0')}`;
|
|
|
+ const md = iso => { const [,mo,da]=iso.split('-'); return `${MON[+mo-1]} ${+da}`; };
|
|
|
+ let sel = days.findIndex(d=>d.tokens===tokMax);
|
|
|
+
|
|
|
+ function pills() {
|
|
|
+ $('day-pills').innerHTML = days.map((d,i)=>
|
|
|
+ `<button class="dpill${d.tokens===tokMax?' heaviest':''}${i===sel?' sel':''}" data-i="${i}">`+
|
|
|
+ `<span class="dow">${esc(d.dow)}</span>`+
|
|
|
+ `<span class="date">${esc(md(d.date))}</span>`+
|
|
|
+ `<span class="pct">${(100*d.tokens/dayTotal).toFixed(1)}%</span>`+
|
|
|
+ `<span class="ns">${d.sessions.length} sess</span></button>`
|
|
|
+ ).join('');
|
|
|
+ $('day-pills').querySelectorAll('.dpill').forEach(el=>
|
|
|
+ el.onclick=()=>{sel=+el.dataset.i;pills();gantt();});
|
|
|
+ }
|
|
|
+ function gantt() {
|
|
|
+ const d = days[sel], DAY = 1440;
|
|
|
+ $('g-day').textContent = `${d.dow} ${md(d.date)}`;
|
|
|
+ $('g-stats').innerHTML = `${d.sessions.length} sessions · ${fmt(d.tokens)} tokens`+
|
|
|
+ ` · peak <b>${d.peak}</b> concurrent at <b>${hhmm(d.peak_at_min)}</b>`;
|
|
|
+ const lanes = [];
|
|
|
+ for (const s of d.sessions) {
|
|
|
+ let placed = false;
|
|
|
+ for (const L of lanes) if (L[L.length-1].end_min <= s.start_min) { L.push(s); placed=true; break; }
|
|
|
+ if (!placed) lanes.push([s]);
|
|
|
+ }
|
|
|
+ let h = '';
|
|
|
+ for (let t=0;t<=24;t+=6) h += `<div class="gantt-rule" style="left:${100*t/24}%"></div>`;
|
|
|
+ h += lanes.map(L=>`<div class="lane">${L.map(s=>{
|
|
|
+ const end = Math.min(s.end_min, DAY);
|
|
|
+ const w = Math.max(0.15, 100*(end-s.start_min)/DAY);
|
|
|
+ const tip = `folder: ${short(s.project)}\n`+
|
|
|
+ `${hhmm(s.start_min)}–${hhmm(s.end_min)} · ${fmt(s.tokens)} tokens\n`+
|
|
|
+ `session ${s.id}`;
|
|
|
+ return `<span class="seg" style="left:${100*s.start_min/DAY}%;width:${w}%;`+
|
|
|
+ `background:${colorOf(s.project)}" title="${esc(tip)}"></span>`;
|
|
|
+ }).join('')}</div>`).join('');
|
|
|
+ $('gantt').innerHTML = h || '<div class="callout">no sessions</div>';
|
|
|
+ }
|
|
|
+ document.addEventListener('keydown',e=>{
|
|
|
+ if (e.key==='ArrowRight'&&sel<days.length-1){sel++;pills();gantt();e.preventDefault();}
|
|
|
+ if (e.key==='ArrowLeft'&&sel>0){sel--;pills();gantt();e.preventDefault();}
|
|
|
+ });
|
|
|
+ $('gantt-leg').innerHTML = projects.slice(0,12).map(p=>
|
|
|
+ `<span><span class="sw" style="background:${colorOf(p)}"></span>${esc(short(p))}</span>`).join('');
|
|
|
+ pills(); gantt();
|
|
|
+ })();
|
|
|
+
|
|
|
// block-char project bars
|
|
|
(function() {
|
|
|
const W = 48;
|
|
|
@@ -366,57 +476,52 @@
|
|
|
return h + '</div>';
|
|
|
}
|
|
|
|
|
|
- // top prompts — share of grand total
|
|
|
- (function() {
|
|
|
- const ps = (DATA.top_prompts||[]).slice(0,100);
|
|
|
+ // expandable drill-down list with "show N more" toggle
|
|
|
+ function drillList(hostId, items, rowFn, empty) {
|
|
|
const SHOW = 5;
|
|
|
- const row = p => {
|
|
|
- const inTot = p.input.uncached+p.input.cache_create+p.input.cache_read;
|
|
|
- return `<details><summary>`+
|
|
|
- `<span class="amt">${share(p.total_tokens)}</span>`+
|
|
|
- `<span class="desc">${esc(p.text)}</span>`+
|
|
|
- `<span class="meta">${niceDate(p.ts)} · ${esc(short(p.project))} · ${p.api_calls} calls`+
|
|
|
- (p.subagent_calls?` · ${p.subagent_calls} subagents`:'')+
|
|
|
- ` · ${pct(p.input.cache_read,inTot)} cached</span>`+
|
|
|
- `</summary><div class="body">`+
|
|
|
- renderContext(p.context)+
|
|
|
- `<div>session <code>${esc(p.session)}</code></div>`+
|
|
|
- `<div>in: uncached ${fmt(p.input.uncached)} · cache-create ${fmt(p.input.cache_create)} · `+
|
|
|
- `cache-read ${fmt(p.input.cache_read)} · out ${fmt(p.output)}</div>`+
|
|
|
- `</div></details>`;
|
|
|
- };
|
|
|
- const head = ps.slice(0,SHOW).map(row).join('');
|
|
|
- const rest = ps.slice(SHOW).map(row).join('');
|
|
|
- $('top-prompts').innerHTML = ps.length
|
|
|
- ? head + (rest
|
|
|
- ? `<div id="tp-rest" hidden>${rest}</div>`+
|
|
|
- `<button id="tp-more" class="more-btn">show ${ps.length-SHOW} more</button>`
|
|
|
- : '')
|
|
|
- : '<div class="callout">No prompts in range.</div>';
|
|
|
- const btn = $('tp-more');
|
|
|
+ const host = $(hostId);
|
|
|
+ if (!items.length) { host.innerHTML = `<div class="callout">${empty}</div>`; return; }
|
|
|
+ const head = items.slice(0,SHOW).map(rowFn).join('');
|
|
|
+ const rest = items.slice(SHOW).map(rowFn).join('');
|
|
|
+ host.innerHTML = head + (rest
|
|
|
+ ? `<div hidden>${rest}</div><button class="more-btn">show ${items.length-SHOW} more</button>`
|
|
|
+ : '');
|
|
|
+ const btn = host.querySelector('.more-btn');
|
|
|
if (btn) btn.onclick = () => {
|
|
|
- const r = $('tp-rest'); r.hidden = !r.hidden;
|
|
|
- btn.textContent = r.hidden ? `show ${ps.length-SHOW} more` : 'show less';
|
|
|
+ const r = btn.previousElementSibling; r.hidden = !r.hidden;
|
|
|
+ btn.textContent = r.hidden ? `show ${items.length-SHOW} more` : 'show less';
|
|
|
};
|
|
|
- })();
|
|
|
+ }
|
|
|
|
|
|
- // cache breaks
|
|
|
- (function() {
|
|
|
- const bs = (DATA.cache_breaks||[]).slice(0,100);
|
|
|
- $('cache-breaks').innerHTML = bs.map(b =>
|
|
|
- `<details><summary>`+
|
|
|
- `<span class="amt">${fmt(b.uncached)}</span>`+
|
|
|
- `<span class="desc">${esc(short(b.project))} · `+
|
|
|
- `${b.kind==='subagent'?esc(b.agentType||'subagent'):'main'}</span>`+
|
|
|
- `<span class="meta">${niceDate(b.ts)} · ${pct(b.uncached,b.total)} of ${fmt(b.total)} uncached</span>`+
|
|
|
+ drillList('top-prompts', (DATA.top_prompts||[]).slice(0,100), p => {
|
|
|
+ const inTot = p.input.uncached+p.input.cache_create+p.input.cache_read;
|
|
|
+ return `<details><summary>`+
|
|
|
+ `<span class="amt">${share(p.total_tokens)}</span>`+
|
|
|
+ `<span class="desc">${esc(p.text)}</span>`+
|
|
|
+ `<span class="meta">${niceDate(p.ts)} · ${esc(short(p.project))} · ${p.api_calls} calls`+
|
|
|
+ (p.subagent_calls?` · ${p.subagent_calls} subagents`:'')+
|
|
|
+ ` · ${pct(p.input.cache_read,inTot)} cached</span>`+
|
|
|
`</summary><div class="body">`+
|
|
|
- renderContext(b.context,
|
|
|
- `<div class="ctx-break"><b>${fmt(b.uncached)}</b> uncached `+
|
|
|
- `(${pct(b.uncached,b.total)} of ${fmt(b.total)}) — cache break here</div>`)+
|
|
|
- `<div>session <code>${esc(b.session)}</code></div>`+
|
|
|
- `</div></details>`
|
|
|
- ).join('') || '<div class="callout">No cache breaks over threshold.</div>';
|
|
|
- })();
|
|
|
+ renderContext(p.context)+
|
|
|
+ `<div>session <code>${esc(p.session)}</code></div>`+
|
|
|
+ `<div>in: uncached ${fmt(p.input.uncached)} · cache-create ${fmt(p.input.cache_create)} · `+
|
|
|
+ `cache-read ${fmt(p.input.cache_read)} · out ${fmt(p.output)}</div>`+
|
|
|
+ `</div></details>`;
|
|
|
+ }, 'No prompts in range.');
|
|
|
+
|
|
|
+ drillList('cache-breaks', (DATA.cache_breaks||[]).slice(0,100), b =>
|
|
|
+ `<details><summary>`+
|
|
|
+ `<span class="amt">${fmt(b.uncached)}</span>`+
|
|
|
+ `<span class="desc">${esc(short(b.project))} · `+
|
|
|
+ `${b.kind==='subagent'?esc(b.agentType||'subagent'):'main'}</span>`+
|
|
|
+ `<span class="meta">${niceDate(b.ts)} · ${pct(b.uncached,b.total)} of ${fmt(b.total)} uncached</span>`+
|
|
|
+ `</summary><div class="body">`+
|
|
|
+ renderContext(b.context,
|
|
|
+ `<div class="ctx-break"><b>${fmt(b.uncached)}</b> uncached `+
|
|
|
+ `(${pct(b.uncached,b.total)} of ${fmt(b.total)}) — cache break here</div>`)+
|
|
|
+ `<div>session <code>${esc(b.session)}</code></div>`+
|
|
|
+ `</div></details>`,
|
|
|
+ 'No cache breaks over threshold.');
|
|
|
|
|
|
// sortable table
|
|
|
function table(el, cols, rows) {
|