ui: GitHub-Actions-style detail page, sub-steps, mini-tile run-view
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:
+408
-15
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user