@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.
Files changed (29) hide show
  1. package/package.json +4 -7
  2. package/src/lib/actions/portal.ts +0 -3
  3. package/src/lib/components/CronInput.svelte +0 -4
  4. package/src/lib/components/DataGrid.svelte +1 -3
  5. package/src/lib/components/Drawer.svelte +0 -3
  6. package/src/lib/components/Dropdown.svelte +2 -6
  7. package/src/lib/components/DurationInput.svelte +1 -1
  8. package/src/lib/components/FormField.svelte +1 -1
  9. package/src/lib/components/Input.svelte +1 -1
  10. package/src/lib/components/Modal.svelte +0 -3
  11. package/src/lib/components/Select.svelte +2 -5
  12. package/src/lib/components/StatusIndicator.svelte +0 -3
  13. package/src/lib/components/Tabs.svelte +0 -2
  14. package/src/lib/components/ToastContainer.svelte +0 -5
  15. package/src/lib/components/Tooltip.svelte +5 -2
  16. package/src/lib/components/dashboard/RunDetailPanel.svelte +164 -238
  17. package/src/lib/components/dashboard/RunFilterPopover.svelte +14 -14
  18. package/src/lib/components/dashboard/RunsList.svelte +29 -29
  19. package/src/lib/components/dashboard/run-filters.test.ts +31 -33
  20. package/src/lib/components/dashboard/run-filters.ts +33 -33
  21. package/src/lib/components/dashboard/run-helpers.test.ts +42 -17
  22. package/src/lib/components/dashboard/run-helpers.ts +53 -17
  23. package/src/lib/components/dashboard/status-config.ts +9 -8
  24. package/src/lib/theme-tokens.css +0 -2
  25. package/src/lib/utils/error.ts +0 -3
  26. package/src/lib/utils/format.test.ts +2 -6
  27. package/src/lib/utils/format.ts +28 -3
  28. package/src/lib/components/FeatureCard.svelte +0 -35
  29. 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 { LogEvent, LogSlice } from "../../log-console/types.js";
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
- // Single SSE call seeds the viewport via backfill (negative
157
- // fromLine = tail-from-end) and continues with live events on
158
- // the same connection. No tail+stream handoff, no duplicate
159
- // replay, native Last-Event-ID resume on reconnect.
160
- cleanup = stream(
161
- id,
162
- (event: LogEvent) => {
163
- if (logConsole) logConsole.onStream(event);
164
- },
165
- { fromLine: -TAIL_LINES },
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
- // One-word context line under the Exit value. Only success/failed carry a
208
- // real process exit code; the others end on a synthetic sentinel, so the
209
- // cell reads the reason ("stopped before exit") rather than "Code -1".
210
- function exitSubLabel(displayed: string): string {
211
- if (displayed === "running" || displayed === "pending") return "in progress";
212
- if (displayed === "success") return "clean";
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
- return "var(--color-surface-raised)";
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.start_at ?? run.created_at}
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.instance_index, getInstanceCount(run.task_name))}
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
- {@const showCode = status === "success" || status === "failed"}
330
- {@const exitClean = status === "success"}
331
- {@const exitFail = status === "failed"}
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 accent = accentVar(status)}
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 black-box readout washed in a faint tint of the
356
- run's outcome. -->
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: {accent}"
350
+ style="--rw-oc: {alarm ?? 'var(--color-surface-raised)'}"
360
351
  >
361
- <div class="pt-[18px] pr-[22px] pb-[14px] pl-[26px]">
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
- <div class="flex flex-wrap items-center gap-2.5">
372
- <h2 class="text-lg font-[650] tracking-tight text-on-surface">
373
- {#if showTaskName}
374
- {run.task_name}{#if suffix}<span class="text-on-surface-muted"
375
- >{suffix}</span
376
- >{/if}
377
- {:else}
378
- <span title={formatFullDateTime(startedAt)}
379
- >Run · {formatDateTime(startedAt)}</span
380
- >{#if suffix}
381
- <span class="text-on-surface-muted"
382
- >· instance {suffix}</span
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
- {/if}
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
- <!-- Status word: the pill is colored by the run's own
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-1.5 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-sm text-on-surface-muted"
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
- {#if showTaskName}
402
- <!-- When the title carries the task name, the run's
403
- date/time lives here in the sub-line (otherwise it's
404
- already the title, so don't repeat it). -->
405
- <span class="font-mono" title={formatFullDateTime(startedAt)}>
406
- Run · {formatCalendarDate(startedAt)}
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
- {#if copiedId}
418
- <Check size={11} class="text-success-surface" />Copied
419
- {:else}
420
- <Hash size={11} />{run.id}
421
- {/if}
422
- </button>
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="hidden h-1 w-1 rounded-full bg-outline-faint sm:block"
425
- ></span>
426
- <span class="inline-flex items-center gap-1.5 font-mono">
427
- <RotateCw size={14} />
428
- Retry #{run.retry_attempt}{#if run.retry_of_run_id}
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}&nbsp;of
444
+ {formatShortId(run.retryOfRunId)}{/if}
432
445
  </span>
433
446
  {/if}
434
447
  {#if paramEntries.length > 0}
435
- <span class="hidden h-1 w-1 rounded-full bg-outline-faint sm:block"
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.5 rounded-[3px] border border-outline-faint bg-surface-sunken px-2.5 py-0.5 font-mono text-xs font-medium text-on-surface-muted hover:border-outline-hover hover:text-primary"
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={12} />
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.task_name,
624
+ run.taskName,
599
625
  )}/runs/{encodeURIComponent(run.id)}/log/raw"
600
- download="{run.task_name}-{run.id}.log"
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.task_name}{suffix}
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
- /* The header is washed in a faint tint of the run's outcome colour
873
- (--rw-oc, set inline). Neutral statuses pass the surface itself, so the
874
- mix collapses to a plain surface with no visible tint. */
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) 4.5%, var(--color-surface-raised));
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.task_name ? 1 : 0));
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, created_after: value ? dayStartIso(value) : undefined };
106
+ filters = { ...filters, createdAfter: value ? dayStartIso(value) : undefined };
107
107
  }
108
108
 
109
109
  function setTo(value: string) {
110
- filters = { ...filters, created_before: value ? dayEndIso(value) : undefined };
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, task_name: value || undefined };
116
+ filters = { ...filters, taskName: value || undefined };
117
117
  }
118
118
 
119
119
  function setTrigger(value: string) {
120
- filters = { ...filters, triggered_by: value || undefined };
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.exit_code ?? "");
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, exit_code: trimmed || undefined };
132
+ filters = { ...filters, exitCode: trimmed || undefined };
133
133
  }
134
134
 
135
135
  function setRetriesOnly(checked: boolean) {
136
- filters = { ...filters, retries_only: checked ? true : undefined };
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 created_at). Independent From/To bounds: a
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.created_after ? isoToDayInput(filters.created_after) : ""}
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.created_before ? isoToDayInput(filters.created_before) : ""}
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.task_name ?? ""}
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.triggered_by ?? ""}
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.retries_only)}
299
+ checked={Boolean(filters.retriesOnly)}
300
300
  label="Only retried runs"
301
301
  onchange={setRetriesOnly}
302
302
  />