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

14 KiB
Raw Blame History

type, work_item_id, adr_id, title, status, created_at, authors, supersedes, superseded_by, labels
type work_item_id adr_id title status created_at authors supersedes superseded_by labels
adr ET-005 adr-0001 ADR-0001: Переключение единиц измерения (км/мили) — клиентское решение с централизованным форматтером accepted 2026-05-21
agent:architect

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