Add activity log system for provisioning lifecycle visibility
Hosts stuck in states like pxe_ready had zero visibility into why. This adds a persistent activity log that records every meaningful step (state transitions, PXE events, cluster join stages, failures) and surfaces it on the host detail page with live SSE updates. Includes a stuck-detection warning banner when a host sits in pxe_ready for >10 minutes with no iPXE request. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+28
-2
@@ -5,6 +5,7 @@ import (
|
||||
"html"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"provisioning/internal/model"
|
||||
)
|
||||
@@ -83,7 +84,7 @@ func hostFormPage(types []string, errMsg string, prefill *model.Host) string {
|
||||
`, errHTML, hostname, mac, opts.String(), notes))
|
||||
}
|
||||
|
||||
func hostDetailPage(h *model.Host, ops []model.Operation) string {
|
||||
func hostDetailPage(h *model.Host, ops []model.Operation, activity []model.ActivityEntry) string {
|
||||
stateClass := stateColor(h.State)
|
||||
led := ledClass(h.State)
|
||||
canRebuild := h.State == model.StateRegistered || h.State == model.StateReady || h.State == model.StateFailed
|
||||
@@ -113,12 +114,29 @@ func hostDetailPage(h *model.Host, ops []model.Operation) string {
|
||||
ip = "—"
|
||||
}
|
||||
|
||||
var stuckWarning string
|
||||
if h.State == model.StatePXEReady && time.Since(h.UpdatedAt) > 10*time.Minute {
|
||||
mins := int(time.Since(h.UpdatedAt).Minutes())
|
||||
stuckWarning = fmt.Sprintf(`<div class="stuck-warning">Host has been in PXE_READY for %d minutes with no iPXE request. This usually means the host failed to PXE boot — check secure boot settings, network connectivity, and BIOS boot order.</div>`, mins)
|
||||
}
|
||||
|
||||
var activityHTML strings.Builder
|
||||
for _, e := range activity {
|
||||
activityHTML.WriteString(fmt.Sprintf(
|
||||
`<div class="log-entry log-%s"><span class="log-time">%s</span><span class="log-source">%s</span><span class="log-msg">%s</span></div>`,
|
||||
e.Level, e.CreatedAt.Format("15:04"), html.EscapeString(e.Source), html.EscapeString(e.Message)))
|
||||
}
|
||||
if len(activity) == 0 {
|
||||
activityHTML.WriteString(`<p class="empty">No activity recorded yet.</p>`)
|
||||
}
|
||||
|
||||
return layout(h.Hostname, fmt.Sprintf(`
|
||||
<div class="host-header">
|
||||
<span class="led led-lg %s"></span>
|
||||
<h2 style="margin-bottom:0">%s</h2>
|
||||
<span class="badge %s">%s</span>
|
||||
</div>
|
||||
%s
|
||||
<div class="panel">
|
||||
<table class="detail-table">
|
||||
<tr><th>MAC</th><td>%s</td></tr>
|
||||
@@ -135,7 +153,15 @@ func hostDetailPage(h *model.Host, ops []model.Operation) string {
|
||||
<tbody>%s</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`, led, html.EscapeString(h.Hostname), stateClass, h.State, h.MAC, h.ServerType, ip, html.EscapeString(h.Notes), actions.String(), opsHTML.String()))
|
||||
<h3>Activity Log</h3>
|
||||
<div class="panel">
|
||||
<div class="activity-log" id="activity-log" data-host-id="%d">%s</div>
|
||||
</div>
|
||||
`, led, html.EscapeString(h.Hostname), stateClass, h.State,
|
||||
stuckWarning,
|
||||
h.MAC, h.ServerType, ip, html.EscapeString(h.Notes), actions.String(),
|
||||
opsHTML.String(),
|
||||
h.ID, activityHTML.String()))
|
||||
}
|
||||
|
||||
func imagesPage(images []model.Image) string {
|
||||
|
||||
Reference in New Issue
Block a user