OnlineBoard, Schedule and FlightsMap each inlined their own swap-cities
wrapper — three different class names and, in FlightsMap's case, a different
inline SVG. Angular keeps logic separate per filter (Schedule/FlightsMap
clear validation on swap, OnlineBoard doesn't) but its DOM is identical
across the three. Mirror that: ship a shared <SwapCityButton> that owns
the `.change-container > .button-change > .svg--change-city` markup and
CSS, keep each caller's onClick local.
Also align filter visuals: FlightsMapFilter row gap $space-m → $space-l to
match OnlineBoard/Schedule, and CityAutocomplete label gutter $space-s2 →
$space-m to match Angular's city-autocomplete.component.scss.
Live-report issues (user-driven smoke test):
1. Schedule city input shown with a thick default PrimeReact border
(no such border on Board). Root cause: CityAutocomplete's outer
wrapper carries the border via box-shadow, but the inner .p-inputtext
still had PrimeReact's 'border: 1px solid #a6a6a6' from the shared
prime-styles.scss. Angular silences it with a global 'city-autocomplete
input.p-inputtext { border: none; box-shadow: none }' rule. Added
the same reset to our shared CityAutocomplete.scss + killed the
PrimeReact focus shadow so only one border remains.
2. Clear-X button hidden on Board + Map (visible only on Schedule).
Root cause: a legacy Angular-port rule in _layout.scss
'.p-accordion .p-accordion-content button.button-clear { display: none }'
beat our '.city-autocomplete__input--has-value .button-clear { display: block }'
on specificity — Board's CityAutocomplete sits inside the accordion
filter. Removed the legacy rule (it targeted an Angular-only close
affordance that doesn't exist in the React app); if we re-add such
an element later it'll need a distinct class.
3. Date-picker placeholder 'ДД.ММ.ГГГГ - ДД.ММ.ГГГГ' truncated because
the ScheduleStartPage inherits 16px font. Stepped calendar font down
to 14px (matches Angular's base body .p-inputtext) + added right
padding so the trigger icon doesn't sit on top of the placeholder.
4. Schedule start page showed a 'Возможности расписания' info section
(TZ Table 9 Title5+Title6) that the Angular reference
(ClientApp/.../schedule-start-page.component.html) has commented out.
Followed Angular — removed the block. Kept i18n keys for future work.
Updated the two corresponding assertion tests to check the block is
NOT rendered (parity with Angular).
Also during the same session, there was a sub-bug introduced in the
first SCSS edit (I accidentally nested .button-clear inside
:focus-within, inverting display state). Fixed by moving the rule back
under __input directly.
2044 unit tests pass, typecheck clean. Live retest across all three
sections (Board / Schedule / Map): X appears only when city is filled,
inner input shows single clean border, Schedule calendar placeholder
fits, 'Возможности расписания' no longer renders.
User report: clicking a 'Расписание туда' popular tile on /onlineboard
filled the Schedule form but clicking Search did nothing.
Two bugs:
1. OnlineBoardStartPage's Schedule-bound popular-click handler wrote
only { departure, arrival, withReturn } to the transient prefill —
it skipped dateFrom/dateTo entirely. The Schedule calendar rendered
empty, and on submit the form defaulted to today..today+7 (acceptable
but TZ §4.1.5 mandates current-week prefill).
2. currentWeekBounds() returned the raw Monday of the ISO week. When
today is mid-week (Tue-Sun), that Monday is N days in the past, so
the Schedule route guard (§4.1.2, -1/+330 window) rejected the URL
and silently redirected back to /schedule — user saw 'Search does
nothing'.
Fix: populate dateFrom/dateTo (and returnDateFrom/returnDateTo for
RouteWithBack) in the Schedule prefill from both handlers, and clamp
the `from` end of currentWeekBounds() to max(Mon, today-1) so the
prefilled range is always inside the window. nextWeekBounds now derives
from the raw Monday (not the clamped `from`) so next-week is always
the true next ISO week.
Live retest: popular 'Москва — Мурманск' → Schedule prefilled with
cities + '21.04.2026 - 26.04.2026' → Search navigates to
/schedule/route/MOW-MMK-20260421-20260426. 0 console errors.
2044 tests pass, typecheck clean.
Earlier §4.1.24.3 R24 fix (commit 0bb6bf2) set the permanent on-map
label to the IATA city code. That mis-read the TZ: §4.1.24.3 describes
the hover tooltip (всплывающая подсказка) as showing the airport
code, not the always-visible marker label. Angular reference + the
user-facing baseline render the permanent label as the localized
city name.
- FlightsMapStartPage: label = city.name (localized via dictionary).
- Updated two test assertions that had codified the previous IATA-
based form.
Live retest: map now shows "Москва", "Санкт-Петербург", "Сочи", etc.
on its markers. 2044 tests pass, typecheck clean.
Three gaps found by navigating the running app with Playwright:
1. parseFlightUrlParams did not zero-pad flight numbers. Deep-link URLs
like /onlineboard/flight/SU100-20260422 produced API calls with
flightNumber=SU100 (3 digits) and backend replied 400. Per TZ
4.1.2-R4, the canonical form is 4-digit zero-padded. Padding moved
into the parser so every downstream consumer sees SU0100.
2. SEO.SCHEDULE.MAIN.TITLE was the long SEO variant
("Расписание прямых и стыковочных рейсов авиакомпании Аэрофлот").
Per TZ Table 6 row 9 the target is the short form "Расписание";
fixed ru + en locales.
3. SEO.SCHEDULE.SEARCH.TITLE was "Расписание рейсов {dep} - {arr}
| Аэрофлот"; per TZ Table 6 row 10 the target is "Расписание по
маршруту: {dep}-{arr}"; fixed ru + en locales.
Three existing url.test.ts cases asserted the unpadded form; updated
them to match TZ and annotated with rule ID. Full suite 2044 pass,
typecheck clean. Live retest confirms 0 console errors / 0 warnings
on start pages, results pages, details pages.
TZ §4.1.24.6: "Если Дата рейса не известна, то переход в SB должен
выполнятся без даты." buildBuyTicketUrl now accepts date as
string | undefined; when undefined the route triple is {dep}.{arr}
instead of {dep}.{YYYYMMDD}.{arr}. FlightsMapStartPage passes
filterState.date directly (possibly undefined) instead of defaulting
to today.
TZ §4.1.24.3 line 3098: "всплывающая подсказка с кодом аэропорта".
The marker `label` now uses `city.code` (IATA city code) instead of the
human-readable city name. On hover, Leaflet's tooltip shows the code.
- Hide `.flights-map-filter-header` on mobile via `@include screen.mobile`
so the "Найдите свой маршрут" label is absent on phone (R7).
- Disable the PrimeReact Calendar and DayQuickPick when `Город вылета` is
empty; date picker must not be selectable without a departure city (R16).
- Add `disabled?` prop to DayQuickPick so callers can block the quick-day
buttons on mobile (R16 mobile quick-day parity).
All three feature seo builders already emitted the full OG set (title,
description, url, image, type, locale, site_name), canonical with no
query params, and hreflang for all 9 locales + x-default. No builder
gaps found. Added explicit §4.1.19/20 requirement-ID test cases to each
seo.test.ts so the contractual coverage is machine-verifiable.
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.
- FlightsMiniListItem: SVO/VKO airport names rendered as role=link spans that
open external sites in a new tab (R19, R20) — avoids <a> nesting inside Link
- ScheduleDetailsPage: wire FlightsMiniList into contentLeft and DayTabs into
stickyContent (schedule window [-1, +330]) per §4.1.16.1 R2/R3 and §4.1.16.3 R22
- Add navigation handler for schedule day-tab clicks (simple date URL swap;
full §4.1.16.3.1 re-search algorithm is deferred)
- Tests: 72 tests across four files covering R12/R13/R16/R17/R22 (mini-list),
R23/R27/R28 (day-tabs), R3/R5/R6/R7 (page structure), R2/R4/R22 (schedule)
Add targeted assertions covering every rendering rule in §4.1.16.8:
- DaysOfWeekStrip: Mon/Wed/Fri, weekend-only, single-day patterns;
explicit bit-index contract (0=Mon … 6=Sun)
- weekDateRange: Mon–Sun ISO week, 6-day span invariant, leading-zero
dd.MM.yyyy format, Sat input resolves to same week as Mon
- FlightSchedule: daysOfWeek.flight (not .current) drives active days;
accordion collapses on click; week note anchored to dep-local date
Three concrete gaps fixed:
1. TransferBar (Online-Board §4.1.15.6): duration now uses actual/estimated
UTC times when viewType=Onlineboard instead of always scheduled UTC.
Adds isIntermediateLanding prop (default true) so the label can switch
between "Промежуточная посадка" and "Пересадка" based on flight-number
identity rather than being hardcoded. StationChange now always rendered
(not only when separated) so city/airport/terminal are always shown.
2. ScheduleFlightBody (§4.1.16.7): transferDuration previously computed
ground time from .local strings ("HH:MM:SS" without timezone), making
new Date() result timezone-dependent and often NaN. Switched to .utc
(ISO 8601 with Z suffix) for a correct, deterministic diff.
Tests: 53 pass (8 TransferBar + 32+3 ScheduleFlightBody + 10 computeTransferTime).
New test cases: isIntermediateLanding=false label, StationChange always-on,
--separated class, UTC-based 90-min duration, label distinction per TZ.
Three fixes:
- Transfer box: use IATA cityCode (not display text) for city-level
station change detection (TZ §4.1.16.6 rule 12), catching cases where
city codes differ even if airport codes are the same.
- Transfer box: add terminal-change case — same airport but different
arrival/departure terminals now renders both codes separated by →
(TZ §4.1.16.6 rule 14).
- ScheduleDetailsPage title: show all connecting flight numbers in the
page <h1> and title string (TZ §4.1.16.6 Table 60 header rule 1+5).
Also fixes a pre-existing flaky test in ScheduleFlightBody: todayUtc()
now always returns UTC noon of today to avoid day-boundary races.
Gap found and fixed: Timeline route bar (Маршрут section) was rendering
departure/arrival times without day-change badges. TZ §4.1.15.5 rows 3
and 9 require +X/-X indicators whenever a leg crosses midnight.
Added TimeCell component to Timeline that emits the badge when
dayChange != 0, with priority to actual times when canChange=true
(Online Board) and fallback to scheduled (Schedule). Added 9 new
assertion tests covering: no badge when 0, +1/+2/-1 on arrival, badge
on departure, actual-takes-priority, and multi-badge (3 badges when 3
of 4 time cells carry non-zero day offsets).
All other multi-segment rules (routeChanged/returnToAirport from any
leg, codesharing in header, StationChange detection, TransferBar,
per-leg LegRoute with its own arrival day-change badge, ScheduleFlightBody
per-leg TimeGroup) were verified as already implemented. Per-segment
collapse/expand accordion (rows 7 of §4.1.15.5) deferred to Task 13.
Angular rule: show the previous-flight identifier as a clickable link
opening the prior flight's details in a new browser tab, gated on the
flight's scheduled departure being > today − 2 days old. Outside that
window it falls back to plain text to avoid stale cross-links.
Threads locale + departureDateLocal from OnlineBoardDetailsPage through
FlightLegs → FlightDetailsAccordion → AircraftPanel. URL is built with
the existing buildFlightUrlParams helper using previousFlight.localDate,
matching Angular's dateToSearchBy = new Date(prevFlight.localDate).
Two gaps: Delayed fell into center--progress (blue) instead of
orange; Sent was excluded from the isInFlight branch despite the
Angular FlightStatusLegacy.inFlight contract. Fixed both and added
8 per-status assertions covering all 8 FlightStatus enum values.
Creates timelineTime.ts with computeTimelineCalc (R94–R97: total/elapsed/
remaining minutes + aircraft position %) and formatTimelineDuration (R98:
omit zero leading units — «45мин.» not «0ч. 45мин.»).
Wires into OnlineBoardDetailsPage: arrival time now uses actual > estimated
> scheduled priority (R94), and В пути / До прилета labels use the new
formatter. 24 unit tests cover all branches.
R4 gap fixed: TimeGroup now accepts scheduledDayChange + actualDayChange props
separately so each time type renders its own independent badge. FlightCard
updated to pass them independently (scheduled vs actual/estimated); expanded
row time block also now shows per-type badges.
R5 tooltip fixed: dayChangeBadgeTooltip() uses string-based date extraction
(no TZ reprojection via new Date()) — avoids viewer-TZ shift for SSR and
cross-TZ correctness. Returns "День" for ±1, DD.MM.YYYY for ±2+.
New shared helper dayChange.ts exports computeDayChange(), dayChangeBadgeTooltip(),
formatDayChangeBadge(). 27 unit tests cover +0/+1/+2/-1/-2, null, malformed
input, month/year boundaries, and per-time-type independence (R4).
R1–R3, R6 confirmed correct (API supplies dayChange per ITimesSet; badge
adjacent to time; hidden when 0). R8 (mobile tooltip suppression) deferred.
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.
Gate Buy button to the TZ §4.1.14.4.4 window: visible only when departure
UTC is > 2 hours ahead AND < 330 days ahead; first leg governs for multi/
connecting. Gate Status button (§4.1.14.4.5) to same-day departure only,
based on UTC calendar date. Add separate Details button (§4.1.14.4.6) that
is always visible when an onStatus handler is provided. Add SCSS for the
new details-btn outline style. Add 25-test ScheduleFlightBody.test.tsx
covering structure, transfer-box labels, buy gate, and status gate.
WeekTabs (§4.1.14.1):
- Fix active range: derive weeks from scheduleWindowBounds() [-1,+330 days]
instead of hardcoded WEEKS_AFTER=30 (≈210 days, less than required 330).
- Fix auto-scroll: sync page via useEffect when selectedMonday prop changes
so navigating to a different week always reveals its tab.
- Add fill-to-7: pad last page with disabled placeholder tabs when the
final active week does not end a complete group of 7; disable next arrow.
Collapsed row (§4.1.14.3): already implemented — add lock-in tests for
Tables 36–40 (direct / multi-leg / connecting) covering flight number,
operator logos (round for multi-leg per commit 3ae59da), dep/arr times,
day-change chips, duration column, expand chevron, and DayGroupedFlightList
day-grouping + column headers.
Departure/route/flight-number modes sort by scheduled departure time;
arrival mode sorts by scheduled arrival time (last leg for MultiLeg).
Day ordering (yesterday < today < tomorrow) emerges from absolute ISO
timestamps — no bespoke bucketing needed. Flights missing a timestamp
are pushed to the end. 18 unit tests lock the contract in.
- Fix daysAfter: 7→14 in OnlineBoardSearchPage (TZ active range is today-1 to today+14)
- Add inactive padding tabs on the last page when it has fewer than 7 slots; right-arrow stays disabled on last page regardless (TZ §4.1.13.1)
- Add aria-current="date" to active DayTabButton for accessible highlight (TZ requires visual highlight + screen-reader signal)
- Add auto-scroll via scrollIntoView when selectedDate changes externally (URL-driven day navigation)
- Convert DayTabButton to forwardRef to support the activeBtnRef scroll anchor
- 9 new TZ-labelled tests locking in all the above behaviors
- New findNearestFlightIndex helper (scrollToCurrentTime.ts) with 5 unit tests
- FlightList: lock scroll-to-nearest behind a ref so live SignalR updates
don't yank the viewport back to the auto-selected flight after the user
has manually scrolled elsewhere
- OnlineBoardSearchPage integration tests: verify today/future/past tab
selection logic via findClosestFlightId (the id-based variant already
wired to FlightList.initialCurrentFlightId)
- 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
- OB flight-number: X was always visible; now conditionally rendered only
when the field has a value (hides when empty)
- OB flight-date and route-date: add X button next to calendar icon,
clears date state and hides itself when empty
- Schedule outbound and return date-range calendars: same inline X pattern
- CSS: .calendar-input-wrapper + .calendar-clear-btn added to both SCSS
files (absolute-positioned left of the calendar icon)
- CityAutocomplete: already correct (CSS show/hide via has-value class)
- 21 new tests across OnlineBoardFilter, ScheduleFilter, CityAutocomplete
(aria-label, visibility toggling, click-to-clear); all 640 pass
Add assertion tests confirming both OnlineBoardStartPage and ScheduleStartPage
render the required info-section headings and content blocks per TZ specifications.
OnlineBoardStartPage (TZ Table 8):
- Heading: 'Что такое Онлайн-табло и что я могу в нем увидеть?'
- 4 info blocks: Актуальная информация, Информация об услугах, Купить билет, Расписание
ScheduleStartPage (TZ Table 9):
- First heading: 'Как пользоваться расписанием?' + 4 info blocks
- Second heading: 'Возможности расписания' + 2 capabilities blocks
- Blocks 5-6 (Купить билет, Расписание) now rendered under capabilities heading
Tests added:
- OnlineBoardStartPage: 6 new assertions (4.1.6-R-info-heading through 4.1.6-R-popular)
- ScheduleStartPage: 8 new assertions (4.1.7-R-info-heading-1 through 4.1.7-R-popular)
Board kinds (Arrival, Departure, Route, FlightNumber): buildOnlineBoardPrefillState
now emits date=today in every case; OnlineBoardStartPage wires it through to
OnlineBoardFilter via initialDate.
Schedule one-way (Route/Schedule): click handler now includes dateFrom/dateTo
= current ISO week (Mon-Sun) in the transient prefill written to sessionStorage.
Schedule round-trip (RouteWithBack/Schedule): additionally includes
returnDateFrom/returnDateTo = next ISO week.
SchedulePrefillState extended with the four new optional date fields;
yyyymmddToDate helper added to ScheduleStartPage; currentWeekBounds /
nextWeekBounds helpers implement the TZ week-boundary logic.
Nine new §4.1.5-labeled tests (4 unit + 5 integration) added; existing
prefill-state tests updated to expect the new date fields. All 55 tests pass.
The final (success) return branch was passing a hardcoded 1-item breadcrumb
array instead of the computed `breadcrumbs` variable, so the leaf crumb built
from ?request= was silently dropped for loaded flights. Loading/error/empty
branches were already correct. Adds 3 unit tests to lock the wiring.
Per TZ §4.1.4 Table 7 rows 6–8, the details page now builds a 3-item
breadcrumb trail [Home, Онлайн-Табло, <leaf>] where <leaf> is mode-aware:
- flight: "Номер рейса: SU-1234" (hyphen per TZ)
- departure: "Вылет: {station}"
- arrival: "Прилет: {station}"
- route: "Маршрут: {dep}-{arr}"
The leaf crumb is a clickable link back to the source search page with
time-range preserved in the URL. Share-link entries (no ?request=) get
only [Home, Онлайн-Табло] with no leaf.
Breadcrumbs component updated to allow last-item links (was suppressed),
since TZ explicitly requires the leaf to be navigatable.
CONFLICT (deferred to Task 16): TZ row 6 says departure/arrival leaf
should show both dep+arr cities; parentRequest only carries one station,
so only that station is shown. Departure/arrival search returns flights
to many arrival cities — "both cities" makes no sense at search-mode level.
- Add formatDateForTitle helper: returns today/tomorrow labels or dd.MM.yyyy
- Switch all search page title builders to use formatDateForTitle; descriptions keep dd.MM.yyyy
- FLIGHT-DETAILS title now uses routeCities (no date) per TZ rows 6-8; adds TITLE-NO-ROUTE fallback for SSR when cities not yet loaded
- buildFlightDetailsSeoFromId accepts optional cityNames param
- Update ru/en i18n TITLE strings to TZ Table 6 format; add TITLE-NO-ROUTE to all 9 locales
- Tests: 32 cases covering today/tomorrow/arbitrary-date branches and routeCities logic