Commit Graph

640 Commits

Author SHA1 Message Date
gnezim f5e41a7911 Add flight details button to schedule search results
ci-deploy / build-deploy-test (push) Successful in 1m49s
- Add flight details button to ScheduleFlightBody component
- Button positioned after Buy button (matching Angular layout)
- Button uses SHARED.FLIGHT-DETAILS translation key
- Add onFlightDetails callback to ScheduleFlightBody props
- Add handleFlightDetails to DayGroupedFlightList
- Pass onFlightDetails to ScheduleFlightBody
- Add E2E tests for flight details button functionality
2026-04-29 20:23:24 +03:00
gnezim 5db509e199 Restore buy/share/status strip in schedule search results body
Angular search-results page renders <flight-details-body-actions> →
<flight-actions> with NO overrides inside every expanded flight body —
share/buy/register/status all surface there. A prior refactor confused
this with the dedicated /schedule/details page, where Angular's
flight-schedule-details DOES set [share]=false [buy]=false [print]=false
[details]=false [register]=false because that page-level summary owns
those affordances. The strip was removed from both contexts, leaving
the search results page (e.g. /ru-ru/schedule/route/AER-LED-…) without
any buy button when a flight is expanded.

ScheduleFlightBody now accepts an opt-in showActions flag and renders
the existing <FlightActions> at the bottom (Angular-parity gating via
canBuyTicket / canViewFlightStatus). DayGroupedFlightList opts in;
ScheduleDetailsPage stays opted out so its page-level summary remains
the single owner of share/buy on the details page.

Note on e2e: tests/e2e/schedule-route-buy-button.spec.ts asserts the
button surfaces after expanding the first card, but the local dev
server's curl-based API proxy is currently being blocked by the
upstream WAF ("Доступ к сайту временно ограничен"), so the spec runs
green only against environments that reach /api. CI + deployed
verification suites cover that path. Behaviour is also locked in by:
- ScheduleFlightBody.test.tsx — strip renders iff showActions=true
- DayGroupedFlightList.test.tsx — passes showActions=true through
2026-04-27 22:08:01 +03:00
gnezim bfd236cf89 Move SSR-stable today loader to data.ts (Modern.js convention)
ci-deploy / build-deploy-test (push) Successful in 1m54s
Inline export const loader from page.tsx didn't run — _ROUTER_DATA
showed loaderData[(lang)/onlineboard/page] = null and useLoaderData()
threw 'Cannot read properties of null'. Modern.js conventional routes
require the loader in a co-located data.ts file.

useLoaderData() now defensively handles null (defaults to undefined,
component falls back to useRef(new Date())). Worst case if loader still
doesn't fire: same hydration drift as before, no crash.
2026-04-27 20:20:18 +03:00
gnezim 5ba34ab507 SSR-stable today for FlightsMap route (hydration step 1b)
Same pattern as OnlineBoard: route loader supplies todayYyyymmdd() once
on the server; FlightsMapStartPage threads it through useMemo dep arrays
for searchParams + calendarParams so SSR and client hydration agree on
the same dateFrom/dateTo values.

Removes the local todayYyyymmdd() copy in favour of the shared util.
2026-04-27 20:11:31 +03:00
gnezim 615c1642b3 SSR-stable today for OnlineBoard route (hydration step 1a)
Route loader at src/routes/[lang]/onlineboard/page.tsx computes today's
yyyyMMdd once on the server. Result rides _ROUTER_DATA into the client
bundle, so the first hydration render sees the same value the SSR render
saw — no diverging new Date() calls during render.

OnlineBoardFilter accepts an optional today prop; getBoardMinDate /
getBoardMaxDate take a base Date instead of calling new Date()
themselves; the four todayIso() callsites read the precomputed
todayIsoStr. Existing tests omit the prop and use a fresh new Date()
fallback (captured once via useRef) — back-compat preserved.

Adds three pure helpers to src/shared/utils/datetime: todayYyyymmdd(),
yyyymmddToDate(), yyyymmddToIso().

Triage doc: docs/superpowers/specs/2026-04-27-ssr-hydration-fix.md
(Step 1, OnlineBoard. FlightsMap to follow in next commit.)
2026-04-27 20:07:25 +03:00
gnezim a0dd0a5596 baseline: carry WIP schedule/UI changes from main
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.
2026-04-25 02:07:35 +03:00
gnezim 5d18544a46 Clean unused helpers after details-page simplification
- ScheduleDetailsPage: drop shiftYmd helper and selectedYmd local —
  both were left over from the removed day-sibling search path.
- ScheduleFlightBody.test: drop fireEvent import + FUTURE_340D /
  FUTURE_1H / YESTERDAY / todayUtc constants; they belonged to the
  Buy/Status button tests that moved to the summary-header layer.
- flight-details + error-handling integration tests: mock
  useCityName / useStationDisplayName so OnlineBoardDetailsPage can
  render without an ApiClientProvider wrapping — the station lookup
  hooks now transitively depend on useApiClient via the cached
  useDictionaries fetcher introduced in 7deb46a.
2026-04-23 18:07:31 +03:00
gnezim 7deb46aeae Cache network fetches + fix console duplicates
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.
2026-04-23 17:57:25 +03:00
gnezim ab09075226 Schedule details: single-card mini-list, drop page tabs sidebar
Left rail previously rendered the open flight PLUS its day-±1
siblings from a route search. For a connecting itinerary the three
rows were visually identical (Moscow → Murmansk on the same times),
so users read them as duplicates. Angular's schedule-flights-mini-list
only shows a multi-day accordion when schedule.length > 1 and falls
back to a single-card view otherwise; mirror that by always passing
an empty flights[] to ScheduleFlightsMiniList — it shows only the
synthesized open flight.

Header-left drops the Онлайн-Табло / Расписание / Карта полетов
tab strip; Angular's schedule-flight-details-view only slots
<details-back>, so the top 'Вернуться к Расписанию' link is the
single navigation affordance on the details page.

FlightsMiniListItem now joins _childFlightIds as 'SU 6188, SU 6341'
for connecting itineraries — Angular's flights-details-list-flight
surfaces every leg's number in the rail label, not just the primary.

Removes the day-sibling useScheduleSearch call + the miniListFlights
filter memo + PageTabs import + pageTabs JSX — all unused now.
2026-04-23 17:35:23 +03:00
gnezim f6b444a7a7 Fix Buy ticket link: drop duplicated locale in URL path
React templated the booking URL as '${locale}-${locale}', which
produced 'sb/app/ru-ru-ru-ru' for a BCP-47 'ru-ru' prop (our router
emits locales in BCP-47 form). The resulting link 404'd on the
Aeroflot booking tool.

Angular's BuyTicketLogic.getLink hardcodes 'sb/app/ru-ru' regardless
of the current UI language; do the same. The 'locale' prop is kept
optional on BuyTicketButton for backward-compat with existing
callers but is no longer consumed inside the URL builder.
2026-04-23 17:26:21 +03:00
gnezim 62136dcde3 Split schedule details into three separate frame blocks
Each flight card and the Пересадка strip are now sibling elements
inside .schedule-details — each flight in its own <section class="frame">,
the TransferBar standalone between them. The shared outer frame
wrapper is gone, so the dark page background shows through the
between-block gaps instead of one continuous white surface.

That produces the 'three separate white cards on dark bg'
visual Angular uses for a connecting itinerary (flight 1 frame |
Пересадка | flight 2 frame) — 40px white margins that previously
bled into the surrounding frame disappeared because the flex gap
now renders against the actual page background.
2026-04-23 17:23:20 +03:00
gnezim fbd819c707 Transfer gaps 40px + always-visible Buy pill in schedule summary
The Пересадка wrapper bumps to $space-xxl (40px) margin: at 15px
the strip blended with the regular between-card gap from
`.schedule-details { gap: $space-l }` and read as a sibling card
rather than a separator. 40/40 mirrors Angular's breathing room.

FlightActions gains a `forceBuy` prop that bypasses the
canBuyTicket() status/window gate. The schedule summary passes it
because the Buy pill there is a generic 'open the Aeroflot booking
tool for this route' affordance — the user picks any date in the
booking flow, so hiding the button on a specific day's 'Cancelled'
status (as the Onlineboard detail page does) loses a useful entry
point. Board detail pages still pass the default (status-gated).
2026-04-23 17:19:32 +03:00
gnezim d8118bafa8 Transfer: wrap Пересадка in rounded-card with vertical gaps
Between two flight cards on a connecting itinerary the TransferBar
used to sit flush against the preceding FlightSchedule block and the
next flight's header — no breathing room, no edge, read as a shared
card rather than a separator. Now the strip is wrapped in a
'__transfer' div that adds '$space-l' top/bottom margin and gives
the inner .transfer-bar the same 'border-radius' Angular uses for its
'.transfer-bar--separated' card variant.
2026-04-23 17:12:57 +03:00
gnezim fa4656dab1 Summary header: round-logo badges + remove share/buy from leg body
Connecting itineraries now render details-header-badge with the small
round airline icon (36×36) from Angular's `[round]="isConnecting"`
path and drop the 'Авиакомпания' caption, so the SU 6188 + SU 6341
row sits compactly next to the share/buy/last-update cluster instead
of stretching two wide wordmarks across the summary.

Share + Buy buttons removed from ScheduleFlightBody — Angular's
`flight-schedule-details` wires `[share]=false [buy]=false
[print]=false [details]=false [register]=false` into its inner
flight-actions, so a per-leg action strip was never meant to exist.
The page-level summary header now owns those affordances.

OperatorLogo.scss: override the 180×46 rule inside .details-header-badge
when the logo carries .operator-logo--round so the connecting-summary
badge doesn't force a wide wordmark.

