--- type: data-requirements work_item_id: ET-008 title: "Требования к данным — ET-008: GPS-треки с публичных платформ" version: 1 status: approved created_at: 2026-06-01 authors: - "agent:architect" --- # Требования к данным — ET-008 ## 1. Резюме ET-008 вводит: - **Новую серверную БД** `data/gps_tracks.sqlite` (Spatialite) с двумя таблицами: `tracks`, `pipeline_runs`. - **Контракт публичного API GeoJSON** и **MVT layer schema** (см. TRZ §4.2, §4.3 — здесь финализируется). - **Внешние входные данные** — GPS-треки с 1–3 публичных платформ. - **Клиентское хранилище** (`localStorage`) — 4 новых ключа состояния UI. - **Персональные данные**: возможно `user` (имя автора публичного трека) для OSM (ADR-009 разрешает); для других источников — пока заблокировано (ADR-010, ADR-011). ## 2. Архитектурные границы данных | Слой данных | Тип | Расположение | Владелец | Lifecycle | |---|---|---|---|---| | OSM-vector (`trails`, `centralfederal.sqlite`) | существующий | `/app/data/centralfederal.sqlite` | ET-001..006 | пересборка из OSM ad-hoc | | Личные GPX треки (ET-006) | существующий | браузер (memory only) | ET-006 | сессия | | **Публичные GPS треки** | **новый** | `/app/data/gps_tracks.sqlite` | **ET-008** | rebuild при необходимости + ежемесячный GC | | OSRM-граф | существующий | `/app/data/enduro.osrm.*` | PH-2 | пересборка после OSM-обновления | | User UI state | существующий + расширение | `localStorage` браузера | каждый work item | до явной очистки | Между новой БД и существующей `centralfederal.sqlite` **нет cross-DB запросов** на горизонте MVP (см. ADR-005 §9). ## 3. Серверные данные — `gps_tracks.sqlite` ### 3.1 Таблица `tracks` ```sql CREATE TABLE tracks ( id INTEGER PRIMARY KEY AUTOINCREMENT, dedup_key TEXT NOT NULL UNIQUE, name TEXT, description TEXT, activity_type TEXT NOT NULL, -- ACTIVITY_TYPES (см. §3.4) user TEXT, -- ADR-009 разрешает; null для ADR-010/011 до accepted created_at TEXT, -- ISO date YYYY-MM-DD; nullable length_m REAL NOT NULL, points_count INTEGER NOT NULL, min_lon REAL NOT NULL, min_lat REAL NOT NULL, max_lon REAL NOT NULL, max_lat REAL NOT NULL, geom BLOB NOT NULL, -- WKB LineString (Spatialite) sources_json TEXT NOT NULL, -- JSON-array ["osm", "enduro_russia"] external_urls_json TEXT NOT NULL, -- JSON-array URLs tags_json TEXT, -- JSON-array string tags inserted_at TEXT NOT NULL, -- ISO datetime updated_at TEXT NOT NULL -- ISO datetime ); CREATE UNIQUE INDEX idx_tracks_dedup ON tracks(dedup_key); CREATE INDEX idx_tracks_activity ON tracks(activity_type); CREATE INDEX idx_tracks_created ON tracks(created_at); -- Spatialite R-tree SELECT CreateSpatialIndex('tracks', 'geom'); ``` Поля `min_lon`/`max_lon`/`min_lat`/`max_lat` денормализованы из `geom` для **раннего отбрасывания** треков в MVT-генерации без парсинга WKB (ADR-005 §2). ### 3.2 `dedup_key` Алгоритм — ADR-006. Формат строки: ``` ((w, s, e, n), length_bucket, "YYYY-MM-DD") ``` где координаты округлены до 2 знаков после запятой, `length_bucket` = `round(length_m / 1000) * 1000`. UNIQUE индекс обеспечивает ON CONFLICT логику. ### 3.3 `sources_json` и `external_urls_json` JSON-массивы строк. Длина ≤ 8 элементов (источников после дедупа). Порядок — стабильный по приоритету в `gps_sources.yaml`. Первый элемент `sources_json` — «первичный» источник; его id попадает в `properties.source` MVT-фичи для цветовой палитры по умолчанию (REQ-F-16). Пример: ```json sources_json = ["osm", "enduro_russia"] external_urls_json = ["https://www.openstreetmap.org/user/Vasya/traces/12345", "https://enduro-russia.ru/treki/678"] ``` Запись фиксирует **тот же индекс** = тот же источник: `external_urls_json[i]` — это URL `sources_json[i]`. ### 3.4 ACTIVITY_TYPES Закрытый enum (TRZ REQ-F-07): | code | label-ru | |---|---| | `enduro` | Эндуро | | `moto` | Мото | | `offroad` | Off-road | | `bicycle` | Велосипед | | `hike` | Пешком | | `ski` | Лыжи | | `other` | Другое | `MAPPING` per source — константа в `.py`. Категории источника, не найденные в MAPPING → `other`. На MVP `MAPPING` для OSM фиксирован: парсим OSM-tags (`tag: enduro` → `enduro`, `tag: motorbike` → `moto`, `tag: mtb`/`tag: bike` → `bicycle`, etc.). Точная таблица — в коде, ревью при ADR-апруве. ### 3.5 Таблица `pipeline_runs` ```sql CREATE TABLE pipeline_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, started_at TEXT NOT NULL, finished_at TEXT, region_id TEXT NOT NULL, source_id TEXT NOT NULL, status TEXT NOT NULL, -- ok | partial | error | skipped_license tracks_new INTEGER DEFAULT 0, tracks_updated INTEGER DEFAULT 0, errors_json TEXT -- JSON object {error_type: count} ); CREATE INDEX idx_pipeline_started ON pipeline_runs(started_at); ``` История прогонов. Read-only для API; пишет только pipeline. Используется `/api/gps-tracks/health`. ### 3.6 Размер БД | Объём | Оценка | |---|---| | Среднее число точек на трек | 1240 (по BRD §3 F-13 popup; реалистично) | | Геометрия WKB на трек | ≈ 16 байт/точка × 1240 = 20 КБ | | Метаданные на трек | ≈ 1 КБ | | Итого на трек | ≈ 21 КБ | | 5000 треков MVP | ≈ 105 МБ | | 50 000 треков (через год при расширении) | ≈ 1.05 ГБ | | Лимит REQ-NF-03 | 2 ГБ | Запас 2× от MVP-объёма до операционного лимита. При превышении — миграция на PostGIS (отдельный work item, тех-долг в ADR-005). ### 3.7 Ротация и GC - Команда `python -m scripts.gps_collect --gc` (ADR-007 §3) — удаляет треки `WHERE updated_at < NOW() - 5 years`. - Параметр `5 years` зашит в `config/gps_sources.yaml::retention_years` (default 5; per-source override возможен). - Cron — 1-е число каждого месяца 04:00 UTC. - Stale-cleanup (трек удалён на источнике) — отдельный GC-режим `--gc-stale`; на MVP не входит (см. ADR-009 §6). ### 3.8 Backup См. `07-infra-requirements.md` §4.4. Ежедневный `.backup`, retention 14 дней. ## 4. Клиентское хранилище | Ключ | Значение | Default | Расход | |---|---|---|---| | `gps-tracks-enabled` | `"true"` \| `"false"` | `"false"` | ≤ 5 байт | | `gps-tracks-activities` | JSON-array из ACTIVITY_TYPES | все 7 значений | ≤ 70 байт | | `gps-tracks-sources` | JSON-array source IDs | все enabled на момент первого открытия | ≤ 80 байт | | `gps-tracks-color-mode` | `"source"` \| `"activity"` | `"source"` | ≤ 8 байт | | **Итого на браузер** | | | ≤ 256 байт | - **Чтение**: `restorePublicTracksState()` в `rebuildMapOverlays()` (REQ-F-19); инициализация при старте приложения. - **Запись**: каждое изменение checkbox / segmented control в `#sheet-gps-filters`. - **Миграция со старых значений**: не требуется (ключи новые). - **Невалидные значения**: ignore + restore defaults; не вызывают исключение. ### 4.1 Конвенция имён Префиксация — `gps-tracks-*`. Согласуется с существующими (`terrain-*`, `trails-*`, `map-base-layer`). ### 4.2 Не-персистентное состояние в памяти браузера ```js window.gpsTracksLayer = { enabled: false, filters: { activities: [...ACTIVITY_TYPES], sources: [...enabledSourceIds], colorMode: 'source' }, sourceId: 'gps-tracks-tiles', // vector source for MVT mode sourceGeoId: 'gps-tracks-geo', // geojson source for GeoJSON mode layerMvtId: 'gps-tracks-layer-mvt', layerGeoId: 'gps-tracks-layer-geo', haloMvtId: 'gps-tracks-halo-mvt-satellite', haloGeoId: 'gps-tracks-halo-geo-satellite', geojsonAbortController: null, geojsonReqDebounceTimer: null, stats: { total: 0, shown: 0 }, activeMode: 'mvt' | 'geo' | 'hidden' // derived from zoom }; ``` Конкретное содержимое и переходы — TRZ §4.4 + ADR-008. ## 5. Внешние входные данные ### 5.1 OSM Public GPS Traces (ADR-009) | Параметр | Значение | |---|---| | Endpoint | `GET https://api.openstreetmap.org/api/0.6/trackpoints?bbox=...&page=...` | | Metadata | `GET https://api.openstreetmap.org/api/0.6/gpx/{id}` | | Формат | XML (GPX 1.1) — `` + `` + meta | | Лицензия | ODbL 1.0 | | Атрибуция | `© OpenStreetMap contributors (ODbL)` | | Rate-limit | 1 req/sec (per OSM policy) | | Объём для ЦФО+Чувашии (оценка) | ≈ 50 000–100 000 точек, ≈ 1 000–5 000 треков | | User-Agent | `enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)` | ### 5.2 EnduroRussia.ru (ADR-010 — БЛОКИРОВАН) До accepted-status — pipeline пропускает. ### 5.3 ttrails.ru (ADR-011 — БЛОКИРОВАН) До accepted-status — pipeline пропускает. ## 6. Контракт публичного API ### 6.1 `GET /api/gps-tracks` **Query params:** | Параметр | Тип | Обязательность | Default | Валидация | |---|---|---|---|---| | `bbox` | 4 float comma-separated | required | — | -180 ≤ lon ≤ 180, -85 ≤ lat ≤ 85, west < east, south < north, площадь ≤ 10 deg² | | `activity` | comma-string из ACTIVITY_TYPES | optional | all | каждое значение — известный enum | | `source` | comma-string source IDs | optional | all enabled | значения сверяются с `gps_sources.yaml` | | `limit` | int | optional | 500 | 1 ≤ limit ≤ 2000 | **Response 200 (`Content-Type: application/json`):** ```json { "type": "FeatureCollection", "features": [ { "type": "Feature", "id": 12345, "geometry": { "type": "LineString", "coordinates": [[lon, lat], ...] }, "properties": { "name": "Утренний эндуро", "activity_type": "enduro", "user": "Vasya", "created_at": "2024-05-12", "length_km": 47.3, "points_count": 1240, "sources": ["osm", "enduro_russia"], "external_urls": ["https://...", "https://..."], "tags": ["forest", "river"] } } ], "total_in_bbox": 743, "returned": 500, "truncated": true } ``` **Error responses:** | Code | Условие | |---|---| | 400 | невалидный bbox / activity / source / limit | | 503 | БД отсутствует или Spatialite не загрузился | ### 6.2 `GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt` **Path params:** `z` 0..18, `x`/`y` валидны для z. **Response:** - 200 `Content-Type: application/x-protobuf`, тело — `mapbox-vector-tile`-encoded MVT. - 200 + пустое тело — если в тайле нет треков. - 304 — стандартная HTTP cache на ETag (опционально, MVP — не реализуется). - Header `X-Cache: HIT | MISS` — для observability. **Layer schema:** | Layer | Geometry | Properties | |---|---|---| | `gps_tracks` | LineString | `id (int)`, `activity (string)`, `source (string, первый)`, `sources (string, comma-separated)`, `length_km (float)`, `name (string)`, `ext_url (string, первый)` | Properties — упрощены под MVT-ограничения (нет массивов). ### 6.3 `GET /api/gps-tracks/health` **Response 200:** ```json { "db_path": "/app/data/gps_tracks.sqlite", "db_size_mb": 124.5, "tracks_total": 8421, "tracks_by_source": {"osm": 5234, "enduro_russia": 2102, "ttrails": 1085}, "tracks_by_activity": {"enduro": 2104, "moto": 850, "offroad": 305, "bicycle": 3201, "hike": 1810, "other": 151}, "last_pipeline_run": { "started_at": "2026-05-30T03:00:00Z", "finished_at": "2026-05-30T05:14:00Z", "regions": ["tsfo_plus_chuvashia"], "sources_ok": ["osm"], "sources_error": [{"source": "ttrails", "error": "HTTP 503"}], "sources_skipped_license": ["enduro_russia"] }, "tile_cache_size": 412, "tile_cache_max": 1024 } ``` **Response 503:** если БД отсутствует или Spatialite не доступен. ### 6.4 `POST /api/gps-tracks/cache/clear` **Auth:** ограничен docker-internal сетью (`07-infra-requirements.md` §3.1). **Response 200:** ```json {"cleared": 412} ``` Запрос идемпотентен, вызывается только pipeline'ом в конце прогона. ## 7. Персональные данные (PII) | Канал | PII | Условия | |---|---|---| | `tracks.user` (имя автора) | да, **публичное** имя | сохраняется **только** если ADR соответствующего источника явно разрешает (`save_user_field: true` в `gps_sources.yaml`). По ADR-009 OSM — разрешено. ADR-010, ADR-011 — пока запрещено | | `tracks.geom` (координаты трека) | низкий риск; **публично выложенные** автором | сохраняются всегда | | `tracks.created_at` | дата проезда | публичная; сохраняется всегда | | `tracks.description`, `tracks.tags` | возможные следы PII в свободном тексте | сохраняются только при `save_description: true` в конфиге источника | | Запросы к `api.openstreetmap.org` (исходящие с mva154) | IP **сервера mva154**, не клиента | да, mva154-IP становится известен OSM (стандартное поведение для скрейпера) | | Запросы к `enduro-russia.ru`, `ttrails.ru` | то же | пока ADR не accepted — не происходит | | `localStorage['gps-tracks-*']` | UI-настройки | нет PII | ### 7.1 Право на удаление - Запись `external_urls_json` сохраняет ссылку на оригинал — оператор может удалить конкретную запись по запросу автора (`DELETE FROM tracks WHERE external_urls_json LIKE '%%'`). - Pipeline уважает «удалённое на источнике» при `--gc-stale` (post-MVP). ### 7.2 GDPR / РФ ФЗ-152 - ET-008 обрабатывает **только публично опубликованные** автором данные. - Имя автора (`user`) — публичное на платформе источника (по ADR-009, ADR-010 для OSM/EnduroRussia это публикуется на странице трека). - Контактные данные (email, телефон) — **не сохраняются ни при каких условиях**; платформы их не отдают в публичных GPX-эндпоинтах. - Локация «дом»/«работа» как отдельная точка интереса — не сохраняется (waypoints без public-флага в OSM не отдаются; для скрейпленых источников — `save_waypoints: false`). - DPO-ответственность minimal — нет сервиса регистрации/учёта пользователей; это публичный read-only слой. ## 8. Атрибуция Обязательное требование BRD §5 «Атрибуция» и AC-15: - **На карте**: MapLibre автоматически отображает `attribution` из source-spec в правом нижнем углу. Каждый source (`gps-tracks-tiles`, `gps-tracks-geo`) указывает `attribution: "© OSM contributors (ODbL) | EnduroRussia.ru | ttrails.ru"` — динамически сформированную клиентом из `/api/gps-tracks/health.tracks_by_source` (только активные источники). - **В popup трека**: ссылки на оригинал по `external_urls_json` (REQ-F-18). - **В docs/architecture/README.md**: новый раздел «GPS Tracks Pipeline» содержит таблицу источников и их атрибуций. ## 9. Backup и retention | Объект | Backup | Retention | |---|---|---| | `data/gps_tracks.sqlite` | Ежедневный `.backup` через cron на mva154 | 14 дней | | `pipeline_runs` (внутри той же БД) | через backup БД | вечно (растёт медленно, ≤ 10⁴ строк/год) | | `tracks` старше 5 лет | удаляются при `--gc` | retention configurable в `gps_sources.yaml` | | `/var/log/enduro-trails/*.log` | через logrotate | 14 дней | | Pipeline JSON-lines logs | через logrotate | 8 недель | ## 10. Контракты, которые **нельзя ломать** 1. `dedup_key` формула (ADR-006 §6) — менять можно только при полном rebuild БД. 2. `ACTIVITY_TYPES` enum — добавление новых значений требует UI-обновления (новый цвет, новая локализация); удаление — миграция существующих треков. 3. GeoJSON response shape (§6.1) — public API, ломающие изменения через v2-endpoint. 4. MVT layer name `gps_tracks` и properties (§6.2) — клиент завязан; ломающие — через новый layer-name. 5. `localStorage` keys (§4) — менять имя ключа требует миграцию (`gps-tracks-enabled-v2`). ## 11. Вывод Серверная модель данных полностью локализована в `data/gps_tracks.sqlite`. Контракты API и MVT-схема финализированы. Клиентское хранилище — 256 байт UI-state. Персональные данные минимизированы по дизайну: только публичные поля от accepted-источников; default-deny для не-accepted.