@webfueler/oc-dash 0.1.1 → 0.1.3
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-CFqTsjeK.js +9 -0
- package/dist/assets/index-Cypmugeh.css +1 -0
- package/dist/index.html +17 -2
- package/dist-server/index.js +38 -3
- package/dist-server/ranges.js +21 -0
- package/package.json +1 -1
- 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,19 @@ 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
|
+
// Mission 026: one best-effort upstream stats call per project id behind
|
|
96
|
+
// the directory filter, all fired in parallel with the main call so the
|
|
97
|
+
// hero's latency is untouched. A failed call drops out of the list; the
|
|
98
|
+
// field is omitted when none succeed (never an error surface).
|
|
99
|
+
const projectCalls = project.map((id) => statsCall(oc, range, tz, id).catch(() => null));
|
|
81
100
|
const raw = await statsCall(oc, range, tz);
|
|
82
101
|
// The promise client returns SessionStatsInfo directly; a raw fetch
|
|
83
102
|
// returns { data: SessionStatsInfo }. Normalize both.
|
|
@@ -88,6 +107,21 @@ app.get("/api/summary", async (c) => {
|
|
|
88
107
|
!Array.isArray(data.models)) {
|
|
89
108
|
throw new Error("unexpected /api/session/stats payload shape");
|
|
90
109
|
}
|
|
110
|
+
const projectStats = [];
|
|
111
|
+
if (projectCalls.length > 0) {
|
|
112
|
+
const settled = await Promise.all(projectCalls);
|
|
113
|
+
for (let i = 0; i < settled.length; i++) {
|
|
114
|
+
const pRaw = settled[i];
|
|
115
|
+
const pData = pRaw?.data ?? pRaw;
|
|
116
|
+
if (pData &&
|
|
117
|
+
typeof pData === "object" &&
|
|
118
|
+
typeof pData.cost === "number") {
|
|
119
|
+
// The project id rides along so the client can verify the field
|
|
120
|
+
// belongs to the filter currently on screen before trusting it.
|
|
121
|
+
projectStats.push({ project: project[i], data: pData });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
91
125
|
// Additive, Today-only: trailing 7 days of activity so the chart can
|
|
92
126
|
// render the in-range day next to muted context days. Fired in parallel
|
|
93
127
|
// with the main stats call (mission 008, 007's F4) so Today pays one
|
|
@@ -109,6 +143,7 @@ app.get("/api/summary", async (c) => {
|
|
|
109
143
|
timezone: tz,
|
|
110
144
|
data,
|
|
111
145
|
...(contextActivity !== undefined ? { contextActivity } : {}),
|
|
146
|
+
...(projectStats.length > 0 ? { projectStats } : {}),
|
|
112
147
|
});
|
|
113
148
|
}
|
|
114
149
|
catch (err) {
|
package/dist-server/ranges.js
CHANGED
|
@@ -37,3 +37,24 @@ 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
|
+
* Mission 026: the param now carries a comma-separated list of project ids
|
|
43
|
+
* (a directory can map to more than one id — the personal directory maps
|
|
44
|
+
* to two), and the parser returns them deduped, order-preserved. Trimmed
|
|
45
|
+
* and non-empty or it is not a project filter at all; hard-capped so a
|
|
46
|
+
* junk param cannot build an absurd upstream URL — anything unusable
|
|
47
|
+
* becomes undefined and the server makes no extra upstream call. Mission
|
|
48
|
+
* 028 (L1): the total cap is now 4096 chars — the old 200 predates id
|
|
49
|
+
* lists and would reject five real 40-hex ids plus separators. The
|
|
50
|
+
* upstream call itself stays best-effort in the handler.
|
|
51
|
+
*/
|
|
52
|
+
export function parseProjectParam(value) {
|
|
53
|
+
const v = value?.trim();
|
|
54
|
+
if (!v || v.length > 4096)
|
|
55
|
+
return undefined;
|
|
56
|
+
const ids = [...new Set(v.split(",").map((s) => s.trim()).filter((s) => s.length > 0))];
|
|
57
|
+
if (ids.length === 0 || ids.length > 10)
|
|
58
|
+
return undefined;
|
|
59
|
+
return ids;
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -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}
|