docs: refresh project documentation

This commit is contained in:
Mikhail Chusavitin
2026-03-15 16:35:16 +03:00
parent 47bb0ee939
commit 0acdc2b202
14 changed files with 508 additions and 1224 deletions

View File

@@ -6,6 +6,6 @@ Start with `bible/rules/patterns/` for specific contracts.
## Project Architecture ## Project Architecture
Read `bible-local/` — LOGPile specific architecture. Read `bible-local/` — LOGPile specific architecture.
Read order: `bible-local/README.md``01-overview.md` → relevant files for the task. Read order: `bible-local/README.md``01-overview.md` `02-architecture.md``04-data-models.md` relevant file(s) for the task.
Every architectural decision specific to this project must be recorded in `bible-local/10-decisions.md`. Every architectural decision specific to this project must be recorded in `bible-local/10-decisions.md`.

View File

@@ -6,6 +6,6 @@ Start with `bible/rules/patterns/` for specific contracts.
## Project Architecture ## Project Architecture
Read `bible-local/` — LOGPile specific architecture. Read `bible-local/` — LOGPile specific architecture.
Read order: `bible-local/README.md``01-overview.md` → relevant files for the task. Read order: `bible-local/README.md``01-overview.md` `02-architecture.md``04-data-models.md` relevant file(s) for the task.
Every architectural decision specific to this project must be recorded in `bible-local/10-decisions.md`. Every architectural decision specific to this project must be recorded in `bible-local/10-decisions.md`.

View File

@@ -2,9 +2,27 @@
Standalone Go application for BMC diagnostics analysis with an embedded web UI. Standalone Go application for BMC diagnostics analysis with an embedded web UI.
## What it does
- Parses vendor diagnostic archives into a normalized hardware inventory
- Collects live BMC data via Redfish
- Exports normalized data as CSV, raw re-analysis bundles, and Reanimator JSON
- Runs as a single Go binary with embedded UI assets
## Documentation ## Documentation
- Architecture and technical documentation (single source of truth): [`docs/bible/README.md`](docs/bible/README.md) - Shared engineering rules: [`bible/README.md`](bible/README.md)
- Project architecture and API contracts: [`bible-local/README.md`](bible-local/README.md)
- Agent entrypoints: [`AGENTS.md`](AGENTS.md), [`CLAUDE.md`](CLAUDE.md)
## Run
```bash
make build
./bin/logpile
```
Default port: `8082`
## License ## License

View File

@@ -1,35 +1,43 @@
# 01 — Overview # 01 — Overview
## What is LOGPile? ## Purpose
LOGPile is a standalone Go application for BMC (Baseboard Management Controller) LOGPile is a standalone Go application for BMC diagnostics analysis with an embedded web UI.
diagnostics analysis with an embedded web UI. It runs as a single binary and normalizes hardware data from archives or live Redfish collection.
It runs as a single binary with no external file dependencies.
## Operating modes ## Operating modes
| Mode | Entry point | Description | | Mode | Entry point | Outcome |
|------|-------------|-------------| |------|-------------|---------|
| **Offline / archive** | `POST /api/upload` | Upload a vendor diagnostic archive or a JSON snapshot; parse and display in UI | | Archive upload | `POST /api/upload` | Parse a supported archive, raw export bundle, or JSON snapshot into `AnalysisResult` |
| **Live / Redfish** | `POST /api/collect` | Connect to a live BMC via Redfish API, collect hardware inventory, display and export | | Live collection | `POST /api/collect` | Collect from a live BMC via Redfish and store the result in memory |
| Batch convert | `POST /api/convert` | Convert multiple supported input files into Reanimator JSON in a ZIP artifact |
Both modes produce the same in-memory `AnalysisResult` structure and expose it All modes converge on the same normalized hardware model and exporter pipeline.
through the same API and UI.
## Key capabilities ## In scope
- Single self-contained binary with embedded HTML/JS/CSS (no static file serving required). - Single-binary desktop/server utility with embedded UI
- Vendor archive parsing: Inspur/Kaytus, Dell TSR, NVIDIA HGX Field Diagnostics, - Vendor archive parsing and live Redfish collection
NVIDIA Bug Report, Unraid, XigmaNAS, Generic text fallback. - Canonical hardware inventory across UI and exports
- Live Redfish collection with async progress tracking. - Reopenable raw export bundles for future re-analysis
- Normalized hardware inventory: CPU / RAM / Storage / GPU / PSU / NIC / PCIe / Firmware. - Reanimator export and batch conversion workflows
- Raw `redfish_tree` snapshot stored in `RawPayloads` for future offline re-analysis. - Embedded `pci.ids` lookup for vendor/device name enrichment
- Re-upload of a JSON snapshot for offline work (`/api/upload` accepts `AnalysisResult` JSON).
- Export in CSV, JSON (full `AnalysisResult`), and Reanimator format.
- PCI device model resolution via embedded `pci.ids` (no hardcoded model strings).
## Non-goals (current scope) ## Current vendor coverage
- No persistent storage — all state is in-memory per process lifetime. - Dell TSR
- IPMI collector is a mock scaffold only; real IPMI support is not implemented. - H3C SDS G5/G6
- No authentication layer on the HTTP server. - Inspur / Kaytus
- NVIDIA HGX Field Diagnostics
- NVIDIA Bug Report
- Unraid
- XigmaNAS
- Generic fallback parser
## Non-goals
- Persistent storage or multi-user state
- Production IPMI collection
- Authentication/authorization on the built-in HTTP server
- Long-term server-side job history beyond in-memory process lifetime

View File

@@ -2,114 +2,85 @@
## Runtime stack ## Runtime stack
| Layer | Technology | | Layer | Implementation |
|-------|------------| |-------|----------------|
| Language | Go 1.22+ | | Language | Go 1.22+ |
| HTTP | `net/http`, `http.ServeMux` | | HTTP | `net/http` + `http.ServeMux` |
| UI | Embedded via `//go:embed` in `web/embed.go` (templates + static assets) | | UI | Embedded templates and static assets via `go:embed` |
| State | In-memory only — no database | | State | In-memory only |
| Build | `CGO_ENABLED=0`, single static binary | | Build | `CGO_ENABLED=0`, single binary |
Default port: **8082** Default port: `8082`
## Directory structure ## Code map
``` ```text
cmd/logpile/main.go # Binary entry point, CLI flag parsing cmd/logpile/main.go entrypoint and CLI flags
internal/ internal/server/ HTTP handlers, jobs, upload/export flows
collector/ # Live data collectors internal/collector/ live collection and Redfish replay
registry.go # Collector registration internal/analyzer/ shared analysis helpers
redfish.go # Redfish connector (real implementation) internal/parser/ archive extraction and parser dispatch
ipmi_mock.go # IPMI mock connector (scaffold) internal/exporter/ CSV and Reanimator conversion
types.go # Connector request/progress contracts internal/models/ stable data contracts
parser/ # Archive parsers web/ embedded UI assets
parser.go # BMCParser (dispatcher) + parse orchestration
archive.go # Archive extraction helpers
registry.go # Parser registry + detect/selection
interface.go # VendorParser interface
vendors/ # Vendor-specific parser modules
vendors.go # Import-side-effect registrations
dell/
inspur/
nvidia/
nvidia_bug_report/
unraid/
xigmanas/
generic/
pciids/ # PCI IDs lookup (embedded pci.ids)
server/ # HTTP layer
server.go # Server struct, route registration
handlers.go # All HTTP handler functions
exporter/ # Export formatters
exporter.go # CSV + JSON exporters
reanimator_models.go
reanimator_converter.go
models/ # Shared data contracts
web/
embed.go # go:embed directive
templates/ # HTML templates
static/ # JS / CSS
js/app.js # Frontend — API contract consumer
``` ```
## In-memory state ## Server state
The `Server` struct in `internal/server/server.go` holds: `internal/server.Server` stores:
| Field | Type | Description | | Field | Purpose |
|-------|------|-------------| |------|---------|
| `result` | `*models.AnalysisResult` | Current parsed/collected dataset | | `result` | Current `AnalysisResult` shown in UI and used by exports |
| `detectedVendor` | `string` | Vendor identifier from last parse | | `detectedVendor` | Parser/collector identity for the current dataset |
| `jobManager` | `*JobManager` | Tracks live collect job status/logs | | `rawExport` | Reopenable raw-export package associated with current result |
| `collectors` | `*collector.Registry` | Registered live collection connectors | | `jobManager` | Shared async job state for collect and convert flows |
| `collectors` | Registered live collectors (`redfish`, `ipmi`) |
| `convertOutput` | Temporary ZIP artifacts for batch convert downloads |
State is replaced atomically on successful upload or collect. State is replaced only on successful upload or successful live collection.
On a failed/canceled collect, the previous `result` is preserved unchanged. Failed or canceled jobs do not overwrite the previous dataset.
## Upload flow (`POST /api/upload`) ## Main flows
``` ### Upload
multipart form field: "archive"
├─ file looks like JSON?
│ └─ parse as models.AnalysisResult snapshot → store in Server.result
└─ otherwise
└─ parser.NewBMCParser().ParseFromReader(...)
├─ try all registered vendor parsers (highest confidence wins)
└─ result → store in Server.result
```
## Live collect flow (`POST /api/collect`) 1. `POST /api/upload` receives multipart field `archive`
2. JSON inputs are checked for raw-export package or `AnalysisResult` snapshot
3. Non-JSON inputs go through `parser.BMCParser`
4. Archive metadata is normalized onto `AnalysisResult`
5. Result becomes the current in-memory dataset
``` ### Live collect
validate request (host / protocol / port / username / auth_type / tls_mode)
└─ launch async job
├─ progress callback → job log (queryable via GET /api/collect/{id})
├─ success:
│ set source metadata (source_type=api, protocol, host, date)
│ store result in Server.result
└─ failure / cancel:
previous Server.result unchanged
```
Job lifecycle states: `queued → running → success | failed | canceled` 1. `POST /api/collect` validates request fields
2. Server creates an async job and returns `202 Accepted`
3. Selected collector gathers raw data
4. For Redfish, collector saves `raw_payloads.redfish_tree`
5. Result is normalized, source metadata applied, and state replaced on success
### Batch convert
1. `POST /api/convert` accepts multiple files
2. Each supported file is analyzed independently
3. Successful results are converted to Reanimator JSON
4. Outputs are packaged into a temporary ZIP artifact
5. Client polls job status and downloads the artifact when ready
## Redfish design rule
Live Redfish collection and offline Redfish re-analysis must use the same replay path.
The collector first captures `raw_payloads.redfish_tree`, then the replay logic builds the normalized result.
## PCI IDs lookup ## PCI IDs lookup
Load/override order (`LOGPILE_PCI_IDS_PATH` has highest priority because it is loaded last): Lookup order:
1. Embedded `internal/parser/vendors/pciids/pci.ids` (base dataset compiled into binary) 1. Embedded `internal/parser/vendors/pciids/pci.ids`
2. `./pci.ids` 2. `./pci.ids`
3. `/usr/share/hwdata/pci.ids` 3. `/usr/share/hwdata/pci.ids`
4. `/usr/share/misc/pci.ids` 4. `/usr/share/misc/pci.ids`
5. `/opt/homebrew/share/pciids/pci.ids` 5. `/opt/homebrew/share/pciids/pci.ids`
6. Paths from `LOGPILE_PCI_IDS_PATH` (colon-separated on Unix; later loaded, override same IDs) 6. Extra paths from `LOGPILE_PCI_IDS_PATH`
This means unknown GPU/NIC model strings can be updated by refreshing `pci.ids` Later sources override earlier ones for the same IDs.
without any code change.

