Improve UI views for assets/components/failures and timeline details

This commit is contained in:
2026-02-15 23:26:12 +03:00
parent 929bf9c524
commit f9ff0e10ff
14 changed files with 1221 additions and 87 deletions

View File

@@ -72,8 +72,8 @@ func TestIngestHardwareIdempotent(t *testing.T) {
if err := json.Unmarshal(createdBody, &createdResp); err != nil {
t.Fatalf("decode body: %v", err)
}
if createdResp.Summary.TimelineEventsCreated != 3 {
t.Fatalf("expected timeline_events_created=3, got %d", createdResp.Summary.TimelineEventsCreated)
if createdResp.Summary.TimelineEventsCreated != 5 {
t.Fatalf("expected timeline_events_created=5, got %d", createdResp.Summary.TimelineEventsCreated)
}
resp, err = http.Post(server.URL+"/ingest/hardware", "application/json", bytes.NewReader(body))
@@ -115,23 +115,29 @@ func TestIngestHardwareStatusEvents(t *testing.T) {
server := httptest.NewServer(mux)
defer server.Close()
collectedAt := "2026-02-10T16:00:00Z"
warningAt := "2026-02-10T15:10:00Z"
failedAt := "2026-02-10T15:12:00Z"
payload := map[string]any{
"target_host": "status-server",
"collected_at": time.Now().UTC().Format(time.RFC3339),
"collected_at": collectedAt,
"hardware": map[string]any{
"board": map[string]any{"serial_number": "STATUS-123"},
"storage": []map[string]any{
{
"slot": "Storage-0",
"serial_number": "WARN-001",
"present": true,
"status": "Warning",
"slot": "Storage-0",
"serial_number": "WARN-001",
"present": true,
"status": "Warning",
"status_changed_at": warningAt,
},
{
"slot": "Storage-1",
"serial_number": "CRIT-001",
"present": true,
"status": "Critical",
"slot": "Storage-1",
"serial_number": "CRIT-001",
"present": true,
"status": "Critical",
"status_changed_at": failedAt,
},
},
},
@@ -153,6 +159,120 @@ func TestIngestHardwareStatusEvents(t *testing.T) {
assertCountQuery(t, db, "SELECT COUNT(*) FROM failure_events", 1)
assertCountQuery(t, db, "SELECT COUNT(*) FROM timeline_events WHERE event_type = 'COMPONENT_WARNING'", 2)
assertCountQuery(t, db, "SELECT COUNT(*) FROM timeline_events WHERE event_type = 'COMPONENT_FAILED'", 2)
wantWarningAt, _ := time.Parse(time.RFC3339, warningAt)
wantFailedAt, _ := time.Parse(time.RFC3339, failedAt)
var gotWarningAt time.Time
if err := db.QueryRow(`SELECT event_time FROM timeline_events WHERE event_type = 'COMPONENT_WARNING' ORDER BY id LIMIT 1`).Scan(&gotWarningAt); err != nil {
t.Fatalf("query warning event_time: %v", err)
}
if !gotWarningAt.UTC().Equal(wantWarningAt.UTC()) {
t.Fatalf("warning event_time = %s, want %s", gotWarningAt.UTC().Format(time.RFC3339), wantWarningAt.UTC().Format(time.RFC3339))
}
var gotFailedAt time.Time
if err := db.QueryRow(`SELECT event_time FROM timeline_events WHERE event_type = 'COMPONENT_FAILED' ORDER BY id LIMIT 1`).Scan(&gotFailedAt); err != nil {
t.Fatalf("query failed event_time: %v", err)
}
if !gotFailedAt.UTC().Equal(wantFailedAt.UTC()) {
t.Fatalf("failed event_time = %s, want %s", gotFailedAt.UTC().Format(time.RFC3339), wantFailedAt.UTC().Format(time.RFC3339))
}
}
func TestIngestHardwareComponentRecoversToOK(t *testing.T) {
dsn := os.Getenv("DATABASE_DSN")
if dsn == "" {
t.Skip("DATABASE_DSN not set")
}
db, err := repository.Open(dsn)
if err != nil {
t.Fatalf("open db: %v", err)
}
defer db.Close()
if err := applyMigrations(db); err != nil {
t.Fatalf("apply migrations: %v", err)
}
if err := cleanupRegistry(db); err != nil {
t.Fatalf("cleanup: %v", err)
}
mux := http.NewServeMux()
RegisterIngestRoutes(mux, IngestDependencies{Service: ingest.NewService(db)})
server := httptest.NewServer(mux)
defer server.Close()
postPayload := func(payload map[string]any) {
body, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal payload: %v", err)
}
resp, err := http.Post(server.URL+"/ingest/hardware", "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("post: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("expected 201, got %d", resp.StatusCode)
}
}
failedAt := "2026-02-10T15:10:00Z"
okAt := "2026-02-10T15:22:00Z"
postPayload(map[string]any{
"target_host": "recover-server",
"collected_at": "2026-02-10T15:10:10Z",
"hardware": map[string]any{
"board": map[string]any{"serial_number": "RECOVER-001"},
"storage": []map[string]any{
{
"slot": "Disk.Bay.0",
"serial_number": "RECOVER-DISK-001",
"present": true,
"status": "Critical",
"status_changed_at": failedAt,
},
},
},
})
postPayload(map[string]any{
"target_host": "recover-server",
"collected_at": "2026-02-10T15:22:10Z",
"hardware": map[string]any{
"board": map[string]any{"serial_number": "RECOVER-001"},
"storage": []map[string]any{
{
"slot": "Disk.Bay.0",
"serial_number": "RECOVER-DISK-001",
"present": true,
"status": "OK",
"status_changed_at": okAt,
},
},
},
})
assertCountQuery(t, db, "SELECT COUNT(*) FROM timeline_events WHERE event_type = 'COMPONENT_FAILED'", 2)
assertCountQuery(t, db, "SELECT COUNT(*) FROM timeline_events WHERE event_type = 'COMPONENT_OK'", 2)
var latestEventType string
if err := db.QueryRow(`
SELECT event_type
FROM timeline_events
WHERE subject_type = 'component'
AND subject_id = (SELECT id FROM parts WHERE vendor_serial = 'RECOVER-DISK-001' LIMIT 1)
ORDER BY event_time DESC, id DESC
LIMIT 1
`).Scan(&latestEventType); err != nil {
t.Fatalf("latest component status query: %v", err)
}
if latestEventType != "COMPONENT_OK" {
t.Fatalf("expected latest component event COMPONENT_OK, got %q", latestEventType)
}
}
func TestIngestHardwareFirmwareChange(t *testing.T) {
@@ -371,4 +491,6 @@ func TestIngestHardwareStoresStatusHistoryInObservationDetails(t *testing.T) {
if statusChangedAt != "2026-02-10T15:22:00Z" {
t.Fatalf("expected status_changed_at=2026-02-10T15:22:00Z, got %q", statusChangedAt)
}
assertCountQuery(t, db, "SELECT COUNT(*) FROM timeline_events WHERE event_type = 'COMPONENT_OK'", 2)
}