ui: GitHub-Actions-style detail page, sub-steps, mini-tile run-view
CI / Lint + build + test (push) Successful in 1m26s
Release / release (push) Successful in 6m47s

Reshapes the detail page into a run-view: hybrid horizontal pipeline
+ expanded active-step pane with sub-steps, a per-step log pane with
line-numbered permalinks and client-side search, and a runs-history
sidebar that navigates via ?run=N. Default step is server-picked
(running → failed → Reporting) so the operator lands on the thing
that's moving.

Adds a sub_steps table + SSE topic (substep-{run}-{stage}-{ordinal})
so per-disk and per-pass work (SMART, CPUStress CPU/RAM, Storage,
GPU) is visible in the UI instead of buried in stage summary JSON.
Agent emits sub-step reports from existing per-iteration loops.

Dashboard tiles become a mini run-view with a 9-dot step strip so
the operator reads run health across the whole grid at a glance.
Register page gets the same card shell + button styling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 19:00:11 -04:00
parent 5c00edd7b6
commit f79fe0f0db
38 changed files with 3972 additions and 936 deletions
+408 -15
View File
@@ -202,8 +202,14 @@ button.danger:hover { background: rgba(229,100,102,.1); }
.tile-pass { border-color: rgba(53,194,123,.5); }
.tile-active { border-color: var(--accent); }
.form-wrap { max-width: 640px; }
.form-wrap h1 { font-size: 20px; }
.form-wrap { max-width: 640px; display: flex; flex-direction: column; gap: 16px; }
.form-wrap h1 { font-size: 20px; margin: 0; }
.form-wrap-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.host-form { display: flex; flex-direction: column; gap: 14px; }
.host-form label { display: flex; flex-direction: column; gap: 4px; color: var(--text-dim); font-size: 13px; }
@@ -245,14 +251,10 @@ button.danger:hover { background: rgba(229,100,102,.1); }
body.bare main { max-width: none; }
.quick-register {
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px 18px;
margin-bottom: 20px;
}
.quick-register h2 { margin: 0 0 8px; font-size: 16px; }
/* .quick-register now inherits card shell from .detail-section; these
rules only cover its own content. */
.quick-register h2 { margin: 0 0 8px; font-size: 15px; text-transform: uppercase; letter-spacing: .5px; color: var(--text-dim); font-weight: 600; }
.quick-register h2 .muted { text-transform: none; letter-spacing: 0; }
.quick-register p { margin: 6px 0; font-size: 13px; color: var(--text-dim); }
.quick-register p b { color: var(--text); }
.quick-register .muted { color: var(--text-dim); font-weight: 400; }
@@ -270,14 +272,31 @@ body.bare main { max-width: none; }
}
.quick-register .one-liner code { white-space: pre; }
.manual-register { margin-top: 16px; }
.manual-register-card { padding-top: 10px; padding-bottom: 14px; }
.manual-register summary {
list-style: none;
cursor: pointer;
color: var(--text-dim);
font-size: 13px;
padding: 6px 0;
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
}
.manual-register summary:hover { color: var(--text); }
.manual-register summary::before {
content: "▸";
color: var(--text-dim);
font-size: 12px;
transition: transform .1s ease;
}
.manual-register[open] > summary::before { transform: rotate(90deg); }
.manual-register summary h2 {
margin: 0;
font-size: 15px;
text-transform: uppercase;
letter-spacing: .5px;
color: var(--text-dim);
font-weight: 600;
}
.manual-register summary:hover h2 { color: var(--text); }
.manual-register[open] summary { margin-bottom: 12px; }
/* ===== Host detail page ===== */
@@ -549,3 +568,377 @@ body.bare main { max-width: none; }
0%, 100% { box-shadow: 0 0 0 0 rgba(60,130,246,.55); }
50% { box-shadow: 0 0 0 8px rgba(60,130,246,0); }
}
/* ===== Host detail v2 — GitHub-Actions-style layout ===== */
.detail-v2 { gap: 12px; }
.host-meta-drawer {
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 8px 16px;
}
.host-meta-drawer > summary {
list-style: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 16px;
font-size: 13px;
color: var(--text-dim);
padding: 4px 0;
}
.host-meta-drawer > summary::before {
content: "▸";
color: var(--text-dim);
font-size: 11px;
transition: transform .1s ease;
}
.host-meta-drawer[open] > summary::before { transform: rotate(90deg); }
.host-meta-drawer .meta-summary-label { color: var(--text); font-weight: 600; }
.host-meta-drawer .meta-summary-mac { font-family: var(--mono); margin-left: auto; }
.host-meta-drawer[open] > summary { margin-bottom: 12px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
.run-header {
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.run-header.tile-fail { border-color: rgba(229,100,102,.6); }
.run-header.tile-pass { border-color: rgba(53,194,123,.5); }
.run-header.tile-active { border-color: var(--accent); }
.run-header-left { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.run-header-right { display: flex; align-items: center; gap: 14px; font-size: 13px; }
.run-header .detail-name { margin: 0; font-size: 22px; }
.run-number { font-family: var(--mono); font-size: 15px; color: var(--text-dim); }
.run-status-badge {
padding: 3px 10px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .5px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--bg-elev-2);
color: var(--text-dim);
font-weight: 600;
}
.run-status-pass { background: rgba(53,194,123,.15); border-color: rgba(53,194,123,.5); color: var(--success); }
.run-status-fail { background: rgba(229,100,102,.12); border-color: rgba(229,100,102,.5); color: var(--danger); }
.run-status-active { background: rgba(60,130,246,.15); border-color: rgba(60,130,246,.5); color: var(--accent); }
.run-duration { font-family: var(--mono); font-size: 13px; color: var(--text-dim); }
.run-failed-stage { color: var(--danger); }
.run-failed-stage strong { font-family: var(--mono); }
.run-diffs { color: var(--danger); }
.hold-banner {
background: rgba(229,100,102,.1);
border: 1px solid rgba(229,100,102,.5);
border-left: 4px solid var(--danger);
border-radius: var(--radius);
padding: 10px 14px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.hold-banner-label { color: var(--danger); font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: .5px; }
.hold-banner .hold-ssh {
font-family: var(--mono);
font-size: 13px;
user-select: all;
padding: 6px 10px;
background: rgba(0,0,0,.3);
border: 1px solid rgba(229,100,102,.3);
border-radius: 4px;
flex: 1 1 auto;
min-width: 0;
word-break: break-all;
}
.detail-hold-placeholder { display: none; }
.detail-body {
display: grid;
grid-template-columns: 1fr 260px;
gap: 16px;
align-items: start;
}
@media (max-width: 900px) {
.detail-body { grid-template-columns: 1fr; }
}
.active-step-pane { display: flex; flex-direction: column; gap: 8px; }
.detail-empty {
padding: 24px;
background: var(--bg-elev);
border: 1px dashed var(--border);
border-radius: var(--radius);
color: var(--text-dim);
text-align: center;
}
.step {
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.step[open] { border-color: var(--accent); }
.step-passed { border-left: 3px solid var(--success); }
.step-running { border-left: 3px solid var(--accent); }
.step-failed { border-left: 3px solid var(--danger); }
.step-skipped { opacity: .55; }
.step-pending { opacity: .7; }
.step > summary {
list-style: none;
cursor: pointer;
padding: 10px 14px;
display: flex;
align-items: center;
gap: 12px;
}
.step > summary::-webkit-details-marker { display: none; }
.step-summary .stage-dot { width: 20px; height: 20px; font-size: 12px; flex-shrink: 0; }
.step-name { font-weight: 600; color: var(--text); flex: 1; }
.step-duration { font-family: var(--mono); font-size: 12px; color: var(--text-dim); }
.step-body {
padding: 8px 14px 14px;
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 10px;
background: var(--bg);
}
.substep-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.substep {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-elev);
font-size: 13px;
}
.substep-badge {
width: 18px;
height: 18px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
background: var(--bg-elev-2);
border: 1px solid var(--border);
color: var(--text-dim);
flex-shrink: 0;
}
.substep-badge-passed { background: var(--success); border-color: var(--success); color: #0b0d12; }
.substep-badge-running { background: var(--accent-strong); border-color: var(--accent); color: #fff; animation: pulse 1.2s ease-in-out infinite; }
.substep-badge-failed { background: var(--danger); border-color: var(--danger); color: #fff; }
.substep-badge-skipped { opacity: .5; }
.substep-name { flex: 1; }
.substep-duration { font-family: var(--mono); font-size: 12px; color: var(--text-dim); }
.substep-failed { border-color: rgba(229,100,102,.5); }
.substep-running { border-color: rgba(60,130,246,.5); }
.log-search-wrap { display: flex; }
.log-search {
flex: 1;
font: inherit;
font-size: 12px;
font-family: var(--mono);
padding: 6px 10px;
background: var(--bg-elev-2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
}
.log-search::placeholder { color: var(--text-dim); }
.log-search:focus { outline: none; border-color: var(--accent); }
/* Log pane: now a standalone block (no tabs). Each step owns its pane. */
.step .log-pane {
display: flex;
flex-direction: column;
gap: 0;
padding: 6px 0;
background: #0b0d12;
border: 1px solid var(--border);
border-radius: 6px;
font-family: var(--mono);
font-size: 12px;
color: var(--text-dim);
max-height: 480px;
overflow-y: auto;
order: unset;
}
.step .log-pane:empty::before {
content: "(no log output for this step yet)";
padding: 10px 12px;
color: var(--text-dim);
opacity: .5;
}
.log-line {
display: grid;
grid-template-columns: 24px 48px 56px 72px auto;
align-items: baseline;
gap: 8px;
padding: 2px 10px;
white-space: pre-wrap;
word-break: break-word;
border-left: 3px solid transparent;
}
.log-line:target { background: rgba(60,130,246,.12); border-left-color: var(--accent); }
.log-line .log-anchor {
color: var(--text-dim);
opacity: 0;
font-weight: 700;
text-decoration: none;
text-align: center;
}
.log-line:hover .log-anchor { opacity: 1; }
.log-line .ln { color: var(--text-dim); opacity: .6; text-align: right; user-select: none; }
.log-line .lvl { color: var(--text-dim); text-transform: uppercase; font-size: 10px; font-weight: 700; letter-spacing: .5px; }
.log-line .log-ts { color: var(--text-dim); opacity: .75; }
.log-line .log-stage { color: var(--text-dim); opacity: .75; margin-right: 4px; }
.log-line .log-text { color: var(--text); white-space: pre-wrap; word-break: break-word; }
.log-line.log-warn .log-text { color: var(--warn); }
.log-line.log-warn .lvl { color: var(--warn); }
.log-line.log-error .log-text { color: var(--danger); }
.log-line.log-error .lvl { color: var(--danger); }
.log-line.log-debug { opacity: .6; }
.log-line.log-hit { background: rgba(228,169,75,.08); }
.runs-sidebar {
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px;
position: sticky;
top: 16px;
max-height: calc(100vh - 32px);
overflow-y: auto;
}
.runs-sidebar-heading {
margin: 0 0 10px;
font-size: 12px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: .5px;
font-weight: 600;
}
.runs-sidebar-empty { color: var(--text-dim); font-size: 13px; margin: 0; }
.runs-sidebar-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.runs-sidebar-item a {
display: grid;
grid-template-columns: 16px auto 1fr auto;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 6px;
color: var(--text);
text-decoration: none;
font-size: 12px;
}
.runs-sidebar-item a:hover { background: var(--bg-elev-2); text-decoration: none; }
.runs-sidebar-active a { background: rgba(60,130,246,.12); border: 1px solid rgba(60,130,246,.5); }
.runs-sidebar-dot {
width: 14px;
height: 14px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 9px;
font-weight: 700;
background: var(--bg-elev-2);
border: 1px solid var(--border);
color: var(--text-dim);
}
.runs-sidebar-dot-pass { background: var(--success); border-color: var(--success); color: #0b0d12; }
.runs-sidebar-dot-fail { background: var(--danger); border-color: var(--danger); color: #fff; }
.runs-sidebar-dot-active { background: var(--accent-strong); border-color: var(--accent); color: #fff; }
.runs-sidebar-id { font-family: var(--mono); font-weight: 600; }
.runs-sidebar-started { color: var(--text-dim); }
.runs-sidebar-duration { font-family: var(--mono); color: var(--text-dim); font-size: 11px; }
.btn-primary {
background: var(--accent-strong);
border-color: var(--accent-strong);
color: #fff;
}
.btn-primary:hover { background: var(--accent); border-color: var(--accent); }
.btn-danger {
border-color: var(--danger);
color: var(--danger);
background: transparent;
}
.btn-danger:hover { background: rgba(229,100,102,.1); }
/* ---------- Dashboard tile mini run-view (Phase 3) ---------------- */
/* Small variant of stage-dot for the compact step list. Same colour
rules as the full-size pipeline dot so operators read one language
everywhere; only the geometry shrinks. */
.stage-dot-sm {
width: 14px;
height: 14px;
font-size: 9px;
border-width: 1px;
flex-shrink: 0;
}
.tile-meta-row {
display: flex;
gap: 8px;
align-items: baseline;
font-size: 12px;
color: var(--text-dim);
padding: 4px 0 6px;
}
.tile-run-id { font-variant-numeric: tabular-nums; }
.tile-run-duration { margin-left: auto; font-variant-numeric: tabular-nums; }
.tile-steplist {
list-style: none;
margin: 0 0 8px;
padding: 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2px 10px;
}
.tile-steplist .tile-step {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
line-height: 1.4;
color: var(--text-dim);
min-width: 0;
}
.tile-steplist .tile-step-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Passed/failed/running steps keep full-strength text so the eye jumps
to active work; pending/skipped fade back into the background. */
.tile-step-passed .tile-step-name,
.tile-step-failed .tile-step-name,
.tile-step-running .tile-step-name { color: var(--text); }
.tile-step-skipped { opacity: .5; }