opencode-dashboard-client 0.3.0 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-dashboard-client",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Front-end for the opencode token-usage dashboard. Ships the built SPA plus a node CLI (opencode-dashboard serve/configure) that serves the SPA and proxies to dashboard backends.",
6
6
  "homepage": "https://github.com/GCS-ZHN/opencode-dashboard#readme",
@@ -30,11 +30,13 @@
30
30
  "prepublishOnly": "npm run build"
31
31
  },
32
32
  "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.30.0",
33
34
  "yaml": "^2.8.0"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/bun": "^1.1.0",
37
38
  "typescript": "^5.7.0",
38
- "vite": "^7.0.0"
39
+ "vite": "^7.0.0",
40
+ "xlsx": "0.18.5"
39
41
  }
40
42
  }
@@ -1 +0,0 @@
1
- (function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))i(r);new MutationObserver(r=>{for(const d of r)if(d.type==="childList")for(const l of d.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&i(l)}).observe(document,{childList:!0,subtree:!0});function n(r){const d={};return r.integrity&&(d.integrity=r.integrity),r.referrerPolicy&&(d.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?d.credentials="include":r.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function i(r){if(r.ep)return;r.ep=!0;const d=n(r);fetch(r.href,d)}})();async function v(s,e){const n=await fetch(s+e);if(!n.ok){let i=`${n.status} ${n.statusText}`;try{const r=await n.json();r.detail&&(i=String(r.detail))}catch{}throw new Error(i)}return n.json()}function C(s){return`/api/s/${s}`}const b={config:()=>v("/","api/config"),overview:s=>v(C(s),"/overview"),projects:s=>v(C(s),"/projects"),project:(s,e)=>v(C(s),`/projects/${encodeURIComponent(e)}`),session:(s,e)=>v(C(s),`/sessions/${encodeURIComponent(e)}`)},I=[["input","seg-in"],["output","seg-out"],["reasoning","seg-rea"],["cacheRead","seg-cr"],["cacheWrite","seg-cw"]];function t(s,e,n){const i=document.createElement(s);return e&&(i.className=e),n!==void 0&&(i.textContent=n),i}function E(s){return s>=1e6?(s/1e6).toFixed(1).replace(/\.0$/,"")+"M":s>=1e3?(s/1e3).toFixed(1).replace(/\.0$/,"")+"K":String(s)}function w(s){if(!s)return"$0";if(s<1e-4)return"< $0.0001";const e=s.toFixed(4).replace(/0+$/,"").replace(/\.$/,"");return"$"+(e===""?"0":e)}function N(s){const e=Math.max(0,Math.round((Date.now()-s)/1e3));return e<60?`${e}s ago`:e<3600?`${Math.round(e/60)}m ago`:e<86400?`${Math.round(e/3600)}h ago`:`${Math.round(e/86400)}d ago`}function O(s){const e=t("div","bar");if(!s.total)return e.classList.add("bar-empty"),e.title="0 tokens",e;for(const[n,i]of I){const r=s[n]/s.total*100;if(r<=0)continue;const d=t("div",`seg ${i}`);d.style.width=`${r}%`,d.title=`${n}: ${E(s[n])}`,e.appendChild(d)}return e}function y(s,e=!1){const n=t("div","tcell"+(e?" hero":"")),i=t("div","bar-wrap");return i.appendChild(O(s)),n.append(i,t("span","num",E(s.total))),n}let $=30,R=[],j=[];const L=["#58a6ff","#3fb950","#a371f7","#d29922","#f85149"];function D(s){if(!s.data)return null;try{return JSON.parse(String(s.data))}catch{return null}}function A(s,e){return{input:s.input+e.input,output:s.output+e.output,reasoning:s.reasoning+e.reasoning,cacheRead:s.cacheRead+e.cacheRead,cacheWrite:s.cacheWrite+e.cacheWrite,total:s.total+e.total}}function P(s,e){s.tabIndex=0,s.addEventListener("click",e),s.addEventListener("keydown",n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),e())})}class U{constructor(e){this.idx=e,this.base=R[e]?.url??"",this.root=t("section","server"),this.root.dataset.url=this.base}idx;overview=null;projects=[];projectDetails=new Map;sessionDetails=new Map;expandedProjects=new Set;expandedSessions=new Set;sessionLimits=new Map;error=null;es=null;liveRef=null;updRef=null;root;base;start(){this.connect(),this.refresh()}touch(){this.updRef&&this.overview&&(this.updRef.textContent=`updated ${N(this.overview.updatedAt)}`)}connect(){this.es?.close();const e=new EventSource(`${C(this.idx)}/stream`);this.es=e;const n=i=>this.onUpdate(D(i));e.addEventListener("update",n),e.onmessage=n,e.onopen=()=>this.setLive(!0),e.onerror=()=>this.setLive(!1)}setLive(e){this.liveRef&&(this.liveRef.textContent=e?"● live":"reconnecting…",this.liveRef.classList.toggle("on",e))}async refresh(){try{const[e,n]=await Promise.all([b.overview(this.idx),b.projects(this.idx)]);this.overview=e,this.projects=n,this.error=null}catch(e){this.error=e instanceof Error?e.message:String(e)}this.render()}async loadProject(e){if(this.expandedProjects.has(e)){try{this.projectDetails.set(e,await b.project(this.idx,e))}catch{this.expandedProjects.delete(e)}this.render()}}async loadSession(e){if(this.expandedSessions.has(e)){try{this.sessionDetails.set(e,await b.session(this.idx,e))}catch{this.expandedSessions.delete(e)}this.render()}}onUpdate(e){if(!e||e.type!=="updated")return;const n=[this.refresh()];for(const i of this.expandedProjects)n.push(this.loadProject(i));for(const i of this.expandedSessions)n.push(this.loadSession(i));Promise.all(n)}render(){if(this.root.replaceChildren(),this.error&&!this.overview){const h=t("div","error");h.append(t("h3","",this.base),t("p","",this.error));const f=t("button","","Retry");f.addEventListener("click",()=>{this.refresh()}),h.appendChild(f),this.root.appendChild(h);return}const e=this.overview,n=t("header","srv-head");n.append(t("h2","srv-host",e?e.host:this.base),t("span","srv-ver",e?`opencode ${e.opencodeVersion}`:""),t("span","srv-url",this.base));const i=t("span","live");this.liveRef=i,this.setLive(this.es?.readyState===EventSource.OPEN);const r=t("span","srv-upd");this.updRef=r,this.touch(),n.append(i,r),this.root.appendChild(n);const d=t("div","stats"),l=k("sessions",e?M(e.mainSessionCount,e.sessionCount):"…");l.title=e?`${e.mainSessionCount} main sessions · ${e.sessionCount} total (incl. subagents)`:"",d.append(l,k("projects",e?String(e.projectCount):"…"));const o=t("div","stat stat-tokens");if(o.appendChild(t("span","lbl","tokens")),e?o.appendChild(y(e.tokens,!0)):o.appendChild(t("div","skeleton","")),d.appendChild(o),d.appendChild(F(e?e.cost:null)),this.root.appendChild(d),this.error&&this.root.appendChild(t("div","error error-bar",`refresh failed: ${this.error}`)),this.root.appendChild(t("h3","sec-head","Projects")),!e){const h=t("table","tbl"),f=t("tbody");for(let S=0;S<3;S++){const g=t("tr","skeleton-row"),x=t("td");x.colSpan=4,x.appendChild(t("div","skeleton")),g.appendChild(x),f.appendChild(g)}h.appendChild(f),this.root.appendChild(h);return}if(this.projects.length===0){this.root.appendChild(t("div","empty","No projects found on this host."));return}const c=t("table","tbl"),p=t("thead"),a=t("tr");for(const h of["Project","Sessions","Tokens","Cost"])a.appendChild(t("th","",h));p.appendChild(a),c.appendChild(p);const u=t("tbody");for(const h of this.projects)this.renderProject(u,h);c.appendChild(u),this.root.appendChild(c)}renderProject(e,n){const i=this.expandedProjects.has(n.id),r=t("td","p-name");r.append(t("span","tgl",i?"▾":"▸"),t("span","",n.name));const d=t("td");d.appendChild(y(n.tokens));const l=t("tr","prow"+(i?" open":""));if(l.append(r,W(n),d,t("td","cost",w(n.cost))),P(l,()=>this.toggleProject(n.id)),e.appendChild(l),i){const o=t("tr","pdetail"),c=t("td");c.colSpan=4;const p=this.projectDetails.get(n.id);if(!p)c.appendChild(t("div","muted","Loading…"));else{const a=this.sessionLimits.get(n.id)??$,u=t("div","stree");this.renderSessions(u,p.sessions,a,()=>{this.sessionLimits.set(n.id,a+$),this.render()}),c.appendChild(u)}o.appendChild(c),e.appendChild(o)}}toggleProject(e){this.expandedProjects.has(e)?(this.expandedProjects.delete(e),this.sessionLimits.delete(e),this.render()):(this.expandedProjects.add(e),this.loadProject(e))}renderSessions(e,n,i,r){const d=new Map(n.map(a=>[a.id,a])),l=new Map,o=[];for(const a of n)if(a.parentId&&d.has(a.parentId)){const u=l.get(a.parentId)??[];u.push(a),l.set(a.parentId,u)}else o.push(a);const c=new Map,p=a=>{let u={...a.tokens},h=a.cost;for(const S of l.get(a.id)??[]){const g=p(S);u=A(u,g.tokens),h+=g.cost}const f={tokens:u,cost:h};return c.set(a.id,f),f};for(const a of o)p(a);for(const a of o.slice(0,i))this.renderSession(e,a,0,l,c);if(o.length>i){const a=t("button","more",`Show ${o.length-i} more sessions`);a.addEventListener("click",r),e.appendChild(a)}}renderSession(e,n,i,r,d){const l=r.get(n.id)??[],o=this.expandedSessions.has(n.id),c=d.get(n.id)??{tokens:n.tokens,cost:n.cost},p=t("div","srow"+(o?" open":""));p.style.setProperty("--depth",String(i));const a=t("span","tgl",o?"▾":"▸");if(l.length){const h=t("span","badge",String(l.length));a.append(h)}const u=t("div","s-main");if(u.append(t("span","s-title",n.title||"Untitled session"),t("span","s-meta",`${n.agent} · ${n.model}`)),p.append(a,u,y(c.tokens),t("span","cost",w(c.cost))),P(p,()=>this.toggleSession(n.id)),e.appendChild(p),o){const h=t("div","s-detail");h.style.setProperty("--depth",String(i+1));const f=this.sessionDetails.get(n.id);f?f.models.length===0?h.appendChild(t("div","muted","No token usage recorded")):(h.appendChild(this.modelsTable(f.models)),l.length&&h.appendChild(t("div","muted note","model breakdown counts only this session — row totals include subagents"))):h.appendChild(t("div","muted","Loading…")),e.appendChild(h)}if(o)for(const h of l)this.renderSession(e,h,i+1,r,d)}toggleSession(e){this.expandedSessions.has(e)?(this.expandedSessions.delete(e),this.render()):(this.expandedSessions.add(e),this.loadSession(e))}modelsTable(e){const n=t("table","tbl tbl-models"),i=t("thead"),r=t("tr");for(const l of["Model","Mode","Msgs","Tokens","Cost"])r.appendChild(t("th","",l));i.appendChild(r),n.appendChild(i);const d=t("tbody");for(const l of e){const o=t("td","p-name");o.append(t("span","",l.model),t("span","muted",l.provider));const c=t("td");c.appendChild(y(l.tokens));const p=t("tr");p.append(o,t("td","",l.mode),t("td","num",String(l.messageCount)),c,t("td","cost",w(l.cost))),d.appendChild(p)}return n.appendChild(d),n}}function k(s,e){const n=t("div","stat");return n.append(t("span","num",e),t("span","lbl",s)),n}function M(s,e){return`${s} / ${e}`}function W(s){const e=t("td","num",M(s.mainSessionCount,s.sessionCount));return e.title=`${s.mainSessionCount} main sessions · ${s.sessionCount} total (incl. subagents)`,e}function F(s){const e=t("div","stat stat-cost");return e.append(t("span","num",s===null?"…":w(s)),t("span","lbl","cost")),e}const m=document.getElementById("app");async function T(){let s=[],e=null;try{const o=await b.config();s=o.servers,$=o.ui?.sessionPage??$}catch(o){e=o instanceof Error?o.message:String(o)}if(R=s,e!==null||s.length===0){const o=t("div","error");o.append(t("h3","","No front-end server configuration"),t("p","",e??"No servers returned by /api/config — check dashboard.yaml."));const c=t("button","","Retry");c.addEventListener("click",()=>{T()}),o.appendChild(c),m.replaceChildren(o);return}s.length>1&&m.classList.add("multi"),j=[];for(let o=0;o<s.length;o++){const c=new U(o);c.root.style.borderTop=`3px solid ${L[o%L.length]}`,j.push(c),m.appendChild(c.root),c.start()}const n="overall",i=t("nav","tabs"),r=new Map,d=(o,c)=>{const p=t("button","tab",o);p.addEventListener("click",()=>l(c)),i.appendChild(p),r.set(c,p)};d("Overall",n),s.forEach((o,c)=>d(o.name,String(c))),m.before(i);function l(o){const c=o===n;m.classList.toggle("multi",c&&s.length>1),j.forEach((p,a)=>{p.root.hidden=!(c||String(a)===o)});for(const[p,a]of r)a.classList.toggle("active",p===o)}l(n)}T();setInterval(()=>{for(const s of j)s.touch()},3e4);
@@ -1 +0,0 @@
1
- :root{--bg: #0d1117;--panel: #161b22;--inset: #21262d;--border: #30363d;--text: #e6edf3;--muted: #8b949e;--accent: #58a6ff;--danger: #f85149;--warn: #d29922;--live: #3fb950;--in: #58a6ff;--out: #3fb950;--rea: #a371f7;--cr: #2f6f78;--cw: #8a6d1f;--mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif}.app-head{position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 20px;background:var(--panel);border-bottom:1px solid var(--border)}.app-head h1{margin:0;font-size:16px;font-weight:600;letter-spacing:.01em}.legend{display:flex;gap:14px;font-size:12px;color:var(--muted)}.legend span{display:inline-flex;align-items:center;gap:5px}.dot{width:10px;height:10px;border-radius:3px;display:inline-block}#app{display:flex;flex-direction:column;gap:16px;padding:20px;max-width:1100px;margin:0 auto}#app.multi{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;max-width:1400px}@media(max-width:860px){#app.multi{grid-template-columns:1fr}}.tabs{display:flex;flex-wrap:wrap;gap:4px;max-width:1100px;margin:16px auto 0;padding:0 20px}.tab{background:none;border:1px solid var(--border);border-bottom:none;border-radius:8px 8px 0 0;color:var(--text-dim);font:inherit;font-size:13px;padding:7px 14px;cursor:pointer;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tab:hover{color:var(--text)}.tab.active{color:var(--text);background:var(--panel);border-color:var(--accent);border-bottom:2px solid var(--accent)}.server{background:var(--panel);border:1px solid var(--border);border-top:3px solid var(--accent);border-radius:10px;padding:16px 20px 20px}.srv-head{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}.srv-host{margin:0;font-size:18px;font-weight:650}.srv-ver,.srv-url{color:var(--muted);font-size:12px}.srv-url{font-family:var(--mono)}.srv-upd{margin-left:auto;color:var(--muted);font-size:12px}.live{font-size:12px;color:var(--warn)}.live.on{color:var(--live);animation:pulse 2s infinite}@media(prefers-reduced-motion:reduce){.live.on{animation:none}}@keyframes pulse{50%{opacity:.4}}.stats{display:flex;gap:12px;margin:16px 0 8px;flex-wrap:wrap}.stat{background:var(--inset);border-radius:8px;padding:10px 14px;min-width:96px;display:flex;flex-direction:column;gap:2px}.stat .num{font-family:var(--mono);font-size:18px;font-weight:600;font-variant-numeric:tabular-nums}.stat .lbl{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.05em}.stat-tokens{flex:1;min-width:220px;gap:8px}.sec-head{margin:18px 0 8px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}.tbl{width:100%;border-collapse:collapse}.tbl th{text-align:left;color:var(--muted);font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.04em;padding:6px 8px;border-bottom:1px solid var(--border)}.tbl td{padding:7px 8px;border-bottom:1px solid var(--border);vertical-align:middle}.tbl tbody tr:last-child td{border-bottom:none}.prow{cursor:pointer;transition:background .12s}.prow:hover,.prow:focus-visible{background:#ffffff0a}.prow:focus-visible,.srow:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.p-name{display:flex;align-items:center;gap:6px}.tgl{width:18px;flex:none;display:inline-flex;align-items:center;color:var(--muted);cursor:pointer;-webkit-user-select:none;user-select:none;font-size:11px}.badge{background:var(--inset);color:var(--muted);border-radius:8px;font-size:10px;padding:1px 5px;margin-left:2px;font-family:var(--mono)}.cost{text-align:right;font-family:var(--mono);font-variant-numeric:tabular-nums;white-space:nowrap}td.cost{text-align:right}.num{font-family:var(--mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted);font-size:12px}.stree{display:flex;flex-direction:column;padding:4px 0}.srow{display:grid;grid-template-columns:18px minmax(0,1fr) 150px 70px;align-items:center;gap:8px;padding:6px 8px;padding-left:calc(8px + var(--depth) * 20px);cursor:pointer;border-radius:6px;transition:background .12s}.srow:hover{background:#ffffff0a}.s-main{display:flex;flex-direction:column;min-width:0}.s-title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.s-meta{color:var(--muted);font-size:11px}.s-detail{padding-left:calc(8px + var(--depth) * 20px + 18px);margin:4px 0 8px}.note{margin-top:8px}.tbl-models{background:var(--inset);border:1px solid var(--border);border-radius:8px;overflow:hidden}.bar-wrap{width:100%}.bar{display:flex;height:8px;border-radius:2px;overflow:hidden;background:var(--inset)}.hero .bar{height:16px;border-radius:3px}.bar-empty{background:var(--inset)}.seg{height:100%;min-width:2px}.seg-in{background:var(--in)}.seg-out{background:var(--out)}.seg-rea{background:var(--rea)}.seg-cr{background:var(--cr)}.seg-cw{background:var(--cw)}.tcell{display:flex;flex-direction:column;gap:4px;min-width:130px;max-width:200px}.tcell .num{font-family:var(--mono);font-size:12px;color:var(--text);text-align:right;font-variant-numeric:tabular-nums}.tcell.hero{min-width:220px;max-width:320px}.tcell.hero .num{font-size:20px;font-weight:600}.empty{color:var(--muted);padding:16px 8px;text-align:center}.more{align-self:flex-start;margin:8px 0 4px 26px;background:var(--inset);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer;transition:border-color .12s,background .12s}.more:hover{border-color:var(--accent)}.skeleton-row td{padding:12px 8px}.skeleton{height:14px;border-radius:4px;background:linear-gradient(90deg,var(--inset) 25%,#2d333b 50%,var(--inset) 75%);background-size:200% 100%;animation:shimmer 1.2s infinite}@keyframes shimmer{to{background-position:-200% 0}}.error{color:var(--danger);padding:8px}.error h3{color:inherit;text-transform:none;letter-spacing:0;margin:0 0 4px;font-size:15px}.error p{margin:0 0 8px;font-size:13px;color:var(--muted)}.error button{background:var(--inset);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 14px;cursor:pointer}.error-bar{background:#f8514914;border:1px solid rgba(248,81,73,.4);border-radius:6px;margin:8px 0;font-size:12px}