diff --git a/audit/internal/webui/server.go b/audit/internal/webui/server.go
index 2425021..163874a 100644
--- a/audit/internal/webui/server.go
+++ b/audit/internal/webui/server.go
@@ -104,19 +104,13 @@ func NewHandler(opts HandlerOptions) http.Handler {
}
http.ServeFile(w, r, filepath.Join(exportDir, clean))
})
- mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
+ mux.HandleFunc("GET /viewer", func(w http.ResponseWriter, r *http.Request) {
snapshot, err := loadSnapshot(auditPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
http.Error(w, fmt.Sprintf("read audit snapshot: %v", err), http.StatusInternalServerError)
return
}
- noticeTitle, noticeBody := runtimeNotice(filepath.Join(exportDir, "runtime-health.json"))
- html, err := viewer.RenderHTMLWithOptions(snapshot, title, viewer.RenderOptions{
- DownloadArchiveURL: "/export/support.tar.gz",
- DownloadArchiveLabel: "Download support bundle",
- NoticeTitle: noticeTitle,
- NoticeBody: noticeBody,
- })
+ html, err := viewer.RenderHTML(snapshot, title)
if err != nil {
http.Error(w, fmt.Sprintf("render snapshot: %v", err), http.StatusInternalServerError)
return
@@ -125,6 +119,13 @@ func NewHandler(opts HandlerOptions) http.Handler {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(html)
})
+ mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
+ noticeTitle, noticeBody := runtimeNotice(filepath.Join(exportDir, "runtime-health.json"))
+ body := renderShellPage(title, noticeTitle, noticeBody)
+ w.Header().Set("Cache-Control", "no-store")
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = w.Write([]byte(body))
+ })
return mux
}
@@ -195,6 +196,41 @@ func renderExportIndex(exportDir string) (string, error) {
return body.String(), nil
}
+func renderShellPage(title, noticeTitle, noticeBody string) string {
+ var body strings.Builder
+ body.WriteString("
")
+ body.WriteString("" + html.EscapeString(title) + "")
+ body.WriteString(``)
+ body.WriteString("")
+ body.WriteString("
")
+ if strings.TrimSpace(noticeTitle) != "" {
+ body.WriteString("
" + html.EscapeString(noticeTitle) + "
" + html.EscapeString(noticeBody) + "
")
+ }
+ body.WriteString("
")
+ body.WriteString("
")
+ return body.String()
+}
+
func firstNonEmpty(value, fallback string) string {
value = strings.TrimSpace(value)
if value == "" {
diff --git a/audit/internal/webui/server_test.go b/audit/internal/webui/server_test.go
index 61d2cc0..5f17b3b 100644
--- a/audit/internal/webui/server_test.go
+++ b/audit/internal/webui/server_test.go
@@ -9,7 +9,7 @@ import (
"testing"
)
-func TestRootRendersLatestSnapshot(t *testing.T) {
+func TestRootRendersShellWithIframe(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
exportDir := filepath.Join(dir, "export")
@@ -31,8 +31,8 @@ func TestRootRendersLatestSnapshot(t *testing.T) {
if first.Code != http.StatusOK {
t.Fatalf("first status=%d", first.Code)
}
- if !strings.Contains(first.Body.String(), "SERIAL-OLD") {
- t.Fatalf("first body missing old serial: %s", first.Body.String())
+ if !strings.Contains(first.Body.String(), `iframe`) || !strings.Contains(first.Body.String(), `src="/viewer"`) {
+ t.Fatalf("first body missing iframe viewer: %s", first.Body.String())
}
if !strings.Contains(first.Body.String(), "/export/support.tar.gz") {
t.Fatalf("first body missing support bundle link: %s", first.Body.String())
@@ -50,11 +50,42 @@ func TestRootRendersLatestSnapshot(t *testing.T) {
if second.Code != http.StatusOK {
t.Fatalf("second status=%d", second.Code)
}
+ if !strings.Contains(second.Body.String(), `src="/viewer"`) {
+ t.Fatalf("second body missing iframe viewer: %s", second.Body.String())
+ }
+}
+
+func TestViewerRendersLatestSnapshot(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "audit.json")
+ if err := os.WriteFile(path, []byte(`{"collected_at":"2026-03-15T00:00:00Z","hardware":{"board":{"serial_number":"SERIAL-OLD"}}}`), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ handler := NewHandler(HandlerOptions{AuditPath: path})
+ first := httptest.NewRecorder()
+ handler.ServeHTTP(first, httptest.NewRequest(http.MethodGet, "/viewer", nil))
+ if first.Code != http.StatusOK {
+ t.Fatalf("first status=%d", first.Code)
+ }
+ if !strings.Contains(first.Body.String(), "SERIAL-OLD") {
+ t.Fatalf("viewer body missing old serial: %s", first.Body.String())
+ }
+
+ if err := os.WriteFile(path, []byte(`{"collected_at":"2026-03-15T00:05:00Z","hardware":{"board":{"serial_number":"SERIAL-NEW"}}}`), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ second := httptest.NewRecorder()
+ handler.ServeHTTP(second, httptest.NewRequest(http.MethodGet, "/viewer", nil))
+ if second.Code != http.StatusOK {
+ t.Fatalf("second status=%d", second.Code)
+ }
if !strings.Contains(second.Body.String(), "SERIAL-NEW") {
- t.Fatalf("second body missing new serial: %s", second.Body.String())
+ t.Fatalf("viewer body missing new serial: %s", second.Body.String())
}
if strings.Contains(second.Body.String(), "SERIAL-OLD") {
- t.Fatalf("second body still contains old serial: %s", second.Body.String())
+ t.Fatalf("viewer body still contains old serial: %s", second.Body.String())
}
}