v1.1.0: Parser versioning, server info, auto-browser, section overviews
- Add parser versioning with Version() method and version display on main screen - Add server model and serial number to Configuration tab and TXT export - Add auto-browser opening on startup with --no-browser flag - Add Restart and Exit buttons with graceful shutdown - Add section overview stats (CPU, Power, Storage, GPU, Network) - Change PCIe Link display to "x16 PCIe Gen4" format - Add Location column to Serials section - Extract BoardInfo from FRU and PlatformId from ThermalConfig Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||||
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors" // Register all vendor parsers
|
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors" // Register all vendor parsers
|
||||||
@@ -21,6 +24,7 @@ func main() {
|
|||||||
port := flag.Int("port", 8080, "HTTP server port")
|
port := flag.Int("port", 8080, "HTTP server port")
|
||||||
file := flag.String("file", "", "Pre-load archive file")
|
file := flag.String("file", "", "Pre-load archive file")
|
||||||
showVersion := flag.Bool("version", false, "Show version")
|
showVersion := flag.Bool("version", false, "Show version")
|
||||||
|
noBrowser := flag.Bool("no-browser", false, "Don't open browser automatically")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
@@ -38,9 +42,37 @@ func main() {
|
|||||||
|
|
||||||
srv := server.New(cfg)
|
srv := server.New(cfg)
|
||||||
|
|
||||||
log.Printf("LOGPile starting on http://localhost:%d", *port)
|
url := fmt.Sprintf("http://localhost:%d", *port)
|
||||||
|
log.Printf("LOGPile starting on %s", url)
|
||||||
log.Printf("Registered parsers: %v", parser.ListParsers())
|
log.Printf("Registered parsers: %v", parser.ListParsers())
|
||||||
|
|
||||||
|
// Open browser automatically
|
||||||
|
if !*noBrowser {
|
||||||
|
go func() {
|
||||||
|
time.Sleep(500 * time.Millisecond) // Wait for server to start
|
||||||
|
openBrowser(url)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
if err := srv.Run(); err != nil {
|
if err := srv.Run(); err != nil {
|
||||||
log.Fatalf("Server error: %v", err)
|
log.Fatalf("Server error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// openBrowser opens the default browser with the given URL
|
||||||
|
func openBrowser(url string) {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "darwin":
|
||||||
|
cmd = exec.Command("open", url)
|
||||||
|
case "windows":
|
||||||
|
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
|
||||||
|
default: // linux and others
|
||||||
|
cmd = exec.Command("xdg-open", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Printf("Failed to open browser: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (e *Exporter) ExportCSV(w io.Writer) error {
|
|||||||
defer writer.Flush()
|
defer writer.Flush()
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
if err := writer.Write([]string{"Component", "Serial Number", "Manufacturer", "Part Number"}); err != nil {
|
if err := writer.Write([]string{"Component", "Serial Number", "Manufacturer", "Location"}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +59,15 @@ func (e *Exporter) ExportCSV(w io.Writer) error {
|
|||||||
if mem.SerialNumber == "" {
|
if mem.SerialNumber == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
location := mem.Location
|
||||||
|
if location == "" {
|
||||||
|
location = mem.Slot
|
||||||
|
}
|
||||||
if err := writer.Write([]string{
|
if err := writer.Write([]string{
|
||||||
fmt.Sprintf("DIMM Slot %d (%s)", mem.Slot, mem.PartNumber),
|
mem.PartNumber,
|
||||||
mem.SerialNumber,
|
mem.SerialNumber,
|
||||||
mem.Manufacturer,
|
mem.Manufacturer,
|
||||||
mem.PartNumber,
|
location,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -75,10 +79,10 @@ func (e *Exporter) ExportCSV(w io.Writer) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := writer.Write([]string{
|
if err := writer.Write([]string{
|
||||||
fmt.Sprintf("%s %s", stor.Type, stor.Model),
|
stor.Model,
|
||||||
stor.SerialNumber,
|
stor.SerialNumber,
|
||||||
"",
|
stor.Manufacturer,
|
||||||
"",
|
stor.Slot,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -90,10 +94,10 @@ func (e *Exporter) ExportCSV(w io.Writer) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := writer.Write([]string{
|
if err := writer.Write([]string{
|
||||||
fmt.Sprintf("%s (%s)", pcie.DeviceClass, pcie.Slot),
|
pcie.DeviceClass,
|
||||||
pcie.SerialNumber,
|
pcie.SerialNumber,
|
||||||
"",
|
pcie.Manufacturer,
|
||||||
pcie.PartNumber,
|
pcie.Slot,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -121,7 +125,15 @@ func (e *Exporter) ExportTXT(w io.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(w, "File: %s\n\n", e.result.Filename)
|
fmt.Fprintf(w, "File: %s\n", e.result.Filename)
|
||||||
|
|
||||||
|
// Server model and serial number
|
||||||
|
if e.result.Hardware != nil && e.result.Hardware.BoardInfo.ProductName != "" {
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
fmt.Fprintf(w, "Server Model: %s\n", e.result.Hardware.BoardInfo.ProductName)
|
||||||
|
fmt.Fprintf(w, "Serial Number: %s\n", e.result.Hardware.BoardInfo.SerialNumber)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
|
||||||
// Hardware summary
|
// Hardware summary
|
||||||
if e.result.Hardware != nil {
|
if e.result.Hardware != nil {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ type VendorParser interface {
|
|||||||
// Vendor returns vendor identifier (e.g., "inspur", "supermicro", "dell")
|
// Vendor returns vendor identifier (e.g., "inspur", "supermicro", "dell")
|
||||||
Vendor() string
|
Vendor() string
|
||||||
|
|
||||||
|
// Version returns parser version string
|
||||||
|
// IMPORTANT: Increment version when modifying parser logic!
|
||||||
|
Version() string
|
||||||
|
|
||||||
// Detect checks if this parser can handle the given files
|
// Detect checks if this parser can handle the given files
|
||||||
// Returns confidence score 0-100 (0 = cannot parse, 100 = definitely this format)
|
// Returns confidence score 0-100 (0 = cannot parse, 100 = definitely this format)
|
||||||
Detect(files []ExtractedFile) int
|
Detect(files []ExtractedFile) int
|
||||||
|
|||||||
@@ -46,6 +46,35 @@ func ListParsers() []string {
|
|||||||
return vendors
|
return vendors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParserInfo contains information about a registered parser
|
||||||
|
type ParserInfo struct {
|
||||||
|
Vendor string `json:"vendor"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListParsersInfo returns detailed info about all registered parsers
|
||||||
|
func ListParsersInfo() []ParserInfo {
|
||||||
|
registryLock.RLock()
|
||||||
|
defer registryLock.RUnlock()
|
||||||
|
|
||||||
|
parsers := make([]ParserInfo, 0, len(registry))
|
||||||
|
for _, p := range registry {
|
||||||
|
parsers = append(parsers, ParserInfo{
|
||||||
|
Vendor: p.Vendor(),
|
||||||
|
Name: p.Name(),
|
||||||
|
Version: p.Version(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by vendor name
|
||||||
|
sort.Slice(parsers, func(i, j int) bool {
|
||||||
|
return parsers[i].Vendor < parsers[j].Vendor
|
||||||
|
})
|
||||||
|
|
||||||
|
return parsers
|
||||||
|
}
|
||||||
|
|
||||||
// DetectResult holds detection result for a parser
|
// DetectResult holds detection result for a parser
|
||||||
type DetectResult struct {
|
type DetectResult struct {
|
||||||
Parser VendorParser
|
Parser VendorParser
|
||||||
|
|||||||
68
internal/parser/vendors/inspur/fru.go
vendored
68
internal/parser/vendors/inspur/fru.go
vendored
@@ -9,8 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fruDeviceRegex = regexp.MustCompile(`^FRU Device Description\s*:\s*(.+)$`)
|
fruDeviceRegex = regexp.MustCompile(`^FRU Device Description\s*:\s*(.+)$`)
|
||||||
fruFieldRegex = regexp.MustCompile(`^\s+(.+?)\s*:\s*(.*)$`)
|
fruFieldRegex = regexp.MustCompile(`^\s+(.+?)\s*:\s*(.*)$`)
|
||||||
|
platformIdRegex = regexp.MustCompile(`(?i)PlatformId\s*=\s*(\S+)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseFRU parses BMC FRU (Field Replaceable Unit) output
|
// ParseFRU parses BMC FRU (Field Replaceable Unit) output
|
||||||
@@ -95,3 +96,66 @@ func ParseFRU(content []byte) []models.FRUInfo {
|
|||||||
|
|
||||||
return fruList
|
return fruList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractBoardInfo extracts main board/chassis information from FRU data
|
||||||
|
func extractBoardInfo(fruList []models.FRUInfo, hw *models.HardwareConfig) {
|
||||||
|
if hw == nil || len(fruList) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the main board/chassis FRU entry
|
||||||
|
// Usually it's the first entry or one with "Builtin FRU" or containing board info
|
||||||
|
for _, fru := range fruList {
|
||||||
|
// Skip empty entries
|
||||||
|
if fru.ProductName == "" && fru.SerialNumber == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritize entries that look like main board info
|
||||||
|
desc := strings.ToLower(fru.Description)
|
||||||
|
isMainBoard := strings.Contains(desc, "builtin") ||
|
||||||
|
strings.Contains(desc, "fru device") ||
|
||||||
|
strings.Contains(desc, "chassis") ||
|
||||||
|
strings.Contains(desc, "board")
|
||||||
|
|
||||||
|
// If we haven't set board info yet, or this is a main board entry
|
||||||
|
if hw.BoardInfo.ProductName == "" || isMainBoard {
|
||||||
|
if fru.ProductName != "" {
|
||||||
|
hw.BoardInfo.ProductName = fru.ProductName
|
||||||
|
}
|
||||||
|
if fru.SerialNumber != "" {
|
||||||
|
hw.BoardInfo.SerialNumber = fru.SerialNumber
|
||||||
|
}
|
||||||
|
if fru.Manufacturer != "" {
|
||||||
|
hw.BoardInfo.Manufacturer = fru.Manufacturer
|
||||||
|
}
|
||||||
|
if fru.PartNumber != "" {
|
||||||
|
hw.BoardInfo.PartNumber = fru.PartNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a main board entry, stop searching
|
||||||
|
if isMainBoard && fru.ProductName != "" && fru.SerialNumber != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPlatformId extracts server model from ThermalConfig (PlatformId)
|
||||||
|
func extractPlatformId(content []byte, hw *models.HardwareConfig) {
|
||||||
|
if hw == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if match := platformIdRegex.FindSubmatch(content); match != nil {
|
||||||
|
platformId := strings.TrimSpace(string(match[1]))
|
||||||
|
if platformId != "" {
|
||||||
|
// Set as ProductName (server model) - this takes priority over FRU data
|
||||||
|
hw.BoardInfo.ProductName = platformId
|
||||||
|
// Also set manufacturer as Inspur if not already set
|
||||||
|
if hw.BoardInfo.Manufacturer == "" {
|
||||||
|
hw.BoardInfo.Manufacturer = "Inspur"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
29
internal/parser/vendors/inspur/parser.go
vendored
29
internal/parser/vendors/inspur/parser.go
vendored
@@ -1,5 +1,8 @@
|
|||||||
// Package inspur provides parser for Inspur/Kaytus BMC diagnostic archives
|
// Package inspur provides parser for Inspur/Kaytus BMC diagnostic archives
|
||||||
// Tested with: Kaytus KR4268X2 (onekeylog format)
|
// Tested with: Inspur NF5468M7 / Kaytus KR4268X2 (onekeylog format)
|
||||||
|
//
|
||||||
|
// IMPORTANT: Increment parserVersion when modifying parser logic!
|
||||||
|
// This helps track which version was used to parse specific logs.
|
||||||
package inspur
|
package inspur
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -9,6 +12,10 @@ import (
|
|||||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// parserVersion - version of this parser module
|
||||||
|
// IMPORTANT: Increment this version when making changes to parser logic!
|
||||||
|
const parserVersion = "1.0.0"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
parser.Register(&Parser{})
|
parser.Register(&Parser{})
|
||||||
}
|
}
|
||||||
@@ -26,6 +33,12 @@ func (p *Parser) Vendor() string {
|
|||||||
return "inspur"
|
return "inspur"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Version returns parser version
|
||||||
|
// IMPORTANT: Update parserVersion constant when modifying parser logic!
|
||||||
|
func (p *Parser) Version() string {
|
||||||
|
return parserVersion
|
||||||
|
}
|
||||||
|
|
||||||
// Detect checks if archive matches Inspur/Kaytus format
|
// Detect checks if archive matches Inspur/Kaytus format
|
||||||
// Returns confidence 0-100
|
// Returns confidence 0-100
|
||||||
func (p *Parser) Detect(files []parser.ExtractedFile) int {
|
func (p *Parser) Detect(files []parser.ExtractedFile) int {
|
||||||
@@ -90,11 +103,19 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract BoardInfo from FRU data
|
||||||
|
if result.Hardware == nil {
|
||||||
|
result.Hardware = &models.HardwareConfig{}
|
||||||
|
}
|
||||||
|
extractBoardInfo(result.FRU, result.Hardware)
|
||||||
|
|
||||||
|
// Extract PlatformId (server model) from ThermalConfig
|
||||||
|
if f := parser.FindFileByName(files, "ThermalConfig_Cur.conf"); f != nil {
|
||||||
|
extractPlatformId(f.Content, result.Hardware)
|
||||||
|
}
|
||||||
|
|
||||||
// Parse component.log for additional data (PSU, etc.)
|
// Parse component.log for additional data (PSU, etc.)
|
||||||
if f := parser.FindFileByName(files, "component.log"); f != nil {
|
if f := parser.FindFileByName(files, "component.log"); f != nil {
|
||||||
if result.Hardware == nil {
|
|
||||||
result.Hardware = &models.HardwareConfig{}
|
|
||||||
}
|
|
||||||
ParseComponentLog(f.Content, result.Hardware)
|
ParseComponentLog(f.Content, result.Hardware)
|
||||||
|
|
||||||
// Extract events from component.log (memory errors, etc.)
|
// Extract events from component.log (memory errors, etc.)
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
@@ -74,7 +76,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) handleGetParsers(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetParsers(w http.ResponseWriter, r *http.Request) {
|
||||||
jsonResponse(w, map[string]interface{}{
|
jsonResponse(w, map[string]interface{}{
|
||||||
"parsers": parser.ListParsers(),
|
"parsers": parser.ListParsersInfo(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +234,7 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Collect all serial numbers from various sources
|
// Collect all serial numbers from various sources
|
||||||
type SerialEntry struct {
|
type SerialEntry struct {
|
||||||
Component string `json:"component"`
|
Component string `json:"component"`
|
||||||
|
Location string `json:"location,omitempty"`
|
||||||
SerialNumber string `json:"serial_number"`
|
SerialNumber string `json:"serial_number"`
|
||||||
Manufacturer string `json:"manufacturer,omitempty"`
|
Manufacturer string `json:"manufacturer,omitempty"`
|
||||||
PartNumber string `json:"part_number,omitempty"`
|
PartNumber string `json:"part_number,omitempty"`
|
||||||
@@ -282,6 +285,7 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
serials = append(serials, SerialEntry{
|
serials = append(serials, SerialEntry{
|
||||||
Component: cpu.Model,
|
Component: cpu.Model,
|
||||||
|
Location: fmt.Sprintf("CPU%d", cpu.Socket),
|
||||||
SerialNumber: sn,
|
SerialNumber: sn,
|
||||||
Category: "CPU",
|
Category: "CPU",
|
||||||
})
|
})
|
||||||
@@ -292,8 +296,13 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
if mem.SerialNumber == "" {
|
if mem.SerialNumber == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
location := mem.Location
|
||||||
|
if location == "" {
|
||||||
|
location = mem.Slot
|
||||||
|
}
|
||||||
serials = append(serials, SerialEntry{
|
serials = append(serials, SerialEntry{
|
||||||
Component: mem.PartNumber,
|
Component: mem.PartNumber,
|
||||||
|
Location: location,
|
||||||
SerialNumber: mem.SerialNumber,
|
SerialNumber: mem.SerialNumber,
|
||||||
Manufacturer: mem.Manufacturer,
|
Manufacturer: mem.Manufacturer,
|
||||||
PartNumber: mem.PartNumber,
|
PartNumber: mem.PartNumber,
|
||||||
@@ -308,9 +317,9 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
serials = append(serials, SerialEntry{
|
serials = append(serials, SerialEntry{
|
||||||
Component: stor.Model,
|
Component: stor.Model,
|
||||||
|
Location: stor.Slot,
|
||||||
SerialNumber: stor.SerialNumber,
|
SerialNumber: stor.SerialNumber,
|
||||||
Manufacturer: stor.Manufacturer,
|
Manufacturer: stor.Manufacturer,
|
||||||
PartNumber: stor.Slot,
|
|
||||||
Category: "Storage",
|
Category: "Storage",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -321,7 +330,8 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
serials = append(serials, SerialEntry{
|
serials = append(serials, SerialEntry{
|
||||||
Component: pcie.DeviceClass + " (" + pcie.Slot + ")",
|
Component: pcie.DeviceClass,
|
||||||
|
Location: pcie.Slot,
|
||||||
SerialNumber: pcie.SerialNumber,
|
SerialNumber: pcie.SerialNumber,
|
||||||
Manufacturer: pcie.Manufacturer,
|
Manufacturer: pcie.Manufacturer,
|
||||||
PartNumber: pcie.PartNumber,
|
PartNumber: pcie.PartNumber,
|
||||||
@@ -348,8 +358,9 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
serials = append(serials, SerialEntry{
|
serials = append(serials, SerialEntry{
|
||||||
Component: psu.Model,
|
Component: psu.Model,
|
||||||
|
Location: psu.Slot,
|
||||||
SerialNumber: psu.SerialNumber,
|
SerialNumber: psu.SerialNumber,
|
||||||
PartNumber: psu.Slot,
|
Manufacturer: psu.Vendor,
|
||||||
Category: "PSU",
|
Category: "PSU",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -510,6 +521,20 @@ func (s *Server) handleClear(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleShutdown(w http.ResponseWriter, r *http.Request) {
|
||||||
|
jsonResponse(w, map[string]string{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "Server shutting down",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Shutdown in a goroutine so the response can be sent
|
||||||
|
go func() {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
s.Shutdown()
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func jsonResponse(w http.ResponseWriter, data interface{}) {
|
func jsonResponse(w http.ResponseWriter, data interface{}) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(data)
|
json.NewEncoder(w).Encode(data)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.mchus.pro/mchus/logpile/internal/models"
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
)
|
)
|
||||||
@@ -19,8 +21,9 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
config Config
|
config Config
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
|
httpServer *http.Server
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
result *models.AnalysisResult
|
result *models.AnalysisResult
|
||||||
@@ -60,11 +63,25 @@ func (s *Server) setupRoutes() {
|
|||||||
s.mux.HandleFunc("GET /api/export/json", s.handleExportJSON)
|
s.mux.HandleFunc("GET /api/export/json", s.handleExportJSON)
|
||||||
s.mux.HandleFunc("GET /api/export/txt", s.handleExportTXT)
|
s.mux.HandleFunc("GET /api/export/txt", s.handleExportTXT)
|
||||||
s.mux.HandleFunc("DELETE /api/clear", s.handleClear)
|
s.mux.HandleFunc("DELETE /api/clear", s.handleClear)
|
||||||
|
s.mux.HandleFunc("POST /api/shutdown", s.handleShutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Run() error {
|
func (s *Server) Run() error {
|
||||||
addr := fmt.Sprintf(":%d", s.config.Port)
|
addr := fmt.Sprintf(":%d", s.config.Port)
|
||||||
return http.ListenAndServe(addr, s.mux)
|
s.httpServer = &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: s.mux,
|
||||||
|
}
|
||||||
|
return s.httpServer.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown gracefully shuts down the server
|
||||||
|
func (s *Server) Shutdown() {
|
||||||
|
if s.httpServer != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
s.httpServer.Shutdown(ctx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetResult sets the analysis result (thread-safe)
|
// SetResult sets the analysis result (thread-safe)
|
||||||
|
|||||||
@@ -88,6 +88,48 @@ main {
|
|||||||
color: #27ae60;
|
color: #27ae60;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Parsers info */
|
||||||
|
.parsers-info {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parsers-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parsers-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parser-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parser-name {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parser-version {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
background: #e8e8e8;
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Tabs */
|
/* Tabs */
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -375,6 +417,13 @@ footer {
|
|||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.footer-info {
|
.footer-info {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
@@ -403,6 +452,32 @@ footer {
|
|||||||
background: #c0392b;
|
background: #c0392b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#restart-btn {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#restart-btn:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#exit-btn {
|
||||||
|
background: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#exit-btn:hover {
|
||||||
|
background: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
/* Utility */
|
/* Utility */
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -414,6 +489,40 @@ footer {
|
|||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Server info header */
|
||||||
|
.server-info {
|
||||||
|
background: #2c3e50;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-info-label {
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-info strong {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-info code {
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Configuration tabs */
|
/* Configuration tabs */
|
||||||
.config-tabs {
|
.config-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -510,30 +619,46 @@ footer {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Memory overview stats */
|
/* Section overview stats */
|
||||||
.memory-overview {
|
.memory-overview,
|
||||||
|
.section-overview {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-box {
|
.stat-box {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
padding: 1rem 1.5rem;
|
padding: 0.75rem 1.25rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-left: 4px solid #3498db;
|
border-left: 4px solid #3498db;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box.model-box {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: left;
|
||||||
|
border-left-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box.model-box .stat-value {
|
||||||
|
font-size: 1rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 1.5rem;
|
font-size: 1.25rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
initUpload();
|
initUpload();
|
||||||
initTabs();
|
initTabs();
|
||||||
initFilters();
|
initFilters();
|
||||||
|
loadParsersInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load and display available parsers
|
||||||
|
async function loadParsersInfo() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/parsers');
|
||||||
|
const data = await response.json();
|
||||||
|
const container = document.getElementById('parsers-info');
|
||||||
|
|
||||||
|
if (data.parsers && data.parsers.length > 0) {
|
||||||
|
let html = '<p class="parsers-title">Поддерживаемые платформы:</p><div class="parsers-list">';
|
||||||
|
data.parsers.forEach(p => {
|
||||||
|
html += `<div class="parser-item">
|
||||||
|
<span class="parser-name">${escapeHtml(p.name)}</span>
|
||||||
|
<span class="parser-version">v${escapeHtml(p.version)}</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load parsers info:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Upload handling
|
// Upload handling
|
||||||
function initUpload() {
|
function initUpload() {
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
@@ -145,6 +169,14 @@ function renderConfig(data) {
|
|||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
|
// Server info header
|
||||||
|
if (config.board) {
|
||||||
|
html += `<div class="server-info">
|
||||||
|
<div class="server-info-item"><span class="server-info-label">Модель сервера:</span> <strong>${escapeHtml(config.board.product_name || '-')}</strong></div>
|
||||||
|
<div class="server-info-item"><span class="server-info-label">Серийный номер:</span> <code>${escapeHtml(config.board.serial_number || '-')}</code></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
// Configuration sub-tabs
|
// Configuration sub-tabs
|
||||||
html += `<div class="config-tabs">
|
html += `<div class="config-tabs">
|
||||||
<button class="config-tab active" data-config-tab="spec">Спецификация</button>
|
<button class="config-tab active" data-config-tab="spec">Спецификация</button>
|
||||||
@@ -155,7 +187,6 @@ function renderConfig(data) {
|
|||||||
<button class="config-tab" data-config-tab="gpu">GPU</button>
|
<button class="config-tab" data-config-tab="gpu">GPU</button>
|
||||||
<button class="config-tab" data-config-tab="network">Network</button>
|
<button class="config-tab" data-config-tab="network">Network</button>
|
||||||
<button class="config-tab" data-config-tab="pcie">Device Inventory</button>
|
<button class="config-tab" data-config-tab="pcie">Device Inventory</button>
|
||||||
<button class="config-tab" data-config-tab="fw">Firmware</button>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// Specification tab
|
// Specification tab
|
||||||
@@ -174,7 +205,18 @@ function renderConfig(data) {
|
|||||||
// CPU tab
|
// CPU tab
|
||||||
html += '<div class="config-tab-content" id="config-cpu">';
|
html += '<div class="config-tab-content" id="config-cpu">';
|
||||||
if (config.cpus && config.cpus.length > 0) {
|
if (config.cpus && config.cpus.length > 0) {
|
||||||
html += '<h3>Процессоры</h3><table class="config-table"><thead><tr><th>Socket</th><th>Модель</th><th>Ядра</th><th>Потоки</th><th>Частота</th><th>Max Turbo</th><th>TDP</th><th>L3 Cache</th><th>PPIN</th></tr></thead><tbody>';
|
const cpuCount = config.cpus.length;
|
||||||
|
const cpuModel = config.cpus[0].model || '-';
|
||||||
|
const totalCores = config.cpus.reduce((sum, c) => sum + (c.cores || 0), 0);
|
||||||
|
const totalThreads = config.cpus.reduce((sum, c) => sum + (c.threads || 0), 0);
|
||||||
|
html += `<h3>Процессоры</h3>
|
||||||
|
<div class="section-overview">
|
||||||
|
<div class="stat-box"><span class="stat-value">${cpuCount}</span><span class="stat-label">Процессоров</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${totalCores}</span><span class="stat-label">Ядер</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${totalThreads}</span><span class="stat-label">Потоков</span></div>
|
||||||
|
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(cpuModel)}</span><span class="stat-label">Модель</span></div>
|
||||||
|
</div>
|
||||||
|
<table class="config-table"><thead><tr><th>Socket</th><th>Модель</th><th>Ядра</th><th>Потоки</th><th>Частота</th><th>Max Turbo</th><th>TDP</th><th>L3 Cache</th><th>PPIN</th></tr></thead><tbody>`;
|
||||||
config.cpus.forEach(cpu => {
|
config.cpus.forEach(cpu => {
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>CPU${cpu.socket}</td>
|
<td>CPU${cpu.socket}</td>
|
||||||
@@ -236,7 +278,20 @@ function renderConfig(data) {
|
|||||||
// Power tab
|
// Power tab
|
||||||
html += '<div class="config-tab-content" id="config-power">';
|
html += '<div class="config-tab-content" id="config-power">';
|
||||||
if (config.power_supplies && config.power_supplies.length > 0) {
|
if (config.power_supplies && config.power_supplies.length > 0) {
|
||||||
html += '<h3>Блоки питания</h3><table class="config-table"><thead><tr><th>Слот</th><th>Производитель</th><th>Модель</th><th>Мощность</th><th>Вход</th><th>Выход</th><th>Напряжение</th><th>Температура</th><th>Статус</th></tr></thead><tbody>';
|
const psuTotal = config.power_supplies.length;
|
||||||
|
const psuPresent = config.power_supplies.filter(p => p.present !== false).length;
|
||||||
|
const psuOK = config.power_supplies.filter(p => p.status === 'OK').length;
|
||||||
|
const psuModel = config.power_supplies[0].model || '-';
|
||||||
|
const psuWattage = config.power_supplies[0].wattage_w || 0;
|
||||||
|
html += `<h3>Блоки питания</h3>
|
||||||
|
<div class="section-overview">
|
||||||
|
<div class="stat-box"><span class="stat-value">${psuTotal}</span><span class="stat-label">Всего</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${psuPresent}</span><span class="stat-label">Подключено</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${psuOK}</span><span class="stat-label">Работает</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${psuWattage}W</span><span class="stat-label">Мощность</span></div>
|
||||||
|
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(psuModel)}</span><span class="stat-label">Модель</span></div>
|
||||||
|
</div>
|
||||||
|
<table class="config-table"><thead><tr><th>Слот</th><th>Производитель</th><th>Модель</th><th>Мощность</th><th>Вход</th><th>Выход</th><th>Напряжение</th><th>Температура</th><th>Статус</th></tr></thead><tbody>`;
|
||||||
config.power_supplies.forEach(psu => {
|
config.power_supplies.forEach(psu => {
|
||||||
const statusClass = psu.status === 'OK' ? '' : 'status-warning';
|
const statusClass = psu.status === 'OK' ? '' : 'status-warning';
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
@@ -260,7 +315,22 @@ function renderConfig(data) {
|
|||||||
// Storage tab
|
// Storage tab
|
||||||
html += '<div class="config-tab-content" id="config-storage">';
|
html += '<div class="config-tab-content" id="config-storage">';
|
||||||
if (config.storage && config.storage.length > 0) {
|
if (config.storage && config.storage.length > 0) {
|
||||||
html += '<h3>Накопители</h3><table class="config-table"><thead><tr><th>Слот</th><th>Тип</th><th>Интерфейс</th><th>Модель</th><th>Производитель</th><th>Размер</th><th>Серийный номер</th></tr></thead><tbody>';
|
const storTotal = config.storage.length;
|
||||||
|
const storHDD = config.storage.filter(s => s.type === 'HDD').length;
|
||||||
|
const storSSD = config.storage.filter(s => s.type === 'SSD').length;
|
||||||
|
const storNVMe = config.storage.filter(s => s.type === 'NVMe').length;
|
||||||
|
const totalTB = (config.storage.reduce((sum, s) => sum + (s.size_gb || 0), 0) / 1000).toFixed(1);
|
||||||
|
let typesSummary = [];
|
||||||
|
if (storHDD > 0) typesSummary.push(`${storHDD} HDD`);
|
||||||
|
if (storSSD > 0) typesSummary.push(`${storSSD} SSD`);
|
||||||
|
if (storNVMe > 0) typesSummary.push(`${storNVMe} NVMe`);
|
||||||
|
html += `<h3>Накопители</h3>
|
||||||
|
<div class="section-overview">
|
||||||
|
<div class="stat-box"><span class="stat-value">${storTotal}</span><span class="stat-label">Всего</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${totalTB} TB</span><span class="stat-label">Объём</span></div>
|
||||||
|
<div class="stat-box model-box"><span class="stat-value">${typesSummary.join(', ') || '-'}</span><span class="stat-label">По типам</span></div>
|
||||||
|
</div>
|
||||||
|
<table class="config-table"><thead><tr><th>Слот</th><th>Тип</th><th>Интерфейс</th><th>Модель</th><th>Производитель</th><th>Размер</th><th>Серийный номер</th></tr></thead><tbody>`;
|
||||||
config.storage.forEach(s => {
|
config.storage.forEach(s => {
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(s.slot || '-')}</td>
|
<td>${escapeHtml(s.slot || '-')}</td>
|
||||||
@@ -281,7 +351,16 @@ function renderConfig(data) {
|
|||||||
// GPU tab
|
// GPU tab
|
||||||
html += '<div class="config-tab-content" id="config-gpu">';
|
html += '<div class="config-tab-content" id="config-gpu">';
|
||||||
if (config.gpus && config.gpus.length > 0) {
|
if (config.gpus && config.gpus.length > 0) {
|
||||||
html += '<h3>Графические процессоры</h3><table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>BDF</th><th>PCIe</th><th>Серийный номер</th></tr></thead><tbody>';
|
const gpuCount = config.gpus.length;
|
||||||
|
const gpuModel = config.gpus[0].model || '-';
|
||||||
|
const gpuVendor = config.gpus[0].manufacturer || '-';
|
||||||
|
html += `<h3>Графические процессоры</h3>
|
||||||
|
<div class="section-overview">
|
||||||
|
<div class="stat-box"><span class="stat-value">${gpuCount}</span><span class="stat-label">Всего GPU</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${escapeHtml(gpuVendor)}</span><span class="stat-label">Производитель</span></div>
|
||||||
|
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(gpuModel)}</span><span class="stat-label">Модель</span></div>
|
||||||
|
</div>
|
||||||
|
<table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>BDF</th><th>PCIe</th><th>Серийный номер</th></tr></thead><tbody>`;
|
||||||
config.gpus.forEach(gpu => {
|
config.gpus.forEach(gpu => {
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(gpu.slot || '-')}</td>
|
<td>${escapeHtml(gpu.slot || '-')}</td>
|
||||||
@@ -301,7 +380,18 @@ function renderConfig(data) {
|
|||||||
// Network tab
|
// Network tab
|
||||||
html += '<div class="config-tab-content" id="config-network">';
|
html += '<div class="config-tab-content" id="config-network">';
|
||||||
if (config.network_adapters && config.network_adapters.length > 0) {
|
if (config.network_adapters && config.network_adapters.length > 0) {
|
||||||
html += '<h3>Сетевые адаптеры</h3><table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>Порты</th><th>Тип</th><th>MAC адреса</th><th>Статус</th></tr></thead><tbody>';
|
const nicCount = config.network_adapters.length;
|
||||||
|
const totalPorts = config.network_adapters.reduce((sum, n) => sum + (n.port_count || 0), 0);
|
||||||
|
const nicTypes = [...new Set(config.network_adapters.map(n => n.port_type).filter(t => t))];
|
||||||
|
const nicModels = [...new Set(config.network_adapters.map(n => n.model).filter(m => m))];
|
||||||
|
html += `<h3>Сетевые адаптеры</h3>
|
||||||
|
<div class="section-overview">
|
||||||
|
<div class="stat-box"><span class="stat-value">${nicCount}</span><span class="stat-label">Адаптеров</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${totalPorts}</span><span class="stat-label">Портов</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${nicTypes.join(', ') || '-'}</span><span class="stat-label">Тип портов</span></div>
|
||||||
|
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(nicModels.join(', ') || '-')}</span><span class="stat-label">Модели</span></div>
|
||||||
|
</div>
|
||||||
|
<table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>Порты</th><th>Тип</th><th>MAC адреса</th><th>Статус</th></tr></thead><tbody>`;
|
||||||
config.network_adapters.forEach(nic => {
|
config.network_adapters.forEach(nic => {
|
||||||
const macs = nic.mac_addresses ? nic.mac_addresses.join(', ') : '-';
|
const macs = nic.mac_addresses ? nic.mac_addresses.join(', ') : '-';
|
||||||
const statusClass = nic.status === 'OK' ? '' : 'status-warning';
|
const statusClass = nic.status === 'OK' ? '' : 'status-warning';
|
||||||
@@ -326,13 +416,14 @@ function renderConfig(data) {
|
|||||||
if (config.pcie_devices && config.pcie_devices.length > 0) {
|
if (config.pcie_devices && config.pcie_devices.length > 0) {
|
||||||
html += '<h3>PCIe устройства</h3><table class="config-table"><thead><tr><th>Слот</th><th>BDF</th><th>Тип</th><th>Производитель</th><th>Vendor:Device ID</th><th>PCIe Link</th></tr></thead><tbody>';
|
html += '<h3>PCIe устройства</h3><table class="config-table"><thead><tr><th>Слот</th><th>BDF</th><th>Тип</th><th>Производитель</th><th>Vendor:Device ID</th><th>PCIe Link</th></tr></thead><tbody>';
|
||||||
config.pcie_devices.forEach(p => {
|
config.pcie_devices.forEach(p => {
|
||||||
|
const pcieLink = formatPCIeLink(p.link_width, p.link_speed);
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(p.slot || '-')}</td>
|
<td>${escapeHtml(p.slot || '-')}</td>
|
||||||
<td><code>${escapeHtml(p.bdf || '-')}</code></td>
|
<td><code>${escapeHtml(p.bdf || '-')}</code></td>
|
||||||
<td>${escapeHtml(p.device_class || '-')}</td>
|
<td>${escapeHtml(p.device_class || '-')}</td>
|
||||||
<td>${escapeHtml(p.manufacturer || '-')}</td>
|
<td>${escapeHtml(p.manufacturer || '-')}</td>
|
||||||
<td><code>${p.vendor_id ? p.vendor_id.toString(16) : '-'}:${p.device_id ? p.device_id.toString(16) : '-'}</code></td>
|
<td><code>${p.vendor_id ? p.vendor_id.toString(16) : '-'}:${p.device_id ? p.device_id.toString(16) : '-'}</code></td>
|
||||||
<td>x${p.link_width || '-'} ${escapeHtml(p.link_speed || '-')}</td>
|
<td>${pcieLink}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
html += '</tbody></table>';
|
html += '</tbody></table>';
|
||||||
@@ -341,9 +432,6 @@ function renderConfig(data) {
|
|||||||
}
|
}
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// Firmware tab (content will be populated after firmware loads)
|
|
||||||
html += '<div class="config-tab-content" id="config-fw"><div id="config-fw-content"><p class="no-data">Загрузка...</p></div></div>';
|
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
// Initialize config sub-tabs
|
// Initialize config sub-tabs
|
||||||
@@ -394,25 +482,6 @@ function renderFirmware(firmware) {
|
|||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render in Config -> Firmware tab
|
|
||||||
const configFwContent = document.getElementById('config-fw-content');
|
|
||||||
if (configFwContent) {
|
|
||||||
if (!firmware || firmware.length === 0) {
|
|
||||||
configFwContent.innerHTML = '<p class="no-data">Нет данных о прошивках</p>';
|
|
||||||
} else {
|
|
||||||
let html = '<h3>Прошивки компонентов</h3><table class="config-table"><thead><tr><th>Компонент</th><th>Модель</th><th>Версия</th></tr></thead><tbody>';
|
|
||||||
firmware.forEach(fw => {
|
|
||||||
html += `<tr>
|
|
||||||
<td>${escapeHtml(fw.component)}</td>
|
|
||||||
<td>${escapeHtml(fw.model)}</td>
|
|
||||||
<td><code>${escapeHtml(fw.version)}</code></td>
|
|
||||||
</tr>`;
|
|
||||||
});
|
|
||||||
html += '</tbody></table>';
|
|
||||||
configFwContent.innerHTML = html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSensors() {
|
async function loadSensors() {
|
||||||
@@ -528,9 +597,9 @@ function renderSerials(serials) {
|
|||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td><span class="category-badge ${item.category.toLowerCase()}">${categoryNames[item.category] || item.category}</span></td>
|
<td><span class="category-badge ${item.category.toLowerCase()}">${categoryNames[item.category] || item.category}</span></td>
|
||||||
<td>${escapeHtml(item.component)}</td>
|
<td>${escapeHtml(item.component)}</td>
|
||||||
|
<td>${escapeHtml(item.location || '-')}</td>
|
||||||
<td><code>${escapeHtml(item.serial_number)}</code></td>
|
<td><code>${escapeHtml(item.serial_number)}</code></td>
|
||||||
<td>${escapeHtml(item.manufacturer || '-')}</td>
|
<td>${escapeHtml(item.manufacturer || '-')}</td>
|
||||||
<td>${escapeHtml(item.part_number || '-')}</td>
|
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
@@ -606,6 +675,28 @@ async function clearData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restart app (reload page)
|
||||||
|
function restartApp() {
|
||||||
|
if (confirm('Перезапустить приложение? Все загруженные данные будут потеряны.')) {
|
||||||
|
fetch('/api/clear', { method: 'DELETE' }).then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit app (shutdown server)
|
||||||
|
async function exitApp() {
|
||||||
|
if (confirm('Завершить работу приложения?')) {
|
||||||
|
try {
|
||||||
|
await fetch('/api/shutdown', { method: 'POST' });
|
||||||
|
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;"><div style="text-align:center;"><h1>LOGPile</h1><p>Приложение завершено. Можете закрыть эту вкладку.</p></div></div>';
|
||||||
|
} catch (err) {
|
||||||
|
// Server shutdown, connection will fail
|
||||||
|
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;"><div style="text-align:center;"><h1>LOGPile</h1><p>Приложение завершено. Можете закрыть эту вкладку.</p></div></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
function formatDate(isoString) {
|
function formatDate(isoString) {
|
||||||
if (!isoString) return '-';
|
if (!isoString) return '-';
|
||||||
@@ -619,3 +710,24 @@ function escapeHtml(text) {
|
|||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPCIeLink(width, speed) {
|
||||||
|
if (!width && !speed) return '-';
|
||||||
|
|
||||||
|
// Convert GT/s to PCIe generation
|
||||||
|
let gen = '';
|
||||||
|
if (speed) {
|
||||||
|
const gtMatch = speed.match(/(\d+\.?\d*)\s*GT/i);
|
||||||
|
if (gtMatch) {
|
||||||
|
const gts = parseFloat(gtMatch[1]);
|
||||||
|
if (gts >= 32) gen = 'Gen5';
|
||||||
|
else if (gts >= 16) gen = 'Gen4';
|
||||||
|
else if (gts >= 8) gen = 'Gen3';
|
||||||
|
else if (gts >= 5) gen = 'Gen2';
|
||||||
|
else if (gts >= 2.5) gen = 'Gen1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const widthStr = width ? `x${width}` : '';
|
||||||
|
return gen ? `${widthStr} PCIe ${gen}` : `${widthStr} ${speed || ''}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<p class="hint">Поддерживаемые форматы: tar.gz, zip</p>
|
<p class="hint">Поддерживаемые форматы: tar.gz, zip</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="upload-status"></div>
|
<div id="upload-status"></div>
|
||||||
|
<div id="parsers-info" class="parsers-info"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="data-section" class="hidden">
|
<section id="data-section" class="hidden">
|
||||||
@@ -90,9 +91,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Категория</th>
|
<th>Категория</th>
|
||||||
<th>Компонент</th>
|
<th>Компонент</th>
|
||||||
|
<th>Расположение</th>
|
||||||
<th>Серийный номер</th>
|
<th>Серийный номер</th>
|
||||||
<th>Производитель</th>
|
<th>Производитель</th>
|
||||||
<th>Part Number</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
@@ -124,7 +125,11 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<button id="clear-btn" class="hidden" onclick="clearData()">Очистить данные</button>
|
<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 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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user