template.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>claude usage</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com">
  8. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  9. <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
  10. <style>
  11. :root {
  12. /* anthropic / claude-code palette */
  13. --ivory: #FAF9F5;
  14. --term-bg: #1a1918;
  15. --term-fg: #d1cfc5;
  16. --titlebar: #252321;
  17. --outline: rgba(255,255,255,0.08);
  18. --hover: rgba(255,255,255,0.035);
  19. --clay: #D97757;
  20. --dim: rgb(136,136,136);
  21. --subtle: rgb(80,80,80);
  22. --green: rgb(78,186,101);
  23. --red: rgb(255,107,128);
  24. --blue: rgb(177,185,249);
  25. --yellow: rgb(255,193,7);
  26. --mono: 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, Monaco, monospace;
  27. }
  28. * { box-sizing: border-box; }
  29. html { background: var(--ivory); }
  30. body {
  31. margin: 0; padding: 48px 24px 80px;
  32. font: 13px/1.55 var(--mono);
  33. font-variant-numeric: tabular-nums;
  34. color: var(--term-fg);
  35. }
  36. /* ——— terminal window chrome ——— */
  37. .term {
  38. max-width: 1180px; margin: 0 auto;
  39. background: var(--term-bg);
  40. border-radius: 8px;
  41. outline: 1px solid var(--outline);
  42. box-shadow: 0 20px 60px rgba(20,20,19,0.22);
  43. }
  44. .titlebar {
  45. background: var(--titlebar);
  46. border-radius: 8px 8px 0 0;
  47. border-bottom: 1px solid var(--outline);
  48. padding: 11px 14px;
  49. display: flex; align-items: center; gap: 7px;
  50. }
  51. .titlebar .dot { width: 11px; height: 11px; border-radius: 50%; background: #3a3836; }
  52. .titlebar .path { margin-left: 14px; color: var(--dim); font-size: 11px; }
  53. .term-body { padding: 22px 30px 30px; }
  54. /* ——— command + hero ——— */
  55. .cmd { color: var(--dim); margin-bottom: 6px; }
  56. .cmd .prompt { color: var(--clay); }
  57. .cmd .flag { color: var(--blue); }
  58. #meta-line { color: var(--subtle); font-size: 11px; }
  59. #hero { margin: 14px 0 6px; }
  60. #hero-total { font-size: 56px; font-weight: 700; line-height: 1; }
  61. #hero-total .unit { color: var(--clay); }
  62. #hero-total .label { font-size: 18px; font-weight: 400; color: var(--dim); margin-left: 8px; }
  63. #hero-split { color: var(--dim); margin-top: 8px; }
  64. #hero-split b { color: var(--term-fg); font-weight: 500; }
  65. #hero-split .ok { color: var(--green); }
  66. #hero-split .warn { color: var(--yellow); }
  67. /* ——— sections ——— */
  68. section { margin-top: 26px; }
  69. .hr { color: var(--subtle); overflow: hidden; white-space: nowrap;
  70. user-select: none; margin-bottom: 8px; }
  71. .hr::after { content: '────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────'; }
  72. h2 { all: unset; display: block; color: var(--clay); font-weight: 500; }
  73. h2::before { content: '▸ '; }
  74. h2 .hint { color: var(--subtle); font-size: 11px; font-weight: 400; margin-left: 10px; }
  75. .section-body { margin-top: 10px; }
  76. /* ——— overall stat grid ——— */
  77. #overall-grid { display: grid; gap: 4px 28px;
  78. grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
  79. .stat { padding: 4px 0; }
  80. .stat .label { font-size: 11px; color: var(--dim); }
  81. .stat .val { font-size: 20px; font-weight: 500; }
  82. .stat .detail { font-size: 11px; color: var(--subtle); }
  83. /* ——— takeaways ——— */
  84. .take { display: grid; grid-template-columns: 9ch 1fr; gap: 18px;
  85. padding: 6px 0; align-items: baseline; }
  86. .take .fig { text-align: right; font-weight: 700; font-size: 15px; }
  87. .take .txt { color: var(--dim); }
  88. .take .txt b { color: var(--term-fg); font-weight: 500; }
  89. .take.bad .fig { color: var(--red); }
  90. .take.good .fig { color: var(--green); }
  91. .take.info .fig { color: var(--blue); }
  92. /* ——— callouts (recommendations) ——— */
  93. .callout { padding: 6px 0 6px 14px; border-left: 2px solid var(--subtle);
  94. color: var(--dim); margin: 6px 0; }
  95. .callout b, .callout code { color: var(--term-fg); }
  96. /* ——— day pills + session gantt ——— */
  97. .days { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
  98. .dpill { flex: 1; min-width: 84px; max-width: 140px; background: none;
  99. border: 1px solid var(--subtle); border-radius: 4px;
  100. padding: 9px 6px; font: inherit; color: var(--dim);
  101. cursor: pointer; text-align: center; }
  102. .dpill:hover { border-color: var(--dim); background: var(--hover); }
  103. .dpill .dow { font-size: 10px; color: var(--subtle); display: block; }
  104. .dpill .date { font-size: 11px; color: var(--term-fg); font-weight: 500;
  105. display: block; margin: 2px 0 4px; }
  106. .dpill .pct { font-size: 16px; font-weight: 700; color: var(--term-fg); display: block; }
  107. .dpill .ns { font-size: 10px; color: var(--subtle); display: block; margin-top: 2px; }
  108. .dpill.heaviest .pct { color: var(--clay); }
  109. .dpill.sel { border-color: var(--clay); background: rgba(217,119,87,0.10); }
  110. .gantt-hd { display: flex; justify-content: space-between; align-items: baseline;
  111. margin-bottom: 6px; }
  112. .gantt-hd .day { color: var(--term-fg); font-weight: 500; }
  113. .gantt-hd .stats { font-size: 11px; color: var(--dim); }
  114. .gantt-hd .stats b { color: var(--clay); }
  115. .gantt { position: relative; border-top: 1px solid var(--outline);
  116. border-bottom: 1px solid var(--outline); min-height: 32px; }
  117. .lane { position: relative; height: 16px;
  118. border-bottom: 1px dashed rgba(255,255,255,0.04); }
  119. .seg { position: absolute; top: 2px; height: 12px; border-radius: 2px;
  120. opacity: .85; cursor: crosshair; }
  121. .seg:hover { opacity: 1; outline: 1px solid var(--term-fg); z-index: 2; }
  122. .gantt-rule { position: absolute; top: 0; bottom: 0; width: 0;
  123. border-left: 1px dashed var(--subtle); opacity: .4;
  124. pointer-events: none; }
  125. .gantt-axis { display: flex; justify-content: space-between;
  126. font-size: 10px; color: var(--subtle); padding: 4px 0; }
  127. .gantt-leg { font-size: 10px; color: var(--subtle); margin-top: 8px;
  128. display: flex; gap: 14px; flex-wrap: wrap; }
  129. .gantt-leg .sw { display: inline-block; width: 14px; height: 10px;
  130. border-radius: 2px; vertical-align: middle; margin-right: 4px; }
  131. /* ——— block-char bars ——— */
  132. .bar { display: grid; grid-template-columns: 26ch 1fr 8ch; gap: 14px;
  133. padding: 2px 0; align-items: center; }
  134. .bar .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  135. .bar .blocks { color: var(--clay); white-space: pre; overflow: hidden; }
  136. .bar .blocks .empty { color: var(--subtle); }
  137. .bar .pct { text-align: right; color: var(--dim); }
  138. .bar:hover .name { color: var(--clay); }
  139. /* ——— drill-down lists (top prompts, cache breaks) ——— */
  140. .drill details { border-top: 1px solid var(--outline); }
  141. .drill details:last-of-type { border-bottom: 1px solid var(--outline); }
  142. .drill summary { list-style: none; cursor: pointer;
  143. display: grid; grid-template-columns: 8ch 1fr; gap: 16px;
  144. padding: 9px 4px; align-items: baseline; }
  145. .drill summary::-webkit-details-marker { display: none; }
  146. .drill summary:hover { background: var(--hover); }
  147. .drill .amt { font-weight: 700; text-align: right; color: var(--clay); }
  148. .drill .desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  149. .drill .meta { grid-column: 2; font-size: 11px; color: var(--subtle); }
  150. .drill details[open] summary .desc { white-space: normal; }
  151. .drill .body { padding: 4px 4px 14px calc(8ch + 20px); font-size: 12px; color: var(--dim); }
  152. /* ——— transcript context (±2 user msgs) — light inset for legibility ——— */
  153. .ctx { margin: 8px 0 12px; padding: 10px 14px;
  154. background: #F0EEE6; color: #1a1918;
  155. border-radius: 6px; font-size: 12px; }
  156. .ctx-msg { padding: 4px 0; white-space: pre-wrap; }
  157. .ctx-msg .who { color: #87867F; font-size: 11px; }
  158. .ctx-msg .ts { color: #87867F; font-size: 10px; margin-left: 8px; }
  159. .ctx-msg.here { margin: 2px -14px; padding: 6px 11px 6px 14px;
  160. border-left: 3px solid var(--clay);
  161. background: rgba(217,119,87,0.10); }
  162. .ctx-msg.here .who { color: var(--clay); font-weight: 500; }
  163. .ctx-gap { color: #87867F; font-size: 11px; padding: 3px 0 3px 7ch; }
  164. .ctx-gap::before { content: '⟨ '; }
  165. .ctx-gap::after { content: ' ⟩'; }
  166. .ctx-break { margin: 2px -14px; padding: 8px 11px 8px 14px;
  167. border-left: 3px solid #BD5E6D;
  168. background: rgba(189,94,109,0.12); color: #A63244; }
  169. .ctx-break b { color: #1a1918; margin-right: 6px; }
  170. .more-btn { display: block; width: 100%; margin-top: 10px; padding: 9px;
  171. background: none; border: 1px dashed var(--subtle); cursor: pointer;
  172. font: 500 11px/1 var(--mono); letter-spacing: 0.06em;
  173. color: var(--dim); }
  174. .more-btn:hover { border-color: var(--dim); color: var(--term-fg); }
  175. /* ——— tables ——— */
  176. .scroll { max-height: 440px; overflow: auto;
  177. border-top: 1px solid var(--outline); border-bottom: 1px solid var(--outline); }
  178. table { width: 100%; border-collapse: collapse; font-size: 12px; }
  179. th, td { text-align: left; padding: 6px 10px; }
  180. td { border-top: 1px solid rgba(255,255,255,0.04); }
  181. th { position: sticky; top: 0; background: var(--term-bg); z-index: 1;
  182. font-weight: 500; font-size: 11px; color: var(--subtle);
  183. cursor: pointer; user-select: none;
  184. border-bottom: 1px solid var(--outline); }
  185. th:hover { color: var(--dim); }
  186. th.sorted { color: var(--clay); }
  187. th.sorted::after { content: ' ↓'; }
  188. th.sorted.asc::after { content: ' ↑'; }
  189. td.num, th.num { text-align: right; }
  190. tbody tr:hover td { background: var(--hover); }
  191. footer { margin-top: 28px; color: var(--subtle); font-size: 11px;
  192. display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
  193. code { color: var(--blue); }
  194. a { color: var(--clay); }
  195. ::selection { background: var(--clay); color: var(--term-bg); }
  196. @media (max-width: 760px) {
  197. body { padding: 20px 12px 48px; }
  198. .term-body { padding: 16px 16px 24px; }
  199. #hero-total { font-size: 40px; }
  200. .bar { grid-template-columns: 14ch 1fr 7ch; gap: 8px; }
  201. .drill summary { grid-template-columns: 6ch 1fr; }
  202. .drill .body { padding-left: 12px; }
  203. .take { grid-template-columns: 7ch 1fr; gap: 12px; }
  204. }
  205. </style>
  206. </head>
  207. <body>
  208. <div class="term">
  209. <div class="titlebar">
  210. <span class="dot"></span><span class="dot"></span><span class="dot"></span>
  211. <span class="path" id="title-path">~/.claude — session-report</span>
  212. </div>
  213. <div class="term-body">
  214. <div class="cmd"><span class="prompt">&gt;</span> claude usage <span id="cmd-flags"></span></div>
  215. <div id="meta-line">loading…</div>
  216. <div id="hero">
  217. <div id="hero-total">—</div>
  218. <div id="hero-split"></div>
  219. </div>
  220. <!-- ====================================================================
  221. FINDINGS — agent fills this. 3–5 one-line takeaways. Use .bad for
  222. waste/anomalies, .good for healthy signals, .info for neutral.
  223. ==================================================================== -->
  224. <section>
  225. <div class="hr"></div>
  226. <h2>findings</h2>
  227. <div class="section-body" id="takeaways">
  228. <!-- AGENT: anomalies -->
  229. <div class="take"><div class="fig">—</div><div class="txt">No findings generated yet.</div></div>
  230. <!-- /AGENT -->
  231. </div>
  232. </section>
  233. <!-- ====================================================================
  234. Everything below renders automatically from #report-data.
  235. ==================================================================== -->
  236. <section>
  237. <div class="hr"></div>
  238. <h2>summary</h2>
  239. <div class="section-body" id="overall-grid"></div>
  240. </section>
  241. <section>
  242. <div class="hr"></div>
  243. <h2>tokens by project<span class="hint">share of total</span></h2>
  244. <div class="section-body" id="project-bars"></div>
  245. </section>
  246. <section id="timeline-section">
  247. <div class="hr"></div>
  248. <h2>session timeline by day<span class="hint">click a day · ←/→ to navigate</span></h2>
  249. <div class="section-body">
  250. <div class="days" id="day-pills"></div>
  251. <div class="gantt-hd">
  252. <span class="day" id="g-day">—</span>
  253. <span class="stats" id="g-stats"></span>
  254. </div>
  255. <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>
  256. <div class="gantt" id="gantt"></div>
  257. <div class="gantt-leg" id="gantt-leg"></div>
  258. </div>
  259. </section>
  260. <section>
  261. <div class="hr"></div>
  262. <h2>most expensive prompts<span class="hint">click to expand context</span></h2>
  263. <div class="section-body drill" id="top-prompts"></div>
  264. </section>
  265. <section>
  266. <div class="hr"></div>
  267. <h2>cache breaks<span class="hint">&gt;100k uncached · click for context</span></h2>
  268. <div class="section-body drill" id="cache-breaks"></div>
  269. </section>
  270. <section>
  271. <div class="hr"></div>
  272. <h2>projects</h2>
  273. <div class="section-body scroll"><table id="tbl-projects"></table></div>
  274. </section>
  275. <section>
  276. <div class="hr"></div>
  277. <h2>subagent types</h2>
  278. <div class="section-body scroll"><table id="tbl-subagents"></table></div>
  279. </section>
  280. <section>
  281. <div class="hr"></div>
  282. <h2>skills &amp; slash commands</h2>
  283. <div class="section-body scroll"><table id="tbl-skills"></table></div>
  284. </section>
  285. <section>
  286. <div class="hr"></div>
  287. <h2>recommendations</h2>
  288. <div class="section-body">
  289. <!-- AGENT: optimizations -->
  290. <div class="callout">No suggestions generated yet.</div>
  291. <!-- /AGENT -->
  292. </div>
  293. </section>
  294. <footer>
  295. <span id="foot-gen"></span>
  296. <span id="foot-stats"></span>
  297. </footer>
  298. </div>
  299. </div>
  300. <!-- ========================================================================
  301. DATA — agent replaces the {} below with the full --json output.
  302. ======================================================================== -->
  303. <script id="report-data" type="application/json">{}</script>
  304. <script>
  305. (function() {
  306. const DATA = JSON.parse(document.getElementById('report-data').textContent || '{}');
  307. const $ = id => document.getElementById(id);
  308. const fmt = n => n>=1e9 ? (n/1e9).toFixed(2)+'B' : n>=1e6 ? (n/1e6).toFixed(2)+'M'
  309. : n>=1e3 ? (n/1e3).toFixed(1)+'k' : String(n);
  310. const pct = (a,b) => b>0 ? ((100*a/b).toFixed(1)+'%') : '—';
  311. const esc = s => String(s).replace(/[&<>"]/g, c =>
  312. ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
  313. const short = p => String(p||'').replace(/^-Users-[^-]+-/,'').replace(/^code-/,'');
  314. const MON = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  315. const niceDate = iso => { const d = new Date(iso); return isNaN(d) ? '' :
  316. `${MON[d.getMonth()]} ${d.getDate()} · ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; };
  317. if (!DATA.overall) { $('meta-line').textContent = 'no data — embed JSON in #report-data.'; return; }
  318. // header + hero
  319. const o = DATA.overall, it = o.input_tokens;
  320. const GRAND = it.total + o.output_tokens;
  321. const share = n => GRAND>0 ? (100*n/GRAND).toFixed(1)+'%' : '—';
  322. const span = o.span;
  323. $('cmd-flags').innerHTML = DATA.since
  324. ? `<span class="flag">--since</span> ${esc(DATA.since)}` : '';
  325. $('meta-line').textContent =
  326. (span ? `${span.from.slice(0,10)} → ${span.to.slice(0,10)}` : '') +
  327. ` · ${DATA.root || ''}`;
  328. $('foot-gen').textContent = `generated ${DATA.generated_at?.slice(0,16).replace('T',' ') || ''}`;
  329. $('foot-stats').textContent =
  330. `${o.sessions} sessions · ${o.api_calls} api calls · ${o.human_messages} prompts`;
  331. const num = fmt(GRAND), m = num.match(/^([\d.]+)([A-Za-z]*)$/);
  332. $('hero-total').innerHTML =
  333. `${m?m[1]:num}<span class="unit">${m?m[2]:''}</span><span class="label">tokens</span>`;
  334. const cacheCls = it.pct_cached>=85 ? 'ok' : 'warn';
  335. $('hero-split').innerHTML =
  336. `<b>${fmt(it.total)}</b> input <span class="${cacheCls}">(${it.pct_cached}% cache-read)</span> · `+
  337. `<b>${fmt(o.output_tokens)}</b> output · all figures below are % of this total`;
  338. // overall stat grid
  339. $('overall-grid').innerHTML = [
  340. ['sessions', o.sessions],
  341. ['api calls', o.api_calls],
  342. ['human msgs', o.human_messages],
  343. ['active hours', o.hours.active, `${o.hours.wall_clock} wall-clock`],
  344. ['cache breaks', o.cache_breaks_over_100k, '>100k uncached'],
  345. ['subagent calls', o.subagent.calls, `avg ${fmt(o.subagent.avg_tokens_per_call)}`],
  346. ].map(([l,v,d]) =>
  347. `<div class="stat"><div class="label">${l}</div>`+
  348. `<div class="val">${typeof v==='number'&&v>=1e4?fmt(v):v}</div>`+
  349. (d?`<div class="detail">${d}</div>`:'')+`</div>`).join('');
  350. // session timeline by day
  351. (function() {
  352. const days = (DATA.by_day||[]).slice(-14);
  353. if (!days.length) { $('timeline-section').style.display='none'; return; }
  354. const PCOL = ['rgb(177,185,249)','rgb(78,186,101)','#D97757','rgb(255,193,7)',
  355. 'rgb(255,107,128)','#9b8cff','#6ec1d6','#c792ea'];
  356. const dayTotal = days.reduce((a,d)=>a+d.tokens,0) || 1;
  357. const tokMax = Math.max(...days.map(d=>d.tokens));
  358. const projects = [...new Set(days.flatMap(d=>d.sessions.map(s=>s.project)))];
  359. const colorOf = p => PCOL[projects.indexOf(p)%PCOL.length];
  360. const hhmm = m => (m>=1440?`+${Math.floor(m/1440)}d `:'') +
  361. `${String(Math.floor(m/60)%24).padStart(2,'0')}:${String(m%60).padStart(2,'0')}`;
  362. const md = iso => { const [,mo,da]=iso.split('-'); return `${MON[+mo-1]} ${+da}`; };
  363. let sel = days.findIndex(d=>d.tokens===tokMax);
  364. function pills() {
  365. $('day-pills').innerHTML = days.map((d,i)=>
  366. `<button class="dpill${d.tokens===tokMax?' heaviest':''}${i===sel?' sel':''}" data-i="${i}">`+
  367. `<span class="dow">${esc(d.dow)}</span>`+
  368. `<span class="date">${esc(md(d.date))}</span>`+
  369. `<span class="pct">${(100*d.tokens/dayTotal).toFixed(1)}%</span>`+
  370. `<span class="ns">${d.sessions.length} sess</span></button>`
  371. ).join('');
  372. $('day-pills').querySelectorAll('.dpill').forEach(el=>
  373. el.onclick=()=>{sel=+el.dataset.i;pills();gantt();});
  374. }
  375. function gantt() {
  376. const d = days[sel], DAY = 1440;
  377. $('g-day').textContent = `${d.dow} ${md(d.date)}`;
  378. $('g-stats').innerHTML = `${d.sessions.length} sessions · ${fmt(d.tokens)} tokens`+
  379. ` · peak <b>${d.peak}</b> concurrent at <b>${hhmm(d.peak_at_min)}</b>`;
  380. const lanes = [];
  381. for (const s of d.sessions) {
  382. let placed = false;
  383. for (const L of lanes) if (L[L.length-1].end_min <= s.start_min) { L.push(s); placed=true; break; }
  384. if (!placed) lanes.push([s]);
  385. }
  386. let h = '';
  387. for (let t=0;t<=24;t+=6) h += `<div class="gantt-rule" style="left:${100*t/24}%"></div>`;
  388. h += lanes.map(L=>`<div class="lane">${L.map(s=>{
  389. const end = Math.min(s.end_min, DAY);
  390. const w = Math.max(0.15, 100*(end-s.start_min)/DAY);
  391. const tip = `folder: ${short(s.project)}\n`+
  392. `${hhmm(s.start_min)}–${hhmm(s.end_min)} · ${fmt(s.tokens)} tokens\n`+
  393. `session ${s.id}`;
  394. return `<span class="seg" style="left:${100*s.start_min/DAY}%;width:${w}%;`+
  395. `background:${colorOf(s.project)}" title="${esc(tip)}"></span>`;
  396. }).join('')}</div>`).join('');
  397. $('gantt').innerHTML = h || '<div class="callout">no sessions</div>';
  398. }
  399. document.addEventListener('keydown',e=>{
  400. if (e.key==='ArrowRight'&&sel<days.length-1){sel++;pills();gantt();e.preventDefault();}
  401. if (e.key==='ArrowLeft'&&sel>0){sel--;pills();gantt();e.preventDefault();}
  402. });
  403. $('gantt-leg').innerHTML = projects.slice(0,12).map(p=>
  404. `<span><span class="sw" style="background:${colorOf(p)}"></span>${esc(short(p))}</span>`).join('');
  405. pills(); gantt();
  406. })();
  407. // block-char project bars
  408. (function() {
  409. const W = 48;
  410. const rows = Object.entries(DATA.by_project||{})
  411. .map(([k,v]) => [k, v.input_tokens.total + v.output_tokens])
  412. .sort((a,b)=>b[1]-a[1]).slice(0,15);
  413. const max = rows[0]?.[1] || 1;
  414. $('project-bars').innerHTML = rows.map(([k,v]) => {
  415. const n = Math.max(1, Math.round(W*v/max));
  416. return `<div class="bar"><span class="name" title="${esc(k)} — ${fmt(v)}">${esc(short(k))}</span>`+
  417. `<span class="blocks">${'█'.repeat(n)}<span class="empty">${'░'.repeat(W-n)}</span></span>`+
  418. `<span class="pct">${share(v)}</span></div>`;
  419. }).join('');
  420. })();
  421. // ±2 user-message transcript context
  422. function renderContext(ctx, mark) {
  423. if (!ctx || !ctx.length) return '<div class="ctx-gap">no transcript context available</div>';
  424. let h = '<div class="ctx">';
  425. ctx.forEach((m, i) => {
  426. const who = m.here ? '&gt; user' : ' user';
  427. h += `<div class="ctx-msg${m.here?' here':''}">`+
  428. `<span class="who">${who}</span> ${esc(m.text||'(non-text)')}`+
  429. `<span class="ts">${niceDate(m.ts)}</span></div>`;
  430. if (m.here && mark) h += mark;
  431. if (i < ctx.length-1 || m.here)
  432. h += `<div class="ctx-gap">${m.calls} api call${m.calls===1?'':'s'}</div>`;
  433. });
  434. return h + '</div>';
  435. }
  436. // expandable drill-down list with "show N more" toggle
  437. function drillList(hostId, items, rowFn, empty) {
  438. const SHOW = 5;
  439. const host = $(hostId);
  440. if (!items.length) { host.innerHTML = `<div class="callout">${empty}</div>`; return; }
  441. const head = items.slice(0,SHOW).map(rowFn).join('');
  442. const rest = items.slice(SHOW).map(rowFn).join('');
  443. host.innerHTML = head + (rest
  444. ? `<div hidden>${rest}</div><button class="more-btn">show ${items.length-SHOW} more</button>`
  445. : '');
  446. const btn = host.querySelector('.more-btn');
  447. if (btn) btn.onclick = () => {
  448. const r = btn.previousElementSibling; r.hidden = !r.hidden;
  449. btn.textContent = r.hidden ? `show ${items.length-SHOW} more` : 'show less';
  450. };
  451. }
  452. drillList('top-prompts', (DATA.top_prompts||[]).slice(0,100), p => {
  453. const inTot = p.input.uncached+p.input.cache_create+p.input.cache_read;
  454. return `<details><summary>`+
  455. `<span class="amt">${share(p.total_tokens)}</span>`+
  456. `<span class="desc">${esc(p.text)}</span>`+
  457. `<span class="meta">${niceDate(p.ts)} · ${esc(short(p.project))} · ${p.api_calls} calls`+
  458. (p.subagent_calls?` · ${p.subagent_calls} subagents`:'')+
  459. ` · ${pct(p.input.cache_read,inTot)} cached</span>`+
  460. `</summary><div class="body">`+
  461. renderContext(p.context)+
  462. `<div>session <code>${esc(p.session)}</code></div>`+
  463. `<div>in: uncached ${fmt(p.input.uncached)} · cache-create ${fmt(p.input.cache_create)} · `+
  464. `cache-read ${fmt(p.input.cache_read)} · out ${fmt(p.output)}</div>`+
  465. `</div></details>`;
  466. }, 'No prompts in range.');
  467. drillList('cache-breaks', (DATA.cache_breaks||[]).slice(0,100), b =>
  468. `<details><summary>`+
  469. `<span class="amt">${fmt(b.uncached)}</span>`+
  470. `<span class="desc">${esc(short(b.project))} · `+
  471. `${b.kind==='subagent'?esc(b.agentType||'subagent'):'main'}</span>`+
  472. `<span class="meta">${niceDate(b.ts)} · ${pct(b.uncached,b.total)} of ${fmt(b.total)} uncached</span>`+
  473. `</summary><div class="body">`+
  474. renderContext(b.context,
  475. `<div class="ctx-break"><b>${fmt(b.uncached)}</b> uncached `+
  476. `(${pct(b.uncached,b.total)} of ${fmt(b.total)}) — cache break here</div>`)+
  477. `<div>session <code>${esc(b.session)}</code></div>`+
  478. `</div></details>`,
  479. 'No cache breaks over threshold.');
  480. // sortable table
  481. function table(el, cols, rows) {
  482. let sortIdx = cols.findIndex(c=>c.sort), asc = false;
  483. function render() {
  484. const sorted = rows.slice().sort((a,b)=>{
  485. const va=a[sortIdx], vb=b[sortIdx];
  486. return (asc?1:-1)*(typeof va==='number' ? va-vb : String(va).localeCompare(String(vb)));
  487. });
  488. el.innerHTML = `<thead><tr>${cols.map((c,i)=>
  489. `<th class="${c.num?'num':''} ${i===sortIdx?'sorted'+(asc?' asc':''):''}" data-i="${i}">${c.h}</th>`
  490. ).join('')}</tr></thead><tbody>${sorted.map(r=>
  491. `<tr>${r.map((v,i)=>`<td class="${cols[i].num?'num':''}">${
  492. cols[i].fmt?cols[i].fmt(v):esc(v)}</td>`).join('')}</tr>`
  493. ).join('')}</tbody>`;
  494. el.querySelectorAll('th').forEach(th=>th.onclick=()=>{
  495. const i=+th.dataset.i; if(i===sortIdx)asc=!asc; else{sortIdx=i;asc=false;} render();
  496. });
  497. }
  498. render();
  499. }
  500. function statRows(obj) {
  501. return Object.entries(obj||{}).map(([k,v])=>[
  502. short(k), GRAND>0 ? 100*(v.input_tokens.total+v.output_tokens)/GRAND : 0,
  503. v.sessions, v.api_calls, v.human_messages,
  504. v.input_tokens.total, v.input_tokens.pct_cached, v.output_tokens,
  505. v.hours.active, v.cache_breaks_over_100k,
  506. v.subagent.calls, v.subagent.avg_tokens_per_call,
  507. ]);
  508. }
  509. const statCols = [
  510. {h:'name'},{h:'% total',num:1,sort:1,fmt:v=>v.toFixed(1)+'%'},
  511. {h:'sess',num:1},{h:'calls',num:1},{h:'msgs',num:1},
  512. {h:'input',num:1,fmt:fmt},{h:'%cached',num:1,fmt:v=>v+'%'},
  513. {h:'output',num:1,fmt:fmt},{h:'active h',num:1},{h:'breaks',num:1},
  514. {h:'subagents',num:1},{h:'avg sub tok',num:1,fmt:fmt},
  515. ];
  516. table($('tbl-projects'), statCols, statRows(DATA.by_project));
  517. table($('tbl-subagents'), statCols, statRows(DATA.by_subagent_type));
  518. table($('tbl-skills'), statCols, statRows(DATA.by_skill));
  519. })();
  520. </script>
  521. </body>
  522. </html>