@openqa/cli 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -5
- package/dist/agent/index-v2.js +33 -55
- package/dist/agent/index-v2.js.map +1 -1
- package/dist/agent/index.js +85 -116
- package/dist/agent/index.js.map +1 -1
- package/dist/cli/daemon.js +1530 -277
- package/dist/cli/dashboard.html.js +55 -15
- package/dist/cli/env-config.js +391 -0
- package/dist/cli/env-routes.js +820 -0
- package/dist/cli/env.html.js +679 -0
- package/dist/cli/index.js +4568 -2317
- package/dist/cli/server.js +2212 -19
- package/install.sh +19 -10
- package/package.json +2 -1
package/dist/cli/daemon.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
var __create = Object.create;
|
|
2
1
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
3
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
4
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
5
|
}) : x)(function(x) {
|
|
@@ -13,29 +9,10 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
13
9
|
var __esm = (fn, res) => function __init() {
|
|
14
10
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
11
|
};
|
|
16
|
-
var __commonJS = (cb, mod) => function __require2() {
|
|
17
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
18
|
-
};
|
|
19
12
|
var __export = (target, all) => {
|
|
20
13
|
for (var name in all)
|
|
21
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
22
15
|
};
|
|
23
|
-
var __copyProps = (to, from, except, desc) => {
|
|
24
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
25
|
-
for (let key of __getOwnPropNames(from))
|
|
26
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
27
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
28
|
-
}
|
|
29
|
-
return to;
|
|
30
|
-
};
|
|
31
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
32
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
33
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
34
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
35
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
36
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
37
|
-
mod
|
|
38
|
-
));
|
|
39
16
|
|
|
40
17
|
// node_modules/tsup/assets/esm_shims.js
|
|
41
18
|
import path from "path";
|
|
@@ -137,173 +114,6 @@ var init_coverage = __esm({
|
|
|
137
114
|
}
|
|
138
115
|
});
|
|
139
116
|
|
|
140
|
-
// node_modules/cookie/index.js
|
|
141
|
-
var require_cookie = __commonJS({
|
|
142
|
-
"node_modules/cookie/index.js"(exports) {
|
|
143
|
-
"use strict";
|
|
144
|
-
init_esm_shims();
|
|
145
|
-
exports.parse = parse;
|
|
146
|
-
exports.serialize = serialize;
|
|
147
|
-
var __toString = Object.prototype.toString;
|
|
148
|
-
var __hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
149
|
-
var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
150
|
-
var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/;
|
|
151
|
-
var domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
|
152
|
-
var pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/;
|
|
153
|
-
function parse(str, opt) {
|
|
154
|
-
if (typeof str !== "string") {
|
|
155
|
-
throw new TypeError("argument str must be a string");
|
|
156
|
-
}
|
|
157
|
-
var obj = {};
|
|
158
|
-
var len = str.length;
|
|
159
|
-
if (len < 2) return obj;
|
|
160
|
-
var dec = opt && opt.decode || decode;
|
|
161
|
-
var index = 0;
|
|
162
|
-
var eqIdx = 0;
|
|
163
|
-
var endIdx = 0;
|
|
164
|
-
do {
|
|
165
|
-
eqIdx = str.indexOf("=", index);
|
|
166
|
-
if (eqIdx === -1) break;
|
|
167
|
-
endIdx = str.indexOf(";", index);
|
|
168
|
-
if (endIdx === -1) {
|
|
169
|
-
endIdx = len;
|
|
170
|
-
} else if (eqIdx > endIdx) {
|
|
171
|
-
index = str.lastIndexOf(";", eqIdx - 1) + 1;
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
var keyStartIdx = startIndex(str, index, eqIdx);
|
|
175
|
-
var keyEndIdx = endIndex(str, eqIdx, keyStartIdx);
|
|
176
|
-
var key = str.slice(keyStartIdx, keyEndIdx);
|
|
177
|
-
if (!__hasOwnProperty.call(obj, key)) {
|
|
178
|
-
var valStartIdx = startIndex(str, eqIdx + 1, endIdx);
|
|
179
|
-
var valEndIdx = endIndex(str, endIdx, valStartIdx);
|
|
180
|
-
if (str.charCodeAt(valStartIdx) === 34 && str.charCodeAt(valEndIdx - 1) === 34) {
|
|
181
|
-
valStartIdx++;
|
|
182
|
-
valEndIdx--;
|
|
183
|
-
}
|
|
184
|
-
var val = str.slice(valStartIdx, valEndIdx);
|
|
185
|
-
obj[key] = tryDecode(val, dec);
|
|
186
|
-
}
|
|
187
|
-
index = endIdx + 1;
|
|
188
|
-
} while (index < len);
|
|
189
|
-
return obj;
|
|
190
|
-
}
|
|
191
|
-
function startIndex(str, index, max) {
|
|
192
|
-
do {
|
|
193
|
-
var code = str.charCodeAt(index);
|
|
194
|
-
if (code !== 32 && code !== 9) return index;
|
|
195
|
-
} while (++index < max);
|
|
196
|
-
return max;
|
|
197
|
-
}
|
|
198
|
-
function endIndex(str, index, min) {
|
|
199
|
-
while (index > min) {
|
|
200
|
-
var code = str.charCodeAt(--index);
|
|
201
|
-
if (code !== 32 && code !== 9) return index + 1;
|
|
202
|
-
}
|
|
203
|
-
return min;
|
|
204
|
-
}
|
|
205
|
-
function serialize(name, val, opt) {
|
|
206
|
-
var enc = opt && opt.encode || encodeURIComponent;
|
|
207
|
-
if (typeof enc !== "function") {
|
|
208
|
-
throw new TypeError("option encode is invalid");
|
|
209
|
-
}
|
|
210
|
-
if (!cookieNameRegExp.test(name)) {
|
|
211
|
-
throw new TypeError("argument name is invalid");
|
|
212
|
-
}
|
|
213
|
-
var value = enc(val);
|
|
214
|
-
if (!cookieValueRegExp.test(value)) {
|
|
215
|
-
throw new TypeError("argument val is invalid");
|
|
216
|
-
}
|
|
217
|
-
var str = name + "=" + value;
|
|
218
|
-
if (!opt) return str;
|
|
219
|
-
if (null != opt.maxAge) {
|
|
220
|
-
var maxAge = Math.floor(opt.maxAge);
|
|
221
|
-
if (!isFinite(maxAge)) {
|
|
222
|
-
throw new TypeError("option maxAge is invalid");
|
|
223
|
-
}
|
|
224
|
-
str += "; Max-Age=" + maxAge;
|
|
225
|
-
}
|
|
226
|
-
if (opt.domain) {
|
|
227
|
-
if (!domainValueRegExp.test(opt.domain)) {
|
|
228
|
-
throw new TypeError("option domain is invalid");
|
|
229
|
-
}
|
|
230
|
-
str += "; Domain=" + opt.domain;
|
|
231
|
-
}
|
|
232
|
-
if (opt.path) {
|
|
233
|
-
if (!pathValueRegExp.test(opt.path)) {
|
|
234
|
-
throw new TypeError("option path is invalid");
|
|
235
|
-
}
|
|
236
|
-
str += "; Path=" + opt.path;
|
|
237
|
-
}
|
|
238
|
-
if (opt.expires) {
|
|
239
|
-
var expires = opt.expires;
|
|
240
|
-
if (!isDate(expires) || isNaN(expires.valueOf())) {
|
|
241
|
-
throw new TypeError("option expires is invalid");
|
|
242
|
-
}
|
|
243
|
-
str += "; Expires=" + expires.toUTCString();
|
|
244
|
-
}
|
|
245
|
-
if (opt.httpOnly) {
|
|
246
|
-
str += "; HttpOnly";
|
|
247
|
-
}
|
|
248
|
-
if (opt.secure) {
|
|
249
|
-
str += "; Secure";
|
|
250
|
-
}
|
|
251
|
-
if (opt.partitioned) {
|
|
252
|
-
str += "; Partitioned";
|
|
253
|
-
}
|
|
254
|
-
if (opt.priority) {
|
|
255
|
-
var priority = typeof opt.priority === "string" ? opt.priority.toLowerCase() : opt.priority;
|
|
256
|
-
switch (priority) {
|
|
257
|
-
case "low":
|
|
258
|
-
str += "; Priority=Low";
|
|
259
|
-
break;
|
|
260
|
-
case "medium":
|
|
261
|
-
str += "; Priority=Medium";
|
|
262
|
-
break;
|
|
263
|
-
case "high":
|
|
264
|
-
str += "; Priority=High";
|
|
265
|
-
break;
|
|
266
|
-
default:
|
|
267
|
-
throw new TypeError("option priority is invalid");
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
if (opt.sameSite) {
|
|
271
|
-
var sameSite = typeof opt.sameSite === "string" ? opt.sameSite.toLowerCase() : opt.sameSite;
|
|
272
|
-
switch (sameSite) {
|
|
273
|
-
case true:
|
|
274
|
-
str += "; SameSite=Strict";
|
|
275
|
-
break;
|
|
276
|
-
case "lax":
|
|
277
|
-
str += "; SameSite=Lax";
|
|
278
|
-
break;
|
|
279
|
-
case "strict":
|
|
280
|
-
str += "; SameSite=Strict";
|
|
281
|
-
break;
|
|
282
|
-
case "none":
|
|
283
|
-
str += "; SameSite=None";
|
|
284
|
-
break;
|
|
285
|
-
default:
|
|
286
|
-
throw new TypeError("option sameSite is invalid");
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return str;
|
|
290
|
-
}
|
|
291
|
-
function decode(str) {
|
|
292
|
-
return str.indexOf("%") !== -1 ? decodeURIComponent(str) : str;
|
|
293
|
-
}
|
|
294
|
-
function isDate(val) {
|
|
295
|
-
return __toString.call(val) === "[object Date]";
|
|
296
|
-
}
|
|
297
|
-
function tryDecode(str, decode2) {
|
|
298
|
-
try {
|
|
299
|
-
return decode2(str);
|
|
300
|
-
} catch (e) {
|
|
301
|
-
return str;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
|
|
307
117
|
// cli/daemon.ts
|
|
308
118
|
init_esm_shims();
|
|
309
119
|
|
|
@@ -1766,13 +1576,9 @@ var BrowserTools = class {
|
|
|
1766
1576
|
{
|
|
1767
1577
|
name: "navigate_to_page",
|
|
1768
1578
|
description: "Navigate to a specific URL in the application",
|
|
1769
|
-
parameters:
|
|
1770
|
-
type: "
|
|
1771
|
-
|
|
1772
|
-
url: { type: "string", description: "The URL to navigate to" }
|
|
1773
|
-
},
|
|
1774
|
-
required: ["url"]
|
|
1775
|
-
},
|
|
1579
|
+
parameters: [
|
|
1580
|
+
{ name: "url", type: "string", description: "The URL to navigate to", required: true }
|
|
1581
|
+
],
|
|
1776
1582
|
execute: async ({ url }) => {
|
|
1777
1583
|
if (!this.page) await this.initialize();
|
|
1778
1584
|
try {
|
|
@@ -1785,24 +1591,20 @@ var BrowserTools = class {
|
|
|
1785
1591
|
input: url,
|
|
1786
1592
|
output: `Page title: ${title}`
|
|
1787
1593
|
});
|
|
1788
|
-
return `Successfully navigated to ${url}. Page title: "${title}"
|
|
1594
|
+
return { output: `Successfully navigated to ${url}. Page title: "${title}"` };
|
|
1789
1595
|
} catch (error) {
|
|
1790
|
-
return `Failed to navigate: ${error instanceof Error ? error.message : String(error)}
|
|
1596
|
+
return { output: `Failed to navigate: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
|
|
1791
1597
|
}
|
|
1792
1598
|
}
|
|
1793
1599
|
},
|
|
1794
1600
|
{
|
|
1795
1601
|
name: "click_element",
|
|
1796
1602
|
description: "Click on an element using a CSS selector",
|
|
1797
|
-
parameters:
|
|
1798
|
-
type: "
|
|
1799
|
-
|
|
1800
|
-
selector: { type: "string", description: "CSS selector of the element to click" }
|
|
1801
|
-
},
|
|
1802
|
-
required: ["selector"]
|
|
1803
|
-
},
|
|
1603
|
+
parameters: [
|
|
1604
|
+
{ name: "selector", type: "string", description: "CSS selector of the element to click", required: true }
|
|
1605
|
+
],
|
|
1804
1606
|
execute: async ({ selector }) => {
|
|
1805
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
1607
|
+
if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
|
|
1806
1608
|
try {
|
|
1807
1609
|
await this.page.click(selector, { timeout: 5e3 });
|
|
1808
1610
|
this.db.createAction({
|
|
@@ -1811,25 +1613,21 @@ var BrowserTools = class {
|
|
|
1811
1613
|
description: `Clicked element: ${selector}`,
|
|
1812
1614
|
input: selector
|
|
1813
1615
|
});
|
|
1814
|
-
return `Successfully clicked element: ${selector}
|
|
1616
|
+
return { output: `Successfully clicked element: ${selector}` };
|
|
1815
1617
|
} catch (error) {
|
|
1816
|
-
return `Failed to click element: ${error instanceof Error ? error.message : String(error)}
|
|
1618
|
+
return { output: `Failed to click element: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
|
|
1817
1619
|
}
|
|
1818
1620
|
}
|
|
1819
1621
|
},
|
|
1820
1622
|
{
|
|
1821
1623
|
name: "fill_input",
|
|
1822
1624
|
description: "Fill an input field with text",
|
|
1823
|
-
parameters:
|
|
1824
|
-
type: "
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
text: { type: "string", description: "Text to fill in the input" }
|
|
1828
|
-
},
|
|
1829
|
-
required: ["selector", "text"]
|
|
1830
|
-
},
|
|
1625
|
+
parameters: [
|
|
1626
|
+
{ name: "selector", type: "string", description: "CSS selector of the input field", required: true },
|
|
1627
|
+
{ name: "text", type: "string", description: "Text to fill in the input", required: true }
|
|
1628
|
+
],
|
|
1831
1629
|
execute: async ({ selector, text }) => {
|
|
1832
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
1630
|
+
if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
|
|
1833
1631
|
try {
|
|
1834
1632
|
await this.page.fill(selector, text);
|
|
1835
1633
|
this.db.createAction({
|
|
@@ -1838,24 +1636,20 @@ var BrowserTools = class {
|
|
|
1838
1636
|
description: `Filled input ${selector}`,
|
|
1839
1637
|
input: `${selector} = ${text}`
|
|
1840
1638
|
});
|
|
1841
|
-
return `Successfully filled input ${selector} with text
|
|
1639
|
+
return { output: `Successfully filled input ${selector} with text` };
|
|
1842
1640
|
} catch (error) {
|
|
1843
|
-
return `Failed to fill input: ${error instanceof Error ? error.message : String(error)}
|
|
1641
|
+
return { output: `Failed to fill input: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
|
|
1844
1642
|
}
|
|
1845
1643
|
}
|
|
1846
1644
|
},
|
|
1847
1645
|
{
|
|
1848
1646
|
name: "take_screenshot",
|
|
1849
1647
|
description: "Take a screenshot of the current page for evidence",
|
|
1850
|
-
parameters:
|
|
1851
|
-
type: "
|
|
1852
|
-
|
|
1853
|
-
name: { type: "string", description: "Name for the screenshot file" }
|
|
1854
|
-
},
|
|
1855
|
-
required: ["name"]
|
|
1856
|
-
},
|
|
1648
|
+
parameters: [
|
|
1649
|
+
{ name: "name", type: "string", description: "Name for the screenshot file", required: true }
|
|
1650
|
+
],
|
|
1857
1651
|
execute: async ({ name }) => {
|
|
1858
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
1652
|
+
if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
|
|
1859
1653
|
try {
|
|
1860
1654
|
const filename = `${Date.now()}_${name}.png`;
|
|
1861
1655
|
const path2 = join4(this.screenshotDir, filename);
|
|
@@ -1866,38 +1660,32 @@ var BrowserTools = class {
|
|
|
1866
1660
|
description: `Screenshot: ${name}`,
|
|
1867
1661
|
screenshot_path: path2
|
|
1868
1662
|
});
|
|
1869
|
-
return `Screenshot saved: ${path2}
|
|
1663
|
+
return { output: `Screenshot saved: ${path2}` };
|
|
1870
1664
|
} catch (error) {
|
|
1871
|
-
return `Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}
|
|
1665
|
+
return { output: `Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
|
|
1872
1666
|
}
|
|
1873
1667
|
}
|
|
1874
1668
|
},
|
|
1875
1669
|
{
|
|
1876
1670
|
name: "get_page_content",
|
|
1877
1671
|
description: "Get the text content of the current page",
|
|
1878
|
-
parameters:
|
|
1879
|
-
type: "object",
|
|
1880
|
-
properties: {}
|
|
1881
|
-
},
|
|
1672
|
+
parameters: [],
|
|
1882
1673
|
execute: async () => {
|
|
1883
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
1674
|
+
if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
|
|
1884
1675
|
try {
|
|
1885
1676
|
const content = await this.page.textContent("body");
|
|
1886
|
-
return content?.slice(0, 1e3) || "No content found";
|
|
1677
|
+
return { output: content?.slice(0, 1e3) || "No content found" };
|
|
1887
1678
|
} catch (error) {
|
|
1888
|
-
return `Failed to get content: ${error instanceof Error ? error.message : String(error)}
|
|
1679
|
+
return { output: `Failed to get content: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
|
|
1889
1680
|
}
|
|
1890
1681
|
}
|
|
1891
1682
|
},
|
|
1892
1683
|
{
|
|
1893
1684
|
name: "check_console_errors",
|
|
1894
1685
|
description: "Check for JavaScript console errors on the page",
|
|
1895
|
-
parameters:
|
|
1896
|
-
type: "object",
|
|
1897
|
-
properties: {}
|
|
1898
|
-
},
|
|
1686
|
+
parameters: [],
|
|
1899
1687
|
execute: async () => {
|
|
1900
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
1688
|
+
if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
|
|
1901
1689
|
const errors = [];
|
|
1902
1690
|
this.page.on("console", (msg) => {
|
|
1903
1691
|
if (msg.type() === "error") {
|
|
@@ -1906,10 +1694,10 @@ var BrowserTools = class {
|
|
|
1906
1694
|
});
|
|
1907
1695
|
await this.page.waitForTimeout(2e3);
|
|
1908
1696
|
if (errors.length > 0) {
|
|
1909
|
-
return `Found ${errors.length} console errors:
|
|
1910
|
-
${errors.join("\n")}
|
|
1697
|
+
return { output: `Found ${errors.length} console errors:
|
|
1698
|
+
${errors.join("\n")}` };
|
|
1911
1699
|
}
|
|
1912
|
-
return "No console errors detected";
|
|
1700
|
+
return { output: "No console errors detected" };
|
|
1913
1701
|
}
|
|
1914
1702
|
}
|
|
1915
1703
|
];
|
|
@@ -3856,8 +3644,8 @@ async function verifyPassword(plain, stored) {
|
|
|
3856
3644
|
|
|
3857
3645
|
// cli/auth/jwt.ts
|
|
3858
3646
|
init_esm_shims();
|
|
3859
|
-
var import_cookie = __toESM(require_cookie(), 1);
|
|
3860
3647
|
import { createHmac, randomBytes as randomBytes2 } from "crypto";
|
|
3648
|
+
import { parse as parseCookies } from "cookie";
|
|
3861
3649
|
var TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3862
3650
|
var COOKIE_NAME = "openqa_token";
|
|
3863
3651
|
var _secret = null;
|
|
@@ -3912,7 +3700,7 @@ function clearAuthCookie(res) {
|
|
|
3912
3700
|
function extractToken(req) {
|
|
3913
3701
|
const cookieHeader = req.headers.cookie ?? "";
|
|
3914
3702
|
if (cookieHeader) {
|
|
3915
|
-
const cookies = (
|
|
3703
|
+
const cookies = parseCookies(cookieHeader);
|
|
3916
3704
|
if (cookies[COOKIE_NAME]) return cookies[COOKIE_NAME];
|
|
3917
3705
|
}
|
|
3918
3706
|
const auth = req.headers.authorization ?? "";
|
|
@@ -3980,7 +3768,7 @@ var loginSchema = z3.object({
|
|
|
3980
3768
|
password: z3.string().min(1).max(200)
|
|
3981
3769
|
});
|
|
3982
3770
|
var setupSchema = z3.object({
|
|
3983
|
-
username: z3.string().min(3).max(
|
|
3771
|
+
username: z3.string().min(3).max(100).regex(/^[a-z0-9_.@-]+$/, "Only lowercase letters, digits, and ._@- characters"),
|
|
3984
3772
|
password: z3.string().min(8).max(200)
|
|
3985
3773
|
});
|
|
3986
3774
|
var changePasswordSchema = z3.object({
|
|
@@ -3988,7 +3776,7 @@ var changePasswordSchema = z3.object({
|
|
|
3988
3776
|
newPassword: z3.string().min(8).max(200)
|
|
3989
3777
|
});
|
|
3990
3778
|
var createUserSchema = z3.object({
|
|
3991
|
-
username: z3.string().min(3).max(
|
|
3779
|
+
username: z3.string().min(3).max(100).regex(/^[a-z0-9_.@-]+$/, "Only lowercase letters, digits, and ._@- characters"),
|
|
3992
3780
|
password: z3.string().min(8).max(200),
|
|
3993
3781
|
role: z3.enum(["admin", "viewer"])
|
|
3994
3782
|
});
|
|
@@ -4118,6 +3906,759 @@ function createAuthRouter(db2) {
|
|
|
4118
3906
|
return router;
|
|
4119
3907
|
}
|
|
4120
3908
|
|
|
3909
|
+
// cli/env-routes.ts
|
|
3910
|
+
init_esm_shims();
|
|
3911
|
+
import { Router as Router3 } from "express";
|
|
3912
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
3913
|
+
import { join as join6 } from "path";
|
|
3914
|
+
|
|
3915
|
+
// cli/env-config.ts
|
|
3916
|
+
init_esm_shims();
|
|
3917
|
+
var ENV_VARIABLES = [
|
|
3918
|
+
// ============================================================================
|
|
3919
|
+
// LLM CONFIGURATION
|
|
3920
|
+
// ============================================================================
|
|
3921
|
+
{
|
|
3922
|
+
key: "LLM_PROVIDER",
|
|
3923
|
+
type: "select",
|
|
3924
|
+
category: "llm",
|
|
3925
|
+
required: true,
|
|
3926
|
+
description: "LLM provider to use for AI operations",
|
|
3927
|
+
options: ["openai", "anthropic", "ollama"],
|
|
3928
|
+
placeholder: "openai",
|
|
3929
|
+
restartRequired: true
|
|
3930
|
+
},
|
|
3931
|
+
{
|
|
3932
|
+
key: "OPENAI_API_KEY",
|
|
3933
|
+
type: "password",
|
|
3934
|
+
category: "llm",
|
|
3935
|
+
required: false,
|
|
3936
|
+
description: "OpenAI API key (required if LLM_PROVIDER=openai)",
|
|
3937
|
+
placeholder: "sk-...",
|
|
3938
|
+
sensitive: true,
|
|
3939
|
+
testable: true,
|
|
3940
|
+
validation: (value) => {
|
|
3941
|
+
if (!value) return { valid: true };
|
|
3942
|
+
if (!value.startsWith("sk-")) {
|
|
3943
|
+
return { valid: false, error: 'OpenAI API key must start with "sk-"' };
|
|
3944
|
+
}
|
|
3945
|
+
if (value.length < 20) {
|
|
3946
|
+
return { valid: false, error: "API key seems too short" };
|
|
3947
|
+
}
|
|
3948
|
+
return { valid: true };
|
|
3949
|
+
},
|
|
3950
|
+
restartRequired: true
|
|
3951
|
+
},
|
|
3952
|
+
{
|
|
3953
|
+
key: "ANTHROPIC_API_KEY",
|
|
3954
|
+
type: "password",
|
|
3955
|
+
category: "llm",
|
|
3956
|
+
required: false,
|
|
3957
|
+
description: "Anthropic API key (required if LLM_PROVIDER=anthropic)",
|
|
3958
|
+
placeholder: "sk-ant-...",
|
|
3959
|
+
sensitive: true,
|
|
3960
|
+
testable: true,
|
|
3961
|
+
validation: (value) => {
|
|
3962
|
+
if (!value) return { valid: true };
|
|
3963
|
+
if (!value.startsWith("sk-ant-")) {
|
|
3964
|
+
return { valid: false, error: 'Anthropic API key must start with "sk-ant-"' };
|
|
3965
|
+
}
|
|
3966
|
+
return { valid: true };
|
|
3967
|
+
},
|
|
3968
|
+
restartRequired: true
|
|
3969
|
+
},
|
|
3970
|
+
{
|
|
3971
|
+
key: "OLLAMA_BASE_URL",
|
|
3972
|
+
type: "url",
|
|
3973
|
+
category: "llm",
|
|
3974
|
+
required: false,
|
|
3975
|
+
description: "Ollama server URL (required if LLM_PROVIDER=ollama)",
|
|
3976
|
+
placeholder: "http://localhost:11434",
|
|
3977
|
+
testable: true,
|
|
3978
|
+
validation: (value) => {
|
|
3979
|
+
if (!value) return { valid: true };
|
|
3980
|
+
try {
|
|
3981
|
+
new URL(value);
|
|
3982
|
+
return { valid: true };
|
|
3983
|
+
} catch {
|
|
3984
|
+
return { valid: false, error: "Invalid URL format" };
|
|
3985
|
+
}
|
|
3986
|
+
},
|
|
3987
|
+
restartRequired: true
|
|
3988
|
+
},
|
|
3989
|
+
{
|
|
3990
|
+
key: "LLM_MODEL",
|
|
3991
|
+
type: "text",
|
|
3992
|
+
category: "llm",
|
|
3993
|
+
required: false,
|
|
3994
|
+
description: "LLM model to use (e.g., gpt-4, claude-3-opus, llama2)",
|
|
3995
|
+
placeholder: "gpt-4",
|
|
3996
|
+
restartRequired: true
|
|
3997
|
+
},
|
|
3998
|
+
// ============================================================================
|
|
3999
|
+
// SECURITY
|
|
4000
|
+
// ============================================================================
|
|
4001
|
+
{
|
|
4002
|
+
key: "OPENQA_JWT_SECRET",
|
|
4003
|
+
type: "password",
|
|
4004
|
+
category: "security",
|
|
4005
|
+
required: true,
|
|
4006
|
+
description: "Secret key for JWT token signing (min 32 characters)",
|
|
4007
|
+
placeholder: "Generate with: openssl rand -hex 32",
|
|
4008
|
+
sensitive: true,
|
|
4009
|
+
validation: (value) => {
|
|
4010
|
+
if (!value) return { valid: false, error: "JWT secret is required" };
|
|
4011
|
+
if (value.length < 32) {
|
|
4012
|
+
return { valid: false, error: "JWT secret must be at least 32 characters" };
|
|
4013
|
+
}
|
|
4014
|
+
return { valid: true };
|
|
4015
|
+
},
|
|
4016
|
+
restartRequired: true
|
|
4017
|
+
},
|
|
4018
|
+
{
|
|
4019
|
+
key: "OPENQA_AUTH_DISABLED",
|
|
4020
|
+
type: "boolean",
|
|
4021
|
+
category: "security",
|
|
4022
|
+
required: false,
|
|
4023
|
+
description: "\u26A0\uFE0F DANGER: Disable authentication (NEVER use in production!)",
|
|
4024
|
+
placeholder: "false",
|
|
4025
|
+
validation: (value) => {
|
|
4026
|
+
if (value === "true" && process.env.NODE_ENV === "production") {
|
|
4027
|
+
return { valid: false, error: "Cannot disable auth in production!" };
|
|
4028
|
+
}
|
|
4029
|
+
return { valid: true };
|
|
4030
|
+
},
|
|
4031
|
+
restartRequired: true
|
|
4032
|
+
},
|
|
4033
|
+
{
|
|
4034
|
+
key: "NODE_ENV",
|
|
4035
|
+
type: "select",
|
|
4036
|
+
category: "security",
|
|
4037
|
+
required: false,
|
|
4038
|
+
description: "Node environment (production enables security features)",
|
|
4039
|
+
options: ["development", "production", "test"],
|
|
4040
|
+
placeholder: "production",
|
|
4041
|
+
restartRequired: true
|
|
4042
|
+
},
|
|
4043
|
+
// ============================================================================
|
|
4044
|
+
// TARGET APPLICATION
|
|
4045
|
+
// ============================================================================
|
|
4046
|
+
{
|
|
4047
|
+
key: "SAAS_URL",
|
|
4048
|
+
type: "url",
|
|
4049
|
+
category: "target",
|
|
4050
|
+
required: true,
|
|
4051
|
+
description: "URL of the application to test",
|
|
4052
|
+
placeholder: "https://your-app.com",
|
|
4053
|
+
testable: true,
|
|
4054
|
+
validation: (value) => {
|
|
4055
|
+
if (!value) return { valid: false, error: "Target URL is required" };
|
|
4056
|
+
try {
|
|
4057
|
+
const url = new URL(value);
|
|
4058
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
4059
|
+
return { valid: false, error: "URL must use http or https protocol" };
|
|
4060
|
+
}
|
|
4061
|
+
return { valid: true };
|
|
4062
|
+
} catch {
|
|
4063
|
+
return { valid: false, error: "Invalid URL format" };
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
},
|
|
4067
|
+
{
|
|
4068
|
+
key: "SAAS_AUTH_TYPE",
|
|
4069
|
+
type: "select",
|
|
4070
|
+
category: "target",
|
|
4071
|
+
required: false,
|
|
4072
|
+
description: "Authentication type for target application",
|
|
4073
|
+
options: ["none", "basic", "session"],
|
|
4074
|
+
placeholder: "none"
|
|
4075
|
+
},
|
|
4076
|
+
{
|
|
4077
|
+
key: "SAAS_USERNAME",
|
|
4078
|
+
type: "text",
|
|
4079
|
+
category: "target",
|
|
4080
|
+
required: false,
|
|
4081
|
+
description: "Username for target application authentication",
|
|
4082
|
+
placeholder: "test@example.com"
|
|
4083
|
+
},
|
|
4084
|
+
{
|
|
4085
|
+
key: "SAAS_PASSWORD",
|
|
4086
|
+
type: "password",
|
|
4087
|
+
category: "target",
|
|
4088
|
+
required: false,
|
|
4089
|
+
description: "Password for target application authentication",
|
|
4090
|
+
placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
|
|
4091
|
+
sensitive: true
|
|
4092
|
+
},
|
|
4093
|
+
// ============================================================================
|
|
4094
|
+
// GITHUB INTEGRATION
|
|
4095
|
+
// ============================================================================
|
|
4096
|
+
{
|
|
4097
|
+
key: "GITHUB_TOKEN",
|
|
4098
|
+
type: "password",
|
|
4099
|
+
category: "github",
|
|
4100
|
+
required: false,
|
|
4101
|
+
description: "GitHub personal access token for issue creation",
|
|
4102
|
+
placeholder: "ghp_...",
|
|
4103
|
+
sensitive: true,
|
|
4104
|
+
testable: true,
|
|
4105
|
+
validation: (value) => {
|
|
4106
|
+
if (!value) return { valid: true };
|
|
4107
|
+
if (!value.startsWith("ghp_") && !value.startsWith("github_pat_")) {
|
|
4108
|
+
return { valid: false, error: 'GitHub token must start with "ghp_" or "github_pat_"' };
|
|
4109
|
+
}
|
|
4110
|
+
return { valid: true };
|
|
4111
|
+
}
|
|
4112
|
+
},
|
|
4113
|
+
{
|
|
4114
|
+
key: "GITHUB_OWNER",
|
|
4115
|
+
type: "text",
|
|
4116
|
+
category: "github",
|
|
4117
|
+
required: false,
|
|
4118
|
+
description: "GitHub repository owner/organization",
|
|
4119
|
+
placeholder: "your-username"
|
|
4120
|
+
},
|
|
4121
|
+
{
|
|
4122
|
+
key: "GITHUB_REPO",
|
|
4123
|
+
type: "text",
|
|
4124
|
+
category: "github",
|
|
4125
|
+
required: false,
|
|
4126
|
+
description: "GitHub repository name",
|
|
4127
|
+
placeholder: "your-repo"
|
|
4128
|
+
},
|
|
4129
|
+
{
|
|
4130
|
+
key: "GITHUB_BRANCH",
|
|
4131
|
+
type: "text",
|
|
4132
|
+
category: "github",
|
|
4133
|
+
required: false,
|
|
4134
|
+
description: "GitHub branch to monitor",
|
|
4135
|
+
placeholder: "main"
|
|
4136
|
+
},
|
|
4137
|
+
// ============================================================================
|
|
4138
|
+
// WEB SERVER
|
|
4139
|
+
// ============================================================================
|
|
4140
|
+
{
|
|
4141
|
+
key: "WEB_PORT",
|
|
4142
|
+
type: "number",
|
|
4143
|
+
category: "web",
|
|
4144
|
+
required: false,
|
|
4145
|
+
description: "Port for web server",
|
|
4146
|
+
placeholder: "4242",
|
|
4147
|
+
validation: (value) => {
|
|
4148
|
+
if (!value) return { valid: true };
|
|
4149
|
+
const port = parseInt(value, 10);
|
|
4150
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
4151
|
+
return { valid: false, error: "Port must be between 1 and 65535" };
|
|
4152
|
+
}
|
|
4153
|
+
return { valid: true };
|
|
4154
|
+
},
|
|
4155
|
+
restartRequired: true
|
|
4156
|
+
},
|
|
4157
|
+
{
|
|
4158
|
+
key: "WEB_HOST",
|
|
4159
|
+
type: "text",
|
|
4160
|
+
category: "web",
|
|
4161
|
+
required: false,
|
|
4162
|
+
description: "Host to bind web server (0.0.0.0 for all interfaces)",
|
|
4163
|
+
placeholder: "0.0.0.0",
|
|
4164
|
+
restartRequired: true
|
|
4165
|
+
},
|
|
4166
|
+
{
|
|
4167
|
+
key: "CORS_ORIGINS",
|
|
4168
|
+
type: "text",
|
|
4169
|
+
category: "web",
|
|
4170
|
+
required: false,
|
|
4171
|
+
description: "Allowed CORS origins (comma-separated)",
|
|
4172
|
+
placeholder: "https://your-domain.com,https://app.example.com",
|
|
4173
|
+
restartRequired: true
|
|
4174
|
+
},
|
|
4175
|
+
// ============================================================================
|
|
4176
|
+
// AGENT CONFIGURATION
|
|
4177
|
+
// ============================================================================
|
|
4178
|
+
{
|
|
4179
|
+
key: "AGENT_AUTO_START",
|
|
4180
|
+
type: "boolean",
|
|
4181
|
+
category: "agent",
|
|
4182
|
+
required: false,
|
|
4183
|
+
description: "Auto-start agent on server launch",
|
|
4184
|
+
placeholder: "false"
|
|
4185
|
+
},
|
|
4186
|
+
{
|
|
4187
|
+
key: "AGENT_INTERVAL_MS",
|
|
4188
|
+
type: "number",
|
|
4189
|
+
category: "agent",
|
|
4190
|
+
required: false,
|
|
4191
|
+
description: "Agent run interval in milliseconds (1 hour = 3600000)",
|
|
4192
|
+
placeholder: "3600000",
|
|
4193
|
+
validation: (value) => {
|
|
4194
|
+
if (!value) return { valid: true };
|
|
4195
|
+
const interval = parseInt(value, 10);
|
|
4196
|
+
if (isNaN(interval) || interval < 6e4) {
|
|
4197
|
+
return { valid: false, error: "Interval must be at least 60000ms (1 minute)" };
|
|
4198
|
+
}
|
|
4199
|
+
return { valid: true };
|
|
4200
|
+
}
|
|
4201
|
+
},
|
|
4202
|
+
{
|
|
4203
|
+
key: "AGENT_MAX_ITERATIONS",
|
|
4204
|
+
type: "number",
|
|
4205
|
+
category: "agent",
|
|
4206
|
+
required: false,
|
|
4207
|
+
description: "Maximum iterations per agent session",
|
|
4208
|
+
placeholder: "20",
|
|
4209
|
+
validation: (value) => {
|
|
4210
|
+
if (!value) return { valid: true };
|
|
4211
|
+
const max = parseInt(value, 10);
|
|
4212
|
+
if (isNaN(max) || max < 1 || max > 1e3) {
|
|
4213
|
+
return { valid: false, error: "Max iterations must be between 1 and 1000" };
|
|
4214
|
+
}
|
|
4215
|
+
return { valid: true };
|
|
4216
|
+
}
|
|
4217
|
+
},
|
|
4218
|
+
{
|
|
4219
|
+
key: "GIT_LISTENER_ENABLED",
|
|
4220
|
+
type: "boolean",
|
|
4221
|
+
category: "agent",
|
|
4222
|
+
required: false,
|
|
4223
|
+
description: "Enable git merge/pipeline detection",
|
|
4224
|
+
placeholder: "true"
|
|
4225
|
+
},
|
|
4226
|
+
{
|
|
4227
|
+
key: "GIT_POLL_INTERVAL_MS",
|
|
4228
|
+
type: "number",
|
|
4229
|
+
category: "agent",
|
|
4230
|
+
required: false,
|
|
4231
|
+
description: "Git polling interval in milliseconds",
|
|
4232
|
+
placeholder: "60000"
|
|
4233
|
+
},
|
|
4234
|
+
// ============================================================================
|
|
4235
|
+
// DATABASE
|
|
4236
|
+
// ============================================================================
|
|
4237
|
+
{
|
|
4238
|
+
key: "DB_PATH",
|
|
4239
|
+
type: "text",
|
|
4240
|
+
category: "database",
|
|
4241
|
+
required: false,
|
|
4242
|
+
description: "Path to SQLite database file",
|
|
4243
|
+
placeholder: "./data/openqa.db",
|
|
4244
|
+
restartRequired: true
|
|
4245
|
+
},
|
|
4246
|
+
// ============================================================================
|
|
4247
|
+
// NOTIFICATIONS
|
|
4248
|
+
// ============================================================================
|
|
4249
|
+
{
|
|
4250
|
+
key: "SLACK_WEBHOOK_URL",
|
|
4251
|
+
type: "url",
|
|
4252
|
+
category: "notifications",
|
|
4253
|
+
required: false,
|
|
4254
|
+
description: "Slack webhook URL for notifications",
|
|
4255
|
+
placeholder: "https://hooks.slack.com/services/...",
|
|
4256
|
+
sensitive: true,
|
|
4257
|
+
testable: true,
|
|
4258
|
+
validation: (value) => {
|
|
4259
|
+
if (!value) return { valid: true };
|
|
4260
|
+
if (!value.startsWith("https://hooks.slack.com/")) {
|
|
4261
|
+
return { valid: false, error: "Invalid Slack webhook URL" };
|
|
4262
|
+
}
|
|
4263
|
+
return { valid: true };
|
|
4264
|
+
}
|
|
4265
|
+
},
|
|
4266
|
+
{
|
|
4267
|
+
key: "DISCORD_WEBHOOK_URL",
|
|
4268
|
+
type: "url",
|
|
4269
|
+
category: "notifications",
|
|
4270
|
+
required: false,
|
|
4271
|
+
description: "Discord webhook URL for notifications",
|
|
4272
|
+
placeholder: "https://discord.com/api/webhooks/...",
|
|
4273
|
+
sensitive: true,
|
|
4274
|
+
testable: true,
|
|
4275
|
+
validation: (value) => {
|
|
4276
|
+
if (!value) return { valid: true };
|
|
4277
|
+
if (!value.startsWith("https://discord.com/api/webhooks/")) {
|
|
4278
|
+
return { valid: false, error: "Invalid Discord webhook URL" };
|
|
4279
|
+
}
|
|
4280
|
+
return { valid: true };
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
];
|
|
4284
|
+
function getEnvVariable(key) {
|
|
4285
|
+
return ENV_VARIABLES.find((v) => v.key === key);
|
|
4286
|
+
}
|
|
4287
|
+
function validateEnvValue(key, value) {
|
|
4288
|
+
const envVar = getEnvVariable(key);
|
|
4289
|
+
if (!envVar) return { valid: false, error: "Unknown environment variable" };
|
|
4290
|
+
if (envVar.required && !value) {
|
|
4291
|
+
return { valid: false, error: "This field is required" };
|
|
4292
|
+
}
|
|
4293
|
+
if (envVar.validation) {
|
|
4294
|
+
return envVar.validation(value);
|
|
4295
|
+
}
|
|
4296
|
+
return { valid: true };
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4299
|
+
// cli/env-routes.ts
|
|
4300
|
+
function createEnvRouter() {
|
|
4301
|
+
const router = Router3();
|
|
4302
|
+
const ENV_FILE_PATH = join6(process.cwd(), ".env");
|
|
4303
|
+
function readEnvFile() {
|
|
4304
|
+
if (!existsSync4(ENV_FILE_PATH)) {
|
|
4305
|
+
return {};
|
|
4306
|
+
}
|
|
4307
|
+
const content = readFileSync5(ENV_FILE_PATH, "utf-8");
|
|
4308
|
+
const env = {};
|
|
4309
|
+
content.split("\n").forEach((line) => {
|
|
4310
|
+
line = line.trim();
|
|
4311
|
+
if (!line || line.startsWith("#")) return;
|
|
4312
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
4313
|
+
if (match) {
|
|
4314
|
+
const [, key, value] = match;
|
|
4315
|
+
env[key.trim()] = value.trim().replace(/^["']|["']$/g, "");
|
|
4316
|
+
}
|
|
4317
|
+
});
|
|
4318
|
+
return env;
|
|
4319
|
+
}
|
|
4320
|
+
function writeEnvFile(env) {
|
|
4321
|
+
const lines = [
|
|
4322
|
+
"# OpenQA Environment Configuration",
|
|
4323
|
+
"# Auto-generated by OpenQA Dashboard",
|
|
4324
|
+
"# Last updated: " + (/* @__PURE__ */ new Date()).toISOString(),
|
|
4325
|
+
""
|
|
4326
|
+
];
|
|
4327
|
+
const categories = {};
|
|
4328
|
+
ENV_VARIABLES.forEach((v) => {
|
|
4329
|
+
if (!categories[v.category]) {
|
|
4330
|
+
categories[v.category] = [];
|
|
4331
|
+
}
|
|
4332
|
+
const value = env[v.key] || "";
|
|
4333
|
+
if (value || v.required) {
|
|
4334
|
+
categories[v.category].push(`${v.key}=${value}`);
|
|
4335
|
+
}
|
|
4336
|
+
});
|
|
4337
|
+
const categoryNames = {
|
|
4338
|
+
llm: "LLM CONFIGURATION",
|
|
4339
|
+
security: "SECURITY",
|
|
4340
|
+
target: "TARGET APPLICATION",
|
|
4341
|
+
github: "GITHUB INTEGRATION",
|
|
4342
|
+
web: "WEB SERVER",
|
|
4343
|
+
agent: "AGENT CONFIGURATION",
|
|
4344
|
+
database: "DATABASE",
|
|
4345
|
+
notifications: "NOTIFICATIONS"
|
|
4346
|
+
};
|
|
4347
|
+
Object.entries(categories).forEach(([category, vars]) => {
|
|
4348
|
+
if (vars.length > 0) {
|
|
4349
|
+
lines.push("# " + "=".repeat(76));
|
|
4350
|
+
lines.push(`# ${categoryNames[category] || category.toUpperCase()}`);
|
|
4351
|
+
lines.push("# " + "=".repeat(76));
|
|
4352
|
+
lines.push(...vars);
|
|
4353
|
+
lines.push("");
|
|
4354
|
+
}
|
|
4355
|
+
});
|
|
4356
|
+
writeFileSync3(ENV_FILE_PATH, lines.join("\n"));
|
|
4357
|
+
}
|
|
4358
|
+
router.get("/api/env", requireAuth, requireAdmin, (_req, res) => {
|
|
4359
|
+
try {
|
|
4360
|
+
const envFile = readEnvFile();
|
|
4361
|
+
const processEnv = process.env;
|
|
4362
|
+
const variables = ENV_VARIABLES.map((v) => ({
|
|
4363
|
+
key: v.key,
|
|
4364
|
+
value: envFile[v.key] || processEnv[v.key] || "",
|
|
4365
|
+
type: v.type,
|
|
4366
|
+
category: v.category,
|
|
4367
|
+
required: v.required,
|
|
4368
|
+
description: v.description,
|
|
4369
|
+
placeholder: v.placeholder,
|
|
4370
|
+
options: v.options,
|
|
4371
|
+
sensitive: v.sensitive,
|
|
4372
|
+
testable: v.testable,
|
|
4373
|
+
restartRequired: v.restartRequired,
|
|
4374
|
+
// Mask sensitive values
|
|
4375
|
+
displayValue: v.sensitive && (envFile[v.key] || processEnv[v.key]) ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : envFile[v.key] || processEnv[v.key] || ""
|
|
4376
|
+
}));
|
|
4377
|
+
res.json({
|
|
4378
|
+
variables,
|
|
4379
|
+
envFileExists: existsSync4(ENV_FILE_PATH),
|
|
4380
|
+
lastModified: existsSync4(ENV_FILE_PATH) ? new Date(readFileSync5(ENV_FILE_PATH, "utf-8").match(/Last updated: (.+)/)?.[1] || 0).toISOString() : null
|
|
4381
|
+
});
|
|
4382
|
+
} catch (error) {
|
|
4383
|
+
res.status(500).json({
|
|
4384
|
+
error: "Failed to read environment variables",
|
|
4385
|
+
details: error instanceof Error ? error.message : String(error)
|
|
4386
|
+
});
|
|
4387
|
+
}
|
|
4388
|
+
});
|
|
4389
|
+
router.get("/api/env/:key", requireAuth, requireAdmin, (req, res) => {
|
|
4390
|
+
try {
|
|
4391
|
+
const { key } = req.params;
|
|
4392
|
+
const envVar = ENV_VARIABLES.find((v) => v.key === key);
|
|
4393
|
+
if (!envVar) {
|
|
4394
|
+
res.status(404).json({ error: "Environment variable not found" });
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4397
|
+
const envFile = readEnvFile();
|
|
4398
|
+
const value = envFile[key] || process.env[key] || "";
|
|
4399
|
+
res.json({
|
|
4400
|
+
...envVar,
|
|
4401
|
+
value,
|
|
4402
|
+
displayValue: envVar.sensitive && value ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : value
|
|
4403
|
+
});
|
|
4404
|
+
} catch (error) {
|
|
4405
|
+
res.status(500).json({
|
|
4406
|
+
error: "Failed to read environment variable",
|
|
4407
|
+
details: error instanceof Error ? error.message : String(error)
|
|
4408
|
+
});
|
|
4409
|
+
}
|
|
4410
|
+
});
|
|
4411
|
+
router.put("/api/env/:key", requireAuth, requireAdmin, (req, res) => {
|
|
4412
|
+
try {
|
|
4413
|
+
const { key } = req.params;
|
|
4414
|
+
const { value } = req.body;
|
|
4415
|
+
const validation = validateEnvValue(key, value);
|
|
4416
|
+
if (!validation.valid) {
|
|
4417
|
+
res.status(400).json({ error: validation.error });
|
|
4418
|
+
return;
|
|
4419
|
+
}
|
|
4420
|
+
const env = readEnvFile();
|
|
4421
|
+
if (value === "" || value === null || value === void 0) {
|
|
4422
|
+
delete env[key];
|
|
4423
|
+
} else {
|
|
4424
|
+
env[key] = value;
|
|
4425
|
+
}
|
|
4426
|
+
writeEnvFile(env);
|
|
4427
|
+
if (value) {
|
|
4428
|
+
process.env[key] = value;
|
|
4429
|
+
} else {
|
|
4430
|
+
delete process.env[key];
|
|
4431
|
+
}
|
|
4432
|
+
const envVar = ENV_VARIABLES.find((v) => v.key === key);
|
|
4433
|
+
res.json({
|
|
4434
|
+
success: true,
|
|
4435
|
+
key,
|
|
4436
|
+
value: envVar?.sensitive ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : value,
|
|
4437
|
+
restartRequired: envVar?.restartRequired || false
|
|
4438
|
+
});
|
|
4439
|
+
} catch (error) {
|
|
4440
|
+
res.status(500).json({
|
|
4441
|
+
error: "Failed to update environment variable",
|
|
4442
|
+
details: error instanceof Error ? error.message : String(error)
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
4445
|
+
});
|
|
4446
|
+
router.post("/api/env/bulk", requireAuth, requireAdmin, (req, res) => {
|
|
4447
|
+
try {
|
|
4448
|
+
const { variables } = req.body;
|
|
4449
|
+
if (!variables || typeof variables !== "object") {
|
|
4450
|
+
res.status(400).json({ error: "Invalid request body" });
|
|
4451
|
+
return;
|
|
4452
|
+
}
|
|
4453
|
+
const errors = {};
|
|
4454
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
4455
|
+
const validation = validateEnvValue(key, value);
|
|
4456
|
+
if (!validation.valid) {
|
|
4457
|
+
errors[key] = validation.error || "Invalid value";
|
|
4458
|
+
}
|
|
4459
|
+
});
|
|
4460
|
+
if (Object.keys(errors).length > 0) {
|
|
4461
|
+
res.status(400).json({ error: "Validation failed", errors });
|
|
4462
|
+
return;
|
|
4463
|
+
}
|
|
4464
|
+
const env = readEnvFile();
|
|
4465
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
4466
|
+
if (value === "" || value === null || value === void 0) {
|
|
4467
|
+
delete env[key];
|
|
4468
|
+
delete process.env[key];
|
|
4469
|
+
} else {
|
|
4470
|
+
env[key] = value;
|
|
4471
|
+
process.env[key] = value;
|
|
4472
|
+
}
|
|
4473
|
+
});
|
|
4474
|
+
writeEnvFile(env);
|
|
4475
|
+
const restartRequired = Object.keys(variables).some((key) => {
|
|
4476
|
+
const envVar = ENV_VARIABLES.find((v) => v.key === key);
|
|
4477
|
+
return envVar?.restartRequired;
|
|
4478
|
+
});
|
|
4479
|
+
res.json({
|
|
4480
|
+
success: true,
|
|
4481
|
+
updated: Object.keys(variables).length,
|
|
4482
|
+
restartRequired
|
|
4483
|
+
});
|
|
4484
|
+
} catch (error) {
|
|
4485
|
+
res.status(500).json({
|
|
4486
|
+
error: "Failed to update environment variables",
|
|
4487
|
+
details: error instanceof Error ? error.message : String(error)
|
|
4488
|
+
});
|
|
4489
|
+
}
|
|
4490
|
+
});
|
|
4491
|
+
router.post("/api/env/test/:key", requireAuth, requireAdmin, async (req, res) => {
|
|
4492
|
+
try {
|
|
4493
|
+
const { key } = req.params;
|
|
4494
|
+
const { value } = req.body;
|
|
4495
|
+
const envVar = ENV_VARIABLES.find((v) => v.key === key);
|
|
4496
|
+
if (!envVar || !envVar.testable) {
|
|
4497
|
+
res.status(400).json({ error: "This variable cannot be tested" });
|
|
4498
|
+
return;
|
|
4499
|
+
}
|
|
4500
|
+
let testResult;
|
|
4501
|
+
switch (key) {
|
|
4502
|
+
case "OPENAI_API_KEY":
|
|
4503
|
+
testResult = await testOpenAIKey(value);
|
|
4504
|
+
break;
|
|
4505
|
+
case "ANTHROPIC_API_KEY":
|
|
4506
|
+
testResult = await testAnthropicKey(value);
|
|
4507
|
+
break;
|
|
4508
|
+
case "OLLAMA_BASE_URL":
|
|
4509
|
+
testResult = await testOllamaURL(value);
|
|
4510
|
+
break;
|
|
4511
|
+
case "GITHUB_TOKEN":
|
|
4512
|
+
testResult = await testGitHubToken(value);
|
|
4513
|
+
break;
|
|
4514
|
+
case "SAAS_URL":
|
|
4515
|
+
testResult = await testURL(value);
|
|
4516
|
+
break;
|
|
4517
|
+
case "SLACK_WEBHOOK_URL":
|
|
4518
|
+
testResult = await testSlackWebhook(value);
|
|
4519
|
+
break;
|
|
4520
|
+
case "DISCORD_WEBHOOK_URL":
|
|
4521
|
+
testResult = await testDiscordWebhook(value);
|
|
4522
|
+
break;
|
|
4523
|
+
default:
|
|
4524
|
+
testResult = { success: false, message: "Test not implemented for this variable" };
|
|
4525
|
+
}
|
|
4526
|
+
res.json(testResult);
|
|
4527
|
+
} catch (error) {
|
|
4528
|
+
res.status(500).json({
|
|
4529
|
+
success: false,
|
|
4530
|
+
message: error instanceof Error ? error.message : "Test failed"
|
|
4531
|
+
});
|
|
4532
|
+
}
|
|
4533
|
+
});
|
|
4534
|
+
router.post("/api/env/generate/:key", requireAuth, requireAdmin, (req, res) => {
|
|
4535
|
+
try {
|
|
4536
|
+
const { key } = req.params;
|
|
4537
|
+
let generated;
|
|
4538
|
+
switch (key) {
|
|
4539
|
+
case "OPENQA_JWT_SECRET":
|
|
4540
|
+
generated = Array.from(
|
|
4541
|
+
{ length: 64 },
|
|
4542
|
+
() => Math.floor(Math.random() * 16).toString(16)
|
|
4543
|
+
).join("");
|
|
4544
|
+
break;
|
|
4545
|
+
default:
|
|
4546
|
+
res.status(400).json({ error: "Generation not supported for this variable" });
|
|
4547
|
+
return;
|
|
4548
|
+
}
|
|
4549
|
+
res.json({ success: true, value: generated });
|
|
4550
|
+
} catch (error) {
|
|
4551
|
+
res.status(500).json({
|
|
4552
|
+
error: "Failed to generate value",
|
|
4553
|
+
details: error instanceof Error ? error.message : String(error)
|
|
4554
|
+
});
|
|
4555
|
+
}
|
|
4556
|
+
});
|
|
4557
|
+
return router;
|
|
4558
|
+
}
|
|
4559
|
+
async function testOpenAIKey(apiKey) {
|
|
4560
|
+
try {
|
|
4561
|
+
const response = await fetch("https://api.openai.com/v1/models", {
|
|
4562
|
+
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
4563
|
+
});
|
|
4564
|
+
if (response.ok) {
|
|
4565
|
+
return { success: true, message: "OpenAI API key is valid" };
|
|
4566
|
+
}
|
|
4567
|
+
return { success: false, message: "Invalid OpenAI API key" };
|
|
4568
|
+
} catch {
|
|
4569
|
+
return { success: false, message: "Failed to connect to OpenAI API" };
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
async function testAnthropicKey(apiKey) {
|
|
4573
|
+
try {
|
|
4574
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
4575
|
+
method: "POST",
|
|
4576
|
+
headers: {
|
|
4577
|
+
"x-api-key": apiKey,
|
|
4578
|
+
"anthropic-version": "2023-06-01",
|
|
4579
|
+
"content-type": "application/json"
|
|
4580
|
+
},
|
|
4581
|
+
body: JSON.stringify({
|
|
4582
|
+
model: "claude-3-haiku-20240307",
|
|
4583
|
+
max_tokens: 1,
|
|
4584
|
+
messages: [{ role: "user", content: "test" }]
|
|
4585
|
+
})
|
|
4586
|
+
});
|
|
4587
|
+
if (response.status === 200 || response.status === 400) {
|
|
4588
|
+
return { success: true, message: "Anthropic API key is valid" };
|
|
4589
|
+
}
|
|
4590
|
+
return { success: false, message: "Invalid Anthropic API key" };
|
|
4591
|
+
} catch {
|
|
4592
|
+
return { success: false, message: "Failed to connect to Anthropic API" };
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
async function testOllamaURL(url) {
|
|
4596
|
+
try {
|
|
4597
|
+
const response = await fetch(`${url}/api/tags`);
|
|
4598
|
+
if (response.ok) {
|
|
4599
|
+
return { success: true, message: "Ollama server is accessible" };
|
|
4600
|
+
}
|
|
4601
|
+
return { success: false, message: "Ollama server returned an error" };
|
|
4602
|
+
} catch {
|
|
4603
|
+
return { success: false, message: "Cannot connect to Ollama server" };
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
async function testGitHubToken(token) {
|
|
4607
|
+
try {
|
|
4608
|
+
const response = await fetch("https://api.github.com/user", {
|
|
4609
|
+
headers: { "Authorization": `token ${token}` }
|
|
4610
|
+
});
|
|
4611
|
+
if (response.ok) {
|
|
4612
|
+
const data = await response.json();
|
|
4613
|
+
return { success: true, message: `GitHub token is valid (user: ${data.login})` };
|
|
4614
|
+
}
|
|
4615
|
+
return { success: false, message: "Invalid GitHub token" };
|
|
4616
|
+
} catch {
|
|
4617
|
+
return { success: false, message: "Failed to connect to GitHub API" };
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
async function testURL(url) {
|
|
4621
|
+
try {
|
|
4622
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
4623
|
+
if (response.ok) {
|
|
4624
|
+
return { success: true, message: "URL is accessible" };
|
|
4625
|
+
}
|
|
4626
|
+
return { success: false, message: `URL returned status ${response.status}` };
|
|
4627
|
+
} catch {
|
|
4628
|
+
return { success: false, message: "Cannot connect to URL" };
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
async function testSlackWebhook(url) {
|
|
4632
|
+
try {
|
|
4633
|
+
const response = await fetch(url, {
|
|
4634
|
+
method: "POST",
|
|
4635
|
+
headers: { "Content-Type": "application/json" },
|
|
4636
|
+
body: JSON.stringify({ text: "OpenQA webhook test" })
|
|
4637
|
+
});
|
|
4638
|
+
if (response.ok) {
|
|
4639
|
+
return { success: true, message: "Slack webhook is valid" };
|
|
4640
|
+
}
|
|
4641
|
+
return { success: false, message: "Invalid Slack webhook" };
|
|
4642
|
+
} catch {
|
|
4643
|
+
return { success: false, message: "Failed to connect to Slack webhook" };
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
async function testDiscordWebhook(url) {
|
|
4647
|
+
try {
|
|
4648
|
+
const response = await fetch(url, {
|
|
4649
|
+
method: "POST",
|
|
4650
|
+
headers: { "Content-Type": "application/json" },
|
|
4651
|
+
body: JSON.stringify({ content: "OpenQA webhook test" })
|
|
4652
|
+
});
|
|
4653
|
+
if (response.ok || response.status === 204) {
|
|
4654
|
+
return { success: true, message: "Discord webhook is valid" };
|
|
4655
|
+
}
|
|
4656
|
+
return { success: false, message: "Invalid Discord webhook" };
|
|
4657
|
+
} catch {
|
|
4658
|
+
return { success: false, message: "Failed to connect to Discord webhook" };
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4121
4662
|
// cli/dashboard.html.ts
|
|
4122
4663
|
init_esm_shims();
|
|
4123
4664
|
function getDashboardHTML() {
|
|
@@ -4196,7 +4737,7 @@ function getDashboardHTML() {
|
|
|
4196
4737
|
.logo-mark {
|
|
4197
4738
|
width: 34px;
|
|
4198
4739
|
height: 34px;
|
|
4199
|
-
background:
|
|
4740
|
+
background: transparent;
|
|
4200
4741
|
border-radius: 8px;
|
|
4201
4742
|
display: grid;
|
|
4202
4743
|
place-items: center;
|
|
@@ -4818,7 +5359,9 @@ function getDashboardHTML() {
|
|
|
4818
5359
|
<!-- Sidebar -->
|
|
4819
5360
|
<aside>
|
|
4820
5361
|
<div class="logo">
|
|
4821
|
-
<div class="logo-mark"
|
|
5362
|
+
<div class="logo-mark">
|
|
5363
|
+
<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;">
|
|
5364
|
+
</div>
|
|
4822
5365
|
<div>
|
|
4823
5366
|
<div class="logo-name">OpenQA</div>
|
|
4824
5367
|
<div class="logo-version">v2.1.0 \xB7 OSS</div>
|
|
@@ -4828,39 +5371,69 @@ function getDashboardHTML() {
|
|
|
4828
5371
|
<div class="nav-section">
|
|
4829
5372
|
<div class="nav-label">Overview</div>
|
|
4830
5373
|
<a class="nav-item active" href="/">
|
|
4831
|
-
<span class="icon"
|
|
5374
|
+
<span class="icon">
|
|
5375
|
+
<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>
|
|
5376
|
+
</span> Dashboard
|
|
4832
5377
|
</a>
|
|
4833
5378
|
<a class="nav-item" href="/kanban">
|
|
4834
|
-
<span class="icon"
|
|
5379
|
+
<span class="icon">
|
|
5380
|
+
<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>
|
|
5381
|
+
</span> Kanban
|
|
4835
5382
|
<span class="badge" id="kanban-count">0</span>
|
|
4836
5383
|
</a>
|
|
4837
5384
|
|
|
4838
5385
|
<div class="nav-label">Agents</div>
|
|
4839
5386
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('agents-table')">
|
|
4840
|
-
<span class="icon"
|
|
5387
|
+
<span class="icon">
|
|
5388
|
+
<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>
|
|
5389
|
+
</span> Active Agents
|
|
4841
5390
|
</a>
|
|
4842
5391
|
<a class="nav-item" href="javascript:void(0)" onclick="switchAgentTab('specialists'); scrollToSection('agents-table')">
|
|
4843
|
-
<span class="icon"
|
|
5392
|
+
<span class="icon">
|
|
5393
|
+
<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>
|
|
5394
|
+
</span> Specialists
|
|
4844
5395
|
</a>
|
|
4845
5396
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('interventions-panel')">
|
|
4846
|
-
<span class="icon"
|
|
5397
|
+
<span class="icon">
|
|
5398
|
+
<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>
|
|
5399
|
+
</span> Interventions
|
|
4847
5400
|
<span class="badge" id="intervention-count" style="background: var(--red);">0</span>
|
|
4848
5401
|
</a>
|
|
4849
5402
|
|
|
4850
5403
|
<div class="nav-label">Analysis</div>
|
|
4851
5404
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('issues-panel')">
|
|
4852
|
-
<span class="icon"
|
|
5405
|
+
<span class="icon">
|
|
5406
|
+
<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"/>
|
|
5407
|
+
<path d="M14.12 3.88 16 2"/>
|
|
5408
|
+
<path d="M21 5a4 4 0 0 1-3.55 3.97"/>
|
|
5409
|
+
<path d="M3 21a4 4 0 0 1 3.81-4"/>
|
|
5410
|
+
<path d="M3 5a4 4 0 0 0 3.55 3.97"/>
|
|
5411
|
+
<path d="M6 13H2"/><path d="m8 2 1.88 1.88"/>
|
|
5412
|
+
<path d="M9 7.13V6a3 3 0 1 1 6 0v1.13"/>
|
|
5413
|
+
</svg>
|
|
5414
|
+
</span> Bug Reports
|
|
4853
5415
|
</a>
|
|
4854
5416
|
<a class="nav-item" href="javascript:void(0)" onclick="switchChartTab('performance'); scrollToSection('chart-performance')">
|
|
4855
|
-
<span class="icon"
|
|
5417
|
+
<span class="icon">
|
|
5418
|
+
<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>
|
|
5419
|
+
</span> Performance
|
|
4856
5420
|
</a>
|
|
4857
5421
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('activity-list')">
|
|
4858
|
-
<span class="icon"
|
|
5422
|
+
<span class="icon">
|
|
5423
|
+
<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>
|
|
5424
|
+
</span> Logs
|
|
4859
5425
|
</a>
|
|
4860
5426
|
|
|
4861
5427
|
<div class="nav-label">System</div>
|
|
4862
5428
|
<a class="nav-item" href="/config">
|
|
4863
|
-
<span class="icon"
|
|
5429
|
+
<span class="icon">
|
|
5430
|
+
<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>
|
|
5431
|
+
</span> Config
|
|
5432
|
+
</a>
|
|
5433
|
+
<a class="nav-item" href="/config/env">
|
|
5434
|
+
<span class="icon">
|
|
5435
|
+
<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>
|
|
5436
|
+
</span> Environment
|
|
4864
5437
|
</a>
|
|
4865
5438
|
</div>
|
|
4866
5439
|
|
|
@@ -4892,7 +5465,9 @@ function getDashboardHTML() {
|
|
|
4892
5465
|
<div class="metric-card">
|
|
4893
5466
|
<div class="metric-header">
|
|
4894
5467
|
<div class="metric-label">Active Agents</div>
|
|
4895
|
-
<div class="metric-icon"
|
|
5468
|
+
<div class="metric-icon">
|
|
5469
|
+
<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>
|
|
5470
|
+
</div>
|
|
4896
5471
|
</div>
|
|
4897
5472
|
<div class="metric-value" id="active-agents">0</div>
|
|
4898
5473
|
<div class="metric-change positive" id="agents-change">\u2191 0 from last hour</div>
|
|
@@ -4900,7 +5475,9 @@ function getDashboardHTML() {
|
|
|
4900
5475
|
<div class="metric-card">
|
|
4901
5476
|
<div class="metric-header">
|
|
4902
5477
|
<div class="metric-label">Total Actions</div>
|
|
4903
|
-
<div class="metric-icon"
|
|
5478
|
+
<div class="metric-icon">
|
|
5479
|
+
<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>
|
|
5480
|
+
</div>
|
|
4904
5481
|
</div>
|
|
4905
5482
|
<div class="metric-value" id="total-actions">0</div>
|
|
4906
5483
|
<div class="metric-change positive" id="actions-change">\u2191 0% this session</div>
|
|
@@ -4908,7 +5485,9 @@ function getDashboardHTML() {
|
|
|
4908
5485
|
<div class="metric-card">
|
|
4909
5486
|
<div class="metric-header">
|
|
4910
5487
|
<div class="metric-label">Bugs Found</div>
|
|
4911
|
-
<div class="metric-icon"
|
|
5488
|
+
<div class="metric-icon">
|
|
5489
|
+
<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>
|
|
5490
|
+
</div>
|
|
4912
5491
|
</div>
|
|
4913
5492
|
<div class="metric-value" id="bugs-found">0</div>
|
|
4914
5493
|
<div class="metric-change negative" id="bugs-change">\u2193 0 from yesterday</div>
|
|
@@ -4916,7 +5495,9 @@ function getDashboardHTML() {
|
|
|
4916
5495
|
<div class="metric-card">
|
|
4917
5496
|
<div class="metric-header">
|
|
4918
5497
|
<div class="metric-label">Success Rate</div>
|
|
4919
|
-
<div class="metric-icon"
|
|
5498
|
+
<div class="metric-icon">
|
|
5499
|
+
<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>
|
|
5500
|
+
</div>
|
|
4920
5501
|
</div>
|
|
4921
5502
|
<div class="metric-value" id="success-rate">\u2014</div>
|
|
4922
5503
|
<div class="metric-change positive" id="rate-change">\u2191 0 pts improvement</div>
|
|
@@ -7072,9 +7653,9 @@ function getSetupHTML() {
|
|
|
7072
7653
|
|
|
7073
7654
|
<form id="setupForm">
|
|
7074
7655
|
<div class="field">
|
|
7075
|
-
<label for="username">Username</label>
|
|
7076
|
-
<input type="text" id="username" name="username" autocomplete="username" autofocus required pattern="[a-z0-9_]+" title="Lowercase letters, digits and
|
|
7077
|
-
<div class="hint">
|
|
7656
|
+
<label for="username">Username or Email</label>
|
|
7657
|
+
<input type="text" id="username" name="username" autocomplete="username" autofocus required pattern="[a-z0-9_.@-]+" title="Lowercase letters, digits, and ._@- characters">
|
|
7658
|
+
<div class="hint">Use your email or a username (lowercase, digits, ._@- allowed)</div>
|
|
7078
7659
|
</div>
|
|
7079
7660
|
<div class="field">
|
|
7080
7661
|
<label for="password">Password</label>
|
|
@@ -7147,6 +7728,684 @@ function getSetupHTML() {
|
|
|
7147
7728
|
</html>`;
|
|
7148
7729
|
}
|
|
7149
7730
|
|
|
7731
|
+
// cli/env.html.ts
|
|
7732
|
+
init_esm_shims();
|
|
7733
|
+
function getEnvHTML() {
|
|
7734
|
+
return `<!DOCTYPE html>
|
|
7735
|
+
<html lang="en">
|
|
7736
|
+
<head>
|
|
7737
|
+
<meta charset="UTF-8">
|
|
7738
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7739
|
+
<title>Environment Variables - OpenQA</title>
|
|
7740
|
+
<style>
|
|
7741
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
7742
|
+
|
|
7743
|
+
body {
|
|
7744
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
7745
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
7746
|
+
min-height: 100vh;
|
|
7747
|
+
padding: 20px;
|
|
7748
|
+
}
|
|
7749
|
+
|
|
7750
|
+
.container {
|
|
7751
|
+
max-width: 1200px;
|
|
7752
|
+
margin: 0 auto;
|
|
7753
|
+
}
|
|
7754
|
+
|
|
7755
|
+
.header {
|
|
7756
|
+
background: rgba(255, 255, 255, 0.95);
|
|
7757
|
+
backdrop-filter: blur(10px);
|
|
7758
|
+
padding: 20px 30px;
|
|
7759
|
+
border-radius: 12px;
|
|
7760
|
+
margin-bottom: 20px;
|
|
7761
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
7762
|
+
display: flex;
|
|
7763
|
+
justify-content: space-between;
|
|
7764
|
+
align-items: center;
|
|
7765
|
+
}
|
|
7766
|
+
|
|
7767
|
+
.header h1 {
|
|
7768
|
+
font-size: 24px;
|
|
7769
|
+
color: #1a202c;
|
|
7770
|
+
display: flex;
|
|
7771
|
+
align-items: center;
|
|
7772
|
+
gap: 10px;
|
|
7773
|
+
}
|
|
7774
|
+
|
|
7775
|
+
.header-actions {
|
|
7776
|
+
display: flex;
|
|
7777
|
+
gap: 10px;
|
|
7778
|
+
}
|
|
7779
|
+
|
|
7780
|
+
.btn {
|
|
7781
|
+
padding: 10px 20px;
|
|
7782
|
+
border: none;
|
|
7783
|
+
border-radius: 8px;
|
|
7784
|
+
font-size: 14px;
|
|
7785
|
+
font-weight: 600;
|
|
7786
|
+
cursor: pointer;
|
|
7787
|
+
transition: all 0.2s;
|
|
7788
|
+
text-decoration: none;
|
|
7789
|
+
display: inline-flex;
|
|
7790
|
+
align-items: center;
|
|
7791
|
+
gap: 8px;
|
|
7792
|
+
}
|
|
7793
|
+
|
|
7794
|
+
.btn-primary {
|
|
7795
|
+
background: #667eea;
|
|
7796
|
+
color: white;
|
|
7797
|
+
}
|
|
7798
|
+
|
|
7799
|
+
.btn-primary:hover {
|
|
7800
|
+
background: #5568d3;
|
|
7801
|
+
transform: translateY(-1px);
|
|
7802
|
+
}
|
|
7803
|
+
|
|
7804
|
+
.btn-secondary {
|
|
7805
|
+
background: #e2e8f0;
|
|
7806
|
+
color: #4a5568;
|
|
7807
|
+
}
|
|
7808
|
+
|
|
7809
|
+
.btn-secondary:hover {
|
|
7810
|
+
background: #cbd5e0;
|
|
7811
|
+
}
|
|
7812
|
+
|
|
7813
|
+
.btn-success {
|
|
7814
|
+
background: #48bb78;
|
|
7815
|
+
color: white;
|
|
7816
|
+
}
|
|
7817
|
+
|
|
7818
|
+
.btn-success:hover {
|
|
7819
|
+
background: #38a169;
|
|
7820
|
+
}
|
|
7821
|
+
|
|
7822
|
+
.btn:disabled {
|
|
7823
|
+
opacity: 0.5;
|
|
7824
|
+
cursor: not-allowed;
|
|
7825
|
+
}
|
|
7826
|
+
|
|
7827
|
+
.content {
|
|
7828
|
+
background: rgba(255, 255, 255, 0.95);
|
|
7829
|
+
backdrop-filter: blur(10px);
|
|
7830
|
+
padding: 30px;
|
|
7831
|
+
border-radius: 12px;
|
|
7832
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
7833
|
+
}
|
|
7834
|
+
|
|
7835
|
+
.tabs {
|
|
7836
|
+
display: flex;
|
|
7837
|
+
gap: 10px;
|
|
7838
|
+
margin-bottom: 30px;
|
|
7839
|
+
border-bottom: 2px solid #e2e8f0;
|
|
7840
|
+
padding-bottom: 10px;
|
|
7841
|
+
}
|
|
7842
|
+
|
|
7843
|
+
.tab {
|
|
7844
|
+
padding: 10px 20px;
|
|
7845
|
+
border: none;
|
|
7846
|
+
background: none;
|
|
7847
|
+
font-size: 14px;
|
|
7848
|
+
font-weight: 600;
|
|
7849
|
+
color: #718096;
|
|
7850
|
+
cursor: pointer;
|
|
7851
|
+
border-bottom: 3px solid transparent;
|
|
7852
|
+
transition: all 0.2s;
|
|
7853
|
+
}
|
|
7854
|
+
|
|
7855
|
+
.tab.active {
|
|
7856
|
+
color: #667eea;
|
|
7857
|
+
border-bottom-color: #667eea;
|
|
7858
|
+
}
|
|
7859
|
+
|
|
7860
|
+
.tab:hover {
|
|
7861
|
+
color: #667eea;
|
|
7862
|
+
}
|
|
7863
|
+
|
|
7864
|
+
.category-section {
|
|
7865
|
+
display: none;
|
|
7866
|
+
}
|
|
7867
|
+
|
|
7868
|
+
.category-section.active {
|
|
7869
|
+
display: block;
|
|
7870
|
+
}
|
|
7871
|
+
|
|
7872
|
+
.category-header {
|
|
7873
|
+
display: flex;
|
|
7874
|
+
justify-content: space-between;
|
|
7875
|
+
align-items: center;
|
|
7876
|
+
margin-bottom: 20px;
|
|
7877
|
+
}
|
|
7878
|
+
|
|
7879
|
+
.category-title {
|
|
7880
|
+
font-size: 18px;
|
|
7881
|
+
font-weight: 600;
|
|
7882
|
+
color: #2d3748;
|
|
7883
|
+
}
|
|
7884
|
+
|
|
7885
|
+
.env-grid {
|
|
7886
|
+
display: grid;
|
|
7887
|
+
gap: 20px;
|
|
7888
|
+
}
|
|
7889
|
+
|
|
7890
|
+
.env-item {
|
|
7891
|
+
border: 1px solid #e2e8f0;
|
|
7892
|
+
border-radius: 8px;
|
|
7893
|
+
padding: 20px;
|
|
7894
|
+
transition: all 0.2s;
|
|
7895
|
+
}
|
|
7896
|
+
|
|
7897
|
+
.env-item:hover {
|
|
7898
|
+
border-color: #cbd5e0;
|
|
7899
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
7900
|
+
}
|
|
7901
|
+
|
|
7902
|
+
.env-item-header {
|
|
7903
|
+
display: flex;
|
|
7904
|
+
justify-content: space-between;
|
|
7905
|
+
align-items: flex-start;
|
|
7906
|
+
margin-bottom: 10px;
|
|
7907
|
+
}
|
|
7908
|
+
|
|
7909
|
+
.env-label {
|
|
7910
|
+
font-weight: 600;
|
|
7911
|
+
color: #2d3748;
|
|
7912
|
+
font-size: 14px;
|
|
7913
|
+
display: flex;
|
|
7914
|
+
align-items: center;
|
|
7915
|
+
gap: 8px;
|
|
7916
|
+
}
|
|
7917
|
+
|
|
7918
|
+
.required-badge {
|
|
7919
|
+
background: #fc8181;
|
|
7920
|
+
color: white;
|
|
7921
|
+
font-size: 10px;
|
|
7922
|
+
padding: 2px 6px;
|
|
7923
|
+
border-radius: 4px;
|
|
7924
|
+
font-weight: 700;
|
|
7925
|
+
}
|
|
7926
|
+
|
|
7927
|
+
.env-description {
|
|
7928
|
+
font-size: 13px;
|
|
7929
|
+
color: #718096;
|
|
7930
|
+
margin-bottom: 10px;
|
|
7931
|
+
}
|
|
7932
|
+
|
|
7933
|
+
.env-input-group {
|
|
7934
|
+
display: flex;
|
|
7935
|
+
gap: 10px;
|
|
7936
|
+
align-items: center;
|
|
7937
|
+
}
|
|
7938
|
+
|
|
7939
|
+
.env-input {
|
|
7940
|
+
flex: 1;
|
|
7941
|
+
padding: 10px 12px;
|
|
7942
|
+
border: 1px solid #e2e8f0;
|
|
7943
|
+
border-radius: 6px;
|
|
7944
|
+
font-size: 14px;
|
|
7945
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
7946
|
+
transition: all 0.2s;
|
|
7947
|
+
}
|
|
7948
|
+
|
|
7949
|
+
.env-input:focus {
|
|
7950
|
+
outline: none;
|
|
7951
|
+
border-color: #667eea;
|
|
7952
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
7953
|
+
}
|
|
7954
|
+
|
|
7955
|
+
.env-input.error {
|
|
7956
|
+
border-color: #fc8181;
|
|
7957
|
+
}
|
|
7958
|
+
|
|
7959
|
+
.env-actions {
|
|
7960
|
+
display: flex;
|
|
7961
|
+
gap: 5px;
|
|
7962
|
+
}
|
|
7963
|
+
|
|
7964
|
+
.icon-btn {
|
|
7965
|
+
padding: 8px;
|
|
7966
|
+
border: none;
|
|
7967
|
+
background: #e2e8f0;
|
|
7968
|
+
border-radius: 6px;
|
|
7969
|
+
cursor: pointer;
|
|
7970
|
+
transition: all 0.2s;
|
|
7971
|
+
font-size: 16px;
|
|
7972
|
+
}
|
|
7973
|
+
|
|
7974
|
+
.icon-btn:hover {
|
|
7975
|
+
background: #cbd5e0;
|
|
7976
|
+
}
|
|
7977
|
+
|
|
7978
|
+
.icon-btn.test {
|
|
7979
|
+
background: #bee3f8;
|
|
7980
|
+
color: #2c5282;
|
|
7981
|
+
}
|
|
7982
|
+
|
|
7983
|
+
.icon-btn.test:hover {
|
|
7984
|
+
background: #90cdf4;
|
|
7985
|
+
}
|
|
7986
|
+
|
|
7987
|
+
.icon-btn.generate {
|
|
7988
|
+
background: #c6f6d5;
|
|
7989
|
+
color: #22543d;
|
|
7990
|
+
}
|
|
7991
|
+
|
|
7992
|
+
.icon-btn.generate:hover {
|
|
7993
|
+
background: #9ae6b4;
|
|
7994
|
+
}
|
|
7995
|
+
|
|
7996
|
+
.error-message {
|
|
7997
|
+
color: #e53e3e;
|
|
7998
|
+
font-size: 12px;
|
|
7999
|
+
margin-top: 5px;
|
|
8000
|
+
}
|
|
8001
|
+
|
|
8002
|
+
.success-message {
|
|
8003
|
+
color: #38a169;
|
|
8004
|
+
font-size: 12px;
|
|
8005
|
+
margin-top: 5px;
|
|
8006
|
+
}
|
|
8007
|
+
|
|
8008
|
+
.alert {
|
|
8009
|
+
padding: 15px 20px;
|
|
8010
|
+
border-radius: 8px;
|
|
8011
|
+
margin-bottom: 20px;
|
|
8012
|
+
display: flex;
|
|
8013
|
+
align-items: center;
|
|
8014
|
+
gap: 10px;
|
|
8015
|
+
}
|
|
8016
|
+
|
|
8017
|
+
.alert-warning {
|
|
8018
|
+
background: #fef5e7;
|
|
8019
|
+
border-left: 4px solid #f59e0b;
|
|
8020
|
+
color: #92400e;
|
|
8021
|
+
}
|
|
8022
|
+
|
|
8023
|
+
.alert-info {
|
|
8024
|
+
background: #eff6ff;
|
|
8025
|
+
border-left: 4px solid #3b82f6;
|
|
8026
|
+
color: #1e40af;
|
|
8027
|
+
}
|
|
8028
|
+
|
|
8029
|
+
.alert-success {
|
|
8030
|
+
background: #f0fdf4;
|
|
8031
|
+
border-left: 4px solid #10b981;
|
|
8032
|
+
color: #065f46;
|
|
8033
|
+
}
|
|
8034
|
+
|
|
8035
|
+
.loading {
|
|
8036
|
+
text-align: center;
|
|
8037
|
+
padding: 40px;
|
|
8038
|
+
color: #718096;
|
|
8039
|
+
}
|
|
8040
|
+
|
|
8041
|
+
.spinner {
|
|
8042
|
+
border: 3px solid #e2e8f0;
|
|
8043
|
+
border-top: 3px solid #667eea;
|
|
8044
|
+
border-radius: 50%;
|
|
8045
|
+
width: 40px;
|
|
8046
|
+
height: 40px;
|
|
8047
|
+
animation: spin 1s linear infinite;
|
|
8048
|
+
margin: 0 auto 20px;
|
|
8049
|
+
}
|
|
8050
|
+
|
|
8051
|
+
@keyframes spin {
|
|
8052
|
+
0% { transform: rotate(0deg); }
|
|
8053
|
+
100% { transform: rotate(360deg); }
|
|
8054
|
+
}
|
|
8055
|
+
|
|
8056
|
+
.modal {
|
|
8057
|
+
display: none;
|
|
8058
|
+
position: fixed;
|
|
8059
|
+
top: 0;
|
|
8060
|
+
left: 0;
|
|
8061
|
+
right: 0;
|
|
8062
|
+
bottom: 0;
|
|
8063
|
+
background: rgba(0, 0, 0, 0.5);
|
|
8064
|
+
z-index: 1000;
|
|
8065
|
+
align-items: center;
|
|
8066
|
+
justify-content: center;
|
|
8067
|
+
}
|
|
8068
|
+
|
|
8069
|
+
.modal.show {
|
|
8070
|
+
display: flex;
|
|
8071
|
+
}
|
|
8072
|
+
|
|
8073
|
+
.modal-content {
|
|
8074
|
+
background: white;
|
|
8075
|
+
padding: 30px;
|
|
8076
|
+
border-radius: 12px;
|
|
8077
|
+
max-width: 500px;
|
|
8078
|
+
width: 90%;
|
|
8079
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
8080
|
+
}
|
|
8081
|
+
|
|
8082
|
+
.modal-header {
|
|
8083
|
+
font-size: 20px;
|
|
8084
|
+
font-weight: 600;
|
|
8085
|
+
margin-bottom: 15px;
|
|
8086
|
+
color: #2d3748;
|
|
8087
|
+
}
|
|
8088
|
+
|
|
8089
|
+
.modal-body {
|
|
8090
|
+
margin-bottom: 20px;
|
|
8091
|
+
color: #4a5568;
|
|
8092
|
+
}
|
|
8093
|
+
|
|
8094
|
+
.modal-footer {
|
|
8095
|
+
display: flex;
|
|
8096
|
+
gap: 10px;
|
|
8097
|
+
justify-content: flex-end;
|
|
8098
|
+
}
|
|
8099
|
+
</style>
|
|
8100
|
+
</head>
|
|
8101
|
+
<body>
|
|
8102
|
+
<div class="container">
|
|
8103
|
+
<div class="header">
|
|
8104
|
+
<h1>
|
|
8105
|
+
<span>\u2699\uFE0F</span>
|
|
8106
|
+
Environment Variables
|
|
8107
|
+
</h1>
|
|
8108
|
+
<div class="header-actions">
|
|
8109
|
+
<a href="/config" class="btn btn-secondary">\u2190 Back to Config</a>
|
|
8110
|
+
<button id="saveBtn" class="btn btn-success" disabled>\u{1F4BE} Save Changes</button>
|
|
8111
|
+
</div>
|
|
8112
|
+
</div>
|
|
8113
|
+
|
|
8114
|
+
<div class="content">
|
|
8115
|
+
<div id="loading" class="loading">
|
|
8116
|
+
<div class="spinner"></div>
|
|
8117
|
+
<div>Loading environment variables...</div>
|
|
8118
|
+
</div>
|
|
8119
|
+
|
|
8120
|
+
<div id="main" style="display: none;">
|
|
8121
|
+
<div id="alerts"></div>
|
|
8122
|
+
|
|
8123
|
+
<div class="tabs">
|
|
8124
|
+
<button class="tab active" data-category="llm">\u{1F916} LLM</button>
|
|
8125
|
+
<button class="tab" data-category="security">\u{1F512} Security</button>
|
|
8126
|
+
<button class="tab" data-category="target">\u{1F3AF} Target App</button>
|
|
8127
|
+
<button class="tab" data-category="github">\u{1F419} GitHub</button>
|
|
8128
|
+
<button class="tab" data-category="web">\u{1F310} Web Server</button>
|
|
8129
|
+
<button class="tab" data-category="agent">\u{1F916} Agent</button>
|
|
8130
|
+
<button class="tab" data-category="database">\u{1F4BE} Database</button>
|
|
8131
|
+
<button class="tab" data-category="notifications">\u{1F514} Notifications</button>
|
|
8132
|
+
</div>
|
|
8133
|
+
|
|
8134
|
+
<div id="categories"></div>
|
|
8135
|
+
</div>
|
|
8136
|
+
</div>
|
|
8137
|
+
</div>
|
|
8138
|
+
|
|
8139
|
+
<!-- Test Result Modal -->
|
|
8140
|
+
<div id="testModal" class="modal">
|
|
8141
|
+
<div class="modal-content">
|
|
8142
|
+
<div class="modal-header">Test Result</div>
|
|
8143
|
+
<div class="modal-body" id="testResult"></div>
|
|
8144
|
+
<div class="modal-footer">
|
|
8145
|
+
<button class="btn btn-secondary" onclick="closeTestModal()">Close</button>
|
|
8146
|
+
</div>
|
|
8147
|
+
</div>
|
|
8148
|
+
</div>
|
|
8149
|
+
|
|
8150
|
+
<script>
|
|
8151
|
+
let envVariables = [];
|
|
8152
|
+
let changedVariables = {};
|
|
8153
|
+
let restartRequired = false;
|
|
8154
|
+
|
|
8155
|
+
// Load environment variables
|
|
8156
|
+
async function loadEnvVariables() {
|
|
8157
|
+
try {
|
|
8158
|
+
const response = await fetch('/api/env');
|
|
8159
|
+
if (!response.ok) throw new Error('Failed to load variables');
|
|
8160
|
+
|
|
8161
|
+
const data = await response.json();
|
|
8162
|
+
envVariables = data.variables;
|
|
8163
|
+
|
|
8164
|
+
renderCategories();
|
|
8165
|
+
document.getElementById('loading').style.display = 'none';
|
|
8166
|
+
document.getElementById('main').style.display = 'block';
|
|
8167
|
+
} catch (error) {
|
|
8168
|
+
showAlert('error', 'Failed to load environment variables: ' + error.message);
|
|
8169
|
+
}
|
|
8170
|
+
}
|
|
8171
|
+
|
|
8172
|
+
// Render categories
|
|
8173
|
+
function renderCategories() {
|
|
8174
|
+
const container = document.getElementById('categories');
|
|
8175
|
+
const categories = [...new Set(envVariables.map(v => v.category))];
|
|
8176
|
+
|
|
8177
|
+
categories.forEach((category, index) => {
|
|
8178
|
+
const section = document.createElement('div');
|
|
8179
|
+
section.className = 'category-section' + (index === 0 ? ' active' : '');
|
|
8180
|
+
section.dataset.category = category;
|
|
8181
|
+
|
|
8182
|
+
const vars = envVariables.filter(v => v.category === category);
|
|
8183
|
+
|
|
8184
|
+
section.innerHTML = \`
|
|
8185
|
+
<div class="category-header">
|
|
8186
|
+
<div class="category-title">\${getCategoryTitle(category)}</div>
|
|
8187
|
+
</div>
|
|
8188
|
+
<div class="env-grid">
|
|
8189
|
+
\${vars.map(v => renderEnvItem(v)).join('')}
|
|
8190
|
+
</div>
|
|
8191
|
+
\`;
|
|
8192
|
+
|
|
8193
|
+
container.appendChild(section);
|
|
8194
|
+
});
|
|
8195
|
+
}
|
|
8196
|
+
|
|
8197
|
+
// Render single env item
|
|
8198
|
+
function renderEnvItem(envVar) {
|
|
8199
|
+
const inputType = envVar.type === 'password' ? 'password' : 'text';
|
|
8200
|
+
const value = envVar.displayValue || '';
|
|
8201
|
+
|
|
8202
|
+
return \`
|
|
8203
|
+
<div class="env-item" data-key="\${envVar.key}">
|
|
8204
|
+
<div class="env-item-header">
|
|
8205
|
+
<div class="env-label">
|
|
8206
|
+
\${envVar.key}
|
|
8207
|
+
\${envVar.required ? '<span class="required-badge">REQUIRED</span>' : ''}
|
|
8208
|
+
</div>
|
|
8209
|
+
</div>
|
|
8210
|
+
<div class="env-description">\${envVar.description}</div>
|
|
8211
|
+
<div class="env-input-group">
|
|
8212
|
+
\${envVar.type === 'select' ?
|
|
8213
|
+
\`<select class="env-input" data-key="\${envVar.key}" onchange="handleChange(this)">
|
|
8214
|
+
<option value="">-- Select --</option>
|
|
8215
|
+
\${envVar.options.map(opt =>
|
|
8216
|
+
\`<option value="\${opt}" \${value === opt ? 'selected' : ''}>\${opt}</option>\`
|
|
8217
|
+
).join('')}
|
|
8218
|
+
</select>\` :
|
|
8219
|
+
envVar.type === 'boolean' ?
|
|
8220
|
+
\`<select class="env-input" data-key="\${envVar.key}" onchange="handleChange(this)">
|
|
8221
|
+
<option value="">-- Select --</option>
|
|
8222
|
+
<option value="true" \${value === 'true' ? 'selected' : ''}>true</option>
|
|
8223
|
+
<option value="false" \${value === 'false' ? 'selected' : ''}>false</option>
|
|
8224
|
+
</select>\` :
|
|
8225
|
+
\`<input
|
|
8226
|
+
type="\${inputType}"
|
|
8227
|
+
class="env-input"
|
|
8228
|
+
data-key="\${envVar.key}"
|
|
8229
|
+
value="\${value}"
|
|
8230
|
+
placeholder="\${envVar.placeholder || ''}"
|
|
8231
|
+
onchange="handleChange(this)"
|
|
8232
|
+
/>\`
|
|
8233
|
+
}
|
|
8234
|
+
<div class="env-actions">
|
|
8235
|
+
\${envVar.testable ? \`<button class="icon-btn test" onclick="testVariable('\${envVar.key}')" title="Test">\u{1F9EA}</button>\` : ''}
|
|
8236
|
+
\${envVar.key === 'OPENQA_JWT_SECRET' ? \`<button class="icon-btn generate" onclick="generateSecret('\${envVar.key}')" title="Generate">\u{1F511}</button>\` : ''}
|
|
8237
|
+
</div>
|
|
8238
|
+
</div>
|
|
8239
|
+
<div class="error-message" id="error-\${envVar.key}"></div>
|
|
8240
|
+
<div class="success-message" id="success-\${envVar.key}"></div>
|
|
8241
|
+
</div>
|
|
8242
|
+
\`;
|
|
8243
|
+
}
|
|
8244
|
+
|
|
8245
|
+
// Handle input change
|
|
8246
|
+
function handleChange(input) {
|
|
8247
|
+
const key = input.dataset.key;
|
|
8248
|
+
const value = input.value;
|
|
8249
|
+
|
|
8250
|
+
changedVariables[key] = value;
|
|
8251
|
+
document.getElementById('saveBtn').disabled = false;
|
|
8252
|
+
|
|
8253
|
+
// Clear messages
|
|
8254
|
+
document.getElementById(\`error-\${key}\`).textContent = '';
|
|
8255
|
+
document.getElementById(\`success-\${key}\`).textContent = '';
|
|
8256
|
+
}
|
|
8257
|
+
|
|
8258
|
+
// Save changes
|
|
8259
|
+
async function saveChanges() {
|
|
8260
|
+
const saveBtn = document.getElementById('saveBtn');
|
|
8261
|
+
saveBtn.disabled = true;
|
|
8262
|
+
saveBtn.textContent = '\u{1F4BE} Saving...';
|
|
8263
|
+
|
|
8264
|
+
try {
|
|
8265
|
+
const response = await fetch('/api/env/bulk', {
|
|
8266
|
+
method: 'POST',
|
|
8267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8268
|
+
body: JSON.stringify({ variables: changedVariables }),
|
|
8269
|
+
});
|
|
8270
|
+
|
|
8271
|
+
if (!response.ok) {
|
|
8272
|
+
const error = await response.json();
|
|
8273
|
+
throw new Error(error.error || 'Failed to save');
|
|
8274
|
+
}
|
|
8275
|
+
|
|
8276
|
+
const result = await response.json();
|
|
8277
|
+
restartRequired = result.restartRequired;
|
|
8278
|
+
|
|
8279
|
+
showAlert('success', \`\u2705 Saved \${result.updated} variable(s) successfully!\` +
|
|
8280
|
+
(restartRequired ? ' \u26A0\uFE0F Restart required for changes to take effect.' : ''));
|
|
8281
|
+
|
|
8282
|
+
changedVariables = {};
|
|
8283
|
+
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
8284
|
+
|
|
8285
|
+
// Reload to show updated values
|
|
8286
|
+
setTimeout(() => location.reload(), 2000);
|
|
8287
|
+
} catch (error) {
|
|
8288
|
+
showAlert('error', 'Failed to save: ' + error.message);
|
|
8289
|
+
saveBtn.disabled = false;
|
|
8290
|
+
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
|
|
8294
|
+
// Test variable
|
|
8295
|
+
async function testVariable(key) {
|
|
8296
|
+
const input = document.querySelector(\`[data-key="\${key}"]\`);
|
|
8297
|
+
const value = input.value;
|
|
8298
|
+
|
|
8299
|
+
if (!value) {
|
|
8300
|
+
showAlert('warning', 'Please enter a value first');
|
|
8301
|
+
return;
|
|
8302
|
+
}
|
|
8303
|
+
|
|
8304
|
+
try {
|
|
8305
|
+
const response = await fetch(\`/api/env/test/\${key}\`, {
|
|
8306
|
+
method: 'POST',
|
|
8307
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8308
|
+
body: JSON.stringify({ value }),
|
|
8309
|
+
});
|
|
8310
|
+
|
|
8311
|
+
const result = await response.json();
|
|
8312
|
+
showTestResult(result);
|
|
8313
|
+
} catch (error) {
|
|
8314
|
+
showTestResult({ success: false, message: 'Test failed: ' + error.message });
|
|
8315
|
+
}
|
|
8316
|
+
}
|
|
8317
|
+
|
|
8318
|
+
// Generate secret
|
|
8319
|
+
async function generateSecret(key) {
|
|
8320
|
+
try {
|
|
8321
|
+
const response = await fetch(\`/api/env/generate/\${key}\`, {
|
|
8322
|
+
method: 'POST',
|
|
8323
|
+
});
|
|
8324
|
+
|
|
8325
|
+
if (!response.ok) throw new Error('Failed to generate');
|
|
8326
|
+
|
|
8327
|
+
const result = await response.json();
|
|
8328
|
+
const input = document.querySelector(\`[data-key="\${key}"]\`);
|
|
8329
|
+
input.value = result.value;
|
|
8330
|
+
handleChange(input);
|
|
8331
|
+
|
|
8332
|
+
document.getElementById(\`success-\${key}\`).textContent = '\u2705 Secret generated!';
|
|
8333
|
+
} catch (error) {
|
|
8334
|
+
document.getElementById(\`error-\${key}\`).textContent = 'Failed to generate: ' + error.message;
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
8337
|
+
|
|
8338
|
+
// Show test result
|
|
8339
|
+
function showTestResult(result) {
|
|
8340
|
+
const modal = document.getElementById('testModal');
|
|
8341
|
+
const resultDiv = document.getElementById('testResult');
|
|
8342
|
+
|
|
8343
|
+
resultDiv.innerHTML = \`
|
|
8344
|
+
<div class="alert \${result.success ? 'alert-success' : 'alert-warning'}">
|
|
8345
|
+
\${result.success ? '\u2705' : '\u274C'} \${result.message}
|
|
8346
|
+
</div>
|
|
8347
|
+
\`;
|
|
8348
|
+
|
|
8349
|
+
modal.classList.add('show');
|
|
8350
|
+
}
|
|
8351
|
+
|
|
8352
|
+
function closeTestModal() {
|
|
8353
|
+
document.getElementById('testModal').classList.remove('show');
|
|
8354
|
+
}
|
|
8355
|
+
|
|
8356
|
+
// Show alert
|
|
8357
|
+
function showAlert(type, message) {
|
|
8358
|
+
const alerts = document.getElementById('alerts');
|
|
8359
|
+
const alertClass = type === 'error' ? 'alert-warning' :
|
|
8360
|
+
type === 'success' ? 'alert-success' : 'alert-info';
|
|
8361
|
+
|
|
8362
|
+
alerts.innerHTML = \`
|
|
8363
|
+
<div class="alert \${alertClass}">
|
|
8364
|
+
\${message}
|
|
8365
|
+
</div>
|
|
8366
|
+
\`;
|
|
8367
|
+
|
|
8368
|
+
setTimeout(() => alerts.innerHTML = '', 5000);
|
|
8369
|
+
}
|
|
8370
|
+
|
|
8371
|
+
// Get category title
|
|
8372
|
+
function getCategoryTitle(category) {
|
|
8373
|
+
const titles = {
|
|
8374
|
+
llm: '\u{1F916} LLM Configuration',
|
|
8375
|
+
security: '\u{1F512} Security Settings',
|
|
8376
|
+
target: '\u{1F3AF} Target Application',
|
|
8377
|
+
github: '\u{1F419} GitHub Integration',
|
|
8378
|
+
web: '\u{1F310} Web Server',
|
|
8379
|
+
agent: '\u{1F916} Agent Configuration',
|
|
8380
|
+
database: '\u{1F4BE} Database',
|
|
8381
|
+
notifications: '\u{1F514} Notifications',
|
|
8382
|
+
};
|
|
8383
|
+
return titles[category] || category;
|
|
8384
|
+
}
|
|
8385
|
+
|
|
8386
|
+
// Tab switching
|
|
8387
|
+
document.addEventListener('click', (e) => {
|
|
8388
|
+
if (e.target.classList.contains('tab')) {
|
|
8389
|
+
const category = e.target.dataset.category;
|
|
8390
|
+
|
|
8391
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
8392
|
+
e.target.classList.add('active');
|
|
8393
|
+
|
|
8394
|
+
document.querySelectorAll('.category-section').forEach(s => s.classList.remove('active'));
|
|
8395
|
+
document.querySelector(\`[data-category="\${category}"]\`).classList.add('active');
|
|
8396
|
+
}
|
|
8397
|
+
});
|
|
8398
|
+
|
|
8399
|
+
// Save button
|
|
8400
|
+
document.getElementById('saveBtn').addEventListener('click', saveChanges);
|
|
8401
|
+
|
|
8402
|
+
// Load on page load
|
|
8403
|
+
loadEnvVariables();
|
|
8404
|
+
</script>
|
|
8405
|
+
</body>
|
|
8406
|
+
</html>`;
|
|
8407
|
+
}
|
|
8408
|
+
|
|
7150
8409
|
// cli/daemon.ts
|
|
7151
8410
|
import rateLimit from "express-rate-limit";
|
|
7152
8411
|
import cors from "cors";
|
|
@@ -7238,6 +8497,7 @@ app.use(["/api/agent/start", "/api/project/setup", "/api/project/test", "/api/br
|
|
|
7238
8497
|
var wss = new WebSocketServer({ noServer: true });
|
|
7239
8498
|
var agent = null;
|
|
7240
8499
|
app.use(createAuthRouter(db));
|
|
8500
|
+
app.use(createEnvRouter());
|
|
7241
8501
|
app.use("/api", (req, res, next) => {
|
|
7242
8502
|
const PUBLIC_PATHS = ["/auth/login", "/auth/logout", "/setup"];
|
|
7243
8503
|
if (PUBLIC_PATHS.some((p) => req.path === p || req.path.startsWith(p + "/"))) return next();
|
|
@@ -7261,6 +8521,9 @@ app.get("/", authOrRedirect(db), (_req, res) => {
|
|
|
7261
8521
|
app.get("/config", authOrRedirect(db), (_req, res) => {
|
|
7262
8522
|
res.send(getConfigHTML(cfg));
|
|
7263
8523
|
});
|
|
8524
|
+
app.get("/config/env", authOrRedirect(db), (_req, res) => {
|
|
8525
|
+
res.send(getEnvHTML());
|
|
8526
|
+
});
|
|
7264
8527
|
app.get("/kanban", authOrRedirect(db), (_req, res) => {
|
|
7265
8528
|
res.send(getKanbanHTML());
|
|
7266
8529
|
});
|
|
@@ -7560,13 +8823,3 @@ process.on("SIGINT", () => {
|
|
|
7560
8823
|
process.exit(0);
|
|
7561
8824
|
});
|
|
7562
8825
|
});
|
|
7563
|
-
/*! Bundled license information:
|
|
7564
|
-
|
|
7565
|
-
cookie/index.js:
|
|
7566
|
-
(*!
|
|
7567
|
-
* cookie
|
|
7568
|
-
* Copyright(c) 2012-2014 Roman Shtylman
|
|
7569
|
-
* Copyright(c) 2015 Douglas Christopher Wilson
|
|
7570
|
-
* MIT Licensed
|
|
7571
|
-
*)
|
|
7572
|
-
*/
|