@hecer/yoke 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,126 +3,71 @@ export function dashboardPage(token, nonce) {
3
3
  if (!/^[a-f0-9]{64}$/u.test(token) || !/^[A-Za-z0-9+/=]+$/u.test(nonce))
4
4
  throw new Error('Invalid dashboard session');
5
5
  return `<!doctype html>
6
- <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Yoke · Project workspace</title>
6
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0b0f12"><title>Yoke · Control room</title>
7
7
  <style nonce="${nonce}">
8
- :root{color-scheme:light;--ink:#15242d;--muted:#647077;--line:#dce1df;--paper:#f4f5f0;--accent:#c34c27;--green:#167452}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.5 system-ui,sans-serif}button{font:inherit;cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #dc734a;outline-offset:3px}.layout{display:grid;grid-template-columns:250px minmax(0,1fr);min-height:100vh}aside{background:#142b32;color:#eef5f1;padding:34px 22px;display:flex;flex-direction:column;gap:30px}.brand{font-size:31px;font-weight:780;letter-spacing:-1.5px}.brand span{color:#ffb185}aside p{color:#afc3c6;font-size:13px}.label{text-transform:uppercase;font-size:11px;letter-spacing:1.7px;font-weight:750;color:var(--muted)}aside .label{color:#91a9ad}nav{display:grid;gap:7px}.nav-button{border:0;background:transparent;color:#cad8d9;text-align:left;padding:11px 12px;border-radius:8px;overflow-wrap:anywhere}.nav-button[aria-current=true]{background:#29444b;color:#fff}.aside-foot{margin-top:auto;border-top:1px solid #355057;padding-top:20px}main{max-width:1500px;width:100%;padding:40px 5vw 70px}.top{display:flex;justify-content:space-between;align-items:center;gap:20px}.local{font-size:12px;color:var(--green);background:#e2ece1;border-radius:30px;padding:6px 12px}.button{border:1px solid #bcc7c5;background:#fff;border-radius:7px;padding:9px 15px;color:var(--ink)}.button.primary{background:var(--ink);color:#fff;border-color:var(--ink)}h1{font-size:clamp(27px,3vw,40px);line-height:1.2;margin:17px 0 8px;letter-spacing:-1.4px}h2{font-size:18px;margin:0 0 18px}h3{font-size:16px;margin:0 0 8px}p{margin:6px 0}.muted{color:var(--muted)}.summary{max-width:850px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin:28px 0}.metric,.panel,.project-card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:22px}.metric strong{display:block;font-size:28px;letter-spacing:-.8px;margin:8px 0 4px}.metric p{font-size:12px;color:var(--muted)}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:18px;margin:28px 0}.project-card{display:flex;flex-direction:column;align-items:flex-start;gap:12px}.project-card p{color:var(--muted);overflow-wrap:anywhere}.project-card button{margin-top:auto}.columns{display:grid;grid-template-columns:1.2fr 1fr;gap:22px}.panel{margin-bottom:22px}.row{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;padding:13px 0;border-top:1px solid #edf0eb}.row:first-child{border-top:0}.row-main{min-width:0;overflow-wrap:anywhere}.row small{display:block;color:var(--muted);margin-top:4px}.badge{font-size:11px;font-weight:650;background:#edf0ed;padding:4px 9px;border-radius:20px;white-space:nowrap}.badge.good{color:#166343;background:#e4f0e7}.badge.attention{color:#a43e22;background:#fff0e7}.notice{padding:13px 16px;margin:18px 0;border-left:3px solid var(--accent);background:#fff2e9;overflow-wrap:anywhere}.actions{display:flex;align-items:center;gap:12px;margin:22px 0}.path{font:12px/1.5 ui-monospace,monospace;word-break:break-all}.empty{padding:26px 0;color:var(--muted)}details{font-size:12px;margin-top:8px}details p{white-space:pre-wrap;max-height:220px;overflow:auto}.timeline .row{font-size:13px}.loading{padding:50px;color:var(--muted)}.overview-tools{display:flex;flex-wrap:wrap;align-items:end;gap:14px;margin:24px 0}.field{display:grid;gap:5px;flex:1 1 260px}.field input{margin:0;width:100%}.filter-group{display:flex;flex-wrap:wrap;gap:7px}.filter-group .button[aria-pressed=true]{background:var(--ink);color:#fff}.state-line{font-size:13px}.comparison{font-weight:650}.chart-gap-note{margin-top:12px}@media(max-width:900px){.layout{grid-template-columns:1fr}aside{padding:16px 24px;gap:12px}.brand{font-size:25px}aside>p,.aside-foot,aside>.label{display:none}nav{display:flex;overflow:auto}.nav-button{white-space:nowrap}.metrics{gap:9px}.metric{padding:15px}.columns{grid-template-columns:1fr}main{padding:25px 22px}.metric strong{font-size:23px}}@media(max-width:520px){.metrics{grid-template-columns:1fr}.top{align-items:flex-start;flex-direction:column}.local{white-space:nowrap}.metric strong{font-size:26px}.metric p{font-size:13px}.overview-tools{align-items:stretch}.filter-group{display:grid;grid-template-columns:1fr}}
9
- main,aside,.panel{min-width:0}.actions{flex-wrap:wrap}.table-scroll{overflow-x:auto;max-width:100%}table{width:100%;border-collapse:collapse;font-size:13px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--line)}select,input{font:inherit;padding:8px;margin:4px;border:1px solid var(--line);border-radius:6px;max-width:100%}</style></head><body><div class="layout"><aside><div class="brand">yoke<span>.</span></div><p>Work you can verify.<br>Projects you can pick up again.</p><div class="label">Your workspace</div><nav id="navigation" aria-label="Projects"></nav><div class="aside-foot"><div class="label">Local workspace</div><p>Project data stays on this machine.</p></div></aside><main><div class="top"><div class="label">Project workspace</div><div><span class="local">● Local session</span> <button id="refresh" class="button">Refresh</button></div></div><div id="content" aria-live="polite"><p class="loading">Loading your projects…</p></div></main></div>
8
+ :root{color-scheme:light;--canvas:#f1f4f3;--surface:#fff;--raised:#f7f9f8;--rule:#d4dddc;--text:#17242a;--muted:#61727a;--action:#a85f00;--signal:#087d79;--positive:#197345;--warning:#875b00;--danger:#ae3434;--focus:#087d79;--rail:#17242a;--rail-text:#e9eff1;--rail-muted:#a9b9bd}:root[data-theme=dark]{color-scheme:dark;--canvas:#0b0f12;--surface:#11171c;--raised:#172027;--rule:#2a3740;--text:#e9eff1;--muted:#91a0a8;--action:#ffb454;--signal:#69d6d0;--positive:#72d39a;--warning:#f2c66d;--danger:#ff8585;--focus:#69d6d0;--rail:#11171c;--rail-text:#e9eff1;--rail-muted:#91a0a8}@media (prefers-color-scheme:dark){:root:not([data-theme=light]){color-scheme:dark;--canvas:#0b0f12;--surface:#11171c;--raised:#172027;--rule:#2a3740;--text:#e9eff1;--muted:#91a0a8;--action:#ffb454;--signal:#69d6d0;--positive:#72d39a;--warning:#f2c66d;--danger:#ff8585;--focus:#69d6d0;--rail:#11171c;--rail-text:#e9eff1;--rail-muted:#91a0a8}}*{box-sizing:border-box}body{margin:0;background:var(--canvas);color:var(--text);font:14px/1.5 'Instrument Sans',system-ui,sans-serif}button,input,select{font:inherit}button{cursor:pointer}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--focus);outline-offset:3px}.control-room-shell{display:grid;grid-template-columns:248px minmax(0,1fr);min-height:100vh}.rail{background:var(--rail);color:var(--rail-text);padding:24px 18px;display:flex;flex-direction:column;gap:22px}.brand{font-size:27px;font-weight:800;letter-spacing:-1.4px}.brand span{color:var(--action)}.rail-copy{color:var(--rail-muted);font-size:12px;margin:0}.rail-label,.label{color:var(--muted);font-size:10px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase}.rail .rail-label{color:var(--rail-muted)}.rail nav{display:grid;gap:4px}.nav-button{border:0;border-left:2px solid transparent;background:transparent;border-radius:4px;color:var(--rail-muted);padding:10px 11px;text-align:left}.nav-button:hover,.nav-button[aria-current=true]{background:var(--raised);border-left-color:var(--action);color:var(--rail-text)}.rail-foot{border-top:1px solid var(--rule);color:var(--rail-muted);font-size:12px;margin-top:auto;padding-top:16px}.canvas{min-width:0;max-width:1600px;padding:24px clamp(20px,4vw,56px) 64px;width:100%}.command-bar{align-items:center;border-bottom:1px solid var(--rule);display:flex;gap:18px;justify-content:space-between;padding-bottom:16px}.command-scope{display:grid;gap:2px}.command-title{font-size:18px;font-weight:750}.command-actions{align-items:center;display:flex;gap:8px}.button{background:var(--surface);border:1px solid var(--rule);border-radius:5px;color:var(--text);padding:8px 12px}.button:hover{border-color:var(--action)}.button.primary{background:var(--action);border-color:var(--action);color:#111}.icon-button{white-space:nowrap}.page-heading{margin:30px 0 22px}.page-heading h1{font-size:clamp(26px,3vw,42px);letter-spacing:-1.7px;line-height:1.1;margin:0 0 7px}.summary{color:var(--muted);margin:0;max-width:760px}.attention-region,.command-panel,.metric-strip,.ranked-projects,.panel,.project-card{background:var(--surface);border:1px solid var(--rule);border-radius:7px}.attention-region{border-left:3px solid var(--action);margin:22px 0;padding:18px 20px}.attention-region h2,.ranked-projects h2{font-size:16px;margin:0}.attention-region>p{color:var(--muted);margin:5px 0 14px}.attention-list{display:grid;gap:0}.attention-item{align-items:center;border-top:1px solid var(--rule);display:flex;gap:14px;justify-content:space-between;padding:11px 0}.attention-item:first-child{border-top:0}.attention-item .row-main{min-width:0}.attention-item small{color:var(--muted);display:block;overflow-wrap:anywhere}.metric-strip{display:grid;gap:0;grid-template-columns:repeat(4,minmax(0,1fr));margin:18px 0}.metric{border-left:1px solid var(--rule);padding:16px 18px}.metric:first-child{border-left:0}.metric strong{display:block;font-family:'JetBrains Mono',ui-monospace,monospace;font-size:24px;font-variant-numeric:tabular-nums;margin:5px 0}.metric p{color:var(--muted);font-size:12px;margin:0}.overview-command-bar{align-items:end;display:grid;gap:12px;grid-template-columns:minmax(220px,2fr) repeat(5,minmax(112px,1fr));margin:22px 0}.field{display:grid;gap:5px;min-width:0}.field input,.field select{background:var(--surface);border:1px solid var(--rule);border-radius:4px;color:var(--text);margin:0;min-width:0;padding:9px;width:100%}.field input::placeholder{color:var(--muted)}.command-panel{padding:14px}.ranked-projects{overflow:hidden}.ranked-heading{align-items:center;display:flex;justify-content:space-between;padding:18px 20px}.ranked-heading p{color:var(--muted);font-size:12px;margin:0}.project-row{align-items:center;border-top:1px solid var(--rule);display:grid;gap:15px;grid-template-columns:34px minmax(190px,2fr) minmax(90px,1fr) minmax(100px,1fr) auto;padding:15px 20px}.project-row:hover{background:var(--raised)}.rank{color:var(--muted);font-family:'JetBrains Mono',ui-monospace,monospace;font-size:12px}.project-identity{min-width:0}.project-name{font-size:15px;font-weight:720;overflow-wrap:anywhere}.project-objective,.project-path,.project-stat{color:var(--muted);font-size:12px;overflow-wrap:anywhere}.project-path{font-family:'JetBrains Mono',ui-monospace,monospace;margin-top:3px}.project-stat{display:grid;gap:2px}.project-stat strong{color:var(--text);font-family:'JetBrains Mono',ui-monospace,monospace;font-variant-numeric:tabular-nums;font-weight:650}.badge{background:var(--raised);border-radius:20px;color:var(--muted);display:inline-block;font-size:11px;font-weight:750;padding:4px 9px;white-space:nowrap}.badge.good{background:color-mix(in srgb,var(--positive) 16%,var(--surface));color:var(--positive)}.badge.attention{background:color-mix(in srgb,var(--danger) 14%,var(--surface));color:var(--danger)}.empty{color:var(--muted);padding:28px 20px}.notice{background:color-mix(in srgb,var(--warning) 12%,var(--surface));border-left:3px solid var(--warning);color:var(--text);margin:12px 0;padding:11px 14px}.row{align-items:flex-start;border-top:1px solid var(--rule);display:flex;gap:18px;justify-content:space-between;padding:13px 0}.row:first-child{border-top:0}.row-main{min-width:0;overflow-wrap:anywhere}.row small{color:var(--muted);display:block;margin-top:4px}.loading{color:var(--muted);padding:50px 0}.actions{align-items:center;display:flex;flex-wrap:wrap;gap:10px;margin:20px 0}.panel{margin-bottom:18px;padding:20px}.panel h2{font-size:16px;margin:0 0 15px}.muted{color:var(--muted)}.path{font-family:'JetBrains Mono',ui-monospace,monospace;overflow-wrap:anywhere}.table-scroll{max-width:100%;overflow-x:auto}table{border-collapse:collapse;font-size:12px;width:100%}th,td{border-bottom:1px solid var(--rule);padding:9px;text-align:left}select,input{max-width:100%}.columns{display:grid;gap:18px;grid-template-columns:1.2fr 1fr}details{font-size:12px;margin-top:8px}details p{max-height:220px;overflow:auto;white-space:pre-wrap}.timeline .row{font-size:13px}.comparison{font-weight:650}.chart-gap-note{margin-top:12px}@media(max-width:1100px){.overview-command-bar{grid-template-columns:repeat(3,minmax(0,1fr))}.overview-command-bar .field:first-child{grid-column:1/-1}}@media(max-width:820px){.control-room-shell{grid-template-columns:1fr}.rail{gap:12px;padding:15px 20px}.rail-copy,.rail-foot,.rail>.rail-label{display:none}.rail nav{display:flex;overflow-x:auto}.nav-button{white-space:nowrap}.canvas{padding:20px 18px 44px}.metric-strip{grid-template-columns:repeat(2,minmax(0,1fr))}.metric:nth-child(3){border-left:0;border-top:1px solid var(--rule)}.metric:nth-child(4){border-top:1px solid var(--rule)}.project-row{grid-template-columns:24px minmax(0,1fr) auto}.project-row .project-stat{display:none}.columns{grid-template-columns:1fr}}@media(max-width:540px){.command-bar{align-items:flex-start;flex-direction:column}.command-actions{width:100%}.command-actions .button{flex:1}.overview-command-bar{grid-template-columns:1fr}.overview-command-bar .field:first-child{grid-column:auto}.metric-strip{grid-template-columns:1fr}.metric,.metric:nth-child(3),.metric:nth-child(4){border-left:0;border-top:1px solid var(--rule)}.metric:first-child{border-top:0}.project-row{align-items:start;grid-template-columns:22px minmax(0,1fr);padding:13px 15px}.project-row .badge,.project-row>.button{grid-column:2}.ranked-heading{align-items:flex-start;flex-direction:column;gap:4px}.attention-item{align-items:flex-start;flex-direction:column;gap:6px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition-duration:0.01ms!important;animation-duration:0.01ms!important}}
9
+ </style></head><body><div class="control-room-shell"><aside class="rail"><div class="brand">yoke<span>.</span></div><p class="rail-copy">Verified work. Clear handoffs.<br>Local by default.</p><div class="rail-label">Workspace</div><nav id="navigation" aria-label="Workspace navigation"></nav><div class="rail-foot">Project data stays on this machine.</div></aside><main class="canvas"><header class="command-bar"><div class="command-scope"><span class="label">Yoke / control room</span><span class="command-title">Workspace overview</span></div><div class="command-actions"><button id="theme-toggle" class="button icon-button" type="button">Use dark theme</button><button id="refresh" class="button primary" type="button">Sync workspace</button></div></header><div id="content" aria-live="polite"><section id="attention-region" class="attention-region" hidden></section><div class="metric-strip" hidden></div><div class="project-row" hidden></div><span hidden>Ranked projects</span><p class="loading">Loading workspace…</p></div></main></div>
10
+ <style nonce="${nonce}">.analytics-chart-grid{display:grid;gap:18px;grid-template-columns:repeat(2,minmax(0,1fr))}.analytics-chart{border-top:1px solid var(--rule);padding-top:14px}.analytics-chart h3{font-size:13px;margin:0 0 10px}.analytics-chart svg{background:var(--raised);display:block;height:auto;min-height:140px;width:100%}.analytics-chart table{font-size:11px}.analytics-view>.metric-strip{margin-top:0}@media(max-width:820px){.analytics-chart-grid{grid-template-columns:1fr}}</style>
11
+ <style nonce="${nonce}">textarea{background:var(--surface);border:1px solid var(--rule);border-radius:4px;color:var(--text);font:inherit;line-height:1.45;max-width:100%;padding:9px;resize:vertical;width:100%}textarea:focus-visible{outline:3px solid var(--focus);outline-offset:3px}.compose-feedback{border-left:3px solid var(--signal);padding-left:12px}.compose-feedback.validation{border-color:var(--warning)}.compose-feedback.failure{border-color:var(--danger)}.status-line{align-items:center;display:flex;gap:8px}.status-line::before{background:var(--signal);border-radius:50%;content:'';display:inline-block;height:8px;width:8px}.status-line.stale::before{background:var(--warning)}.status-line.error::before{background:var(--danger)}.rail,.rail nav{min-width:0}.nav-button{max-width:100%;min-width:0;overflow-wrap:anywhere;white-space:normal}.nav-button:hover,.nav-button[aria-current=true]{color:var(--text)}@media(max-width:820px){.nav-button{flex:0 0 auto;max-width:240px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}</style>
10
12
  <script nonce="${nonce}">
11
13
  const sessionToken = ${JSON.stringify(token)};
12
- const content=document.getElementById('content'), navigation=document.getElementById('navigation');
14
+ const content=document.getElementById('content'),navigation=document.getElementById('navigation'),themeToggle=document.getElementById('theme-toggle');
15
+ function themeState(){return document.documentElement.dataset.theme||((typeof matchMedia==='function'&&matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light')}
16
+ function setTheme(value,persist=true){const theme=value==='dark'?'dark':'light';document.documentElement.dataset.theme=theme;document.documentElement.style.colorScheme=theme;if(persist)localStorage.setItem('yoke-dashboard-theme',theme);themeToggle.textContent=theme==='dark'?'Use light theme':'Use dark theme'}
17
+ function initializeTheme(){const saved=localStorage.getItem('yoke-dashboard-theme');setTheme(saved==='dark'||saved==='light'?saved:((typeof matchMedia==='function'&&matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light'),false)}
18
+ initializeTheme();themeToggle.onclick=()=>setTheme(themeState()==='dark'?'light':'dark');
13
19
  const defaultNavigation={screen:'overview',project:null,view:'now',period:30,group:'day',from:'',to:''};
14
20
  let navigationState=parseNavigationHash(typeof location==='undefined'?'':location.hash);
15
- let selected=navigationState.screen==='project'?navigationState.project:null, projects=[];
21
+ let selected=navigationState.screen==='project'?navigationState.project:null,projects=[],overviewMetrics=[],overviewAnalytics=null;
16
22
  let projectView=navigationState.view,periodDays=navigationState.period,bucket=navigationState.group,customFrom=navigationState.from,customTo=navigationState.to;
17
- let overviewQuery='',overviewFilter='all',requestVersion=0,activeController=null;
23
+ let overviewQuery=navigationState.query||'',overviewFilter=navigationState.status||'all',overviewSort=navigationState.sort||'attention',requestVersion=0,activeController=null;
18
24
  function el(tag,text,cls){const node=document.createElement(tag);if(text!==undefined)node.textContent=String(text);if(cls)node.className=cls;return node}
19
- function append(parent,...children){for(const child of children)parent.append(child);return parent}
25
+ function append(parent,...children){for(const child of children)if(child)parent.append(child);return parent}
20
26
  function badge(state){return el('span',state||'unknown','badge '+(['complete','passed'].includes(state)?'good':['blocked','failed','paused','unverified','unconfirmed','unavailable'].includes(state)?'attention':''))}
21
- function button(label,action,primary=false){const node=el('button',label,'button'+(primary?' primary':''));node.addEventListener('click',()=>Promise.resolve().then(action).catch(error=>content.append(el('p',error.message,'notice'))));return node}
27
+ function button(label,action,primary=false){const node=el('button',label,'button'+(primary?' primary':''));node.type='button';node.addEventListener('click',()=>Promise.resolve().then(action).catch(error=>content.append(el('p',error.message,'notice'))));return node}
22
28
  function duration(ms){if(!Number.isFinite(ms))return 'Unknown';if(ms<60000)return Math.round(ms/1000)+'s';if(ms<3600000)return Math.round(ms/60000)+'m';return (ms/3600000).toFixed(1)+'h'}
23
29
  function tokenText(value){return Number.isFinite(value)?value.toLocaleString('en-US'):'Unknown'}
24
30
  function taskTiming(task,estimate){const planned=estimate?.available?estimate.tasks.find(item=>item.storyId===task.id):undefined;return planned?'Predicted '+duration(planned.endMs-planned.startMs)+' · planned start +'+duration(planned.startMs):task.passes?'Completed · timing unknown':'Duration and planned start unknown'}
25
31
  function unknownCallCount(status){return Number.isFinite(status?.measurement?.unknownCalls)?status.measurement.unknownCalls:Array.isArray(status?.tokens?.calls)?status.tokens.calls.filter(call=>call.usageAvailable!==true).length:undefined}
26
- function renderUsage(project){const status=project.status,usage=status?.tokens,measurement=status?.measurement;const panel=append(el('section',undefined,'panel'),el('h2','Token usage'));panel.append(el('p','Recorded input: '+tokenText(usage?.inputTokens)+' · recorded output: '+tokenText(usage?.outputTokens)));panel.append(el('p','Calls with unknown usage: '+tokenText(unknownCallCount(status))+' · unmeasured attempts: '+tokenText(measurement?.unmeasuredAttempts),'muted'));if(usage?.measurementComplete===false)panel.append(el('p','Partial measurement. These counts are the reported portion; total usage is unknown.','muted'));if(!usage)panel.append(el('p','No token totals have been reported.','muted'));content.append(panel)}
27
- function metric(title,value,note){return append(el('div',undefined,'metric'),el('div',title,'label'),el('strong',value),el('p',note))}
28
32
  function validDate(value){if(!/^\\d{4}-\\d{2}-\\d{2}$/u.test(value))return false;const time=Date.parse(value+'T00:00:00Z');return Number.isFinite(time)&&new Date(time).toISOString().slice(0,10)===value}
29
33
  function parseNavigationHash(hash){
30
- const params=new URLSearchParams(String(hash||'').replace(/^#/u,''));
31
- const screen=params.get('screen')||'overview',project=params.get('project'),view=params.get('view')||'now';
32
- const period=Number(params.get('period')||30),group=params.get('group')||'day',from=params.get('from')||'',to=params.get('to')||'';
34
+ const params=new URLSearchParams(String(hash||'').replace(/^#/u,''));const screen=params.get('screen')||'overview',project=params.get('project'),view=params.get('view')||'now',period=Number(params.get('period')||30),group=params.get('group')||'day',from=params.get('from')||'',to=params.get('to')||'',query=params.get('query')||'',status=params.get('status')||'all',sort=params.get('sort')||'attention';
33
35
  const datesValid=(!from&&!to)||(validDate(from)&&validDate(to)&&from<=to&&Date.parse(to+'T00:00:00Z')-Date.parse(from+'T00:00:00Z')<=365*86400000);
34
- if(!['overview','workspace','project'].includes(screen)||!['now','usage','results'].includes(view)||![1,7,30,90,365].includes(period)||!['day','week','month'].includes(group)||!datesValid||(screen==='project'&&!/^[a-f0-9]{32}$/u.test(project||'')))return {...defaultNavigation};
35
- return {screen,project:screen==='project'?project:null,view,period,group,from,to};
36
- }
37
- function serializeNavigationHash(state){
38
- const params=new URLSearchParams({screen:state.screen,view:state.view,period:String(state.period),group:state.group});
39
- if(state.project)params.set('project',state.project);
40
- if(state.from&&state.to){params.set('from',state.from);params.set('to',state.to)}
41
- return '#'+params.toString();
42
- }
43
- function setNavigation(change,replace=false){
44
- const next=parseNavigationHash(serializeNavigationHash({...navigationState,...change}));
45
- navigationState=next;selected=next.screen==='project'?next.project:null;projectView=next.view;periodDays=next.period;bucket=next.group;customFrom=next.from;customTo=next.to;
46
- if(typeof history!=='undefined'&&typeof location!=='undefined')history[replace?'replaceState':'pushState'](null,'',serializeNavigationHash(next));
47
- renderRoute();
48
- }
49
- function restoreNavigation(){
50
- const restored=parseNavigationHash(location.hash);
51
- // Back/forward can emit both popstate and hashchange for one transition.
52
- if(serializeNavigationHash(restored)===serializeNavigationHash(navigationState))return;
53
- navigationState=restored;selected=navigationState.screen==='project'?navigationState.project:null;projectView=navigationState.view;periodDays=navigationState.period;bucket=navigationState.group;customFrom=navigationState.from;customTo=navigationState.to;
54
- if(selected&&!projects.some(project=>project.id===selected)){navigationState={...navigationState,screen:'overview',project:null};selected=null}
55
- if(typeof history!=='undefined'&&location.hash!==serializeNavigationHash(navigationState))history.replaceState(null,'',serializeNavigationHash(navigationState));
56
- renderRoute();
36
+ if(!['overview','workspace','project'].includes(screen)||!['now','usage','results','history'].includes(view)||![1,7,30,90,365].includes(period)||!['day','week','month'].includes(group)||!['all','active','attention'].includes(status)||!['attention','activity','tokens','cost','acceptance','name'].includes(sort)||query.length>200||!datesValid||(screen==='project'&&!/^[a-f0-9]{32}$/u.test(project||'')))return {...defaultNavigation};
37
+ const state={screen,project:screen==='project'?project:null,view,period,group,from,to};if(params.has('query'))state.query=query;if(params.has('status'))state.status=status;if(params.has('sort'))state.sort=sort;return state;
57
38
  }
39
+ function serializeNavigationHash(state){const params=new URLSearchParams({screen:state.screen||'overview',view:state.view||'now',period:String(state.period||30),group:state.group||'day'});if(state.project)params.set('project',state.project);if(state.from&&state.to){params.set('from',state.from);params.set('to',state.to)}if(state.query)params.set('query',state.query);if(state.status&&state.status!=='all')params.set('status',state.status);if(state.sort&&state.sort!=='attention')params.set('sort',state.sort);return '#'+params.toString()}
40
+ function applyNavigation(next){navigationState=next;selected=next.screen==='project'?next.project:null;projectView=next.view;periodDays=next.period;bucket=next.group;customFrom=next.from;customTo=next.to;overviewQuery=next.query||'';overviewFilter=next.status||'all';overviewSort=next.sort||'attention'}
41
+ function setNavigation(change,replace=false){const next=parseNavigationHash(serializeNavigationHash({...navigationState,...change}));applyNavigation(next);if(typeof history!=='undefined'&&typeof location!=='undefined')history[replace?'replaceState':'pushState'](null,'',serializeNavigationHash(next));renderRoute()}
42
+ function updateOverviewNavigation(change){const next=parseNavigationHash(serializeNavigationHash({...navigationState,...change}));applyNavigation(next);if(typeof history!=='undefined'&&typeof location!=='undefined')history.replaceState(null,'',serializeNavigationHash(next))}
43
+ function restoreNavigation(){const restored=parseNavigationHash(location.hash);if(serializeNavigationHash(restored)===serializeNavigationHash(navigationState))return;applyNavigation(restored);if(selected&&!projects.some(project=>project.id===selected))applyNavigation({...restored,screen:'overview',project:null});if(typeof history!=='undefined'&&location.hash!==serializeNavigationHash(navigationState))history.replaceState(null,'',serializeNavigationHash(navigationState));renderRoute()}
58
44
  function beginRequest(){requestVersion++;if(activeController)activeController.abort();activeController=new AbortController();return {version:requestVersion,signal:activeController.signal}}
59
45
  function currentRequest(version){return version===requestVersion}
60
46
  function isAbort(error){return error&&error.name==='AbortError'}
61
47
  async function api(path,options={},signal){const response=await fetch(path,{...options,signal});const data=await response.json();if(!response.ok)throw Error(data.error||'Request failed');return data}
62
48
  function projectStatus(project,now=Date.now()){
63
- const goal=project.goal?.status||null,loop=project.status?.state||null,attentionStates=['blocked','failed','paused'],activeStates=['active','running'];
64
- const observed=(project.status?.supervision||[]).filter(p=>p.liveness==='alive'&&now-Date.parse(p.heartbeatAt)<15000).map(p=>Date.parse(p.lastProgressAt||'')).filter(Number.isFinite);
65
- const updated=Math.max(Date.parse(project.status?.updatedAt||''),...observed),stale=activeStates.includes(loop)&&Number.isFinite(updated)&&now-updated>20*60000;
66
- const attention=Boolean(project.errors?.length)||attentionStates.includes(loop)||attentionStates.includes(goal)||stale;
67
- const active=activeStates.includes(loop)||activeStates.includes(goal);
68
- let primary=project.errors?.length?'unavailable':attentionStates.includes(loop)?loop:attentionStates.includes(goal)?goal:activeStates.includes(loop)?loop:activeStates.includes(goal)?goal:goal||loop||'ready';
69
- if(stale&&!project.errors?.length&&!attentionStates.includes(goal))primary='unconfirmed';
70
- return {primary,goal,loop,attention,active,stale};
49
+ const goal=project.goal?.status||null,loop=project.status?.state||null,attentionStates=['blocked','failed','paused'],activeStates=['active','running'];const observed=(project.status?.supervision||[]).filter(p=>p.liveness==='alive'&&now-Date.parse(p.heartbeatAt)<15000).map(p=>Date.parse(p.lastProgressAt||'')).filter(Number.isFinite),updated=Math.max(Date.parse(project.status?.updatedAt||''),...observed),stale=activeStates.includes(loop)&&Number.isFinite(updated)&&now-updated>20*60000;const attention=Boolean(project.errors?.length)||attentionStates.includes(loop)||attentionStates.includes(goal)||stale,active=activeStates.includes(loop)||activeStates.includes(goal);let primary=project.errors?.length?'unavailable':attentionStates.includes(loop)?loop:attentionStates.includes(goal)?goal:activeStates.includes(loop)?loop:activeStates.includes(goal)?goal:goal||loop||'ready';if(stale&&!project.errors?.length&&!attentionStates.includes(goal))primary='unconfirmed';return {primary,goal,loop,attention,active,stale}
71
50
  }
72
- function visibleProjects(values,query,filter,now=Date.now()){
73
- const needle=query.trim().toLowerCase();
74
- return values.filter(project=>{
75
- const status=projectStatus(project,now),matches=!needle||[project.name,project.root,project.goal?.objective].some(value=>String(value||'').toLowerCase().includes(needle));
76
- return matches&&(filter==='all'||filter==='active'&&status.active||filter==='attention'&&status.attention);
77
- }).map((project,index)=>({project,index,status:projectStatus(project,now)})).sort((a,b)=>Number(b.status.attention)-Number(a.status.attention)||Number(b.status.active)-Number(a.status.active)||a.index-b.index).map(item=>item.project);
78
- }
79
- function nav(){
80
- navigation.replaceChildren();
81
- const all=el('button','All projects','nav-button');all.setAttribute('aria-current',String(navigationState.screen==='overview'));all.onclick=()=>setNavigation({screen:'overview',project:null});navigation.append(all);
82
- const compare=el('button','Compare consumption','nav-button');compare.setAttribute('aria-current',String(navigationState.screen==='workspace'));compare.onclick=()=>setNavigation({screen:'workspace',project:null});navigation.append(compare);
83
- for(const project of projects){const item=el('button',project.name,'nav-button');item.setAttribute('aria-current',String(navigationState.screen==='project'&&selected===project.id));item.onclick=()=>setNavigation({screen:'project',project:project.id});navigation.append(item)}
84
- }
85
- function heading(title,subtitle){content.replaceChildren(el('h1',title),el('p',subtitle,'summary muted'))}
51
+ function rankingValue(project,key){if(key==='name')return project.name;if(Number.isFinite(project[key]))return project[key];const row=overviewMetrics.find(item=>item.id===project.id);if(row&&Number.isFinite(row[key]))return row[key];if(key==='attention')return projectStatus(project).attention?1:0;return null}
52
+ function rankProjects(values,sort='attention'){return [...values].sort((a,b)=>{if(sort==='name')return String(a.name).localeCompare(String(b.name))||String(a.id).localeCompare(String(b.id));const left=rankingValue(a,sort),right=rankingValue(b,sort);if(left===null&&right!==null)return 1;if(left!==null&&right===null)return -1;if(left!==right)return (right??0)-(left??0);return String(a.name).localeCompare(String(b.name))||String(a.id).localeCompare(String(b.id))})}
53
+ function visibleProjects(values,query,filter,now=Date.now(),sort='attention'){const needle=query.trim().toLowerCase();const filtered=values.filter(project=>{const status=projectStatus(project,now),matches=!needle||[project.name,project.root,project.goal?.objective].some(value=>String(value||'').toLowerCase().includes(needle));return matches&&(filter==='all'||filter==='active'&&status.active||filter==='attention'&&status.attention)});return rankProjects(filtered,sort)}
54
+ function nav(){navigation.replaceChildren();const overview=el('button','Overview','nav-button');overview.type='button';overview.setAttribute('aria-current',String(navigationState.screen==='overview'));overview.onclick=()=>setNavigation({screen:'overview',project:null});navigation.append(overview);const compare=el('button','Workspace analytics','nav-button');compare.type='button';compare.setAttribute('aria-current',String(navigationState.screen==='workspace'));compare.onclick=()=>setNavigation({screen:'workspace',project:null});navigation.append(compare);for(const project of projects){const item=el('button',project.name,'nav-button');item.type='button';item.setAttribute('aria-current',String(navigationState.screen==='project'&&selected===project.id));item.onclick=()=>setNavigation({screen:'project',project:project.id});navigation.append(item)}}
55
+ function heading(title,subtitle){content.setAttribute?.('aria-busy','false');content.replaceChildren(append(el('div',undefined,'page-heading'),el('h1',title),el('p',subtitle,'summary')))}
56
+ function projectMetric(project,key){const value=rankingValue(project,key);return key==='cost'?(Number.isFinite(value)?'$'+value.toFixed(3):'Unknown'):key==='acceptance'?(Number.isFinite(value)?String(value):'Unknown'):key==='tokens'?tokenText(value):Number.isFinite(value)?String(value):'Unknown'}
86
57
  function renderOverview(){
87
- beginRequest();nav();heading('A clear view of the work.','Goals, loops, independent checks and the next thing that needs your attention.');
88
- const statuses=projects.map(project=>projectStatus(project)),attention=statuses.filter(status=>status.attention).length,active=statuses.filter(status=>status.active).length;
89
- content.append(append(el('div',undefined,'metrics'),metric('Projects',projects.length,'Registered on this machine'),metric('In progress',active,'Active goals or reported loops'),metric('Needs attention',attention,'Blocked, paused, stale or unavailable')));
90
- const tools=el('div',undefined,'overview-tools'),field=el('label',undefined,'field'),searchLabel=el('span','Search projects','label'),search=el('input');search.type='search';search.value=overviewQuery;search.placeholder='Name, path or objective';field.append(searchLabel,search);tools.append(field);
91
- const filters=el('div',undefined,'filter-group');filters.setAttribute('role','group');filters.setAttribute('aria-label','Project status filter');
92
- const cards=el('div',undefined,'cards');
93
- function draw(){
94
- cards.replaceChildren();
95
- const visible=visibleProjects(projects,overviewQuery,overviewFilter);
96
- for(const project of visible){const status=projectStatus(project),card=append(el('article',undefined,'project-card'),badge(status.primary),el('h2',project.name),el('p',project.goal?.objective||'No active objective yet.'),el('p',project.root,'path'));
97
- if(status.goal&&status.loop&&status.goal!==status.loop)card.append(el('p','Goal: '+status.goal+' · Loop: '+(status.stale?'unconfirmed (last reported '+status.loop+')':status.loop),'state-line'));
98
- else if(status.stale)card.append(el('p','Loop activity is unconfirmed; last report was '+status.loop+'.','state-line notice'));
99
- if(['active','running'].includes(status.loop)&&project.status?.storyTitle)card.append(el('p','Reported task: '+project.status.storyTitle,'state-line'));
100
- const reason=['blocked','failed','paused'].includes(status.loop)?project.status?.reason:['blocked','paused'].includes(status.goal)?project.goal?.reason:null;
101
- if(reason)card.append(el('p',reason,'notice'));
102
- if(project.errors.length)card.append(el('p',project.errors.join('; '),'notice'));
103
- card.append(button('Open project →',()=>setNavigation({screen:'project',project:project.id})));cards.append(card);
104
- }
105
- if(!visible.length){const empty=append(el('div',undefined,'empty'),el('p',projects.length?'No projects match this search and filter.':'No registered projects. Run yoke dashboard from a project directory.'));if(projects.length)empty.append(button('Clear search and filters',()=>{overviewQuery='';overviewFilter='all';renderOverview()}));cards.append(empty)}
106
- for(const child of filters.children)child.setAttribute('aria-pressed',String(child.dataset.filter===overviewFilter));
107
- }
108
- for(const [value,label] of [['all','All'],['active','Active'],['attention','Needs attention']]){const control=button(label,()=>{overviewFilter=value;draw()});control.dataset.filter=value;filters.append(control)}
109
- search.oninput=()=>{overviewQuery=search.value;draw()};tools.append(filters);content.append(tools,cards);draw();
58
+ const request=beginRequest();nav();content.setAttribute('aria-busy','true');heading('Workspace control room','A ranked view of active work, attention signals, and recorded telemetry.');const statuses=projects.map(project=>projectStatus(project)),attentionProjects=projects.filter(project=>projectStatus(project).attention),active=statuses.filter(status=>status.active).length;
59
+ const attention=append(el('section',undefined,'attention-region'),el('h2','Attention'),el('p',attentionProjects.length?'Projects that need a decision or a fresh report.':'No projects need attention right now.'));attention.id='attention-region';const attentionList=el('div',undefined,'attention-list');for(const project of rankProjects(attentionProjects,'attention').slice(0,4)){const state=projectStatus(project),item=append(el('article',undefined,'attention-item'),append(el('div',undefined,'row-main'),el('strong',project.name),el('small',project.status?.reason||project.goal?.reason||state.primary)));item.append(button('Open',()=>setNavigation({screen:'project',project:project.id}),true));attentionList.append(item)}attention.append(attentionList);content.append(attention);
60
+ const strip=el('div',undefined,'metric-strip');content.append(strip);
61
+ const command=append(el('section',undefined,'command-panel overview-command-bar'),el('label',undefined,'field'),el('label',undefined,'field'),el('label',undefined,'field'),el('label',undefined,'field'),el('label',undefined,'field'),el('label',undefined,'field'));const searchField=command.children[0],statusField=command.children[1],sortField=command.children[2],scopeField=command.children[3],fromField=command.children[4],toField=command.children[5];searchField.append(el('span','Search','label'));const search=el('input');search.type='search';search.placeholder='Project, path or objective';search.value=overviewQuery;search.setAttribute('aria-label','Search projects');searchField.append(search);statusField.append(el('span','Status','label'));const status=el('select');status.setAttribute('aria-label','Project status');for(const [value,label] of [['all','All states'],['attention','Needs attention'],['active','Active work']]){const option=el('option',label);option.value=value;option.selected=value===overviewFilter;status.append(option)}statusField.append(status);sortField.append(el('span','Sort projects','label'));const sort=el('select');sort.setAttribute('aria-label','Sort projects');for(const [value,label] of [['attention','Attention'],['activity','Last activity'],['tokens','Token usage'],['cost','Reported cost'],['acceptance','Accepted work'],['name','Name']]){const option=el('option',label);option.value=value;option.selected=value===overviewSort;sort.append(option)}sortField.append(sort);scopeField.append(el('span','Date scope · UTC','label'));const scope=el('select');scope.setAttribute('aria-label','Date scope');for(const [value,label] of [[1,'Last 24 hours'],[7,'Last 7 days'],[30,'Last 30 days'],[90,'Last 90 days'],[365,'Last 365 days']]){const option=el('option',label);option.value=String(value);option.selected=value===periodDays&&!customFrom;scope.append(option)}scopeField.append(scope);fromField.append(el('span','From','label'));const from=el('input');from.type='date';from.value=customFrom;from.setAttribute('aria-label','From date UTC');fromField.append(from);toField.append(el('span','Through','label'));const to=el('input');to.type='date';to.value=customTo;to.setAttribute('aria-label','Through date UTC');toField.append(to);content.append(command);
62
+ const ranked=append(el('section',undefined,'ranked-projects'),append(el('div',undefined,'ranked-heading'),el('h2','Ranked projects'),el('p','Sorted by '+sort.options[sort.selectedIndex].textContent+' · recorded values only')));const rows=el('div');ranked.append(rows);content.append(ranked);
63
+ function draw(){const total=overviewAnalytics?.total,recorded=total?.tokens?.total;strip.replaceChildren(metric('Projects',String(projects.length),'Registered locally'),metric('In progress',String(active),'Active goals or loops'),metric('Needs attention',String(attentionProjects.length),'Blocked, stale, or unavailable'),metric('Recorded tokens',tokenText(recorded),'Selected UTC scope'));rows.replaceChildren();const visible=visibleProjects(projects,overviewQuery,overviewFilter,Date.now(),overviewSort);for(const [index,project] of visible.entries()){const state=projectStatus(project),row=append(el('article',undefined,'project-row'),el('span',String(index+1).padStart(2,'0'),'rank'),append(el('div',undefined,'project-identity'),el('div',project.name,'project-name'),el('div',project.goal?.objective||'No active objective yet.','project-objective'),el('div',project.root,'project-path')),append(el('div',undefined,'project-stat'),badge(state.primary)),append(el('div',undefined,'project-stat'),el('strong',projectMetric(project,'tokens')),el('span','tokens')),append(el('div',undefined,'project-stat'),el('strong',projectMetric(project,'acceptance')),el('span','accepted')),button('Open',()=>setNavigation({screen:'project',project:project.id})));rows.append(row)}if(!visible.length)rows.append(append(el('div',undefined,'empty'),el('p',projects.length?'No projects match these controls.':'No registered projects.'),projects.length?button('Reset controls',()=>{overviewQuery='';overviewFilter='all';overviewSort='attention';updateOverviewNavigation({query:'',status:'all',sort:'attention'});renderOverview()}):null))}
64
+ search.oninput=()=>{overviewQuery=search.value;updateOverviewNavigation({query:overviewQuery});draw()};status.onchange=()=>{overviewFilter=status.value;updateOverviewNavigation({status:overviewFilter});draw()};sort.onchange=()=>{overviewSort=sort.value;updateOverviewNavigation({sort:overviewSort});draw()};scope.onchange=()=>setNavigation({period:Number(scope.value),from:'',to:''});const applyDates=button('Apply dates',()=>{if(!validDate(from.value)||!validDate(to.value)||from.value>to.value||Date.parse(to.value+'T00:00:00Z')-Date.parse(from.value+'T00:00:00Z')>365*86400000){command.append(el('p','Choose both dates in chronological order within 366 days.','notice'));return}setNavigation({from:from.value,to:to.value})});toField.append(applyDates);draw();
65
+ const bounds=periodWindows(Date.now(),periodDays,customFrom,customTo);void api('/api/workspace/analytics?'+queryPeriod(bounds.current)+'&sort='+encodeURIComponent(overviewSort),{},request.signal).then(data=>{if(!currentRequest(request.version)||navigationState.screen!=='overview')return;overviewAnalytics=data;overviewMetrics=data.projects||[];draw()}).catch(error=>{if(!isAbort(error)&&currentRequest(request.version))ranked.append(el('p','Historical ranking unavailable; live project state remains visible.','notice'))});
110
66
  }
111
67
  function row(title,note,state){return append(el('div',undefined,'row'),append(el('div',undefined,'row-main'),el('div',title),el('small',note)),badge(state))}
112
-
113
68
  ${dashboardPanels()}
114
69
  function renderRoute(){if(navigationState.screen==='workspace')showWorkspaceUsage();else if(navigationState.screen==='project'&&selected&&projects.some(project=>project.id===selected))showProject(selected);else renderOverview()}
115
- async function refresh(){
116
- const request=beginRequest();
117
- try{
118
- const loaded=await api('/api/projects',{},request.signal);if(!currentRequest(request.version))return;projects=loaded;
119
- if(selected&&!projects.some(project=>project.id===selected)){navigationState={...navigationState,screen:'overview',project:null};selected=null}
120
- if(typeof location!=='undefined'&&typeof history!=='undefined'&&location.hash!==serializeNavigationHash(navigationState))history.replaceState(null,'',serializeNavigationHash(navigationState));
121
- renderRoute();
122
- }catch(error){if(currentRequest(request.version)&&!isAbort(error))heading('Workspace unavailable',error.message)}
123
- }
124
- document.getElementById('refresh').onclick=refresh;
125
- if(typeof window!=='undefined'){window.addEventListener('popstate',restoreNavigation);window.addEventListener('hashchange',restoreNavigation)}
126
- refresh();
70
+ async function refresh(){const request=beginRequest();content.setAttribute?.('aria-busy','true');try{const loaded=await api('/api/projects',{},request.signal);if(!currentRequest(request.version))return;projects=loaded;overviewMetrics=[];overviewAnalytics=null;if(selected&&!projects.some(project=>project.id===selected))applyNavigation({...navigationState,screen:'overview',project:null});if(typeof location!=='undefined'&&typeof history!=='undefined'&&location.hash!==serializeNavigationHash(navigationState))history.replaceState(null,'',serializeNavigationHash(navigationState));renderRoute()}catch(error){if(currentRequest(request.version)&&!isAbort(error)){heading('Workspace unavailable','Offline or unavailable: '+error.message);content.append(el('p','Saved local data could not be read. Retry when the local Yoke process is available.','notice'))}}}
71
+ document.getElementById('refresh').onclick=refresh;if(typeof window!=='undefined'){window.addEventListener('popstate',restoreNavigation);window.addEventListener('hashchange',restoreNavigation)}refresh();
127
72
  </script></body></html>`;
128
73
  }
