architect(ET-005): ADR, infra-requirements, data-requirements, tech-risks
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 5s
CI / build (push) Successful in 1s

This commit is contained in:
2026-05-21 22:18:08 +03:00
parent 2458a3efe0
commit bafbea2dab
4 changed files with 516 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
---
type: adr
work_item_id: ET-005
adr_id: adr-0001
title: "ADR-0001: Переключение единиц измерения (км/мили) — клиентское решение с централизованным форматтером"
status: accepted
created_at: 2026-05-21
authors:
- "agent:architect"
supersedes: []
superseded_by: []
labels: []
---
# ADR-0001 — Переключение единиц измерения (км/мили): клиентское решение с централизованным форматтером
## Статус
Accepted
## Контекст
ET-005 добавляет переключатель единиц измерения расстояний (км/мили) в
панель настроек карты. Выбор сохраняется в `localStorage`, по умолчанию —
километры, при переключении все видимые расстояния пересчитываются за
< 100 мс (см. `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`).
Существующее состояние кодовой базы (`src/web/`):
- Фронтенд **плоский, без сборщика и без модульной системы**:
`index.html`, `app.js`, `app.css`, `style.json`, `style-dark.json`.
`app.js` (≈ 3036 строк) подключается как **классический скрипт**
(`<script src="app.js"></script>`, `index.html:405`), а не ES-модуль.
Каталога `src/web/static/js/` в репозитории **нет**.
- Форматирование расстояний **не централизовано** — захардкоженный
паттерн `(m / 1000).toFixed(N) + ' км'` встречается минимум в **13
местах** `app.js`: `formatDist()` (стр. 187189), карточки сегментов
(638), карточки маршрута (1157, 1191, 2202, 2212, 2357, 2370, 2605),
scale-bar (14161440), всплывающие подсказки (1478), линейка/ruler
(1837, 1875, 1885, 1931). Единого форматтера нет.
- Внутренняя каноническая единица расстояния — **метры**
(`route.distance_m`), для линейки — километры (`rulerTotal`).
- Сложился устойчивый паттерн персистентности UI-настроек в
`localStorage`: ключи `enduro-theme-mode`, `terrain-hillshade`,
`terrain-tri`, `trails-track`, `trails-path`, `poi-visible` (ET-002),
`MARKERS_KEY`.
- «Панель настроек карты» из BRD/ТЗ де-факто реализована как попап
`#terrain-popup` (заголовок «Эндуро»), открываемый кнопкой «Рельеф».
В ET-002 в этот же попап добавлен чекбокс POI. В `app.css` уже есть
готовый компонент сегментированного переключателя `.seg-control` /
`.seg-btn` (стр. 360363).
Backend (FastAPI), БД (SQLite/Spatialite), тайл-сервер и OSRM к выбору
единиц измерения отношения **не имеют**: расстояния приходят с backend
в метрах, перевод — исключительно вопрос представления на клиенте.
## Рассматриваемые варианты
### Вариант A — Централизованный модуль-форматтер + единый оркестратор (выбран)
Новый модуль `src/web/units.js`, подключаемый как **классический скрипт
до `app.js`**, экспортирует глобальный неймспейс `window.Units` с
функциями состояния (`getUnit()` / `setUnit()`) и форматирования
(`formatDistance(meters, {precision})`). Все 13 мест форматирования в
`app.js` переводятся на вызов `Units.formatDistance(...)`. При смене
единицы `setUnit()` пишет в `localStorage` и диспатчит `unitchange` на
`document`; в `app.js` регистрируется **один** обработчик-оркестратор
`onUnitChange()`, который пере-вызывает существующие функции отрисовки
видимых поверхностей с расстояниями.
- **Плюсы:** единственный источник истины по форматированию убирает
риск рассинхрона; нулевые изменения backend/БД/инфраструктуры;
пересчёт мгновенный (нет ни сети, ни перезагрузки тайлов) — НФТ
«< 100 мс» выполняется тривиально; согласуется с принципом «минимум
зависимостей»; модуль покрывается unit-тестами изолированно.
- **Минусы:** требует рефакторинга 13 мест в `app.js` (риск пропустить
вызов — вынесен в `10-tech-risks.md`, R1); глобальный неймспейс вместо
ES-import — но это сознательное соответствие текущему стилю `app.js`.
### Вариант B — Рассыпанные подписчики `unitchange` по компонентам (буквальный тех-дизайн ТЗ)
Каждый компонент с расстоянием самостоятельно подписывается на
`unitchange` и сам себя перерисовывает.
- **Плюсы:** формально совпадает с разделом «Технический дизайн» ТЗ.
- **Минусы:** N подписок вместо одной — высокая связность, легко
забыть подписку у нового компонента; дублирование логики перерисовки;
не решает корневую проблему — захардкоженное `+ ' км'` остаётся
размазанным. Отклонён: оркестрация в одном месте надёжнее.
### Вариант C — Вынести `units.js` в ES-модуль `src/web/static/js/units.js`
Буквально следовать пути из ТЗ: создать `src/web/static/js/units.js` как
ES-модуль.
- **Плюсы:** «правильная» модульность.
- **Минусы:** в проекте нет каталога `static/js/`, нет сборщика, а
`app.js` подключён как классический скрипт. Полноценный `import`
потребовал бы перевода `app.js` в `type="module"` и реструктуризации
≈ 3000 строк — несопоставимо со scope ET-005 и противоречит принципу
«минимум зависимостей». Отклонён.
## Решение
Принимается **Вариант A**.
1. **Только клиент.** Backend, БД, API, тайл-сервер, OSRM, инфраструктура
— без изменений. Расстояния продолжают приходить с backend в метрах.
2. **Новый модуль `src/web/units.js`** (НЕ `src/web/static/js/units.js`).
Раздел «Технический дизайн» ТЗ носит рекомендательный характер;
финальное размещение файлов — мандат архитектуры. Путь скорректирован
под фактическую плоскую структуру `src/web/`. Модуль подключается в
`index.html` как классический скрипт **строго перед `app.js`**:
`<script src="units.js"></script>` затем `<script src="app.js">`.
3. **Глобальный API модуля** — неймспейс `window.Units` (соответствует
стилю `app.js`, где функции глобальны):
- `Units.getUnit()``'km' | 'mi'`;
- `Units.setUnit(unit)` — валидирует, пишет `localStorage`, диспатчит
`unitchange`;
- `Units.formatDistance(meters, { precision })` — возвращает строку с
корректной единицей в текущем режиме;
- `Units.KM_TO_MI = 0.621371` — единственная константа коэффициента.
4. **Каноническая единица — метры.** Конвертация выполняется **только в
момент форматирования**. Запрещено хранить или пересчитывать значения
расстояний в милях во внутреннем состоянии (`route.distance_m`,
`rulerTotal` и т.п. остаются метрическими) — это исключает накопление
ошибок округления при многократном переключении (ФТ-3, AC-2/TP-04).
5. **Персистентность**`localStorage`, ключ `distance_unit` (как в ТЗ),
значения `'km'` / `'mi'`. При отсутствии или некорректном значении —
дефолт `'km'` (AC-3, TP-01). Чтение — при инициализации модуля.
6. **Пересчёт — единый оркестратор.** `setUnit()` диспатчит `unitchange`
на `document`. В `app.js` регистрируется ровно **один** обработчик
`onUnitChange()`, пере-вызывающий существующие функции отрисовки
видимых поверхностей с расстояниями (лист маршрута, список точек,
карточки маршрутов, линейка, scale-bar). Рассыпать подписки по
компонентам запрещено.
7. **Все 13 мест форматирования** `(m/1000).toFixed(N)+' км'` в `app.js`
переводятся на `Units.formatDistance()`. Список call-sites и риск
пропуска зафиксированы в `10-tech-risks.md` (R1). Форматтер принимает
`precision`, чтобы сохранить текущую точность карточек (`toFixed(0)`)
и при этом по умолчанию давать 1 знак (AC-2, R5).
8. **UI-элемент** — сегментированный переключатель `km | mi` на готовом
компоненте `.seg-control` / `.seg-btn`, размещаемый в попапе
`#terrain-popup` (та же «панель настроек», что и чекбоксы слоёв и
POI-чекбокс из ET-002). Новый CSS-компонент не вводится.
9. **C4 без изменений.** Состав компонентов системы не меняется,
обновление `docs/architecture/c4-*.mmd` не требуется (диаграммы C4 в
репозитории отсутствуют — только `docs/architecture/README.md`).
## Последствия
### Положительные
- Изменение полностью клиентское: деплой без миграций и без изменения
состава контейнеров (штатный релиз фронтенда).
- Пересчёт < 100 мс (НФТ ТЗ) — нет ни сетевых вызовов, ни перезагрузки
тайлов.
- Появляется единственная точка форматирования расстояний — снижается
будущий риск рассинхрона и упрощается поддержка (положительный
побочный эффект рефакторинга 13 call-sites).
- Модуль `units.js` изолирован и тестируется юнит-тестами отдельно от
`app.js`.
### Отрицательные / ограничения
- Настройка локальна для браузера: при очистке `localStorage` или смене
устройства сбрасывается на дефолт `km`. Принято осознанно — вне scope
BRD (синхронизация между устройствами не требуется).
- Требуется рефакторинг 13 мест в `app.js` — операционный риск пропуска
call-site (митигация — `10-tech-risks.md`, R1; grep-аудит + e2e).
- Имперский эквивалент суб-километровых значений («м») и единиц
scale-bar требует политики — см. `10-tech-risks.md` R2, R3.
### Технический долг
- `units.js` подключается как глобальный классический скрипт. Если в
будущих фазах появится сборщик/модульность фронтенда, модуль легко
переводится в ES-модуль без изменения публичного контракта. На ET-005
перевод `app.js` в модули — вне scope.
## Классификация изменения
**Minor change.** Новые сервисы, БД, API-эндпоинты и контейнеры не
вводятся; состав компонентов C4 не меняется; backend и инфраструктура не
затрагиваются. Лейбл `arch:major-change` **не требуется**. Обязательного
архитектурного approve не требуется.
## Связанные документы
- `docs/work-items/ET-005/01-brd.md`
- `docs/work-items/ET-005/02-trz.md`
- `docs/work-items/ET-005/03-acceptance-criteria.md`
- `docs/work-items/ET-005/04-test-plan.yaml`
- `docs/work-items/ET-005/07-infra-requirements.md`
- `docs/work-items/ET-005/08-data-requirements.md`
- `docs/work-items/ET-005/10-tech-risks.md`
- `docs/architecture/README.md`