View File

@@ -2,38 +2,37 @@
## Conventions ## Conventions
- All endpoints under `/api/`. - All endpoints are under `/api/`
- Request bodies: `application/json` or `multipart/form-data` where noted. - JSON responses are used unless the endpoint downloads a file
- Responses: `application/json` unless file download. - Async jobs share the same status model: `queued`, `running`, `success`, `failed`, `canceled`
- Export filenames follow pattern: `YYYY-MM-DD (SERVER MODEL) - SERVER SN.<ext>` - Export filenames use `YYYY-MM-DD (MODEL) - SERIAL.<ext>` when board metadata exists
--- ## Input endpoints
## Upload & Data Input
### `POST /api/upload` ### `POST /api/upload`
Upload a vendor diagnostic archive or a JSON snapshot. Uploads one file in multipart field `archive`.
**Request:** `multipart/form-data`, field name `archive`.
Server-side multipart limit: **100 MiB**.
Accepted inputs: Accepted inputs:
- `.tar`, `.tar.gz`, `.tgz` — vendor diagnostic archives - supported archive/log formats from the parser registry
- `.txt` — plain text files - `.json` `AnalysisResult` snapshots
- JSON file containing a serialized `AnalysisResult` — re-loaded as-is - raw-export JSON packages
- raw-export ZIP bundles
**Response:** `200 OK` with parsed result summary, or `4xx`/`5xx` on error. Result:
- parses or replays the input
- stores the result as current in-memory state
- returns parsed summary JSON
--- Related helper:
- `GET /api/file-types` returns `archive_extensions`, `upload_extensions`, and `convert_extensions`
## Live Collection
### `POST /api/collect` ### `POST /api/collect`
Start a live collection job (`redfish` or `ipmi`). Starts a live collection job.
Request body:
**Request body:**
```json ```json
{ {
"host": "bmc01.example.local", "host": "bmc01.example.local",
@@ -47,138 +46,125 @@ Start a live collection job (`redfish` or `ipmi`).
``` ```
Supported values: Supported values:
- `protocol`: `redfish` | `ipmi` - `protocol`: `redfish` or `ipmi`
- `auth_type`: `password` | `token` - `auth_type`: `password` or `token`
- `tls_mode`: `strict` | `insecure` - `tls_mode`: `strict` or `insecure`
**Response:** `202 Accepted` Responses:
```json - `202` on accepted job creation
{ - `400` on malformed JSON
"job_id": "job_a1b2c3d4e5f6", - `422` on validation errors
"status": "queued",
"message": "Collection job accepted",
"created_at": "2026-02-23T12:00:00Z"
}
```
Validation behavior:
- `400 Bad Request` for invalid JSON
- `422 Unprocessable Entity` for semantic validation errors (missing/invalid fields)
### `GET /api/collect/{id}` ### `GET /api/collect/{id}`
Poll job status and progress log. Returns async collection job status, progress, timestamps, and accumulated logs.
**Response:**
```json
{
"job_id": "job_a1b2c3d4e5f6",
"status": "running",
"progress": 55,
"logs": ["..."],
"created_at": "2026-02-23T12:00:00Z",
"updated_at": "2026-02-23T12:00:10Z"
}
```
Status values: `queued` | `running` | `success` | `failed` | `canceled`
### `POST /api/collect/{id}/cancel` ### `POST /api/collect/{id}/cancel`
Cancel a running job. Requests cancellation for a running collection job.
--- ### `POST /api/convert`
## Data Queries Starts a batch conversion job that accepts multiple files under `files[]` or `files`.
Each supported file is parsed independently and converted to Reanimator JSON.
Response fields:
- `job_id`
- `status`
- `accepted`
- `skipped`
- `total_files`
### `GET /api/convert/{id}`
Returns batch convert job status using the same async job envelope as collection.
### `GET /api/convert/{id}/download`
Downloads the ZIP artifact produced by a successful convert job.
## Read endpoints
### `GET /api/status` ### `GET /api/status`
Returns source metadata for the current dataset. Returns source metadata for the current dataset.
If nothing is loaded, response is `{ "loaded": false }`.
```json Typical fields:
{ - `loaded`
"loaded": true, - `filename`
"filename": "redfish://bmc01.example.local", - `vendor`
"vendor": "redfish", - `source_type`
"source_type": "api", - `protocol`
"protocol": "redfish", - `target_host`
"target_host": "bmc01.example.local", - `source_timezone`
"collected_at": "2026-02-10T15:30:00Z", - `collected_at`
"stats": { "events": 0, "sensors": 0, "fru": 0 } - `stats`
}
```
`source_type`: `archive` | `api`
When no dataset is loaded, response is `{ "loaded": false }`.
### `GET /api/config` ### `GET /api/config`
Returns source metadata plus: Returns the main UI configuration payload, including:
- source metadata
- `hardware.board` - `hardware.board`
- `hardware.firmware` - `hardware.firmware`
- canonical `hardware.devices` - canonical `hardware.devices`
- computed `specification` summary lines - computed specification lines
### `GET /api/events` ### `GET /api/events`
Returns parsed diagnostic events. Returns events sorted newest first.
### `GET /api/sensors` ### `GET /api/sensors`
Returns sensor readings (temperatures, voltages, fan speeds). Returns parsed sensors plus synthesized PSU voltage sensors when telemetry is available.
### `GET /api/serials` ### `GET /api/serials`
Returns serial numbers built from canonical `hardware.devices`. Returns serial-oriented inventory built from canonical devices.
### `GET /api/firmware` ### `GET /api/firmware`
Returns firmware versions built from canonical `hardware.devices`. Returns firmware-oriented inventory built from canonical devices.
### `GET /api/parse-errors`
Returns normalized parse and collection issues combined from:
- Redfish fetch errors in `raw_payloads`
- raw-export collect logs
- derived partial-inventory warnings
### `GET /api/parsers` ### `GET /api/parsers`
Returns list of registered vendor parsers with their identifiers. Returns registered parser metadata.
--- ### `GET /api/file-types`
## Export Returns supported file extensions for upload and batch convert.
## Export endpoints
### `GET /api/export/csv` ### `GET /api/export/csv`
Download serial numbers as CSV. Downloads serial-number CSV.
### `GET /api/export/json` ### `GET /api/export/json`
Download full `AnalysisResult` as JSON (includes `raw_payloads`). Downloads a raw-export artifact for reopen and re-analysis.
Current implementation emits a ZIP bundle containing:
- `raw_export.json`
- `collect.log`
- `parser_fields.json`
### `GET /api/export/reanimator` ### `GET /api/export/reanimator`
Download hardware data in Reanimator format for asset tracking integration. Downloads Reanimator JSON built from the current normalized result.
See [`07-exporters.md`](07-exporters.md) for full format spec.
--- ## Management endpoints
## Management
### `DELETE /api/clear` ### `DELETE /api/clear`
Clear current in-memory dataset. Clears current in-memory dataset, raw export state, and temporary convert artifacts.
### `POST /api/shutdown` ### `POST /api/shutdown`
Gracefully shut down the server process. Gracefully shuts down the process after responding.
This endpoint terminates the current process after responding.
---
## Source metadata fields
Fields present in `/api/status` and `/api/config`:
| Field | Values |
|-------|--------|
| `source_type` | `archive` \| `api` |
| `protocol` | `redfish` \| `ipmi` (may be empty for archive uploads) |
| `target_host` | IP or hostname |
| `collected_at` | RFC3339 timestamp |

View File

@@ -1,104 +1,87 @@
# 04 — Data Models # 04 — Data Models
## AnalysisResult ## Core contract: `AnalysisResult`
`internal/models/` — the central data contract shared by parsers, collectors, exporters, and the HTTP layer. `internal/models/models.go` defines the shared result passed between parsers, collectors, server handlers, and exporters.
**Stability rule:** Never break the JSON shape of `AnalysisResult`. Stability rule:
Backward-compatible additions are allowed; removals or renames are not. - do not rename or remove JSON fields from `AnalysisResult`
- additive fields are allowed
- UI and exporter compatibility depends on this shape remaining stable
Key top-level fields: Key fields:
| Field | Type | Description | | Field | Meaning |
|-------|------|-------------| |------|---------|
| `filename` | `string` | Uploaded filename or generated live source identifier | | `filename` | Original upload name or synthesized live source name |
| `source_type` | `string` | `archive` or `api` | | `source_type` | `archive` or `api` |
| `protocol` | `string` | `redfish`, `ipmi`, or empty for archive uploads | | `protocol` | `redfish`, `ipmi`, or empty for archive uploads |
| `target_host` | `string` | BMC host for live collection | | `target_host` | Hostname or IP for live collection |
| `collected_at` | `time.Time` | Upload/collection timestamp | | `source_timezone` | Source timezone/offset if known |
| `hardware` | `*HardwareConfig` | All normalized hardware inventory | | `collected_at` | Canonical collection/upload time |
| `events` | `[]Event` | Diagnostic events from parsers | | `raw_payloads` | Raw source data used for replay or diagnostics |
| `fru` | `[]FRUInfo` | FRU/SDR-derived inventory details | | `events` | Parsed event timeline |
| `sensors` | `[]SensorReading` | Sensor readings | | `fru` | FRU-derived inventory details |
| `raw_payloads` | `map[string]any` | Raw vendor data (e.g. `redfish_tree`) | | `sensors` | Sensor readings |
| `hardware` | Normalized hardware inventory |
`raw_payloads` is the durable source for offline re-analysis (especially for Redfish). ## `HardwareConfig`
Normalized fields should be treated as derivable output from raw source data.
### Hardware sub-structure Main sections:
``` ```text
HardwareConfig hardware.board
├── board BoardInfo — server/motherboard identity hardware.devices
├── devices []HardwareDevice — CANONICAL INVENTORY (see below) hardware.cpus
├── cpus []CPU hardware.memory
├── memory []MemoryDIMM hardware.storage
├── storage []Storage hardware.volumes
├── volumes []StorageVolume — logical RAID/VROC volumes hardware.pcie_devices
├── pcie_devices []PCIeDevice hardware.gpus
├── gpus []GPU hardware.network_adapters
├── network_adapters []NetworkAdapter hardware.network_cards
├── network_cards []NIC (legacy/alternate source field) hardware.power_supplies
├── power_supplies []PSU hardware.firmware
└── firmware []FirmwareInfo
``` ```
--- `network_cards` is legacy/alternate source data.
`hardware.devices` is the canonical cross-section inventory.
## Canonical Device Repository (`hardware.devices`) ## Canonical inventory: `hardware.devices`
`hardware.devices` is the **single source of truth** for hardware inventory. `hardware.devices` is the single source of truth for device-oriented UI and Reanimator export.
### Rules — must not be violated Required rules:
1. All UI tabs displaying hardware components **must read from `hardware.devices`**. 1. UI hardware views must read from `hardware.devices`
2. The Device Inventory tab shows kinds: `pcie`, `storage`, `gpu`, `network`. 2. Reanimator conversion must derive device sections from `hardware.devices`
3. The Reanimator exporter **must use the same `hardware.devices`** as the UI. 3. UI/export mismatches are bugs, not accepted divergence
4. Any discrepancy between UI data and Reanimator export data is a **bug**. 4. New shared device fields belong in `HardwareDevice` first
5. New hardware attributes must be added to the canonical device schema **first**,
then mapped to Reanimator/UI — never the other way around.
6. The exporter should group/filter canonical records by section, not rebuild data
from multiple sources.
### Deduplication logic (applied once by repository builder) Deduplication priority:
| Priority | Key used | | Priority | Key |
|----------|----------| |----------|-----|
| 1 | `serial_number` — usable (not empty, not `N/A`, `NA`, `NONE`, `NULL`, `UNKNOWN`, `-`) | | 1 | usable `serial_number` |
| 2 | `bdf` — PCI Bus:Device.Function address | | 2 | `bdf` |
| 3 | No merge — records remain distinct if both serial and bdf are absent | | 3 | keep records separate |
### Device schema alignment ## Raw payloads
Keep `hardware.devices` schema as close as possible to Reanimator JSON field names. `raw_payloads` is authoritative for replayable sources.
This minimizes translation logic in the exporter and prevents drift.
--- Current important payloads:
- `redfish_tree`
- `redfish_fetch_errors`
- `source_timezone`
## Source metadata fields (stored directly on `AnalysisResult`) Normalized hardware fields are derived output, not the long-term source of truth.
Carried by both `/api/status` and `/api/config`: ## Raw export package
```json `/api/export/json` produces a reopenable raw-export artifact.
{
"source_type": "api",
"protocol": "redfish",
"target_host": "10.0.0.1",
"collected_at": "2026-02-10T15:30:00Z"
}
```
Valid `source_type` values: `archive`, `api`
Valid `protocol` values: `redfish`, `ipmi` (empty is allowed for archive uploads)
---
## Raw Export Package (reopenable artifact)
`Export Raw Data` does not merely dump `AnalysisResult`; it emits a reopenable raw package
(JSON or ZIP bundle) that carries source data required for re-analysis.
Design rules: Design rules:
- raw source is authoritative (`redfish_tree` or original file bytes) - raw source stays authoritative
- imports must re-analyze from raw source - uploads of raw-export artifacts must re-analyze from raw source
- parsed field snapshots included in bundles are diagnostic artifacts, not the source of truth - parsed snapshots inside the bundle are diagnostic only

View File

@@ -3,107 +3,69 @@
Collectors live in `internal/collector/`. Collectors live in `internal/collector/`.
Core files: Core files:
- `internal/collector/registry.go` — connector registry (`redfish`, `ipmi`) - `registry.go` for protocol registration
- `internal/collector/redfish.go` — real Redfish connector - `redfish.go` for live collection
- `internal/collector/ipmi_mock.go` — IPMI mock connector scaffold - `redfish_replay.go` for replay from raw payloads
- `internal/collector/types.go` — request/progress contracts - `ipmi_mock.go` for the placeholder IPMI implementation
- `types.go` for request/progress contracts
--- ## Redfish collector
## Redfish Collector (`redfish`) Status: active production path.
**Status:** Production-ready. Request fields passed from the server:
- `host`
- `port`
- `username`
- `auth_type`
- credential field (`password` or token)
- `tls_mode`
### Request contract (from server) ### Core rule
Passed through from `/api/collect` after validation: Live collection and replay must stay behaviorally aligned.
- `host`, `port`, `username` If the collector adds a fallback, probe, or normalization rule, replay must mirror it.
- `auth_type=password|token` (+ matching credential field)
- `tls_mode=strict|insecure`
### Discovery ### Discovery model
Dynamic — does not assume fixed paths. Discovers: The collector does not rely on one fixed vendor tree.
- `Systems` collection → per-system resources It discovers and follows Redfish resources dynamically from root collections such as:
- `Chassis` collection → enclosure/board data - `Systems`
- `Managers` collection → BMC/firmware info - `Chassis`
- `Managers`
### Collected data ### Stored raw data
| Category | Notes | Important raw payloads:
|----------|-------| - `raw_payloads.redfish_tree`
| CPU | Model, cores, threads, socket, status | - `raw_payloads.redfish_fetch_errors`
| Memory | DIMM slot, size, type, speed, serial, manufacturer | - `raw_payloads.source_timezone` when available
| Storage | Slot, type, model, serial, firmware, interface, status |
| GPU | Detected via PCIe class + NVIDIA vendor ID |
| PSU | Model, serial, wattage, firmware, telemetry (input/output power, voltage) |
| NIC | Model, serial, port count, BDF |
| PCIe | Slot, vendor_id, device_id, BDF, link width/speed |
| Firmware | BIOS, BMC versions |
### Raw snapshot ### Snapshot crawler rules
Full Redfish response tree is stored in `result.RawPayloads["redfish_tree"]`. - bounded by `LOGPILE_REDFISH_SNAPSHOT_MAX_DOCS`
This allows future offline re-analysis without re-collecting from a live BMC. - prioritized toward high-value inventory paths
- tolerant of expected vendor-specific failures
- normalizes `@odata.id` values before queueing
### Unified Redfish analysis pipeline (live == replay) ### Redfish implementation guidance
LOGPile uses a **single Redfish analyzer path**: When changing collection logic:
1. Live collector crawls the Redfish API and builds `raw_payloads.redfish_tree` 1. Prefer alternate-path support over vendor hardcoding
2. Parsed result is produced by replaying that tree through the same analyzer used by raw import 2. Keep expensive probing bounded
3. Deduplicate by serial, then BDF, then location/model fallbacks
4. Preserve replay determinism from saved raw payloads
5. Add tests for both the motivating topology and a negative case
This guarantees that live collection and `Export Raw Data` re-open/re-analyze produce the same ### Known vendor fallbacks
normalized output for the same `redfish_tree`.
### Snapshot crawler behavior (important) - empty standard drive collections may trigger bounded `Disk.Bay` probing
- `Storage.Links.Enclosures[*]` may be followed to recover physical drives
- `PowerSubsystem/PowerSupplies` is preferred over legacy `Power` when available
The Redfish snapshot crawler is intentionally: ## IPMI collector
- **bounded** (`LOGPILE_REDFISH_SNAPSHOT_MAX_DOCS`)
- **prioritized** (PCIe, Fabrics, FirmwareInventory, Storage, PowerSubsystem, ThermalSubsystem)
- **tolerant** (skips noisy expected failures, strips `#fragment` from `@odata.id`)
Design notes: Status: mock scaffold only.
- Queue capacity is sized to snapshot cap to avoid worker deadlocks on large trees.
- UI progress is coarse and human-readable; detailed per-request diagnostics are available via debug logs.
- `LOGPILE_REDFISH_DEBUG=1` and `LOGPILE_REDFISH_SNAPSHOT_DEBUG=1` enable console diagnostics.
### Parsing guidelines It remains registered for protocol completeness, but it is not a real collection path.
When adding Redfish mappings, follow these principles:
- Support alternate collection paths (resources may appear at different odata URLs).
- Follow `@odata.id` references and handle embedded `Members` arrays.
- Prefer **raw-tree replay compatibility**: if live collector adds a fallback/probe, replay analyzer must mirror it.
- Deduplicate by serial / BDF / slot+model (in that priority order).
- Prefer tolerant/fallback parsing — missing fields should be silently skipped,
not cause the whole collection to fail.
### Vendor-specific storage fallbacks (Supermicro and similar)
When standard `Storage/.../Drives` collections are empty, collector/replay may recover drives via:
- `Storage.Links.Enclosures[*] -> .../Drives`
- direct probing of finite `Disk.Bay` candidates (`Disk.Bay.0`, `Disk.Bay0`, `.../0`)
This is required for some BMCs that publish drive inventory in vendor-specific paths while leaving
standard collections empty.
### PSU source preference (newer Redfish)
PSU inventory source order:
1. `Chassis/*/PowerSubsystem/PowerSupplies` (preferred on X14+/newer Redfish)
2. `Chassis/*/Power` (legacy fallback)
### Progress reporting
The collector emits progress log entries at each stage (connecting, enumerating systems,
collecting CPUs, etc.) so the UI can display meaningful status.
Current progress message strings are user-facing and may be localized.
---
## IPMI Collector (`ipmi`)
**Status:** Mock scaffold only — not implemented.
Registered in the collector registry but returns placeholder data.
Real IPMI support is a future work item.

View File

@@ -2,261 +2,69 @@
## Framework ## Framework
### Registration Parsers live in `internal/parser/` and vendor implementations live in `internal/parser/vendors/`.
Each vendor parser registers itself via Go's `init()` side-effect import pattern. Core behavior:
- registration uses `init()` side effects
- all registered parsers run `Detect()`
- the highest-confidence parser wins
- generic fallback stays last and low-confidence
All registrations are collected in `internal/parser/vendors/vendors.go`: `VendorParser` contract:
```go
import (
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/inspur"
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/dell"
// etc.
)
```
### VendorParser interface
```go ```go
type VendorParser interface { type VendorParser interface {
Name() string // human-readable name Name() string
Vendor() string // vendor identifier string Vendor() string
Version() string // parser version (increment on logic changes) Version() string
Detect(files []ExtractedFile) int // confidence 0100 Detect(files []ExtractedFile) int
Parse(files []ExtractedFile) (*models.AnalysisResult, error) Parse(files []ExtractedFile) (*models.AnalysisResult, error)
} }
``` ```
### Selection logic ## Adding a parser
All registered parsers run `Detect()` against the uploaded archive's file list. 1. Create `internal/parser/vendors/<vendor>/`
The parser with the **highest confidence score** is selected. 2. Start from `internal/parser/vendors/template/parser.go.template`
Multiple parsers may return >0; only the top scorer is used. 3. Implement `Detect()` and `Parse()`
4. Add a blank import in `internal/parser/vendors/vendors.go`
5. Add at least one positive and one negative detection test
### Adding a new vendor parser ## Data quality rules
1. `mkdir -p internal/parser/vendors/VENDORNAME` ### System firmware only in `hardware.firmware`
2. Copy `internal/parser/vendors/template/parser.go.template` as starting point.
3. Implement `Detect()` and `Parse()`.
4. Add blank import to `vendors/vendors.go`.
`Detect()` tips: `hardware.firmware` must contain system-level firmware only.
- Look for unique filenames or directory names. Device-bound firmware belongs on the device record and must not be duplicated at the top level.
- Check file content for vendor-specific markers.
- Return 70+ only when confident; return 0 if clearly not a match.
### Parser versioning ### Strip embedded MAC addresses from model names
Each parser file contains a `parserVersion` constant. If a source embeds ` - XX:XX:XX:XX:XX:XX` in a model/name field, remove that suffix before storing it.
Increment the version whenever parsing logic changes — this helps trace which
version produced a given result.
--- ### Use `pci.ids` for empty or generic PCI model names
## Parser data quality rules When `vendor_id` and `device_id` are known but the model name is missing or generic, resolve the name via `internal/parser/vendors/pciids`.
### FirmwareInfo — system-level only ## Active vendor coverage
`Hardware.Firmware` must contain **only system-level firmware**: BIOS, BMC/iDRAC, | Vendor ID | Input family | Notes |
Lifecycle Controller, CPLD, storage controllers, BOSS adapters. |-----------|--------------|-------|
| `dell` | TSR ZIP archives | Broad hardware, firmware, sensors, lifecycle events |
| `h3c_g5` | H3C SDS G5 bundles | INI/XML/CSV-driven hardware and event parsing |
| `h3c_g6` | H3C SDS G6 bundles | Similar flow with G6-specific files |
| `inspur` | onekeylog archives | FRU/SDR plus optional Redis enrichment |
| `nvidia` | HGX Field Diagnostics | GPU- and fabric-heavy diagnostic input |
| `nvidia_bug_report` | `nvidia-bug-report-*.log.gz` | dmidecode, lspci, NVIDIA driver sections |
| `unraid` | Unraid diagnostics/log bundles | Server and storage-focused parsing |
| `xigmanas` | XigmaNAS plain logs | FreeBSD/NAS-oriented inventory |
| `generic` | fallback | Low-confidence text fallback when nothing else matches |
**Device-bound firmware** (NIC, GPU, PSU, disk, backplane) **must NOT be added to ## Practical guidance
`Hardware.Firmware`**. It belongs to the device's own `Firmware` field and is already
present there. Duplicating it in `Hardware.Firmware` causes double entries in Reanimator.
The Reanimator exporter filters by `FirmwareInfo.DeviceName` prefix and by - Be conservative with high detect scores
`FirmwareInfo.Description` (FQDD prefix). Parsers must cooperate: - Prefer filling missing fields over overwriting stronger source data
- Keep parser version constants current when behavior changes
- Store the device's FQDD (or equivalent slot identifier) in `FirmwareInfo.Description` - Any new vendor-specific filtering or dedup logic must ship with tests for that vendor format
for all firmware entries that come from a per-device inventory source (e.g. Dell
`DCIM_SoftwareIdentity`).
- FQDD prefixes that are device-bound: `NIC.`, `PSU.`, `Disk.`, `RAID.Backplane.`, `GPU.`
### NIC/device model names — strip embedded MAC addresses
Some vendors (confirmed: Dell TSR) embed the MAC address in the device model name field,
e.g. `ProductName = "NVIDIA ConnectX-6 Lx 2x 25G SFP28 OCP3.0 SFF - C4:70:BD:DB:56:08"`.
**Rule:** Strip any ` - XX:XX:XX:XX:XX:XX` suffix from model/name strings before storing
them in `FirmwareInfo.DeviceName`, `NetworkAdapter.Model`, or any other model field.
Use `nicMACInModelRE` (defined in the Dell parser) or an equivalent regex:
```
\s+-\s+([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$
```
This applies to **all** string fields used as device names or model identifiers.
### PCI device name enrichment via pci.ids
If a PCIe device, GPU, NIC, or any hardware component has a `vendor_id` + `device_id`
but its model/name field is **empty or generic** (e.g. blank, equals the description,
or is just a raw hex ID), the parser **must** attempt to resolve the human-readable
model name from the embedded `pci.ids` database before storing the result.
**Rule:** When `Model` (or equivalent name field) is empty and both `VendorID` and
`DeviceID` are non-zero, call the pciids lookup and use the result as the model name.
```go
// Example pattern — use in any parser that handles PCIe/GPU/NIC devices:
if strings.TrimSpace(device.Model) == "" && device.VendorID != 0 && device.DeviceID != 0 {
if name := pciids.Lookup(device.VendorID, device.DeviceID); name != "" {
device.Model = name
}
}
```
This rule applies to all vendor parsers. The pciids package is available at
`internal/parser/vendors/pciids`. See ADL-005 for the rationale.
**Do not hardcode model name strings.** If a device is unknown today, it will be
resolved automatically once `pci.ids` is updated.
---
## Vendor parsers
### Inspur / Kaytus (`inspur`)
**Status:** Ready. Tested on KR4268X2 (onekeylog format).
**Archive format:** `.tar.gz` onekeylog
**Primary source files:**
| File | Content |
|------|---------|
| `asset.json` | Base hardware inventory |
| `component.log` | Component list |
| `devicefrusdr.log` | FRU and SDR data |
| `onekeylog/runningdata/redis-dump.rdb` | Runtime enrichment (optional) |
**Redis RDB enrichment** (applied conservatively — fills missing fields only):
- GPU: `serial_number`, `firmware` (VBIOS/FW), runtime telemetry
- NIC: firmware, serial, part number (when text logs leave fields empty)
**Module structure:**
```
inspur/
parser.go — main parser + registration
sdr.go — sensor/SDR parsing
fru.go — FRU serial parsing
asset.go — asset.json parsing
syslog.go — syslog parsing
```
---
### Dell TSR (`dell`)
**Status:** Ready (v3.0). Tested on nested TSR archives with embedded `*.pl.zip`.
**Archive format:** `.zip` (outer archive + nested `*.pl.zip`)
**Primary source files:**
- `tsr/metadata.json`
- `tsr/hardware/sysinfo/inventory/sysinfo_DCIM_View.xml`
- `tsr/hardware/sysinfo/inventory/sysinfo_DCIM_SoftwareIdentity.xml`
- `tsr/hardware/sysinfo/inventory/sysinfo_CIM_Sensor.xml`
- `tsr/hardware/sysinfo/lcfiles/curr_lclog.xml`
**Extracted data:**
- Board/system identity and BIOS/iDRAC firmware
- CPU, memory, physical disks, virtual disks, PSU, NIC, PCIe
- GPU inventory (`DCIM_VideoView`) + GPU sensor enrichment (`DCIM_GPUSensor`)
- Controller/backplane inventory (`DCIM_ControllerView`, `DCIM_EnclosureView`)
- Sensor readings (temperature/voltage/current/power/fan/utilization)
- Lifecycle events (`curr_lclog.xml`)
---
### NVIDIA HGX Field Diagnostics (`nvidia`)
**Status:** Ready (v1.1.0). Works with any server vendor.
**Archive format:** `.tar` / `.tar.gz`
**Confidence scoring:**
| File | Score |
|------|-------|
| `unified_summary.json` with "HGX Field Diag" marker | +40 |
| `summary.json` | +20 |
| `summary.csv` | +15 |
| `gpu_fieldiag/` directory | +15 |
**Source files:**
| File | Content |
|------|---------|
| `output.log` | dmidecode — server manufacturer, model, serial number |
| `unified_summary.json` | GPU details, NVSwitch devices, PCI addresses |
| `summary.json` | Diagnostic test results and error codes |
| `summary.csv` | Alternative test results format |
**Extracted data:**
- GPUs: slot, model, manufacturer, firmware (VBIOS), BDF
- NVSwitch devices: slot, device_class, vendor_id, device_id, BDF, link speed/width
- Events: diagnostic test failures (connectivity, gpumem, gpustress, pcie, nvlink, nvswitch, power)
**Severity mapping:**
- `info` — tests passed
- `warning` — e.g. "Row remapping failed"
- `critical` — error codes 300+
**Known limitations:**
- Detailed logs in `gpu_fieldiag/*.log` are not parsed.
- No CPU, memory, or storage extraction (not present in field diag archives).
---
### NVIDIA Bug Report (`nvidia_bug_report`)
**Status:** Ready (v1.0.0).
**File format:** `nvidia-bug-report-*.log.gz` (gzip-compressed text)
**Confidence:** 85 (high priority for matching filename pattern)
**Source sections parsed:**
| dmidecode section | Extracts |
|-------------------|---------|
| System Information | server serial, UUID, manufacturer, product name |
| Processor Information | CPU model, serial, core/thread count, frequency |
| Memory Device | DIMM slot, size, type, manufacturer, serial, part number, speed |
| System Power Supply | PSU location, manufacturer, model, serial, wattage, firmware, status |
| Other source | Extracts |
|--------------|---------|
| `lspci -vvv` (Ethernet/Network/IB) | NIC model (from VPD), BDF, slot, P/N, S/N, port count, port type |
| `/proc/driver/nvidia/gpus/*/information` | GPU model, BDF, UUID, VBIOS version, IRQ |
| NVRM version line | NVIDIA driver version |
**Known limitations:**
- Driver error/warning log lines not yet extracted.
- GPU temperature/utilization metrics require additional parsing sections.
---
### XigmaNAS (`xigmanas`)
**Status:** Ready.
**Archive format:** Plain log files (FreeBSD-based NAS system)
**Detection:** Files named `xigmanas`, `system`, or `dmesg`; content containing "XigmaNAS" or "FreeBSD"; SMART data presence.
**Extracted data:**
- System: firmware version, uptime, CPU model, memory configuration, hardware platform
- Storage: disk models, serial numbers, capacity, health, SMART temperatures
- Populates: `Hardware.Firmware`, `Hardware.CPUs`, `Hardware.Memory`, `Hardware.Storage`, `Sensors`
---
### Unraid (`unraid`)
**Status:** Ready (v1.0.0).
**Archive format:** Unraid diagnostics archive contents (text-heavy diagnostics directories). **Archive format:** Unraid diagnostics archive contents (text-heavy diagnostics directories).

View File

@@ -1,366 +1,63 @@
# 07 — Exporters & Reanimator Integration # 07 — Exporters
## Export endpoints summary ## Export surfaces
| Endpoint | Format | Filename pattern | | Endpoint | Output | Purpose |
|----------|--------|-----------------| |----------|--------|---------|
| `GET /api/export/csv` | CSV — serial numbers | `YYYY-MM-DD (MODEL) - SN.csv` | | `GET /api/export/csv` | CSV | Serial-number export |
| `GET /api/export/json` | **Raw export package** (JSON or ZIP bundle) for reopen/re-analysis | `YYYY-MM-DD (MODEL) - SN.(json|zip)` | | `GET /api/export/json` | raw-export ZIP bundle | Reopen and re-analyze later |
| `GET /api/export/reanimator` | Reanimator hardware JSON | `YYYY-MM-DD (MODEL) - SN.json` | | `GET /api/export/reanimator` | JSON | Reanimator hardware payload |
| `POST /api/convert` | async ZIP artifact | Batch archive-to-Reanimator conversion |
--- ## Raw export
## Raw Export (`Export Raw Data`) Raw export is not a final report dump.
It is a replayable artifact that preserves enough source data for future parser improvements.
### Purpose Current bundle contents:
- `raw_export.json`
- `collect.log`
- `parser_fields.json`
Preserve enough source data to reproduce parsing later after parser fixes, without requiring Design rules:
another live collection from the target system. - raw source is authoritative
- uploads of raw export must replay from raw source
- parsed snapshots inside the bundle are diagnostic only
### Format ## Reanimator export
`/api/export/json` returns a **raw export package**: Implementation files:
- JSON package (machine-readable), or - `internal/exporter/reanimator_models.go`
- ZIP bundle containing: - `internal/exporter/reanimator_converter.go`
- `raw_export.json` — machine-readable package - `internal/server/handlers.go`
- `collect.log` — human-readable collection + parsing summary
- `parser_fields.json` — structured parsed field snapshot for diffs between parser versions
### Import / reopen behavior Conversion rules:
- canonical source is `hardware.devices`
- timestamps are RFC3339
- status is normalized to Reanimator-friendly values
- missing PCIe serials may be generated from board serial + slot
- `NULL`-style board manufacturer/product values are treated as absent
When a raw export package is uploaded back into LOGPile: ## Inclusion rules
- the app **re-analyzes from raw source**
- it does **not** trust embedded parsed output as source of truth
For Redfish, this means replay from `raw_payloads.redfish_tree`. Included:
- empty memory slots (`present=false`) for topology visibility
- PCIe-class devices even when serial must be synthesized
### Design rule Excluded:
- storage without `serial_number`
- power supplies without `serial_number`
- non-present network adapters
- device-bound firmware duplicated at top-level firmware list
Raw export is a **re-analysis artifact**, not a final report dump. Keep it self-contained and ## Batch convert
forward-compatible where possible (versioned package format, additive fields only).
--- `POST /api/convert` accepts multiple supported files and produces a ZIP with:
- one `*.reanimator.json` file per successful input
- `convert-summary.txt`
## Reanimator Export Behavior:
- unsupported filenames are skipped
### Purpose - each file is parsed independently
- one bad file must not fail the whole batch if at least one conversion succeeds
Exports hardware inventory data in the format expected by the Reanimator asset tracking - result artifact is temporary and deleted after download
system. Enables one-click push from LOGPile to an external asset management platform.
### Implementation files
| File | Role |
|------|------|
| `internal/exporter/reanimator_models.go` | Go structs for Reanimator JSON |
| `internal/exporter/reanimator_converter.go` | `ConvertToReanimator()` and helpers |
| `internal/server/handlers.go` | `handleExportReanimator()` HTTP handler |
### Conversion rules
- Source: canonical `hardware.devices` repository (see [`04-data-models.md`](04-data-models.md))
- CPU manufacturer inferred from model string (Intel / AMD / ARM / Ampere)
- PCIe serial number generated when absent: `{board_serial}-PCIE-{slot}`
- Status values normalized to: `OK`, `Warning`, `Critical`, `Unknown` (`Empty` only for memory slots)
- Timestamps in RFC3339 format
- `target_host` derived from `filename` field (`redfish://…`, `ipmi://…`) if not in source; omitted if undeterminable
- `board.manufacturer` and `board.product_name` values of `"NULL"` treated as absent
### LOGPile → Reanimator field mapping
| LOGPile type | Reanimator section | Notes |
|---|---|---|
| `BoardInfo` | `board` | Direct mapping |
| `CPU` | `cpus` | + manufacturer (inferred) |
| `MemoryDIMM` | `memory` | Direct; empty slots included (`present=false`) |
| `Storage` | `storage` | Excluded if no `serial_number` |
| `PCIeDevice` | `pcie_devices` | Serial generated if missing |
| `GPU` | `pcie_devices` | `device_class=DisplayController` |
| `NetworkAdapter` | `pcie_devices` | `device_class=NetworkController` |
| `PSU` | `power_supplies` | Excluded if no serial or `present=false` |
| `FirmwareInfo` | `firmware` | Direct mapping |
### Inclusion / exclusion rules
**Included:**
- Memory slots with `present=false` (as Empty slots)
- PCIe devices without serial number (serial is generated)
**Excluded:**
- Storage without `serial_number`
- PSU without `serial_number` or with `present=false`
- NetworkAdapters with `present=false`
---
## Reanimator Integration Guide
This section documents the Reanimator receiver-side JSON format (what the Reanimator
system expects when it ingests a LOGPile export).
> **Important:** The Reanimator endpoint uses a strict JSON decoder (`DisallowUnknownFields`).
> Any unknown field — including nested ones — causes `400 Bad Request`.
> Use only `snake_case` keys listed here.
### Top-level structure
```json
{
"filename": "redfish://10.10.10.103",
"source_type": "api",
"protocol": "redfish",
"target_host": "10.10.10.103",
"collected_at": "2026-02-10T15:30:00Z",
"hardware": {
"board": {...},
"firmware": [...],
"cpus": [...],
"memory": [...],
"storage": [...],
"pcie_devices": [...],
"power_supplies": [...]
}
}
```
**Required:** `collected_at`, `hardware.board.serial_number`
**Optional:** `target_host`, `source_type`, `protocol`, `filename`
`source_type` values: `api`, `logfile`, `manual`
`protocol` values: `redfish`, `ipmi`, `snmp`, `ssh`
### Component status fields (all component sections)
Each component may carry:
| Field | Type | Description |
|-------|------|-------------|
| `status` | string | `OK`, `Warning`, `Critical`, `Unknown`, `Empty` |
| `status_checked_at` | RFC3339 | When status was last verified |
| `status_changed_at` | RFC3339 | When status last changed |
| `status_at_collection` | object | `{ "status": "...", "at": "..." }` — snapshot-time status |
| `status_history` | array | `[{ "status": "...", "changed_at": "...", "details": "..." }]` |
| `error_description` | string | Human-readable error for Warning/Critical |
### Board
```json
{
"board": {
"manufacturer": "Supermicro",
"product_name": "X12DPG-QT6",
"serial_number": "21D634101",
"part_number": "X12DPG-QT6-REV1.01",
"uuid": "d7ef2fe5-2fd0-11f0-910a-346f11040868"
}
}
```
`serial_number` required. `manufacturer` / `product_name` of `"NULL"` treated as absent.
### CPUs
```json
{
"socket": 0,
"model": "INTEL(R) XEON(R) GOLD 6530",
"cores": 32,
"threads": 64,
"frequency_mhz": 2100,
"max_frequency_mhz": 4000,
"manufacturer": "Intel",
"status": "OK"
}
```
`socket` (int) and `model` required. Serial generated: `{board_serial}-CPU-{socket}`.
LOT format: `CPU_{VENDOR}_{MODEL_NORMALIZED}` → e.g. `CPU_INTEL_XEON_GOLD_6530`
### Memory
```json
{
"slot": "CPU0_C0D0",
"location": "CPU0_C0D0",
"present": true,
"size_mb": 32768,
"type": "DDR5",
"max_speed_mhz": 4800,
"current_speed_mhz": 4800,
"manufacturer": "Hynix",
"serial_number": "80AD032419E17CEEC1",
"part_number": "HMCG88AGBRA191N",
"status": "OK"
}
```
`slot` and `present` required. `serial_number` required when `present=true`.
Empty slots (`present=false`, `status="Empty"`) are included but no component created.
LOT format: `DIMM_{TYPE}_{SIZE_GB}GB` → e.g. `DIMM_DDR5_32GB`
### Storage
```json
{
"slot": "OB01",
"type": "NVMe",
"model": "INTEL SSDPF2KX076T1",
"size_gb": 7680,
"serial_number": "BTAX41900GF87P6DGN",
"manufacturer": "Intel",
"firmware": "9CV10510",
"interface": "NVMe",
"present": true,
"status": "OK"
}
```
`slot`, `model`, `serial_number`, `present` required.
LOT format: `{TYPE}_{INTERFACE}_{SIZE_TB}TB` → e.g. `SSD_NVME_07.68TB`
### Power Supplies
```json
{
"slot": "0",
"present": true,
"model": "GW-CRPS3000LW",
"vendor": "Great Wall",
"wattage_w": 3000,
"serial_number": "2P06C102610",
"part_number": "V0310C9000000000",
"firmware": "00.03.05",
"status": "OK",
"input_power_w": 137,
"output_power_w": 104,
"input_voltage": 215.25
}
```
`slot`, `present` required. `serial_number` required when `present=true`.
Telemetry fields (`input_power_w`, `output_power_w`, `input_voltage`) stored in observation only.
LOT format: `PSU_{WATTAGE}W_{VENDOR_NORMALIZED}` → e.g. `PSU_3000W_GREAT_WALL`
### PCIe Devices
```json
{
"slot": "PCIeCard1",
"vendor_id": 32902,
"device_id": 2912,
"bdf": "0000:18:00.0",
"device_class": "MassStorageController",
"manufacturer": "Intel",
"model": "RAID Controller RSP3DD080F",
"link_width": 8,
"link_speed": "Gen3",
"max_link_width": 8,
"max_link_speed": "Gen3",
"serial_number": "RAID-001-12345",
"firmware": "50.9.1-4296",
"status": "OK"
}
```
`slot` required. Serial generated if absent: `{board_serial}-PCIE-{slot}`.
`device_class` values: `NetworkController`, `MassStorageController`, `DisplayController`, etc.
LOT format: `PCIE_{DEVICE_CLASS}_{MODEL_NORMALIZED}` → e.g. `PCIE_NETWORK_CONNECTX5`
### Firmware
```json
[
{ "device_name": "BIOS", "version": "06.08.05" },
{ "device_name": "BMC", "version": "5.17.00" }
]
```
Both fields required. Changes trigger `FIRMWARE_CHANGED` timeline events.
---
### Import process (Reanimator side)
1. Validate `collected_at` (RFC3339) and `hardware.board.serial_number`.
2. Find or create Asset by `board.serial_number``vendor_serial`.
3. For each component: filter `present=false`, auto-determine LOT, find or create Component,
create Observation, update Installations.
4. Detect removed components (present in previous snapshot, absent in current) → close Installation.
5. Generate timeline events: `LOG_COLLECTED`, `INSTALLED`, `REMOVED`, `FIRMWARE_CHANGED`.
**Idempotency:** Repeated import of the same snapshot (same content hash) returns `200 OK`
with `"duplicate": true` and does not create duplicate records.
### Reanimator API endpoint
```http
POST /ingest/hardware
Content-Type: application/json
```
**Success (201):**
```json
{
"status": "success",
"bundle_id": "lb_01J...",
"asset_id": "mach_01J...",
"collected_at": "2026-02-10T15:30:00Z",
"duplicate": false,
"summary": {
"parts_observed": 15,
"parts_created": 2,
"installations_created": 2,
"timeline_events_created": 9
}
}
```
**Duplicate (200):**
```json
{ "status": "success", "duplicate": true, "message": "LogBundle with this content hash already exists" }
```
**Error (400):**
```json
{ "status": "error", "error": "validation_failed", "details": { "field": "...", "message": "..." } }
```
Common `400` causes:
- Unknown JSON field (strict decoder)
- Wrong key name (e.g. `targetHost` instead of `target_host`)
- Invalid `collected_at` format (must be RFC3339)
- Empty `hardware.board.serial_number`
### LOT normalization rules
1. Remove special chars `( ) - ® ™`; replace spaces with `_`
2. Uppercase all
3. Collapse multiple underscores to one
4. Strip common prefixes like `MODEL:`, `PN:`
### Status values
| Value | Meaning | Action |
|-------|---------|--------|
| `OK` | Normal | — |
| `Warning` | Degraded | Create `COMPONENT_WARNING` event (optional) |
| `Critical` | Failed | Auto-create `failure_event`, create `COMPONENT_FAILED` event |
| `Unknown` | Not determinable | Treat as working |
| `Empty` | Slot unpopulated | No component created (memory/PCIe only) |
### Missing field handling
| Field | Fallback |
|-------|---------|
| CPU serial | Generated: `{board_serial}-CPU-{socket}` |
| PCIe serial | Generated: `{board_serial}-PCIE-{slot}` |
| Other serial | Component skipped if absent |
| manufacturer (PCIe) | Looked up from `vendor_id` (8086→Intel, 10de→NVIDIA, 15b3→Mellanox…) |
| status | Treated as `Unknown` |
| firmware | No `FIRMWARE_CHANGED` event |

View File

@@ -4,86 +4,74 @@
Defined in `cmd/logpile/main.go`: Defined in `cmd/logpile/main.go`:
| Flag | Default | Description | | Flag | Default | Purpose |
|------|---------|-------------| |------|---------|---------|
| `--port` | `8082` | HTTP server port | | `--port` | `8082` | HTTP server port |
| `--file` | — | Reserved for archive preload (not active) | | `--file` | empty | Preload archive file |
| `--version` | | Print version and exit | | `--version` | `false` | Print version and exit |
| `--no-browser` | | Do not open browser on start | | `--no-browser` | `false` | Do not auto-open browser |
| `--hold-on-crash` | `true` on Windows | Keep console open on fatal crash for debugging | | `--hold-on-crash` | `true` on Windows | Keep console open after fatal crash |
## Build ## Common commands
```bash ```bash
# Local binary (current OS/arch)
make build make build
# Output: bin/logpile
# Cross-platform binaries
make build-all make build-all
# Output: make test
# bin/logpile-linux-amd64 make fmt
# bin/logpile-linux-arm64
# bin/logpile-darwin-amd64
# bin/logpile-darwin-arm64
# bin/logpile-windows-amd64.exe
```
Both `make build` and `make build-all` run `scripts/update-pci-ids.sh --best-effort`
before compilation to sync `pci.ids` from the submodule.
To skip PCI IDs update:
```bash
SKIP_PCI_IDS_UPDATE=1 make build
```
Build flags: `CGO_ENABLED=0` — fully static binary, no C runtime dependency.
## PCI IDs submodule
Source: `third_party/pciids` (git submodule → `github.com/pciutils/pciids`)
Local copy embedded at build time: `internal/parser/vendors/pciids/pci.ids`
```bash
# Manual update
make update-pci-ids make update-pci-ids
```
# Init submodule after fresh clone Notes:
- `make build` outputs `bin/logpile`
- `make build-all` builds the supported cross-platform binaries
- `make build` and `make build-all` run `scripts/update-pci-ids.sh --best-effort` unless `SKIP_PCI_IDS_UPDATE=1`
## PCI IDs
Source submodule: `third_party/pciids`
Embedded copy: `internal/parser/vendors/pciids/pci.ids`
Typical setup after clone:
```bash
git submodule update --init third_party/pciids git submodule update --init third_party/pciids
``` ```
## Release process ## Release script
Run:
```bash ```bash
scripts/release.sh ./scripts/release.sh
``` ```
What it does: Current behavior:
1. Reads version from `git describe --tags` 1. Reads version from `git describe --tags`
2. Validates clean working tree (override: `ALLOW_DIRTY=1`) 2. Refuses a dirty tree unless `ALLOW_DIRTY=1`
3. Sets stable `GOPATH` / `GOCACHE` / `GOTOOLCHAIN` env 3. Sets stable Go cache/toolchain environment
4. Creates `releases/{VERSION}/` directory 4. Creates `releases/{VERSION}/`
5. Generates `RELEASE_NOTES.md` template if not present 5. Creates a release-notes template if missing
6. Builds `darwin-arm64` and `windows-amd64` binaries 6. Builds `darwin-arm64` and `windows-amd64`
7. Packages all binaries found in `bin/` as `.tar.gz` / `.zip` 7. Packages any already-present binaries from `bin/`
8. Generates `SHA256SUMS.txt` 8. Generates `SHA256SUMS.txt`
9. Prints next steps (tag, push, create release manually)
Release notes template is created in `releases/{VERSION}/RELEASE_NOTES.md`. Important limitation:
- `scripts/release.sh` does not run `make build-all` for you
- if you want Linux or additional macOS archives in the release directory, build them before running the script
## Running ## Run locally
```bash ```bash
./bin/logpile ./bin/logpile
./bin/logpile --port 9090 ./bin/logpile --port 9090
./bin/logpile --no-browser ./bin/logpile --no-browser
./bin/logpile --version ./bin/logpile --version
./bin/logpile --hold-on-crash # keep console open on crash (default on Windows)
``` ```
## macOS Gatekeeper ## macOS Gatekeeper
After downloading a binary, remove the quarantine attribute:
```bash ```bash
xattr -d com.apple.quarantine /path/to/logpile-darwin-arm64 xattr -d com.apple.quarantine /path/to/logpile-darwin-arm64
``` ```

View File

@@ -1,134 +1,54 @@
# 09 — Testing # 09 — Testing
## Required before merge ## Baseline
Required before merge:
```bash ```bash
go test ./... go test ./...
``` ```
All tests must pass before any change is merged. ## Test locations
## Where to add tests | Area | Location |
|------|----------|
| Change area | Test location | | Collectors and replay | `internal/collector/*_test.go` |
|-------------|---------------| | HTTP handlers and jobs | `internal/server/*_test.go` |
| Collectors | `internal/collector/*_test.go` |
| HTTP handlers | `internal/server/*_test.go` |
| Exporters | `internal/exporter/*_test.go` | | Exporters | `internal/exporter/*_test.go` |
| Parsers | `internal/parser/vendors/<vendor>/*_test.go` | | Vendor parsers | `internal/parser/vendors/<vendor>/*_test.go` |
## Exporter tests ## General rules
The Reanimator exporter has comprehensive coverage: - Prefer table-driven tests
- No network access in unit tests
- Cover happy path and realistic failure/partial-data cases
- New vendor parsers need both detection and parse coverage
| Test file | Coverage | ## Mandatory coverage for dedup/filter/classify logic
|-----------|----------|
| `reanimator_converter_test.go` | Unit tests per conversion function | Any new deduplication, filtering, or classification function must have:
| `reanimator_integration_test.go` | Full export with realistic `AnalysisResult` |
1. A true-positive case
2. A true-negative case
3. A regression case for the vendor or topology that motivated the change
This is mandatory for inventory logic, firmware filtering, and similar code paths where silent data drift is likely.
## Mandatory coverage for expensive path selection
Any function that decides whether to crawl or probe an expensive path must have:
1. A positive selection case
2. A negative exclusion case
3. A topology-level count/integration case
The goal is to catch runaway I/O regressions before they ship.
## Useful focused commands
Run exporter tests only:
```bash ```bash
go test ./internal/exporter/... go test ./internal/exporter/...
go test ./internal/exporter/... -v -run Reanimator go test ./internal/collector/...
go test ./internal/exporter/... -cover go test ./internal/server/...
``` go test ./internal/parser/vendors/...
## Guidelines
- Prefer table-driven tests for parsing logic (multiple input variants).
- Do not rely on network access in unit tests.
- Test both the happy path and edge cases (missing fields, empty collections).
- When adding a new vendor parser, include at minimum:
- `Detect()` test with a positive and a negative sample file list.
- `Parse()` test with a minimal but representative archive.
## Dedup and filtering functions — mandatory coverage
Any function that deduplicates, filters, or classifies hardware inventory items
**must** have tests covering all three axes before the code is considered done:
| Axis | What to test | Why |
|------|-------------|-----|
| **True positive** | Items that ARE duplicates are collapsed to one | Proves the function works |
| **True negative** | Items that are NOT duplicates are kept separate | Proves the function doesn't over-collapse |
| **Counter-case** | The scenario that motivated the original code still works after changes | Prevents regression from future fixes |
### Worked example — GPU dedup regression (2026-03-11)
`collectGPUsFromProcessors` was added for MSI (chassis Id matches processor Id).
No tests → when Supermicro HGX arrived (chassis Id = "HGX_GPU_SXM_1", processor Id = "GPU_SXM_1"),
the chassis lookup silently returned nothing, serial stayed empty, UUID was new → 8 duplicate GPUs.
Simultaneously, fixing `gpuDocDedupKey` to use `slot|model` before path collapsed two distinct
GraphicsControllers GPUs with the same model into one — breaking an existing test that had no
counter-case for the path-fallback scenario.
**Required test matrix for any dedup function:**
```
TestXxx_CollapsesDuplicates — same item via two sources → 1 result
TestXxx_KeepsDistinct — two different items with same model → 2 results
TestXxx_<VendorThatMotivated> — the specific vendor/setup that triggered the code
```
### Worked example — firmware filter regression (2026-03-12)
`collectFirmwareInventory` was added in `6c19a58` without coverage for Supermicro naming.
`isDeviceBoundFirmwareName` had patterns for Dell-style names (`"GPU SomeDevice"`, `"NIC OnboardLAN"`)
but Supermicro Redfish uses `"GPU1 System Slot0"` and `"NIC1 System Slot0 ..."` — digit follows
immediately after the type prefix. 29 device-bound entries leaked into `hardware.firmware`.
`9c5512d` attempted to fix this with HGX ID patterns (`_fw_gpu_`, etc.) in the wrong field:
the filter checked `DeviceName` but `collectFirmwareInventory` populates it from `Name` first
(`"Software Inventory"` for all HGX per-component slots), not from the `Id` field that contains
the firmware ID like `"HGX_FW_GPU_SXM_1"`. The patterns were effectively dead code from day one.
**Required test matrix for any filter function:**
```
TestXxx_FiltersDeviceBound_Dell — Dell-style names that motivated the original code
TestXxx_FiltersDeviceBound_Supermicro — Supermicro names with digit suffix (GPU1/NIC1)
TestXxx_KeepsSystemLevel — BIOS, BMC, CPLD names must NOT be filtered
```
### Practical rule
When you write a new filter/dedup/classify function, ask:
1. Does my test cover the vendor that motivated this code?
2. Does my test cover a *different* vendor or naming convention where the function must NOT fire?
3. If I change the dedup key logic, do existing tests still exercise the old correct behavior?
4. When the filter checks a field on a model struct, does my test verify that the field is
actually populated by the collector? (Dead-code filter pattern: `9c5512d` `_fw_gpu_` check.)
If any answer is "no" — add the missing test before committing.
## Collector candidate-selection functions — mandatory coverage
Any function that selects paths for an expensive operation (probing, crawling, plan-B retry)
**must** have tests covering:
| Axis | What to test | Why |
|------|-------------|-----|
| **Positive** | Paths that should be selected ARE selected | Proves the feature works |
| **Negative** | Paths that should be excluded ARE excluded | Prevents runaway I/O |
| **Topology integration** | Given a realistic `out` map, the count of selected paths matches expectations | Catches implicit coupling between the selector and the surrounding data shape |
### Worked example — NVMe post-probe regression (2026-03-12)
`shouldAdaptiveNVMeProbe` was added in `2fa4a12` for Supermicro NVMe backplanes that return
`Members: []` but serve disks at `Disk.Bay.N` paths. No topology-level test was added.
When SYS-A21GE-NBRT (HGX B200) arrived, its 35 sub-chassis (GPU, NVSwitch, PCIeRetimer,
ERoT, IRoT, BMC, FPGA) all have `ChassisType=Module/Component/Zone` and empty `/Drives`
all 35 passed the filter → 35 × 384 = 13 440 HTTP requests → 22 min extra per collection.
A topology integration test (`TestNVMePostProbeSkipsNonStorageChassis`) would have caught
this at commit time: given GPU chassis + backplane, exactly 1 candidate must be selected.
**Required test matrix for any path-selection function:**
```
TestXxx_SelectsTargetPath — the path that motivated the code IS selected
TestXxx_SkipsIrrelevantPath — a path that must never be selected IS skipped
TestXxx_TopologyCount — given a realistic multi-chassis map, selected count = N
``` ```

View File

@@ -1,59 +1,41 @@
# LOGPile Bible # LOGPile Bible
> **Documentation language:** English only. All maintained project documentation must be written in English. `bible-local/` is the project-specific source of truth for LOGPile.
> Keep top-level docs minimal and put maintained architecture/API contracts here.
> **Architectural decisions:** Every significant architectural decision **must** be recorded in
> [`10-decisions.md`](10-decisions.md) before or alongside the code change.
>
> **Single source of truth:** Architecture and technical design documentation belongs in `docs/bible/`.
> Keep `README.md` and `CLAUDE.md` minimal to avoid duplicate documentation.
This directory is the single source of truth for LOGPile's architecture, design, and integration contracts. ## Rules
It is structured so that both humans and AI assistants can navigate it quickly.
--- - Documentation language: English only
- Update relevant bible files in the same change as the code
- Record significant architectural decisions in [`10-decisions.md`](10-decisions.md)
- Do not duplicate shared rules from `bible/`
## Reading Map (Hierarchical) ## Read order
### 1. Foundations (read first) | File | Purpose |
|------|---------|
| [01-overview.md](01-overview.md) | Product scope, modes, non-goals |
| [02-architecture.md](02-architecture.md) | Runtime structure, state, main flows |
| [04-data-models.md](04-data-models.md) | Stable data contracts and canonical inventory |
| [03-api.md](03-api.md) | HTTP endpoints and response contracts |
| [05-collectors.md](05-collectors.md) | Live collection behavior |
| [06-parsers.md](06-parsers.md) | Archive parser framework and vendor coverage |
| [07-exporters.md](07-exporters.md) | Raw export, Reanimator export, batch convert |
| [08-build-release.md](08-build-release.md) | Build and release workflow |
| [09-testing.md](09-testing.md) | Test expectations and regression rules |
| [10-decisions.md](10-decisions.md) | Architectural Decision Log |
| File | What it covers | ## Fast orientation
|------|----------------|
| [01-overview.md](01-overview.md) | Product purpose, operating modes, scope |
| [02-architecture.md](02-architecture.md) | Runtime structure, control flow, in-memory state |
| [04-data-models.md](04-data-models.md) | Core contracts (`AnalysisResult`, canonical `hardware.devices`) |
### 2. Runtime Interfaces
| File | What it covers |
|------|----------------|
| [03-api.md](03-api.md) | HTTP API contracts and endpoint behavior |
| [05-collectors.md](05-collectors.md) | Live collection connectors (Redfish, IPMI mock) |
| [06-parsers.md](06-parsers.md) | Archive parser framework and vendor parsers |
| [07-exporters.md](07-exporters.md) | CSV / JSON / Reanimator exports and integration mapping |
### 3. Delivery & Quality
| File | What it covers |
|------|----------------|
| [08-build-release.md](08-build-release.md) | Build, packaging, release workflow |
| [09-testing.md](09-testing.md) | Testing expectations and verification guidance |
### 4. Governance (always current)
| File | What it covers |
|------|----------------|
| [10-decisions.md](10-decisions.md) | Architectural Decision Log (ADL) |
---
## Quick orientation for AI assistants
- Read order for most changes: `01``02``04` → relevant interface doc(s) → `10`
- Entry point: `cmd/logpile/main.go` - Entry point: `cmd/logpile/main.go`
- HTTP server: `internal/server/` — handlers in `handlers.go`, routes in `server.go` - HTTP layer: `internal/server/`
- Data contracts: `internal/models/` — never break `AnalysisResult` JSON shape - Core contracts: `internal/models/models.go`
- Frontend contract: `web/static/js/app.js` — keep API responses stable - Live collection: `internal/collector/`
- Canonical inventory: `hardware.devices` in `AnalysisResult` — source of truth for UI and exports - Archive parsing: `internal/parser/`
- Parser registry: `internal/parser/vendors/``init()` auto-registration pattern - Export conversion: `internal/exporter/`
- Collector registry: `internal/collector/registry.go` - Frontend consumer: `web/static/js/app.js`
## Maintenance rule
If a document becomes stale, either fix it immediately or delete it.
Stale docs are worse than missing docs.

View File

@@ -1,39 +0,0 @@
# Test Server Collection Memory
Keep this table updated after each test-server run.
Definition:
- `Collection Time` = total Redfish collection duration from `collect.log`.
- `Speed` = `Documents / seconds`.
- `Metrics Collected` = sum of `Counts` fields (`cpus + memory + storage + pcie + gpus + nics + psus + firmware`).
- `n/a` means the log does not contain enough timestamp metadata to calculate duration/speed.
## Server Model: `NF5688M7`
| Date (UTC) | App Version | Collection Time | Documents | Speed | Metrics Collected | Notes |
|---|---|---:|---:|---:|---:|---|
| 2026-02-28 | `v1.7.1-12-g612058e` (`612058e`) | 10m10s (610s) | 228 | 0.37 docs/s | 98 | 2026-02-28 (SERVER MODEL) - 23E100043.zip |
| 2026-02-28 | `v1.7.1-11-ge0146ad` (`e0146ad`) | 9m36s (576s) | 138 | 0.24 docs/s | 110 | 2026-02-28 (SERVER MODEL) - 23E100042.zip |
| 2026-02-28 | `v1.7.1-10-g9a30705` (`9a30705`) | 20m47s (1247s) | 106 | 0.09 docs/s | 97 | 2026-02-28 (SERVER MODEL) - 23E100053.zip |
| 2026-02-28 | `v1.7.1` (`6c19a58`) | 15m08s (908s) | 184 | 0.20 docs/s | 96 | 2026-02-28 (DDR5 DIMM) - 23E100051.zip |
| 2026-02-28 | `v1.7.0` (`ddab93a`) | n/a | 193 | n/a | 61 | 2026-02-28 (NULL) - 23E100051.zip |
| 2026-02-28 | `v1.7.0` (`ddab93a`) | n/a | 291 | n/a | 61 | 2026-02-28 (NULL) - 23E100206.zip |
## Server Model: `SYS-A21GE-NBRT` (Supermicro HGX B200)
> **HGX note:** this model has ~40 sub-chassis (GPU, NVSwitch, PCIeRetimer, ERoT/IRoT, BMC, FPGA)
> all exposing empty `/Drives` collections. Post-probe NVMe must skip `ChassisType=Module/Component/Zone`
> or it probes 35 × 384 = 13 440 URLs → ~22 min wasted. Fixed in the commit that added
> `chassisTypeCanHaveNVMe` (2026-03-12). Expected post-probe NVMe time after fix: <5s.
| Date (UTC) | App Version | Collection Time | Documents | Speed | Metrics Collected | Notes |
|---|---|---:|---:|---:|---:|---|
| 2026-03-12 | `v1.8.0-6-ga9f58b3` (`a9f58b3`) | 35m28s (2128s) — **before fix** | 3197 | 1.50 docs/s | 140 | 2026-03-12 (SYS-A21GE-NBRT) - A936564X5C17287.zip |
## Server Model: `KR1280-X2-A0-R0-00`
| Date (UTC) | App Version | Collection Time | Documents | Speed | Metrics Collected | Notes |
|---|---|---:|---:|---:|---:|---|
| 2026-02-28 | `v1.7.1-12-g612058e` (`612058e`) | 6m15s (375s) | 185 | 0.49 docs/s | 46 | 2026-02-28 (KR1280-X2-A0-R0-00) - 23D401657.zip |
| 2026-02-28 | `v1.7.1-9-g8dbbec3-dirty` (`8dbbec3`) | 6m16s (376s) | 165 | 0.44 docs/s | 46 | 2026-02-28 (KR1280-X2-A0-R0-00) - 23D401657-2.zip |
| 2026-02-28 | `v1.7.1-7-gc52fea2` (`c52fea2`) | 10m51s (651s) | 227 | 0.35 docs/s | 40 | 2026-02-28 (KR1280-X2-A0-R0-00) - 23D401657 copy.zip |