# Реализация JSON hardware-ingest по INTEGRATION_GUIDE.md ## Summary Добавляем новый инструмент импорта hardware snapshot: `POST /ingest/hardware` + форму в ingest UI, без ломки текущего `POST /ingest/logbundle`. Импорт должен: 1. находить/создавать `asset` по `hardware.board.serial_number`, 2. автоматически назначать новые asset в служебный customer/project (`Без заказчика` / `Без проекта`), 3. создавать/обновлять компоненты + LOT, 4. писать `observations` с `details JSON`, 5. синхронизировать `installations`, 6. генерировать timeline events (`LOG_COLLECTED`, `INSTALLED`, `REMOVED`, `FIRMWARE_CHANGED`, `COMPONENT_WARNING`, `COMPONENT_FAILED`), 7. создавать `failure_events` при `Critical`. ## Подтвержденные решения 1. Scope: `API + UI`. 2. Endpoint: основной `POST /ingest/hardware`. 3. Новые asset по умолчанию идут в служебный customer/project: - customer: `Без заказчика` - project: `Без проекта` 4. Статусы: - `Critical` -> `failure_event` + `COMPONENT_FAILED` - `Warning` -> `COMPONENT_WARNING` 5. Расширенные observation-поля храним в `observations.details JSON`. 6. Ответ endpoint: как в guide (`status`, `bundle_id`, `asset_id`, `collected_at`, `duplicate`, `summary|message`). ## Публичные API / контракты 1. Новый маршрут в `internal/api/ingest.go`: - `POST /ingest/hardware` 2. Request JSON: - `collected_at` (required, RFC3339) - `target_host` (required) - `hardware` (required) с `board.serial_number` (required) - опционально: `filename`, `source_type`, `protocol`, секции `cpus/memory/storage/pcie_devices/power_supplies/firmware` 3. Response success (201): - `status: "success"` - `bundle_id`, `asset_id`, `collected_at`, `duplicate: false` - `summary` с детализированными счетчиками 4. Response duplicate (200): - те же поля + `duplicate: true`, `message` 5. Validation error (400): - `status: "error"`, `error: "validation_failed"`, `details.field`, `details.message` ## Изменения по файлам (implementation design) 1. `migrations/0007_hardware_ingest/up.sql` и `down.sql` - `ALTER TABLE observations ADD COLUMN details JSON NULL;` 2. `internal/api/ingest.go` - новый handler `handleHardware` - строгая decode/validate - вызов `IngestHardware(...)` 3. `internal/ingest/service.go` - `HardwareInput`, `HardwareResult`, summary counters - transactional pipeline - `ensureServiceProject()`: - customer `Без заказчика` (create if missing) - project `Без проекта` (create if missing) - `ensureAssetByBoardSerial()`: - lookup по `assets.vendor_serial = board.serial_number` - create если нет (`name=target_host`, `project_id=service_project_id`, `vendor/model` по board) - update vendor/model при валидных значениях 4. `internal/ingest/parser_hardware.go` - parse + normalize + flatten snapshot - генерация serial: - CPU: `{board_serial}-CPU-{socket}` - PCIe: `{board_serial}-PCIE-{slot}` если пусто/N/A - фильтрация `present=false`, `status=Empty` 5. `internal/ingest/lot_classifier.go` - LOT rules для CPU/DIMM/Storage/PSU/PCIe - `ensureLotByCode()` 6. `internal/ingest/service.go` (status/failure/events) - `observations.details` хранит: `component_type`, `slot`, `status`, `present`, `telemetry`, raw attrs - `Critical`: - `COMPONENT_FAILED` в timeline - `failure_events` (`source=hardware_ingest`, deterministic `external_id`) - `Warning`: - `COMPONENT_WARNING` в timeline - system firmware (`hardware.firmware`): - сравнение с последним значением и `FIRMWARE_CHANGED` для asset 7. UI: - `internal/api/ui_ingest.tmpl` - `internal/api/ui.go` - добавить секцию “Hardware Ingest” с примером payload на основе `bible/docs/import-example-full.json` ## Идемпотентность 1. Хеш считается от канонически marshaled hardware request. 2. Если hash уже есть: - без side effects - `200 duplicate=true` 3. Если hash новый: - полный pipeline - `201` ## Тесты и acceptance 1. `TestIngestHardwareIdempotent`: 201 -> 200 duplicate, без дублей в `log_bundles/observations/installations`. 2. Тест авто-создания служебного customer/project и auto-create asset. 3. Тест замены компонента: `REMOVED` + `INSTALLED`. 4. Тест firmware change: `FIRMWARE_CHANGED`. 5. Тест статусов: - Warning -> `COMPONENT_WARNING` - Critical -> `COMPONENT_FAILED` + `failure_events` 6. Validation tests: - missing required fields - invalid RFC3339 - unknown field -> 400 7. UI smoke: `/ui/ingest` содержит форму hardware ingest. ## Assumptions and defaults 1. Служебные записи: - customer.name = `Без заказчика` - project.name = `Без проекта` 2. При дублях по имени используется запись с минимальным `id`. 3. `manufacturer/product_name == "NULL"` -> `NULL` в БД. 4. Новые `event_type` (`COMPONENT_WARNING`, `COMPONENT_FAILED`) хранятся как строки без enum-миграции. 5. `/ingest/logbundle` остается полностью совместимым.