383 lines
20 KiB
Markdown
383 lines
20 KiB
Markdown
---
|
||
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 — константа в `<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) — `<trkpt>` + `<wpt>` + 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 '%<url>%'`).
|
||
- 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.
|