ui: embed reanimator chart viewer
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -4,3 +4,6 @@
|
|||||||
[submodule "bible"]
|
[submodule "bible"]
|
||||||
path = bible
|
path = bible
|
||||||
url = https://git.mchus.pro/mchus/bible.git
|
url = https://git.mchus.pro/mchus/bible.git
|
||||||
|
[submodule "internal/chart"]
|
||||||
|
path = internal/chart
|
||||||
|
url = https://git.mchus.pro/reanimator/chart.git
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
Default port: `8082`
|
Default port: `8082`
|
||||||
|
|
||||||
|
Audit result rendering is delegated to embedded `reanimator/chart`, vendored as git submodule `internal/chart`.
|
||||||
|
LOGPile remains responsible for upload, collection, parsing, normalization, and Reanimator export generation.
|
||||||
|
|
||||||
## Code map
|
## Code map
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -21,6 +24,7 @@ internal/collector/ live collection and Redfish replay
|
|||||||
internal/analyzer/ shared analysis helpers
|
internal/analyzer/ shared analysis helpers
|
||||||
internal/parser/ archive extraction and parser dispatch
|
internal/parser/ archive extraction and parser dispatch
|
||||||
internal/exporter/ CSV and Reanimator conversion
|
internal/exporter/ CSV and Reanimator conversion
|
||||||
|
internal/chart/ vendored `reanimator/chart` viewer submodule
|
||||||
internal/models/ stable data contracts
|
internal/models/ stable data contracts
|
||||||
web/ embedded UI assets
|
web/ embedded UI assets
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
- JSON responses are used unless the endpoint downloads a file
|
- JSON responses are used unless the endpoint downloads a file
|
||||||
- Async jobs share the same status model: `queued`, `running`, `success`, `failed`, `canceled`
|
- Async jobs share the same status model: `queued`, `running`, `success`, `failed`, `canceled`
|
||||||
- Export filenames use `YYYY-MM-DD (MODEL) - SERIAL.<ext>` when board metadata exists
|
- Export filenames use `YYYY-MM-DD (MODEL) - SERIAL.<ext>` when board metadata exists
|
||||||
|
- Embedded chart viewer routes live under `/chart/` and return HTML/CSS, not JSON
|
||||||
|
|
||||||
## Input endpoints
|
## Input endpoints
|
||||||
|
|
||||||
@@ -158,6 +159,17 @@ Returns registered parser metadata.
|
|||||||
|
|
||||||
Returns supported file extensions for upload and batch convert.
|
Returns supported file extensions for upload and batch convert.
|
||||||
|
|
||||||
|
## Viewer endpoints
|
||||||
|
|
||||||
|
### `GET /chart/current`
|
||||||
|
|
||||||
|
Renders the current in-memory dataset as Reanimator HTML using embedded `reanimator/chart`.
|
||||||
|
The server first converts the current result to Reanimator JSON, then passes that snapshot to the viewer.
|
||||||
|
|
||||||
|
### `GET /chart/static/...`
|
||||||
|
|
||||||
|
Serves embedded `reanimator/chart` static assets.
|
||||||
|
|
||||||
## Export endpoints
|
## Export endpoints
|
||||||
|
|
||||||
### `GET /api/export/csv`
|
### `GET /api/export/csv`
|
||||||
|
|||||||
@@ -581,3 +581,26 @@ matter for LOGPile documentation: ingest-side serial fallback rules, canonical P
|
|||||||
coordinate.
|
coordinate.
|
||||||
- LOGPile event export remains strictly source-derived; internal warnings such as LOGPile analysis
|
- LOGPile event export remains strictly source-derived; internal warnings such as LOGPile analysis
|
||||||
notes do not leak into Reanimator `event_logs`.
|
notes do not leak into Reanimator `event_logs`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ADL-030 — Audit result rendering is delegated to embedded reanimator/chart
|
||||||
|
|
||||||
|
**Date:** 2026-03-16
|
||||||
|
**Context:**
|
||||||
|
LOGPile already owns file upload, Redfish collection, archive parsing, normalization, and
|
||||||
|
Reanimator export. Maintaining a second host-side audit renderer for the same data created
|
||||||
|
presentation drift and duplicated UI logic.
|
||||||
|
|
||||||
|
**Decision:**
|
||||||
|
- Use vendored `reanimator/chart` as the only audit result viewer.
|
||||||
|
- Keep LOGPile responsible for service flows: upload, live collection, batch convert, raw export,
|
||||||
|
Reanimator export, and parse-error reporting.
|
||||||
|
- Render the current dataset by converting it to Reanimator JSON and passing that snapshot to
|
||||||
|
embedded `chart` under `/chart/current`.
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- Reanimator JSON becomes the single presentation contract for the audit surface.
|
||||||
|
- The host UI becomes a service shell around the viewer instead of maintaining its own
|
||||||
|
field-by-field tabs.
|
||||||
|
- `internal/chart` must be updated explicitly as a git submodule when the viewer changes.
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -1,3 +1,7 @@
|
|||||||
module git.mchus.pro/mchus/logpile
|
module git.mchus.pro/mchus/logpile
|
||||||
|
|
||||||
go 1.22
|
go 1.24.0
|
||||||
|
|
||||||
|
require reanimator/chart v0.0.0
|
||||||
|
|
||||||
|
replace reanimator/chart => ./internal/chart
|
||||||
|
|||||||
1
internal/chart
Submodule
1
internal/chart
Submodule
Submodule internal/chart added at a71f55a6f9
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/exporter"
|
||||||
"git.mchus.pro/mchus/logpile/internal/models"
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||||
|
chartviewer "reanimator/chart/viewer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
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)
|
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) {
|
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, uploadMultipartMaxBytes())
|
r.Body = http.MaxBytesReader(w, r.Body, uploadMultipartMaxBytes())
|
||||||
if err := r.ParseMultipartForm(uploadMultipartFormMemoryBytes()); err != nil {
|
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/collector"
|
||||||
"git.mchus.pro/mchus/logpile/internal/models"
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
chartviewer "reanimator/chart/viewer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebFS holds embedded web files (set by main package)
|
// WebFS holds embedded web files (set by main package)
|
||||||
@@ -64,9 +65,13 @@ func (s *Server) setupRoutes() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticContent))))
|
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
|
// Pages
|
||||||
s.mux.HandleFunc("/", s.handleIndex)
|
s.mux.HandleFunc("/", s.handleIndex)
|
||||||
|
s.mux.HandleFunc("GET /chart/current", s.handleChartCurrent)
|
||||||
|
|
||||||
// API endpoints
|
// API endpoints
|
||||||
s.mux.HandleFunc("POST /api/upload", s.handleUpload)
|
s.mux.HandleFunc("POST /api/upload", s.handleUpload)
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ header {
|
|||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header-brand {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
header h1 {
|
header h1 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -33,9 +45,34 @@ header p {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-log-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.55rem 0.9rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
max-width: 1400px;
|
width: 100%;
|
||||||
margin: 2rem auto;
|
max-width: none;
|
||||||
|
margin: 1rem 0 2rem;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +146,10 @@ main {
|
|||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#data-section {
|
||||||
|
margin: 0 -1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.api-form-grid {
|
.api-form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
@@ -437,18 +478,6 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* File Info */
|
/* File Info */
|
||||||
.file-info {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 2rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parser-badge, .file-name {
|
.parser-badge, .file-name {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -476,6 +505,28 @@ main {
|
|||||||
border-color: #81c784;
|
border-color: #81c784;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-panel {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-viewer-shell {
|
||||||
|
min-height: 60vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-viewer-frame {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 60vh;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #fff;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/* Tabs */
|
/* Tabs */
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
initApiSource();
|
initApiSource();
|
||||||
initUpload();
|
initUpload();
|
||||||
initConvertMode();
|
initConvertMode();
|
||||||
initTabs();
|
initAuditViewer();
|
||||||
initFilters();
|
|
||||||
loadParsersInfo();
|
loadParsersInfo();
|
||||||
loadSupportedFileTypes();
|
loadSupportedFileTypes();
|
||||||
});
|
});
|
||||||
@@ -27,6 +26,30 @@ let isAutoUpdatingApiPort = false;
|
|||||||
let apiProbeResult = null;
|
let apiProbeResult = null;
|
||||||
let apiPowerDecisionTimer = null;
|
let apiPowerDecisionTimer = null;
|
||||||
|
|
||||||
|
function initAuditViewer() {
|
||||||
|
const frame = document.getElementById('audit-viewer-frame');
|
||||||
|
if (!frame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.addEventListener('load', () => {
|
||||||
|
resizeAuditViewerFrame();
|
||||||
|
try {
|
||||||
|
const win = frame.contentWindow;
|
||||||
|
if (win) {
|
||||||
|
win.setTimeout(resizeAuditViewerFrame, 50);
|
||||||
|
win.setTimeout(resizeAuditViewerFrame, 250);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to schedule viewer resize:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
resizeAuditViewerFrame();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initSourceType() {
|
function initSourceType() {
|
||||||
const sourceButtons = document.querySelectorAll('.source-switch-btn');
|
const sourceButtons = document.querySelectorAll('.source-switch-btn');
|
||||||
sourceButtons.forEach(button => {
|
sourceButtons.forEach(button => {
|
||||||
@@ -1191,6 +1214,7 @@ let allSerials = [];
|
|||||||
let allParseErrors = [];
|
let allParseErrors = [];
|
||||||
|
|
||||||
let currentVendor = '';
|
let currentVendor = '';
|
||||||
|
let auditViewerNonce = 0;
|
||||||
|
|
||||||
// Load data from API
|
// Load data from API
|
||||||
async function loadData(vendor, filename) {
|
async function loadData(vendor, filename) {
|
||||||
@@ -1198,16 +1222,9 @@ async function loadData(vendor, filename) {
|
|||||||
document.getElementById('upload-section').classList.add('hidden');
|
document.getElementById('upload-section').classList.add('hidden');
|
||||||
document.getElementById('data-section').classList.remove('hidden');
|
document.getElementById('data-section').classList.remove('hidden');
|
||||||
document.getElementById('clear-btn').classList.remove('hidden');
|
document.getElementById('clear-btn').classList.remove('hidden');
|
||||||
|
document.getElementById('header-raw-btn').classList.remove('hidden');
|
||||||
// Update parser name and filename
|
document.getElementById('header-reanimator-btn').classList.remove('hidden');
|
||||||
const parserName = document.getElementById('parser-name');
|
document.getElementById('header-log-meta').classList.remove('hidden');
|
||||||
const fileNameElem = document.getElementById('file-name');
|
|
||||||
if (parserName && currentVendor) {
|
|
||||||
parserName.textContent = currentVendor;
|
|
||||||
}
|
|
||||||
if (fileNameElem && filename) {
|
|
||||||
fileNameElem.textContent = filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update vendor badge if exists (legacy support)
|
// Update vendor badge if exists (legacy support)
|
||||||
const vendorBadge = document.getElementById('vendor-badge');
|
const vendorBadge = document.getElementById('vendor-badge');
|
||||||
@@ -1216,14 +1233,40 @@ async function loadData(vendor, filename) {
|
|||||||
vendorBadge.classList.remove('hidden');
|
vendorBadge.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([
|
loadAuditViewer();
|
||||||
loadConfig(),
|
}
|
||||||
loadFirmware(),
|
|
||||||
loadSensors(),
|
function loadAuditViewer() {
|
||||||
loadSerials(),
|
const frame = document.getElementById('audit-viewer-frame');
|
||||||
loadEvents(),
|
if (!frame) {
|
||||||
loadParseErrors()
|
return;
|
||||||
]);
|
}
|
||||||
|
auditViewerNonce += 1;
|
||||||
|
frame.style.height = '60vh';
|
||||||
|
frame.src = `/chart/current?ts=${auditViewerNonce}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeAuditViewerFrame() {
|
||||||
|
const frame = document.getElementById('audit-viewer-frame');
|
||||||
|
if (!frame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const doc = frame.contentDocument || (frame.contentWindow && frame.contentWindow.document);
|
||||||
|
if (!doc || !doc.documentElement || !doc.body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextHeight = Math.max(
|
||||||
|
doc.documentElement.scrollHeight,
|
||||||
|
doc.body.scrollHeight,
|
||||||
|
640
|
||||||
|
);
|
||||||
|
frame.style.height = `${nextHeight}px`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to resize audit viewer frame:', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
@@ -1936,11 +1979,19 @@ async function clearData() {
|
|||||||
document.getElementById('upload-section').classList.remove('hidden');
|
document.getElementById('upload-section').classList.remove('hidden');
|
||||||
document.getElementById('data-section').classList.add('hidden');
|
document.getElementById('data-section').classList.add('hidden');
|
||||||
document.getElementById('clear-btn').classList.add('hidden');
|
document.getElementById('clear-btn').classList.add('hidden');
|
||||||
|
document.getElementById('header-raw-btn').classList.add('hidden');
|
||||||
|
document.getElementById('header-reanimator-btn').classList.add('hidden');
|
||||||
|
document.getElementById('header-log-meta').classList.add('hidden');
|
||||||
document.getElementById('upload-status').textContent = '';
|
document.getElementById('upload-status').textContent = '';
|
||||||
allSensors = [];
|
allSensors = [];
|
||||||
allEvents = [];
|
allEvents = [];
|
||||||
allSerials = [];
|
allSerials = [];
|
||||||
allParseErrors = [];
|
allParseErrors = [];
|
||||||
|
currentVendor = '';
|
||||||
|
const frame = document.getElementById('audit-viewer-frame');
|
||||||
|
if (frame) {
|
||||||
|
frame.src = 'about:blank';
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to clear data:', err);
|
console.error('Failed to clear data:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,21 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>LOGPile <span class="header-domain">mchus.pro</span></h1>
|
<div class="app-header-row">
|
||||||
<p>Анализатор диагностических данных BMC/IPMI</p>
|
<div class="app-header-brand">
|
||||||
|
<h1>LOGPile <span class="header-domain">mchus.pro</span></h1>
|
||||||
|
<p>Анализатор диагностических данных BMC/IPMI</p>
|
||||||
|
</div>
|
||||||
|
<div id="header-log-meta" class="header-log-meta hidden">
|
||||||
|
<div class="header-actions">
|
||||||
|
<button id="clear-btn" class="hidden" onclick="clearData()">Очистить данные</button>
|
||||||
|
<button id="header-raw-btn" class="hidden" onclick="exportData('json')">Export Raw Data</button>
|
||||||
|
<button id="header-reanimator-btn" class="hidden" onclick="exportData('reanimator')">Экспорт Reanimator</button>
|
||||||
|
<button id="restart-btn" onclick="restartApp()">Перезапуск</button>
|
||||||
|
<button id="exit-btn" onclick="exitApp()">Выход</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@@ -124,142 +137,23 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="data-section" class="hidden">
|
<section id="data-section" class="hidden">
|
||||||
<div class="file-info">
|
<section class="result-panel">
|
||||||
<div class="parser-badge">
|
<div class="audit-viewer-shell">
|
||||||
<span class="badge-label">Парсер:</span>
|
<iframe
|
||||||
<span id="parser-name" class="badge-value"></span>
|
id="audit-viewer-frame"
|
||||||
|
class="audit-viewer-frame"
|
||||||
|
title="Reanimator chart viewer"
|
||||||
|
loading="eager"
|
||||||
|
scrolling="no"
|
||||||
|
referrerpolicy="same-origin">
|
||||||
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-name">
|
</section>
|
||||||
<span class="badge-label">Файл:</span>
|
|
||||||
<span id="file-name" class="badge-value"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav class="tabs">
|
|
||||||
<button class="tab active" data-tab="config">Конфигурация</button>
|
|
||||||
<button class="tab" data-tab="firmware">Прошивки</button>
|
|
||||||
<button class="tab" data-tab="sensors">Сенсоры</button>
|
|
||||||
<button class="tab" data-tab="serials">Серийные номера</button>
|
|
||||||
<button class="tab" data-tab="events">События</button>
|
|
||||||
<button class="tab" data-tab="parse-errors">Ошибки разбора</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="tab-content active" id="config">
|
|
||||||
<div class="toolbar">
|
|
||||||
<button onclick="exportData('json')">Export Raw Data</button>
|
|
||||||
<button onclick="exportData('reanimator')">Экспорт Reanimator</button>
|
|
||||||
</div>
|
|
||||||
<div id="config-content"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content" id="firmware">
|
|
||||||
<div class="toolbar">
|
|
||||||
<span class="toolbar-label">Версии прошивок компонентов</span>
|
|
||||||
</div>
|
|
||||||
<table id="firmware-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Компонент</th>
|
|
||||||
<th>Модель</th>
|
|
||||||
<th>Версия</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content" id="sensors">
|
|
||||||
<div class="toolbar">
|
|
||||||
<select id="sensor-filter">
|
|
||||||
<option value="">Все сенсоры</option>
|
|
||||||
<option value="temperature">Температура</option>
|
|
||||||
<option value="voltage">Напряжение</option>
|
|
||||||
<option value="power">Мощность</option>
|
|
||||||
<option value="fan_speed">Вентиляторы</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div id="sensors-content"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content" id="serials">
|
|
||||||
<div class="toolbar">
|
|
||||||
<select id="serial-filter">
|
|
||||||
<option value="">Все компоненты</option>
|
|
||||||
<option value="Board">Материнская плата</option>
|
|
||||||
<option value="CPU">Процессоры</option>
|
|
||||||
<option value="Memory">Память</option>
|
|
||||||
<option value="Storage">Накопители</option>
|
|
||||||
<option value="PCIe">PCIe устройства</option>
|
|
||||||
<option value="Network">Сетевые адаптеры</option>
|
|
||||||
<option value="PSU">Блоки питания</option>
|
|
||||||
<option value="Firmware">Прошивки</option>
|
|
||||||
<option value="FRU">FRU</option>
|
|
||||||
</select>
|
|
||||||
<button onclick="exportData('csv')">Экспорт CSV</button>
|
|
||||||
</div>
|
|
||||||
<table id="serials-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Категория</th>
|
|
||||||
<th>Компонент</th>
|
|
||||||
<th>Расположение</th>
|
|
||||||
<th>Серийный номер</th>
|
|
||||||
<th>Производитель</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content" id="events">
|
|
||||||
<div class="toolbar">
|
|
||||||
<select id="severity-filter">
|
|
||||||
<option value="">Все события</option>
|
|
||||||
<option value="critical">Критические</option>
|
|
||||||
<option value="warning">Предупреждения</option>
|
|
||||||
<option value="info">Информационные</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<table id="events-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Время</th>
|
|
||||||
<th>Источник</th>
|
|
||||||
<th>Описание</th>
|
|
||||||
<th>Важность</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content" id="parse-errors">
|
|
||||||
<div class="toolbar">
|
|
||||||
<span class="toolbar-label">Ошибки сборки / разбора (Redfish, parser, файл)</span>
|
|
||||||
</div>
|
|
||||||
<div class="table-scroll">
|
|
||||||
<table id="parse-errors-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Источник</th>
|
|
||||||
<th>Категория</th>
|
|
||||||
<th>Важность</th>
|
|
||||||
<th>Endpoint / Path</th>
|
|
||||||
<th>Сообщение</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="footer-buttons">
|
<div class="footer-buttons">
|
||||||
<button id="clear-btn" class="hidden" onclick="clearData()">Очистить данные</button>
|
|
||||||
<button id="restart-btn" onclick="restartApp()">Перезапуск</button>
|
|
||||||
<button id="exit-btn" onclick="exitApp()">Выход</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-info">
|
<div class="footer-info">
|
||||||
<p>Автор: <a href="https://mchus.pro" target="_blank">mchus.pro</a> | <a href="https://git.mchus.pro/mchus/logpile" target="_blank">Git Repository</a></p>
|
<p>Автор: <a href="https://mchus.pro" target="_blank">mchus.pro</a> | <a href="https://git.mchus.pro/mchus/logpile" target="_blank">Git Repository</a></p>
|
||||||
|
|||||||
Reference in New Issue
Block a user