plum-e2e 2.4.13 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/backend/_scaffold/utils/browser.ts +47 -2
- package/backend/_scaffold/utils/hooks.ts +25 -2
- package/backend/config/scripts/run-tests.js +35 -13
- package/backend/logs/runner-cmr8o2x3n0000mg01410tpuck.log +20 -0
- package/backend/logs/runner-cmr90hvcx0000n2016edtmrtv.log +43 -0
- package/backend/logs/runner-cmr90i7h20001n201g2wg6v3n.log +43 -0
- package/backend/logs/runner-cmr90iilm0002n201mmisva95.log +43 -0
- package/backend/logs/runner-cmr90j70r0003n201xz8525dz.log +20 -0
- package/backend/logs/runner-cmr90yup70000qp017ianc2gp.log +43 -0
- package/backend/logs/runner-cmr90zbtm0001qp01u9tfdkh1.log +20 -0
- package/backend/logs/runner-cmr91a3mw0000tb01414rd1df.log +20 -0
- package/backend/logs/runner-cmr91dhla0001tb01jp7thjnl.log +20 -0
- package/backend/prisma/migrations/20260706000000_add_report_logs/migration.sql +2 -0
- package/backend/prisma/migrations/20260707032554_add_perf_indexes/migration.sql +44 -0
- package/backend/prisma/schema.prisma +20 -0
- package/backend/routes/node.routes.js +42 -4
- package/backend/routes/reports.routes.js +4 -2
- package/backend/services/reportService.js +58 -20
- package/backend/services/runnerService.js +11 -1
- package/backend/websockets/socketHandler.js +162 -16
- package/bin/plum.js +96 -68
- package/frontend/package.json +1 -1
- package/frontend/src/lib/api/reports.js +16 -5
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +53 -55
- package/frontend/src/lib/components/ui/AutomatedBadge.svelte +56 -0
- package/frontend/src/lib/components/ui/BackLink.svelte +77 -0
- package/frontend/src/lib/components/ui/Badge.svelte +1 -1
- package/frontend/src/lib/components/ui/Button.svelte +1 -1
- package/frontend/src/lib/components/ui/CaseIdChip.svelte +59 -0
- package/frontend/src/lib/components/ui/ConfirmModal.svelte +1 -1
- package/frontend/src/lib/components/ui/Pagination.svelte +1 -1
- package/frontend/src/lib/components/ui/PriorityBadge.svelte +83 -0
- package/frontend/src/lib/components/ui/ResultChip.svelte +92 -0
- package/frontend/src/lib/components/ui/StatusDot.svelte +64 -0
- package/frontend/src/lib/components/ui/StepKeyword.svelte +79 -0
- package/frontend/src/lib/components/ui/StepStatusIcon.svelte +66 -0
- package/frontend/src/lib/components/ui/TagChip.svelte +51 -0
- package/frontend/src/lib/constants.js +14 -0
- package/frontend/src/lib/stores/runner.js +5 -3
- package/frontend/src/lib/styles/tokens.css +26 -0
- package/frontend/src/lib/utils/format.js +74 -0
- package/frontend/src/routes/+page.svelte +3 -3
- package/frontend/src/routes/login/+page.svelte +55 -40
- package/frontend/src/routes/reports/+page.svelte +35 -27
- package/frontend/src/routes/reports/[id]/+page.svelte +703 -215
- package/frontend/src/routes/reports/live/+page.svelte +281 -283
- package/frontend/src/routes/scheduled-tests/+page.svelte +3 -3
- package/frontend/src/routes/settings/+page.svelte +6 -6
- package/frontend/src/routes/setup/+page.svelte +2 -1
- package/frontend/src/routes/test-repository/+page.svelte +5 -5
- package/frontend/src/routes/test-repository/runs/[id]/+page.svelte +33 -152
- package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +4 -4
- package/frontend/vite.config.js +7 -1
- package/package.json +1 -1
|
@@ -16,51 +16,55 @@
|
|
|
16
16
|
-->
|
|
17
17
|
|
|
18
18
|
<script>
|
|
19
|
-
import {
|
|
19
|
+
import { afterUpdate, onDestroy } from 'svelte';
|
|
20
20
|
import { goto } from '$app/navigation';
|
|
21
21
|
import { fly, fade } from 'svelte/transition';
|
|
22
22
|
import { runnerState, cancelRun, activeCronJobs } from '$lib/stores/runner';
|
|
23
23
|
import { reportUrl } from '$lib/api/reports';
|
|
24
|
+
import { REDIRECT_DELAY_MS, ALL_TESTS_LABEL } from '$lib/constants';
|
|
25
|
+
import BackLink from '$lib/components/ui/BackLink.svelte';
|
|
26
|
+
import Badge from '$lib/components/ui/Badge.svelte';
|
|
24
27
|
|
|
25
28
|
let terminalEl;
|
|
26
|
-
let
|
|
29
|
+
let laneTerminalEl;
|
|
27
30
|
let redirectTimer = null;
|
|
28
|
-
let redirectCountdown =
|
|
31
|
+
let redirectCountdown = REDIRECT_DELAY_MS / 1000;
|
|
29
32
|
let countdownInterval = null;
|
|
33
|
+
let activeTab = null;
|
|
30
34
|
|
|
31
35
|
$: state = $runnerState;
|
|
32
36
|
$: isMulti = state.lanes.length > 0;
|
|
33
37
|
$: cronJobs = Object.keys($activeCronJobs);
|
|
34
38
|
$: anyCronRunning = cronJobs.length > 0;
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
// Keep active tab pointing at a valid lane
|
|
37
41
|
$: {
|
|
38
|
-
if (
|
|
39
|
-
|
|
42
|
+
if (state.lanes.length === 0) {
|
|
43
|
+
activeTab = null;
|
|
44
|
+
} else if (!state.lanes.find((l) => l.id === activeTab)) {
|
|
45
|
+
activeTab = state.lanes[0].id;
|
|
40
46
|
}
|
|
41
|
-
wasAnyCronRunning = anyCronRunning;
|
|
42
47
|
}
|
|
48
|
+
$: activeLane = state.lanes.find((l) => l.id === activeTab) ?? null;
|
|
49
|
+
|
|
50
|
+
let wasAnyCronRunning = false;
|
|
43
51
|
|
|
44
|
-
// Auto-scroll terminals
|
|
45
52
|
afterUpdate(() => {
|
|
46
53
|
if (terminalEl) terminalEl.scrollTop = terminalEl.scrollHeight;
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
if (laneTerminalEl) laneTerminalEl.scrollTop = laneTerminalEl.scrollHeight;
|
|
55
|
+
if (wasAnyCronRunning && !anyCronRunning && !state.running && !state.testCompleted) {
|
|
56
|
+
goto('/reports');
|
|
49
57
|
}
|
|
58
|
+
wasAnyCronRunning = anyCronRunning;
|
|
50
59
|
});
|
|
51
60
|
|
|
52
|
-
// Watch for test completion → auto-navigate to report
|
|
53
61
|
$: if (state.testCompleted && state.latestReportId && !redirectTimer) {
|
|
54
|
-
redirectCountdown =
|
|
62
|
+
redirectCountdown = REDIRECT_DELAY_MS / 1000;
|
|
55
63
|
countdownInterval = setInterval(() => {
|
|
56
64
|
redirectCountdown--;
|
|
57
|
-
if (redirectCountdown <= 0)
|
|
58
|
-
clearInterval(countdownInterval);
|
|
59
|
-
}
|
|
65
|
+
if (redirectCountdown <= 0) clearInterval(countdownInterval);
|
|
60
66
|
}, 1000);
|
|
61
|
-
redirectTimer = setTimeout(() =>
|
|
62
|
-
goto(reportUrl(state.latestReportId));
|
|
63
|
-
}, 3000);
|
|
67
|
+
redirectTimer = setTimeout(() => goto(reportUrl(state.latestReportId)), REDIRECT_DELAY_MS);
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
onDestroy(() => {
|
|
@@ -73,40 +77,13 @@
|
|
|
73
77
|
if (countdownInterval) clearInterval(countdownInterval);
|
|
74
78
|
goto(reportUrl(state.latestReportId));
|
|
75
79
|
}
|
|
76
|
-
|
|
77
|
-
function handleCancel() {
|
|
78
|
-
cancelRun();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function laneStatusColor(status) {
|
|
82
|
-
if (status === 'done') return 'var(--pass)';
|
|
83
|
-
if (status === 'error') return 'var(--fail)';
|
|
84
|
-
return 'var(--accent)';
|
|
85
|
-
}
|
|
86
80
|
</script>
|
|
87
81
|
|
|
88
82
|
<svelte:head><title>Live Run — Plum</title></svelte:head>
|
|
89
83
|
|
|
90
|
-
<
|
|
91
|
-
<a href="/reports" class="back-link">
|
|
92
|
-
<svg
|
|
93
|
-
width="14"
|
|
94
|
-
height="14"
|
|
95
|
-
viewBox="0 0 24 24"
|
|
96
|
-
fill="none"
|
|
97
|
-
stroke="currentColor"
|
|
98
|
-
stroke-width="2"
|
|
99
|
-
stroke-linecap="round"
|
|
100
|
-
>
|
|
101
|
-
<line x1="19" y1="12" x2="5" y2="12" />
|
|
102
|
-
<polyline points="12 19 5 12 12 5" />
|
|
103
|
-
</svg>
|
|
104
|
-
Reports
|
|
105
|
-
</a>
|
|
106
|
-
</div>
|
|
84
|
+
<BackLink href="/reports" label="Reports" />
|
|
107
85
|
|
|
108
86
|
{#if !state.running && !state.testCompleted && !anyCronRunning}
|
|
109
|
-
<!-- Nothing running and no completed test -->
|
|
110
87
|
<div class="idle-state">
|
|
111
88
|
<div class="idle-icon">
|
|
112
89
|
<svg
|
|
@@ -128,11 +105,8 @@
|
|
|
128
105
|
<a href="/reports" class="idle-link">View past reports →</a>
|
|
129
106
|
</div>
|
|
130
107
|
{:else if anyCronRunning && !state.running && !state.testCompleted}
|
|
131
|
-
<!-- Scheduled run(s) in progress -->
|
|
132
108
|
<div class="cron-running-state">
|
|
133
|
-
<div class="cron-running-icon">
|
|
134
|
-
<span class="cron-pulse-dot"></span>
|
|
135
|
-
</div>
|
|
109
|
+
<div class="cron-running-icon"><span class="cron-pulse-dot"></span></div>
|
|
136
110
|
<h2>Scheduled {cronJobs.length === 1 ? 'run' : 'runs'} in progress</h2>
|
|
137
111
|
<div class="cron-task-list">
|
|
138
112
|
{#each cronJobs as name}
|
|
@@ -142,7 +116,7 @@
|
|
|
142
116
|
<p>The report will open automatically when the run finishes.</p>
|
|
143
117
|
</div>
|
|
144
118
|
{:else}
|
|
145
|
-
<!-- Run header -->
|
|
119
|
+
<!-- ── Run header ── -->
|
|
146
120
|
<div
|
|
147
121
|
class="run-header"
|
|
148
122
|
class:header-pass={state.status === 'pass'}
|
|
@@ -150,42 +124,11 @@
|
|
|
150
124
|
>
|
|
151
125
|
<div class="header-left">
|
|
152
126
|
{#if state.running}
|
|
153
|
-
<span class="live-badge">
|
|
154
|
-
<span class="live-dot"></span>
|
|
155
|
-
Live
|
|
156
|
-
</span>
|
|
127
|
+
<span class="live-badge"><span class="live-dot"></span>Live</span>
|
|
157
128
|
{:else if state.status === 'pass'}
|
|
158
|
-
<
|
|
159
|
-
<svg
|
|
160
|
-
width="12"
|
|
161
|
-
height="12"
|
|
162
|
-
viewBox="0 0 24 24"
|
|
163
|
-
fill="none"
|
|
164
|
-
stroke="currentColor"
|
|
165
|
-
stroke-width="2.5"
|
|
166
|
-
stroke-linecap="round"
|
|
167
|
-
stroke-linejoin="round"
|
|
168
|
-
>
|
|
169
|
-
<polyline points="20 6 9 17 4 12" />
|
|
170
|
-
</svg>
|
|
171
|
-
Passed
|
|
172
|
-
</span>
|
|
129
|
+
<Badge variant="pass">Passed</Badge>
|
|
173
130
|
{:else}
|
|
174
|
-
<
|
|
175
|
-
<svg
|
|
176
|
-
width="12"
|
|
177
|
-
height="12"
|
|
178
|
-
viewBox="0 0 24 24"
|
|
179
|
-
fill="none"
|
|
180
|
-
stroke="currentColor"
|
|
181
|
-
stroke-width="2.5"
|
|
182
|
-
stroke-linecap="round"
|
|
183
|
-
stroke-linejoin="round"
|
|
184
|
-
>
|
|
185
|
-
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
186
|
-
</svg>
|
|
187
|
-
Failed
|
|
188
|
-
</span>
|
|
131
|
+
<Badge variant="fail">Failed</Badge>
|
|
189
132
|
{/if}
|
|
190
133
|
|
|
191
134
|
{#if state.currentRun}
|
|
@@ -194,7 +137,7 @@
|
|
|
194
137
|
<span class="run-title-label">{state.currentRun.runTitle}</span>
|
|
195
138
|
<span class="run-sep">·</span>
|
|
196
139
|
{/if}
|
|
197
|
-
<span class="run-tag-label">{state.currentRun.tag ||
|
|
140
|
+
<span class="run-tag-label">{state.currentRun.tag || ALL_TESTS_LABEL}</span>
|
|
198
141
|
<span class="run-sep">·</span>
|
|
199
142
|
<span class="run-detail"
|
|
200
143
|
>{state.currentRun.workers} worker{state.currentRun.workers !== 1 ? 's' : ''}</span
|
|
@@ -210,7 +153,7 @@
|
|
|
210
153
|
</div>
|
|
211
154
|
|
|
212
155
|
{#if state.running}
|
|
213
|
-
<button class="cancel-btn" on:click={
|
|
156
|
+
<button class="cancel-btn" on:click={cancelRun}>
|
|
214
157
|
<svg
|
|
215
158
|
width="12"
|
|
216
159
|
height="12"
|
|
@@ -230,7 +173,7 @@
|
|
|
230
173
|
{/if}
|
|
231
174
|
</div>
|
|
232
175
|
|
|
233
|
-
<!-- Completion
|
|
176
|
+
<!-- ── Completion bar ── -->
|
|
234
177
|
{#if state.testCompleted && state.latestReportId}
|
|
235
178
|
<div
|
|
236
179
|
class="completion-bar"
|
|
@@ -248,10 +191,8 @@
|
|
|
248
191
|
stroke="currentColor"
|
|
249
192
|
stroke-width="2.5"
|
|
250
193
|
stroke-linecap="round"
|
|
251
|
-
stroke-linejoin="round"
|
|
194
|
+
stroke-linejoin="round"><polyline points="20 6 9 17 4 12" /></svg
|
|
252
195
|
>
|
|
253
|
-
<polyline points="20 6 9 17 4 12" />
|
|
254
|
-
</svg>
|
|
255
196
|
All tests passed
|
|
256
197
|
{:else}
|
|
257
198
|
<svg
|
|
@@ -263,9 +204,8 @@
|
|
|
263
204
|
stroke-width="2.5"
|
|
264
205
|
stroke-linecap="round"
|
|
265
206
|
stroke-linejoin="round"
|
|
207
|
+
><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg
|
|
266
208
|
>
|
|
267
|
-
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
268
|
-
</svg>
|
|
269
209
|
Some tests failed
|
|
270
210
|
{/if}
|
|
271
211
|
</div>
|
|
@@ -282,83 +222,133 @@
|
|
|
282
222
|
stroke-width="2"
|
|
283
223
|
stroke-linecap="round"
|
|
284
224
|
stroke-linejoin="round"
|
|
225
|
+
><line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" /></svg
|
|
285
226
|
>
|
|
286
|
-
<line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" />
|
|
287
|
-
</svg>
|
|
288
227
|
</button>
|
|
289
228
|
</div>
|
|
290
229
|
</div>
|
|
291
230
|
{/if}
|
|
292
231
|
|
|
293
|
-
<!--
|
|
232
|
+
<!-- ── Multi-runner tabs ── -->
|
|
294
233
|
{#if isMulti}
|
|
295
|
-
|
|
296
|
-
<div
|
|
297
|
-
class="lanes-grid"
|
|
298
|
-
style="grid-template-columns: repeat({Math.min(state.lanes.length, 2)}, 1fr)"
|
|
299
|
-
>
|
|
234
|
+
<div class="lane-tabs">
|
|
300
235
|
{#each state.lanes as lane}
|
|
301
|
-
<
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
{
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
236
|
+
<button
|
|
237
|
+
class="lane-tab"
|
|
238
|
+
class:active={activeTab === lane.id}
|
|
239
|
+
on:click={() => (activeTab = lane.id)}
|
|
240
|
+
>
|
|
241
|
+
<span
|
|
242
|
+
class="tab-dot"
|
|
243
|
+
class:tab-dot-running={lane.status === 'running'}
|
|
244
|
+
class:tab-dot-done={lane.status === 'done'}
|
|
245
|
+
class:tab-dot-error={lane.status === 'error'}
|
|
246
|
+
></span>
|
|
247
|
+
{lane.name}
|
|
248
|
+
{#if lane.testCount}
|
|
249
|
+
<span class="tab-count">{lane.testCount}</span>
|
|
250
|
+
{/if}
|
|
251
|
+
</button>
|
|
252
|
+
{/each}
|
|
253
|
+
</div>
|
|
254
|
+
{/if}
|
|
255
|
+
|
|
256
|
+
<!-- ── Dual-stream view ── -->
|
|
257
|
+
<div class="run-view" class:no-top-radius={isMulti}>
|
|
258
|
+
<!-- Screenshot panel -->
|
|
259
|
+
<div class="screenshot-panel">
|
|
260
|
+
{#if isMulti}
|
|
261
|
+
{#if activeLane?.latestScreenshot}
|
|
262
|
+
{#key activeLane.latestScreenshot.data}
|
|
263
|
+
<img
|
|
264
|
+
src="data:image/jpeg;base64,{activeLane.latestScreenshot.data}"
|
|
265
|
+
class="live-shot"
|
|
266
|
+
alt="Live browser view"
|
|
267
|
+
in:fade={{ duration: 120 }}
|
|
268
|
+
/>
|
|
269
|
+
{/key}
|
|
270
|
+
<div class="step-overlay">
|
|
271
|
+
<span class="step-keyword">Step</span>
|
|
272
|
+
<span class="step-name">{activeLane.latestScreenshot.stepName}</span>
|
|
316
273
|
</div>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
<
|
|
320
|
-
|
|
274
|
+
{:else}
|
|
275
|
+
<div class="awaiting-state">
|
|
276
|
+
<svg
|
|
277
|
+
width="32"
|
|
278
|
+
height="32"
|
|
279
|
+
viewBox="0 0 24 24"
|
|
280
|
+
fill="none"
|
|
281
|
+
stroke="currentColor"
|
|
282
|
+
stroke-width="1.2"
|
|
283
|
+
stroke-linecap="round"
|
|
284
|
+
stroke-linejoin="round"
|
|
285
|
+
opacity="0.25"
|
|
286
|
+
>
|
|
287
|
+
<rect x="2" y="5" width="20" height="14" rx="2" />
|
|
288
|
+
<circle cx="12" cy="12" r="3" />
|
|
289
|
+
</svg>
|
|
290
|
+
<span>{activeLane?.status === 'running' ? 'Awaiting stream...' : 'No stream...'}</span>
|
|
321
291
|
</div>
|
|
322
|
-
|
|
323
|
-
|
|
292
|
+
{/if}
|
|
293
|
+
{:else if state.latestScreenshot}
|
|
294
|
+
{#key state.latestScreenshot.data}
|
|
295
|
+
<img
|
|
296
|
+
src="data:image/jpeg;base64,{state.latestScreenshot.data}"
|
|
297
|
+
class="live-shot"
|
|
298
|
+
alt="Live browser view"
|
|
299
|
+
in:fade={{ duration: 120 }}
|
|
300
|
+
/>
|
|
301
|
+
{/key}
|
|
302
|
+
<div class="step-overlay">
|
|
303
|
+
<span class="step-keyword">Step</span>
|
|
304
|
+
<span class="step-name">{state.latestScreenshot.stepName}</span>
|
|
324
305
|
</div>
|
|
325
|
-
{
|
|
306
|
+
{:else}
|
|
307
|
+
<div class="awaiting-state">
|
|
308
|
+
<svg
|
|
309
|
+
width="32"
|
|
310
|
+
height="32"
|
|
311
|
+
viewBox="0 0 24 24"
|
|
312
|
+
fill="none"
|
|
313
|
+
stroke="currentColor"
|
|
314
|
+
stroke-width="1.2"
|
|
315
|
+
stroke-linecap="round"
|
|
316
|
+
stroke-linejoin="round"
|
|
317
|
+
opacity="0.25"
|
|
318
|
+
>
|
|
319
|
+
<rect x="2" y="5" width="20" height="14" rx="2" />
|
|
320
|
+
<circle cx="12" cy="12" r="3" />
|
|
321
|
+
</svg>
|
|
322
|
+
<span>{state.running ? 'Awaiting stream...' : 'No stream'}</span>
|
|
323
|
+
</div>
|
|
324
|
+
{/if}
|
|
326
325
|
</div>
|
|
327
|
-
|
|
328
|
-
<!--
|
|
329
|
-
<div class="terminal-
|
|
326
|
+
|
|
327
|
+
<!-- Terminal panel -->
|
|
328
|
+
<div class="terminal-panel">
|
|
330
329
|
<div class="terminal-bar">
|
|
331
330
|
<span class="dot red"></span>
|
|
332
331
|
<span class="dot yellow"></span>
|
|
333
332
|
<span class="dot green"></span>
|
|
334
333
|
<span class="terminal-label">
|
|
335
|
-
{
|
|
334
|
+
{#if isMulti}
|
|
335
|
+
{activeLane?.name ?? 'Runner'}
|
|
336
|
+
{:else}
|
|
337
|
+
{state.running ? 'Running…' : 'Finished'}
|
|
338
|
+
{/if}
|
|
336
339
|
</span>
|
|
337
340
|
</div>
|
|
338
|
-
|
|
341
|
+
{#if isMulti}
|
|
342
|
+
<pre class="terminal" bind:this={laneTerminalEl}>{activeLane?.logs ||
|
|
343
|
+
'(waiting for output…)'}</pre>
|
|
344
|
+
{:else}
|
|
345
|
+
<pre class="terminal" bind:this={terminalEl}>{state.output}</pre>
|
|
346
|
+
{/if}
|
|
339
347
|
</div>
|
|
340
|
-
|
|
348
|
+
</div>
|
|
341
349
|
{/if}
|
|
342
350
|
|
|
343
351
|
<style>
|
|
344
|
-
.back-row {
|
|
345
|
-
margin-bottom: 1.5rem;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
.back-link {
|
|
349
|
-
display: inline-flex;
|
|
350
|
-
align-items: center;
|
|
351
|
-
gap: 0.35rem;
|
|
352
|
-
font-size: 0.8125rem;
|
|
353
|
-
color: var(--text-muted);
|
|
354
|
-
text-decoration: none;
|
|
355
|
-
transition: color var(--duration-fast);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
.back-link:hover {
|
|
359
|
-
color: var(--text);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
352
|
/* ── Idle state ── */
|
|
363
353
|
.idle-state {
|
|
364
354
|
display: flex;
|
|
@@ -369,31 +359,26 @@
|
|
|
369
359
|
text-align: center;
|
|
370
360
|
gap: 0.75rem;
|
|
371
361
|
}
|
|
372
|
-
|
|
373
362
|
.idle-icon {
|
|
374
363
|
color: var(--text-muted);
|
|
375
364
|
opacity: 0.4;
|
|
376
365
|
margin-bottom: 0.5rem;
|
|
377
366
|
}
|
|
378
|
-
|
|
379
367
|
.idle-state h2 {
|
|
380
368
|
font-size: 1.5rem;
|
|
381
369
|
font-weight: 400;
|
|
382
370
|
}
|
|
383
|
-
|
|
384
371
|
.idle-state p {
|
|
385
372
|
font-size: 0.9375rem;
|
|
386
373
|
color: var(--text-muted);
|
|
387
374
|
max-width: 360px;
|
|
388
375
|
}
|
|
389
|
-
|
|
390
376
|
.idle-link {
|
|
391
377
|
font-size: 0.875rem;
|
|
392
378
|
color: var(--accent);
|
|
393
379
|
text-decoration: none;
|
|
394
380
|
margin-top: 0.5rem;
|
|
395
381
|
}
|
|
396
|
-
|
|
397
382
|
.idle-link:hover {
|
|
398
383
|
text-decoration: underline;
|
|
399
384
|
}
|
|
@@ -408,11 +393,9 @@
|
|
|
408
393
|
text-align: center;
|
|
409
394
|
gap: 0.75rem;
|
|
410
395
|
}
|
|
411
|
-
|
|
412
396
|
.cron-running-icon {
|
|
413
397
|
margin-bottom: 0.5rem;
|
|
414
398
|
}
|
|
415
|
-
|
|
416
399
|
.cron-pulse-dot {
|
|
417
400
|
display: inline-block;
|
|
418
401
|
width: 18px;
|
|
@@ -421,7 +404,6 @@
|
|
|
421
404
|
background: var(--pass);
|
|
422
405
|
animation: pulse-pass 1.4s ease-in-out infinite;
|
|
423
406
|
}
|
|
424
|
-
|
|
425
407
|
@keyframes pulse-pass {
|
|
426
408
|
0%,
|
|
427
409
|
100% {
|
|
@@ -433,29 +415,25 @@
|
|
|
433
415
|
transform: scale(0.8);
|
|
434
416
|
}
|
|
435
417
|
}
|
|
436
|
-
|
|
437
418
|
.cron-running-state h2 {
|
|
438
419
|
font-size: 1.5rem;
|
|
439
420
|
font-weight: 400;
|
|
440
421
|
}
|
|
441
|
-
|
|
442
422
|
.cron-task-list {
|
|
443
423
|
display: flex;
|
|
444
424
|
flex-wrap: wrap;
|
|
445
425
|
gap: 0.5rem;
|
|
446
426
|
justify-content: center;
|
|
447
427
|
}
|
|
448
|
-
|
|
449
428
|
.cron-task-chip {
|
|
450
429
|
font-size: 0.8125rem;
|
|
451
430
|
font-weight: 500;
|
|
452
431
|
color: var(--accent);
|
|
453
432
|
background: var(--accent-soft);
|
|
454
433
|
border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
|
|
455
|
-
border-radius:
|
|
434
|
+
border-radius: var(--radius-pill);
|
|
456
435
|
padding: 0.2rem 0.65rem;
|
|
457
436
|
}
|
|
458
|
-
|
|
459
437
|
.cron-running-state p {
|
|
460
438
|
font-size: 0.9375rem;
|
|
461
439
|
color: var(--text-muted);
|
|
@@ -468,15 +446,13 @@
|
|
|
468
446
|
align-items: center;
|
|
469
447
|
justify-content: space-between;
|
|
470
448
|
gap: 1rem;
|
|
471
|
-
padding:
|
|
472
|
-
margin-bottom: 0;
|
|
449
|
+
padding: 0.875rem 1.25rem;
|
|
473
450
|
background: var(--bg-elevated);
|
|
474
451
|
border: 1px solid var(--border);
|
|
475
452
|
border-bottom: none;
|
|
476
453
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
477
454
|
animation: fadeUp 0.3s var(--ease-out) both;
|
|
478
455
|
}
|
|
479
|
-
|
|
480
456
|
.run-header.header-pass {
|
|
481
457
|
border-top: 3px solid var(--pass);
|
|
482
458
|
}
|
|
@@ -491,7 +467,6 @@
|
|
|
491
467
|
flex-wrap: wrap;
|
|
492
468
|
}
|
|
493
469
|
|
|
494
|
-
/* Live badge */
|
|
495
470
|
.live-badge {
|
|
496
471
|
display: inline-flex;
|
|
497
472
|
align-items: center;
|
|
@@ -500,12 +475,11 @@
|
|
|
500
475
|
font-weight: 600;
|
|
501
476
|
letter-spacing: 0.08em;
|
|
502
477
|
text-transform: uppercase;
|
|
503
|
-
color:
|
|
478
|
+
color: var(--white);
|
|
504
479
|
background: var(--accent);
|
|
505
480
|
padding: 0.2rem 0.6rem;
|
|
506
|
-
border-radius:
|
|
481
|
+
border-radius: var(--radius-pill);
|
|
507
482
|
}
|
|
508
|
-
|
|
509
483
|
.live-dot {
|
|
510
484
|
width: 6px;
|
|
511
485
|
height: 6px;
|
|
@@ -514,78 +488,39 @@
|
|
|
514
488
|
animation: dotPulse 1.2s ease-in-out infinite;
|
|
515
489
|
}
|
|
516
490
|
|
|
517
|
-
@keyframes dotPulse {
|
|
518
|
-
0%,
|
|
519
|
-
100% {
|
|
520
|
-
opacity: 1;
|
|
521
|
-
transform: scale(1);
|
|
522
|
-
}
|
|
523
|
-
50% {
|
|
524
|
-
opacity: 0.4;
|
|
525
|
-
transform: scale(0.65);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/* Result badges */
|
|
530
|
-
.result-badge {
|
|
531
|
-
display: inline-flex;
|
|
532
|
-
align-items: center;
|
|
533
|
-
gap: 0.4rem;
|
|
534
|
-
font-size: 0.7rem;
|
|
535
|
-
font-weight: 600;
|
|
536
|
-
letter-spacing: 0.08em;
|
|
537
|
-
text-transform: uppercase;
|
|
538
|
-
padding: 0.2rem 0.6rem;
|
|
539
|
-
border-radius: 100px;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
.pass-badge {
|
|
543
|
-
background: var(--pass-soft);
|
|
544
|
-
color: var(--pass);
|
|
545
|
-
}
|
|
546
|
-
.fail-badge {
|
|
547
|
-
background: var(--fail-soft);
|
|
548
|
-
color: var(--fail);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
491
|
.run-info {
|
|
552
492
|
display: flex;
|
|
553
493
|
align-items: center;
|
|
554
494
|
gap: 0.35rem;
|
|
555
495
|
flex-wrap: wrap;
|
|
556
496
|
}
|
|
557
|
-
|
|
558
497
|
.run-title-label {
|
|
559
498
|
font-size: 0.875rem;
|
|
560
499
|
font-weight: 500;
|
|
561
500
|
color: var(--accent);
|
|
562
501
|
background: var(--accent-soft);
|
|
563
502
|
border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
|
|
564
|
-
border-radius:
|
|
503
|
+
border-radius: var(--radius-pill);
|
|
565
504
|
padding: 0.15rem 0.6rem;
|
|
566
505
|
white-space: nowrap;
|
|
567
506
|
}
|
|
568
|
-
|
|
569
507
|
.run-tag-label {
|
|
570
508
|
font-family: 'JetBrains Mono', monospace;
|
|
571
509
|
font-size: 0.8125rem;
|
|
572
510
|
font-weight: 500;
|
|
573
511
|
color: var(--text);
|
|
574
512
|
}
|
|
575
|
-
|
|
576
513
|
.run-sep {
|
|
577
514
|
color: var(--text-muted);
|
|
578
515
|
opacity: 0.4;
|
|
579
516
|
font-size: 0.75rem;
|
|
580
517
|
}
|
|
581
|
-
|
|
582
518
|
.run-detail {
|
|
583
519
|
font-size: 0.8rem;
|
|
584
520
|
color: var(--text-muted);
|
|
585
521
|
font-family: 'JetBrains Mono', monospace;
|
|
586
522
|
}
|
|
587
523
|
|
|
588
|
-
/* Cancel button */
|
|
589
524
|
.cancel-btn {
|
|
590
525
|
display: inline-flex;
|
|
591
526
|
align-items: center;
|
|
@@ -604,10 +539,9 @@
|
|
|
604
539
|
color var(--duration-fast);
|
|
605
540
|
flex-shrink: 0;
|
|
606
541
|
}
|
|
607
|
-
|
|
608
542
|
.cancel-btn:hover {
|
|
609
543
|
background: var(--fail);
|
|
610
|
-
color:
|
|
544
|
+
color: var(--white);
|
|
611
545
|
}
|
|
612
546
|
|
|
613
547
|
/* ── Completion bar ── */
|
|
@@ -620,21 +554,17 @@
|
|
|
620
554
|
border: 1px solid var(--border);
|
|
621
555
|
border-top: none;
|
|
622
556
|
border-bottom: none;
|
|
623
|
-
animation: fadeUp 0.3s var(--ease-out) both;
|
|
624
557
|
}
|
|
625
|
-
|
|
626
558
|
.completion-bar.pass-bar {
|
|
627
559
|
background: var(--pass-soft);
|
|
628
560
|
border-color: color-mix(in srgb, var(--pass) 30%, var(--border));
|
|
629
561
|
color: var(--pass);
|
|
630
562
|
}
|
|
631
|
-
|
|
632
563
|
.completion-bar.fail-bar {
|
|
633
564
|
background: var(--fail-soft);
|
|
634
565
|
border-color: color-mix(in srgb, var(--fail) 30%, var(--border));
|
|
635
566
|
color: var(--fail);
|
|
636
567
|
}
|
|
637
|
-
|
|
638
568
|
.completion-left {
|
|
639
569
|
display: flex;
|
|
640
570
|
align-items: center;
|
|
@@ -642,20 +572,17 @@
|
|
|
642
572
|
font-size: 0.9rem;
|
|
643
573
|
font-weight: 500;
|
|
644
574
|
}
|
|
645
|
-
|
|
646
575
|
.completion-right {
|
|
647
576
|
display: flex;
|
|
648
577
|
align-items: center;
|
|
649
578
|
gap: 0.875rem;
|
|
650
579
|
}
|
|
651
|
-
|
|
652
580
|
.redirect-hint {
|
|
653
581
|
font-size: 0.8rem;
|
|
654
582
|
opacity: 0.7;
|
|
655
583
|
font-family: 'JetBrains Mono', monospace;
|
|
656
584
|
color: inherit;
|
|
657
585
|
}
|
|
658
|
-
|
|
659
586
|
.view-now-btn {
|
|
660
587
|
display: inline-flex;
|
|
661
588
|
align-items: center;
|
|
@@ -671,22 +598,152 @@
|
|
|
671
598
|
cursor: pointer;
|
|
672
599
|
transition: background var(--duration-fast);
|
|
673
600
|
}
|
|
674
|
-
|
|
675
601
|
.view-now-btn:hover {
|
|
676
602
|
background: rgba(0, 0, 0, 0.06);
|
|
677
603
|
}
|
|
678
604
|
|
|
679
|
-
/* ──
|
|
680
|
-
.
|
|
605
|
+
/* ── Lane tabs ── */
|
|
606
|
+
.lane-tabs {
|
|
607
|
+
display: flex;
|
|
608
|
+
gap: 2px;
|
|
609
|
+
padding: 0.5rem 1rem 0;
|
|
610
|
+
background: var(--bg-elevated);
|
|
611
|
+
border: 1px solid var(--border);
|
|
612
|
+
border-top: none;
|
|
613
|
+
border-bottom: none;
|
|
614
|
+
}
|
|
615
|
+
.lane-tab {
|
|
616
|
+
display: inline-flex;
|
|
617
|
+
align-items: center;
|
|
618
|
+
gap: 0.4rem;
|
|
619
|
+
padding: 0.375rem 0.875rem;
|
|
620
|
+
border: 1px solid transparent;
|
|
621
|
+
border-bottom: none;
|
|
622
|
+
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
|
623
|
+
background: transparent;
|
|
624
|
+
font-family: var(--font-body);
|
|
625
|
+
font-size: 0.8rem;
|
|
626
|
+
color: var(--text-muted);
|
|
627
|
+
cursor: pointer;
|
|
628
|
+
transition:
|
|
629
|
+
color var(--duration-fast),
|
|
630
|
+
background var(--duration-fast),
|
|
631
|
+
border-color var(--duration-fast);
|
|
632
|
+
white-space: nowrap;
|
|
633
|
+
}
|
|
634
|
+
.lane-tab:hover {
|
|
635
|
+
color: var(--text);
|
|
636
|
+
background: var(--bg-subtle);
|
|
637
|
+
}
|
|
638
|
+
.lane-tab.active {
|
|
639
|
+
color: var(--text);
|
|
640
|
+
background: var(--bg-elevated);
|
|
641
|
+
border-color: var(--border);
|
|
642
|
+
border-bottom-color: var(--bg-elevated);
|
|
643
|
+
}
|
|
644
|
+
.tab-dot {
|
|
645
|
+
width: 6px;
|
|
646
|
+
height: 6px;
|
|
647
|
+
border-radius: 50%;
|
|
648
|
+
flex-shrink: 0;
|
|
649
|
+
background: var(--accent);
|
|
650
|
+
}
|
|
651
|
+
.tab-dot-running {
|
|
652
|
+
background: var(--accent);
|
|
653
|
+
animation: dotPulse 1.6s ease-in-out infinite;
|
|
654
|
+
}
|
|
655
|
+
.tab-dot-done {
|
|
656
|
+
background: var(--pass);
|
|
657
|
+
}
|
|
658
|
+
.tab-dot-error {
|
|
659
|
+
background: var(--fail);
|
|
660
|
+
}
|
|
661
|
+
.tab-count {
|
|
662
|
+
font-size: 0.65rem;
|
|
663
|
+
font-weight: 600;
|
|
664
|
+
font-family: 'JetBrains Mono', monospace;
|
|
665
|
+
opacity: 0.55;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/* ── Dual-stream view ── */
|
|
669
|
+
.run-view {
|
|
670
|
+
display: grid;
|
|
671
|
+
grid-template-columns: 1fr 420px;
|
|
672
|
+
height: calc(100vh - 290px);
|
|
673
|
+
min-height: 380px;
|
|
681
674
|
border: 1px solid var(--border);
|
|
682
675
|
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
|
676
|
+
border-top: none;
|
|
683
677
|
overflow: hidden;
|
|
684
678
|
animation: fadeUp 0.35s var(--ease-out) 0.05s both;
|
|
685
679
|
}
|
|
680
|
+
.run-view.no-top-radius {
|
|
681
|
+
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
|
682
|
+
}
|
|
686
683
|
|
|
687
|
-
|
|
688
|
-
.
|
|
689
|
-
|
|
684
|
+
/* ── Screenshot panel ── */
|
|
685
|
+
.screenshot-panel {
|
|
686
|
+
position: relative;
|
|
687
|
+
background: var(--terminal-bg);
|
|
688
|
+
display: flex;
|
|
689
|
+
align-items: center;
|
|
690
|
+
justify-content: center;
|
|
691
|
+
overflow: hidden;
|
|
692
|
+
border-right: 1px solid var(--border);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.live-shot {
|
|
696
|
+
max-width: 100%;
|
|
697
|
+
max-height: 100%;
|
|
698
|
+
object-fit: contain;
|
|
699
|
+
display: block;
|
|
700
|
+
border-radius: 2px;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.step-overlay {
|
|
704
|
+
position: absolute;
|
|
705
|
+
bottom: 0;
|
|
706
|
+
left: 0;
|
|
707
|
+
right: 0;
|
|
708
|
+
padding: 1.25rem 1rem 0.625rem;
|
|
709
|
+
background: linear-gradient(transparent, rgba(0, 0, 0, 0.75));
|
|
710
|
+
display: flex;
|
|
711
|
+
align-items: baseline;
|
|
712
|
+
gap: 0.5rem;
|
|
713
|
+
pointer-events: none;
|
|
714
|
+
}
|
|
715
|
+
.step-keyword {
|
|
716
|
+
font-size: 0.65rem;
|
|
717
|
+
font-weight: 700;
|
|
718
|
+
letter-spacing: 0.1em;
|
|
719
|
+
text-transform: uppercase;
|
|
720
|
+
color: var(--accent);
|
|
721
|
+
flex-shrink: 0;
|
|
722
|
+
}
|
|
723
|
+
.step-name {
|
|
724
|
+
font-size: 0.8125rem;
|
|
725
|
+
color: rgba(255, 255, 255, 0.85);
|
|
726
|
+
font-family: 'JetBrains Mono', monospace;
|
|
727
|
+
white-space: nowrap;
|
|
728
|
+
overflow: hidden;
|
|
729
|
+
text-overflow: ellipsis;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.awaiting-state {
|
|
733
|
+
display: flex;
|
|
734
|
+
flex-direction: column;
|
|
735
|
+
align-items: center;
|
|
736
|
+
gap: 0.625rem;
|
|
737
|
+
color: rgba(255, 255, 255, 0.2);
|
|
738
|
+
font-size: 0.8rem;
|
|
739
|
+
font-family: 'JetBrains Mono', monospace;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/* ── Terminal panel ── */
|
|
743
|
+
.terminal-panel {
|
|
744
|
+
display: flex;
|
|
745
|
+
flex-direction: column;
|
|
746
|
+
overflow: hidden;
|
|
690
747
|
}
|
|
691
748
|
|
|
692
749
|
.terminal-bar {
|
|
@@ -695,15 +752,14 @@
|
|
|
695
752
|
gap: 6px;
|
|
696
753
|
padding: 0.6rem 0.875rem;
|
|
697
754
|
background: rgba(0, 0, 0, 0.35);
|
|
755
|
+
flex-shrink: 0;
|
|
698
756
|
}
|
|
699
|
-
|
|
700
757
|
.dot {
|
|
701
758
|
display: block;
|
|
702
759
|
width: 10px;
|
|
703
760
|
height: 10px;
|
|
704
761
|
border-radius: 50%;
|
|
705
762
|
}
|
|
706
|
-
|
|
707
763
|
.dot.red {
|
|
708
764
|
background: #ff5f57;
|
|
709
765
|
}
|
|
@@ -713,7 +769,6 @@
|
|
|
713
769
|
.dot.green {
|
|
714
770
|
background: #28c840;
|
|
715
771
|
}
|
|
716
|
-
|
|
717
772
|
.terminal-label {
|
|
718
773
|
margin-left: auto;
|
|
719
774
|
font-size: 0.68rem;
|
|
@@ -731,14 +786,12 @@
|
|
|
731
786
|
background: var(--terminal-bg);
|
|
732
787
|
color: var(--terminal-text);
|
|
733
788
|
padding: 1rem 1.25rem;
|
|
734
|
-
|
|
735
|
-
min-height: 240px;
|
|
789
|
+
flex: 1;
|
|
736
790
|
overflow-y: auto;
|
|
737
791
|
white-space: pre-wrap;
|
|
738
792
|
word-break: break-word;
|
|
739
793
|
margin: 0;
|
|
740
794
|
}
|
|
741
|
-
|
|
742
795
|
.terminal::-webkit-scrollbar {
|
|
743
796
|
width: 4px;
|
|
744
797
|
}
|
|
@@ -749,59 +802,4 @@
|
|
|
749
802
|
background: rgba(255, 255, 255, 0.1);
|
|
750
803
|
border-radius: 2px;
|
|
751
804
|
}
|
|
752
|
-
|
|
753
|
-
/* ── Multi-runner lanes ── */
|
|
754
|
-
.lanes-grid {
|
|
755
|
-
display: grid;
|
|
756
|
-
gap: 1rem;
|
|
757
|
-
animation: fadeUp 0.35s var(--ease-out) both;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
.lane-terminal {
|
|
761
|
-
border: 1px solid var(--border);
|
|
762
|
-
border-radius: var(--radius-md);
|
|
763
|
-
overflow: hidden;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
.lane-header {
|
|
767
|
-
display: flex;
|
|
768
|
-
align-items: center;
|
|
769
|
-
gap: 0.5rem;
|
|
770
|
-
padding: 0.625rem 0.875rem;
|
|
771
|
-
background: var(--bg-subtle);
|
|
772
|
-
border-bottom: 1px solid var(--border);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
.lane-dot {
|
|
776
|
-
width: 7px;
|
|
777
|
-
height: 7px;
|
|
778
|
-
border-radius: 50%;
|
|
779
|
-
flex-shrink: 0;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
.lane-name {
|
|
783
|
-
font-size: 0.8125rem;
|
|
784
|
-
font-weight: 500;
|
|
785
|
-
color: var(--text);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
.lane-count {
|
|
789
|
-
font-size: 0.72rem;
|
|
790
|
-
color: var(--text-muted);
|
|
791
|
-
font-family: 'JetBrains Mono', monospace;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
.lane-status-text {
|
|
795
|
-
margin-left: auto;
|
|
796
|
-
font-size: 0.7rem;
|
|
797
|
-
font-weight: 600;
|
|
798
|
-
letter-spacing: 0.06em;
|
|
799
|
-
text-transform: uppercase;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
.lane-terminal .terminal {
|
|
803
|
-
height: 280px;
|
|
804
|
-
min-height: unset;
|
|
805
|
-
font-size: 0.75rem;
|
|
806
|
-
}
|
|
807
805
|
</style>
|