View File

@@ -0,0 +1,107 @@
---
type: infra-requirements
work_item_id: ET-005
title: "Инфраструктурные требования — ET-005: Переключение единиц измерения (км/мили)"
version: 1
status: approved
created_at: 2026-05-21
authors:
- "agent:architect"
---
# Инфраструктурные требования — ET-005
## 1. Резюме
ET-005 — изменение **исключительно фронтенда** (`src/web/index.html`,
`src/web/app.js`, `src/web/app.css` и новый файл `src/web/units.js`).
Новой инфраструктуры не требуется. Документ зафиксирован для полноты
work-item и явно подтверждает отсутствие инфра-воздействия (см.
`06-adr/adr-0001-unit-toggle-client-side.md`).
## 2. Контейнеры и сервисы
| Аспект | Требование |
|--------|------------|
| Новые контейнеры | Нет |
| Изменения существующих сервисов (api, osrm, nginx) | Нет |
| Изменения `docker-compose.yml` | Нет |
| Изменения `Dockerfile` | Нет — `units.js` это статика, попадает в образ тем же путём, что и `app.js` |
## 3. Сеть
| Аспект | Требование |
|--------|------------|
| Новые порты | Нет |
| Изменения reverse proxy (nginx, `/enduro/`) | Нет |
| Новые внешние домены / DNS | Нет |
| Исходящие сетевые вызовы из фронтенда | Нет (конвертация и хранение — локальные) |
## 4. Хранилища данных
| Аспект | Требование |
|--------|------------|
| Изменения схемы SQLite/Spatialite | Нет |
| Миграции БД (`migrations/`) | Нет |
| Серверное хранилище состояния | Нет |
| Клиентское хранилище | `localStorage`, ключ `distance_unit` (`'km'`/`'mi'`), ≈ 2 байта полезной нагрузки на браузер |
Подробности по данным — `08-data-requirements.md`.
## 5. Конфигурация и секреты
| Аспект | Требование |
|--------|------------|
| Новые переменные окружения | Нет |
| Новые секреты | Нет |
| Изменения конфигурации FastAPI / uvicorn | Нет |
## 6. Зависимости
| Аспект | Требование |
|--------|------------|
| Новые npm/Python пакеты | Нет |
| Новые внешние сервисы | Нет |
| Новые CDN-скрипты | Нет — `units.js` это локальный файл проекта, не внешний CDN |
| Версия MapLibre GL JS | Без изменений |
## 7. Сборка и деплой
- **Pipeline:** существующий Gitea Actions без изменений (`lint`, `test`,
`build`).
- **Артефакт:** статические ассеты фронтенда (`src/web/`), включая новый
`units.js`. Деплой — штатная пересборка/перевыкладка и
`docker compose up -d` на mva154.
- **Порядок подключения скриптов:** `units.js` должен подключаться в
`index.html` **строго перед** `app.js` (см. ADR-0001, риск R7 в
`10-tech-risks.md`). Контрольная точка для review.
- **Простой (downtime):** нет — изменение только в статике фронтенда.
- **План отката:** обратный коммит (revert) и повторный деплой;
миграций/серверного состояния, требующих отдельного отката, нет.
`localStorage`-ключ `distance_unit` у пользователей при откате
безвреден (игнорируется старым кодом).
## 8. Ресурсы (CPU / RAM / диск)
Воздействие отсутствует. Конвертация расстояний — несколько
арифметических операций в браузере клиента; тайлы и маршруты не
перезапрашиваются. Размер `units.js` пренебрежимо мал (единицы КБ).
## 9. Наблюдаемость
Новые метрики, логи и алерты не требуются. Поведение проверяется
unit-тестами модуля `units.js` и e2e-тестами фронтенда согласно
`04-test-plan.yaml` (TP-01…TP-05).
## 10. Влияние на C4
Состав компонентов системы **не меняется** — добавляется лишь файл
внутри уже существующего компонента «Frontend». Обновление
`docs/architecture/c4-*.mmd` не требуется (диаграммы C4 в репозитории
на данный момент отсутствуют — только `docs/architecture/README.md`).
## 11. Вывод
Инфраструктурных, сетевых, конфигурационных изменений и изменений БД
**нет**. ET-005 безопасен для деплоя в рамках обычного релизного цикла
фронтенда. Эскалация `arch:major-change` не требуется.

