codex-copilot-dx 0.9.7 → 0.9.9
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/README.md +4 -0
- package/package.json +1 -1
- package/src/adapter.mjs +1 -1
- package/src/dashboard/index.html +45 -44
- package/src/dashboard/language.js +216 -0
- package/src/dashboard/theme.js +7 -2
- package/src/dashboard/ui.css +2 -1
- package/src/dashboard/ui.js +69 -42
- package/src/dashboard-api.mjs +1 -1
- package/src/dashboard.mjs +1 -0
package/README.md
CHANGED
|
@@ -238,6 +238,10 @@ The transparent, single-color double-peak favicon follows the dashboard's saved
|
|
|
238
238
|
|
|
239
239
|
The saved GitHub Copilot account appears immediately to the left of the Snapshot time. It uses the same local credentials and fingerprint-bound identity metadata as `ccdx auth status`, with username (or numeric ID fallback) and a `SAVED` label; this is not an online entitlement check. Missing, invalid, unidentified, or unreadable credentials are shown explicitly. The account read runs once on page load and on the top Refresh action, independently of adapter status, with no polling or upstream authentication calls. Its loopback-only, custom-header-protected API exposes no token, fingerprint, credential path, or raw metadata. The general status endpoint retains its existing identity-free contract.
|
|
240
240
|
|
|
241
|
+
Account names containing underscores are supported, including `dingxiao_microsoft`. A numeric ID is a fallback, not a replacement for an available, safely displayable username; the existing length bounds and credential-shaped text suppression still apply.
|
|
242
|
+
|
|
243
|
+
The language button immediately right of the appearance button switches between English and Simplified Chinese. English remains the default; the choice is saved only in the current browser, separately from light/dark mode. Switching translates the existing page in place: it does not fetch API data, rebuild charts, change selected filters, discard unsaved animation choices, or reset expanded failure details. Static labels, live status text, chart tooltips and accessibility labels follow the selected language. Model IDs, account names, dates, numeric values and upstream diagnostic prose stay unchanged; copied diagnostic fields retain their original names and values. The local translation asset adds no dependency, external service, polling or inference-path work.
|
|
244
|
+
|
|
241
245
|
Usage Analytics is collapsed by default. Opening it adds daily input/output token bars (7/30/90 days) and a 365-day calendar of recorded usage calls or tokens, grouped in the browser's time zone. Cache-read tokens are already part of input and are not added again. Model and range filters use the current snapshot without additional requests; selecting a day shows its filtered model totals in the Usage table, and All history restores the original table. Analytics shares the existing current/rotated-log scan and exposes only bounded metadata (365 days, up to 100 model filters). Days before retained history are hatched, not presented as zero; the oldest retained day may be partial. Counts describe retained usage records, not messages, session time, or all HTTP requests. Refresh explicitly to update the snapshot; there is no background collection or polling.
|
|
242
246
|
|
|
243
247
|
Terminal Animation is collapsed at the bottom of the page, with an Animation shortcut in the header. Preview frames load only when opened; closing it, hiding the page, or requesting reduced motion stops the preview timer. Unsaved choices survive folding and reopening. Recent upstream failures expand to existing diagnostic fields (time, model, code, message, response/request IDs, and retry policy) with a Copy diagnostic control. These are the last 10 retained `response.failed` events, not complete session histories; a retry attempt does not prove recovery. Diagnostic prose is redacted by the existing recorder but can still contain sensitive context: review before copying or sharing.
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -64,7 +64,7 @@ export {
|
|
|
64
64
|
} from "./responses-request.mjs";
|
|
65
65
|
|
|
66
66
|
const ADAPTER_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
67
|
-
const DASHBOARD_PATHS = new Set(["/", "/ui.css", "/theme.js", "/ui.js"]);
|
|
67
|
+
const DASHBOARD_PATHS = new Set(["/", "/ui.css", "/theme.js", "/language.js", "/ui.js"]);
|
|
68
68
|
const DASHBOARD_API_PATHS = new Set(["/_ccdx/ui/animation", "/_ccdx/ui/models/live", "/_ccdx/ui/usage", "/_ccdx/ui/auth"]);
|
|
69
69
|
|
|
70
70
|
export function requestPath(reqUrl) {
|
package/src/dashboard/index.html
CHANGED
|
@@ -5,92 +5,93 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<meta name="color-scheme" content="dark light">
|
|
7
7
|
<link id="favicon" rel="icon" type="image/svg+xml" href="data:,">
|
|
8
|
+
<script src="/language.js"></script>
|
|
8
9
|
<script src="/theme.js"></script>
|
|
9
10
|
<link rel="stylesheet" href="/ui.css">
|
|
10
11
|
<script src="/ui.js" defer></script>
|
|
11
|
-
<title>CCDX · Local dashboard</title>
|
|
12
|
+
<title data-i18n>CCDX · Local dashboard</title>
|
|
12
13
|
</head>
|
|
13
14
|
<body>
|
|
14
15
|
<div class="shell">
|
|
15
16
|
<header class="topbar">
|
|
16
|
-
<div class="brand">CCDX <span class="brand-detail">/ LOCAL STATUS</span></div>
|
|
17
|
-
<div class="topbar-actions"><span id="connection" class="badge neutral" role="status">READING</span><button id="theme-toggle" class="icon-button" type="button" aria-label="Switch to light mode" title="Switch to light mode"><svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.93 4.93l1.42 1.42m11.3 11.3 1.42 1.42M2 12h2m16 0h2M4.93 19.07l1.42-1.42m11.3-11.3 1.42-1.42"/></svg><svg class="icon-moon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 3A9 9 0 1 0 21 12 7 7 0 0 1 12 3Z"/></svg></button><button id="open-animation" type="button" aria-controls="animation-settings" aria-expanded="false">Animation</button><button id="refresh" type="button">Refresh</button></div>
|
|
17
|
+
<div class="brand">CCDX <span class="brand-detail" data-i18n>/ LOCAL STATUS</span></div>
|
|
18
|
+
<div class="topbar-actions"><span id="connection" class="badge neutral" role="status" data-i18n>READING</span><button id="theme-toggle" class="icon-button" type="button" aria-label="Switch to light mode" title="Switch to light mode"><svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.93 4.93l1.42 1.42m11.3 11.3 1.42 1.42M2 12h2m16 0h2M4.93 19.07l1.42-1.42m11.3-11.3 1.42-1.42"/></svg><svg class="icon-moon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 3A9 9 0 1 0 21 12 7 7 0 0 1 12 3Z"/></svg></button><button id="language-toggle" class="icon-button language-button" type="button" aria-label="Switch to Chinese" title="Switch to Chinese">中文</button><button id="open-animation" type="button" aria-controls="animation-settings" aria-expanded="false" data-i18n>Animation</button><button id="refresh" type="button" data-i18n>Refresh</button></div>
|
|
18
19
|
</header>
|
|
19
20
|
|
|
20
21
|
<main>
|
|
21
22
|
<section class="intro">
|
|
22
23
|
<h1><span aria-hidden="true">$</span> ccdx status</h1>
|
|
23
|
-
<div class="snapshot-meta"><p id="auth-account" role="status" title="Saved GitHub Copilot account / local credentials only; not an online entitlement check.">GitHub / READING</p><p id="updated">Reading status…</p></div>
|
|
24
|
+
<div class="snapshot-meta"><p id="auth-account" role="status" data-i18n data-i18n-title title="Saved GitHub Copilot account / local credentials only; not an online entitlement check.">GitHub / READING</p><p id="updated" data-i18n>Reading status…</p></div>
|
|
24
25
|
</section>
|
|
25
26
|
|
|
26
|
-
<section class="overview" aria-label="Overview">
|
|
27
|
-
<article class="card"><h2>ADAPTER</h2><strong id="version">—</strong><p id="uptime">—</p></article>
|
|
27
|
+
<section class="overview" aria-label="Overview" data-i18n-label>
|
|
28
|
+
<article class="card"><h2 data-i18n>ADAPTER</h2><strong id="version">—</strong><p id="uptime">—</p></article>
|
|
28
29
|
<article class="card"><h2>COPILOT</h2><strong id="copilot">—</strong><p id="token">—</p></article>
|
|
29
|
-
<article class="card"><h2>KNOWN MODELS</h2><strong id="models">—</strong><p id="model-source">—</p></article>
|
|
30
|
-
<article class="card"><h2>REQUESTS</h2><strong id="requests">—</strong><p id="request-errors">—</p><p>Per-request limits (active)
|
|
30
|
+
<article class="card"><h2 data-i18n>KNOWN MODELS</h2><strong id="models">—</strong><p id="model-source">—</p></article>
|
|
31
|
+
<article class="card"><h2 data-i18n>REQUESTS</h2><strong id="requests">—</strong><p id="request-errors">—</p><p><span data-i18n>Per-request limits (active)</span><br><span id="body-limits">—</span></p></article>
|
|
31
32
|
</section>
|
|
32
33
|
|
|
33
|
-
<section class="details" aria-label="Runtime details">
|
|
34
|
+
<section class="details" aria-label="Runtime details" data-i18n-label>
|
|
34
35
|
<article class="panel">
|
|
35
|
-
<div class="panel-heading"><h2>MODEL OUTCOMES</h2><span>RESPONSES</span></div>
|
|
36
|
-
<dl class="stats"><div><dt>Completed</dt><dd id="completed">—</dd></div><div><dt>Incomplete</dt><dd id="incomplete">—</dd></div><div><dt>Failed</dt><dd id="failed">—</dd></div><div><dt>Cancelled</dt><dd id="cancelled">—</dd></div></dl>
|
|
37
|
-
<p class="hint">HTTP 2xx != model completed.</p>
|
|
36
|
+
<div class="panel-heading"><h2 data-i18n>MODEL OUTCOMES</h2><span>RESPONSES</span></div>
|
|
37
|
+
<dl class="stats"><div><dt data-i18n>Completed</dt><dd id="completed">—</dd></div><div><dt data-i18n>Incomplete</dt><dd id="incomplete">—</dd></div><div><dt data-i18n>Failed</dt><dd id="failed">—</dd></div><div><dt data-i18n>Cancelled</dt><dd id="cancelled">—</dd></div></dl>
|
|
38
|
+
<p class="hint" data-i18n>HTTP 2xx != model completed.</p>
|
|
38
39
|
</article>
|
|
39
40
|
<article class="panel">
|
|
40
|
-
<div class="panel-heading"><h2>LOCAL HISTORY</h2><span>ACTIVE LIMIT</span></div>
|
|
41
|
-
<strong id="history">—</strong><progress id="history-meter" class="meter" max="100" value="0" aria-label="History cache usage"></progress>
|
|
41
|
+
<div class="panel-heading"><h2 data-i18n>LOCAL HISTORY</h2><span data-i18n>ACTIVE LIMIT</span></div>
|
|
42
|
+
<strong id="history">—</strong><progress id="history-meter" class="meter" max="100" value="0" aria-label="History cache usage" data-i18n-label></progress>
|
|
42
43
|
<p id="history-detail" class="hint">—</p>
|
|
43
44
|
</article>
|
|
44
45
|
<article class="panel">
|
|
45
|
-
<div class="panel-heading"><h2>IMAGE GENERATION</h2><span>OPTIONAL</span></div>
|
|
46
|
+
<div class="panel-heading"><h2 data-i18n>IMAGE GENERATION</h2><span data-i18n>OPTIONAL</span></div>
|
|
46
47
|
<strong id="image">—</strong><p id="image-detail" class="hint">—</p>
|
|
47
48
|
</article>
|
|
48
49
|
</section>
|
|
49
50
|
|
|
50
|
-
<section class="panel dashboard-section" aria-label="Live upstream models">
|
|
51
|
-
<div class="panel-heading"><h2>UPSTREAM GPT MODELS</h2><div class="heading-actions"><span id="models-state">CHECKING LIVE</span><button id="refresh-models" type="button">Refresh</button></div></div>
|
|
52
|
-
<p id="models-note" class="table-note">Querying GitHub Copilot…</p>
|
|
53
|
-
<div class="table-wrap"><table><thead><tr><th scope="col">MODEL</th><th scope="col">VENDOR</th><th scope="col">API</th><th scope="col">FLAG</th></tr></thead><tbody id="live-models-body"><tr><td colspan="4">Loading…</td></tr></tbody></table></div>
|
|
54
|
-
<p class="hint">Catalog listing does not guarantee inference access.</p>
|
|
51
|
+
<section class="panel dashboard-section" aria-label="Live upstream models" data-i18n-label>
|
|
52
|
+
<div class="panel-heading"><h2 data-i18n>UPSTREAM GPT MODELS</h2><div class="heading-actions"><span id="models-state" data-i18n>CHECKING LIVE</span><button id="refresh-models" type="button" data-i18n>Refresh</button></div></div>
|
|
53
|
+
<p id="models-note" class="table-note" data-i18n>Querying GitHub Copilot…</p>
|
|
54
|
+
<div class="table-wrap"><table><thead><tr><th scope="col" data-i18n>MODEL</th><th scope="col" data-i18n>VENDOR</th><th scope="col">API</th><th scope="col" data-i18n>FLAG</th></tr></thead><tbody id="live-models-body"><tr><td colspan="4" data-i18n>Loading…</td></tr></tbody></table></div>
|
|
55
|
+
<p class="hint" data-i18n>Catalog listing does not guarantee inference access.</p>
|
|
55
56
|
</section>
|
|
56
57
|
|
|
57
|
-
<section class="panel dashboard-section" aria-label="Usage by model">
|
|
58
|
-
<div class="panel-heading"><h2>USAGE</h2><div class="heading-actions"><span id="usage-state">READING LOG</span><button id="clear-usage-day" type="button" hidden>All history</button><button id="refresh-usage" type="button">Refresh</button></div></div>
|
|
59
|
-
<p id="usage-note" class="table-note">Aggregating local usage metadata…</p>
|
|
60
|
-
<div class="table-wrap"><table class="usage-table"><thead><tr><th scope="col">MODEL</th><th scope="col">CALLS</th><th scope="col">INPUT</th><th scope="col">CACHE READ</th><th scope="col">OUTPUT</th><th scope="col">TOTAL</th><th scope="col">CACHE HIT</th></tr></thead><tbody id="usage-body"><tr><td colspan="7">Loading…</td></tr></tbody></table></div>
|
|
61
|
-
<p class="hint">Cache hit = recorded cached / input tokens. ~ = incomplete history. — = unavailable or invalid counts.</p>
|
|
58
|
+
<section class="panel dashboard-section" aria-label="Usage by model" data-i18n-label>
|
|
59
|
+
<div class="panel-heading"><h2 data-i18n>USAGE</h2><div class="heading-actions"><span id="usage-state" data-i18n>READING LOG</span><button id="clear-usage-day" type="button" hidden data-i18n>All history</button><button id="refresh-usage" type="button" data-i18n>Refresh</button></div></div>
|
|
60
|
+
<p id="usage-note" class="table-note" data-i18n>Aggregating local usage metadata…</p>
|
|
61
|
+
<div class="table-wrap"><table class="usage-table"><thead><tr><th scope="col" data-i18n>MODEL</th><th scope="col" data-i18n>CALLS</th><th scope="col" data-i18n>INPUT</th><th scope="col" data-i18n>CACHE READ</th><th scope="col" data-i18n>OUTPUT</th><th scope="col" data-i18n>TOTAL</th><th scope="col" data-i18n>CACHE HIT</th></tr></thead><tbody id="usage-body"><tr><td colspan="7" data-i18n>Loading…</td></tr></tbody></table></div>
|
|
62
|
+
<p class="hint" data-i18n>Cache hit = recorded cached / input tokens. ~ = incomplete history. — = unavailable or invalid counts.</p>
|
|
62
63
|
<details id="usage-analytics" class="analytics-section">
|
|
63
|
-
<summary>USAGE ANALYTICS</summary>
|
|
64
|
+
<summary data-i18n>USAGE ANALYTICS</summary>
|
|
64
65
|
<div class="analytics-controls">
|
|
65
|
-
<label>Model <select id="analytics-model"><option value="">All models</option></select></label>
|
|
66
|
-
<label>Daily range <select id="analytics-range"><option value="7">7 days</option><option value="30" selected>30 days</option><option value="90">90 days</option></select></label>
|
|
67
|
-
<label>Activity <select id="analytics-metric"><option value="requests">Calls</option><option value="tokens">Tokens</option></select></label>
|
|
66
|
+
<label><span data-i18n>Model</span> <select id="analytics-model"><option value="" data-i18n>All models</option></select></label>
|
|
67
|
+
<label><span data-i18n>Daily range</span> <select id="analytics-range"><option value="7" data-i18n>7 days</option><option value="30" selected data-i18n>30 days</option><option value="90" data-i18n>90 days</option></select></label>
|
|
68
|
+
<label><span data-i18n>Activity</span> <select id="analytics-metric"><option value="requests" data-i18n>Calls</option><option value="tokens" data-i18n>Tokens</option></select></label>
|
|
68
69
|
</div>
|
|
69
|
-
<p id="analytics-note" class="table-note" role="status">Open to read retained usage history.</p>
|
|
70
|
-
<div class="chart-heading"><h3>DAILY TOKENS</h3><span>INPUT + OUTPUT / CACHE INCLUDED IN INPUT</span></div>
|
|
71
|
-
<div id="daily-bars" class="daily-bars" aria-label="Daily token usage"></div>
|
|
70
|
+
<p id="analytics-note" class="table-note" role="status" data-i18n>Open to read retained usage history.</p>
|
|
71
|
+
<div class="chart-heading"><h3 data-i18n>DAILY TOKENS</h3><span data-i18n>INPUT + OUTPUT / CACHE INCLUDED IN INPUT</span></div>
|
|
72
|
+
<div id="daily-bars" class="daily-bars" aria-label="Daily token usage" data-i18n-label></div>
|
|
72
73
|
<div class="daily-axis"><span id="daily-from"></span><span id="daily-to"></span></div>
|
|
73
|
-
<div class="chart-legend"><span class="legend-input">Input</span><span class="legend-output">Output</span></div>
|
|
74
|
-
<div class="chart-heading"><h3>REQUEST ACTIVITY · 365 DAYS</h3><span id="activity-scale">—</span></div>
|
|
75
|
-
<div class="calendar-scroll"><div class="calendar-grid"><div id="calendar-months" class="calendar-months" aria-hidden="true"></div><div id="activity-calendar" class="activity-calendar" aria-label="Daily recorded request activity"></div></div></div>
|
|
76
|
-
<p id="analytics-selection" class="hint" role="status">Select a day to inspect its model totals. No history is distinct from zero recorded calls.</p>
|
|
74
|
+
<div class="chart-legend"><span class="legend-input" data-i18n>Input</span><span class="legend-output" data-i18n>Output</span></div>
|
|
75
|
+
<div class="chart-heading"><h3 data-i18n>REQUEST ACTIVITY · 365 DAYS</h3><span id="activity-scale">—</span></div>
|
|
76
|
+
<div class="calendar-scroll"><div class="calendar-grid"><div id="calendar-months" class="calendar-months" aria-hidden="true"></div><div id="activity-calendar" class="activity-calendar" aria-label="Daily recorded request activity" data-i18n-label></div></div></div>
|
|
77
|
+
<p id="analytics-selection" class="hint" role="status" data-i18n>Select a day to inspect its model totals. No history is distinct from zero recorded calls.</p>
|
|
77
78
|
</details>
|
|
78
79
|
</section>
|
|
79
80
|
|
|
80
|
-
<section class="lower" aria-label="Recent failures">
|
|
81
|
-
<div class="panel-heading"><h2>RECENT UPSTREAM FAILURES</h2><span id="failure-count">response.failed</span></div>
|
|
82
|
-
<ol id="failures"><li class="empty">Reading…</li></ol>
|
|
83
|
-
<p class="hint">Upstream messages may contain sensitive text.</p>
|
|
81
|
+
<section class="lower" aria-label="Recent failures" data-i18n-label>
|
|
82
|
+
<div class="panel-heading"><h2 data-i18n>RECENT UPSTREAM FAILURES</h2><span id="failure-count">response.failed</span></div>
|
|
83
|
+
<ol id="failures"><li class="empty" data-i18n>Reading…</li></ol>
|
|
84
|
+
<p class="hint" data-i18n>Upstream messages may contain sensitive text.</p>
|
|
84
85
|
</section>
|
|
85
86
|
|
|
86
87
|
<details id="animation-settings" class="panel dashboard-section settings-panel">
|
|
87
|
-
<summary class="panel-heading"><h2>TERMINAL ANIMATION</h2><span id="animation-state">READING</span></summary>
|
|
88
|
-
<div id="animation-options" class="animation-options" role="group" aria-label="Animation theme"></div>
|
|
89
|
-
<div class="section-actions"><button id="save-animation" type="button" disabled>Save selection</button><span id="animation-note" role="status">Applies on next ccdx start.</span></div>
|
|
88
|
+
<summary class="panel-heading"><h2 data-i18n>TERMINAL ANIMATION</h2><span id="animation-state" data-i18n>READING</span></summary>
|
|
89
|
+
<div id="animation-options" class="animation-options" role="group" aria-label="Animation theme" data-i18n-label></div>
|
|
90
|
+
<div class="section-actions"><button id="save-animation" type="button" disabled data-i18n>Save selection</button><span id="animation-note" role="status" data-i18n>Applies on next ccdx start.</span></div>
|
|
90
91
|
</details>
|
|
91
92
|
</main>
|
|
92
93
|
|
|
93
|
-
<footer>LOCAL ONLY / NO BACKGROUND POLLING</footer>
|
|
94
|
+
<footer data-i18n>LOCAL ONLY / NO BACKGROUND POLLING</footer>
|
|
94
95
|
</div>
|
|
95
96
|
</body>
|
|
96
97
|
</html>
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const translations = {
|
|
3
|
+
"CCDX · Local dashboard": "CCDX · 本地看板",
|
|
4
|
+
"/ LOCAL STATUS": "/ 本地状态",
|
|
5
|
+
"READING": "读取中",
|
|
6
|
+
"LOCAL OK": "本机正常",
|
|
7
|
+
"UNAVAILABLE": "不可用",
|
|
8
|
+
"Refresh": "刷新",
|
|
9
|
+
"Animation": "动效",
|
|
10
|
+
"Switch to light mode": "切换为日间模式",
|
|
11
|
+
"Switch to dark mode": "切换为夜间模式",
|
|
12
|
+
"Overview": "概览",
|
|
13
|
+
"Runtime details": "运行详情",
|
|
14
|
+
"ADAPTER": "适配服务",
|
|
15
|
+
"KNOWN MODELS": "已知模型",
|
|
16
|
+
"REQUESTS": "请求",
|
|
17
|
+
"Per-request limits (active)": "单次请求上限(当前)",
|
|
18
|
+
"MODEL OUTCOMES": "模型终态",
|
|
19
|
+
"Completed": "已完成",
|
|
20
|
+
"Incomplete": "未完成",
|
|
21
|
+
"Failed": "失败",
|
|
22
|
+
"Cancelled": "已取消",
|
|
23
|
+
"HTTP 2xx != model completed.": "HTTP 2xx 不代表模型已完成。",
|
|
24
|
+
"LOCAL HISTORY": "本机历史缓存",
|
|
25
|
+
"ACTIVE LIMIT": "当前上限",
|
|
26
|
+
"History cache usage": "历史缓存用量",
|
|
27
|
+
"IMAGE GENERATION": "图片生成",
|
|
28
|
+
"OPTIONAL": "可选功能",
|
|
29
|
+
"Live upstream models": "上游在线模型",
|
|
30
|
+
"UPSTREAM GPT MODELS": "上游 GPT 模型",
|
|
31
|
+
"CHECKING LIVE": "在线查询中",
|
|
32
|
+
"Querying GitHub Copilot…": "正在查询 GitHub Copilot…",
|
|
33
|
+
"MODEL": "模型",
|
|
34
|
+
"VENDOR": "提供方",
|
|
35
|
+
"FLAG": "标记",
|
|
36
|
+
"Loading…": "加载中…",
|
|
37
|
+
"Catalog listing does not guarantee inference access.": "模型出现在目录中,不代表当前账号可调用它。",
|
|
38
|
+
"Usage by model": "模型用量",
|
|
39
|
+
"USAGE": "用量",
|
|
40
|
+
"READING LOG": "读取日志中",
|
|
41
|
+
"All history": "全部历史",
|
|
42
|
+
"Aggregating local usage metadata…": "正在汇总本机用量记录…",
|
|
43
|
+
"CALLS": "调用次数",
|
|
44
|
+
"INPUT": "输入",
|
|
45
|
+
"CACHE READ": "缓存读取",
|
|
46
|
+
"OUTPUT": "输出",
|
|
47
|
+
"TOTAL": "合计",
|
|
48
|
+
"CACHE HIT": "缓存命中率",
|
|
49
|
+
"Cache hit = recorded cached / input tokens. ~ = incomplete history. — = unavailable or invalid counts.": "命中率 = 已记录的缓存输入 / 输入 Token。~ 表示历史不完整;— 表示数据不可用或无效。",
|
|
50
|
+
"USAGE ANALYTICS": "用量分析",
|
|
51
|
+
"Model": "模型",
|
|
52
|
+
"All models": "全部模型",
|
|
53
|
+
"Daily range": "统计范围",
|
|
54
|
+
"7 days": "7 天",
|
|
55
|
+
"30 days": "30 天",
|
|
56
|
+
"90 days": "90 天",
|
|
57
|
+
"Activity": "活跃指标",
|
|
58
|
+
"Calls": "调用次数",
|
|
59
|
+
"Tokens": "Token 数",
|
|
60
|
+
"Open to read retained usage history.": "展开后读取已保留的用量历史。",
|
|
61
|
+
"DAILY TOKENS": "每日 Token 用量",
|
|
62
|
+
"INPUT + OUTPUT / CACHE INCLUDED IN INPUT": "输入 + 输出 / 缓存已包含在输入中",
|
|
63
|
+
"Daily token usage": "每日 Token 用量",
|
|
64
|
+
"Input": "输入",
|
|
65
|
+
"Output": "输出",
|
|
66
|
+
"REQUEST ACTIVITY · 365 DAYS": "调用热力图 · 365 天",
|
|
67
|
+
"Daily recorded request activity": "每日已记录的调用次数",
|
|
68
|
+
"Select a day to inspect its model totals. No history is distinct from zero recorded calls.": "点击日期查看模型用量。无留存历史不等于零次调用。",
|
|
69
|
+
"Recent failures": "近期失败",
|
|
70
|
+
"RECENT UPSTREAM FAILURES": "近期上游失败",
|
|
71
|
+
"Reading…": "读取中…",
|
|
72
|
+
"Reading status…": "正在读取状态…",
|
|
73
|
+
"Upstream messages may contain sensitive text.": "上游错误信息可能含敏感内容。",
|
|
74
|
+
"TERMINAL ANIMATION": "终端动效",
|
|
75
|
+
"Animation theme": "动效主题",
|
|
76
|
+
"Save selection": "保存选择",
|
|
77
|
+
"Applies on next ccdx start.": "下次启动 ccdx 时生效。",
|
|
78
|
+
"LOCAL ONLY / NO BACKGROUND POLLING": "仅限本机 / 无后台轮询",
|
|
79
|
+
"Saved GitHub Copilot account / local credentials only; not an online entitlement check.": "已保存的 GitHub Copilot 账号;仅读取本机凭据,不代表在线权限验证。",
|
|
80
|
+
"GitHub / READING": "GitHub / 读取中",
|
|
81
|
+
"GitHub / UNAVAILABLE": "GitHub / 不可用",
|
|
82
|
+
"GitHub / NOT CONFIGURED": "GitHub / 未配置",
|
|
83
|
+
"GitHub / INVALID": "GitHub / 无效",
|
|
84
|
+
"GitHub / {account} / SAVED": "GitHub / {account} / 已保存",
|
|
85
|
+
"account unknown": "账号未知",
|
|
86
|
+
"pid {pid} / uptime {duration}": "PID {pid} / 运行时间 {duration}",
|
|
87
|
+
"BOUND": "已绑定",
|
|
88
|
+
"UNCONFIRMED": "未确认",
|
|
89
|
+
"service token / TTL {duration}": "服务令牌 / 剩余有效期 {duration}",
|
|
90
|
+
"service token not cached": "服务令牌未缓存",
|
|
91
|
+
"source {source} / live: ccdx models": "来源 {source} / 在线目录:ccdx models",
|
|
92
|
+
"built-in": "内置",
|
|
93
|
+
"cache": "缓存",
|
|
94
|
+
"live": "在线",
|
|
95
|
+
"unknown": "未知",
|
|
96
|
+
"4xx {client} / 5xx {server} / active {active}": "4xx {client} / 5xx {server} / 进行中 {active}",
|
|
97
|
+
"raw {raw} / decoded {decoded}": "原始 {raw} / 解码后 {decoded}",
|
|
98
|
+
"entries {entries} / trees {trees} / misses {misses} (evicted {evicted})": "记录 {entries} / 历史树 {trees} / 未命中 {misses}(已淘汰 {evicted})",
|
|
99
|
+
"{count} succeeded": "成功 {count} 次",
|
|
100
|
+
"NOT INITIALIZED": "未初始化",
|
|
101
|
+
"active {active} / failed {failed} / delivery failures {delivery}": "进行中 {active} / 失败 {failed} / 交付失败 {delivery}",
|
|
102
|
+
"Setup state unknown / ccdx image-status": "配置状态未知 / ccdx image-status",
|
|
103
|
+
"Snapshot {time}": "快照 {time}",
|
|
104
|
+
"Status unavailable / check ccdx, then refresh": "状态不可用 / 请检查 ccdx 后刷新",
|
|
105
|
+
"{count} retained / response.failed": "保留 {count} 条 / response.failed",
|
|
106
|
+
"No response.failed events recorded.": "未记录 response.failed 事件。",
|
|
107
|
+
"{detail} / {retry}": "{detail} / {retry}",
|
|
108
|
+
"retry attempted": "已尝试重试",
|
|
109
|
+
"no retry": "未重试",
|
|
110
|
+
"time": "时间",
|
|
111
|
+
"model": "模型",
|
|
112
|
+
"event": "事件",
|
|
113
|
+
"code": "错误码",
|
|
114
|
+
"message": "错误信息",
|
|
115
|
+
"response_id": "响应 ID",
|
|
116
|
+
"upstream_request_id": "上游请求 ID",
|
|
117
|
+
"retry": "重试",
|
|
118
|
+
"retry_policy": "重试策略",
|
|
119
|
+
"retry_skipped": "跳过重试原因",
|
|
120
|
+
"attempted (outcome not recorded here)": "已尝试(此处未记录结果)",
|
|
121
|
+
"not attempted": "未尝试",
|
|
122
|
+
"Copy diagnostic": "复制诊断信息",
|
|
123
|
+
"Copied": "已复制",
|
|
124
|
+
"Copy unavailable": "无法复制",
|
|
125
|
+
"DEFAULT": "默认",
|
|
126
|
+
"Disabled by CCDX_TERMINAL_ANIMATION; saved choice applies after override removal and next start.": "CCDX_TERMINAL_ANIMATION 已禁用动效;移除覆盖设置后,保存的选择在下次启动时生效。",
|
|
127
|
+
"Saved choice applies on next ccdx start.": "保存的选择在下次启动 ccdx 时生效。",
|
|
128
|
+
"CURRENT {theme}{unsaved}": "当前 {theme}{unsaved}",
|
|
129
|
+
" / UNSAVED": " / 未保存",
|
|
130
|
+
"Saving…": "保存中…",
|
|
131
|
+
"Not saved / {error}": "未保存 / {error}",
|
|
132
|
+
"preview": "预览版",
|
|
133
|
+
"No selectable GPT models advertised.": "上游未提供可选的 GPT 模型。",
|
|
134
|
+
"LIVE SNAPSHOT": "在线快照",
|
|
135
|
+
"{selectable} selectable / {advertised} advertised / {host} / {time}": "可选 {selectable} / 上游提供 {advertised} / {host} / {time}",
|
|
136
|
+
"LIVE LOOKUP FAILED": "在线查询失败",
|
|
137
|
+
"No live catalog available.": "在线模型目录不可用。",
|
|
138
|
+
"No usage records.": "暂无用量记录。",
|
|
139
|
+
"LOCAL LOG": "本机日志",
|
|
140
|
+
"{records} records / {shown} of {models} models shown{day}": "记录 {records} 条 / 展示 {shown} 个模型,共 {models} 个{day}",
|
|
141
|
+
" / {zone} / {model}": " / {zone} / {model}",
|
|
142
|
+
"no retained history": "无留存历史",
|
|
143
|
+
"{calls} recorded calls / input {input} / output {output} / cached {cached} (included in input){partial}": "记录 {calls} 次调用 / 输入 {input} / 输出 {output} / 缓存 {cached}(已包含在输入中){partial}",
|
|
144
|
+
" / partial token counts": " / Token 数据不完整",
|
|
145
|
+
"{date} / {description}": "{date} / {description}",
|
|
146
|
+
"{metric} / PEAK {peak}": "{metric} / 峰值 {peak}",
|
|
147
|
+
"INPUT + OUTPUT": "输入 + 输出",
|
|
148
|
+
"RECORDED CALLS": "已记录调用",
|
|
149
|
+
"{zone} / {from} — {to} / retained history starts {first}; oldest day may be partial.{excluded}{limit}": "{zone} / {from} — {to} / 留存历史始于 {first};最早一天的数据可能不完整。{excluded}{limit}",
|
|
150
|
+
" {count} records with invalid/future timestamps excluded.": " 已排除 {count} 条时间无效或位于未来的记录。",
|
|
151
|
+
" Model filters limited to 100; all-model totals include the remainder.": " 模型筛选最多展示 100 个模型;全部模型合计包含其余模型。",
|
|
152
|
+
"Selected {date} / {model}: totals in the Usage table above / All history resets the table.": "已选 {date} / {model}:用量见上方表格 / 点击“全部历史”恢复总表。",
|
|
153
|
+
"Select a day for model totals. Calls are usage records, not messages or time. Hatched cells: no retained history; empty cells: zero recorded calls.": "点击日期查看模型用量。调用次数来自用量记录,不代表消息数或时长。斜线格表示无留存历史;空白格表示零次已记录调用。",
|
|
154
|
+
"LOG UNAVAILABLE": "日志不可用",
|
|
155
|
+
"No usage summary available.": "用量汇总不可用。",
|
|
156
|
+
"Analytics unavailable / refresh Usage to retry.": "分析不可用 / 请刷新用量后重试。",
|
|
157
|
+
"Animation settings are invalid; inspect them with ccdx animation": "动效设置无效;请运行 ccdx animation 检查。",
|
|
158
|
+
"Animation setting was not saved; check ccdx animation": "动效设置未保存;请运行 ccdx animation 检查。",
|
|
159
|
+
"Could not read local usage summary": "无法读取本机用量汇总。",
|
|
160
|
+
"Live model lookup timed out. Retry or run ccdx models.": "在线模型查询超时;请重试或运行 ccdx models。",
|
|
161
|
+
"GitHub token not found. Start ccdx to sign in.": "未找到 GitHub 令牌;请启动 ccdx 登录。",
|
|
162
|
+
"Live model lookup failed. Run ccdx models for details.": "在线模型查询失败;请运行 ccdx models 查看详情。",
|
|
163
|
+
};
|
|
164
|
+
const storageKey = "ccdx.dashboard.language";
|
|
165
|
+
let language = "en";
|
|
166
|
+
try { if (localStorage.getItem(storageKey) === "zh") language = "zh"; } catch {}
|
|
167
|
+
document.documentElement.lang = language === "zh" ? "zh-CN" : "en";
|
|
168
|
+
const messages = new WeakMap();
|
|
169
|
+
const attributes = { textContent: "data-i18n", title: "data-i18n-title", "aria-label": "data-i18n-label" };
|
|
170
|
+
|
|
171
|
+
function format(key, values = {}) {
|
|
172
|
+
const template = language === "zh" ? translations[key] || key : key;
|
|
173
|
+
return String(template).replace(/\{(\w+)\}/g, (match, name) => {
|
|
174
|
+
if (!Object.hasOwn(values, name)) return match;
|
|
175
|
+
const value = values[name];
|
|
176
|
+
return value && typeof value === "object" && typeof value.key === "string"
|
|
177
|
+
? format(value.key, value.values) : String(value ?? "");
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function set(target, key, values = {}, attribute = "textContent") {
|
|
182
|
+
const saved = messages.get(target) || {};
|
|
183
|
+
saved[attribute] = { key, values };
|
|
184
|
+
messages.set(target, saved);
|
|
185
|
+
target.setAttribute(attributes[attribute], "");
|
|
186
|
+
const value = format(key, values);
|
|
187
|
+
if (attribute === "textContent") target.textContent = value;
|
|
188
|
+
else target.setAttribute(attribute, value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function apply() {
|
|
192
|
+
document.documentElement.lang = language === "zh" ? "zh-CN" : "en";
|
|
193
|
+
for (const target of document.querySelectorAll("[data-i18n], [data-i18n-title], [data-i18n-label]")) {
|
|
194
|
+
for (const [attribute, marker] of Object.entries(attributes)) {
|
|
195
|
+
if (!target.hasAttribute(marker)) continue;
|
|
196
|
+
const saved = messages.get(target)?.[attribute];
|
|
197
|
+
set(target, saved?.key ?? (attribute === "textContent" ? target.textContent : target.getAttribute(attribute)), saved?.values, attribute);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const button = document.getElementById("language-toggle");
|
|
201
|
+
button.textContent = language === "en" ? "中文" : "EN";
|
|
202
|
+
const label = language === "en" ? "Switch to Chinese" : "切换为英文";
|
|
203
|
+
button.setAttribute("aria-label", label);
|
|
204
|
+
button.title = label;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
globalThis.ccdxLanguage = { set, format };
|
|
208
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
209
|
+
apply();
|
|
210
|
+
document.getElementById("language-toggle").addEventListener("click", () => {
|
|
211
|
+
language = language === "en" ? "zh" : "en";
|
|
212
|
+
try { localStorage.setItem(storageKey, language); } catch {}
|
|
213
|
+
apply();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
})();
|
package/src/dashboard/theme.js
CHANGED
|
@@ -16,8 +16,13 @@
|
|
|
16
16
|
const button = document.getElementById("theme-toggle");
|
|
17
17
|
const updateLabel = () => {
|
|
18
18
|
const label = `Switch to ${theme === "dark" ? "light" : "dark"} mode`;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
if (globalThis.ccdxLanguage) {
|
|
20
|
+
globalThis.ccdxLanguage.set(button, label, {}, "aria-label");
|
|
21
|
+
globalThis.ccdxLanguage.set(button, label, {}, "title");
|
|
22
|
+
} else {
|
|
23
|
+
button.setAttribute("aria-label", label);
|
|
24
|
+
button.title = label;
|
|
25
|
+
}
|
|
21
26
|
};
|
|
22
27
|
updateLabel();
|
|
23
28
|
button.addEventListener("click", () => {
|
package/src/dashboard/ui.css
CHANGED
|
@@ -7,7 +7,7 @@ body { margin: 0; color: var(--text); background: var(--page); font-size: 13px;
|
|
|
7
7
|
.topbar { min-height: 68px; display: flex; justify-content: space-between; align-items: center; gap: 20px; border-bottom: 1px solid var(--top-border); }
|
|
8
8
|
.brand { color: var(--accent); font-size: 16px; font-weight: 750; letter-spacing: .07em; white-space: nowrap; }
|
|
9
9
|
.brand-detail { color: var(--subtle); font-size: 11px; font-weight: 500; letter-spacing: .03em; }
|
|
10
|
-
.topbar-actions { display: flex; align-items: center; gap: 20px; }
|
|
10
|
+
.topbar-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 20px; }
|
|
11
11
|
.badge { color: var(--badge); white-space: nowrap; font-size: 11px; font-weight: 650; text-transform: uppercase; }
|
|
12
12
|
.badge::before { content: "●"; margin-right: 8px; }
|
|
13
13
|
.badge.good { color: var(--accent-soft); }
|
|
@@ -19,6 +19,7 @@ button:disabled { opacity: .55; cursor: wait; }
|
|
|
19
19
|
.icon-button { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 6px; border: 0; background: transparent; }
|
|
20
20
|
.icon-button:hover { background: transparent; color: var(--accent); }
|
|
21
21
|
.icon-button svg { width: 18px; height: 18px; }
|
|
22
|
+
.language-button { padding: 4px; white-space: nowrap; }
|
|
22
23
|
.icon-moon, :root[data-theme="light"] .icon-sun { display: none; }
|
|
23
24
|
:root[data-theme="light"] .icon-moon { display: block; }
|
|
24
25
|
.intro { display: flex; justify-content: space-between; align-items: baseline; gap: 20px; padding: 38px 0 25px; }
|
package/src/dashboard/ui.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
const element = (id) => document.getElementById(id);
|
|
2
|
-
const
|
|
2
|
+
const phrase = (key, values = {}) => ({ key, values });
|
|
3
|
+
const english = (key, values = {}) => String(key).replace(/\{(\w+)\}/g, (match, name) => {
|
|
4
|
+
if (!Object.hasOwn(values, name)) return match;
|
|
5
|
+
const value = values[name];
|
|
6
|
+
return value && typeof value === "object" && typeof value.key === "string" ? english(value.key, value.values) : String(value ?? "");
|
|
7
|
+
});
|
|
8
|
+
const localize = (target, key, values = {}, attribute = "textContent") => {
|
|
9
|
+
if (globalThis.ccdxLanguage) return globalThis.ccdxLanguage.set(target, key, values, attribute);
|
|
10
|
+
const value = english(key, values);
|
|
11
|
+
if (attribute === "textContent") target.textContent = value;
|
|
12
|
+
else target.setAttribute(attribute, value);
|
|
13
|
+
};
|
|
14
|
+
const text = (id, key, values) => { localize(element(id), key, values); };
|
|
3
15
|
const finite = (value) => value !== null && value !== undefined && Number.isFinite(Number(value));
|
|
4
16
|
const number = (value) => finite(value) ? Math.max(0, Math.round(Number(value))).toLocaleString() : "—";
|
|
5
17
|
const mib = (value) => finite(value) ? `${(Number(value) / 1048576).toFixed(1)} MiB` : "—";
|
|
6
18
|
let authLoading = false;
|
|
7
19
|
|
|
8
20
|
function renderAuth(data) {
|
|
9
|
-
const account = data.login ? `@${data.login}` : data.id ? `ID ${data.id}` : "account unknown";
|
|
21
|
+
const account = data.login ? `@${data.login}` : data.id ? `ID ${data.id}` : phrase("account unknown");
|
|
10
22
|
const label = data.reason === "credential_read_failed" ? "UNAVAILABLE"
|
|
11
|
-
: !data.configured ? "NOT CONFIGURED" : !data.valid ? "INVALID" :
|
|
12
|
-
text("auth-account", `GitHub / ${label}
|
|
23
|
+
: !data.configured ? "NOT CONFIGURED" : !data.valid ? "INVALID" : "SAVED";
|
|
24
|
+
text("auth-account", label === "SAVED" ? "GitHub / {account} / SAVED" : `GitHub / ${label}`, { account });
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
async function loadAuth() {
|
|
@@ -40,11 +52,11 @@ function renderFailures(recent) {
|
|
|
40
52
|
const expanded = new Set([...list.querySelectorAll("details[open]")].map((item) => item.dataset.key));
|
|
41
53
|
list.replaceChildren();
|
|
42
54
|
const failures = Array.isArray(recent) ? recent.slice(-10).reverse() : [];
|
|
43
|
-
text("failure-count",
|
|
55
|
+
text("failure-count", "{count} retained / response.failed", { count: failures.length });
|
|
44
56
|
if (!failures.length) {
|
|
45
57
|
const item = document.createElement("li");
|
|
46
58
|
item.className = "empty";
|
|
47
|
-
item
|
|
59
|
+
localize(item, "No response.failed events recorded.");
|
|
48
60
|
list.append(item);
|
|
49
61
|
return;
|
|
50
62
|
}
|
|
@@ -54,7 +66,10 @@ function renderFailures(recent) {
|
|
|
54
66
|
details.dataset.key = `${failure.at}/${failure.response_id}/${failure.code}`;
|
|
55
67
|
details.open = expanded.has(details.dataset.key);
|
|
56
68
|
const title = document.createElement("summary");
|
|
57
|
-
title
|
|
69
|
+
localize(title, "{detail} / {retry}", {
|
|
70
|
+
detail: `${String(failure.at || "unknown time")} / ${String(failure.model || "unknown_model").slice(0, 80)} / ${String(failure.code || "unknown_error").slice(0, 80)}`,
|
|
71
|
+
retry: phrase(failure.retried ? "retry attempted" : "no retry"),
|
|
72
|
+
});
|
|
58
73
|
const fields = { time: failure.at, model: failure.model, event: failure.event_type, code: failure.code,
|
|
59
74
|
message: failure.message, response_id: failure.response_id, upstream_request_id: failure.upstream_request_id,
|
|
60
75
|
retry: failure.retried ? "attempted (outcome not recorded here)" : "not attempted",
|
|
@@ -66,19 +81,21 @@ function renderFailures(recent) {
|
|
|
66
81
|
if (value === undefined || value === null || value === "") continue;
|
|
67
82
|
const label = document.createElement("dt");
|
|
68
83
|
const content = document.createElement("dd");
|
|
69
|
-
label
|
|
70
|
-
|
|
84
|
+
localize(label, key);
|
|
85
|
+
const rawValue = String(value).slice(0, 500);
|
|
86
|
+
if (key === "retry") localize(content, rawValue);
|
|
87
|
+
else content.textContent = rawValue;
|
|
71
88
|
diagnostic.append(label, content);
|
|
72
|
-
lines.push(`${key}: ${
|
|
89
|
+
lines.push(`${key}: ${rawValue}`);
|
|
73
90
|
}
|
|
74
91
|
const copy = document.createElement("button");
|
|
75
92
|
copy.type = "button";
|
|
76
|
-
copy
|
|
93
|
+
localize(copy, "Copy diagnostic");
|
|
77
94
|
copy.addEventListener("click", async () => {
|
|
78
95
|
try {
|
|
79
96
|
await navigator.clipboard.writeText(lines.join("\n"));
|
|
80
|
-
copy
|
|
81
|
-
} catch { copy
|
|
97
|
+
localize(copy, "Copied");
|
|
98
|
+
} catch { localize(copy, "Copy unavailable"); }
|
|
82
99
|
});
|
|
83
100
|
details.append(title, diagnostic, copy);
|
|
84
101
|
item.append(details);
|
|
@@ -88,20 +105,20 @@ function renderFailures(recent) {
|
|
|
88
105
|
|
|
89
106
|
function render(data) {
|
|
90
107
|
text("version", `v${data.version || "?"}`);
|
|
91
|
-
text("uptime",
|
|
108
|
+
text("uptime", "pid {pid} / uptime {duration}", { pid: number(data.pid), duration: duration(data.uptime_ms) });
|
|
92
109
|
|
|
93
110
|
const copilot = data.copilot || {};
|
|
94
111
|
text("copilot", copilot.account_bound ? "BOUND" : "UNCONFIRMED");
|
|
95
|
-
text("token", copilot.token_cached ?
|
|
112
|
+
text("token", copilot.token_cached ? "service token / TTL {duration}" : "service token not cached", { duration: duration(copilot.token_expires_in_ms) });
|
|
96
113
|
|
|
97
114
|
const models = data.models || {};
|
|
98
115
|
text("models", number(models.models));
|
|
99
|
-
text("model-source",
|
|
116
|
+
text("model-source", "source {source} / live: ccdx models", { source: phrase(String(models.source || "unknown").slice(0, 80)) });
|
|
100
117
|
|
|
101
118
|
const requests = data.requests || {};
|
|
102
119
|
text("requests", number(requests.total));
|
|
103
|
-
text("request-errors",
|
|
104
|
-
text("body-limits",
|
|
120
|
+
text("request-errors", "4xx {client} / 5xx {server} / active {active}", { client: number(requests.status_4xx), server: number(requests.status_5xx), active: number(requests.active) });
|
|
121
|
+
text("body-limits", "raw {raw} / decoded {decoded}", { raw: mib(data.limits?.max_body_bytes), decoded: mib(data.limits?.max_decoded_body_bytes) });
|
|
105
122
|
|
|
106
123
|
const outcomes = data.stream_performance?.by_route?.responses?.terminal_outcomes?.totals || {};
|
|
107
124
|
for (const key of ["completed", "incomplete", "failed", "cancelled"]) text(key, number(outcomes[key]));
|
|
@@ -110,17 +127,17 @@ function render(data) {
|
|
|
110
127
|
const limit = Number(history.maxBytes || data.limits?.response_history_max_bytes || 0);
|
|
111
128
|
const used = Number(history.bytes || 0);
|
|
112
129
|
text("history", `${mib(used)} / ${mib(limit)}`);
|
|
113
|
-
text("history-detail",
|
|
130
|
+
text("history-detail", "entries {entries} / trees {trees} / misses {misses} (evicted {evicted})", { entries: number(history.entries), trees: number(history.tree_count), misses: number(history.lookup_misses), evicted: number(history.evicted_lookup_misses) });
|
|
114
131
|
element("history-meter").value = limit > 0 ? Math.min(100, Math.max(0, used / limit * 100)) : 0;
|
|
115
132
|
|
|
116
133
|
const image = data.image_generation;
|
|
117
|
-
text("image", image ?
|
|
134
|
+
text("image", image ? "{count} succeeded" : "NOT INITIALIZED", { count: number(image?.succeeded) });
|
|
118
135
|
text("image-detail", image
|
|
119
|
-
?
|
|
120
|
-
: "Setup state unknown / ccdx image-status");
|
|
136
|
+
? "active {active} / failed {failed} / delivery failures {delivery}"
|
|
137
|
+
: "Setup state unknown / ccdx image-status", { active: number(image?.active), failed: number(image?.failed), delivery: number(image?.delivery_failures) });
|
|
121
138
|
|
|
122
139
|
renderFailures(data.response_failures?.recent);
|
|
123
|
-
text("updated",
|
|
140
|
+
text("updated", "Snapshot {time}", { time: new Date().toLocaleTimeString() });
|
|
124
141
|
}
|
|
125
142
|
|
|
126
143
|
async function refresh() {
|
|
@@ -248,7 +265,7 @@ function buildAnimationOptions(data) {
|
|
|
248
265
|
name.textContent = `${String(index + 1).padStart(2, "0")} ${theme.label}`;
|
|
249
266
|
if (theme.default) {
|
|
250
267
|
const marker = document.createElement("small");
|
|
251
|
-
marker
|
|
268
|
+
localize(marker, "DEFAULT");
|
|
252
269
|
name.append(marker);
|
|
253
270
|
}
|
|
254
271
|
const preview = document.createElement("span");
|
|
@@ -299,7 +316,7 @@ async function loadAnimation() {
|
|
|
299
316
|
}
|
|
300
317
|
|
|
301
318
|
function updateAnimationState() {
|
|
302
|
-
text("animation-state",
|
|
319
|
+
text("animation-state", "CURRENT {theme}{unsaved}", { theme: savedAnimation.toUpperCase(), unsaved: selectedAnimation !== savedAnimation ? phrase(" / UNSAVED") : "" });
|
|
303
320
|
}
|
|
304
321
|
|
|
305
322
|
async function saveAnimation() {
|
|
@@ -320,7 +337,7 @@ async function saveAnimation() {
|
|
|
320
337
|
updateAnimationState();
|
|
321
338
|
text("animation-note", animationNote(data));
|
|
322
339
|
} catch (error) {
|
|
323
|
-
text("animation-note",
|
|
340
|
+
text("animation-note", "Not saved / {error}", { error: String(error.message || error).slice(0, 150) });
|
|
324
341
|
} finally {
|
|
325
342
|
button.disabled = selectedAnimation === savedAnimation;
|
|
326
343
|
}
|
|
@@ -332,17 +349,18 @@ function emptyTable(bodyId, columns, message) {
|
|
|
332
349
|
const row = document.createElement("tr");
|
|
333
350
|
const cell = document.createElement("td");
|
|
334
351
|
cell.colSpan = columns;
|
|
335
|
-
cell
|
|
352
|
+
localize(cell, message);
|
|
336
353
|
row.append(cell);
|
|
337
354
|
body.append(row);
|
|
338
355
|
}
|
|
339
356
|
|
|
340
|
-
function tableRow(values, { total = false } = {}) {
|
|
357
|
+
function tableRow(values, { total = false, labels = [] } = {}) {
|
|
341
358
|
const row = document.createElement("tr");
|
|
342
359
|
if (total) row.className = "total-row";
|
|
343
|
-
for (const value of values) {
|
|
360
|
+
for (const [index, value] of values.entries()) {
|
|
344
361
|
const cell = document.createElement("td");
|
|
345
|
-
|
|
362
|
+
if ((total && index === 0) || labels.includes(index)) localize(cell, value);
|
|
363
|
+
else cell.textContent = value;
|
|
346
364
|
row.append(cell);
|
|
347
365
|
}
|
|
348
366
|
return row;
|
|
@@ -361,11 +379,11 @@ async function loadModels() {
|
|
|
361
379
|
const body = element("live-models-body");
|
|
362
380
|
body.replaceChildren();
|
|
363
381
|
for (const model of data.models) {
|
|
364
|
-
body.append(tableRow([model.id, model.vendor, model.endpoints.join(" / "), model.preview ? "preview" : "—"]));
|
|
382
|
+
body.append(tableRow([model.id, model.vendor, model.endpoints.join(" / "), model.preview ? "preview" : "—"], { labels: [3] }));
|
|
365
383
|
}
|
|
366
384
|
if (!data.models.length) emptyTable("live-models-body", 4, "No selectable GPT models advertised.");
|
|
367
385
|
text("models-state", "LIVE SNAPSHOT");
|
|
368
|
-
text("models-note",
|
|
386
|
+
text("models-note", "{selectable} selectable / {advertised} advertised / {host} / {time}", { selectable: number(data.selectable), advertised: number(data.advertised), host: data.upstream_host, time: new Date(data.checked_at).toLocaleTimeString() });
|
|
369
387
|
} catch (error) {
|
|
370
388
|
text("models-state", "LIVE LOOKUP FAILED");
|
|
371
389
|
text("models-note", String(error.message || error).slice(0, 240));
|
|
@@ -388,7 +406,7 @@ let selectedUsageDate = "";
|
|
|
388
406
|
function renderUsageTable() {
|
|
389
407
|
const day = selectedUsageDate && usageData.analytics?.days.find((entry) => entry.date === selectedUsageDate);
|
|
390
408
|
const model = element("analytics-model").value;
|
|
391
|
-
const total = day ? {
|
|
409
|
+
const total = day ? { ...analyticsPoint(day, model), model: "TOTAL" } : usageData.total;
|
|
392
410
|
const rows = day ? day.models.filter((row) => !model || row.model === model).sort((a, b) => b.total_tokens - a.total_tokens || a.model.localeCompare(b.model)) : usageData.rows;
|
|
393
411
|
const body = element("usage-body");
|
|
394
412
|
body.replaceChildren();
|
|
@@ -398,7 +416,10 @@ function renderUsageTable() {
|
|
|
398
416
|
} else { emptyTable("usage-body", 7, "No usage records."); }
|
|
399
417
|
element("clear-usage-day").hidden = !day;
|
|
400
418
|
text("usage-state", day ? day.date : "LOCAL LOG");
|
|
401
|
-
text("usage-note",
|
|
419
|
+
text("usage-note", "{records} records / {shown} of {models} models shown{day}", {
|
|
420
|
+
records: number(total.requests), shown: number(rows.length), models: number(day ? rows.length : usageData.model_count),
|
|
421
|
+
day: day ? phrase(" / {zone} / {model}", { zone: usageData.analytics.time_zone, model: model || phrase("All models") }) : "",
|
|
422
|
+
});
|
|
402
423
|
}
|
|
403
424
|
|
|
404
425
|
function analyticsPoint(day, model) {
|
|
@@ -416,10 +437,11 @@ function dayButton(day, point) {
|
|
|
416
437
|
button.type = "button";
|
|
417
438
|
button.disabled = !day.available;
|
|
418
439
|
button.setAttribute("aria-pressed", String(day.date === selectedUsageDate));
|
|
419
|
-
const description = !day.available ? "no retained history"
|
|
420
|
-
:
|
|
421
|
-
|
|
422
|
-
|
|
440
|
+
const description = !day.available ? phrase("no retained history")
|
|
441
|
+
: phrase("{calls} recorded calls / input {input} / output {output} / cached {cached} (included in input){partial}", {
|
|
442
|
+
calls: number(point.requests), input: number(point.input_tokens), output: number(point.output_tokens), cached: number(point.cache_read_tokens), partial: point.tokens_partial ? phrase(" / partial token counts") : "",
|
|
443
|
+
});
|
|
444
|
+
for (const attribute of ["title", "aria-label"]) localize(button, "{date} / {description}", { date: day.date, description }, attribute);
|
|
423
445
|
button.addEventListener("click", () => selectUsageDay(day.date));
|
|
424
446
|
return button;
|
|
425
447
|
}
|
|
@@ -456,7 +478,7 @@ function renderAnalytics() {
|
|
|
456
478
|
return metric === "tokens" ? point.input_tokens + point.output_tokens : point.requests;
|
|
457
479
|
};
|
|
458
480
|
const peak = Math.max(1, ...days.map(value));
|
|
459
|
-
text("activity-scale",
|
|
481
|
+
text("activity-scale", "{metric} / PEAK {peak}", { metric: phrase(metric === "tokens" ? "INPUT + OUTPUT" : "RECORDED CALLS"), peak: number(peak === 1 && days.every((day) => value(day) === 0) ? 0 : peak) });
|
|
460
482
|
const calendar = element("activity-calendar");
|
|
461
483
|
calendar.replaceChildren();
|
|
462
484
|
const offset = (new Date(`${days[0].date}T00:00:00Z`).getUTCDay() + 6) % 7;
|
|
@@ -486,8 +508,12 @@ function renderAnalytics() {
|
|
|
486
508
|
button.dataset.level = String(count > 0 ? Math.min(4, Math.ceil(count / peak * 4)) : 0);
|
|
487
509
|
calendar.append(button);
|
|
488
510
|
}
|
|
489
|
-
text("analytics-note",
|
|
490
|
-
|
|
511
|
+
text("analytics-note", "{zone} / {from} — {to} / retained history starts {first}; oldest day may be partial.{excluded}{limit}", {
|
|
512
|
+
zone: analytics.time_zone, from: daily[0].date, to: analytics.to, first: analytics.first_date || phrase("unknown"),
|
|
513
|
+
excluded: analytics.undated_records ? phrase(" {count} records with invalid/future timestamps excluded.", { count: number(analytics.undated_records) }) : "",
|
|
514
|
+
limit: analytics.models_truncated ? phrase(" Model filters limited to 100; all-model totals include the remainder.") : "",
|
|
515
|
+
});
|
|
516
|
+
text("analytics-selection", selectedUsageDate ? "Selected {date} / {model}: totals in the Usage table above / All history resets the table." : "Select a day for model totals. Calls are usage records, not messages or time. Hatched cells: no retained history; empty cells: zero recorded calls.", { date: selectedUsageDate, model: model || phrase("All models") });
|
|
491
517
|
}
|
|
492
518
|
|
|
493
519
|
function updateAnalyticsModels() {
|
|
@@ -497,7 +523,8 @@ function updateAnalyticsModels() {
|
|
|
497
523
|
for (const model of ["", ...usageData.analytics.models]) {
|
|
498
524
|
const option = document.createElement("option");
|
|
499
525
|
option.value = model;
|
|
500
|
-
option.textContent = model
|
|
526
|
+
if (model) option.textContent = model;
|
|
527
|
+
else localize(option, "All models");
|
|
501
528
|
select.append(option);
|
|
502
529
|
}
|
|
503
530
|
select.value = usageData.analytics.models.includes(previous) ? previous : "";
|
package/src/dashboard-api.mjs
CHANGED
|
@@ -129,7 +129,7 @@ export async function handleDashboardApi(req, res, pathname, {
|
|
|
129
129
|
try {
|
|
130
130
|
const profile = authStatusFn()?.profiles?.codex;
|
|
131
131
|
if (!profile || typeof profile.configured !== "boolean" || typeof profile.valid !== "boolean") throw new Error("Invalid auth status");
|
|
132
|
-
const login = typeof profile.login === "string" && /^[a-z\
|
|
132
|
+
const login = typeof profile.login === "string" && /^[a-z\d_-]{1,39}$/i.test(profile.login)
|
|
133
133
|
&& redactDiagnosticText(profile.login) === profile.login ? profile.login : "";
|
|
134
134
|
const id = /^\d{1,20}$/.test(String(profile.id || "")) ? String(profile.id) : "";
|
|
135
135
|
sendJson(res, 200, {
|
package/src/dashboard.mjs
CHANGED
|
@@ -5,6 +5,7 @@ const ASSETS = new Map([
|
|
|
5
5
|
["/", ["./dashboard/index.html", "text/html; charset=utf-8"]],
|
|
6
6
|
["/ui.css", ["./dashboard/ui.css", "text/css; charset=utf-8"]],
|
|
7
7
|
["/theme.js", ["./dashboard/theme.js", "text/javascript; charset=utf-8"]],
|
|
8
|
+
["/language.js", ["./dashboard/language.js", "text/javascript; charset=utf-8"]],
|
|
8
9
|
["/ui.js", ["./dashboard/ui.js", "text/javascript; charset=utf-8"]],
|
|
9
10
|
]);
|
|
10
11
|
|