diff --git a/internal/api/ui_handlers.go b/internal/api/ui_handlers.go index fd5829a..9b7ad67 100644 --- a/internal/api/ui_handlers.go +++ b/internal/api/ui_handlers.go @@ -70,6 +70,11 @@ func (u *UI) reloadPXE(ctx context.Context) { } } +func renderNotFound(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _ = templates.NotFound().Render(r.Context(), w) +} + var macRe = regexp.MustCompile(`^[0-9a-f]{2}(:[0-9a-f]{2}){5}$`) // quickRegisterTmpl is parsed once at startup — a malformed template @@ -126,7 +131,7 @@ func (u *UI) HostPage(w http.ResponseWriter, r *http.Request) { data, err := u.LoadHostPageData(r.Context(), id) if err != nil { if errors.Is(err, store.ErrNotFound) { - http.NotFound(w, r) + renderNotFound(w, r) return } http.Error(w, err.Error(), http.StatusInternalServerError) @@ -190,7 +195,7 @@ func (u *UI) RunPage(w http.ResponseWriter, r *http.Request) { data, err := u.LoadRunPageData(r.Context(), runID) if err != nil { if errors.Is(err, store.ErrNotFound) { - http.NotFound(w, r) + renderNotFound(w, r) return } http.Error(w, err.Error(), http.StatusInternalServerError) @@ -293,7 +298,7 @@ func (u *UI) StartRun(w http.ResponseWriter, r *http.Request) { host, err := u.Hosts.Get(r.Context(), hostID) if err != nil { if errors.Is(err, store.ErrNotFound) { - http.NotFound(w, r) + renderNotFound(w, r) return } http.Error(w, err.Error(), http.StatusInternalServerError) @@ -685,7 +690,7 @@ func (u *UI) DeleteHost(w http.ResponseWriter, r *http.Request) { } if err := u.Hosts.Delete(r.Context(), id); err != nil { if errors.Is(err, store.ErrNotFound) { - http.NotFound(w, r) + renderNotFound(w, r) return } http.Error(w, err.Error(), http.StatusInternalServerError) @@ -722,7 +727,7 @@ func (u *UI) Report(w http.ResponseWriter, r *http.Request) { } } if path == "" { - http.NotFound(w, r) + renderNotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 94a4b55..b02e1a9 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -215,31 +215,14 @@ body.bare main { max-width: none; } .quick-register .one-liner code { white-space: pre; } .manual-register-card { padding-top: 10px; padding-bottom: 14px; } -.manual-register summary { - list-style: none; - cursor: pointer; - display: flex; - align-items: center; - gap: 8px; - padding: 4px 0; -} -.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; +.manual-register-card h2 { + margin: 0 0 12px; 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 ===== */ .detail { display: flex; flex-direction: column; gap: 20px; } @@ -777,14 +760,14 @@ body.bare main { max-width: none; } .host-profile-picker { border: 1px solid var(--border); border-radius: var(--radius); - padding: 6px 10px; - display: inline-flex; - gap: 12px; - align-items: center; + padding: 8px 12px; + display: flex; + flex-direction: column; + gap: 6px; margin: 0 8px 0 0; } .host-profile-picker legend { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .05em; padding: 0 4px; } -.host-profile-picker label { display: inline-flex; gap: 4px; align-items: center; font-family: var(--mono); font-size: 13px; cursor: pointer; } +.host-profile-picker label { cursor: pointer; } .in-flight-banner-wrap { display: contents; } .in-flight-banner { @@ -864,3 +847,139 @@ body.bare main { max-width: none; } .run-page { display: flex; flex-direction: column; gap: 12px; } .run-body { display: flex; flex-direction: column; gap: 10px; } .run-header-name { margin: 0; font-size: 20px; font-weight: 600; } + +/* ---------- UX fixes ------------------------------------------------ */ + +/* #1: Active nav indicator */ +.topbar nav a.nav-active { color: var(--text); } + +/* #3: Copy button for code blocks */ +.copyable-wrap { position: relative; display: flex; align-items: stretch; gap: 0; } +.copyable-wrap .one-liner, +.copyable-wrap .hold-ssh { flex: 1; margin: 0; border-top-right-radius: 0; border-bottom-right-radius: 0; } +.copy-btn { + padding: 6px 12px; + font-size: 11px; + font-family: var(--mono); + text-transform: uppercase; + letter-spacing: .5px; + background: var(--bg-elev-2); + border: 1px solid var(--border); + border-left: none; + border-radius: 0 var(--radius) var(--radius) 0; + color: var(--text-dim); + cursor: pointer; + white-space: nowrap; +} +.copy-btn:hover { color: var(--text); background: var(--bg-elev); } +.copy-btn.copied { color: var(--success); } + +/* #4: Profile picker descriptions */ +.host-profile-picker label { + display: flex; + flex-direction: column; + gap: 2px; + align-items: flex-start; + font-family: var(--mono); + font-size: 13px; + cursor: pointer; +} +.host-profile-picker label > .profile-label { display: inline-flex; align-items: center; gap: 4px; } +.host-profile-picker label > .profile-desc { font-family: var(--font); font-size: 11px; color: var(--text-dim); padding-left: 18px; } + +/* #5: Non-destructive hint */ +.nd-hint { + display: block; + font-size: 11px; + color: var(--text-dim); + padding-left: 20px; + margin-top: 2px; +} + +/* #6: Offline guidance */ +.offline-hint { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} +.offline-hint span { font-size: 13px; color: var(--text-dim); } +.offline-hint a { color: var(--accent); } + +/* #7: SSE connection indicator */ +.heartbeat { transition: color .3s ease; } +.heartbeat-live { color: var(--success) !important; } +.heartbeat-stale { color: var(--danger) !important; } + +/* #9: Diff badge on collapsed spec diffs */ +.diff-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 6px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + font-family: var(--mono); + line-height: 1; +} +.diff-badge-critical { background: rgba(229,100,102,.2); color: var(--danger); border: 1px solid rgba(229,100,102,.5); } +.diff-badge-warn { background: rgba(228,169,75,.15); color: var(--warn); border: 1px solid rgba(228,169,75,.4); } + +/* #10: Cancelled run state */ +.run-status-cancelled { background: rgba(154,162,177,.12); border-color: rgba(154,162,177,.4); color: var(--text-dim); } +.tile-cancelled { border-color: rgba(154,162,177,.3); } + +/* #11: Small status badge on tiles */ +.run-status-badge-sm { font-size: 10px; padding: 2px 7px; } +.tile-status { display: flex; } + +/* #12: Log search match count */ +.log-match-count { + font-family: var(--mono); + font-size: 11px; + color: var(--text-dim); + padding: 6px 8px; + white-space: nowrap; + align-self: center; +} +.log-match-count:empty { display: none; } + +/* #8: Inline confirm */ +.confirm-overlay { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + background: rgba(229,100,102,.1); + border: 1px solid rgba(229,100,102,.5); + border-radius: var(--radius); + font-size: 13px; + color: var(--danger); +} +.confirm-overlay .confirm-msg { flex: 1; } +.confirm-overlay .confirm-yes { + background: var(--danger); + border-color: var(--danger); + color: #fff; + font-weight: 600; + padding: 6px 14px; +} +.confirm-overlay .confirm-no { + background: transparent; + border-color: var(--border); + color: var(--text-dim); + padding: 6px 14px; +} + +/* #13: 404 page */ +.not-found { + text-align: center; + padding: 80px 24px; + color: var(--text-dim); +} +.not-found h1 { font-size: 24px; color: var(--text); margin: 0 0 8px; } +.not-found p { margin: 0 0 20px; } +.not-found .button { display: inline-block; } diff --git a/internal/web/static/app.js b/internal/web/static/app.js index f288652..64bf398 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -1,16 +1,5 @@ -// Detail-page client behaviors. Loaded in layout.templ with `defer` so the -// DOM is parsed before any listeners fire. Three jobs: -// -// 1. Auto-advance: when a substep-* SSE event lands with state=running, -// open the parent step panel and collapse any previously-running step -// that's now completed. Keeps the operator's attention on the thing -// that's currently moving without manual clicks. -// 2. In-step search: filter `.log-line` rows inside the current step by -// substring match. Client-side only — the log pane's `
` ancestor -// scopes the filter naturally. -// 3. Permalink scroll + highlight: when the URL carries `#L{run}-{stage}-{ord}` -// on load, scroll that log line into view; anchor clicks update -// `location.hash` without a reload. +// Client behaviors for the Vetting UI. Loaded in layout.templ with `defer` +// so the DOM is parsed before any listeners fire. (function () { 'use strict'; @@ -21,9 +10,6 @@ if (!name || name.indexOf('substep-') !== 0) { return; } - // After htmx has applied the swap, check which step the just-updated - // substep belongs to. We scan *after* the swap so we see the new - // class ("substep-running" / "substep-passed") rather than the old. setTimeout(function () { autoAdvance(); }, 0); @@ -40,24 +26,19 @@ if (!runningStep) { return; } - // Open the running step; collapse any other open step that no longer - // has a running substep. The default-open step picked server-side - // stays open if nothing is running yet. steps.forEach(function (step) { if (step === runningStep) { if (!step.open) { step.open = true; } return; } if (step.open && !step.querySelector('.substep-running')) { - // Leave the "currently-failed" step open even when we - // auto-advance — operator still wants to see what broke. if (step.classList.contains('step-failed')) { return; } step.open = false; } }); } - // --- 2. in-step search ---------------------------------------------- + // --- 2. in-step search + match count -------------------------------- document.body.addEventListener('input', function (ev) { var el = ev.target; @@ -67,6 +48,7 @@ var step = el.closest('.step'); if (!step) { return; } var query = el.value.trim().toLowerCase(); + var matchCount = 0; step.querySelectorAll('.log-line').forEach(function (line) { if (!query) { line.style.display = ''; @@ -80,17 +62,22 @@ } else { line.style.display = ''; line.classList.add('log-hit'); + matchCount++; } }); + var counter = el.closest('.log-search-wrap').querySelector('.log-match-count'); + if (counter) { + if (!query) { + counter.textContent = ''; + } else if (matchCount === 0) { + counter.textContent = 'No matches'; + } else { + counter.textContent = matchCount + (matchCount === 1 ? ' match' : ' matches'); + } + } }); // --- 3. live duration tick ------------------------------------------ - // - // .run-duration spans carry data-started-at (RFC3339) while the run is - // non-terminal. Every second we rewrite their text with the current - // elapsed so the header timer ticks between SSE pushes. When an SSE - // swap drops the attribute (run finished), the tick silently skips it - // and the server-rendered final value stays put. function formatDuration(ms) { if (ms < 0) { ms = 0; } @@ -126,7 +113,6 @@ if (!hash) { return; } var target = document.getElementById(hash); if (!target) { return; } - // Open the enclosing step so the target is actually visible. var step = target.closest('.step'); if (step && !step.open) { step.open = true; } target.scrollIntoView({ block: 'center' }); @@ -135,8 +121,6 @@ window.addEventListener('load', scrollToHash); window.addEventListener('hashchange', scrollToHash); - // Anchor clicks update location.hash without triggering navigation; - // the hashchange listener above handles the scroll + highlight. document.body.addEventListener('click', function (ev) { var a = ev.target.closest && ev.target.closest('.log-anchor'); if (!a) { return; } @@ -147,4 +131,108 @@ scrollToHash(); } }); + + // --- 5. active nav indicator ---------------------------------------- + + (function setActiveNav() { + var path = location.pathname; + var nav = document.querySelector('[data-nav]'); + if (!nav) { return; } + nav.querySelectorAll('a').forEach(function (a) { + var href = a.getAttribute('href'); + if (href === '/hosts/new' && path === '/hosts/new') { + a.classList.add('nav-active'); + } else if (href === '/' && path !== '/hosts/new') { + a.classList.add('nav-active'); + } + }); + })(); + + // --- 6. copy-to-clipboard buttons ----------------------------------- + + document.body.addEventListener('click', function (ev) { + var btn = ev.target.closest && ev.target.closest('.copy-btn'); + if (!btn) { return; } + var wrap = btn.closest('.copyable-wrap'); + if (!wrap) { return; } + var source = wrap.querySelector('.one-liner code, .hold-ssh'); + if (!source) { return; } + var text = source.textContent || ''; + navigator.clipboard.writeText(text.trim()).then(function () { + btn.textContent = 'Copied'; + btn.classList.add('copied'); + setTimeout(function () { + btn.textContent = 'Copy'; + btn.classList.remove('copied'); + }, 2000); + }); + }); + + // --- 7. SSE connection health indicator ------------------------------ + + var lastSSE = 0; + var HEARTBEAT_INTERVAL = 15000; + var STALE_THRESHOLD = HEARTBEAT_INTERVAL * 2.5; + + document.body.addEventListener('htmx:sseMessage', function () { + lastSSE = Date.now(); + var hb = document.querySelector('.heartbeat'); + if (hb) { + hb.classList.add('heartbeat-live'); + hb.classList.remove('heartbeat-stale'); + } + }); + + setInterval(function () { + if (!lastSSE) { return; } + var hb = document.querySelector('.heartbeat'); + if (!hb) { return; } + if (Date.now() - lastSSE > STALE_THRESHOLD) { + hb.classList.remove('heartbeat-live'); + hb.classList.add('heartbeat-stale'); + } + }, 5000); + + // --- 8. inline confirmation for destructive actions ------------------ + + document.body.addEventListener('submit', function (ev) { + var form = ev.target; + if (!form || !form.hasAttribute('data-confirm')) { return; } + if (form.dataset.confirmed === 'yes') { + form.removeAttribute('data-confirmed'); + return; + } + ev.preventDefault(); + if (form.querySelector('.confirm-overlay')) { return; } + + var msg = form.getAttribute('data-confirm'); + var btn = form.querySelector('button[type="submit"]'); + if (!btn) { return; } + btn.style.display = 'none'; + + var overlay = document.createElement('div'); + overlay.className = 'confirm-overlay'; + overlay.innerHTML = + '' + escapeHtml(msg) + '' + + '' + + ''; + + form.appendChild(overlay); + + overlay.querySelector('.confirm-yes').addEventListener('click', function () { + form.dataset.confirmed = 'yes'; + form.requestSubmit(); + }); + overlay.querySelector('.confirm-no').addEventListener('click', function () { + overlay.remove(); + btn.style.display = ''; + }); + }); + + function escapeHtml(str) { + var div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + })(); diff --git a/internal/web/templates/active_step.templ b/internal/web/templates/active_step.templ index 3d72293..7c8252e 100644 --- a/internal/web/templates/active_step.templ +++ b/internal/web/templates/active_step.templ @@ -39,7 +39,8 @@ templ ActiveStep(d ActiveStepData) { }
- + +
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" sse-swap=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("log-%d-%s", d.RunID, d.Stage.Name)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/active_step.templ`, Line: 48, Col: 62} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" hx-swap=\"beforeend show:bottom\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -221,7 +234,7 @@ func ActiveStep(d ActiveStepData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/web/templates/host_page.templ b/internal/web/templates/host_page.templ index ba92027..73229ee 100644 --- a/internal/web/templates/host_page.templ +++ b/internal/web/templates/host_page.templ @@ -104,31 +104,38 @@ templ HostActions(d HostPageData) {
Profile -
} else if hostCanStartIfOnline(d) { - +
+ + Host is offline — run the reporter script on the target host to bring it online. +
} else { } -
+
@@ -168,7 +175,10 @@ templ HostEmptyState(d HostPageData) { } else { - +
+ + Host is offline — run the reporter script on the target host to bring it online. +
} } @@ -322,6 +332,15 @@ func hasCriticalDiff(diffs []model.SpecDiff) bool { return false } +func diffBadgeClass(diffs []model.SpecDiff) string { + for _, d := range diffs { + if d.Severity == "critical" && !d.Ignored { + return "diff-badge-critical" + } + } + return "diff-badge-warn" +} + // relativeTime renders a past time as "2m ago" / "1h ago" / "3d ago". // Future times (clock skew) render as "now" so the runs table never // shows nonsense when a host's clock is ahead of the orchestrator. diff --git a/internal/web/templates/host_page_templ.go b/internal/web/templates/host_page_templ.go index 5dcb87c..b247599 100644 --- a/internal/web/templates/host_page_templ.go +++ b/internal/web/templates/host_page_templ.go @@ -361,12 +361,12 @@ func HostActions(d HostPageData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"inline host-start-form\">
Profile
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"inline host-start-form\">
Profile
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if hostCanStartIfOnline(d) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
Host is offline — run the reporter script on the target host to bring it online.
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -383,13 +383,13 @@ func HostActions(d HostPageData) templ.Component { var templ_7745c5c3_Var19 templ.SafeURL templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/hosts/%d/delete", d.Host.ID))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 131, Col: 89} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 138, Col: 89} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" class=\"inline\" onsubmit=\"return confirm('Delete host and all its runs?');\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" class=\"inline\" data-confirm=\"Delete this host and all its runs?\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -428,7 +428,7 @@ func InFlightBanner(d HostPageData) templ.Component { var templ_7745c5c3_Var21 string templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("detail-inflight-%d", d.Host.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 143, Col: 51} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 150, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { @@ -441,7 +441,7 @@ func InFlightBanner(d HostPageData) templ.Component { var templ_7745c5c3_Var22 string templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("detail-inflight-%d", d.Host.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 145, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 152, Col: 57} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { @@ -459,7 +459,7 @@ func InFlightBanner(d HostPageData) templ.Component { var templ_7745c5c3_Var23 templ.SafeURL templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/runs/%d", d.ActiveRun.ID))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 149, Col: 92} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 156, Col: 92} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { @@ -472,7 +472,7 @@ func InFlightBanner(d HostPageData) templ.Component { var templ_7745c5c3_Var24 string templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", d.ActiveRun.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 150, Col: 74} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 157, Col: 74} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { @@ -485,7 +485,7 @@ func InFlightBanner(d HostPageData) templ.Component { var templ_7745c5c3_Var25 string templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tileStatus(d.ActiveRun)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 151, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 158, Col: 59} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { @@ -541,7 +541,7 @@ func HostEmptyState(d HostPageData) templ.Component { var templ_7745c5c3_Var27 templ.SafeURL templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/hosts/%d/start", d.Host.ID))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 167, Col: 88} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 174, Col: 88} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { @@ -552,7 +552,7 @@ func HostEmptyState(d HostPageData) templ.Component { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
Host is offline — run the reporter script on the target host to bring it online.
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -655,7 +655,7 @@ func RunRow(d RunRowData) templ.Component { var templ_7745c5c3_Var31 string templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("runrow-%d", d.Run.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 219, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 229, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { @@ -681,7 +681,7 @@ func RunRow(d RunRowData) templ.Component { var templ_7745c5c3_Var33 string templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("runrow-%d", d.Run.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 221, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 231, Col: 47} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { @@ -694,7 +694,7 @@ func RunRow(d RunRowData) templ.Component { var templ_7745c5c3_Var34 templ.SafeURL templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/runs/%d", d.Run.ID))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 225, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 235, Col: 61} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { @@ -707,7 +707,7 @@ func RunRow(d RunRowData) templ.Component { var templ_7745c5c3_Var35 string templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("#%d", d.Run.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 225, Col: 94} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 235, Col: 94} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { @@ -742,7 +742,7 @@ func RunRow(d RunRowData) templ.Component { var templ_7745c5c3_Var38 string templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(tileStatus(&d.Run)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 228, Col: 92} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 238, Col: 92} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) if templ_7745c5c3_Err != nil { @@ -755,7 +755,7 @@ func RunRow(d RunRowData) templ.Component { var templ_7745c5c3_Var39 string templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(relativeTime(d.Run.StartedAt)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 230, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 240, Col: 62} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) if templ_7745c5c3_Err != nil { @@ -768,7 +768,7 @@ func RunRow(d RunRowData) templ.Component { var templ_7745c5c3_Var40 string templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(runDuration(&d.Run)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 231, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 241, Col: 53} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) if templ_7745c5c3_Err != nil { @@ -805,7 +805,7 @@ func RunRow(d RunRowData) templ.Component { var templ_7745c5c3_Var43 string templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 236, Col: 94} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 246, Col: 94} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { @@ -823,7 +823,7 @@ func RunRow(d RunRowData) templ.Component { var templ_7745c5c3_Var44 templ.SafeURL templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/runs/%d", d.Run.ID))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 241, Col: 84} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_page.templ`, Line: 251, Col: 84} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { @@ -916,6 +916,15 @@ func hasCriticalDiff(diffs []model.SpecDiff) bool { return false } +func diffBadgeClass(diffs []model.SpecDiff) string { + for _, d := range diffs { + if d.Severity == "critical" && !d.Ignored { + return "diff-badge-critical" + } + } + return "diff-badge-warn" +} + // relativeTime renders a past time as "2m ago" / "1h ago" / "3d ago". // Future times (clock skew) render as "now" so the runs table never // shows nonsense when a host's clock is ahead of the orchestrator. diff --git a/internal/web/templates/host_tile.templ b/internal/web/templates/host_tile.templ index 10b627e..965b250 100644 --- a/internal/web/templates/host_tile.templ +++ b/internal/web/templates/host_tile.templ @@ -16,7 +16,7 @@ import ( templ HostTile(t TileData) {
@@ -25,6 +25,11 @@ templ HostTile(t TileData) {
{ t.Host.Name }
{ lastSeenLabel(t.LastSeenAt) } + if t.Latest != nil { +
+ { tileStatus(t.Latest) } +
+ }
} @@ -84,7 +89,9 @@ func tileMood(r *model.Run) string { return "pass" case model.StateFailed, model.StateFailedHolding: return "fail" - case model.StateReleased, model.StateCancelled: + case model.StateCancelled: + return "cancelled" + case model.StateReleased: return "idle" } return "active" diff --git a/internal/web/templates/host_tile_templ.go b/internal/web/templates/host_tile_templ.go index 5c624e0..b9a623f 100644 --- a/internal/web/templates/host_tile_templ.go +++ b/internal/web/templates/host_tile_templ.go @@ -42,107 +42,170 @@ func HostTile(t TileData) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var2 = []any{"tile", "tile-" + tileMood(t.Latest)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-swap=\"outerHTML\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" aria-label=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var7 = []any{"tile-last-seen", lastSeenClass(t.LastSeenAt)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...) + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("Open " + t.Host.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 23, Col: 117} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var7).String()) + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(t.Host.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 1, Col: 0} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 25, Col: 39} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(lastSeenLabel(t.LastSeenAt)) + var templ_7745c5c3_Var9 = []any{"tile-last-seen", lastSeenClass(t.LastSeenAt)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(lastSeenLabel(t.LastSeenAt)) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 26, Col: 94} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if t.Latest != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 = []any{"run-status-badge", "run-status-badge-sm", "run-status-" + tileMood(t.Latest)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(tileStatus(t.Latest)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 30, Col: 120} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -206,7 +269,9 @@ func tileMood(r *model.Run) string { return "pass" case model.StateFailed, model.StateFailedHolding: return "fail" - case model.StateReleased, model.StateCancelled: + case model.StateCancelled: + return "cancelled" + case model.StateReleased: return "idle" } return "active" diff --git a/internal/web/templates/host_tile_test.go b/internal/web/templates/host_tile_test.go index ed6002f..750ab65 100644 --- a/internal/web/templates/host_tile_test.go +++ b/internal/web/templates/host_tile_test.go @@ -62,10 +62,11 @@ func TestHostTile_OverlayLink(t *testing.T) { t.Fatalf("tile missing tile-link class: %s", html) } // Dropped content that used to live on the tile — confirm it has - // actually moved off so the slim-down is real. + // actually moved off so the slim-down is real. tile-status is + // intentionally re-added as a minimal status badge (issue #11). for _, dropped := range []string{ `tile-meta`, `tile-log`, `tile-actions`, `tile-hold`, - `tile-primary-action`, `tile-status`, `tile-start-form`, + `tile-primary-action`, `tile-start-form`, `tile-nd-toggle`, `tile-cancel-form`, `/hosts/42/start`, `/hosts/42/cancel`, `Start vetting`, `Non-destructive`, `Cancel run`, `View report`, @@ -129,10 +130,10 @@ func TestTileStatusCancelledFromHold(t *testing.T) { wantMood: "fail", }, { - name: "mid-stage cancel stays plain cancelled", + name: "mid-stage cancel gets cancelled mood", run: &model.Run{State: model.StateCancelled}, wantStatus: "Cancelled", - wantMood: "idle", + wantMood: "cancelled", }, { name: "failed-holding itself still reads as FailedHolding", diff --git a/internal/web/templates/layout.templ b/internal/web/templates/layout.templ index 05a2310..95633c8 100644 --- a/internal/web/templates/layout.templ +++ b/internal/web/templates/layout.templ @@ -15,12 +15,12 @@ templ Layout(title string) {
Vetting
-
diff --git a/internal/web/templates/layout_templ.go b/internal/web/templates/layout_templ.go index 0d5ce70..22b30fd 100644 --- a/internal/web/templates/layout_templ.go +++ b/internal/web/templates/layout_templ.go @@ -42,7 +42,7 @@ func Layout(title string) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " — Vetting
Vetting
·
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " — Vetting
Vetting
·
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/web/templates/not_found.templ b/internal/web/templates/not_found.templ new file mode 100644 index 0000000..f98220b --- /dev/null +++ b/internal/web/templates/not_found.templ @@ -0,0 +1,11 @@ +package templates + +templ NotFound() { + @Layout("Not found") { +
+

Page not found

+

The host or run you're looking for doesn't exist or has been deleted.

+ Back to dashboard +
+ } +} diff --git a/internal/web/templates/not_found_templ.go b/internal/web/templates/not_found_templ.go new file mode 100644 index 0000000..e48e725 --- /dev/null +++ b/internal/web/templates/not_found_templ.go @@ -0,0 +1,58 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1001 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +func NotFound() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Page not found

The host or run you're looking for doesn't exist or has been deleted.

Back to dashboard
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = Layout("Not found").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/web/templates/registration.templ b/internal/web/templates/registration.templ index 49794d7..bcdf49d 100644 --- a/internal/web/templates/registration.templ +++ b/internal/web/templates/registration.templ @@ -25,14 +25,16 @@ templ Registration(form RegistrationForm) {

Quick register (recommended)

Run this on the target host as root before wiping. It auto-detects MAC and hardware, then registers with this orchestrator:

-
{ "curl -fsSL " + form.QuickRegisterURL + "/register/quick.sh | sudo bash" }
+
+
{ "curl -fsSL " + form.QuickRegisterURL + "/register/quick.sh | sudo bash" }
+ +

After the script prints OK, refresh the dashboard and click Start vetting on the new host.

}
-
-

Register manually

-
+

Register manually

+
} diff --git a/internal/web/templates/registration_templ.go b/internal/web/templates/registration_templ.go index ed0cfeb..028aaac 100644 --- a/internal/web/templates/registration_templ.go +++ b/internal/web/templates/registration_templ.go @@ -76,32 +76,32 @@ func Registration(form RegistrationForm) templ.Component { } } if form.QuickRegisterURL != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

Quick register (recommended)

Run this on the target host as root before wiping. It auto-detects MAC and hardware, then registers with this orchestrator:

")
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

Quick register (recommended)

Run this on the target host as root before wiping. It auto-detects MAC and hardware, then registers with this orchestrator:

")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
 				var templ_7745c5c3_Var4 string
 				templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("curl -fsSL " + form.QuickRegisterURL + "/register/quick.sh | sudo bash")
 				if templ_7745c5c3_Err != nil {
-					return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/registration.templ`, Line: 28, Col: 108}
+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/registration.templ`, Line: 29, Col: 109}
 				}
 				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

After the script prints OK, refresh the dashboard and click Start vetting on the new host.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

After the script prints OK, refresh the dashboard and click Start vetting on the new host.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

Register manually

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
Cancel
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/web/templates/run_detail.templ b/internal/web/templates/run_detail.templ index ef57703..8712a4e 100644 --- a/internal/web/templates/run_detail.templ +++ b/internal/web/templates/run_detail.templ @@ -101,11 +101,11 @@ templ RunHeader(d RunPageData) {
if canCancel(&d.Run) { if d.Run.State == model.StateFailedHolding { -
+
} else { -
+
} @@ -140,7 +140,10 @@ templ HoldBanner(d RunPageData) { hx-swap="outerHTML" > Host is holding — SSH available: - { sshInvocation(d.HoldKeyPath, d.Run.HoldIP) } +
+ { sshInvocation(d.HoldKeyPath, d.Run.HoldIP) } + +
} else {
if len(d.SpecDiffs) > 0 {
-

Spec diffs ({ fmt.Sprintf("%d", len(d.SpecDiffs)) })

+ +

Spec diffs

+ { fmt.Sprintf("%d", len(d.SpecDiffs)) } +
    for _, diff := range d.SpecDiffs {
  • diff --git a/internal/web/templates/run_detail_templ.go b/internal/web/templates/run_detail_templ.go index 83e841f..3c01905 100644 --- a/internal/web/templates/run_detail_templ.go +++ b/internal/web/templates/run_detail_templ.go @@ -419,7 +419,7 @@ func RunHeader(d RunPageData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"inline\" onsubmit=\"return confirm('Cancel held run? The host will reboot to local disk.');\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"inline\" data-confirm=\"Cancel held run? The host will reboot to local disk.\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -437,7 +437,7 @@ func RunHeader(d RunPageData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" class=\"inline\" onsubmit=\"return confirm('Cancel run? Destructive stages may leave the host in an intermediate state requiring manual cleanup.');\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" class=\"inline\" data-confirm=\"Cancel run? Destructive stages may leave the host mid-operation.\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -560,20 +560,20 @@ func HoldBanner(d RunPageData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" hx-swap=\"outerHTML\">Host is holding — SSH available: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" hx-swap=\"outerHTML\">Host is holding — SSH available:
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(sshInvocation(d.HoldKeyPath, d.Run.HoldIP)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 143, Col: 70} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 144, Col: 71} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -585,7 +585,7 @@ func HoldBanner(d RunPageData) templ.Component { var templ_7745c5c3_Var31 string templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("detail-hold-%d", d.Run.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 147, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 150, Col: 47} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { @@ -598,7 +598,7 @@ func HoldBanner(d RunPageData) templ.Component { var templ_7745c5c3_Var32 string templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("detail-hold-%d", d.Run.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 149, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 152, Col: 53} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) if templ_7745c5c3_Err != nil { @@ -644,7 +644,7 @@ func RunSpecDiffs(d RunPageData) templ.Component { var templ_7745c5c3_Var34 string templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("detail-specdiffs-%d", d.Run.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 160, Col: 51} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 163, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { @@ -657,7 +657,7 @@ func RunSpecDiffs(d RunPageData) templ.Component { var templ_7745c5c3_Var35 string templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("detail-specdiffs-%d", d.Run.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 162, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 165, Col: 57} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { @@ -678,92 +678,114 @@ func RunSpecDiffs(d RunPageData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">

Spec diffs (") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">

Spec diffs

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var36 string - templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(d.SpecDiffs))) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 167, Col: 66} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) + var templ_7745c5c3_Var36 = []any{"diff-badge", diffBadgeClass(d.SpecDiffs)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var36...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, ")

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var38 string + templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(d.SpecDiffs))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 172, Col: 100} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
      ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, diff := range d.SpecDiffs { - var templ_7745c5c3_Var37 = []any{"diff-row", "diff-" + diff.Severity} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var37...) + var templ_7745c5c3_Var39 = []any{"diff-row", "diff-" + diff.Severity} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var39...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
    • ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var39 string - templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(diff.Field) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 171, Col: 43} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
      expected: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
    • actual: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\">
      ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var41 string - templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(diff.Actual) + templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(diff.Field) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 173, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 177, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "
    • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "
      expected: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var42 string + templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(diff.Expected) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 178, Col: 65} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
      actual: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var43 string + templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(diff.Actual) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/run_detail.templ`, Line: 179, Col: 59} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
      ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }