@openqa/cli 1.3.2 → 1.3.3

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/cli/index.js CHANGED
@@ -363,15 +363,50 @@ async function startWebServer() {
363
363
  res.status(500).json({ success: false, error: error.message });
364
364
  }
365
365
  });
366
- app.post("/api/intervention/:id", (req, res) => {
366
+ app.post("/api/intervention/:id", async (req, res) => {
367
367
  const { id } = req.params;
368
368
  const { response } = req.body;
369
- console.log(`Intervention ${id}: ${response}`);
370
- broadcast({
371
- type: "intervention-response",
372
- data: { id, response, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
373
- });
374
- res.json({ success: true });
369
+ console.log(`Intervention ${id} ${response} by user`);
370
+ res.json({ success: true, message: `Intervention ${response}d` });
371
+ });
372
+ app.post("/api/test-connection", async (req, res) => {
373
+ try {
374
+ const cfg2 = config.getConfigSync();
375
+ if (cfg2.saas.url) {
376
+ const controller = new AbortController();
377
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
378
+ try {
379
+ const response = await fetch(cfg2.saas.url, {
380
+ method: "HEAD",
381
+ signal: controller.signal,
382
+ headers: cfg2.saas.authType === "basic" && cfg2.saas.username && cfg2.saas.password ? {
383
+ "Authorization": "Basic " + Buffer.from(`${cfg2.saas.username}:${cfg2.saas.password}`).toString("base64")
384
+ } : {}
385
+ });
386
+ clearTimeout(timeoutId);
387
+ if (response.ok) {
388
+ res.json({ success: true, message: "SaaS connection successful" });
389
+ } else {
390
+ res.json({ success: false, message: "SaaS connection failed" });
391
+ }
392
+ } catch (fetchError) {
393
+ clearTimeout(timeoutId);
394
+ throw fetchError;
395
+ }
396
+ } else {
397
+ res.json({ success: false, message: "No SaaS URL configured" });
398
+ }
399
+ } catch (error) {
400
+ res.json({ success: false, message: "Connection error: " + (error.message || String(error)) });
401
+ }
402
+ });
403
+ app.post("/api/start", async (req, res) => {
404
+ try {
405
+ console.log("Starting agent session...");
406
+ res.json({ success: true, message: "Session started" });
407
+ } catch (error) {
408
+ res.json({ success: false, message: "Failed to start session: " + (error.message || String(error)) });
409
+ }
375
410
  });
376
411
  app.get("/api/tasks", async (req, res) => {
377
412
  const tasks = [
@@ -1626,228 +1661,720 @@ async function startWebServer() {
1626
1661
  `);
1627
1662
  });
1628
1663
  app.get("/config", (req, res) => {
1664
+ const cfg2 = config.getConfigSync();
1629
1665
  res.send(`
1630
- <!DOCTYPE html>
1631
- <html>
1632
- <head>
1633
- <title>OpenQA - Configuration</title>
1634
- <style>
1635
- body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
1636
- h1 { color: #38bdf8; }
1637
- nav { margin: 20px 0; }
1638
- nav a { color: #38bdf8; text-decoration: none; margin-right: 20px; }
1639
- nav a:hover { text-decoration: underline; }
1640
- .section { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
1641
- .section h2 { margin-top: 0; color: #38bdf8; font-size: 18px; }
1642
- .config-item { margin: 15px 0; }
1643
- .config-item label { display: block; margin-bottom: 5px; color: #94a3b8; font-size: 14px; }
1644
- .config-item input, .config-item select {
1645
- background: #334155;
1646
- border: 1px solid #475569;
1647
- color: #e2e8f0;
1648
- padding: 8px 12px;
1649
- border-radius: 4px;
1650
- font-family: monospace;
1651
- font-size: 14px;
1652
- width: 100%;
1653
- max-width: 400px;
1654
- }
1655
- .config-item input:focus, .config-item select:focus {
1656
- outline: none;
1657
- border-color: #38bdf8;
1658
- box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.1);
1659
- }
1660
- .btn {
1661
- background: #38bdf8;
1662
- color: white;
1663
- border: none;
1664
- padding: 10px 20px;
1665
- border-radius: 6px;
1666
- cursor: pointer;
1667
- font-size: 14px;
1668
- margin-right: 10px;
1669
- }
1670
- .btn:hover { background: #0ea5e9; }
1671
- .btn-secondary { background: #64748b; }
1672
- .btn-secondary:hover { background: #475569; }
1673
- .success { color: #10b981; margin-left: 10px; }
1674
- .error { color: #ef4444; margin-left: 10px; }
1675
- code { background: #334155; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
1676
- .checkbox { margin-right: 8px; }
1677
- </style>
1678
- </head>
1679
- <body>
1680
- <h1>\u2699\uFE0F Configuration</h1>
1681
- <nav>
1682
- <a href="/">Dashboard</a>
1683
- <a href="/kanban">Kanban</a>
1684
- <a href="/config">Config</a>
1685
- </nav>
1686
-
1687
- <div class="section">
1688
- <h2>SaaS Target</h2>
1689
- <form id="configForm">
1690
- <div class="config-item">
1691
- <label>URL</label>
1692
- <input type="url" id="saas_url" name="saas.url" value="${cfg.saas.url || ""}" placeholder="https://your-app.com">
1693
- </div>
1694
- <div class="config-item">
1695
- <label>Auth Type</label>
1696
- <select id="saas_authType" name="saas.authType">
1697
- <option value="none" ${cfg.saas.authType === "none" ? "selected" : ""}>None</option>
1698
- <option value="basic" ${cfg.saas.authType === "basic" ? "selected" : ""}>Basic Auth</option>
1699
- <option value="bearer" ${cfg.saas.authType === "bearer" ? "selected" : ""}>Bearer Token</option>
1700
- <option value="session" ${cfg.saas.authType === "session" ? "selected" : ""}>Session</option>
1701
- </select>
1666
+ <!DOCTYPE html>
1667
+ <html lang="en">
1668
+ <head>
1669
+ <meta charset="UTF-8">
1670
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1671
+ <title>OpenQA \u2014 Configuration</title>
1672
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1673
+ <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">
1674
+ <style>
1675
+ :root {
1676
+ --bg: #080b10;
1677
+ --surface: #0d1117;
1678
+ --panel: #111720;
1679
+ --border: rgba(255,255,255,0.06);
1680
+ --border-hi: rgba(255,255,255,0.12);
1681
+ --accent: #f97316;
1682
+ --accent-lo: rgba(249,115,22,0.08);
1683
+ --accent-md: rgba(249,115,22,0.18);
1684
+ --green: #22c55e;
1685
+ --green-lo: rgba(34,197,94,0.08);
1686
+ --red: #ef4444;
1687
+ --red-lo: rgba(239,68,68,0.08);
1688
+ --amber: #f59e0b;
1689
+ --blue: #38bdf8;
1690
+ --text-1: #f1f5f9;
1691
+ --text-2: #8b98a8;
1692
+ --text-3: #4b5563;
1693
+ --mono: 'DM Mono', monospace;
1694
+ --sans: 'Syne', sans-serif;
1695
+ --radius: 10px;
1696
+ --radius-lg: 16px;
1697
+ }
1698
+
1699
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
1700
+
1701
+ body {
1702
+ font-family: var(--sans);
1703
+ background: var(--bg);
1704
+ color: var(--text-1);
1705
+ min-height: 100vh;
1706
+ overflow-x: hidden;
1707
+ }
1708
+
1709
+ /* \u2500\u2500 Layout \u2500\u2500 */
1710
+ .shell {
1711
+ display: grid;
1712
+ grid-template-columns: 220px 1fr;
1713
+ grid-template-rows: 1fr;
1714
+ min-height: 100vh;
1715
+ }
1716
+
1717
+ /* \u2500\u2500 Sidebar \u2500\u2500 */
1718
+ aside {
1719
+ background: var(--surface);
1720
+ border-right: 1px solid var(--border);
1721
+ display: flex;
1722
+ flex-direction: column;
1723
+ padding: 28px 0;
1724
+ position: sticky;
1725
+ top: 0;
1726
+ height: 100vh;
1727
+ }
1728
+
1729
+ .logo {
1730
+ display: flex;
1731
+ align-items: center;
1732
+ gap: 10px;
1733
+ padding: 0 24px 32px;
1734
+ border-bottom: 1px solid var(--border);
1735
+ margin-bottom: 12px;
1736
+ }
1737
+
1738
+ .logo-mark {
1739
+ width: 34px;
1740
+ height: 34px;
1741
+ background: var(--accent);
1742
+ border-radius: 8px;
1743
+ display: grid;
1744
+ place-items: center;
1745
+ font-size: 16px;
1746
+ flex-shrink: 0;
1747
+ }
1748
+
1749
+ .logo-name {
1750
+ font-family: var(--sans);
1751
+ font-weight: 800;
1752
+ font-size: 18px;
1753
+ letter-spacing: -0.5px;
1754
+ color: var(--text-1);
1755
+ }
1756
+
1757
+ .logo-version {
1758
+ font-family: var(--mono);
1759
+ font-size: 10px;
1760
+ color: var(--text-3);
1761
+ letter-spacing: 0.5px;
1762
+ margin-top: 2px;
1763
+ }
1764
+
1765
+ .nav-section {
1766
+ padding: 8px 12px;
1767
+ flex: 1;
1768
+ }
1769
+
1770
+ .nav-label {
1771
+ font-family: var(--mono);
1772
+ font-size: 10px;
1773
+ color: var(--text-3);
1774
+ letter-spacing: 1.5px;
1775
+ text-transform: uppercase;
1776
+ padding: 0 12px;
1777
+ margin: 16px 0 6px;
1778
+ }
1779
+
1780
+ .nav-item {
1781
+ display: flex;
1782
+ align-items: center;
1783
+ gap: 10px;
1784
+ padding: 9px 12px;
1785
+ border-radius: var(--radius);
1786
+ color: var(--text-2);
1787
+ text-decoration: none;
1788
+ font-size: 14px;
1789
+ font-weight: 600;
1790
+ transition: all 0.15s ease;
1791
+ cursor: pointer;
1792
+ }
1793
+
1794
+ .nav-item:hover { color: var(--text-1); background: var(--panel); }
1795
+ .nav-item.active { color: var(--accent); background: var(--accent-lo); }
1796
+ .nav-item .icon { font-size: 15px; width: 20px; text-align: center; }
1797
+
1798
+ .sidebar-footer {
1799
+ padding: 16px 24px;
1800
+ border-top: 1px solid var(--border);
1801
+ }
1802
+
1803
+ .status-pill {
1804
+ display: flex;
1805
+ align-items: center;
1806
+ gap: 8px;
1807
+ font-family: var(--mono);
1808
+ font-size: 11px;
1809
+ color: var(--text-2);
1810
+ }
1811
+
1812
+ .dot {
1813
+ width: 7px;
1814
+ height: 7px;
1815
+ border-radius: 50%;
1816
+ background: var(--green);
1817
+ box-shadow: 0 0 8px var(--green);
1818
+ }
1819
+
1820
+ /* \u2500\u2500 Main \u2500\u2500 */
1821
+ main {
1822
+ display: flex;
1823
+ flex-direction: column;
1824
+ min-height: 100vh;
1825
+ overflow-y: auto;
1826
+ }
1827
+
1828
+ .topbar {
1829
+ display: flex;
1830
+ align-items: center;
1831
+ justify-content: space-between;
1832
+ padding: 20px 32px;
1833
+ border-bottom: 1px solid var(--border);
1834
+ background: var(--surface);
1835
+ position: sticky;
1836
+ top: 0;
1837
+ z-index: 10;
1838
+ }
1839
+
1840
+ .page-title {
1841
+ font-size: 15px;
1842
+ font-weight: 700;
1843
+ color: var(--text-1);
1844
+ letter-spacing: -0.2px;
1845
+ }
1846
+
1847
+ .page-breadcrumb {
1848
+ font-family: var(--mono);
1849
+ font-size: 11px;
1850
+ color: var(--text-3);
1851
+ margin-top: 2px;
1852
+ }
1853
+
1854
+ .topbar-actions {
1855
+ display: flex;
1856
+ align-items: center;
1857
+ gap: 12px;
1858
+ }
1859
+
1860
+ .btn-sm {
1861
+ font-family: var(--sans);
1862
+ font-weight: 700;
1863
+ font-size: 12px;
1864
+ padding: 8px 16px;
1865
+ border-radius: 8px;
1866
+ border: none;
1867
+ cursor: pointer;
1868
+ transition: all 0.15s ease;
1869
+ letter-spacing: 0.2px;
1870
+ }
1871
+
1872
+ .btn-ghost {
1873
+ background: var(--panel);
1874
+ color: var(--text-2);
1875
+ border: 1px solid var(--border);
1876
+ }
1877
+ .btn-ghost:hover { border-color: var(--border-hi); color: var(--text-1); }
1878
+
1879
+ .btn-primary {
1880
+ background: var(--accent);
1881
+ color: #fff;
1882
+ }
1883
+ .btn-primary:hover { background: #ea580c; box-shadow: 0 0 20px rgba(249,115,22,0.35); }
1884
+
1885
+ /* \u2500\u2500 Content \u2500\u2500 */
1886
+ .content {
1887
+ padding: 28px 32px;
1888
+ display: flex;
1889
+ flex-direction: column;
1890
+ gap: 24px;
1891
+ }
1892
+
1893
+ /* \u2500\u2500 Panel \u2500\u2500 */
1894
+ .panel {
1895
+ background: var(--panel);
1896
+ border: 1px solid var(--border);
1897
+ border-radius: var(--radius-lg);
1898
+ overflow: hidden;
1899
+ }
1900
+
1901
+ .panel-head {
1902
+ display: flex;
1903
+ align-items: center;
1904
+ justify-content: space-between;
1905
+ padding: 18px 24px;
1906
+ border-bottom: 1px solid var(--border);
1907
+ }
1908
+
1909
+ .panel-title {
1910
+ font-size: 13px;
1911
+ font-weight: 700;
1912
+ color: var(--text-1);
1913
+ letter-spacing: -0.1px;
1914
+ }
1915
+
1916
+ .panel-body {
1917
+ padding: 24px;
1918
+ }
1919
+
1920
+ /* \u2500\u2500 Form \u2500\u2500 */
1921
+ .form-grid {
1922
+ display: grid;
1923
+ gap: 20px;
1924
+ }
1925
+
1926
+ .form-section {
1927
+ display: flex;
1928
+ flex-direction: column;
1929
+ gap: 16px;
1930
+ }
1931
+
1932
+ .form-section-title {
1933
+ font-size: 12px;
1934
+ font-weight: 700;
1935
+ color: var(--text-2);
1936
+ text-transform: uppercase;
1937
+ letter-spacing: 1px;
1938
+ margin-bottom: 8px;
1939
+ padding-bottom: 8px;
1940
+ border-bottom: 1px solid var(--border);
1941
+ }
1942
+
1943
+ .form-row {
1944
+ display: grid;
1945
+ grid-template-columns: 1fr 1fr;
1946
+ gap: 16px;
1947
+ }
1948
+
1949
+ .form-field {
1950
+ display: flex;
1951
+ flex-direction: column;
1952
+ gap: 6px;
1953
+ }
1954
+
1955
+ .form-field.full {
1956
+ grid-column: 1 / -1;
1957
+ }
1958
+
1959
+ label {
1960
+ font-family: var(--mono);
1961
+ font-size: 11px;
1962
+ color: var(--text-3);
1963
+ text-transform: uppercase;
1964
+ letter-spacing: 0.5px;
1965
+ font-weight: 500;
1966
+ }
1967
+
1968
+ input, select {
1969
+ background: var(--surface);
1970
+ border: 1px solid var(--border);
1971
+ color: var(--text-1);
1972
+ padding: 10px 14px;
1973
+ border-radius: var(--radius);
1974
+ font-family: var(--mono);
1975
+ font-size: 12px;
1976
+ transition: all 0.15s ease;
1977
+ }
1978
+
1979
+ input:focus, select:focus {
1980
+ outline: none;
1981
+ border-color: var(--accent);
1982
+ box-shadow: 0 0 0 1px var(--accent);
1983
+ }
1984
+
1985
+ input[type="checkbox"] {
1986
+ width: 16px;
1987
+ height: 16px;
1988
+ margin: 0;
1989
+ cursor: pointer;
1990
+ }
1991
+
1992
+ .checkbox-label {
1993
+ display: flex;
1994
+ align-items: center;
1995
+ gap: 8px;
1996
+ cursor: pointer;
1997
+ font-size: 12px;
1998
+ color: var(--text-2);
1999
+ text-transform: none;
2000
+ letter-spacing: normal;
2001
+ }
2002
+
2003
+ /* \u2500\u2500 Actions \u2500\u2500 */
2004
+ .actions {
2005
+ display: flex;
2006
+ gap: 12px;
2007
+ padding: 20px 24px;
2008
+ background: var(--surface);
2009
+ border-top: 1px solid var(--border);
2010
+ }
2011
+
2012
+ .message {
2013
+ font-family: var(--mono);
2014
+ font-size: 11px;
2015
+ padding: 8px 12px;
2016
+ border-radius: var(--radius);
2017
+ margin-left: auto;
2018
+ }
2019
+
2020
+ .message.success {
2021
+ background: var(--green-lo);
2022
+ color: var(--green);
2023
+ border: 1px solid rgba(34,197,94,0.2);
2024
+ }
2025
+
2026
+ .message.error {
2027
+ background: var(--red-lo);
2028
+ color: var(--red);
2029
+ border: 1px solid rgba(239,68,68,0.2);
2030
+ }
2031
+
2032
+ /* \u2500\u2500 Code Block \u2500\u2500 */
2033
+ .code-block {
2034
+ background: var(--surface);
2035
+ border: 1px solid var(--border);
2036
+ border-radius: var(--radius);
2037
+ padding: 20px;
2038
+ font-family: var(--mono);
2039
+ font-size: 11px;
2040
+ color: var(--text-2);
2041
+ overflow-x: auto;
2042
+ }
2043
+
2044
+ .code-block pre {
2045
+ margin: 0;
2046
+ line-height: 1.6;
2047
+ }
2048
+
2049
+ /* \u2500\u2500 Responsive \u2500\u2500 */
2050
+ @media (max-width: 900px) {
2051
+ .shell { grid-template-columns: 1fr; }
2052
+ aside { display: none; }
2053
+ .form-row { grid-template-columns: 1fr; }
2054
+ }
2055
+ </style>
2056
+ </head>
2057
+ <body>
2058
+
2059
+ <div class="shell">
2060
+
2061
+ <!-- \u2500\u2500 Sidebar \u2500\u2500 -->
2062
+ <aside>
2063
+ <div class="logo">
2064
+ <div class="logo-mark">\u2699</div>
2065
+ <div>
2066
+ <div class="logo-name">OpenQA</div>
2067
+ <div class="logo-version">v2.1.0 \xB7 OSS</div>
2068
+ </div>
2069
+ </div>
2070
+
2071
+ <div class="nav-section">
2072
+ <div class="nav-label">Overview</div>
2073
+ <a class="nav-item" href="/">
2074
+ <span class="icon">\u25A6</span> Dashboard
2075
+ </a>
2076
+ <a class="nav-item" href="/kanban">
2077
+ <span class="icon">\u229E</span> Kanban
2078
+ </a>
2079
+
2080
+ <div class="nav-label">System</div>
2081
+ <a class="nav-item active" href="/config">
2082
+ <span class="icon">\u2699</span> Configuration
2083
+ </a>
2084
+ </div>
2085
+
2086
+ <div class="sidebar-footer">
2087
+ <div class="status-pill">
2088
+ <div class="dot"></div>
2089
+ <span>System Ready</span>
2090
+ </div>
2091
+ </div>
2092
+ </aside>
2093
+
2094
+ <!-- \u2500\u2500 Main \u2500\u2500 -->
2095
+ <main>
2096
+
2097
+ <!-- Topbar -->
2098
+ <div class="topbar">
2099
+ <div>
2100
+ <div class="page-title">Configuration</div>
2101
+ <div class="page-breadcrumb">openqa / system / settings</div>
2102
+ </div>
2103
+ <div class="topbar-actions">
2104
+ <button class="btn-sm btn-ghost">Export Config</button>
2105
+ <button class="btn-sm btn-ghost">Import Config</button>
2106
+ <button class="btn-sm btn-primary" onclick="saveAllConfig()">Save All</button>
2107
+ </div>
2108
+ </div>
2109
+
2110
+ <!-- Content -->
2111
+ <div class="content">
2112
+
2113
+ <!-- SaaS Configuration -->
2114
+ <div class="panel">
2115
+ <div class="panel-head">
2116
+ <span class="panel-title">\u{1F310} SaaS Target Configuration</span>
2117
+ </div>
2118
+ <div class="panel-body">
2119
+ <form class="form-grid" id="saas-form">
2120
+ <div class="form-section">
2121
+ <div class="form-section-title">Target Application</div>
2122
+ <div class="form-field full">
2123
+ <label>Application URL</label>
2124
+ <input type="url" id="saas_url" name="saas.url" value="${cfg2.saas.url || ""}" placeholder="https://your-app.com">
1702
2125
  </div>
1703
- <div class="config-item">
1704
- <label>Username (for Basic Auth)</label>
1705
- <input type="text" id="saas_username" name="saas.username" value="${cfg.saas.username || ""}" placeholder="username">
2126
+ <div class="form-row">
2127
+ <div class="form-field">
2128
+ <label>Authentication Type</label>
2129
+ <select id="saas_authType" name="saas.authType">
2130
+ <option value="none" ${cfg2.saas.authType === "none" ? "selected" : ""}>None</option>
2131
+ <option value="basic" ${cfg2.saas.authType === "basic" ? "selected" : ""}>Basic Auth</option>
2132
+ <option value="bearer" ${cfg2.saas.authType === "bearer" ? "selected" : ""}>Bearer Token</option>
2133
+ <option value="session" ${cfg2.saas.authType === "session" ? "selected" : ""}>Session</option>
2134
+ </select>
2135
+ </div>
2136
+ <div class="form-field">
2137
+ <label>Timeout (seconds)</label>
2138
+ <input type="number" id="saas_timeout" name="saas.timeout" value="30" min="5" max="300">
2139
+ </div>
1706
2140
  </div>
1707
- <div class="config-item">
1708
- <label>Password (for Basic Auth)</label>
1709
- <input type="password" id="saas_password" name="saas.password" value="${cfg.saas.password || ""}" placeholder="password">
2141
+ <div class="form-row">
2142
+ <div class="form-field">
2143
+ <label>Username</label>
2144
+ <input type="text" id="saas_username" name="saas.username" value="${cfg2.saas.username || ""}" placeholder="username">
2145
+ </div>
2146
+ <div class="form-field">
2147
+ <label>Password</label>
2148
+ <input type="password" id="saas_password" name="saas.password" value="${cfg2.saas.password || ""}" placeholder="password">
2149
+ </div>
1710
2150
  </div>
1711
- </form>
1712
- </div>
2151
+ </div>
2152
+ </form>
2153
+ </div>
2154
+ </div>
1713
2155
 
1714
- <div class="section">
1715
- <h2>LLM Configuration</h2>
1716
- <form id="configForm">
1717
- <div class="config-item">
1718
- <label>Provider</label>
1719
- <select id="llm_provider" name="llm.provider">
1720
- <option value="openai" ${cfg.llm.provider === "openai" ? "selected" : ""}>OpenAI</option>
1721
- <option value="anthropic" ${cfg.llm.provider === "anthropic" ? "selected" : ""}>Anthropic</option>
1722
- <option value="ollama" ${cfg.llm.provider === "ollama" ? "selected" : ""}>Ollama</option>
1723
- </select>
1724
- </div>
1725
- <div class="config-item">
1726
- <label>Model</label>
1727
- <input type="text" id="llm_model" name="llm.model" value="${cfg.llm.model || ""}" placeholder="gpt-4, claude-3-sonnet, etc.">
2156
+ <!-- LLM Configuration -->
2157
+ <div class="panel">
2158
+ <div class="panel-head">
2159
+ <span class="panel-title">\u{1F916} LLM Configuration</span>
2160
+ </div>
2161
+ <div class="panel-body">
2162
+ <form class="form-grid" id="llm-form">
2163
+ <div class="form-section">
2164
+ <div class="form-section-title">Language Model Provider</div>
2165
+ <div class="form-row">
2166
+ <div class="form-field">
2167
+ <label>Provider</label>
2168
+ <select id="llm_provider" name="llm.provider">
2169
+ <option value="openai" ${cfg2.llm.provider === "openai" ? "selected" : ""}>OpenAI</option>
2170
+ <option value="anthropic" ${cfg2.llm.provider === "anthropic" ? "selected" : ""}>Anthropic</option>
2171
+ <option value="ollama" ${cfg2.llm.provider === "ollama" ? "selected" : ""}>Ollama</option>
2172
+ </select>
2173
+ </div>
2174
+ <div class="form-field">
2175
+ <label>Model</label>
2176
+ <input type="text" id="llm_model" name="llm.model" value="${cfg2.llm.model || ""}" placeholder="gpt-4, claude-3-sonnet, etc.">
2177
+ </div>
1728
2178
  </div>
1729
- <div class="config-item">
2179
+ <div class="form-field full">
1730
2180
  <label>API Key</label>
1731
- <input type="password" id="llm_apiKey" name="llm.apiKey" value="${cfg.llm.apiKey || ""}" placeholder="Your API key">
2181
+ <input type="password" id="llm_apiKey" name="llm.apiKey" value="${cfg2.llm.apiKey || ""}" placeholder="Your API key">
1732
2182
  </div>
1733
- <div class="config-item">
2183
+ <div class="form-field full">
1734
2184
  <label>Base URL (for Ollama)</label>
1735
- <input type="url" id="llm_baseUrl" name="llm.baseUrl" value="${cfg.llm.baseUrl || ""}" placeholder="http://localhost:11434">
2185
+ <input type="url" id="llm_baseUrl" name="llm.baseUrl" value="${cfg2.llm.baseUrl || ""}" placeholder="http://localhost:11434">
1736
2186
  </div>
1737
- </form>
1738
- </div>
2187
+ </div>
2188
+ </form>
2189
+ </div>
2190
+ </div>
1739
2191
 
1740
- <div class="section">
1741
- <h2>Agent Settings</h2>
1742
- <form id="configForm">
1743
- <div class="config-item">
1744
- <label>
1745
- <input type="checkbox" id="agent_autoStart" name="agent.autoStart" class="checkbox" ${cfg.agent.autoStart ? "checked" : ""}>
1746
- Auto-start
2192
+ <!-- Agent Configuration -->
2193
+ <div class="panel">
2194
+ <div class="panel-head">
2195
+ <span class="panel-title">\u{1F3AF} Agent Settings</span>
2196
+ </div>
2197
+ <div class="panel-body">
2198
+ <form class="form-grid" id="agent-form">
2199
+ <div class="form-section">
2200
+ <div class="form-section-title">Agent Behavior</div>
2201
+ <div class="form-field">
2202
+ <label class="checkbox-label">
2203
+ <input type="checkbox" id="agent_autoStart" name="agent.autoStart" ${cfg2.agent.autoStart ? "checked" : ""}>
2204
+ Auto-start on launch
1747
2205
  </label>
1748
2206
  </div>
1749
- <div class="config-item">
1750
- <label>Interval (ms)</label>
1751
- <input type="number" id="agent_intervalMs" name="agent.intervalMs" value="${cfg.agent.intervalMs}" min="60000">
1752
- </div>
1753
- <div class="config-item">
1754
- <label>Max Iterations</label>
1755
- <input type="number" id="agent_maxIterations" name="agent.maxIterations" value="${cfg.agent.maxIterations}" min="1" max="100">
2207
+ <div class="form-row">
2208
+ <div class="form-field">
2209
+ <label>Check Interval (ms)</label>
2210
+ <input type="number" id="agent_intervalMs" name="agent.intervalMs" value="${cfg2.agent.intervalMs}" min="60000" step="60000">
2211
+ </div>
2212
+ <div class="form-field">
2213
+ <label>Max Iterations</label>
2214
+ <input type="number" id="agent_maxIterations" name="agent.maxIterations" value="${cfg2.agent.maxIterations}" min="1" max="100">
2215
+ </div>
1756
2216
  </div>
1757
- </form>
1758
- </div>
2217
+ </div>
2218
+ </form>
2219
+ </div>
2220
+ </div>
1759
2221
 
1760
- <div class="section">
1761
- <h2>Actions</h2>
1762
- <button type="button" class="btn" onclick="saveConfig()">Save Configuration</button>
1763
- <button type="button" class="btn btn-secondary" onclick="resetConfig()">Reset to Defaults</button>
1764
- <span id="message"></span>
1765
- </div>
2222
+ <!-- Environment Variables -->
2223
+ <div class="panel">
2224
+ <div class="panel-head">
2225
+ <span class="panel-title">\u{1F4DD} Environment Variables</span>
2226
+ </div>
2227
+ <div class="panel-body">
2228
+ <p style="color: var(--text-3); font-size: 12px; margin-bottom: 16px;">
2229
+ You can also set these environment variables before starting OpenQA:
2230
+ </p>
2231
+ <div class="code-block">
2232
+ <pre>export SAAS_URL="https://your-app.com"
2233
+ export SAAS_AUTH_TYPE="basic"
2234
+ export SAAS_USERNAME="admin"
2235
+ export SAAS_PASSWORD="secret"
2236
+
2237
+ export LLM_PROVIDER="openai"
2238
+ export OPENAI_API_KEY="your-openai-key"
2239
+ export LLM_MODEL="gpt-4"
1766
2240
 
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
2241
  export AGENT_AUTO_START=true
1772
- export LLM_PROVIDER=openai
1773
- export OPENAI_API_KEY="your-key"
2242
+ export AGENT_INTERVAL_MS=3600000
2243
+ export AGENT_MAX_ITERATIONS=20
1774
2244
 
1775
- openqa start</code></pre>
2245
+ openqa start</pre>
1776
2246
  </div>
2247
+ </div>
2248
+ </div>
1777
2249
 
1778
- <script>
1779
- async function saveConfig() {
1780
- const form = document.getElementById('configForm');
1781
- const formData = new FormData(form);
1782
- const config = {};
1783
-
1784
- for (let [key, value] of formData.entries()) {
1785
- if (value === '') continue;
1786
-
1787
- // Handle nested keys like "saas.url"
1788
- const keys = key.split('.');
1789
- let obj = config;
1790
- for (let i = 0; i < keys.length - 1; i++) {
1791
- if (!obj[keys[i]]) obj[keys[i]] = {};
1792
- obj = obj[keys[i]];
1793
- }
1794
-
1795
- // Convert checkbox values to boolean
1796
- if (key.includes('autoStart')) {
1797
- obj[keys[keys.length - 1]] = value === 'on';
1798
- } else if (key.includes('intervalMs') || key.includes('maxIterations')) {
1799
- obj[keys[keys.length - 1]] = parseInt(value);
1800
- } else {
1801
- obj[keys[keys.length - 1]] = value;
1802
- }
1803
- }
1804
-
1805
- try {
1806
- const response = await fetch('/api/config', {
1807
- method: 'POST',
1808
- headers: { 'Content-Type': 'application/json' },
1809
- body: JSON.stringify(config)
1810
- });
1811
-
1812
- const result = await response.json();
1813
- if (result.success) {
1814
- showMessage('Configuration saved successfully!', 'success');
1815
- setTimeout(() => location.reload(), 1500);
1816
- } else {
1817
- showMessage('Failed to save configuration', 'error');
1818
- }
1819
- } catch (error) {
1820
- showMessage('Error: ' + error.message, 'error');
1821
- }
1822
- }
1823
-
1824
- async function resetConfig() {
1825
- if (confirm('Are you sure you want to reset all configuration to defaults?')) {
1826
- try {
1827
- const response = await fetch('/api/config/reset', { method: 'POST' });
1828
- const result = await response.json();
1829
- if (result.success) {
1830
- showMessage('Configuration reset to defaults', 'success');
1831
- setTimeout(() => location.reload(), 1500);
1832
- }
1833
- } catch (error) {
1834
- showMessage('Error: ' + error.message, 'error');
1835
- }
1836
- }
1837
- }
1838
-
1839
- function showMessage(text, type) {
1840
- const messageEl = document.getElementById('message');
1841
- messageEl.textContent = text;
1842
- messageEl.className = type;
1843
- setTimeout(() => {
1844
- messageEl.textContent = '';
1845
- messageEl.className = '';
1846
- }, 3000);
1847
- }
1848
- </script>
1849
- </body>
1850
- </html>
2250
+ <!-- Actions -->
2251
+ <div class="actions">
2252
+ <button class="btn-sm btn-ghost" onclick="testConnection()">Test Connection</button>
2253
+ <button class="btn-sm btn-ghost" onclick="exportConfig()">Export Config</button>
2254
+ <button class="btn-sm btn-ghost" onclick="resetConfig()">Reset to Defaults</button>
2255
+ <div id="message"></div>
2256
+ </div>
2257
+
2258
+ </div><!-- /content -->
2259
+ </main>
2260
+ </div><!-- /shell -->
2261
+
2262
+ <script>
2263
+ async function saveAllConfig() {
2264
+ const forms = ['saas-form', 'llm-form', 'agent-form'];
2265
+ const config = {};
2266
+
2267
+ for (const formId of forms) {
2268
+ const form = document.getElementById(formId);
2269
+ const formData = new FormData(form);
2270
+
2271
+ for (let [key, value] of formData.entries()) {
2272
+ if (value === '') continue;
2273
+
2274
+ // Handle nested keys like "saas.url"
2275
+ const keys = key.split('.');
2276
+ let obj = config;
2277
+ for (let i = 0; i < keys.length - 1; i++) {
2278
+ if (!obj[keys[i]]) obj[keys[i]] = {};
2279
+ obj = obj[keys[i]];
2280
+ }
2281
+
2282
+ // Convert checkbox values to boolean
2283
+ if (key.includes('autoStart')) {
2284
+ obj[keys[keys.length - 1]] = value === 'on';
2285
+ } else if (key.includes('intervalMs') || key.includes('maxIterations') || key.includes('timeout')) {
2286
+ obj[keys[keys.length - 1]] = parseInt(value);
2287
+ } else {
2288
+ obj[keys[keys.length - 1]] = value;
2289
+ }
2290
+ }
2291
+ }
2292
+
2293
+ try {
2294
+ const response = await fetch('/api/config', {
2295
+ method: 'POST',
2296
+ headers: { 'Content-Type': 'application/json' },
2297
+ body: JSON.stringify(config)
2298
+ });
2299
+
2300
+ if (response.ok) {
2301
+ showMessage('Configuration saved successfully!', 'success');
2302
+ } else {
2303
+ showMessage('Failed to save configuration', 'error');
2304
+ }
2305
+ } catch (error) {
2306
+ showMessage('Error: ' + error.message, 'error');
2307
+ }
2308
+ }
2309
+
2310
+ async function testConnection() {
2311
+ showMessage('Testing connection...', 'success');
2312
+
2313
+ try {
2314
+ const response = await fetch('/api/test-connection', { method: 'POST' });
2315
+
2316
+ if (response.ok) {
2317
+ showMessage('Connection successful!', 'success');
2318
+ } else {
2319
+ showMessage('Connection failed', 'error');
2320
+ }
2321
+ } catch (error) {
2322
+ showMessage('Connection error: ' + error.message, 'error');
2323
+ }
2324
+ }
2325
+
2326
+ async function exportConfig() {
2327
+ try {
2328
+ const response = await fetch('/api/config');
2329
+ const config = await response.json();
2330
+
2331
+ const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
2332
+ const url = URL.createObjectURL(blob);
2333
+ const a = document.createElement('a');
2334
+ a.href = url;
2335
+ a.download = 'openqa-config.json';
2336
+ a.click();
2337
+ URL.revokeObjectURL(url);
2338
+
2339
+ showMessage('Configuration exported', 'success');
2340
+ } catch (error) {
2341
+ showMessage('Export failed: ' + error.message, 'error');
2342
+ }
2343
+ }
2344
+
2345
+ async function resetConfig() {
2346
+ if (confirm('Are you sure you want to reset all configuration to defaults? This cannot be undone.')) {
2347
+ try {
2348
+ const response = await fetch('/api/config/reset', { method: 'POST' });
2349
+
2350
+ if (response.ok) {
2351
+ location.reload();
2352
+ } else {
2353
+ showMessage('Failed to reset configuration', 'error');
2354
+ }
2355
+ } catch (error) {
2356
+ showMessage('Error: ' + error.message, 'error');
2357
+ }
2358
+ }
2359
+ }
2360
+
2361
+ function showMessage(msg, type) {
2362
+ const el = document.getElementById('message');
2363
+ el.textContent = msg;
2364
+ el.className = 'message ' + type;
2365
+ setTimeout(() => { el.textContent = ''; el.className = ''; }, 5000);
2366
+ }
2367
+
2368
+ // Auto-save on field change
2369
+ document.querySelectorAll('input, select').forEach(field => {
2370
+ field.addEventListener('change', () => {
2371
+ showMessage('Changes made - click "Save All" to apply', 'success');
2372
+ });
2373
+ });
2374
+ </script>
2375
+
2376
+ </body>
2377
+ </html>
1851
2378
  `);
1852
2379
  });
1853
2380
  const server = app.listen(cfg.web.port, cfg.web.host, () => {