Files
flights_web/docs/user-stories-7-errors-accessibility.md
T
gnezim 71d0c983fd
CI / ci (push) Failing after 28s
Deploy / build-and-deploy (push) Failing after 5s
Fix API calls: bind fetch to globalThis, fix date format for calendar
Root cause of search not working: globalThis.fetch stored as a class
field loses its Window binding, causing 'Illegal invocation'. Fixed
with fetch.bind(globalThis).

Also fix calendar days endpoint date format from yyyyMMdd to
yyyy-MM-ddT00:00:00 matching Angular's ApiFormatterService.
2026-04-15 22:32:51 +03:00

533 lines
42 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.
# Пользовательские истории: Обработка ошибок, граничные случаи и доступность
## US-85: Ошибка 404 — Страница не найдена
**Цель:** Пользователь видит понятное сообщение при переходе на несуществующий URL и может быстро вернуться к рабочему разделу.
**Путь клиента:**
1. **Переход по неверному URL — ввод или клик по битой ссылке** — пользователь вводит `/onlineboard/invalid` или попадает на устаревшую ссылку.
2. **Router сопоставляет маршрут — wildcard redirect** — роутер не находит соответствия и перенаправляет на `/error/404`.
3. **Страница ошибки загружается — ErrorPageComponent** — компонент читает `errorCode`, `title`, `description` из `route.snapshot.data`.
4. **Отображение сообщения — код и текст** — на экране виден код «404», заголовок `PAGE404.HEADER` и описание `PAGE404.DESCRIPTION`.
5. **Скрытие баннеров хоста — hideAflComponents()** — страница скрывает `.afl-component--banners` на время показа ошибки.
6. **Поле поиска на странице ошибки** — на странице отображается текстовое поле с кнопкой поиска; ввод запроса и клик по кнопке открывает результаты поиска на aeroflot.ru в новой вкладке.
7. **Возврат к работе — переход на главную** — пользователь нажимает кнопку и возвращается на `/onlineboard` или в поиск.
**Критерии приёмки:**
- ✅ Маршрут `/error/404` отображает страницу с кодом 404.
- ✅ Любой неизвестный URL приводит к редиректу на `/error/404`.
- ✅ Заголовок и описание берутся из переводов (`PAGE404.*`).
- ✅ Поле поиска позволяет выполнить поиск на aeroflot.ru из страницы ошибки (открывается в новой вкладке).
- ✅ Кнопка «На главную» возвращает на рабочий раздел.
- ✅ Баннеры хост-приложения скрыты на время показа ошибки и восстанавливаются при уходе со страницы (`ngOnDestroy`).
**Примечание:** URL онлайн-табло — `/onlineboard` (без дефиса). Компонент ошибки — общий для 404 и 500, различие задаётся через `route.data`.
---
## US-86: Ошибка 500 — Внутренняя ошибка сервера
**Цель:** Пользователь получает понятное сообщение при серверной ошибке и возможность повторить запрос или вернуться на главную.
**Путь клиента:**
1. **Действие пользователя — поиск или загрузка расписания** — пользователь запускает поиск рейсов.
2. **Ошибка на бэкенде — HTTP 5xx** — API возвращает 500 либо приложение роутится на `/error/500` как default.
3. **Показ страницы ошибки — ErrorPageComponent с errorCode=500** — читаются `PAGE500.HEADER` и `PAGE500.DESCRIPTION`.
4. **Скрытие баннеров — hideAflComponents()** — сторонние компоненты хоста временно скрываются.
5. **Действия пользователя — повтор или возврат** — пользователь может повторить запрос или перейти на главную.
6. **Восстановление — ngOnDestroy** — при уходе со страницы баннеры возвращаются в прежнее состояние.
**Критерии приёмки:**
- ✅ При неизвестной ошибке приложение показывает `/error/500` (дефолтный редирект с `/error`).
- ✅ На странице отображается код «500» и локализованные `PAGE500.*` строки.
- ✅ Кнопка «На главную» доступна и работает.
- ✅ Баннеры хоста скрываются и корректно восстанавливаются.
- ✅ Страница не ломает верстку хост-приложения.
**Примечание:** URL онлайн-табло — `/onlineboard` (без дефиса). В Angular-источнике ErrorPage один компонент на оба кода; дифференциация — через `route.data`.
---
## US-88: Timeout при загрузке данных
**Цель:** Пользователь видит корректное состояние, если ответ сервера долго не приходит, и может отменить или повторить запрос.
**Путь клиента:**
1. **Запуск запроса — пользователь выполняет поиск** — отправляется запрос на API рейсов/расписания.
2. **Отображение спиннера — loading state** — UI показывает индикатор загрузки до получения ответа.
3. **Ожидание превышает лимит — длительное отсутствие ответа** — сеть медленная или сервер не отвечает.
4. **Обработка таймаута — http-cancel / error handler** — запрос завершается с ошибкой, приложение переходит в состояние ошибки.
5. **Сообщение пользователю — понятный текст** — отображается сообщение о проблеме соединения и предложение повторить.
6. **Повтор или отмена — пользователь управляет ситуацией** — можно повторить поиск либо отказаться.
**Критерии приёмки:**
- ✅ Во время загрузки виден индикатор прогресса.
- ✅ При длительном отсутствии ответа приложение не «зависает».
- ✅ После таймаута пользователь видит понятное сообщение об ошибке.
- ✅ Доступно действие «Повторить».
- ✅ Отменённые запросы не обновляют UI задним числом.
**Примечание:** Сценарий тестовый — описывает ожидаемое поведение; в Angular-источнике явного лимита таймаута нет, обработка опирается на общий error handler и `HttpCancelService`.
---
## US-89: Некорректный ввод (специальные символы)
**Цель:** Система безопасно обрабатывает ввод со специальными символами и не допускает XSS.
**Путь клиента:**
1. **Ввод спецсимволов — пользователь печатает `<script>`, кавычки, HTML-теги** — данные попадают в поле поиска/фильтра.
2. **Санитизация — Angular/React встроенная защита** — фреймворк экранирует значения при выводе.
3. **Валидация — проверка на валидные коды IATA или названия** — некорректные значения не проходят в запрос к API.
4. **Безопасное отображение — текст как текст** — символы показываются экранированными, без исполнения.
5. **Ответ пользователю — подсказка об ошибке** — при невалидном вводе показывается сообщение.
**Критерии приёмки:**
- ✅ Ввод `<script>`-тегов не приводит к исполнению JS.
- ✅ HTML-теги отображаются как текст, а не как разметка.
- ✅ Невалидные IATA/названия отклоняются до отправки на сервер.
- ✅ Ответ сервера также экранируется при выводе.
- ✅ Атрибуты DOM не собираются из пользовательского ввода напрямую.
**Примечание:** Тестовый сценарий безопасности; в кодовой базе обеспечивается framework-уровнем (автоэкранирование).
---
## US-90: Невалидная комбинация параметров
**Цель:** Система валидирует комбинации параметров поиска и предотвращает бессмысленные запросы.
**Путь клиента:**
1. **Пользователь заполняет форму — города, даты, направление** — вводятся параметры поиска.
2. **Обнаружение конфликта — валидаторы формы** — пользователь выбирает одинаковый город для «Откуда» и «Куда», либо прошлую дату, либо недопустимый диапазон.
3. **Блокировка отправки — disable submit** — кнопка «Найти» остаётся неактивной или поиск не запускается.
4. **Отображение ошибок — подсказки под полями** — рядом с полем выводится причина ошибки.
5. **Исправление — пользователь меняет параметры** — после корректного ввода ошибки очищаются и поиск становится доступен.
**Критерии приёмки:**
- ✅ Совпадающие города «Откуда»/«Куда» отклоняются.
- ✅ Даты в прошлом недопустимы для выбора.
- ✅ Недопустимый диапазон дат показывает сообщение об ошибке.
- ✅ Пока форма невалидна, кнопка поиска неактивна.
- ✅ Сообщения об ошибках локализованы.
**Примечание:** Тестовый сценарий; в Angular-источнике правила валидации частично присутствуют в `shared/services/validators`.
---
## US-91: Пустой результат поиска
**Цель:** Пользователь получает понятное сообщение, когда по запросу нет рейсов.
**Путь клиента:**
1. **Поиск — пользователь задаёт параметры** — выбирает города и даты, запускает поиск.
2. **Запрос к API — корректные параметры** — валидация пройдена, запрос уходит на сервер.
3. **Пустой ответ — нет рейсов** — API возвращает пустой список для указанных условий.
4. **Отображение empty state — сообщение «Рейсов не найдено»** — вместо списка показывается информационный блок.
5. **Подсказки пользователю — альтернативы** — предлагается изменить дату/направление или сбросить фильтры.
**Критерии приёмки:**
- ✅ Пустой массив результатов не показывает «сломанный» UI.
- ✅ Отображается локализованное сообщение об отсутствии рейсов.
- ✅ Пользователь может легко изменить параметры поиска.
- ✅ Активные фильтры не блокируют новый поиск.
- ✅ Состояние empty отличается визуально от состояния ошибки.
---
## US-92: Unicode символы в вводе
**Цель:** Система корректно отображает и обрабатывает строки в Unicode (кириллица, CJK, арабский, emoji).
**Путь клиента:**
1. **Ввод Unicode — пользователь печатает нелатинские символы** — в поле поиска появляется текст в разных алфавитах.
2. **Передача в API — UTF-8 кодирование** — запрос корректно кодирует параметры.
3. **Ответ сервера — Unicode в названиях** — города и аэропорты возвращаются с корректной кодировкой.
4. **Отображение — шрифты и вёрстка** — названия видны без «кракозябр».
5. **Фильтрация и поиск — регистронезависимое сравнение** — совпадения находятся вне зависимости от регистра и алфавита.
**Критерии приёмки:**
- ✅ Кириллица, CJK и арабский отображаются корректно.
- ✅ Emoji не ломают верстку (допустимо отклонить при валидации IATA).
- ✅ Поиск работает с Unicode-запросами.
- ✅ Параметры корректно кодируются в URL.
- ✅ Ответ API с Unicode-строками рендерится без повреждений.
---
## US-93: Очень длинные имена городов и аэропортов
**Цель:** Интерфейс не ломается при очень длинных названиях городов, аэропортов и авиакомпаний.
**Путь клиента:**
1. **Загрузка данных — длинное название** — API возвращает запись с длинным именем (например, полное название аэропорта).
2. **Отображение в списке — ограниченная ширина** — карточка или строка имеет фиксированную ширину.
3. **Обрезка текста — CSS ellipsis** — длинный текст усекается с многоточием.
4. **Полное имя — tooltip/title** — при наведении доступно полное название.
5. **Адаптив — разные breakpoints** — поведение сохраняется на мобильных и десктопе.
**Критерии приёмки:**
- ✅ Длинные названия не выходят за границы контейнера.
- ✅ Текст усекается с видимым многоточием.
- ✅ Полное название доступно через tooltip или аналогичный механизм.
- ✅ Верстка соседних элементов не сдвигается.
- ✅ Поведение работает на всех breakpoint.
**Примечание:** Тестовый сценарий устойчивости; в кодовой базе опирается на CSS-классы текстовых ячеек.
---
## US-94: Быстрая последовательность поисков
**Цель:** Система корректно обрабатывает быстро выполняемые подряд запросы и показывает результат последнего.
**Путь клиента:**
1. **Пользователь запускает поиск — первый запрос** — уходит HTTP-запрос на сервер.
2. **Смена параметров — новый запрос до завершения предыдущего** — пользователь меняет город/дату и снова нажимает поиск.
3. **Отмена предыдущего — HttpCancelService** — незавершённые запросы отменяются через cancel-механизм.
4. **Обработка последнего — применение свежего ответа** — в UI показывается результат только последнего запроса.
5. **Защита от гонок — state не перезаписывается старым ответом** — отменённые запросы не обновляют список.
**Критерии приёмки:**
- ✅ Отменённый запрос не обновляет UI.
- ✅ В результатах отображается ответ последнего запроса.
- ✅ Индикатор загрузки виден до завершения последнего запроса.
- ✅ Быстрые клики не приводят к миганию результатов.
- ✅ Нет утечек подписок/обработчиков.
**Примечание:** В Angular-источнике есть `shared/services/http-cancel.service.ts`, обеспечивающий отмену предыдущих запросов.
---
## US-95: Клавиатурная навигация
**Цель:** Пользователь может полностью управлять приложением только с клавиатуры.
**Путь клиента:**
1. **Навигация по Tab — обход интерактивных элементов** — фокус проходит по полям, кнопкам и ссылкам в логичном порядке.
2. **Активация — Enter/Space** — кнопки и ссылки активируются клавиатурой.
3. **Отправка формы — Enter в поле поиска** — запускает поиск без использования мыши.
4. **Закрытие попапов — Escape** — выпадающие списки и диалоги закрываются по Esc.
5. **Навигация по спискам — стрелки** — в dropdown и списках работает перемещение стрелками.
6. **Видимый фокус — focus ring** — текущий элемент визуально выделен.
**Критерии приёмки:**
- ✅ Tab-порядок соответствует визуальному порядку элементов.
- ✅ Нет «ловушек» фокуса вне диалогов.
- ✅ Enter отправляет форму поиска.
- ✅ Escape закрывает выпадающие меню и диалоги.
- ✅ На всех интерактивных элементах виден focus-индикатор.
**Примечание:** Тестовый сценарий доступности; поведение собирается из встроенной семантики и ARIA.
---
## US-96: Читатели экрана (ARIA-метки)
**Цель:** Приложение корректно работает со screen reader (NVDA, VoiceOver, JAWS).
**Путь клиента:**
1. **Загрузка страницы — screen reader озвучивает заголовок**`<title>` и `h1` корректно читаются.
2. **Формы — label и aria-label** — каждое поле связано с текстовой меткой.
3. **Кнопки и иконки — aria-label для icon-only** — иконочные кнопки имеют текстовую аннотацию.
4. **Динамический контент — aria-live** — появление результатов поиска или ошибок объявляется.
5. **Списки и таблицы — корректные роли** — расписание использует семантическую структуру.
6. **Изображения — alt** — декоративные скрыты (`alt=""`), смысловые имеют описание.
**Критерии приёмки:**
- ✅ Все поля форм имеют связанный label или aria-label.
- ✅ Icon-only кнопки озвучиваются со смысловой подписью.
- ✅ Динамические обновления (результаты, ошибки) объявляются screen reader.
- ✅ Используется семантический HTML (`button`, `nav`, `main`, `h1``h3`).
- ✅ Изображения имеют корректный alt.
**Примечание:** Тестовый сценарий WCAG; конкретная реализация ARIA — часть компонентного слоя.
---
## US-98: Фокусное кольцо (focus-visible)
**Цель:** Пользователь, навигирующий с клавиатуры, всегда видит, какой элемент находится в фокусе.
**Путь клиента:**
1. **Навигация Tab — пользователь переходит между элементами** — каждое нажатие Tab смещает фокус.
2. **Рендеринг focus ring — CSS `:focus-visible`** — обводка появляется только при клавиатурной навигации.
3. **Скрытие при клике мышью — `:focus:not(:focus-visible)`** — кольцо не отображается при обычных кликах.
4. **Контрастность — видимое кольцо** — цвет и толщина заметны на фоне.
5. **Единообразие — стиль одинаков для всех интерактивных элементов** — кнопки, ссылки, поля ввода.
**Критерии приёмки:**
- ✅ При Tab-навигации фокус виден на всех интерактивных элементах.
- ✅ При кликах мышью кольцо не появляется (используется `:focus-visible`).
- ✅ Контраст кольца соответствует WCAG AA.
- ✅ Стиль фокуса согласован между компонентами.
- ✅ Ни один интерактивный элемент не остаётся без индикатора фокуса.
**Примечание:** Тестовый сценарий доступности, реализуется через глобальные CSS-стили.
---
## US-99: Масштабирование текста (zoom)
**Цель:** Пользователь может увеличивать текст/страницу до 200% без потери функциональности.
**Путь клиента:**
1. **Запуск zoom — Ctrl + / Cmd +** — браузер увеличивает масштаб страницы.
2. **Адаптация текста — относительные единицы**`rem`/`em` корректно пересчитываются.
3. **Адаптивный layout — reflow** — сетка перестраивается под новую ширину.
4. **Отсутствие горизонтального скролла — контент остаётся в viewport** — до 200% без обрезки.
5. **Интерактив доступен — все кнопки и поля видны** — форма поиска и результаты пригодны к использованию.
**Критерии приёмки:**
- ✅ Увеличение до 200% не ломает layout.
- ✅ Нет горизонтального скролла на типовых breakpoint.
- ✅ Текст не перекрывается и не обрезается.
- ✅ Все интерактивные элементы остаются доступными.
- ✅ Relative units используются для размеров текста и отступов.
**Примечание:** Тестовый сценарий WCAG 1.4.4; реализация зависит от CSS.
---
## US-100: Сенсорная навигация (touch)
**Цель:** Мобильные пользователи могут полностью управлять приложением пальцами.
**Путь клиента:**
1. **Touch-жесты — tap, swipe, pinch** — пользователь взаимодействует с экраном.
2. **Размер целей — минимум 44×44px** — кнопки и ссылки достаточны для попадания пальцем.
3. **Интервалы — достаточные отступы** — соседние цели не кликаются случайно.
4. **Альтернативы жестам — обычные кнопки** — любое действие через swipe дублируется кнопкой.
5. **Отсутствие hover-only — tap доступен всегда** — функциональность не скрыта за hover.
**Критерии приёмки:**
- ✅ Touch-таргеты не меньше 44×44px.
- ✅ Расстояние между целями предотвращает случайные нажатия.
- ✅ Все функции доступны без hover.
- ✅ Жесты (где используются) имеют альтернативу обычным тапом.
- ✅ Скролл и pinch-zoom работают штатно.
**Примечание:** Тестовый сценарий мобильной доступности.
---
## US-101: Персистентное состояние (StateService)
**Цель:** Ключевое состояние приложения сохраняется между переходами и переживает перезагрузку страницы там, где это предусмотрено.
**Путь клиента:**
1. **Сохранение — StateService.set(key, data)** — страница кладёт состояние в Map по ключу.
2. **Переход между разделами — навигация** — пользователь уходит с экрана и возвращается.
3. **Чтение — StateService.get(key)** — экран восстанавливает данные из сервиса.
4. **Параметры поиска в URL — query params** — часть состояния синхронизируется через URL.
5. **Перезагрузка страницы — refresh** — параметры из URL позволяют восстановить форму и запустить поиск.
6. **Очистка — StateService.delete(key)** — при необходимости состояние сбрасывается.
**Критерии приёмки:**
-`StateService.set`/`get`/`delete` работают как in-memory Map хранилище.
- ✅ При возврате на страницу её состояние восстанавливается из сервиса.
- ✅ Параметры поиска присутствуют в URL и читаются при старте.
- ✅ После refresh форма заполняется из URL-параметров.
- ✅ Данные не «текут» между независимыми ключами.
**Примечание:** В Angular-источнике `StateService` — простой in-memory Map (`shared/services/state.service.ts`), он не использует localStorage; персистенция между перезагрузками достигается через URL.
---
## US-102: История браузера (back/forward)
**Цель:** Кнопки браузера «Назад» и «Вперёд» корректно перемещают пользователя по состояниям приложения.
**Путь клиента:**
1. **Переход по разделам — пользователь открывает страницы** — роутер добавляет записи в history.
2. **Обновление URL — query params отражают фильтры** — параметры поиска и сортировки видны в URL.
3. **Нажатие Back — popstate** — роутер восстанавливает предыдущее состояние.
4. **Нажатие Forward — возврат к следующему состоянию** — аналогично, без потери данных.
5. **Синхронизация UI — чтение query params** — фильтры и форма обновляются под URL.
**Критерии приёмки:**
- ✅ Каждая значимая навигация создаёт запись в history.
- ✅ Back возвращает на предыдущий URL с корректным состоянием.
- ✅ Forward работает симметрично.
- ✅ Query params читаются при навигации и обновляют UI.
- ✅ Глубокие ссылки открывают нужный раздел напрямую.
**Примечание:** В Angular-источнике навигация — стандартный Angular Router; синхронизация через query params.
---
## US-103: Обработка больших объёмов данных
**Цель:** Приложение остаётся отзывчивым при отображении длинных списков рейсов/расписания.
**Путь клиента:**
1. **Загрузка данных — большой ответ API** — приходит расписание с сотнями рейсов.
2. **Рендер списка — оптимизированный вывод** — используется виртуализация либо разумная пагинация/ограничение.
3. **Скролл — плавная прокрутка** — браузер не проседает по FPS.
4. **Фильтрация и сортировка — мгновенная реакция** — действия не вызывают полной перерисовки.
5. **Освобождение памяти — unsubscribe при уходе** — подписки и кэши корректно очищаются.
**Критерии приёмки:**
- ✅ Список из сотен элементов рендерится без ощутимых задержек.
- ✅ Скролл остаётся плавным на типовых устройствах.
- ✅ Фильтрация и сортировка не «замораживают» UI.
- ✅ При уходе со страницы не остаётся зомби-подписок.
- ✅ Память приложения не растёт неограниченно при повторных поисках.
**Примечание:** Тестовый сценарий производительности; конкретный механизм (виртуализация/пагинация) — на выбор реализации.
---
## US-104: Кэширование ответов (CacheService 30s)
**Цель:** Повторные одинаковые запросы к API обслуживаются из кэша, чтобы снизить нагрузку и ускорить отклик.
**Путь клиента:**
1. **Первый запрос — CacheService.get(key, options)** — кэш пуст, запрос уходит на сервер.
2. **Сохранение ответа — CacheService.set(key, options, result)** — результат кладётся в `requestsMap` по ключу.
3. **Таймер инвалидации — setTimeout 30s** — через `INVALID_TIMEOUT = 30 * 1000` запись удаляется.
4. **Повторный запрос в пределах окна — быстрый ответ**`get()` возвращает закэшированный `result`.
5. **Истечение 30 секунд — удаление ключа** — следующий запрос снова идёт на сервер.
6. **Ручная инвалидация — CacheService.delete(key)** — позволяет сбросить запись принудительно.
**Критерии приёмки:**
-`CacheService.set/get/delete` работают по in-memory Map.
- ✅ Таймаут инвалидации — 30 секунд (`INVALID_TIMEOUT = 30 * 1000`).
- ✅ Повторный запрос в пределах окна не порождает сетевой вызов.
- ✅ После таймаута запись удаляется и следующий запрос уходит на сервер.
-`delete(key)` очищает указанную запись вручную.
**Примечание:** Источник истины — `shared/services/cache.service.ts`. Таймер на каждую `set()` перезапускается (`clearTimeout`), поэтому окно отсчитывается от последнего обновления записи.
---
## US-105: Обновление данных в реальном времени (SignalR)
**Цель:** Данные онлайн-табло автоматически обновляются при поступлении серверных событий, чтобы пользователь видел актуальную информацию без ручной перезагрузки.
**Путь клиента:**
1. **Подключение к хабу** — при загрузке страницы результатов или деталей рейса `RefreshBoardService` / `RefreshService` инициализирует SignalR-соединение с `environment.urlForTrackerHub`.
2. **Подписка на события** — сервис подписывается на события `RefreshDate` (для списка рейсов) или `Refresh` (для деталей рейса по ID).
3. **Тихое обновление** — при поступлении события данные перезагружаются с флагом `silent`, без показа индикатора загрузки, чтобы не отвлекать пользователя.
4. **Оверлей устаревших данных**`FadeService` запускает таймер; при бездействии пользователя в течение `environment.refreshPauseMin` минут поверх страницы появляется полупрозрачный оверлей с сообщением «Данные устарели, обновите страницу!».
5. **Автоматический редирект** — если бездействие превышает `environment.refreshStopMin` минут, приложение перенаправляет пользователя на главную страницу (`/`).
6. **Очистка** — при уходе со страницы (`ngOnDestroy`) подписки SignalR и таймеры `FadeService` очищаются.
**Критерии приёмки:**
- ✅ На страницах результатов онлайн-табло устанавливается SignalR-соединение для получения обновлений.
- ✅ На странице деталей рейса подписка привязана к конкретному ID рейса.
- ✅ Обновление данных происходит без видимого индикатора загрузки (silent reload).
- ✅ После `refreshPauseMin` минут бездействия показывается полноэкранный оверлей (z-index 9999, полупрозрачный фон).
- ✅ После `refreshStopMin` минут бездействия происходит автоматический редирект на главную.
- ✅ SignalR-подписки и таймеры корректно очищаются при уничтожении компонентов.
**Примечание:** Функция актуальна только для онлайн-табло (реальное время). Расписание и карта полётов не используют SignalR. `FadeService` создаёт overlay программно через DOM API (`document.createElement`).
---
## US-106: Встроенный чат-бот
**Цель:** Пользователь может воспользоваться чат-ботом для получения справочной информации.
**Путь клиента:**
1. **Инициализация** — компонент `chat-bot` в `ngAfterViewInit` динамически создаёт `<script>` элемент с URL из `environment.chatBotScript`.
2. **Загрузка виджета** — скрипт монтируется к `#app-chat-bot-container`, загружая внешний чат-виджет.
3. **Использование** — после загрузки пользователь видит виджет чата и может задавать вопросы.
**Критерии приёмки:**
- ✅ Чат-бот загружается динамически через внешний скрипт из `environment.chatBotScript`.
- ✅ Виджет монтируется в контейнер `#app-chat-bot-container`.
- ✅ Ошибки загрузки скрипта не ломают остальной интерфейс.
**Примечание:** URL скрипта настраивается через конфигурацию окружения.
---
## US-107: Canonical URL и SEO мета-теги по страницам
**Цель:** Каждая страница приложения имеет корректные canonical URL и уникальные SEO мета-теги для правильной индексации поисковыми системами.
**Путь клиента:**
1. **Установка canonical**`CanonicalService` формирует `<link rel="canonical">` для текущей страницы.
2. **Мета-теги по разделам** — каждый раздел (онлайн-табло, расписание, карта полётов) имеет собственные компоненты мета-тегов с уникальными ключами `SEO.*`:
- Онлайн-табло: стартовая, результаты (по номеру/маршруту/отправлению/прибытию), детали рейса
- Расписание: стартовая, результаты, детали рейса
- Карта полётов: стартовая
3. **Динамические значения** — для страниц результатов и деталей мета-теги включают названия городов, даты и номера рейсов.
4. **OpenGraph** — для страниц деталей формируются `og:title`, `og:description`, `og:image` теги.
5. **noRobots** — на страницах с динамическими параметрами (результаты, детали) установлен `noRobots: true` для предотвращения индексации.
**Критерии приёмки:**
- ✅ Canonical URL корректен для каждой страницы.
- ✅ Мета-теги `title` и `description` уникальны для каждого типа страницы.
- ✅ Динамические страницы включают контекст (город, дата, номер рейса) в мета-теги.
- ✅ Стартовые страницы разделов индексируются (`noRobots: false`), страницы результатов — нет.
- ✅ OpenGraph-теги формируются для страниц деталей.
**Примечание:** Компоненты мета-тегов наследуют от `MetaBaseComponent` и переопределяют `translateTitle()`/`translateDescription()`.
---
## US-108: Отображение версии API (debug-режим)
**Цель:** В debug-режиме разработчик может видеть текущую версию API для диагностики.
**Путь клиента:**
1. **Проверка флага**`AppVersionComponent` проверяет `settings.showDebugVersion` из `APP_SETTINGS`.
2. **Запрос версии** — если флаг включён, `NetworkService.getApiVersion()` загружает номер версии.
3. **Отображение** — версия выводится компонентом `app-version` рядом с навигацией.
4. **Скрытие баннера** — при включённом debug-режиме скрывается элемент `.banner--top`.
**Критерии приёмки:**
- ✅ В обычном режиме версия не отображается.
- ✅ При `showDebugVersion = true` показывается номер версии API.
- ✅ Компонент `app-show-debug` условно рендерит debug-информацию.
**Примечание:** Предназначено для разработки и тестирования, не для конечных пользователей.
---