View File

@@ -0,0 +1,76 @@
---
type: data-requirements
work_item_id: ET-005
title: "Требования к данным — ET-005: Переключение единиц измерения (км/мили)"
version: 1
status: approved
created_at: 2026-05-21
authors:
- "agent:architect"
---
# Требования к данным — ET-005
## 1. Резюме
ET-005 не вводит и не изменяет ни одной серверной структуры данных.
Единственное «данное» фичи — пользовательская UI-настройка выбранной
единицы измерения, хранимая на клиенте.
## 2. Серверные данные
| Аспект | Требование |
|--------|------------|
| Изменения схемы SQLite/Spatialite | Нет |
| Новые таблицы / колонки / индексы | Нет |
| Миграции (`migrations/`) | Нет |
| Изменения контракта API | Нет — расстояния и далее отдаются в **метрах** |
## 3. Каноническая единица расстояния
- Внутренняя каноническая единица системы — **метр**. Backend отдаёт
`route.distance_m` (метры); линейка во фронтенде оперирует километрами
(`rulerTotal`).
- ET-005 **не меняет** ни хранимые, ни передаваемые значения. Перевод в
мили выполняется **только в момент форматирования строки для UI**
(`Units.formatDistance()`).
- Запрещено хранить в рантайм-состоянии значения, конвертированные в
мили — это исключает накопление ошибок округления при многократном
переключении (ФТ-3 ТЗ, AC-2, тест TP-04).
## 4. Клиентское хранилище
| Параметр | Значение |
|----------|----------|
| Механизм | `localStorage` |
| Ключ | `distance_unit` |
| Допустимые значения | `'km'`, `'mi'` |
| Значение по умолчанию | `'km'` (при отсутствии ключа или некорректном значении) |
| Объём | ≈ 2 байта полезной нагрузки на браузер |
| Запись | при переключении (`Units.setUnit()`) |
| Чтение | при инициализации модуля `units.js` |
Ключ согласуется с уже сложившейся в проекте конвенцией UI-настроек в
`localStorage` (`enduro-theme-mode`, `terrain-*`, `trails-*`,
`poi-visible`).
## 5. Константы и точность
| Параметр | Значение |
|----------|----------|
| Коэффициент перевода | `1 km = 0.621371 mi` (`Units.KM_TO_MI`), единственное место объявления |
| Точность по умолчанию | 1 знак после запятой (AC-2) |
| Параметризация точности | `formatDistance()` принимает `precision`, чтобы сохранить существующую точность карточек маршрутов (`toFixed(0)`) — см. `10-tech-risks.md`, R5 |
| Разделитель дробной части | Привести к единому виду; сейчас в `app.js` есть расхождение (`.` и `,`) — см. `10-tech-risks.md`, R4 |
## 6. Персональные данные
Персональные данные (PII) отсутствуют. `distance_unit` — обезличенная
UI-настройка, не покидает браузер пользователя, не логируется на
backend.
## 7. Вывод
Серверных данных, схем и миграций ET-005 **не затрагивает**. Единственная
единица данных — клиентский флаг `localStorage:distance_unit`. Требований
к резервному копированию, ретенции и приватности не возникает.

