@q-agent/agent 0.1.1 → 0.1.7
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 +28 -1
- package/dist/src/api.js +97 -1
- package/dist/src/cli.js +18 -3
- package/dist/src/config.js +25 -0
- package/dist/src/ensureBrowser.js +4 -1
- package/dist/src/paths.js +15 -0
- package/dist/src/playwrightConfig.js +87 -42
- package/dist/src/report.js +4 -0
- package/dist/src/runner.js +354 -63
- package/dist/src/ui.js +400 -161
- package/dist/src/version.js +61 -0
- package/dist/test/api.test.js +50 -0
- package/dist/test/playwrightConfig.test.js +84 -0
- package/dist/test/report.test.js +4 -0
- package/dist/test/version.test.js +21 -0
- package/package.json +36 -2
package/dist/src/ui.js
CHANGED
|
@@ -35,22 +35,44 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.startUi = startUi;
|
|
37
37
|
/**
|
|
38
|
-
* Local web UI for the agent, served on 127.0.0.1
|
|
38
|
+
* Local web UI for the agent, served on 127.0.0.1 (opened in the browser by the
|
|
39
|
+
* `ui` command, or shown in the Electron desktop window). Pairs by entering a
|
|
40
|
+
* server URL + 6-digit code, then shows the live agent status + log.
|
|
39
41
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* The page implements the client's "Local Agent" design (dark, glass,
|
|
43
|
+
* purple→indigo, custom frameless titlebar, segmented 6-digit code, compact
|
|
44
|
+
* connected view). No framework / no extra deps — Node's built-in http plus the
|
|
45
|
+
* in-process event bus (bus.ts).
|
|
44
46
|
*/
|
|
45
47
|
const node_child_process_1 = require("node:child_process");
|
|
46
48
|
const http = __importStar(require("node:http"));
|
|
47
49
|
const os = __importStar(require("node:os"));
|
|
50
|
+
const path = __importStar(require("node:path"));
|
|
48
51
|
const api_1 = require("./api");
|
|
49
52
|
const bus_1 = require("./bus");
|
|
50
53
|
const config_1 = require("./config");
|
|
54
|
+
const paths_1 = require("./paths");
|
|
51
55
|
const runner_1 = require("./runner");
|
|
52
56
|
let signal = { aborted: false };
|
|
53
57
|
let running = false;
|
|
58
|
+
/** Playwright's version, for the environment chips (best-effort). */
|
|
59
|
+
function playwrightVersion() {
|
|
60
|
+
try {
|
|
61
|
+
return require(path.join((0, paths_1.agentNodeModules)(), "playwright", "package.json")).version;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** This agent build's version (from its own package.json), shown bottom-right. */
|
|
68
|
+
function appVersion() {
|
|
69
|
+
try {
|
|
70
|
+
return require(path.join(__dirname, "..", "..", "package.json")).version || "";
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
54
76
|
/** Start the claim→run loop for `cfg` (no-op if already running). */
|
|
55
77
|
function startLoop(cfg) {
|
|
56
78
|
if (running)
|
|
@@ -76,8 +98,14 @@ function state() {
|
|
|
76
98
|
paired: Boolean(cfg),
|
|
77
99
|
running,
|
|
78
100
|
deviceId: cfg?.deviceId ?? null,
|
|
79
|
-
deviceName: cfg?.deviceName ??
|
|
80
|
-
|
|
101
|
+
deviceName: cfg?.deviceName ?? os.hostname(),
|
|
102
|
+
// Pre-fill the server from the paired config, else the value baked into this
|
|
103
|
+
// build — so a downloaded desktop app needs only the 6-digit code.
|
|
104
|
+
serverUrl: cfg?.serverUrl ?? (0, config_1.defaultServerUrl)(),
|
|
105
|
+
machine: os.hostname(),
|
|
106
|
+
nodeVersion: process.versions.node,
|
|
107
|
+
playwrightVersion: playwrightVersion(),
|
|
108
|
+
appVersion: appVersion(),
|
|
81
109
|
events: (0, bus_1.recentEvents)(),
|
|
82
110
|
};
|
|
83
111
|
}
|
|
@@ -128,7 +156,7 @@ async function handle(req, res) {
|
|
|
128
156
|
const { deviceToken, deviceId } = await (0, api_1.redeemDevice)(serverUrl, String(code).trim(), name);
|
|
129
157
|
const cfg = { serverUrl, deviceToken, deviceId, deviceName: name };
|
|
130
158
|
(0, config_1.saveConfig)(cfg);
|
|
131
|
-
(0, bus_1.emit)("log", { message: `
|
|
159
|
+
(0, bus_1.emit)("log", { message: `paired as device #${deviceId} (${name})` });
|
|
132
160
|
startLoop(cfg);
|
|
133
161
|
sendJson(res, 200, { ok: true, deviceId });
|
|
134
162
|
}
|
|
@@ -138,6 +166,12 @@ async function handle(req, res) {
|
|
|
138
166
|
return;
|
|
139
167
|
}
|
|
140
168
|
if (req.method === "POST" && url === "/api/disconnect") {
|
|
169
|
+
// Tell the server first (token still valid) so the device drops off the
|
|
170
|
+
// owner's SPA list immediately, then wipe the local token + stop the loop.
|
|
171
|
+
const cfg = (0, config_1.loadConfig)();
|
|
172
|
+
if (cfg) {
|
|
173
|
+
await (0, api_1.disconnectDevice)(cfg).catch((err) => (0, bus_1.emit)("log", { message: `server disconnect failed: ${err.message}` }));
|
|
174
|
+
}
|
|
141
175
|
stopLoop();
|
|
142
176
|
(0, config_1.clearConfig)();
|
|
143
177
|
(0, bus_1.emit)("agent-status", { running: false });
|
|
@@ -165,7 +199,9 @@ async function handle(req, res) {
|
|
|
165
199
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
166
200
|
res.end("Not found");
|
|
167
201
|
}
|
|
168
|
-
/** Start the UI server, begin the loop if already paired, and open the browser.
|
|
202
|
+
/** Start the UI server, begin the loop if already paired, and open the browser.
|
|
203
|
+
* `onListening` receives the actual URL (the port may bump if 7420 is taken) —
|
|
204
|
+
* the Electron shell uses it to load the window. */
|
|
169
205
|
function startUi(opts = {}) {
|
|
170
206
|
let port = opts.port ?? 7420;
|
|
171
207
|
const server = http.createServer((req, res) => {
|
|
@@ -190,199 +226,402 @@ function startUi(opts = {}) {
|
|
|
190
226
|
const cfg = (0, config_1.loadConfig)();
|
|
191
227
|
if (cfg)
|
|
192
228
|
startLoop(cfg);
|
|
229
|
+
opts.onListening?.(addr);
|
|
193
230
|
if (opts.open !== false)
|
|
194
231
|
openBrowser(addr);
|
|
195
232
|
});
|
|
196
233
|
}
|
|
197
234
|
// ---------------------------------------------------------------- the page
|
|
235
|
+
// Client's "Local Agent" design, ported to vanilla HTML/CSS/JS and wired to the
|
|
236
|
+
// /api/* endpoints. The client script avoids template literals / ${} so this
|
|
237
|
+
// server-side template literal needs no escaping.
|
|
198
238
|
const PAGE = `<!doctype html>
|
|
199
239
|
<html lang="en">
|
|
200
240
|
<head>
|
|
201
241
|
<meta charset="utf-8" />
|
|
202
242
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
203
243
|
<title>Q-Agent · Local Agent</title>
|
|
244
|
+
<link rel="preconnect" href="https://api.fontshare.com" />
|
|
245
|
+
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,600,700,900&display=swap" rel="stylesheet" />
|
|
246
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
204
247
|
<style>
|
|
205
|
-
:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
.card h2 { margin: 0 0 14px; font-size: 11px; letter-spacing: .1em; color: var(--dim); font-weight: 800; }
|
|
225
|
-
label { display: block; font-size: 12px; color: var(--soft); margin: 12px 0 5px; }
|
|
226
|
-
input, textarea { width: 100%; background: rgba(0,0,0,.30); border: 1px solid var(--border); border-radius: 10px;
|
|
227
|
-
color: var(--ink); padding: 10px 12px; font: inherit; }
|
|
228
|
-
input:focus, textarea:focus { outline: none; border-color: var(--violet); }
|
|
229
|
-
textarea { resize: vertical; min-height: 60px; font-family: ui-monospace, monospace; font-size: 12px; }
|
|
230
|
-
button { cursor: pointer; border: none; border-radius: 10px; font: inherit; font-weight: 700; padding: 10px 16px; }
|
|
231
|
-
.primary { background: linear-gradient(135deg,#8b5cf6,#7c3aed); color: #fff; }
|
|
232
|
-
.primary:disabled { opacity: .6; cursor: default; }
|
|
233
|
-
.ghost { background: rgba(255,255,255,.06); color: var(--soft); }
|
|
234
|
-
.row { display: flex; align-items: center; gap: 10px; }
|
|
235
|
-
.pill { font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 999px; display: inline-flex; align-items: center; gap: 6px; }
|
|
236
|
-
.dot { width: 7px; height: 7px; border-radius: 50%; }
|
|
237
|
-
.err { color: var(--red); font-size: 12px; margin-top: 10px; min-height: 16px; }
|
|
238
|
-
.mut { color: var(--dim); font-size: 12px; }
|
|
239
|
-
.bar { height: 8px; border-radius: 999px; background: rgba(255,255,255,.08); overflow: hidden; margin: 12px 0 6px; }
|
|
240
|
-
.bar > i { display: block; height: 100%; background: linear-gradient(90deg,#8b5cf6,#a78bfa); width: 0; transition: width .3s; }
|
|
241
|
-
.counts { display: flex; gap: 16px; font-size: 12px; }
|
|
242
|
-
.banner { background: rgba(245,158,11,.12); border: 1px solid rgba(245,158,11,.35); color: #f6c177;
|
|
243
|
-
border-radius: 10px; padding: 10px 12px; font-size: 12.5px; margin-bottom: 12px; display: none; }
|
|
244
|
-
.feed { display: flex; flex-direction: column; gap: 8px; max-height: 360px; overflow: auto; }
|
|
245
|
-
.ev { display: flex; gap: 10px; align-items: baseline; font-size: 12.5px; padding: 7px 10px; border-radius: 9px;
|
|
246
|
-
background: rgba(255,255,255,.03); }
|
|
247
|
-
.ev .t { color: var(--dim); font-family: ui-monospace, monospace; font-size: 10.5px; flex-shrink: 0; }
|
|
248
|
-
.ev .m { color: var(--soft); }
|
|
249
|
-
.ev.pass { border-left: 2px solid var(--green); } .ev.fail { border-left: 2px solid var(--red); }
|
|
250
|
-
.ev.err { border-left: 2px solid var(--red); } .ev.err .m { color: #fca5a5; }
|
|
251
|
-
.badge { font-size: 10px; font-weight: 800; padding: 1px 7px; border-radius: 999px; }
|
|
252
|
-
.badge.pass { background: rgba(52,211,153,.18); color: var(--green); }
|
|
253
|
-
.badge.fail { background: rgba(244,63,94,.18); color: var(--red); }
|
|
254
|
-
.hidden { display: none; }
|
|
255
|
-
code { background: rgba(0,0,0,.3); padding: 1px 5px; border-radius: 5px; font-size: 11.5px; color: var(--violet-2); }
|
|
248
|
+
*{box-sizing:border-box}
|
|
249
|
+
html,body{margin:0;height:100%}
|
|
250
|
+
body{background:#0a0a0f;color:#ECECF1;font-family:'Satoshi',system-ui,sans-serif;-webkit-font-smoothing:antialiased;overflow:hidden}
|
|
251
|
+
::selection{background:rgba(139,92,246,.4);color:#fff}
|
|
252
|
+
input::placeholder{color:#5c5c6e}
|
|
253
|
+
::-webkit-scrollbar{width:9px}::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:8px}
|
|
254
|
+
@keyframes fadeUp{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
|
|
255
|
+
@keyframes glowPulse{0%,100%{opacity:.5}50%{opacity:.85}}
|
|
256
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
257
|
+
@keyframes livePulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.7)}}
|
|
258
|
+
@keyframes logoHalo{0%,100%{opacity:.5;transform:scale(.92)}50%{opacity:1;transform:scale(1.12)}}
|
|
259
|
+
@keyframes logIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
|
|
260
|
+
@keyframes barPulse{0%,100%{transform:scaleY(.35)}50%{transform:scaleY(1)}}
|
|
261
|
+
@keyframes caret{0%,49%{opacity:1}50%,100%{opacity:0}}
|
|
262
|
+
.ctl{width:44px;height:32px;display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;color:#8b8b9e;border-radius:7px}
|
|
263
|
+
.ctl:hover{background:rgba(255,255,255,.07);color:#ececf1}
|
|
264
|
+
.ctl.close:hover{background:#e5484d;color:#fff}
|
|
265
|
+
input:focus{outline:none}
|
|
266
|
+
.cell{position:relative;display:flex;align-items:center;justify-content:center;height:54px;border-radius:12px;background:#16161f;font-family:'JetBrains Mono',monospace;font-size:24px;font-weight:600;color:#ECECF1;border:1.5px solid rgba(255,255,255,.09);transition:border-color .16s,box-shadow .16s}
|
|
256
267
|
</style>
|
|
257
268
|
</head>
|
|
258
269
|
<body>
|
|
259
|
-
<div
|
|
260
|
-
<div
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
270
|
+
<div style="position:fixed;inset:0;display:flex;flex-direction:column;background:#0a0a0f;overflow:hidden;border:1px solid rgba(255,255,255,.08)">
|
|
271
|
+
<div style="position:absolute;top:-16%;left:-10%;width:520px;height:520px;border-radius:50%;background:radial-gradient(circle,rgba(139,92,246,.26),transparent 62%);filter:blur(30px);z-index:0;pointer-events:none;animation:glowPulse 9s ease-in-out infinite"></div>
|
|
272
|
+
<div style="position:absolute;bottom:-22%;right:-12%;width:600px;height:600px;border-radius:50%;background:radial-gradient(circle,rgba(99,102,241,.22),transparent 62%);filter:blur(30px);z-index:0;pointer-events:none;animation:glowPulse 11s ease-in-out infinite 1s"></div>
|
|
273
|
+
|
|
274
|
+
<!-- titlebar (frameless electron chrome; draggable) -->
|
|
275
|
+
<div style="position:relative;z-index:5;flex-shrink:0;height:40px;display:flex;align-items:center;justify-content:space-between;padding:0 4px 0 13px;background:rgba(14,14,20,.72);backdrop-filter:blur(24px);border-bottom:1px solid rgba(255,255,255,.06);-webkit-app-region:drag">
|
|
276
|
+
<div style="display:flex;align-items:center;gap:9px">
|
|
277
|
+
<div style="width:18px;height:18px;border-radius:6px;background:linear-gradient(135deg,#8b5cf6,#6366f1);display:flex;align-items:center;justify-content:center;box-shadow:0 3px 9px -3px rgba(139,92,246,.8)"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.9 5.3L19 10l-5.1 1.7L12 17l-1.9-5.3L5 10l5.1-1.7z"/></svg></div>
|
|
278
|
+
<span style="font-size:11.5px;font-weight:600;color:#a0a0b2">Q‑Agent · Local Agent</span>
|
|
279
|
+
</div>
|
|
280
|
+
<div id="winctl" style="display:none;align-items:center;-webkit-app-region:no-drag">
|
|
281
|
+
<button class="ctl" onclick="qa.win('minimize')"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg></button>
|
|
282
|
+
<button class="ctl" onclick="qa.win('maximize')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/></svg></button>
|
|
283
|
+
<button class="ctl close" onclick="qa.win('close')"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
265
284
|
</div>
|
|
266
|
-
<div style="margin-left:auto"><span id="status" class="pill"><span class="dot"></span><span id="statusText">…</span></span></div>
|
|
267
285
|
</div>
|
|
268
286
|
|
|
269
|
-
<!--
|
|
270
|
-
<div id="
|
|
271
|
-
|
|
272
|
-
<
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
<
|
|
278
|
-
<div class="row" style="margin-top:12px"><button id="connect" class="primary">Connect</button></div>
|
|
287
|
+
<!-- version tag (bottom-right) + update banner (desktop only; shown on demand) -->
|
|
288
|
+
<div id="versionTag" style="position:fixed;bottom:7px;right:11px;z-index:6;font-size:10px;font-weight:600;letter-spacing:.02em;color:#4e4e5e;font-family:'JetBrains Mono',ui-monospace,monospace;pointer-events:none">v—</div>
|
|
289
|
+
<div id="updateBar" style="display:none;position:fixed;left:14px;right:14px;bottom:16px;z-index:60;align-items:center;gap:11px;padding:11px 13px;border-radius:13px;background:rgba(24,24,32,.97);backdrop-filter:blur(22px);border:1px solid rgba(139,92,246,.4);box-shadow:0 20px 55px -18px rgba(0,0,0,.95);animation:fadeUp .4s ease both">
|
|
290
|
+
<span style="flex-shrink:0;width:30px;height:30px;border-radius:9px;background:rgba(139,92,246,.16);border:1px solid rgba(139,92,246,.32);display:flex;align-items:center;justify-content:center;color:#c4b5fd"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12M7 10l5 5 5-5M5 21h14"/></svg></span>
|
|
291
|
+
<div style="flex:1;min-width:0">
|
|
292
|
+
<div id="updTitle" style="font-size:12.5px;font-weight:700;color:#ECECF1;line-height:1.25">Update available</div>
|
|
293
|
+
<div id="updSub" style="font-size:11px;color:#9494a6;line-height:1.3;margin-top:1px"></div>
|
|
294
|
+
</div>
|
|
295
|
+
<button id="updBtn" style="flex-shrink:0;height:32px;padding:0 14px;border:none;border-radius:9px;font-family:inherit;font-size:12.5px;font-weight:700;color:#fff;background:linear-gradient(135deg,#8b5cf6,#6366f1);cursor:pointer;box-shadow:0 8px 22px -8px rgba(139,92,246,.8)">Install</button>
|
|
279
296
|
</div>
|
|
280
297
|
|
|
281
|
-
<!--
|
|
282
|
-
<div
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
298
|
+
<!-- body -->
|
|
299
|
+
<div style="position:relative;z-index:2;flex:1;overflow-y:auto;padding:22px 22px 18px">
|
|
300
|
+
<div style="max-width:420px;margin:0 auto">
|
|
301
|
+
|
|
302
|
+
<!-- header -->
|
|
303
|
+
<div style="display:flex;align-items:flex-start;gap:14px;margin-bottom:26px">
|
|
304
|
+
<div style="position:relative;width:46px;height:46px;flex-shrink:0">
|
|
305
|
+
<span style="position:absolute;inset:-6px;border-radius:16px;background:radial-gradient(circle,rgba(139,92,246,.5),transparent 70%);filter:blur(8px);animation:logoHalo 3s ease-in-out infinite"></span>
|
|
306
|
+
<div style="position:relative;width:46px;height:46px;border-radius:15px;background:linear-gradient(135deg,#8b5cf6,#6366f1);display:flex;align-items:center;justify-content:center;box-shadow:0 10px 26px -8px rgba(139,92,246,.85)"><svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.9 5.3L19 10l-5.1 1.7L12 17l-1.9-5.3L5 10l5.1-1.7z"/></svg></div>
|
|
307
|
+
</div>
|
|
308
|
+
<div style="flex:1;min-width:0;padding-top:1px">
|
|
309
|
+
<div style="font-size:20px;font-weight:900;letter-spacing:-.02em;line-height:1.1">Local Agent</div>
|
|
310
|
+
<div style="font-size:12.5px;color:#8b8b9e;margin-top:4px;line-height:1.45">Runs Playwright on this machine — your session stays local.</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
|
|
313
|
+
<div id="pill" style="display:flex;align-items:center;gap:7px;padding:6px 12px;border-radius:20px">
|
|
314
|
+
<span id="pillDot" style="width:7px;height:7px;border-radius:50%"></span>
|
|
315
|
+
<span id="pillLabel" style="font-size:11.5px;font-weight:700;white-space:nowrap"></span>
|
|
316
|
+
</div>
|
|
317
|
+
<button id="hdrDisconnect" title="Disconnect" style="display:none;align-items:center;justify-content:center;width:32px;height:32px;border-radius:10px;background:rgba(229,72,77,.12);border:1px solid rgba(229,72,77,.3);cursor:pointer;color:#ff8589"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M18.36 6.64A9 9 0 1 1 5.64 6.64M12 2v10"/></svg></button>
|
|
288
318
|
</div>
|
|
289
|
-
<button id="disconnect" class="ghost" style="margin-left:auto">Disconnect</button>
|
|
290
319
|
</div>
|
|
291
|
-
</div>
|
|
292
320
|
|
|
293
|
-
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
<
|
|
301
|
-
|
|
321
|
+
<!-- CONNECT form -->
|
|
322
|
+
<div id="formView" style="display:none">
|
|
323
|
+
<div style="animation:fadeUp .5s ease both;border-radius:22px;background:rgba(20,20,28,.55);backdrop-filter:blur(28px);border:1px solid rgba(255,255,255,.08);padding:24px;box-shadow:0 24px 60px -22px rgba(0,0,0,.7)">
|
|
324
|
+
<div style="display:flex;align-items:center;gap:9px;margin-bottom:6px">
|
|
325
|
+
<span style="font-size:11px;font-weight:700;letter-spacing:.14em;color:#a78bfa">CONNECT</span>
|
|
326
|
+
<span style="flex:1;height:1px;background:linear-gradient(90deg,rgba(167,139,250,.35),transparent)"></span>
|
|
327
|
+
</div>
|
|
328
|
+
<p style="margin:0 0 20px;font-size:12.5px;color:#9494a6;line-height:1.5">Generate a pairing code on the Q‑Agent app's <span style="color:#c3c3d0;font-weight:600">Local Agent</span> screen, then enter it here.</p>
|
|
329
|
+
|
|
330
|
+
<!-- Compact "known server" line, shown when the server is baked in/paired
|
|
331
|
+
so the user only enters the 6-digit code. A "Change" link reveals the
|
|
332
|
+
full input (#serverBlock) for a different server. -->
|
|
333
|
+
<div id="serverKnown" style="display:none;align-items:center;gap:8px;margin-bottom:16px;padding:9px 12px;border-radius:11px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);font-size:12px;color:#9494a6">
|
|
334
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#7a7a8c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18"/></svg>
|
|
335
|
+
<span style="color:#7a7a8c">Server</span>
|
|
336
|
+
<span id="serverHost" style="color:#c3c3d0;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
|
|
337
|
+
<a id="serverChange" href="#" style="margin-left:auto;flex-shrink:0;color:#a78bfa;text-decoration:none;font-weight:600">Change</a>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<div id="serverBlock" style="margin-bottom:16px">
|
|
341
|
+
<label style="display:block;font-size:12px;font-weight:600;color:#9494a6;margin-bottom:8px">Server URL</label>
|
|
342
|
+
<div id="urlBox" style="display:flex;align-items:center;gap:10px;height:46px;padding:0 14px;border-radius:12px;background:#16161f;border:1px solid rgba(255,255,255,.1);transition:border-color .18s">
|
|
343
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7a7a8c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18"/></svg>
|
|
344
|
+
<input id="server" placeholder="https://your-qagent-server/api" style="flex:1;min-width:0;background:none;border:none;color:#ECECF1;font-size:13.5px;font-family:inherit" />
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<div style="margin-bottom:22px">
|
|
349
|
+
<label style="display:flex;justify-content:space-between;align-items:center;font-size:12px;font-weight:600;color:#9494a6;margin-bottom:8px"><span>Pair code</span><span style="font-size:10.5px;font-weight:600;color:#6c6c7e">one-time · 6 digits</span></label>
|
|
350
|
+
<div style="position:relative">
|
|
351
|
+
<input id="code" inputmode="numeric" maxlength="6" style="position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:text;font-size:16px;z-index:2" />
|
|
352
|
+
<div style="display:flex;align-items:center;gap:11px;pointer-events:none">
|
|
353
|
+
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:9px;flex:1">
|
|
354
|
+
<div class="cell" id="cell0"><span class="d"></span><span class="cr" style="display:none;width:2px;height:24px;background:#a78bfa;animation:caret 1.1s step-end infinite"></span></div>
|
|
355
|
+
<div class="cell" id="cell1"><span class="d"></span><span class="cr" style="display:none;width:2px;height:24px;background:#a78bfa;animation:caret 1.1s step-end infinite"></span></div>
|
|
356
|
+
<div class="cell" id="cell2"><span class="d"></span><span class="cr" style="display:none;width:2px;height:24px;background:#a78bfa;animation:caret 1.1s step-end infinite"></span></div>
|
|
357
|
+
</div>
|
|
358
|
+
<span style="width:13px;height:2.5px;border-radius:2px;background:#3a3a48;flex-shrink:0"></span>
|
|
359
|
+
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:9px;flex:1">
|
|
360
|
+
<div class="cell" id="cell3"><span class="d"></span><span class="cr" style="display:none;width:2px;height:24px;background:#a78bfa;animation:caret 1.1s step-end infinite"></span></div>
|
|
361
|
+
<div class="cell" id="cell4"><span class="d"></span><span class="cr" style="display:none;width:2px;height:24px;background:#a78bfa;animation:caret 1.1s step-end infinite"></span></div>
|
|
362
|
+
<div class="cell" id="cell5"><span class="d"></span><span class="cr" style="display:none;width:2px;height:24px;background:#a78bfa;animation:caret 1.1s step-end infinite"></span></div>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<button id="connect" style="display:flex;align-items:center;justify-content:center;gap:9px;width:100%;height:47px;border-radius:13px;border:none;font-family:inherit;font-weight:700;font-size:14.5px;transition:transform .15s,box-shadow .2s">
|
|
369
|
+
<span id="connSpin" style="display:none;width:16px;height:16px;border:2px solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite"></span>
|
|
370
|
+
<svg id="connBolt" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 3 14h7l-1 8 10-12h-7z"/></svg>
|
|
371
|
+
<span id="connLabel">Connect</span>
|
|
372
|
+
</button>
|
|
373
|
+
<div id="pairErr" style="margin-top:12px;font-size:12px;color:#ff9ea1;min-height:15px"></div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div style="display:flex;align-items:center;gap:8px;margin:16px 2px 14px;font-size:11.5px;color:#7a7a8c">
|
|
377
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#6ee7b7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V8a5 5 0 0 1 10 0v3"/></svg>
|
|
378
|
+
<span>Your session and credentials never leave this machine.</span>
|
|
379
|
+
</div>
|
|
380
|
+
<div id="envChips" style="display:flex;flex-wrap:wrap;gap:8px;padding:0 2px"></div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<!-- CONNECTED -->
|
|
384
|
+
<div id="connView" style="display:none">
|
|
385
|
+
<div style="animation:fadeUp .5s ease both">
|
|
386
|
+
<div style="position:relative;overflow:hidden;border-radius:22px;background:linear-gradient(135deg,rgba(16,185,129,.12),rgba(20,20,28,.55));backdrop-filter:blur(28px);border:1px solid rgba(52,211,153,.28);padding:22px;box-shadow:0 24px 60px -22px rgba(0,0,0,.7);margin-bottom:14px">
|
|
387
|
+
<div style="display:flex;align-items:center;gap:13px">
|
|
388
|
+
<div style="width:42px;height:42px;border-radius:12px;background:rgba(52,211,153,.13);border:1px solid rgba(52,211,153,.28);display:flex;align-items:center;justify-content:center;flex-shrink:0"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></div>
|
|
389
|
+
<div style="flex:1;min-width:0">
|
|
390
|
+
<div id="machineName" style="font-size:15px;font-weight:700;color:#eafff6;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></div>
|
|
391
|
+
<div style="display:flex;align-items:center;gap:6px;margin-top:3px">
|
|
392
|
+
<span style="font-size:10px;font-weight:700;letter-spacing:.09em;color:#6c8c7e">SESSION</span>
|
|
393
|
+
<span id="sessionId" style="font-size:11.5px;color:#8fc7b3;font-family:'JetBrains Mono',monospace"></span>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
<div style="border-radius:18px;background:rgba(8,8,13,.72);border:1px solid rgba(255,255,255,.08);overflow:hidden">
|
|
400
|
+
<div style="display:flex;align-items:center;gap:8px;padding:11px 15px;border-bottom:1px solid rgba(255,255,255,.06)">
|
|
401
|
+
<span style="width:7px;height:7px;border-radius:50%;background:#34d399;box-shadow:0 0 8px #34d399;animation:livePulse 1.6s ease-in-out infinite"></span>
|
|
402
|
+
<span style="font-size:11px;font-weight:700;letter-spacing:.06em;color:#9494a6">AGENT LOG</span>
|
|
403
|
+
<span style="margin-left:auto;display:flex;align-items:flex-end;gap:2px;height:12px">
|
|
404
|
+
<span style="width:2.5px;height:100%;border-radius:2px;background:#34d399;transform-origin:bottom;animation:barPulse 1s ease-in-out infinite"></span>
|
|
405
|
+
<span style="width:2.5px;height:100%;border-radius:2px;background:#34d399;transform-origin:bottom;animation:barPulse 1s ease-in-out infinite .2s"></span>
|
|
406
|
+
<span style="width:2.5px;height:100%;border-radius:2px;background:#34d399;transform-origin:bottom;animation:barPulse 1s ease-in-out infinite .4s"></span>
|
|
407
|
+
</span>
|
|
408
|
+
</div>
|
|
409
|
+
<div id="log" style="padding:13px 15px;font-family:'JetBrains Mono',monospace;font-size:11px;line-height:1.9;max-height:210px;overflow-y:auto"></div>
|
|
410
|
+
</div>
|
|
302
411
|
</div>
|
|
303
412
|
</div>
|
|
304
|
-
|
|
413
|
+
|
|
414
|
+
</div>
|
|
305
415
|
</div>
|
|
306
416
|
</div>
|
|
307
417
|
|
|
308
418
|
<script>
|
|
309
|
-
|
|
310
|
-
|
|
419
|
+
var qa = {};
|
|
420
|
+
var el = function(id){ return document.getElementById(id); };
|
|
421
|
+
var es = null;
|
|
422
|
+
var codeFocused = false;
|
|
311
423
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
424
|
+
if (window.qagentDesktop) {
|
|
425
|
+
el("winctl").style.display = "flex";
|
|
426
|
+
qa.win = function(a){ try { window.qagentDesktop[a](); } catch(e){} };
|
|
427
|
+
} else {
|
|
428
|
+
qa.win = function(){};
|
|
317
429
|
}
|
|
318
430
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const b = document.createElement("span"); b.className = "badge " + (ev.status==="pass"?"pass":"fail"); b.textContent = ev.status.toUpperCase();
|
|
332
|
-
m = \`\${ev.ticket||""} \${ev.caseCode||""}\`;
|
|
333
|
-
row.appendChild(mkTime(ev.ts)); const mm = document.createElement("span"); mm.className="m"; mm.textContent=m; row.appendChild(mm); row.appendChild(b);
|
|
334
|
-
feed.prepend(row); return;
|
|
431
|
+
// Explicit, user-driven update flow (desktop only). We ANNOUNCE a new version
|
|
432
|
+
// and only download/install when the user clicks — nothing happens silently.
|
|
433
|
+
(function(){
|
|
434
|
+
if (!window.qagentUpdate) return;
|
|
435
|
+
var bar = el("updateBar"), title = el("updTitle"), sub = el("updSub"), btn = el("updBtn");
|
|
436
|
+
var stage = null; // "download" (offer to download) | "install" (offer to restart)
|
|
437
|
+
function show(){ bar.style.display = "flex"; }
|
|
438
|
+
btn.addEventListener("click", function(){
|
|
439
|
+
if (stage === "install"){ window.qagentUpdate.install(); return; }
|
|
440
|
+
if (stage === "download"){
|
|
441
|
+
title.textContent = "Downloading update\\u2026"; sub.textContent = "0%";
|
|
442
|
+
btn.style.display = "none"; window.qagentUpdate.download();
|
|
335
443
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
444
|
+
});
|
|
445
|
+
window.qagentUpdate.on(function(ch, d){
|
|
446
|
+
d = d || {};
|
|
447
|
+
if (ch === "update:available"){
|
|
448
|
+
stage = "download";
|
|
449
|
+
title.textContent = "Version " + d.version + " available";
|
|
450
|
+
sub.textContent = "A newer Local Agent is ready to install.";
|
|
451
|
+
btn.style.display = ""; btn.textContent = "Install"; show();
|
|
452
|
+
} else if (ch === "update:progress"){
|
|
453
|
+
sub.textContent = (d.percent || 0) + "% downloaded";
|
|
454
|
+
} else if (ch === "update:downloaded"){
|
|
455
|
+
stage = "install";
|
|
456
|
+
title.textContent = "Update ready";
|
|
457
|
+
sub.textContent = "Restart to finish installing v" + d.version + ".";
|
|
458
|
+
btn.style.display = ""; btn.textContent = "Restart & install"; show();
|
|
459
|
+
}
|
|
460
|
+
// update:none / update:error → leave the bar as-is (hidden or mid-flow).
|
|
461
|
+
});
|
|
462
|
+
// Check now (the renderer is already subscribed) and every 30 minutes after.
|
|
463
|
+
try { window.qagentUpdate.check(); } catch(e){}
|
|
464
|
+
setInterval(function(){ try { window.qagentUpdate.check(); } catch(e){} }, 1800000);
|
|
465
|
+
})();
|
|
466
|
+
|
|
467
|
+
var PILL = {
|
|
468
|
+
idle: { label:"Not connected", dot:"#7a7a8c", color:"#a0a0b2", bg:"rgba(255,255,255,.05)", border:"rgba(255,255,255,.1)", anim:"none" },
|
|
469
|
+
connecting: { label:"Connecting\\u2026", dot:"#a78bfa", color:"#c4b5fd", bg:"rgba(139,92,246,.16)", border:"rgba(139,92,246,.35)", anim:"livePulse 1.4s ease-in-out infinite" },
|
|
470
|
+
connected: { label:"Connected \\u00b7 Live", dot:"#34d399", color:"#6ee7b7", bg:"rgba(16,185,129,.14)", border:"rgba(52,211,153,.35)", anim:"livePulse 1.6s ease-in-out infinite" }
|
|
471
|
+
};
|
|
472
|
+
function setPill(k){
|
|
473
|
+
var p = PILL[k]; var box = el("pill");
|
|
474
|
+
box.style.background = p.bg; box.style.border = "1px solid " + p.border;
|
|
475
|
+
el("pillDot").style.background = p.dot; el("pillDot").style.boxShadow = "0 0 8px " + p.dot; el("pillDot").style.animation = p.anim;
|
|
476
|
+
el("pillLabel").textContent = p.label; el("pillLabel").style.color = p.color;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function code(){ return el("code").value; }
|
|
480
|
+
function canConnect(){ return el("server").value.trim() && code().length === 6; }
|
|
481
|
+
function paintConnectBtn(){
|
|
482
|
+
var b = el("connect"), ok = canConnect();
|
|
483
|
+
if (ok) { b.style.background = "linear-gradient(135deg,#8b5cf6,#6366f1)"; b.style.color = "#fff"; b.style.cursor = "pointer"; b.style.boxShadow = "0 12px 30px -10px rgba(139,92,246,.8)"; }
|
|
484
|
+
else { b.style.background = "rgba(255,255,255,.06)"; b.style.color = "#6c6c7e"; b.style.cursor = "not-allowed"; b.style.boxShadow = "none"; }
|
|
485
|
+
}
|
|
486
|
+
function renderCells(){
|
|
487
|
+
var v = code();
|
|
488
|
+
for (var i=0;i<6;i++){
|
|
489
|
+
var cell = el("cell"+i);
|
|
490
|
+
var filled = i < v.length;
|
|
491
|
+
var active = codeFocused && i === v.length && v.length < 6;
|
|
492
|
+
cell.style.border = active ? "1.5px solid #8b5cf6" : (filled ? "1.5px solid rgba(255,255,255,.16)" : "1.5px solid rgba(255,255,255,.09)");
|
|
493
|
+
cell.style.boxShadow = active ? "0 0 0 3px rgba(139,92,246,.18)" : "none";
|
|
494
|
+
cell.querySelector(".d").textContent = v[i] || "";
|
|
495
|
+
cell.querySelector(".cr").style.display = active ? "inline-block" : "none";
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function timeStr(ts){ return new Date(ts).toTimeString().slice(0,8); }
|
|
500
|
+
function logColor(t){
|
|
501
|
+
if (t === "error" || t === "auth-error") return "#ff9ea1";
|
|
502
|
+
if (t === "auth-waiting") return "#fbbf24";
|
|
503
|
+
if (t === "auth-captured") return "#6ee7b7";
|
|
504
|
+
if (t === "job-claimed") return "#c4b5fd";
|
|
505
|
+
if (t === "job-complete") return "#6ee7b7";
|
|
506
|
+
return "#8b8b9e";
|
|
507
|
+
}
|
|
508
|
+
function logMsg(ev){
|
|
509
|
+
switch (ev.type){
|
|
510
|
+
case "job-claimed": return "claimed execution #" + ev.executionId + " \\u00b7 run " + ev.runCode + " (" + ev.total + " spec" + (ev.total===1?"":"s") + ")";
|
|
511
|
+
case "auth-waiting": return "waiting for manual login\\u2026" + (ev.url?" ("+ev.url+")":"");
|
|
512
|
+
case "auth-captured": return "login captured \\u00b7 session saved locally";
|
|
513
|
+
case "case-running": return "running " + ev.index + "/" + ev.total + " \\u00b7 " + (ev.ticket||"") + " " + (ev.caseCode||"");
|
|
514
|
+
case "case-result": return (ev.status==="pass"?"\\u2713":"\\u2717") + " " + (ev.ticket||"") + " " + (ev.caseCode||"");
|
|
515
|
+
case "progress": return null;
|
|
516
|
+
case "job-complete": return "execution #" + ev.executionId + " complete \\u00b7 " + ev.passed + " passed, " + ev.failed + " failed";
|
|
517
|
+
case "error": return ev.message || "error";
|
|
518
|
+
case "log": return ev.message || "";
|
|
519
|
+
default: return null;
|
|
342
520
|
}
|
|
343
|
-
if (!m) return;
|
|
344
|
-
row.appendChild(mkTime(ev.ts));
|
|
345
|
-
const mm = document.createElement("span"); mm.className = "m"; mm.textContent = m; row.appendChild(mm);
|
|
346
|
-
feed.prepend(row);
|
|
347
521
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
522
|
+
function addLog(ev){
|
|
523
|
+
if (ev.type === "agent-status") return; // status events aren't log lines; pill is driven by view state
|
|
524
|
+
var msg = logMsg(ev); if (!msg) return;
|
|
525
|
+
var row = document.createElement("div");
|
|
526
|
+
row.style.cssText = "display:flex;gap:10px;animation:logIn .4s ease both";
|
|
527
|
+
var t = document.createElement("span"); t.style.cssText = "color:#5c5c6e;flex-shrink:0"; t.textContent = timeStr(ev.ts);
|
|
528
|
+
var m = document.createElement("span"); m.style.color = logColor(ev.type); m.textContent = msg;
|
|
529
|
+
row.appendChild(t); row.appendChild(m);
|
|
530
|
+
var log = el("log"); log.appendChild(row); log.scrollTop = log.scrollHeight;
|
|
531
|
+
}
|
|
532
|
+
function openStream(){ if (es) return; es = new EventSource("/api/events"); es.onmessage = function(e){ try { addLog(JSON.parse(e.data)); } catch(x){} }; }
|
|
533
|
+
function closeStream(){ if (es){ es.close(); es = null; } }
|
|
353
534
|
|
|
354
|
-
|
|
355
|
-
|
|
535
|
+
function renderChips(s){
|
|
536
|
+
var chips = [
|
|
537
|
+
{ name:"Playwright", ver:s.playwrightVersion || "\\u2014", dot:"#34d399" },
|
|
538
|
+
{ name:"Node", ver:(s.nodeVersion||"").replace(/^v/,"") || "\\u2014", dot:"#6ee7b7" },
|
|
539
|
+
{ name:"Chromium", ver:"auto", dot:"#60a5fa" }
|
|
540
|
+
];
|
|
541
|
+
var html = "";
|
|
542
|
+
for (var i=0;i<chips.length;i++){
|
|
543
|
+
var c = chips[i];
|
|
544
|
+
html += '<span style="display:inline-flex;align-items:center;gap:6px;padding:5px 10px;border-radius:9px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);font-size:10.5px;color:#9494a6">'
|
|
545
|
+
+ '<span style="width:6px;height:6px;border-radius:50%;background:' + c.dot + '"></span>'
|
|
546
|
+
+ '<span style="color:#c3c3d0;font-weight:600">' + c.name + '</span>'
|
|
547
|
+
+ '<span style="font-family:\\'JetBrains Mono\\',monospace;color:#7a7a8c">' + c.ver + '</span></span>';
|
|
548
|
+
}
|
|
549
|
+
el("envChips").innerHTML = html;
|
|
550
|
+
}
|
|
356
551
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
552
|
+
function hostOf(u){ try { return new URL(u).host; } catch(e) { return u; } }
|
|
553
|
+
function applyServerView(){
|
|
554
|
+
// A known server (baked into the build, or from a prior pairing) collapses to
|
|
555
|
+
// a compact line so the user only deals with the 6-digit code; empty shows the
|
|
556
|
+
// full URL input.
|
|
557
|
+
var known = el("server").value.trim();
|
|
558
|
+
if (known){
|
|
559
|
+
el("serverKnown").style.display = "flex";
|
|
560
|
+
el("serverBlock").style.display = "none";
|
|
561
|
+
el("serverHost").textContent = hostOf(known);
|
|
367
562
|
} else {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (s.serverUrl) $("server").value = s.serverUrl;
|
|
563
|
+
el("serverKnown").style.display = "none";
|
|
564
|
+
el("serverBlock").style.display = "block";
|
|
371
565
|
}
|
|
372
566
|
}
|
|
567
|
+
function showForm(s){
|
|
568
|
+
el("formView").style.display = "block"; el("connView").style.display = "none";
|
|
569
|
+
el("hdrDisconnect").style.display = "none";
|
|
570
|
+
closeStream(); setPill("idle"); renderChips(s);
|
|
571
|
+
if (s.serverUrl) el("server").value = s.serverUrl;
|
|
572
|
+
applyServerView();
|
|
573
|
+
renderCells(); paintConnectBtn();
|
|
574
|
+
}
|
|
575
|
+
function showConnected(s){
|
|
576
|
+
el("formView").style.display = "none"; el("connView").style.display = "block";
|
|
577
|
+
el("hdrDisconnect").style.display = "flex";
|
|
578
|
+
setPill("connected");
|
|
579
|
+
el("machineName").textContent = s.machine || "this machine";
|
|
580
|
+
el("sessionId").textContent = "device #" + (s.deviceId==null?"?":s.deviceId);
|
|
581
|
+
// The SSE stream replays the recent-events buffer on connect, so seed the log
|
|
582
|
+
// ONLY from the stream (seeding from /api/state too would double every line).
|
|
583
|
+
el("log").innerHTML = ""; openStream();
|
|
584
|
+
}
|
|
585
|
+
function refresh(){
|
|
586
|
+
fetch("/api/state").then(function(r){ return r.json(); }).then(function(s){
|
|
587
|
+
if (s.appVersion) el("versionTag").textContent = "v" + s.appVersion;
|
|
588
|
+
if (s.paired) showConnected(s); else showForm(s);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
373
591
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
592
|
+
el("server").addEventListener("input", paintConnectBtn);
|
|
593
|
+
el("server").addEventListener("focus", function(){ el("urlBox").style.borderColor = "rgba(139,92,246,.55)"; });
|
|
594
|
+
el("server").addEventListener("blur", function(){ el("urlBox").style.borderColor = "rgba(255,255,255,.1)"; });
|
|
595
|
+
el("serverChange").addEventListener("click", function(e){
|
|
596
|
+
e.preventDefault();
|
|
597
|
+
el("serverKnown").style.display = "none";
|
|
598
|
+
el("serverBlock").style.display = "block";
|
|
599
|
+
el("server").focus();
|
|
600
|
+
});
|
|
601
|
+
el("code").addEventListener("input", function(){
|
|
602
|
+
el("code").value = el("code").value.replace(/[^0-9]/g,"").slice(0,6);
|
|
603
|
+
renderCells(); paintConnectBtn();
|
|
604
|
+
});
|
|
605
|
+
el("code").addEventListener("focus", function(){ codeFocused = true; renderCells(); });
|
|
606
|
+
el("code").addEventListener("blur", function(){ codeFocused = false; renderCells(); });
|
|
607
|
+
|
|
608
|
+
el("connect").addEventListener("click", function(){
|
|
609
|
+
if (!canConnect()) return;
|
|
610
|
+
el("pairErr").textContent = "";
|
|
611
|
+
el("connSpin").style.display = "block"; el("connBolt").style.display = "none";
|
|
612
|
+
el("connLabel").textContent = "Connecting\\u2026"; setPill("connecting");
|
|
613
|
+
fetch("/api/pair", { method:"POST", headers:{"Content-Type":"application/json"},
|
|
614
|
+
body: JSON.stringify({ code: code().trim(), server: el("server").value.trim() }) })
|
|
615
|
+
.then(function(r){ return r.json().then(function(j){ return { ok:r.ok, j:j }; }); })
|
|
616
|
+
.then(function(res){ if (!res.ok) throw new Error(res.j.error || "Pairing failed"); refresh(); })
|
|
617
|
+
.catch(function(e){ el("pairErr").textContent = e.message; setPill("idle"); })
|
|
618
|
+
.then(function(){
|
|
619
|
+
el("connSpin").style.display = "none"; el("connBolt").style.display = "block"; el("connLabel").textContent = "Connect";
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
el("hdrDisconnect").addEventListener("click", function(){
|
|
623
|
+
fetch("/api/disconnect", { method:"POST" }).then(function(){ closeStream(); refresh(); });
|
|
624
|
+
});
|
|
386
625
|
|
|
387
626
|
refresh();
|
|
388
627
|
</script>
|