Commit Graph

777 Commits

Author SHA1 Message Date
gnezim 557ecefd4b Add Onlineboard row Buy/Register regression e2e (TIRREDESIGN-10)
Verified that React already surfaces the Купить билет and Онлайн
регистрация buttons in the expanded row body via FlightActions when
the per-flight visibility rules pass (canBuyTicket: now within
[dep-72h, dep-2h]; canRegister: registration.status==='InProgress'
and operating carrier in AIRLINES). No code change required for
TIRREDESIGN-10 — adding a regression test to lock the behaviour.
2026-04-23 13:42:27 +03:00
gnezim c90b33368c Add Test Rules: every fix must ship with an e2e test 2026-04-23 13:36:16 +03:00
gnezim 49a19a7f63 Schedule date-picker: snap single click to Mon-Sun week + auto-close
Angular's schedule date-picker is week-granular (TZ §4.1.9.4): one
click anywhere selects the whole calendar week, the panel closes and
the input shows the resulting range. React was using PrimeReact's
plain range-mode (two clicks required), so a single click left the
range half-set and the panel open.

Add snapToWeek() in ScheduleStartPage and ScheduleFilter, route both
outbound + return Calendars through new onSelect handlers that
compute Mon-Sun, commit it as the value, and call cal.hide() via
ref. Enable selectOtherMonths so bleed-in days from the previous /
next month are clickable. Add 3-test e2e spec (week snap from a
mid-week day, snap from a next-month bleed-in day, range placeholder
when empty).
2026-04-23 13:29:04 +03:00
gnezim c6055d94ba Add details-page breadcrumb leaf with Angular-correct labels
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).
2026-04-23 13:11:39 +03:00
gnezim ed3dc1053b Stop breadcrumb trail at section name (Angular parity)
Live audit of flights.test.aeroflot.ru shows the trail never adds a
third 'leaf' crumb — even on details pages reached with a ?request=
context. React was emitting an extra crumb in three places:
ScheduleSearchPage (route heading), ScheduleDetailsPage (back-to-
search leaf), OnlineBoardDetailsPage (back-to-search leaf). Strip
all three; rewrite the affected unit tests to assert the leaf is
absent; add an e2e parity spec covering all six page types.
2026-04-23 12:57:16 +03:00
gnezim 3d32897b10 Add e2e test for TIRREDESIGN-8 day-tabs window
Covers: full -1/+14 range across 3 pages (16 in-range dates), 5 greyed
out-of-range dates on the last page, right-arrow disabled at boundary,
sibling tabs stay enabled after consecutive clicks.
2026-04-23 12:43:39 +03:00
gnezim b5755ca0f9 Render out-of-range DayTabs as greyed-out (TIRREDESIGN-8 parity)
Angular keeps generating dates past +daysAfter and disables them, so the
user sees where the boundary is. React was emitting blank padding cells
instead. Replace the placeholder <div>s with disabled DayTabButtons
showing the next out-of-range dates.
2026-04-23 12:39:53 +03:00
gnezim 5fa42ba102 Bump vitest testTimeout to 15s to absorb parallel-run CPU contention
CI / ci (push) Failing after 59s
Deploy / build-and-deploy (push) Failing after 5s
Previously the 5s default starved several tests during full-suite
parallel runs — most reliably OnlineBoardSearchPage.error's timeout
test and the eslint boundary tests that fork a lint process per check.
Each of those passes in isolation in ≤3s; the 3x headroom keeps them
stable without masking genuine hangs. Individual tests can still
override via the third arg to it().
2026-04-22 19:22:08 +03:00
gnezim 80fe071e1a Use $space-s gap in ScheduleFlightsMiniList body
CI / ci (push) Failing after 58s
Deploy / build-and-deploy (push) Failing after 5s
$space-xs wasn't defined in src/styles/_variables.scss, which broke
the SCSS compile when the details page mounted the new mini-list.
2026-04-22 17:33:44 +03:00
gnezim e48e7436d0 Cache schedule calendar-days for 1 hour (TZ §4.1.16.8)
Wrap useScheduleCalendar's data fetch in a ClientMemoryCache with a
1-hour TTL keyed on date+departure+arrival+connections. Identical
route/date lookups across the session (filter, mini-list day groups,
details card week strip) now share a single response instead of
re-hitting the API.
2026-04-22 17:28:05 +03:00
gnezim d220c76be7 Persist Schedule filter into cross-section store on submit (TZ §4.1.8)
ScheduleFilter's handleSubmit now calls setScheduleFilter with the
submitted outbound + optional return snapshot. This completes the
cross-section wiring: OnlineBoardFilter already wrote to the store
and both start pages read getBoardFilter/getScheduleFilter for
Table-10 projection on subsystem switch.
2026-04-22 17:24:15 +03:00
gnezim f11cb7b15e Pin 'Россия и СНГ' first in city picker direction tabs (TZ §4.1.9.2)
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.
2026-04-22 17:21:58 +03:00
gnezim 2e05b92e4e Schedule mini-list: three-day [X-1]/[X]/[X+1] accordion (TZ §4.1.16.2)
New ScheduleFlightsMiniList groups sibling flights by scheduled
departure date into three accordions. [X] (the selected flight's day)
opens by default; adjacent days open only when the user clicks them.
Days without any flights in the loaded context render locked and
dimmed and cannot be expanded, matching TZ §4.1.16.2 R10-R21.

ScheduleDetailsPage swaps the flat FlightsMiniList for this new
component; the OB mini-list remains unchanged since its layout is
per-day-tabs-driven and already matches §4.1.15.2.
2026-04-22 17:19:47 +03:00
gnezim 6d87b8fa36 Special-meal availability probe via ws2 meal API (TZ §4.1.15.10)
- POSTs to https://www.aeroflot.ru/ws2/v.0.0.2/json/meal with the exact
  payload shape the TZ documents (origin, destination, airline, number,
  flight_datetime UTC, cabin).
- Cabin selection follows the spec: Economy present → economy;
  only Business defined → business; skip the call otherwise.
- 3-hour client-side cache keyed by the full payload so repeated opens
  of the same flight card don't re-hit the API.
- Ignores is_available_now per TZ; any populated special_meals / meals
  array or a truthy available flag surfaces the Special icon in the
  MealPanel on top of the class-based icons.
- MealPanel receives an optional specialMealContext; OnlineBoard details
  page threads the flight carrier+number through FlightLegs →
  FlightDetailsAccordion → MealPanel so the hook has everything it needs.
2026-04-22 17:15:06 +03:00
gnezim e444b6e261 Enforce outbound↔return week coupling in Schedule filter (TZ §4.1.9.4)
- Tie return calendar minDate to outbound dateTo so earlier days grey
  out in the picker (Table 16: return cannot start before outbound ends;
  same week allowed).
- Auto-clear the return range when the user moves outbound forward and
  strands the previously-chosen return in an invalid state.
- Clearing outbound via the X button now cascades to the return range.

Rewrote two tests that previously asserted the submit-time error path;
the new proactive clearing makes that path unreachable for this case,
which is closer to the intent of the TZ.
2026-04-22 17:09:14 +03:00
gnezim e7eca164f0 Aurora/Pobeda-only redirect banner for flight-number search (TZ §4.1.10.1)
When an OB flight-number search returns results where every flight is
operated by DP (Pobeda) or HZ (Aurora), render a redirect banner with
links to pobeda.aero and flyaurora.ru instead of the flight list.

Detection respects the §4.1.22 fallback table: an SU flight with no
operatingBy resolves via its number range (SU5000-5399 → DP,
SU5400-5799 → HZ), so subsidiary flights show the banner even when
the telegram carrier field is empty.

Translations added across all 9 locales.
2026-04-22 17:05:57 +03:00
gnezim 2e13d2d7ef Fix Schedule UI regressions and complete non-RU/EN locale translations
- 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.
2026-04-22 17:02:31 +03:00
gnezim a9dacf0b97 Clear lint backlog so make check runs green
CI / ci (push) Failing after 57s
Deploy / build-and-deploy (push) Failing after 5s
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.
2026-04-22 15:13:43 +03:00
gnezim 35cae21d92 Fix operator-icon overlap + restore trailing schedule-copy paragraph
CI / ci (push) Failing after 33s
Deploy / build-and-deploy (push) Failing after 5s
- 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.
2026-04-22 14:57:41 +03:00
gnezim 08f06ff1f4 Board time-slider now filters, day-tabs stop blocking (TIRREDESIGN-8 + 11)
CI / ci (push) Has been cancelled
Deploy / build-and-deploy (push) Has been cancelled
TIRREDESIGN-11: the board + schedule endpoints ignore the raw 4-digit
HHMM query values the slider produces and only honour HH:MM:SS (Angular
formats via ApiFormatterService.formatTime). Normalise both at the API
layer so the slider actually narrows results; '2400' collapses to
'23:59:59' since midnight-of-next-day isn't representable.

TIRREDESIGN-8: the 31-day availability bitmask is always anchored to
today (Angular parity — updateCalendar() uses new Date() - 1). We were
passing params.date as the anchor, which shifted the window forward
every time the user picked a future day and caused earlier DayTabs to
fall outside the returned bitmask, grey-listing days that still have
flights.
2026-04-22 14:44:44 +03:00
gnezim c2f2c9e089 Grey out non-operating days in filter calendars (TIRREDESIGN-12)
CI / ci (push) Failing after 35s
Deploy / build-and-deploy (push) Failing after 5s
The Online-Board + Schedule filter calendars ignored the 31-day
operating-days bitmask the API ships, so users could pick dates that
have no flights and land on empty result pages. Angular wires
[disabledDates] from the same endpoint; we do the same here.

- useCalendarDays / useScheduleCalendar now accept null params so the
  callers can skip the fetch until they have enough input to resolve
  a calendar segment (full flight number, route with both cities).
- OnlineBoardFilter + ScheduleFilter compute disabledDates by
  differencing the min/max window against the API's available-days
  array, then feed that into PrimeReact's Calendar.
- Test mocks added to sidestep the api provider requirement in the
  filter/start-page/integration trees that render these components.
2026-04-22 14:17:00 +03:00
gnezim 7cc0327a12 Show all active transition blocks inline + gate on isActual (TIRREDESIGN-7)
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.
2026-04-22 13:55:53 +03:00
gnezim 31751d0e84 'Купить билет' hover link + anchor semantics (TIRREDESIGN-6)
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.
2026-04-22 13:45:40 +03:00
gnezim 8bde3904e1 Per-section history cap (8) + rename 'Вы искали' → 'Ранее искали' (TIRREDESIGN-5)
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.
2026-04-22 13:45:30 +03:00
gnezim a26adad895 Schedule row click opens flight details (TIRREDESIGN-4)
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.
2026-04-22 13:45:21 +03:00
gnezim 99d86fba29 Show full date-range placeholder in Schedule filter
The schedule calendar input had `padding-right: 2rem` left over from
the era when the calendar icon sat inside the <input> as a background
image. The trigger icon now lives in a sibling `.p-datepicker-trigger`
button (also 2rem wide), so the input was reserving an extra 32 px on
top of that — which truncated 'ДД.ММ.ГГГГ - ДД.ММ.ГГГГ' to
'ДД.ММ.ГГГГ - ДД.ММ....' in the narrow left-column layout. Drop the
redundant override; the input now uses the shared 15 px horizontal
padding from `_prime-calendar.scss` and the full placeholder fits.
2026-04-22 12:27:37 +03:00
gnezim a1089e07dd Port Angular time-range slider styling to the filter sidebars
Angular's `page-time-selector.scss` paints the whole filter slider in
brand blues — pale $blue-light2 track, solid $blue-light range, and
solid $blue-light thumbs with a 2px white ring — and thickens the
horizontal track from PrimeReact's 4px default to 6px. React carried
the stock gray/white PrimeReact theme, which read as a dead, low-
contrast control next to the blue chevrons and calendar icon.

Add the same overrides scoped under `.wrapper--time-selector` so both
OnlineBoard and Schedule (the two sidebars that render the range
slider) pick them up.
2026-04-22 12:13:43 +03:00
gnezim aad94636c7 Use Angular's outlined calendar SVG in the datepicker trigger
PrimeReact ships a solid filled-calendar glyph; Angular's filter calendar
uses a thinner outlined glyph with six "row" tiles drawn in the brand
blue (#418fde). Replace the default icon by hiding PrimeReact's <svg> +
painting `/assets/img/calendar.svg` as a background on
.p-datepicker-trigger, scoped to the three filter-sidebar roots.

Consolidate the per-page .p-datepicker-trigger styling — the three
filter SCSS files each carried their own background/border/color
overrides that kept drifting. Now only the width override lives per
page, everything else is shared in styles/_prime-calendar.scss.
2026-04-22 12:01:21 +03:00
gnezim d7a0d715b7 Stack breadcrumbs above the page H1 on all layout pages
CI / ci (push) Failing after 32s
Deploy / build-and-deploy (push) Failing after 6s
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.
2026-04-22 11:44:55 +03:00
gnezim 8feb5de70e Dev-server: fall back between direct and HTTPS_PROXY transports
The local WAF is unpredictable: some windows the gost VPN tunnel at
127.0.0.1:8888 is 503-ing (direct must work), other windows the direct IP
is throttled to 403 by Ngenix (VPN must be used). The previous hardcoded
`--noproxy '*'` survived one of those states only, which is why the
dictionary load surfaced as console 403s as soon as the state flipped.

Try direct first (faster when it works, simpler cookie jar), fall back
through the system HTTPS_PROXY on 4xx/5xx or curl failure, keep a
separate cookie jar per transport so the Ngenix cookies don't cross-
contaminate edge nodes.
2026-04-22 11:44:46 +03:00
gnezim 1d3f0efc5f Align filter-sidebar label + datepicker styling across all 3 pages
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.
2026-04-22 11:28:56 +03:00
gnezim 848ba48484 Extract SwapCityButton so all 3 filter blocks share the same DOM
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.
2026-04-22 11:03:57 +03:00
gnezim 408afa6ab5 Resolve IATA to city names in search-page <title>
Deep-linked search pages rendered `Расписание по маршруту: MOW-MMK`
in the document title because the route page called the SEO builder
synchronously with the raw URL params, before the dictionary was
available. The on-page H1 resolved correctly via `useDictionaries`
inside the child component, but the parent never re-rendered so the
title stayed frozen on IATA codes. Wire `useCityName` into the 5 deep-
link route pages (schedule one-way / round-trip, onlineboard route /
departure / arrival) so the SEO title reflects city names once the
dictionary loads — per TZ §4.1.14.1.
2026-04-22 09:41:16 +03:00
gnezim a4e8d87688 Fix dev-server proxy so API forwarding survives WAF cookie challenge
curl was inheriting HTTPS_PROXY=127.0.0.1:8888 (a local gost tunnel whose
upstream VPN intermittently 503s), making the app fail to load dictionaries
in dev. Upstream Ngenix WAF also newly requires a 307-to-self cookie
handshake (ngenix_valid) before issuing JSON. Bypass the system proxy
directly and keep a per-session cookie jar so the handshake only runs once.
2026-04-22 09:41:03 +03:00
gnezim 678cde3ed2 Fix city-input + date-picker styling + remove extra Schedule section
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.
2026-04-22 03:53:11 +03:00
gnezim c18b4b212e Fix popular-flight Search-button no-op when today is mid-week
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.
2026-04-22 03:26:46 +03:00
gnezim b5b5131eee Emit document title on error pages (404/500) per TZ 4.1.21
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 — Страница
не найдена".
2026-04-22 03:07:55 +03:00
gnezim 06b1aba530 Revert map marker permanent label to city name (not IATA code)
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.
2026-04-22 02:56:45 +03:00
gnezim 26d116f18e Fix browser-runtime TZ-compliance gaps found during live smoke test
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.
2026-04-22 02:48:48 +03:00
gnezim 2f386cbaf0 Mark P6 rules Done + project-complete summary in TZ audit spec
CI / ci (push) Failing after 35s
Deploy / build-and-deploy (push) Failing after 5s
2026-04-22 02:21:57 +03:00
gnezim e433c0dc13 Fix noUncheckedIndexedAccess errors in ErrorPage.test.tsx 2026-04-22 02:13:15 +03:00
gnezim 83a9edb44e §4.1.24: assertion tests for all 6 sub-subsection clusters
Adds 48 new rule-tagged tests across 7 test files covering:
- §4.1.24.1/.2: filter disabled states (R13/R14/R16), swap button (R11),
  no-collapse (R8), hint text (R19), IATA tooltip label (R24)
- §4.1.24.3: arc style (R25), rendering mode (R21/R27/R28/R29),
  domestic/intl/connecting filters (R32-R35), zoom tiers (R23)
- §4.1.24.4: click sequence first/second/third (R36/R37/R39/R41)
- §4.1.24.5: API endpoint contract, bit-string parsing (R44/R45)
- §4.1.24.6: CTA URL format, new-tab intent, date-omit (R46/R47/R48)

Total: 175 tests, all passing.
2026-04-22 02:09:06 +03:00
gnezim 41d229a197 §4.1.24.6 R48: omit date segment in SB URL when Дата рейса not set
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.
2026-04-22 02:08:56 +03:00
gnezim 0bb6bf2032 §4.1.24.3 R24: map marker tooltip label = IATA code, not city name
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.
2026-04-22 02:08:49 +03:00
gnezim f5dfa14eab §4.1.24.1/.2: filter label hidden on mobile; date locked until departure set
- 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).
2026-04-22 02:08:43 +03:00
gnezim a94b01cee9 Audit 404 + 500 error pages per TZ §4.1.21
Gaps closed:
- noindex meta: ErrorPage now emits <meta name="robots" content="noindex,nofollow"> (R6)
- 500 refresh CTA: add PAGE500.REFRESH i18n key (9 locales) and a reload button (R5)
- SSR HTTP status: $.tsx converted to $/page.tsx+error.tsx; loader throws Response(404)
  so Modern.js emits the correct status code; same pattern for error/[code]/page.tsx (R8)
- Add error.tsx error-elements so the branded page renders after the loader throws

Pre-existing (already compliant): URL preservation, root link, all 9 locales, support link.
Tests: 34 new assertions cover R4–R8 (noindex, root link, refresh, i18n keys, loader status).
2026-04-22 01:56:53 +03:00
gnezim 5286049420 Audit OpenGraph + canonical + hreflang per TZ 4.1.19/20 (assertion tests)
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.
2026-04-22 01:48:00 +03:00
gnezim 944015d658 Add JSON-LD microdata builders per TZ 4.1.19 + CLAUDE.md #6
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.
2026-04-22 01:45:32 +03:00
gnezim 4904ba31c9 Audit caching behavior per TZ 4.1.18 (all 3 rules backend-responsibility)
Client makes no stale-serving assumptions: every Online-Board and Schedule
search triggers a fresh upstream fetch; dictionaries are loaded once per
session. Cache-Control TTLs (≤1 min board, ≤10 min schedule) and ETag/304
for dictionary endpoints are set by the backend HTTP layer. CachedApiClient
/ ServerLruCache infrastructure exists in src/shared/api/ for future SSR
caching if needed. All three 4.1.18 rules marked Out-of-scope (backend) in
the spec; coverage counters updated (13 → 16 out-of-scope, TBD −3).
2026-04-22 01:40:42 +03:00
gnezim 187977a39f Close C5 TZ 4.1.1-R22 typo conflict (Flight-Map placeholder uses ДД.ММ.ГГГГ) 2026-04-22 01:37:16 +03:00