@@ -3,12 +3,64 @@ export function dashboardPanels() {
3
3
  return String.raw `
4
4
  function panel(title){return append(el('section',undefined,'panel'),el('h2',title))}
5
5
  function table(headers,rows){const wrap=el('div',undefined,'table-scroll'),t=el('table'),head=el('thead'),hr=el('tr');for(const title of headers)hr.append(el('th',title));head.append(hr);t.append(head);const body=el('tbody');for(const values of rows){const r=el('tr');for(const value of values)r.append(el('td',String(value)));body.append(r)}t.append(body);wrap.append(t);return wrap}
6
- function costText(t){return t.costState==='unknown'?'Unknown':'$'+t.reportedCostUsd.toFixed(3)+(t.costState==='partial'?' reported (partial)':' reported')}
6
+ function metric(label,value,note){return append(el('div',undefined,'metric'),el('span',label,'label'),el('strong',value),el('p',note))}
7
+ function safeMetricTotal(values){return values.every(Number.isFinite)?values.reduce((sum,value)=>sum+value,0):null}
8
+ function chartValues(buckets,key){return buckets.map(bucket=>{if(key==='tokens')return bucket.tokens?.total??safeMetricTotal([bucket.inputTokens,bucket.outputTokens]);if(key==='calls')return bucket.calls?.total??(Number.isFinite(bucket.measuredCalls)&&Number.isFinite(bucket.unknownCalls)?bucket.measuredCalls+bucket.unknownCalls:null);if(key==='duration')return bucket.time?.attemptDurationMs??bucket.attemptDurationMs??null;if(key==='outcomes')return bucket.outcomes?.accepted??bucket.accepted??null;return bucket.cost?.reportedUsd??bucket.reportedCostUsd??null})}
9
+ function dimensionLabel(value){if(value===undefined||value===null||value==='')return 'Not reported';if(String(value).toLowerCase()==='unknown')return 'Unknown';return String(value)}
10
+ function rankRows(rows,key='tokens'){return [...rows].sort((left,right)=>{const a=Number.isFinite(left[key])?left[key]:null,b=Number.isFinite(right[key])?right[key]:null;if(a===null&&b!==null)return 1;if(a!==null&&b===null)return -1;if(a!==b)return (b??0)-(a??0);return dimensionLabel(left.name).localeCompare(dimensionLabel(right.name))})}
11
+ function coverageMessages(data,project={}){const messages=[];if(project.unavailable||data?.projects?.some(value=>value.errors?.length&&!value.analytics))messages.push('Unavailable telemetry: this project could not be read.');if(data?.errors?.length)messages.push('Corrupt or partial history: '+data.errors.join(' · '));const total=data?.total||{};if(total.costState==='partial'||total.cost?.state==='partial'||total.unknownCalls>0||total.unmeasuredAttempts>0||total.incompleteCosts>0||total.cost?.incompleteCalls>0)messages.push('Partial telemetry: some calls or costs are not measured.');if(project.stale)messages.push('Stale telemetry: the latest active status is unconfirmed.');return messages}
12
+ let lastSuccessfulSync='',composeFeedbackState=null;
13
+ function liveFreshness(project,now=Date.now()){
14
+ const status=project.status||{},times=[status.updatedAt,...(status.supervision||[]).flatMap(value=>[value.heartbeatAt,value.lastProgressAt])].map(value=>Date.parse(value||'')).filter(Number.isFinite),latest=Math.max(...times);
15
+ if(!Number.isFinite(latest))return 'unknown';
16
+ if(now-latest<=20*60000)return 'fresh';
17
+ return ['running','active'].includes(status.state)?'unconfirmed':'stale'
18
+ }
19
+ function workerMetadata(worker){return [worker.provider?'Agent '+worker.provider:null,worker.selectedProvider&&worker.selectedProvider!==worker.provider?'Selected provider '+worker.selectedProvider:null,'Model '+(worker.selectedModel||worker.model||'provider default'),worker.selectedVariant?'Variant '+worker.selectedVariant:null,worker.role?'Role '+worker.role:null,'Phase '+(worker.phase||worker.lifecycle||'working')].filter(Boolean).join(' · ')}
20
+ function timelineEvents(events,limit=100){return [...events].sort((left,right)=>String(left.timestamp).localeCompare(String(right.timestamp))).slice(-limit)}
21
+ function composeFeedbackStatus(result){if(result?.status==='pending')return 'pending';if(['note-added','applied'].includes(result?.status))return 'applied';if(result?.status==='validation'||/invalid|validation|required|empty/iu.test(String(result?.error||'')))return 'validation';return 'failure'}
22
+ function localAndUtc(timestamp){const date=new Date(timestamp);return 'Local time: '+date.toLocaleString()+' · UTC: '+date.toISOString()}
23
+ function timelineDetail(title,value){const detail=el('details'),summary=el('summary',title);detail.append(summary,el('p',value||'Unknown'));return detail}
24
+ function timelineItem(event){
25
+ const item=el('details'),data=event.data||{},input=data.inputTokens??event.inputTokens,output=data.outputTokens??event.outputTokens,usage=data.usageAvailable===false||event.usageAvailable===false?'unavailable':input!==undefined||output!==undefined?'reported':'not recorded';
26
+ item.append(el('summary',event.type+' · '+localAndUtc(event.timestamp)),timelineDetail('Run details',event.runId),timelineDetail('Story details',event.storyId),timelineDetail('Phase details',event.phase),timelineDetail('Agent details',[event.agent,event.provider,event.model,event.variant,event.role].filter(Boolean).join(' · ')),timelineDetail('Token details','Input '+(input===undefined?'Unknown':input)+' · output '+(output===undefined?'Unknown':output)+' · '+usage));
27
+ return item
28
+ }
29
+ function feedbackText(state){return state.status+': '+state.message}
30
+ function feedbackNode(state){return el('p',feedbackText(state),'compose-feedback '+state.status)}
31
+ function renderTimeline(project){
32
+ const view=panel('Event timeline');view.append(el('p','Bounded to the latest 100 events in chronological order. Each event expands run, story, phase, agent, and token details.','muted'));
33
+ const events=timelineEvents(project.events||[]);if(!events.length)view.append(el('p','No recorded events.','empty'));else{const list=el('div',undefined,'timeline');for(const event of events)list.append(timelineItem(event));view.append(list)}content.append(view)
34
+ }
35
+ function renderOperatorInput(project){
36
+ const view=panel('Operator input');if(composeFeedbackState)view.append(feedbackNode(composeFeedbackState));
37
+ const noteLabel=el('label','Add a note','field'),note=el('textarea');note.rows=3;note.placeholder='Record context for the next worker or handoff';note.setAttribute('aria-label','Operator note');noteLabel.append(note);view.append(noteLabel,button('Add note',()=>submitOperatorInput(project,'note',note.value,view)));
38
+ const changeLabel=el('label','Queue a change','field'),change=el('textarea');change.rows=3;change.placeholder='Describe a change for the pending inbox';change.setAttribute('aria-label','Change request');changeLabel.append(change);view.append(changeLabel,button('Queue change',()=>submitOperatorInput(project,'change',change.value,view)));content.append(view)
39
+ }
40
+ async function submitOperatorInput(project,kind,value,view){
41
+ const text=value.trim();if(!text){composeFeedbackState={status:'validation',message:kind==='note'?'Enter a note before adding it.':'Describe a change before queueing it.'};view.append(feedbackNode(composeFeedbackState));return}
42
+ try{
43
+ const data=await api('/api/projects/'+project.id+'/'+(kind==='note'?'notes':'changes'),{method:'POST',headers:{'content-type':'application/json','x-yoke-token':sessionToken},body:JSON.stringify(kind==='note'?{note:text}:{request:text})});
44
+ const status=composeFeedbackStatus(data),message=status==='pending'?'Change queued as '+data.requestId+'.': 'Note applied to the live timeline.';composeFeedbackState={status,message};await showProject(project.id)
45
+ }catch(error){composeFeedbackState={status:composeFeedbackStatus({error:error.message}),message:error.message};view.append(feedbackNode(composeFeedbackState))}
46
+ }
47
+ async function safeLiveControl(project,action){
48
+ try{const data=await api('/api/projects/'+project.id+'/'+action,{method:'POST',headers:{'content-type':'application/json','x-yoke-token':sessionToken},body:JSON.stringify({action})});composeFeedbackState={status:'pending',message:data.status==='pause-requested'?'Pause requested at the next safe boundary.':'Resume requested at the next safe boundary.'};await showProject(project.id)}
49
+ catch(error){composeFeedbackState={status:composeFeedbackStatus({error:error.message}),message:error.message};await showProject(project.id)}
50
+ }
51
+ function renderLive(project){
52
+ const status=project.status||{},goal=project.goal||{},workers=status.parallel?.workers||[],state=projectStatus(project),freshness=liveFreshness(project),tasks=project.stories||[],progress=status.progress||{passed:tasks.filter(task=>task.passes).length,total:tasks.length};
53
+ const view=panel('Live operations');view.append(row('Goal state',goal.status||'not reported',state.goal||'unknown'),row('Loop state',status.state||'not reported',state.loop||'unknown'),row('Freshness',freshness,'Last status '+(status.updatedAt||'unknown')),row('Objective progress',progress.total?progress.passed+'/'+progress.total+' tasks accepted':'Unknown','Saved acceptance state'),row('Last successful sync',lastSuccessfulSync?localAndUtc(lastSuccessfulSync):'Not synced yet','Dashboard refresh'));
54
+ const metadata=panel('Worker metadata');if(!workers.length)metadata.append(el('p','No current workers reported.','empty'));else for(const worker of workers)metadata.append(row(worker.storyTitle||worker.story,workerMetadata(worker),state.stale?'unconfirmed':worker.lifecycle||'running'));view.append(metadata);
55
+ const controls=panel('Safe controls');if(state.active)controls.append(button('Request pause',()=>safeLiveControl(project,'pause')));if(state.goal!=='complete'&&(!state.active||state.loop==='paused'||state.goal==='paused'))controls.append(button('Resume',()=>safeLiveControl(project,'resume'),true));if(!state.active&&!['paused','complete'].includes(state.goal||''))controls.append(el('p','Resume starts through the existing goal or loop boundary.','muted'));view.append(controls);content.append(view)
56
+ renderOperatorInput(project);renderTimeline(project)
57
+ }
58
+ function costText(t){const state=t.costState||t.cost?.state||'unknown',value=t.reportedCostUsd??t.cost?.reportedUsd??(Number.isFinite(t.cost)?t.cost:null);return !Number.isFinite(value)||state==='unknown'?'Unknown':'$'+value.toFixed(3)+(state==='partial'?' reported (partial)':' reported')}
7
59
  function historyCost(total,errors){return errors.length&&total.costState==='measured'?{...total,costState:'partial'}:total}
8
60
  function rate(value){return Number.isFinite(value)?value.toLocaleString('en-US',{maximumFractionDigits:1}):'Unknown'}
9
- function comparisonText(current,previous){if(previous===0)return current===0?'No change; both periods recorded 0':'New recorded activity; previous period recorded 0';const change=(current-previous)/previous*100;return (change>0?'+':'')+change.toFixed(1)+'% vs previous period'}
61
+ function comparisonText(current,previous){if(!Number.isFinite(current)||!Number.isFinite(previous))return 'Comparison unavailable; one or both periods are not fully measured';if(previous===0)return current===0?'No change; both periods recorded 0':'New recorded activity; previous period recorded 0';const change=(current-previous)/previous*100;return (change>0?'+':'')+change.toFixed(1)+'% vs previous period'}
10
62
  function costComparisonText(current,previous){if(current.costState!=='measured'||previous.costState!=='measured')return 'Percentage unavailable because cost coverage is incomplete in one or both periods';return comparisonText(current.reportedCostUsd,previous.reportedCostUsd)}
11
- function usageCoverage(total){const missing=total.unknownCalls+total.unmeasuredAttempts;return missing?missing+' call'+(missing===1?'':'s')+' or attempt'+(missing===1?'':'s')+' missing usage; totals are recorded portions':'No missing usage recorded in this period'}
63
+ function usageCoverage(total){const missing=(Number.isFinite(total.unknownCalls)?total.unknownCalls:0)+(Number.isFinite(total.unmeasuredAttempts)?total.unmeasuredAttempts:0);return missing?missing+' call'+(missing===1?'':'s')+' or attempt'+(missing===1?'':'s')+' missing usage; totals are recorded portions':'No missing usage recorded in this period'}
12
64
  function periodWindows(now=Date.now(),days=periodDays,from=customFrom,to=customTo){const end=to?Date.parse(to+'T00:00:00Z')+86400000:now,start=from?Date.parse(from+'T00:00:00Z'):end-days*86400000,duration=end-start;return {current:{from:start,to:end},previous:{from:start-duration,to:start}}}
13
65
  function queryPeriod(range){return new URLSearchParams({from:new Date(range.from).toISOString(),to:new Date(range.to).toISOString(),bucket}).toString()}
14
66
  async function mapLimited(items,limit,shouldContinue,worker){
@@ -17,12 +69,20 @@ async function mapLimited(items,limit,shouldContinue,worker){
17
69
  await Promise.all(Array.from({length:Math.min(limit,items.length)},run));
18
70
  return results.filter(value=>value!==undefined);
19
71
  }
20
- function usageChart(buckets){const data=buckets.slice(-31);const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox','0 0 800 220');svg.setAttribute('role','img');svg.setAttribute('aria-label','Recorded input and output tokens over the latest '+data.length+' non-empty periods; omitted periods mean no recorded activity, not measured zero');const max=Math.max(1,...data.map(d=>d.inputTokens+d.outputTokens));const step=760/Math.max(1,data.length);data.forEach((d,i)=>{let y=180;for(const [value,color] of [[d.inputTokens,'#29444b'],[d.outputTokens,'#c34c27']]){const h=value/max*160;y-=h;const r=document.createElementNS(svg.namespaceURI,'rect');for(const [k,v] of Object.entries({x:20+i*step,y,width:Math.max(1,step-4),height:h,fill:color}))r.setAttribute(k,String(v));const title=document.createElementNS(svg.namespaceURI,'title');title.textContent=d.label+': input '+d.inputTokens+', output '+d.outputTokens;r.append(title);svg.append(r)}if(i===0||i===data.length-1){const label=document.createElementNS(svg.namespaceURI,'text');label.setAttribute('x',String(i===0?20:780));label.setAttribute('y','205');label.setAttribute('text-anchor',i===0?'start':'end');label.setAttribute('font-size','12');label.textContent=d.label;svg.append(label)}});return svg}
72
+ function numericChart(buckets,key,title){const data=(buckets||[]).slice(-31),values=chartValues(data,key),finiteValues=values.filter(Number.isFinite),max=Math.max(1,...finiteValues),svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox','0 0 800 220');svg.setAttribute('role','img');svg.setAttribute('aria-label',title+' over the selected UTC period; unknown measurements are omitted, not plotted as zero');const step=760/Math.max(1,data.length);data.forEach((d,i)=>{const value=values[i];if(Number.isFinite(value)){const h=value/max*160,r=document.createElementNS(svg.namespaceURI,'rect');for(const [k,v] of Object.entries({x:20+i*step,y:180-h,width:Math.max(1,step-4),height:h,fill:'#29444b'}))r.setAttribute(k,String(v));const tip=document.createElementNS(svg.namespaceURI,'title');tip.textContent=d.label+': '+value;r.append(tip);svg.append(r)}if(i===0||i===data.length-1){const label=document.createElementNS(svg.namespaceURI,'text');label.setAttribute('x',String(i===0?20:780));label.setAttribute('y','205');label.setAttribute('text-anchor',i===0?'start':'end');label.setAttribute('font-size','12');label.textContent=d.label;svg.append(label)}});return svg}
73
+ function usageChart(buckets){const data=buckets.slice(-31),svg=numericChart(data,'tokens','Recorded tokens');svg.setAttribute('aria-label','Recorded input and output tokens over the latest '+data.length+' non-empty periods; omitted periods mean no recorded activity, not measured zero');return svg}
74
+ function renderAnalyticsCharts(data){const buckets=data.buckets||[],view=panel('Analytics over time'),grid=el('div',undefined,'analytics-chart-grid');for(const [key,title] of [['tokens','Tokens over time'],['calls','Calls over time'],['duration','Duration over time']]){const chart=append(el('div',undefined,'analytics-chart'),el('h3',title),numericChart(buckets,key,title));grid.append(chart)}const outcomes=append(el('div',undefined,'analytics-chart'),el('h3','Outcomes over time'),numericChart(buckets,'outcomes','Outcomes'),table(['UTC '+(data.bucket||'period'),'Attempts','Successful','Accepted'],buckets.map(value=>[value.label,rankingText(value.outcomes?.attempts??value.attempts),rankingText(value.outcomes?.successfulAttempts??value.successfulAttempts),rankingText(value.outcomes?.accepted??value.accepted)])));grid.append(outcomes);const cost=append(el('div',undefined,'analytics-chart'),el('h3','Cost state over time'),table(['UTC '+(data.bucket||'period'),'Cost state','Reported cost'],buckets.map(value=>[value.label,value.cost?.state||value.costState||'Unknown',costText(value)])));grid.append(cost);view.append(el('p','Each series uses recorded measurements for the selected UTC window. Unknown values are left blank and are never treated as zero.','muted'),grid);return view}
75
+ function rowTotal(row){return row.total||row}
76
+ function normalizedRankingRows(rows){return rows.map(row=>{const total=rowTotal(row),reportedCost=row.reportedCostUsd??(Number.isFinite(row.cost)?row.cost:total.cost?.reportedUsd??null);return {...row,name:dimensionLabel(row.name),reportedCostUsd:reportedCost,tokens:row.tokens??total.tokens?.total??null,calls:row.calls?.total??total.calls?.total??null,cost:row.cost??total.cost?.reportedUsd??null,outcomes:row.outcomes?.accepted??total.outcomes?.accepted??null,duration:row.duration??total.time?.attemptDurationMs??null,costState:row.costState||total.costState||total.cost?.state||'unknown'}})}
77
+ function rankingText(value){return Number.isFinite(value)?String(value):'Unknown'}
78
+ function renderDimensionTables(data){const view=panel('Analytics dimensions'),rankings=data.rankings||{};for(const [key,title] of [['agents','Agents'],['providers','Providers'],['models','Models'],['roles','Roles'],['phases','Phases'],['projects','Projects'],['runs','Runs']]){if(!rankings[key])continue;const rows=rankRows(normalizedRankingRows(rankings[key].tokens||rankings[key]),'tokens');view.append(el('h3',title),table(['Rank','Name','Tokens','Calls','Cost','Cost state','Accepted','Duration'],rows.map((row,index)=>[index+1,dimensionLabel(row.name),rankingText(row.tokens),rankingText(row.calls),costText(row),dimensionLabel(row.costState),rankingText(row.outcomes),duration(row.duration)])))}return view}
79
+ function renderCoverage(data,project={}){const view=panel('Measurement coverage');view.append(el('p',data.coverage||'Recorded measurements only; missing telemetry is not estimated.'));for(const message of coverageMessages(data,project))view.append(el('p',message,'notice'));if(!coverageMessages(data,project).length)view.append(el('p','No telemetry gaps reported for this selected window.','muted'));return view}
80
+ function renderAnalytics(data,project={}){const view=el('div',undefined,'analytics-view'),total=data.total||{};view.append(append(el('div',undefined,'metric-strip'),metric('Recorded tokens',tokenText(total.tokens?.total),'Input '+tokenText(total.tokens?.input)+' · output '+tokenText(total.tokens?.output)),metric('Calls',rankingText(total.calls?.total),'Measured '+rankingText(total.calls?.measured)+' · unknown '+rankingText(total.calls?.unknown)),metric('Cost state',dimensionLabel(total.cost?.state||total.costState),'Reported charges only'),metric('Duration',duration(total.time?.attemptDurationMs),'Summed recorded attempt time')),renderCoverage(data,project),renderAnalyticsCharts(data),renderDimensionTables(data));return view}
21
81
  async function showWorkspaceUsage(){
22
- const request=beginRequest();selected=null;nav();heading('Compare project consumption','Recorded usage in the selected period; missing measurements remain unknown.');periodControls(null);const p=panel('Projects');p.append(el('p','Loading…','muted'));content.append(p);
23
- const bounds=periodWindows(Date.now()),query=queryPeriod(bounds.current),valid=()=>currentRequest(request.version)&&navigationState.screen==='workspace'&&!request.signal.aborted;
24
- const rows=await mapLimited(projects,3,valid,async project=>{try{const a=await api('/api/projects/'+project.id+'/analytics?'+query,{},request.signal);return [project.name,tokenText(a.total.inputTokens),tokenText(a.total.outputTokens),costText(a.total),a.total.accepted,a.total.unknownCalls+a.total.unmeasuredAttempts,a.errors.length?'Partial history':usageCoverage(a.total)]}catch(error){if(isAbort(error))return undefined;return [project.name,'Unknown','Unknown','Unknown','Unknown','Unknown',error.message]}});
25
- if(valid())p.replaceChildren(el('h2','Projects'),table(['Project','Input','Output','Cost','Accepted','Missing usage','Coverage'],rows));
82
+ const request=beginRequest();selected=null;nav();heading('Workspace analytics','Time-bucketed recorded usage, outcomes, and coverage for the selected UTC window.');periodControls(null);const p=panel('Workspace analytics');p.append(el('p','Loading…','muted'));content.append(p);
83
+ const bounds=periodWindows(Date.now(),periodDays,customFrom,customTo),query=queryPeriod(bounds.current)+'&sort='+encodeURIComponent(overviewSort),valid=()=>currentRequest(request.version)&&navigationState.screen==='workspace'&&!request.signal.aborted;
84
+ try{const data=await api('/api/workspace/analytics?'+query,{},request.signal);if(valid())p.replaceChildren(el('h2','Workspace analytics'),renderAnalytics(data,{stale:projects.some(project=>projectStatus(project).stale)}));}
85
+ catch(error){if(valid()&&!isAbort(error))p.replaceChildren(el('h2','Workspace analytics'),el('p','Offline or unavailable: '+error.message,'notice'),el('p','Retry when the local Yoke process is available.','muted'))}
26
86
  }
27
87
  function periodControls(id){
28
88
  const p=panel('Period · UTC'),select=el('select');select.setAttribute('aria-label','Period');
@@ -51,44 +111,38 @@ function renderNow(p){
51
111
  summary.append(el('p','Status refreshes every 5 seconds. Requested models are not proof of the model actually used; reported model identities appear under Usage & time.','muted'));content.append(summary);
52
112
  const tasks=panel('Tasks');for(const task of p.stories||[])tasks.append(row(task.title,task.id+(task.needs?.length?' · after '+task.needs.join(', '):'')+' · '+taskTiming(task,p.estimate),task.passes?'passed':workers.some(worker=>worker.story===task.id)?state.stale?'unconfirmed':'running':'open'));content.append(tasks);
53
113
  const routing=panel('Model selection');for(const [id,decision] of Object.entries(status.routingDecisions||{})){routing.append(row(id,decision.provider+(decision.providerModel?' ('+decision.providerModel+')':'')+' / '+(decision.model||'provider default')+(decision.reasoningEffort?' · '+decision.reasoningEffort:'')+(decision.variant?' · variant '+decision.variant:''),decision.profile));routing.append(el('p',decision.reason));routing.append(el('p','Next escalation: '+decision.next,'muted'))}if(!Object.keys(status.routingDecisions||{}).length)routing.append(el('p','No recorded model selection for this run.','empty'));content.append(routing);
54
- const activity=panel('Recent activity');for(const event of (p.events||[]).slice(-16).reverse())activity.append(row(event.storyId||event.type,event.timestamp+(event.phase?' · '+event.phase:'')+(Number.isFinite(event.durationMs)?' · '+duration(event.durationMs):''),event.outcome||event.type));content.append(activity);
114
+ renderLive(p);
55
115
  }
56
- function renderUsageHistory(current,previous){
57
- const total=current.total,prior=previous.total,currentTokens=total.inputTokens+total.outputTokens,previousTokens=prior.inputTokens+prior.outputTokens;
58
- const currentCoverage=usageCoverage(total)+(current.errors.length?' · measurement history is incomplete':''),previousCoverage=usageCoverage(prior)+(previous.errors.length?' · measurement history is incomplete':'');
59
- const currentCost=historyCost(total,current.errors),previousCost=historyCost(prior,previous.errors);
60
- const comparison=panel('Compared with the immediately preceding equal period');
61
- const boundary=value=>new Date(value).toISOString().slice(0,16).replace('T',' ');
116
+ function renderUsageHistory(current,previous,project={}){
117
+ const total=current.total,prior=previous.total,currentTokens=total.tokens?.total??safeMetricTotal([total.inputTokens,total.outputTokens]),previousTokens=prior.tokens?.total??safeMetricTotal([prior.inputTokens,prior.outputTokens]);
118
+ const currentCoverage=usageCoverage(total)+(current.errors.length?' · measurement history is incomplete':''),previousCoverage=usageCoverage(prior)+(previous.errors.length?' · measurement history is incomplete':''),currentCost=historyCost(total,current.errors),previousCost=historyCost(prior,previous.errors);
119
+ const comparison=panel('Compared with the immediately preceding equal period'),boundary=value=>new Date(value).toISOString().slice(0,16).replace('T',' ');
62
120
  comparison.append(el('p','Selected: '+boundary(current.from)+' → '+boundary(current.to)+' UTC','muted'),el('p','Previous: '+boundary(previous.from)+' → '+boundary(previous.to)+' UTC · end boundaries excluded','muted'));
63
- comparison.append(append(el('div',undefined,'metrics'),metric('Recorded tokens',tokenText(currentTokens),'Previous: '+tokenText(previousTokens)+' · '+comparisonText(currentTokens,previousTokens)+' · '+currentCoverage),metric('Accepted tasks',tokenText(total.accepted),'Previous: '+tokenText(prior.accepted)+' · '+comparisonText(total.accepted,prior.accepted)+' · recorded acceptance events'),metric('Reported cost',costText(currentCost),'Previous: '+costText(previousCost)+' · '+costComparisonText(currentCost,previousCost))));
64
- comparison.append(el('p','Current coverage: '+currentCoverage+'. Previous coverage: '+previousCoverage+'. Missing measurements are not estimated and recorded totals are not treated as complete totals.','muted'));for(const error of previous.errors)comparison.append(el('p','Previous period: '+error,'notice'));content.append(comparison);
65
- content.append(append(el('div',undefined,'metrics'),metric('Recorded input',tokenText(total.inputTokens),'Output: '+tokenText(total.outputTokens)),metric('Reported cost',costText(currentCost),'Missing charges are not estimated'),metric('Tokens / elapsed minute',rate(total.tokensPerElapsedMinute),'Input + output / entire selected period')));
66
- const notes=panel('Measurement coverage');notes.append(el('p',current.coverage));notes.append(el('p','First record in period: '+(current.earliest||'none')+' · latest: '+(current.latest||'none'),'muted'));notes.append(el('p','Measured calls: '+total.measuredCalls+' · calls with unknown usage: '+total.unknownCalls+' · unmeasured attempts: '+total.unmeasuredAttempts));notes.append(el('p','Tokens / recorded call minute: '+rate(total.tokensPerCallMinute)+' · summed call time: '+duration(total.callDurationMs)+'. Parallel calls overlap; this is consumption intensity, not generation speed.'));notes.append(el('p','Cache reads: '+tokenText(total.cachedInputTokens)+' · cache writes: '+tokenText(total.cacheWriteInputTokens)+'. These are reported categories and are not added again to input totals.','muted'));for(const error of current.errors)notes.append(el('p',error,'notice'));content.append(notes);
67
- const series=panel('Consumption over time');series.append(usageChart(current.buckets));series.append(el('p','Dark: input · orange: output. Chart shows up to 31 latest non-empty periods; the table includes every recorded period. Periods with no recorded events are omitted, so calendar gaps mean no recorded activity and do not assert a measured zero.','muted chart-gap-note'));series.append(table(['UTC '+current.bucket,'Input','Output','Cache reads','Reported cost'],current.buckets.map(value=>[value.label,tokenText(value.inputTokens),tokenText(value.outputTokens),tokenText(value.cachedInputTokens),costText(value)])));if(!current.buckets.length)series.append(el('p','No recorded activity in this period; measurement coverage may still be incomplete.','empty'));content.append(series);
68
- const models=panel('Reported models in this period');models.append(table(['Provider','Actual model','Role','Input','Output','Call time','Cost'],current.models.map(value=>[value.provider,value.model,value.role,tokenText(value.inputTokens),tokenText(value.outputTokens),duration(value.callDurationMs),costText(value)])));content.append(models);
69
- const modelTimeline=panel('Models over time');modelTimeline.append(table(['UTC '+current.bucket,'Provider','Actual model','Role','Input','Output'],current.modelBuckets.map(value=>[value.label,value.provider,value.model,value.role,tokenText(value.inputTokens),tokenText(value.outputTokens)])));content.append(modelTimeline);
70
- const tasks=panel('Consumption by task');tasks.append(table(['Task','Input','Output','Attempt time','Cost'],current.tasks.map(value=>[value.storyId,tokenText(value.inputTokens),tokenText(value.outputTokens),duration(value.attemptDurationMs),costText(value)])));content.append(tasks);
71
- const phases=panel('Recorded phase time');phases.append(table(['Phase','Summed duration'],current.phases.map(value=>[value.phase,duration(value.durationMs)])));phases.append(el('p','Overlapping worker time is summed. Unrecorded queue time and human waiting time remain unknown.','muted'));content.append(phases);
121
+ comparison.append(append(el('div',undefined,'metrics'),metric('Recorded tokens',tokenText(currentTokens),'Previous: '+tokenText(previousTokens)+' · '+comparisonText(currentTokens,previousTokens)+' · '+currentCoverage),metric('Accepted tasks',tokenText(total.accepted),'Previous: '+comparisonText(total.accepted,prior.accepted)+' · recorded acceptance events'),metric('Reported cost',costText(currentCost),'Previous: '+costText(previousCost)+' · '+costComparisonText(currentCost,previousCost))));
122
+ comparison.append(el('p','Current coverage: '+currentCoverage+'. Previous coverage: '+previousCoverage+'. Missing measurements are not estimated and recorded totals are not treated as complete totals.','muted'));for(const error of previous.errors)comparison.append(el('p','Previous period: '+error,'notice'));content.append(comparison,renderAnalytics(current,project));
72
123
  }
73
124
  function renderResults(p,a){const t=a.total;content.append(append(el('div',undefined,'metrics'),metric('Accepted in period',t.accepted,'Recorded acceptance events'),metric('Repair phases',t.repairs,'Routing escalations: '+t.escalations),metric('Tokens / acceptance',rate(t.tokensPerAccepted),'Reported input + output, including failed work')));const attempts=panel('Measured outcomes');attempts.append(el('p','Ended attempts: '+t.attempts+' · explicitly successful attempts: '+t.successfulAttempts+' · average summed attempt time per acceptance: '+duration(t.timePerAcceptedMs)));attempts.append(el('p','Worker termination alone is not counted as successful acceptance. Rates only describe recorded activity in the selected period.','muted'));attempts.append(table(['Task','Attempts','Accepted','Repairs','Escalations','Tokens / acceptance'],a.tasks.map(v=>[v.storyId,v.attempts,v.accepted,v.repairs,v.escalations,rate(v.tokensPerAccepted)])));content.append(attempts);const checks=panel('Latest saved acceptance evidence');if(p.check){checks.append(el('p',p.check.summary));for(const c of p.check.criteria){const item=row(c.text,c.id,c.status);item.firstChild.append(append(el('details'),el('summary','View evidence'),el('p',c.summary)));checks.append(item)}checks.append(el('p','Checked '+p.check.generatedAt+' · historical evidence; independent of the selected statistics period.','muted'))}else checks.append(el('p','No saved independent check.','empty'));content.append(checks);const goals=panel('Saved goal attempts');for(const attempt of p.goal?.attempts||[])goals.append(row(attempt.provider||'Agent',(attempt.summary||'')+' · '+duration(attempt.durationMs)+' · input '+tokenText(attempt.inputTokens)+' / output '+tokenText(attempt.outputTokens),attempt.success?'finished':'failed'));content.append(goals)}
125
+ function renderHistoryExplorer(project,history){const state=projectStatus(project),view=panel('History explorer');view.append(el('p','Recorded events and completed runs in the selected UTC window. The list is bounded to the latest 100 events.','muted'));const events=timelineEvents(history.events||[]);if(!events.length)view.append(el('p','No recorded history in this period.','empty'));else{const list=el('div',undefined,'timeline');for(const event of events)list.append(timelineItem(event));view.append(list)}const runs=panel('Run history');if(!(history.runs||[]).length)runs.append(el('p','No recorded runs in this period.','empty'));else runs.append(table(['Run','Started','Ended','Agent','Provider','Model','Tokens','Accepted','Outcome'],history.runs.map(run=>[dimensionLabel(run.runId),run.startedAt||'Not reported',run.endedAt||'Not reported',dimensionLabel(run.agent),dimensionLabel(run.provider),dimensionLabel(run.model),rankingText(run.total?.tokens?.total),rankingText(run.total?.outcomes?.accepted),rankingText(run.total?.outcomes?.successfulAttempts)])));content.append(renderCoverage(history,{stale:state.stale,unavailable:false}),runs,view)}
74
126
  async function showProject(id){
75
127
  const request=beginRequest();selected=id;nav();
76
128
  try{
77
- const bounds=periodWindows(Date.now()),detailRequest=api('/api/projects/'+id,{},request.signal);
78
- let project,current=null,previous=null;
129
+ const bounds=periodWindows(Date.now(),periodDays,customFrom,customTo),detailRequest=api('/api/projects/'+id,{},request.signal);
130
+ let project,current=null,previous=null,history=null;
79
131
  if(projectView==='usage'){
80
132
  [project,current,previous]=await Promise.all([detailRequest,api('/api/projects/'+id+'/analytics?'+queryPeriod(bounds.current),{},request.signal),api('/api/projects/'+id+'/analytics?'+queryPeriod(bounds.previous),{},request.signal)]);
81
133
  }else if(projectView==='results'){
82
134
  [project,current]=await Promise.all([detailRequest,api('/api/projects/'+id+'/analytics?'+queryPeriod(bounds.current),{},request.signal)]);
135
+ }else if(projectView==='history'){
136
+ [project,history]=await Promise.all([detailRequest,api('/api/projects/'+id+'/history?'+queryPeriod(bounds.current)+'&limit=100',{},request.signal)]);
83
137
  }else project=await detailRequest;
84
138
  if(!currentRequest(request.version)||navigationState.screen!=='project'||selected!==id)return;
85
- heading(project.name,project.goal?.objective||'Saved project work');content.append(el('p',project.root,'path'));for(const error of project.errors)content.append(el('p',error,'notice'));
86
- const tabs=el('div',undefined,'actions');for(const [view,label] of [['now','Now'],['usage','Usage & time'],['results','Results']]){const control=button(label,()=>setNavigation({view}),projectView===view);control.setAttribute('aria-pressed',String(projectView===view));tabs.append(control)}
87
- if(project.goal&&!['complete','paused'].includes(project.goal.status))tabs.append(button('Request pause',async()=>{await api('/api/projects/'+id+'/pause',{method:'POST',headers:{'x-yoke-token':sessionToken}},request.signal);await refresh()}));content.append(tabs);
139
+ lastSuccessfulSync=new Date().toISOString();heading(project.name,project.goal?.objective||'Saved project work');content.append(el('p',project.root,'path'));for(const error of project.errors)content.append(el('p',error,'notice'));
140
+ const tabs=el('div',undefined,'actions');for(const [view,label] of [['now','Now'],['usage','Usage & time'],['results','Results'],['history','History']]){const control=button(label,()=>setNavigation({view}),projectView===view);control.setAttribute('aria-pressed',String(projectView===view));tabs.append(control)}
141
+ content.append(tabs);
88
142
  if(projectView==='now'){
89
143
  const tasks=project.stories||[],state=projectStatus(project);content.append(append(el('div',undefined,'metrics'),metric('Accepted tasks',tasks.filter(task=>task.passes).length+' / '+tasks.length,'Saved acceptance state'),metric('Remaining time',project.estimate?.available?duration(project.estimate.lowerMs)+' – '+duration(project.estimate.upperMs):'Unknown',project.estimate?.available?project.estimate.sampleCount+' samples · empirical range':'No reliable duration history'),metric('Project status',state.primary,state.goal&&state.loop&&state.goal!==state.loop?'Goal '+state.goal+' · loop '+(state.stale?'unconfirmed':state.loop):'Latest reported state')));renderNow(project);
90
- }else{periodControls(id);if(projectView==='usage')renderUsageHistory(current,previous);else renderResults(project,current)}
91
- }catch(error){if(currentRequest(request.version)&&navigationState.screen==='project'&&selected===id&&!isAbort(error))heading('Project unavailable',error.message)}
144
+ }else{periodControls(id);if(projectView==='usage')renderUsageHistory(current,previous,projectStatus(project));else if(projectView==='history')renderHistoryExplorer(project,history);else renderResults(project,current)}
145
+ }catch(error){if(currentRequest(request.version)&&navigationState.screen==='project'&&selected===id&&!isAbort(error)){heading('Project unavailable','Offline or unavailable: '+error.message);content.append(renderCoverage({errors:[],coverage:'Historical analytics are unavailable for this project.'},{unavailable:true}))}}
92
146
  }
93
147
  if(typeof setInterval==='function')setInterval(()=>{if(selected&&projectView==='now'&&typeof document.hidden!=='undefined'&&!document.hidden&&!['INPUT','SELECT','TEXTAREA'].includes(document.activeElement?.tagName)&&!document.activeElement?.matches?.(':focus-visible'))showProject(selected)},5000);
94
148
  `;