20 KiB
type, work_item_id, title, version, status, created_at, authors
| type | work_item_id | title | version | status | created_at | authors | |
|---|---|---|---|---|---|---|---|
| data-requirements | ET-008 | Требования к данным — ET-008: GPS-треки с публичных платформ | 1 | approved | 2026-06-01 |
|
Требования к данным — 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
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).
Пример:
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
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 Не-персистентное состояние в памяти браузера
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):
{
"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:
{
"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:
{"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. Контракты, которые нельзя ломать
dedup_keyформула (ADR-006 §6) — менять можно только при полном rebuild БД.ACTIVITY_TYPESenum — добавление новых значений требует UI-обновления (новый цвет, новая локализация); удаление — миграция существующих треков.- GeoJSON response shape (§6.1) — public API, ломающие изменения через v2-endpoint.
- MVT layer name
gps_tracksи properties (§6.2) — клиент завязан; ломающие — через новый layer-name. localStoragekeys (§4) — менять имя ключа требует миграцию (gps-tracks-enabled-v2).
11. Вывод
Серверная модель данных полностью локализована в data/gps_tracks.sqlite. Контракты API и MVT-схема финализированы. Клиентское хранилище — 256 байт UI-state. Персональные данные минимизированы по дизайну: только публичные поля от accepted-источников; default-deny для не-accepted.