diff --git a/internal/web/static/app.css b/internal/web/static/app.css
index 7a19397..fbdf3d1 100644
--- a/internal/web/static/app.css
+++ b/internal/web/static/app.css
@@ -44,7 +44,7 @@ a:hover { text-decoration: underline; }
.topbar .heartbeat { color: var(--text-dim); font-family: var(--mono); font-size: 12px; }
.topbar .logout-form { margin: 0; }
-main { max-width: 1280px; margin: 0 auto; padding: 24px; }
+main { max-width: 1440px; margin: 0 auto; padding: 24px; }
button, .button, .button-secondary {
appearance: none;
@@ -492,16 +492,16 @@ body.bare main { max-width: none; }
}
/* ===== Pipeline timeline =====
- 13 nodes (3 pre-stage + 9 stage + Completed). flex:1 on every node
- so they share the full width evenly; overflow-x:auto only kicks in
- on very narrow viewports. */
+ 15 nodes (3 pre-stage + 11 stage + Completed). flex:1 on every node
+ so they share the full width evenly. Multi-word labels
+ (stageDisplayName in pipeline.templ) wrap on two lines so nodes pack
+ tight without horizontal scroll. */
.pipeline {
display: flex;
align-items: stretch;
gap: 0;
width: 100%;
- overflow-x: auto;
- padding: 28px 12px 16px;
+ padding: 28px 8px 16px;
background: #0b0d12;
border: 1px solid var(--border);
border-radius: var(--radius);
@@ -512,8 +512,8 @@ body.bare main { max-width: none; }
align-items: center;
gap: 6px;
flex: 1 1 0;
- min-width: 72px;
- padding: 0 4px;
+ min-width: 56px;
+ padding: 0 2px;
}
.stage-dot {
width: 28px;
@@ -543,6 +543,7 @@ body.bare main { max-width: none; }
text-align: center;
font-weight: 500;
line-height: 1.2;
+ overflow-wrap: anywhere;
}
.stage-node-passed .stage-name { color: var(--text); }
.stage-node-running .stage-name { color: var(--accent); font-weight: 600; }
diff --git a/internal/web/templates/pipeline.templ b/internal/web/templates/pipeline.templ
index 9a2419e..b8bae19 100644
--- a/internal/web/templates/pipeline.templ
+++ b/internal/web/templates/pipeline.templ
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
+ "strings"
"time"
"vetting/internal/model"
@@ -250,6 +251,33 @@ func stageDuration(n PipelineNode) string {
}
}
+// stageDisplayName turns the internal single-word state/stage identifier
+// into a human-readable label by inserting spaces before interior capital
+// letters. "WaitingReboot" → "Waiting Reboot", "SpecValidate" →
+// "Spec Validate", "CPUStress" → "CPU Stress". The space lets the
+// pipeline node wrap the label onto two lines on narrow layouts instead
+// of forcing horizontal scroll — single-word names ("Inventory",
+// "Completed") pass through unchanged.
+func stageDisplayName(name string) string {
+ var b strings.Builder
+ b.Grow(len(name) + 2)
+ for i, r := range name {
+ if i > 0 && r >= 'A' && r <= 'Z' {
+ prev := rune(name[i-1])
+ // Insert a space at every lower→upper boundary ("Spec|Validate")
+ // and at upper→upper when the next char is lower ("CPU|Stress",
+ // where we split before the S that starts a new word). This
+ // keeps acronyms like "GPU"/"PSU" intact.
+ if (prev >= 'a' && prev <= 'z') ||
+ (prev >= 'A' && prev <= 'Z' && i+1 < len(name) && name[i+1] >= 'a' && name[i+1] <= 'z') {
+ b.WriteByte(' ')
+ }
+ }
+ b.WriteRune(r)
+ }
+ return b.String()
+}
+
// stageMarker returns the single-char glyph shown in the node's dot.
// Dots stay colored-via-class; the glyph is redundant-but-helpful.
func stageMarker(state string) string {
@@ -277,7 +305,7 @@ templ Pipeline(nodes []PipelineNode) {
}
{ stageMarker(n.State) }
-
{ n.Name }
+
{ stageDisplayName(n.Name) }
{ stageDuration(n) }
}
diff --git a/internal/web/templates/pipeline_templ.go b/internal/web/templates/pipeline_templ.go
index f9a4ffb..bd2a183 100644
--- a/internal/web/templates/pipeline_templ.go
+++ b/internal/web/templates/pipeline_templ.go
@@ -12,6 +12,7 @@ import (
"bytes"
"context"
"fmt"
+ "strings"
"time"
"vetting/internal/model"
@@ -258,6 +259,33 @@ func stageDuration(n PipelineNode) string {
}
}
+// stageDisplayName turns the internal single-word state/stage identifier
+// into a human-readable label by inserting spaces before interior capital
+// letters. "WaitingReboot" → "Waiting Reboot", "SpecValidate" →
+// "Spec Validate", "CPUStress" → "CPU Stress". The space lets the
+// pipeline node wrap the label onto two lines on narrow layouts instead
+// of forcing horizontal scroll — single-word names ("Inventory",
+// "Completed") pass through unchanged.
+func stageDisplayName(name string) string {
+ var b strings.Builder
+ b.Grow(len(name) + 2)
+ for i, r := range name {
+ if i > 0 && r >= 'A' && r <= 'Z' {
+ prev := rune(name[i-1])
+ // Insert a space at every lower→upper boundary ("Spec|Validate")
+ // and at upper→upper when the next char is lower ("CPU|Stress",
+ // where we split before the S that starts a new word). This
+ // keeps acronyms like "GPU"/"PSU" intact.
+ if (prev >= 'a' && prev <= 'z') ||
+ (prev >= 'A' && prev <= 'Z' && i+1 < len(name) && name[i+1] >= 'a' && name[i+1] <= 'z') {
+ b.WriteByte(' ')
+ }
+ }
+ b.WriteRune(r)
+ }
+ return b.String()
+}
+
// stageMarker returns the single-char glyph shown in the node's dot.
// Dots stay colored-via-class; the glyph is redundant-but-helpful.
func stageMarker(state string) string {
@@ -378,7 +406,7 @@ func Pipeline(nodes []PipelineNode) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(stageMarker(n.State))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 279, Col: 77}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 307, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -389,9 +417,9 @@ func Pipeline(nodes []PipelineNode) templ.Component {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(n.Name)
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(stageDisplayName(n.Name))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 280, Col: 36}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 308, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -404,7 +432,7 @@ func Pipeline(nodes []PipelineNode) templ.Component {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(stageDuration(n))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 281, Col: 50}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 309, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -458,7 +486,7 @@ func PipelineSection(run *model.Run, nodes []PipelineNode) templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("pipeline-%d", run.ID))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 296, Col: 41}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 324, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -471,7 +499,7 @@ func PipelineSection(run *model.Run, nodes []PipelineNode) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("pipeline-%d", run.ID))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 298, Col: 47}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 326, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {