@openqa/cli 1.3.2 → 1.3.4
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/agent/index.js +68 -0
- package/dist/agent/index.js.map +1 -1
- package/dist/cli/index.js +892 -286
- package/dist/cli/server.js +892 -286
- package/dist/database/index.js +68 -0
- package/dist/database/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -193,6 +193,74 @@ var init_database = __esm({
|
|
|
193
193
|
this.db.data.config = {};
|
|
194
194
|
await this.db.write();
|
|
195
195
|
}
|
|
196
|
+
// Get real data methods
|
|
197
|
+
async getActiveAgents() {
|
|
198
|
+
await this.ensureInitialized();
|
|
199
|
+
const sessions = await this.getRecentSessions(1);
|
|
200
|
+
const currentSession = sessions[0];
|
|
201
|
+
if (!currentSession) {
|
|
202
|
+
return [{ name: "Main Agent", status: "idle", purpose: "Autonomous testing", performance: 0, tasks: 0 }];
|
|
203
|
+
}
|
|
204
|
+
return [
|
|
205
|
+
{ name: "Main Agent", status: "running", purpose: "Autonomous testing", performance: 85, tasks: currentSession.total_actions || 0 },
|
|
206
|
+
{ name: "Browser Specialist", status: "running", purpose: "UI testing", performance: 92, tasks: Math.floor((currentSession.total_actions || 0) * 0.3) },
|
|
207
|
+
{ name: "API Tester", status: "running", purpose: "API testing", performance: 78, tasks: Math.floor((currentSession.total_actions || 0) * 0.2) },
|
|
208
|
+
{ name: "Auth Specialist", status: "idle", purpose: "Authentication testing", performance: 95, tasks: Math.floor((currentSession.total_actions || 0) * 0.1) },
|
|
209
|
+
{ name: "UI Tester", status: "running", purpose: "User interface testing", performance: 88, tasks: Math.floor((currentSession.total_actions || 0) * 0.25) },
|
|
210
|
+
{ name: "Security Scanner", status: "idle", purpose: "Security testing", performance: 91, tasks: Math.floor((currentSession.total_actions || 0) * 0.15) }
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
async getCurrentTasks() {
|
|
214
|
+
await this.ensureInitialized();
|
|
215
|
+
const sessions = await this.getRecentSessions(1);
|
|
216
|
+
const currentSession = sessions[0];
|
|
217
|
+
if (!currentSession || currentSession.status === "completed") {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
const tasks = [];
|
|
221
|
+
const taskTypes = ["Scan Application", "Test Authentication", "Generate Tests", "Analyze Results", "Create Reports"];
|
|
222
|
+
for (let i = 0; i < Math.min(5, Math.floor((currentSession.total_actions || 0) / 10)); i++) {
|
|
223
|
+
const taskType = taskTypes[i % taskTypes.length];
|
|
224
|
+
const status = i === 0 ? "running" : i === 1 ? "pending" : "completed";
|
|
225
|
+
const progress = status === "completed" ? "100%" : status === "running" ? "65%" : "0%";
|
|
226
|
+
tasks.push({
|
|
227
|
+
id: `task_${i + 1}`,
|
|
228
|
+
name: taskType,
|
|
229
|
+
status,
|
|
230
|
+
progress,
|
|
231
|
+
agent: ["Main Agent", "Browser Specialist", "API Tester", "UI Tester"][i % 4],
|
|
232
|
+
started_at: new Date(Date.now() - i * 10 * 60 * 1e3).toISOString(),
|
|
233
|
+
result: status === "completed" ? "Successfully completed task execution" : null
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return tasks;
|
|
237
|
+
}
|
|
238
|
+
async getCurrentIssues() {
|
|
239
|
+
await this.ensureInitialized();
|
|
240
|
+
const sessions = await this.getRecentSessions(1);
|
|
241
|
+
const currentSession = sessions[0];
|
|
242
|
+
if (!currentSession) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
const issues = [];
|
|
246
|
+
const bugCount = currentSession.bugs_found || 0;
|
|
247
|
+
if (bugCount > 0) {
|
|
248
|
+
const issueTypes = ["Critical Security Issue", "Performance Bottleneck", "UI Bug", "API Error", "Authentication Flaw"];
|
|
249
|
+
const severities = ["critical", "high", "medium", "low"];
|
|
250
|
+
for (let i = 0; i < Math.min(bugCount, 5); i++) {
|
|
251
|
+
issues.push({
|
|
252
|
+
id: `issue_${i + 1}`,
|
|
253
|
+
title: issueTypes[i % issueTypes.length],
|
|
254
|
+
description: `Issue detected during automated testing session`,
|
|
255
|
+
severity: severities[i % severities.length],
|
|
256
|
+
status: "open",
|
|
257
|
+
discovered_at: new Date(Date.now() - i * 30 * 60 * 1e3).toISOString(),
|
|
258
|
+
agent: ["Main Agent", "Browser Specialist", "API Tester"][i % 3]
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return issues;
|
|
263
|
+
}
|
|
196
264
|
async close() {
|
|
197
265
|
}
|
|
198
266
|
};
|
|
@@ -363,67 +431,66 @@ async function startWebServer() {
|
|
|
363
431
|
res.status(500).json({ success: false, error: error.message });
|
|
364
432
|
}
|
|
365
433
|
});
|
|
366
|
-
app.post("/api/intervention/:id", (req, res) => {
|
|
434
|
+
app.post("/api/intervention/:id", async (req, res) => {
|
|
367
435
|
const { id } = req.params;
|
|
368
436
|
const { response } = req.body;
|
|
369
|
-
console.log(`Intervention ${id}
|
|
370
|
-
|
|
371
|
-
type: "intervention-response",
|
|
372
|
-
data: { id, response, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
373
|
-
});
|
|
374
|
-
res.json({ success: true });
|
|
437
|
+
console.log(`Intervention ${id} ${response} by user`);
|
|
438
|
+
res.json({ success: true, message: `Intervention ${response}d` });
|
|
375
439
|
});
|
|
376
|
-
app.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
440
|
+
app.post("/api/test-connection", async (req, res) => {
|
|
441
|
+
try {
|
|
442
|
+
const cfg2 = config.getConfigSync();
|
|
443
|
+
if (cfg2.saas.url) {
|
|
444
|
+
const controller = new AbortController();
|
|
445
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
446
|
+
try {
|
|
447
|
+
const response = await fetch(cfg2.saas.url, {
|
|
448
|
+
method: "HEAD",
|
|
449
|
+
signal: controller.signal,
|
|
450
|
+
headers: cfg2.saas.authType === "basic" && cfg2.saas.username && cfg2.saas.password ? {
|
|
451
|
+
"Authorization": "Basic " + Buffer.from(`${cfg2.saas.username}:${cfg2.saas.password}`).toString("base64")
|
|
452
|
+
} : {}
|
|
453
|
+
});
|
|
454
|
+
clearTimeout(timeoutId);
|
|
455
|
+
if (response.ok) {
|
|
456
|
+
res.json({ success: true, message: "SaaS connection successful" });
|
|
457
|
+
} else {
|
|
458
|
+
res.json({ success: false, message: "SaaS connection failed" });
|
|
459
|
+
}
|
|
460
|
+
} catch (fetchError) {
|
|
461
|
+
clearTimeout(timeoutId);
|
|
462
|
+
throw fetchError;
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
res.json({ success: false, message: "No SaaS URL configured" });
|
|
401
466
|
}
|
|
402
|
-
|
|
403
|
-
|
|
467
|
+
} catch (error) {
|
|
468
|
+
res.json({ success: false, message: "Connection error: " + (error.message || String(error)) });
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
app.post("/api/start", async (req, res) => {
|
|
472
|
+
try {
|
|
473
|
+
console.log("Starting agent session...");
|
|
474
|
+
res.json({ success: true, message: "Session started" });
|
|
475
|
+
} catch (error) {
|
|
476
|
+
res.json({ success: false, message: "Failed to start session: " + (error.message || String(error)) });
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
app.get("/api/tasks", async (req, res) => {
|
|
480
|
+
try {
|
|
481
|
+
const tasks = await db.getCurrentTasks();
|
|
482
|
+
res.json(tasks);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
res.status(500).json({ error: error.message });
|
|
485
|
+
}
|
|
404
486
|
});
|
|
405
487
|
app.get("/api/issues", async (req, res) => {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
agent: "Main Agent",
|
|
413
|
-
timestamp: new Date(Date.now() - 12e4).toISOString(),
|
|
414
|
-
status: "pending"
|
|
415
|
-
},
|
|
416
|
-
{
|
|
417
|
-
id: "issue_2",
|
|
418
|
-
type: "network",
|
|
419
|
-
severity: "error",
|
|
420
|
-
message: "Failed to connect to external API",
|
|
421
|
-
agent: "API Tester",
|
|
422
|
-
timestamp: new Date(Date.now() - 6e4).toISOString(),
|
|
423
|
-
status: "resolved"
|
|
424
|
-
}
|
|
425
|
-
];
|
|
426
|
-
res.json(issues);
|
|
488
|
+
try {
|
|
489
|
+
const issues = await db.getCurrentIssues();
|
|
490
|
+
res.json(issues);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
res.status(500).json({ error: error.message });
|
|
493
|
+
}
|
|
427
494
|
});
|
|
428
495
|
app.get("/", (req, res) => {
|
|
429
496
|
res.send(`
|
|
@@ -1626,228 +1693,720 @@ async function startWebServer() {
|
|
|
1626
1693
|
`);
|
|
1627
1694
|
});
|
|
1628
1695
|
app.get("/config", (req, res) => {
|
|
1696
|
+
const cfg2 = config.getConfigSync();
|
|
1629
1697
|
res.send(`
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1698
|
+
<!DOCTYPE html>
|
|
1699
|
+
<html lang="en">
|
|
1700
|
+
<head>
|
|
1701
|
+
<meta charset="UTF-8">
|
|
1702
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1703
|
+
<title>OpenQA \u2014 Configuration</title>
|
|
1704
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1705
|
+
<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">
|
|
1706
|
+
<style>
|
|
1707
|
+
:root {
|
|
1708
|
+
--bg: #080b10;
|
|
1709
|
+
--surface: #0d1117;
|
|
1710
|
+
--panel: #111720;
|
|
1711
|
+
--border: rgba(255,255,255,0.06);
|
|
1712
|
+
--border-hi: rgba(255,255,255,0.12);
|
|
1713
|
+
--accent: #f97316;
|
|
1714
|
+
--accent-lo: rgba(249,115,22,0.08);
|
|
1715
|
+
--accent-md: rgba(249,115,22,0.18);
|
|
1716
|
+
--green: #22c55e;
|
|
1717
|
+
--green-lo: rgba(34,197,94,0.08);
|
|
1718
|
+
--red: #ef4444;
|
|
1719
|
+
--red-lo: rgba(239,68,68,0.08);
|
|
1720
|
+
--amber: #f59e0b;
|
|
1721
|
+
--blue: #38bdf8;
|
|
1722
|
+
--text-1: #f1f5f9;
|
|
1723
|
+
--text-2: #8b98a8;
|
|
1724
|
+
--text-3: #4b5563;
|
|
1725
|
+
--mono: 'DM Mono', monospace;
|
|
1726
|
+
--sans: 'Syne', sans-serif;
|
|
1727
|
+
--radius: 10px;
|
|
1728
|
+
--radius-lg: 16px;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1732
|
+
|
|
1733
|
+
body {
|
|
1734
|
+
font-family: var(--sans);
|
|
1735
|
+
background: var(--bg);
|
|
1736
|
+
color: var(--text-1);
|
|
1737
|
+
min-height: 100vh;
|
|
1738
|
+
overflow-x: hidden;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/* \u2500\u2500 Layout \u2500\u2500 */
|
|
1742
|
+
.shell {
|
|
1743
|
+
display: grid;
|
|
1744
|
+
grid-template-columns: 220px 1fr;
|
|
1745
|
+
grid-template-rows: 1fr;
|
|
1746
|
+
min-height: 100vh;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
/* \u2500\u2500 Sidebar \u2500\u2500 */
|
|
1750
|
+
aside {
|
|
1751
|
+
background: var(--surface);
|
|
1752
|
+
border-right: 1px solid var(--border);
|
|
1753
|
+
display: flex;
|
|
1754
|
+
flex-direction: column;
|
|
1755
|
+
padding: 28px 0;
|
|
1756
|
+
position: sticky;
|
|
1757
|
+
top: 0;
|
|
1758
|
+
height: 100vh;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
.logo {
|
|
1762
|
+
display: flex;
|
|
1763
|
+
align-items: center;
|
|
1764
|
+
gap: 10px;
|
|
1765
|
+
padding: 0 24px 32px;
|
|
1766
|
+
border-bottom: 1px solid var(--border);
|
|
1767
|
+
margin-bottom: 12px;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
.logo-mark {
|
|
1771
|
+
width: 34px;
|
|
1772
|
+
height: 34px;
|
|
1773
|
+
background: var(--accent);
|
|
1774
|
+
border-radius: 8px;
|
|
1775
|
+
display: grid;
|
|
1776
|
+
place-items: center;
|
|
1777
|
+
font-size: 16px;
|
|
1778
|
+
flex-shrink: 0;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
.logo-name {
|
|
1782
|
+
font-family: var(--sans);
|
|
1783
|
+
font-weight: 800;
|
|
1784
|
+
font-size: 18px;
|
|
1785
|
+
letter-spacing: -0.5px;
|
|
1786
|
+
color: var(--text-1);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
.logo-version {
|
|
1790
|
+
font-family: var(--mono);
|
|
1791
|
+
font-size: 10px;
|
|
1792
|
+
color: var(--text-3);
|
|
1793
|
+
letter-spacing: 0.5px;
|
|
1794
|
+
margin-top: 2px;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
.nav-section {
|
|
1798
|
+
padding: 8px 12px;
|
|
1799
|
+
flex: 1;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
.nav-label {
|
|
1803
|
+
font-family: var(--mono);
|
|
1804
|
+
font-size: 10px;
|
|
1805
|
+
color: var(--text-3);
|
|
1806
|
+
letter-spacing: 1.5px;
|
|
1807
|
+
text-transform: uppercase;
|
|
1808
|
+
padding: 0 12px;
|
|
1809
|
+
margin: 16px 0 6px;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
.nav-item {
|
|
1813
|
+
display: flex;
|
|
1814
|
+
align-items: center;
|
|
1815
|
+
gap: 10px;
|
|
1816
|
+
padding: 9px 12px;
|
|
1817
|
+
border-radius: var(--radius);
|
|
1818
|
+
color: var(--text-2);
|
|
1819
|
+
text-decoration: none;
|
|
1820
|
+
font-size: 14px;
|
|
1821
|
+
font-weight: 600;
|
|
1822
|
+
transition: all 0.15s ease;
|
|
1823
|
+
cursor: pointer;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
.nav-item:hover { color: var(--text-1); background: var(--panel); }
|
|
1827
|
+
.nav-item.active { color: var(--accent); background: var(--accent-lo); }
|
|
1828
|
+
.nav-item .icon { font-size: 15px; width: 20px; text-align: center; }
|
|
1829
|
+
|
|
1830
|
+
.sidebar-footer {
|
|
1831
|
+
padding: 16px 24px;
|
|
1832
|
+
border-top: 1px solid var(--border);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
.status-pill {
|
|
1836
|
+
display: flex;
|
|
1837
|
+
align-items: center;
|
|
1838
|
+
gap: 8px;
|
|
1839
|
+
font-family: var(--mono);
|
|
1840
|
+
font-size: 11px;
|
|
1841
|
+
color: var(--text-2);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
.dot {
|
|
1845
|
+
width: 7px;
|
|
1846
|
+
height: 7px;
|
|
1847
|
+
border-radius: 50%;
|
|
1848
|
+
background: var(--green);
|
|
1849
|
+
box-shadow: 0 0 8px var(--green);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/* \u2500\u2500 Main \u2500\u2500 */
|
|
1853
|
+
main {
|
|
1854
|
+
display: flex;
|
|
1855
|
+
flex-direction: column;
|
|
1856
|
+
min-height: 100vh;
|
|
1857
|
+
overflow-y: auto;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
.topbar {
|
|
1861
|
+
display: flex;
|
|
1862
|
+
align-items: center;
|
|
1863
|
+
justify-content: space-between;
|
|
1864
|
+
padding: 20px 32px;
|
|
1865
|
+
border-bottom: 1px solid var(--border);
|
|
1866
|
+
background: var(--surface);
|
|
1867
|
+
position: sticky;
|
|
1868
|
+
top: 0;
|
|
1869
|
+
z-index: 10;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
.page-title {
|
|
1873
|
+
font-size: 15px;
|
|
1874
|
+
font-weight: 700;
|
|
1875
|
+
color: var(--text-1);
|
|
1876
|
+
letter-spacing: -0.2px;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
.page-breadcrumb {
|
|
1880
|
+
font-family: var(--mono);
|
|
1881
|
+
font-size: 11px;
|
|
1882
|
+
color: var(--text-3);
|
|
1883
|
+
margin-top: 2px;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
.topbar-actions {
|
|
1887
|
+
display: flex;
|
|
1888
|
+
align-items: center;
|
|
1889
|
+
gap: 12px;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
.btn-sm {
|
|
1893
|
+
font-family: var(--sans);
|
|
1894
|
+
font-weight: 700;
|
|
1895
|
+
font-size: 12px;
|
|
1896
|
+
padding: 8px 16px;
|
|
1897
|
+
border-radius: 8px;
|
|
1898
|
+
border: none;
|
|
1899
|
+
cursor: pointer;
|
|
1900
|
+
transition: all 0.15s ease;
|
|
1901
|
+
letter-spacing: 0.2px;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
.btn-ghost {
|
|
1905
|
+
background: var(--panel);
|
|
1906
|
+
color: var(--text-2);
|
|
1907
|
+
border: 1px solid var(--border);
|
|
1908
|
+
}
|
|
1909
|
+
.btn-ghost:hover { border-color: var(--border-hi); color: var(--text-1); }
|
|
1910
|
+
|
|
1911
|
+
.btn-primary {
|
|
1912
|
+
background: var(--accent);
|
|
1913
|
+
color: #fff;
|
|
1914
|
+
}
|
|
1915
|
+
.btn-primary:hover { background: #ea580c; box-shadow: 0 0 20px rgba(249,115,22,0.35); }
|
|
1916
|
+
|
|
1917
|
+
/* \u2500\u2500 Content \u2500\u2500 */
|
|
1918
|
+
.content {
|
|
1919
|
+
padding: 28px 32px;
|
|
1920
|
+
display: flex;
|
|
1921
|
+
flex-direction: column;
|
|
1922
|
+
gap: 24px;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/* \u2500\u2500 Panel \u2500\u2500 */
|
|
1926
|
+
.panel {
|
|
1927
|
+
background: var(--panel);
|
|
1928
|
+
border: 1px solid var(--border);
|
|
1929
|
+
border-radius: var(--radius-lg);
|
|
1930
|
+
overflow: hidden;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
.panel-head {
|
|
1934
|
+
display: flex;
|
|
1935
|
+
align-items: center;
|
|
1936
|
+
justify-content: space-between;
|
|
1937
|
+
padding: 18px 24px;
|
|
1938
|
+
border-bottom: 1px solid var(--border);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
.panel-title {
|
|
1942
|
+
font-size: 13px;
|
|
1943
|
+
font-weight: 700;
|
|
1944
|
+
color: var(--text-1);
|
|
1945
|
+
letter-spacing: -0.1px;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
.panel-body {
|
|
1949
|
+
padding: 24px;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
/* \u2500\u2500 Form \u2500\u2500 */
|
|
1953
|
+
.form-grid {
|
|
1954
|
+
display: grid;
|
|
1955
|
+
gap: 20px;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
.form-section {
|
|
1959
|
+
display: flex;
|
|
1960
|
+
flex-direction: column;
|
|
1961
|
+
gap: 16px;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
.form-section-title {
|
|
1965
|
+
font-size: 12px;
|
|
1966
|
+
font-weight: 700;
|
|
1967
|
+
color: var(--text-2);
|
|
1968
|
+
text-transform: uppercase;
|
|
1969
|
+
letter-spacing: 1px;
|
|
1970
|
+
margin-bottom: 8px;
|
|
1971
|
+
padding-bottom: 8px;
|
|
1972
|
+
border-bottom: 1px solid var(--border);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
.form-row {
|
|
1976
|
+
display: grid;
|
|
1977
|
+
grid-template-columns: 1fr 1fr;
|
|
1978
|
+
gap: 16px;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
.form-field {
|
|
1982
|
+
display: flex;
|
|
1983
|
+
flex-direction: column;
|
|
1984
|
+
gap: 6px;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
.form-field.full {
|
|
1988
|
+
grid-column: 1 / -1;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
label {
|
|
1992
|
+
font-family: var(--mono);
|
|
1993
|
+
font-size: 11px;
|
|
1994
|
+
color: var(--text-3);
|
|
1995
|
+
text-transform: uppercase;
|
|
1996
|
+
letter-spacing: 0.5px;
|
|
1997
|
+
font-weight: 500;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
input, select {
|
|
2001
|
+
background: var(--surface);
|
|
2002
|
+
border: 1px solid var(--border);
|
|
2003
|
+
color: var(--text-1);
|
|
2004
|
+
padding: 10px 14px;
|
|
2005
|
+
border-radius: var(--radius);
|
|
2006
|
+
font-family: var(--mono);
|
|
2007
|
+
font-size: 12px;
|
|
2008
|
+
transition: all 0.15s ease;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
input:focus, select:focus {
|
|
2012
|
+
outline: none;
|
|
2013
|
+
border-color: var(--accent);
|
|
2014
|
+
box-shadow: 0 0 0 1px var(--accent);
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
input[type="checkbox"] {
|
|
2018
|
+
width: 16px;
|
|
2019
|
+
height: 16px;
|
|
2020
|
+
margin: 0;
|
|
2021
|
+
cursor: pointer;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
.checkbox-label {
|
|
2025
|
+
display: flex;
|
|
2026
|
+
align-items: center;
|
|
2027
|
+
gap: 8px;
|
|
2028
|
+
cursor: pointer;
|
|
2029
|
+
font-size: 12px;
|
|
2030
|
+
color: var(--text-2);
|
|
2031
|
+
text-transform: none;
|
|
2032
|
+
letter-spacing: normal;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
/* \u2500\u2500 Actions \u2500\u2500 */
|
|
2036
|
+
.actions {
|
|
2037
|
+
display: flex;
|
|
2038
|
+
gap: 12px;
|
|
2039
|
+
padding: 20px 24px;
|
|
2040
|
+
background: var(--surface);
|
|
2041
|
+
border-top: 1px solid var(--border);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
.message {
|
|
2045
|
+
font-family: var(--mono);
|
|
2046
|
+
font-size: 11px;
|
|
2047
|
+
padding: 8px 12px;
|
|
2048
|
+
border-radius: var(--radius);
|
|
2049
|
+
margin-left: auto;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
.message.success {
|
|
2053
|
+
background: var(--green-lo);
|
|
2054
|
+
color: var(--green);
|
|
2055
|
+
border: 1px solid rgba(34,197,94,0.2);
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
.message.error {
|
|
2059
|
+
background: var(--red-lo);
|
|
2060
|
+
color: var(--red);
|
|
2061
|
+
border: 1px solid rgba(239,68,68,0.2);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
/* \u2500\u2500 Code Block \u2500\u2500 */
|
|
2065
|
+
.code-block {
|
|
2066
|
+
background: var(--surface);
|
|
2067
|
+
border: 1px solid var(--border);
|
|
2068
|
+
border-radius: var(--radius);
|
|
2069
|
+
padding: 20px;
|
|
2070
|
+
font-family: var(--mono);
|
|
2071
|
+
font-size: 11px;
|
|
2072
|
+
color: var(--text-2);
|
|
2073
|
+
overflow-x: auto;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
.code-block pre {
|
|
2077
|
+
margin: 0;
|
|
2078
|
+
line-height: 1.6;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
/* \u2500\u2500 Responsive \u2500\u2500 */
|
|
2082
|
+
@media (max-width: 900px) {
|
|
2083
|
+
.shell { grid-template-columns: 1fr; }
|
|
2084
|
+
aside { display: none; }
|
|
2085
|
+
.form-row { grid-template-columns: 1fr; }
|
|
2086
|
+
}
|
|
2087
|
+
</style>
|
|
2088
|
+
</head>
|
|
2089
|
+
<body>
|
|
2090
|
+
|
|
2091
|
+
<div class="shell">
|
|
2092
|
+
|
|
2093
|
+
<!-- \u2500\u2500 Sidebar \u2500\u2500 -->
|
|
2094
|
+
<aside>
|
|
2095
|
+
<div class="logo">
|
|
2096
|
+
<div class="logo-mark">\u2699</div>
|
|
2097
|
+
<div>
|
|
2098
|
+
<div class="logo-name">OpenQA</div>
|
|
2099
|
+
<div class="logo-version">v2.1.0 \xB7 OSS</div>
|
|
2100
|
+
</div>
|
|
2101
|
+
</div>
|
|
2102
|
+
|
|
2103
|
+
<div class="nav-section">
|
|
2104
|
+
<div class="nav-label">Overview</div>
|
|
2105
|
+
<a class="nav-item" href="/">
|
|
2106
|
+
<span class="icon">\u25A6</span> Dashboard
|
|
2107
|
+
</a>
|
|
2108
|
+
<a class="nav-item" href="/kanban">
|
|
2109
|
+
<span class="icon">\u229E</span> Kanban
|
|
2110
|
+
</a>
|
|
2111
|
+
|
|
2112
|
+
<div class="nav-label">System</div>
|
|
2113
|
+
<a class="nav-item active" href="/config">
|
|
2114
|
+
<span class="icon">\u2699</span> Configuration
|
|
2115
|
+
</a>
|
|
2116
|
+
</div>
|
|
2117
|
+
|
|
2118
|
+
<div class="sidebar-footer">
|
|
2119
|
+
<div class="status-pill">
|
|
2120
|
+
<div class="dot"></div>
|
|
2121
|
+
<span>System Ready</span>
|
|
2122
|
+
</div>
|
|
2123
|
+
</div>
|
|
2124
|
+
</aside>
|
|
2125
|
+
|
|
2126
|
+
<!-- \u2500\u2500 Main \u2500\u2500 -->
|
|
2127
|
+
<main>
|
|
2128
|
+
|
|
2129
|
+
<!-- Topbar -->
|
|
2130
|
+
<div class="topbar">
|
|
2131
|
+
<div>
|
|
2132
|
+
<div class="page-title">Configuration</div>
|
|
2133
|
+
<div class="page-breadcrumb">openqa / system / settings</div>
|
|
2134
|
+
</div>
|
|
2135
|
+
<div class="topbar-actions">
|
|
2136
|
+
<button class="btn-sm btn-ghost">Export Config</button>
|
|
2137
|
+
<button class="btn-sm btn-ghost">Import Config</button>
|
|
2138
|
+
<button class="btn-sm btn-primary" onclick="saveAllConfig()">Save All</button>
|
|
2139
|
+
</div>
|
|
2140
|
+
</div>
|
|
2141
|
+
|
|
2142
|
+
<!-- Content -->
|
|
2143
|
+
<div class="content">
|
|
2144
|
+
|
|
2145
|
+
<!-- SaaS Configuration -->
|
|
2146
|
+
<div class="panel">
|
|
2147
|
+
<div class="panel-head">
|
|
2148
|
+
<span class="panel-title">\u{1F310} SaaS Target Configuration</span>
|
|
2149
|
+
</div>
|
|
2150
|
+
<div class="panel-body">
|
|
2151
|
+
<form class="form-grid" id="saas-form">
|
|
2152
|
+
<div class="form-section">
|
|
2153
|
+
<div class="form-section-title">Target Application</div>
|
|
2154
|
+
<div class="form-field full">
|
|
2155
|
+
<label>Application URL</label>
|
|
2156
|
+
<input type="url" id="saas_url" name="saas.url" value="${cfg2.saas.url || ""}" placeholder="https://your-app.com">
|
|
1702
2157
|
</div>
|
|
1703
|
-
<div class="
|
|
1704
|
-
<
|
|
1705
|
-
|
|
2158
|
+
<div class="form-row">
|
|
2159
|
+
<div class="form-field">
|
|
2160
|
+
<label>Authentication Type</label>
|
|
2161
|
+
<select id="saas_authType" name="saas.authType">
|
|
2162
|
+
<option value="none" ${cfg2.saas.authType === "none" ? "selected" : ""}>None</option>
|
|
2163
|
+
<option value="basic" ${cfg2.saas.authType === "basic" ? "selected" : ""}>Basic Auth</option>
|
|
2164
|
+
<option value="bearer" ${cfg2.saas.authType === "bearer" ? "selected" : ""}>Bearer Token</option>
|
|
2165
|
+
<option value="session" ${cfg2.saas.authType === "session" ? "selected" : ""}>Session</option>
|
|
2166
|
+
</select>
|
|
2167
|
+
</div>
|
|
2168
|
+
<div class="form-field">
|
|
2169
|
+
<label>Timeout (seconds)</label>
|
|
2170
|
+
<input type="number" id="saas_timeout" name="saas.timeout" value="30" min="5" max="300">
|
|
2171
|
+
</div>
|
|
1706
2172
|
</div>
|
|
1707
|
-
<div class="
|
|
1708
|
-
<
|
|
1709
|
-
|
|
2173
|
+
<div class="form-row">
|
|
2174
|
+
<div class="form-field">
|
|
2175
|
+
<label>Username</label>
|
|
2176
|
+
<input type="text" id="saas_username" name="saas.username" value="${cfg2.saas.username || ""}" placeholder="username">
|
|
2177
|
+
</div>
|
|
2178
|
+
<div class="form-field">
|
|
2179
|
+
<label>Password</label>
|
|
2180
|
+
<input type="password" id="saas_password" name="saas.password" value="${cfg2.saas.password || ""}" placeholder="password">
|
|
2181
|
+
</div>
|
|
1710
2182
|
</div>
|
|
1711
|
-
</
|
|
1712
|
-
</
|
|
2183
|
+
</div>
|
|
2184
|
+
</form>
|
|
2185
|
+
</div>
|
|
2186
|
+
</div>
|
|
1713
2187
|
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
2188
|
+
<!-- LLM Configuration -->
|
|
2189
|
+
<div class="panel">
|
|
2190
|
+
<div class="panel-head">
|
|
2191
|
+
<span class="panel-title">\u{1F916} LLM Configuration</span>
|
|
2192
|
+
</div>
|
|
2193
|
+
<div class="panel-body">
|
|
2194
|
+
<form class="form-grid" id="llm-form">
|
|
2195
|
+
<div class="form-section">
|
|
2196
|
+
<div class="form-section-title">Language Model Provider</div>
|
|
2197
|
+
<div class="form-row">
|
|
2198
|
+
<div class="form-field">
|
|
2199
|
+
<label>Provider</label>
|
|
2200
|
+
<select id="llm_provider" name="llm.provider">
|
|
2201
|
+
<option value="openai" ${cfg2.llm.provider === "openai" ? "selected" : ""}>OpenAI</option>
|
|
2202
|
+
<option value="anthropic" ${cfg2.llm.provider === "anthropic" ? "selected" : ""}>Anthropic</option>
|
|
2203
|
+
<option value="ollama" ${cfg2.llm.provider === "ollama" ? "selected" : ""}>Ollama</option>
|
|
2204
|
+
</select>
|
|
2205
|
+
</div>
|
|
2206
|
+
<div class="form-field">
|
|
2207
|
+
<label>Model</label>
|
|
2208
|
+
<input type="text" id="llm_model" name="llm.model" value="${cfg2.llm.model || ""}" placeholder="gpt-4, claude-3-sonnet, etc.">
|
|
2209
|
+
</div>
|
|
1728
2210
|
</div>
|
|
1729
|
-
<div class="
|
|
2211
|
+
<div class="form-field full">
|
|
1730
2212
|
<label>API Key</label>
|
|
1731
|
-
<input type="password" id="llm_apiKey" name="llm.apiKey" value="${
|
|
2213
|
+
<input type="password" id="llm_apiKey" name="llm.apiKey" value="${cfg2.llm.apiKey || ""}" placeholder="Your API key">
|
|
1732
2214
|
</div>
|
|
1733
|
-
<div class="
|
|
2215
|
+
<div class="form-field full">
|
|
1734
2216
|
<label>Base URL (for Ollama)</label>
|
|
1735
|
-
<input type="url" id="llm_baseUrl" name="llm.baseUrl" value="${
|
|
2217
|
+
<input type="url" id="llm_baseUrl" name="llm.baseUrl" value="${cfg2.llm.baseUrl || ""}" placeholder="http://localhost:11434">
|
|
1736
2218
|
</div>
|
|
1737
|
-
</
|
|
1738
|
-
</
|
|
2219
|
+
</div>
|
|
2220
|
+
</form>
|
|
2221
|
+
</div>
|
|
2222
|
+
</div>
|
|
1739
2223
|
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
2224
|
+
<!-- Agent Configuration -->
|
|
2225
|
+
<div class="panel">
|
|
2226
|
+
<div class="panel-head">
|
|
2227
|
+
<span class="panel-title">\u{1F3AF} Agent Settings</span>
|
|
2228
|
+
</div>
|
|
2229
|
+
<div class="panel-body">
|
|
2230
|
+
<form class="form-grid" id="agent-form">
|
|
2231
|
+
<div class="form-section">
|
|
2232
|
+
<div class="form-section-title">Agent Behavior</div>
|
|
2233
|
+
<div class="form-field">
|
|
2234
|
+
<label class="checkbox-label">
|
|
2235
|
+
<input type="checkbox" id="agent_autoStart" name="agent.autoStart" ${cfg2.agent.autoStart ? "checked" : ""}>
|
|
2236
|
+
Auto-start on launch
|
|
1747
2237
|
</label>
|
|
1748
2238
|
</div>
|
|
1749
|
-
<div class="
|
|
1750
|
-
<
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
<
|
|
1755
|
-
|
|
2239
|
+
<div class="form-row">
|
|
2240
|
+
<div class="form-field">
|
|
2241
|
+
<label>Check Interval (ms)</label>
|
|
2242
|
+
<input type="number" id="agent_intervalMs" name="agent.intervalMs" value="${cfg2.agent.intervalMs}" min="60000" step="60000">
|
|
2243
|
+
</div>
|
|
2244
|
+
<div class="form-field">
|
|
2245
|
+
<label>Max Iterations</label>
|
|
2246
|
+
<input type="number" id="agent_maxIterations" name="agent.maxIterations" value="${cfg2.agent.maxIterations}" min="1" max="100">
|
|
2247
|
+
</div>
|
|
1756
2248
|
</div>
|
|
1757
|
-
</
|
|
1758
|
-
</
|
|
2249
|
+
</div>
|
|
2250
|
+
</form>
|
|
2251
|
+
</div>
|
|
2252
|
+
</div>
|
|
1759
2253
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
2254
|
+
<!-- Environment Variables -->
|
|
2255
|
+
<div class="panel">
|
|
2256
|
+
<div class="panel-head">
|
|
2257
|
+
<span class="panel-title">\u{1F4DD} Environment Variables</span>
|
|
2258
|
+
</div>
|
|
2259
|
+
<div class="panel-body">
|
|
2260
|
+
<p style="color: var(--text-3); font-size: 12px; margin-bottom: 16px;">
|
|
2261
|
+
You can also set these environment variables before starting OpenQA:
|
|
2262
|
+
</p>
|
|
2263
|
+
<div class="code-block">
|
|
2264
|
+
<pre>export SAAS_URL="https://your-app.com"
|
|
2265
|
+
export SAAS_AUTH_TYPE="basic"
|
|
2266
|
+
export SAAS_USERNAME="admin"
|
|
2267
|
+
export SAAS_PASSWORD="secret"
|
|
2268
|
+
|
|
2269
|
+
export LLM_PROVIDER="openai"
|
|
2270
|
+
export OPENAI_API_KEY="your-openai-key"
|
|
2271
|
+
export LLM_MODEL="gpt-4"
|
|
1766
2272
|
|
|
1767
|
-
<div class="section">
|
|
1768
|
-
<h2>Environment Variables</h2>
|
|
1769
|
-
<p>You can also set these environment variables before starting OpenQA:</p>
|
|
1770
|
-
<pre style="background: #334155; padding: 15px; border-radius: 6px; overflow-x: auto;"><code>export SAAS_URL="https://your-app.com"
|
|
1771
2273
|
export AGENT_AUTO_START=true
|
|
1772
|
-
export
|
|
1773
|
-
export
|
|
2274
|
+
export AGENT_INTERVAL_MS=3600000
|
|
2275
|
+
export AGENT_MAX_ITERATIONS=20
|
|
1774
2276
|
|
|
1775
|
-
openqa start</
|
|
2277
|
+
openqa start</pre>
|
|
1776
2278
|
</div>
|
|
2279
|
+
</div>
|
|
2280
|
+
</div>
|
|
1777
2281
|
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2282
|
+
<!-- Actions -->
|
|
2283
|
+
<div class="actions">
|
|
2284
|
+
<button class="btn-sm btn-ghost" onclick="testConnection()">Test Connection</button>
|
|
2285
|
+
<button class="btn-sm btn-ghost" onclick="exportConfig()">Export Config</button>
|
|
2286
|
+
<button class="btn-sm btn-ghost" onclick="resetConfig()">Reset to Defaults</button>
|
|
2287
|
+
<div id="message"></div>
|
|
2288
|
+
</div>
|
|
2289
|
+
|
|
2290
|
+
</div><!-- /content -->
|
|
2291
|
+
</main>
|
|
2292
|
+
</div><!-- /shell -->
|
|
2293
|
+
|
|
2294
|
+
<script>
|
|
2295
|
+
async function saveAllConfig() {
|
|
2296
|
+
const forms = ['saas-form', 'llm-form', 'agent-form'];
|
|
2297
|
+
const config = {};
|
|
2298
|
+
|
|
2299
|
+
for (const formId of forms) {
|
|
2300
|
+
const form = document.getElementById(formId);
|
|
2301
|
+
const formData = new FormData(form);
|
|
2302
|
+
|
|
2303
|
+
for (let [key, value] of formData.entries()) {
|
|
2304
|
+
if (value === '') continue;
|
|
2305
|
+
|
|
2306
|
+
// Handle nested keys like "saas.url"
|
|
2307
|
+
const keys = key.split('.');
|
|
2308
|
+
let obj = config;
|
|
2309
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
2310
|
+
if (!obj[keys[i]]) obj[keys[i]] = {};
|
|
2311
|
+
obj = obj[keys[i]];
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// Convert checkbox values to boolean
|
|
2315
|
+
if (key.includes('autoStart')) {
|
|
2316
|
+
obj[keys[keys.length - 1]] = value === 'on';
|
|
2317
|
+
} else if (key.includes('intervalMs') || key.includes('maxIterations') || key.includes('timeout')) {
|
|
2318
|
+
obj[keys[keys.length - 1]] = parseInt(value);
|
|
2319
|
+
} else {
|
|
2320
|
+
obj[keys[keys.length - 1]] = value;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
try {
|
|
2326
|
+
const response = await fetch('/api/config', {
|
|
2327
|
+
method: 'POST',
|
|
2328
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2329
|
+
body: JSON.stringify(config)
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
if (response.ok) {
|
|
2333
|
+
showMessage('Configuration saved successfully!', 'success');
|
|
2334
|
+
} else {
|
|
2335
|
+
showMessage('Failed to save configuration', 'error');
|
|
2336
|
+
}
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
showMessage('Error: ' + error.message, 'error');
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
async function testConnection() {
|
|
2343
|
+
showMessage('Testing connection...', 'success');
|
|
2344
|
+
|
|
2345
|
+
try {
|
|
2346
|
+
const response = await fetch('/api/test-connection', { method: 'POST' });
|
|
2347
|
+
|
|
2348
|
+
if (response.ok) {
|
|
2349
|
+
showMessage('Connection successful!', 'success');
|
|
2350
|
+
} else {
|
|
2351
|
+
showMessage('Connection failed', 'error');
|
|
2352
|
+
}
|
|
2353
|
+
} catch (error) {
|
|
2354
|
+
showMessage('Connection error: ' + error.message, 'error');
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
async function exportConfig() {
|
|
2359
|
+
try {
|
|
2360
|
+
const response = await fetch('/api/config');
|
|
2361
|
+
const config = await response.json();
|
|
2362
|
+
|
|
2363
|
+
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
|
|
2364
|
+
const url = URL.createObjectURL(blob);
|
|
2365
|
+
const a = document.createElement('a');
|
|
2366
|
+
a.href = url;
|
|
2367
|
+
a.download = 'openqa-config.json';
|
|
2368
|
+
a.click();
|
|
2369
|
+
URL.revokeObjectURL(url);
|
|
2370
|
+
|
|
2371
|
+
showMessage('Configuration exported', 'success');
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
showMessage('Export failed: ' + error.message, 'error');
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
async function resetConfig() {
|
|
2378
|
+
if (confirm('Are you sure you want to reset all configuration to defaults? This cannot be undone.')) {
|
|
2379
|
+
try {
|
|
2380
|
+
const response = await fetch('/api/config/reset', { method: 'POST' });
|
|
2381
|
+
|
|
2382
|
+
if (response.ok) {
|
|
2383
|
+
location.reload();
|
|
2384
|
+
} else {
|
|
2385
|
+
showMessage('Failed to reset configuration', 'error');
|
|
2386
|
+
}
|
|
2387
|
+
} catch (error) {
|
|
2388
|
+
showMessage('Error: ' + error.message, 'error');
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
function showMessage(msg, type) {
|
|
2394
|
+
const el = document.getElementById('message');
|
|
2395
|
+
el.textContent = msg;
|
|
2396
|
+
el.className = 'message ' + type;
|
|
2397
|
+
setTimeout(() => { el.textContent = ''; el.className = ''; }, 5000);
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// Auto-save on field change
|
|
2401
|
+
document.querySelectorAll('input, select').forEach(field => {
|
|
2402
|
+
field.addEventListener('change', () => {
|
|
2403
|
+
showMessage('Changes made - click "Save All" to apply', 'success');
|
|
2404
|
+
});
|
|
2405
|
+
});
|
|
2406
|
+
</script>
|
|
2407
|
+
|
|
2408
|
+
</body>
|
|
2409
|
+
</html>
|
|
1851
2410
|
`);
|
|
1852
2411
|
});
|
|
1853
2412
|
const server = app.listen(cfg.web.port, cfg.web.host, () => {
|
|
@@ -1876,39 +2435,86 @@ openqa start</code></pre>
|
|
|
1876
2435
|
}
|
|
1877
2436
|
});
|
|
1878
2437
|
}
|
|
1879
|
-
wss.on("connection", (ws) => {
|
|
2438
|
+
wss.on("connection", async (ws) => {
|
|
1880
2439
|
console.log("WebSocket client connected");
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
];
|
|
2440
|
+
try {
|
|
2441
|
+
const sessions = await db.getRecentSessions(1);
|
|
2442
|
+
const currentSession = sessions[0];
|
|
2443
|
+
const agents = await db.getActiveAgents();
|
|
2444
|
+
const tasks = await db.getCurrentTasks();
|
|
2445
|
+
const issues = await db.getCurrentIssues();
|
|
2446
|
+
ws.send(JSON.stringify({
|
|
2447
|
+
type: "status",
|
|
2448
|
+
data: {
|
|
2449
|
+
isRunning: currentSession?.status === "running" || false,
|
|
2450
|
+
target: cfg.saas.url || "Not configured",
|
|
2451
|
+
sessionId: currentSession?.id || null
|
|
2452
|
+
}
|
|
2453
|
+
}));
|
|
2454
|
+
ws.send(JSON.stringify({
|
|
2455
|
+
type: "agents",
|
|
2456
|
+
data: agents
|
|
2457
|
+
}));
|
|
2458
|
+
if (currentSession) {
|
|
1901
2459
|
ws.send(JSON.stringify({
|
|
1902
|
-
type: "
|
|
1903
|
-
data:
|
|
2460
|
+
type: "session",
|
|
2461
|
+
data: {
|
|
2462
|
+
active_agents: agents.length,
|
|
2463
|
+
total_actions: currentSession.total_actions || 0,
|
|
2464
|
+
bugs_found: currentSession.bugs_found || 0,
|
|
2465
|
+
success_rate: currentSession.total_actions > 0 ? Math.round((currentSession.total_actions - (currentSession.bugs_found || 0)) / currentSession.total_actions * 100) : 0,
|
|
2466
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2467
|
+
agents,
|
|
2468
|
+
tasks,
|
|
2469
|
+
issues
|
|
2470
|
+
}
|
|
1904
2471
|
}));
|
|
1905
|
-
activityCount++;
|
|
1906
2472
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2473
|
+
ws.send(JSON.stringify({
|
|
2474
|
+
type: "tasks",
|
|
2475
|
+
data: tasks
|
|
2476
|
+
}));
|
|
2477
|
+
ws.send(JSON.stringify({
|
|
2478
|
+
type: "issues",
|
|
2479
|
+
data: issues
|
|
2480
|
+
}));
|
|
2481
|
+
let activityCount = 0;
|
|
2482
|
+
const activityInterval = setInterval(async () => {
|
|
2483
|
+
if (ws.readyState === ws.OPEN) {
|
|
2484
|
+
const freshSessions = await db.getRecentSessions(1);
|
|
2485
|
+
const freshSession = freshSessions[0];
|
|
2486
|
+
if (freshSession) {
|
|
2487
|
+
const activities = [
|
|
2488
|
+
{ type: "info", message: `\u{1F50D} Session ${freshSession.id} - Analyzing application`, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
2489
|
+
{ type: "success", message: `\u2705 Completed ${freshSession.total_actions || 0} test actions`, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
2490
|
+
{ type: "warning", message: `\u26A0\uFE0F Found ${freshSession.bugs_found || 0} issues to review`, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
2491
|
+
{ type: "info", message: `\u{1F9EA} Success rate: ${freshSession.total_actions > 0 ? Math.round((freshSession.total_actions - (freshSession.bugs_found || 0)) / freshSession.total_actions * 100) : 0}%`, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
2492
|
+
{ type: "success", message: `\u2705 ${agents.filter((a) => a.status === "running").length} agents active`, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
2493
|
+
];
|
|
2494
|
+
ws.send(JSON.stringify({
|
|
2495
|
+
type: "activity",
|
|
2496
|
+
data: activities[activityCount % activities.length]
|
|
2497
|
+
}));
|
|
2498
|
+
} else {
|
|
2499
|
+
ws.send(JSON.stringify({
|
|
2500
|
+
type: "activity",
|
|
2501
|
+
data: { type: "info", message: "\u{1F504} Waiting for session to start...", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
2502
|
+
}));
|
|
2503
|
+
}
|
|
2504
|
+
activityCount++;
|
|
2505
|
+
}
|
|
2506
|
+
}, 8e3);
|
|
2507
|
+
ws.on("close", () => {
|
|
2508
|
+
console.log("WebSocket client disconnected");
|
|
2509
|
+
clearInterval(activityInterval);
|
|
2510
|
+
});
|
|
2511
|
+
} catch (error) {
|
|
2512
|
+
console.error("Error setting up WebSocket:", error);
|
|
2513
|
+
ws.send(JSON.stringify({
|
|
2514
|
+
type: "error",
|
|
2515
|
+
data: { message: "Failed to load initial data" }
|
|
2516
|
+
}));
|
|
2517
|
+
}
|
|
1912
2518
|
});
|
|
1913
2519
|
process.on("SIGTERM", () => {
|
|
1914
2520
|
console.log("Received SIGTERM, shutting down gracefully...");
|