feat(runs): add runs pages, polling, autorefresh indicator, and scheduler fix

- add global runs page and per-scenario runs/run-detail pages
- run detail page polls every 1s until pass/fail, cleans up on unmount
- runs list pages poll every 10s with AutoRefreshIndicator (pulse on data load)
- fix scheduler: set run to pass after empty step loop to prevent stuck in_progress
- fix table header colors and link cell color for readability
- fix play button to navigate to the new run after creation
- fix package.json import paths in server for Docker build context
This commit is contained in:
2026-04-09 21:58:27 +03:00
parent ebcb8b8ff4
commit 1f3a604940
20 changed files with 745 additions and 22 deletions
@@ -0,0 +1,69 @@
.root {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: var(--font-size-xs);
font-family: var(--font-family);
user-select: none;
}
/* ── Dot ──────────────────────────────────────────────────── */
.dot {
position: relative;
width: 10px;
height: 10px;
flex-shrink: 0;
}
.dot::before {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
transition: background var(--transition);
}
/* ── Pulse ring ───────────────────────────────────────────── */
.ring {
position: absolute;
inset: 0;
border-radius: 50%;
animation: pulse 0.8s ease-out 1;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 70%, transparent);
}
70% {
box-shadow: 0 0 0 7px color-mix(in srgb, var(--color-primary) 0%, transparent);
}
100% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 0%, transparent);
}
}
/* ── Active state ─────────────────────────────────────────── */
.active .dot::before {
background: var(--color-primary);
}
.active .label {
color: var(--color-primary);
font-weight: 600;
}
/* ── Inactive / disabled state ────────────────────────────── */
.inactive .dot::before {
background: var(--color-text-muted);
opacity: 0.4;
}
.inactive .label {
color: var(--color-text-muted);
opacity: 0.5;
}
@@ -0,0 +1,25 @@
import styles from './AutoRefreshIndicator.module.css';
export interface AutoRefreshIndicatorProps {
active: boolean;
pulseKey?: number;
label?: string;
}
export function AutoRefreshIndicator({ active, pulseKey, label = 'Live' }: AutoRefreshIndicatorProps) {
return (
<div
className={[styles.root, active ? styles.active : styles.inactive].join(' ')}
title={active ? 'Auto-refreshing' : 'Auto-refresh stopped'}
aria-label={active ? 'Auto-refreshing' : 'Auto-refresh stopped'}
aria-live="polite"
>
<span className={styles.dot}>
{pulseKey !== undefined && pulseKey > 0 && (
<span key={pulseKey} className={styles.ring} />
)}
</span>
<span className={styles.label}>{label}</span>
</div>
);
}
+6 -1
View File
@@ -19,12 +19,17 @@
padding: var(--space-3) var(--space-4);
font-weight: 700;
font-size: var(--font-size-xs);
color: var(--color-secondary-fg);
color: #ffffff;
text-transform: uppercase;
letter-spacing: 0.06em;
border-bottom: var(--border-width) solid var(--color-secondary-border);
white-space: nowrap;
background: var(--color-secondary-fg);
}
[data-theme="dark"] .th {
background: var(--color-secondary);
color: var(--color-secondary-fg);
}
/* Rounded top corners on first/last header cells */
+3
View File
@@ -1,3 +1,6 @@
export { AutoRefreshIndicator } from './AutoRefreshIndicator/AutoRefreshIndicator';
export type { AutoRefreshIndicatorProps } from './AutoRefreshIndicator/AutoRefreshIndicator';
export { Button } from './Button/Button';
export type { ButtonProps } from './Button/Button';