diff --git a/src/ci_orchestrator/web/static/app.css b/src/ci_orchestrator/web/static/app.css index f1b2236..869097f 100644 --- a/src/ci_orchestrator/web/static/app.css +++ b/src/ci_orchestrator/web/static/app.css @@ -91,10 +91,16 @@ dd code, dd .mono { overflow-wrap: anywhere; } } .navbar-brand { + display: inline-flex; + align-items: center; + gap: 0.4rem; color: var(--text); font-weight: 600; font-size: 1.05rem; } +/* Gear: automation/pipeline, fits an orchestrator's brand mark better than a + * subject-specific glyph (server/box/etc. already belong to their own card). */ +.navbar-brand .icon { width: 1.3em; height: 1.3em; } .navbar-brand:hover { color: var(--accent-hover); text-decoration: none; } .navbar-links { @@ -120,63 +126,16 @@ dd code, dd .mono { overflow-wrap: anywhere; } #action-slot:empty { display: none; } #action-slot:not(:empty) { margin-bottom: 1.25rem; } -/* ── status header (A3) ──────────────────────────────────────────────── */ +/* ── resource gauges inside cards (Stage F) ──────────────────────────── */ -.status-header { - display: grid; - /* Six tiles (VMs, Runners, CPU, RAM, Disk, Cache) -- a smaller min track - * lets all six sit on one row on a wide screen, wrapping gracefully below. */ - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 1rem; - margin-bottom: 1.25rem; -} +/* The Host card's CPU / RAM / disk readouts. These lived in the retired + * status-header tiles; the `tile-*` level class names survive as what the + * gauge fill colour keys off. Stage G dropped the level word + check-icon + * strip that used to follow each gauge: the state is stated as a number + * ("42%", "20.0 / 62 GB") right above the bar, and the level word moved into + * the gauge's aria-label -- so no reading of this card depends on colour. */ -.stat-tile { - display: flex; - align-items: flex-start; - gap: 0.75rem; - background: var(--panel); - border: 1px solid var(--border); - border-left: 3px solid var(--border); - border-radius: var(--radius); - padding: 0.9rem 1rem; -} - -.stat-tile > .icon { flex-shrink: 0; margin-top: 0.15rem; color: var(--text-dim); } - -.stat-tile-body { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; flex: 1; } - -.stat-tile-label { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.03em; - color: var(--text-dim); -} - -.stat-tile-value { font-size: 1.15rem; font-weight: 600; color: var(--text); } -.stat-tile-sub { font-size: 0.8rem; font-weight: 400; color: var(--warn); } - -.stat-tile-status { - display: inline-flex; - align-items: center; - gap: 0.3rem; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.03em; -} - -/* Health must never rely on color alone -- the border accent + icon + - * label all move together, so a color-blind viewer still reads the state - * from the text/icon. */ -.tile-ok { border-left-color: var(--success); } -.tile-ok .stat-tile-status { color: var(--success); } -.tile-warn { border-left-color: var(--warn); } -.tile-warn .stat-tile-status { color: var(--warn); } -.tile-bad { border-left-color: var(--danger); } -.tile-bad .stat-tile-status { color: var(--danger); } -.tile-unknown { border-left-color: var(--border); } -.tile-unknown .stat-tile-status { color: var(--text-dim); } +.resource-value { display: block; font-variant-numeric: tabular-nums; } .gauge { width: 100%; @@ -184,24 +143,30 @@ dd code, dd .mono { overflow-wrap: anywhere; } background: var(--panel-2); border-radius: 999px; overflow: hidden; - margin: 0.15rem 0; + margin: 0.25rem 0; } .gauge-fill { height: 100%; background: var(--success); } .tile-warn .gauge-fill { background: var(--warn); } -.tile-bad .gauge-fill { background: var(--danger); } +.tile-bad .gauge-fill { background: var(--danger); } + +/* Sub-line under a gauge (Host: the load average under CPU), same role as the + * dim "GB free" beside the disk percentage. */ +.resource-sub { + display: block; + font-size: 0.78rem; + font-variant-numeric: tabular-nums; +} .dashboard-grid { display: grid; /* FIXED four-column track, NOT auto-fit. A `.card--wide` spans `1 / -1`, * i.e. occupies every column on its row -- with `auto-fit` that means no - * track is ever globally empty, so auto-fit can't collapse the compact - * row's spare tracks and the four info cards stayed narrow with dead - * columns on the right. A fixed count makes the four compact cards fill - * their row exactly, and the wide tables still span all four via - * `grid-column: 1 / -1`. minmax(0,1fr) lets a card shrink below its - * content width so a wide table scrolls inside its own .table-wrap - * instead of forcing the page to scroll. Column count steps down with - * width (below). */ + * track is ever globally empty, so auto-fit could not collapse a compact + * row's spare tracks. Since Stage F every card is `.card--wide` (the four + * compact info cards became the one Platform card), so the track count only + * matters if a compact card is ever added back. minmax(0,1fr) lets a card + * shrink below its content width so a wide table scrolls inside its own + * .table-wrap instead of forcing the page to scroll. */ grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1.25rem; /* stretch: every card in a row shares the row's height, so the grid reads @@ -285,6 +250,19 @@ dd code, dd .mono { overflow-wrap: anywhere; } * the bar's right end. Compact and case-normal so it reads as a control rather * than part of the uppercase heading text, and short enough not to grow the * bar's height. */ +/* Right-hand end of a card header bar: a single near-static fact that would + * otherwise cost a whole
row (Host: uptime). `text-transform` must be + * reset -- the h2 uppercases, and "26H 03M" is not a duration anyone reads. */ +.head-meta { + margin-left: auto; + font-size: 0.8rem; + font-weight: 400; + text-transform: none; + letter-spacing: normal; + font-variant-numeric: tabular-nums; + color: var(--text-dim); +} + .head-select { margin-left: auto; padding: 0.1rem 0.35rem; @@ -295,7 +273,10 @@ dd code, dd .mono { overflow-wrap: anywhere; } color: var(--text); } -/* Per-template expandable backup list (Templates panel). */ +/* Per-template expandable backup list (Templates panel). Each row is its own + * bordered line (not just a wrapping flex list run into the next) so a + * multi-backup template reads as a small table, not a paragraph the delete + * button happened to float in. */ .backups-details > summary { cursor: pointer; color: var(--link); @@ -306,14 +287,19 @@ dd code, dd .mono { overflow-wrap: anywhere; } .backups-list { list-style: none; margin: 0; padding: 0; } .backups-list li { display: flex; - flex-wrap: wrap; - gap: 0.5rem; - align-items: baseline; - padding: 0.2rem 0; - font-size: 0.82rem; + align-items: center; + gap: 0.75rem; + padding: 0.45rem 0; + font-size: 0.85rem; + border-top: 1px solid var(--border); } +.backups-list li:first-child { border-top: none; } +/* Name + size/date as one shrinkable unit, so the row wraps as a block + * instead of the delete button drifting away from a name that wrapped. */ +.backup-info { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem; min-width: 0; flex: 1 1 auto; } +.backup-info code { overflow-wrap: anywhere; } /* Per-backup delete button pinned to the right end of its row. */ -.backup-del { margin-left: auto; align-self: center; } +.backup-del { margin-left: auto; flex: none; } .card h2 { display: flex; @@ -339,7 +325,9 @@ dd code, dd .mono { overflow-wrap: anywhere; } dl { display: grid; grid-template-columns: auto 1fr; gap: 0 0.9rem; } dt, dd { padding: 0.45rem 0; border-bottom: 1px solid var(--border); } dt { color: var(--text-dim); font-weight: 500; } -dd { margin: 0; } +/* Long unbreakable values (a cache root, a kernel string, a CPU model) + * wrap inside the card rather than widening its grid track. */ +dd { margin: 0; overflow-wrap: anywhere; } /* ── tables ───────────────────────────────────────────────────────────── */ @@ -347,7 +335,13 @@ dd { margin: 0; } table { width: 100%; border-collapse: collapse; font-size: 0.9rem; } -th, td { text-align: left; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); } +/* vertical-align:middle is explicit, not left to the UA default (baseline) -- + * keeps every cell in a row anchored the same way regardless of its content + * (a row of buttons vs. one line of text). The Actions-column border step + * this was chasing turned out to be .actions-row's flex box sitting directly + * on the (see the comment there); this rule stays for alignment, not as + * that fix. */ +th, td { text-align: left; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); vertical-align: middle; } th { color: var(--text-dim); @@ -358,7 +352,7 @@ th { } tbody tr { transition: background-color 120ms ease; } -tbody tr:hover { background: var(--panel-2); } +tbody tr:hover { background: rgba(65, 131, 196, 0.12); } tr.row-orphan td { color: var(--warn); } tr.row-alert td { color: var(--danger); } @@ -396,8 +390,37 @@ tr.row-alert td { color: var(--danger); } .badge-info { background: rgba(65, 131, 196, 0.15); color: var(--accent); border: 1px solid rgba(65, 131, 196, 0.4); } .badge-muted { background: var(--panel-2); color: var(--text-dim); border: 1px solid var(--border); } +/* A badge sitting right before a dim sub-state ("loaded" + "kernel module", + * "active" + "running") only had the markup's one collapsed whitespace + * character between them -- reads as glued together. */ +.badge + .text-dim { margin-left: 0.4rem; } + +/* VMware/Runners (Stage H): "loaded"/"not loaded", "active"/"down" are + * different lengths, so the pill in each
row was a different width from + * its neighbours. Levelled to fit the longest word in either card, same + * technique as the Executions Status column. */ +#vmware-card .badge, +#runner-card .badge { + min-width: 5rem; + justify-content: flex-start; +} + +/* Executions card: the Status column mixes run outcomes (success/failure/ + * queued/cancelled...) and VM-only states (orphan/running) of very different + * word lengths -- a ragged column of differently-sized pills. Both row kinds + * put their badge in the 8th (Run, Repo, Workflow, Branch, Commit, + * Trigger, Actor, Status, ...), so one rule levels every pill in the column + * to "cancelled"'s width (the longest word) with room to spare, and + * `justify-content` keeps icon+text hugging the left edge of the now-wider + * pill rather than centering. */ +#executions-card td:nth-child(8) { width: 5rem; } +#executions-card td:nth-child(8) .badge { width: 100%; justify-content: flex-start; } + /* ── buttons ──────────────────────────────────────────────────────────── */ +/* Two filled button kinds, no neutral/outline third state: every action is + * either "normal" (blue) or destructive (red) -- the confirm gate is still + * the real safeguard on a destructive action, this is just legibility. */ .btn { display: inline-flex; align-items: center; @@ -406,26 +429,31 @@ tr.row-alert td { color: var(--danger); } border-radius: var(--radius); padding: 0.45rem 0.9rem; font-size: 0.875rem; - font-weight: 600; + font-weight: 400; font-family: inherit; cursor: pointer; - background: var(--panel-2); - color: var(--text); + background: #4382c2; + color: #ffffff; + /* Same neutral gray as every table/card border, not the fill colour -- + * a solid-colour border read as an outline duplicating the fill. */ + border-color: var(--border); transition: background-color 120ms ease, border-color 120ms ease, transform 80ms ease; } -.btn:hover { background: var(--border); } +/* Hover tint: +25 per channel, same lighten-by magnitude as --accent-hover. + * Border stays the neutral gray -- only the fill shifts on hover. */ +.btn:hover { background: #5c9bdb; } .btn:active { transform: translateY(1px); } -.btn-primary { background: var(--accent); color: #ffffff; border-color: var(--accent); } -.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); } +/* A plain-weight button label leaves its icon looking thin/washed-out next + * to a solid fill -- bumped stroke-width and a couple px of extra size make + * it read as clearly as the bold text used to. */ +.btn .icon { stroke-width: 2.25; width: 1.15em; height: 1.15em; } -/* Destructive actions: a NEUTRAL button at rest (same panel surface + border - * as .btn) that only reads as red through its text/icon -- no red border or - * fill glaring on the page. Danger becomes fully visible on hover (red border - * + soft red wash). The real safeguard is the two-step confirm gate, not the - * colour. */ -.btn-danger { background: var(--panel-2); color: var(--danger); border-color: var(--border); } -.btn-danger:hover { background: rgba(248, 81, 73, 0.14); color: var(--danger); border-color: var(--danger); } +.btn-primary { background: #4382c2; color: #ffffff; border-color: var(--border); } +.btn-primary:hover { background: #5c9bdb; } + +.btn-danger { background: #cb4b4c; color: #ffffff; border-color: var(--border); } +.btn-danger:hover { background: #e46465; } /* Compact variant for buttons that live inside a table row (VM remove, * per-service start/stop/restart) so a row of them stays low-profile. */ @@ -458,8 +486,17 @@ input[type="text"], input[type="number"], select { /* Multiple one-button action forms side-by-side in one table cell (Stage D * services panel: Start/Stop/Restart). Overrides the default form's * margin-top so the row stays compact inside a . */ -.service-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; } -.service-actions form { margin-top: 0; } +/* Wraps the row of action buttons INSIDE the td rather than making the td + * itself a flex box -- a table-cell that is also a flex container computes + * its content-box height slightly differently from a plain-text sibling + * cell in some engines, which showed up as the row's collapsed bottom + * border sitting 1px higher under this column than its neighbours. */ +.actions-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; } +/* Each
is itself display:flex (global form rule) -- as a flex item of + * .actions-row that can size to fill the row instead of its own content, + * pushing the next form onto a new line. flex:none pins it to content width + * so multiple forms actually sit side by side. */ +.actions-row form { margin-top: 0; flex: none; } /* Muted inline text (e.g. a relative-time suffix next to a file name). */ .text-dim { color: var(--text-dim); } @@ -600,18 +637,26 @@ input[type="text"], input[type="number"], select { * references render. currentColor lets every icon pick up whatever text * color its container already has -- no per-icon color rules needed. */ .icon { - width: 1em; - height: 1em; + /* calc(...+1px) rather than a bigger em multiplier: a flat 1px bump reads + * the same at every context size (a 0.75rem badge icon and a 15px body + * icon both just gain one pixel), where a fixed em delta would swing by + * context. .btn .icon overrides this with a plain em value -- no +1px -- + * so buttons are unaffected. */ + width: calc(1em + 1px); + height: calc(1em + 1px); vertical-align: -0.15em; fill: none; stroke: currentColor; + /* Bolder than the sprite's own stroke-width="2" attribute -- CSS wins over + * the SVG presentation attribute. Button icons dial this back down, see + * .btn .icon below. */ + stroke-width: 2.5; flex-shrink: 0; } -.card h2 .icon, -.stat-tile > .icon { - width: 1.1em; - height: 1.1em; +.card h2 .icon { + width: calc(1.1em + 1px); + height: calc(1.1em + 1px); } /* ── accessibility ────────────────────────────────────────────────────── */ @@ -637,5 +682,4 @@ input[type="text"], input[type="number"], select { .navbar-inner { flex-direction: column; align-items: flex-start; gap: 0.5rem; } form { flex-direction: column; align-items: flex-start; } dl { grid-template-columns: 1fr; } - .status-header { grid-template-columns: 1fr; } } diff --git a/src/ci_orchestrator/web/templates/base.html b/src/ci_orchestrator/web/templates/base.html index 78304ef..724430b 100644 --- a/src/ci_orchestrator/web/templates/base.html +++ b/src/ci_orchestrator/web/templates/base.html @@ -13,7 +13,7 @@ {% set logged_in_user = request.session.get("user") %}