@webfueler/oc-dash 0.1.5 → 0.1.7

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/index.html CHANGED
@@ -21,8 +21,8 @@
21
21
  else document.documentElement.dataset.theme = c
22
22
  })()
23
23
  </script>
24
- <script type="module" crossorigin src="/assets/index-BGry4XSt.js"></script>
25
- <link rel="stylesheet" crossorigin href="/assets/index-HDE3yxEk.css">
24
+ <script type="module" crossorigin src="/assets/index-B9io-Y-k.js"></script>
25
+ <link rel="stylesheet" crossorigin href="/assets/index-CCkSH4wy.css">
26
26
  </head>
27
27
  <body>
28
28
  <div id="root"></div>
@@ -4,6 +4,7 @@ import { Hono } from "hono";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { errorMessage, getOpencode, ocGetJson } from "./opencode.js";
7
+ import { modelNames } from "./models.js";
7
8
  import { contextStatsRange, localTimezone, parseProjectParam, parseRangePreset, resolveRange } from "./ranges.js";
8
9
  import { compareVersions } from "./version.js";
9
10
  import { walkSessions } from "./walk.js";
@@ -235,6 +236,20 @@ app.get("/api/sessions", async (c) => {
235
236
  return c.json({ error: `opencode service request failed: ${errorMessage(err)}` }, 502);
236
237
  }
237
238
  });
239
+ // Mission 044: the providerID/id -> display-name map for the label surfaces
240
+ // (session chips, model filter, card header, models table). A separate
241
+ // lookup, not the stats path; the cache inside modelNames() keeps this at
242
+ // one upstream fetch per TTL window. Never an error surface: the client
243
+ // falls back to the raw ids when the map is empty.
244
+ app.get("/api/model-names", async (c) => {
245
+ try {
246
+ const oc = await getOpencode();
247
+ return c.json({ names: await modelNames(oc) });
248
+ }
249
+ catch {
250
+ return c.json({ names: {} });
251
+ }
252
+ });
238
253
  // Built frontend (production). API routes above take precedence.
239
254
  app.use("*", serveStatic({ root: DIST_ROOT }));
240
255
  app.get("*", serveStatic({ path: DIST_INDEX }));
@@ -0,0 +1,67 @@
1
+ import { ocGetJson } from "./opencode.js";
2
+ /**
3
+ * Pure parse of the opencode2 service's GET /api/model payload into the
4
+ * providerID/id -> name map. Entries missing any of the three fields — or
5
+ * carrying an empty or whitespace-only name — drop out, so the client's
6
+ * fallback (the raw id) never receives a blank or wrong label. Accepts both
7
+ * the raw array and the { data: [...] } wrapper the promise client can
8
+ * produce.
9
+ */
10
+ export function parseModelNames(payload) {
11
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
12
+ const wrapped = payload;
13
+ if (Array.isArray(wrapped.data))
14
+ payload = wrapped.data;
15
+ }
16
+ if (!Array.isArray(payload))
17
+ return {};
18
+ const names = {};
19
+ for (const entry of payload) {
20
+ if (!entry || typeof entry !== "object")
21
+ continue;
22
+ const { providerID, id, name } = entry;
23
+ if (typeof providerID !== "string" || typeof id !== "string" || typeof name !== "string") {
24
+ continue;
25
+ }
26
+ // Trim before the non-empty check so a whitespace-only name can never
27
+ // reach the label surfaces as a blank; surrounding whitespace on a real
28
+ // name is noise, so the trimmed value is what gets stored.
29
+ const trimmed = name.trim();
30
+ if (!providerID || !id || !trimmed)
31
+ continue;
32
+ names[`${providerID}/${id}`] = trimmed;
33
+ }
34
+ return names;
35
+ }
36
+ // The model catalog is static for the life of a service, so a success lives
37
+ // an hour and a failure retries sooner — the update-check pattern
38
+ // (index.ts:43-44): the caller never waits on a cold fetch more than once
39
+ // per window, and any failure degrades to an empty map (raw-id labels).
40
+ const MODEL_NAMES_TTL_MS = 60 * 60 * 1000;
41
+ const MODEL_NAMES_RETRY_MS = 5 * 60 * 1000;
42
+ let cache = null;
43
+ /**
44
+ * The cached name map. One /api/model fetch per TTL window, never per
45
+ * request; any failure is an empty map so every label surface falls back to
46
+ * today's raw-id behavior.
47
+ */
48
+ export async function modelNames(oc) {
49
+ if (cache && Date.now() < cache.expiresAt)
50
+ return cache.names;
51
+ let names;
52
+ let ttl;
53
+ try {
54
+ names = parseModelNames(await ocGetJson(oc, "/api/model"));
55
+ ttl = MODEL_NAMES_TTL_MS;
56
+ }
57
+ catch {
58
+ names = {};
59
+ ttl = MODEL_NAMES_RETRY_MS;
60
+ }
61
+ cache = { names, expiresAt: Date.now() + ttl };
62
+ return names;
63
+ }
64
+ /** Test seam: forget the module-level cache. */
65
+ export function resetModelNamesCache() {
66
+ cache = null;
67
+ }
Binary file
package/package.json CHANGED
@@ -1,8 +1,19 @@
1
1
  {
2
2
  "name": "@webfueler/oc-dash",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Local dashboard for opencode2 session costs, token totals, and subagent spend",
5
5
  "license": "MIT",
6
+ "repository": "https://github.com/webfueler/oc-dash",
7
+ "homepage": "https://github.com/webfueler/oc-dash",
8
+ "author": "webfueler",
9
+ "keywords": [
10
+ "opencode",
11
+ "opencode2",
12
+ "cost",
13
+ "tokens",
14
+ "spend",
15
+ "dashboard"
16
+ ],
6
17
  "type": "module",
7
18
  "engines": {
8
19
  "node": ">=22"
@@ -13,7 +24,8 @@
13
24
  "files": [
14
25
  "bin",
15
26
  "dist",
16
- "dist-server"
27
+ "dist-server",
28
+ "docs/screenshot.png"
17
29
  ],
18
30
  "publishConfig": {
19
31
  "access": "public"