BoardDetailsHeader.scss is imported from DetailsHeaderBadge.tsx so
consumers (schedule details summary) that use the badge without the
full BoardDetailsHeader wrapper still pick up flex/gap/typography.
2026-04-23 17:07:25 +03:00
gnezim cbced8d4b6 Schedule details: summary header, fix mini-list duplicates, fix timeline times
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).
2026-04-23 16:53:38 +03:00
gnezim 7324b4c03a Schedule mini-list: drop day-grouping accordions, render flat list
Per the Суббота/Воскресенье/Понедельник headers added an extra
click and zero information — every FlightsMiniListItem already
carries its own date. Replace the per-day accordion wrapper with a
straight chronological column. Always merge the open flight into
the rendered list (the open flight loads via a separate details
endpoint and may not appear in the [-1, +1] sibling search). Strip
the now-orphan day-header / day-body SCSS rules and rewrite the
unit tests to assert the flat-list behaviour.
2026-04-23 16:10:13 +03:00
gnezim 37ae7dcd46 Schedule details mini-list: filter to the open flight only
Mirrors Angular's CurrentScheduleService.getScheduleType +
compareFlightsByPId: when the [-1, +1] route search returns the
open flight (matched by carrier+number signature, including each
leg of a connecting itinerary), keep only those instances; when
no match exists, fall back to a 1-item list with just the open
flight (Angular's 'default-schedule' branch). Old behaviour
returned the full route search and dumped every unrelated MOW-MMK
option into the rail.

Add e2e regression that loads the SU 6188 + SU 6341 itinerary and
asserts the rail shows only SU 6188 — not SU 6190 / SU 6699 (the
other Sunday MOW-MMK options that used to appear).
2026-04-23 15:54:19 +03:00
gnezim 6dcbb332be Schedule details: use sprite #company icon for Борт (Angular parity)
Probed the live Angular page and the schedule-details Борт row uses
`<svg><use xlink:href="/assets/img/sprite.svg#company">` — a
stroked side-view jet silhouette — NOT the simpler top-down plane
glyph from toolkit/icons/plane (that one only appears in the row
indicator next to flight times). Port the seven #company paths
from sprite.svg verbatim into the leg-details panel so the icon
matches the legacy app exactly.
2026-04-23 15:45:03 +03:00
gnezim 3304e76d4f Schedule details: port plane + dining SVGs from Angular toolkit
The plane and food row icons in ScheduleLegDetails were custom React
SVGs (a cargo-airliner side-view and a knife+fork glyph) that didn't
match the legacy app. Copy the actual paths verbatim from
ClientApp/src/app/toolkit/icons/plane and .../dining so the
React panel shows the same stylised top-view plane and the cloche-
on-base 'Питание на борту' icon Angular ships. Use currentColor on
both fills/strokes so the existing $blue link colour still applies.
Reserve a 36px slot in __icon so the rows line up despite the
plane (19×19) vs dining (34×26) intrinsic-size mismatch.
2026-04-23 15:38:15 +03:00
gnezim 2ba4c152e8 Schedule details: gate Питание sub-icons on equipment.meal[] (Angular parity)
Angular's flight-details-meal.component.html renders each
Эконом / Комфорт / Бизнес sub-icon under *ngIf=hasEconomyMeal etc —
flights with no meal data show just the cutlery icon and caption
with no class pills. React was hardcoding all three regardless of
data, so SU 6188 (whose API returns meal=[]) showed three meaningless
icons; SU 6341 (meal=[Comfort, Economy, Business]) showed the right
ones by accident.

Read leg.equipment.meal, build a Set<MealType>, render each pill
only when its type is in the set. Add a unit test covering empty,
partial, and full meal data and an e2e regression on the live
MOW→LED→MMK itinerary (test asserts SU 6188 has none, SU 6341 has
all three). The e2e depends on backend data and can flake when the
dev proxy WAF cookie has expired.
2026-04-23 15:33:17 +03:00
gnezim 013ca3ed91 Fix Дни выполнения рейса parsing: digit-list, not bitmask
API returns daysOfWeek.flight as a string of ISO weekday digits
("1"=Mon.."7"=Sun) where each character is the number of one
operating day, NOT a 7-char position bitmask. E.g. SU 6188's
flight value is "156" → Mon + Fri + Sat operating; SU 6341's is
"1234567" → daily. The old reader treated it as bitmask and only
checked position[i]==='1', so SU 6188 highlighted only Mon and SU
6341 highlighted only Mon — the highlighting looked random
relative to Angular which uses the digit-list semantics.

Walk the input character-by-character, build a Set<weekdayNumber>,
mark badge active when its day-number is in the set. Defends
against non-digit characters and out-of-range digits. Rewrite the
unit tests to match the real wire format and add a regression
case for the SU 6188 "156" pattern.
2026-04-23 15:25:53 +03:00
gnezim 4c79914313 Schedule details: include every leg in URL for connecting itineraries
handleFlightClick previously emitted only the first flight ID even
when the row was a connecting itinerary, so the details page only
showed leg 1 (e.g. SU 6188 Moscow→St-Petersburg) and dropped leg 2
(SU 6341 St-Petersburg→Murmansk). Walk `_childFlightIds` instead,
interleaving each leg's airport codes around its segment so the
output URL is /schedule/{depAir}/{flight1}-{date}/{midAir}/
{flight2}-{date}/{arrAir}?request=… — the splat route already
parses any number of segments and the details page already maps
over flights[], so both cards + the Пересадка transfer bar render
correctly.

Add an e2e that clicks a connecting row, asserts the multi-segment
URL pattern, the two .schedule-details__flight cards, and the
Пересадка bar. The test depends on live backend data so it can be
flaky in environments where the dev proxy cookie has expired.
2026-04-23 15:17:11 +03:00
gnezim 72e6149320 Restore Schedule inline-expand to match Angular (revert TIRREDESIGN-4 forward-ship)
The Schedule row had been switched to navigate-on-click in commit
a26adad as a forward-looking implementation of TIRREDESIGN-4. Angular
on the live test stand still uses inline-expand (verified 2026-04-23
on flights.test.aeroflot.ru — clicking a row toggles
.flight-list-item.selected and renders schedule-search-result-flight-
body / connecting-flight-body inline). React must not lead Angular —
restore the inline-expand wiring so both stays in lockstep.

Drops the schedule-specific branch in FlightList that disabled
expandable and wired onClick to navigate. The expand-via-onFlightClick-
or-renderExpandedBody rule applies uniformly to Board and Schedule
rows again, exactly like before commit a26adad.
2026-04-23 14:59:13 +03:00
gnezim e52d673658 Revert "Schedule list: row click navigates, no inline expand (TIRREDESIGN-4)"
This reverts commit 7f9ce8bf26.
2026-04-23 14:54:28 +03:00
gnezim 7f9ce8bf26 Schedule list: row click navigates, no inline expand (TIRREDESIGN-4)
Spec calls for the Schedule list to drop the inline expanded view
entirely — clicking a row should take the user straight to the
flight-details page, with the per-row 'Купить билет' affordance
exposed only on hover.

FlightList: gate inline-expand on whether renderExpandedBody is
provided, not on onFlightClick. When the caller supplies onFlightClick
without a body renderer, wire it to FlightCard.onClick (single-click
navigate). When both are present (Online-Board), keep the existing
expandable + onViewDetails wiring.

DayGroupedFlightList: drop renderScheduleBody/renderExpandedBody
from both FlightList sites (single-day and per-day-group). Schedule
rows now navigate via onFlightClick; the 'Купить билет' link is the
inlineBuyUrl rendered by FlightCard with hover-only CSS.

Add a 2-spec e2e: row click changes URL to /schedule/<segment>?
request=schedule-route-… and the per-row buy link is anchor-tagged
to the SB booking URL on every visible row.
2026-04-23 14:52:28 +03:00
gnezim efe6b8be0a Rename search-history block 'Вы искали' → 'Ранее искали' (TIRREDESIGN-5)
Spec calls out the exact label change for the recent-searches sidebar
on Schedule and Online-Board start pages. RU was the literal 'Вы
искали' (You searched) — switch to 'Ранее искали' (Previously
searched), matching the section heading and the inline 'Ранее искали
в Онлайн-Табло' / 'Ранее искали в Расписании' captions. Other
locales already used 'Previous searches' / 'Search history' wording
and stay unchanged. Add 2-spec e2e seeding sessionStorage with a
valid history item and asserting the new label appears.
2026-04-23 14:37:19 +03:00
gnezim bd3bb1450c Fix Schedule calendar operating-days lookup (TIRREDESIGN-12)
ScheduleFilter.computeDisabledDates compared the cursor date in
yyyymmdd form against the schedule /days API output which is
yyyy-MM-dd. Lookups never matched, so every calendar cell ended up
disabled. Add a small dateToIsoYmd helper, switch the comparison
to ISO format, and add an e2e regression that asserts the picker
contains both enabled and disabled cells for a real route.
2026-04-23 14:23:40 +03:00
gnezim ec0c9f857e Drop non-existent CalendarChangeEvent import
PrimeReact v10's calendar.d.ts does not export CalendarChangeEvent
(uses internal FormEvent<TValue> for onChange instead). My earlier
commit referenced the symbol and the dev bundle threw 'undefined
factory ./src/shared/hooks/useDictionaries.ts' downstream because
ScheduleFilter/ScheduleStartPage failed to load. Fall back to a
narrow inline { value: unknown } parameter type — the handler reads
e.value into a local Date | Date[] anyway.
2026-04-23 13:58:43 +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 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 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