ui: embed reanimator chart viewer
This commit is contained in:
69
internal/server/chart_view_test.go
Normal file
69
internal/server/chart_view_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
func TestHandleChartCurrent_RendersCurrentReanimatorSnapshot(t *testing.T) {
|
||||
s := New(Config{})
|
||||
s.SetResult(&models.AnalysisResult{
|
||||
SourceType: models.SourceTypeArchive,
|
||||
Filename: "example.zip",
|
||||
CollectedAt: time.Date(2026, 3, 16, 10, 0, 0, 0, time.UTC),
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: models.BoardInfo{
|
||||
ProductName: "SYS-TEST",
|
||||
SerialNumber: "SN123",
|
||||
},
|
||||
CPUs: []models.CPU{
|
||||
{
|
||||
Socket: 1,
|
||||
Model: "Xeon Gold",
|
||||
Cores: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/chart/current", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
s.mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "SYS-TEST - SN123") {
|
||||
t.Fatalf("expected chart title in body, got %q", body)
|
||||
}
|
||||
if !strings.Contains(body, `/chart/static/view.css`) {
|
||||
t.Fatalf("expected rewritten chart static path, got %q", body)
|
||||
}
|
||||
if !strings.Contains(body, "Snapshot Metadata") {
|
||||
t.Fatalf("expected rendered chart output, got %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleChartCurrent_RendersEmptyViewerWithoutResult(t *testing.T) {
|
||||
s := New(Config{})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/chart/current", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
s.mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "Snapshot Viewer") {
|
||||
t.Fatalf("expected empty chart viewer, got %q", body)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"git.mchus.pro/mchus/logpile/internal/exporter"
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
chartviewer "reanimator/chart/viewer"
|
||||
)
|
||||
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -47,6 +48,82 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
tmpl.Execute(w, nil)
|
||||
}
|
||||
|
||||
func (s *Server) handleChartCurrent(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
title := chartTitle(result)
|
||||
if result == nil || result.Hardware == nil {
|
||||
html, err := chartviewer.RenderHTML(nil, title)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to render viewer", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write(rewriteChartStaticPaths(html))
|
||||
return
|
||||
}
|
||||
|
||||
snapshotBytes, err := currentReanimatorSnapshotBytes(result)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to build reanimator snapshot: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
html, err := chartviewer.RenderHTML(snapshotBytes, title)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to render chart: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write(rewriteChartStaticPaths(html))
|
||||
}
|
||||
|
||||
func currentReanimatorSnapshotBytes(result *models.AnalysisResult) ([]byte, error) {
|
||||
reanimatorData, err := exporter.ConvertToReanimator(result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(reanimatorData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func chartTitle(result *models.AnalysisResult) string {
|
||||
const fallback = "LOGPile Reanimator Viewer"
|
||||
if result == nil {
|
||||
return fallback
|
||||
}
|
||||
if result.Hardware != nil {
|
||||
board := result.Hardware.BoardInfo
|
||||
product := strings.TrimSpace(board.ProductName)
|
||||
serial := strings.TrimSpace(board.SerialNumber)
|
||||
switch {
|
||||
case product != "" && serial != "":
|
||||
return product + " - " + serial
|
||||
case product != "":
|
||||
return product
|
||||
case serial != "":
|
||||
return serial
|
||||
}
|
||||
}
|
||||
if host := strings.TrimSpace(result.TargetHost); host != "" {
|
||||
return host
|
||||
}
|
||||
if filename := strings.TrimSpace(result.Filename); filename != "" {
|
||||
return filename
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func rewriteChartStaticPaths(html []byte) []byte {
|
||||
return bytes.ReplaceAll(html, []byte(`href="/static/view.css"`), []byte(`href="/chart/static/view.css"`))
|
||||
}
|
||||
|
||||
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, uploadMultipartMaxBytes())
|
||||
if err := r.ParseMultipartForm(uploadMultipartFormMemoryBytes()); err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/collector"
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
chartviewer "reanimator/chart/viewer"
|
||||
)
|
||||
|
||||
// WebFS holds embedded web files (set by main package)
|
||||
@@ -64,9 +65,13 @@ func (s *Server) setupRoutes() {
|
||||
panic(err)
|
||||
}
|
||||
s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticContent))))
|
||||
s.mux.Handle("/chart/", http.StripPrefix("/chart", chartviewer.NewHandler(chartviewer.HandlerOptions{
|
||||
Title: "LOGPile Reanimator Viewer",
|
||||
})))
|
||||
|
||||
// Pages
|
||||
s.mux.HandleFunc("/", s.handleIndex)
|
||||
s.mux.HandleFunc("GET /chart/current", s.handleChartCurrent)
|
||||
|
||||
// API endpoints
|
||||
s.mux.HandleFunc("POST /api/upload", s.handleUpload)
|
||||
|
||||
Reference in New Issue
Block a user