fix(webui): fix viewer static path so Reanimator Chart CSS loads correctly
Mount chart submodule static assets at /static/ (matching the template's hardcoded href), fix nav to include Audit Snapshot tab, remove dead renderViewerPage code and iframe from Dashboard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -84,15 +84,16 @@ tbody tr:hover td{background:rgba(0,0,0,.03)}
|
||||
}
|
||||
|
||||
func layoutNav(active string) string {
|
||||
items := []struct{ id, icon, label string }{
|
||||
{"dashboard", "", "Dashboard"},
|
||||
{"metrics", "", "Metrics"},
|
||||
{"tests", "", "Acceptance Tests"},
|
||||
{"burn-in", "", "Burn-in"},
|
||||
{"network", "", "Network"},
|
||||
{"services", "", "Services"},
|
||||
{"export", "", "Export"},
|
||||
{"tools", "", "Tools"},
|
||||
items := []struct{ id, label, href string }{
|
||||
{"dashboard", "Dashboard", "/"},
|
||||
{"viewer", "Audit Snapshot", "/viewer"},
|
||||
{"metrics", "Metrics", "/metrics"},
|
||||
{"tests", "Acceptance Tests", "/tests"},
|
||||
{"burn-in", "Burn-in", "/burn-in"},
|
||||
{"network", "Network", "/network"},
|
||||
{"services", "Services", "/services"},
|
||||
{"export", "Export", "/export"},
|
||||
{"tools", "Tools", "/tools"},
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(`<aside class="sidebar">`)
|
||||
@@ -103,12 +104,8 @@ func layoutNav(active string) string {
|
||||
if item.id == active {
|
||||
cls += " active"
|
||||
}
|
||||
href := "/"
|
||||
if item.id != "dashboard" {
|
||||
href = "/" + item.id
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(`<a class="%s" href="%s">%s</a>`,
|
||||
cls, href, item.label))
|
||||
cls, item.href, item.label))
|
||||
}
|
||||
b.WriteString(`</nav></aside>`)
|
||||
return b.String()
|
||||
@@ -182,11 +179,6 @@ func renderDashboard(opts HandlerOptions) string {
|
||||
b.WriteString(`</div></div>`)
|
||||
b.WriteString(`</div>`)
|
||||
b.WriteString(`</div>`)
|
||||
// Audit viewer iframe
|
||||
b.WriteString(`<div class="card"><div class="card-head">Audit Snapshot</div><div class="card-body" style="padding:0">`)
|
||||
b.WriteString(`<iframe class="viewer-frame" src="/viewer" loading="eager" referrerpolicy="same-origin"></iframe>`)
|
||||
b.WriteString(`</div></div>`)
|
||||
|
||||
// Audit run output div
|
||||
b.WriteString(`<div id="audit-output" style="display:none" class="card"><div class="card-head">Audit Output</div><div class="card-body"><div id="audit-terminal" class="terminal"></div></div></div>`)
|
||||
|
||||
@@ -566,101 +558,6 @@ checkTools();
|
||||
</script>`
|
||||
}
|
||||
|
||||
// ── Viewer (compatibility) ────────────────────────────────────────────────────
|
||||
|
||||
// renderViewerPage renders the audit snapshot as a styled HTML page.
|
||||
// This endpoint is embedded as an iframe on the Dashboard page.
|
||||
func renderViewerPage(title string, snapshot []byte) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(`<!DOCTYPE html><html><head><meta charset="utf-8">`)
|
||||
b.WriteString(`<title>` + html.EscapeString(title) + `</title>`)
|
||||
b.WriteString(`<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:system-ui,sans-serif;background:#0f1117;color:#e2e8f0;padding:20px}
|
||||
h2{font-size:14px;color:#64748b;margin-bottom:8px;margin-top:16px;text-transform:uppercase;letter-spacing:.05em}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
|
||||
.card{background:#161b25;border:1px solid #1e2535;border-radius:8px;padding:14px}
|
||||
.card-title{font-size:12px;color:#64748b;margin-bottom:6px}
|
||||
.card-value{font-size:15px;font-weight:600}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600}
|
||||
.ok{background:#166534;color:#86efac}.warn{background:#713f12;color:#fde68a}.err{background:#7f1d1d;color:#fca5a5}
|
||||
pre{background:#0a0d14;border:1px solid #1e2535;border-radius:6px;padding:12px;font-size:11px;overflow-x:auto;color:#94a3b8;white-space:pre-wrap;word-break:break-word;max-height:400px;overflow-y:auto}
|
||||
</style></head><body>
|
||||
`)
|
||||
if len(snapshot) == 0 {
|
||||
b.WriteString(`<p style="color:var(--muted)">No audit snapshot available yet. Re-run audit from the Dashboard.</p>`)
|
||||
b.WriteString(`</body></html>`)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(snapshot, &data); err != nil {
|
||||
// Fallback: render raw JSON
|
||||
b.WriteString(`<pre>` + html.EscapeString(string(snapshot)) + `</pre>`)
|
||||
b.WriteString(`</body></html>`)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Collected at
|
||||
if t, ok := data["collected_at"].(string); ok {
|
||||
b.WriteString(`<p style="font-size:12px;color:var(--muted);margin-bottom:16px">Collected: ` + html.EscapeString(t) + `</p>`)
|
||||
}
|
||||
|
||||
// Hardware section
|
||||
hw, _ := data["hardware"].(map[string]any)
|
||||
if hw == nil {
|
||||
hw = data
|
||||
}
|
||||
|
||||
renderHWCards(&b, hw)
|
||||
|
||||
// Full JSON below
|
||||
b.WriteString(`<h2>Raw JSON</h2>`)
|
||||
pretty, _ := json.MarshalIndent(data, "", " ")
|
||||
b.WriteString(`<pre>` + html.EscapeString(string(pretty)) + `</pre>`)
|
||||
b.WriteString(`</body></html>`)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func renderHWCards(b *strings.Builder, hw map[string]any) {
|
||||
sections := []struct{ key, label string }{
|
||||
{"board", "Board"},
|
||||
{"cpus", "CPUs"},
|
||||
{"memory", "Memory"},
|
||||
{"storage", "Storage"},
|
||||
{"gpus", "GPUs"},
|
||||
{"nics", "NICs"},
|
||||
{"psus", "Power Supplies"},
|
||||
}
|
||||
for _, s := range sections {
|
||||
v, ok := hw[s.key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
b.WriteString(`<h2>` + s.label + `</h2><div class="grid">`)
|
||||
renderValue(b, v)
|
||||
b.WriteString(`</div>`)
|
||||
}
|
||||
}
|
||||
|
||||
func renderValue(b *strings.Builder, v any) {
|
||||
switch val := v.(type) {
|
||||
case []any:
|
||||
for _, item := range val {
|
||||
renderValue(b, item)
|
||||
}
|
||||
case map[string]any:
|
||||
b.WriteString(`<div class="card">`)
|
||||
for k, vv := range val {
|
||||
b.WriteString(fmt.Sprintf(`<div class="card-title">%s</div><div class="card-value">%s</div>`,
|
||||
html.EscapeString(k), html.EscapeString(fmt.Sprintf("%v", vv))))
|
||||
}
|
||||
b.WriteString(`</div>`)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Export index (compatibility) ──────────────────────────────────────────────
|
||||
|
||||
func renderExportIndex(exportDir string) (string, error) {
|
||||
entries, err := listExportFiles(exportDir)
|
||||
if err != nil {
|
||||
|
||||
@@ -153,8 +153,8 @@ func NewHandler(opts HandlerOptions) http.Handler {
|
||||
mux.HandleFunc("GET /api/metrics/stream", h.handleAPIMetricsStream)
|
||||
mux.HandleFunc("GET /api/metrics/chart/", h.handleMetricsChartSVG)
|
||||
|
||||
// Reanimator chart static assets
|
||||
mux.Handle("GET /chart/static/", http.StripPrefix("/chart/static/", web.Static()))
|
||||
// Reanimator chart static assets (viewer template expects /static/*)
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", web.Static()))
|
||||
|
||||
// ── Pages ────────────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /", h.handlePage)
|
||||
|
||||
Reference in New Issue
Block a user