Files
enduro-trails/docs/work-items/ET-005/06-adr/adr-0001-unit-toggle-client-side.md
claude-bot bafbea2dab
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 5s
CI / build (push) Successful in 1s
architect(ET-005): ADR, infra-requirements, data-requirements, tech-risks
2026-05-21 22:18:08 +03:00

206 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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`