From b965184e7123e9f01aef737b716a243c6e784dc6 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Mon, 16 Mar 2026 18:26:05 +0300 Subject: [PATCH] feat: wrap chart viewer in web shell --- audit/internal/webui/server.go | 52 ++++++++++++++++++++++++----- audit/internal/webui/server_test.go | 41 ++++++++++++++++++++--- 2 files changed, 80 insertions(+), 13 deletions(-) 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("

" + html.EscapeString(title) + "

Audit viewer with support bundle and raw export access.

") + 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()) } }