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
package/bin/plum.js
CHANGED
|
@@ -247,20 +247,20 @@ function applyServerConfig(cfg) {
|
|
|
247
247
|
clack.log.success('docker-compose.override.yml written');
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
250
|
+
// Node's fetch resolves "localhost" to ::1 first on many Linux distros (Debian
|
|
251
|
+
// included). If Docker only published the port on IPv4, that first attempt hangs
|
|
252
|
+
// until it times out on every poll, eating the whole budget even though the port
|
|
253
|
+
// is reachable — and reachable fine from a browser, which races both families.
|
|
254
|
+
// 127.0.0.1 sidesteps the DNS/happy-eyeballs mismatch entirely.
|
|
255
|
+
const READY_POLL_INTERVAL_MS = 2000;
|
|
256
|
+
const READY_POLL_MAX_ATTEMPTS = 90; // ~3 minutes
|
|
257
|
+
|
|
258
|
+
async function waitForServerReady(apiBase) {
|
|
259
259
|
const s = clack.spinner();
|
|
260
260
|
s.start('Waiting for server to be ready…');
|
|
261
261
|
let ready = false;
|
|
262
|
-
for (let i = 0; i <
|
|
263
|
-
await new Promise((r) => setTimeout(r,
|
|
262
|
+
for (let i = 0; i < READY_POLL_MAX_ATTEMPTS; i++) {
|
|
263
|
+
await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
|
|
264
264
|
try {
|
|
265
265
|
const res = await fetch(`${apiBase}/auth/needs-setup`);
|
|
266
266
|
if (res.ok) {
|
|
@@ -268,54 +268,90 @@ async function serverStart() {
|
|
|
268
268
|
break;
|
|
269
269
|
}
|
|
270
270
|
} catch {}
|
|
271
|
+
if (i > 0 && i % 15 === 0) {
|
|
272
|
+
s.message(
|
|
273
|
+
`Still waiting for server to be ready… (${Math.round((i * READY_POLL_INTERVAL_MS) / 1000)}s — check "docker compose logs -f backend" if this feels stuck)`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
271
276
|
}
|
|
272
|
-
s.stop(
|
|
277
|
+
s.stop(
|
|
278
|
+
ready
|
|
279
|
+
? pc.green('✓ Server is ready')
|
|
280
|
+
: pc.yellow('Server did not respond in time — it may still be starting')
|
|
281
|
+
);
|
|
282
|
+
return ready;
|
|
283
|
+
}
|
|
273
284
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
async function runFirstUserSetup(apiBase, frontendPort) {
|
|
286
|
+
let needsSetup = false;
|
|
287
|
+
try {
|
|
288
|
+
const res = await fetch(`${apiBase}/auth/needs-setup`);
|
|
289
|
+
const data = await res.json();
|
|
290
|
+
needsSetup = data.needsSetup;
|
|
291
|
+
} catch {}
|
|
281
292
|
|
|
282
|
-
|
|
283
|
-
clack.log.info('No users found — create your first account to get started.');
|
|
293
|
+
if (!needsSetup) return;
|
|
284
294
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
295
|
+
if (!interactiveAllowed()) {
|
|
296
|
+
clack.log.info(
|
|
297
|
+
`No users found. Open ${pc.cyan(`http://localhost:${frontendPort}/setup`)} to create your first account.`
|
|
298
|
+
);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
clack.log.info('No users found — create your first account to get started.');
|
|
303
|
+
|
|
304
|
+
const name = await clack.text({ message: 'Your name', placeholder: 'Jane Smith' });
|
|
305
|
+
if (clack.isCancel(name)) {
|
|
306
|
+
clack.log.warn('Skipped. Create a user at /setup in the UI.');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const email = await clack.text({ message: 'Email address', placeholder: 'jane@example.com' });
|
|
310
|
+
if (clack.isCancel(email)) {
|
|
311
|
+
clack.log.warn('Skipped. Create a user at /setup in the UI.');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const password = await clack.password({ message: 'Password (min 8 characters)' });
|
|
315
|
+
if (clack.isCancel(password)) {
|
|
316
|
+
clack.log.warn('Skipped. Create a user at /setup in the UI.');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const res = await fetch(`${apiBase}/auth/setup`, {
|
|
322
|
+
method: 'POST',
|
|
323
|
+
headers: { 'Content-Type': 'application/json' },
|
|
324
|
+
body: JSON.stringify({ name, email, password })
|
|
325
|
+
});
|
|
326
|
+
if (res.ok) {
|
|
327
|
+
clack.log.success(`Account created for ${email}. You can now log in.`);
|
|
328
|
+
} else {
|
|
329
|
+
const err = await res.json();
|
|
330
|
+
clack.log.error(`Failed to create account: ${err.error ?? 'unknown error'}`);
|
|
318
331
|
}
|
|
332
|
+
} catch (e) {
|
|
333
|
+
clack.log.error(`Failed to create account: ${e.message}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function serverStart() {
|
|
338
|
+
clack.intro(pc.bgMagenta(pc.white(' 🟣 Plum — Server ')));
|
|
339
|
+
const cfg = await configureServer({ force: false });
|
|
340
|
+
applyServerConfig(cfg);
|
|
341
|
+
clack.log.info(`UI: ${pc.cyan(`http://localhost:${cfg.frontendPort}`)}`);
|
|
342
|
+
|
|
343
|
+
execSync('docker compose up --build -d', { cwd: plumRoot, stdio: 'inherit' });
|
|
344
|
+
|
|
345
|
+
const apiBase = `http://127.0.0.1:${cfg.backendPort}`;
|
|
346
|
+
const ready = await waitForServerReady(apiBase);
|
|
347
|
+
|
|
348
|
+
if (ready) {
|
|
349
|
+
await runFirstUserSetup(apiBase, cfg.frontendPort);
|
|
350
|
+
} else {
|
|
351
|
+
clack.log.warn(
|
|
352
|
+
`Could not confirm the backend is ready. Check ${pc.cyan('docker compose logs -f backend')}.\n` +
|
|
353
|
+
`Once it responds, open ${pc.cyan(`http://localhost:${cfg.frontendPort}/setup`)} to create your first account (if this is a fresh install).`
|
|
354
|
+
);
|
|
319
355
|
}
|
|
320
356
|
|
|
321
357
|
clack.log.info(`UI: ${pc.cyan(`http://localhost:${cfg.frontendPort}`)}`);
|
|
@@ -332,21 +368,13 @@ async function serverRestart() {
|
|
|
332
368
|
|
|
333
369
|
execSync('docker compose up --build -d', { cwd: plumRoot, stdio: 'inherit' });
|
|
334
370
|
|
|
335
|
-
const apiBase = `http://
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
const res = await fetch(`${apiBase}/auth/needs-setup`);
|
|
343
|
-
if (res.ok) {
|
|
344
|
-
ready = true;
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
} catch {}
|
|
371
|
+
const apiBase = `http://127.0.0.1:${cfg.backendPort}`;
|
|
372
|
+
const ready = await waitForServerReady(apiBase);
|
|
373
|
+
if (!ready) {
|
|
374
|
+
clack.log.warn(
|
|
375
|
+
`Could not confirm the backend is ready. Check ${pc.cyan('docker compose logs -f backend')}.`
|
|
376
|
+
);
|
|
348
377
|
}
|
|
349
|
-
s.stop(ready ? pc.green('✓ Server is ready') : pc.yellow('Server may still be starting'));
|
|
350
378
|
clack.log.info(`UI: ${pc.cyan(`http://localhost:${cfg.frontendPort}`)}`);
|
|
351
379
|
clack.log.info(`API: ${pc.cyan(`http://localhost:${cfg.backendPort}`)}`);
|
|
352
380
|
clack.outro(pc.green('Server restarted.'));
|
package/frontend/package.json
CHANGED
|
@@ -15,12 +15,23 @@
|
|
|
15
15
|
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { API_BASE } from '$lib/constants';
|
|
18
|
+
import { API_BASE, REPORTS_PER_PAGE } from '$lib/constants';
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
function withDate(r) {
|
|
21
|
+
return { ...r, date: new Date(r.createdAt).toLocaleString() };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function fetchReports({ page = 1, limit = REPORTS_PER_PAGE } = {}) {
|
|
25
|
+
const params = new URLSearchParams({ page, limit });
|
|
26
|
+
const res = await fetch(`${API_BASE}/reports?${params}`);
|
|
27
|
+
const { reports, total, passCount, failCount, trend } = await res.json();
|
|
28
|
+
return {
|
|
29
|
+
reports: reports.map(withDate),
|
|
30
|
+
total,
|
|
31
|
+
passCount,
|
|
32
|
+
failCount,
|
|
33
|
+
trend: trend.map(withDate)
|
|
34
|
+
};
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
export async function fetchLatestReportId() {
|
|
@@ -35,8 +35,19 @@
|
|
|
35
35
|
import { fetchRunners } from '$lib/api/runners';
|
|
36
36
|
import { fetchRuns, fetchRun } from '$lib/api/repository';
|
|
37
37
|
import { fetchIntegrations } from '$lib/api/settings';
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
API_BASE,
|
|
40
|
+
BROWSERS,
|
|
41
|
+
BUILTIN_RUNNER_ID,
|
|
42
|
+
BUILTIN_RUNNER_LABEL,
|
|
43
|
+
WORKERS_MIN,
|
|
44
|
+
WORKERS_MAX,
|
|
45
|
+
RUN_PICKER_LIMIT,
|
|
46
|
+
RUN_TAG_DISPLAY_LIMIT,
|
|
47
|
+
ALL_TESTS_LABEL
|
|
48
|
+
} from '$lib/constants';
|
|
39
49
|
import ConfirmModal from '$lib/components/ui/ConfirmModal.svelte';
|
|
50
|
+
import Badge from '$lib/components/ui/Badge.svelte';
|
|
40
51
|
|
|
41
52
|
let availableRunners = [];
|
|
42
53
|
let testRuns = [];
|
|
@@ -83,10 +94,10 @@
|
|
|
83
94
|
availableRunners = r;
|
|
84
95
|
// Drop any saved selection pointing at runners that no longer exist,
|
|
85
96
|
// so a deleted runner can't linger in the selection and break runs.
|
|
86
|
-
const validIds = new Set([
|
|
97
|
+
const validIds = new Set([BUILTIN_RUNNER_ID, ...r.map((x) => x.id)]);
|
|
87
98
|
runnerConfig.update((c) => {
|
|
88
99
|
const pruned = c.selectedRunners.filter((id) => validIds.has(id));
|
|
89
|
-
return { ...c, selectedRunners: pruned.length > 0 ? pruned : [
|
|
100
|
+
return { ...c, selectedRunners: pruned.length > 0 ? pruned : [BUILTIN_RUNNER_ID] };
|
|
90
101
|
});
|
|
91
102
|
})
|
|
92
103
|
.catch(() => {});
|
|
@@ -110,8 +121,8 @@
|
|
|
110
121
|
localStorage.setItem('plum:builtInEnabled', String(v));
|
|
111
122
|
} catch {}
|
|
112
123
|
runnerConfig.update((c) => {
|
|
113
|
-
if (!v && c.selectedRunners.includes(
|
|
114
|
-
const others = c.selectedRunners.filter((r) => r !==
|
|
124
|
+
if (!v && c.selectedRunners.includes(BUILTIN_RUNNER_ID)) {
|
|
125
|
+
const others = c.selectedRunners.filter((r) => r !== BUILTIN_RUNNER_ID);
|
|
115
126
|
return { ...c, selectedRunners: others.length > 0 ? others : c.selectedRunners };
|
|
116
127
|
}
|
|
117
128
|
return c;
|
|
@@ -155,7 +166,7 @@
|
|
|
155
166
|
s.on('runner-lanes-init', (lanes) => {
|
|
156
167
|
runnerState.update((r) => ({
|
|
157
168
|
...r,
|
|
158
|
-
lanes: lanes.map((l) => ({ ...l, status: 'running', logs: '' }))
|
|
169
|
+
lanes: lanes.map((l) => ({ ...l, status: 'running', logs: '', latestScreenshot: null }))
|
|
159
170
|
}));
|
|
160
171
|
});
|
|
161
172
|
|
|
@@ -173,6 +184,19 @@
|
|
|
173
184
|
}));
|
|
174
185
|
});
|
|
175
186
|
|
|
187
|
+
s.on('step-screenshot', ({ stepName, data }) => {
|
|
188
|
+
runnerState.update((r) => ({ ...r, latestScreenshot: { stepName, data } }));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
s.on('runner-lane-screenshot', ({ id, stepName, data }) => {
|
|
192
|
+
runnerState.update((r) => ({
|
|
193
|
+
...r,
|
|
194
|
+
lanes: r.lanes.map((l) =>
|
|
195
|
+
l.id === id ? { ...l, latestScreenshot: { stepName, data } } : l
|
|
196
|
+
)
|
|
197
|
+
}));
|
|
198
|
+
});
|
|
199
|
+
|
|
176
200
|
s.on('tests-changed', () => testsVersion.update((v) => v + 1));
|
|
177
201
|
s.on('report-ready', () => reportsVersion.update((v) => v + 1));
|
|
178
202
|
|
|
@@ -200,17 +224,20 @@
|
|
|
200
224
|
$: cfg = $runnerConfig;
|
|
201
225
|
|
|
202
226
|
$: truncatedRunTag = (() => {
|
|
203
|
-
if (!state.currentRun?.tag) return
|
|
227
|
+
if (!state.currentRun?.tag) return ALL_TESTS_LABEL;
|
|
204
228
|
const parts = state.currentRun.tag.split(/ or /i);
|
|
205
|
-
if (parts.length <=
|
|
206
|
-
return
|
|
229
|
+
if (parts.length <= RUN_TAG_DISPLAY_LIMIT) return state.currentRun.tag;
|
|
230
|
+
return (
|
|
231
|
+
parts.slice(0, RUN_TAG_DISPLAY_LIMIT).join(' or ') +
|
|
232
|
+
` +${parts.length - RUN_TAG_DISPLAY_LIMIT} more`
|
|
233
|
+
);
|
|
207
234
|
})();
|
|
208
235
|
$: cronJobs = Object.keys($activeCronJobs);
|
|
209
236
|
$: anyCronRunning = cronJobs.length > 0;
|
|
210
237
|
$: anyRunning = state.running || anyCronRunning;
|
|
211
238
|
|
|
212
239
|
$: if ($runsVersion >= 0)
|
|
213
|
-
fetchRuns({ limit:
|
|
240
|
+
fetchRuns({ limit: RUN_PICKER_LIMIT })
|
|
214
241
|
.then((r) => (testRuns = r.runs))
|
|
215
242
|
.catch(() => {});
|
|
216
243
|
|
|
@@ -238,8 +265,8 @@
|
|
|
238
265
|
$: currentBrowser = BROWSERS.find((b) => b.id === cfg.browser) ?? BROWSERS[0];
|
|
239
266
|
|
|
240
267
|
$: runnerSummary =
|
|
241
|
-
cfg.selectedRunners.length === 1 && cfg.selectedRunners[0] ===
|
|
242
|
-
?
|
|
268
|
+
cfg.selectedRunners.length === 1 && cfg.selectedRunners[0] === BUILTIN_RUNNER_ID
|
|
269
|
+
? BUILTIN_RUNNER_LABEL
|
|
243
270
|
: cfg.selectedRunners.length === 1
|
|
244
271
|
? (availableRunners.find((r) => r.id === cfg.selectedRunners[0])?.name ?? '1 node')
|
|
245
272
|
: `${cfg.selectedRunners.length} nodes`;
|
|
@@ -285,7 +312,7 @@
|
|
|
285
312
|
function adjustWorkers(delta) {
|
|
286
313
|
runnerConfig.update((c) => ({
|
|
287
314
|
...c,
|
|
288
|
-
workers: Math.max(
|
|
315
|
+
workers: Math.max(WORKERS_MIN, Math.min(WORKERS_MAX, c.workers + delta))
|
|
289
316
|
}));
|
|
290
317
|
}
|
|
291
318
|
|
|
@@ -483,13 +510,13 @@
|
|
|
483
510
|
<button
|
|
484
511
|
class="step-btn"
|
|
485
512
|
on:click={() => adjustWorkers(-1)}
|
|
486
|
-
disabled={cfg.workers <=
|
|
513
|
+
disabled={cfg.workers <= WORKERS_MIN || state.running}>−</button
|
|
487
514
|
>
|
|
488
515
|
<span class="step-val">{cfg.workers}</span>
|
|
489
516
|
<button
|
|
490
517
|
class="step-btn"
|
|
491
518
|
on:click={() => adjustWorkers(1)}
|
|
492
|
-
disabled={cfg.workers >=
|
|
519
|
+
disabled={cfg.workers >= WORKERS_MAX || state.running}>+</button
|
|
493
520
|
>
|
|
494
521
|
</div>
|
|
495
522
|
</div>
|
|
@@ -551,7 +578,7 @@
|
|
|
551
578
|
<button
|
|
552
579
|
class="dropdown-trigger"
|
|
553
580
|
class:open={runnersOpen}
|
|
554
|
-
class:has-remote={cfg.selectedRunners.some((r) => r !==
|
|
581
|
+
class:has-remote={cfg.selectedRunners.some((r) => r !== BUILTIN_RUNNER_ID)}
|
|
555
582
|
on:click={() => {
|
|
556
583
|
if (!state.running) runnersOpen = !runnersOpen;
|
|
557
584
|
}}
|
|
@@ -578,11 +605,11 @@
|
|
|
578
605
|
<label class="runner-option">
|
|
579
606
|
<input
|
|
580
607
|
type="checkbox"
|
|
581
|
-
checked={isRunnerSelected(
|
|
582
|
-
on:change={() => toggleRunner(
|
|
608
|
+
checked={isRunnerSelected(BUILTIN_RUNNER_ID)}
|
|
609
|
+
on:change={() => toggleRunner(BUILTIN_RUNNER_ID)}
|
|
583
610
|
/>
|
|
584
611
|
<span class="runner-dot built-in"></span>
|
|
585
|
-
<span>
|
|
612
|
+
<span>{BUILTIN_RUNNER_LABEL}</span>
|
|
586
613
|
</label>
|
|
587
614
|
{/if}
|
|
588
615
|
{#each availableRunners as r}
|
|
@@ -704,7 +731,7 @@
|
|
|
704
731
|
</span>
|
|
705
732
|
{/if}
|
|
706
733
|
</div>
|
|
707
|
-
<
|
|
734
|
+
<Badge variant="tag">Live</Badge>
|
|
708
735
|
<svg
|
|
709
736
|
width="13"
|
|
710
737
|
height="13"
|
|
@@ -727,7 +754,7 @@
|
|
|
727
754
|
<span class="run-card-label">{name}</span>
|
|
728
755
|
<span class="run-card-meta">Scheduled run</span>
|
|
729
756
|
</div>
|
|
730
|
-
<
|
|
757
|
+
<Badge variant="schedule">Scheduled</Badge>
|
|
731
758
|
<svg
|
|
732
759
|
width="13"
|
|
733
760
|
height="13"
|
|
@@ -860,18 +887,6 @@
|
|
|
860
887
|
animation: dotPulse 1.6s ease-in-out infinite;
|
|
861
888
|
}
|
|
862
889
|
|
|
863
|
-
@keyframes dotPulse {
|
|
864
|
-
0%,
|
|
865
|
-
100% {
|
|
866
|
-
opacity: 1;
|
|
867
|
-
transform: scale(1);
|
|
868
|
-
}
|
|
869
|
-
50% {
|
|
870
|
-
opacity: 0.4;
|
|
871
|
-
transform: scale(0.65);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
890
|
.status-word {
|
|
876
891
|
font-size: 0.72rem;
|
|
877
892
|
font-weight: 600;
|
|
@@ -887,7 +902,7 @@
|
|
|
887
902
|
color: var(--text-muted);
|
|
888
903
|
background: var(--bg-subtle);
|
|
889
904
|
border: 1px solid var(--border);
|
|
890
|
-
border-radius:
|
|
905
|
+
border-radius: var(--radius-pill);
|
|
891
906
|
padding: 0.1rem 0.45rem;
|
|
892
907
|
white-space: nowrap;
|
|
893
908
|
overflow: hidden;
|
|
@@ -915,7 +930,7 @@
|
|
|
915
930
|
|
|
916
931
|
.view-report-btn:hover {
|
|
917
932
|
background: var(--accent);
|
|
918
|
-
color:
|
|
933
|
+
color: var(--white);
|
|
919
934
|
}
|
|
920
935
|
|
|
921
936
|
/* ── Center: controls ── */
|
|
@@ -1175,7 +1190,7 @@
|
|
|
1175
1190
|
height: 30px;
|
|
1176
1191
|
padding: 0 0.875rem;
|
|
1177
1192
|
background: var(--accent);
|
|
1178
|
-
color: white;
|
|
1193
|
+
color: var(--white);
|
|
1179
1194
|
border: none;
|
|
1180
1195
|
border-radius: var(--radius-sm);
|
|
1181
1196
|
font-family: var(--font-body);
|
|
@@ -1199,7 +1214,7 @@
|
|
|
1199
1214
|
width: 10px;
|
|
1200
1215
|
height: 10px;
|
|
1201
1216
|
border: 1.5px solid rgba(255, 255, 255, 0.35);
|
|
1202
|
-
border-top-color: white;
|
|
1217
|
+
border-top-color: var(--white);
|
|
1203
1218
|
border-radius: 50%;
|
|
1204
1219
|
animation: spin 0.65s linear infinite;
|
|
1205
1220
|
flex-shrink: 0;
|
|
@@ -1241,7 +1256,7 @@
|
|
|
1241
1256
|
.notify-btn.active {
|
|
1242
1257
|
background: var(--accent);
|
|
1243
1258
|
border-color: var(--accent);
|
|
1244
|
-
color:
|
|
1259
|
+
color: var(--white);
|
|
1245
1260
|
}
|
|
1246
1261
|
|
|
1247
1262
|
.notify-btn:disabled {
|
|
@@ -1363,23 +1378,6 @@
|
|
|
1363
1378
|
margin: 0 0.15rem;
|
|
1364
1379
|
}
|
|
1365
1380
|
|
|
1366
|
-
.run-card-badge {
|
|
1367
|
-
font-size: 0.62rem;
|
|
1368
|
-
font-weight: 600;
|
|
1369
|
-
letter-spacing: 0.07em;
|
|
1370
|
-
text-transform: uppercase;
|
|
1371
|
-
color: var(--accent);
|
|
1372
|
-
background: var(--accent-soft);
|
|
1373
|
-
padding: 0.1rem 0.4rem;
|
|
1374
|
-
border-radius: 100px;
|
|
1375
|
-
flex-shrink: 0;
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
.run-card-badge.cron-badge {
|
|
1379
|
-
color: var(--warn);
|
|
1380
|
-
background: var(--warn-soft);
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
1381
|
.run-card-arrow {
|
|
1384
1382
|
color: var(--text-muted);
|
|
1385
1383
|
flex-shrink: 0;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
* This file is part of Plum.
|
|
3
|
+
*
|
|
4
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* Plum is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License
|
|
15
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
-->
|
|
17
|
+
|
|
18
|
+
<!--
|
|
19
|
+
* This file is part of Plum.
|
|
20
|
+
*
|
|
21
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
22
|
+
* it under the terms of the GNU General Public License as published by
|
|
23
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
24
|
+
* (at your option) any later version.
|
|
25
|
+
*
|
|
26
|
+
* Plum is distributed in the hope that it will be useful,
|
|
27
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
28
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
29
|
+
* GNU General Public License for more details.
|
|
30
|
+
*
|
|
31
|
+
* You should have received a copy of the GNU General Public License
|
|
32
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
33
|
+
-->
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
import { AUTOMATED_LABEL } from '$lib/constants';
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<span class="auto-badge">{AUTOMATED_LABEL}</span>
|
|
40
|
+
|
|
41
|
+
<style>
|
|
42
|
+
.auto-badge {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
font-size: 0.62rem;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
text-transform: uppercase;
|
|
48
|
+
letter-spacing: 0.05em;
|
|
49
|
+
color: var(--accent);
|
|
50
|
+
background: var(--accent-soft);
|
|
51
|
+
border-radius: var(--radius-pill);
|
|
52
|
+
padding: 0.1rem 0.45rem;
|
|
53
|
+
flex-shrink: 0;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
* This file is part of Plum.
|
|
3
|
+
*
|
|
4
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* Plum is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License
|
|
15
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
-->
|
|
17
|
+
|
|
18
|
+
<!--
|
|
19
|
+
* This file is part of Plum.
|
|
20
|
+
*
|
|
21
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
22
|
+
* it under the terms of the GNU General Public License as published by
|
|
23
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
24
|
+
* (at your option) any later version.
|
|
25
|
+
*
|
|
26
|
+
* Plum is distributed in the hope that it will be useful,
|
|
27
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
28
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
29
|
+
* GNU General Public License for more details.
|
|
30
|
+
*
|
|
31
|
+
* You should have received a copy of the GNU General Public License
|
|
32
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
33
|
+
-->
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
export let href = '/reports';
|
|
37
|
+
export let label = 'Reports';
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div class="back-row">
|
|
41
|
+
<a {href} class="back-link">
|
|
42
|
+
<svg
|
|
43
|
+
width="14"
|
|
44
|
+
height="14"
|
|
45
|
+
viewBox="0 0 24 24"
|
|
46
|
+
fill="none"
|
|
47
|
+
stroke="currentColor"
|
|
48
|
+
stroke-width="2"
|
|
49
|
+
stroke-linecap="round"
|
|
50
|
+
stroke-linejoin="round"
|
|
51
|
+
>
|
|
52
|
+
<line x1="19" y1="12" x2="5" y2="12" />
|
|
53
|
+
<polyline points="12 19 5 12 12 5" />
|
|
54
|
+
</svg>
|
|
55
|
+
{label}
|
|
56
|
+
</a>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<style>
|
|
60
|
+
.back-row {
|
|
61
|
+
margin-bottom: 1.5rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.back-link {
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 0.35rem;
|
|
68
|
+
font-size: 0.8125rem;
|
|
69
|
+
color: var(--text-muted);
|
|
70
|
+
text-decoration: none;
|
|
71
|
+
transition: color var(--duration-fast);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.back-link:hover {
|
|
75
|
+
color: var(--text);
|
|
76
|
+
}
|
|
77
|
+
</style>
|