codex-copilot-dx 0.8.6 → 0.8.8

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.
@@ -0,0 +1,347 @@
1
+ const element = (id) => document.getElementById(id);
2
+ const text = (id, value) => { element(id).textContent = value; };
3
+ const finite = (value) => value !== null && value !== undefined && Number.isFinite(Number(value));
4
+ const number = (value) => finite(value) ? Math.max(0, Math.round(Number(value))).toLocaleString() : "—";
5
+ const mib = (value) => finite(value) ? `${(Number(value) / 1048576).toFixed(1)} MiB` : "—";
6
+
7
+ function duration(value) {
8
+ if (!finite(value)) return "—";
9
+ const seconds = Math.max(0, Math.round(Number(value) / 1000));
10
+ const days = Math.floor(seconds / 86400);
11
+ const hours = Math.floor((seconds % 86400) / 3600);
12
+ const minutes = Math.floor((seconds % 3600) / 60);
13
+ return days ? `${days}d ${hours}h` : hours ? `${hours}h ${minutes}m` : `${minutes}m`;
14
+ }
15
+
16
+ function renderFailures(recent) {
17
+ const list = element("failures");
18
+ list.replaceChildren();
19
+ const failures = Array.isArray(recent) ? recent.slice(-5).reverse() : [];
20
+ if (!failures.length) {
21
+ const item = document.createElement("li");
22
+ item.className = "empty";
23
+ item.textContent = "No response.failed events recorded.";
24
+ list.append(item);
25
+ return;
26
+ }
27
+ for (const failure of failures) {
28
+ const item = document.createElement("li");
29
+ const title = document.createElement("strong");
30
+ const detail = document.createElement("span");
31
+ title.textContent = `${String(failure.model || "unknown_model").slice(0, 80)} / ${String(failure.code || "unknown_error").slice(0, 80)}`;
32
+ detail.textContent = `${String(failure.message || "No message").slice(0, 240)}${failure.retried ? " / retried" : ""}`;
33
+ item.append(title, detail);
34
+ list.append(item);
35
+ }
36
+ }
37
+
38
+ function render(data) {
39
+ text("version", `v${data.version || "?"}`);
40
+ text("uptime", `pid ${number(data.pid)} / uptime ${duration(data.uptime_ms)}`);
41
+
42
+ const copilot = data.copilot || {};
43
+ text("copilot", copilot.account_bound ? "BOUND" : "UNCONFIRMED");
44
+ text("token", copilot.token_cached ? `service token / TTL ${duration(copilot.token_expires_in_ms)}` : "service token not cached");
45
+
46
+ const models = data.models || {};
47
+ text("models", number(models.models));
48
+ text("model-source", `source ${String(models.source || "unknown").slice(0, 80)} / live: ccdx models`);
49
+
50
+ const requests = data.requests || {};
51
+ text("requests", number(requests.total));
52
+ text("request-errors", `4xx ${number(requests.status_4xx)} / 5xx ${number(requests.status_5xx)} / active ${number(requests.active)}`);
53
+
54
+ const outcomes = data.stream_performance?.by_route?.responses?.terminal_outcomes?.totals || {};
55
+ for (const key of ["completed", "incomplete", "failed", "cancelled"]) text(key, number(outcomes[key]));
56
+
57
+ const history = data.response_history || {};
58
+ const limit = Number(history.maxBytes || data.limits?.response_history_max_bytes || 0);
59
+ const used = Number(history.bytes || 0);
60
+ text("history", `${mib(used)} / ${mib(limit)}`);
61
+ text("history-detail", `entries ${number(history.entries)} / trees ${number(history.tree_count)} / misses ${number(history.lookup_misses)} (evicted ${number(history.evicted_lookup_misses)})`);
62
+ element("history-meter").value = limit > 0 ? Math.min(100, Math.max(0, used / limit * 100)) : 0;
63
+
64
+ const image = data.image_generation;
65
+ text("image", image ? `${number(image.succeeded)} succeeded` : "NOT INITIALIZED");
66
+ text("image-detail", image
67
+ ? `active ${number(image.active)} / failed ${number(image.failed)} / delivery failures ${number(image.delivery_failures)}`
68
+ : "Setup state unknown / ccdx image-status");
69
+
70
+ renderFailures(data.response_failures?.recent);
71
+ text("updated", `Snapshot ${new Date().toLocaleTimeString()}`);
72
+ }
73
+
74
+ async function refresh() {
75
+ const button = element("refresh");
76
+ button.disabled = true;
77
+ text("connection", "READING");
78
+ element("connection").className = "badge neutral";
79
+ try {
80
+ const response = await fetch("/_ccdx/status", {
81
+ cache: "no-store",
82
+ headers: { Accept: "application/json" },
83
+ signal: AbortSignal.timeout(5000),
84
+ });
85
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
86
+ const data = await response.json();
87
+ if (data?.ok !== true || data?.name !== "codex-copilot-dx") throw new Error("Unexpected status response");
88
+ render(data);
89
+ text("connection", "LOCAL OK");
90
+ element("connection").className = "badge good";
91
+ } catch {
92
+ text("connection", "UNAVAILABLE");
93
+ element("connection").className = "badge bad";
94
+ text("updated", "Status unavailable / check ccdx, then refresh");
95
+ } finally {
96
+ button.disabled = false;
97
+ }
98
+ }
99
+
100
+ element("refresh").addEventListener("click", refresh);
101
+ refresh();
102
+
103
+ const animationRows = new Map();
104
+ let animationTimelines = [];
105
+ let savedAnimation = "";
106
+ let selectedAnimation = "";
107
+ let previewDisabledByEnvironment = false;
108
+ let previewVisible = false;
109
+ let previewTimer = null;
110
+ let previewStarted = 0;
111
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
112
+
113
+ function renderAnsiFrame(target, frame) {
114
+ const fragments = String(frame).split(/\u001b\[([0-9;]+)m/);
115
+ const content = document.createDocumentFragment();
116
+ let color = "";
117
+ for (let index = 0; index < fragments.length; index += 2) {
118
+ if (fragments[index]) {
119
+ const span = document.createElement("span");
120
+ span.className = color;
121
+ span.textContent = fragments[index];
122
+ content.append(span);
123
+ }
124
+ const code = fragments[index + 1];
125
+ color = code === "97" ? "ansi-white"
126
+ : /^38;5;(159|123|87|51|45|39|33)$/.test(code || "") ? `ansi-${code.slice(5)}` : "";
127
+ }
128
+ target.replaceChildren(content);
129
+ }
130
+
131
+ function renderAnimationFrames(elapsed) {
132
+ for (const timeline of animationTimelines) {
133
+ const position = timeline.id === selectedAnimation ? elapsed % timeline.duration : 0;
134
+ const frameIndex = timeline.frames.findIndex(({ until }) => position < until);
135
+ if (frameIndex === timeline.lastFrame) continue;
136
+ timeline.lastFrame = frameIndex;
137
+ renderAnsiFrame(timeline.target, frameIndex < 0 ? `[${" ".repeat(20)}]` : timeline.frames[frameIndex].ansi);
138
+ }
139
+ }
140
+
141
+ function stopAnimationPreview() {
142
+ if (previewTimer !== null) clearTimeout(previewTimer);
143
+ previewTimer = null;
144
+ }
145
+
146
+ function tickAnimationPreview() {
147
+ previewTimer = null;
148
+ if (!previewVisible || document.visibilityState !== "visible" || reducedMotion.matches || previewDisabledByEnvironment) return;
149
+ renderAnimationFrames(performance.now() - previewStarted);
150
+ previewTimer = setTimeout(tickAnimationPreview, 32);
151
+ }
152
+
153
+ function syncAnimationPreview() {
154
+ stopAnimationPreview();
155
+ if (!previewVisible || document.visibilityState !== "visible" || reducedMotion.matches || previewDisabledByEnvironment) {
156
+ for (const timeline of animationTimelines) timeline.lastFrame = -2;
157
+ renderAnimationFrames(0);
158
+ return;
159
+ }
160
+ previewStarted = performance.now();
161
+ tickAnimationPreview();
162
+ }
163
+
164
+ new IntersectionObserver(([entry]) => {
165
+ previewVisible = entry.isIntersecting;
166
+ syncAnimationPreview();
167
+ }).observe(element("animation-options"));
168
+ document.addEventListener("visibilitychange", syncAnimationPreview);
169
+ reducedMotion.addEventListener("change", syncAnimationPreview);
170
+
171
+ function buildAnimationOptions(data) {
172
+ const container = element("animation-options");
173
+ container.replaceChildren();
174
+ animationRows.clear();
175
+ animationTimelines = [];
176
+ savedAnimation = data.theme;
177
+ selectedAnimation = data.theme;
178
+ previewDisabledByEnvironment = data.disabled_by_environment === true;
179
+ for (const [index, theme] of data.themes.entries()) {
180
+ const option = document.createElement("button");
181
+ option.type = "button";
182
+ option.className = "theme-option";
183
+ option.setAttribute("aria-pressed", String(theme.id === data.theme));
184
+ option.addEventListener("click", () => {
185
+ selectedAnimation = theme.id;
186
+ for (const [id, row] of animationRows) row.setAttribute("aria-pressed", String(id === theme.id));
187
+ element("save-animation").disabled = selectedAnimation === savedAnimation;
188
+ syncAnimationPreview();
189
+ });
190
+ const name = document.createElement("span");
191
+ name.className = "theme-name";
192
+ name.textContent = `${String(index + 1).padStart(2, "0")} ${theme.label}`;
193
+ if (theme.default) {
194
+ const marker = document.createElement("small");
195
+ marker.textContent = "DEFAULT";
196
+ name.append(marker);
197
+ }
198
+ const preview = document.createElement("span");
199
+ preview.className = "theme-preview";
200
+ preview.setAttribute("aria-hidden", "true");
201
+ option.append(name, preview);
202
+ container.append(option);
203
+ animationRows.set(theme.id, option);
204
+ let until = 0;
205
+ const frames = theme.frames.map((frame) => {
206
+ until += frame.delay_ms;
207
+ return { ansi: frame.ansi, until };
208
+ });
209
+ animationTimelines.push({ id: theme.id, target: preview, frames, duration: until + theme.loop_pause_ms, lastFrame: -2 });
210
+ }
211
+ element("save-animation").disabled = true;
212
+ syncAnimationPreview();
213
+ }
214
+
215
+ function animationNote(data) {
216
+ return data.disabled_by_environment
217
+ ? "Disabled by CCDX_TERMINAL_ANIMATION; saved choice applies after override removal and next start."
218
+ : "Saved choice applies on next ccdx start.";
219
+ }
220
+
221
+ async function loadAnimation() {
222
+ text("animation-state", "READING");
223
+ try {
224
+ const response = await fetch("/_ccdx/ui/animation", { cache: "no-store", headers: { "X-CCDX-Dashboard": "1" }, signal: AbortSignal.timeout(8000) });
225
+ const data = await response.json();
226
+ if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
227
+ buildAnimationOptions(data);
228
+ text("animation-state", `CURRENT ${data.theme.toUpperCase()}`);
229
+ text("animation-note", animationNote(data));
230
+ } catch (error) {
231
+ text("animation-state", "UNAVAILABLE");
232
+ text("animation-note", String(error.message || error).slice(0, 180));
233
+ }
234
+ }
235
+
236
+ async function saveAnimation() {
237
+ const button = element("save-animation");
238
+ if (!selectedAnimation || selectedAnimation === savedAnimation) return;
239
+ button.disabled = true;
240
+ text("animation-note", "Saving…");
241
+ try {
242
+ const response = await fetch("/_ccdx/ui/animation", {
243
+ method: "POST",
244
+ headers: { "Content-Type": "application/json", "X-CCDX-Dashboard": "1" },
245
+ body: JSON.stringify({ theme: selectedAnimation }),
246
+ signal: AbortSignal.timeout(8000),
247
+ });
248
+ const data = await response.json();
249
+ if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
250
+ savedAnimation = data.theme;
251
+ text("animation-state", `CURRENT ${data.theme.toUpperCase()}`);
252
+ text("animation-note", animationNote(data));
253
+ } catch (error) {
254
+ text("animation-note", `Not saved / ${String(error.message || error).slice(0, 150)}`);
255
+ } finally {
256
+ button.disabled = selectedAnimation === savedAnimation;
257
+ }
258
+ }
259
+
260
+ function emptyTable(bodyId, columns, message) {
261
+ const body = element(bodyId);
262
+ body.replaceChildren();
263
+ const row = document.createElement("tr");
264
+ const cell = document.createElement("td");
265
+ cell.colSpan = columns;
266
+ cell.textContent = message;
267
+ row.append(cell);
268
+ body.append(row);
269
+ }
270
+
271
+ function tableRow(values, { total = false } = {}) {
272
+ const row = document.createElement("tr");
273
+ if (total) row.className = "total-row";
274
+ for (const value of values) {
275
+ const cell = document.createElement("td");
276
+ cell.textContent = value;
277
+ row.append(cell);
278
+ }
279
+ return row;
280
+ }
281
+
282
+ async function loadModels() {
283
+ const button = element("refresh-models");
284
+ button.disabled = true;
285
+ text("models-state", "CHECKING LIVE");
286
+ text("models-note", "Querying GitHub Copilot…");
287
+ emptyTable("live-models-body", 4, "Loading…");
288
+ try {
289
+ const response = await fetch("/_ccdx/ui/models/live", { cache: "no-store", headers: { "X-CCDX-Dashboard": "1" }, signal: AbortSignal.timeout(15000) });
290
+ const data = await response.json();
291
+ if (!response.ok || data.source !== "live") throw new Error(data.error || `HTTP ${response.status}`);
292
+ const body = element("live-models-body");
293
+ body.replaceChildren();
294
+ for (const model of data.models) {
295
+ body.append(tableRow([model.id, model.vendor, model.endpoints.join(" / "), model.preview ? "preview" : "—"]));
296
+ }
297
+ if (!data.models.length) emptyTable("live-models-body", 4, "No selectable GPT models advertised.");
298
+ text("models-state", "LIVE SNAPSHOT");
299
+ text("models-note", `${number(data.selectable)} selectable / ${number(data.advertised)} advertised / ${data.upstream_host} / ${new Date(data.checked_at).toLocaleTimeString()}`);
300
+ } catch (error) {
301
+ text("models-state", "LIVE LOOKUP FAILED");
302
+ text("models-note", String(error.message || error).slice(0, 240));
303
+ emptyTable("live-models-body", 4, "No live catalog available.");
304
+ } finally {
305
+ button.disabled = false;
306
+ }
307
+ }
308
+
309
+ function usageValues(row) {
310
+ return [row.model, number(row.requests), number(row.input_tokens), number(row.cache_read_tokens), number(row.output_tokens), number(row.total_tokens)];
311
+ }
312
+
313
+ async function loadUsage() {
314
+ const button = element("refresh-usage");
315
+ button.disabled = true;
316
+ text("usage-state", "READING LOG");
317
+ text("usage-note", "Aggregating local usage metadata…");
318
+ emptyTable("usage-body", 6, "Loading…");
319
+ try {
320
+ const response = await fetch("/_ccdx/ui/usage", { cache: "no-store", headers: { "X-CCDX-Dashboard": "1" }, signal: AbortSignal.timeout(15000) });
321
+ const data = await response.json();
322
+ if (!response.ok || data.source !== "local_usage_log") throw new Error(data.error || `HTTP ${response.status}`);
323
+ const body = element("usage-body");
324
+ body.replaceChildren();
325
+ if (data.total.requests > 0) {
326
+ body.append(tableRow(usageValues(data.total), { total: true }));
327
+ for (const row of data.rows) body.append(tableRow(usageValues(row)));
328
+ } else {
329
+ emptyTable("usage-body", 6, "No usage records.");
330
+ }
331
+ text("usage-state", "LOCAL LOG");
332
+ text("usage-note", `${number(data.total.requests)} records / ${number(data.rows.length)} of ${number(data.model_count)} models shown`);
333
+ } catch (error) {
334
+ text("usage-state", "LOG UNAVAILABLE");
335
+ text("usage-note", String(error.message || error).slice(0, 180));
336
+ emptyTable("usage-body", 6, "No usage summary available.");
337
+ } finally {
338
+ button.disabled = false;
339
+ }
340
+ }
341
+
342
+ element("save-animation").addEventListener("click", saveAnimation);
343
+ element("refresh-models").addEventListener("click", loadModels);
344
+ element("refresh-usage").addEventListener("click", loadUsage);
345
+ loadAnimation();
346
+ loadModels();
347
+ loadUsage();
@@ -0,0 +1,204 @@
1
+ import os from "node:os";
2
+ import { fetchLiveCopilotModels } from "./cli-models.mjs";
3
+ import { createRequestAbort, readJsonBody } from "./http-transport.mjs";
4
+ import { isLoopbackAddress, isLoopbackHostHeader } from "./observability.mjs";
5
+ import {
6
+ DEFAULT_TERMINAL_ANIMATION_THEME,
7
+ getTerminalAnimationFrameDelay,
8
+ isTerminalAnimationTheme,
9
+ renderTerminalAnimationFrame,
10
+ TERMINAL_ANIMATION_THEMES,
11
+ } from "./terminal-animation.mjs";
12
+ import { summarizeUsageLogs } from "./usage.mjs";
13
+ import { readUserSettings, terminalAnimationPreference, writeTerminalAnimationTheme } from "./user-settings.mjs";
14
+
15
+ const ANIMATION_PATH = "/_ccdx/ui/animation";
16
+ const MODELS_PATH = "/_ccdx/ui/models/live";
17
+ const USAGE_PATH = "/_ccdx/ui/usage";
18
+ const DISABLED_ANIMATION = /^(0|false|no|off)$/i;
19
+
20
+ function sendJson(res, statusCode, value) {
21
+ res.writeHead(statusCode, {
22
+ "Content-Type": "application/json; charset=utf-8",
23
+ "Cache-Control": "no-store",
24
+ "X-Content-Type-Options": "nosniff",
25
+ });
26
+ res.end(JSON.stringify(value));
27
+ }
28
+
29
+ function safeText(value, length = 160) {
30
+ return String(value ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").trim().slice(0, length);
31
+ }
32
+
33
+ function liveModelError(error) {
34
+ const message = String(error?.message || error || "");
35
+ const status = /\bHTTP (\d{3})\b/.exec(message)?.[1];
36
+ if (status) return `Live model lookup failed (HTTP ${status}). Run ccdx models for details.`;
37
+ if (/timed out/i.test(message)) return "Live model lookup timed out. Retry or run ccdx models.";
38
+ if (/GitHub token not found/i.test(message)) return "GitHub token not found. Start ccdx to sign in.";
39
+ return "Live model lookup failed. Run ccdx models for details.";
40
+ }
41
+
42
+ function animationState(env, home, { includeFrames = false } = {}) {
43
+ readUserSettings({ env, home, strict: true });
44
+ const preference = terminalAnimationPreference({ env, home });
45
+ return {
46
+ theme: preference.theme,
47
+ source: preference.source,
48
+ disabled_by_environment: DISABLED_ANIMATION.test(String(env.CCDX_TERMINAL_ANIMATION || "").trim()),
49
+ themes: TERMINAL_ANIMATION_THEMES.map((theme) => ({
50
+ id: theme.id,
51
+ label: theme.label,
52
+ default: theme.id === DEFAULT_TERMINAL_ANIMATION_THEME,
53
+ ...(includeFrames ? {
54
+ loop_pause_ms: theme.loopPauseMs,
55
+ frames: Array.from({ length: theme.frameCount }, (_, offset) => {
56
+ const index = theme.startFrame + offset;
57
+ return {
58
+ ansi: renderTerminalAnimationFrame(theme.id, index),
59
+ delay_ms: getTerminalAnimationFrameDelay(theme.id, index),
60
+ };
61
+ }),
62
+ } : {}),
63
+ })),
64
+ };
65
+ }
66
+
67
+ function sameOrigin(req) {
68
+ const origin = req.headers?.origin;
69
+ const host = req.headers?.host;
70
+ if (typeof origin !== "string" || typeof host !== "string") return false;
71
+ try {
72
+ return new URL(origin).origin === new URL(`http://${host}`).origin;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ function finite(value) {
79
+ return Number.isFinite(value) ? Math.max(0, value) : null;
80
+ }
81
+
82
+ function cacheReadTokens(usage = {}) {
83
+ const values = [usage.cache_read_input_tokens, usage.cached_input_tokens].filter(Number.isFinite);
84
+ return values.length ? values.reduce((sum, value) => sum + value, 0) : null;
85
+ }
86
+
87
+ function usageRow(model, values) {
88
+ return {
89
+ model: safeText(model, 100),
90
+ requests: finite(values.requests),
91
+ input_tokens: finite(values.input_tokens),
92
+ cache_read_tokens: cacheReadTokens(values),
93
+ output_tokens: finite(values.output_tokens),
94
+ total_tokens: finite(values.total_tokens),
95
+ };
96
+ }
97
+
98
+ function usageTable(summary) {
99
+ const models = Object.entries(summary.byModel || {})
100
+ .sort(([leftName, left], [rightName, right]) => (right.total_tokens || 0) - (left.total_tokens || 0)
101
+ || leftName.localeCompare(rightName, "en"));
102
+ return {
103
+ total: usageRow("TOTAL", { requests: summary.requests, ...summary.totals }),
104
+ model_count: models.length,
105
+ rows: models.slice(0, 100).map(([name, values]) => usageRow(name, values)),
106
+ };
107
+ }
108
+
109
+ export async function handleDashboardApi(req, res, pathname, {
110
+ env = process.env,
111
+ home = os.homedir(),
112
+ liveModelsFn = () => fetchLiveCopilotModels({ home }),
113
+ usageSummaryFn = () => summarizeUsageLogs(),
114
+ } = {}) {
115
+ if (!isLoopbackAddress(req.socket?.remoteAddress)
116
+ || req.headers?.host === undefined
117
+ || !isLoopbackHostHeader(req.headers.host)) {
118
+ sendJson(res, 403, { error: "Dashboard API is available only at a loopback address" });
119
+ return;
120
+ }
121
+ if (req.headers?.["x-ccdx-dashboard"] !== "1") {
122
+ sendJson(res, 403, { error: "Dashboard API requires a same-origin page request" });
123
+ return;
124
+ }
125
+
126
+ if (pathname === ANIMATION_PATH && req.method === "GET") {
127
+ try {
128
+ sendJson(res, 200, animationState(env, home, { includeFrames: true }));
129
+ } catch {
130
+ sendJson(res, 409, { error: "Animation settings are invalid; inspect them with ccdx animation" });
131
+ }
132
+ return;
133
+ }
134
+
135
+ if (pathname === ANIMATION_PATH && req.method === "POST") {
136
+ if (!sameOrigin(req)) {
137
+ sendJson(res, 403, { error: "Animation changes require the same-origin dashboard" });
138
+ return;
139
+ }
140
+ if (String(req.headers?.["content-type"] || "").split(";", 1)[0].trim().toLowerCase() !== "application/json") {
141
+ sendJson(res, 415, { error: "Animation changes require application/json" });
142
+ return;
143
+ }
144
+ const abort = createRequestAbort(req, res);
145
+ abort.setTimeout(5000, "request_body_timeout");
146
+ let body;
147
+ try {
148
+ body = await readJsonBody(req, { maxBodyBytes: 2048, maxDecodedBodyBytes: 2048, signal: abort.signal });
149
+ } catch (error) {
150
+ sendJson(res, error?.statusCode === 413 ? 413 : 400, { error: "Invalid animation request body" });
151
+ return;
152
+ } finally {
153
+ abort.cleanup();
154
+ }
155
+ if (!isTerminalAnimationTheme(body?.theme)) {
156
+ sendJson(res, 400, { error: "Select a listed animation theme" });
157
+ return;
158
+ }
159
+ try {
160
+ const result = writeTerminalAnimationTheme(body.theme, { env, home });
161
+ sendJson(res, 200, { changed: result.changed, ...animationState(env, home) });
162
+ } catch {
163
+ sendJson(res, 409, { error: "Animation setting was not saved; check ccdx animation" });
164
+ }
165
+ return;
166
+ }
167
+
168
+ if (pathname === MODELS_PATH && req.method === "GET") {
169
+ try {
170
+ const catalog = await liveModelsFn();
171
+ if (!Array.isArray(catalog?.models) || !Number.isFinite(catalog?.advertised)) {
172
+ throw new Error("Invalid live model catalog");
173
+ }
174
+ const models = catalog.models;
175
+ sendJson(res, 200, {
176
+ source: "live",
177
+ checked_at: new Date().toISOString(),
178
+ upstream_host: safeText(catalog?.upstreamHost || "GitHub Copilot", 120),
179
+ advertised: finite(catalog?.advertised),
180
+ selectable: models.length,
181
+ models: models.slice(0, 500).map((model) => ({
182
+ id: safeText(model.id, 100),
183
+ vendor: safeText(model.vendor, 80),
184
+ endpoints: Array.isArray(model.endpoints) ? model.endpoints.filter((value) => value === "responses" || value === "chat") : [],
185
+ preview: model.preview === true,
186
+ })),
187
+ });
188
+ } catch (error) {
189
+ sendJson(res, 502, { error: liveModelError(error) });
190
+ }
191
+ return;
192
+ }
193
+
194
+ if (pathname === USAGE_PATH && req.method === "GET") {
195
+ try {
196
+ sendJson(res, 200, { source: "local_usage_log", ...(usageTable(await usageSummaryFn())) });
197
+ } catch {
198
+ sendJson(res, 500, { error: "Could not read local usage summary" });
199
+ }
200
+ return;
201
+ }
202
+
203
+ sendJson(res, 405, { error: "Method not allowed" });
204
+ }
@@ -0,0 +1,30 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { isLoopbackAddress, isLoopbackHostHeader } from "./observability.mjs";
3
+
4
+ const ASSETS = new Map([
5
+ ["/", ["./dashboard/index.html", "text/html; charset=utf-8"]],
6
+ ["/ui.css", ["./dashboard/ui.css", "text/css; charset=utf-8"]],
7
+ ["/ui.js", ["./dashboard/ui.js", "text/javascript; charset=utf-8"]],
8
+ ]);
9
+
10
+ export async function serveDashboard(req, res, pathname) {
11
+ if (!isLoopbackAddress(req.socket?.remoteAddress)
12
+ || req.headers?.host === undefined
13
+ || !isLoopbackHostHeader(req.headers.host)) {
14
+ res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" });
15
+ res.end("The CCDX dashboard is available only at a loopback address.");
16
+ return;
17
+ }
18
+
19
+ const [file, contentType] = ASSETS.get(pathname);
20
+ const body = await readFile(new URL(file, import.meta.url));
21
+ res.writeHead(200, {
22
+ "Content-Type": contentType,
23
+ "Cache-Control": "no-store",
24
+ "Content-Security-Policy": "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
25
+ "X-Content-Type-Options": "nosniff",
26
+ "X-Frame-Options": "DENY",
27
+ "Referrer-Policy": "no-referrer",
28
+ });
29
+ res.end(req.method === "HEAD" ? undefined : body);
30
+ }
@@ -108,6 +108,19 @@ export function isLoopbackAddress(address) {
108
108
  return isIP(ipv4) === 4 && ipv4.startsWith("127.");
109
109
  }
110
110
 
111
+ export function isLoopbackHostHeader(host) {
112
+ if (host === undefined) return true; // HTTP/1.0 and in-process callers may omit Host.
113
+ if (typeof host !== "string" || !host || host.includes(",")) return false;
114
+ try {
115
+ const url = new URL(`http://${host}`);
116
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
117
+ return !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash
118
+ && (hostname === "localhost" || isLoopbackAddress(hostname));
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
111
124
  function modelRegistryStatus(modelRegistry) {
112
125
  const modelData = Array.isArray(modelRegistry?.models)
113
126
  ? modelRegistry.models
@@ -81,6 +81,9 @@ const treeLru = new Map();
81
81
  const pinnedTrees = new Map();
82
82
  const evictedIds = new Set();
83
83
  let totalBytes = 0;
84
+ let evictedEntriesTotal = 0;
85
+ let lookupMisses = 0;
86
+ let evictedLookupMisses = 0;
84
87
  const HISTORY_RUNTIME_CONFIG = loadRuntimeConfig();
85
88
  let maxBytes = HISTORY_RUNTIME_CONFIG.responseHistoryMaxBytes;
86
89
  let maxEntries = HISTORY_RUNTIME_CONFIG.responseHistoryMaxEntries;
@@ -133,6 +136,7 @@ function removeSubtree(rootId) {
133
136
  const pending = [rootId];
134
137
  const removed = new Set();
135
138
  const affectedRoots = new Set();
139
+ let removedEntries = 0;
136
140
  while (pending.length) {
137
141
  const id = pending.pop();
138
142
  if (removed.has(id)) continue;
@@ -149,7 +153,9 @@ function removeSubtree(rootId) {
149
153
  totalBytes -= entry.bytes;
150
154
  histories.delete(id);
151
155
  rememberEvictedId(id);
156
+ removedEntries += 1;
152
157
  }
158
+ evictedEntriesTotal += removedEntries;
153
159
  for (const affectedRoot of affectedRoots) {
154
160
  const entry = histories.get(affectedRoot);
155
161
  if (!entry || entry.rootId !== affectedRoot) treeLru.delete(affectedRoot);
@@ -223,6 +229,9 @@ export function clearResponseHistory() {
223
229
  pinnedTrees.clear();
224
230
  evictedIds.clear();
225
231
  totalBytes = 0;
232
+ evictedEntriesTotal = 0;
233
+ lookupMisses = 0;
234
+ evictedLookupMisses = 0;
226
235
  return removed;
227
236
  }
228
237
 
@@ -253,7 +262,18 @@ export function configureResponseHistoryForTests({ maxBytes: nextMaxBytes, maxEn
253
262
  }
254
263
 
255
264
  export function responseHistoryStats() {
265
+ const trees = new Map();
266
+ for (const entry of histories.values()) {
267
+ trees.set(entry.rootId, (trees.get(entry.rootId) || 0) + entry.bytes);
268
+ }
269
+ let largestTreeBytes = 0;
270
+ for (const bytes of trees.values()) largestTreeBytes = Math.max(largestTreeBytes, bytes);
256
271
  return { entries: histories.size, bytes: totalBytes, evicted: evictedIds.size,
272
+ evicted_entries_total: evictedEntriesTotal,
273
+ lookup_misses: lookupMisses,
274
+ evicted_lookup_misses: evictedLookupMisses,
275
+ tree_count: trees.size,
276
+ largest_tree_bytes: largestTreeBytes,
257
277
  pinnedTrees: pinnedTrees.size, maxBytes, maxEntries };
258
278
  }
259
279
 
@@ -270,7 +290,10 @@ function responseHistoryChain(responseId) {
270
290
  seen.add(currentId);
271
291
  const entry = histories.get(currentId);
272
292
  if (!entry) {
273
- const reason = evictedIds.has(currentId) ? " was evicted after reaching the local history limit" : " is not available";
293
+ lookupMisses += 1;
294
+ const wasEvicted = evictedIds.has(currentId);
295
+ if (wasEvicted) evictedLookupMisses += 1;
296
+ const reason = wasEvicted ? " was evicted after reaching the local history limit" : " is not available";
274
297
  throw httpError(`previous_response_id${reason}: ${currentId}`, 400);
275
298
  }
276
299
  chain.push(entry);