Click a tile to open /hosts/{id} — the canonical control surface per
host. Timeline renders every pre-stage, stage, and terminal node in
order, with the current one pulsing, failed ones flagged, and
downstream ones dimmed as skipped. Detail page shows summary, hold
card (when holding), all action buttons, spec diffs, a full-height
log pane, and a collapsed expected-spec YAML.
Tile slims to name, last-seen, status, and one primary action; a
CSS-overlay <a> makes the whole card clickable while buttons stay
receptive via z-index.
Runner.publishTileUpdate now also emits pipeline-{runID} fragments,
and CompleteStage wraps Stages.CompleteByName so stage completions
advance the timeline live — without this the dots only moved on
state transitions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -104,7 +104,22 @@ button.danger:hover { background: rgba(229,100,102,.1); }
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
transition: border-color .12s ease, transform .12s ease;
|
||||
}
|
||||
.tile:hover { border-color: var(--accent); }
|
||||
.tile-link {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
border-radius: var(--radius);
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
.tile > *:not(.tile-link) { position: relative; z-index: 1; }
|
||||
.tile-primary-action { display: flex; gap: 8px; }
|
||||
.tile-primary-action .inline { margin: 0; }
|
||||
.tile-primary-action:empty { display: none; }
|
||||
.tile-head { display: flex; justify-content: space-between; align-items: center; }
|
||||
.tile-name { font-weight: 600; }
|
||||
.tile-header-right { display: flex; align-items: center; gap: 10px; }
|
||||
@@ -264,3 +279,184 @@ body.bare main { max-width: none; }
|
||||
}
|
||||
.manual-register summary:hover { color: var(--text); }
|
||||
.manual-register[open] summary { margin-bottom: 12px; }
|
||||
|
||||
/* ===== Host detail page ===== */
|
||||
.detail { display: flex; flex-direction: column; gap: 20px; }
|
||||
.breadcrumb { color: var(--text-dim); font-size: 13px; display: flex; gap: 6px; }
|
||||
.breadcrumb a { color: var(--text-dim); }
|
||||
.breadcrumb a:hover { color: var(--text); }
|
||||
.breadcrumb-sep { opacity: .5; }
|
||||
|
||||
.detail-summary {
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.detail-summary.tile-fail { border-color: rgba(229,100,102,.6); }
|
||||
.detail-summary.tile-pass { border-color: rgba(53,194,123,.5); }
|
||||
.detail-summary.tile-active { border-color: var(--accent); }
|
||||
.detail-summary-head { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; flex-wrap: wrap; }
|
||||
.detail-name { margin: 0; font-size: 22px; }
|
||||
.detail-status-row { display: flex; align-items: center; gap: 12px; }
|
||||
.detail-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 8px 24px;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.detail-meta div { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
|
||||
.detail-meta dt { color: var(--text-dim); }
|
||||
.detail-meta dd { margin: 0; font-family: var(--mono); }
|
||||
.detail-meta dd.bad { color: var(--danger); }
|
||||
|
||||
.detail-section {
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px 20px;
|
||||
}
|
||||
.detail-section h2 { margin: 0 0 12px; font-size: 15px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .5px; font-weight: 600; }
|
||||
.detail-section details > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px; }
|
||||
.detail-section details > summary::before { content: "▸"; color: var(--text-dim); font-size: 12px; transition: transform .1s ease; }
|
||||
.detail-section details[open] > summary::before { transform: rotate(90deg); }
|
||||
.detail-section details > summary h2 { margin: 0; }
|
||||
|
||||
.detail-hold {
|
||||
background: rgba(229,100,102,.08);
|
||||
border-color: rgba(229,100,102,.35);
|
||||
}
|
||||
.detail-hold h2 { color: var(--danger); }
|
||||
.hold-ssh {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
word-break: break-all;
|
||||
user-select: all;
|
||||
display: block;
|
||||
padding: 10px 12px;
|
||||
background: #0b0d12;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.detail-actions-row { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.detail-actions-row .inline { margin: 0; }
|
||||
|
||||
.detail-log {
|
||||
background: #0b0d12;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 10px 12px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.detail-log:empty::before { content: "(no log output yet)"; color: var(--text-dim); opacity: .5; }
|
||||
.detail-log .log-line { white-space: pre-wrap; }
|
||||
.detail-log .log-warn { color: var(--warn); }
|
||||
.detail-log .log-error { color: var(--danger); }
|
||||
|
||||
.diff-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
||||
.diff-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 12px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 13px;
|
||||
}
|
||||
.diff-row code { font-family: var(--mono); font-size: 12px; color: var(--text); }
|
||||
.diff-field { font-weight: 600; }
|
||||
.diff-expected, .diff-actual { color: var(--text-dim); }
|
||||
.diff-critical { border-color: rgba(229,100,102,.5); background: rgba(229,100,102,.06); }
|
||||
.diff-critical .diff-field { color: var(--danger); }
|
||||
.diff-warning { border-color: rgba(228,169,75,.45); background: rgba(228,169,75,.05); }
|
||||
.diff-warning .diff-field { color: var(--warn); }
|
||||
.diff-info { opacity: .75; }
|
||||
|
||||
.detail-host-meta h3 { margin: 12px 0 6px; font-size: 13px; color: var(--text-dim); }
|
||||
.detail-notes p { margin: 0; color: var(--text); }
|
||||
.detail-spec-yaml {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
background: #0b0d12;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 10px 12px;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ===== Pipeline timeline ===== */
|
||||
.pipeline {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
overflow-x: auto;
|
||||
padding: 12px 4px 6px;
|
||||
}
|
||||
.stage-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 82px;
|
||||
padding: 0 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stage-dot {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
border: 2px solid var(--border);
|
||||
background: var(--bg-elev-2);
|
||||
color: var(--text-dim);
|
||||
line-height: 1;
|
||||
}
|
||||
.stage-dot-passed { background: var(--success); border-color: var(--success); color: #0b0d12; }
|
||||
.stage-dot-running { background: var(--accent-strong); border-color: var(--accent); color: #fff; animation: pulse 1.2s ease-in-out infinite; }
|
||||
.stage-dot-failed { background: var(--danger); border-color: var(--danger); color: #fff; }
|
||||
.stage-dot-skipped { background: transparent; border-color: var(--border); color: var(--text-dim); opacity: .45; }
|
||||
.stage-dot-pending { background: transparent; border-color: var(--border); color: transparent; }
|
||||
|
||||
.stage-name { font-size: 11px; color: var(--text-dim); text-align: center; }
|
||||
.stage-node-passed .stage-name { color: var(--text); }
|
||||
.stage-node-running .stage-name { color: var(--accent); }
|
||||
.stage-node-failed .stage-name { color: var(--danger); }
|
||||
.stage-node-skipped .stage-name { opacity: .5; }
|
||||
.stage-duration { font-size: 10px; color: var(--text-dim); font-family: var(--mono); min-height: 12px; }
|
||||
|
||||
.stage-connector {
|
||||
flex: 1;
|
||||
min-width: 12px;
|
||||
height: 2px;
|
||||
align-self: center;
|
||||
margin-top: -18px;
|
||||
background: var(--border);
|
||||
}
|
||||
.stage-connector-passed { background: var(--success); }
|
||||
.stage-connector-running { background: linear-gradient(90deg, var(--success), var(--accent)); }
|
||||
.stage-connector-failed { background: var(--danger); }
|
||||
.stage-connector-skipped { background: var(--border); opacity: .5; }
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(60,130,246,.55); }
|
||||
50% { box-shadow: 0 0 0 6px rgba(60,130,246,0); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user