71d0c983fd
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.
533 lines
42 KiB
Markdown
533 lines
42 KiB
Markdown
# Пользовательские истории: Обработка ошибок, граничные случаи и доступность
|
||
|
||
## 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-информацию.
|
||
|
||
**Примечание:** Предназначено для разработки и тестирования, не для конечных пользователей.
|
||
|
||
---
|