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.
- package/README.md +9 -3
- package/package.json +1 -1
- package/src/adapter.mjs +17 -4
- package/src/cli-status.mjs +9 -1
- package/src/dashboard/index.html +78 -0
- package/src/dashboard/ui.css +83 -0
- package/src/dashboard/ui.js +347 -0
- package/src/dashboard-api.mjs +204 -0
- package/src/dashboard.mjs +30 -0
- package/src/observability.mjs +13 -0
- package/src/response-history.mjs +24 -1
- package/src/responses-bridge.mjs +54 -16
- package/src/responses-handler.mjs +21 -4
- package/src/responses-proxy.mjs +33 -1
- package/src/stream-performance.mjs +69 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ One in-process adapter listens on `127.0.0.1:2026` by default and exposes:
|
|
|
12
12
|
- `POST /v1/responses/compact`
|
|
13
13
|
- `GET /v1/models`
|
|
14
14
|
|
|
15
|
-
Responses-only models and compaction use Copilot's Responses transport directly. For
|
|
15
|
+
Responses-only models and compaction use Copilot's Responses transport directly. For requests routed through Chat Completions, CCDX converts the supported text, image, and function-tool subset and rejects incompatible message content rather than turning it into JSON text. Chat `length` and `content_filter` finish reasons become incomplete Responses in both streaming and non-streaming modes. Both streaming SSE and non-streaming responses are supported.
|
|
16
16
|
|
|
17
17
|
The adapter handles response history, encrypted reasoning state, function/custom tools, images, compaction, cancellation, retries, and usage metadata within the compatibility boundaries below. Routing prefers endpoint metadata from Copilot's live or cached catalog; when metadata is unavailable, a small built-in set identifies known Responses-only GPT models.
|
|
18
18
|
|
|
@@ -213,6 +213,12 @@ If a configured npm mirror has not synchronized the current release yet, use the
|
|
|
213
213
|
|
|
214
214
|
## Diagnostics
|
|
215
215
|
|
|
216
|
+
### Local dashboard
|
|
217
|
+
|
|
218
|
+
After starting `ccdx`, open [http://127.0.0.1:2026/](http://127.0.0.1:2026/) in a browser. If `ADAPTER_PORT` is customized, use that port instead. The dashboard shares the existing adapter listener; CCDX does not open a browser automatically or start another service. The page and its assets require both a loopback connection and a loopback Host header, even when LAN binding is enabled.
|
|
219
|
+
|
|
220
|
+
The dashboard loads local status, a live GitHub Copilot model directory, and a summary of local usage metadata once when opened. Each section has an explicit Refresh control; nothing polls in the background. The live model table uses the same fresh account validation and selectable GPT filtering as `ccdx models`, never labels cached fallback data as online, and makes no inference request. A catalog listing does not guarantee inference access. The Usage table combines the current and rotated local usage logs, shows totals and up to 100 model rows, and contains no prompt, response body, or credential fields. The animation selector previews the existing themes and saves through the same settings writer as `ccdx animation`; changes take effect on the next adapter start, and `CCDX_TERMINAL_ANIMATION=off` still disables terminal motion. Dashboard data requests require a custom header that cross-site simple requests cannot send; animation writes also require a matching same-origin `Origin`. The dashboard does not initialize the optional image handler. The overview's known-model count is the adapter's current in-memory catalog, not the live table. A missing image-generation status does not prove the feature is disabled: use `ccdx image-status` to check setup. Recent upstream error prose may contain sensitive details, so review the page before sharing screenshots.
|
|
221
|
+
|
|
216
222
|
Inspect only the local Codex configuration:
|
|
217
223
|
|
|
218
224
|
```bash
|
|
@@ -259,7 +265,7 @@ For the complete machine-readable status payload:
|
|
|
259
265
|
curl -s http://127.0.0.1:2026/_ccdx/status | jq
|
|
260
266
|
```
|
|
261
267
|
|
|
262
|
-
The status endpoint is restricted to the socket's real loopback address even when LAN binding is explicitly enabled. It reports fixed-size request counters, bounded TTFT/TPOT histograms, admission pressure, memory use, response-history size, image queue/cache and adaptive-history state, model-cache counts, token expiry state, and the ten most recent upstream `response.failed` diagnostics retained by the running process. Failure diagnostics include the event type, model, error code/message, response/request IDs when supplied upstream, and retry outcome. Common credential forms, long opaque encrypted values, and explicitly labelled input/prompt/tool-content echoes are redacted. They are cleared when the adapter restarts. No request/response bodies, credential fields, or account profiles are copied into status. Arbitrary upstream prose cannot be guaranteed free of sensitive content; review diagnostics before sharing them.
|
|
268
|
+
The status endpoint is restricted to the socket's real loopback address even when LAN binding is explicitly enabled. It reports fixed-size HTTP request counters separately from bounded `completed` / `incomplete` / `failed` / `cancelled` response outcomes by model and origin, plus bounded incomplete-reason counts, TTFT/TPOT histograms, admission pressure, memory use, response-history size/tree concentration/lookup misses, image queue/cache and adaptive-history state, model-cache counts, token expiry state, and the ten most recent upstream `response.failed` diagnostics retained by the running process. HTTP errors without a model terminal remain "without terminal" and retain their known origin; they are not counted as model failures. History eviction markers are not failed-request counts; `ccdx status` warns near the cache limit without deleting or compressing history. Failure diagnostics include the event type, model, error code/message, response/request IDs when supplied upstream, and retry outcome. Common credential forms, long opaque encrypted values, and explicitly labelled input/prompt/tool-content echoes are redacted. They are cleared when the adapter restarts. No request/response bodies, credential fields, or account profiles are copied into status. Arbitrary upstream prose cannot be guaranteed free of sensitive content; review diagnostics before sharing them.
|
|
263
269
|
|
|
264
270
|
`image_mcp` has its own HTTP route counter. After the image handler has been used, `image_generation` separately reports active, succeeded, failed, busy, cancelled, and chat-file delivery-failure counts, including tool errors returned with HTTP 200. A delivery failure can accompany a successful generation; it never causes another generation. Before handler initialization this field is `null`, meaning not observed, not necessarily disabled. Status never initializes that handler, reads its provider credential, installs guidance, or probes the provider. Input-image optimization metrics remain separate. `logging` reports pending records/bytes, queue caps, overflow drops, and write failures for usage and enabled debug-file logging.
|
|
265
271
|
|
|
@@ -367,7 +373,7 @@ Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-d
|
|
|
367
373
|
|
|
368
374
|
Long computer-use sessions can accumulate screenshots that are sent again on later turns. This can increase local preparation time, upstream handshake latency, and the chance of a `413 Payload Too Large` response.
|
|
369
375
|
|
|
370
|
-
The response-history cache is process-local and byte/entry bounded. Restarting the adapter discards it; a `previous_response_id` stored only in that process becomes unavailable. CCDX does not persist conversation history to disk. Clients can still submit explicit history in the request body, subject to the same limits.
|
|
376
|
+
The response-history cache is process-local and byte/entry bounded. Restarting the adapter discards it; a `previous_response_id` stored only in that process becomes unavailable. CCDX does not persist conversation history to disk. Near-limit status diagnostics report the largest tree and actual lookup misses, but do not recover an already evicted ID or change the cache budget. Clients can still submit explicit history in the request body, subject to the same limits.
|
|
371
377
|
|
|
372
378
|
CCDX attempts to downsample inline base64 images to model-appropriate pixel bounds, with a 2048 px long-edge fallback, and initially encodes them as WebP quality 82. It never replaces an image with a larger encoding and uses one global concurrency limit across direct images and function/custom-tool outputs. GIFs and non-inline references are not re-encoded. An encoding/decode failure keeps the original image; the final request-byte limit still applies.
|
|
373
379
|
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
classifyAdapterRoute,
|
|
8
8
|
createRequestMetrics,
|
|
9
9
|
isLoopbackAddress,
|
|
10
|
+
isLoopbackHostHeader,
|
|
10
11
|
runtimeStatusPayload,
|
|
11
12
|
} from "./observability.mjs";
|
|
12
13
|
import { createRequestId, runWithRequestContext } from "./request-context.mjs";
|
|
@@ -63,6 +64,8 @@ export {
|
|
|
63
64
|
} from "./responses-request.mjs";
|
|
64
65
|
|
|
65
66
|
const ADAPTER_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
67
|
+
const DASHBOARD_PATHS = new Set(["/", "/ui.css", "/ui.js"]);
|
|
68
|
+
const DASHBOARD_API_PATHS = new Set(["/_ccdx/ui/animation", "/_ccdx/ui/models/live", "/_ccdx/ui/usage"]);
|
|
66
69
|
|
|
67
70
|
export function requestPath(reqUrl) {
|
|
68
71
|
return new URL(reqUrl || "/", "http://localhost").pathname;
|
|
@@ -200,6 +203,14 @@ export function createAdapterHandler(options = {}) {
|
|
|
200
203
|
upstreamTimeoutMs,
|
|
201
204
|
});
|
|
202
205
|
const dispatch = async (req, res, pathname) => {
|
|
206
|
+
if (DASHBOARD_API_PATHS.has(pathname)) {
|
|
207
|
+
const { handleDashboardApi } = await import("./dashboard-api.mjs");
|
|
208
|
+
return handleDashboardApi(req, res, pathname, options.dashboardOptions);
|
|
209
|
+
}
|
|
210
|
+
if ((req.method === "GET" || req.method === "HEAD") && DASHBOARD_PATHS.has(pathname)) {
|
|
211
|
+
const { serveDashboard } = await import("./dashboard.mjs");
|
|
212
|
+
return serveDashboard(req, res, pathname);
|
|
213
|
+
}
|
|
203
214
|
if (pathname === "/mcp/image") {
|
|
204
215
|
if (imageMcpHandler) return imageMcpHandler(req, res);
|
|
205
216
|
return import("./image-mcp.mjs").then(({ createImageMcpHandler }) => {
|
|
@@ -208,7 +219,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
208
219
|
});
|
|
209
220
|
}
|
|
210
221
|
if (req.method === "GET" && pathname === ADAPTER_STATUS_PATH) {
|
|
211
|
-
if (!isLoopbackAddress(req.socket?.remoteAddress)) {
|
|
222
|
+
if (!isLoopbackAddress(req.socket?.remoteAddress) || !isLoopbackHostHeader(req.headers?.host)) {
|
|
212
223
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
213
224
|
res.end(JSON.stringify({ error: "Runtime status is available only from loopback" }));
|
|
214
225
|
return;
|
|
@@ -235,7 +246,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
235
246
|
}
|
|
236
247
|
|
|
237
248
|
if (pathname === ADAPTER_CACHE_PATH) {
|
|
238
|
-
if (!isLoopbackAddress(req.socket?.remoteAddress)) {
|
|
249
|
+
if (!isLoopbackAddress(req.socket?.remoteAddress) || !isLoopbackHostHeader(req.headers?.host)) {
|
|
239
250
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
240
251
|
res.end(JSON.stringify({ error: "Cache control is available only from loopback" }));
|
|
241
252
|
return;
|
|
@@ -373,7 +384,9 @@ export function createAdapterHandler(options = {}) {
|
|
|
373
384
|
|
|
374
385
|
const trackRequest = pathname !== ADAPTER_HEALTH_PATH
|
|
375
386
|
&& pathname !== ADAPTER_STATUS_PATH
|
|
376
|
-
&& pathname !== ADAPTER_CACHE_PATH
|
|
387
|
+
&& pathname !== ADAPTER_CACHE_PATH
|
|
388
|
+
&& !DASHBOARD_API_PATHS.has(pathname)
|
|
389
|
+
&& !((req.method === "GET" || req.method === "HEAD") && DASHBOARD_PATHS.has(pathname));
|
|
377
390
|
const routeName = classifyAdapterRoute(req.method, pathname);
|
|
378
391
|
const complete = trackRequest ? requestMetrics.begin(routeName) : () => {};
|
|
379
392
|
const streamPerformance = trackRequest ? streamPerformanceMetrics.begin(routeName) : null;
|
|
@@ -389,7 +402,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
389
402
|
if (requestFinished) return;
|
|
390
403
|
requestFinished = true;
|
|
391
404
|
try { complete({ statusCode, aborted }); } catch {}
|
|
392
|
-
try { streamPerformance?.finish({ failed: aborted || statusCode >= 400 }); } catch {}
|
|
405
|
+
try { streamPerformance?.finish({ failed: aborted || statusCode >= 400, aborted }); } catch {}
|
|
393
406
|
try { finishTerminalActivity(); } catch {}
|
|
394
407
|
};
|
|
395
408
|
const containUnexpectedError = (error) => {
|
package/src/cli-status.mjs
CHANGED
|
@@ -132,6 +132,7 @@ export async function readAdapterStatus({
|
|
|
132
132
|
export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", cliVersion = data.version } = {}) {
|
|
133
133
|
const requests = data.requests || {};
|
|
134
134
|
const performance = data.stream_performance?.by_route?.responses || {};
|
|
135
|
+
const terminalOutcomes = performance.terminal_outcomes?.totals;
|
|
135
136
|
const admission = data.admission || {};
|
|
136
137
|
const history = data.response_history || {};
|
|
137
138
|
const images = data.image_optimization || {};
|
|
@@ -150,11 +151,18 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
|
|
|
150
151
|
adapterLine,
|
|
151
152
|
status("info", `Requests: ${count(requests.completed)}/${count(requests.total)} completed, ${count(requests.active)} active, ${count(requests.status_4xx)} 4xx, ${count(requests.status_5xx)} 5xx, ${count(requests.aborted)} aborted`),
|
|
152
153
|
status("info", `Responses stream: TTFT avg ${latency(performance.ttft_ms?.avg)} (${count(performance.ttft_ms?.samples)} samples), TPOT avg ${tpot(performance.tpot_us)}`),
|
|
154
|
+
...(terminalOutcomes ? [status("info", `Model outcomes: ${count(terminalOutcomes.completed)} completed, ${count(terminalOutcomes.incomplete)} incomplete, ${count(terminalOutcomes.failed)} failed, ${count(terminalOutcomes.cancelled)} cancelled, ${count(terminalOutcomes.unknown)} without terminal`)] : []),
|
|
153
155
|
status("info", `Admission: ${count(admission.activeRequests)} active, ${count(admission.queued)} queued, ${count(admission.rejected)} rejected, ${count(admission.timedOut)} timed out, wait avg ${latency(admission.waitMsAvg)}`),
|
|
154
156
|
status("info", `Memory: RSS ${mebibytes(processStats.rss_bytes)}, heap ${mebibytes(processStats.heap_used_bytes)}`),
|
|
155
|
-
status("info", `History: ${mebibytes(history.bytes)} / ${mebibytes(limits.response_history_max_bytes)}, ${count(history.entries)} entries, ${count(history.evicted)} evicted`),
|
|
157
|
+
status("info", `History: ${mebibytes(history.bytes)} / ${mebibytes(limits.response_history_max_bytes)}, ${count(history.entries)} entries, ${count(history.evicted)} evicted ID markers`),
|
|
156
158
|
status("info", `Image cache: ${count(images.cache_entries)} entries, ${cacheHitRate(images)} hits, ${mebibytes(images.cache_bytes)} / ${mebibytes(images.cache_max_bytes)}`),
|
|
157
159
|
];
|
|
160
|
+
if (Number.isFinite(history.tree_count)) {
|
|
161
|
+
const limit = Number(limits.response_history_max_bytes || history.maxBytes);
|
|
162
|
+
const nearLimit = Number.isFinite(limit) && limit > 0 && history.bytes / limit >= 0.9;
|
|
163
|
+
lines.push(status(nearLimit ? "warn" : "info",
|
|
164
|
+
`${nearLimit ? "History near limit" : "History diagnostics"}: largest tree ${mebibytes(history.largest_tree_bytes)} across ${count(history.tree_count)} trees; ${count(history.lookup_misses)} lookup misses, ${count(history.evicted_lookup_misses)} for evicted IDs`));
|
|
165
|
+
}
|
|
158
166
|
if (imageHistory && typeof imageHistory === "object") {
|
|
159
167
|
lines.push(status("info", `Visual history: ${count(imageHistory.active_recovery_trees)} recovery trees, ${count(imageHistory.adapted_requests)} adapted requests, ${count(imageHistory.historical_images_omitted)} older images omitted, ${count(imageHistory.timeouts_recorded)} timeouts`));
|
|
160
168
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="dark">
|
|
7
|
+
<link rel="icon" href="data:,">
|
|
8
|
+
<link rel="stylesheet" href="/ui.css">
|
|
9
|
+
<script src="/ui.js" defer></script>
|
|
10
|
+
<title>CCDX · Local dashboard</title>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div class="shell">
|
|
14
|
+
<header class="topbar">
|
|
15
|
+
<div class="brand">CCDX <span class="brand-detail">/ LOCAL STATUS</span></div>
|
|
16
|
+
<div class="topbar-actions"><span id="connection" class="badge neutral" role="status">READING</span><button id="refresh" type="button">Refresh</button></div>
|
|
17
|
+
</header>
|
|
18
|
+
|
|
19
|
+
<main>
|
|
20
|
+
<section class="intro">
|
|
21
|
+
<h1><span aria-hidden="true">$</span> ccdx status</h1>
|
|
22
|
+
<p id="updated">Reading status…</p>
|
|
23
|
+
</section>
|
|
24
|
+
|
|
25
|
+
<section class="overview" aria-label="Overview">
|
|
26
|
+
<article class="card"><h2>ADAPTER</h2><strong id="version">—</strong><p id="uptime">—</p></article>
|
|
27
|
+
<article class="card"><h2>COPILOT</h2><strong id="copilot">—</strong><p id="token">—</p></article>
|
|
28
|
+
<article class="card"><h2>KNOWN MODELS</h2><strong id="models">—</strong><p id="model-source">—</p></article>
|
|
29
|
+
<article class="card"><h2>REQUESTS</h2><strong id="requests">—</strong><p id="request-errors">—</p></article>
|
|
30
|
+
</section>
|
|
31
|
+
|
|
32
|
+
<section class="details" aria-label="Runtime details">
|
|
33
|
+
<article class="panel">
|
|
34
|
+
<div class="panel-heading"><h2>MODEL OUTCOMES</h2><span>RESPONSES</span></div>
|
|
35
|
+
<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>
|
|
36
|
+
<p class="hint">HTTP 2xx != model completed.</p>
|
|
37
|
+
</article>
|
|
38
|
+
<article class="panel">
|
|
39
|
+
<div class="panel-heading"><h2>LOCAL HISTORY</h2><span>MEMORY</span></div>
|
|
40
|
+
<strong id="history">—</strong><progress id="history-meter" class="meter" max="100" value="0" aria-label="History cache usage"></progress>
|
|
41
|
+
<p id="history-detail" class="hint">—</p>
|
|
42
|
+
</article>
|
|
43
|
+
<article class="panel">
|
|
44
|
+
<div class="panel-heading"><h2>IMAGE GENERATION</h2><span>OPTIONAL</span></div>
|
|
45
|
+
<strong id="image">—</strong><p id="image-detail" class="hint">—</p>
|
|
46
|
+
</article>
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<section class="panel dashboard-section" aria-label="Terminal animation">
|
|
50
|
+
<div class="panel-heading"><h2>TERMINAL ANIMATION</h2><span id="animation-state">READING</span></div>
|
|
51
|
+
<div id="animation-options" class="animation-options" role="group" aria-label="Animation theme"></div>
|
|
52
|
+
<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>
|
|
53
|
+
</section>
|
|
54
|
+
|
|
55
|
+
<section class="panel dashboard-section" aria-label="Live upstream models">
|
|
56
|
+
<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>
|
|
57
|
+
<p id="models-note" class="table-note">Querying GitHub Copilot…</p>
|
|
58
|
+
<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>
|
|
59
|
+
<p class="hint">Catalog listing does not guarantee inference access.</p>
|
|
60
|
+
</section>
|
|
61
|
+
|
|
62
|
+
<section class="panel dashboard-section" aria-label="Usage by model">
|
|
63
|
+
<div class="panel-heading"><h2>USAGE</h2><div class="heading-actions"><span id="usage-state">READING LOG</span><button id="refresh-usage" type="button">Refresh</button></div></div>
|
|
64
|
+
<p id="usage-note" class="table-note">Aggregating local usage metadata…</p>
|
|
65
|
+
<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></tr></thead><tbody id="usage-body"><tr><td colspan="6">Loading…</td></tr></tbody></table></div>
|
|
66
|
+
</section>
|
|
67
|
+
|
|
68
|
+
<section class="lower" aria-label="Recent failures">
|
|
69
|
+
<div class="panel-heading"><h2>RECENT UPSTREAM FAILURES</h2><span>response.failed</span></div>
|
|
70
|
+
<ol id="failures"><li class="empty">Reading…</li></ol>
|
|
71
|
+
<p class="hint">Upstream messages may contain sensitive text.</p>
|
|
72
|
+
</section>
|
|
73
|
+
</main>
|
|
74
|
+
|
|
75
|
+
<footer>LOCAL ONLY / NO BACKGROUND POLLING</footer>
|
|
76
|
+
</div>
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
:root { color-scheme: dark; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
body { margin: 0; color: #e0e9df; background: #0b110d; font-size: 13px; }
|
|
4
|
+
.shell { max-width: 1220px; margin: 0 auto; padding: 0 24px; }
|
|
5
|
+
.topbar { min-height: 68px; display: flex; justify-content: space-between; align-items: center; gap: 20px; border-bottom: 1px solid #2a3a2e; }
|
|
6
|
+
.brand { color: #a6f5a8; font-size: 16px; font-weight: 750; letter-spacing: .07em; white-space: nowrap; }
|
|
7
|
+
.brand-detail { color: #78917d; font-size: 11px; font-weight: 500; letter-spacing: .03em; }
|
|
8
|
+
.topbar-actions { display: flex; align-items: center; gap: 20px; }
|
|
9
|
+
.badge { color: #a9b9aa; white-space: nowrap; font-size: 11px; font-weight: 650; text-transform: uppercase; }
|
|
10
|
+
.badge::before { content: "●"; margin-right: 8px; }
|
|
11
|
+
.badge.good { color: #84e99e; }
|
|
12
|
+
.badge.bad { color: #ff947e; }
|
|
13
|
+
button { border: 1px solid #45634b; border-radius: 2px; background: #152219; color: #b2f1bd; padding: 8px 13px; font: inherit; font-size: 11px; font-weight: 700; text-transform: uppercase; cursor: pointer; }
|
|
14
|
+
button:hover { background: #203526; }
|
|
15
|
+
button:focus-visible { outline: 2px solid #a6f5a8; outline-offset: 3px; }
|
|
16
|
+
button:disabled { opacity: .55; cursor: wait; }
|
|
17
|
+
.intro { display: flex; justify-content: space-between; align-items: baseline; gap: 20px; padding: 38px 0 25px; }
|
|
18
|
+
h1 { margin: 0; font-size: clamp(20px, 3vw, 27px); font-weight: 650; letter-spacing: -.035em; }
|
|
19
|
+
h1 span { color: #84e99e; }
|
|
20
|
+
.intro p { margin: 0; color: #78917d; font-size: 11px; }
|
|
21
|
+
.overview { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 11px; }
|
|
22
|
+
.card, .panel, .lower { min-width: 0; background: #111a13; border: 1px solid #2b3b2f; border-radius: 2px; }
|
|
23
|
+
.card { min-height: 139px; padding: 18px; }
|
|
24
|
+
.card h2, .panel h2, .lower h2 { color: #83b98b; font-size: 11px; font-weight: 700; letter-spacing: .08em; margin: 0; }
|
|
25
|
+
.card strong, .panel > strong { display: block; margin-top: 21px; color: #f0f6ec; font-size: 23px; font-weight: 650; line-height: 1.25; letter-spacing: -.035em; overflow-wrap: anywhere; }
|
|
26
|
+
.card p { margin: 9px 0 0; color: #9aada0; font-size: 11px; line-height: 1.45; overflow-wrap: anywhere; }
|
|
27
|
+
.details { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 11px; margin-top: 11px; }
|
|
28
|
+
.panel { min-height: 205px; padding: 18px; }
|
|
29
|
+
.panel-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
|
|
30
|
+
.panel-heading span { color: #6d8371; font-size: 10px; white-space: nowrap; }
|
|
31
|
+
.stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 17px; margin: 26px 0 0; }
|
|
32
|
+
.stats div { display: flex; justify-content: space-between; border-bottom: 1px solid #2b3b2f; padding-bottom: 8px; gap: 8px; }
|
|
33
|
+
.stats dt { color: #9aada0; font-size: 11px; }
|
|
34
|
+
.stats dd { margin: 0; font-size: 14px; font-weight: 650; }
|
|
35
|
+
#incomplete { color: #e3c77c; }
|
|
36
|
+
#failed { color: #ff9f91; }
|
|
37
|
+
.hint { color: #78917d; font-size: 11px; line-height: 1.5; margin: 15px 0 0; overflow-wrap: anywhere; }
|
|
38
|
+
.meter { display: block; width: 100%; height: 6px; border: 0; border-radius: 0; background: #2b3b2f; margin-top: 20px; appearance: none; }
|
|
39
|
+
.meter::-webkit-progress-bar { background: #2b3b2f; }
|
|
40
|
+
.meter::-webkit-progress-value { background: #84e99e; }
|
|
41
|
+
.meter::-moz-progress-bar { background: #84e99e; }
|
|
42
|
+
.dashboard-section { min-height: 0; margin-top: 11px; }
|
|
43
|
+
.heading-actions { display: flex; align-items: center; gap: 16px; }
|
|
44
|
+
.heading-actions button { padding: 5px 9px; }
|
|
45
|
+
.animation-options { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 18px; }
|
|
46
|
+
.theme-option { display: grid; grid-template-columns: 18px 76px minmax(0, 1fr); align-items: center; gap: 8px; min-width: 0; width: 100%; border: 1px solid #2b3b2f; padding: 10px; background: transparent; color: #e0e9df; text-align: left; text-transform: none; font-size: 11px; cursor: pointer; }
|
|
47
|
+
.theme-option::before { content: "○"; color: #78917d; font-size: 17px; line-height: 1; }
|
|
48
|
+
.theme-option[aria-pressed="true"] { border-color: #84e99e; background: #192a1d; }
|
|
49
|
+
.theme-option[aria-pressed="true"]::before { content: "●"; color: #84e99e; }
|
|
50
|
+
.theme-option:hover { background: #1a2a1e; }
|
|
51
|
+
.theme-name { color: #e0e9df; font-size: 11px; white-space: nowrap; }
|
|
52
|
+
.theme-name small { display: block; color: #78917d; font-size: 9px; margin-top: 2px; }
|
|
53
|
+
.theme-preview { color: #657d69; white-space: pre; overflow: hidden; font-size: 11px; letter-spacing: 0; }
|
|
54
|
+
.ansi-white { color: #f5fff5; }
|
|
55
|
+
.ansi-159 { color: #9af5e9; }
|
|
56
|
+
.ansi-123 { color: #75e4d9; }
|
|
57
|
+
.ansi-87 { color: #5ad0d4; }
|
|
58
|
+
.ansi-51 { color: #42b9d8; }
|
|
59
|
+
.ansi-45 { color: #3ca2d0; }
|
|
60
|
+
.ansi-39 { color: #4389c2; }
|
|
61
|
+
.ansi-33 { color: #4775b2; }
|
|
62
|
+
.section-actions { display: flex; align-items: center; gap: 15px; margin-top: 16px; }
|
|
63
|
+
.section-actions span { color: #9aada0; font-size: 11px; }
|
|
64
|
+
.table-note { margin: 15px 0 8px; color: #9aada0; font-size: 11px; }
|
|
65
|
+
.table-wrap { width: 100%; overflow-x: auto; }
|
|
66
|
+
table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
|
67
|
+
th { color: #83b98b; text-align: left; font-weight: 700; letter-spacing: .05em; }
|
|
68
|
+
th, td { padding: 9px 12px 9px 0; border-bottom: 1px solid #2b3b2f; white-space: nowrap; }
|
|
69
|
+
th:last-child, td:last-child { padding-right: 0; }
|
|
70
|
+
td { color: #d8e2d8; }
|
|
71
|
+
.usage-table th:not(:first-child), .usage-table td:not(:first-child) { text-align: right; }
|
|
72
|
+
.usage-table tr.total-row td { color: #a6f5a8; font-weight: 700; }
|
|
73
|
+
.lower { margin-top: 11px; padding: 18px; }
|
|
74
|
+
.lower ol { list-style: none; margin: 16px 0 0; padding: 0; }
|
|
75
|
+
.lower li { display: grid; grid-template-columns: minmax(150px, 1fr) minmax(0, 2fr); gap: 14px; padding: 9px 0; border-top: 1px solid #2b3b2f; font-size: 11px; }
|
|
76
|
+
.lower li strong, .lower li span { overflow-wrap: anywhere; }
|
|
77
|
+
.lower li strong { color: #e0e9df; font-weight: 650; }
|
|
78
|
+
.lower li span, .lower li.empty { color: #9aada0; line-height: 1.5; }
|
|
79
|
+
footer { color: #617966; font-size: 10px; letter-spacing: .05em; padding: 22px 0 30px; }
|
|
80
|
+
@media (max-width: 1000px) { .animation-options { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
81
|
+
@media (max-width: 900px) { .overview { grid-template-columns: repeat(2, minmax(0, 1fr)); } .details { grid-template-columns: repeat(2, minmax(0, 1fr)); } .details .panel:last-child { grid-column: 1 / -1; } }
|
|
82
|
+
@media (max-width: 680px) { .animation-options { grid-template-columns: 1fr; } }
|
|
83
|
+
@media (max-width: 620px) { .shell { padding: 0 16px; } .topbar { min-height: 60px; } .brand-detail { display: none; } .topbar-actions { gap: 11px; } .intro { display: block; padding: 29px 0 21px; } .intro p { margin-top: 9px; } .overview, .details { grid-template-columns: 1fr; } .details .panel:last-child { grid-column: auto; } .card { min-height: 122px; } .card strong { margin-top: 14px; } .lower li { display: block; } .lower li span { display: block; margin-top: 5px; } }
|