@webfueler/oc-dash 0.1.0 → 0.1.2
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-CpyUcAbL.js +9 -0
- package/dist/assets/index-Cypmugeh.css +1 -0
- package/dist/index.html +17 -2
- package/dist-server/index.js +31 -3
- package/dist-server/ranges.js +13 -0
- package/package.json +2 -2
- package/dist/assets/index-C0tQcZFS.css +0 -1
- package/dist/assets/index-DsO0kXln.js +0 -9
package/dist-server/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { serveStatic } from "@hono/node-server/serve-static";
|
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { errorMessage, getOpencode, ocGetJson } from "./opencode.js";
|
|
6
|
-
import { contextStatsRange, localTimezone, parseRangePreset, resolveRange } from "./ranges.js";
|
|
6
|
+
import { contextStatsRange, localTimezone, parseProjectParam, parseRangePreset, resolveRange } from "./ranges.js";
|
|
7
7
|
import { walkSessions } from "./walk.js";
|
|
8
8
|
const PAGE_LIMIT = 100;
|
|
9
9
|
const PORT = Number(process.env.PORT) || 4021;
|
|
@@ -25,14 +25,20 @@ async function listPage(oc, cursor) {
|
|
|
25
25
|
return (await ocGetJson(oc, `/api/session?${params}`));
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* session.stats via the typed client with a raw-fetch fallback.
|
|
30
|
+
* Mission 014 (PD Q4a): an optional project pass-through — the upstream
|
|
31
|
+
* param already exists (project id); only the per-project tier-2 call
|
|
32
|
+
* sends it, the hero's main stats call never does.
|
|
33
|
+
*/
|
|
34
|
+
async function statsCall(oc, range, tz, project) {
|
|
30
35
|
try {
|
|
31
36
|
return await oc.client.session.stats({
|
|
32
37
|
from: range.from,
|
|
33
38
|
to: range.to,
|
|
34
39
|
timezone: tz,
|
|
35
40
|
tools: "summary",
|
|
41
|
+
...(project ? { project } : {}),
|
|
36
42
|
});
|
|
37
43
|
}
|
|
38
44
|
catch {
|
|
@@ -41,6 +47,8 @@ async function statsCall(oc, range, tz) {
|
|
|
41
47
|
params.set("from", String(range.from));
|
|
42
48
|
if (range.to != null)
|
|
43
49
|
params.set("to", String(range.to));
|
|
50
|
+
if (project)
|
|
51
|
+
params.set("project", project);
|
|
44
52
|
return await ocGetJson(oc, `/api/session/stats?${params}`);
|
|
45
53
|
}
|
|
46
54
|
}
|
|
@@ -76,8 +84,15 @@ app.get("/api/summary", async (c) => {
|
|
|
76
84
|
}
|
|
77
85
|
const range = resolveRange(preset);
|
|
78
86
|
const tz = localTimezone();
|
|
87
|
+
// Mission 014 (PD Q4a): additive project param. When present, ONE extra
|
|
88
|
+
// best-effort upstream stats call with project=<id> feeds the filtered
|
|
89
|
+
// card's tier-2 tiles. Fired in parallel with the main call so the hero's
|
|
90
|
+
// latency is untouched; on failure it resolves to undefined and the field
|
|
91
|
+
// is omitted (the contextActivity pattern), never an error surface.
|
|
92
|
+
const project = parseProjectParam(c.req.query("project"));
|
|
79
93
|
try {
|
|
80
94
|
const oc = await getOpencode();
|
|
95
|
+
const projectCall = project ? statsCall(oc, range, tz, project).catch(() => undefined) : undefined;
|
|
81
96
|
const raw = await statsCall(oc, range, tz);
|
|
82
97
|
// The promise client returns SessionStatsInfo directly; a raw fetch
|
|
83
98
|
// returns { data: SessionStatsInfo }. Normalize both.
|
|
@@ -88,6 +103,18 @@ app.get("/api/summary", async (c) => {
|
|
|
88
103
|
!Array.isArray(data.models)) {
|
|
89
104
|
throw new Error("unexpected /api/session/stats payload shape");
|
|
90
105
|
}
|
|
106
|
+
let projectStats;
|
|
107
|
+
if (project && projectCall) {
|
|
108
|
+
const pRaw = await projectCall;
|
|
109
|
+
const pData = pRaw?.data ?? pRaw;
|
|
110
|
+
if (pData &&
|
|
111
|
+
typeof pData === "object" &&
|
|
112
|
+
typeof pData.cost === "number") {
|
|
113
|
+
// The project id rides along so the client can verify the field
|
|
114
|
+
// belongs to the filter currently on screen before trusting it.
|
|
115
|
+
projectStats = { project, data: pData };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
91
118
|
// Additive, Today-only: trailing 7 days of activity so the chart can
|
|
92
119
|
// render the in-range day next to muted context days. Fired in parallel
|
|
93
120
|
// with the main stats call (mission 008, 007's F4) so Today pays one
|
|
@@ -109,6 +136,7 @@ app.get("/api/summary", async (c) => {
|
|
|
109
136
|
timezone: tz,
|
|
110
137
|
data,
|
|
111
138
|
...(contextActivity !== undefined ? { contextActivity } : {}),
|
|
139
|
+
...(projectStats !== undefined ? { projectStats } : {}),
|
|
112
140
|
});
|
|
113
141
|
}
|
|
114
142
|
catch (err) {
|
package/dist-server/ranges.js
CHANGED
|
@@ -37,3 +37,16 @@ export function localTimezone() {
|
|
|
37
37
|
export function contextStatsRange(preset, now = new Date()) {
|
|
38
38
|
return preset === "today" ? resolveRange("7d", now) : null;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Mission 014 (PD Q4a): additive project pass-through for /api/summary.
|
|
42
|
+
* Trimmed and non-empty or it is not a project filter at all; hard-capped
|
|
43
|
+
* so a junk param cannot build an absurd upstream URL — anything unusable
|
|
44
|
+
* becomes undefined and the server makes no extra upstream call. The
|
|
45
|
+
* upstream call itself stays best-effort in the handler.
|
|
46
|
+
*/
|
|
47
|
+
export function parseProjectParam(value) {
|
|
48
|
+
const v = value?.trim();
|
|
49
|
+
if (!v || v.length > 200)
|
|
50
|
+
return undefined;
|
|
51
|
+
return v;
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webfueler/oc-dash",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Local dashboard for opencode2 session costs, token totals, and subagent spend",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@hono/node-server": "^2.1.1",
|
|
33
|
-
"@opencode/client": "
|
|
33
|
+
"@opencode/client": "0.0.0-beta-19296",
|
|
34
34
|
"hono": "^4.13.7",
|
|
35
35
|
"react": "^19.2.8",
|
|
36
36
|
"react-dom": "^19.2.8"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:#0d1117;--panel:#161b22;--panel2:#1c2128;--border:#30363d;--text:#e6edf3;--dim:#8b949e;--accent:#4493f8;--ok:#3fb950;--warn:#d29922;--bad:#f85149;--money:#7ee2a8}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}@media (prefers-color-scheme:light){:root{--bg:#f6f8fa;--panel:#fff;--panel2:#f6f8fa;--border:#d0d7de;--text:#1f2328;--dim:#656d76;--accent:#0969da;--ok:#1a7f37;--warn:#9a6700;--bad:#cf222e;--money:#1a7f37}}*{box-sizing:border-box}body{background:var(--bg);color:var(--text);margin:0;font:14px/1.45 ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}.app{max-width:1080px;margin:0 auto;padding:16px 20px 48px}.topbar{flex-wrap:wrap;align-items:center;gap:16px;margin-bottom:12px;display:flex}h1{margin:0;font-size:20px}h2{color:var(--text);margin:0 0 8px;font-size:15px}.dim{color:var(--dim)}.updated{margin-left:auto;font-size:12px}.tabs{border:1px solid var(--border);border-radius:8px;display:inline-flex;overflow:hidden}.tab{color:var(--dim);cursor:pointer;font:inherit;background:0 0;border:0;padding:6px 14px}.tab+.tab{border-left:1px solid var(--border)}.tab.active{background:var(--accent);color:#fff}.health{border:1px solid var(--border);color:var(--dim);border-radius:8px;align-items:center;gap:8px;margin-bottom:12px;padding:6px 12px;font-size:12.5px;display:flex}.health.ok .dot{background:var(--ok)}.health.bad .dot{background:var(--bad)}.dot{border-radius:50%;flex:none;width:8px;height:8px}.error{border:1px solid var(--bad);color:var(--bad);border-radius:8px;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;padding:8px 12px;display:flex}button{font:inherit}.error button{color:var(--bad);border:1px solid var(--bad);cursor:pointer;background:0 0;border-radius:6px;padding:2px 10px}.loading{color:var(--dim);padding:24px 0}.badge{border:1px solid var(--border);color:var(--dim);border-radius:999px;padding:1px 8px;font-size:11px;display:inline-block}.badge.warn{border-color:var(--warn);color:var(--warn)}.badge.succeeded{border-color:var(--ok);color:var(--ok)}.badge.failed,.badge.interrupted{border-color:var(--bad);color:var(--bad)}.degraded-note{margin-bottom:12px;padding:6px 12px;display:block}.kpis{margin-bottom:20px}.hero-kpi{background:linear-gradient(180deg, var(--panel2), var(--panel));border:1px solid var(--border);border-radius:12px;flex-wrap:wrap;justify-content:space-between;align-items:flex-end;gap:18px;padding:18px 20px;display:flex}.hero-label{color:var(--dim);letter-spacing:.04em;text-transform:uppercase;font-size:12px}.hero-value{color:var(--money);font-variant-numeric:tabular-nums;margin-top:4px;font-size:42px;font-weight:700;line-height:1.05}.hero-sub{color:var(--dim);margin-top:6px;font-size:12.5px}.hero-kpi .badge.warn{margin-top:10px;display:inline-block}.statline{flex-wrap:wrap;gap:8px;display:flex}.stat{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:7px 12px}.stat b{font-variant-numeric:tabular-nums;font-size:13.5px;font-weight:600;display:block}.stat span{color:var(--dim);letter-spacing:.04em;text-transform:uppercase;font-size:10.5px}section{margin-bottom:24px}.section-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:8px;display:flex}.section-head h2{margin:0}.section-tools{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.tree-controls{gap:6px;display:flex}.tree-controls button{background:var(--panel);color:var(--text);border:1px solid var(--border);cursor:pointer;border-radius:6px;padding:3px 10px;font-size:12px}.tree-controls button:hover{border-color:var(--dim)}.filter select{font:inherit;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:3px 8px}table{border-collapse:collapse;background:var(--panel);border:1px solid var(--border);border-radius:10px;width:100%;overflow:hidden}th,td{text-align:left;border-top:1px solid var(--border);white-space:nowrap;text-overflow:ellipsis;max-width:340px;padding:6px 10px;overflow:hidden}th{text-transform:uppercase;letter-spacing:.04em;color:var(--dim);border-top:0;font-size:11.5px;font-weight:600}td.num,th.num{text-align:right;font-variant-numeric:tabular-nums}.root-row td{background:var(--panel2)}.root-row td.title{font-weight:600}.togglable{cursor:pointer}.togglable:hover td{filter:brightness(1.08)}tr:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.child-row td{background:color-mix(in srgb, var(--panel) 88%, var(--text) 12%)}.child-row td.title{color:var(--dim);font-size:12.5px;position:relative}.child-row td.title:before{content:"";left:calc(var(--indent,34px) - 12px);background:var(--border);width:1px;position:absolute;top:0;bottom:0}.title{white-space:normal;min-width:200px}.desc{color:var(--dim);margin-top:2px;font-size:11.5px;font-weight:400}.money{color:var(--money);font-weight:600}.model{color:var(--dim);font-size:12.5px}.mchip{border:1px solid var(--border);color:var(--dim);text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;border-radius:6px;max-width:100%;padding:1px 7px;font-size:11px;display:inline-block;overflow:hidden}.check{color:var(--dim)}.unpriced{margin-left:8px}.empty{color:var(--dim);background:var(--panel);border:1px dashed var(--border);border-radius:10px;padding:14px}.activity{background:var(--panel);border:1px solid var(--border);border-radius:10px;width:100%}.activity .bar{fill:var(--accent)}.activity .bar.muted,.activity .bar.zero{fill:var(--border)}.activity .grid{stroke:var(--border);stroke-dasharray:3 4}.activity .tick{fill:var(--dim);font-size:11px}.footnotes{border-top:1px solid var(--border);color:var(--dim);padding-top:12px;font-size:12.5px}.footnotes ol{margin:0;padding-left:20px}.footnotes li{margin-bottom:4px}
|