@runwisp/ui 0.14.0 → 0.15.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/package.json +4 -7
- package/src/lib/actions/portal.ts +0 -3
- package/src/lib/components/CronInput.svelte +0 -4
- package/src/lib/components/DataGrid.svelte +1 -3
- package/src/lib/components/Drawer.svelte +0 -3
- package/src/lib/components/Dropdown.svelte +2 -6
- package/src/lib/components/DurationInput.svelte +1 -1
- package/src/lib/components/FormField.svelte +1 -1
- package/src/lib/components/Input.svelte +1 -1
- package/src/lib/components/Modal.svelte +0 -3
- package/src/lib/components/Select.svelte +2 -5
- package/src/lib/components/StatusIndicator.svelte +0 -3
- package/src/lib/components/Tabs.svelte +0 -2
- package/src/lib/components/ToastContainer.svelte +0 -5
- package/src/lib/components/Tooltip.svelte +5 -2
- package/src/lib/components/dashboard/RunDetailPanel.svelte +164 -238
- package/src/lib/components/dashboard/RunFilterPopover.svelte +14 -14
- package/src/lib/components/dashboard/RunsList.svelte +29 -29
- package/src/lib/components/dashboard/run-filters.test.ts +31 -33
- package/src/lib/components/dashboard/run-filters.ts +33 -33
- package/src/lib/components/dashboard/run-helpers.test.ts +42 -17
- package/src/lib/components/dashboard/run-helpers.ts +53 -17
- package/src/lib/components/dashboard/status-config.ts +9 -8
- package/src/lib/theme-tokens.css +0 -2
- package/src/lib/utils/error.ts +0 -3
- package/src/lib/utils/format.test.ts +2 -6
- package/src/lib/utils/format.ts +28 -3
- package/src/lib/components/FeatureCard.svelte +0 -35
- package/src/lib/components/FeatureGrid.svelte +0 -37
|
@@ -30,20 +30,15 @@
|
|
|
30
30
|
import Tooltip from "../Tooltip.svelte";
|
|
31
31
|
import { portal } from "../../actions/portal.js";
|
|
32
32
|
import type { Run } from "./types.js";
|
|
33
|
-
import type
|
|
34
|
-
import {
|
|
35
|
-
formatDateTime,
|
|
36
|
-
formatFullDateTime,
|
|
37
|
-
formatClockTime,
|
|
38
|
-
formatCalendarDate,
|
|
39
|
-
formatTimeHM,
|
|
40
|
-
} from "../../utils/format.js";
|
|
33
|
+
import { isLogEvent, type LogEvent, type LogSlice } from "../../log-console/types.js";
|
|
34
|
+
import { formatClockTime, formatCalendarDate } from "../../utils/format.js";
|
|
41
35
|
import { formatShortId } from "../../utils/id.js";
|
|
42
36
|
import { TickingNow } from "../../utils/ticking-now.svelte.js";
|
|
43
37
|
import { getRunStatusConfig, runDisplayStatus } from "./status-config.js";
|
|
44
38
|
import {
|
|
45
39
|
runDuration,
|
|
46
40
|
runStartDelay,
|
|
41
|
+
runVerdict,
|
|
47
42
|
formatTriggeredByLabel,
|
|
48
43
|
runRetryLabel,
|
|
49
44
|
instanceSuffix,
|
|
@@ -151,22 +146,41 @@
|
|
|
151
146
|
if (!stream) return;
|
|
152
147
|
|
|
153
148
|
let cleanup: (() => void) | undefined;
|
|
149
|
+
let cancelled = false;
|
|
154
150
|
|
|
155
151
|
untrack(() => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
152
|
+
void (async () => {
|
|
153
|
+
// Paint the tail in one batched page so the console opens
|
|
154
|
+
// already at the newest output, instead of replaying 1000
|
|
155
|
+
// backfill lines over SSE and visibly scrolling down to reach
|
|
156
|
+
// the end. The live stream then picks up after the seeded tail
|
|
157
|
+
// (its disk backfill from that anchor closes the fetch↔stream
|
|
158
|
+
// race, and the daemon dedupes anything already shown).
|
|
159
|
+
let fromLine = -TAIL_LINES;
|
|
160
|
+
try {
|
|
161
|
+
const seed = await fetchLogs(id, -TAIL_LINES, -1);
|
|
162
|
+
if (cancelled) return;
|
|
163
|
+
if (isLogEvent(seed)) {
|
|
164
|
+
if (logConsole) logConsole.onStream(seed);
|
|
165
|
+
if (seed.finished) return; // ended run: nothing live to follow
|
|
166
|
+
if (seed.sizeLines > 0) fromLine = seed.sizeLines;
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// Seed failed — fall back to the SSE tail backfill.
|
|
170
|
+
}
|
|
171
|
+
if (cancelled) return;
|
|
172
|
+
cleanup = stream(
|
|
173
|
+
id,
|
|
174
|
+
(event: LogEvent) => {
|
|
175
|
+
if (logConsole) logConsole.onStream(event);
|
|
176
|
+
},
|
|
177
|
+
{ fromLine },
|
|
178
|
+
);
|
|
179
|
+
})();
|
|
167
180
|
});
|
|
168
181
|
|
|
169
182
|
return () => {
|
|
183
|
+
cancelled = true;
|
|
170
184
|
if (cleanup) cleanup();
|
|
171
185
|
};
|
|
172
186
|
});
|
|
@@ -204,41 +218,12 @@
|
|
|
204
218
|
}
|
|
205
219
|
}
|
|
206
220
|
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (displayed === "failed") return "non-zero";
|
|
214
|
-
if (displayed === "stopped" || displayed === "daemon_stopped") return "stopped before exit";
|
|
215
|
-
if (displayed === "timeout") return "timed out";
|
|
216
|
-
if (displayed === "crashed") return "killed";
|
|
217
|
-
return "no exit code";
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// One-word context line under each Trigger value, mirroring the source.
|
|
221
|
-
function triggerSub(triggeredBy: Run["triggered_by"]): string {
|
|
222
|
-
if (triggeredBy === "cron") return "schedule";
|
|
223
|
-
if (triggeredBy === "api") return "manual";
|
|
224
|
-
if (triggeredBy === "service") return "supervised";
|
|
225
|
-
if (triggeredBy === "startup") return "on boot";
|
|
226
|
-
return "remote";
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// The status's accent color as a runtime CSS variable, used to wash the
|
|
230
|
-
// header in a faint tint of the outcome (the "black-box readout" look).
|
|
231
|
-
// Neutral statuses resolve to the surface itself, so the tint vanishes.
|
|
232
|
-
function accentVar(displayed: string): string {
|
|
233
|
-
if (displayed === "running" || displayed === "pending") return "var(--color-info)";
|
|
234
|
-
if (displayed === "success") return "var(--color-success-surface)";
|
|
235
|
-
if (
|
|
236
|
-
displayed === "stopped" ||
|
|
237
|
-
displayed === "timeout" ||
|
|
238
|
-
displayed === "daemon_stopped" ||
|
|
239
|
-
displayed === "queue_full"
|
|
240
|
-
)
|
|
241
|
-
return "var(--color-warning-surface)";
|
|
221
|
+
// The accent for a status that means "something to triage", or undefined
|
|
222
|
+
// when there is nothing wrong. Tint is reserved for alarms (DESIGN.md) so a
|
|
223
|
+
// failure is the one lit thing on the page — a deliberate stop or a skip is
|
|
224
|
+
// not a failure, and success least of all. Doubles as the switch for the
|
|
225
|
+
// verdict phrase's colour, so wash and wording light up together.
|
|
226
|
+
function alarmAccent(displayed: string): string | undefined {
|
|
242
227
|
if (
|
|
243
228
|
displayed === "failed" ||
|
|
244
229
|
displayed === "crashed" ||
|
|
@@ -247,7 +232,9 @@
|
|
|
247
232
|
displayed === "start_failed"
|
|
248
233
|
)
|
|
249
234
|
return "var(--color-danger-surface)";
|
|
250
|
-
|
|
235
|
+
if (displayed === "timeout" || displayed === "daemon_stopped" || displayed === "queue_full")
|
|
236
|
+
return "var(--color-warning-surface)";
|
|
237
|
+
return undefined;
|
|
251
238
|
}
|
|
252
239
|
|
|
253
240
|
function handleConsoleKeydown(event: KeyboardEvent) {
|
|
@@ -320,15 +307,16 @@
|
|
|
320
307
|
run.status === "running" ? durationTicker.now.getTime() : undefined,
|
|
321
308
|
)}
|
|
322
309
|
{@const startDelay = runStartDelay(run)}
|
|
323
|
-
{@const startedAt = run.
|
|
310
|
+
{@const startedAt = run.startAt ?? run.createdAt}
|
|
324
311
|
{@const retry = runRetryLabel(run)}
|
|
325
312
|
{@const paramEntries = run.params ? Object.entries(run.params) : []}
|
|
326
|
-
{@const suffix = instanceSuffix(run.
|
|
313
|
+
{@const suffix = instanceSuffix(run.instanceIndex, getInstanceCount(run.taskName))}
|
|
327
314
|
{@const spine = config.dot.replace(" animate-pulse", "")}
|
|
328
315
|
{@const isRunning = run.status === "running"}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
{@const
|
|
316
|
+
<!-- A code is worth the ink only when it is news: `exit 0` restates
|
|
317
|
+
"succeeded", while a non-zero code is the first thing to triage on. -->
|
|
318
|
+
{@const showCode = status === "failed"}
|
|
319
|
+
{@const verdict = runVerdict(status)}
|
|
332
320
|
{@const endLabel =
|
|
333
321
|
status === "stopped"
|
|
334
322
|
? "run stopped by operator"
|
|
@@ -341,7 +329,7 @@
|
|
|
341
329
|
status === "stopped" || status === "daemon_stopped" || status === "timeout"
|
|
342
330
|
? "warn"
|
|
343
331
|
: "muted"}
|
|
344
|
-
{@const
|
|
332
|
+
{@const alarm = alarmAccent(status)}
|
|
345
333
|
<!-- The panel: a status spine runs the full left edge across both the header
|
|
346
334
|
readout and the console below, hugging the rail divider (artifact
|
|
347
335
|
".detail .spine"). -->
|
|
@@ -352,95 +340,118 @@
|
|
|
352
340
|
class="absolute inset-y-0 left-0 z-[3] w-1 {spine} {isRunning ? 'spine-flow' : ''}"
|
|
353
341
|
aria-hidden="true"
|
|
354
342
|
></div>
|
|
355
|
-
<!-- Detailed header: a
|
|
356
|
-
run
|
|
343
|
+
<!-- Detailed header: a readout whose ink scales with how much went wrong.
|
|
344
|
+
Every run states its outcome in one phrase over one quiet fact line;
|
|
345
|
+
only a run worth triaging colours the phrase and tints the surface.
|
|
346
|
+
Nothing here is a fixed slot — every fact renders only when it is
|
|
347
|
+
true, so the header's height is itself a signal. -->
|
|
357
348
|
<div
|
|
358
349
|
class="head-region @container relative shrink-0 border-b border-outline-faint"
|
|
359
|
-
style="--rw-oc: {
|
|
350
|
+
style="--rw-oc: {alarm ?? 'var(--color-surface-raised)'}"
|
|
360
351
|
>
|
|
361
|
-
<div class="pt-[18px] pr-[22px] pb-[
|
|
352
|
+
<div class="pt-[18px] pr-[22px] pb-[16px] pl-[26px]">
|
|
362
353
|
<div class="flex items-start gap-4">
|
|
363
|
-
<!-- Verdict tile -->
|
|
364
|
-
<div
|
|
365
|
-
class="flex h-[38px] w-[38px] shrink-0 items-center justify-center rounded-[4px] {config.bg} {config.color} ring-1 ring-current/25 ring-inset"
|
|
366
|
-
>
|
|
367
|
-
<DetailIcon size={20} class={isRunning ? "animate-spin" : ""} />
|
|
368
|
-
</div>
|
|
369
|
-
|
|
370
354
|
<div class="min-w-0 flex-1">
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
355
|
+
<!-- Identity, but only where the page around the panel isn't
|
|
356
|
+
already saying it: the cross-task runs list needs the task
|
|
357
|
+
name, a task's own page already has it in the breadcrumb. -->
|
|
358
|
+
{#if showTaskName || suffix}
|
|
359
|
+
<div
|
|
360
|
+
class="mb-1.5 font-mono text-[11.5px] font-medium tracking-[0.06em] text-on-surface-muted"
|
|
361
|
+
>
|
|
362
|
+
{showTaskName ? `${run.taskName}${suffix}` : `instance ${suffix}`}
|
|
363
|
+
</div>
|
|
364
|
+
{/if}
|
|
365
|
+
|
|
366
|
+
<!-- Verdict: the outcome as one sentence, and the only large
|
|
367
|
+
type in the panel. It is prose, so the phrase is sans and
|
|
368
|
+
only the tokens inside it — the duration, a failing exit —
|
|
369
|
+
are mono (DESIGN.md's mono-vs-sans rule). The glyph is bare
|
|
370
|
+
— no plate, no ring — so it reads as part of the sentence,
|
|
371
|
+
and it alone carries the outcome colour on a healthy run:
|
|
372
|
+
green words are ink spent on nothing being wrong. -->
|
|
373
|
+
<Tooltip content={config.description} position="right" wide>
|
|
374
|
+
<h2
|
|
375
|
+
class="flex flex-wrap items-center gap-x-2.5 text-[22px] @max-2xl:text-[18px] @max-xs:text-[16px]"
|
|
376
|
+
data-testid="run-verdict"
|
|
377
|
+
data-status={status}
|
|
378
|
+
>
|
|
379
|
+
<DetailIcon
|
|
380
|
+
size={18}
|
|
381
|
+
strokeWidth={2.25}
|
|
382
|
+
class="shrink-0 {config.color} {isRunning
|
|
383
|
+
? 'animate-spin'
|
|
384
|
+
: ''}"
|
|
385
|
+
/>
|
|
386
|
+
<span
|
|
387
|
+
class="font-sans font-semibold tracking-[-0.01em] {alarm
|
|
388
|
+
? config.color
|
|
389
|
+
: 'text-on-surface'}">{verdict.verb}</span
|
|
390
|
+
>
|
|
391
|
+
{#if verdict.timed && duration}
|
|
392
|
+
<span
|
|
393
|
+
class="font-mono text-[0.86em] font-medium text-on-surface tabular-nums"
|
|
394
|
+
data-testid="run-duration">{duration}</span
|
|
395
|
+
>
|
|
396
|
+
{/if}
|
|
397
|
+
{#if showCode}
|
|
398
|
+
<!-- Grouped with its own separator so a wrap can
|
|
399
|
+
never leave the dot dangling at a line end. -->
|
|
400
|
+
<span
|
|
401
|
+
class="inline-flex items-center gap-x-2.5 font-mono text-[0.86em] font-medium tabular-nums"
|
|
402
|
+
>
|
|
403
|
+
<span class="text-on-surface-faint" aria-hidden="true"
|
|
404
|
+
>·</span
|
|
383
405
|
>
|
|
384
|
-
|
|
406
|
+
<span class="text-danger-surface" data-testid="run-exit"
|
|
407
|
+
>exit {run.exitCode}</span
|
|
408
|
+
>
|
|
409
|
+
</span>
|
|
385
410
|
{/if}
|
|
386
411
|
</h2>
|
|
387
|
-
|
|
388
|
-
outcome, so stopped reads amber, not red. -->
|
|
389
|
-
<Tooltip content={config.description} position="bottom" wide>
|
|
390
|
-
<span
|
|
391
|
-
class="rounded-[3px] border px-2.5 py-0.5 font-mono text-2xs font-bold tracking-wider uppercase {config.color} {config.bg} {config.border}"
|
|
392
|
-
>
|
|
393
|
-
{status.toUpperCase()}
|
|
394
|
-
</span>
|
|
395
|
-
</Tooltip>
|
|
396
|
-
</div>
|
|
412
|
+
</Tooltip>
|
|
397
413
|
|
|
414
|
+
<!-- Facts: when it happened and what shaped it. Queue wait,
|
|
415
|
+
retries and parameters are absent on an ordinary run and
|
|
416
|
+
appear inline the moment they exist. -->
|
|
398
417
|
<div
|
|
399
|
-
class="mt-
|
|
418
|
+
class="mt-2.5 flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-[11.5px] text-on-surface-faint tabular-nums"
|
|
400
419
|
>
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
{formatTimeHM(startedAt)}
|
|
408
|
-
</span>
|
|
409
|
-
{/if}
|
|
410
|
-
<!-- Run-id chip: click to copy the full ULID -->
|
|
411
|
-
<button
|
|
412
|
-
type="button"
|
|
413
|
-
onclick={() => copyRunId(run.id)}
|
|
414
|
-
title="Copy run ID"
|
|
415
|
-
class="inline-flex items-center gap-1.5 rounded-[3px] border border-outline-faint bg-surface-sunken px-1.5 py-0.5 font-mono text-2xs text-on-surface-faint hover:border-outline-hover hover:text-primary"
|
|
420
|
+
<!-- No hover-for-the-full-timestamp here: the date sits
|
|
421
|
+
right beside the clock time, so a tooltip could only
|
|
422
|
+
repeat it. The line wraps at narrow widths instead of
|
|
423
|
+
dropping the date and hiding it behind a hover. -->
|
|
424
|
+
<span data-testid="run-started" class="text-on-surface-muted"
|
|
425
|
+
>{run.startAt ? formatClockTime(run.startAt) : "—"}</span
|
|
416
426
|
>
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
427
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
428
|
+
<span>{formatCalendarDate(startedAt)}</span>
|
|
429
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
430
|
+
<span>via {formatTriggeredByLabel(run.triggeredBy).toLowerCase()}</span>
|
|
431
|
+
{#if startDelay}
|
|
432
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
433
|
+
<span
|
|
434
|
+
class="text-warning-soft-text"
|
|
435
|
+
title="Waited between its scheduled tick and actually starting"
|
|
436
|
+
>{startDelay} queued</span
|
|
437
|
+
>
|
|
438
|
+
{/if}
|
|
423
439
|
{#if retry}
|
|
424
|
-
<span class="
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
<span class="text-on-surface-faint"
|
|
430
|
-
>of run {formatShortId(run.retry_of_run_id)}</span
|
|
431
|
-
>{/if}
|
|
440
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
441
|
+
<span class="inline-flex items-center gap-1">
|
|
442
|
+
<RotateCw size={11} />
|
|
443
|
+
retry #{run.retryAttempt}{#if run.retryOfRunId} of
|
|
444
|
+
{formatShortId(run.retryOfRunId)}{/if}
|
|
432
445
|
</span>
|
|
433
446
|
{/if}
|
|
434
447
|
{#if paramEntries.length > 0}
|
|
435
|
-
<span class="
|
|
436
|
-
></span>
|
|
448
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
437
449
|
<Popover placement="bottom-start">
|
|
438
450
|
{#snippet trigger()}
|
|
439
451
|
<span
|
|
440
|
-
class="inline-flex cursor-pointer items-center gap-1
|
|
441
|
-
title="View run parameters"
|
|
452
|
+
class="inline-flex cursor-pointer items-center gap-1 border-b border-dotted border-outline-hover text-on-surface-muted hover:text-primary"
|
|
442
453
|
>
|
|
443
|
-
<SlidersHorizontal size={
|
|
454
|
+
<SlidersHorizontal size={11} />
|
|
444
455
|
{paramEntries.length}
|
|
445
456
|
{paramEntries.length === 1 ? "parameter" : "parameters"}
|
|
446
457
|
</span>
|
|
@@ -467,6 +478,21 @@
|
|
|
467
478
|
</div>
|
|
468
479
|
</Popover>
|
|
469
480
|
{/if}
|
|
481
|
+
<!-- Run-id chip: click to copy the full ULID. Last, because
|
|
482
|
+
it is the fact you reach for least and the only one you
|
|
483
|
+
interact with. -->
|
|
484
|
+
<button
|
|
485
|
+
type="button"
|
|
486
|
+
onclick={() => copyRunId(run.id)}
|
|
487
|
+
title="Copy run ID"
|
|
488
|
+
class="inline-flex items-center gap-1 rounded-[3px] border border-outline-faint bg-surface-sunken px-1.5 text-on-surface-faint hover:border-outline-hover hover:text-primary"
|
|
489
|
+
>
|
|
490
|
+
{#if copiedId}
|
|
491
|
+
<Check size={11} class="text-success-surface" />Copied
|
|
492
|
+
{:else}
|
|
493
|
+
<Hash size={11} />{run.id}
|
|
494
|
+
{/if}
|
|
495
|
+
</button>
|
|
470
496
|
</div>
|
|
471
497
|
</div>
|
|
472
498
|
|
|
@@ -595,9 +621,9 @@
|
|
|
595
621
|
{/if}
|
|
596
622
|
<a
|
|
597
623
|
href="/api/tasks/{encodeURIComponent(
|
|
598
|
-
run.
|
|
624
|
+
run.taskName,
|
|
599
625
|
)}/runs/{encodeURIComponent(run.id)}/log/raw"
|
|
600
|
-
download="{run.
|
|
626
|
+
download="{run.taskName}-{run.id}.log"
|
|
601
627
|
class="inline-flex items-center justify-center rounded-[3px] border border-outline-faint bg-surface-raised p-2 text-on-surface-muted hover:border-outline-hover hover:bg-surface-sunken hover:text-primary"
|
|
602
628
|
title="Download the full log (rotated and current parts as one file)"
|
|
603
629
|
aria-label="Download log"
|
|
@@ -647,107 +673,6 @@
|
|
|
647
673
|
{/if}
|
|
648
674
|
</div>
|
|
649
675
|
</div>
|
|
650
|
-
|
|
651
|
-
<!-- Instrument cluster: the run's vital readout — value over a one-word
|
|
652
|
-
context line, hairline-divided. -->
|
|
653
|
-
<div
|
|
654
|
-
class="mt-4 flex flex-wrap overflow-hidden rounded-[4px] border border-outline-faint bg-surface-raised"
|
|
655
|
-
>
|
|
656
|
-
<div
|
|
657
|
-
class="flex min-w-30 flex-1 flex-col gap-0.5 border-l border-outline-faint px-4 py-2.5 first:border-l-0"
|
|
658
|
-
>
|
|
659
|
-
<span
|
|
660
|
-
class="font-mono text-2xs font-bold tracking-[0.12em] text-on-surface-faint uppercase"
|
|
661
|
-
>Queued</span
|
|
662
|
-
>
|
|
663
|
-
<span
|
|
664
|
-
class="font-mono text-base font-medium tracking-tight whitespace-nowrap text-on-surface tabular-nums"
|
|
665
|
-
title={formatFullDateTime(run.created_at)}
|
|
666
|
-
>
|
|
667
|
-
{formatClockTime(run.created_at)}
|
|
668
|
-
</span>
|
|
669
|
-
<span class="font-mono text-2xs text-on-surface-muted"
|
|
670
|
-
>+{startDelay ?? "0s"} queued</span
|
|
671
|
-
>
|
|
672
|
-
</div>
|
|
673
|
-
|
|
674
|
-
<div
|
|
675
|
-
class="flex min-w-30 flex-1 flex-col gap-0.5 border-l border-outline-faint px-4 py-2.5 first:border-l-0"
|
|
676
|
-
>
|
|
677
|
-
<span
|
|
678
|
-
class="font-mono text-2xs font-bold tracking-[0.12em] text-on-surface-faint uppercase"
|
|
679
|
-
>Started</span
|
|
680
|
-
>
|
|
681
|
-
<span
|
|
682
|
-
class="font-mono text-base font-medium tracking-tight whitespace-nowrap text-on-surface tabular-nums"
|
|
683
|
-
>
|
|
684
|
-
{run.start_at ? formatClockTime(run.start_at) : "—"}
|
|
685
|
-
</span>
|
|
686
|
-
<span class="font-mono text-2xs text-on-surface-muted">
|
|
687
|
-
{run.start_at ? formatCalendarDate(run.start_at) : "not started"}
|
|
688
|
-
</span>
|
|
689
|
-
</div>
|
|
690
|
-
|
|
691
|
-
<div
|
|
692
|
-
class="flex min-w-30 flex-1 flex-col gap-0.5 border-l border-outline-faint px-4 py-2.5 first:border-l-0"
|
|
693
|
-
>
|
|
694
|
-
<span
|
|
695
|
-
class="font-mono text-2xs font-bold tracking-[0.12em] text-on-surface-faint uppercase"
|
|
696
|
-
>Ran for</span
|
|
697
|
-
>
|
|
698
|
-
<span
|
|
699
|
-
class="font-mono text-base font-medium tracking-tight whitespace-nowrap text-on-surface tabular-nums"
|
|
700
|
-
>
|
|
701
|
-
{duration ?? "—"}
|
|
702
|
-
</span>
|
|
703
|
-
<span class="font-mono text-2xs text-on-surface-muted">
|
|
704
|
-
{isRunning ? "and counting" : run.end_at ? "wall clock" : "—"}
|
|
705
|
-
</span>
|
|
706
|
-
</div>
|
|
707
|
-
|
|
708
|
-
<div
|
|
709
|
-
class="flex min-w-30 flex-1 flex-col gap-0.5 border-l border-outline-faint px-4 py-2.5 first:border-l-0"
|
|
710
|
-
>
|
|
711
|
-
<span
|
|
712
|
-
class="font-mono text-2xs font-bold tracking-[0.12em] text-on-surface-faint uppercase"
|
|
713
|
-
>Exit</span
|
|
714
|
-
>
|
|
715
|
-
<span
|
|
716
|
-
class="flex items-baseline gap-1.5 font-mono text-base font-medium tracking-tight tabular-nums {exitClean
|
|
717
|
-
? 'text-success-surface'
|
|
718
|
-
: exitFail
|
|
719
|
-
? 'text-danger-surface'
|
|
720
|
-
: 'text-on-surface'}"
|
|
721
|
-
>
|
|
722
|
-
<span>{showCode ? run.exit_code : "—"}</span>
|
|
723
|
-
{#if exitFail}
|
|
724
|
-
<span
|
|
725
|
-
class="rounded-[3px] bg-danger-soft px-1 py-px font-mono text-[9px] font-bold tracking-wide text-danger-surface uppercase"
|
|
726
|
-
>fail</span
|
|
727
|
-
>
|
|
728
|
-
{/if}
|
|
729
|
-
</span>
|
|
730
|
-
<span class="font-mono text-2xs text-on-surface-muted"
|
|
731
|
-
>{exitSubLabel(status)}</span
|
|
732
|
-
>
|
|
733
|
-
</div>
|
|
734
|
-
|
|
735
|
-
<div
|
|
736
|
-
class="flex min-w-30 flex-1 flex-col gap-0.5 border-l border-outline-faint px-4 py-2.5 first:border-l-0"
|
|
737
|
-
>
|
|
738
|
-
<span
|
|
739
|
-
class="font-mono text-2xs font-bold tracking-[0.12em] text-on-surface-faint uppercase"
|
|
740
|
-
>Trigger</span
|
|
741
|
-
>
|
|
742
|
-
<span
|
|
743
|
-
class="font-mono text-base font-medium tracking-tight whitespace-nowrap text-on-surface tabular-nums"
|
|
744
|
-
>{formatTriggeredByLabel(run.triggered_by)}</span
|
|
745
|
-
>
|
|
746
|
-
<span class="font-mono text-2xs text-on-surface-muted"
|
|
747
|
-
>{triggerSub(run.triggered_by)}</span
|
|
748
|
-
>
|
|
749
|
-
</div>
|
|
750
|
-
</div>
|
|
751
676
|
</div>
|
|
752
677
|
</div>
|
|
753
678
|
|
|
@@ -775,7 +700,7 @@
|
|
|
775
700
|
<span class="flex min-w-0 items-center gap-2">
|
|
776
701
|
<span class="h-2 w-2 shrink-0 rounded-full {spine}"></span>
|
|
777
702
|
<span class="truncate font-medium text-[var(--rw-con-text)]">
|
|
778
|
-
{run.
|
|
703
|
+
{run.taskName}{suffix}
|
|
779
704
|
</span>
|
|
780
705
|
<span class="text-[var(--rw-con-gutter)]">·</span>
|
|
781
706
|
<span class="font-semibold capitalize {config.color}">{status}</span>
|
|
@@ -869,11 +794,12 @@
|
|
|
869
794
|
{/if}
|
|
870
795
|
|
|
871
796
|
<style>
|
|
872
|
-
/*
|
|
873
|
-
|
|
874
|
-
mix collapses to a plain surface
|
|
797
|
+
/* Only a run worth triaging is washed in its outcome colour (--rw-oc, set
|
|
798
|
+
inline). Everything else — success included — passes the surface itself so
|
|
799
|
+
the mix collapses to a plain surface, which is what lets a genuine failure
|
|
800
|
+
be the one lit thing on the page. */
|
|
875
801
|
.head-region {
|
|
876
|
-
background: color-mix(in srgb, var(--rw-oc)
|
|
802
|
+
background: color-mix(in srgb, var(--rw-oc) 7%, var(--color-surface-raised));
|
|
877
803
|
}
|
|
878
804
|
|
|
879
805
|
/* Gentle fade for the maximize backdrop; disabled under reduced motion. */
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
|
|
66
66
|
let advancedOpen = $state(false);
|
|
67
67
|
|
|
68
|
-
const count = $derived(activeFilterCount(filters) + (showTask && filters.
|
|
68
|
+
const count = $derived(activeFilterCount(filters) + (showTask && filters.taskName ? 1 : 0));
|
|
69
69
|
|
|
70
70
|
const headerClass =
|
|
71
71
|
"font-mono text-xs font-semibold tracking-[0.14em] text-on-surface-muted uppercase";
|
|
@@ -103,37 +103,37 @@
|
|
|
103
103
|
// both captures the whole day.
|
|
104
104
|
|
|
105
105
|
function setFrom(value: string) {
|
|
106
|
-
filters = { ...filters,
|
|
106
|
+
filters = { ...filters, createdAfter: value ? dayStartIso(value) : undefined };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function setTo(value: string) {
|
|
110
|
-
filters = { ...filters,
|
|
110
|
+
filters = { ...filters, createdBefore: value ? dayEndIso(value) : undefined };
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// --- Other dimensions -------------------------------------------------
|
|
114
114
|
|
|
115
115
|
function setTask(value: string) {
|
|
116
|
-
filters = { ...filters,
|
|
116
|
+
filters = { ...filters, taskName: value || undefined };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
function setTrigger(value: string) {
|
|
120
|
-
filters = { ...filters,
|
|
120
|
+
filters = { ...filters, triggeredBy: value || undefined };
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// Exit code: a free-form expression (`137`, `>100`, `>100 <150`) normalized
|
|
124
124
|
// to an inclusive range at the wire. Edited in a local buffer for smooth
|
|
125
125
|
// typing and committed on change; the popover content remounts on each open,
|
|
126
126
|
// so the buffer re-seeds from `filters` every time it opens.
|
|
127
|
-
let exitCodeInput = $state(filters.
|
|
127
|
+
let exitCodeInput = $state(filters.exitCode ?? "");
|
|
128
128
|
const exitCodeValid = $derived(isExitCodeExprValid(exitCodeInput));
|
|
129
129
|
|
|
130
130
|
function commitExitCode() {
|
|
131
131
|
const trimmed = exitCodeInput.trim();
|
|
132
|
-
filters = { ...filters,
|
|
132
|
+
filters = { ...filters, exitCode: trimmed || undefined };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
function setRetriesOnly(checked: boolean) {
|
|
136
|
-
filters = { ...filters,
|
|
136
|
+
filters = { ...filters, retriesOnly: checked ? true : undefined };
|
|
137
137
|
}
|
|
138
138
|
</script>
|
|
139
139
|
|
|
@@ -212,7 +212,7 @@
|
|
|
212
212
|
</details>
|
|
213
213
|
</section>
|
|
214
214
|
|
|
215
|
-
<!-- Date range (filters on
|
|
215
|
+
<!-- Date range (filters on createdAt). Independent From/To bounds: a
|
|
216
216
|
bare date is that day's 00:00 (From) or its end (To), so either edge
|
|
217
217
|
applies on its own and the same date in both captures the day. -->
|
|
218
218
|
<section class="flex flex-col gap-2">
|
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
<span class={labelClass}>From</span>
|
|
223
223
|
<input
|
|
224
224
|
type="date"
|
|
225
|
-
value={filters.
|
|
225
|
+
value={filters.createdAfter ? isoToDayInput(filters.createdAfter) : ""}
|
|
226
226
|
onchange={(e) => setFrom(e.currentTarget.value)}
|
|
227
227
|
class={selectClass}
|
|
228
228
|
/>
|
|
@@ -231,7 +231,7 @@
|
|
|
231
231
|
<span class={labelClass}>To</span>
|
|
232
232
|
<input
|
|
233
233
|
type="date"
|
|
234
|
-
value={filters.
|
|
234
|
+
value={filters.createdBefore ? isoToDayInput(filters.createdBefore) : ""}
|
|
235
235
|
onchange={(e) => setTo(e.currentTarget.value)}
|
|
236
236
|
class={selectClass}
|
|
237
237
|
/>
|
|
@@ -245,7 +245,7 @@
|
|
|
245
245
|
<span class={headerClass}>Task</span>
|
|
246
246
|
<select
|
|
247
247
|
class={selectClass}
|
|
248
|
-
value={filters.
|
|
248
|
+
value={filters.taskName ?? ""}
|
|
249
249
|
onchange={(e) => setTask(e.currentTarget.value)}
|
|
250
250
|
>
|
|
251
251
|
<option value="">Any task</option>
|
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
<span class={headerClass}>Triggered by</span>
|
|
262
262
|
<select
|
|
263
263
|
class={selectClass}
|
|
264
|
-
value={filters.
|
|
264
|
+
value={filters.triggeredBy ?? ""}
|
|
265
265
|
onchange={(e) => setTrigger(e.currentTarget.value)}
|
|
266
266
|
>
|
|
267
267
|
<option value="">Any trigger</option>
|
|
@@ -296,7 +296,7 @@
|
|
|
296
296
|
<span class={headerClass}>Retries</span>
|
|
297
297
|
<Toggle
|
|
298
298
|
size="sm"
|
|
299
|
-
checked={Boolean(filters.
|
|
299
|
+
checked={Boolean(filters.retriesOnly)}
|
|
300
300
|
label="Only retried runs"
|
|
301
301
|
onchange={setRetriesOnly}
|
|
302
302
|
/>
|