skopix 2.0.96 → 2.0.97
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/cli/commands/agent.js +7 -1
- package/cli/commands/dashboard.js +25 -10
- package/package.json +1 -1
- package/web/index.html +105 -46
package/cli/commands/agent.js
CHANGED
|
@@ -114,6 +114,10 @@ export async function agentCommand(options) {
|
|
|
114
114
|
console.log('');
|
|
115
115
|
console.log(' Machine : ' + chalk.white(machine));
|
|
116
116
|
console.log(' Server : ' + chalk.white(serverUrl));
|
|
117
|
+
const hasDisplay = process.platform !== 'linux' || !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY;
|
|
118
|
+
if (!hasDisplay) {
|
|
119
|
+
console.log(chalk.yellow(' ⚠ No display detected — headless only (replay/suites work, recording/debug require a display)'));
|
|
120
|
+
}
|
|
117
121
|
console.log('');
|
|
118
122
|
|
|
119
123
|
// Authenticate to identify this agent with the correct user
|
|
@@ -172,7 +176,9 @@ export async function agentCommand(options) {
|
|
|
172
176
|
|
|
173
177
|
ws.addEventListener('open', () => {
|
|
174
178
|
reconnectDelay = 2000;
|
|
175
|
-
|
|
179
|
+
// Detect if this machine has a display (headed browser capability)
|
|
180
|
+
const hasDisplay = process.platform !== 'linux' || !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY;
|
|
181
|
+
ws.send(JSON.stringify({ type: 'register', agentId, name: userName, machine, userId, hasDisplay }));
|
|
176
182
|
console.log(chalk.green(' ✔ Connected — waiting for jobs\n'));
|
|
177
183
|
});
|
|
178
184
|
|
|
@@ -31,16 +31,19 @@ export async function dashboardCommand(options) {
|
|
|
31
31
|
|
|
32
32
|
// ── AGENT REGISTRY ────────────────────────────────────────────────────────
|
|
33
33
|
// Tracks connected skopix agents (teammates' machines that do browser work)
|
|
34
|
-
const agents = new Map(); // agentId -> { id, name, machine, userId, ws, status, currentJob, connectedAt }
|
|
34
|
+
const agents = new Map(); // agentId -> { id, name, machine, userId, ws, status, currentJob, connectedAt, hasDisplay }
|
|
35
35
|
|
|
36
|
-
function getAvailableAgents() {
|
|
37
|
-
return [...agents.values()].filter(a =>
|
|
36
|
+
function getAvailableAgents(requireDisplay) {
|
|
37
|
+
return [...agents.values()].filter(a => {
|
|
38
|
+
if (a.status !== 'idle' || a.ws.readyState !== 1) return false;
|
|
39
|
+
if (requireDisplay && !a.hasDisplay) return false;
|
|
40
|
+
return true;
|
|
41
|
+
});
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
function getLeastBusyAgent(preferUserId) {
|
|
41
|
-
const available = getAvailableAgents();
|
|
44
|
+
function getLeastBusyAgent(preferUserId, requireDisplay) {
|
|
45
|
+
const available = getAvailableAgents(requireDisplay);
|
|
42
46
|
if (!available.length) return null;
|
|
43
|
-
// Prefer the agent belonging to the requesting user
|
|
44
47
|
const mine = available.find(a => a.userId === preferUserId);
|
|
45
48
|
if (mine) return mine;
|
|
46
49
|
return available[0];
|
|
@@ -1045,6 +1048,7 @@ export async function dashboardCommand(options) {
|
|
|
1045
1048
|
const list = [...agents.values()].map(a => ({
|
|
1046
1049
|
id: a.id, name: a.name, machine: a.machine, userId: a.userId,
|
|
1047
1050
|
status: a.status, currentJob: a.currentJob || null, connectedAt: a.connectedAt,
|
|
1051
|
+
hasDisplay: a.hasDisplay !== false,
|
|
1048
1052
|
}));
|
|
1049
1053
|
sendJSON(res, 200, list);
|
|
1050
1054
|
return;
|
|
@@ -1067,12 +1071,12 @@ export async function dashboardCommand(options) {
|
|
|
1067
1071
|
const config = JSON.parse(body);
|
|
1068
1072
|
if (!config.url) { sendJSON(res, 400, { error: 'url is required' }); return; }
|
|
1069
1073
|
|
|
1070
|
-
// In team mode, dispatch to
|
|
1074
|
+
// In team mode, dispatch to a headed agent (needs display for recording)
|
|
1071
1075
|
const userId = currentUser?.id || null;
|
|
1072
|
-
const agent = teamMode ? getLeastBusyAgent(userId) : null;
|
|
1076
|
+
const agent = teamMode ? getLeastBusyAgent(userId, true) : null;
|
|
1073
1077
|
|
|
1074
1078
|
if (teamMode && !agent) {
|
|
1075
|
-
sendJSON(res, 503, { error: 'No agent connected. Run "skopix agent --server ' + (req.headers.host || 'localhost:9000') + '" on your machine first.' });
|
|
1079
|
+
sendJSON(res, 503, { error: 'No agent with a display connected. Run "skopix agent --server ' + (req.headers.host || 'localhost:9000') + '" on your local machine first.' });
|
|
1076
1080
|
return;
|
|
1077
1081
|
}
|
|
1078
1082
|
|
|
@@ -1245,6 +1249,17 @@ export async function dashboardCommand(options) {
|
|
|
1245
1249
|
const broadcast = (line) => { run.output.push(line); run.listeners.forEach(l => l(line)); };
|
|
1246
1250
|
const broadcastRec = (line) => { recording.output.push(line); recording.listeners.forEach(l => l(line)); };
|
|
1247
1251
|
|
|
1252
|
+
// In team mode, check we have a headed agent available before starting
|
|
1253
|
+
if (teamMode) {
|
|
1254
|
+
const headedAgent = getLeastBusyAgent(currentUser?.id, true);
|
|
1255
|
+
if (!headedAgent) {
|
|
1256
|
+
sendJSON(res, 503, { error: 'No agent with a display connected. Run "skopix agent --server ' + (req.headers.host || 'localhost:9000') + '" on your local machine first to use debug mode.' });
|
|
1257
|
+
activeRuns.delete(runId);
|
|
1258
|
+
activeRecordings.delete(recordingId);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1248
1263
|
// Start the debug session asynchronously
|
|
1249
1264
|
(async () => {
|
|
1250
1265
|
const sessionDir = path.join(reportsDir, runId);
|
|
@@ -2338,7 +2353,7 @@ export async function dashboardCommand(options) {
|
|
|
2338
2353
|
|
|
2339
2354
|
if (msg.type === 'register') {
|
|
2340
2355
|
const agentId = msg.agentId || crypto.randomUUID();
|
|
2341
|
-
const agent = { id: agentId, name: msg.name || msg.machine, machine: msg.machine, userId: msg.userId || null, ws, status: 'idle', currentJob: null, connectedAt: Date.now() };
|
|
2356
|
+
const agent = { id: agentId, name: msg.name || msg.machine, machine: msg.machine, userId: msg.userId || null, ws, status: 'idle', currentJob: null, connectedAt: Date.now(), hasDisplay: msg.hasDisplay !== false };
|
|
2342
2357
|
agents.set(agentId, agent);
|
|
2343
2358
|
ws.agentId = agentId;
|
|
2344
2359
|
ws.send(JSON.stringify({ type: 'registered', agentId }));
|
package/package.json
CHANGED
package/web/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Skopix — Record. Replay. Ship with confidence.</title>
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%230d1117'/%3E%3Cpolygon points='16,6 26,11 16,16 6,11' fill='%2300d4ff'/%3E%3Cpolygon points='6,11 16,16 26,11 26,14 16,19 6,14' fill='%2300a8cc'/%3E%3Cpolygon points='6,14 16,19 26,14 26,17 16,22 6,17' fill='%23007a99'/%3E%3C/svg%3E">
|
|
8
|
-
<meta name="description" content="Skopix is
|
|
8
|
+
<meta name="description" content="Skopix is an AI-powered QA platform — record tests, build from a reusable element library, run on a shared team server with MFA, and catch regressions before your users do.">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
11
|
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
@@ -197,11 +197,12 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
197
197
|
<a href="#" class="nav-logo">SKOP<span>IX</span></a>
|
|
198
198
|
<ul class="nav-links">
|
|
199
199
|
<li><a href="#how">How it works</a></li>
|
|
200
|
+
<li><a href="#library">Step library</a></li>
|
|
200
201
|
<li><a href="#ai">AI providers</a></li>
|
|
201
|
-
<li><a href="#modes">
|
|
202
|
+
<li><a href="#modes">Deploy</a></li>
|
|
202
203
|
<li><a href="#install">Install</a></li>
|
|
203
204
|
<li><a href="#faq">FAQ</a></li>
|
|
204
|
-
<li><a href="
|
|
205
|
+
<li><a href="#install" class="nav-cta">Install →</a></li>
|
|
205
206
|
</ul>
|
|
206
207
|
</nav>
|
|
207
208
|
|
|
@@ -210,7 +211,7 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
210
211
|
<div class="hero-glow"></div>
|
|
211
212
|
<div class="hero-glow2"></div>
|
|
212
213
|
|
|
213
|
-
<div class="badge"><span class="badge-dot"></span>Record ·
|
|
214
|
+
<div class="badge"><span class="badge-dot"></span>Record · Build · Library · Team · MFA</div>
|
|
214
215
|
|
|
215
216
|
<h1 class="hero-title">
|
|
216
217
|
QA that works<br>
|
|
@@ -218,7 +219,7 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
218
219
|
</h1>
|
|
219
220
|
|
|
220
221
|
<p class="hero-sub">
|
|
221
|
-
|
|
222
|
+
Record tests by using your app. Build tests from a reusable element library. Deploy to a shared team server with user accounts and MFA. AI stabilises selectors, matches known elements automatically, and replays everything — deterministically — every time.
|
|
222
223
|
</p>
|
|
223
224
|
|
|
224
225
|
<div class="hero-actions">
|
|
@@ -235,16 +236,17 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
235
236
|
<span style="font-family:var(--mono);font-size:11px;color:var(--muted);margin-left:8px">skopix dashboard</span>
|
|
236
237
|
</div>
|
|
237
238
|
<div class="terminal-body">
|
|
238
|
-
<div><span class="t-cmd">$</span> SKOPIX_SECRET_KEY="..." skopix
|
|
239
|
-
<div class="t-out">✔ Dashboard running at http://
|
|
240
|
-
<div class="t-out">✔ Team mode
|
|
241
|
-
<div
|
|
242
|
-
<div
|
|
243
|
-
<div
|
|
244
|
-
<div><span class="t-pass">✓</span> <span class="t-step">[
|
|
245
|
-
<div><span class="t-pass">✓</span> <span class="t-step">[
|
|
246
|
-
<div><span class="t-pass">✓</span> <span class="t-step">[
|
|
247
|
-
<div
|
|
239
|
+
<div><span class="t-cmd">$</span> SKOPIX_SECRET_KEY="..." skopix start --host 0.0.0.0 --port 9000</div>
|
|
240
|
+
<div class="t-out">✔ Dashboard running at http://141.147.107.213:9000</div>
|
|
241
|
+
<div class="t-out">✔ Team mode · MFA enabled · 4 users online</div>
|
|
242
|
+
<div class="t-out">✔ Step library · 42 elements · 12 folders</div>
|
|
243
|
+
<div style="margin-top:12px;color:var(--muted2)">── Agent: Adils-Laptop.local (darwin) connected ────</div>
|
|
244
|
+
<div style="margin-top:4px;color:var(--muted2)">── Replaying: Create Chart With Sort ──────────────</div>
|
|
245
|
+
<div><span class="t-pass">✓</span> <span class="t-step">[1/18] CLICK — Login Button</span> <span style="color:var(--muted2);font-size:11px">← from library</span></div>
|
|
246
|
+
<div><span class="t-pass">✓</span> <span class="t-step">[5/18] CLICK — Category - Add New Chart Button</span> <span style="color:var(--muted2);font-size:11px">← from library</span></div>
|
|
247
|
+
<div><span class="t-pass">✓</span> <span class="t-step">[12/18] CLICK — Chart Editor - Save Chart</span></div>
|
|
248
|
+
<div><span class="t-pass">✓</span> <span class="t-step">[18/18] ASSERT — First row contains 60.00</span></div>
|
|
249
|
+
<div style="margin-top:8px"><span class="t-pass">✔ PASSED</span> <span style="color:var(--muted)">· 18 steps · 11.2s · run by adil aslam</span></div>
|
|
248
250
|
</div>
|
|
249
251
|
</div>
|
|
250
252
|
</section>
|
|
@@ -254,7 +256,7 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
254
256
|
<div class="how-inner">
|
|
255
257
|
<div class="section-label observe">How it works</div>
|
|
256
258
|
<h2 class="section-title observe">Record once.<br>Replay forever.</h2>
|
|
257
|
-
<p class="section-sub observe">No scripting. No selectors.
|
|
259
|
+
<p class="section-sub observe">No scripting. No selectors. Record by using your app, or build tests from a reusable element library — Skopix handles the rest.</p>
|
|
258
260
|
|
|
259
261
|
<div class="steps-grid">
|
|
260
262
|
<div class="step-item observe">
|
|
@@ -268,8 +270,8 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
268
270
|
<div class="step-item observe">
|
|
269
271
|
<div class="step-num">02 — AI PROCESSING</div>
|
|
270
272
|
<div class="step-icon">◆</div>
|
|
271
|
-
<div class="step-title">AI stabilises
|
|
272
|
-
<div class="step-desc">After
|
|
273
|
+
<div class="step-title">AI stabilises & matches</div>
|
|
274
|
+
<div class="step-desc">After recording, the LLM rewrites fragile positional selectors into stable ones (pi-test-identifier, aria-label, title, data-testid). It then scans your step library — if a recorded element matches a known library entry, it substitutes the canonical name and selector automatically. New elements go to pending review, not duplicated silently.</div>
|
|
273
275
|
<span class="step-tag">Gemini · OpenAI · Ollama</span>
|
|
274
276
|
</div>
|
|
275
277
|
|
|
@@ -290,19 +292,61 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
290
292
|
</div>
|
|
291
293
|
|
|
292
294
|
<div class="step-item observe">
|
|
293
|
-
<div class="step-num">05 —
|
|
295
|
+
<div class="step-num">05 — STEP LIBRARY</div>
|
|
294
296
|
<div class="step-icon">♻</div>
|
|
295
|
-
<div class="step-title">
|
|
296
|
-
<div class="step-desc">
|
|
297
|
-
<span class="step-tag">
|
|
297
|
+
<div class="step-title">Build a reusable element library</div>
|
|
298
|
+
<div class="step-desc">Harvest UI elements into the step library — name them, store stable selectors, organise into folders. Build new tests entirely from the library using the visual test builder. Every recording auto-matches known elements. Update an element's selector once and it propagates to all tests that use it instantly.</div>
|
|
299
|
+
<span class="step-tag">Library · Builder · Auto-sync</span>
|
|
298
300
|
</div>
|
|
299
301
|
|
|
300
302
|
<div class="step-item observe">
|
|
301
|
-
<div class="step-num">06 —
|
|
303
|
+
<div class="step-num">06 — TEAM & REPORTS</div>
|
|
302
304
|
<div class="step-icon">📋</div>
|
|
303
|
-
<div class="step-title">Full
|
|
304
|
-
<div class="step-desc">Every replay generates a full report
|
|
305
|
-
<span class="step-tag">Video ·
|
|
305
|
+
<div class="step-title">Full reports + team audit</div>
|
|
306
|
+
<div class="step-desc">Every replay generates a full report with video and step-by-step screenshots. Deploy to a cloud server — team members log in with MFA, run tests through their local agent, and all results stream back to the shared dashboard. Audit log tracks exactly who ran what test, when, and from which machine.</div>
|
|
307
|
+
<span class="step-tag">Video · MFA · Audit log</span>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</section>
|
|
312
|
+
|
|
313
|
+
<!-- STEP LIBRARY -->
|
|
314
|
+
<section id="library">
|
|
315
|
+
<div class="section-inner">
|
|
316
|
+
<div class="section-label observe">Step library</div>
|
|
317
|
+
<h2 class="section-title observe">One library.<br>Every test.</h2>
|
|
318
|
+
<p class="section-sub observe">Build a catalogue of reusable UI elements. Record a test — known elements are substituted automatically. Update a selector once and every test that uses it updates instantly.</p>
|
|
319
|
+
|
|
320
|
+
<div class="features-grid" style="margin-top:64px">
|
|
321
|
+
<div class="feature-card observe">
|
|
322
|
+
<div class="feature-icon" style="background:var(--cyan-dim)">🎯</div>
|
|
323
|
+
<div class="feature-title">Harvest elements</div>
|
|
324
|
+
<div class="feature-desc">Open the harvester, navigate your app, click "Capture" on any element. Name it, store the stable selector, assign a folder. That element is now in the library ready to use in any test.</div>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="feature-card observe">
|
|
327
|
+
<div class="feature-icon" style="background:var(--cyan-dim)">🔄</div>
|
|
328
|
+
<div class="feature-title">Auto-match on record</div>
|
|
329
|
+
<div class="feature-desc">Record a new test and click through your app. After AI processing, each step is compared against the library. Matching elements get the canonical name and selector substituted automatically — no manual cleanup.</div>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="feature-card observe">
|
|
332
|
+
<div class="feature-icon" style="background:var(--cyan-dim)">🔨</div>
|
|
333
|
+
<div class="feature-title">Visual test builder</div>
|
|
334
|
+
<div class="feature-desc">Build tests without recording. Pick elements from the library, choose an action (click, type, assert), set a value, preview in a real browser, save. Full debug and replay capability — identical to recorded tests.</div>
|
|
335
|
+
</div>
|
|
336
|
+
<div class="feature-card observe">
|
|
337
|
+
<div class="feature-icon" style="background:var(--cyan-dim)">⚡</div>
|
|
338
|
+
<div class="feature-title">One update, all tests</div>
|
|
339
|
+
<div class="feature-desc">Change an element's selector in the library — every test step using that selector updates instantly. Merge duplicate elements and all tests get the canonical version. Fragile selectors get a warning badge.</div>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="feature-card observe">
|
|
342
|
+
<div class="feature-icon" style="background:var(--cyan-dim)">📁</div>
|
|
343
|
+
<div class="feature-title">Folders & organisation</div>
|
|
344
|
+
<div class="feature-desc">Organise elements and tests into nested folders. Drag to reorder. Multi-select and move. Folders are separate from tags — tag for filtering, folder for structure. Same organisation system for both the library and All Tests.</div>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="feature-card observe">
|
|
347
|
+
<div class="feature-icon" style="background:var(--cyan-dim)">⏳</div>
|
|
348
|
+
<div class="feature-title">Pending review queue</div>
|
|
349
|
+
<div class="feature-desc">New elements from recordings don't enter the library silently. They queue in pending review — you name them properly, edit the selector if needed, then approve. The amber badge tells you how many are waiting.</div>
|
|
306
350
|
</div>
|
|
307
351
|
</div>
|
|
308
352
|
</div>
|
|
@@ -411,8 +455,8 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
411
455
|
<section id="modes">
|
|
412
456
|
<div class="section-inner">
|
|
413
457
|
<div class="section-label observe">Deployment modes</div>
|
|
414
|
-
<h2 class="section-title observe">Solo or
|
|
415
|
-
<p class="section-sub observe">
|
|
458
|
+
<h2 class="section-title observe">Solo, team, or<br>cloud-hosted.</h2>
|
|
459
|
+
<p class="section-sub observe">Run locally for personal use, share with your team on a local server, or deploy to a cloud VM for always-on access with MFA and API key authentication for agents.</p>
|
|
416
460
|
|
|
417
461
|
<div class="modes-grid">
|
|
418
462
|
<div class="mode-card mode-solo observe">
|
|
@@ -428,14 +472,15 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
428
472
|
</div>
|
|
429
473
|
|
|
430
474
|
<div class="mode-card mode-team observe">
|
|
431
|
-
<div class="mode-label" style="color:var(--purple)">⬡ TEAM MODE</div>
|
|
475
|
+
<div class="mode-label" style="color:var(--purple)">⬡ TEAM / CLOUD MODE</div>
|
|
432
476
|
<div class="mode-title">You and your team</div>
|
|
433
|
-
<div class="mode-desc">
|
|
477
|
+
<div class="mode-desc">Mandatory MFA for all users. User accounts with roles. Audit log. Shared step library. API keys for agents. Deploy to Oracle Cloud, Hetzner, or any Linux VM — teammates log in from anywhere, run tests via their local agent.</div>
|
|
434
478
|
<div class="mode-code">
|
|
435
479
|
<div><span style="color:var(--purple)">SKOPIX_SECRET_KEY</span>=<span style="color:var(--text)">"long-random-string"</span> \</div>
|
|
436
|
-
<div style="padding-left:16px"><span style="color:var(--cyan)">skopix
|
|
480
|
+
<div style="padding-left:16px"><span style="color:var(--cyan)">skopix start --host 0.0.0.0 --port 9000</span></div>
|
|
437
481
|
<div style="color:var(--muted);font-size:11px;margin-top:8px">→ Visit /setup to create the first admin</div>
|
|
438
|
-
<div style="color:var(--muted);font-size:11px">→
|
|
482
|
+
<div style="color:var(--muted);font-size:11px">→ Mandatory MFA on first login for every user</div>
|
|
483
|
+
<div style="color:var(--muted);font-size:11px">→ Agents connect with API keys — no MFA needed</div>
|
|
439
484
|
</div>
|
|
440
485
|
</div>
|
|
441
486
|
</div>
|
|
@@ -452,8 +497,8 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
452
497
|
<div style="color:var(--muted)">Run <code style="color:var(--text)">portix 9000 --name skopix</code> for an instant public URL. Teammates open it from anywhere.</div>
|
|
453
498
|
</div>
|
|
454
499
|
<div>
|
|
455
|
-
<div style="color:var(--cyan);margin-bottom:6px">Always-on ·
|
|
456
|
-
<div style="color:var(--muted)">
|
|
500
|
+
<div style="color:var(--cyan);margin-bottom:6px">Always-on · Cloud VM</div>
|
|
501
|
+
<div style="color:var(--muted)">Deploy to Oracle Cloud Free Tier, Hetzner (~£4/mo), or any Ubuntu VM. Dashboard runs 24/7 via PM2. Each teammate runs an agent locally — recordings open Chrome on their screen.</div>
|
|
457
502
|
</div>
|
|
458
503
|
</div>
|
|
459
504
|
</div>
|
|
@@ -506,10 +551,12 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
506
551
|
<div class="code-comment"># Each teammate runs this on their own machine:</div>
|
|
507
552
|
<div><span class="code-cmd">$</span> npm install -g skopix</div>
|
|
508
553
|
<div><span class="code-cmd">$</span> npx playwright install chromium</div>
|
|
554
|
+
<div class="code-comment"># Login with your dashboard credentials (triggers MFA):</div>
|
|
509
555
|
<div><span class="code-cmd">$</span> skopix agent --server http://HOST-IP:9000 --key "your-secret"</div>
|
|
510
|
-
<div class="code-
|
|
556
|
+
<div class="code-comment"># Or use an API key (no MFA — generate one in My Settings):</div>
|
|
557
|
+
<div><span class="code-cmd">$</span> skopix agent --server http://HOST-IP:9000 --key "secret" --api-key sk_live_...</div>
|
|
511
558
|
<div class="code-out">→ Chrome opens on YOUR machine when you trigger tests</div>
|
|
512
|
-
<div class="code-out">→
|
|
559
|
+
<div class="code-out">→ Results stream back to the shared cloud dashboard</div>
|
|
513
560
|
</div>
|
|
514
561
|
|
|
515
562
|
<div class="code-label observe">USING OLLAMA (LOCAL AI — NO API KEY NEEDED)</div>
|
|
@@ -526,16 +573,16 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
526
573
|
<div style="font-family:var(--mono);font-size:10px;color:var(--cyan);letter-spacing:0.15em;margin-bottom:16px">HOW IT ALL FITS TOGETHER</div>
|
|
527
574
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:24px;font-family:var(--mono);font-size:12px;line-height:1.8">
|
|
528
575
|
<div>
|
|
529
|
-
<div style="color:var(--white);margin-bottom:6px">
|
|
530
|
-
<div style="color:var(--muted)">Runs <code style="color:var(--cyan)">
|
|
576
|
+
<div style="color:var(--white);margin-bottom:6px">Dashboard on a cloud VM</div>
|
|
577
|
+
<div style="color:var(--muted)">Runs 24/7 via PM2. Everyone visits <code style="color:var(--cyan)">http://your-ip:9000</code> and logs in with email + MFA. No local install needed to use the dashboard.</div>
|
|
531
578
|
</div>
|
|
532
579
|
<div>
|
|
533
|
-
<div style="color:var(--white);margin-bottom:6px">Everyone runs an agent</div>
|
|
534
|
-
<div style="color:var(--muted)">Runs <code style="color:var(--cyan)">skopix agent</code>
|
|
580
|
+
<div style="color:var(--white);margin-bottom:6px">Everyone runs an agent locally</div>
|
|
581
|
+
<div style="color:var(--muted)">Runs <code style="color:var(--cyan)">skopix agent</code> on their machine with an API key. Their browser opens when they trigger a recording or replay.</div>
|
|
535
582
|
</div>
|
|
536
583
|
<div>
|
|
537
|
-
<div style="color:var(--white);margin-bottom:6px">
|
|
538
|
-
<div style="color:var(--muted)">
|
|
584
|
+
<div style="color:var(--white);margin-bottom:6px">Shared step library</div>
|
|
585
|
+
<div style="color:var(--muted)">One library, all team members. Record a new test — known elements auto-match. New elements go to pending review. Update a selector once, all tests update.</div>
|
|
539
586
|
</div>
|
|
540
587
|
</div>
|
|
541
588
|
</div>
|
|
@@ -585,7 +632,7 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
585
632
|
|
|
586
633
|
<details class="observe">
|
|
587
634
|
<summary>How is this different from Playwright's own recorder? <span class="faq-icon">+</span></summary>
|
|
588
|
-
<div class="faq-body">Playwright's recorder generates raw selectors as-is — often fragile positional ones. Skopix uses an LLM to <strong>stabilise</strong> those selectors into semantic, stable equivalents.
|
|
635
|
+
<div class="faq-body">Playwright's recorder generates raw selectors as-is — often fragile positional ones. Skopix uses an LLM to <strong>stabilise</strong> those selectors into semantic, stable equivalents. Beyond that, Skopix adds a full dashboard, step library with auto-matching, visual test builder, team mode with MFA, cloud deployment, audit log, debug recording, credential management, suite running, and session history with video. It's a complete QA platform, not just a code generator.</div>
|
|
589
636
|
</details>
|
|
590
637
|
|
|
591
638
|
<details class="observe">
|
|
@@ -593,6 +640,16 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
593
640
|
<div class="faq-body">Open the step editor (✏ on any test), find the failing step, and update the selector directly. Or use the ⏺ debug button on that step — Skopix replays everything up to that point in a real browser, then lets you record the correct action from scratch. New steps are AI-processed and inserted automatically.</div>
|
|
594
641
|
</details>
|
|
595
642
|
|
|
643
|
+
<details class="observe">
|
|
644
|
+
<summary>What is the Step Library? <span class="faq-icon">+</span></summary>
|
|
645
|
+
<div class="faq-body">The step library is a reusable catalogue of UI elements — each with a name, stable selector, and optional default action. Harvest elements from your app using the harvester tool, or they get added automatically from recordings (pending your review). Once in the library, the visual <strong>Test Builder</strong> lets you build new tests by picking elements and choosing actions — no recording needed. Update an element's selector once and every test using it updates automatically.</div>
|
|
646
|
+
</details>
|
|
647
|
+
|
|
648
|
+
<details class="observe">
|
|
649
|
+
<summary>How does MFA work and how do agents authenticate? <span class="faq-icon">+</span></summary>
|
|
650
|
+
<div class="faq-body">In team mode, MFA is mandatory for all users. On first login, users are prompted to scan a QR code with any TOTP app (Google Authenticator, Authy, 1Password). Every subsequent login requires email + password + 6-digit code. Admins can reset MFA for users who lose their device. For agents running headlessly, use API keys instead — generate one in My Settings, pass it with <code>--api-key sk_live_...</code>. No MFA required for API keys.</div>
|
|
651
|
+
</details>
|
|
652
|
+
|
|
596
653
|
<details class="observe">
|
|
597
654
|
<summary>How does the login/setup flow work? <span class="faq-icon">+</span></summary>
|
|
598
655
|
<div class="faq-body">Record your login flow once and mark it as <strong>Reusable</strong>. In any other test's editor, set that login test as the <strong>Setup</strong>. When that test runs, Skopix runs the login steps first in the same browser session — so the test starts already authenticated. No duplicating login steps across every test.</div>
|
|
@@ -610,7 +667,7 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
610
667
|
|
|
611
668
|
<details class="observe">
|
|
612
669
|
<summary>How do remote teammates connect? <span class="faq-icon">+</span></summary>
|
|
613
|
-
<div class="faq-body"><strong>Same office:</strong> connect at <code>http://your-ip:9000</code> on the same network. <strong>Remote:</strong> run <code>portix 9000 --name skopix</code>
|
|
670
|
+
<div class="faq-body"><strong>Same office:</strong> connect at <code>http://your-ip:9000</code> on the same network. <strong>Remote / cloud:</strong> deploy the dashboard to a cloud VM (Oracle Cloud Free Tier, Hetzner ~£4/mo) — teammates connect from anywhere at your public IP. <strong>Portix tunnel:</strong> run <code>portix 9000 --name skopix</code> for an instant public URL without a server. Each teammate still runs the agent locally — their Chrome opens on their own machine when tests run.</div>
|
|
614
671
|
</details>
|
|
615
672
|
|
|
616
673
|
<details class="observe">
|
|
@@ -634,8 +691,8 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
634
691
|
|
|
635
692
|
<!-- CTA -->
|
|
636
693
|
<section class="cta-section">
|
|
637
|
-
<div class="cta-title observe">
|
|
638
|
-
<p class="cta-sub observe">Free. Self-hosted.
|
|
694
|
+
<div class="cta-title observe">Record. Build. Deploy.<br><span style="color:var(--cyan)">Ship with confidence.</span></div>
|
|
695
|
+
<p class="cta-sub observe">Free. Self-hosted. Step library, team mode, MFA, cloud-ready. One command to get started.</p>
|
|
639
696
|
<div style="display:flex;gap:16px;justify-content:center;flex-wrap:wrap" class="observe">
|
|
640
697
|
<a href="#install" class="btn-primary">Install now →</a>
|
|
641
698
|
<a href="https://github.com/x444dyx/skopix" class="btn-ghost">GitHub</a>
|
|
@@ -648,7 +705,9 @@ footer { border-top: 1px solid var(--border); padding: 48px; display: flex; alig
|
|
|
648
705
|
<div class="footer-brand">SKOPIX</div>
|
|
649
706
|
<ul class="footer-links">
|
|
650
707
|
<li><a href="#how">How it works</a></li>
|
|
708
|
+
<li><a href="#library">Step library</a></li>
|
|
651
709
|
<li><a href="#ai">AI providers</a></li>
|
|
710
|
+
<li><a href="#modes">Deploy</a></li>
|
|
652
711
|
<li><a href="#install">Install</a></li>
|
|
653
712
|
<li><a href="#faq">FAQ</a></li>
|
|
654
713
|
<li><a href="https://github.com/x444dyx/skopix">GitHub</a></li>
|