Files
enduro-trails/docs/work-items/ET-008/08-data-requirements.md
claude-bot d33f360a2f
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 6s
CI / build (push) Successful in 2s
architect(ET-008): ADRs, infra/data requirements, tech risks
2026-06-01 12:15:05 +00:00

20 KiB
Raw Permalink Blame History

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
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-треки с 13 публичных платформ.
  • Клиентское хранилище (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: enduroenduro, tag: motorbikemoto, tag: mtb/tag: bikebicycle, 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 000100 000 точек, ≈ 1 0005 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. Контракты, которые нельзя ломать

  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.