@runwisp/ui 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/lib/assets/runwisp-logo.svg +2 -2
- package/src/lib/components/LogConsole.svelte +518 -68
- package/src/lib/components/Popover.svelte +64 -31
- package/src/lib/components/dashboard/RunDetailPanel.svelte +753 -212
- package/src/lib/components/dashboard/RunFilterPopover.svelte +299 -0
- package/src/lib/components/dashboard/RunsList.svelte +418 -234
- package/src/lib/components/dashboard/run-filters.test.ts +299 -0
- package/src/lib/components/dashboard/run-filters.ts +384 -0
- package/src/lib/components/dashboard/run-helpers.test.ts +20 -0
- package/src/lib/components/dashboard/run-helpers.ts +15 -0
- package/src/lib/index.ts +20 -4
- package/src/lib/log-console/LogCache.svelte.ts +50 -1
- package/src/lib/log-console/types.ts +17 -0
- package/src/lib/theme-tokens.css +74 -62
- package/src/lib/theme.css +1 -0
- package/src/lib/utils/format.test.ts +38 -0
- package/src/lib/utils/format.ts +36 -0
- package/src/stories/assets/runwisp_c.svg +2 -2
|
@@ -4,24 +4,39 @@
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import { untrack } from "svelte";
|
|
6
6
|
import {
|
|
7
|
-
Server,
|
|
8
7
|
Hash,
|
|
8
|
+
Check,
|
|
9
9
|
Terminal as TerminalIcon,
|
|
10
10
|
Download,
|
|
11
11
|
Trash2,
|
|
12
|
+
Play,
|
|
13
|
+
Square,
|
|
12
14
|
MousePointerClick,
|
|
13
15
|
RotateCw,
|
|
16
|
+
RefreshCcw,
|
|
17
|
+
ChevronDown,
|
|
18
|
+
History,
|
|
19
|
+
PanelLeftClose,
|
|
14
20
|
SlidersHorizontal,
|
|
21
|
+
Maximize2,
|
|
22
|
+
Minimize2,
|
|
23
|
+
TextWrap,
|
|
15
24
|
} from "@lucide/svelte";
|
|
16
|
-
import Badge from "../Badge.svelte";
|
|
17
25
|
import Button from "../Button.svelte";
|
|
18
26
|
import EmptyState from "../EmptyState.svelte";
|
|
19
27
|
import LogConsole from "../LogConsole.svelte";
|
|
20
28
|
import Popover from "../Popover.svelte";
|
|
21
29
|
import Tooltip from "../Tooltip.svelte";
|
|
30
|
+
import { portal } from "../../actions/portal.js";
|
|
22
31
|
import type { Run } from "./types.js";
|
|
23
32
|
import type { LogEvent, LogSlice } from "../../log-console/types.js";
|
|
24
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
formatDateTime,
|
|
35
|
+
formatFullDateTime,
|
|
36
|
+
formatClockTime,
|
|
37
|
+
formatCalendarDate,
|
|
38
|
+
formatTimeHM,
|
|
39
|
+
} from "../../utils/format.js";
|
|
25
40
|
import { formatShortId } from "../../utils/id.js";
|
|
26
41
|
import { getRunStatusConfig, runDisplayStatus } from "./status-config.js";
|
|
27
42
|
import {
|
|
@@ -29,15 +44,28 @@
|
|
|
29
44
|
runStartDelay,
|
|
30
45
|
formatTriggeredByLabel,
|
|
31
46
|
runRetryLabel,
|
|
47
|
+
instanceSuffix,
|
|
32
48
|
} from "./run-helpers.js";
|
|
33
49
|
|
|
34
50
|
let {
|
|
35
51
|
run,
|
|
36
52
|
fetchLogs,
|
|
37
53
|
streamLogs,
|
|
54
|
+
fetchLineHistory,
|
|
38
55
|
showTaskName = false,
|
|
39
56
|
onDelete,
|
|
57
|
+
onRun,
|
|
58
|
+
onRunAgain,
|
|
59
|
+
onStop,
|
|
60
|
+
onRunTask,
|
|
61
|
+
onStopService,
|
|
62
|
+
onRestartService,
|
|
63
|
+
serviceStopped = false,
|
|
64
|
+
serviceBusy = false,
|
|
65
|
+
onToggleHistory,
|
|
66
|
+
historyVisible = false,
|
|
40
67
|
highlightLine = null,
|
|
68
|
+
getInstanceCount = () => 1,
|
|
41
69
|
}: {
|
|
42
70
|
run: Run | undefined;
|
|
43
71
|
fetchLogs: (
|
|
@@ -50,9 +78,39 @@
|
|
|
50
78
|
onEvent: (event: LogEvent) => void,
|
|
51
79
|
initialState?: { fromLine: number },
|
|
52
80
|
) => () => void;
|
|
81
|
+
// Resolves the prior whole-region frames of a settled progress bar /
|
|
82
|
+
// redraw line so the log console can offer inline rewind. Optional.
|
|
83
|
+
fetchLineHistory?: (runId: string, lineNum: number) => Promise<string[][]>;
|
|
53
84
|
showTaskName?: boolean;
|
|
54
85
|
onDelete?: (runId: string) => void;
|
|
86
|
+
// Start a new run with default options. When set, the action cluster's
|
|
87
|
+
// primary control is a Run button (the cold-start / re-trigger path).
|
|
88
|
+
onRun?: (() => void) | undefined;
|
|
89
|
+
// Re-run reusing the selected run's parameters. Passed only when the task
|
|
90
|
+
// actually has parameters (otherwise it is identical to onRun); it becomes
|
|
91
|
+
// the dropdown half of the Run split button. Omitted on the cross-task
|
|
92
|
+
// runs page or for tasks whose API trigger is disabled.
|
|
93
|
+
onRunAgain?: (() => void) | undefined;
|
|
94
|
+
// Stop the selected run while it is live. When provided and the run is
|
|
95
|
+
// running, the action cluster shows Stop alongside Run.
|
|
96
|
+
onStop?: ((runId: string) => void) | undefined;
|
|
97
|
+
// Trigger the task from the empty state (no run selected yet) — the
|
|
98
|
+
// cold-start path so a never-run task is still launchable from here.
|
|
99
|
+
onRunTask?: (() => void) | undefined;
|
|
100
|
+
// Service lifecycle controls. When either is set the cluster swaps the
|
|
101
|
+
// Run control for Stop Service / Restart Service (chosen by serviceStopped).
|
|
102
|
+
onStopService?: (() => void) | undefined;
|
|
103
|
+
onRestartService?: (() => void) | undefined;
|
|
104
|
+
serviceStopped?: boolean;
|
|
105
|
+
serviceBusy?: boolean;
|
|
106
|
+
// Toggle the (single-instance service) history rail from the panel header,
|
|
107
|
+
// since these tasks hide the rail by default and there is no top bar.
|
|
108
|
+
onToggleHistory?: (() => void) | undefined;
|
|
109
|
+
historyVisible?: boolean;
|
|
55
110
|
highlightLine?: number | null;
|
|
111
|
+
// Resolves a task's currently configured instance count so multi-instance
|
|
112
|
+
// services render a 1-based #N suffix. Defaults to single-instance.
|
|
113
|
+
getInstanceCount?: (taskName: string) => number;
|
|
56
114
|
} = $props();
|
|
57
115
|
|
|
58
116
|
let canDelete = $derived.by(() => {
|
|
@@ -96,267 +154,750 @@
|
|
|
96
154
|
if (cleanup) cleanup();
|
|
97
155
|
};
|
|
98
156
|
});
|
|
157
|
+
|
|
158
|
+
// Maximize the console to a full-bleed overlay. The console wrapper is
|
|
159
|
+
// never remounted — `maximizePortal` relocates the *live* node to <body>
|
|
160
|
+
// (escaping any transformed/blurred ancestor that would otherwise trap a
|
|
161
|
+
// position:fixed child) and moves it back on restore, so the SSE stream,
|
|
162
|
+
// scroll position, and the {#key run.id} mount all survive the toggle.
|
|
163
|
+
let consoleMaximized = $state(false);
|
|
164
|
+
// Wrap long console lines instead of horizontally scrolling them. Owned
|
|
165
|
+
// here so the toolbar toggle and the <LogConsole> share one source of truth.
|
|
166
|
+
let consoleWrap = $state(false);
|
|
167
|
+
|
|
168
|
+
// A run going away (deselect) tears down the console; drop the overlay too.
|
|
169
|
+
$effect(() => {
|
|
170
|
+
if (!run) consoleMaximized = false;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Inline-confirm popover for delete, and a transient "Copied" on the run-id chip.
|
|
174
|
+
let confirmDeleteOpen = $state(false);
|
|
175
|
+
// Dropdown half of the Run split button (the "reuse parameters" variant).
|
|
176
|
+
let runMenuOpen = $state(false);
|
|
177
|
+
let copiedId = $state(false);
|
|
178
|
+
let copyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
179
|
+
|
|
180
|
+
async function copyRunId(id: string) {
|
|
181
|
+
try {
|
|
182
|
+
await navigator.clipboard.writeText(id);
|
|
183
|
+
copiedId = true;
|
|
184
|
+
if (copyTimer) clearTimeout(copyTimer);
|
|
185
|
+
copyTimer = setTimeout(() => (copiedId = false), 1200);
|
|
186
|
+
} catch {
|
|
187
|
+
// Clipboard blocked (insecure context / denied) — leave the chip as-is.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// One-word context line under the Exit value. Only success/failed carry a
|
|
192
|
+
// real process exit code; the others end on a synthetic sentinel, so the
|
|
193
|
+
// cell reads the reason ("stopped before exit") rather than "Code -1".
|
|
194
|
+
function exitSubLabel(displayed: string): string {
|
|
195
|
+
if (displayed === "running" || displayed === "pending") return "in progress";
|
|
196
|
+
if (displayed === "success") return "clean";
|
|
197
|
+
if (displayed === "failed") return "non-zero";
|
|
198
|
+
if (displayed === "stopped" || displayed === "daemon_stopped") return "stopped before exit";
|
|
199
|
+
if (displayed === "timeout") return "timed out";
|
|
200
|
+
if (displayed === "crashed") return "killed";
|
|
201
|
+
return "no exit code";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// One-word context line under each Trigger value, mirroring the source.
|
|
205
|
+
function triggerSub(triggeredBy: Run["triggered_by"]): string {
|
|
206
|
+
if (triggeredBy === "cron") return "schedule";
|
|
207
|
+
if (triggeredBy === "api") return "manual";
|
|
208
|
+
if (triggeredBy === "service") return "supervised";
|
|
209
|
+
if (triggeredBy === "startup") return "on boot";
|
|
210
|
+
return "remote";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// The status's accent color as a runtime CSS variable, used to wash the
|
|
214
|
+
// header in a faint tint of the outcome (the "black-box readout" look).
|
|
215
|
+
// Neutral statuses resolve to the surface itself, so the tint vanishes.
|
|
216
|
+
function accentVar(displayed: string): string {
|
|
217
|
+
if (displayed === "running" || displayed === "pending") return "var(--color-info)";
|
|
218
|
+
if (displayed === "success") return "var(--color-success-surface)";
|
|
219
|
+
if (
|
|
220
|
+
displayed === "stopped" ||
|
|
221
|
+
displayed === "timeout" ||
|
|
222
|
+
displayed === "daemon_stopped" ||
|
|
223
|
+
displayed === "queue_full"
|
|
224
|
+
)
|
|
225
|
+
return "var(--color-warning-surface)";
|
|
226
|
+
if (
|
|
227
|
+
displayed === "failed" ||
|
|
228
|
+
displayed === "crashed" ||
|
|
229
|
+
displayed === "log_overflow" ||
|
|
230
|
+
displayed === "missed" ||
|
|
231
|
+
displayed === "start_failed"
|
|
232
|
+
)
|
|
233
|
+
return "var(--color-danger-surface)";
|
|
234
|
+
return "var(--color-surface-raised)";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function handleConsoleKeydown(event: KeyboardEvent) {
|
|
238
|
+
if (!run) return;
|
|
239
|
+
if (event.key === "Escape" && consoleMaximized) {
|
|
240
|
+
consoleMaximized = false;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// `F` toggles focus mode, the way a media player does — but never while
|
|
244
|
+
// the operator is typing into a field (search box, etc.).
|
|
245
|
+
if (
|
|
246
|
+
(event.key === "f" || event.key === "F") &&
|
|
247
|
+
!event.metaKey &&
|
|
248
|
+
!event.ctrlKey &&
|
|
249
|
+
!event.altKey
|
|
250
|
+
) {
|
|
251
|
+
const target = event.target;
|
|
252
|
+
if (
|
|
253
|
+
target instanceof HTMLElement &&
|
|
254
|
+
(target.tagName === "INPUT" ||
|
|
255
|
+
target.tagName === "TEXTAREA" ||
|
|
256
|
+
target.isContentEditable)
|
|
257
|
+
)
|
|
258
|
+
return;
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
consoleMaximized = !consoleMaximized;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function maximizePortal(node: HTMLElement, active: boolean) {
|
|
265
|
+
const anchor = document.createComment("maximized-console");
|
|
266
|
+
function moveOut() {
|
|
267
|
+
const parent = node.parentNode;
|
|
268
|
+
if (parent && parent !== document.body) {
|
|
269
|
+
parent.insertBefore(anchor, node);
|
|
270
|
+
document.body.appendChild(node);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function moveBack() {
|
|
274
|
+
const parent = anchor.parentNode;
|
|
275
|
+
if (parent) {
|
|
276
|
+
parent.insertBefore(node, anchor);
|
|
277
|
+
anchor.remove();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (active) moveOut();
|
|
281
|
+
return {
|
|
282
|
+
update(next: boolean) {
|
|
283
|
+
if (next) moveOut();
|
|
284
|
+
else moveBack();
|
|
285
|
+
},
|
|
286
|
+
destroy() {
|
|
287
|
+
// Mirror the `portal` action: if still parked on <body>, remove
|
|
288
|
+
// the node so Svelte's own teardown doesn't leak it there.
|
|
289
|
+
if (node.parentNode === document.body) node.remove();
|
|
290
|
+
anchor.remove();
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
99
294
|
</script>
|
|
100
295
|
|
|
296
|
+
<svelte:window onkeydown={handleConsoleKeydown} />
|
|
297
|
+
|
|
101
298
|
{#if run}
|
|
102
|
-
{@const
|
|
299
|
+
{@const status = runDisplayStatus(run)}
|
|
300
|
+
{@const config = getRunStatusConfig(status)}
|
|
103
301
|
{@const DetailIcon = config.icon}
|
|
104
302
|
{@const duration = runDuration(run)}
|
|
105
303
|
{@const startDelay = runStartDelay(run)}
|
|
106
304
|
{@const startedAt = run.start_at ?? run.created_at}
|
|
107
305
|
{@const retry = runRetryLabel(run)}
|
|
108
306
|
{@const paramEntries = run.params ? Object.entries(run.params) : []}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
307
|
+
{@const suffix = instanceSuffix(run.instance_index, getInstanceCount(run.task_name))}
|
|
308
|
+
{@const spine = config.dot.replace(" animate-pulse", "")}
|
|
309
|
+
{@const isRunning = run.status === "running"}
|
|
310
|
+
{@const showCode = status === "success" || status === "failed"}
|
|
311
|
+
{@const exitClean = status === "success"}
|
|
312
|
+
{@const exitFail = status === "failed"}
|
|
313
|
+
{@const endLabel =
|
|
314
|
+
status === "stopped"
|
|
315
|
+
? "run stopped by operator"
|
|
316
|
+
: status === "daemon_stopped"
|
|
317
|
+
? "daemon stopped mid-run"
|
|
318
|
+
: status === "timeout"
|
|
319
|
+
? "run timed out"
|
|
320
|
+
: "end of output"}
|
|
321
|
+
{@const endTone =
|
|
322
|
+
status === "stopped" || status === "daemon_stopped" || status === "timeout"
|
|
323
|
+
? "warn"
|
|
324
|
+
: "muted"}
|
|
325
|
+
{@const accent = accentVar(status)}
|
|
326
|
+
<!-- The panel: a status spine runs the full left edge across both the header
|
|
327
|
+
readout and the console below, hugging the rail divider (artifact
|
|
328
|
+
".detail .spine"). -->
|
|
329
|
+
<div class="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
|
330
|
+
<!-- Status spine: a vivid edge coloring the panel by outcome; it streams
|
|
331
|
+
a light sweep while a run is live. -->
|
|
332
|
+
<div
|
|
333
|
+
class="absolute inset-y-0 left-0 z-[3] w-1 {spine} {isRunning ? 'spine-flow' : ''}"
|
|
334
|
+
aria-hidden="true"
|
|
335
|
+
></div>
|
|
336
|
+
<!-- Detailed header: a black-box readout washed in a faint tint of the
|
|
337
|
+
run's outcome. -->
|
|
338
|
+
<div
|
|
339
|
+
class="head-region @container relative shrink-0 border-b border-outline-faint"
|
|
340
|
+
style="--rw-oc: {accent}"
|
|
341
|
+
>
|
|
342
|
+
<div class="pt-[18px] pr-[22px] pb-[14px] pl-[26px]">
|
|
343
|
+
<div class="flex items-start gap-4">
|
|
344
|
+
<!-- Verdict tile -->
|
|
345
|
+
<div
|
|
346
|
+
class="flex h-[38px] w-[38px] shrink-0 items-center justify-center rounded-[11px] {config.bg} {config.color} ring-1 ring-current/25 ring-inset"
|
|
347
|
+
>
|
|
348
|
+
<DetailIcon size={20} class={isRunning ? "animate-spin" : ""} />
|
|
349
|
+
</div>
|
|
119
350
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
351
|
+
<div class="min-w-0 flex-1">
|
|
352
|
+
<div class="flex flex-wrap items-center gap-2.5">
|
|
353
|
+
<h2 class="text-lg font-[650] tracking-tight text-on-surface">
|
|
354
|
+
{#if showTaskName}
|
|
355
|
+
{run.task_name}{#if suffix}<span class="text-on-surface-muted"
|
|
356
|
+
>{suffix}</span
|
|
357
|
+
>{/if}
|
|
358
|
+
{:else}
|
|
359
|
+
<span title={formatFullDateTime(startedAt)}
|
|
360
|
+
>Run · {formatDateTime(startedAt)}</span
|
|
361
|
+
>{#if suffix}
|
|
362
|
+
<span class="text-on-surface-muted"
|
|
363
|
+
>· instance {suffix}</span
|
|
364
|
+
>
|
|
365
|
+
{/if}
|
|
134
366
|
{/if}
|
|
367
|
+
</h2>
|
|
368
|
+
<!-- Status word: the pill is colored by the run's own
|
|
369
|
+
outcome, so stopped reads amber, not red. -->
|
|
370
|
+
<Tooltip content={config.description} position="bottom" wide>
|
|
371
|
+
<span
|
|
372
|
+
class="rounded-full border px-2.5 py-0.5 text-2xs font-bold tracking-wider uppercase {config.color} {config.bg} {config.border}"
|
|
373
|
+
>
|
|
374
|
+
{status.toUpperCase()}
|
|
375
|
+
</span>
|
|
376
|
+
</Tooltip>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div
|
|
380
|
+
class="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-sm text-on-surface-muted"
|
|
381
|
+
>
|
|
382
|
+
{#if showTaskName}
|
|
383
|
+
<!-- When the title carries the task name, the run's
|
|
384
|
+
date/time lives here in the sub-line (otherwise it's
|
|
385
|
+
already the title, so don't repeat it). -->
|
|
386
|
+
<span title={formatFullDateTime(startedAt)}>
|
|
387
|
+
Run · {formatCalendarDate(startedAt)}
|
|
388
|
+
{formatTimeHM(startedAt)}
|
|
389
|
+
</span>
|
|
135
390
|
{/if}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
? "info"
|
|
143
|
-
: "danger"}
|
|
144
|
-
>
|
|
145
|
-
{runDisplayStatus(run).toUpperCase()}
|
|
146
|
-
</Badge>
|
|
147
|
-
</Tooltip>
|
|
148
|
-
{#if canDelete}
|
|
149
|
-
<Button
|
|
150
|
-
variant="ghost"
|
|
151
|
-
size="xs"
|
|
152
|
-
class="ml-1 text-danger-surface hover:bg-danger-soft"
|
|
153
|
-
onclick={() => onDelete?.(run.id)}
|
|
154
|
-
title="Delete this run"
|
|
155
|
-
aria-label="Delete run"
|
|
391
|
+
<!-- Run-id chip: click to copy the full ULID -->
|
|
392
|
+
<button
|
|
393
|
+
type="button"
|
|
394
|
+
onclick={() => copyRunId(run.id)}
|
|
395
|
+
title="Copy run ID"
|
|
396
|
+
class="inline-flex items-center gap-1.5 rounded-md border border-outline-faint bg-surface-sunken px-1.5 py-0.5 font-mono text-2xs text-on-surface-faint transition-colors hover:border-outline hover:text-on-surface-muted"
|
|
156
397
|
>
|
|
157
|
-
{#
|
|
158
|
-
|
|
159
|
-
|
|
398
|
+
{#if copiedId}
|
|
399
|
+
<Check size={11} class="text-success-surface" />Copied
|
|
400
|
+
{:else}
|
|
401
|
+
<Hash size={11} />{run.id}
|
|
402
|
+
{/if}
|
|
403
|
+
</button>
|
|
404
|
+
{#if retry}
|
|
405
|
+
<span class="hidden h-1 w-1 rounded-full bg-outline-faint sm:block"
|
|
406
|
+
></span>
|
|
407
|
+
<span class="inline-flex items-center gap-1.5">
|
|
408
|
+
<RotateCw size={14} />
|
|
409
|
+
Retry #{run.retry_attempt}{#if run.retry_of_run_id}
|
|
410
|
+
<span class="text-on-surface-faint"
|
|
411
|
+
>of run {formatShortId(run.retry_of_run_id)}</span
|
|
412
|
+
>{/if}
|
|
413
|
+
</span>
|
|
414
|
+
{/if}
|
|
415
|
+
{#if paramEntries.length > 0}
|
|
416
|
+
<span class="hidden h-1 w-1 rounded-full bg-outline-faint sm:block"
|
|
417
|
+
></span>
|
|
418
|
+
<Popover placement="bottom-start">
|
|
419
|
+
{#snippet trigger()}
|
|
420
|
+
<span
|
|
421
|
+
class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-outline-faint bg-surface-sunken px-2.5 py-0.5 text-xs font-medium text-on-surface-muted transition-colors hover:border-outline hover:text-on-surface"
|
|
422
|
+
title="View run parameters"
|
|
423
|
+
>
|
|
424
|
+
<SlidersHorizontal size={12} />
|
|
425
|
+
{paramEntries.length}
|
|
426
|
+
{paramEntries.length === 1 ? "parameter" : "parameters"}
|
|
427
|
+
</span>
|
|
428
|
+
{/snippet}
|
|
429
|
+
<div class="max-w-md min-w-48">
|
|
430
|
+
<span
|
|
431
|
+
class="text-xs font-semibold tracking-wide text-on-surface-faint uppercase"
|
|
432
|
+
>Parameters</span
|
|
433
|
+
>
|
|
434
|
+
<dl
|
|
435
|
+
class="mt-2 grid max-h-72 grid-cols-[max-content_1fr] gap-x-4 gap-y-1.5 overflow-y-auto"
|
|
436
|
+
>
|
|
437
|
+
{#each paramEntries as [key, value] (key)}
|
|
438
|
+
<dt class="font-mono text-xs text-on-surface-muted">
|
|
439
|
+
{key}
|
|
440
|
+
</dt>
|
|
441
|
+
<dd
|
|
442
|
+
class="font-mono text-xs font-medium break-all text-on-surface"
|
|
443
|
+
>
|
|
444
|
+
{value}
|
|
445
|
+
</dd>
|
|
446
|
+
{/each}
|
|
447
|
+
</dl>
|
|
448
|
+
</div>
|
|
449
|
+
</Popover>
|
|
450
|
+
{/if}
|
|
451
|
+
</div>
|
|
160
452
|
</div>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
453
|
+
|
|
454
|
+
<!-- Actions: history toggle · run (split) / service lifecycle ·
|
|
455
|
+
stop · download · delete (delete behind an inline confirm) -->
|
|
456
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
457
|
+
{#if onToggleHistory}
|
|
458
|
+
<button
|
|
459
|
+
type="button"
|
|
460
|
+
onclick={() => onToggleHistory()}
|
|
461
|
+
class="inline-flex items-center justify-center rounded-lg border border-outline-faint bg-surface-raised p-2 text-on-surface-muted transition-colors hover:bg-surface-sunken hover:text-on-surface"
|
|
462
|
+
title={historyVisible ? "Hide run history" : "Show run history"}
|
|
463
|
+
aria-label={historyVisible
|
|
464
|
+
? "Hide run history"
|
|
465
|
+
: "Show run history"}
|
|
167
466
|
>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
467
|
+
{#if historyVisible}
|
|
468
|
+
<PanelLeftClose size={15} />
|
|
469
|
+
{:else}
|
|
470
|
+
<History size={15} />
|
|
471
|
+
{/if}
|
|
472
|
+
</button>
|
|
473
|
+
{/if}
|
|
474
|
+
|
|
475
|
+
{#if onStopService || onRestartService}
|
|
476
|
+
<!-- Service lifecycle replaces the run control. -->
|
|
477
|
+
{#if serviceStopped}
|
|
478
|
+
{#if onRestartService}
|
|
479
|
+
<button
|
|
480
|
+
type="button"
|
|
481
|
+
onclick={() => onRestartService()}
|
|
482
|
+
disabled={serviceBusy}
|
|
483
|
+
class="inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-lg border border-primary-soft bg-surface-raised px-3 text-sm font-medium text-primary transition-colors hover:bg-primary-soft disabled:cursor-not-allowed disabled:opacity-60"
|
|
484
|
+
>
|
|
485
|
+
<RefreshCcw size={15} />
|
|
486
|
+
<span class="@max-md:hidden">Restart Service</span>
|
|
487
|
+
</button>
|
|
488
|
+
{/if}
|
|
489
|
+
{:else if onStopService}
|
|
490
|
+
<button
|
|
491
|
+
type="button"
|
|
492
|
+
onclick={() => onStopService()}
|
|
493
|
+
disabled={serviceBusy}
|
|
494
|
+
title="Stops the service for the rest of the daemon's lifetime"
|
|
495
|
+
class="inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-lg border border-danger-soft bg-surface-raised px-3 text-sm font-medium text-danger-surface transition-colors hover:bg-danger-soft disabled:cursor-not-allowed disabled:opacity-60"
|
|
180
496
|
>
|
|
181
|
-
|
|
497
|
+
<Square size={15} fill="currentColor" stroke="none" />
|
|
498
|
+
<span class="@max-md:hidden">Stop Service</span>
|
|
499
|
+
</button>
|
|
500
|
+
{/if}
|
|
182
501
|
{:else}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
></span
|
|
502
|
+
{#if onRun}
|
|
503
|
+
<!-- Split button: Run (defaults) + an arrow for the
|
|
504
|
+
reuse-parameters variant, shown only when there is one. -->
|
|
505
|
+
<div
|
|
506
|
+
class="inline-flex overflow-hidden rounded-lg border border-primary-soft"
|
|
189
507
|
>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
508
|
+
<button
|
|
509
|
+
type="button"
|
|
510
|
+
onclick={() => onRun()}
|
|
511
|
+
title="Run this task now with default options"
|
|
512
|
+
class="inline-flex h-9 cursor-pointer items-center gap-1.5 bg-surface-raised px-3 text-sm font-medium text-primary transition-colors hover:bg-primary-soft"
|
|
513
|
+
>
|
|
514
|
+
<Play size={15} fill="currentColor" stroke="none" />
|
|
515
|
+
<span class="@max-md:hidden">Run</span>
|
|
516
|
+
</button>
|
|
517
|
+
{#if onRunAgain}
|
|
518
|
+
<Popover bind:open={runMenuOpen} placement="bottom-end">
|
|
519
|
+
{#snippet trigger()}
|
|
520
|
+
<span
|
|
521
|
+
class="flex h-9 cursor-pointer items-center border-l border-primary-soft bg-surface-raised px-2 text-primary transition-colors hover:bg-primary-soft"
|
|
522
|
+
title="More run options"
|
|
523
|
+
aria-label="More run options"
|
|
524
|
+
>
|
|
525
|
+
<ChevronDown size={15} />
|
|
526
|
+
</span>
|
|
527
|
+
{/snippet}
|
|
528
|
+
<button
|
|
529
|
+
type="button"
|
|
530
|
+
onclick={() => {
|
|
531
|
+
runMenuOpen = false;
|
|
532
|
+
onRunAgain?.();
|
|
533
|
+
}}
|
|
534
|
+
class="flex w-56 items-start gap-2.5 text-left"
|
|
535
|
+
>
|
|
536
|
+
<RotateCw
|
|
537
|
+
size={15}
|
|
538
|
+
class="mt-0.5 shrink-0 text-on-surface-muted"
|
|
539
|
+
/>
|
|
540
|
+
<span class="flex flex-col">
|
|
541
|
+
<span
|
|
542
|
+
class="text-sm font-medium text-on-surface"
|
|
543
|
+
>Run again</span
|
|
544
|
+
>
|
|
545
|
+
<span class="text-xs text-on-surface-muted"
|
|
546
|
+
>Reuse this run's parameters</span
|
|
547
|
+
>
|
|
548
|
+
</span>
|
|
549
|
+
</button>
|
|
550
|
+
</Popover>
|
|
551
|
+
{/if}
|
|
552
|
+
</div>
|
|
553
|
+
{/if}
|
|
554
|
+
{#if isRunning && onStop}
|
|
555
|
+
<button
|
|
556
|
+
type="button"
|
|
557
|
+
onclick={() => onStop(run.id)}
|
|
558
|
+
class="inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-lg border border-danger-soft bg-surface-raised px-3 text-sm font-medium text-danger-surface transition-colors hover:bg-danger-soft"
|
|
212
559
|
>
|
|
213
|
-
|
|
560
|
+
<Square size={15} fill="currentColor" stroke="none" />
|
|
561
|
+
<span class="@max-md:hidden">Stop</span>
|
|
562
|
+
</button>
|
|
563
|
+
{/if}
|
|
214
564
|
{/if}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
565
|
+
<a
|
|
566
|
+
href="/api/tasks/{encodeURIComponent(
|
|
567
|
+
run.task_name,
|
|
568
|
+
)}/runs/{encodeURIComponent(run.id)}/log/raw"
|
|
569
|
+
download="{run.task_name}-{run.id}.log"
|
|
570
|
+
class="inline-flex items-center justify-center rounded-lg border border-outline-faint bg-surface-raised p-2 text-on-surface-muted transition-colors hover:bg-surface-sunken hover:text-on-surface"
|
|
571
|
+
title="Download the full log (rotated and current parts as one file)"
|
|
572
|
+
aria-label="Download log"
|
|
573
|
+
>
|
|
574
|
+
<Download size={15} />
|
|
575
|
+
</a>
|
|
576
|
+
{#if canDelete}
|
|
577
|
+
<Popover bind:open={confirmDeleteOpen} placement="bottom-end">
|
|
220
578
|
{#snippet trigger()}
|
|
221
579
|
<span
|
|
222
|
-
class="
|
|
223
|
-
title="
|
|
580
|
+
class="flex cursor-pointer items-center justify-center rounded-lg border border-outline-faint bg-surface-raised p-2 text-on-surface-muted transition-colors hover:border-danger-soft hover:bg-danger-soft hover:text-danger-surface"
|
|
581
|
+
title="Delete this run"
|
|
582
|
+
aria-label="Delete run"
|
|
224
583
|
>
|
|
225
|
-
<
|
|
226
|
-
{paramEntries.length}
|
|
227
|
-
{paramEntries.length === 1 ? "parameter" : "parameters"}
|
|
584
|
+
<Trash2 size={15} />
|
|
228
585
|
</span>
|
|
229
586
|
{/snippet}
|
|
230
|
-
<div class="
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
587
|
+
<div class="w-60">
|
|
588
|
+
<div class="text-sm font-semibold text-on-surface">
|
|
589
|
+
Delete this run?
|
|
590
|
+
</div>
|
|
591
|
+
<div class="mt-1 text-xs leading-relaxed text-on-surface-muted">
|
|
592
|
+
Its captured output is removed from disk. This can't be
|
|
593
|
+
undone.
|
|
594
|
+
</div>
|
|
595
|
+
<div class="mt-3 flex justify-end gap-2">
|
|
596
|
+
<Button
|
|
597
|
+
variant="ghost"
|
|
598
|
+
size="sm"
|
|
599
|
+
onclick={() => (confirmDeleteOpen = false)}
|
|
600
|
+
>
|
|
601
|
+
Cancel
|
|
602
|
+
</Button>
|
|
603
|
+
<Button
|
|
604
|
+
variant="danger"
|
|
605
|
+
size="sm"
|
|
606
|
+
onclick={() => {
|
|
607
|
+
confirmDeleteOpen = false;
|
|
608
|
+
onDelete?.(run.id);
|
|
609
|
+
}}
|
|
610
|
+
>
|
|
611
|
+
Delete run
|
|
612
|
+
</Button>
|
|
613
|
+
</div>
|
|
249
614
|
</div>
|
|
250
615
|
</Popover>
|
|
251
616
|
{/if}
|
|
252
617
|
</div>
|
|
253
618
|
</div>
|
|
254
|
-
</div>
|
|
255
619
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
<
|
|
262
|
-
class="
|
|
263
|
-
>Started</span
|
|
620
|
+
<!-- Instrument cluster: the run's vital readout — value over a one-word
|
|
621
|
+
context line, hairline-divided. -->
|
|
622
|
+
<div
|
|
623
|
+
class="mt-4 flex flex-wrap overflow-hidden rounded-xl border border-outline-faint bg-surface-raised"
|
|
624
|
+
>
|
|
625
|
+
<div
|
|
626
|
+
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"
|
|
264
627
|
>
|
|
265
|
-
<span class="font-mono text-sm font-medium whitespace-nowrap text-on-surface">
|
|
266
|
-
{run.start_at ? formatDateTime(run.start_at) : "--:--"}
|
|
267
|
-
</span>
|
|
268
|
-
{#if startDelay}
|
|
269
628
|
<span
|
|
270
|
-
class="
|
|
271
|
-
|
|
629
|
+
class="text-2xs font-bold tracking-wider text-on-surface-faint uppercase"
|
|
630
|
+
>Queued</span
|
|
272
631
|
>
|
|
273
|
-
|
|
632
|
+
<span
|
|
633
|
+
class="font-mono text-base font-medium tracking-tight whitespace-nowrap text-on-surface tabular-nums"
|
|
634
|
+
title={formatFullDateTime(run.created_at)}
|
|
635
|
+
>
|
|
636
|
+
{formatClockTime(run.created_at)}
|
|
274
637
|
</span>
|
|
275
|
-
|
|
276
|
-
|
|
638
|
+
<span class="text-2xs text-on-surface-muted"
|
|
639
|
+
>+{startDelay ?? "0s"} queued</span
|
|
640
|
+
>
|
|
641
|
+
</div>
|
|
277
642
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
>
|
|
281
|
-
<span
|
|
282
|
-
class="text-xs font-semibold tracking-wide text-on-surface-faint uppercase"
|
|
283
|
-
>Duration</span
|
|
643
|
+
<div
|
|
644
|
+
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"
|
|
284
645
|
>
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
646
|
+
<span
|
|
647
|
+
class="text-2xs font-bold tracking-wider text-on-surface-faint uppercase"
|
|
648
|
+
>Started</span
|
|
649
|
+
>
|
|
650
|
+
<span
|
|
651
|
+
class="font-mono text-base font-medium tracking-tight whitespace-nowrap text-on-surface tabular-nums"
|
|
652
|
+
>
|
|
653
|
+
{run.start_at ? formatClockTime(run.start_at) : "—"}
|
|
654
|
+
</span>
|
|
655
|
+
<span class="text-2xs text-on-surface-muted">
|
|
656
|
+
{run.start_at ? formatCalendarDate(run.start_at) : "not started"}
|
|
657
|
+
</span>
|
|
658
|
+
</div>
|
|
289
659
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
660
|
+
<div
|
|
661
|
+
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"
|
|
662
|
+
>
|
|
663
|
+
<span
|
|
664
|
+
class="text-2xs font-bold tracking-wider text-on-surface-faint uppercase"
|
|
665
|
+
>Ran for</span
|
|
666
|
+
>
|
|
667
|
+
<span
|
|
668
|
+
class="font-mono text-base font-medium tracking-tight whitespace-nowrap text-on-surface tabular-nums"
|
|
669
|
+
>
|
|
670
|
+
{duration ?? "—"}
|
|
671
|
+
</span>
|
|
672
|
+
<span class="text-2xs text-on-surface-muted">
|
|
673
|
+
{isRunning ? "and counting" : run.end_at ? "wall clock" : "—"}
|
|
674
|
+
</span>
|
|
675
|
+
</div>
|
|
676
|
+
|
|
677
|
+
<div
|
|
678
|
+
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"
|
|
296
679
|
>
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
680
|
+
<span
|
|
681
|
+
class="text-2xs font-bold tracking-wider text-on-surface-faint uppercase"
|
|
682
|
+
>Exit</span
|
|
683
|
+
>
|
|
684
|
+
<span
|
|
685
|
+
class="flex items-baseline gap-1.5 font-mono text-base font-medium tracking-tight tabular-nums {exitClean
|
|
686
|
+
? 'text-success-surface'
|
|
687
|
+
: exitFail
|
|
688
|
+
? 'text-danger-surface'
|
|
689
|
+
: 'text-on-surface'}"
|
|
690
|
+
>
|
|
691
|
+
<span>{showCode ? run.exit_code : "—"}</span>
|
|
692
|
+
{#if exitFail}
|
|
693
|
+
<span
|
|
694
|
+
class="rounded bg-danger-soft px-1 py-px text-[9px] font-bold tracking-wide text-danger-surface uppercase"
|
|
695
|
+
>fail</span
|
|
696
|
+
>
|
|
697
|
+
{/if}
|
|
698
|
+
</span>
|
|
699
|
+
<span class="text-2xs text-on-surface-muted">{exitSubLabel(status)}</span>
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
<div
|
|
703
|
+
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"
|
|
301
704
|
>
|
|
302
|
-
|
|
303
|
-
|
|
705
|
+
<span
|
|
706
|
+
class="text-2xs font-bold tracking-wider text-on-surface-faint uppercase"
|
|
707
|
+
>Trigger</span
|
|
708
|
+
>
|
|
709
|
+
<span
|
|
710
|
+
class="font-mono text-base font-medium tracking-tight whitespace-nowrap text-on-surface tabular-nums"
|
|
711
|
+
>{formatTriggeredByLabel(run.triggered_by)}</span
|
|
712
|
+
>
|
|
713
|
+
<span class="text-2xs text-on-surface-muted"
|
|
714
|
+
>{triggerSub(run.triggered_by)}</span
|
|
715
|
+
>
|
|
716
|
+
</div>
|
|
304
717
|
</div>
|
|
305
718
|
</div>
|
|
306
719
|
</div>
|
|
307
|
-
</div>
|
|
308
720
|
|
|
309
|
-
|
|
310
|
-
|
|
721
|
+
<!-- Console hero — maximizes to a full-bleed overlay via portal -->
|
|
722
|
+
{#if consoleMaximized}
|
|
723
|
+
<div
|
|
724
|
+
use:portal
|
|
725
|
+
class="console-backdrop fixed inset-0 z-40 bg-backdrop backdrop-blur-sm"
|
|
726
|
+
onclick={() => (consoleMaximized = false)}
|
|
727
|
+
role="presentation"
|
|
728
|
+
></div>
|
|
729
|
+
{/if}
|
|
311
730
|
<div
|
|
312
|
-
|
|
731
|
+
use:maximizePortal={consoleMaximized}
|
|
732
|
+
class={consoleMaximized
|
|
733
|
+
? "console-zoom fixed inset-3 z-50 flex flex-col overflow-hidden rounded-xl bg-[var(--rw-con-bg)] shadow-2xl ring-1 ring-[rgb(255_255_255_/_0.08)] sm:inset-6"
|
|
734
|
+
: "ml-1 flex min-h-[300px] flex-1 flex-col overflow-hidden border-t border-[rgb(255_255_255_/_0.06)] bg-[var(--rw-con-bg)]"}
|
|
313
735
|
>
|
|
314
|
-
<div
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
736
|
+
<div
|
|
737
|
+
class="flex shrink-0 items-center justify-between gap-3 border-b border-[rgb(255_255_255_/_0.06)] bg-[var(--rw-con-panel)] px-3.5 py-[9px] font-mono text-[11.5px] text-[var(--rw-con-dim)]"
|
|
738
|
+
>
|
|
739
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
740
|
+
{#if consoleMaximized}
|
|
741
|
+
<!-- Focus-mode identity: which run is filling the screen -->
|
|
742
|
+
<span class="flex min-w-0 items-center gap-2">
|
|
743
|
+
<span class="h-2 w-2 shrink-0 rounded-full {spine}"></span>
|
|
744
|
+
<span class="truncate font-medium text-[#b8c5d1]">
|
|
745
|
+
{run.task_name}{suffix}
|
|
746
|
+
</span>
|
|
747
|
+
<span class="text-[var(--rw-con-gutter)]">·</span>
|
|
748
|
+
<span class="font-semibold capitalize {config.color}">{status}</span>
|
|
749
|
+
</span>
|
|
750
|
+
{:else}
|
|
751
|
+
<span class="flex items-center gap-2 font-semibold text-[#98a7b5]">
|
|
752
|
+
<TerminalIcon size={14} class="opacity-70" />
|
|
753
|
+
Console output
|
|
754
|
+
</span>
|
|
755
|
+
<span class="hidden text-[var(--rw-con-gutter)] sm:inline"
|
|
756
|
+
>stdout + stderr</span
|
|
757
|
+
>
|
|
758
|
+
{/if}
|
|
759
|
+
</div>
|
|
760
|
+
<div class="flex shrink-0 items-center gap-3">
|
|
761
|
+
<button
|
|
762
|
+
type="button"
|
|
763
|
+
onclick={() => (consoleWrap = !consoleWrap)}
|
|
764
|
+
class="flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 transition-colors hover:bg-[rgb(255_255_255_/_0.05)] hover:text-[#b8c5d1] {consoleWrap
|
|
765
|
+
? 'text-[#b8c5d1]'
|
|
766
|
+
: 'text-[var(--rw-con-dim)]'}"
|
|
767
|
+
title={consoleWrap ? "Unwrap lines" : "Wrap long lines"}
|
|
768
|
+
aria-pressed={consoleWrap}
|
|
769
|
+
aria-label="Toggle line wrapping"
|
|
770
|
+
>
|
|
771
|
+
<TextWrap size={13} />
|
|
772
|
+
<span class="hidden sm:inline">Wrap</span>
|
|
773
|
+
</button>
|
|
774
|
+
<button
|
|
775
|
+
type="button"
|
|
776
|
+
onclick={() => (consoleMaximized = !consoleMaximized)}
|
|
777
|
+
class="flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-[var(--rw-con-dim)] transition-colors hover:bg-[rgb(255_255_255_/_0.05)] hover:text-[#b8c5d1]"
|
|
778
|
+
title={consoleMaximized ? "Restore console (Esc)" : "Maximize console · F"}
|
|
779
|
+
aria-label={consoleMaximized ? "Restore console" : "Maximize console"}
|
|
780
|
+
>
|
|
781
|
+
{#if consoleMaximized}
|
|
782
|
+
<Minimize2 size={13} />
|
|
783
|
+
<span
|
|
784
|
+
class="rounded border border-[var(--rw-con-gutter)] px-1.5 text-[10px] tracking-wide"
|
|
785
|
+
>Esc</span
|
|
786
|
+
>
|
|
787
|
+
{:else}
|
|
788
|
+
<Maximize2 size={13} />
|
|
789
|
+
Expand
|
|
790
|
+
{/if}
|
|
791
|
+
</button>
|
|
792
|
+
</div>
|
|
343
793
|
</div>
|
|
794
|
+
{#key run.id}
|
|
795
|
+
<LogConsole
|
|
796
|
+
bind:this={logConsole}
|
|
797
|
+
fetchLogs={(f: number, t: number) => fetchLogs(run.id, f, t)}
|
|
798
|
+
fetchLineHistory={fetchLineHistory
|
|
799
|
+
? (n: number) => fetchLineHistory(run.id, n)
|
|
800
|
+
: undefined}
|
|
801
|
+
bind:wrap={consoleWrap}
|
|
802
|
+
class="min-h-0 flex-1"
|
|
803
|
+
{highlightLine}
|
|
804
|
+
{endLabel}
|
|
805
|
+
{endTone}
|
|
806
|
+
/>
|
|
807
|
+
{/key}
|
|
344
808
|
</div>
|
|
345
|
-
{#key run.id}
|
|
346
|
-
<LogConsole
|
|
347
|
-
bind:this={logConsole}
|
|
348
|
-
fetchLogs={(f: number, t: number) => fetchLogs(run.id, f, t)}
|
|
349
|
-
class="min-h-0 flex-1"
|
|
350
|
-
{highlightLine}
|
|
351
|
-
/>
|
|
352
|
-
{/key}
|
|
353
809
|
</div>
|
|
354
810
|
{:else}
|
|
355
|
-
<div
|
|
811
|
+
<div
|
|
812
|
+
class="flex min-w-0 flex-1 flex-col items-center justify-center gap-4 bg-surface-sunken/30"
|
|
813
|
+
>
|
|
356
814
|
<EmptyState
|
|
357
|
-
title="Select a run"
|
|
358
|
-
description=
|
|
815
|
+
title={onRunTask ? "No runs yet" : "Select a run"}
|
|
816
|
+
description={onRunTask
|
|
817
|
+
? "This task hasn't run yet. Trigger it to see its output here."
|
|
818
|
+
: "Pick a run from the list to view details and logs."}
|
|
359
819
|
icon={MousePointerClick}
|
|
360
820
|
/>
|
|
821
|
+
{#if onRunTask}
|
|
822
|
+
<button
|
|
823
|
+
type="button"
|
|
824
|
+
onclick={() => onRunTask()}
|
|
825
|
+
class="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-primary-soft bg-surface-raised px-3 py-2 text-sm font-medium text-primary transition-colors hover:bg-primary-soft"
|
|
826
|
+
>
|
|
827
|
+
<Play size={15} fill="currentColor" stroke="none" />
|
|
828
|
+
Run task
|
|
829
|
+
</button>
|
|
830
|
+
{/if}
|
|
361
831
|
</div>
|
|
362
832
|
{/if}
|
|
833
|
+
|
|
834
|
+
<style>
|
|
835
|
+
/* The header is washed in a faint tint of the run's outcome colour
|
|
836
|
+
(--rw-oc, set inline). Neutral statuses pass the surface itself, so the
|
|
837
|
+
mix collapses to a plain surface with no visible tint. */
|
|
838
|
+
.head-region {
|
|
839
|
+
background: color-mix(in srgb, var(--rw-oc) 4.5%, var(--color-surface-raised));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/* Gentle fade for the maximize backdrop; disabled under reduced motion. */
|
|
843
|
+
.console-backdrop {
|
|
844
|
+
animation: console-backdrop-in 150ms ease-out;
|
|
845
|
+
}
|
|
846
|
+
@keyframes console-backdrop-in {
|
|
847
|
+
from {
|
|
848
|
+
opacity: 0;
|
|
849
|
+
}
|
|
850
|
+
to {
|
|
851
|
+
opacity: 1;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/* The console zooms up as it takes over the viewport. */
|
|
856
|
+
.console-zoom {
|
|
857
|
+
animation: console-zoom-in 260ms cubic-bezier(0.33, 0.05, 0.2, 1);
|
|
858
|
+
}
|
|
859
|
+
@keyframes console-zoom-in {
|
|
860
|
+
from {
|
|
861
|
+
opacity: 0.4;
|
|
862
|
+
transform: scale(0.96);
|
|
863
|
+
}
|
|
864
|
+
to {
|
|
865
|
+
opacity: 1;
|
|
866
|
+
transform: none;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/* Living spine: a light sweep travels down the edge while a run is in flight. */
|
|
871
|
+
.spine-flow {
|
|
872
|
+
overflow: hidden;
|
|
873
|
+
}
|
|
874
|
+
.spine-flow::after {
|
|
875
|
+
content: "";
|
|
876
|
+
position: absolute;
|
|
877
|
+
inset: 0;
|
|
878
|
+
height: 40%;
|
|
879
|
+
background: linear-gradient(
|
|
880
|
+
180deg,
|
|
881
|
+
transparent,
|
|
882
|
+
color-mix(in srgb, #fff 70%, transparent),
|
|
883
|
+
transparent
|
|
884
|
+
);
|
|
885
|
+
animation: spine-flow 1.9s linear infinite;
|
|
886
|
+
}
|
|
887
|
+
@keyframes spine-flow {
|
|
888
|
+
from {
|
|
889
|
+
transform: translateY(-100%);
|
|
890
|
+
}
|
|
891
|
+
to {
|
|
892
|
+
transform: translateY(300%);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
@media (prefers-reduced-motion: reduce) {
|
|
897
|
+
.console-backdrop,
|
|
898
|
+
.console-zoom,
|
|
899
|
+
.spine-flow::after {
|
|
900
|
+
animation: none;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
</style>
|