plum-e2e 2.8.6 → 2.9.0
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/backend/_scaffold/utils/browser.ts +184 -30
- package/backend/_scaffold/utils/hooks.ts +37 -8
- package/backend/app.js +0 -5
- package/backend/config/scripts/generate-report.js +2 -2
- package/backend/constants/socketEvents.js +7 -4
- package/backend/lib/reportFilename.js +1 -2
- package/backend/lib/{screenshotPoller.js → rrwebPoller.js} +7 -4
- package/backend/lib/serverBootstrap.js +14 -0
- package/backend/logs/runner-cmtbz5b1l0000mr0110b27w5k.log +8 -0
- package/backend/mcp/server.js +3 -47
- package/backend/package-lock.json +199 -1
- package/backend/package.json +3 -1
- package/backend/prisma/migrations/20260828120000_add_recording_and_split_runner_worker_count/migration.sql +39 -0
- package/backend/prisma/migrations/20260828140000_add_recording_started_ended_at/migration.sql +5 -0
- package/backend/prisma/migrations/20260828150000_strip_screenshot_refs_from_reports/migration.sql +34 -0
- package/backend/prisma/migrations/20260828160000_add_backup_include_reports/migration.sql +4 -0
- package/backend/prisma/schema.prisma +97 -70
- package/backend/routes/backup.routes.js +47 -1
- package/backend/routes/reports.routes.js +23 -0
- package/backend/server.js +1 -1
- package/backend/services/backupCronService.js +1 -1
- package/backend/services/backupService.js +134 -44
- package/backend/services/cronService.js +23 -15
- package/backend/services/nodeExecutionService.js +41 -15
- package/backend/services/nodeStreamRegistry.js +24 -0
- package/backend/services/reportService.js +167 -83
- package/backend/services/runnerService.js +19 -7
- package/backend/services/settingsService.js +6 -3
- package/backend/services/triggerService.js +20 -13
- package/backend/websockets/nodeSocketHandler.js +40 -0
- package/backend/websockets/socketHandler.js +9 -7
- package/frontend/.svelte-kit/generated/server/internal.js +1 -1
- package/frontend/package-lock.json +121 -32
- package/frontend/package.json +1 -0
- package/frontend/src/lib/api/reports.js +13 -4
- package/frontend/src/lib/api/settings.js +16 -1
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +9 -24
- package/frontend/src/lib/components/reports/ElementInspector.svelte +141 -0
- package/frontend/src/lib/components/reports/LiveReplayer.svelte +110 -0
- package/frontend/src/lib/components/reports/MultiTabTimeline.svelte +115 -0
- package/frontend/src/lib/components/reports/RecordingPlayer.svelte +786 -0
- package/frontend/src/lib/components/reports/StepsRail.svelte +109 -0
- package/frontend/src/lib/components/ui/CodeViewer.svelte +61 -0
- package/frontend/src/lib/constants.js +0 -1
- package/frontend/src/lib/copy/reports.js +18 -8
- package/frontend/src/lib/copy/settings.js +25 -4
- package/frontend/src/lib/socketEvents.js +7 -4
- package/frontend/src/lib/stores/runner.js +26 -3
- package/frontend/src/lib/styles/tokens.css +7 -0
- package/frontend/src/lib/utils/format.js +108 -2
- package/frontend/src/lib/utils/inspectElement.js +34 -0
- package/frontend/src/routes/reports/+page.svelte +79 -1
- package/frontend/src/routes/reports/[id]/+page.svelte +304 -495
- package/frontend/src/routes/reports/live/+page.svelte +236 -260
- package/frontend/src/routes/settings/+page.svelte +246 -8
- package/package.json +1 -1
- package/backend/playwright.config.js +0 -85
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
import { afterUpdate, onDestroy } from 'svelte';
|
|
8
8
|
import { goto } from '$app/navigation';
|
|
9
9
|
import { page } from '$app/stores';
|
|
10
|
-
import { fly
|
|
10
|
+
import { fly } from 'svelte/transition';
|
|
11
11
|
import { runnerState, cancelRun, backgroundRuns } from '$lib/stores/runner';
|
|
12
12
|
import { reportUrl } from '$lib/api/reports';
|
|
13
|
-
import { REDIRECT_DELAY_MS } from '$lib/constants';
|
|
13
|
+
import { REDIRECT_DELAY_MS, BUILTIN_RUNNER_ID } from '$lib/constants';
|
|
14
|
+
import LiveReplayer from '$lib/components/reports/LiveReplayer.svelte';
|
|
14
15
|
import { ALL_TESTS_LABEL, pluralize } from '$lib/copy/common';
|
|
15
16
|
import {
|
|
16
17
|
REPORTS_BACK_LABEL,
|
|
@@ -26,8 +27,6 @@
|
|
|
26
27
|
ALL_TESTS_PASSED,
|
|
27
28
|
SOME_TESTS_FAILED,
|
|
28
29
|
VIEW_REPORT_NOW_LABEL,
|
|
29
|
-
LIVE_STEP_LABEL,
|
|
30
|
-
LIVE_BROWSER_VIEW_ALT,
|
|
31
30
|
AWAITING_STREAM_LABEL,
|
|
32
31
|
NO_STREAM_LABEL,
|
|
33
32
|
RUNNER_LABEL,
|
|
@@ -37,7 +36,8 @@
|
|
|
37
36
|
runsInProgressHeading,
|
|
38
37
|
workersCountLabel,
|
|
39
38
|
redirectingIn,
|
|
40
|
-
runnersBadge
|
|
39
|
+
runnersBadge,
|
|
40
|
+
workerLabel
|
|
41
41
|
} from '$lib/copy/reports';
|
|
42
42
|
import { triggerLabel } from '$lib/utils/format';
|
|
43
43
|
import BackLink from '$lib/components/ui/BackLink.svelte';
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
status: 'idle',
|
|
52
52
|
lanes: [],
|
|
53
53
|
currentRun: null,
|
|
54
|
-
|
|
54
|
+
rrwebByLane: {}
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
let terminalEl;
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
let redirectCountdown = REDIRECT_DELAY_MS / 1000;
|
|
61
61
|
let countdownInterval = null;
|
|
62
62
|
let activeTab = null;
|
|
63
|
+
let activeWorkerId = null;
|
|
63
64
|
|
|
64
65
|
$: bgRunId = $page.url.searchParams.get('run');
|
|
65
66
|
$: state = bgRunId ? ($backgroundRuns[bgRunId] ?? EMPTY_BG_STATE) : $runnerState;
|
|
@@ -82,6 +83,21 @@
|
|
|
82
83
|
}
|
|
83
84
|
$: activeLane = state.lanes.find((l) => l.id === activeTab) ?? null;
|
|
84
85
|
|
|
86
|
+
// rrwebByLane is always keyed by lane id, even for a plain single-runner
|
|
87
|
+
// run (BUILTIN_RUNNER_ID) — so worker tabs work the same way regardless of
|
|
88
|
+
// whether this run actually has multiple runner lanes.
|
|
89
|
+
$: activeLaneId = isMulti ? activeTab : BUILTIN_RUNNER_ID;
|
|
90
|
+
$: laneWorkers = state.rrwebByLane[activeLaneId] ?? {};
|
|
91
|
+
$: workerIds = Object.keys(laneWorkers).sort((a, b) => Number(a) - Number(b));
|
|
92
|
+
$: {
|
|
93
|
+
if (workerIds.length === 0) {
|
|
94
|
+
activeWorkerId = null;
|
|
95
|
+
} else if (!workerIds.includes(activeWorkerId)) {
|
|
96
|
+
activeWorkerId = workerIds[0];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
$: activeWorkerEvents = activeWorkerId ? (laneWorkers[activeWorkerId]?.events ?? []) : [];
|
|
100
|
+
|
|
85
101
|
afterUpdate(() => {
|
|
86
102
|
if (terminalEl) terminalEl.scrollTop = terminalEl.scrollHeight;
|
|
87
103
|
if (laneTerminalEl) laneTerminalEl.scrollTop = laneTerminalEl.scrollHeight;
|
|
@@ -110,9 +126,8 @@
|
|
|
110
126
|
|
|
111
127
|
<svelte:head><title>{LIVE_PAGE_TITLE}</title></svelte:head>
|
|
112
128
|
|
|
113
|
-
<BackLink href="/reports" label={REPORTS_BACK_LABEL} />
|
|
114
|
-
|
|
115
129
|
{#if showIdle}
|
|
130
|
+
<BackLink href="/reports" label={REPORTS_BACK_LABEL} />
|
|
116
131
|
<div class="idle-state">
|
|
117
132
|
<div class="idle-icon">
|
|
118
133
|
<svg
|
|
@@ -134,6 +149,7 @@
|
|
|
134
149
|
<a href="/reports" class="idle-link">{VIEW_PAST_REPORTS_LINK}</a>
|
|
135
150
|
</div>
|
|
136
151
|
{:else if showPicker}
|
|
152
|
+
<BackLink href="/reports" label={REPORTS_BACK_LABEL} />
|
|
137
153
|
<div class="cron-running-state">
|
|
138
154
|
<div class="cron-running-icon"><span class="cron-pulse-dot"></span></div>
|
|
139
155
|
<h2>{runsInProgressHeading(runningBgList.length)}</h2>
|
|
@@ -147,101 +163,47 @@
|
|
|
147
163
|
<p>{SELECT_RUN_HINT}</p>
|
|
148
164
|
</div>
|
|
149
165
|
{:else}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
class="run-header"
|
|
153
|
-
class:header-pass={state.status === 'pass'}
|
|
154
|
-
class:header-fail={state.status === 'fail'}
|
|
155
|
-
>
|
|
156
|
-
<div class="header-left">
|
|
157
|
-
{#if state.running}
|
|
158
|
-
<span class="live-badge"><span class="live-dot"></span>{LIVE_BADGE_LABEL}</span>
|
|
159
|
-
{:else if state.status === 'pass'}
|
|
160
|
-
<Badge variant="pass">{PASSED_LABEL}</Badge>
|
|
161
|
-
{:else}
|
|
162
|
-
<Badge variant="fail">{FAILED_LABEL}</Badge>
|
|
163
|
-
{/if}
|
|
164
|
-
|
|
165
|
-
{#if state.currentRun}
|
|
166
|
-
<div class="run-info">
|
|
167
|
-
{#if state.currentRun.runTitle}
|
|
168
|
-
<span class="run-title-label">{state.currentRun.runTitle}</span>
|
|
169
|
-
<span class="run-sep">·</span>
|
|
170
|
-
{/if}
|
|
171
|
-
<span class="run-tag-label">{state.currentRun.tag || ALL_TESTS_LABEL}</span>
|
|
172
|
-
<span class="run-sep">·</span>
|
|
173
|
-
<span class="run-detail">{workersCountLabel(state.currentRun.workers)}</span>
|
|
174
|
-
<span class="run-sep">·</span>
|
|
175
|
-
<span class="run-detail">{state.currentRun.browser}</span>
|
|
176
|
-
{#if state.lanes.length > 1}
|
|
177
|
-
<span class="run-sep">·</span>
|
|
178
|
-
<span class="run-detail">{runnersBadge(state.lanes.length)}</span>
|
|
179
|
-
{/if}
|
|
180
|
-
</div>
|
|
181
|
-
{/if}
|
|
182
|
-
</div>
|
|
183
|
-
|
|
184
|
-
{#if state.running && !bgRunId}
|
|
185
|
-
<button class="cancel-btn" on:click={cancelRun}>
|
|
186
|
-
<svg
|
|
187
|
-
width="12"
|
|
188
|
-
height="12"
|
|
189
|
-
viewBox="0 0 24 24"
|
|
190
|
-
fill="none"
|
|
191
|
-
stroke="currentColor"
|
|
192
|
-
stroke-width="2"
|
|
193
|
-
stroke-linecap="round"
|
|
194
|
-
stroke-linejoin="round"
|
|
195
|
-
>
|
|
196
|
-
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
197
|
-
<line x1="9" y1="9" x2="15" y2="15" />
|
|
198
|
-
<line x1="15" y1="9" x2="9" y2="15" />
|
|
199
|
-
</svg>
|
|
200
|
-
{CANCEL_RUN_LABEL}
|
|
201
|
-
</button>
|
|
202
|
-
{/if}
|
|
203
|
-
</div>
|
|
204
|
-
|
|
205
|
-
<!-- ── Completion bar ── -->
|
|
206
|
-
{#if state.testCompleted && state.latestReportId}
|
|
166
|
+
<div class="live-fullscreen">
|
|
167
|
+
<!-- ── Run header ── -->
|
|
207
168
|
<div
|
|
208
|
-
class="
|
|
209
|
-
class:pass
|
|
210
|
-
class:fail
|
|
211
|
-
transition:fly={{ y: -8, duration: 250 }}
|
|
169
|
+
class="run-header"
|
|
170
|
+
class:header-pass={state.status === 'pass'}
|
|
171
|
+
class:header-fail={state.status === 'fail'}
|
|
212
172
|
>
|
|
213
|
-
<div class="
|
|
214
|
-
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
stroke-linecap="round"
|
|
223
|
-
stroke-linejoin="round"><polyline points="20 6 9 17 4 12" /></svg
|
|
224
|
-
>
|
|
225
|
-
{ALL_TESTS_PASSED}
|
|
173
|
+
<div class="header-left">
|
|
174
|
+
<div class="header-back">
|
|
175
|
+
<BackLink href="/reports" label={REPORTS_BACK_LABEL} />
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{#if state.running}
|
|
179
|
+
<span class="live-badge"><span class="live-dot"></span>{LIVE_BADGE_LABEL}</span>
|
|
180
|
+
{:else if state.status === 'pass'}
|
|
181
|
+
<Badge variant="pass">{PASSED_LABEL}</Badge>
|
|
226
182
|
{:else}
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
183
|
+
<Badge variant="fail">{FAILED_LABEL}</Badge>
|
|
184
|
+
{/if}
|
|
185
|
+
|
|
186
|
+
{#if state.currentRun}
|
|
187
|
+
<div class="run-info">
|
|
188
|
+
{#if state.currentRun.runTitle}
|
|
189
|
+
<span class="run-title-label">{state.currentRun.runTitle}</span>
|
|
190
|
+
<span class="run-sep">·</span>
|
|
191
|
+
{/if}
|
|
192
|
+
<span class="run-tag-label">{state.currentRun.tag || ALL_TESTS_LABEL}</span>
|
|
193
|
+
<span class="run-sep">·</span>
|
|
194
|
+
<span class="run-detail">{workersCountLabel(state.currentRun.workers)}</span>
|
|
195
|
+
<span class="run-sep">·</span>
|
|
196
|
+
<span class="run-detail">{state.currentRun.browser}</span>
|
|
197
|
+
{#if state.lanes.length > 1}
|
|
198
|
+
<span class="run-sep">·</span>
|
|
199
|
+
<span class="run-detail">{runnersBadge(state.lanes.length)}</span>
|
|
200
|
+
{/if}
|
|
201
|
+
</div>
|
|
239
202
|
{/if}
|
|
240
203
|
</div>
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
<button class="
|
|
244
|
-
{VIEW_REPORT_NOW_LABEL}
|
|
204
|
+
|
|
205
|
+
{#if state.running && !bgRunId}
|
|
206
|
+
<button class="cancel-btn" on:click={cancelRun}>
|
|
245
207
|
<svg
|
|
246
208
|
width="12"
|
|
247
209
|
height="12"
|
|
@@ -251,55 +213,117 @@
|
|
|
251
213
|
stroke-width="2"
|
|
252
214
|
stroke-linecap="round"
|
|
253
215
|
stroke-linejoin="round"
|
|
254
|
-
><line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" /></svg
|
|
255
216
|
>
|
|
217
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
218
|
+
<line x1="9" y1="9" x2="15" y2="15" />
|
|
219
|
+
<line x1="15" y1="9" x2="9" y2="15" />
|
|
220
|
+
</svg>
|
|
221
|
+
{CANCEL_RUN_LABEL}
|
|
256
222
|
</button>
|
|
257
|
-
|
|
223
|
+
{/if}
|
|
258
224
|
</div>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
225
|
+
|
|
226
|
+
<!-- ── Completion bar ── -->
|
|
227
|
+
{#if state.testCompleted && state.latestReportId}
|
|
228
|
+
<div
|
|
229
|
+
class="completion-bar"
|
|
230
|
+
class:pass-bar={state.status === 'pass'}
|
|
231
|
+
class:fail-bar={state.status === 'fail'}
|
|
232
|
+
transition:fly={{ y: -8, duration: 250 }}
|
|
233
|
+
>
|
|
234
|
+
<div class="completion-left">
|
|
235
|
+
{#if state.status === 'pass'}
|
|
236
|
+
<svg
|
|
237
|
+
width="16"
|
|
238
|
+
height="16"
|
|
239
|
+
viewBox="0 0 24 24"
|
|
240
|
+
fill="none"
|
|
241
|
+
stroke="currentColor"
|
|
242
|
+
stroke-width="2.5"
|
|
243
|
+
stroke-linecap="round"
|
|
244
|
+
stroke-linejoin="round"><polyline points="20 6 9 17 4 12" /></svg
|
|
245
|
+
>
|
|
246
|
+
{ALL_TESTS_PASSED}
|
|
247
|
+
{:else}
|
|
248
|
+
<svg
|
|
249
|
+
width="16"
|
|
250
|
+
height="16"
|
|
251
|
+
viewBox="0 0 24 24"
|
|
252
|
+
fill="none"
|
|
253
|
+
stroke="currentColor"
|
|
254
|
+
stroke-width="2.5"
|
|
255
|
+
stroke-linecap="round"
|
|
256
|
+
stroke-linejoin="round"
|
|
257
|
+
><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg
|
|
258
|
+
>
|
|
259
|
+
{SOME_TESTS_FAILED}
|
|
279
260
|
{/if}
|
|
280
|
-
</
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
261
|
+
</div>
|
|
262
|
+
<div class="completion-right">
|
|
263
|
+
<span class="redirect-hint">{redirectingIn(redirectCountdown)}</span>
|
|
264
|
+
<button class="view-now-btn" on:click={goNow}>
|
|
265
|
+
{VIEW_REPORT_NOW_LABEL}
|
|
266
|
+
<svg
|
|
267
|
+
width="12"
|
|
268
|
+
height="12"
|
|
269
|
+
viewBox="0 0 24 24"
|
|
270
|
+
fill="none"
|
|
271
|
+
stroke="currentColor"
|
|
272
|
+
stroke-width="2"
|
|
273
|
+
stroke-linecap="round"
|
|
274
|
+
stroke-linejoin="round"
|
|
275
|
+
><line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" /></svg
|
|
276
|
+
>
|
|
277
|
+
</button>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
{/if}
|
|
281
|
+
|
|
282
|
+
<!-- ── Multi-runner tabs ── -->
|
|
283
|
+
{#if isMulti}
|
|
284
|
+
<div class="lane-tabs">
|
|
285
|
+
{#each state.lanes as lane}
|
|
286
|
+
<button
|
|
287
|
+
class="lane-tab"
|
|
288
|
+
class:active={activeTab === lane.id}
|
|
289
|
+
on:click={() => (activeTab = lane.id)}
|
|
290
|
+
>
|
|
291
|
+
<span
|
|
292
|
+
class="tab-dot"
|
|
293
|
+
class:tab-dot-running={lane.status === 'running'}
|
|
294
|
+
class:tab-dot-done={lane.status === 'done'}
|
|
295
|
+
class:tab-dot-error={lane.status === 'error'}
|
|
296
|
+
></span>
|
|
297
|
+
{lane.name}
|
|
298
|
+
{#if lane.testCount}
|
|
299
|
+
<span class="tab-count">{lane.testCount}</span>
|
|
300
|
+
{/if}
|
|
301
|
+
</button>
|
|
302
|
+
{/each}
|
|
303
|
+
</div>
|
|
304
|
+
{/if}
|
|
305
|
+
|
|
306
|
+
<!-- ── Worker tabs (only when this lane has more than one) ── -->
|
|
307
|
+
{#if workerIds.length > 1}
|
|
308
|
+
<div class="lane-tabs worker-tabs">
|
|
309
|
+
{#each workerIds as workerId}
|
|
310
|
+
<button
|
|
311
|
+
class="lane-tab"
|
|
312
|
+
class:active={activeWorkerId === workerId}
|
|
313
|
+
on:click={() => (activeWorkerId = workerId)}
|
|
314
|
+
>
|
|
315
|
+
{workerLabel(workerId)}
|
|
316
|
+
</button>
|
|
317
|
+
{/each}
|
|
318
|
+
</div>
|
|
319
|
+
{/if}
|
|
320
|
+
|
|
321
|
+
<!-- ── Dual-stream view ── -->
|
|
322
|
+
<div class="run-view">
|
|
323
|
+
<!-- Live stream panel -->
|
|
324
|
+
<div class="stream-panel">
|
|
325
|
+
{#if activeWorkerEvents.length > 0}
|
|
326
|
+
<LiveReplayer events={activeWorkerEvents} />
|
|
303
327
|
{:else}
|
|
304
328
|
<div class="awaiting-state">
|
|
305
329
|
<svg
|
|
@@ -316,64 +340,32 @@
|
|
|
316
340
|
<rect x="2" y="5" width="20" height="14" rx="2" />
|
|
317
341
|
<circle cx="12" cy="12" r="3" />
|
|
318
342
|
</svg>
|
|
319
|
-
<span>{
|
|
320
|
-
>
|
|
343
|
+
<span>{state.running ? AWAITING_STREAM_LABEL : NO_STREAM_LABEL}</span>
|
|
321
344
|
</div>
|
|
322
345
|
{/if}
|
|
323
|
-
|
|
324
|
-
{#key state.latestScreenshot.data}
|
|
325
|
-
<img
|
|
326
|
-
src="data:image/jpeg;base64,{state.latestScreenshot.data}"
|
|
327
|
-
class="live-shot"
|
|
328
|
-
alt={LIVE_BROWSER_VIEW_ALT}
|
|
329
|
-
in:fade={{ duration: 120 }}
|
|
330
|
-
/>
|
|
331
|
-
{/key}
|
|
332
|
-
<div class="step-overlay">
|
|
333
|
-
<span class="step-keyword">{LIVE_STEP_LABEL}</span>
|
|
334
|
-
<span class="step-name">{state.latestScreenshot.stepName}</span>
|
|
335
|
-
</div>
|
|
336
|
-
{:else}
|
|
337
|
-
<div class="awaiting-state">
|
|
338
|
-
<svg
|
|
339
|
-
width="32"
|
|
340
|
-
height="32"
|
|
341
|
-
viewBox="0 0 24 24"
|
|
342
|
-
fill="none"
|
|
343
|
-
stroke="currentColor"
|
|
344
|
-
stroke-width="1.2"
|
|
345
|
-
stroke-linecap="round"
|
|
346
|
-
stroke-linejoin="round"
|
|
347
|
-
opacity="0.25"
|
|
348
|
-
>
|
|
349
|
-
<rect x="2" y="5" width="20" height="14" rx="2" />
|
|
350
|
-
<circle cx="12" cy="12" r="3" />
|
|
351
|
-
</svg>
|
|
352
|
-
<span>{state.running ? AWAITING_STREAM_LABEL : NO_STREAM_LABEL}</span>
|
|
353
|
-
</div>
|
|
354
|
-
{/if}
|
|
355
|
-
</div>
|
|
346
|
+
</div>
|
|
356
347
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
348
|
+
<!-- Terminal panel -->
|
|
349
|
+
<div class="terminal-panel">
|
|
350
|
+
<div class="terminal-bar">
|
|
351
|
+
<span class="dot red"></span>
|
|
352
|
+
<span class="dot yellow"></span>
|
|
353
|
+
<span class="dot green"></span>
|
|
354
|
+
<span class="terminal-label">
|
|
355
|
+
{#if isMulti}
|
|
356
|
+
{activeLane?.name ?? RUNNER_LABEL}
|
|
357
|
+
{:else}
|
|
358
|
+
{state.running ? RUNNING_LABEL : FINISHED_LABEL}
|
|
359
|
+
{/if}
|
|
360
|
+
</span>
|
|
361
|
+
</div>
|
|
362
|
+
{#if isMulti}
|
|
363
|
+
<pre class="terminal" bind:this={laneTerminalEl}>{activeLane?.logs ||
|
|
364
|
+
WAITING_FOR_OUTPUT}</pre>
|
|
365
|
+
{:else}
|
|
366
|
+
<pre class="terminal" bind:this={terminalEl}>{state.output}</pre>
|
|
367
|
+
{/if}
|
|
370
368
|
</div>
|
|
371
|
-
{#if isMulti}
|
|
372
|
-
<pre class="terminal" bind:this={laneTerminalEl}>{activeLane?.logs ||
|
|
373
|
-
WAITING_FOR_OUTPUT}</pre>
|
|
374
|
-
{:else}
|
|
375
|
-
<pre class="terminal" bind:this={terminalEl}>{state.output}</pre>
|
|
376
|
-
{/if}
|
|
377
369
|
</div>
|
|
378
370
|
</div>
|
|
379
371
|
{/if}
|
|
@@ -476,18 +468,35 @@
|
|
|
476
468
|
max-width: 360px;
|
|
477
469
|
}
|
|
478
470
|
|
|
471
|
+
/* ── Fullscreen run shell ──
|
|
472
|
+
Breaks out of PageShell's centered 1200px column so the live stream gets
|
|
473
|
+
the whole viewport; height is pinned to the viewport (minus Nav) so the
|
|
474
|
+
stream/log split scrolls internally instead of the page. */
|
|
475
|
+
.live-fullscreen {
|
|
476
|
+
width: 100vw;
|
|
477
|
+
position: relative;
|
|
478
|
+
left: 50%;
|
|
479
|
+
margin-left: -50vw;
|
|
480
|
+
margin-top: -2.5rem;
|
|
481
|
+
margin-bottom: -5rem;
|
|
482
|
+
padding-bottom: 5rem;
|
|
483
|
+
height: calc(100vh - 56px);
|
|
484
|
+
box-sizing: border-box;
|
|
485
|
+
display: flex;
|
|
486
|
+
flex-direction: column;
|
|
487
|
+
}
|
|
488
|
+
|
|
479
489
|
/* ── Run header ── */
|
|
480
490
|
.run-header {
|
|
481
491
|
display: flex;
|
|
482
492
|
align-items: center;
|
|
483
493
|
justify-content: space-between;
|
|
484
494
|
gap: 1rem;
|
|
485
|
-
padding: 0.
|
|
495
|
+
padding: 0.75rem clamp(1rem, 3vw, 2rem);
|
|
486
496
|
background: var(--bg-elevated);
|
|
487
|
-
border: 1px solid var(--border);
|
|
488
|
-
border-bottom: none;
|
|
489
|
-
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
497
|
+
border-bottom: 1px solid var(--border);
|
|
490
498
|
animation: fadeUp 0.3s var(--ease-out) both;
|
|
499
|
+
flex-shrink: 0;
|
|
491
500
|
}
|
|
492
501
|
.run-header.header-pass {
|
|
493
502
|
border-top: 3px solid var(--pass);
|
|
@@ -503,6 +512,16 @@
|
|
|
503
512
|
flex-wrap: wrap;
|
|
504
513
|
}
|
|
505
514
|
|
|
515
|
+
.header-back {
|
|
516
|
+
display: flex;
|
|
517
|
+
align-items: center;
|
|
518
|
+
padding-right: 0.875rem;
|
|
519
|
+
border-right: 1px solid var(--border);
|
|
520
|
+
}
|
|
521
|
+
.header-back :global(.back-row) {
|
|
522
|
+
margin-bottom: 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
506
525
|
.live-badge {
|
|
507
526
|
display: inline-flex;
|
|
508
527
|
align-items: center;
|
|
@@ -585,20 +604,19 @@
|
|
|
585
604
|
display: flex;
|
|
586
605
|
align-items: center;
|
|
587
606
|
justify-content: space-between;
|
|
588
|
-
padding: 0.75rem
|
|
607
|
+
padding: 0.75rem clamp(1rem, 3vw, 2rem);
|
|
589
608
|
gap: 1rem;
|
|
590
|
-
border: 1px solid var(--border);
|
|
591
|
-
|
|
592
|
-
border-bottom: none;
|
|
609
|
+
border-bottom: 1px solid var(--border);
|
|
610
|
+
flex-shrink: 0;
|
|
593
611
|
}
|
|
594
612
|
.completion-bar.pass-bar {
|
|
595
613
|
background: var(--pass-soft);
|
|
596
|
-
border-color: color-mix(in srgb, var(--pass) 30%, var(--border));
|
|
614
|
+
border-bottom-color: color-mix(in srgb, var(--pass) 30%, var(--border));
|
|
597
615
|
color: var(--pass);
|
|
598
616
|
}
|
|
599
617
|
.completion-bar.fail-bar {
|
|
600
618
|
background: var(--fail-soft);
|
|
601
|
-
border-color: color-mix(in srgb, var(--fail) 30%, var(--border));
|
|
619
|
+
border-bottom-color: color-mix(in srgb, var(--fail) 30%, var(--border));
|
|
602
620
|
color: var(--fail);
|
|
603
621
|
}
|
|
604
622
|
.completion-left {
|
|
@@ -642,11 +660,10 @@
|
|
|
642
660
|
.lane-tabs {
|
|
643
661
|
display: flex;
|
|
644
662
|
gap: 2px;
|
|
645
|
-
padding: 0.5rem 1rem 0;
|
|
663
|
+
padding: 0.5rem clamp(1rem, 3vw, 2rem) 0;
|
|
646
664
|
background: var(--bg-elevated);
|
|
647
|
-
border: 1px solid var(--border);
|
|
648
|
-
|
|
649
|
-
border-bottom: none;
|
|
665
|
+
border-bottom: 1px solid var(--border);
|
|
666
|
+
flex-shrink: 0;
|
|
650
667
|
}
|
|
651
668
|
.lane-tab {
|
|
652
669
|
display: inline-flex;
|
|
@@ -701,24 +718,20 @@
|
|
|
701
718
|
opacity: 0.55;
|
|
702
719
|
}
|
|
703
720
|
|
|
704
|
-
/* ── Dual-stream view ──
|
|
721
|
+
/* ── Dual-stream view ──
|
|
722
|
+
Fills whatever vertical space live-fullscreen has left over after the
|
|
723
|
+
header/tabs above it, rather than a fixed viewport-relative height. */
|
|
705
724
|
.run-view {
|
|
706
725
|
display: grid;
|
|
707
|
-
grid-template-columns: 1fr
|
|
708
|
-
|
|
709
|
-
min-height:
|
|
710
|
-
border: 1px solid var(--border);
|
|
711
|
-
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
|
712
|
-
border-top: none;
|
|
726
|
+
grid-template-columns: 1fr 340px;
|
|
727
|
+
flex: 1;
|
|
728
|
+
min-height: 0;
|
|
713
729
|
overflow: hidden;
|
|
714
730
|
animation: fadeUp 0.35s var(--ease-out) 0.05s both;
|
|
715
731
|
}
|
|
716
|
-
.run-view.no-top-radius {
|
|
717
|
-
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
|
718
|
-
}
|
|
719
732
|
|
|
720
|
-
/* ──
|
|
721
|
-
.
|
|
733
|
+
/* ── Live stream panel ── */
|
|
734
|
+
.stream-panel {
|
|
722
735
|
position: relative;
|
|
723
736
|
background: var(--terminal-bg);
|
|
724
737
|
display: flex;
|
|
@@ -728,43 +741,6 @@
|
|
|
728
741
|
border-right: 1px solid var(--border);
|
|
729
742
|
}
|
|
730
743
|
|
|
731
|
-
.live-shot {
|
|
732
|
-
max-width: 100%;
|
|
733
|
-
max-height: 100%;
|
|
734
|
-
object-fit: contain;
|
|
735
|
-
display: block;
|
|
736
|
-
border-radius: 2px;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
.step-overlay {
|
|
740
|
-
position: absolute;
|
|
741
|
-
bottom: 0;
|
|
742
|
-
left: 0;
|
|
743
|
-
right: 0;
|
|
744
|
-
padding: 1.25rem 1rem 0.625rem;
|
|
745
|
-
background: linear-gradient(transparent, rgba(0, 0, 0, 0.75));
|
|
746
|
-
display: flex;
|
|
747
|
-
align-items: baseline;
|
|
748
|
-
gap: 0.5rem;
|
|
749
|
-
pointer-events: none;
|
|
750
|
-
}
|
|
751
|
-
.step-keyword {
|
|
752
|
-
font-size: 0.65rem;
|
|
753
|
-
font-weight: 700;
|
|
754
|
-
letter-spacing: 0.1em;
|
|
755
|
-
text-transform: uppercase;
|
|
756
|
-
color: var(--accent);
|
|
757
|
-
flex-shrink: 0;
|
|
758
|
-
}
|
|
759
|
-
.step-name {
|
|
760
|
-
font-size: 0.8125rem;
|
|
761
|
-
color: rgba(255, 255, 255, 0.85);
|
|
762
|
-
font-family: 'JetBrains Mono', monospace;
|
|
763
|
-
white-space: nowrap;
|
|
764
|
-
overflow: hidden;
|
|
765
|
-
text-overflow: ellipsis;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
744
|
.awaiting-state {
|
|
769
745
|
display: flex;
|
|
770
746
|
flex-direction: column;
|