@agent-inspect/studio 6.4.0 → 6.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +141 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +142 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -179,7 +179,7 @@ interface StudioServerInfo {
|
|
|
179
179
|
declare function createStudioServer(options?: StudioServerOptions): Server;
|
|
180
180
|
declare function startStudioServer(options?: StudioServerOptions): Promise<StudioServerInfo>;
|
|
181
181
|
|
|
182
|
-
declare const studioIndexHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <title>AgentInspect Studio</title>\n <style>\n body { font-family: system-ui, sans-serif; margin:
|
|
182
|
+
declare const studioIndexHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'\" />\n <title>AgentInspect Studio</title>\n <style>\n body { font-family: system-ui, sans-serif; margin: 0; line-height: 1.4; color: #111; }\n header { padding: 1rem 1.5rem; border-bottom: 1px solid #ddd; background: #fafafa; }\n nav a { margin-right: 1rem; color: #0b57d0; text-decoration: none; }\n nav a.active { font-weight: 600; text-decoration: underline; }\n main { padding: 1.5rem; max-width: 1100px; }\n .muted { color: #666; }\n table { border-collapse: collapse; width: 100%; margin: 1rem 0; }\n th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; vertical-align: top; }\n th { background: #f6f6f6; }\n pre { background: #f4f4f5; padding: 1rem; overflow: auto; max-height: 60vh; }\n .error { color: #b42318; }\n </style>\n</head>\n<body>\n <header>\n <h1>AgentInspect Studio</h1>\n <p class=\"muted\">Self-hosted, read-only. SQLite only. JSONL remains canonical.</p>\n <nav id=\"nav\">\n <a href=\"#projects\" data-page=\"projects\">Projects</a>\n <a href=\"#runs\" data-page=\"runs\">Runs</a>\n <a href=\"#sessions\" data-page=\"sessions\">Sessions</a>\n <a href=\"#suites\" data-page=\"suites\">Suites</a>\n <a href=\"#safety\" data-page=\"safety\">Safety</a>\n <a href=\"#imports\" data-page=\"imports\">Imports</a>\n <a href=\"#search\" data-page=\"search\">Search</a>\n </nav>\n </header>\n <main id=\"content\"><p class=\"muted\">Loading\u2026</p></main>\n <script>\n function escapeHtml(value) {\n return String(value ?? \"\")\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n }\n\n let state = { projects: [], projectId: null };\n\n async function api(path) {\n const res = await fetch(path);\n const data = await res.json();\n if (!res.ok) throw new Error(data.error || res.statusText);\n return data;\n }\n\n function setPage(page) {\n document.querySelectorAll(\"#nav a\").forEach((a) => {\n a.classList.toggle(\"active\", a.dataset.page === page);\n });\n }\n\n function renderProjects() {\n setPage(\"projects\");\n const rows = state.projects.map((p) =>\n '<tr><td><a href=\"#runs\" data-select-project=\"' + escapeHtml(p.id) + '\">' +\n escapeHtml(p.label || p.id) + '</a></td><td>' + escapeHtml(p.traceCount) +\n '</td><td>' + escapeHtml(p.path) + '</td></tr>'\n ).join(\"\");\n document.getElementById(\"content\").innerHTML =\n '<h2>Projects</h2><table><thead><tr><th>Project</th><th>Runs</th><th>Path</th></tr></thead><tbody>' +\n (rows || '<tr><td colspan=\"3\">No projects imported</td></tr>') + '</tbody></table>';\n document.querySelectorAll(\"[data-select-project]\").forEach((el) => {\n el.addEventListener(\"click\", (ev) => {\n ev.preventDefault();\n state.projectId = el.getAttribute(\"data-select-project\");\n location.hash = \"runs\";\n renderRuns();\n });\n });\n }\n\n async function renderRuns() {\n setPage(\"runs\");\n if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;\n if (!state.projectId) {\n document.getElementById(\"content\").innerHTML = '<p>Select a project first.</p>';\n return;\n }\n const data = await api(\"/api/projects/\" + encodeURIComponent(state.projectId) + \"/runs\");\n const rows = (data.runs || []).map((r) =>\n '<tr><td>' + escapeHtml(r.runId) + '</td><td>' + escapeHtml(r.status) +\n '</td><td>' + escapeHtml(r.name) + '</td><td>' + escapeHtml(r.durationMs) + '</td></tr>'\n ).join(\"\");\n document.getElementById(\"content\").innerHTML =\n '<h2>Runs \u2014 ' + escapeHtml(state.projectId) + '</h2>' +\n '<table><thead><tr><th>Run ID</th><th>Status</th><th>Name</th><th>Duration ms</th></tr></thead><tbody>' +\n rows + '</tbody></table>';\n }\n\n async function renderSimple(title, path) {\n setPage(title.toLowerCase());\n if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;\n const data = await api(\"/api/projects/\" + encodeURIComponent(state.projectId) + \"/\" + path);\n document.getElementById(\"content\").innerHTML =\n '<h2>' + escapeHtml(title) + '</h2><pre>' + escapeHtml(JSON.stringify(data, null, 2)) + '</pre>';\n }\n\n async function renderImports() {\n setPage(\"imports\");\n const health = await api(\"/api/health\");\n document.getElementById(\"content\").innerHTML =\n '<h2>Imports</h2><p>Registry: ' + escapeHtml(health.registryName) + '</p>' +\n '<pre>' + escapeHtml(JSON.stringify(health.warnings || [], null, 2)) + '</pre>';\n }\n\n async function renderSearch() {\n setPage(\"search\");\n if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;\n document.getElementById(\"content\").innerHTML =\n '<h2>Search</h2><p><input id=\"q\" placeholder=\"run id or name\" /> <button id=\"go\">Search</button></p><pre id=\"out\"></pre>';\n document.getElementById(\"go\").onclick = async () => {\n const q = document.getElementById(\"q\").value;\n const data = await api(\"/api/search?projectId=\" + encodeURIComponent(state.projectId) + \"&q=\" + encodeURIComponent(q));\n document.getElementById(\"out\").textContent = JSON.stringify(data, null, 2);\n };\n }\n\n async function boot() {\n const health = await api(\"/api/health\");\n const projects = await api(\"/api/projects\");\n state.projects = projects.projects || health.projects || [];\n const page = (location.hash || \"#projects\").slice(1);\n if (page === \"runs\") return renderRuns();\n if (page === \"sessions\") return renderSimple(\"Sessions\", \"sessions\");\n if (page === \"suites\") return renderSimple(\"Suites\", \"suites\");\n if (page === \"safety\") return renderSimple(\"Safety\", \"redaction\");\n if (page === \"imports\") return renderImports();\n if (page === \"search\") return renderSearch();\n return renderProjects();\n }\n\n window.addEventListener(\"hashchange\", () => boot().catch(showError));\n function showError(err) {\n document.getElementById(\"content\").innerHTML = '<p class=\"error\">' + escapeHtml(String(err)) + '</p>';\n }\n boot().catch(showError);\n </script>\n</body>\n</html>";
|
|
183
183
|
|
|
184
184
|
type StudioFetch = typeof fetch;
|
|
185
185
|
interface GitHubArtifactImportOptions {
|
package/dist/index.d.ts
CHANGED
|
@@ -179,7 +179,7 @@ interface StudioServerInfo {
|
|
|
179
179
|
declare function createStudioServer(options?: StudioServerOptions): Server;
|
|
180
180
|
declare function startStudioServer(options?: StudioServerOptions): Promise<StudioServerInfo>;
|
|
181
181
|
|
|
182
|
-
declare const studioIndexHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <title>AgentInspect Studio</title>\n <style>\n body { font-family: system-ui, sans-serif; margin:
|
|
182
|
+
declare const studioIndexHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'\" />\n <title>AgentInspect Studio</title>\n <style>\n body { font-family: system-ui, sans-serif; margin: 0; line-height: 1.4; color: #111; }\n header { padding: 1rem 1.5rem; border-bottom: 1px solid #ddd; background: #fafafa; }\n nav a { margin-right: 1rem; color: #0b57d0; text-decoration: none; }\n nav a.active { font-weight: 600; text-decoration: underline; }\n main { padding: 1.5rem; max-width: 1100px; }\n .muted { color: #666; }\n table { border-collapse: collapse; width: 100%; margin: 1rem 0; }\n th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; vertical-align: top; }\n th { background: #f6f6f6; }\n pre { background: #f4f4f5; padding: 1rem; overflow: auto; max-height: 60vh; }\n .error { color: #b42318; }\n </style>\n</head>\n<body>\n <header>\n <h1>AgentInspect Studio</h1>\n <p class=\"muted\">Self-hosted, read-only. SQLite only. JSONL remains canonical.</p>\n <nav id=\"nav\">\n <a href=\"#projects\" data-page=\"projects\">Projects</a>\n <a href=\"#runs\" data-page=\"runs\">Runs</a>\n <a href=\"#sessions\" data-page=\"sessions\">Sessions</a>\n <a href=\"#suites\" data-page=\"suites\">Suites</a>\n <a href=\"#safety\" data-page=\"safety\">Safety</a>\n <a href=\"#imports\" data-page=\"imports\">Imports</a>\n <a href=\"#search\" data-page=\"search\">Search</a>\n </nav>\n </header>\n <main id=\"content\"><p class=\"muted\">Loading\u2026</p></main>\n <script>\n function escapeHtml(value) {\n return String(value ?? \"\")\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n }\n\n let state = { projects: [], projectId: null };\n\n async function api(path) {\n const res = await fetch(path);\n const data = await res.json();\n if (!res.ok) throw new Error(data.error || res.statusText);\n return data;\n }\n\n function setPage(page) {\n document.querySelectorAll(\"#nav a\").forEach((a) => {\n a.classList.toggle(\"active\", a.dataset.page === page);\n });\n }\n\n function renderProjects() {\n setPage(\"projects\");\n const rows = state.projects.map((p) =>\n '<tr><td><a href=\"#runs\" data-select-project=\"' + escapeHtml(p.id) + '\">' +\n escapeHtml(p.label || p.id) + '</a></td><td>' + escapeHtml(p.traceCount) +\n '</td><td>' + escapeHtml(p.path) + '</td></tr>'\n ).join(\"\");\n document.getElementById(\"content\").innerHTML =\n '<h2>Projects</h2><table><thead><tr><th>Project</th><th>Runs</th><th>Path</th></tr></thead><tbody>' +\n (rows || '<tr><td colspan=\"3\">No projects imported</td></tr>') + '</tbody></table>';\n document.querySelectorAll(\"[data-select-project]\").forEach((el) => {\n el.addEventListener(\"click\", (ev) => {\n ev.preventDefault();\n state.projectId = el.getAttribute(\"data-select-project\");\n location.hash = \"runs\";\n renderRuns();\n });\n });\n }\n\n async function renderRuns() {\n setPage(\"runs\");\n if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;\n if (!state.projectId) {\n document.getElementById(\"content\").innerHTML = '<p>Select a project first.</p>';\n return;\n }\n const data = await api(\"/api/projects/\" + encodeURIComponent(state.projectId) + \"/runs\");\n const rows = (data.runs || []).map((r) =>\n '<tr><td>' + escapeHtml(r.runId) + '</td><td>' + escapeHtml(r.status) +\n '</td><td>' + escapeHtml(r.name) + '</td><td>' + escapeHtml(r.durationMs) + '</td></tr>'\n ).join(\"\");\n document.getElementById(\"content\").innerHTML =\n '<h2>Runs \u2014 ' + escapeHtml(state.projectId) + '</h2>' +\n '<table><thead><tr><th>Run ID</th><th>Status</th><th>Name</th><th>Duration ms</th></tr></thead><tbody>' +\n rows + '</tbody></table>';\n }\n\n async function renderSimple(title, path) {\n setPage(title.toLowerCase());\n if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;\n const data = await api(\"/api/projects/\" + encodeURIComponent(state.projectId) + \"/\" + path);\n document.getElementById(\"content\").innerHTML =\n '<h2>' + escapeHtml(title) + '</h2><pre>' + escapeHtml(JSON.stringify(data, null, 2)) + '</pre>';\n }\n\n async function renderImports() {\n setPage(\"imports\");\n const health = await api(\"/api/health\");\n document.getElementById(\"content\").innerHTML =\n '<h2>Imports</h2><p>Registry: ' + escapeHtml(health.registryName) + '</p>' +\n '<pre>' + escapeHtml(JSON.stringify(health.warnings || [], null, 2)) + '</pre>';\n }\n\n async function renderSearch() {\n setPage(\"search\");\n if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;\n document.getElementById(\"content\").innerHTML =\n '<h2>Search</h2><p><input id=\"q\" placeholder=\"run id or name\" /> <button id=\"go\">Search</button></p><pre id=\"out\"></pre>';\n document.getElementById(\"go\").onclick = async () => {\n const q = document.getElementById(\"q\").value;\n const data = await api(\"/api/search?projectId=\" + encodeURIComponent(state.projectId) + \"&q=\" + encodeURIComponent(q));\n document.getElementById(\"out\").textContent = JSON.stringify(data, null, 2);\n };\n }\n\n async function boot() {\n const health = await api(\"/api/health\");\n const projects = await api(\"/api/projects\");\n state.projects = projects.projects || health.projects || [];\n const page = (location.hash || \"#projects\").slice(1);\n if (page === \"runs\") return renderRuns();\n if (page === \"sessions\") return renderSimple(\"Sessions\", \"sessions\");\n if (page === \"suites\") return renderSimple(\"Suites\", \"suites\");\n if (page === \"safety\") return renderSimple(\"Safety\", \"redaction\");\n if (page === \"imports\") return renderImports();\n if (page === \"search\") return renderSearch();\n return renderProjects();\n }\n\n window.addEventListener(\"hashchange\", () => boot().catch(showError));\n function showError(err) {\n document.getElementById(\"content\").innerHTML = '<p class=\"error\">' + escapeHtml(String(err)) + '</p>';\n }\n boot().catch(showError);\n </script>\n</body>\n</html>";
|
|
183
183
|
|
|
184
184
|
type StudioFetch = typeof fetch;
|
|
185
185
|
interface GitHubArtifactImportOptions {
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFile,
|
|
1
|
+
import { readFile, access, readdir, stat, mkdir, writeFile, copyFile, rename } from 'fs/promises';
|
|
2
2
|
import path8 from 'path';
|
|
3
3
|
import { loadSessionRunRecords, buildSessionIndex, runSuite, buildRunTimeline, extractOutcomesFromTraceEvents, resolveTraceDir, searchTraces, TraceDirectory, loadTraceMetadataList } from 'agent-inspect/advanced';
|
|
4
4
|
import { resolveWorkspaceLocation, readWorkspaceManifestFile } from 'agent-inspect/workspace';
|
|
5
|
+
import { mkdirSync } from 'fs';
|
|
5
6
|
import Database from 'better-sqlite3';
|
|
6
7
|
import { timingSafeEqual, createHash } from 'crypto';
|
|
7
8
|
import { createServer } from 'http';
|
|
@@ -310,11 +311,16 @@ function isPostgresDbPath(dbPath) {
|
|
|
310
311
|
function openStudioDb(dbPath) {
|
|
311
312
|
if (isPostgresDbPath(dbPath)) {
|
|
312
313
|
throw new Error(
|
|
313
|
-
"Postgres studio databases are not
|
|
314
|
+
"Postgres studio databases are not supported; use a SQLite file path (preview only)."
|
|
314
315
|
);
|
|
315
316
|
}
|
|
316
317
|
const dir = path8.dirname(dbPath);
|
|
317
|
-
|
|
318
|
+
try {
|
|
319
|
+
mkdirSync(dir, { recursive: true });
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
322
|
+
throw new Error(`Failed to create studio database directory: ${message}`);
|
|
323
|
+
}
|
|
318
324
|
const db = new Database(dbPath);
|
|
319
325
|
db.pragma("journal_mode = WAL");
|
|
320
326
|
db.exec(SCHEMA_SQL);
|
|
@@ -968,27 +974,147 @@ var studioIndexHtml = `<!DOCTYPE html>
|
|
|
968
974
|
<html lang="en">
|
|
969
975
|
<head>
|
|
970
976
|
<meta charset="utf-8" />
|
|
977
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'" />
|
|
971
978
|
<title>AgentInspect Studio</title>
|
|
972
979
|
<style>
|
|
973
|
-
body { font-family: system-ui, sans-serif; margin:
|
|
974
|
-
|
|
980
|
+
body { font-family: system-ui, sans-serif; margin: 0; line-height: 1.4; color: #111; }
|
|
981
|
+
header { padding: 1rem 1.5rem; border-bottom: 1px solid #ddd; background: #fafafa; }
|
|
982
|
+
nav a { margin-right: 1rem; color: #0b57d0; text-decoration: none; }
|
|
983
|
+
nav a.active { font-weight: 600; text-decoration: underline; }
|
|
984
|
+
main { padding: 1.5rem; max-width: 1100px; }
|
|
975
985
|
.muted { color: #666; }
|
|
986
|
+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
987
|
+
th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; vertical-align: top; }
|
|
988
|
+
th { background: #f6f6f6; }
|
|
989
|
+
pre { background: #f4f4f5; padding: 1rem; overflow: auto; max-height: 60vh; }
|
|
990
|
+
.error { color: #b42318; }
|
|
976
991
|
</style>
|
|
977
992
|
</head>
|
|
978
993
|
<body>
|
|
979
|
-
<
|
|
980
|
-
|
|
981
|
-
|
|
994
|
+
<header>
|
|
995
|
+
<h1>AgentInspect Studio</h1>
|
|
996
|
+
<p class="muted">Self-hosted, read-only. SQLite only. JSONL remains canonical.</p>
|
|
997
|
+
<nav id="nav">
|
|
998
|
+
<a href="#projects" data-page="projects">Projects</a>
|
|
999
|
+
<a href="#runs" data-page="runs">Runs</a>
|
|
1000
|
+
<a href="#sessions" data-page="sessions">Sessions</a>
|
|
1001
|
+
<a href="#suites" data-page="suites">Suites</a>
|
|
1002
|
+
<a href="#safety" data-page="safety">Safety</a>
|
|
1003
|
+
<a href="#imports" data-page="imports">Imports</a>
|
|
1004
|
+
<a href="#search" data-page="search">Search</a>
|
|
1005
|
+
</nav>
|
|
1006
|
+
</header>
|
|
1007
|
+
<main id="content"><p class="muted">Loading\u2026</p></main>
|
|
982
1008
|
<script>
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1009
|
+
function escapeHtml(value) {
|
|
1010
|
+
return String(value ?? "")
|
|
1011
|
+
.replaceAll("&", "&")
|
|
1012
|
+
.replaceAll("<", "<")
|
|
1013
|
+
.replaceAll(">", ">")
|
|
1014
|
+
.replaceAll('"', """)
|
|
1015
|
+
.replaceAll("'", "'");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
let state = { projects: [], projectId: null };
|
|
1019
|
+
|
|
1020
|
+
async function api(path) {
|
|
1021
|
+
const res = await fetch(path);
|
|
1022
|
+
const data = await res.json();
|
|
1023
|
+
if (!res.ok) throw new Error(data.error || res.statusText);
|
|
1024
|
+
return data;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function setPage(page) {
|
|
1028
|
+
document.querySelectorAll("#nav a").forEach((a) => {
|
|
1029
|
+
a.classList.toggle("active", a.dataset.page === page);
|
|
991
1030
|
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function renderProjects() {
|
|
1034
|
+
setPage("projects");
|
|
1035
|
+
const rows = state.projects.map((p) =>
|
|
1036
|
+
'<tr><td><a href="#runs" data-select-project="' + escapeHtml(p.id) + '">' +
|
|
1037
|
+
escapeHtml(p.label || p.id) + '</a></td><td>' + escapeHtml(p.traceCount) +
|
|
1038
|
+
'</td><td>' + escapeHtml(p.path) + '</td></tr>'
|
|
1039
|
+
).join("");
|
|
1040
|
+
document.getElementById("content").innerHTML =
|
|
1041
|
+
'<h2>Projects</h2><table><thead><tr><th>Project</th><th>Runs</th><th>Path</th></tr></thead><tbody>' +
|
|
1042
|
+
(rows || '<tr><td colspan="3">No projects imported</td></tr>') + '</tbody></table>';
|
|
1043
|
+
document.querySelectorAll("[data-select-project]").forEach((el) => {
|
|
1044
|
+
el.addEventListener("click", (ev) => {
|
|
1045
|
+
ev.preventDefault();
|
|
1046
|
+
state.projectId = el.getAttribute("data-select-project");
|
|
1047
|
+
location.hash = "runs";
|
|
1048
|
+
renderRuns();
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async function renderRuns() {
|
|
1054
|
+
setPage("runs");
|
|
1055
|
+
if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;
|
|
1056
|
+
if (!state.projectId) {
|
|
1057
|
+
document.getElementById("content").innerHTML = '<p>Select a project first.</p>';
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const data = await api("/api/projects/" + encodeURIComponent(state.projectId) + "/runs");
|
|
1061
|
+
const rows = (data.runs || []).map((r) =>
|
|
1062
|
+
'<tr><td>' + escapeHtml(r.runId) + '</td><td>' + escapeHtml(r.status) +
|
|
1063
|
+
'</td><td>' + escapeHtml(r.name) + '</td><td>' + escapeHtml(r.durationMs) + '</td></tr>'
|
|
1064
|
+
).join("");
|
|
1065
|
+
document.getElementById("content").innerHTML =
|
|
1066
|
+
'<h2>Runs \u2014 ' + escapeHtml(state.projectId) + '</h2>' +
|
|
1067
|
+
'<table><thead><tr><th>Run ID</th><th>Status</th><th>Name</th><th>Duration ms</th></tr></thead><tbody>' +
|
|
1068
|
+
rows + '</tbody></table>';
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
async function renderSimple(title, path) {
|
|
1072
|
+
setPage(title.toLowerCase());
|
|
1073
|
+
if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;
|
|
1074
|
+
const data = await api("/api/projects/" + encodeURIComponent(state.projectId) + "/" + path);
|
|
1075
|
+
document.getElementById("content").innerHTML =
|
|
1076
|
+
'<h2>' + escapeHtml(title) + '</h2><pre>' + escapeHtml(JSON.stringify(data, null, 2)) + '</pre>';
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
async function renderImports() {
|
|
1080
|
+
setPage("imports");
|
|
1081
|
+
const health = await api("/api/health");
|
|
1082
|
+
document.getElementById("content").innerHTML =
|
|
1083
|
+
'<h2>Imports</h2><p>Registry: ' + escapeHtml(health.registryName) + '</p>' +
|
|
1084
|
+
'<pre>' + escapeHtml(JSON.stringify(health.warnings || [], null, 2)) + '</pre>';
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function renderSearch() {
|
|
1088
|
+
setPage("search");
|
|
1089
|
+
if (!state.projectId && state.projects[0]) state.projectId = state.projects[0].id;
|
|
1090
|
+
document.getElementById("content").innerHTML =
|
|
1091
|
+
'<h2>Search</h2><p><input id="q" placeholder="run id or name" /> <button id="go">Search</button></p><pre id="out"></pre>';
|
|
1092
|
+
document.getElementById("go").onclick = async () => {
|
|
1093
|
+
const q = document.getElementById("q").value;
|
|
1094
|
+
const data = await api("/api/search?projectId=" + encodeURIComponent(state.projectId) + "&q=" + encodeURIComponent(q));
|
|
1095
|
+
document.getElementById("out").textContent = JSON.stringify(data, null, 2);
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async function boot() {
|
|
1100
|
+
const health = await api("/api/health");
|
|
1101
|
+
const projects = await api("/api/projects");
|
|
1102
|
+
state.projects = projects.projects || health.projects || [];
|
|
1103
|
+
const page = (location.hash || "#projects").slice(1);
|
|
1104
|
+
if (page === "runs") return renderRuns();
|
|
1105
|
+
if (page === "sessions") return renderSimple("Sessions", "sessions");
|
|
1106
|
+
if (page === "suites") return renderSimple("Suites", "suites");
|
|
1107
|
+
if (page === "safety") return renderSimple("Safety", "redaction");
|
|
1108
|
+
if (page === "imports") return renderImports();
|
|
1109
|
+
if (page === "search") return renderSearch();
|
|
1110
|
+
return renderProjects();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
window.addEventListener("hashchange", () => boot().catch(showError));
|
|
1114
|
+
function showError(err) {
|
|
1115
|
+
document.getElementById("content").innerHTML = '<p class="error">' + escapeHtml(String(err)) + '</p>';
|
|
1116
|
+
}
|
|
1117
|
+
boot().catch(showError);
|
|
992
1118
|
</script>
|
|
993
1119
|
</body>
|
|
994
1120
|
</html>`;
|