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) {
} else if hostCanStartIfOnline(d) {
-
+
} else {
}
-
@@ -168,7 +175,10 @@ templ HostEmptyState(d HostPageData) {
} else {
-
+
}
}
@@ -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\"> ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"inline host-start-form\"> ")
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, "")
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, "")
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) {
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")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " — 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.
}
-
}
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, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "")
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) {