---
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.
