@crvouga/atlas 0.10.1 → 0.11.0
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/CHANGELOG.md +12 -0
- package/README.md +7 -0
- package/dist/reports/src/layer-merge.d.ts +5 -1
- package/dist/reports/src/layer-merge.js +17 -2
- package/dist/server/src/settings-page.d.ts +4 -4
- package/dist/server/src/settings-page.js +17 -78
- package/docs/agents.md +25 -0
- package/docs/reference/reports.md +2 -0
- package/docs/reference/server.md +4 -4
- package/docs/report-contract.md +3 -0
- package/docs/ui.md +142 -0
- package/docs/workspaces.md +130 -0
- package/llms-full.txt +306 -0
- package/llms.txt +2 -0
- package/package.json +17 -10
- package/packages/visualizer/src/admin/SettingsPage.tsx +456 -0
- package/packages/visualizer/src/admin/main.tsx +11 -0
- package/packages/visualizer/src/app/AppShell.tsx +62 -38
- package/packages/visualizer/src/app/gazetteer.tsx +90 -83
- package/packages/visualizer/src/app/router.tsx +33 -15
- package/packages/visualizer/src/components/ActivityIndicator.tsx +4 -5
- package/packages/visualizer/src/components/DataIssues.tsx +36 -54
- package/packages/visualizer/src/components/Header.tsx +197 -125
- package/packages/visualizer/src/components/Legend.tsx +13 -41
- package/packages/visualizer/src/components/ProjectPicker.tsx +61 -26
- package/packages/visualizer/src/components/Screen.tsx +80 -71
- package/packages/visualizer/src/components/SpecLogo.tsx +15 -0
- package/packages/visualizer/src/components/Status.tsx +20 -34
- package/packages/visualizer/src/components/labels.tsx +8 -3
- package/packages/visualizer/src/components/spec-logos/APACHE-2.0.txt +206 -0
- package/packages/visualizer/src/components/spec-logos/CAMUNDA-MIT.txt +21 -0
- package/packages/visualizer/src/components/spec-logos/FILE-ICONS-ISC.txt +13 -0
- package/packages/visualizer/src/components/spec-logos/README.md +23 -0
- package/packages/visualizer/src/components/spec-logos/SIMPLE-ICONS-CC0.txt +30 -0
- package/packages/visualizer/src/components/spec-logos/alloy.svg +1 -0
- package/packages/visualizer/src/components/spec-logos/dmn.svg +3 -0
- package/packages/visualizer/src/components/spec-logos/gherkin.svg +1 -0
- package/packages/visualizer/src/components/spec-logos/openslo.svg +63 -0
- package/packages/visualizer/src/components/spec-logos/quint.svg +1 -0
- package/packages/visualizer/src/evidence/artifacts.tsx +263 -110
- package/packages/visualizer/src/evidence/chart-source.tsx +18 -0
- package/packages/visualizer/src/evidence/check-detail.tsx +283 -139
- package/packages/visualizer/src/evidence/decision-table.tsx +35 -26
- package/packages/visualizer/src/evidence/document-page.tsx +51 -29
- package/packages/visualizer/src/evidence/feature-page.tsx +121 -53
- package/packages/visualizer/src/evidence/marks.tsx +112 -79
- package/packages/visualizer/src/evidence/model-page.tsx +77 -46
- package/packages/visualizer/src/evidence/model-trace.tsx +55 -35
- package/packages/visualizer/src/evidence/overview.tsx +137 -69
- package/packages/visualizer/src/evidence/recording-page.tsx +70 -31
- package/packages/visualizer/src/evidence/recording.tsx +135 -68
- package/packages/visualizer/src/evidence/snapshot.tsx +99 -43
- package/packages/visualizer/src/evidence/source-text.tsx +33 -0
- package/packages/visualizer/src/evidence/sources-panel.tsx +138 -144
- package/packages/visualizer/src/evidence/spec-header.tsx +52 -36
- package/packages/visualizer/src/evidence/spec-index.tsx +61 -32
- package/packages/visualizer/src/evidence/spec-page.tsx +56 -25
- package/packages/visualizer/src/evidence/test-filter.tsx +38 -14
- package/packages/visualizer/src/explorer/Explorer.tsx +688 -261
- package/packages/visualizer/src/explorer/ExplorerIndex.tsx +136 -68
- package/packages/visualizer/src/explorer/RunsView.tsx +307 -94
- package/packages/visualizer/src/explorer/TestDetail.tsx +393 -118
- package/packages/visualizer/src/explorer/parts.tsx +35 -13
- package/packages/visualizer/src/find/Finder.tsx +106 -90
- package/packages/visualizer/src/find/open.ts +17 -20
- package/packages/visualizer/src/live/test-progress.tsx +60 -22
- package/packages/visualizer/src/looks/Stage.tsx +223 -110
- package/packages/visualizer/src/main.tsx +9 -11
- package/packages/visualizer/src/map/ChartMap.tsx +125 -69
- package/packages/visualizer/src/map/EventEdge.tsx +59 -37
- package/packages/visualizer/src/map/map.module.css +12 -546
- package/packages/visualizer/src/map/nodes.tsx +124 -70
- package/packages/visualizer/src/reachable.test.ts +3 -2
- package/packages/visualizer/src/spec-kind.ts +2 -2
- package/packages/visualizer/src/ui/grammars/ALLOY-MIT.txt +21 -0
- package/packages/visualizer/src/ui/grammars/QUINT-APACHE-2.0.txt +206 -0
- package/packages/visualizer/src/ui/grammars/README.md +14 -0
- package/packages/visualizer/src/ui/grammars/alloy.json +71 -0
- package/packages/visualizer/src/ui/grammars/quint.json +152 -0
- package/packages/visualizer/src/ui/highlight.ts +50 -0
- package/packages/visualizer/src/ui/theme.tsx +109 -0
- package/packages/visualizer/src/views/ChartPage.tsx +46 -26
- package/packages/visualizer/src/views/ChartTests.tsx +59 -22
- package/packages/visualizer/src/views/ProductPage.tsx +16 -15
- package/packages/visualizer/src/views/SidePanel.tsx +241 -131
- package/packages/visualizer/src/app/gazetteer.module.css +0 -107
- package/packages/visualizer/src/components/ActivityIndicator.module.css +0 -4
- package/packages/visualizer/src/components/DataIssues.module.css +0 -136
- package/packages/visualizer/src/components/Header.module.css +0 -114
- package/packages/visualizer/src/components/Legend.module.css +0 -84
- package/packages/visualizer/src/components/Screen.module.css +0 -312
- package/packages/visualizer/src/components/Status.module.css +0 -116
- package/packages/visualizer/src/components/icons.tsx +0 -156
- package/packages/visualizer/src/evidence/artifacts.module.css +0 -213
- package/packages/visualizer/src/evidence/evidence.module.css +0 -907
- package/packages/visualizer/src/evidence/recording.module.css +0 -460
- package/packages/visualizer/src/evidence/sources.module.css +0 -168
- package/packages/visualizer/src/evidence/spec-snapshot.tsx +0 -26
- package/packages/visualizer/src/explorer/explorer.module.css +0 -1238
- package/packages/visualizer/src/find/Finder.module.css +0 -168
- package/packages/visualizer/src/looks/stage.module.css +0 -300
- package/packages/visualizer/src/styles/global.css +0 -105
- package/packages/visualizer/src/styles/tokens.css +0 -80
- package/packages/visualizer/src/views/views.module.css +0 -546
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
+
## 0.11.0 (2026-10-25)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- rebuild Atlas UI with Mantine and add IDE tooling (#42) ([90d4736](https://github.com/crvouga/atlas/commit/90d47369801a9cf5af78939c25a52333be1dc207))
|
|
8
|
+
|
|
9
|
+
## 0.10.2 (2026-10-24)
|
|
10
|
+
|
|
11
|
+
### Fixes and improvements
|
|
12
|
+
|
|
13
|
+
- stabilize report identity and add issue resolution skill (#19) ([1772e03](https://github.com/crvouga/atlas/commit/1772e03b6376854aecc1da692a1da87d21ebf370))
|
|
14
|
+
|
|
3
15
|
## 0.10.1 (2026-10-24)
|
|
4
16
|
|
|
5
17
|
### Fixes and improvements
|
package/README.md
CHANGED
|
@@ -48,6 +48,13 @@ version, running without a server, connecting real suites, and proving completio
|
|
|
48
48
|
is a copyable walkthrough with expected JSON and exit codes; it needs no credentials or existing test suite.
|
|
49
49
|
The machine-readable index is [llms.txt](llms.txt), and [llms-full.txt](llms-full.txt) contains the guides in one file.
|
|
50
50
|
|
|
51
|
+
## Develop in an agent IDE
|
|
52
|
+
|
|
53
|
+
Superset, super.engineering and Orca lifecycle configuration is checked in. Run `bun run workspace:setup`,
|
|
54
|
+
then use the IDE's Run target or `bun run workspace:run` for the API, settings and hot-reloading viewer.
|
|
55
|
+
Use `bun run workspace:preview` for populated examples, and `bun run workspace:status` for this checkout's
|
|
56
|
+
actual URLs. [Workspace development](docs/workspaces.md) covers ports, cleanup and agent workflows.
|
|
57
|
+
|
|
51
58
|
## Run this repository's example
|
|
52
59
|
|
|
53
60
|
For contributors working in an Atlas checkout with dependencies installed (`bun install --frozen-lockfile`).
|
|
@@ -128,5 +128,9 @@ export declare function distinctHeaders<T extends {
|
|
|
128
128
|
export declare function distinctEvidence(layers: readonly LayerEvidence[]): LayerEvidence[];
|
|
129
129
|
/** Drops each mutable layer whose execution is already archived as an immutable one, and keeps one summary per id. */
|
|
130
130
|
export declare function distinctLayers(layers: readonly LayerSummary[]): LayerSummary[];
|
|
131
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* A string equal for the mutable and the archived address of one execution: its producer, sha, time and parts.
|
|
133
|
+
* Object key order is ignored at every depth, including check evidence and extensions; array order is preserved.
|
|
134
|
+
* This is an advisory execution identity, not the content digest that addresses an immutable layer in storage.
|
|
135
|
+
*/
|
|
132
136
|
export declare function layerExecutionKey(layer: LayerSummary): string;
|
|
@@ -269,7 +269,22 @@ export function distinctLayers(layers) {
|
|
|
269
269
|
const kept = new Set(layers.filter(layer => archived(layer.id)).map(layerExecutionKey));
|
|
270
270
|
return [...new Map(layers.filter(layer => archived(layer.id) || !kept.has(layerExecutionKey(layer))).map(layer => [layer.id, layer])).values()];
|
|
271
271
|
}
|
|
272
|
-
/**
|
|
272
|
+
/**
|
|
273
|
+
* A string equal for the mutable and the archived address of one execution: its producer, sha, time and parts.
|
|
274
|
+
* Object key order is ignored at every depth, including check evidence and extensions; array order is preserved.
|
|
275
|
+
* This is an advisory execution identity, not the content digest that addresses an immutable layer in storage.
|
|
276
|
+
*/
|
|
273
277
|
export function layerExecutionKey(layer) {
|
|
274
|
-
return JSON.stringify([layer.producer, layer.sha, layer.recordedAtMs, layer.parts]);
|
|
278
|
+
return JSON.stringify([layer.producer, layer.sha, layer.recordedAtMs, orderedValue(layer.parts)]);
|
|
279
|
+
}
|
|
280
|
+
/** Sort JSON objects recursively without changing array order or interpreting extension keys as prototypes. */
|
|
281
|
+
function orderedValue(value) {
|
|
282
|
+
if (Array.isArray(value))
|
|
283
|
+
return value.map(orderedValue);
|
|
284
|
+
if (value === null || typeof value !== 'object')
|
|
285
|
+
return value;
|
|
286
|
+
const ordered = Object.create(null);
|
|
287
|
+
for (const key of Object.keys(value).sort())
|
|
288
|
+
ordered[key] = orderedValue(value[key]);
|
|
289
|
+
return ordered;
|
|
275
290
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export declare const SETTINGS_PAGE = "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Settings \u00B7 Atlas</title>\n<style>\n:root{--font:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;--mono:ui-monospace,monospace;--bg:#fff;--bg-2:#f7f9f9;--ink:#0f1419;--ink-3:#536471;--line:#eff3f4;--line-2:#cfd9de;--place:#0b63ce;--place-soft:#e8f1fd;--passed-ink:#116329;--passed-soft:#e6f4ea;--failed-ink:#a40e26;--failed-soft:#ffebe9;--focus:#0b63ce}\n/* atlas-client-tokens */\n*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 var(--font);-webkit-font-smoothing:antialiased}a{color:var(--place);text-decoration:none}button,input,select,textarea{font:inherit}button,a,input,select,textarea,summary{outline-offset:3px}:focus-visible{outline:2px solid var(--focus)}[hidden]{display:none!important}\n.topbar{height:54px;display:flex;align-items:center;gap:16px;padding:0 24px;border-bottom:1px solid var(--line);background:var(--bg)}.brand{font-size:15px;font-weight:700;color:var(--ink)}.crumb{display:flex;align-items:center;gap:16px;color:var(--ink-3)}.crumb span:first-child{color:var(--line-2)}.back{margin-left:auto;font-size:13px;color:var(--ink-3)}\n.layout{display:grid;grid-template-columns:180px minmax(0,760px);gap:48px;max-width:1080px;padding:40px 24px 64px;margin:auto}.side{position:sticky;top:24px;align-self:start}.side p{margin:0 0 12px 10px;color:var(--ink-3);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.side a{display:block;border-radius:6px;padding:8px 10px;color:var(--ink-3);font-size:13px}.side a:hover{background:var(--bg-2);color:var(--ink)}main{min-width:0}h1{margin:0 0 8px;font-size:26px;line-height:1.25;letter-spacing:-.025em}h2{font-size:16px;margin:0 0 4px}h3{font-size:13px;margin:0 0 8px}.lead{margin:0 0 24px;color:var(--ink-3);max-width:62ch}.section{padding:24px 0;border-top:1px solid var(--line);scroll-margin-top:24px}.description{margin:0 0 18px;color:var(--ink-3);font-size:13px}.access{border:1px solid var(--line-2);border-radius:8px;padding:20px;margin-bottom:24px}.access h2{margin-bottom:6px}.access .description{margin-bottom:14px}.access-row{display:flex;align-items:flex-end;gap:10px}.access-row .field{flex:1;margin:0}.field{margin:0 0 18px;min-width:0}.field:last-child{margin-bottom:0}label{display:block;font-weight:500;font-size:13px;margin-bottom:6px}input,select,textarea{display:block;min-height:36px;width:100%;min-width:0;padding:8px 10px;border:1px solid var(--line-2);border-radius:6px;color:var(--ink);background:var(--bg);line-height:1.4}input::placeholder{color:var(--ink-3);opacity:.8}input:disabled,select:disabled,textarea:disabled{background:var(--bg-2);color:var(--ink-3)}.hint,.current{margin:6px 0 0;color:var(--ink-3);font-size:12px;overflow-wrap:anywhere}.current:empty{display:none}.grid{display:grid;grid-template-columns:1fr 1fr;gap:0 20px}fieldset{border:0;padding:0;margin:0;min-width:0}button,.button{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:34px;padding:7px 12px;border:1px solid var(--line-2);border-radius:6px;background:var(--bg);color:var(--ink);font-size:13px;font-weight:500;cursor:pointer;line-height:1.4;white-space:nowrap}button:hover:not(:disabled){background:var(--bg-2)}button:disabled{opacity:.5;cursor:default}.primary{background:var(--place);border-color:var(--place);color:#fff}.primary:hover:not(:disabled){background:var(--place);filter:brightness(.95)}.badge{font-size:11px;border-radius:4px;background:var(--passed-soft);color:var(--passed-ink);padding:3px 6px;margin-left:8px;font-weight:500}.notice{padding:12px 14px;border-radius:6px;background:var(--place-soft);font-size:13px;color:var(--ink-3);margin:0 0 20px}.notice strong{color:var(--ink)}#status:empty{display:none}#status{padding:12px 14px;border-radius:6px;font-size:13px;margin:0 0 20px;overflow-wrap:anywhere}#status[data-state=error]{background:var(--failed-soft);color:var(--failed-ink)}#status[data-state=success]{background:var(--passed-soft);color:var(--passed-ink)}#status[data-state=info]{background:var(--bg-2);color:var(--ink-3)}\n.backend{border:1px solid var(--line);border-radius:8px;padding:14px;margin:0 0 12px}.backend-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.backend-head strong{font-size:13px}.remove{border:0;color:var(--ink-3);min-height:28px;padding:4px 8px}.empty{padding:12px 0;color:var(--ink-3);font-size:13px}.savebar{display:flex;align-items:center;gap:16px;padding:20px 0;border-top:1px solid var(--line);margin-top:20px}.savebar p{margin:0;color:var(--ink-3);font-size:12px}details{border:1px solid var(--line);border-radius:6px;margin:12px 0}summary{padding:12px 14px;cursor:pointer;font-size:13px;font-weight:500}details>div{padding:0 14px 14px}textarea#settings{min-height:240px;resize:vertical;font:12px/1.6 var(--mono);tab-size:2}pre{font:12px/1.6 var(--mono);margin:0;white-space:pre-wrap;overflow-wrap:anywhere}code{font-family:var(--mono);font-size:12px}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}\n#load[aria-busy=true]::before{content:\"\";width:10px;height:10px;border:1.5px solid currentColor;border-right-color:transparent;border-bottom-color:transparent;border-radius:50%;opacity:.65;animation:working 1.8s linear infinite}@keyframes working{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){#load[aria-busy=true]::before{animation:none}}\n@media(max-width:760px){.topbar{padding:0 16px}.layout{display:block;padding:24px 16px 40px}.side{position:static;display:flex;flex-wrap:wrap;gap:4px;margin-bottom:24px}.side p{display:none}.side a{padding:6px 10px}.grid{grid-template-columns:1fr;gap:0}.access-row{align-items:stretch;flex-direction:column}.savebar{align-items:flex-start;flex-direction:column;gap:10px}h1{font-size:24px}}\n</style></head><body>\n<header class=\"topbar\"><a class=\"brand report-link\" href=\"/\">Atlas</a><div class=\"crumb\"><span aria-hidden=\"true\">\u203A</span><span>Settings</span></div><a class=\"back report-link\" href=\"/\">\u2190 Back to reports</a></header>\n<div class=\"layout\"><nav class=\"side\" aria-label=\"Settings sections\"><p>Server settings</p><a href=\"#access\">Access</a><a href=\"#storage\">Storage</a><a href=\"#live\">Live updates</a><a href=\"#rules\">Report rules</a><a href=\"#advanced\">Advanced</a></nav>\n<main><h1>Atlas server settings</h1><p class=\"lead\">Configure how this Atlas node stores reports and tracks test runs. These settings apply to every project on this node.</p>\n<section class=\"access\" id=\"access\"><h2>Administrator access <span id=\"connected\" class=\"badge\" hidden>Connected</span></h2><p class=\"description\">Enter an admin token to view and edit settings. For a local server without authentication, leave it blank.</p><div class=\"access-row\"><div class=\"field\"><label for=\"token\">Admin token</label><input id=\"token\" type=\"password\" autocomplete=\"off\" spellcheck=\"false\" placeholder=\"Enter your admin token\" aria-describedby=\"token-hint\"></div><button id=\"load\" type=\"button\">Load settings</button></div><p class=\"hint\" id=\"token-hint\">Your token stays in this page and is never saved in browser storage.</p></section>\n<p id=\"status\" role=\"status\" aria-live=\"polite\"></p>\n<div id=\"configuration\" hidden>\n<p class=\"notice\"><strong>Changes apply after a server restart.</strong> Environment variables and command-line flags can override saved settings. Current values are shown under each field.</p>\n<form id=\"form\"><fieldset id=\"fields\" disabled>\n<section class=\"section\" id=\"storage\"><h2>Storage</h2><p class=\"description\">Choose where reports are stored. Changing a location does not move existing reports.</p>\n<div class=\"field\"><label for=\"data\">Local data folder</label><input id=\"data\" type=\"text\" placeholder=\"./atlas-data\"><p class=\"hint\">The node's local storage. Relative paths start from the server configuration directory.</p><p class=\"current\" id=\"data-current\"></p></div>\n<div class=\"field\"><label for=\"write\">Upload destination</label><select id=\"write\"><option value=\"\">Use default destination</option><option value=\"data\">Local data folder</option></select><p class=\"hint\">New uploads go here. Reports are read from all configured storage locations.</p><p class=\"current\" id=\"write-current\"></p></div>\n<h3>Additional storage</h3><p class=\"description\">Add a folder, S3-compatible bucket, or another Atlas server. Keep credentials in the server environment.</p><div id=\"backends\"></div><p class=\"empty\" id=\"no-backends\">No additional storage configured. Atlas uses its local data folder.</p><button id=\"add-backend\" type=\"button\">Add storage</button>\n</section>\n<section class=\"section\" id=\"live\"><h2>Live updates</h2><p class=\"description\">Control when silent or stalled test runs are marked interrupted.</p><div class=\"grid\">\n<div class=\"field\"><label for=\"runTimeoutSeconds\">Missing heartbeat timeout (seconds)</label><input id=\"runTimeoutSeconds\" type=\"number\" min=\"10\" max=\"3600\" step=\"any\"><p class=\"hint\">Interrupt a run when its producer stops checking in. Default: 30 seconds.</p><p class=\"current\" id=\"runTimeoutSeconds-current\"></p></div>\n<div class=\"field\"><label for=\"runStallSeconds\">No-progress timeout (seconds)</label><input id=\"runStallSeconds\" type=\"number\" min=\"0\" max=\"86400\" step=\"any\"><p class=\"hint\">Interrupt runs that stop making progress. Use 0 to disable. Default: 900 seconds.</p><p class=\"current\" id=\"runStallSeconds-current\"></p></div>\n<div class=\"field\"><label for=\"refreshSeconds\">Storage refresh interval (seconds)</label><input id=\"refreshSeconds\" type=\"number\" min=\"1\" step=\"any\"><p class=\"hint\">Check for reports written outside this node. Default: 10 seconds.</p><p class=\"current\" id=\"refreshSeconds-current\"></p></div>\n<div class=\"field\"><label for=\"heartbeatSeconds\">Client keepalive interval (seconds)</label><input id=\"heartbeatSeconds\" type=\"number\" min=\"1\" step=\"any\"><p class=\"hint\">Keep browser connections open between updates. Default: 15 seconds.</p><p class=\"current\" id=\"heartbeatSeconds-current\"></p></div>\n</div></section>\n<section class=\"section\" id=\"rules\"><h2>Report rules</h2><p class=\"description\">Choose which uploaded results contribute to the combined report.</p>\n<div class=\"field\"><label for=\"outlineBranch\">Preferred specification branch</label><input id=\"outlineBranch\" type=\"text\" placeholder=\"Automatic\"><p class=\"hint\">Leave blank to prefer main, then master, then the newest available specs.</p><p class=\"current\" id=\"outlineBranch-current\"></p></div>\n<div class=\"field\"><label for=\"branches\">Include branches</label><input id=\"branches\" type=\"text\" placeholder=\"All branches\"><p class=\"hint\">Comma-separated branch names. Leave blank to include every branch.</p><p class=\"current\" id=\"branches-current\"></p></div>\n<div class=\"field\"><label for=\"ignoreModified\">Results from modified checkouts</label><select id=\"ignoreModified\"><option value=\"\">Use default (include)</option><option value=\"false\">Include</option><option value=\"true\">Exclude</option></select><p class=\"current\" id=\"ignoreModified-current\"></p></div>\n</section>\n<section class=\"section\" id=\"advanced\"><h2>Advanced</h2><p class=\"description\">Request limits, logging, and the complete editable configuration.</p><div class=\"grid\">\n<div class=\"field\"><label for=\"maxObjectMiB\">Maximum upload size (MiB)</label><input id=\"maxObjectMiB\" type=\"number\" min=\"0.001\" step=\"any\"><p class=\"current\" id=\"maxObjectMiB-current\"></p></div>\n<div class=\"field\"><label for=\"maxJsonMiB\">Maximum JSON size (MiB)</label><input id=\"maxJsonMiB\" type=\"number\" min=\"0.001\" step=\"any\"><p class=\"current\" id=\"maxJsonMiB-current\"></p></div>\n<div class=\"field\"><label for=\"maxStreams\">Maximum live connections</label><input id=\"maxStreams\" type=\"number\" min=\"1\" step=\"1\"><p class=\"current\" id=\"maxStreams-current\"></p></div>\n<div class=\"field\"><label for=\"log\">Request logging</label><select id=\"log\"><option value=\"\">Use default (enabled)</option><option value=\"true\">Enabled</option><option value=\"false\">Disabled</option></select><p class=\"current\" id=\"log-current\"></p></div>\n</div><details id=\"json-editor\"><summary>Edit configuration as JSON</summary><div><p class=\"description\">This is the same saved configuration as the form above. Omitted values use defaults after restart.</p><label for=\"settings\" class=\"visually-hidden\">Storage and report settings (JSON)</label><textarea id=\"settings\" spellcheck=\"false\" disabled></textarea></div></details>\n<details><summary>View currently active settings</summary><div><p class=\"description\">The configuration running now, including environment and command-line overrides.</p><pre id=\"active\"></pre></div></details>\n</section></fieldset><footer class=\"savebar\"><button id=\"save\" class=\"primary\" type=\"button\" disabled>Save changes</button><p>Saved to <code>atlas.server.json</code>. Restart Atlas to apply.</p></footer></form></div>\n</main></div>\n<script type=\"module\">\nconst el=id=>document.getElementById(id);\nconst fields=[['data',['data']],['outlineBranch',['outlineBranch']],['runTimeoutSeconds',['runTimeoutSeconds']],['runStallSeconds',['runStallSeconds']],['refreshSeconds',['refreshSeconds']],['heartbeatSeconds',['heartbeatSeconds']],['branches',['include','branches']],['ignoreModified',['include','ignoreModified']],['log',['log']],['maxObjectMiB',['limits','maxObjectMiB']],['maxJsonMiB',['limits','maxJsonMiB']],['maxStreams',['limits','maxStreams']]];\nlet revision, draft={}, active={}, stale=false, busy=false;\nconst at=(object,path)=>path.reduce((value,key)=>value&&typeof value==='object'?value[key]:undefined,object);\nconst copy=value=>JSON.parse(JSON.stringify(value));\nfunction set(object,path,value){let held=object;for(const key of path.slice(0,-1)){if(!held[key]||typeof held[key]!=='object')held[key]={};held=held[key];}if(value===undefined)delete held[path.at(-1)];else held[path.at(-1)]=value;}\nfunction message(text,state='info'){el('status').textContent=text;el('status').dataset.state=state;el('status').setAttribute('role',state==='error'?'alert':'status');}\nfunction option(text,value){const node=document.createElement('option');node.textContent=text;node.value=value;return node;}\nfunction destinations(){const select=el('write'), value=select.value;select.replaceChildren(option('Use default destination',''),option('Local data folder','data'));for(const row of el('backends').children){const id=row.querySelector('[data-key=id]').value.trim();if(id){const label=row.querySelector('[data-key=label]').value.trim();select.add(option(label?label+' ('+id+')':id,id));}}if([...select.options].some(option=>option.value===value))select.value=value;}\nfunction addBackend(backend={}){const row=document.createElement('div');row.className='backend';if(backend.write!==undefined)row.dataset.write=String(backend.write);const head=document.createElement('div');head.className='backend-head';const title=document.createElement('strong');title.textContent='Storage location';const remove=document.createElement('button');remove.type='button';remove.className='remove';remove.textContent='Remove';remove.onclick=()=>{row.remove();destinations();el('no-backends').hidden=el('backends').children.length>0;sync();};head.append(title,remove);row.append(head);const group=document.createElement('div');group.className='grid';for(const [key,label,placeholder] of [['id','Storage ID','archive'],['label','Display name (optional)','Archive'],['url','Location','s3://reports/atlas']]){const field=document.createElement('div');field.className='field';const input=document.createElement('input');input.type='text';input.dataset.key=key;input.value=backend[key]??'';input.placeholder=placeholder;input.id='backend-'+key+'-'+Math.random().toString(36).slice(2);input.required=key!=='label';if(key==='id')input.pattern='[a-z0-9-]+';const caption=document.createElement('label');caption.htmlFor=input.id;caption.textContent=label;field.append(caption,input);if(key==='url')row.append(field);else group.append(field);}row.insertBefore(group,row.children[1]??null);el('backends').append(row);el('no-backends').hidden=true;destinations();}\nfunction fill(settings){draft=copy(settings);for(const [id,path]of fields){const value=at(settings,path);el(id).value=Array.isArray(value)?value.join(', '):value===undefined?'':String(value);}el('backends').replaceChildren();for(const backend of settings.backends??[])addBackend(backend);el('no-backends').hidden=el('backends').children.length>0;destinations();if(settings.write&&!Array.from(el('write').options).some(option=>option.value===settings.write))el('write').add(option(settings.write,settings.write));el('write').value=settings.write??'';}\nfunction formSettings(){const value=copy(draft);for(const [id,path]of fields){const input=el(id),text=input.value.trim();set(value,path,text===''?undefined:id==='branches'?text.split(',').map(item=>item.trim()).filter(Boolean):input.type==='number'?Number(text):id==='log'||id==='ignoreModified'?text==='true':text);}for(const key of ['include','limits'])if(value[key]&&Object.keys(value[key]).length===0)delete value[key];const backends=[...el('backends').children].map(row=>{const backend={};for(const input of row.querySelectorAll('[data-key]')){const text=input.value.trim();if(text||input.dataset.key!=='label')backend[input.dataset.key]=text;}if(row.dataset.write!==undefined)backend.write=row.dataset.write==='true';return backend;});if(backends.length||draft.backends!==undefined)value.backends=backends;else delete value.backends;if(el('write').value)value.write=el('write').value;else delete value.write;return value;}\nfunction sync(){el('settings').value=JSON.stringify(formSettings(),null,2);}\nfunction currentValues(){for(const [id,path]of fields){const value=at(active,path);el(id+'-current').textContent=value===undefined?'':'Current: '+(Array.isArray(value)?value.join(', ')||'All branches':String(value));if(el(id).tagName==='INPUT'&&value!==undefined&&el(id).value==='')el(id).placeholder=String(value);}el('write-current').textContent='Current: '+(active.write??'data');}\nfunction controls(){el('load').setAttribute('aria-busy',String(busy));el('load').disabled=busy;el('load').textContent=busy?'Working\u2026':revision?'Reload settings':'Load settings';el('save').disabled=busy||!revision||stale;el('fields').disabled=busy||!revision;el('settings').disabled=busy||!revision;}\nasync function request(method){const token=el('token').value;const headers=token?{authorization:'Bearer '+token}:{};let body;if(method==='PUT'){const parsed=JSON.parse(el('settings').value);if(!parsed||typeof parsed!=='object'||Array.isArray(parsed))throw Error('Settings must be a JSON object.');headers['content-type']='application/json';headers['if-match']='\"'+revision+'\"';body=JSON.stringify(parsed);}const response=await fetch('/api/settings',{method,headers,body,cache:'no-store'});const data=await response.json();if(!response.ok){if(response.status===409||response.status===428)stale=true;throw Error(data.detail||data.error||'Request failed ('+response.status+')');}revision=data.revision;stale=false;active=data.active;fill(data.settings);el('settings').value=JSON.stringify(data.settings,null,2);el('active').textContent=JSON.stringify(data.active,null,2);currentValues();el('configuration').hidden=false;el('connected').hidden=false;message(method==='PUT'||data.restartRequired?'Saved. Restart Atlas to apply.':'Settings loaded. Edit saved values below; blank fields use defaults after restart.',method==='PUT'||data.restartRequired?'success':'info');}\nfor(const [id,method]of [['load','GET'],['save','PUT']])el(id).onclick=async()=>{if(busy||(method==='PUT'&&(!revision||stale)))return;if(method==='PUT'&&!el('json-editor').open&&!el('form').reportValidity()){message('Check the highlighted fields before saving.','error');return;}if(method==='GET'){revision=undefined;el('connected').hidden=true;}busy=true;controls();try{await request(method);}catch(error){message(error instanceof SyntaxError?'The JSON configuration is invalid. Fix it before saving.':error.message,'error');}finally{busy=false;controls();}};\nel('form').onsubmit=event=>{event.preventDefault();if(!el('save').disabled)el('save').click();};\nel('fields').addEventListener('input',event=>{if(event.target===el('settings'))return;if(event.target.dataset.key==='id'||event.target.dataset.key==='label')destinations();sync();});\nel('fields').addEventListener('change',event=>{if(event.target===el('settings'))return;if(event.target===el('write'))for(const row of el('backends').children)delete row.dataset.write;sync();});\nel('settings').addEventListener('input',()=>{try{const settings=JSON.parse(el('settings').value);if(settings&&typeof settings==='object'&&!Array.isArray(settings))fill(settings);}catch{}});\nel('add-backend').onclick=()=>{addBackend();sync();el('backends').lastElementChild.querySelector('input').focus();};\nconst project=new URL(location.href).searchParams.get('project');if(project&&/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(project)&&project.length<=64)for(const link of document.querySelectorAll('.report-link'))link.href='/?project='+encodeURIComponent(project)+'#/';\n</script></body></html>";
|
|
1
|
+
/** Document shell for the shared React settings UI. No configuration or credentials are embedded. */
|
|
2
|
+
export declare const SETTINGS_PAGE = "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Settings \u00B7 Atlas</title><!-- atlas-admin-styles --></head><body><div id=\"root\"><h1>Atlas server settings</h1><p>Loading settings interface\u2026</p></div><!-- atlas-admin-script --></body></html>";
|
|
3
3
|
/**
|
|
4
|
-
* Render
|
|
5
|
-
*
|
|
4
|
+
* Render the settings UI with the same provider and theme as the viewer. Bundle once on first request,
|
|
5
|
+
* without a prebuilt viewer, CDN, configuration data, or import-time work. A failed build can be retried.
|
|
6
6
|
* @example
|
|
7
7
|
* const html = await renderSettingsPage();
|
|
8
8
|
*/
|
|
@@ -1,93 +1,32 @@
|
|
|
1
1
|
/** @module Settings page
|
|
2
|
-
*
|
|
2
|
+
* The administrator's React and Mantine entry point, bundled lazily for API-only servers too.
|
|
3
3
|
* Credentials remain in page memory; the settings API owns authorization and validation.
|
|
4
4
|
*/
|
|
5
|
-
import { readFile } from 'node:fs/promises';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { visualizerRoot } from './site.js';
|
|
8
|
-
/**
|
|
9
|
-
export const SETTINGS_PAGE = `<!doctype html>
|
|
10
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Settings · Atlas</title>
|
|
11
|
-
<style>
|
|
12
|
-
:root{--font:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;--mono:ui-monospace,monospace;--bg:#fff;--bg-2:#f7f9f9;--ink:#0f1419;--ink-3:#536471;--line:#eff3f4;--line-2:#cfd9de;--place:#0b63ce;--place-soft:#e8f1fd;--passed-ink:#116329;--passed-soft:#e6f4ea;--failed-ink:#a40e26;--failed-soft:#ffebe9;--focus:#0b63ce}
|
|
13
|
-
/* atlas-client-tokens */
|
|
14
|
-
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 var(--font);-webkit-font-smoothing:antialiased}a{color:var(--place);text-decoration:none}button,input,select,textarea{font:inherit}button,a,input,select,textarea,summary{outline-offset:3px}:focus-visible{outline:2px solid var(--focus)}[hidden]{display:none!important}
|
|
15
|
-
.topbar{height:54px;display:flex;align-items:center;gap:16px;padding:0 24px;border-bottom:1px solid var(--line);background:var(--bg)}.brand{font-size:15px;font-weight:700;color:var(--ink)}.crumb{display:flex;align-items:center;gap:16px;color:var(--ink-3)}.crumb span:first-child{color:var(--line-2)}.back{margin-left:auto;font-size:13px;color:var(--ink-3)}
|
|
16
|
-
.layout{display:grid;grid-template-columns:180px minmax(0,760px);gap:48px;max-width:1080px;padding:40px 24px 64px;margin:auto}.side{position:sticky;top:24px;align-self:start}.side p{margin:0 0 12px 10px;color:var(--ink-3);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.side a{display:block;border-radius:6px;padding:8px 10px;color:var(--ink-3);font-size:13px}.side a:hover{background:var(--bg-2);color:var(--ink)}main{min-width:0}h1{margin:0 0 8px;font-size:26px;line-height:1.25;letter-spacing:-.025em}h2{font-size:16px;margin:0 0 4px}h3{font-size:13px;margin:0 0 8px}.lead{margin:0 0 24px;color:var(--ink-3);max-width:62ch}.section{padding:24px 0;border-top:1px solid var(--line);scroll-margin-top:24px}.description{margin:0 0 18px;color:var(--ink-3);font-size:13px}.access{border:1px solid var(--line-2);border-radius:8px;padding:20px;margin-bottom:24px}.access h2{margin-bottom:6px}.access .description{margin-bottom:14px}.access-row{display:flex;align-items:flex-end;gap:10px}.access-row .field{flex:1;margin:0}.field{margin:0 0 18px;min-width:0}.field:last-child{margin-bottom:0}label{display:block;font-weight:500;font-size:13px;margin-bottom:6px}input,select,textarea{display:block;min-height:36px;width:100%;min-width:0;padding:8px 10px;border:1px solid var(--line-2);border-radius:6px;color:var(--ink);background:var(--bg);line-height:1.4}input::placeholder{color:var(--ink-3);opacity:.8}input:disabled,select:disabled,textarea:disabled{background:var(--bg-2);color:var(--ink-3)}.hint,.current{margin:6px 0 0;color:var(--ink-3);font-size:12px;overflow-wrap:anywhere}.current:empty{display:none}.grid{display:grid;grid-template-columns:1fr 1fr;gap:0 20px}fieldset{border:0;padding:0;margin:0;min-width:0}button,.button{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:34px;padding:7px 12px;border:1px solid var(--line-2);border-radius:6px;background:var(--bg);color:var(--ink);font-size:13px;font-weight:500;cursor:pointer;line-height:1.4;white-space:nowrap}button:hover:not(:disabled){background:var(--bg-2)}button:disabled{opacity:.5;cursor:default}.primary{background:var(--place);border-color:var(--place);color:#fff}.primary:hover:not(:disabled){background:var(--place);filter:brightness(.95)}.badge{font-size:11px;border-radius:4px;background:var(--passed-soft);color:var(--passed-ink);padding:3px 6px;margin-left:8px;font-weight:500}.notice{padding:12px 14px;border-radius:6px;background:var(--place-soft);font-size:13px;color:var(--ink-3);margin:0 0 20px}.notice strong{color:var(--ink)}#status:empty{display:none}#status{padding:12px 14px;border-radius:6px;font-size:13px;margin:0 0 20px;overflow-wrap:anywhere}#status[data-state=error]{background:var(--failed-soft);color:var(--failed-ink)}#status[data-state=success]{background:var(--passed-soft);color:var(--passed-ink)}#status[data-state=info]{background:var(--bg-2);color:var(--ink-3)}
|
|
17
|
-
.backend{border:1px solid var(--line);border-radius:8px;padding:14px;margin:0 0 12px}.backend-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.backend-head strong{font-size:13px}.remove{border:0;color:var(--ink-3);min-height:28px;padding:4px 8px}.empty{padding:12px 0;color:var(--ink-3);font-size:13px}.savebar{display:flex;align-items:center;gap:16px;padding:20px 0;border-top:1px solid var(--line);margin-top:20px}.savebar p{margin:0;color:var(--ink-3);font-size:12px}details{border:1px solid var(--line);border-radius:6px;margin:12px 0}summary{padding:12px 14px;cursor:pointer;font-size:13px;font-weight:500}details>div{padding:0 14px 14px}textarea#settings{min-height:240px;resize:vertical;font:12px/1.6 var(--mono);tab-size:2}pre{font:12px/1.6 var(--mono);margin:0;white-space:pre-wrap;overflow-wrap:anywhere}code{font-family:var(--mono);font-size:12px}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}
|
|
18
|
-
#load[aria-busy=true]::before{content:"";width:10px;height:10px;border:1.5px solid currentColor;border-right-color:transparent;border-bottom-color:transparent;border-radius:50%;opacity:.65;animation:working 1.8s linear infinite}@keyframes working{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){#load[aria-busy=true]::before{animation:none}}
|
|
19
|
-
@media(max-width:760px){.topbar{padding:0 16px}.layout{display:block;padding:24px 16px 40px}.side{position:static;display:flex;flex-wrap:wrap;gap:4px;margin-bottom:24px}.side p{display:none}.side a{padding:6px 10px}.grid{grid-template-columns:1fr;gap:0}.access-row{align-items:stretch;flex-direction:column}.savebar{align-items:flex-start;flex-direction:column;gap:10px}h1{font-size:24px}}
|
|
20
|
-
</style></head><body>
|
|
21
|
-
<header class="topbar"><a class="brand report-link" href="/">Atlas</a><div class="crumb"><span aria-hidden="true">›</span><span>Settings</span></div><a class="back report-link" href="/">← Back to reports</a></header>
|
|
22
|
-
<div class="layout"><nav class="side" aria-label="Settings sections"><p>Server settings</p><a href="#access">Access</a><a href="#storage">Storage</a><a href="#live">Live updates</a><a href="#rules">Report rules</a><a href="#advanced">Advanced</a></nav>
|
|
23
|
-
<main><h1>Atlas server settings</h1><p class="lead">Configure how this Atlas node stores reports and tracks test runs. These settings apply to every project on this node.</p>
|
|
24
|
-
<section class="access" id="access"><h2>Administrator access <span id="connected" class="badge" hidden>Connected</span></h2><p class="description">Enter an admin token to view and edit settings. For a local server without authentication, leave it blank.</p><div class="access-row"><div class="field"><label for="token">Admin token</label><input id="token" type="password" autocomplete="off" spellcheck="false" placeholder="Enter your admin token" aria-describedby="token-hint"></div><button id="load" type="button">Load settings</button></div><p class="hint" id="token-hint">Your token stays in this page and is never saved in browser storage.</p></section>
|
|
25
|
-
<p id="status" role="status" aria-live="polite"></p>
|
|
26
|
-
<div id="configuration" hidden>
|
|
27
|
-
<p class="notice"><strong>Changes apply after a server restart.</strong> Environment variables and command-line flags can override saved settings. Current values are shown under each field.</p>
|
|
28
|
-
<form id="form"><fieldset id="fields" disabled>
|
|
29
|
-
<section class="section" id="storage"><h2>Storage</h2><p class="description">Choose where reports are stored. Changing a location does not move existing reports.</p>
|
|
30
|
-
<div class="field"><label for="data">Local data folder</label><input id="data" type="text" placeholder="./atlas-data"><p class="hint">The node's local storage. Relative paths start from the server configuration directory.</p><p class="current" id="data-current"></p></div>
|
|
31
|
-
<div class="field"><label for="write">Upload destination</label><select id="write"><option value="">Use default destination</option><option value="data">Local data folder</option></select><p class="hint">New uploads go here. Reports are read from all configured storage locations.</p><p class="current" id="write-current"></p></div>
|
|
32
|
-
<h3>Additional storage</h3><p class="description">Add a folder, S3-compatible bucket, or another Atlas server. Keep credentials in the server environment.</p><div id="backends"></div><p class="empty" id="no-backends">No additional storage configured. Atlas uses its local data folder.</p><button id="add-backend" type="button">Add storage</button>
|
|
33
|
-
</section>
|
|
34
|
-
<section class="section" id="live"><h2>Live updates</h2><p class="description">Control when silent or stalled test runs are marked interrupted.</p><div class="grid">
|
|
35
|
-
<div class="field"><label for="runTimeoutSeconds">Missing heartbeat timeout (seconds)</label><input id="runTimeoutSeconds" type="number" min="10" max="3600" step="any"><p class="hint">Interrupt a run when its producer stops checking in. Default: 30 seconds.</p><p class="current" id="runTimeoutSeconds-current"></p></div>
|
|
36
|
-
<div class="field"><label for="runStallSeconds">No-progress timeout (seconds)</label><input id="runStallSeconds" type="number" min="0" max="86400" step="any"><p class="hint">Interrupt runs that stop making progress. Use 0 to disable. Default: 900 seconds.</p><p class="current" id="runStallSeconds-current"></p></div>
|
|
37
|
-
<div class="field"><label for="refreshSeconds">Storage refresh interval (seconds)</label><input id="refreshSeconds" type="number" min="1" step="any"><p class="hint">Check for reports written outside this node. Default: 10 seconds.</p><p class="current" id="refreshSeconds-current"></p></div>
|
|
38
|
-
<div class="field"><label for="heartbeatSeconds">Client keepalive interval (seconds)</label><input id="heartbeatSeconds" type="number" min="1" step="any"><p class="hint">Keep browser connections open between updates. Default: 15 seconds.</p><p class="current" id="heartbeatSeconds-current"></p></div>
|
|
39
|
-
</div></section>
|
|
40
|
-
<section class="section" id="rules"><h2>Report rules</h2><p class="description">Choose which uploaded results contribute to the combined report.</p>
|
|
41
|
-
<div class="field"><label for="outlineBranch">Preferred specification branch</label><input id="outlineBranch" type="text" placeholder="Automatic"><p class="hint">Leave blank to prefer main, then master, then the newest available specs.</p><p class="current" id="outlineBranch-current"></p></div>
|
|
42
|
-
<div class="field"><label for="branches">Include branches</label><input id="branches" type="text" placeholder="All branches"><p class="hint">Comma-separated branch names. Leave blank to include every branch.</p><p class="current" id="branches-current"></p></div>
|
|
43
|
-
<div class="field"><label for="ignoreModified">Results from modified checkouts</label><select id="ignoreModified"><option value="">Use default (include)</option><option value="false">Include</option><option value="true">Exclude</option></select><p class="current" id="ignoreModified-current"></p></div>
|
|
44
|
-
</section>
|
|
45
|
-
<section class="section" id="advanced"><h2>Advanced</h2><p class="description">Request limits, logging, and the complete editable configuration.</p><div class="grid">
|
|
46
|
-
<div class="field"><label for="maxObjectMiB">Maximum upload size (MiB)</label><input id="maxObjectMiB" type="number" min="0.001" step="any"><p class="current" id="maxObjectMiB-current"></p></div>
|
|
47
|
-
<div class="field"><label for="maxJsonMiB">Maximum JSON size (MiB)</label><input id="maxJsonMiB" type="number" min="0.001" step="any"><p class="current" id="maxJsonMiB-current"></p></div>
|
|
48
|
-
<div class="field"><label for="maxStreams">Maximum live connections</label><input id="maxStreams" type="number" min="1" step="1"><p class="current" id="maxStreams-current"></p></div>
|
|
49
|
-
<div class="field"><label for="log">Request logging</label><select id="log"><option value="">Use default (enabled)</option><option value="true">Enabled</option><option value="false">Disabled</option></select><p class="current" id="log-current"></p></div>
|
|
50
|
-
</div><details id="json-editor"><summary>Edit configuration as JSON</summary><div><p class="description">This is the same saved configuration as the form above. Omitted values use defaults after restart.</p><label for="settings" class="visually-hidden">Storage and report settings (JSON)</label><textarea id="settings" spellcheck="false" disabled></textarea></div></details>
|
|
51
|
-
<details><summary>View currently active settings</summary><div><p class="description">The configuration running now, including environment and command-line overrides.</p><pre id="active"></pre></div></details>
|
|
52
|
-
</section></fieldset><footer class="savebar"><button id="save" class="primary" type="button" disabled>Save changes</button><p>Saved to <code>atlas.server.json</code>. Restart Atlas to apply.</p></footer></form></div>
|
|
53
|
-
</main></div>
|
|
54
|
-
<script type="module">
|
|
55
|
-
const el=id=>document.getElementById(id);
|
|
56
|
-
const fields=[['data',['data']],['outlineBranch',['outlineBranch']],['runTimeoutSeconds',['runTimeoutSeconds']],['runStallSeconds',['runStallSeconds']],['refreshSeconds',['refreshSeconds']],['heartbeatSeconds',['heartbeatSeconds']],['branches',['include','branches']],['ignoreModified',['include','ignoreModified']],['log',['log']],['maxObjectMiB',['limits','maxObjectMiB']],['maxJsonMiB',['limits','maxJsonMiB']],['maxStreams',['limits','maxStreams']]];
|
|
57
|
-
let revision, draft={}, active={}, stale=false, busy=false;
|
|
58
|
-
const at=(object,path)=>path.reduce((value,key)=>value&&typeof value==='object'?value[key]:undefined,object);
|
|
59
|
-
const copy=value=>JSON.parse(JSON.stringify(value));
|
|
60
|
-
function set(object,path,value){let held=object;for(const key of path.slice(0,-1)){if(!held[key]||typeof held[key]!=='object')held[key]={};held=held[key];}if(value===undefined)delete held[path.at(-1)];else held[path.at(-1)]=value;}
|
|
61
|
-
function message(text,state='info'){el('status').textContent=text;el('status').dataset.state=state;el('status').setAttribute('role',state==='error'?'alert':'status');}
|
|
62
|
-
function option(text,value){const node=document.createElement('option');node.textContent=text;node.value=value;return node;}
|
|
63
|
-
function destinations(){const select=el('write'), value=select.value;select.replaceChildren(option('Use default destination',''),option('Local data folder','data'));for(const row of el('backends').children){const id=row.querySelector('[data-key=id]').value.trim();if(id){const label=row.querySelector('[data-key=label]').value.trim();select.add(option(label?label+' ('+id+')':id,id));}}if([...select.options].some(option=>option.value===value))select.value=value;}
|
|
64
|
-
function addBackend(backend={}){const row=document.createElement('div');row.className='backend';if(backend.write!==undefined)row.dataset.write=String(backend.write);const head=document.createElement('div');head.className='backend-head';const title=document.createElement('strong');title.textContent='Storage location';const remove=document.createElement('button');remove.type='button';remove.className='remove';remove.textContent='Remove';remove.onclick=()=>{row.remove();destinations();el('no-backends').hidden=el('backends').children.length>0;sync();};head.append(title,remove);row.append(head);const group=document.createElement('div');group.className='grid';for(const [key,label,placeholder] of [['id','Storage ID','archive'],['label','Display name (optional)','Archive'],['url','Location','s3://reports/atlas']]){const field=document.createElement('div');field.className='field';const input=document.createElement('input');input.type='text';input.dataset.key=key;input.value=backend[key]??'';input.placeholder=placeholder;input.id='backend-'+key+'-'+Math.random().toString(36).slice(2);input.required=key!=='label';if(key==='id')input.pattern='[a-z0-9-]+';const caption=document.createElement('label');caption.htmlFor=input.id;caption.textContent=label;field.append(caption,input);if(key==='url')row.append(field);else group.append(field);}row.insertBefore(group,row.children[1]??null);el('backends').append(row);el('no-backends').hidden=true;destinations();}
|
|
65
|
-
function fill(settings){draft=copy(settings);for(const [id,path]of fields){const value=at(settings,path);el(id).value=Array.isArray(value)?value.join(', '):value===undefined?'':String(value);}el('backends').replaceChildren();for(const backend of settings.backends??[])addBackend(backend);el('no-backends').hidden=el('backends').children.length>0;destinations();if(settings.write&&!Array.from(el('write').options).some(option=>option.value===settings.write))el('write').add(option(settings.write,settings.write));el('write').value=settings.write??'';}
|
|
66
|
-
function formSettings(){const value=copy(draft);for(const [id,path]of fields){const input=el(id),text=input.value.trim();set(value,path,text===''?undefined:id==='branches'?text.split(',').map(item=>item.trim()).filter(Boolean):input.type==='number'?Number(text):id==='log'||id==='ignoreModified'?text==='true':text);}for(const key of ['include','limits'])if(value[key]&&Object.keys(value[key]).length===0)delete value[key];const backends=[...el('backends').children].map(row=>{const backend={};for(const input of row.querySelectorAll('[data-key]')){const text=input.value.trim();if(text||input.dataset.key!=='label')backend[input.dataset.key]=text;}if(row.dataset.write!==undefined)backend.write=row.dataset.write==='true';return backend;});if(backends.length||draft.backends!==undefined)value.backends=backends;else delete value.backends;if(el('write').value)value.write=el('write').value;else delete value.write;return value;}
|
|
67
|
-
function sync(){el('settings').value=JSON.stringify(formSettings(),null,2);}
|
|
68
|
-
function currentValues(){for(const [id,path]of fields){const value=at(active,path);el(id+'-current').textContent=value===undefined?'':'Current: '+(Array.isArray(value)?value.join(', ')||'All branches':String(value));if(el(id).tagName==='INPUT'&&value!==undefined&&el(id).value==='')el(id).placeholder=String(value);}el('write-current').textContent='Current: '+(active.write??'data');}
|
|
69
|
-
function controls(){el('load').setAttribute('aria-busy',String(busy));el('load').disabled=busy;el('load').textContent=busy?'Working…':revision?'Reload settings':'Load settings';el('save').disabled=busy||!revision||stale;el('fields').disabled=busy||!revision;el('settings').disabled=busy||!revision;}
|
|
70
|
-
async function request(method){const token=el('token').value;const headers=token?{authorization:'Bearer '+token}:{};let body;if(method==='PUT'){const parsed=JSON.parse(el('settings').value);if(!parsed||typeof parsed!=='object'||Array.isArray(parsed))throw Error('Settings must be a JSON object.');headers['content-type']='application/json';headers['if-match']='"'+revision+'"';body=JSON.stringify(parsed);}const response=await fetch('/api/settings',{method,headers,body,cache:'no-store'});const data=await response.json();if(!response.ok){if(response.status===409||response.status===428)stale=true;throw Error(data.detail||data.error||'Request failed ('+response.status+')');}revision=data.revision;stale=false;active=data.active;fill(data.settings);el('settings').value=JSON.stringify(data.settings,null,2);el('active').textContent=JSON.stringify(data.active,null,2);currentValues();el('configuration').hidden=false;el('connected').hidden=false;message(method==='PUT'||data.restartRequired?'Saved. Restart Atlas to apply.':'Settings loaded. Edit saved values below; blank fields use defaults after restart.',method==='PUT'||data.restartRequired?'success':'info');}
|
|
71
|
-
for(const [id,method]of [['load','GET'],['save','PUT']])el(id).onclick=async()=>{if(busy||(method==='PUT'&&(!revision||stale)))return;if(method==='PUT'&&!el('json-editor').open&&!el('form').reportValidity()){message('Check the highlighted fields before saving.','error');return;}if(method==='GET'){revision=undefined;el('connected').hidden=true;}busy=true;controls();try{await request(method);}catch(error){message(error instanceof SyntaxError?'The JSON configuration is invalid. Fix it before saving.':error.message,'error');}finally{busy=false;controls();}};
|
|
72
|
-
el('form').onsubmit=event=>{event.preventDefault();if(!el('save').disabled)el('save').click();};
|
|
73
|
-
el('fields').addEventListener('input',event=>{if(event.target===el('settings'))return;if(event.target.dataset.key==='id'||event.target.dataset.key==='label')destinations();sync();});
|
|
74
|
-
el('fields').addEventListener('change',event=>{if(event.target===el('settings'))return;if(event.target===el('write'))for(const row of el('backends').children)delete row.dataset.write;sync();});
|
|
75
|
-
el('settings').addEventListener('input',()=>{try{const settings=JSON.parse(el('settings').value);if(settings&&typeof settings==='object'&&!Array.isArray(settings))fill(settings);}catch{}});
|
|
76
|
-
el('add-backend').onclick=()=>{addBackend();sync();el('backends').lastElementChild.querySelector('input').focus();};
|
|
77
|
-
const project=new URL(location.href).searchParams.get('project');if(project&&/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(project)&&project.length<=64)for(const link of document.querySelectorAll('.report-link'))link.href='/?project='+encodeURIComponent(project)+'#/';
|
|
78
|
-
</script></body></html>`;
|
|
7
|
+
/** Document shell for the shared React settings UI. No configuration or credentials are embedded. */
|
|
8
|
+
export const SETTINGS_PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Settings · Atlas</title><!-- atlas-admin-styles --></head><body><div id="root"><h1>Atlas server settings</h1><p>Loading settings interface…</p></div><!-- atlas-admin-script --></body></html>`;
|
|
79
9
|
let page;
|
|
80
10
|
/**
|
|
81
|
-
* Render
|
|
82
|
-
*
|
|
11
|
+
* Render the settings UI with the same provider and theme as the viewer. Bundle once on first request,
|
|
12
|
+
* without a prebuilt viewer, CDN, configuration data, or import-time work. A failed build can be retried.
|
|
83
13
|
* @example
|
|
84
14
|
* const html = await renderSettingsPage();
|
|
85
15
|
*/
|
|
86
16
|
export function renderSettingsPage() {
|
|
87
17
|
page ??= (async () => {
|
|
88
18
|
const root = visualizerRoot();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
19
|
+
if (!root)
|
|
20
|
+
throw new Error('The Atlas client is not installed alongside the server.');
|
|
21
|
+
const result = await Bun.build({ entrypoints: [path.join(root, 'src/admin/main.tsx')], target: 'browser', minify: true });
|
|
22
|
+
if (!result.success)
|
|
23
|
+
throw new Error(`The settings interface could not be built: ${result.logs.join('\n')}`);
|
|
24
|
+
const js = await Promise.all(result.outputs.filter((output) => output.path.endsWith('.js')).map((output) => output.text()));
|
|
25
|
+
const css = await Promise.all(result.outputs.filter((output) => output.path.endsWith('.css')).map((output) => output.text()));
|
|
26
|
+
return SETTINGS_PAGE.replace('<!-- atlas-admin-styles -->', () => `<style>${css.join('\n').replaceAll('</style', '<\\/style')}</style>`).replace('<!-- atlas-admin-script -->', () => `<script type="module">${js.join('\n').replaceAll('</script', '<\\/script')}</script>`);
|
|
27
|
+
})().catch((error) => {
|
|
28
|
+
page = undefined;
|
|
29
|
+
throw error;
|
|
30
|
+
});
|
|
92
31
|
return page;
|
|
93
32
|
}
|
package/docs/agents.md
CHANGED
|
@@ -13,6 +13,7 @@ commands provide JSON results with published schemas; the HTTP API is described
|
|
|
13
13
|
- [Standards used](#standards-used)
|
|
14
14
|
- [Where the documentation comes from](#where-the-documentation-comes-from)
|
|
15
15
|
- [An integration brief you can give an agent](#an-integration-brief-you-can-give-an-agent)
|
|
16
|
+
- [Resolve this repository's open issues](#resolve-this-repositorys-open-issues)
|
|
16
17
|
|
|
17
18
|
## Given only this repository link
|
|
18
19
|
|
|
@@ -287,3 +288,27 @@ docs/reporting-issues.md; do not edit node_modules or vendor Atlas into this pro
|
|
|
287
288
|
If server access or CI secrets are unavailable, finish the local integration and report exactly which shared
|
|
288
289
|
server/CI checks remain unverified. A synthetic pass, a generated plan, or an unavailable backend is not proof
|
|
289
290
|
that the product's real suites are integrated.
|
|
291
|
+
|
|
292
|
+
## Resolve this repository's open issues
|
|
293
|
+
|
|
294
|
+
For contributors working in this Atlas checkout, the
|
|
295
|
+
[resolve-open-issues skill](https://github.com/crvouga/atlas/blob/main/.agents/skills/resolve-open-issues/SKILL.md)
|
|
296
|
+
handles the entire open GitHub issue inventory in one PR, branch and worktree. It uses the
|
|
297
|
+
[Agent Skills format](https://agentskills.io/specification) in the
|
|
298
|
+
[shared `.agents/skills/` location](https://agentskills.io/integrate-skills#where-to-scan).
|
|
299
|
+
Discovery varies by agent; any coding agent with repository file access and GitHub access can use
|
|
300
|
+
the same instructions by reading the file directly:
|
|
301
|
+
|
|
302
|
+
```text
|
|
303
|
+
Read .agents/skills/resolve-open-issues/SKILL.md and follow it to resolve all open GitHub issues
|
|
304
|
+
in this repository in a single PR, using one branch and the current worktree. Continue through
|
|
305
|
+
implementation, validation and PR checks. Track evidence for every issue, handle attestations
|
|
306
|
+
autonomously, and report precise blockers without claiming unresolved issues are fixed.
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Use the agent's native skill invocation when it discovers `resolve-open-issues`; the file-reading
|
|
310
|
+
prompt is the portable fallback. The skill can also be copied as a whole directory into another
|
|
311
|
+
repository's skill location; it reads that repository's instructions and validation requirements.
|
|
312
|
+
Keep this workflow in the repository being fixed. Atlas consumer integrations still follow the
|
|
313
|
+
playbook above and report Atlas defects as issues. The contributor skill lives in the Git checkout;
|
|
314
|
+
it is not part of the npm package.
|
|
@@ -3340,6 +3340,8 @@ export function layerExecutionKey(layer: LayerSummary): string
|
|
|
3340
3340
|
```
|
|
3341
3341
|
|
|
3342
3342
|
A string equal for the mutable and the archived address of one execution: its producer, sha, time and parts.
|
|
3343
|
+
Object key order is ignored at every depth, including check evidence and extensions; array order is preserved.
|
|
3344
|
+
This is an advisory execution identity, not the content digest that addresses an immutable layer in storage.
|
|
3343
3345
|
|
|
3344
3346
|
### Archiving a recorded run as a layer
|
|
3345
3347
|
|
package/docs/reference/server.md
CHANGED
|
@@ -607,7 +607,7 @@ Wrap the single-project protocol in project selection. Auth, upload validation a
|
|
|
607
607
|
Source: `packages/server/src/settings-page.ts`
|
|
608
608
|
|
|
609
609
|
@module Settings page
|
|
610
|
-
|
|
610
|
+
The administrator's React and Mantine entry point, bundled lazily for API-only servers too.
|
|
611
611
|
Credentials remain in page memory; the settings API owns authorization and validation.
|
|
612
612
|
|
|
613
613
|
#### `SETTINGS_PAGE`
|
|
@@ -616,7 +616,7 @@ Credentials remain in page memory; the settings API owns authorization and valid
|
|
|
616
616
|
export const SETTINGS_PAGE = …
|
|
617
617
|
```
|
|
618
618
|
|
|
619
|
-
|
|
619
|
+
Document shell for the shared React settings UI. No configuration or credentials are embedded.
|
|
620
620
|
|
|
621
621
|
#### `renderSettingsPage`
|
|
622
622
|
|
|
@@ -624,8 +624,8 @@ The settings form and its browser controller, also usable without a built client
|
|
|
624
624
|
export function renderSettingsPage(): Promise<string>
|
|
625
625
|
```
|
|
626
626
|
|
|
627
|
-
Render
|
|
628
|
-
|
|
627
|
+
Render the settings UI with the same provider and theme as the viewer. Bundle once on first request,
|
|
628
|
+
without a prebuilt viewer, CDN, configuration data, or import-time work. A failed build can be retried.
|
|
629
629
|
|
|
630
630
|
**Example**
|
|
631
631
|
|
package/docs/report-contract.md
CHANGED
|
@@ -178,6 +178,9 @@ Consequences you can rely on:
|
|
|
178
178
|
was removed keeps its evidence, outside the outline.
|
|
179
179
|
- An immutable layer held by several backends is **one** layer, reported by the backend whose id sorts
|
|
180
180
|
first.
|
|
181
|
+
- The mutable address of an execution already held as an immutable layer is suppressed by the
|
|
182
|
+
de-duplication helpers and static export. Execution identity compares producer, commit, time and
|
|
183
|
+
parts; object key order at any depth does not matter, while array order does.
|
|
181
184
|
|
|
182
185
|
Leaving a layer, backend, environment, branch or version out of the input is how earlier outcomes are
|
|
183
186
|
inspected.
|
package/docs/ui.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Viewer UI conventions
|
|
2
|
+
|
|
3
|
+
Read this before changing the Atlas viewer or settings UI. React and Mantine are the design system.
|
|
4
|
+
Use the installed version in `package.json` (currently 9.7.3) and its [official documentation](https://mantine.dev/).
|
|
5
|
+
Look up unfamiliar interactions before writing them; component props and compound components usually solve them.
|
|
6
|
+
|
|
7
|
+
## One setup
|
|
8
|
+
|
|
9
|
+
- `packages/visualizer/src/ui/theme.tsx` owns the only theme and `AtlasThemeProvider`.
|
|
10
|
+
- Both `src/main.tsx` (reports) and `src/admin/main.tsx` (settings) use that provider and Mantine's styles.
|
|
11
|
+
- Keep Atlas's blue navigation, neutral surfaces, system typography, and light color scheme. Outcome colors
|
|
12
|
+
come from `statusColor`: green passed, red failed, orange flaky, blue running, gray unknown/unrun.
|
|
13
|
+
Always accompany color with text or a named status mark. Unknown evidence must never look passed.
|
|
14
|
+
- The theme darkens the existing Mantine outcome scales and muted text to meet small-text contrast requirements. Use the default sizes, radii, font sizes, shadows and variants. Defaults belong in the theme, not in features.
|
|
15
|
+
The spacing vocabulary is Mantine's `xs`, `sm`, `md`, `lg`, `xl`.
|
|
16
|
+
|
|
17
|
+
## Compose directly
|
|
18
|
+
|
|
19
|
+
Use `AppShell` for persistent navigation, `Container size="xl"` for report pages (`md` for settings),
|
|
20
|
+
`Stack` for vertical flow, `Group` for horizontal flow, and `SimpleGrid` for responsive metrics/forms.
|
|
21
|
+
Page headers contain a `Title order={1}`, descriptive `Text c="dimmed"`, then the page's controls.
|
|
22
|
+
Use `Title order={2}` for sections and `order={3}` for subsections; do not choose heading sizes per page.
|
|
23
|
+
|
|
24
|
+
Use Mantine components directly. Do not add wrappers for ordinary buttons, inputs, cards, menus, or dialogs.
|
|
25
|
+
Shared product compositions include `SpecHeader`, `ResultRow`, `StatusMark`, `Origin`, `Stage`, and `TestDetail`:
|
|
26
|
+
they preserve evidence semantics, provenance, URL selection, or playback behavior.
|
|
27
|
+
|
|
28
|
+
- Primary action: `Button variant="filled"`. Secondary: default `Button` (light). Quiet: `variant="subtle"`.
|
|
29
|
+
Destructive action: `color="red"`. Icon-only action: `ActionIcon` with an `aria-label`, usually a `Tooltip`.
|
|
30
|
+
- Inputs: Mantine inputs with a visible `label`, `description` when needed, and `error` for validation.
|
|
31
|
+
Use `NativeSelect` for small native option lists; keep form values controlled. Settings validation and
|
|
32
|
+
authorization stay on the server; its optional JSON editor also validates with the shared schema.
|
|
33
|
+
- Use `Menu` for navigation menus, `Popover` for view options, `Drawer` for sources/test details,
|
|
34
|
+
`Modal` for modal content, and `Spotlight` for search. Let Mantine own dismissal, focus and keyboard behavior.
|
|
35
|
+
When an action opens a drawer from a popover, close and disable the parent popover during the drawer's lifetime;
|
|
36
|
+
return focus to its persistent trigger. Do not fix overlapping focus traps by raising z-index values.
|
|
37
|
+
- Use `Tabs` for view modes and `Accordion` for disclosure. URL-backed selections stay in the existing router.
|
|
38
|
+
- Render typed router links with Mantine's `renderRoot={props => <Link {...props} ... />}`.
|
|
39
|
+
This keeps route/search type inference and native navigation semantics.
|
|
40
|
+
- Use `Table` in `Table.ScrollContainer` for wide evidence, with column headers. Long lists must retain
|
|
41
|
+
`Bounded`/show-more controls. Do not fetch unopened results' recordings eagerly.
|
|
42
|
+
- Loading: named `Loader`/`Skeleton` or explicit text with `role="status"`. Error: `Alert color="red"` and
|
|
43
|
+
a retry when possible. Empty: descriptive `Text` and a relevant action. Use built-in loading/disabled states.
|
|
44
|
+
- Spec overview links use a centered `Group`: status, flexible title/folder, then outcome and time. Keep gaps
|
|
45
|
+
explicit with theme spacing; stack metadata on narrow screens instead of crowding the title.
|
|
46
|
+
|
|
47
|
+
## Executable spec source
|
|
48
|
+
|
|
49
|
+
`SourceText` is the common source reader for Gherkin, Quint, DMN/XML, Alloy, OpenSLO/YAML and statechart
|
|
50
|
+
XML/JSON/YAML. Show it expanded near the top of document pages; preserve indentation, comments and the
|
|
51
|
+
complete file. Do not replace source with a trimmed snapshot. Mantine `CodeHighlightTabs` supplies file
|
|
52
|
+
labels, line numbers, horizontal scrolling and copying. The full file is expanded by default. Native
|
|
53
|
+
collapse/expand controls can limit long files to 70vh; the Recorded results action jumps to the existing
|
|
54
|
+
results section and moves keyboard focus there. Keep scenario selection,
|
|
55
|
+
filters, traces, decision tables and model declaration links working alongside the source.
|
|
56
|
+
|
|
57
|
+
`ui/highlight.ts` owns the one lazy Shiki adapter. It imports only the supported grammars and caches the
|
|
58
|
+
engine. The shared source composition installs that adapter when source is displayed; the Mantine theme
|
|
59
|
+
and component defaults still live only in `ui/theme.tsx`. Quint/Alloy grammar sources and licenses live in
|
|
60
|
+
[`ui/grammars/README.md`](../packages/visualizer/src/ui/grammars/README.md). Unknown formats remain plain
|
|
61
|
+
text, and parse failures must leave the source visible. Syntax highlighting never means a spec passed.
|
|
62
|
+
|
|
63
|
+
## Icons and notation marks
|
|
64
|
+
|
|
65
|
+
Use named imports from `@tabler/icons-react` for all UI glyphs. The icon package supplies SVG paths;
|
|
66
|
+
Mantine still supplies every control and interaction. Use size 16 for compact inline glyphs and size 20
|
|
67
|
+
for standalone actions, with `aria-hidden="true"` when the adjacent text or control names the action.
|
|
68
|
+
Do not draw SVG icons, use Unicode glyphs as buttons, or add another icon library. Mantine's built-in
|
|
69
|
+
icons are fine. Keep textual keyboard shortcuts and path separators as text.
|
|
70
|
+
|
|
71
|
+
`SpecLogo` maps every `SpecKind` to a bundled SVG mark, used in navigation and page headers. Keep the
|
|
72
|
+
visible notation label; marks are decorative. Sources, licenses and the DMN notation-mark choice are in
|
|
73
|
+
[`spec-logos/README.md`](../packages/visualizer/src/components/spec-logos/README.md). Add a sourced SVG
|
|
74
|
+
and update this mapping when adding a format. Brand artwork preserves upstream colors independently of
|
|
75
|
+
Atlas's theme. SVG data plots and statechart arrow markers are geometry, not handcrafted UI icons.
|
|
76
|
+
|
|
77
|
+
## Accessibility and responsive behavior
|
|
78
|
+
|
|
79
|
+
The shell collapses its index below Mantine's `md` breakpoint; its named Burger toggles mobile navigation.
|
|
80
|
+
Navigation closes after choosing a destination. Use responsive props and the theme's breakpoints; no custom
|
|
81
|
+
media-query system. Let controls wrap, use one form column on mobile, and keep dialogs inside the viewport.
|
|
82
|
+
Wide tables scroll inside their container. Native video/audio controls remain available.
|
|
83
|
+
|
|
84
|
+
Preserve labels, status announcements, keyboard shortcuts, selected states, meaningful headings and focus
|
|
85
|
+
restoration. Mantine respects reduced motion through the central theme. Avoid nested interactive controls.
|
|
86
|
+
Never rely on color alone, remove focus outlines, or hide an important action only to make a layout fit.
|
|
87
|
+
|
|
88
|
+
## Styling policy and exceptions
|
|
89
|
+
|
|
90
|
+
Default custom CSS: zero. First try Mantine defaults, central defaults, variants, props, layout components,
|
|
91
|
+
and theme tokens. Do not add Tailwind, Radix, another design system, CSS modules, custom token scales,
|
|
92
|
+
inline styles, or `styles` objects for ordinary UI. Document a concrete functional limitation here before
|
|
93
|
+
adding an exception; `scripts/ui-architecture.test.ts` checks these boundaries in the normal test suite.
|
|
94
|
+
|
|
95
|
+
The central List Styles API adjustment makes item labels fill the available width, including Mantine's column wrappers;
|
|
96
|
+
List has no full-width label prop. This keeps all report rows usable without feature styles.
|
|
97
|
+
|
|
98
|
+
The narrowly scoped exceptions are:
|
|
99
|
+
|
|
100
|
+
1. **Interactive statechart graph:** React Flow and ELK provide graph layout, pan/zoom, edges and minimap,
|
|
101
|
+
which Mantine does not supply. Graph nodes and controls use Mantine. `src/map/map.module.css` only covers
|
|
102
|
+
SVG strokes, graph handles, edge-label geometry and graph selection/dimming. The two edge-label transforms
|
|
103
|
+
and node dimensions are computed by the layout engine, not visual design tokens.
|
|
104
|
+
2. **Captured evidence:** native video/audio and sandboxed iframes render recorded bytes. HTML snapshots use
|
|
105
|
+
the existing isolated shadow tree and intrinsic phone coordinates; their captured styles never style Atlas.
|
|
106
|
+
Recorded click markers and intrinsic image/video dimensions are evidence coordinates, not UI spacing.
|
|
107
|
+
3. **Public compatibility APIs:** `solid-js` and `@tanstack/solid-query` remain dependencies of existing
|
|
108
|
+
report-library entry points. They render no Atlas screen. Do not import them into the viewer or add Solid UI.
|
|
109
|
+
4. **Syntax rendering:** Shiki is the syntax engine behind Mantine's official code-highlight adapter,
|
|
110
|
+
not a second component system. Its token styles and escaped HTML are generated by the highlighter.
|
|
111
|
+
The supported Shiki line transformer supplies the existing Quint `line-N` navigation anchors.
|
|
112
|
+
No feature CSS, handcrafted code controls or custom syntax color theme is introduced.
|
|
113
|
+
5. **Consumer test fixture:** `examples/todo/app` is the app Atlas tests and records, not an Atlas screen.
|
|
114
|
+
Its intentionally separate implementation and selectors are test inputs; the overhaul does not restyle it.
|
|
115
|
+
|
|
116
|
+
## Inventory and migration boundary
|
|
117
|
+
|
|
118
|
+
| Area | Screens/interactions | Mantine composition |
|
|
119
|
+
| --- | --- | --- |
|
|
120
|
+
| Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
|
|
121
|
+
| Project home | Tests/runs tabs, metrics, search, status/environment/domain filters, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, Button, Accordion |
|
|
122
|
+
| Test detail | Current outcomes, statistics, duration graph, history, artifacts, recording | Drawer, Table, Badge, Alert, Stage |
|
|
123
|
+
| Specs | All six existing kinds, overview/index, Gherkin rules/scenarios/examples, Quint declarations/traces, DMN tables, source text | SpecHeader, Accordion, Table, CodeHighlightTabs, ResultRow |
|
|
124
|
+
| Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
|
|
125
|
+
| Evidence | Steps, retries, annotations, facts, unknown extensions, safe downloads and previews | List, Badge, Accordion, Code, Image, native media |
|
|
126
|
+
| Recordings | Pictures/video/timeline, actor selection, moments, failure jumps, timing, model-state differences | Tabs, Slider, Button, ScrollArea, Table, Progress |
|
|
127
|
+
| Search/help | Ranked and semantic search, shortcut help, recording shortcuts | Spotlight, Modal, Kbd |
|
|
128
|
+
| Administration | Token access, storage backends/write destination, live timeouts, report rules, limits, raw JSON, conflicts | Same provider/theme, Fieldset, Mantine inputs, Accordion, Alert |
|
|
129
|
+
|
|
130
|
+
The hash routes (`/`, `/chart/$chartId`, `/specs/$kind`), query contracts, project scoping, streaming,
|
|
131
|
+
report APIs and data adapters remain the migration boundary. `/admin` still serves an independent document:
|
|
132
|
+
it lazily bundles the shared React settings entry once, so API-only servers do not need a prebuilt viewer.
|
|
133
|
+
|
|
134
|
+
## Checks
|
|
135
|
+
|
|
136
|
+
Run `bunx tsc --noEmit -p packages/visualizer`,
|
|
137
|
+
`bun test --cwd packages/visualizer --preload ./tests/preload.ts tests`,
|
|
138
|
+
`bun test scripts/ui-architecture.test.ts`, `bun run client:build`, then `bun run docs` and `bun run docs:check`.
|
|
139
|
+
Before pushing, run the full repository checks from `AGENTS.md`, including package build and consumer smoke
|
|
140
|
+
when dependencies change. DOM tests use the same `AtlasThemeProvider` with Mantine's supported `env="test"`.
|
|
141
|
+
Use semantic/product selectors, not Mantine's private class names. Inspect real desktop and narrow layouts,
|
|
142
|
+
menus, dialogs, complex evidence and settings in the browser; do not describe a DOM test as a visual audit.
|