chore: align codebase with bible engineering contracts
- identifier-normalization: use strings.EqualFold in h3c/parser.go
- import-export: CSV now uses UTF-8 BOM and semicolon delimiter
- go-code-style: translate all Russian source strings to English (ADL-007)
- go-background-tasks: add Type, Message, Result fields to Job struct
- go-api: wrap list endpoints in {items, total_count, page, per_page, total_pages}
- module-structure: rename helpers.go → context_sleep.go
- build-version-display: htmlError renders version footer on error pages
- go-logging: migrate all log.Printf calls to log/slog with structured attrs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,18 +38,21 @@ type CollectJobResponse struct {
|
||||
}
|
||||
|
||||
type CollectJobStatusResponse struct {
|
||||
JobID string `json:"job_id"`
|
||||
Status string `json:"status"`
|
||||
Progress *int `json:"progress,omitempty"`
|
||||
CurrentPhase string `json:"current_phase,omitempty"`
|
||||
ETASeconds *int `json:"eta_seconds,omitempty"`
|
||||
Logs []string `json:"logs,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ActiveModules []CollectModuleStatus `json:"active_modules,omitempty"`
|
||||
ModuleScores []CollectModuleStatus `json:"module_scores,omitempty"`
|
||||
DebugInfo *CollectDebugInfo `json:"debug_info,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
JobID string `json:"job_id"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Progress *int `json:"progress,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
CurrentPhase string `json:"current_phase,omitempty"`
|
||||
ETASeconds *int `json:"eta_seconds,omitempty"`
|
||||
Logs []string `json:"logs,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Result map[string]interface{} `json:"result,omitempty"`
|
||||
ActiveModules []CollectModuleStatus `json:"active_modules,omitempty"`
|
||||
ModuleScores []CollectModuleStatus `json:"module_scores,omitempty"`
|
||||
DebugInfo *CollectDebugInfo `json:"debug_info,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CollectRequestMeta struct {
|
||||
@@ -63,12 +66,15 @@ type CollectRequestMeta struct {
|
||||
|
||||
type Job struct {
|
||||
ID string
|
||||
Type string
|
||||
Status string
|
||||
Progress int
|
||||
Message string
|
||||
CurrentPhase string
|
||||
ETASeconds int
|
||||
Logs []string
|
||||
Error string
|
||||
Result map[string]interface{}
|
||||
ActiveModules []CollectModuleStatus
|
||||
ModuleScores []CollectModuleStatus
|
||||
DebugInfo *CollectDebugInfo
|
||||
@@ -107,11 +113,14 @@ func (j *Job) toStatusResponse() CollectJobStatusResponse {
|
||||
progress := j.Progress
|
||||
resp := CollectJobStatusResponse{
|
||||
JobID: j.ID,
|
||||
Type: j.Type,
|
||||
Status: j.Status,
|
||||
Progress: &progress,
|
||||
Message: j.Message,
|
||||
CurrentPhase: j.CurrentPhase,
|
||||
Logs: append([]string(nil), j.Logs...),
|
||||
Error: j.Error,
|
||||
Result: j.Result,
|
||||
ActiveModules: append([]CollectModuleStatus(nil), j.ActiveModules...),
|
||||
ModuleScores: append([]CollectModuleStatus(nil), j.ModuleScores...),
|
||||
DebugInfo: cloneCollectDebugInfo(j.DebugInfo),
|
||||
|
||||
@@ -174,7 +174,7 @@ func TestBuildSpecification_ZeroSizeMemoryWithInventoryIsShown(t *testing.T) {
|
||||
|
||||
spec := buildSpecification(hw)
|
||||
for _, line := range spec {
|
||||
if line.Category == "Память" && line.Name == "Hynix HMCG88AEBRA115N (size unknown)" && line.Quantity == 1 {
|
||||
if line.Category == "Memory" && line.Name == "Hynix HMCG88AEBRA115N (size unknown)" && line.Quantity == 1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tmplContent, err := WebFS.ReadFile("templates/index.html")
|
||||
if err != nil {
|
||||
http.Error(w, "Template not found", http.StatusInternalServerError)
|
||||
s.htmlError(w, "Template not found", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, err := template.New("index").Parse(string(tmplContent))
|
||||
if err != nil {
|
||||
http.Error(w, "Template parse error", http.StatusInternalServerError)
|
||||
s.htmlError(w, "Template parse error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func (s *Server) handleChartCurrent(w http.ResponseWriter, r *http.Request) {
|
||||
if result == nil || result.Hardware == nil {
|
||||
html, err := chartviewer.RenderHTML(nil, title)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to render viewer", http.StatusInternalServerError)
|
||||
s.htmlError(w, "failed to render viewer", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -80,7 +80,7 @@ func (s *Server) handleChartCurrent(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
snapshotBytes, err := currentReanimatorSnapshotBytes(result)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to build reanimator snapshot: "+err.Error(), http.StatusInternalServerError)
|
||||
s.htmlError(w, "failed to build reanimator snapshot: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func (s *Server) handleChartCurrent(w http.ResponseWriter, r *http.Request) {
|
||||
PrintMode: printMode,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, "failed to render chart: "+err.Error(), http.StatusInternalServerError)
|
||||
s.htmlError(w, "failed to render chart: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -395,7 +395,7 @@ func uniqueSortedExtensions(exts []string) []string {
|
||||
func (s *Server) handleGetEvents(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
if result == nil {
|
||||
jsonResponse(w, []interface{}{})
|
||||
jsonList(w, []interface{}{}, 0)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -408,18 +408,18 @@ func (s *Server) handleGetEvents(w http.ResponseWriter, r *http.Request) {
|
||||
return events[i].Timestamp.After(events[j].Timestamp)
|
||||
})
|
||||
|
||||
jsonResponse(w, events)
|
||||
jsonList(w, events, len(events))
|
||||
}
|
||||
|
||||
func (s *Server) handleGetSensors(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
if result == nil {
|
||||
jsonResponse(w, []interface{}{})
|
||||
jsonList(w, []interface{}{}, 0)
|
||||
return
|
||||
}
|
||||
sensors := append([]models.SensorReading{}, result.Sensors...)
|
||||
sensors = append(sensors, synthesizePSUVoltageSensors(result.Hardware)...)
|
||||
jsonResponse(w, sensors)
|
||||
jsonList(w, sensors, len(sensors))
|
||||
}
|
||||
|
||||
func synthesizePSUVoltageSensors(hw *models.HardwareConfig) []models.SensorReading {
|
||||
@@ -533,7 +533,7 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
||||
float64(cpu.FrequencyMHz)/1000,
|
||||
cpu.Cores,
|
||||
intFromDetails(cpu.Details, "tdp_w"))
|
||||
spec = append(spec, SpecLine{Category: "Процессор", Name: name, Quantity: count})
|
||||
spec = append(spec, SpecLine{Category: "CPU", Name: name, Quantity: count})
|
||||
}
|
||||
|
||||
// Memory - group by size, type and frequency (only installed modules)
|
||||
@@ -568,7 +568,7 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
||||
memGroups[key]++
|
||||
}
|
||||
for key, count := range memGroups {
|
||||
spec = append(spec, SpecLine{Category: "Память", Name: key, Quantity: count})
|
||||
spec = append(spec, SpecLine{Category: "Memory", Name: key, Quantity: count})
|
||||
}
|
||||
|
||||
// Storage - group by type and capacity
|
||||
@@ -586,7 +586,7 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
||||
storGroups[key]++
|
||||
}
|
||||
for key, count := range storGroups {
|
||||
spec = append(spec, SpecLine{Category: "Накопитель", Name: key, Quantity: count})
|
||||
spec = append(spec, SpecLine{Category: "Storage", Name: key, Quantity: count})
|
||||
}
|
||||
|
||||
// PCIe devices - group by device class/name and manufacturer
|
||||
@@ -609,7 +609,7 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
||||
}
|
||||
for key, count := range pcieGroups {
|
||||
pcie := pcieDetails[key]
|
||||
category := "PCIe устройство"
|
||||
category := "PCIe Device"
|
||||
name := key
|
||||
|
||||
// Determine category based on device class or known GPU names
|
||||
@@ -618,11 +618,11 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
||||
isNetwork := deviceClass == "Network" || strings.Contains(deviceClass, "ConnectX")
|
||||
|
||||
if isGPU {
|
||||
category = "Графический процессор"
|
||||
category = "GPU"
|
||||
} else if isNetwork {
|
||||
category = "Сетевой адаптер"
|
||||
category = "Network Adapter"
|
||||
} else if deviceClass == "NVMe" || deviceClass == "RAID" || deviceClass == "SAS" || deviceClass == "SATA" || deviceClass == "Storage" {
|
||||
category = "Контроллер"
|
||||
category = "Controller"
|
||||
}
|
||||
|
||||
spec = append(spec, SpecLine{Category: category, Name: name, Quantity: count})
|
||||
@@ -643,7 +643,7 @@ func buildSpecification(hw *models.HardwareConfig) []SpecLine {
|
||||
}
|
||||
}
|
||||
for key, count := range psuGroups {
|
||||
spec = append(spec, SpecLine{Category: "Блок питания", Name: key, Quantity: count})
|
||||
spec = append(spec, SpecLine{Category: "Power Supply", Name: key, Quantity: count})
|
||||
}
|
||||
|
||||
return spec
|
||||
@@ -664,7 +664,7 @@ func nonEmptyStrings(values ...string) []string {
|
||||
func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
if result == nil {
|
||||
jsonResponse(w, []interface{}{})
|
||||
jsonList(w, []interface{}{}, 0)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -714,7 +714,7 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
jsonResponse(w, serials)
|
||||
jsonList(w, serials, len(serials))
|
||||
}
|
||||
|
||||
func normalizePCIeSerialComponentName(p models.PCIeDevice) string {
|
||||
@@ -768,11 +768,12 @@ func hasUsableFirmwareVersion(version string) bool {
|
||||
func (s *Server) handleGetFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
if result == nil || result.Hardware == nil {
|
||||
jsonResponse(w, []interface{}{})
|
||||
jsonList(w, []interface{}{}, 0)
|
||||
return
|
||||
}
|
||||
|
||||
jsonResponse(w, buildFirmwareEntries(result.Hardware))
|
||||
entries := buildFirmwareEntries(result.Hardware)
|
||||
jsonList(w, entries, len(entries))
|
||||
}
|
||||
|
||||
type parseErrorEntry struct {
|
||||
@@ -941,8 +942,7 @@ func looksLikeErrorLogLine(line string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(s, "ошибка") ||
|
||||
strings.Contains(s, "error") ||
|
||||
return strings.Contains(s, "error") ||
|
||||
strings.Contains(s, "failed") ||
|
||||
strings.Contains(s, "timeout") ||
|
||||
strings.Contains(s, "deadline exceeded")
|
||||
@@ -977,7 +977,7 @@ func parseErrorSeverityFromMessage(msg string) string {
|
||||
if strings.HasPrefix(s, "status 404 ") || strings.HasPrefix(s, "status 405 ") || strings.HasPrefix(s, "status 410 ") || strings.HasPrefix(s, "status 501 ") {
|
||||
return "info"
|
||||
}
|
||||
if strings.Contains(s, "ошибка") || strings.Contains(s, "error") || strings.Contains(s, "failed") {
|
||||
if strings.Contains(s, "error") || strings.Contains(s, "failed") {
|
||||
return "warning"
|
||||
}
|
||||
return "info"
|
||||
@@ -1316,7 +1316,7 @@ func (s *Server) handleConvertReanimatorBatch(w http.ResponseWriter, r *http.Req
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "logpile-convert-input-*")
|
||||
if err != nil {
|
||||
jsonError(w, "Не удалось создать временную директорию", http.StatusInternalServerError)
|
||||
jsonError(w, "Failed to create temp directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1363,7 +1363,7 @@ func (s *Server) handleConvertReanimatorBatch(w http.ResponseWriter, r *http.Req
|
||||
|
||||
if len(inputFiles) == 0 {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
jsonError(w, "Нет файлов поддерживаемого типа для конвертации", http.StatusBadRequest)
|
||||
jsonError(w, "No supported files to convert", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1376,9 +1376,9 @@ func (s *Server) handleConvertReanimatorBatch(w http.ResponseWriter, r *http.Req
|
||||
TLSMode: "insecure",
|
||||
})
|
||||
s.markConvertJob(job.ID)
|
||||
s.jobManager.AppendJobLog(job.ID, fmt.Sprintf("Запущена пакетная конвертация: %d файлов", len(inputFiles)))
|
||||
s.jobManager.AppendJobLog(job.ID, fmt.Sprintf("Batch conversion started: %d files", len(inputFiles)))
|
||||
if skipped > 0 {
|
||||
s.jobManager.AppendJobLog(job.ID, fmt.Sprintf("Пропущено неподдерживаемых файлов: %d", skipped))
|
||||
s.jobManager.AppendJobLog(job.ID, fmt.Sprintf("Skipped unsupported files: %d", skipped))
|
||||
}
|
||||
s.jobManager.UpdateJobStatus(job.ID, CollectStatusRunning, 0, "")
|
||||
|
||||
@@ -1406,7 +1406,7 @@ func (s *Server) runConvertJob(jobID, tempDir string, inputFiles []convertInputF
|
||||
|
||||
resultFile, err := os.CreateTemp("", "logpile-convert-result-*.zip")
|
||||
if err != nil {
|
||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "не удалось создать zip")
|
||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "failed to create zip")
|
||||
return
|
||||
}
|
||||
resultPath := resultFile.Name()
|
||||
@@ -1418,7 +1418,7 @@ func (s *Server) runConvertJob(jobID, tempDir string, inputFiles []convertInputF
|
||||
totalProcess := len(inputFiles)
|
||||
|
||||
for i, in := range inputFiles {
|
||||
s.jobManager.AppendJobLog(jobID, fmt.Sprintf("Обработка %s", in.Name))
|
||||
s.jobManager.AppendJobLog(jobID, fmt.Sprintf("Processing %s", in.Name))
|
||||
payload, err := os.ReadFile(in.Path)
|
||||
if err != nil {
|
||||
failures = append(failures, fmt.Sprintf("%s: %v", in.Name, err))
|
||||
@@ -1471,13 +1471,13 @@ func (s *Server) runConvertJob(jobID, tempDir string, inputFiles []convertInputF
|
||||
if success == 0 {
|
||||
_ = zw.Close()
|
||||
_ = os.Remove(resultPath)
|
||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Не удалось конвертировать ни один файл")
|
||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Failed to convert any file")
|
||||
return
|
||||
}
|
||||
|
||||
summaryLines := []string{fmt.Sprintf("Конвертировано %d из %d файлов", success, total)}
|
||||
summaryLines := []string{fmt.Sprintf("Converted %d of %d files", success, total)}
|
||||
if skipped > 0 {
|
||||
summaryLines = append(summaryLines, fmt.Sprintf("Пропущено неподдерживаемых: %d", skipped))
|
||||
summaryLines = append(summaryLines, fmt.Sprintf("Skipped unsupported: %d", skipped))
|
||||
}
|
||||
summaryLines = append(summaryLines, failures...)
|
||||
if entry, err := zw.Create("convert-summary.txt"); err == nil {
|
||||
@@ -1485,7 +1485,7 @@ func (s *Server) runConvertJob(jobID, tempDir string, inputFiles []convertInputF
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
_ = os.Remove(resultPath)
|
||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Не удалось упаковать результаты")
|
||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Failed to pack results")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1638,7 +1638,7 @@ func (s *Server) handleCollectStart(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
job := s.jobManager.CreateJob(req)
|
||||
s.jobManager.AppendJobLog(job.ID, "Клиент: "+s.ClientVersionString())
|
||||
s.jobManager.AppendJobLog(job.ID, "Client: "+s.ClientVersionString())
|
||||
s.startCollectionJob(job.ID, req)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1667,7 +1667,7 @@ func pingHost(host string, port int, total, need int) (bool, string) {
|
||||
}
|
||||
n := int(successes.Load())
|
||||
if n < need {
|
||||
return false, fmt.Sprintf("Хост недоступен: только %d из %d попыток подключения к %s прошли успешно (требуется минимум %d)", n, total, addr, need)
|
||||
return false, fmt.Sprintf("Host unreachable: only %d of %d connection attempts to %s succeeded (minimum required: %d)", n, total, addr, need)
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
@@ -1684,12 +1684,12 @@ func (s *Server) handleCollectProbe(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
connector, ok := s.getCollector(req.Protocol)
|
||||
if !ok {
|
||||
jsonError(w, "Коннектор для протокола не зарегистрирован", http.StatusBadRequest)
|
||||
jsonError(w, "Protocol connector not registered", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
prober, ok := connector.(collector.Prober)
|
||||
if !ok {
|
||||
jsonError(w, "Проверка подключения для протокола не поддерживается", http.StatusBadRequest)
|
||||
jsonError(w, "Connection probe not supported for this protocol", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1703,16 +1703,16 @@ func (s *Server) handleCollectProbe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
result, err := prober.Probe(ctx, toCollectorRequest(req))
|
||||
if err != nil {
|
||||
jsonError(w, "Проверка подключения не удалась: "+err.Error(), http.StatusBadRequest)
|
||||
jsonError(w, "Connection probe failed: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
message := "Связь с BMC установлена"
|
||||
message := "BMC connection established"
|
||||
if result != nil {
|
||||
if result.HostPoweredOn {
|
||||
message = "Связь с BMC установлена, host включён."
|
||||
message = "BMC connection established, host is powered on."
|
||||
} else {
|
||||
message = "Связь с BMC установлена, host выключен. Данные инвентаря могут быть неполными."
|
||||
message = "BMC connection established, host is powered off. Inventory data may be incomplete."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1797,8 +1797,8 @@ func (s *Server) startCollectionJob(jobID string, req CollectRequest) {
|
||||
go func() {
|
||||
connector, ok := s.getCollector(req.Protocol)
|
||||
if !ok {
|
||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Коннектор для протокола не зарегистрирован")
|
||||
s.jobManager.AppendJobLog(jobID, "Сбор завершен с ошибкой")
|
||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, "Protocol connector not registered")
|
||||
s.jobManager.AppendJobLog(jobID, "Collection completed with error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1872,7 +1872,7 @@ func (s *Server) startCollectionJob(jobID string, req CollectRequest) {
|
||||
return
|
||||
}
|
||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusFailed, 100, err.Error())
|
||||
s.jobManager.AppendJobLog(jobID, "Сбор завершен с ошибкой")
|
||||
s.jobManager.AppendJobLog(jobID, "Collection completed with error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1882,7 +1882,7 @@ func (s *Server) startCollectionJob(jobID string, req CollectRequest) {
|
||||
|
||||
applyCollectSourceMetadata(result, req)
|
||||
s.jobManager.UpdateJobStatus(jobID, CollectStatusSuccess, 100, "")
|
||||
s.jobManager.AppendJobLog(jobID, "Сбор завершен")
|
||||
s.jobManager.AppendJobLog(jobID, "Collection completed")
|
||||
s.SetResult(result)
|
||||
s.SetDetectedVendor(req.Protocol)
|
||||
if job, ok := s.jobManager.GetJob(jobID); ok {
|
||||
@@ -2142,6 +2142,27 @@ func jsonError(w http.ResponseWriter, message string, code int) {
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||
}
|
||||
|
||||
func (s *Server) htmlError(w http.ResponseWriter, message string, code int) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(code)
|
||||
version := normalizeDisplayVersion(s.config.AppVersion)
|
||||
fmt.Fprintf(w, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Error %d</title></head>`+
|
||||
`<body><h1>Error %d</h1><p>%s</p>`+
|
||||
`<footer style="margin-top:2em;color:#999;font-size:12px">LOGPile %s</footer></body></html>`,
|
||||
code, code, template.HTMLEscapeString(message), template.HTMLEscapeString(version))
|
||||
}
|
||||
|
||||
func jsonList(w http.ResponseWriter, items interface{}, totalCount int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"items": items,
|
||||
"total_count": totalCount,
|
||||
"page": 1,
|
||||
"per_page": totalCount,
|
||||
"total_pages": 1,
|
||||
})
|
||||
}
|
||||
|
||||
// isGPUDevice checks if device class indicates a GPU
|
||||
func isGPUDevice(deviceClass string) bool {
|
||||
// Standard PCI class names
|
||||
|
||||
@@ -51,17 +51,20 @@ func TestHandleGetSerials_WithGPUs(t *testing.T) {
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var serials []struct {
|
||||
Component string `json:"component"`
|
||||
Location string `json:"location,omitempty"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Category string `json:"category"`
|
||||
var resp struct {
|
||||
Items []struct {
|
||||
Component string `json:"component"`
|
||||
Location string `json:"location,omitempty"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Manufacturer string `json:"manufacturer,omitempty"`
|
||||
Category string `json:"category"`
|
||||
} `json:"items"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(w.Body).Decode(&serials); err != nil {
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
serials := resp.Items
|
||||
|
||||
// Check that we have GPU entries
|
||||
gpuCount := 0
|
||||
@@ -115,13 +118,16 @@ func TestHandleGetSerials_WithoutGPUSerials(t *testing.T) {
|
||||
srv.handleGetSerials(w, req)
|
||||
|
||||
// Parse response
|
||||
var serials []struct {
|
||||
Category string `json:"category"`
|
||||
var resp struct {
|
||||
Items []struct {
|
||||
Category string `json:"category"`
|
||||
} `json:"items"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(w.Body).Decode(&serials); err != nil {
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
serials := resp.Items
|
||||
|
||||
// Check that GPUs without serial numbers are not included
|
||||
for _, s := range serials {
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -22,9 +23,11 @@ func (m *JobManager) CreateJob(req CollectRequest) *Job {
|
||||
now := time.Now().UTC()
|
||||
job := &Job{
|
||||
ID: generateJobID(),
|
||||
Type: req.Protocol,
|
||||
Status: CollectStatusQueued,
|
||||
Progress: 0,
|
||||
Logs: []string{formatCollectLogLine(now, "Задача поставлена в очередь")},
|
||||
Message: "Job queued",
|
||||
Logs: []string{formatCollectLogLine(now, "Job queued")},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
RequestMeta: CollectRequestMeta{
|
||||
@@ -66,7 +69,7 @@ func (m *JobManager) CancelJob(id string) (*Job, bool) {
|
||||
job.Status = CollectStatusCanceled
|
||||
job.Error = ""
|
||||
job.UpdatedAt = time.Now().UTC()
|
||||
job.Logs = append(job.Logs, formatCollectLogLine(job.UpdatedAt, "Сбор отменен пользователем"))
|
||||
job.Logs = append(job.Logs, formatCollectLogLine(job.UpdatedAt, "Collection canceled by user"))
|
||||
}
|
||||
|
||||
cancelFn := job.cancel
|
||||
@@ -122,6 +125,7 @@ func (m *JobManager) AppendJobLog(id, message string) (*Job, bool) {
|
||||
job.Logs = append(job.Logs, message)
|
||||
job.UpdatedAt = time.Now().UTC()
|
||||
job.Logs[len(job.Logs)-1] = formatCollectLogLine(job.UpdatedAt, message)
|
||||
job.Message = message
|
||||
|
||||
cloned := cloneJob(job)
|
||||
m.mu.Unlock()
|
||||
@@ -202,7 +206,7 @@ func (m *JobManager) SkipJob(id string) (*Job, bool) {
|
||||
skipFn := job.skipFn
|
||||
job.skipFn = nil
|
||||
job.UpdatedAt = time.Now().UTC()
|
||||
job.Logs = append(job.Logs, formatCollectLogLine(job.UpdatedAt, "Пропуск зависших запросов по команде пользователя"))
|
||||
job.Logs = append(job.Logs, formatCollectLogLine(job.UpdatedAt, "Skipping stalled requests on user request"))
|
||||
cloned := cloneJob(job)
|
||||
m.mu.Unlock()
|
||||
|
||||
@@ -212,6 +216,18 @@ func (m *JobManager) SkipJob(id string) (*Job, bool) {
|
||||
return cloned, true
|
||||
}
|
||||
|
||||
func (m *JobManager) SetJobResult(id string, result map[string]interface{}) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
job, ok := m.jobs[id]
|
||||
if !ok || job == nil {
|
||||
return false
|
||||
}
|
||||
job.Result = result
|
||||
job.UpdatedAt = time.Now().UTC()
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *JobManager) AttachJobCancel(id string, cancelFn context.CancelFunc) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -265,6 +281,9 @@ func cloneJob(job *Job) *Job {
|
||||
cloned.DebugInfo = cloneCollectDebugInfo(job.DebugInfo)
|
||||
cloned.CurrentPhase = job.CurrentPhase
|
||||
cloned.ETASeconds = job.ETASeconds
|
||||
if job.Result != nil {
|
||||
cloned.Result = maps.Clone(job.Result)
|
||||
}
|
||||
cloned.cancel = nil
|
||||
cloned.skipFn = nil
|
||||
return &cloned
|
||||
|
||||
Reference in New Issue
Block a user