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:
2026-03-28 08:19:17 +03:00
parent 6caf771d6e
commit cd2853ad99
2 changed files with 13 additions and 116 deletions

View File

@@ -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 {

View File

@@ -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)