Fix API calls: bind fetch to globalThis, fix date format for calendar
CI / ci (push) Failing after 28s
Deploy / build-and-deploy (push) Failing after 5s

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.
This commit is contained in:
2026-04-15 22:32:51 +03:00
parent e7c20c3d2d
commit 71d0c983fd
12 changed files with 187 additions and 57 deletions
+25 -17
View File
@@ -2,9 +2,9 @@
## ✅ ВЕРИФИЦИРОВАНО: Только реально реализованные функции Angular
**Дата последнего обновления:** 9 апреля 2026 GMT+3
**Статус:** Очищено - удалены все нереализованные/гипотетические функции
**Источник верификации:** `/Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/ClientApp` (Angular 12.2.13)
**Дата последнего обновления:** 15 апреля 2026 GMT+3
**Статус:** Верифицировано v4.1 — полный аудит Angular-кода, добавлены пропущенные функции
**Источник верификации:** `ClientApp/` (Angular 12.2.13)
**Итоги аудита (Версия 4.0):**
@@ -42,7 +42,7 @@
Это набор пользовательских историй для **UI Dashboard** — React-версии онлайн доски полётов Аэрофлота. Документы организованы по функциональным областям и содержат описание целей, путей клиента и критериев приёмки.
**Всего историй:** 85 пользовательских историй ✅
**Всего историй:** 89 пользовательских историй ✅
**Документов:** 7
**Источник:** Angular 12.2.13 (Aeroflot.Flights.Web/ClientApp)
**Языки:** 9 (ru-ru, en-us, zh-cn, ko-kr, ja-jp, de-de, fr-fr, es-es, it-it)
@@ -162,7 +162,7 @@
### 📄 [Документ 7: Обработка ошибок и доступность](user-stories-7-errors-accessibility.md)
**US-85 до US-104 (без US-87, 97)**18 историй
**US-85 до US-108 (без US-87, 97)**22 истории
**Ошибки:**
@@ -194,20 +194,27 @@
- US-103: Обработка больших объёмов
- US-104: Кэширование (CacheService, 30s)
**Real-time и инфраструктура (добавлено в v4.1):**
- US-105: Обновление данных в реальном времени (SignalR + FadeService)
- US-106: Встроенный чат-бот
- US-107: Canonical URL и SEO мета-теги по страницам
- US-108: Отображение версии API (debug-режим)
---
## Статистика
| Документ | Диапазон | Историй | Статус |
| ------------------------ | ----------------------- | ------- | --------------------------- |
| ------------------------ | -------------------------------- | ------- | --------------------------- |
| 1. Навигация | US-1 до US-11 | 11 | ✅ Все реализованы |
| 2. Онлайн-Табло | US-12 до US-22 | 10 | ✅ Все реализованы |
| 3. Поиск расписания | US-23 до US-33 | 11 | ✅ Все реализованы |
| 4. Результаты расписания | US-35-39, 42, 46 | 7 | ✅ Все реализованы |
| 5. Детали полёта | US-40, 41, 47-56, 62-64 | 15 | ✅ Все реализованы |
| 6. Карта полётов | US-65-76, 79 | 13 | ✅ Все реализованы |
| 7. Ошибки и доступность | US-85-86, 88-96, 98-104 | 18 | ✅ Все реализованы |
| **ИТОГО** | **85 историй** | **85** | ✅ Полностью верифицировано |
| 7. Ошибки и доступность | US-85-86, 88-96, 98-104, 105-108 | 22 | ✅ Все реализованы |
| **ИТОГО** | **89 историй** | **89** | ✅ Полностью верифицировано |
---
@@ -241,14 +248,14 @@
Каждая оставшаяся история подтверждена в исходном коде по следующим путям:
- `src/app/features/online-board/` — Doc 2
- `src/app/features/schedule/` — Docs 3, 4
- `src/app/features/flights-map/` — Doc 6
- `src/app/features/popular-requests/` — Doc 1 US-7
- `src/app/modules/pages/details/` — Doc 5
- `src/app/components/` (city-autocomplete, dates-selectors, breadcrumds, search-history, page) — Doc 1, общие
- `src/app/shared/services/` (localization, state, cache) — Doc 1, Doc 7
- `src/app/modules/pages/error-pages/` — Doc 7
- `ClientApp/src/app/features/online-board/` — Doc 2
- `ClientApp/src/app/features/schedule/` — Docs 3, 4
- `ClientApp/src/app/features/flights-map/` — Doc 6
- `ClientApp/src/app/features/popular-requests/` — Doc 1 US-7
- `ClientApp/src/app/modules/pages/details/` — Doc 5
- `ClientApp/src/app/components/` (city-autocomplete, dates-selectors, breadcrumds, search-history, page) — Doc 1, общие
- `ClientApp/src/app/shared/services/` (localization, state, cache) — Doc 1, Doc 7
- `ClientApp/src/app/modules/pages/error-pages/` — Doc 7
---
@@ -260,4 +267,5 @@
| 1.4 | Mar 2026 | 103 | 7 | Исправления, восстановлены US-40, US-41 |
| 2.0 | Apr 2026 | 210 | 14 | Расширение (многолётные, история, производительность) |
| 3.0 | Apr 2026 | 175 | 11 | Удалены удалённые: история, уведомления, аккаунты |
| **4.0** | **Apr 9 2026** | **85** | **7** | **Кодовая верификация: удалено всё неподтверждённое** |
| 4.0 | Apr 9 2026 | 85 | 7 | Кодовая верификация: удалено всё неподтверждённое |
| **4.1** | **Apr 15 2026**| **89** | **7** | **Повторная верификация: +4 истории (SignalR, чат-бот, SEO, debug), обновлены тултипы, календарь, автодополнение, код-шеринг** |
+7 -3
View File
@@ -176,8 +176,9 @@
1. **Отображение секции** — компонент `search-history` рендерит `<section class="frame search-history">`, но только если `historyItems.length > 0`.
2. **Аккордеон** — внутри секции расположен `<p-accordion>` с одним `<p-accordionTab>`, заголовок которого содержит ключ `BOARD.YOU_SEARCH` и `arrow-down-icon`.
3. **Список элементов** — в содержимом таба через `*ngFor` выводятся `<search-history-item>` для каждой записи из `historyItems`.
4. **Клик по элементу** — обработчик `(click)="openHistoryUrl(item)"` выполняет переход по сохранённому URL запроса, восстанавливая параметры формы.
5. **Типы элементов**поддерживаются разные шаблоны записей: `online-board-flight-number-history-item`, `online-board-route-history-item`, `schedule-item` — каждый отрисовывает свой набор полей.
4. **Иконка с тултипом типа поиска** — каждая запись начинается с иконки: для онлайн-табло (`plane-icon`) с `pTooltip` `SHARED.LAST-SEARCH-BOARD`, для расписания (`alarm-clock-icon`) с `pTooltip` `SHARED.LAST-SEARCH-SCHEDULE` (позиция `top`, стиль `afl-tooltip`).
5. **Клик по элементу**обработчик `(click)="openHistoryUrl(item)"` выполняет переход по сохранённому URL запроса, восстанавливая параметры формы.
6. **Типы элементов** — поддерживаются разные шаблоны записей: `online-board-flight-number-history-item`, `online-board-route-history-item`, `schedule-item` — каждый отрисовывает свой набор полей.
6. **Отсутствие на карте полётов**`search-history` не включён в шаблон страницы `/flights-map`, поэтому там секция не показывается.
**Критерии приёмки:**
@@ -185,6 +186,7 @@
- ✅ Секция видна только при наличии хотя бы одной записи.
- ✅ Заголовок таба переведён через ключ `BOARD.YOU_SEARCH`.
- ✅ Каждая запись — отдельный `<search-history-item>` с собственным шаблоном.
- ✅ Иконка типа записи сопровождается PrimeNG-тултипом (`SHARED.LAST-SEARCH-BOARD` или `SHARED.LAST-SEARCH-SCHEDULE`).
- ✅ Клик по записи вызывает `openHistoryUrl(item)` и выполняет переход.
- ✅ На странице «Карта полётов» секция отсутствует.
@@ -199,7 +201,8 @@
1. **Инициализация локали** — при загрузке модуля раздела `LocalizationService` уже содержит корректный `Language`, извлечённый из `APP_BASE_HREF`.
2. **Установка заголовка** — резолвер маршрута (`SettingsResolver`) подготавливает данные, а компоненты страницы через `Title` и `Meta` сервисы Angular устанавливают `document.title` и мета-теги.
3. **Источник строк** — ключи заголовков и описаний (например, `BOARD.TITLE`, `SCHEDULE.TITLE-TAB`, `FLIGHTS-MAP.TITLE`) берутся из JSON-файлов переводов через `translate.instant`.
4. **Применение языка** — значения подставляются в `<title>`, `<meta name="description">` и OpenGraph-теги на языке, соответствующем `baseHref`.
4. **Тултип заголовка страницы** — компонент `page-title` дублирует текст заголовка в `pTooltip` (позиция `bottom`, `hideDelay: 1000`, стиль `afl-tooltip tooltip--one-line`), чтобы длинные заголовки, усечённые CSS, были полностью доступны при наведении.
5. **Применение языка** — значения подставляются в `<title>`, `<meta name="description">` и OpenGraph-теги на языке, соответствующем `baseHref`.
5. **Смена языка через URL** — при переходе на префикс другого языка приложение перезагружается, резолвер и компоненты формируют мета-информацию уже на новом языке.
**Критерии приёмки:**
@@ -208,6 +211,7 @@
- ✅ Мета-теги `description`/`keywords` формируются из ключей перевода.
- ✅ Смена языка через URL-префикс обновляет title и meta.
- ✅ Все три раздела (`onlineboard`, `schedule`, `flights-map`) имеют собственные ключи заголовков.
- ✅ Заголовок `<h1>` сопровождается `pTooltip` (bottom, hideDelay 1000) для доступности длинных усечённых заголовков.
- ✅ При отсутствии перевода подставляется fallback-значение ключа.
---
+18 -8
View File
@@ -8,10 +8,11 @@
1. **Открытие фильтра** — пользователь открывает аккордеон «Номер рейса» в панели фильтров онлайн-табло; активная вкладка выделяется цветом.
2. **Ввод номера** — в составном поле слева зафиксирован префикс «SU», справа — текстовое поле с плейсхолдером (пример «1234»), максимальная длина 5 символов.
3. **Валидация формата** — сервис проверяет регулярное выражение `^\d\d\d\d?[A-Za-z]?$`: 3–4 цифры и опциональный буквенный суффикс; при ошибке показывается тултип с сообщением.
3. **Валидация формата** — сервис проверяет регулярное выражение `^\d\d\d\d?[A-Za-z]?$`: 3–4 цифры и опциональный буквенный суффикс; при ошибке показывается кастомный компонент `<tooltip>` с локализованным сообщением (не PrimeNG `pTooltip`, а отдельный всплывающий блок ошибки).
4. **Выбор даты** — ниже расположен `calendar-input` с меткой «Дата вылета»; диапазон ограничен `boardMinDate`/`boardMaxDate`, недоступные даты заблокированы.
5. **Запуск поиска** — кнопка «Найти» активна; при успешной валидации вызывается `handleFlightNumberSearch`, происходит переход на страницу результатов.
6. **Очистка поля** — кнопка-крестик справа от ввода очищает номер одним кликом.
5. **Автодополнение номера** — при потере фокуса метод `addZeros()` дополняет номер нулями до 4 цифр и отделяет буквенный суффикс (например, ввод `14``0014`, `1402A``1402` + суффикс `A`).
6. **Запуск поиска** — кнопка «Найти» активна; при успешной валидации вызывается `handleFlightNumberSearch`, происходит переход на страницу результатов.
7. **Очистка поля** — кнопка-крестик справа от ввода очищает номер одним кликом.
**Критерии приёмки:**
@@ -20,6 +21,7 @@
- ✅ Пустой номер показывает ошибку `FLIGHT_NUMBER-ERROR-EMPTY`.
- ✅ Некорректный формат показывает ошибку `FLIGHT_NUMBER-ERROR-ONLY-NUMBER`.
- ✅ Поиск не запускается, если не выбрана валидная дата.
- ✅ При потере фокуса `addZeros()` дополняет номер до 4 цифр и выделяет буквенный суффикс.
- ✅ Кнопка очистки сбрасывает значение номера рейса.
---
@@ -33,8 +35,10 @@
1. **Открытие календаря** — пользователь кликает по `calendar-input` с меткой «Дата вылета»; открывается поповер с месячной сеткой.
2. **Ограничение диапазона** — календарь принимает `minDate = boardMinDate` (вчера, −1 день) и `maxDate = boardMaxDate` (по умолчанию +7 дней от сегодня, настраивается через серверный контракт `boardSearchTo`).
3. **Блокировка недоступных дат** — массив `disabledDates` затемняет даты, по которым нет расписания, клик по ним игнорируется.
4. **Выбор даты** — при клике на активную дату значение записывается в модель фильтра, поповер закрывается.
5. **Отображение ошибки** — некорректное значение (пустое или невалидное) показывает сообщение `SHARED.DATE_FORMAT-WRONG` под полем.
4. **Быстрые кнопки** — под календарной сеткой отображаются кнопки «Сегодня» и «Завтра» (локализованные), позволяющие выбрать дату одним кликом без навигации по месяцам.
5. **Выбор даты** — при клике на активную дату значение записывается в модель фильтра, поповер закрывается.
6. **Автообновление диапазона в полночь** — таймер отслеживает смену суток и автоматически сдвигает `boardMinDate`/`boardMaxDate` без перезагрузки страницы.
7. **Отображение ошибки** — некорректное значение (пустое или невалидное) показывает сообщение `SHARED.DATE_FORMAT-WRONG` под полем.
6. **Синхронизация с результатами** — выбранная дата передаётся в страницу поиска и синхронизируется с компонентом `day-tabs`.
**Критерии приёмки:**
@@ -42,6 +46,8 @@
- ✅ Недоступны даты раньше `boardMinDate` и позже `boardMaxDate`.
- ✅ Даты из `disabledDates` визуально выключены и не кликабельны.
- ✅ Невалидная дата вызывает показ сообщения об ошибке.
- ✅ Быстрые кнопки «Сегодня»/«Завтра» позволяют выбрать дату одним кликом.
- ✅ При смене суток min/max даты автоматически сдвигаются через таймер.
- ✅ Выбранная дата сохраняется в модели и передаётся в URL при поиске.
- ✅ Календарь закрывается после выбора даты.
@@ -54,8 +60,8 @@
**Путь клиента:**
1. **Открытие аккордеона «Маршрут»** — раскрывается вкладка с формой маршрутного поиска внутри `online-board-filter`.
2. **Ввод города отправления** — компонент `city-autocomplete` с меткой «Город отправления» принимает ввод и показывает выпадающий список подсказок.
3. **Выбор из подсказок** — по мере набора сервис `cities-search-service` возвращает совпадения; клик по элементу подставляет город в поле.
2. **Ввод города отправления** — компонент `city-autocomplete` с меткой «Город отправления» принимает ввод и показывает выпадающий список подсказок. Поддерживается конвертация раскладки клавиатуры (`ruKeyboardLayout`): пользователь может набирать латиницей при включённой русской раскладке, и наоборот.
3. **Выбор из подсказок** — по мере набора сервис `cities-search-service` возвращает совпадения; клик по элементу подставляет город в поле. В выпадающем списке `city-select`, если у города несколько аэропортов, каждый аэропорт сопровождается `pTooltip` с полным названием аэропорта (`airport.name`, позиция `top`).
4. **Оставление поля прибытия пустым** — для поиска только по отправлению пользователь не заполняет второе поле.
5. **Выбор даты и запуск поиска** — указывается дата в `calendar-input`, клик по «Найти» переходит на страницу результатов вылетов.
6. **Обработка ошибки** — если город не выбран из списка, показывается ошибка `departureError` под полем.
@@ -63,7 +69,10 @@
**Критерии приёмки:**
- ✅ Автодополнение работает при вводе минимум нескольких символов.
- ✅ Поддерживается конвертация раскладки клавиатуры (русская ↔ латиница).
- ✅ При точном совпадении ввода с кодом или названием города происходит авто-выбор.
- ✅ Принимается только город, выбранный из списка подсказок.
- ✅ При наличии нескольких аэропортов у города каждый показывает `pTooltip` с полным названием.
- ✅ Ошибка отображается, если город введён, но не распознан.
- ✅ Поиск возможен только с заполненным полем отправления и валидной датой.
- ✅ Результаты содержат только рейсы с вылетом из указанного города.
@@ -104,7 +113,8 @@
3. **Кнопка обмена** — между полями расположена кнопка со SVG-иконкой `changeCity`; клик вызывает метод `exchange()` и меняет значения полей местами.
4. **Выбор даты** — используется `calendar-input` с ограничениями `boardMinDate`/`boardMaxDate`.
5. **Опциональный фильтр времени** — под датой компонент `time-selector` в режиме `fullView=false` позволяет ограничить диапазон времени вылета.
6. **Запуск поиска** — кнопка «Найти» вызывает `handleRouteSearch`, результаты открываются на маршрутной странице.
6. **Автозаполнение по геолокации** — если `UserLocationService` определил местоположение пользователя и поле отправления пусто, оно предзаполняется кодом ближайшей станции.
7. **Запуск поиска** — кнопка «Найти» вызывает `handleRouteSearch`, результаты открываются на маршрутной странице.
**Критерии приёмки:**
+1 -1
View File
@@ -37,7 +37,7 @@
1. **Фокус на поле** — пользователь кликает в поле "Город отправления" (`city-autocomplete`, `data-testid="schedule-departure-city-input"`).
2. **Ввод текста** — пользователь вводит название города или IATA-код (например, "Мос" или "SVO").
3. **Автодополнение** — компонент `city-autocomplete` запрашивает справочник и показывает выпадающий список совпадений.
3. **Автодополнение** — компонент `city-autocomplete` запрашивает справочник и показывает выпадающий список совпадений. В компоненте `city-select`, если у города несколько аэропортов, каждый аэропорт сопровождается `pTooltip` с полным названием (`airport.name`, позиция `top`).
4. **Выбор значения** — пользователь выбирает пункт списка, поле привязывается к `state.departure` через `ngModel`.
5. **Валидация кода** — сеттер `departure` вызывает `updateCalendar()`, который через `validationService.validateCode()` проверяет код станции.
6. **Обновление календаря** — если оба кода валидны, вызывается `apiService.getFlightDaysByRoute()` и формируется массив `disabledDates`.
+2 -2
View File
@@ -12,7 +12,7 @@
4. **Вкладки недель** — под заголовком расположен `week-tabs` (понедельник–воскресенье) с активной текущей неделей.
5. **Список дней**`schedule-days` рендерит аккордеон по дням недели; по умолчанию раскрыт день `state.selectedDate`, иначе сегодня, иначе первый непустой.
6. **Карточки рейсов** — внутри раскрытого дня `list-scheduled-flight` показывает рейсы, первый из которых развёрнут по умолчанию.
7. **Переключение направления** — при наличии обратного рейса пользователь может переключать `OUTBOUND`/`INBOUND` через контрол направления.
7. **Переключение направления** — при наличии обратного рейса пользователь может переключать `OUTBOUND`/`INBOUND` через контрол `schedule-direction-switch`. Кнопки-иконки самолётов сопровождаются `pTooltip`: `SHARED.DIRECT_FLIGHTS` для исходящего направления и `SHARED.RETURN_FLIGHTS` для обратного (позиция `top`, стиль `afl-tooltip`).
**Критерии приёмки:**
@@ -20,7 +20,7 @@
- ✅ Заголовок, week-tabs, schedule-days и карточки рейсов отрисованы
- ✅ Первый день с рейсами раскрыт автоматически
- ✅ При пустом расписании показывается `page-empty-list`
- ✅ Переключение OUTBOUND/INBOUND меняет отображаемый набор рейсов
- ✅ Переключение OUTBOUND/INBOUND меняет отображаемый набор рейсов; кнопки направления имеют `pTooltip` (`SHARED.DIRECT_FLIGHTS` / `SHARED.RETURN_FLIGHTS`)
- ✅ Макет адаптивен под мобильные экраны
---
+15 -10
View File
@@ -55,7 +55,8 @@
3. **Мета-теги и SEO** — рендерится компонент `*-flight-details-meta-tags`, формирующий JsonLD/OpenGraph для страницы.
4. **Заголовок и обёртка**`details-view` (online-board или schedule вариант) принимает проекцию `title` и выводит соответствующий `flight-details-title`.
5. **Секции деталей** — внутри обёртки последовательно отображаются заголовок (header), статус маршрута, полное расписание маршрута (`flight-details-full-route`), расписание (`flight-schedule`), воздушное судно, услуги и другие секции.
6. **Обработка событий** — страница подписана на `open` (навигация по сегментам) и `dateChange` (смена даты в онлайн-табло) через обработчики компонента.
6. **Индикатор смены дня** — в компонентах времени (`time-group`, `time-group-legacy`) при пересечении полуночи рендерится `day-change-square` (или `day-change-square-legacy`) с `pTooltip`, показывающим дату смены через пайп `dayChange` (позиция `top`, стиль `afl-tooltip`).
7. **Обработка событий** — страница подписана на `open` (навигация по сегментам) и `dateChange` (смена даты в онлайн-табло) через обработчики компонента.
**Критерии приёмки:**
@@ -63,6 +64,7 @@
- ✅ Пока данные не загружены, пользователю показан индикатор загрузки.
- ✅ Рендерится компонент мета-тегов для SEO.
- ✅ В макет проецируются заголовок и все секции деталей, предусмотренные Angular-шаблоном.
- ✅ Индикатор смены дня (`day-change-square`) сопровождается `pTooltip` с датой пересечения полуночи.
- ✅ События `open` и `dateChange` корректно пробрасываются в обработчики страницы.
---
@@ -140,7 +142,7 @@
**Путь клиента:**
1. **Отображение логотипа** — компонент `operator-logo` рендерит блок `company-logo` с классами, вычисляемыми из оператора рейса (свойство `classes`).
1. **Отображение логотипа** — компонент `operator-logo` рендерит блок `company-logo` с классами, вычисляемыми из оператора рейса (свойство `classes`). При код-шеринге компонент отображает фактического оператора рейса (`actualOperator`), а не маркетингового перевозчика.
2. **Подпись блока** — при входном флаге `caption` сверху показывается описание `SHARED.AVIACOMPANY`.
3. **Подсказка оператора** — блок логотипа содержит атрибут `pTooltip` с названием `operatingBy`, что при наведении показывает человеко-читаемое название авиакомпании.
4. **Атрибут для тестирования** — у элемента задан `data-testid="flight-company-logo"` для автотестов и селекторов.
@@ -152,6 +154,7 @@
- ✅ Подпись `SHARED.AVIACOMPANY` показывается только при активном флаге `caption`.
- ✅ На наведении отображается тултип с именем оператора.
- ✅ У элемента присутствует `data-testid="flight-company-logo"`.
- ✅ При код-шеринге показывается логотип фактического оператора, а не маркетингового перевозчика.
- ✅ Компонент не создаёт лишнего контента внутри `company-logo`.
---
@@ -164,7 +167,7 @@
1. **Отображение города** — компонент `station` выводит название города через `text` с параметрами выравнивания, размера и жирности (`cityStyles`) и включённым `ellipsis`.
2. **Тултип города** — при наведении на название показывается полный `city` через `tooltip`.
3. **Ссылка на терминал** — под городом выводится `terminal-link`, связанный с `station`, что даёт пользователю информацию о номере терминала.
3. **Ссылка на терминал** — под городом выводится `terminal-link`, связанный с `station`, что даёт пользователю информацию о номере терминала. Элемент имеет `pTooltip` с полным названием аэропорта и терминалом в формате скобок (`station | airportTerminal: 'brackets'`, позиция `top`, стиль `afl-tooltip`).
4. **Отображение старого терминала** — при наличии `oldStation` показывается второй `terminal-link` с флагом `oldValue`, визуально отмечающим предыдущий терминал.
5. **Отображение старого города** — при `shouldShowOldCity` выводится `text` с `oldCity` красного цвета и размера 12, сигнализируя пользователю об изменении станции.
6. **Использование в заголовках и списках** — компонент переиспользуется в бейджах деталей, списках рейсов и секциях маршрута, сохраняя одинаковое поведение.
@@ -172,7 +175,7 @@
**Критерии приёмки:**
- ✅ Название города отображается с усечением и тултипом.
- ✅ Терминал выводится через компонент `terminal-link` для текущей станции.
- ✅ Терминал выводится через компонент `terminal-link` для текущей станции; элемент имеет `pTooltip` с аэропортом и терминалом в формате скобок.
- ✅ При смене станции показывается второй `terminal-link` для старого значения.
- ✅ При смене города показывается старое название красным цветом.
- ✅ Компонент корректно переиспользуется во всех местах, где он встраивается в Angular-приложении.
@@ -208,16 +211,18 @@
**Путь клиента:**
1. **Рендер `flight-actions`** — компонент принимает флаги `print`, `share`, `buy`, `register`, `status`, `details`, `wide` и рейс `flight`, а также `viewType`.
2. **Кнопка печати** — при `print` отображается `print-button` с данными рейса для вывода страницы на печать.
2. **Кнопка печати** — при `print` отображается `print-button` с данными рейса для вывода страницы на печать. Кнопка имеет `pTooltip` с текстом `SHARED.FLIGHT-INFO` + номер рейса (позиция `top`, стиль `afl-tooltip tooltip--one-line`).
3. **Кнопка «Поделиться»** — при `share` отображается `share-button`, открывающий `share-panel` со ссылкой `getDetailsAbsoluteUrl(flight, viewType)` (US-62).
4. **Кнопка покупки** — при `buy && canBuy` отображается `buy-ticket-button`, использующий `flight` как `direct` для передачи параметров.
5. **Кнопка регистрации** — при `register && canRegister` отображается `registration-button` с рейсом, доступный только для поддерживаемых рейсов.
6. **Кнопки статуса и деталей** — при `status && canViewStatus` выводится `flight-status-button`; при `details``flight-details-button`, эмитирующий событие `toDetails` через обработчик `handleToDetailsEvent`.
6. **Кнопки статуса и деталей** — при `status && canViewStatus` выводится `flight-status-button` с `pTooltip` `SHARED.DETAILS-TOOLTIP` (позиция `top`, стиль `afl-tooltip`); при `details``flight-details-button`, эмитирующий событие `toDetails` через обработчик `handleToDetailsEvent`.
**Критерии приёмки:**
- ✅ Кнопки `buy`, `register`, `status` рендерятся при истинности входного флага И соответствующего `can*`-условия (`canBuy`, `canRegister`, `canViewStatus`). Кнопки `print`, `share`, `details` зависят только от входного флага.
-`share-button` открывает панель шаринга с абсолютной ссылкой на детали.
-`print-button` сопровождается `pTooltip` с текстом `SHARED.FLIGHT-INFO` + номер рейса.
-`flight-status-button` сопровождается `pTooltip` `SHARED.DETAILS-TOOLTIP`.
-`buy-ticket-button` получает `flight` как `direct`.
-`flight-details-button` эмитит событие `toDetails`, обработанное страницей.
- ✅ При флаге `wide` добавляется разделитель `k-space` перед блоком покупки.
@@ -231,7 +236,7 @@
**Путь клиента:**
1. **Обёртка секции** — компонент `flight-details-full-route` рендерит элемент `section.frame`, группирующий содержимое блока маршрута.
2. **Таймлайн** — внутри `section.frame` выводится компонент `timeline` с массивом `legs`, показывающий последовательность сегментов рейса.
2. **Таймлайн** — внутри `section.frame` выводится компонент `timeline` с массивом `legs`, показывающий последовательность сегментов рейса. При смене станции в сегменте рендерится `station-change` с `pTooltip`, показывающим название города (`from?.latest.city`, позиция `top`, стиль `afl-tooltip`).
3. **Возможность изменения сегментов** — флаг `canChange` у таймлайна равен `viewType === ViewType.Onlineboard`, включая интерактивность только на онлайн-табло.
4. **Отключённый `flight-brief`** — в шаблоне присутствует закомментированный через `*ngIf="false"` компонент `flight-brief` (отключён по задаче 8072); он не отображается.
5. **Использование на обеих страницах** — компонент применяется и в онлайн-табло, и в расписании с разницей только в `viewType`.
@@ -300,7 +305,7 @@
1. **Инициализация страницы**`flight-details-page` (или `schedule-flight-details-page`) подписывается на `dataSource` и корректно обрабатывает начальное состояние `loading: true`.
2. **Отсутствующие поля** — компоненты `flight-details-airplane`, `operator-logo`, `transfer` проверяют наличие данных (`*ngIf`) перед рендером, что исключает ошибки при отсутствии значений.
3. **Условные ветки статуса**`route-status` рендерится только при наличии `leg` и выбирает ветку (`inFlight`/`finished`/`starting`) без падений.
4. **Действия** — кнопки во `flight-actions` активируются только при соответствующих `can*`-флагах, предотвращая вызов недоступных действий.
4. **Действия** — кнопки `buy`, `register`, `status` во `flight-actions` активируются при соответствующих `can*`-флагах (`canBuy`, `canRegister`, `canViewStatus`), предотвращая вызов недоступных действий.
5. **События страницы** — обработчики `handleOpenEvent`, `handleDateChange`, `handleToDetailsEvent` получают события без выбрасывания ошибок.
6. **Верификация через автотесты** — существующие `*.spec.ts` для `route-status`, `flight-actions`, `share-button`, `print-button`, `operator-logo` подтверждают стабильность компонентов.
@@ -309,7 +314,7 @@
- ✅ При загрузке страницы деталей не возникает ошибок в консоли.
- ✅ Все условные блоки корректно скрываются при отсутствии данных.
- ✅ Обработчики событий страницы отрабатывают без исключений.
- ✅ Действия выполняются только при активных `can*`-флагах.
- ✅ Действия `buy`, `register`, `status` выполняются только при активных `canBuy`, `canRegister`, `canViewStatus`-флагах.
- ✅ Unit-тесты ключевых компонентов проходят.
---
@@ -333,6 +338,6 @@
- ✅ Ни одна секция не подставляет значения «по умолчанию» при отсутствии данных.
- ✅ Секции маршрута, расписания и статуса согласованы по сегментам `legs`.
- ✅ Услуги, места и предыдущий рейс отображаются только при наличии соответствующих полей.
- ✅ Кнопки действий отражают реальные возможности рейса через `can*`-флаги.
- ✅ Кнопки действий `buy`, `register`, `status` отражают реальные возможности рейса через `canBuy`, `canRegister`, `canViewStatus`-флаги.
---
+5 -2
View File
@@ -16,7 +16,7 @@
- ✅ Вкладка "Карта полётов" видна только при включённом feature flag.
- ✅ Клик по вкладке переводит пользователя на страницу карты без перезагрузки приложения.
- ✅ До готовности справочников отображается индикатор загрузки.
- ✅ До готовности справочников и при загрузке маршрутов отображается `loader-sheet` — полупрозрачный оверлей с анимированной иконкой самолёта поверх карты.
- ✅ После инициализации отображаются компоненты `flights-map-filter` и `flights-map-body`.
- ✅ Если feature flag выключен, вкладка не рендерится в навигации.
@@ -64,7 +64,7 @@
**Критерии приёмки:**
- ✅ Поле "Откуда" использует компонент `city-autocomplete` с автодополнением по справочнику городов.
- ✅ Поле "Откуда" использует компонент `city-autocomplete` с автодополнением по справочнику городов; при наличии нескольких аэропортов у города каждый показывает `pTooltip` с полным названием.
- ✅ Выбор города вызывает `filterStateService.setDeparture(code)`.
- ✅ Маркер выбранного города меняет иконку на оранжевую.
- ✅ При выборе только города отправления рисуется "паук" из всех доступных направлений.
@@ -86,6 +86,7 @@
4. **Шаг 4 — подсветка обоих маркеров** — маркеры отправления и прибытия получают иконку `markerOrange` и переносятся в `highlightedLayer`.
5. **Шаг 5 — запрос маршрута** — срабатывает `fetchAndDrawRoute(departure, arrival, dateFrom, dateTo, connections)`; если прямых рейсов нет, автоматически делается повторный запрос с `connections: 1`.
6. **Шаг 6 — отрисовка маршрута** — отображаются все найденные маршруты (прямые сплошной линией, с пересадками — пунктиром) и показываются popup в точках отправления и прибытия.
7. **Шаг 7 — отсутствие направлений** — если ни прямых, ни пересадочных маршрутов не найдено, поверх карты показывается `no-directions-sheet` — полупрозрачный оверлей с карточкой `FLIGHTS-MAP.NO_DIRECTIONS_INFO`. Клик вне карточки закрывает оверлей.
**Критерии приёмки:**
@@ -94,6 +95,7 @@
- ✅ Оба маркера (отправления и прибытия) подсвечиваются оранжевой иконкой.
- ✅ При отсутствии прямых рейсов автоматически делается fallback-запрос с пересадками.
- ✅ Если fallback вернул рейсы, тумблер "Соединительные" автоматически включается (`setConnections(true)`).
- ✅ Если маршрутов нет вообще, показывается оверлей `no-directions-sheet` с информационным сообщением.
**Примечание:** Клик по маркеру при уже выбранном отправлении автоматически устанавливает его как прибытие.
@@ -142,6 +144,7 @@
- ✅ Изменение даты вызывает `filterStateService.setDate(date)`.
- ✅ При наличии маршрутов popup с ссылкой на бронирование обновляется под новую дату.
- ✅ Изменение даты не приводит к повторному запросу списка маршрутов (карта не меняется, меняется только ссылка).
- ✅ При изменении доступных дат (`disabledDates`), если текущая выбранная дата стала недоступной, автоматически выбирается ближайшая доступная дата (date snapping).
**Примечание:** Сам запрос направлений использует окно `[вчера; +6 месяцев]` — выбранная дата влияет только на ссылку "Купить билет".
+98 -2
View File
@@ -11,15 +11,17 @@
3. **Страница ошибки загружается — ErrorPageComponent** — компонент читает `errorCode`, `title`, `description` из `route.snapshot.data`.
4. **Отображение сообщения — код и текст** — на экране виден код «404», заголовок `PAGE404.HEADER` и описание `PAGE404.DESCRIPTION`.
5. **Скрытие баннеров хоста — hideAflComponents()** — страница скрывает `.afl-component--banners` на время показа ошибки.
6. **Возврат к работе — переход на главную** — пользователь нажимает кнопку и возвращается на `/onlineboard` или в поиск.
6. **Поле поиска на странице ошибки** — на странице отображается текстовое поле с кнопкой поиска; ввод запроса и клик по кнопке открывает результаты поиска на aeroflot.ru в новой вкладке.
7. **Возврат к работе — переход на главную** — пользователь нажимает кнопку и возвращается на `/onlineboard` или в поиск.
**Критерии приёмки:**
- ✅ Маршрут `/error/404` отображает страницу с кодом 404.
- ✅ Любой неизвестный URL приводит к редиректу на `/error/404`.
- ✅ Заголовок и описание берутся из переводов (`PAGE404.*`).
- ✅ Поле поиска позволяет выполнить поиск на aeroflot.ru из страницы ошибки (открывается в новой вкладке).
- ✅ Кнопка «На главную» возвращает на рабочий раздел.
- ✅ Баннеры хост-приложения скрыты на время показа ошибки и восстанавливаются при уходе со страницы.
- ✅ Баннеры хост-приложения скрыты на время показа ошибки и восстанавливаются при уходе со страницы (`ngOnDestroy`).
**Примечание:** URL онлайн-табло — `/onlineboard` (без дефиса). Компонент ошибки — общий для 404 и 500, различие задаётся через `route.data`.
@@ -434,3 +436,97 @@
**Примечание:** Источник истины — `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-информацию.
**Примечание:** Предназначено для разработки и тестирования, не для конечных пользователей.
---
+2
View File
@@ -9,6 +9,8 @@ const EnvSchema = z.object({
NODE_ENV: z.enum(["development", "test", "testing", "staging", "production"]).default("development"),
BUILD_TARGET: z.enum(["standalone", "remote"]).default("standalone"),
PROD_ORIGIN: z.string().url().default("http://localhost:8080"),
// In dev, points to Angular's proxy (:4200) which forwards to flights.test.aeroflot.ru.
// In production, this would be the customer's API gateway.
API_BASE_URL: z.string().url().default("http://localhost:4200/api"),
SIGNALR_HUB_URL: z.string().url().default("http://platform.yc.webzavod.ru/tracker/hub"),
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),
@@ -96,7 +96,7 @@ function toCalendarParams(
params: OnlineBoardSearchPageProps["params"],
): CalendarParams {
const base: CalendarParams = {
date: params.date,
date: formatDateForApi(params.date),
searchType: params.type as FlightRequestType,
};
@@ -50,13 +50,15 @@ export function useOnlineBoard(
searchFlights(client, paramsRef.current)
.then((response) => {
if (!cancelled) {
setFlights(response.data.routes);
setFlights(response.data?.routes ?? []);
setLoading(false);
}
})
.catch((err: ApiError) => {
.catch((err: unknown) => {
// eslint-disable-next-line no-console
console.error("[useOnlineBoard] API error:", err);
if (!cancelled) {
setError(err);
setError(err as ApiError);
setLoading(false);
}
});
+1 -1
View File
@@ -35,7 +35,7 @@ export class ApiClient {
this.baseUrl = options.baseUrl.replace(/\/$/, "");
this.locale = options.locale;
this.traceId = options.traceId;
this.fetchFn = options.fetchImpl ?? globalThis.fetch;
this.fetchFn = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
this.timeoutMs = options.defaultTimeoutMs ?? 5000;
this.maxRetries = options.retry?.maxRetries ?? 3;
this.timeoutFactor = options.retry?.timeoutFactor ?? 2;