Pulls in 13 modified + 4 new source files that were uncommitted on main
when this branch forked. Without them, ScheduleStartPage.test.tsx fails
4 tests against the committed main state, which would mask real
regressions during the CI/CD pipeline rollout.
Source files only — no test infra or pipeline code. The user's main
checkout still owns these changes; this commit will dedupe naturally
once the branches reconcile.
On a typical page the console showed 25-30 duplicate 'Failed to load
resource' errors because every consumer hook fired its own copy of
the same network request:
- useDictionaries: once per `useCityName`/`useStationDisplayName`
call (6-10x per render across StationDisplay, PopularRequestItem,
mini-list rows, etc.) — now a module-level WeakMap<ApiClient>
single-flight cache returns the same in-flight Promise.
- usePopularRequests: same pattern across start-page and search-
history dropdowns — cached via the same mechanism.
- useAppSettings: 7+ callers — cached.
Dropped console error count on /ru-ru/ from 29 to 5 (the remaining 5
are WAF 403 infra issues from the dev:full proxy cookie, not code).
Also updates e2e specs:
- schedule-details-mini-list-scoped: asserts the new single-card
rail behaviour (was still checking for the old 3-row flat list).
- smoke /xx/smoke: targets `[data-testid=error-page-404]` instead
of `text=404` — the latter matches both the <title> tag (hidden
by user-agent CSS) and multiple DOM nodes, tripping strict-mode.
The schedule details page now renders Angular's <schedule-details-header>
summary block (badges per flight + share/last-update + full-route
timeline) between the day-tabs strip and the per-leg cards, so a
connecting itinerary like SU 6188 + SU 6341 surfaces both flight
numbers and the combined Moscow→Murmansk timeline up top instead of
jumping straight from the date tabs to the first-leg detail card.
Mini-list duplicate fix: when the sibling search returned 0 matches
the fallback path used to leak the URL-parsed per-leg breakdown into
the rail, producing a first-leg-only row stacked next to the
synthesized combined row. Now the fallback is empty — the mini-list
just shows the (synthesized) current flight on its own.
FullRouteTimeline now uses the API's pre-formatted .localTime instead
of the full ISO .local, so 00:30 / 02:00 shows up instead of
2026-04-26T00:30:00+03:00.
useAppSettings.buyTicketMaxHours: parse <n>d as well as <n>h (Angular
ships 330d for buyPeriod.max). Without this the Buy button hides for
any flight more than ~3 days out.
Plumbed sortMode/onSortChange/hideColumnHeaders through DayGroupedFlightList
so the sticky ScheduleColumnHeaders and the inner list stay in sync
(removes 2 TS errors in ScheduleSearchPage).
Live audit shows Angular DOES add a third crumb on /onlineboard and
/schedule details pages when the user reached them through ?request=:
- onlineboard-flight → 'Рейс: SU 6188' (carrier+number space-separated)
- onlineboard-route → 'Маршрут: Москва - Санкт-Петербург'
- onlineboard-departure → 'Вылет: Шереметьево' (airport name when IATA is airport-only)
- onlineboard-arrival → 'Прилет: Санкт-Петербург' (city name when IATA is also a city)
- schedule-route → 'Москва - Санкт-Петербург' (no 'Маршрут:' prefix)
Restore the leaf-emit logic, fix RU FLIGHT-NUMBER label to 'Рейс:',
add spaces around the dash in ROUTE/SCHEDULE-ROUTE across all 9
locales, and add useStationDisplayName (city dict first, airport
dict fallback — no parent-city escalation, matches Angular's
getCityOrAirport).
ESLint had 30 findings (13 errors, 17 warnings) that had accumulated
across the codebase. Most came out of --fix; the rest needed small
manual cleanups:
- storage.ts: replace import('zod') type annotations with the already-
imported ZodSchema type
- CityPickerPopup.tsx: drop a stale jsx-a11y disable directive for a
rule that isn't in the shared config, and narrow row.city1 so the
explicit non-null assertions are no longer needed
- keyboardLayoutConverter.ts: guard the per-index reads so we can drop
the trailing ! from the string indexing
- TimeGroup.tsx: narrow actual via the hasDelay condition and default
the day-change numbers to 0 instead of asserting non-null
- seo.ts: throw on the unreachable empty-flightIds branch rather than
fabricating a partial SeoHeadProps
- Various test files: remove captured-but-unused onCity/shouldApply
refs and stale makeStation/emptyCity locals that drifted during
earlier refactors
make check now passes typecheck + lint; the one remaining test
failure is the pre-existing OnlineBoardSearchPage timeout test that
flakes under the full suite and passes in isolation.
Angular keeps up to 8 items in each sidebar section (board / schedule
/ flight-number). We were capping the union at 10, which let a burst of
flight-number lookups evict board-route entries. Split the cap by
section so each bucket is independent.
Label also moves from 'Вы искали' → 'Ранее искали' (en: 'Previous
searches') per the redesign copy. Tests cover both the single-section
cap and the independence invariant.
WebSite JSON-LD now emitted on Online Board and Schedule start pages
via buildWebsiteJsonLd in the shared jsonLdBuilders module. Flight Map
already had WebPage JSON-LD; Online Board/Schedule search and details
pages already rendered Flight/ItemList JSON-LD directly in components.
Extracts the 35-carrier logo path table from OperatorLogo into a shared
pure module (src/shared/operatorIcon.ts) so the mapping can be tested and
reused independently. Adds the 7-range SU flight-number fallback that the
TZ requires when OperatingBy is null — SU5000-5399 shows Pobeda (DP),
SU5400-5799 shows Aurora (HZ), SU6000-6999 shows Rossiya (FV), and the
3000-4999 / 5800-5999 bands explicitly render no logo.
63 table-driven tests lock in every range boundary and carrier entry.
FlightCard and ScheduleFlightBody both apply the range resolution before
falling back to the flight's own carrier code.
- AbortController wired through ApiClient → api functions → hooks so a
new search immediately aborts the previous in-flight request (§4.1.12)
- cancel() exposed from useOnlineBoard / useScheduleSearch; Escape key
triggers it while the loader is showing (§4.1.12)
- «Отменить поиск» button rendered during loading; hides when idle (§4.1.12)
- data-searching attribute on search pages disables filter/tabs/breadcrumbs
via pointer-events:none CSS while a search is running (§4.1.10/11)
- Submit buttons disabled for 30 s after each search (hardcoded, per TZ
§4.1.10/11: «не должно выноситься в конфигурационный файл»)
- Per-status error messages: BOARD.ERROR-TIMEOUT / ERROR-4XX / ERROR-5XX
replace the generic LOAD-FAILED-MESSAGE (§4.1.10.1/11.1)
- Error messages added to all 9 locales
- 8 new tests: 3 for AbortController wiring, 5 for error banners + cancel
button visibility
TZ §4.1.9.5 requires session-scoped history ("в рамках одной сессии").
Migrate useSearchHistory from localStorage to sessionStorage so history
clears on tab close / page reload.
Add schema-validated get/set/deleteNs helpers to sessionStore in storage.ts
so the hook stays under no-restricted-globals constraints.
Fix hover style in SearchHistory.scss: TZ specifies голубая подложка with
white text/icon on hover — replace the near-white tint with the full blue.
Add TZ §4.1.9.5 assertion tests: session storage target, dedup + bump-to-top,
most-recent-first ordering, item types, empty initial state.
Schedule:
- ScheduleSearchPage wires handleFlightClick to DayGroupedFlightList so
the "Детали рейса" button in the expanded flight body navigates to
/{lang}/schedule/{carrier}{flightNumber}-{yyyyMMdd} (Angular's
ScheduleNavigationService.toDetailsPage equivalent). Previously the
Details button fired onStatus → no handler → no-op.
Search history:
- useSearchHistory now broadcasts a custom `afl:search-history-changed`
window event on add/clear and listens for it in a useEffect. Fixes
the case where a route-level component (ScheduleSearchPage) adds to
storage while a sibling SearchHistory sidebar had already captured
an empty initial value via useState — the sidebar now re-reads
storage and shows the history without a page reload.
- storage.ts: add sessionStore wrapper (getRaw/setRaw/delete/clear) so
transientPrefill + ScheduleStartPage tests don't trip the
no-restricted-globals rule.
- transientPrefill.ts + ScheduleStartPage.test.tsx: use sessionStore.
- closestFlight.ts: hoist bracket-index key so no newline-before-[ ASI.
- Test files: hoist typeof import(...) into named type alias with
type-only namespace import.
- Drop unused imports: FlightCard (Link, languageToLocale),
OnlineBoardDetailsPage (operatingCarrier),
ScheduleSearchPage (FlightList, inline import() types),
PageLayout (FeedbackButton).
- Drop react-hooks/exhaustive-deps disable comments for a rule not
registered in eslint.config.js.
Each schedule + onlineboard search now records itself into the
existing useSearchHistory localStorage hook, with a structured
params payload (departure/arrival/dates/flightNumber). The
SearchHistory sidebar renders the rich Angular layout: clock or
plane icon, optional sub-title (e.g. "Расписание рейсов, в одну
сторону"), city pair, and date range, with inbound dates appended
for round-trip searches.
Angular's getCityOrAirport walks airport→city when the input code is
an airport (SVO → Москва), only falling back to the airport name when
no parent city is dictionarised. React was returning the raw airport
name (Шереметьево) on the popular requests panel.
Angular's LocalizationService reads `Country = baseHref[1..3]` and
`Language = baseHref[4..6]` — both halves are the same 2-letter
language code (`/ru-ru/`, `/en-en/`, `/zh-zh/`, …), confirmed by
the spec fixtures using `/en-en/onlineboard/...`. The previous
shipping codes mixed in IETF region codes (`en-us`, `ja-jp`, `ko-kr`,
`zh-cn`) which do not match the customer's URL surface.
Renamed:
en-us → en-en
ja-jp → ja-ja
ko-kr → ko-ko
zh-cn → zh-zh
The `LANGUAGE_TO_LOCALE_CODE` table now mirrors Angular exactly.
Resolver/hreflang tests + layout 404 message updated.
- URL surface now matches Angular: `/ru-ru/`, `/en-us/`, `/zh-cn/`, …
(BCP-47). Bare short codes still work — the [lang]/layout auto-
promotes them with a replace navigation. Internally everything that
needs the short language (i18n file lookup, API path segment,
Accept-Language header, dictionary `title[lang]` key, Intl
formatters) reads it through the new `useLocale()` hook, which
returns both `locale` (BCP-47) and `language` (short).
- ApiClient.locale is now mutable and is updated from the [lang]
layout whenever the URL locale changes — was hard-coded to "ru" in
the root layout before, so backend responses for /en/... still came
back in Russian. Cities / airports / flight statuses now arrive in
the active language.
- All 21 empty EN translation keys filled in (AIRPLANE.*, BOARD.
PREVIOUS-FLIGHT, SCHEDULE.FILE-NAME, SEO.SCHEDULE.*, SEO.FLIGHTS-
MAP.*, SHARED.FLIGHT-TRANSFER-PLURAL-*, SHARED.WEEK_FORMAT-WRONG)
so /en-us renders without falling back to raw keys.
- Added BOARD.LOAD-FAILED-TITLE / -MESSAGE keys (RU + EN) and removed
the three hardcoded Russian error strings from the search-page
error card.
- FlightStatus now reads `FLIGHT-STATUSES.{Status}` from i18n instead
of hardcoding the Russian labels.
- FlightCard's OperatorLogo now picks the en/ru carrier-logo variant
from `useLocale().language` instead of always passing "ru" — the
Aeroflot/Rossiya logos display in the active language where
variants exist.
- registerPrimeLocales(): all 9 supported languages get a PrimeReact
`addLocale` entry at module load (RU + EN hand-curated, others built
from Intl). Calendar/AutoComplete widgets switch with the URL.
- ErrorBoundary catches outside the i18n provider, so it now ships
its own minimal localised string table keyed off the URL locale —
no more "Something went wrong" leaking on the Russian site.
- Hreflang URLs now emit BCP-47 (`/en-us/...`) while `hreflang="en"`
stays the short Google-friendly form.
- Datetime helpers accept either short or BCP-47 locale (`isRussianLocale`)
so callers can pass through whatever the route hands them.
- Drop the React-only standalone /popular route (and its e2e
smoketest). Angular returns 404 for /ru-ru/popular; popular
requests are surfaced inline on onlineboard/schedule start pages
via PopularRequestsPanel (which stays). Matching the URL surface
is a contractual requirement for the MF remote.
- Replace ?tab/?departure/?arrival/?return query-string prefill on
the onlineboard and schedule start pages with a sessionStorage
transient slot. Mirrors Angular's OnlineBoardFiltersStateService /
ScheduleFiltersStateService cross-page singletons: URLs stay
clean of query strings, the start-page form still seeds itself
from a popular-request click, and a fresh page reload (which
bypasses the in-memory state in Angular) lands on a pristine form.
Same-page popular clicks remount the filter via key bump so the
useState initializers pick up the new prefill.
Angular's captioned-time-group renders 'UTC {{ utc }}', producing
'UTC +03:00' on screen. React was emitting 'UTC+03:00' without the
separator, making the time details read slightly differently. Insert a
U+00A0 non-breaking space between 'UTC' and the signed offset so the
time-table values ('15:30 UTC +03:00 18.04.2026') line up with Angular.
- formatDuration(locale='ru') now emits 'Xч. Xмин.' (and 'Xд. Xч. Xмин.')
with trailing dots, matching Angular's DurationPipe + SHARED.SHORT-HOUR
translations. Every 'В пути', 'До прилета', and 'Время в пути' label on
the details page now reads identically to Angular.
- FlightSchedule shows the SCHEDULED duration (dep→arr from the timestamps)
instead of the actual flyingTime the API reports, so the Расписание рейса
row reads '1ч. 30мин.' for a 15:30→17:00 schedule even after an early
landing. The Вылет / Прилет columns also surface the 'UTC+HH:MM' offset
below each time, matching the Angular layout.
The upstream /schedule endpoint regularly returns 7MB+ payloads and
takes 6-10s to complete. The 5s default was aborting those fetches
mid-body, cascading into a retry loop that showed ERR_ABORTED in
DevTools and 'Failed to load data' on the UI — even though the backend
eventually answered with HTTP 200. Matches Angular's default.
useCityName was a 'phase 2 stub' that returned the IATA code
unchanged, so the popular-requests panel read 'Маршрут: SVO - LED'
instead of 'Маршрут: Шереметьево - Санкт-Петербург'. Rewire the hook
to read the current locale and look up cityByCode / airportByCode
from the loaded dictionaries, falling back to the code only while
dictionaries are loading or for unknown codes (matches Angular's
DictionariesService.getCityOrAirport).
Tests expanded to cover the city/airport resolutions and the
loading / unknown fallback paths (5 total, 1258 suite green).
Render ISO timestamps as "23:30 UTC+03:00 17.04.2026" instead of the
raw "2026-04-17T23:30:00+03:00" that the API returns. Applies to the
per-leg station blocks (new LegStation helper) plus the
Registration / Boarding / Deboarding panels.
formatLocalTime now preserves the wall-clock in the source string
(previously `new Date(...)` would shift it to the runtime's locale).
Added formatUtcOffset (UTC±HH:mm) and formatDayMonthYear (DD.MM.YYYY)
helpers next to it.
Also ship the Angular footer notes on the details page (estimated
times / local-time disclaimer) and added BOARD.LOCAL-TIME-NOTE,
BOARD.ESTIMATED-TIME-NOTE keys to all nine locales.
1. FlightsMap tiles didn't render: MapCanvas inline height:100% resolved
to 0 against min-height parents. Hand sizing to consumer CSS so
.flights-map-start__map height:500px wins.
2. FlightsMap /map/api/tile/{z}/{x}/{y}.jpeg requests fell through to
Modern.js SSR (HTML body). Dev proxy now forwards /map/* to the
test env via curl with image headers and binary-safe piping.
3. PopularRequestsPanel duplicate React key (Route-SVO-LED appears
twice in upstream). Suffix the key with the visible index.
4. OnlineBoardDetailsPage /onlineboard/details 400. Upstream expects
an ISO datetime (yyyy-MM-DDTHH:mm:ss), matching Angular's
ApiFormatterService.formatDate. Append T00:00:00.
5. Browser-level SignalR CORS errors on every details page: the
default SIGNALR_HUB_URL pointed at an unreachable placeholder.
Default to empty + skip the connection in useLiveFlights when
blank. Also configureLogging(LogLevel.None) so SignalR stops
writing its own negotiation failures to console. Live updates
re-enable by setting SIGNALR_HUB_URL on a deployment.
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.
useCitySearch hook loads cities from /api/dictionary/1/cities on first
use, then searches in-memory by name prefix and code -- matching the
Angular CitiesSearchService behavior. Wired into OnlineBoardFilter,
ScheduleStartPage, and FlightsMapFilter AutoComplete components.
API functions now build the full localized path matching the Angular
EndpointService pattern (/api/flights/{version}/{locale}/{endpoint}).
The dev proxy forwards /api and /flights to the test backend.
Wrap connection.start() in try/catch so that when the SignalR hub is
unreachable the status transitions to "offline" silently instead of
throwing unhandled errors that flood the browser console.
Persists search history to localStorage via @/shared/storage with
language-scoped keys (afl_history_{lang}). Supports dedup by URL,
max 10 items, and clear functionality.