View File

@@ -0,0 +1,128 @@
---
type: tech-risks
work_item_id: ET-005
title: "Технические риски — ET-005: Переключение единиц измерения (км/мили)"
version: 1
status: approved
created_at: 2026-05-21
authors:
- "agent:architect"
---
# Технические риски — ET-005
Шкала: вероятность (Н/С/В) × влияние (Н/С/В).
## R1 — Пропуск одного из мест форматирования расстояний
- **Описание:** форматирование `(m/1000).toFixed(N)+' км'` захардкожено
минимум в **13 местах** `src/web/app.js`. Если хотя бы одно не
переведено на `Units.formatDistance()`, после переключения на мили это
расстояние останется в км — нарушение AC-2 («все расстояния»).
- **Известные call-sites** (ориентир для разработки и review):
`formatDist()` ≈ стр. 187189; карточка сегмента ≈ 638; карточки
маршрута ≈ 1157, 1191, 2202/2212, 2357/2370, 2605; scale-bar
≈ 14161440; всплывающая подсказка ≈ 1478; линейка/ruler ≈ 1837,
1875, 1885, 1931. Список ориентировочный — нумерация строк может
сдвинуться; обязателен повторный grep-аудит.
- **Вероятность / Влияние:** С / В.
- **Митигация:** централизация в `units.js` (ADR-0001); grep-аудит
по `' км'` / `/ 1000` / `toFixed` на этапе разработки; e2e TP-02
должен проверять **каждую** видимую поверхность с расстоянием, а не
одну.
## R2 — Отсутствие политики для суб-километровых расстояний
- **Описание:** часть мест отображает расстояния < 1 км в метрах
(«м» — напр. линейка, стр. 1837/1885). Имперский эквивалент в AC не
определён (футы? мили с долями?). Без явной политики возможен
показ «м» в режиме «mi».
- **Вероятность / Влияние:** В / С.
- **Митигация:** зафиксировать политику в реализации `units.js`.
Рекомендация архитектуры: в режиме `mi` суб-километровые значения
показывать в милях с увеличенной точностью (напр. 2 знака), **футы
не вводить** — это упрощает форматтер и достаточно для эндуро-сценария.
Финальное решение — за разработкой/ревью, но политика должна быть
единой и явной.
## R3 — Scale-bar имеет собственную логику единиц
- **Описание:** масштабная линейка карты (`szb`, стр. 14161440) уже
содержит независимую логику `km`/`m` с «красивым» округлением. Это
отдельный код-путь, который легко не заметить при рефакторинге.
- **Вероятность / Влияние:** С / С.
- **Митигация:** явно включить scale-bar в список перевода на `Units`
и в оркестратор `onUnitChange()`; добавить scale-bar в проверки e2e
TP-02.
## R4 — Несогласованный разделитель дробной части
- **Описание:** в `app.js` есть расхождение — `formatDist()` (стр. 189)
использует точку (`12.3 км`), карточка сегмента (стр. 638) —
`.replace('.', ',')` (`12,3 км`). При централизации форматтер должен
выбрать один вариант, иначе UI станет визуально неоднородным.
- **Вероятность / Влияние:** В / Н.
- **Митигация:** `units.js` задаёт единый разделитель для всех
поверхностей. Так как у задачи нет требования на локаль, рекомендуется
оставить запятую (текущий пользовательский язык интерфейса — русский),
но решение должно быть единым и явным.
## R5 — Конфликт точности: AC-2 vs существующие карточки
- **Описание:** AC-2 требует «округление до 1 знака», но карточки
маршрутов сейчас используют `toFixed(0)` (целые км, стр. 2202, 2357).
Слепое применение «1 знака» изменит вид карточек; слепое сохранение
`toFixed(0)` нарушит букву AC-2.
- **Вероятность / Влияние:** С / Н.
- **Митигация:** `formatDistance(meters, {precision})` параметризует
точность; по умолчанию — 1 знак (AC-2), карточки явно запрашивают
свою точность. Трактовка: AC-2 задаёт точность пересчёта/дефолта, а
не запрещает целочисленный показ в карточках.
## R6 — Экспорт GPX и параметрические значения не должны конвертироваться
- **Описание:** есть риск «заодно» сконвертировать величины, которые
обязаны остаться метрическими: экспорт GPX (стандарт GPX — метры) и
кнопки-параметры построения маршрута (`data-km` радиусы разведки/
scenic: «20 км», «50», «100», `index.html:130180`). Это **входные
параметры**, а не отображаемые расстояния.
- **Вероятность / Влияние:** Н / С.
- **Митигация:** scope ET-005 — **только отображаемые** расстояния.
GPX-данные и `data-km`-параметры из перевода единиц **исключены**;
зафиксировать в review-чеклисте.
## R7 — Порядок подключения скриптов
- **Описание:** `units.js` подключается как классический скрипт; `app.js`
обращается к `window.Units` на старте. Если `units.js` подключён после
`app.js`, инициализация упадёт с `Units is not defined`.
- **Вероятность / Влияние:** Н / В.
- **Митигация:** `<script src="units.js">` строго перед
`<script src="app.js">` в `index.html`; контрольная точка review;
smoke-проверка загрузки страницы.
## R8 — Мобильное размещение переключателя
- **Описание:** НФТ ТЗ и AC-4 требуют доступности кнопки на всех
размерах экрана и неперекрытия других элементов. Попап `#terrain-popup`
пополняется ещё одним рядом — на узких экранах (375px, TP-05) возможен
выход за пределы/перекрытие.
- **Вероятность / Влияние:** Н / С.
- **Митигация:** переиспользовать готовый `.seg-control` (адаптивен по
ширине); проверить попап на 375px; e2e TP-05.
## Сводка
| ID | Риск | Вер. | Влияние | Класс |
|----|------|------|---------|-------|
| R1 | Пропуск call-site форматирования | С | В | Высокий |
| R2 | Нет политики для суб-км значений | В | С | Высокий |
| R3 | Scale-bar — отдельная логика единиц | С | С | Средний |
| R4 | Несогласованный десятичный разделитель | В | Н | Средний |
| R5 | Конфликт точности AC-2 vs карточки | С | Н | Низкий |
| R6 | GPX / параметры не конвертировать | Н | С | Низкий |
| R7 | Порядок подключения скриптов | Н | В | Средний |
| R8 | Мобильное размещение переключателя | Н | С | Низкий |
Блокирующих рисков нет. R1 и R2 требуют явного внимания на разработке и
ревью. Эскалация `arch:major-change` или возврат в Анализ не требуются.