feat: complete dashcam catalog ingestion demo
Master Production Deploy / production (push) Failing after 49s

This commit is contained in:
2026-07-31 02:01:58 +03:00
parent ae6c35145f
commit 2ce58fcbc0
62 changed files with 6341 additions and 178 deletions
+34 -28
View File
@@ -74,7 +74,10 @@ Headless browser допустим только как ручной diagnostic to
## 4. Deployment Model
Ingestion живет отдельно от облачного runtime приложения.
Ingestion деплоится в то же облако, что и web-приложение (один Swarm/Portainer
environment), а не на отдельный сервер. Раньше предполагался отдельный cloud, но
управлять двумя независимыми облаками оказалось слишком сложно. См. решение
`videoreg-nnv.3`.
Код хранится в этом же репозитории:
@@ -86,18 +89,20 @@ ingestion/
Dockerfile
```
Стек ingestion деплоится отдельным `docker-compose.yml`.
Минимальные сервисы:
```text
postgres
ingestion-worker
ingestion-admin
export-server
```
Основное Next.js-приложение не подключается напрямую к ingestion DB.
`export-server` больше не нужен: ingestion-worker пишет валидированный
`catalog.json` + `manifest.json` в общий (shared) export-том, а web читает их
напрямую с этого тома. Никакого HTTP-экспорта и Bearer-токена.
Основное Next.js-приложение по-прежнему не подключается напрямую к ingestion DB —
единственный контакт с каталогом идёт через export-артефакт на shared-томе.
---
@@ -107,29 +112,27 @@ Source of truth для каталога: PostgreSQL ingestion stack.
Текущее примитивное хранилище приложения (`data/videoreg.json`) является только read-model cache.
Bridge работает со стороны приложения:
Bridge работает со стороны приложения через общий export-том:
1. ingestion публикует versioned HTTPS export;
2. приложение вручную запускает operator import command;
3. import command скачивает manifest;
4. проверяет `schemaVersion`, `exportId`, `createdAt`, `sha256`, `itemCount`;
5. скачивает catalog artifact;
1. ingestion пишет versioned export (`manifest.json` + `catalog.json`) на shared-том;
2. приложение/сборка читает `manifest.json` с того же тома;
3. проверяет `schemaVersion`, `exportId`, `createdAt`, `sha256`, `itemCount`;
4. читает `catalog.json` с тома;
5. сверяет `sha256` артефакта;
6. валидирует payload через shared Zod schema;
7. атомарно заменяет `data/videoreg.json`.
7. атомарно заменяет `data/videoreg.json` (или используется как build-time источник).
В MVP import запускается вручную оператором, не по расписанию.
В MVP import запускается вручную оператором, не по расписанию. HTTP/токен в этом
обмене не участвуют — оба сервиса в одном облаке и делят том.
---
## 6. Export Contract
Export доступен по HTTPS со статичным токеном:
```http
Authorization: Bearer <token>
```
Токен передается через env import-команды. Токен не передается в query string.
Export публикуется как файлы на общем (shared) export-томе: `manifest.json` и
`artifacts/<exportId>/catalog.json`. HTTP-эндпоинт и Bearer-токен не используются —
ingestion и web живут в одном облаке и делят том. Целостность гарантируется
`sha256` в манифесте и shared Zod-схемой при чтении.
### Manifest
@@ -138,7 +141,7 @@ type CatalogExportManifest = {
schemaVersion: 1;
exportId: string;
createdAt: string;
artifactUrl: string;
artifactPath: string;
sha256: string;
itemCount: number;
};
@@ -158,6 +161,7 @@ type ExportedDashcamProduct = {
id: string;
brand: string;
model: string;
canonicalName?: string;
title: string;
specs: Record<string, string | number | boolean>;
images: ExportedProductImage[];
@@ -169,19 +173,21 @@ type ExportedDashcamProduct = {
type ExportedProductImage = {
url: string;
originalUrl: string;
source: "yandex_market";
source: MarketSource;
sha256?: string;
width?: number;
height?: number;
};
type ExportedProductSource = {
source: "yandex_market";
source: MarketSource;
sourceProductId?: string;
url: string;
stableContentHash: string;
fetchedAt: string;
};
type MarketSource = "yandex_market" | "mvideo";
```
Runtime validation: shared Zod schema в репозитории. TypeScript-типы без runtime-валидации недостаточны.
@@ -260,7 +266,7 @@ Category pages используются вторично:
```ts
type DiscoveredProduct = {
source: "yandex_market";
source: MarketSource;
categoryUrl?: string;
productUrl: string;
sourceProductId?: string;
@@ -344,7 +350,7 @@ JSON-LD > embedded state > semantic DOM > fallback DOM
```ts
type ParsedMarketProduct = {
source: "yandex_market";
source: MarketSource;
sourceProductId?: string;
title: string;
brand?: string;
@@ -372,7 +378,7 @@ type ParsedMarketProduct = {
```ts
type ProductSourceSnapshot = {
source: "yandex_market";
source: MarketSource;
url: string;
fetchedAt: string;
httpStatus: number;
@@ -623,7 +629,7 @@ export_runs
* publish versioned manifest
* publish versioned catalog artifact
* publish versioned image artifacts
* protect endpoints with Bearer token
* write export artifacts to the shared export volume (no HTTP, no token)
* verify checksum in app import
* atomic replace of `data/videoreg.json`
@@ -633,7 +639,7 @@ export_runs
Система считается готовой, если:
* ingestion stack запускается отдельно через `docker-compose.yml`
* ingestion и web деплоятся в одно облако; export идёт через shared-том, без HTTP/токена
* PostgreSQL является source of truth
* приложение импортирует только approved canonical catalog
* `data/videoreg.json` заменяется только после manifest/checksum/schema validation