The dictionary API returns regions in arbitrary order. CityPickerPopup
now sorts them alphabetically by localized name and pulls the
Russia-and-CIS direction to the front — matching the TZ Table 14
listing order. Detection is loose (substring match on 'Россия' /
'Russia' / 'СНГ' / 'CIS') so minor backend renames still pin
correctly.
- Duration now sums segments + transfers (last arrival − first departure)
for multi-leg/connecting in Schedule, matching TZ §4.1.14.3 and Angular.
- Default day auto-expands per TZ §4.1.14: current-week today, future-week
first valid day, last-valid-day fallback when earlier days are out of
window.
- Aircraft model no longer leaks into collapsed rows; shown only when
expanded AND direct, mirroring Angular's
operator-logo-and-model [showModel]="expanded && direct".
- Week tabs use MONTH-SHORT.* translation table so Russian renders
"27 апр. - 3 май." instead of genitive "мая" from Intl.
- "Ранее искали" → "Вы искали" across all 9 locales (TZ §4.1.9.5).
- Sort-arrow headers compacted (inline-flex nowrap, zero gap) so labels
stay on one line next to the chevrons.
- robots.txt allows Yandex/Googlebot/* with no Disallow (TZ §4.1.20).
- 6 non-RU/EN locales (de/es/fr/it/ja/ko) + zh were missing ~45 strings
each; translated from Angular where present, hand-translated otherwise
so every locale is down to the two intentional `.undefined` stubs.
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.
- OperatorLogo: moved &--round after per-carrier width rules so the
round variant wins the cascade. Previously .operator-logo--FV
(90×15) outranked --round (36×36) for FV flights and the second
logo on a multi-leg schedule row spilled across the time column.
Also added a tablet-viewport shrink for --round to 30×30.
- FlightCard now emits the flight-card--schedule modifier when
direction='schedule' so the 80px/120px/100px/... grid actually
applies. The default board grid was active on schedule rows, giving
a too-narrow flight-number column and misaligned logos.
- i18n: replaced single-quoted HTML attributes with double quotes in
every common.json. i18next-icu parses single quotes as ICU literal
delimiters and silently drops the closing apostrophe in
href='…booking'>…, truncating everything after <a ...> inside the
rendered innerHTML. The schedule start-page bottom-description lost
its 'онлайн-сервисом' link paragraph as a result.
The inline expanded flight card used to pick one of boarding /
deboarding based on the search direction and show just that block.
Angular's board-flight-body renders registration, boarding and
deboarding side-by-side, each gated on the API payload's isActual flag
— TIRREDESIGN-7 expects the same so a flight mid-boarding can still
show that its registration already finished.
- FlightCard now iterates registration/boarding/deboarding and renders
each row when its isActual flag is set. Gate + dispatch still come
from depStation on boarding, gate + bag-belt from arrStation on
deboarding.
- shared.shouldShowTransition swaps 'status != Scheduled' for the
isActual flag to match Angular's same-payload semantics on the
details accordion. The Schedule/Cancelled short-circuits stand.
- Test fixture makeFlightWithBoarding scopes its transition to the
direction under test so the two blocks don't collide on testids.
The Buy action is now an <a href> instead of a <button> that opens a
window, so users can inspect / middle-click / right-click it like any
normal link. The inline per-row link on the schedule results list only
appears on hover (desktop) — touch devices still navigate via the
details card's Buy button. Copy updated to 'Купить билет' / 'Buy a
ticket' per §4.1.14.4.4.
ScheduleFlightBody, DayGroupedFlightList, ScheduleSearchPage and
ScheduleDetailsPage thread a buyUrlFor → buyUrl URL instead of an
onBuy callback. FlightList/FlightCard gain an inlineBuyUrl prop plus
overlay CSS so the 8-column grid stays intact.
FlightList on direction=schedule now wires a row-level onClick so the
entire row navigates to the details page instead of expanding inline.
Matches Angular's schedule-search-result behaviour where each flight
row is a link to the details card.
Angular's page-layout template renders the breadcrumb trail and the
page title as separate rows (page-layout.component.html:7-8). React
wrapped both in a single block div, so the inline-flex breadcrumb pill
sat next to the <h1> instead of above it. Flip the wrapper to
`display: flex; flex-direction: column; align-items: flex-start` so the
pill sits on its own row above the heading, keeping its content-sized
width.
OnlineBoard, Schedule and FlightsMap filter sidebars drifted visually:
ScheduleFilter used $light-gray + 4px gutter for .label--filter while the
other two used $gray + $label-margin-bottom (10px). CityAutocomplete's own
__label also defaulted to $light-gray, making city labels a different tone
from the date/time labels alongside them.
Angular's canonical is $gray + $label-margin-bottom everywhere on the
filter sidebar — align both rules to that.
Also fix the datepicker's internal seam: PrimeReact's p-calendar-w-btn
put the 1px border on the input, leaving the trigger icon visibly outside
the bordered area. Angular renders the whole control as a single pill.
Promote the border to the outer .p-calendar wrapper and strip the inner
input's border + shadow, scoped to the three filter-panel roots.
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.
Previously the 404/500 React ErrorPage set the page content but not
document.title, so the browser tab showed the URL path instead of
a localized title. Added <title> element + imperative document.title
assignment (pattern from SeoHead.tsx) so both SSR and client set
the tab title to "<code> — <localized-title>", e.g. "404 — Страница
не найдена".
- 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).
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.
Gap audit against §4.1.13.4.3 (Tables 29/30) found that the inline
boarding/deboarding row in FlightCard's default expanded body was
missing three attributes:
- departure.gate / arrival.gate (boarding gate number)
- departure.dispatch (трап/автобус transfer type)
- arrival.bagBelt (baggage belt, deboarding only)
Add all three as conditional fields in the transition block, guarded
by the existing isArrival flag so departure shows gate+dispatch and
arrival shows gate+bagBelt. Add DETAILS.DISPATCH i18n label (ru + en).
Add 16 assertion tests covering time rows, transition status/times,
gate, dispatch, bagBelt, and the share/details buttons.
Deferred (DONE_WITH_CONCERNS):
- Check-in counter number: API type has checkingStatus string but no
counter number field; requires backend extension.
- Aircraft tail number: field (aircraft.registration) exists in types
but is only shown in the details-page AircraftPanel, not in the
FlightCard expanded body; deferred to details-page parity task.
- Code-share chips in expanded segment body: currently merged into the
collapsed header number column via _childFlightIds; per-segment
expanded display deferred to multi-leg task.
- 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)
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.
Add keyboard navigation (ArrowDown/Up + Enter to commit highlighted item),
Escape closes the popup without committing, role=dialog + aria-activedescendant
for a11y, and city-highlighted visual feedback. All §4.1.9.2 structural rules
(grouping, RU/CIS-first, MOW-first, alpha ordering, scrollable panel, selected
highlight) confirmed by assertion tests. 14 new assertion tests added across
CityPickerPopup.test.tsx and CityAutocomplete.test.tsx.
Two gaps filled vs the Angular reference:
1. EN→RU keyboard layout translit fallback in searchCities (TZ §4.1.9.1:
retry query converted from EN layout, e.g. "vjc" → "мос" → Москва).
2. ESC key cancels manual entry and restores the last committed value's
display (TZ §4.1.9.1 / mirrors Angular focusOut behaviour on Escape).
All other §4.1.9.1 rules (case-insensitive search, substring match, city+
airports grouping, 3-letter code lookup, top-10 cap, alpha sort, no auto-
submit on typing, exact-match auto-commit) were already present; assertion
tests lock them in.
- 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
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.
Typing a full city name (or airport name) and clicking search without
picking a dropdown row previously did nothing: the parent-held city
code stayed empty and the submit handler silently short-circuited.
Exact case-insensitive name matches now resolve to the owning city
code immediately, so the Schedule and OnlineBoard start pages can act
on keyboard-only input. Partial text still requires a dropdown pick.
Expanding a connecting-flight row on /schedule/route used to swap the small
round airline badges in the header for the wide rectangular logo, which
overflowed the operator column and overlapped the departure time. The
header now always renders the round variant on schedule pages, regardless
of the expansion state.