@openqa/cli 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/agent/index-v2.js +277 -92
- package/dist/agent/index-v2.js.map +1 -1
- package/dist/agent/index.js +109 -118
- package/dist/agent/index.js.map +1 -1
- package/dist/cli/daemon.js +1045 -635
- package/dist/cli/dashboard.html.js +53 -16
- package/dist/cli/env.html.js +717 -529
- package/dist/cli/index.js +762 -537
- package/dist/cli/server.js +762 -537
- package/package.json +1 -1
package/dist/cli/daemon.js
CHANGED
|
@@ -861,9 +861,9 @@ function createQuickConfig(name, description, url, options) {
|
|
|
861
861
|
// agent/brain/index.ts
|
|
862
862
|
init_esm_shims();
|
|
863
863
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
864
|
-
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, existsSync as
|
|
865
|
-
import { join as
|
|
866
|
-
import { execSync } from "child_process";
|
|
864
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
865
|
+
import { join as join4 } from "path";
|
|
866
|
+
import { execSync as execSync2 } from "child_process";
|
|
867
867
|
|
|
868
868
|
// agent/brain/llm-resilience.ts
|
|
869
869
|
init_esm_shims();
|
|
@@ -1101,10 +1101,150 @@ var ResilientLLM = class extends EventEmitter {
|
|
|
1101
1101
|
}
|
|
1102
1102
|
};
|
|
1103
1103
|
|
|
1104
|
+
// agent/brain/diff-analyzer.ts
|
|
1105
|
+
init_esm_shims();
|
|
1106
|
+
import { execSync } from "child_process";
|
|
1107
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1108
|
+
import { join as join3, basename, dirname as dirname2 } from "path";
|
|
1109
|
+
var DiffAnalyzer = class {
|
|
1110
|
+
/**
|
|
1111
|
+
* Get files changed between current branch and base branch
|
|
1112
|
+
*/
|
|
1113
|
+
getChangedFiles(repoPath, baseBranch = "main") {
|
|
1114
|
+
try {
|
|
1115
|
+
const output = execSync(`git diff --name-only ${baseBranch}...HEAD`, {
|
|
1116
|
+
cwd: repoPath,
|
|
1117
|
+
stdio: "pipe"
|
|
1118
|
+
}).toString().trim();
|
|
1119
|
+
if (!output) return [];
|
|
1120
|
+
return output.split("\n").filter(Boolean);
|
|
1121
|
+
} catch {
|
|
1122
|
+
try {
|
|
1123
|
+
const output = execSync("git diff --name-only HEAD~1", {
|
|
1124
|
+
cwd: repoPath,
|
|
1125
|
+
stdio: "pipe"
|
|
1126
|
+
}).toString().trim();
|
|
1127
|
+
if (!output) return [];
|
|
1128
|
+
return output.split("\n").filter(Boolean);
|
|
1129
|
+
} catch {
|
|
1130
|
+
return [];
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Map changed source files to their likely test files
|
|
1136
|
+
*/
|
|
1137
|
+
mapFilesToTests(changedFiles, repoPath) {
|
|
1138
|
+
const testFiles = /* @__PURE__ */ new Set();
|
|
1139
|
+
for (const file of changedFiles) {
|
|
1140
|
+
if (!this.isSourceFile(file)) continue;
|
|
1141
|
+
const candidates = this.getTestCandidates(file);
|
|
1142
|
+
for (const candidate of candidates) {
|
|
1143
|
+
if (existsSync2(join3(repoPath, candidate))) {
|
|
1144
|
+
testFiles.add(candidate);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
for (const file of changedFiles) {
|
|
1149
|
+
if (this.isTestFile(file)) {
|
|
1150
|
+
testFiles.add(file);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return Array.from(testFiles);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Analyze a diff and return risk assessment + affected tests
|
|
1157
|
+
*/
|
|
1158
|
+
analyze(repoPath, baseBranch = "main") {
|
|
1159
|
+
const changedFiles = this.getChangedFiles(repoPath, baseBranch);
|
|
1160
|
+
const affectedTests = this.mapFilesToTests(changedFiles, repoPath);
|
|
1161
|
+
const riskLevel = this.assessRisk(changedFiles);
|
|
1162
|
+
const summary = this.buildSummary(changedFiles, affectedTests, riskLevel);
|
|
1163
|
+
return { changedFiles, affectedTests, riskLevel, summary };
|
|
1164
|
+
}
|
|
1165
|
+
getTestCandidates(filePath) {
|
|
1166
|
+
const dir = dirname2(filePath);
|
|
1167
|
+
const base = basename(filePath);
|
|
1168
|
+
const candidates = [];
|
|
1169
|
+
const nameMatch = base.match(/^(.+)\.(tsx?|jsx?|vue|svelte|py|go|rs)$/);
|
|
1170
|
+
if (!nameMatch) return candidates;
|
|
1171
|
+
const name = nameMatch[1];
|
|
1172
|
+
const ext = nameMatch[2];
|
|
1173
|
+
const testExts = ext.startsWith("ts") ? ["test.ts", "test.tsx", "spec.ts", "spec.tsx"] : ext.startsWith("js") ? ["test.js", "test.jsx", "spec.js", "spec.jsx"] : ext === "py" ? ["test.py"] : ext === "go" ? ["_test.go"] : [];
|
|
1174
|
+
for (const testExt of testExts) {
|
|
1175
|
+
candidates.push(join3(dir, `${name}.${testExt}`));
|
|
1176
|
+
candidates.push(join3(dir, "__tests__", `${name}.${testExt}`));
|
|
1177
|
+
candidates.push(join3(dir, "test", `${name}.${testExt}`));
|
|
1178
|
+
candidates.push(join3(dir, "tests", `${name}.${testExt}`));
|
|
1179
|
+
candidates.push(join3("__tests__", dir, `${name}.${testExt}`));
|
|
1180
|
+
}
|
|
1181
|
+
if (ext === "go") {
|
|
1182
|
+
candidates.push(join3(dir, `${name}_test.go`));
|
|
1183
|
+
}
|
|
1184
|
+
return candidates;
|
|
1185
|
+
}
|
|
1186
|
+
isSourceFile(file) {
|
|
1187
|
+
return /\.(tsx?|jsx?|vue|svelte|py|go|rs)$/.test(file) && !this.isTestFile(file);
|
|
1188
|
+
}
|
|
1189
|
+
isTestFile(file) {
|
|
1190
|
+
return /\.(test|spec)\.(tsx?|jsx?|py)$/.test(file) || /_test\.go$/.test(file) || file.includes("__tests__/");
|
|
1191
|
+
}
|
|
1192
|
+
assessRisk(changedFiles) {
|
|
1193
|
+
const highRiskPatterns = [
|
|
1194
|
+
/auth/i,
|
|
1195
|
+
/security/i,
|
|
1196
|
+
/middleware/i,
|
|
1197
|
+
/database/i,
|
|
1198
|
+
/migration/i,
|
|
1199
|
+
/config/i,
|
|
1200
|
+
/\.env/,
|
|
1201
|
+
/package\.json$/,
|
|
1202
|
+
/docker/i,
|
|
1203
|
+
/ci\//i,
|
|
1204
|
+
/payment/i,
|
|
1205
|
+
/billing/i,
|
|
1206
|
+
/permission/i
|
|
1207
|
+
];
|
|
1208
|
+
const mediumRiskPatterns = [
|
|
1209
|
+
/api/i,
|
|
1210
|
+
/route/i,
|
|
1211
|
+
/controller/i,
|
|
1212
|
+
/service/i,
|
|
1213
|
+
/model/i,
|
|
1214
|
+
/hook/i,
|
|
1215
|
+
/context/i,
|
|
1216
|
+
/store/i,
|
|
1217
|
+
/util/i
|
|
1218
|
+
];
|
|
1219
|
+
let highCount = 0;
|
|
1220
|
+
let mediumCount = 0;
|
|
1221
|
+
for (const file of changedFiles) {
|
|
1222
|
+
if (highRiskPatterns.some((p) => p.test(file))) highCount++;
|
|
1223
|
+
else if (mediumRiskPatterns.some((p) => p.test(file))) mediumCount++;
|
|
1224
|
+
}
|
|
1225
|
+
if (highCount >= 2 || highCount >= 1 && changedFiles.length > 5) return "high";
|
|
1226
|
+
if (highCount >= 1 || mediumCount >= 3) return "medium";
|
|
1227
|
+
return "low";
|
|
1228
|
+
}
|
|
1229
|
+
buildSummary(changedFiles, affectedTests, riskLevel) {
|
|
1230
|
+
const lines = [];
|
|
1231
|
+
lines.push(`${changedFiles.length} file(s) changed, ${affectedTests.length} test(s) affected.`);
|
|
1232
|
+
lines.push(`Risk level: ${riskLevel}.`);
|
|
1233
|
+
if (affectedTests.length > 0) {
|
|
1234
|
+
lines.push(`Run: ${affectedTests.slice(0, 5).join(", ")}${affectedTests.length > 5 ? ` (+${affectedTests.length - 5} more)` : ""}`);
|
|
1235
|
+
} else if (changedFiles.length > 0) {
|
|
1236
|
+
lines.push("No matching test files found \u2014 consider running full suite.");
|
|
1237
|
+
}
|
|
1238
|
+
return lines.join(" ");
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1104
1242
|
// agent/brain/index.ts
|
|
1105
1243
|
var OpenQABrain = class extends EventEmitter2 {
|
|
1106
1244
|
db;
|
|
1107
1245
|
llm;
|
|
1246
|
+
cache = new LLMCache();
|
|
1247
|
+
diffAnalyzer = new DiffAnalyzer();
|
|
1108
1248
|
saasConfig;
|
|
1109
1249
|
generatedTests = /* @__PURE__ */ new Map();
|
|
1110
1250
|
dynamicAgents = /* @__PURE__ */ new Map();
|
|
@@ -1165,7 +1305,9 @@ Respond in JSON format:
|
|
|
1165
1305
|
"suggestedAgents": ["Agent for X", "Agent for Y"],
|
|
1166
1306
|
"risks": ["Risk 1", "Risk 2"]
|
|
1167
1307
|
}`;
|
|
1168
|
-
const
|
|
1308
|
+
const cached = this.cache.get(prompt);
|
|
1309
|
+
const response = cached ?? await this.llm.generate(prompt);
|
|
1310
|
+
if (!cached) this.cache.set(prompt, response);
|
|
1169
1311
|
try {
|
|
1170
1312
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
1171
1313
|
if (jsonMatch) {
|
|
@@ -1184,11 +1326,11 @@ Respond in JSON format:
|
|
|
1184
1326
|
async analyzeCodebase() {
|
|
1185
1327
|
let repoPath = this.saasConfig.localPath;
|
|
1186
1328
|
if (this.saasConfig.repoUrl && !this.saasConfig.localPath) {
|
|
1187
|
-
repoPath =
|
|
1188
|
-
if (!
|
|
1329
|
+
repoPath = join4(this.workDir, "repo");
|
|
1330
|
+
if (!existsSync3(repoPath)) {
|
|
1189
1331
|
logger.info("Cloning repository", { url: this.saasConfig.repoUrl });
|
|
1190
1332
|
try {
|
|
1191
|
-
|
|
1333
|
+
execSync2(`git clone --depth 1 ${this.saasConfig.repoUrl} ${repoPath}`, {
|
|
1192
1334
|
stdio: "pipe"
|
|
1193
1335
|
});
|
|
1194
1336
|
} catch (e) {
|
|
@@ -1197,13 +1339,13 @@ Respond in JSON format:
|
|
|
1197
1339
|
}
|
|
1198
1340
|
}
|
|
1199
1341
|
}
|
|
1200
|
-
if (!repoPath || !
|
|
1342
|
+
if (!repoPath || !existsSync3(repoPath)) {
|
|
1201
1343
|
return "";
|
|
1202
1344
|
}
|
|
1203
1345
|
const analysis = [];
|
|
1204
1346
|
try {
|
|
1205
|
-
const packageJsonPath =
|
|
1206
|
-
if (
|
|
1347
|
+
const packageJsonPath = join4(repoPath, "package.json");
|
|
1348
|
+
if (existsSync3(packageJsonPath)) {
|
|
1207
1349
|
const pkg = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
|
|
1208
1350
|
analysis.push(`### Package Info`);
|
|
1209
1351
|
analysis.push(`- Name: ${pkg.name}`);
|
|
@@ -1228,8 +1370,8 @@ Respond in JSON format:
|
|
|
1228
1370
|
}
|
|
1229
1371
|
const routePatterns = ["routes", "pages", "views", "controllers", "api"];
|
|
1230
1372
|
for (const pattern of routePatterns) {
|
|
1231
|
-
const routeDir =
|
|
1232
|
-
if (
|
|
1373
|
+
const routeDir = join4(repoPath, "src", pattern);
|
|
1374
|
+
if (existsSync3(routeDir)) {
|
|
1233
1375
|
const routes = this.findFiles(routeDir, [".ts", ".tsx", ".js", ".jsx"], 20);
|
|
1234
1376
|
if (routes.length > 0) {
|
|
1235
1377
|
analysis.push(`
|
|
@@ -1256,7 +1398,7 @@ Respond in JSON format:
|
|
|
1256
1398
|
for (const item of items) {
|
|
1257
1399
|
if (results.length >= limit) return;
|
|
1258
1400
|
if (item.startsWith(".") || item === "node_modules" || item === "dist" || item === "build") continue;
|
|
1259
|
-
const fullPath =
|
|
1401
|
+
const fullPath = join4(d, item);
|
|
1260
1402
|
const stat = statSync2(fullPath);
|
|
1261
1403
|
if (stat.isDirectory()) {
|
|
1262
1404
|
find(fullPath);
|
|
@@ -1302,7 +1444,9 @@ Respond with JSON:
|
|
|
1302
1444
|
"code": "// complete test code here",
|
|
1303
1445
|
"priority": 1-5
|
|
1304
1446
|
}`;
|
|
1305
|
-
const
|
|
1447
|
+
const cached2 = this.cache.get(prompt);
|
|
1448
|
+
const response = cached2 ?? await this.llm.generate(prompt);
|
|
1449
|
+
if (!cached2) this.cache.set(prompt, response);
|
|
1306
1450
|
let testData = { name: target, description: "", code: "", priority: 3 };
|
|
1307
1451
|
try {
|
|
1308
1452
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
@@ -1329,7 +1473,7 @@ Respond with JSON:
|
|
|
1329
1473
|
}
|
|
1330
1474
|
saveTest(test) {
|
|
1331
1475
|
const filename = `${test.type}_${test.id}.ts`;
|
|
1332
|
-
const filepath =
|
|
1476
|
+
const filepath = join4(this.testsDir, filename);
|
|
1333
1477
|
const content = `/**
|
|
1334
1478
|
* Generated by OpenQA
|
|
1335
1479
|
* Type: ${test.type}
|
|
@@ -1367,7 +1511,9 @@ Respond with JSON:
|
|
|
1367
1511
|
"prompt": "Complete system prompt for this agent (be specific and detailed)",
|
|
1368
1512
|
"tools": ["tool1", "tool2"] // from: navigate, click, fill, screenshot, check_console, create_issue, create_ticket
|
|
1369
1513
|
}`;
|
|
1370
|
-
const
|
|
1514
|
+
const cached3 = this.cache.get(prompt);
|
|
1515
|
+
const response = cached3 ?? await this.llm.generate(prompt);
|
|
1516
|
+
if (!cached3) this.cache.set(prompt, response);
|
|
1371
1517
|
let agentData = { name: purpose, purpose, prompt: "", tools: [] };
|
|
1372
1518
|
try {
|
|
1373
1519
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
@@ -1398,8 +1544,8 @@ Respond with JSON:
|
|
|
1398
1544
|
test.executedAt = /* @__PURE__ */ new Date();
|
|
1399
1545
|
this.emit("test-started", test);
|
|
1400
1546
|
try {
|
|
1401
|
-
if (test.targetFile &&
|
|
1402
|
-
const result =
|
|
1547
|
+
if (test.targetFile && existsSync3(test.targetFile)) {
|
|
1548
|
+
const result = execSync2(`npx playwright test ${test.targetFile} --reporter=json`, {
|
|
1403
1549
|
cwd: this.testsDir,
|
|
1404
1550
|
stdio: "pipe",
|
|
1405
1551
|
timeout: 12e4
|
|
@@ -1453,7 +1599,9 @@ Respond with JSON:
|
|
|
1453
1599
|
{ "type": "analyze", "target": "what to analyze", "reason": "why" }
|
|
1454
1600
|
]
|
|
1455
1601
|
}`;
|
|
1456
|
-
const
|
|
1602
|
+
const cached4 = this.cache.get(prompt);
|
|
1603
|
+
const response = cached4 ?? await this.llm.generate(prompt);
|
|
1604
|
+
if (!cached4) this.cache.set(prompt, response);
|
|
1457
1605
|
try {
|
|
1458
1606
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
1459
1607
|
if (jsonMatch) {
|
|
@@ -1545,13 +1693,50 @@ Respond with JSON:
|
|
|
1545
1693
|
}
|
|
1546
1694
|
};
|
|
1547
1695
|
}
|
|
1696
|
+
/**
|
|
1697
|
+
* B8 — Incremental mode: only generate tests for files changed vs baseBranch.
|
|
1698
|
+
* Emits 'diff-analyzed' with the DiffResult, then runs autonomously with a reduced
|
|
1699
|
+
* scope derived from the changed files.
|
|
1700
|
+
*/
|
|
1701
|
+
async runIncrementally(baseBranch = "main") {
|
|
1702
|
+
const repoPath = this.saasConfig.localPath;
|
|
1703
|
+
if (!repoPath) {
|
|
1704
|
+
logger.warn("runIncrementally: no localPath configured, falling back to full run");
|
|
1705
|
+
return this.runAutonomously();
|
|
1706
|
+
}
|
|
1707
|
+
const diff = this.diffAnalyzer.analyze(repoPath, baseBranch);
|
|
1708
|
+
logger.info("Incremental diff", { changedFiles: diff.changedFiles.length, affectedTests: diff.affectedTests.length, riskLevel: diff.riskLevel });
|
|
1709
|
+
this.emit("diff-analyzed", diff);
|
|
1710
|
+
if (diff.changedFiles.length === 0) {
|
|
1711
|
+
logger.info("No changed files \u2014 skipping incremental run");
|
|
1712
|
+
this.emit("session-complete", { testsGenerated: 0, agentsCreated: 0, incremental: true });
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
for (const file of diff.changedFiles.slice(0, 10)) {
|
|
1716
|
+
const testType = this.inferTestType(file);
|
|
1717
|
+
const context = `Changed file: ${file}
|
|
1718
|
+
Risk level: ${diff.riskLevel}
|
|
1719
|
+
${diff.summary}`;
|
|
1720
|
+
try {
|
|
1721
|
+
await this.generateTest(testType, `Test coverage for ${file}`, context);
|
|
1722
|
+
} catch (e) {
|
|
1723
|
+
logger.error("Failed to generate test for file", { file, error: e instanceof Error ? e.message : String(e) });
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
this.emit("session-complete", {
|
|
1727
|
+
testsGenerated: this.generatedTests.size,
|
|
1728
|
+
agentsCreated: this.dynamicAgents.size,
|
|
1729
|
+
incremental: true,
|
|
1730
|
+
diff
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1548
1733
|
};
|
|
1549
1734
|
|
|
1550
1735
|
// agent/tools/browser.ts
|
|
1551
1736
|
init_esm_shims();
|
|
1552
1737
|
import { chromium } from "playwright";
|
|
1553
1738
|
import { mkdirSync as mkdirSync3 } from "fs";
|
|
1554
|
-
import { join as
|
|
1739
|
+
import { join as join5 } from "path";
|
|
1555
1740
|
var BrowserTools = class {
|
|
1556
1741
|
browser = null;
|
|
1557
1742
|
page = null;
|
|
@@ -1576,13 +1761,9 @@ var BrowserTools = class {
|
|
|
1576
1761
|
{
|
|
1577
1762
|
name: "navigate_to_page",
|
|
1578
1763
|
description: "Navigate to a specific URL in the application",
|
|
1579
|
-
parameters:
|
|
1580
|
-
type: "
|
|
1581
|
-
|
|
1582
|
-
url: { type: "string", description: "The URL to navigate to" }
|
|
1583
|
-
},
|
|
1584
|
-
required: ["url"]
|
|
1585
|
-
},
|
|
1764
|
+
parameters: [
|
|
1765
|
+
{ name: "url", type: "string", description: "The URL to navigate to", required: true }
|
|
1766
|
+
],
|
|
1586
1767
|
execute: async ({ url }) => {
|
|
1587
1768
|
if (!this.page) await this.initialize();
|
|
1588
1769
|
try {
|
|
@@ -1595,24 +1776,20 @@ var BrowserTools = class {
|
|
|
1595
1776
|
input: url,
|
|
1596
1777
|
output: `Page title: ${title}`
|
|
1597
1778
|
});
|
|
1598
|
-
return `Successfully navigated to ${url}. Page title: "${title}"
|
|
1779
|
+
return { output: `Successfully navigated to ${url}. Page title: "${title}"` };
|
|
1599
1780
|
} catch (error) {
|
|
1600
|
-
return `Failed to navigate: ${error instanceof Error ? error.message : String(error)}
|
|
1781
|
+
return { output: `Failed to navigate: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
|
|
1601
1782
|
}
|
|
1602
1783
|
}
|
|
1603
1784
|
},
|
|
1604
1785
|
{
|
|
1605
1786
|
name: "click_element",
|
|
1606
1787
|
description: "Click on an element using a CSS selector",
|
|
1607
|
-
parameters:
|
|
1608
|
-
type: "
|
|
1609
|
-
|
|
1610
|
-
selector: { type: "string", description: "CSS selector of the element to click" }
|
|
1611
|
-
},
|
|
1612
|
-
required: ["selector"]
|
|
1613
|
-
},
|
|
1788
|
+
parameters: [
|
|
1789
|
+
{ name: "selector", type: "string", description: "CSS selector of the element to click", required: true }
|
|
1790
|
+
],
|
|
1614
1791
|
execute: async ({ selector }) => {
|
|
1615
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
1792
|
+
if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
|
|
1616
1793
|
try {
|
|
1617
1794
|
await this.page.click(selector, { timeout: 5e3 });
|
|
1618
1795
|
this.db.createAction({
|
|
@@ -1621,25 +1798,21 @@ var BrowserTools = class {
|
|
|
1621
1798
|
description: `Clicked element: ${selector}`,
|
|
1622
1799
|
input: selector
|
|
1623
1800
|
});
|
|
1624
|
-
return `Successfully clicked element: ${selector}
|
|
1801
|
+
return { output: `Successfully clicked element: ${selector}` };
|
|
1625
1802
|
} catch (error) {
|
|
1626
|
-
return `Failed to click element: ${error instanceof Error ? error.message : String(error)}
|
|
1803
|
+
return { output: `Failed to click element: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
|
|
1627
1804
|
}
|
|
1628
1805
|
}
|
|
1629
1806
|
},
|
|
1630
1807
|
{
|
|
1631
1808
|
name: "fill_input",
|
|
1632
1809
|
description: "Fill an input field with text",
|
|
1633
|
-
parameters:
|
|
1634
|
-
type: "
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
text: { type: "string", description: "Text to fill in the input" }
|
|
1638
|
-
},
|
|
1639
|
-
required: ["selector", "text"]
|
|
1640
|
-
},
|
|
1810
|
+
parameters: [
|
|
1811
|
+
{ name: "selector", type: "string", description: "CSS selector of the input field", required: true },
|
|
1812
|
+
{ name: "text", type: "string", description: "Text to fill in the input", required: true }
|
|
1813
|
+
],
|
|
1641
1814
|
execute: async ({ selector, text }) => {
|
|
1642
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
1815
|
+
if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
|
|
1643
1816
|
try {
|
|
1644
1817
|
await this.page.fill(selector, text);
|
|
1645
1818
|
this.db.createAction({
|
|
@@ -1648,27 +1821,23 @@ var BrowserTools = class {
|
|
|
1648
1821
|
description: `Filled input ${selector}`,
|
|
1649
1822
|
input: `${selector} = ${text}`
|
|
1650
1823
|
});
|
|
1651
|
-
return `Successfully filled input ${selector} with text
|
|
1824
|
+
return { output: `Successfully filled input ${selector} with text` };
|
|
1652
1825
|
} catch (error) {
|
|
1653
|
-
return `Failed to fill input: ${error instanceof Error ? error.message : String(error)}
|
|
1826
|
+
return { output: `Failed to fill input: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
|
|
1654
1827
|
}
|
|
1655
1828
|
}
|
|
1656
1829
|
},
|
|
1657
1830
|
{
|
|
1658
1831
|
name: "take_screenshot",
|
|
1659
1832
|
description: "Take a screenshot of the current page for evidence",
|
|
1660
|
-
parameters:
|
|
1661
|
-
type: "
|
|
1662
|
-
|
|
1663
|
-
name: { type: "string", description: "Name for the screenshot file" }
|
|
1664
|
-
},
|
|
1665
|
-
required: ["name"]
|
|
1666
|
-
},
|
|
1833
|
+
parameters: [
|
|
1834
|
+
{ name: "name", type: "string", description: "Name for the screenshot file", required: true }
|
|
1835
|
+
],
|
|
1667
1836
|
execute: async ({ name }) => {
|
|
1668
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
1837
|
+
if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
|
|
1669
1838
|
try {
|
|
1670
1839
|
const filename = `${Date.now()}_${name}.png`;
|
|
1671
|
-
const path2 =
|
|
1840
|
+
const path2 = join5(this.screenshotDir, filename);
|
|
1672
1841
|
await this.page.screenshot({ path: path2, fullPage: true });
|
|
1673
1842
|
this.db.createAction({
|
|
1674
1843
|
session_id: this.sessionId,
|
|
@@ -1676,38 +1845,32 @@ var BrowserTools = class {
|
|
|
1676
1845
|
description: `Screenshot: ${name}`,
|
|
1677
1846
|
screenshot_path: path2
|
|
1678
1847
|
});
|
|
1679
|
-
return `Screenshot saved: ${path2}
|
|
1848
|
+
return { output: `Screenshot saved: ${path2}` };
|
|
1680
1849
|
} catch (error) {
|
|
1681
|
-
return `Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}
|
|
1850
|
+
return { output: `Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
|
|
1682
1851
|
}
|
|
1683
1852
|
}
|
|
1684
1853
|
},
|
|
1685
1854
|
{
|
|
1686
1855
|
name: "get_page_content",
|
|
1687
1856
|
description: "Get the text content of the current page",
|
|
1688
|
-
parameters:
|
|
1689
|
-
type: "object",
|
|
1690
|
-
properties: {}
|
|
1691
|
-
},
|
|
1857
|
+
parameters: [],
|
|
1692
1858
|
execute: async () => {
|
|
1693
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
1859
|
+
if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
|
|
1694
1860
|
try {
|
|
1695
1861
|
const content = await this.page.textContent("body");
|
|
1696
|
-
return content?.slice(0, 1e3) || "No content found";
|
|
1862
|
+
return { output: content?.slice(0, 1e3) || "No content found" };
|
|
1697
1863
|
} catch (error) {
|
|
1698
|
-
return `Failed to get content: ${error instanceof Error ? error.message : String(error)}
|
|
1864
|
+
return { output: `Failed to get content: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
|
|
1699
1865
|
}
|
|
1700
1866
|
}
|
|
1701
1867
|
},
|
|
1702
1868
|
{
|
|
1703
1869
|
name: "check_console_errors",
|
|
1704
1870
|
description: "Check for JavaScript console errors on the page",
|
|
1705
|
-
parameters:
|
|
1706
|
-
type: "object",
|
|
1707
|
-
properties: {}
|
|
1708
|
-
},
|
|
1871
|
+
parameters: [],
|
|
1709
1872
|
execute: async () => {
|
|
1710
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
1873
|
+
if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
|
|
1711
1874
|
const errors = [];
|
|
1712
1875
|
this.page.on("console", (msg) => {
|
|
1713
1876
|
if (msg.type() === "error") {
|
|
@@ -1716,10 +1879,10 @@ var BrowserTools = class {
|
|
|
1716
1879
|
});
|
|
1717
1880
|
await this.page.waitForTimeout(2e3);
|
|
1718
1881
|
if (errors.length > 0) {
|
|
1719
|
-
return `Found ${errors.length} console errors:
|
|
1720
|
-
${errors.join("\n")}
|
|
1882
|
+
return { output: `Found ${errors.length} console errors:
|
|
1883
|
+
${errors.join("\n")}` };
|
|
1721
1884
|
}
|
|
1722
|
-
return "No console errors detected";
|
|
1885
|
+
return { output: "No console errors detected" };
|
|
1723
1886
|
}
|
|
1724
1887
|
}
|
|
1725
1888
|
];
|
|
@@ -1830,6 +1993,16 @@ var GitListener = class extends EventEmitter3 {
|
|
|
1830
1993
|
for (const commit of commits) {
|
|
1831
1994
|
if (this.lastCommitSha && commit.sha === this.lastCommitSha) break;
|
|
1832
1995
|
const isMerge = commit.parents && commit.parents.length > 1;
|
|
1996
|
+
let changedFiles;
|
|
1997
|
+
try {
|
|
1998
|
+
const { data: detail } = await this.octokit.repos.getCommit({
|
|
1999
|
+
owner: this.config.owner,
|
|
2000
|
+
repo: this.config.repo,
|
|
2001
|
+
ref: commit.sha
|
|
2002
|
+
});
|
|
2003
|
+
changedFiles = (detail.files ?? []).map((f) => f.filename);
|
|
2004
|
+
} catch {
|
|
2005
|
+
}
|
|
1833
2006
|
const event = {
|
|
1834
2007
|
type: isMerge ? "merge" : "push",
|
|
1835
2008
|
provider: "github",
|
|
@@ -1837,7 +2010,8 @@ var GitListener = class extends EventEmitter3 {
|
|
|
1837
2010
|
commit: commit.sha,
|
|
1838
2011
|
author: commit.commit.author?.name || "unknown",
|
|
1839
2012
|
message: commit.commit.message,
|
|
1840
|
-
timestamp: new Date(commit.commit.author?.date || Date.now())
|
|
2013
|
+
timestamp: new Date(commit.commit.author?.date || Date.now()),
|
|
2014
|
+
changedFiles
|
|
1841
2015
|
};
|
|
1842
2016
|
this.emit("git-event", event);
|
|
1843
2017
|
if (isMerge) {
|
|
@@ -1923,6 +2097,16 @@ var GitListener = class extends EventEmitter3 {
|
|
|
1923
2097
|
for (const commit of commits) {
|
|
1924
2098
|
if (this.lastCommitSha && commit.id === this.lastCommitSha) break;
|
|
1925
2099
|
const isMerge = commit.parent_ids && commit.parent_ids.length > 1;
|
|
2100
|
+
let changedFiles;
|
|
2101
|
+
try {
|
|
2102
|
+
const diffRes = await fetch(
|
|
2103
|
+
`${baseUrl}/api/v4/projects/${projectPath}/repository/commits/${commit.id}/diff`,
|
|
2104
|
+
{ headers }
|
|
2105
|
+
);
|
|
2106
|
+
const diffs = await diffRes.json();
|
|
2107
|
+
changedFiles = diffs.map((d) => d.new_path);
|
|
2108
|
+
} catch {
|
|
2109
|
+
}
|
|
1926
2110
|
const event = {
|
|
1927
2111
|
type: isMerge ? "merge" : "push",
|
|
1928
2112
|
provider: "gitlab",
|
|
@@ -1930,7 +2114,8 @@ var GitListener = class extends EventEmitter3 {
|
|
|
1930
2114
|
commit: commit.id,
|
|
1931
2115
|
author: commit.author_name,
|
|
1932
2116
|
message: commit.message,
|
|
1933
|
-
timestamp: new Date(commit.created_at)
|
|
2117
|
+
timestamp: new Date(commit.created_at),
|
|
2118
|
+
changedFiles
|
|
1934
2119
|
};
|
|
1935
2120
|
this.emit("git-event", event);
|
|
1936
2121
|
if (isMerge) {
|
|
@@ -2025,8 +2210,8 @@ var GitListener = class extends EventEmitter3 {
|
|
|
2025
2210
|
init_esm_shims();
|
|
2026
2211
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
2027
2212
|
import { spawn } from "child_process";
|
|
2028
|
-
import { existsSync as
|
|
2029
|
-
import { join as
|
|
2213
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, statSync } from "fs";
|
|
2214
|
+
import { join as join6, resolve } from "path";
|
|
2030
2215
|
function sanitizeRepoPath(inputPath) {
|
|
2031
2216
|
if (typeof inputPath !== "string" || !inputPath.trim()) {
|
|
2032
2217
|
throw new ProjectRunnerError("repoPath must be a non-empty string");
|
|
@@ -2054,14 +2239,14 @@ var ProjectRunner = class extends EventEmitter4 {
|
|
|
2054
2239
|
*/
|
|
2055
2240
|
detectProjectType(repoPath) {
|
|
2056
2241
|
repoPath = sanitizeRepoPath(repoPath);
|
|
2057
|
-
const pkgPath =
|
|
2058
|
-
if (
|
|
2242
|
+
const pkgPath = join6(repoPath, "package.json");
|
|
2243
|
+
if (existsSync4(pkgPath)) {
|
|
2059
2244
|
return this.detectNodeProject(repoPath, pkgPath);
|
|
2060
2245
|
}
|
|
2061
|
-
if (
|
|
2246
|
+
if (existsSync4(join6(repoPath, "requirements.txt")) || existsSync4(join6(repoPath, "pyproject.toml"))) {
|
|
2062
2247
|
return this.detectPythonProject(repoPath);
|
|
2063
2248
|
}
|
|
2064
|
-
if (
|
|
2249
|
+
if (existsSync4(join6(repoPath, "go.mod"))) {
|
|
2065
2250
|
return {
|
|
2066
2251
|
language: "go",
|
|
2067
2252
|
packageManager: "go",
|
|
@@ -2070,7 +2255,7 @@ var ProjectRunner = class extends EventEmitter4 {
|
|
|
2070
2255
|
devCommand: "go run ."
|
|
2071
2256
|
};
|
|
2072
2257
|
}
|
|
2073
|
-
if (
|
|
2258
|
+
if (existsSync4(join6(repoPath, "Cargo.toml"))) {
|
|
2074
2259
|
return {
|
|
2075
2260
|
language: "rust",
|
|
2076
2261
|
packageManager: "cargo",
|
|
@@ -2086,9 +2271,9 @@ var ProjectRunner = class extends EventEmitter4 {
|
|
|
2086
2271
|
const scripts = pkg.scripts || {};
|
|
2087
2272
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2088
2273
|
let packageManager = "npm";
|
|
2089
|
-
if (
|
|
2090
|
-
else if (
|
|
2091
|
-
else if (
|
|
2274
|
+
if (existsSync4(join6(repoPath, "pnpm-lock.yaml"))) packageManager = "pnpm";
|
|
2275
|
+
else if (existsSync4(join6(repoPath, "yarn.lock"))) packageManager = "yarn";
|
|
2276
|
+
else if (existsSync4(join6(repoPath, "bun.lockb"))) packageManager = "bun";
|
|
2092
2277
|
let framework;
|
|
2093
2278
|
if (deps["next"]) framework = "next";
|
|
2094
2279
|
else if (deps["nuxt"]) framework = "nuxt";
|
|
@@ -2140,13 +2325,13 @@ var ProjectRunner = class extends EventEmitter4 {
|
|
|
2140
2325
|
}
|
|
2141
2326
|
detectPythonProject(repoPath) {
|
|
2142
2327
|
let testRunner;
|
|
2143
|
-
const reqPath =
|
|
2144
|
-
if (
|
|
2328
|
+
const reqPath = join6(repoPath, "requirements.txt");
|
|
2329
|
+
if (existsSync4(reqPath)) {
|
|
2145
2330
|
const content = readFileSync4(reqPath, "utf-8");
|
|
2146
2331
|
if (content.includes("pytest")) testRunner = "pytest";
|
|
2147
2332
|
}
|
|
2148
|
-
const pyprojectPath =
|
|
2149
|
-
if (
|
|
2333
|
+
const pyprojectPath = join6(repoPath, "pyproject.toml");
|
|
2334
|
+
if (existsSync4(pyprojectPath)) {
|
|
2150
2335
|
const content = readFileSync4(pyprojectPath, "utf-8");
|
|
2151
2336
|
if (content.includes("pytest")) testRunner = "pytest";
|
|
2152
2337
|
}
|
|
@@ -3931,8 +4116,8 @@ function createAuthRouter(db2) {
|
|
|
3931
4116
|
// cli/env-routes.ts
|
|
3932
4117
|
init_esm_shims();
|
|
3933
4118
|
import { Router as Router3 } from "express";
|
|
3934
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as
|
|
3935
|
-
import { join as
|
|
4119
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
|
|
4120
|
+
import { join as join7 } from "path";
|
|
3936
4121
|
|
|
3937
4122
|
// cli/env-config.ts
|
|
3938
4123
|
init_esm_shims();
|
|
@@ -4321,9 +4506,9 @@ function validateEnvValue(key, value) {
|
|
|
4321
4506
|
// cli/env-routes.ts
|
|
4322
4507
|
function createEnvRouter() {
|
|
4323
4508
|
const router = Router3();
|
|
4324
|
-
const ENV_FILE_PATH =
|
|
4509
|
+
const ENV_FILE_PATH = join7(process.cwd(), ".env");
|
|
4325
4510
|
function readEnvFile() {
|
|
4326
|
-
if (!
|
|
4511
|
+
if (!existsSync5(ENV_FILE_PATH)) {
|
|
4327
4512
|
return {};
|
|
4328
4513
|
}
|
|
4329
4514
|
const content = readFileSync5(ENV_FILE_PATH, "utf-8");
|
|
@@ -4398,8 +4583,8 @@ function createEnvRouter() {
|
|
|
4398
4583
|
}));
|
|
4399
4584
|
res.json({
|
|
4400
4585
|
variables,
|
|
4401
|
-
envFileExists:
|
|
4402
|
-
lastModified:
|
|
4586
|
+
envFileExists: existsSync5(ENV_FILE_PATH),
|
|
4587
|
+
lastModified: existsSync5(ENV_FILE_PATH) ? new Date(readFileSync5(ENV_FILE_PATH, "utf-8").match(/Last updated: (.+)/)?.[1] || 0).toISOString() : null
|
|
4403
4588
|
});
|
|
4404
4589
|
} catch (error) {
|
|
4405
4590
|
res.status(500).json({
|
|
@@ -4759,7 +4944,7 @@ function getDashboardHTML() {
|
|
|
4759
4944
|
.logo-mark {
|
|
4760
4945
|
width: 34px;
|
|
4761
4946
|
height: 34px;
|
|
4762
|
-
background:
|
|
4947
|
+
background: transparent;
|
|
4763
4948
|
border-radius: 8px;
|
|
4764
4949
|
display: grid;
|
|
4765
4950
|
place-items: center;
|
|
@@ -5381,7 +5566,9 @@ function getDashboardHTML() {
|
|
|
5381
5566
|
<!-- Sidebar -->
|
|
5382
5567
|
<aside>
|
|
5383
5568
|
<div class="logo">
|
|
5384
|
-
<div class="logo-mark"
|
|
5569
|
+
<div class="logo-mark">
|
|
5570
|
+
<img src="https://openqa.orkajs.com/_next/image?url=https%3A%2F%2Forkajs.com%2Floutre-orka-qa.png&w=256&q=75" alt="OpenQA Logo" style="width: 40px; height: 40px;">
|
|
5571
|
+
</div>
|
|
5385
5572
|
<div>
|
|
5386
5573
|
<div class="logo-name">OpenQA</div>
|
|
5387
5574
|
<div class="logo-version">v2.1.0 \xB7 OSS</div>
|
|
@@ -5391,42 +5578,69 @@ function getDashboardHTML() {
|
|
|
5391
5578
|
<div class="nav-section">
|
|
5392
5579
|
<div class="nav-label">Overview</div>
|
|
5393
5580
|
<a class="nav-item active" href="/">
|
|
5394
|
-
<span class="icon"
|
|
5581
|
+
<span class="icon">
|
|
5582
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-gauge-icon lucide-gauge"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg>
|
|
5583
|
+
</span> Dashboard
|
|
5395
5584
|
</a>
|
|
5396
5585
|
<a class="nav-item" href="/kanban">
|
|
5397
|
-
<span class="icon"
|
|
5586
|
+
<span class="icon">
|
|
5587
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dashed-kanban-icon lucide-square-dashed-kanban"><path d="M8 7v7"/><path d="M12 7v4"/><path d="M16 7v9"/><path d="M5 3a2 2 0 0 0-2 2"/><path d="M9 3h1"/><path d="M14 3h1"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M21 9v1"/><path d="M21 14v1"/><path d="M21 19a2 2 0 0 1-2 2"/><path d="M14 21h1"/><path d="M9 21h1"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M3 14v1"/><path d="M3 9v1"/></svg>
|
|
5588
|
+
</span> Kanban
|
|
5398
5589
|
<span class="badge" id="kanban-count">0</span>
|
|
5399
5590
|
</a>
|
|
5400
5591
|
|
|
5401
5592
|
<div class="nav-label">Agents</div>
|
|
5402
5593
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('agents-table')">
|
|
5403
|
-
<span class="icon"
|
|
5594
|
+
<span class="icon">
|
|
5595
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-activity-icon lucide-activity"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/></svg>
|
|
5596
|
+
</span> Active Agents
|
|
5404
5597
|
</a>
|
|
5405
5598
|
<a class="nav-item" href="javascript:void(0)" onclick="switchAgentTab('specialists'); scrollToSection('agents-table')">
|
|
5406
|
-
<span class="icon"
|
|
5599
|
+
<span class="icon">
|
|
5600
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-hat-glasses-icon lucide-hat-glasses"><path d="M14 18a2 2 0 0 0-4 0"/><path d="m19 11-2.11-6.657a2 2 0 0 0-2.752-1.148l-1.276.61A2 2 0 0 1 12 4H8.5a2 2 0 0 0-1.925 1.456L5 11"/><path d="M2 11h20"/><circle cx="17" cy="18" r="3"/><circle cx="7" cy="18" r="3"/></svg>
|
|
5601
|
+
</span> Specialists
|
|
5407
5602
|
</a>
|
|
5408
5603
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('interventions-panel')">
|
|
5409
|
-
<span class="icon"
|
|
5604
|
+
<span class="icon">
|
|
5605
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-cog-icon lucide-user-cog"><path d="M10 15H6a4 4 0 0 0-4 4v2"/><path d="m14.305 16.53.923-.382"/><path d="m15.228 13.852-.923-.383"/><path d="m16.852 12.228-.383-.923"/><path d="m16.852 17.772-.383.924"/><path d="m19.148 12.228.383-.923"/><path d="m19.53 18.696-.382-.924"/><path d="m20.772 13.852.924-.383"/><path d="m20.772 16.148.924.383"/><circle cx="18" cy="15" r="3"/><circle cx="9" cy="7" r="4"/></svg>
|
|
5606
|
+
</span> Interventions
|
|
5410
5607
|
<span class="badge" id="intervention-count" style="background: var(--red);">0</span>
|
|
5411
5608
|
</a>
|
|
5412
5609
|
|
|
5413
5610
|
<div class="nav-label">Analysis</div>
|
|
5414
5611
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('issues-panel')">
|
|
5415
|
-
<span class="icon"
|
|
5612
|
+
<span class="icon">
|
|
5613
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug-play-icon lucide-bug-play"><path d="M10 19.655A6 6 0 0 1 6 14v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 3.97"/><path d="M14 15.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997a1 1 0 0 1-1.517-.86z"/>
|
|
5614
|
+
<path d="M14.12 3.88 16 2"/>
|
|
5615
|
+
<path d="M21 5a4 4 0 0 1-3.55 3.97"/>
|
|
5616
|
+
<path d="M3 21a4 4 0 0 1 3.81-4"/>
|
|
5617
|
+
<path d="M3 5a4 4 0 0 0 3.55 3.97"/>
|
|
5618
|
+
<path d="M6 13H2"/><path d="m8 2 1.88 1.88"/>
|
|
5619
|
+
<path d="M9 7.13V6a3 3 0 1 1 6 0v1.13"/>
|
|
5620
|
+
</svg>
|
|
5621
|
+
</span> Bug Reports
|
|
5416
5622
|
</a>
|
|
5417
5623
|
<a class="nav-item" href="javascript:void(0)" onclick="switchChartTab('performance'); scrollToSection('chart-performance')">
|
|
5418
|
-
<span class="icon"
|
|
5624
|
+
<span class="icon">
|
|
5625
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chart-spline-icon lucide-chart-spline"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M7 16c.5-2 1.5-7 4-7 2 0 2 3 4 3 2.5 0 4.5-5 5-7"/></svg>
|
|
5626
|
+
</span> Performance
|
|
5419
5627
|
</a>
|
|
5420
5628
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('activity-list')">
|
|
5421
|
-
<span class="icon"
|
|
5629
|
+
<span class="icon">
|
|
5630
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scroll-text-icon lucide-scroll-text"><path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/></svg>
|
|
5631
|
+
</span> Logs
|
|
5422
5632
|
</a>
|
|
5423
5633
|
|
|
5424
5634
|
<div class="nav-label">System</div>
|
|
5425
5635
|
<a class="nav-item" href="/config">
|
|
5426
|
-
<span class="icon"
|
|
5636
|
+
<span class="icon">
|
|
5637
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-columns3-cog-icon lucide-columns-3-cog"><path d="M10.5 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5.5"/><path d="m14.3 19.6 1-.4"/><path d="M15 3v7.5"/><path d="m15.2 16.9-.9-.3"/><path d="m16.6 21.7.3-.9"/><path d="m16.8 15.3-.4-1"/><path d="m19.1 15.2.3-.9"/><path d="m19.6 21.7-.4-1"/><path d="m20.7 16.8 1-.4"/><path d="m21.7 19.4-.9-.3"/><path d="M9 3v18"/><circle cx="18" cy="18" r="3"/></svg>
|
|
5638
|
+
</span> Config
|
|
5427
5639
|
</a>
|
|
5428
5640
|
<a class="nav-item" href="/config/env">
|
|
5429
|
-
<span class="icon"
|
|
5641
|
+
<span class="icon">
|
|
5642
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-columns3-cog-icon lucide-columns-3-cog"><path d="M10.5 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5.5"/><path d="m14.3 19.6 1-.4"/><path d="M15 3v7.5"/><path d="m15.2 16.9-.9-.3"/><path d="m16.6 21.7.3-.9"/><path d="m16.8 15.3-.4-1"/><path d="m19.1 15.2.3-.9"/><path d="m19.6 21.7-.4-1"/><path d="m20.7 16.8 1-.4"/><path d="m21.7 19.4-.9-.3"/><path d="M9 3v18"/><circle cx="18" cy="18" r="3"/></svg>
|
|
5643
|
+
</span> Environment
|
|
5430
5644
|
</a>
|
|
5431
5645
|
</div>
|
|
5432
5646
|
|
|
@@ -5458,7 +5672,9 @@ function getDashboardHTML() {
|
|
|
5458
5672
|
<div class="metric-card">
|
|
5459
5673
|
<div class="metric-header">
|
|
5460
5674
|
<div class="metric-label">Active Agents</div>
|
|
5461
|
-
<div class="metric-icon"
|
|
5675
|
+
<div class="metric-icon">
|
|
5676
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-terminal-icon lucide-square-terminal"><path d="m7 11 2-2-2-2"/><path d="M11 13h4"/><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/></svg>
|
|
5677
|
+
</div>
|
|
5462
5678
|
</div>
|
|
5463
5679
|
<div class="metric-value" id="active-agents">0</div>
|
|
5464
5680
|
<div class="metric-change positive" id="agents-change">\u2191 0 from last hour</div>
|
|
@@ -5466,7 +5682,9 @@ function getDashboardHTML() {
|
|
|
5466
5682
|
<div class="metric-card">
|
|
5467
5683
|
<div class="metric-header">
|
|
5468
5684
|
<div class="metric-label">Total Actions</div>
|
|
5469
|
-
<div class="metric-icon"
|
|
5685
|
+
<div class="metric-icon">
|
|
5686
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-todo-icon lucide-list-todo"><path d="M13 5h8"/><path d="M13 12h8"/><path d="M13 19h8"/><path d="m3 17 2 2 4-4"/><rect x="3" y="4" width="6" height="6" rx="1"/></svg>
|
|
5687
|
+
</div>
|
|
5470
5688
|
</div>
|
|
5471
5689
|
<div class="metric-value" id="total-actions">0</div>
|
|
5472
5690
|
<div class="metric-change positive" id="actions-change">\u2191 0% this session</div>
|
|
@@ -5474,7 +5692,9 @@ function getDashboardHTML() {
|
|
|
5474
5692
|
<div class="metric-card">
|
|
5475
5693
|
<div class="metric-header">
|
|
5476
5694
|
<div class="metric-label">Bugs Found</div>
|
|
5477
|
-
<div class="metric-icon"
|
|
5695
|
+
<div class="metric-icon">
|
|
5696
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-todo-icon lucide-list-todo"><path d="M13 5h8"/><path d="M13 12h8"/><path d="M13 19h8"/><path d="m3 17 2 2 4-4"/><rect x="3" y="4" width="6" height="6" rx="1"/></svg>
|
|
5697
|
+
</div>
|
|
5478
5698
|
</div>
|
|
5479
5699
|
<div class="metric-value" id="bugs-found">0</div>
|
|
5480
5700
|
<div class="metric-change negative" id="bugs-change">\u2193 0 from yesterday</div>
|
|
@@ -5482,7 +5702,9 @@ function getDashboardHTML() {
|
|
|
5482
5702
|
<div class="metric-card">
|
|
5483
5703
|
<div class="metric-header">
|
|
5484
5704
|
<div class="metric-label">Success Rate</div>
|
|
5485
|
-
<div class="metric-icon"
|
|
5705
|
+
<div class="metric-icon">
|
|
5706
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud-check-icon lucide-cloud-check"><path d="m17 15-5.5 5.5L9 18"/><path d="M5.516 16.07A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 3.501 7.327"/></svg>
|
|
5707
|
+
</div>
|
|
5486
5708
|
</div>
|
|
5487
5709
|
<div class="metric-value" id="success-rate">\u2014</div>
|
|
5488
5710
|
<div class="metric-change positive" id="rate-change">\u2191 0 pts improvement</div>
|
|
@@ -7721,672 +7943,860 @@ function getEnvHTML() {
|
|
|
7721
7943
|
<head>
|
|
7722
7944
|
<meta charset="UTF-8">
|
|
7723
7945
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7724
|
-
<title>
|
|
7946
|
+
<title>OpenQA \u2014 Environment</title>
|
|
7947
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
7948
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
|
|
7725
7949
|
<style>
|
|
7726
|
-
|
|
7727
|
-
|
|
7950
|
+
:root {
|
|
7951
|
+
--bg: #080b10;
|
|
7952
|
+
--surface: #0d1117;
|
|
7953
|
+
--panel: #111720;
|
|
7954
|
+
--border: rgba(255,255,255,0.06);
|
|
7955
|
+
--border-hi: rgba(255,255,255,0.12);
|
|
7956
|
+
--accent: #f97316;
|
|
7957
|
+
--accent-lo: rgba(249,115,22,0.08);
|
|
7958
|
+
--accent-md: rgba(249,115,22,0.18);
|
|
7959
|
+
--green: #22c55e;
|
|
7960
|
+
--green-lo: rgba(34,197,94,0.08);
|
|
7961
|
+
--red: #ef4444;
|
|
7962
|
+
--red-lo: rgba(239,68,68,0.08);
|
|
7963
|
+
--amber: #f59e0b;
|
|
7964
|
+
--amber-lo: rgba(245,158,11,0.08);
|
|
7965
|
+
--blue: #38bdf8;
|
|
7966
|
+
--blue-lo: rgba(56,189,248,0.08);
|
|
7967
|
+
--text-1: #f1f5f9;
|
|
7968
|
+
--text-2: #8b98a8;
|
|
7969
|
+
--text-3: #4b5563;
|
|
7970
|
+
--mono: 'DM Mono', monospace;
|
|
7971
|
+
--sans: 'Syne', sans-serif;
|
|
7972
|
+
--radius: 10px;
|
|
7973
|
+
--radius-lg: 16px;
|
|
7974
|
+
}
|
|
7975
|
+
|
|
7976
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
7977
|
+
|
|
7728
7978
|
body {
|
|
7729
|
-
font-family:
|
|
7730
|
-
background:
|
|
7979
|
+
font-family: var(--sans);
|
|
7980
|
+
background: var(--bg);
|
|
7981
|
+
color: var(--text-1);
|
|
7731
7982
|
min-height: 100vh;
|
|
7732
|
-
|
|
7983
|
+
overflow-x: hidden;
|
|
7733
7984
|
}
|
|
7734
7985
|
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7986
|
+
/* \u2500\u2500 Layout \u2500\u2500 */
|
|
7987
|
+
.shell {
|
|
7988
|
+
display: grid;
|
|
7989
|
+
grid-template-columns: 220px 1fr;
|
|
7990
|
+
min-height: 100vh;
|
|
7738
7991
|
}
|
|
7739
7992
|
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
border-radius: 12px;
|
|
7745
|
-
margin-bottom: 20px;
|
|
7746
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
7993
|
+
/* \u2500\u2500 Sidebar \u2500\u2500 */
|
|
7994
|
+
aside {
|
|
7995
|
+
background: var(--surface);
|
|
7996
|
+
border-right: 1px solid var(--border);
|
|
7747
7997
|
display: flex;
|
|
7748
|
-
|
|
7749
|
-
|
|
7998
|
+
flex-direction: column;
|
|
7999
|
+
padding: 28px 0;
|
|
8000
|
+
position: sticky;
|
|
8001
|
+
top: 0;
|
|
8002
|
+
height: 100vh;
|
|
7750
8003
|
}
|
|
7751
8004
|
|
|
7752
|
-
.
|
|
7753
|
-
font-size: 24px;
|
|
7754
|
-
color: #1a202c;
|
|
8005
|
+
.logo {
|
|
7755
8006
|
display: flex;
|
|
7756
8007
|
align-items: center;
|
|
7757
8008
|
gap: 10px;
|
|
8009
|
+
padding: 0 24px 32px;
|
|
8010
|
+
border-bottom: 1px solid var(--border);
|
|
8011
|
+
margin-bottom: 12px;
|
|
7758
8012
|
}
|
|
7759
8013
|
|
|
7760
|
-
.
|
|
7761
|
-
|
|
7762
|
-
|
|
8014
|
+
.logo-mark {
|
|
8015
|
+
width: 34px; height: 34px;
|
|
8016
|
+
background: var(--accent);
|
|
8017
|
+
border-radius: 8px;
|
|
8018
|
+
display: grid;
|
|
8019
|
+
place-items: center;
|
|
8020
|
+
font-size: 17px;
|
|
8021
|
+
font-weight: 800;
|
|
8022
|
+
color: #fff;
|
|
7763
8023
|
}
|
|
7764
8024
|
|
|
7765
|
-
.
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
8025
|
+
.logo-name { font-weight: 800; font-size: 18px; letter-spacing: -0.5px; }
|
|
8026
|
+
.logo-version { font-family: var(--mono); font-size: 10px; color: var(--text-3); }
|
|
8027
|
+
|
|
8028
|
+
.nav-section { padding: 8px 12px; flex: 1; overflow-y: auto; }
|
|
8029
|
+
|
|
8030
|
+
.nav-label {
|
|
8031
|
+
font-family: var(--mono);
|
|
8032
|
+
font-size: 10px;
|
|
8033
|
+
color: var(--text-3);
|
|
8034
|
+
letter-spacing: 1.5px;
|
|
8035
|
+
text-transform: uppercase;
|
|
8036
|
+
padding: 0 12px;
|
|
8037
|
+
margin: 16px 0 6px;
|
|
8038
|
+
}
|
|
8039
|
+
|
|
8040
|
+
.nav-item {
|
|
8041
|
+
display: flex;
|
|
8042
|
+
align-items: center;
|
|
8043
|
+
gap: 10px;
|
|
8044
|
+
padding: 9px 12px;
|
|
8045
|
+
border-radius: var(--radius);
|
|
8046
|
+
color: var(--text-2);
|
|
8047
|
+
text-decoration: none;
|
|
7769
8048
|
font-size: 14px;
|
|
7770
8049
|
font-weight: 600;
|
|
8050
|
+
transition: all 0.15s ease;
|
|
7771
8051
|
cursor: pointer;
|
|
7772
|
-
transition: all 0.2s;
|
|
7773
|
-
text-decoration: none;
|
|
7774
|
-
display: inline-flex;
|
|
7775
|
-
align-items: center;
|
|
7776
|
-
gap: 8px;
|
|
7777
8052
|
}
|
|
8053
|
+
.nav-item:hover { color: var(--text-1); background: var(--panel); }
|
|
8054
|
+
.nav-item.active { color: var(--accent); background: var(--accent-lo); }
|
|
8055
|
+
.nav-item .icon { font-size: 15px; width: 20px; text-align: center; }
|
|
7778
8056
|
|
|
7779
|
-
.
|
|
7780
|
-
|
|
7781
|
-
|
|
8057
|
+
.sidebar-footer {
|
|
8058
|
+
padding: 16px 24px;
|
|
8059
|
+
border-top: 1px solid var(--border);
|
|
7782
8060
|
}
|
|
7783
8061
|
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
transform: translateY(-1px);
|
|
7787
|
-
}
|
|
8062
|
+
/* \u2500\u2500 Main \u2500\u2500 */
|
|
8063
|
+
main { display: flex; flex-direction: column; min-height: 100vh; overflow-y: auto; }
|
|
7788
8064
|
|
|
7789
|
-
.
|
|
7790
|
-
|
|
7791
|
-
|
|
8065
|
+
.topbar {
|
|
8066
|
+
display: flex;
|
|
8067
|
+
align-items: center;
|
|
8068
|
+
justify-content: space-between;
|
|
8069
|
+
padding: 20px 32px;
|
|
8070
|
+
border-bottom: 1px solid var(--border);
|
|
8071
|
+
background: var(--surface);
|
|
8072
|
+
position: sticky;
|
|
8073
|
+
top: 0;
|
|
8074
|
+
z-index: 10;
|
|
7792
8075
|
}
|
|
7793
8076
|
|
|
7794
|
-
.
|
|
7795
|
-
|
|
7796
|
-
}
|
|
8077
|
+
.page-title { font-size: 15px; font-weight: 700; letter-spacing: -0.2px; }
|
|
8078
|
+
.page-sub { font-family: var(--mono); font-size: 11px; color: var(--text-3); margin-top: 2px; }
|
|
7797
8079
|
|
|
7798
|
-
.
|
|
7799
|
-
background: #48bb78;
|
|
7800
|
-
color: white;
|
|
7801
|
-
}
|
|
8080
|
+
.topbar-actions { display: flex; align-items: center; gap: 10px; }
|
|
7802
8081
|
|
|
7803
|
-
.btn
|
|
7804
|
-
|
|
8082
|
+
.btn {
|
|
8083
|
+
font-family: var(--sans);
|
|
8084
|
+
font-weight: 700;
|
|
8085
|
+
font-size: 12px;
|
|
8086
|
+
padding: 8px 16px;
|
|
8087
|
+
border-radius: 8px;
|
|
8088
|
+
border: none;
|
|
8089
|
+
cursor: pointer;
|
|
8090
|
+
transition: all 0.15s ease;
|
|
8091
|
+
display: inline-flex;
|
|
8092
|
+
align-items: center;
|
|
8093
|
+
gap: 6px;
|
|
8094
|
+
text-decoration: none;
|
|
7805
8095
|
}
|
|
8096
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
7806
8097
|
|
|
7807
|
-
.btn
|
|
7808
|
-
|
|
7809
|
-
|
|
8098
|
+
.btn-ghost {
|
|
8099
|
+
background: var(--panel);
|
|
8100
|
+
color: var(--text-2);
|
|
8101
|
+
border: 1px solid var(--border);
|
|
7810
8102
|
}
|
|
8103
|
+
.btn-ghost:hover { border-color: var(--border-hi); color: var(--text-1); }
|
|
7811
8104
|
|
|
7812
|
-
.
|
|
7813
|
-
background:
|
|
7814
|
-
|
|
7815
|
-
padding: 30px;
|
|
7816
|
-
border-radius: 12px;
|
|
7817
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
8105
|
+
.btn-primary {
|
|
8106
|
+
background: var(--accent);
|
|
8107
|
+
color: #fff;
|
|
7818
8108
|
}
|
|
8109
|
+
.btn-primary:hover:not(:disabled) { background: #ea580c; box-shadow: 0 0 20px rgba(249,115,22,0.35); }
|
|
7819
8110
|
|
|
7820
|
-
|
|
8111
|
+
/* \u2500\u2500 Content \u2500\u2500 */
|
|
8112
|
+
.content { padding: 28px 32px; display: flex; flex-direction: column; gap: 24px; }
|
|
8113
|
+
|
|
8114
|
+
/* \u2500\u2500 Tabs (category selector) \u2500\u2500 */
|
|
8115
|
+
.tab-bar {
|
|
7821
8116
|
display: flex;
|
|
7822
|
-
gap:
|
|
7823
|
-
|
|
7824
|
-
border
|
|
7825
|
-
|
|
8117
|
+
gap: 4px;
|
|
8118
|
+
background: var(--surface);
|
|
8119
|
+
border: 1px solid var(--border);
|
|
8120
|
+
border-radius: 10px;
|
|
8121
|
+
padding: 4px;
|
|
8122
|
+
flex-wrap: wrap;
|
|
7826
8123
|
}
|
|
7827
8124
|
|
|
7828
|
-
.tab {
|
|
7829
|
-
padding:
|
|
8125
|
+
.tab-btn {
|
|
8126
|
+
padding: 7px 14px;
|
|
8127
|
+
background: transparent;
|
|
7830
8128
|
border: none;
|
|
7831
|
-
|
|
7832
|
-
|
|
8129
|
+
border-radius: 7px;
|
|
8130
|
+
color: var(--text-3);
|
|
8131
|
+
font-family: var(--sans);
|
|
8132
|
+
font-size: 12px;
|
|
7833
8133
|
font-weight: 600;
|
|
7834
|
-
color: #718096;
|
|
7835
8134
|
cursor: pointer;
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
color: #667eea;
|
|
7842
|
-
border-bottom-color: #667eea;
|
|
8135
|
+
transition: all 0.15s ease;
|
|
8136
|
+
white-space: nowrap;
|
|
8137
|
+
display: flex;
|
|
8138
|
+
align-items: center;
|
|
8139
|
+
gap: 5px;
|
|
7843
8140
|
}
|
|
7844
|
-
|
|
7845
|
-
.tab
|
|
7846
|
-
|
|
8141
|
+
.tab-btn:hover { color: var(--text-2); }
|
|
8142
|
+
.tab-btn.active {
|
|
8143
|
+
background: var(--panel);
|
|
8144
|
+
color: var(--text-1);
|
|
8145
|
+
border: 1px solid var(--border-hi);
|
|
7847
8146
|
}
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
8147
|
+
.tab-btn .tab-dot {
|
|
8148
|
+
width: 6px; height: 6px;
|
|
8149
|
+
border-radius: 50%;
|
|
8150
|
+
background: var(--text-3);
|
|
7851
8151
|
}
|
|
8152
|
+
.tab-btn.has-required .tab-dot { background: var(--amber); }
|
|
8153
|
+
.tab-btn.active .tab-dot { background: var(--accent); }
|
|
7852
8154
|
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
}
|
|
8155
|
+
/* \u2500\u2500 Section \u2500\u2500 */
|
|
8156
|
+
.section { display: none; flex-direction: column; gap: 16px; }
|
|
8157
|
+
.section.active { display: flex; }
|
|
7856
8158
|
|
|
7857
|
-
.
|
|
8159
|
+
.section-header {
|
|
7858
8160
|
display: flex;
|
|
7859
|
-
justify-content: space-between;
|
|
7860
8161
|
align-items: center;
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
.category-title {
|
|
7865
|
-
font-size: 18px;
|
|
7866
|
-
font-weight: 600;
|
|
7867
|
-
color: #2d3748;
|
|
7868
|
-
}
|
|
7869
|
-
|
|
7870
|
-
.env-grid {
|
|
7871
|
-
display: grid;
|
|
7872
|
-
gap: 20px;
|
|
8162
|
+
gap: 12px;
|
|
8163
|
+
margin-bottom: 4px;
|
|
7873
8164
|
}
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
8165
|
+
.section-icon {
|
|
8166
|
+
width: 36px; height: 36px;
|
|
8167
|
+
background: var(--accent-lo);
|
|
8168
|
+
border: 1px solid var(--accent-md);
|
|
7877
8169
|
border-radius: 8px;
|
|
7878
|
-
|
|
7879
|
-
|
|
8170
|
+
display: grid;
|
|
8171
|
+
place-items: center;
|
|
8172
|
+
font-size: 16px;
|
|
7880
8173
|
}
|
|
8174
|
+
.section-title { font-size: 15px; font-weight: 700; }
|
|
8175
|
+
.section-desc { font-family: var(--mono); font-size: 11px; color: var(--text-3); margin-top: 2px; }
|
|
7881
8176
|
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
8177
|
+
/* \u2500\u2500 Env card \u2500\u2500 */
|
|
8178
|
+
.env-card {
|
|
8179
|
+
background: var(--panel);
|
|
8180
|
+
border: 1px solid var(--border);
|
|
8181
|
+
border-radius: var(--radius-lg);
|
|
8182
|
+
padding: 20px 24px;
|
|
8183
|
+
transition: border-color 0.15s;
|
|
7885
8184
|
}
|
|
8185
|
+
.env-card:hover { border-color: var(--border-hi); }
|
|
8186
|
+
.env-card.has-value { border-color: rgba(249,115,22,0.15); }
|
|
7886
8187
|
|
|
7887
|
-
.env-
|
|
8188
|
+
.env-card-head {
|
|
7888
8189
|
display: flex;
|
|
7889
8190
|
justify-content: space-between;
|
|
7890
8191
|
align-items: flex-start;
|
|
7891
|
-
margin-bottom:
|
|
8192
|
+
margin-bottom: 6px;
|
|
7892
8193
|
}
|
|
7893
8194
|
|
|
7894
|
-
.env-
|
|
7895
|
-
font-
|
|
7896
|
-
|
|
7897
|
-
font-
|
|
8195
|
+
.env-key {
|
|
8196
|
+
font-family: var(--mono);
|
|
8197
|
+
font-size: 13px;
|
|
8198
|
+
font-weight: 500;
|
|
8199
|
+
color: var(--text-1);
|
|
7898
8200
|
display: flex;
|
|
7899
8201
|
align-items: center;
|
|
7900
8202
|
gap: 8px;
|
|
7901
8203
|
}
|
|
7902
8204
|
|
|
7903
|
-
.required
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
font-
|
|
7907
|
-
|
|
8205
|
+
.badge-required {
|
|
8206
|
+
font-family: var(--sans);
|
|
8207
|
+
font-size: 9px;
|
|
8208
|
+
font-weight: 700;
|
|
8209
|
+
letter-spacing: 0.08em;
|
|
8210
|
+
text-transform: uppercase;
|
|
8211
|
+
background: rgba(239,68,68,0.15);
|
|
8212
|
+
color: var(--red);
|
|
8213
|
+
border: 1px solid rgba(239,68,68,0.25);
|
|
7908
8214
|
border-radius: 4px;
|
|
8215
|
+
padding: 2px 6px;
|
|
8216
|
+
}
|
|
8217
|
+
.badge-sensitive {
|
|
8218
|
+
font-size: 9px;
|
|
7909
8219
|
font-weight: 700;
|
|
8220
|
+
font-family: var(--sans);
|
|
8221
|
+
letter-spacing: 0.08em;
|
|
8222
|
+
text-transform: uppercase;
|
|
8223
|
+
background: var(--amber-lo);
|
|
8224
|
+
color: var(--amber);
|
|
8225
|
+
border: 1px solid rgba(245,158,11,0.2);
|
|
8226
|
+
border-radius: 4px;
|
|
8227
|
+
padding: 2px 6px;
|
|
7910
8228
|
}
|
|
7911
8229
|
|
|
7912
|
-
.env-
|
|
7913
|
-
font-
|
|
7914
|
-
|
|
7915
|
-
|
|
8230
|
+
.env-desc {
|
|
8231
|
+
font-family: var(--mono);
|
|
8232
|
+
font-size: 11px;
|
|
8233
|
+
color: var(--text-3);
|
|
8234
|
+
margin-bottom: 14px;
|
|
8235
|
+
line-height: 1.5;
|
|
7916
8236
|
}
|
|
7917
8237
|
|
|
7918
|
-
.env-input-
|
|
8238
|
+
.env-input-row {
|
|
7919
8239
|
display: flex;
|
|
7920
|
-
gap:
|
|
8240
|
+
gap: 8px;
|
|
7921
8241
|
align-items: center;
|
|
7922
8242
|
}
|
|
7923
8243
|
|
|
7924
|
-
.env-input {
|
|
8244
|
+
.env-input, .env-select {
|
|
7925
8245
|
flex: 1;
|
|
7926
|
-
|
|
7927
|
-
border: 1px solid
|
|
7928
|
-
border-radius:
|
|
7929
|
-
|
|
7930
|
-
font-family:
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
.env-input:focus {
|
|
8246
|
+
background: var(--surface);
|
|
8247
|
+
border: 1px solid var(--border-hi);
|
|
8248
|
+
border-radius: 8px;
|
|
8249
|
+
padding: 10px 14px;
|
|
8250
|
+
font-family: var(--mono);
|
|
8251
|
+
font-size: 13px;
|
|
8252
|
+
color: var(--text-1);
|
|
7935
8253
|
outline: none;
|
|
7936
|
-
border-color
|
|
7937
|
-
|
|
8254
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
8255
|
+
appearance: none;
|
|
7938
8256
|
}
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
8257
|
+
.env-input:focus, .env-select:focus {
|
|
8258
|
+
border-color: var(--accent);
|
|
8259
|
+
box-shadow: 0 0 0 3px rgba(249,115,22,0.12);
|
|
7942
8260
|
}
|
|
8261
|
+
.env-input.changed { border-color: rgba(249,115,22,0.5); }
|
|
8262
|
+
.env-input.invalid { border-color: var(--red); }
|
|
7943
8263
|
|
|
7944
|
-
.env-
|
|
7945
|
-
display: flex;
|
|
7946
|
-
gap: 5px;
|
|
7947
|
-
}
|
|
8264
|
+
.env-select option { background: var(--panel); }
|
|
7948
8265
|
|
|
7949
|
-
.
|
|
7950
|
-
|
|
7951
|
-
border:
|
|
7952
|
-
|
|
7953
|
-
|
|
8266
|
+
.env-action-btn {
|
|
8267
|
+
width: 36px; height: 36px;
|
|
8268
|
+
border-radius: 8px;
|
|
8269
|
+
border: 1px solid var(--border-hi);
|
|
8270
|
+
background: var(--surface);
|
|
8271
|
+
color: var(--text-2);
|
|
7954
8272
|
cursor: pointer;
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
background: #cbd5e0;
|
|
7961
|
-
}
|
|
7962
|
-
|
|
7963
|
-
.icon-btn.test {
|
|
7964
|
-
background: #bee3f8;
|
|
7965
|
-
color: #2c5282;
|
|
7966
|
-
}
|
|
7967
|
-
|
|
7968
|
-
.icon-btn.test:hover {
|
|
7969
|
-
background: #90cdf4;
|
|
7970
|
-
}
|
|
7971
|
-
|
|
7972
|
-
.icon-btn.generate {
|
|
7973
|
-
background: #c6f6d5;
|
|
7974
|
-
color: #22543d;
|
|
7975
|
-
}
|
|
7976
|
-
|
|
7977
|
-
.icon-btn.generate:hover {
|
|
7978
|
-
background: #9ae6b4;
|
|
8273
|
+
display: grid;
|
|
8274
|
+
place-items: center;
|
|
8275
|
+
font-size: 14px;
|
|
8276
|
+
transition: all 0.15s;
|
|
8277
|
+
flex-shrink: 0;
|
|
7979
8278
|
}
|
|
8279
|
+
.env-action-btn:hover { background: var(--panel); color: var(--text-1); border-color: var(--border-hi); }
|
|
8280
|
+
.env-action-btn.test-btn:hover { background: var(--blue-lo); color: var(--blue); border-color: rgba(56,189,248,0.25); }
|
|
8281
|
+
.env-action-btn.gen-btn:hover { background: var(--green-lo); color: var(--green); border-color: rgba(34,197,94,0.25); }
|
|
7980
8282
|
|
|
7981
|
-
.
|
|
7982
|
-
|
|
7983
|
-
font-size:
|
|
7984
|
-
margin-top:
|
|
8283
|
+
.env-feedback {
|
|
8284
|
+
font-family: var(--mono);
|
|
8285
|
+
font-size: 11px;
|
|
8286
|
+
margin-top: 8px;
|
|
8287
|
+
min-height: 16px;
|
|
7985
8288
|
}
|
|
8289
|
+
.env-feedback.error { color: var(--red); }
|
|
8290
|
+
.env-feedback.success { color: var(--green); }
|
|
7986
8291
|
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
8292
|
+
/* \u2500\u2500 Toast \u2500\u2500 */
|
|
8293
|
+
.toast-zone {
|
|
8294
|
+
position: fixed;
|
|
8295
|
+
bottom: 24px;
|
|
8296
|
+
right: 24px;
|
|
8297
|
+
display: flex;
|
|
8298
|
+
flex-direction: column;
|
|
8299
|
+
gap: 8px;
|
|
8300
|
+
z-index: 100;
|
|
7991
8301
|
}
|
|
7992
8302
|
|
|
7993
|
-
.
|
|
7994
|
-
padding:
|
|
7995
|
-
border-radius:
|
|
7996
|
-
|
|
8303
|
+
.toast {
|
|
8304
|
+
padding: 12px 18px;
|
|
8305
|
+
border-radius: 10px;
|
|
8306
|
+
font-size: 13px;
|
|
8307
|
+
font-weight: 600;
|
|
7997
8308
|
display: flex;
|
|
7998
8309
|
align-items: center;
|
|
7999
8310
|
gap: 10px;
|
|
8311
|
+
animation: slideIn 0.2s ease;
|
|
8312
|
+
max-width: 380px;
|
|
8000
8313
|
}
|
|
8314
|
+
.toast.success { background: var(--panel); border: 1px solid rgba(34,197,94,0.3); color: var(--green); }
|
|
8315
|
+
.toast.error { background: var(--panel); border: 1px solid rgba(239,68,68,0.3); color: var(--red); }
|
|
8316
|
+
.toast.warning { background: var(--panel); border: 1px solid rgba(245,158,11,0.3); color: var(--amber); }
|
|
8317
|
+
.toast.info { background: var(--panel); border: 1px solid rgba(56,189,248,0.3); color: var(--blue); }
|
|
8001
8318
|
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
color: #92400e;
|
|
8006
|
-
}
|
|
8007
|
-
|
|
8008
|
-
.alert-info {
|
|
8009
|
-
background: #eff6ff;
|
|
8010
|
-
border-left: 4px solid #3b82f6;
|
|
8011
|
-
color: #1e40af;
|
|
8319
|
+
@keyframes slideIn {
|
|
8320
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
8321
|
+
to { opacity: 1; transform: translateY(0); }
|
|
8012
8322
|
}
|
|
8013
8323
|
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8324
|
+
/* \u2500\u2500 Modal (test result) \u2500\u2500 */
|
|
8325
|
+
.modal-backdrop {
|
|
8326
|
+
display: none;
|
|
8327
|
+
position: fixed; inset: 0;
|
|
8328
|
+
background: rgba(0,0,0,0.6);
|
|
8329
|
+
z-index: 200;
|
|
8330
|
+
align-items: center;
|
|
8331
|
+
justify-content: center;
|
|
8018
8332
|
}
|
|
8333
|
+
.modal-backdrop.open { display: flex; }
|
|
8019
8334
|
|
|
8020
|
-
.
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8335
|
+
.modal {
|
|
8336
|
+
background: var(--surface);
|
|
8337
|
+
border: 1px solid var(--border-hi);
|
|
8338
|
+
border-radius: var(--radius-lg);
|
|
8339
|
+
padding: 28px;
|
|
8340
|
+
width: 420px;
|
|
8341
|
+
max-width: 90vw;
|
|
8342
|
+
box-shadow: 0 24px 64px rgba(0,0,0,0.5);
|
|
8343
|
+
}
|
|
8344
|
+
.modal-title { font-size: 15px; font-weight: 700; margin-bottom: 16px; }
|
|
8345
|
+
.modal-body { margin-bottom: 20px; }
|
|
8346
|
+
.modal-result {
|
|
8347
|
+
padding: 14px;
|
|
8348
|
+
border-radius: 8px;
|
|
8349
|
+
font-family: var(--mono);
|
|
8350
|
+
font-size: 12px;
|
|
8024
8351
|
}
|
|
8352
|
+
.modal-result.ok { background: var(--green-lo); border: 1px solid rgba(34,197,94,0.2); color: var(--green); }
|
|
8353
|
+
.modal-result.fail { background: var(--red-lo); border: 1px solid rgba(239,68,68,0.2); color: var(--red); }
|
|
8354
|
+
.modal-footer { display: flex; justify-content: flex-end; }
|
|
8025
8355
|
|
|
8356
|
+
/* \u2500\u2500 Spinner \u2500\u2500 */
|
|
8026
8357
|
.spinner {
|
|
8027
|
-
|
|
8028
|
-
border
|
|
8358
|
+
width: 36px; height: 36px;
|
|
8359
|
+
border: 3px solid var(--border);
|
|
8360
|
+
border-top-color: var(--accent);
|
|
8029
8361
|
border-radius: 50%;
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
animation: spin 1s linear infinite;
|
|
8033
|
-
margin: 0 auto 20px;
|
|
8362
|
+
animation: spin 0.8s linear infinite;
|
|
8363
|
+
margin: 0 auto 16px;
|
|
8034
8364
|
}
|
|
8365
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
8035
8366
|
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8367
|
+
.loading-state {
|
|
8368
|
+
text-align: center;
|
|
8369
|
+
padding: 60px 0;
|
|
8370
|
+
color: var(--text-3);
|
|
8371
|
+
font-family: var(--mono);
|
|
8372
|
+
font-size: 12px;
|
|
8039
8373
|
}
|
|
8040
8374
|
|
|
8041
|
-
|
|
8375
|
+
/* \u2500\u2500 Restart banner \u2500\u2500 */
|
|
8376
|
+
.restart-banner {
|
|
8042
8377
|
display: none;
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8378
|
+
background: var(--amber-lo);
|
|
8379
|
+
border: 1px solid rgba(245,158,11,0.25);
|
|
8380
|
+
border-radius: 10px;
|
|
8381
|
+
padding: 12px 18px;
|
|
8382
|
+
font-size: 13px;
|
|
8383
|
+
color: var(--amber);
|
|
8384
|
+
font-weight: 600;
|
|
8050
8385
|
align-items: center;
|
|
8051
|
-
|
|
8386
|
+
gap: 10px;
|
|
8052
8387
|
}
|
|
8388
|
+
.restart-banner.show { display: flex; }
|
|
8389
|
+
</style>
|
|
8390
|
+
</head>
|
|
8391
|
+
<body>
|
|
8392
|
+
<div class="shell">
|
|
8053
8393
|
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8394
|
+
<!-- Sidebar -->
|
|
8395
|
+
<aside>
|
|
8396
|
+
<div class="logo">
|
|
8397
|
+
<div class="logo-mark">Q</div>
|
|
8398
|
+
<div>
|
|
8399
|
+
<div class="logo-name">OpenQA</div>
|
|
8400
|
+
<div class="logo-version">v1.3.4</div>
|
|
8401
|
+
</div>
|
|
8402
|
+
</div>
|
|
8057
8403
|
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8404
|
+
<div class="nav-section">
|
|
8405
|
+
<div class="nav-label">Overview</div>
|
|
8406
|
+
<a class="nav-item" href="/">
|
|
8407
|
+
<span class="icon">\u{1F4CA}</span> Dashboard
|
|
8408
|
+
</a>
|
|
8409
|
+
<a class="nav-item" href="/sessions">
|
|
8410
|
+
<span class="icon">\u{1F9EA}</span> Sessions
|
|
8411
|
+
</a>
|
|
8412
|
+
<a class="nav-item" href="/issues">
|
|
8413
|
+
<span class="icon">\u{1F41B}</span> Issues
|
|
8414
|
+
</a>
|
|
8066
8415
|
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8416
|
+
<div class="nav-label">Testing</div>
|
|
8417
|
+
<a class="nav-item" href="/tests">
|
|
8418
|
+
<span class="icon">\u26A1</span> Tests
|
|
8419
|
+
</a>
|
|
8420
|
+
<a class="nav-item" href="/coverage">
|
|
8421
|
+
<span class="icon">\u{1F4C8}</span> Coverage
|
|
8422
|
+
</a>
|
|
8423
|
+
<a class="nav-item" href="/kanban">
|
|
8424
|
+
<span class="icon">\u{1F4CB}</span> Kanban
|
|
8425
|
+
</a>
|
|
8073
8426
|
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8427
|
+
<div class="nav-label">System</div>
|
|
8428
|
+
<a class="nav-item" href="/config">
|
|
8429
|
+
<span class="icon">\u2699\uFE0F</span> Config
|
|
8430
|
+
</a>
|
|
8431
|
+
<a class="nav-item active" href="/config/env">
|
|
8432
|
+
<span class="icon">\u{1F527}</span> Environment
|
|
8433
|
+
</a>
|
|
8434
|
+
<a class="nav-item" href="/logs">
|
|
8435
|
+
<span class="icon">\u{1F4DC}</span> Logs
|
|
8436
|
+
</a>
|
|
8437
|
+
</div>
|
|
8078
8438
|
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
gap: 10px;
|
|
8082
|
-
justify-content: flex-end;
|
|
8083
|
-
}
|
|
8084
|
-
</style>
|
|
8085
|
-
</head>
|
|
8086
|
-
<body>
|
|
8087
|
-
<div class="container">
|
|
8088
|
-
<div class="header">
|
|
8089
|
-
<h1>
|
|
8090
|
-
<span>\u2699\uFE0F</span>
|
|
8439
|
+
<div class="sidebar-footer">
|
|
8440
|
+
<div style="font-family:var(--mono);font-size:11px;color:var(--text-3);">
|
|
8091
8441
|
Environment Variables
|
|
8092
|
-
</
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8442
|
+
</div>
|
|
8443
|
+
</div>
|
|
8444
|
+
</aside>
|
|
8445
|
+
|
|
8446
|
+
<!-- Main -->
|
|
8447
|
+
<main>
|
|
8448
|
+
<div class="topbar">
|
|
8449
|
+
<div>
|
|
8450
|
+
<div class="page-title">Environment Variables</div>
|
|
8451
|
+
<div class="page-sub">Configure runtime variables for OpenQA</div>
|
|
8452
|
+
</div>
|
|
8453
|
+
<div class="topbar-actions">
|
|
8454
|
+
<a class="btn btn-ghost" href="/config">\u2190 Back to Config</a>
|
|
8455
|
+
<button id="saveBtn" class="btn btn-primary" disabled>
|
|
8456
|
+
\u{1F4BE} Save Changes
|
|
8457
|
+
</button>
|
|
8096
8458
|
</div>
|
|
8097
8459
|
</div>
|
|
8098
8460
|
|
|
8099
8461
|
<div class="content">
|
|
8100
|
-
|
|
8462
|
+
|
|
8463
|
+
<!-- Restart banner -->
|
|
8464
|
+
<div class="restart-banner" id="restartBanner">
|
|
8465
|
+
\u26A0\uFE0F Some changes require a server restart to take effect.
|
|
8466
|
+
</div>
|
|
8467
|
+
|
|
8468
|
+
<!-- Loading -->
|
|
8469
|
+
<div class="loading-state" id="loadingState">
|
|
8101
8470
|
<div class="spinner"></div>
|
|
8102
|
-
|
|
8471
|
+
Loading environment variables\u2026
|
|
8103
8472
|
</div>
|
|
8104
8473
|
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
<button class="tab" data-category="web">\u{1F310} Web Server</button>
|
|
8114
|
-
<button class="tab" data-category="agent">\u{1F916} Agent</button>
|
|
8115
|
-
<button class="tab" data-category="database">\u{1F4BE} Database</button>
|
|
8116
|
-
<button class="tab" data-category="notifications">\u{1F514} Notifications</button>
|
|
8117
|
-
</div>
|
|
8474
|
+
<!-- Main content (hidden while loading) -->
|
|
8475
|
+
<div id="mainContent" style="display:none;flex-direction:column;gap:24px;">
|
|
8476
|
+
|
|
8477
|
+
<!-- Tab bar -->
|
|
8478
|
+
<div class="tab-bar" id="tabBar"></div>
|
|
8479
|
+
|
|
8480
|
+
<!-- Sections -->
|
|
8481
|
+
<div id="sections"></div>
|
|
8118
8482
|
|
|
8119
|
-
<div id="categories"></div>
|
|
8120
8483
|
</div>
|
|
8121
8484
|
</div>
|
|
8122
|
-
</
|
|
8485
|
+
</main>
|
|
8486
|
+
</div>
|
|
8123
8487
|
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
<div class="modal-
|
|
8130
|
-
|
|
8131
|
-
|
|
8488
|
+
<!-- Test result modal -->
|
|
8489
|
+
<div class="modal-backdrop" id="testModal">
|
|
8490
|
+
<div class="modal">
|
|
8491
|
+
<div class="modal-title">Connection Test</div>
|
|
8492
|
+
<div class="modal-body">
|
|
8493
|
+
<div class="modal-result" id="testResultBox">\u2026</div>
|
|
8494
|
+
</div>
|
|
8495
|
+
<div class="modal-footer">
|
|
8496
|
+
<button class="btn btn-ghost" onclick="closeModal()">Close</button>
|
|
8132
8497
|
</div>
|
|
8133
8498
|
</div>
|
|
8499
|
+
</div>
|
|
8134
8500
|
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
let changedVariables = {};
|
|
8138
|
-
let restartRequired = false;
|
|
8501
|
+
<!-- Toast zone -->
|
|
8502
|
+
<div class="toast-zone" id="toastZone"></div>
|
|
8139
8503
|
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8504
|
+
<script>
|
|
8505
|
+
/* \u2500\u2500 State \u2500\u2500 */
|
|
8506
|
+
let envVars = [];
|
|
8507
|
+
let changed = {};
|
|
8508
|
+
let hasRequiredMissing = false;
|
|
8509
|
+
|
|
8510
|
+
const TABS = [
|
|
8511
|
+
{ id: 'llm', label: '\u{1F916} LLM', desc: 'Language model provider & API keys' },
|
|
8512
|
+
{ id: 'security', label: '\u{1F512} Security', desc: 'Authentication & JWT configuration' },
|
|
8513
|
+
{ id: 'target', label: '\u{1F3AF} Target App', desc: 'Application under test settings' },
|
|
8514
|
+
{ id: 'github', label: '\u{1F419} GitHub', desc: 'Repository & CI/CD integration' },
|
|
8515
|
+
{ id: 'web', label: '\u{1F310} Web Server', desc: 'HTTP host, port & CORS settings' },
|
|
8516
|
+
{ id: 'agent', label: '\u{1F916} Agent', desc: 'Autonomous agent behaviour' },
|
|
8517
|
+
{ id: 'database', label: '\u{1F4BE} Database', desc: 'Persistence & storage' },
|
|
8518
|
+
{ id: 'notifications', label: '\u{1F514} Notifications', desc: 'Slack & Discord webhooks' },
|
|
8519
|
+
];
|
|
8156
8520
|
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
</div>
|
|
8173
|
-
<div class="env-grid">
|
|
8174
|
-
\${vars.map(v => renderEnvItem(v)).join('')}
|
|
8175
|
-
</div>
|
|
8176
|
-
\`;
|
|
8177
|
-
|
|
8178
|
-
container.appendChild(section);
|
|
8179
|
-
});
|
|
8180
|
-
}
|
|
8521
|
+
/* \u2500\u2500 Init \u2500\u2500 */
|
|
8522
|
+
async function init() {
|
|
8523
|
+
try {
|
|
8524
|
+
const res = await fetch('/api/env');
|
|
8525
|
+
if (!res.ok) { toast('error', 'Failed to load environment variables (status ' + res.status + ')'); return; }
|
|
8526
|
+
const data = await res.json();
|
|
8527
|
+
envVars = data.variables || [];
|
|
8528
|
+
renderAll();
|
|
8529
|
+
document.getElementById('loadingState').style.display = 'none';
|
|
8530
|
+
const mc = document.getElementById('mainContent');
|
|
8531
|
+
mc.style.display = 'flex';
|
|
8532
|
+
} catch (e) {
|
|
8533
|
+
toast('error', 'Network error \u2014 ' + e.message);
|
|
8534
|
+
}
|
|
8535
|
+
}
|
|
8181
8536
|
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
type="\${inputType}"
|
|
8212
|
-
class="env-input"
|
|
8213
|
-
data-key="\${envVar.key}"
|
|
8214
|
-
value="\${value}"
|
|
8215
|
-
placeholder="\${envVar.placeholder || ''}"
|
|
8216
|
-
onchange="handleChange(this)"
|
|
8217
|
-
/>\`
|
|
8218
|
-
}
|
|
8219
|
-
<div class="env-actions">
|
|
8220
|
-
\${envVar.testable ? \`<button class="icon-btn test" onclick="testVariable('\${envVar.key}')" title="Test">\u{1F9EA}</button>\` : ''}
|
|
8221
|
-
\${envVar.key === 'OPENQA_JWT_SECRET' ? \`<button class="icon-btn generate" onclick="generateSecret('\${envVar.key}')" title="Generate">\u{1F511}</button>\` : ''}
|
|
8222
|
-
</div>
|
|
8223
|
-
</div>
|
|
8224
|
-
<div class="error-message" id="error-\${envVar.key}"></div>
|
|
8225
|
-
<div class="success-message" id="success-\${envVar.key}"></div>
|
|
8537
|
+
/* \u2500\u2500 Render \u2500\u2500 */
|
|
8538
|
+
function renderAll() {
|
|
8539
|
+
renderTabBar();
|
|
8540
|
+
renderSections();
|
|
8541
|
+
activateTab(TABS[0].id);
|
|
8542
|
+
}
|
|
8543
|
+
|
|
8544
|
+
function renderTabBar() {
|
|
8545
|
+
const bar = document.getElementById('tabBar');
|
|
8546
|
+
bar.innerHTML = TABS.map(t => {
|
|
8547
|
+
const vars = envVars.filter(v => v.category === t.id);
|
|
8548
|
+
const hasRequired = vars.some(v => v.required);
|
|
8549
|
+
return \`<button class="tab-btn\${hasRequired ? ' has-required' : ''}" data-tab="\${t.id}" onclick="activateTab('\${t.id}')">
|
|
8550
|
+
<span class="tab-dot"></span>
|
|
8551
|
+
\${t.label}
|
|
8552
|
+
</button>\`;
|
|
8553
|
+
}).join('');
|
|
8554
|
+
}
|
|
8555
|
+
|
|
8556
|
+
function renderSections() {
|
|
8557
|
+
const container = document.getElementById('sections');
|
|
8558
|
+
container.innerHTML = TABS.map(t => {
|
|
8559
|
+
const vars = envVars.filter(v => v.category === t.id);
|
|
8560
|
+
return \`<div class="section" id="section-\${t.id}">
|
|
8561
|
+
<div class="section-header">
|
|
8562
|
+
<div class="section-icon">\${t.label.split(' ')[0]}</div>
|
|
8563
|
+
<div>
|
|
8564
|
+
<div class="section-title">\${t.label.slice(t.label.indexOf(' ')+1)}</div>
|
|
8565
|
+
<div class="section-desc">\${t.desc}</div>
|
|
8226
8566
|
</div>
|
|
8227
|
-
|
|
8228
|
-
|
|
8567
|
+
</div>
|
|
8568
|
+
\${vars.map(renderCard).join('')}
|
|
8569
|
+
\${vars.length === 0 ? '<div style="color:var(--text-3);font-family:var(--mono);font-size:12px;padding:20px 0">No variables in this category.</div>' : ''}
|
|
8570
|
+
</div>\`;
|
|
8571
|
+
}).join('');
|
|
8572
|
+
}
|
|
8229
8573
|
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8574
|
+
function renderCard(v) {
|
|
8575
|
+
const displayVal = v.displayValue || '';
|
|
8576
|
+
const isSensitive = v.sensitive;
|
|
8577
|
+
const inputType = (v.type === 'password' && !changed[v.key]) ? 'password' : 'text';
|
|
8578
|
+
|
|
8579
|
+
let inputHTML = '';
|
|
8580
|
+
if (v.type === 'select' || v.type === 'boolean') {
|
|
8581
|
+
const opts = v.type === 'boolean'
|
|
8582
|
+
? [{ val: 'true', lbl: 'true' }, { val: 'false', lbl: 'false' }]
|
|
8583
|
+
: (v.options || []).map(o => ({ val: o, lbl: o }));
|
|
8584
|
+
inputHTML = \`<select class="env-select" data-key="\${v.key}" onchange="handleChange(this)">
|
|
8585
|
+
<option value="">\u2014 Select \u2014</option>
|
|
8586
|
+
\${opts.map(o => \`<option value="\${o.val}" \${displayVal === o.val ? 'selected' : ''}>\${o.lbl}</option>\`).join('')}
|
|
8587
|
+
</select>\`;
|
|
8588
|
+
} else {
|
|
8589
|
+
inputHTML = \`<input
|
|
8590
|
+
type="\${inputType}"
|
|
8591
|
+
class="env-input"
|
|
8592
|
+
data-key="\${v.key}"
|
|
8593
|
+
value="\${escHtml(displayVal)}"
|
|
8594
|
+
placeholder="\${escHtml(v.placeholder || '')}"
|
|
8595
|
+
oninput="handleChange(this)"
|
|
8596
|
+
autocomplete="off"
|
|
8597
|
+
/>\`;
|
|
8598
|
+
}
|
|
8599
|
+
|
|
8600
|
+
const testBtn = v.testable
|
|
8601
|
+
? \`<button class="env-action-btn test-btn" onclick="testVar('\${v.key}')" title="Test connection">\u{1F9EA}</button>\`
|
|
8602
|
+
: '';
|
|
8603
|
+
|
|
8604
|
+
const genBtn = v.key === 'OPENQA_JWT_SECRET'
|
|
8605
|
+
? \`<button class="env-action-btn gen-btn" onclick="generateSecret('\${v.key}')" title="Generate secret">\u{1F511}</button>\`
|
|
8606
|
+
: '';
|
|
8607
|
+
|
|
8608
|
+
const toggleBtn = (v.type === 'password' || isSensitive)
|
|
8609
|
+
? \`<button class="env-action-btn" onclick="toggleVis('\${v.key}')" title="Toggle visibility" id="vis-\${v.key}">\u{1F441}</button>\`
|
|
8610
|
+
: '';
|
|
8611
|
+
|
|
8612
|
+
return \`<div class="env-card\${displayVal ? ' has-value' : ''}" id="card-\${v.key}">
|
|
8613
|
+
<div class="env-card-head">
|
|
8614
|
+
<div class="env-key">
|
|
8615
|
+
\${v.key}
|
|
8616
|
+
\${v.required ? '<span class="badge-required">Required</span>' : ''}
|
|
8617
|
+
\${isSensitive ? '<span class="badge-sensitive">Sensitive</span>' : ''}
|
|
8618
|
+
</div>
|
|
8619
|
+
</div>
|
|
8620
|
+
<div class="env-desc">\${v.description}</div>
|
|
8621
|
+
<div class="env-input-row">
|
|
8622
|
+
\${inputHTML}
|
|
8623
|
+
\${toggleBtn}
|
|
8624
|
+
\${testBtn}
|
|
8625
|
+
\${genBtn}
|
|
8626
|
+
</div>
|
|
8627
|
+
<div class="env-feedback" id="fb-\${v.key}"></div>
|
|
8628
|
+
</div>\`;
|
|
8629
|
+
}
|
|
8242
8630
|
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8631
|
+
/* \u2500\u2500 Tab switching \u2500\u2500 */
|
|
8632
|
+
function activateTab(id) {
|
|
8633
|
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === id));
|
|
8634
|
+
document.querySelectorAll('.section').forEach(s => s.classList.toggle('active', s.id === 'section-' + id));
|
|
8635
|
+
}
|
|
8636
|
+
|
|
8637
|
+
/* \u2500\u2500 Input handling \u2500\u2500 */
|
|
8638
|
+
function handleChange(el) {
|
|
8639
|
+
const key = el.dataset.key;
|
|
8640
|
+
const val = el.value;
|
|
8641
|
+
changed[key] = val;
|
|
8642
|
+
el.classList.add('changed');
|
|
8643
|
+
el.classList.remove('invalid');
|
|
8644
|
+
clearFeedback(key);
|
|
8645
|
+
document.getElementById('saveBtn').disabled = false;
|
|
8646
|
+
}
|
|
8647
|
+
|
|
8648
|
+
/* \u2500\u2500 Toggle password visibility \u2500\u2500 */
|
|
8649
|
+
function toggleVis(key) {
|
|
8650
|
+
const inp = document.querySelector('[data-key="' + key + '"]');
|
|
8651
|
+
if (!inp || inp.tagName !== 'INPUT') return;
|
|
8652
|
+
inp.type = inp.type === 'password' ? 'text' : 'password';
|
|
8653
|
+
}
|
|
8654
|
+
|
|
8655
|
+
/* \u2500\u2500 Save \u2500\u2500 */
|
|
8656
|
+
async function saveChanges() {
|
|
8657
|
+
if (!Object.keys(changed).length) return;
|
|
8658
|
+
|
|
8659
|
+
const btn = document.getElementById('saveBtn');
|
|
8660
|
+
btn.disabled = true;
|
|
8661
|
+
btn.textContent = '\u23F3 Saving\u2026';
|
|
8662
|
+
|
|
8663
|
+
try {
|
|
8664
|
+
const res = await fetch('/api/env/bulk', {
|
|
8665
|
+
method: 'POST',
|
|
8666
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8667
|
+
body: JSON.stringify({ variables: changed }),
|
|
8668
|
+
credentials: 'include',
|
|
8669
|
+
});
|
|
8670
|
+
|
|
8671
|
+
const body = await res.json().catch(() => ({}));
|
|
8672
|
+
|
|
8673
|
+
if (!res.ok) {
|
|
8674
|
+
const errStr = body.errors
|
|
8675
|
+
? Object.entries(body.errors).map(([k, v]) => k + ': ' + v).join('; ')
|
|
8676
|
+
: body.error || 'Failed to save';
|
|
8677
|
+
// Show per-field errors
|
|
8678
|
+
if (body.errors) {
|
|
8679
|
+
for (const [k, msg] of Object.entries(body.errors)) {
|
|
8680
|
+
setFeedback(k, 'error', msg);
|
|
8681
|
+
const inp = document.querySelector('[data-key="' + k + '"]');
|
|
8682
|
+
if (inp) inp.classList.add('invalid');
|
|
8259
8683
|
}
|
|
8260
|
-
|
|
8261
|
-
const result = await response.json();
|
|
8262
|
-
restartRequired = result.restartRequired;
|
|
8263
|
-
|
|
8264
|
-
showAlert('success', \`\u2705 Saved \${result.updated} variable(s) successfully!\` +
|
|
8265
|
-
(restartRequired ? ' \u26A0\uFE0F Restart required for changes to take effect.' : ''));
|
|
8266
|
-
|
|
8267
|
-
changedVariables = {};
|
|
8268
|
-
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
8269
|
-
|
|
8270
|
-
// Reload to show updated values
|
|
8271
|
-
setTimeout(() => location.reload(), 2000);
|
|
8272
|
-
} catch (error) {
|
|
8273
|
-
showAlert('error', 'Failed to save: ' + error.message);
|
|
8274
|
-
saveBtn.disabled = false;
|
|
8275
|
-
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
8276
8684
|
}
|
|
8685
|
+
toast('error', errStr);
|
|
8686
|
+
btn.disabled = false;
|
|
8687
|
+
btn.innerHTML = '\u{1F4BE} Save Changes';
|
|
8688
|
+
return;
|
|
8277
8689
|
}
|
|
8278
8690
|
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
const value = input.value;
|
|
8283
|
-
|
|
8284
|
-
if (!value) {
|
|
8285
|
-
showAlert('warning', 'Please enter a value first');
|
|
8286
|
-
return;
|
|
8287
|
-
}
|
|
8288
|
-
|
|
8289
|
-
try {
|
|
8290
|
-
const response = await fetch(\`/api/env/test/\${key}\`, {
|
|
8291
|
-
method: 'POST',
|
|
8292
|
-
headers: { 'Content-Type': 'application/json' },
|
|
8293
|
-
body: JSON.stringify({ value }),
|
|
8294
|
-
});
|
|
8295
|
-
|
|
8296
|
-
const result = await response.json();
|
|
8297
|
-
showTestResult(result);
|
|
8298
|
-
} catch (error) {
|
|
8299
|
-
showTestResult({ success: false, message: 'Test failed: ' + error.message });
|
|
8300
|
-
}
|
|
8691
|
+
toast('success', '\u2705 Saved ' + body.updated + ' variable(s)');
|
|
8692
|
+
if (body.restartRequired) {
|
|
8693
|
+
document.getElementById('restartBanner').classList.add('show');
|
|
8301
8694
|
}
|
|
8302
8695
|
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
const input = document.querySelector(\`[data-key="\${key}"]\`);
|
|
8314
|
-
input.value = result.value;
|
|
8315
|
-
handleChange(input);
|
|
8316
|
-
|
|
8317
|
-
document.getElementById(\`success-\${key}\`).textContent = '\u2705 Secret generated!';
|
|
8318
|
-
} catch (error) {
|
|
8319
|
-
document.getElementById(\`error-\${key}\`).textContent = 'Failed to generate: ' + error.message;
|
|
8320
|
-
}
|
|
8321
|
-
}
|
|
8696
|
+
changed = {};
|
|
8697
|
+
btn.innerHTML = '\u{1F4BE} Save Changes';
|
|
8698
|
+
// Reload to reflect masked values
|
|
8699
|
+
setTimeout(() => location.reload(), 1200);
|
|
8700
|
+
} catch (e) {
|
|
8701
|
+
toast('error', 'Network error \u2014 ' + e.message);
|
|
8702
|
+
btn.disabled = false;
|
|
8703
|
+
btn.innerHTML = '\u{1F4BE} Save Changes';
|
|
8704
|
+
}
|
|
8705
|
+
}
|
|
8322
8706
|
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
resultDiv.innerHTML = \`
|
|
8329
|
-
<div class="alert \${result.success ? 'alert-success' : 'alert-warning'}">
|
|
8330
|
-
\${result.success ? '\u2705' : '\u274C'} \${result.message}
|
|
8331
|
-
</div>
|
|
8332
|
-
\`;
|
|
8333
|
-
|
|
8334
|
-
modal.classList.add('show');
|
|
8335
|
-
}
|
|
8707
|
+
/* \u2500\u2500 Test variable \u2500\u2500 */
|
|
8708
|
+
async function testVar(key) {
|
|
8709
|
+
const inp = document.querySelector('[data-key="' + key + '"]');
|
|
8710
|
+
const val = inp ? inp.value : '';
|
|
8711
|
+
if (!val) { toast('warning', 'Enter a value first'); return; }
|
|
8336
8712
|
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8713
|
+
setFeedback(key, '', '');
|
|
8714
|
+
const btn = document.querySelector('[onclick="testVar(\\''+key+'\\')"]');
|
|
8715
|
+
if (btn) { btn.textContent = '\u23F3'; btn.disabled = true; }
|
|
8340
8716
|
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
alerts.innerHTML = \`
|
|
8348
|
-
<div class="alert \${alertClass}">
|
|
8349
|
-
\${message}
|
|
8350
|
-
</div>
|
|
8351
|
-
\`;
|
|
8352
|
-
|
|
8353
|
-
setTimeout(() => alerts.innerHTML = '', 5000);
|
|
8354
|
-
}
|
|
8355
|
-
|
|
8356
|
-
// Get category title
|
|
8357
|
-
function getCategoryTitle(category) {
|
|
8358
|
-
const titles = {
|
|
8359
|
-
llm: '\u{1F916} LLM Configuration',
|
|
8360
|
-
security: '\u{1F512} Security Settings',
|
|
8361
|
-
target: '\u{1F3AF} Target Application',
|
|
8362
|
-
github: '\u{1F419} GitHub Integration',
|
|
8363
|
-
web: '\u{1F310} Web Server',
|
|
8364
|
-
agent: '\u{1F916} Agent Configuration',
|
|
8365
|
-
database: '\u{1F4BE} Database',
|
|
8366
|
-
notifications: '\u{1F514} Notifications',
|
|
8367
|
-
};
|
|
8368
|
-
return titles[category] || category;
|
|
8369
|
-
}
|
|
8370
|
-
|
|
8371
|
-
// Tab switching
|
|
8372
|
-
document.addEventListener('click', (e) => {
|
|
8373
|
-
if (e.target.classList.contains('tab')) {
|
|
8374
|
-
const category = e.target.dataset.category;
|
|
8375
|
-
|
|
8376
|
-
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
8377
|
-
e.target.classList.add('active');
|
|
8378
|
-
|
|
8379
|
-
document.querySelectorAll('.category-section').forEach(s => s.classList.remove('active'));
|
|
8380
|
-
document.querySelector(\`[data-category="\${category}"]\`).classList.add('active');
|
|
8381
|
-
}
|
|
8717
|
+
try {
|
|
8718
|
+
const res = await fetch('/api/env/test/' + key, {
|
|
8719
|
+
method: 'POST',
|
|
8720
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8721
|
+
body: JSON.stringify({ value: val }),
|
|
8722
|
+
credentials: 'include',
|
|
8382
8723
|
});
|
|
8724
|
+
const result = await res.json();
|
|
8725
|
+
openModal(result.success, result.message);
|
|
8726
|
+
setFeedback(key, result.success ? 'success' : 'error', result.success ? '\u2713 Connected' : '\u2717 ' + result.message);
|
|
8727
|
+
} catch (e) {
|
|
8728
|
+
openModal(false, 'Network error: ' + e.message);
|
|
8729
|
+
} finally {
|
|
8730
|
+
if (btn) { btn.textContent = '\u{1F9EA}'; btn.disabled = false; }
|
|
8731
|
+
}
|
|
8732
|
+
}
|
|
8383
8733
|
|
|
8384
|
-
|
|
8385
|
-
|
|
8734
|
+
/* \u2500\u2500 Generate secret \u2500\u2500 */
|
|
8735
|
+
async function generateSecret(key) {
|
|
8736
|
+
try {
|
|
8737
|
+
const res = await fetch('/api/env/generate/' + key, {
|
|
8738
|
+
method: 'POST', credentials: 'include'
|
|
8739
|
+
});
|
|
8740
|
+
if (!res.ok) throw new Error('Failed to generate');
|
|
8741
|
+
const { value } = await res.json();
|
|
8742
|
+
const inp = document.querySelector('[data-key="' + key + '"]');
|
|
8743
|
+
if (inp) {
|
|
8744
|
+
inp.type = 'text';
|
|
8745
|
+
inp.value = value;
|
|
8746
|
+
handleChange(inp);
|
|
8747
|
+
}
|
|
8748
|
+
setFeedback(key, 'success', '\u2713 Secret generated \u2014 save to persist');
|
|
8749
|
+
} catch (e) {
|
|
8750
|
+
setFeedback(key, 'error', e.message);
|
|
8751
|
+
}
|
|
8752
|
+
}
|
|
8386
8753
|
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8754
|
+
/* \u2500\u2500 Modal \u2500\u2500 */
|
|
8755
|
+
function openModal(ok, msg) {
|
|
8756
|
+
const box = document.getElementById('testResultBox');
|
|
8757
|
+
box.className = 'modal-result ' + (ok ? 'ok' : 'fail');
|
|
8758
|
+
box.textContent = (ok ? '\u2713 ' : '\u2717 ') + msg;
|
|
8759
|
+
document.getElementById('testModal').classList.add('open');
|
|
8760
|
+
}
|
|
8761
|
+
function closeModal() {
|
|
8762
|
+
document.getElementById('testModal').classList.remove('open');
|
|
8763
|
+
}
|
|
8764
|
+
|
|
8765
|
+
/* \u2500\u2500 Toast \u2500\u2500 */
|
|
8766
|
+
function toast(type, msg) {
|
|
8767
|
+
const zone = document.getElementById('toastZone');
|
|
8768
|
+
const el = document.createElement('div');
|
|
8769
|
+
el.className = 'toast ' + type;
|
|
8770
|
+
el.textContent = msg;
|
|
8771
|
+
zone.appendChild(el);
|
|
8772
|
+
setTimeout(() => el.remove(), 4500);
|
|
8773
|
+
}
|
|
8774
|
+
|
|
8775
|
+
/* \u2500\u2500 Feedback \u2500\u2500 */
|
|
8776
|
+
function setFeedback(key, type, msg) {
|
|
8777
|
+
const el = document.getElementById('fb-' + key);
|
|
8778
|
+
if (!el) return;
|
|
8779
|
+
el.className = 'env-feedback' + (type ? ' ' + type : '');
|
|
8780
|
+
el.textContent = msg;
|
|
8781
|
+
}
|
|
8782
|
+
function clearFeedback(key) { setFeedback(key, '', ''); }
|
|
8783
|
+
|
|
8784
|
+
/* \u2500\u2500 Helpers \u2500\u2500 */
|
|
8785
|
+
function escHtml(s) {
|
|
8786
|
+
return String(s).replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]));
|
|
8787
|
+
}
|
|
8788
|
+
|
|
8789
|
+
/* \u2500\u2500 Wire save button \u2500\u2500 */
|
|
8790
|
+
document.getElementById('saveBtn').addEventListener('click', saveChanges);
|
|
8791
|
+
|
|
8792
|
+
/* \u2500\u2500 Close modal on backdrop click \u2500\u2500 */
|
|
8793
|
+
document.getElementById('testModal').addEventListener('click', function(e) {
|
|
8794
|
+
if (e.target === this) closeModal();
|
|
8795
|
+
});
|
|
8796
|
+
|
|
8797
|
+
/* \u2500\u2500 Boot \u2500\u2500 */
|
|
8798
|
+
init();
|
|
8799
|
+
</script>
|
|
8390
8800
|
</body>
|
|
8391
8801
|
</html>`;
|
|
8392
8802
|
}
|