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/dist/assets/index-C1-pcNt6.css +1 -0
- package/dist/assets/index-DznBpFHu.js +1 -0
- package/dist/assets/xlsx-CNerDvZX.js +105 -0
- package/dist/index.html +9 -3
- package/dist-cli/cli.mjs +16344 -165
- package/package.json +4 -2
- package/dist/assets/index-CHy6WqXK.js +0 -1
- package/dist/assets/index-DsbIuDpC.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
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-title{display:flex;align-items:center;gap:10px}.app-title h1{margin:0;font-size:16px;font-weight:600;letter-spacing:.01em}.app-head .mcp-box{flex:none}.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)}.tab-export{margin-left:auto;border:1px solid var(--accent);border-radius:8px;color:var(--accent);background:#58a6ff14}.tab-export:hover{background:#58a6ff2e}.tab-export:disabled{opacity:.6;cursor:default}.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}.mcp-box{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);border-radius:6px;padding:2px 8px;background:none;color:inherit;font:inherit;cursor:pointer;-webkit-user-select:none;user-select:none;transition:border-color .12s}.mcp-box:hover{border-color:var(--accent)}.mcp-box.copied{border-color:var(--live)}.mcp-url{font-size:11px;font-family:var(--mono);color:var(--muted);-webkit-user-select:all;user-select:all;white-space:nowrap}.mcp-copy{font-size:11px;color:var(--text);border-left:1px solid var(--border);padding-left:6px}.mcp-box.copied .mcp-copy{color:var(--live)}@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)}.pies{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0 8px}@media(max-width:860px){.pies{grid-template-columns:1fr}}.pie-card{background:var(--inset);border:1px solid var(--border);border-radius:8px;padding:12px 14px}.pie-title{margin:0 0 10px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}.pie{display:flex;flex-direction:column;align-items:center;gap:8px}.donut{flex:none;display:block}.donut path{transition:opacity .15s,filter .15s}.donut path.hot{filter:brightness(1.25)}.donut path.dim{opacity:.35}.pie-readout{font-family:var(--mono);font-size:12px;font-variant-numeric:tabular-nums;color:var(--fg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;min-height:1.2em}.pie-readout.muted{color:var(--muted)}.pie-empty{color:var(--muted);font-size:12px;padding:24px 0;text-align:center}.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}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.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 a of d.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&i(a)}).observe(document,{childList:!0,subtree:!0});function s(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=s(r);fetch(r.href,d)}})();async function y(e,t){const s=await fetch(e+t);if(!s.ok){let i=`${s.status} ${s.statusText}`;try{const r=await s.json();r.detail&&(i=String(r.detail))}catch{}throw new Error(i)}return s.json()}function S(e){return`/api/s/${e}`}const w={config:()=>y("/","api/config"),overview:e=>y(S(e),"/overview"),projects:e=>y(S(e),"/projects"),models:e=>y(S(e),"/models"),project:(e,t)=>y(S(e),`/projects/${encodeURIComponent(t)}`),session:(e,t)=>y(S(e),`/sessions/${encodeURIComponent(t)}`)},J=[["input","seg-in"],["output","seg-out"],["reasoning","seg-rea"],["cacheRead","seg-cr"],["cacheWrite","seg-cw"]];function o(e,t,s){const i=document.createElement(e);return t&&(i.className=t),s!==void 0&&(i.textContent=s),i}function R(e){return e>=1e6?(e/1e6).toFixed(1).replace(/\.0$/,"")+"M":e>=1e3?(e/1e3).toFixed(1).replace(/\.0$/,"")+"K":String(e)}function x(e){if(!e)return"$0";if(e<1e-4)return"< $0.0001";const t=e.toFixed(4).replace(/0+$/,"").replace(/\.$/,"");return"$"+(t===""?"0":t)}function Y(e){const t=Math.max(0,Math.round((Date.now()-e)/1e3));return t<60?`${t}s ago`:t<3600?`${Math.round(t/60)}m ago`:t<86400?`${Math.round(t/3600)}h ago`:`${Math.round(t/86400)}d ago`}function Z(e){const t=o("div","bar");if(!e.total)return t.classList.add("bar-empty"),t.title="0 tokens",t;for(const[s,i]of J){const r=e[s]/e.total*100;if(r<=0)continue;const d=o("div",`seg ${i}`);d.style.width=`${r}%`,d.title=`${s}: ${R(e[s])}`,t.appendChild(d)}return t}function j(e,t=!1){const s=o("div","tcell"+(t?" hero":"")),i=o("div","bar-wrap");return i.appendChild(Z(e)),s.append(i,o("span","num",R(e.total))),s}const z="modulepreload",H=function(e){return"/"+e},N={},Q=function(t,s,i){let r=Promise.resolve();if(s&&s.length>0){let f=function(n){return Promise.all(n.map(l=>Promise.resolve(l).then(c=>({status:"fulfilled",value:c}),c=>({status:"rejected",reason:c}))))};var a=f;document.getElementsByTagName("link");const p=document.querySelector("meta[property=csp-nonce]"),u=p?.nonce||p?.getAttribute("nonce");r=f(s.map(n=>{if(n=H(n),n in N)return;N[n]=!0;const l=n.endsWith(".css"),c=l?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${n}"]${c}`))return;const h=document.createElement("link");if(h.rel=l?"stylesheet":z,l||(h.as="script"),h.crossOrigin="",h.href=n,u&&h.setAttribute("nonce",u),document.head.appendChild(h),l)return new Promise((g,C)=>{h.addEventListener("load",g),h.addEventListener("error",()=>C(new Error(`Unable to preload CSS for ${n}`)))})}))}function d(p){const u=new Event("vite:preloadError",{cancelable:!0});if(u.payload=p,window.dispatchEvent(u),!u.defaultPrevented)throw p}return r.then(p=>{for(const u of p||[])u.status==="rejected"&&d(u.reason);return t().catch(d)})},tt=["Project","Session ID","Parent ID","Title","Agent","Model","Created","Updated","Input Tokens","Output Tokens","Reasoning","Cache Read","Cache Write","Total Tokens","Cost ($)"],et=["input","output","reasoning","cacheRead","cacheWrite"];function I(e){return new Date(e).toISOString()}function L(e){return e??""}function st(e,t){const s=t.tokens;return[e,t.id,L(t.parentId),L(t.title),L(t.agent),L(t.model),I(t.timeCreated),I(t.timeUpdated),...et.map(i=>s[i]),s.total,t.cost]}function O(e){return e.replace(/[\[\]:*?/\\]/g,"_").slice(0,31)}async function ot(e){const t=await w.projects(e),s=await Promise.all(t.map(r=>w.project(e,r.id))),i=[tt];for(const r of s){const d=r.sessions.slice().sort((a,p)=>a.timeCreated-p.timeCreated);for(const a of d)i.push(st(r.project.name,a))}return i}async function nt(e){const t=await Q(()=>import("./xlsx-CNerDvZX.js"),[]),s=t.utils.book_new(),i=new Set;for(const n of e){const l=await ot(n.idx);let c=O(n.name);for(let h=2;i.has(c);h++)c=`${O(n.name).slice(0,28)} (${h})`;i.add(c),t.utils.book_append_sheet(s,t.utils.aoa_to_sheet(l),c)}const d=`opencode-usage-${new Date(Date.now()-new Date().getTimezoneOffset()*6e4).toISOString().slice(0,10)}.xlsx`,a=t.write(s,{bookType:"xlsx",type:"array"}),p=new Blob([a],{type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}),u=URL.createObjectURL(p),f=o("a");f.href=u,f.download=d,document.body.appendChild(f),f.click(),f.remove(),setTimeout(()=>URL.revokeObjectURL(u),6e4)}let T=30,_=[],P=[];const A=["#58a6ff","#3fb950","#a371f7","#d29922","#f85149","#39c5cf","#db61a2","#79c0ff"];function rt(e){if(!e.data)return null;try{return JSON.parse(String(e.data))}catch{return null}}function it(e,t){return{input:e.input+t.input,output:e.output+t.output,reasoning:e.reasoning+t.reasoning,cacheRead:e.cacheRead+t.cacheRead,cacheWrite:e.cacheWrite+t.cacheWrite,total:e.total+t.total}}function D(e,t){e.tabIndex=0,e.addEventListener("click",t),e.addEventListener("keydown",s=>{(s.key==="Enter"||s.key===" ")&&(s.preventDefault(),t())})}class at{constructor(t){this.idx=t,this.base=_[t]?.url??"",this.root=o("section","server"),this.root.dataset.url=this.base}idx;overview=null;projects=[];models=[];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 ${Y(this.overview.updatedAt)}`)}connect(){this.es?.close();const t=new EventSource(`${S(this.idx)}/stream`);this.es=t;const s=i=>this.onUpdate(rt(i));t.addEventListener("update",s),t.onmessage=s,t.onopen=()=>this.setLive(!0),t.onerror=()=>this.setLive(!1)}setLive(t){this.liveRef&&(this.liveRef.textContent=t?"● live":"reconnecting…",this.liveRef.classList.toggle("on",t))}async refresh(){try{const[t,s,i]=await Promise.all([w.overview(this.idx),w.projects(this.idx),w.models(this.idx).catch(r=>/\b404\b/.test(r instanceof Error?r.message:String(r))?null:Promise.reject(r))]);this.overview=t,this.projects=s,this.models=i??[],this.error=null}catch(t){this.error=t instanceof Error?t.message:String(t)}this.render()}async loadProject(t){if(this.expandedProjects.has(t)){try{this.projectDetails.set(t,await w.project(this.idx,t))}catch{this.expandedProjects.delete(t)}this.render()}}async loadSession(t){if(this.expandedSessions.has(t)){try{this.sessionDetails.set(t,await w.session(this.idx,t))}catch{this.expandedSessions.delete(t)}this.render()}}onUpdate(t){if(!t||t.type!=="updated")return;const s=[this.refresh()];for(const i of this.expandedProjects)s.push(this.loadProject(i));for(const i of this.expandedSessions)s.push(this.loadSession(i));Promise.all(s)}render(){if(this.root.replaceChildren(),this.error&&!this.overview){const c=o("div","error");c.append(o("h3","",this.base),o("p","",this.error));const h=o("button","","Retry");h.addEventListener("click",()=>{this.refresh()}),c.appendChild(h),this.root.appendChild(c);return}const t=this.overview,s=o("header","srv-head");s.append(o("h2","srv-host",t?t.host:this.base),o("span","srv-ver",t?`opencode ${t.opencodeVersion}`:""),o("span","srv-url",this.base));const i=o("span","live");this.liveRef=i,this.setLive(this.es?.readyState===EventSource.OPEN);const r=o("span","srv-upd");this.updRef=r,this.touch(),s.append(i,r),this.root.appendChild(s);const d=o("div","stats"),a=U("sessions",t?B(t.mainSessionCount,t.sessionCount):"…");a.title=t?`${t.mainSessionCount} main sessions · ${t.sessionCount} total (incl. subagents)`:"",d.append(a,U("projects",t?String(t.projectCount):"…"));const p=o("div","stat stat-tokens");if(p.appendChild(o("span","lbl","tokens")),t?p.appendChild(j(t.tokens,!0)):p.appendChild(o("div","skeleton","")),d.appendChild(p),d.appendChild(pt(t?t.cost:null)),this.root.appendChild(d),this.error&&this.root.appendChild(o("div","error error-bar",`refresh failed: ${this.error}`)),t&&this.root.appendChild(this.pies()),this.root.appendChild(o("h3","sec-head","Projects")),!t){const c=o("table","tbl"),h=o("tbody");for(let g=0;g<3;g++){const C=o("tr","skeleton-row"),m=o("td");m.colSpan=4,m.appendChild(o("div","skeleton")),C.appendChild(m),h.appendChild(C)}c.appendChild(h),this.root.appendChild(c);return}if(this.projects.length===0){this.root.appendChild(o("div","empty","No projects found on this host."));return}const u=o("table","tbl"),f=o("thead"),n=o("tr");for(const c of["Project","Sessions","Tokens","Cost"])n.appendChild(o("th","",c));f.appendChild(n),u.appendChild(f);const l=o("tbody");for(const c of this.projects)this.renderProject(l,c);u.appendChild(l),this.root.appendChild(u)}pies(){const t=o("div","pies"),s=this.models.map(a=>({label:a.model,value:a.tokens.total})),i=this.models.map(a=>({label:a.model,value:a.cost})),r=this.projects.map(a=>({label:a.name,value:a.tokens.total})),d=this.projects.map(a=>({label:a.name,value:a.cost}));return t.append(k("Tokens by model",s,R),k("Cost by model",i,x,"No cost reported"),k("Tokens by project",r,R),k("Cost by project",d,x,"No cost reported")),t}renderProject(t,s){const i=this.expandedProjects.has(s.id),r=o("td","p-name");r.append(o("span","tgl",i?"▾":"▸"),o("span","",s.name));const d=o("td");d.appendChild(j(s.tokens));const a=o("tr","prow"+(i?" open":""));if(a.append(r,lt(s),d,o("td","cost",x(s.cost))),D(a,()=>this.toggleProject(s.id)),t.appendChild(a),i){const p=o("tr","pdetail"),u=o("td");u.colSpan=4;const f=this.projectDetails.get(s.id);if(!f)u.appendChild(o("div","muted","Loading…"));else{const n=this.sessionLimits.get(s.id)??T,l=o("div","stree");this.renderSessions(l,f.sessions,n,()=>{this.sessionLimits.set(s.id,n+T),this.render()}),u.appendChild(l)}p.appendChild(u),t.appendChild(p)}}toggleProject(t){this.expandedProjects.has(t)?(this.expandedProjects.delete(t),this.sessionLimits.delete(t),this.render()):(this.expandedProjects.add(t),this.loadProject(t))}renderSessions(t,s,i,r){const d=new Map(s.map(n=>[n.id,n])),a=new Map,p=[];for(const n of s)if(n.parentId&&d.has(n.parentId)){const l=a.get(n.parentId)??[];l.push(n),a.set(n.parentId,l)}else p.push(n);const u=new Map,f=n=>{let l={...n.tokens},c=n.cost;for(const g of a.get(n.id)??[]){const C=f(g);l=it(l,C.tokens),c+=C.cost}const h={tokens:l,cost:c};return u.set(n.id,h),h};for(const n of p)f(n);for(const n of p.slice(0,i))this.renderSession(t,n,0,a,u);if(p.length>i){const n=o("button","more",`Show ${p.length-i} more sessions`);n.addEventListener("click",r),t.appendChild(n)}}renderSession(t,s,i,r,d){const a=r.get(s.id)??[],p=this.expandedSessions.has(s.id),u=d.get(s.id)??{tokens:s.tokens,cost:s.cost},f=o("div","srow"+(p?" open":""));f.style.setProperty("--depth",String(i));const n=o("span","tgl",p?"▾":"▸");if(a.length){const c=o("span","badge",String(a.length));n.append(c)}const l=o("div","s-main");if(l.append(o("span","s-title",s.title||"Untitled session"),o("span","s-meta",`${s.agent} · ${s.model}`)),f.append(n,l,j(u.tokens),o("span","cost",x(u.cost))),D(f,()=>this.toggleSession(s.id)),t.appendChild(f),p){const c=o("div","s-detail");c.style.setProperty("--depth",String(i+1));const h=this.sessionDetails.get(s.id);h?h.models.length===0?c.appendChild(o("div","muted","No token usage recorded")):(c.appendChild(this.modelsTable(h.models)),a.length&&c.appendChild(o("div","muted note","model breakdown counts only this session — row totals include subagents"))):c.appendChild(o("div","muted","Loading…")),t.appendChild(c)}if(p)for(const c of a)this.renderSession(t,c,i+1,r,d)}toggleSession(t){this.expandedSessions.has(t)?(this.expandedSessions.delete(t),this.render()):(this.expandedSessions.add(t),this.loadSession(t))}modelsTable(t){const s=o("table","tbl tbl-models"),i=o("thead"),r=o("tr");for(const a of["Model","Mode","Msgs","Tokens","Cost"])r.appendChild(o("th","",a));i.appendChild(r),s.appendChild(i);const d=o("tbody");for(const a of t){const p=o("td","p-name");p.append(o("span","",a.model),o("span","muted",a.provider));const u=o("td");u.appendChild(j(a.tokens));const f=o("tr");f.append(p,o("td","",a.mode),o("td","num",String(a.messageCount)),u,o("td","cost",x(a.cost))),d.appendChild(f)}return s.appendChild(d),s}}function U(e,t){const s=o("div","stat");return s.append(o("span","num",t),o("span","lbl",e)),s}async function dt(e){const t=`${location.origin}/mcp`;try{if(navigator.clipboard&&window.isSecureContext)await navigator.clipboard.writeText(t);else throw new Error("clipboard unavailable")}catch{const d=o("textarea");d.value=t,d.style.position="fixed",d.style.opacity="0",document.body.appendChild(d),d.select(),document.execCommand("copy"),d.remove()}const s=e.querySelector(".mcp-copy");if(!s)return;const r=e.classList.contains("copied")?"copy":s.textContent;s.textContent="copied",e.classList.add("copied"),setTimeout(()=>{s.textContent=r,e.classList.remove("copied")},1200)}function ct(){const e=document.getElementById("mcp-url"),t=document.getElementById("mcp-box");!e||!t||(e.textContent=`${location.origin}/mcp`,t.addEventListener("click",()=>{dt(t)}))}function B(e,t){return`${e} / ${t}`}function lt(e){const t=o("td","num",B(e.mainSessionCount,e.sessionCount));return t.title=`${e.mainSessionCount} main sessions · ${e.sessionCount} total (incl. subagents)`,t}function pt(e){const t=o("div","stat stat-cost");return t.append(o("span","num",e===null?"…":x(e)),o("span","lbl","cost")),t}function ut(e,t,s="No data"){const i=o("div","pie"),r=e.reduce((m,v)=>m+v.value,0);if(r<=0)return i.appendChild(o("div","pie-empty",s)),i;const d=120,a=54,p=d/2,u=d/2,f=(m,v)=>{const $=(m-90)*Math.PI/180;return[p+v*Math.cos($),u+v*Math.sin($)]},n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("viewBox",`0 0 ${d} ${d}`),n.setAttribute("role","img"),n.setAttribute("width",String(d)),n.setAttribute("height",String(d)),n.classList.add("donut");const l=e.map(m=>`${m.label}: ${t(m.value)}`).join(", ");n.setAttribute("aria-label",l);let c=0;e.forEach((m,v)=>{const $=c/r*360;c+=m.value;const M=c/r*360,[q,F]=f($,a),[V,K]=f(M,a),X=M-$>180?1:0,b=document.createElementNS("http://www.w3.org/2000/svg","path");b.setAttribute("d",`M ${p} ${u} L ${q} ${F} A ${a} ${a} 0 ${X} 1 ${V} ${K} Z`),b.setAttribute("fill",A[v%A.length]),b.setAttribute("stroke","var(--inset)"),b.setAttribute("stroke-width","1"),b.dataset.label=m.label,b.dataset.value=t(m.value);const G=(m.value/r*100).toFixed(1).replace(/\.0$/,"");b.dataset.pct=G,b.addEventListener("mouseenter",()=>C(b)),n.appendChild(b)});const h=o("div","pie-readout"),g=(m,v)=>{h.textContent=m,h.classList.toggle("muted",v)};g(`total ${t(r)}`,!0);function C(m){for(const v of Array.from(n.querySelectorAll("path")))v.classList.toggle("dim",v!==m),v.classList.toggle("hot",v===m);g(`${m.dataset.label} · ${m.dataset.value} (${m.dataset.pct}%)`,!1)}return n.addEventListener("mouseleave",()=>{for(const m of Array.from(n.querySelectorAll("path")))m.classList.remove("dim","hot");g(`total ${t(r)}`,!0)}),i.append(n,h),i}function k(e,t,s,i){const r=o("div","pie-card");return r.append(o("h4","pie-title",e),ut(t,s,i)),r}const E=document.getElementById("app");async function W(){ct();let e=[],t=null;try{const n=await w.config();e=n.servers,T=n.ui?.sessionPage??T}catch(n){t=n instanceof Error?n.message:String(n)}if(_=e,t!==null||e.length===0){const n=o("div","error");n.append(o("h3","","No front-end server configuration"),o("p","",t??"No servers returned by /api/config — check dashboard.yaml."));const l=o("button","","Retry");l.addEventListener("click",()=>{W()}),n.appendChild(l),E.replaceChildren(n);return}e.length>1&&E.classList.add("multi"),P=[];for(let n=0;n<e.length;n++){const l=new at(n);l.root.style.borderTop=`3px solid ${A[n%A.length]}`,P.push(l),E.appendChild(l.root),l.start()}const s="overall",i=o("nav","tabs"),r=new Map,d=(n,l)=>{const c=o("button","tab",n);c.addEventListener("click",()=>u(l)),i.appendChild(c),r.set(l,c)};d("Overall",s),e.forEach((n,l)=>d(n.name,String(l)));const a=o("button","tab tab-export","Export Excel");a.title="Download the current view as an .xlsx workbook",i.appendChild(a),E.before(i);let p=s;function u(n){p=n;const l=n===s;E.classList.toggle("multi",l&&e.length>1),P.forEach((c,h)=>{c.root.hidden=!(l||String(h)===n)});for(const[c,h]of r)h.classList.toggle("active",c===n)}u(s),a.addEventListener("click",()=>{f()});async function f(){const n=p===s?e.map((c,h)=>({idx:h,name:c.name})):[{idx:Number(p),name:e[Number(p)]?.name??`server-${p}`}];a.disabled=!0;const l=a.textContent??"";a.textContent="Exporting…";try{await nt(n)}catch(c){alert(`Export failed: ${c instanceof Error?c.message:String(c)}`)}finally{a.disabled=!1,a.textContent=l}}}W();setInterval(()=>{for(const e of P)e.touch()},3e4);
|