Commit Graph

432 Commits

Author SHA1 Message Date
gnezim 0bf4e23815 Port Angular's closest-flight auto-select + scroll-into-view behavior
Angular's route/departure/arrival search result list picks a 'current
flight' on load and auto-expands + scrolls it into view — the flight
whose dep/arr time is closest to 'now' for today's searches, or the
first/last flight when the search is for a future/past day. React was
always rendering the list scrolled to the top, so on today's route
search the user sees flights from 00:30 onwards instead of landing on
whatever is departing right now.

- Add features/online-board/closestFlight.ts with a React-flavored port
  of find-closest-flight.ts (plus a today-guard that reuses the same
  'yyyymmdd' shape the URL parser produces).
- FlightList takes an optional initialCurrentFlightId, attaches a ref
  to each card, and scrollIntoView's it on mount / list change.
- FlightCard takes an initialExpanded prop and seeds its useState so
  the selected flight lands expanded, matching the Angular 'expanded:
  true' assignment after setCurrentFlight.
- OnlineBoardSearchPage computes the id via findClosestFlightId using
  the current params (type + date) and forwards it to FlightList.
2026-04-19 14:18:23 +03:00
gnezim f50a0d5b33 Show 'Сегодня' in filter date field when the selected date is today
Angular's search filter rewrites the 'Дата рейса' input value to the
translated 'Сегодня' label whenever the picked date equals today,
matching the 'Сегодня' that appears in the H1 and SEO strings. React
was showing the raw 'DD.MM.YYYY' even when today, so the filter read
clinical next to the warm page heading.

PrimeReact's Calendar doesn't support a custom display formatter, but
exposes an inputRef. Wire one up on both Calendar instances (flight
number tab + route tab) and rewrite the DOM value to SHARED.TODAY
whenever flightDate / routeDate is today. The ref update runs on
every mount + date change, so navigating between tabs also gets it
right.
2026-04-19 13:50:37 +03:00
gnezim eea8d92212 Resolve IATA → city + today → 'Сегодня' in search-page SEO
On /ru/onlineboard/route/MOW-LED-20260419 (and /departure/, /arrival/)
the H1 already read 'Маршрут: Москва - Санкт-Петербург, Сегодня' but
document.title and meta[name=description] carried the raw 'MOW - LED
19.04.2026' because SeoHead runs at the route level with URL-only
params. Angular ships the resolved city names + 'Сегодня' in both.

Add a useEffect in OnlineBoardSearchPage that, once the dictionary
hook returns, overwrites document.title + meta description using the
same describeStation/dateLabel helpers that feed the H1. Route,
departure, and arrival search types all get handled; flight-number
search is unchanged.
2026-04-19 11:43:01 +03:00
gnezim 2ae25e630a Match Angular route-strip grid proportions + dotted hairlines
Measured track Angular uses for .flight-route:
  grid-template-columns: [depart-at] 97 [depart-to] 233
                         [status]   472 [arrive-at] 97
                         [arrive-to] 107
  padding: 50px 20px 0;

React was rendering a symmetric 5-column grid
(1fr for every non-time column), which cut the progress/status column
to ~25% of the strip instead of ~40%. Visually the effect was a
cramped 'Прибыл' label with barely any room for the green progress
bar. Retune to an asymmetric grid with `minmax(300px, 2.5fr)` on the
status column and lift the top padding to 40px.

Also switch the route→details and accordion-row hairlines from
#e0e6f0 / dashed to Angular's measured 1.3px dotted #D1DCEA for a
softer, identical visual.
2026-04-19 10:04:26 +03:00
gnezim 3c7dad5fd7 Scale accordion row icons up to match Angular's ~47×47 sprite size
Angular's sprite icons (#service, #board, #deboard, #company, #food,
#additional_service) render at ~47×47 in the row caption column —
significantly larger than a typical inline affordance, creating a
clear visual anchor for each row. React's inline SVGs were sized at
28×28 (a quarter of the area), which made the icon column feel
like an afterthought next to the large red 'Закончена' status.

Bump .details-row__icon to 44×44 and set the svg width/height attrs
to match. Keep the grey stroke color (#657282).
2026-04-19 03:14:36 +03:00
gnezim cfa33d0586 Match Angular back button: full-width blue fill, white text
Angular ships '← Вернуться к Онлайн-Табло' as a solid 48px-tall
primary button spanning the full 285px mini-list column (bg #4A90E2,
white text, 3px radius). React had it as a narrow pale-blue badge
(bg #E3F0FF, dark-navy text, 35px tall), which read as a secondary
link rather than the primary navigation affordance above the
sibling-flights list. Retune DetailsBackButton to the measured
Angular values.
2026-04-19 03:04:01 +03:00
gnezim 845f84ba01 Match Angular day-chip + schedule value + week-note styling
Measured against the Angular deploy:
  day chip:       12px / #333 / transparent w/ 1px #D6DDE6 border
  '15:30' value:  12px / 400 / #F37B09  (dep/arr time = number-group)
  '1ч. 30мин.':   16px / 500 / #333     (duration = magnitude)
  week note:      12px / #657282
  'Дни выполнения рейса' label: sentence case (no uppercase)

React was rendering the day-of-week strip as filled blue pills at
14px/500, the schedule 'Время в пути' value at 14px/600/#222, and the
schedule label with an uppercase text-transform. Swap day chips to a
minimal bordered-but-transparent style, split the flight-schedule value
into a --duration modifier so dep/arr render orange and duration
renders dark+larger, and drop the text-transform on the label.
2026-04-19 02:55:48 +03:00
gnezim 3383015bb0 Match Angular mini-list header/time sizing + soften accordion headers
Measured computed styles on the deployed Angular reference:
  mini-list flight-number: 16px / 400 / #000  (mine: 12px / grey)
  mini-list time:          20px / 500 / #022040 (mine: 17px / 600 / #222)
  'Детали рейса' header:   16px / 400 / #333  (mine: 18px / 500 / #222)
  'Расписание рейса' hdr:  16px / 400 / #333  (mine: 18px / 500 / #222)

React's mini-list was reading the carrier+number as secondary metadata
and the time as a loud bold chunk; Angular reverses the hierarchy —
the carrier+number is the tile's identifier, the time is a darker navy
number-group. Retune both. Also drop the collapse-header weight on
both details+schedule accordions so they read as section separators
rather than section titles; the row content below is the focus.
2026-04-19 02:43:46 +03:00
gnezim 83d110d3c6 Match Angular meal/service link color and DayTab number size
- Meal + on-board-service tile links ('Эконом класс', 'Комфорт класс',
  'Выбор места', 'Space+') were rendering at 14px / #333 — readable but
  not discoverable as clickable. Angular serves them at 12px / #4A90E2
  with a darker hover so the whole tile reads as a link. Retune
  .details-panel__icon accordingly.
- DayTab day number was 20px / 500; Angular uses 16px / 500 with a
  smaller 11px weekday above it. Shrink day + weekday to match so the
  date strip doesn't dominate the card.
2026-04-19 02:32:09 +03:00
gnezim f18e6d4bc0 Match Angular accordion time values + aircraft property values
Measured Angular computed styles on the deployed reference:
  time-value:    12px / 400 / #F37B09  (mine: bold / #222)
  time-date:     10px / #333            (mine: 12px / blue)
  time-label:    12px / #333            (mine: 12px / #8A8A8A)
  day-change:    10px / baseline / #4A90E2 (mine: 11px / super / blue)
  aircraft lbl:  12px / #657282         (mine: 12px / #8A8A8A)
  aircraft val:  16px / 500 / #333      (mine: bold / #222)

Retune the accordion's transition-time cells ('15:30', '14:45') to
render in brand orange so the red 'Закончена' status stays the loudest
signal, keep the dayChange '-1' badge inline at baseline, and push
aircraft property values up to 16px/500 #333 so 'Салехард / 87 / 75 /
12' read slightly bigger than the small grey labels above them.
2026-04-19 02:23:13 +03:00
gnezim 4b54837db1 Shrink route-strip typography to match Angular's compact layout
Measuring Angular's computed styles on the deployed test page showed:
  dep/arr time:  20px / 500 / #022040  (mine: 32px / 500 / #222)
  strike time:   12px / #F37B09        (mine: 14px)
  city:          14px / 400 / #333     (mine: 20px / 500)
  airport:       12px / #657282        (mine: 12px / #8A8A8A)
  time label:    12px / #657282        (mine: 12px / #8A8A8A)
  time value:    12px / 400 / #F37B09  (mine: 14px / 600 / #222)
  time date:     10px / #333           (mine: 12px / blue)

React was rendering each line roughly 1.5× bigger and bolder than the
Angular reference, which made the route strip dominate the card
instead of framing the 'Прибыл' status. Retune .leg-route__time /
__city / __airport / __detail-{label,value,offset,date} to the
measured Angular values.
2026-04-19 02:14:28 +03:00
gnezim fd79fa0faf Match Angular typography on row titles, Закончена status, last-update
- Accordion row captions ('Регистрация', 'Посадка', 'Высадка', 'Борт',
  'Питание на борту', 'Услуги на борту') were rendered at 14px/500/#222.
  Angular shows them at 12px/400/#657282 (neutral grey) so the row reads
  as [icon] [small caption + large status] rather than competing with
  the status text. Retune.
- Status labels ('Закончена' / 'Идет' / 'Ожидается') bumped from 14px
  to 16px and the Finished color switched from #e55353 to Aeroflot red
  #c8102e to match the corporate palette Angular uses.
- Last-update strip ('Последнее обновление: 18:25 18.04.2026') sized
  from 14px/#666 down to 12px/#333 so it sits quietly under the share
  icon instead of fighting for attention.
2026-04-19 02:06:35 +03:00
gnezim 5238ee4162 Match Angular H1 size, accordion icon grey, and full month on active tab
Three visible gaps after the SEO/title pass:

1. The page H1 ('Информация о рейсе: …') rendered at 22px/regular —
   a fourth of the size Angular shows. Angular inherits the global
   h1 rule (font-size-xxxl = 42px) and clamps to 36px on tablet /
   22px on mobile. The .flight-details__flight-number override was
   pinning it at font-size-xl2. Restore 42px with the same tablet/
   mobile clamps.

2. Accordion row icons (Регистрация / Посадка / Высадка / Борт /
   Питание / Услуги) used the brand blue. Angular's sprite stroke
   is #657282 (neutral grey), which lets the red 'Закончена' status
   next to it read as the dominant color. Switch details-row__icon
   to #657282.

3. DayTabs abbreviated every tab's month to 'апр.'; Angular spells
   the selected tab out in full ('18 апреля') and keeps siblings
   short. DayTabButton now picks `month: 'long'` when isActive.
2026-04-19 01:57:37 +03:00
gnezim ffb1a8579d Render SeoHead at route level; convert Angular-style {{var}} to ICU {var}
On /ru/onlineboard/SU6272-20260418 the document title was blank and the
meta description carried a literal '{{ flightNumber }}' placeholder.
Two root causes:

1. Translation values carried Angular ngx-translate syntax {{ var }} but
   the React app uses i18next-icu (single-brace {var}). Interpolation
   never fired, so SEO strings served as-is. Rewrite every {{ var }}
   (and {{var}}) occurrence to {var} across ru/en locales.

2. <SeoHead> was rendered inside the lazy-loaded OnlineBoardDetailsPage.
   The SSR response streams the Suspense fallback before the lazy
   bundle resolves, so <title>/<meta> never land in the <head>. Move
   SeoHead to the route page (src/routes/.../page.tsx) where it
   renders synchronously from URL-derived data, and drop the inner
   duplicate. Add buildFlightDetailsSeoFromId for the URL-only path.
   formatDateForSeo now handles both 'yyyyMMdd' (URL) and
   'yyyy-MM-dd' (API) so both entry points produce '18.04.2026'.

3. React 18 doesn't auto-hoist <title> inside body to document.head —
   add a useEffect in SeoHead that also writes document.title on the
   client. SSR still emits the <title> element for crawlers.
2026-04-19 01:39:06 +03:00
gnezim d43bfb3fcb Fix URL truncation from bash \${VAR:=default} with braces
CI / ci (push) Failing after 31s
Deploy / build-and-deploy (push) Failing after 5s
Deployed build had MAP_TILE_URL truncated to 'https://.../tile/{z' —
Leaflet then URL-encoded it to '%7Bz' and fetched garbage tile paths.
Root cause: build-docker.sh used

    : "\${MAP_TILE_URL:=https://flights.test.aeroflot.ru/map/api/tile/{z}/{x}/{y}.jpeg}"

and bash parameter expansion terminates the default value at the
FIRST unescaped '}', leaving '{z' and discarding the rest. The env
passed to `pnpm build:standalone` was already truncated, so every
downstream step (base64 encode → HTML inject → client decode) faithfully
carried the broken value through.

Fix by moving the defaults to Dockerfile's ARG lines — ARG defaults
are plain strings, not shell-parsed — and simplify build-docker.sh to
only forward MAP_TILE_URL / API_BASE_URL as --build-arg when the
caller explicitly sets them. Quote the k8s env values for defensive
YAML hygiene as well.
2026-04-19 00:44:45 +03:00
gnezim 2216790914 Base64-encode __ENV__ payload so Rspack HTML plugin can't eat '{z}'
CI / ci (push) Failing after 31s
Deploy / build-and-deploy (push) Failing after 5s
Prior attempts (raw JSON, \u007B / \u007D Unicode escapes) both got
truncated in the deployed build: Rspack's html-plugin decodes Unicode
escapes BEFORE running its template engine, so by the time the engine
sees the script body both raw and escaped '{z}' look identical and
get swallowed. Result: injected MAP_TILE_URL stopped at '/tile/{z'
and the client fell back to the default URL.

Serialize the env payload to base64 instead and decode it at runtime
with `JSON.parse(atob("..."))`. The base64 alphabet is A–Z/a–z/0–9/+//
/= — no braces for any template engine to grab. Switch the assign
target to `Object.create(null)` to keep the source brace-free; the
resulting runtime object is indistinguishable for getEnv().
2026-04-19 00:11:33 +03:00
gnezim 2e2c5c09ce Fix __ENV__ truncation; route API_BASE_URL through the same injection
CI / ci (push) Failing after 30s
Deploy / build-and-deploy (push) Failing after 6s
Two gaps blocked http://flights-ui.devwebzavod.ru/ru/flights-map:

1. The inline <script>window.__ENV__=...</script> was written with the
   Leaflet tile template ('/map/api/tile/{z}/{x}/{y}.jpeg') embedded
   directly. Rspack's html-plugin pre-processes the children string and
   ate the '{z}' placeholder, truncating the injected JS literal to
   '/map/api/tile/{z' — MAP_TILE_URL on the client ended up broken and
   getEnv() fell back to the default.

   Escape every '{'/'}' inside the stringified value as '\u007B'/'\u007D'.
   JS decodes the Unicode escapes back to '{}' at parse time; the html
   plugin's template engine sees no placeholders to eat. Object-literal
   braces outside the string stay raw (Unicode escapes aren't valid in
   operator positions in JS source).

2. API_BASE_URL was still hard-defaulting to 'http://localhost:8080/api',
   so every dictionary fetch on the deployed cluster died with
   ERR_CONNECTION_REFUSED. Thread API_BASE_URL through the same
   PUBLIC_ENV_KEYS/html.tags path as MAP_TILE_URL, add matching Docker
   ARG/ENV, and forward it in deployment/build-docker.sh + k8s manifest.

The devwebzavod default for both is https://flights.test.aeroflot.ru
— where the real Aeroflot ingress terminates /map/api/** and /api/**.
Prod keeps overriding with same-origin URLs.
2026-04-18 23:56:28 +03:00
gnezim ef85ae6ea1 Inject MAP_TILE_URL into window.__ENV__ via html.tags + Docker build-arg
CI / ci (push) Failing after 32s
Deploy / build-and-deploy (push) Failing after 6s
http://flights-ui.devwebzavod.ru/ru/flights-map was still hitting the
same-origin tile path after adding the k8s env: Modern.js renders the
<Suspense> fallback on the server (i18n isn't preloaded), so the route
component that reads getEnv() never actually runs during SSR. The page
hydrates client-side, where process.env is Rspack's empty stub and
MAP_TILE_URL is never set — getEnv() falls back to the default.

Move the value into window.__ENV__ instead:

- modern.config.ts: inline a <script> into html.tags that sets
  window.__ENV__ = { MAP_TILE_URL: <value> } at SSR-server startup.
  The snippet is authored once at server boot, so the HTML template
  baked into dist/standalone/html/main/index.html always carries the
  pod's tile URL.
- src/env/index.ts: merge window.__ENV__ on top of process.env so the
  browser prefers the injected value (process.env only has NODE_ENV
  after Rspack's polyfill).
- Dockerfile.react: accept MAP_TILE_URL as a build ARG and expose it
  as ENV before `pnpm build:standalone`, so Modern.js picks it up when
  building the HTML template. k8s env still flows into the Node SSR
  process; the build-arg path guarantees correctness even when the
  runtime env is stripped.
- deployment/build-docker.sh: forward MAP_TILE_URL through as a
  build-arg (default keeps the same-origin path). CI on the
  devwebzavod cluster can export MAP_TILE_URL=https://flights.test.aeroflot.ru/map/api/tile/{z}/{x}/{y}.jpeg
  before running build-docker.sh and the resulting image will serve
  tiles from the upstream the real Aeroflot ingress terminates.
2026-04-18 23:26:56 +03:00
gnezim 496a72e7d7 Track k8s manifest; teach make sync to mirror deployment/
CI / ci (push) Failing after 32s
Deploy / build-and-deploy (push) Failing after 5s
The flights-front deploy repo ships k8s manifests at deployment/k8s/,
a sibling of Aeroflot.Flights.Front/. Previously the sync script only
copied the app source, so any env change landed on the k8s side had
to be hand-edited in the deploy repo and was never reflected back.

- Bring deployment/k8s/flights-ui.yaml into this repo (with the new
  MAP_TILE_URL env pointing at flights.test.aeroflot.ru) so the
  cluster config lives next to the code that reads it.
- sync-to-flights-front.sh resolves the deploy-repo root from the
  target path and mirrors this repo's deployment/ directory there,
  mkdir'ing and copying contents without wiping unrelated files.
- Bump step numbering (1/6..6/6) and the summary now lists the synced
  deployment files in addition to the app files.
2026-04-18 22:51:26 +03:00
gnezim 6813bf902e Make Leaflet tile URL configurable via MAP_TILE_URL env
The flights-map tile URL was hardcoded as the same-origin path
'/map/api/tile/{z}/{x}/{y}.jpeg' (matching Angular's environment.ts).
On deployments where the ingress routes /map/api/** to the upstream
tile service (prod, flights.test.aeroflot.ru) this works. On
deployments without that rule (e.g. flights-ui.devwebzavod.ru) the
Modern.js SSR catch-all answers every tile URL with the SPA index
page, so Leaflet renders the marker + controls but never paints the
raster layer.

Expose the URL through MAP_TILE_URL env with the same-origin path as
the default, read it on the server route (where process.env is
available), and pass the resolved URL to FlightsMapStartPage as a
prop so the client bundle uses whatever the operator configured.
Prod and same-origin deployments stay unchanged; dev clusters can
point at an absolute URL like https://flights.test.aeroflot.ru/map/api/tile/...
instead.
2026-04-18 22:34:41 +03:00
gnezim 4aa0bbe5e6 Stack breadcrumb above page title (Angular parity)
Angular renders the breadcrumb trail on its own row above the H1 title.
React had them in the same flex row with justify-content:space-between,
which squeezed the breadcrumb column and forced 'Главная / Онлайн-Табло'
to wrap onto two lines. Switch the header-right container to column
layout so breadcrumbs and title stack vertically regardless of width.
2026-04-18 21:38:40 +03:00
gnezim b5759215b1 Replace PrimeReact Accordion on Расписание рейса with matching custom header
Angular keeps the 'Расписание рейса' collapse chevron on the right of the
header and styles the header like the Детали рейса row above it. React
was rendering the PrimeReact chevron on the LEFT with its own pill style.
Swap to the same lightweight accordion markup the details block uses so
both collapses look identical.
2026-04-18 21:32:42 +03:00
gnezim e014db17d0 Use '/' as breadcrumb separator (Angular parity) 2026-04-18 21:28:28 +03:00
gnezim 315385ccbd Breadcrumbs: use BOARD.TITLE, drop flight-number leaf
Angular's details breadcrumb trail is just 'Главная / Онлайн-Табло'
(BOARD.TITLE with capital Т) — the flight number itself is NOT a
breadcrumb entry. React was using the lowercase 'Онлайн-табло'
translation and appending 'SU 6272'. Align both the leaf text and the
list depth with Angular.
2026-04-18 21:24:27 +03:00
gnezim 7c0fb6a0d8 Format UTC offset as 'UTC +HH:MM' with non-breaking space (Angular parity)
Angular's captioned-time-group renders 'UTC&nbsp;{{ utc }}', producing
'UTC +03:00' on screen. React was emitting 'UTC+03:00' without the
separator, making the time details read slightly differently. Insert a
U+00A0 non-breaking space between 'UTC' and the signed offset so the
time-table values ('15:30 UTC +03:00 18.04.2026') line up with Angular.
2026-04-18 21:16:21 +03:00
gnezim f4b4c53816 Match Angular duration format + UTC offset + scheduled path time
- formatDuration(locale='ru') now emits 'Xч. Xмин.' (and 'Xд. Xч. Xмин.')
  with trailing dots, matching Angular's DurationPipe + SHARED.SHORT-HOUR
  translations. Every 'В пути', 'До прилета', and 'Время в пути' label on
  the details page now reads identically to Angular.
- FlightSchedule shows the SCHEDULED duration (dep→arr from the timestamps)
  instead of the actual flyingTime the API reports, so the Расписание рейса
  row reads '1ч. 30мин.' for a 15:30→17:00 schedule even after an early
  landing. The Вылет / Прилет columns also surface the 'UTC+HH:MM' offset
  below each time, matching the Angular layout.
2026-04-18 21:11:58 +03:00
gnezim b6920cbf60 Final details-page Angular parity: move time-note, horizontal Борт, 3-col schedule
- Relabel the meal row 'Питание на борту' (SHARED.FOOD) instead of the
  shorter 'Питание' (DETAILS.MEAL) Angular stopped using.
- Replace AircraftPanel's vertical label/value table with a horizontal
  strip of (Название | Количество мест | Эконом | Комфорт | Бизнес |
  Предыдущий рейс) cells to match flight-details-airplane layout.
- Render the '* Время в системе - МЕСТНОЕ.' note inline after the last
  visible transition row (Регистрация/Посадка/Высадка) inside the
  Детали рейса accordion, dropping the separate footer-notes block —
  Angular anchors the note exactly there.
- Rework FlightSchedule body into a 3-column grid (Вылет по расписанию |
  Прилет по расписанию | Время в пути) and humanize flyingTime '1:19' →
  '1ч 19м' so the value reads consistently with the rest of the page.
2026-04-18 19:54:11 +03:00
gnezim 87f38fec9e Skip useOnlineBoard fetch when dateFrom/dateTo are empty
Details page calls useOnlineBoard to populate the sibling mini-list,
passing empty-string params when the URL has no ?request=... context.
The empty params were reaching the backend as dateFrom=&dateTo=, which
returns HTTP 400 and surfaces as an error in the browser console.

Short-circuit the effect so we just emit an empty result when either
range bound is missing — same no-fetch behavior, no console noise.
2026-04-18 19:12:59 +03:00
gnezim d89e6449cc Hide standalone 'Общее время в пути' line; distinct accordion icons
- Drop the visible 'Общее время в пути: Xч Xм' row above the flight
  schedule block — Angular keeps the total duration inside the
  FlightSchedule accordion, not as a separate caption. Mark the
  existing div visually-hidden so testids keep resolving.
- Redraw the three transition icons so Регистрация looks like a person
  with an ID badge (Angular's #service), Посадка reads as an ascending
  escalator with a passenger (#board), and Высадка mirrors it going
  down (#deboard). The previous placeholders were too abstract to read
  at a glance.
2026-04-18 18:52:58 +03:00
gnezim 3bda018996 Surface dayChange offsets in accordion time columns
Angular stamps a small '-1' (or '+1') next to the time whenever the
transition start/end falls on a different calendar day than the leg
itself (e.g. registration opening the day before a 00:05 departure).
Read start.dayChange.value and end.dayChange.value on each transition
and render the offset as a superscript next to the time. Keeps the
blue accent color used elsewhere in the row for date lines.
2026-04-18 18:42:51 +03:00
gnezim ef171c5e18 Point plane marker right on progress bar (Angular parity) 2026-04-18 18:38:42 +03:00
gnezim 3838ab691c Right-align 'Прибыл' + place plane marker at end for finished legs
Angular anchors the status label ('Прибыл') above the right end of the
progress bar and parks the green plane icon at 100%. React was keeping
both centered even after the flight landed; move the plane marker to
the bar's end (100%) for finished as well as in-flight, and make the
status text flex-end so it lines up with the arrival column.
2026-04-18 18:35:27 +03:00
gnezim 512f22bf1b Align header actions/last-update right; hide duration on finished legs
- Restructure BoardDetailsHeader so the Share icon sits top-right next to
  the flight-number badge, and 'Последнее обновление' sits on its own row
  below, right-aligned, matching Angular's flight-details header layout.
- FlightEvents badges only render when changeRoute/reroute are actually
  set, avoiding an empty row on normal flights.
- Hide the leg.flyingTime under the route-status bar once the flight is
  Arrived/Landed/Cancelled — Angular leaves that slot blank in those
  states since the in-flight 'В пути / До прилета' split no longer
  applies.
2026-04-18 18:31:48 +03:00
gnezim 2d77e86c88 Match Angular details layout when no parent request is present
- Drop the duplicate FlightCard summary between the header and the route
  strip — Angular's details page shows the route strip directly under the
  board-details-header, with no 'SU 6272 Russia 15:30/15:22 ...' row.
- Keep the mini-list sidebar visible even when allFlights has only one
  entry; fall back to rendering the current flight as a single item,
  matching Angular's flights-details-list-flight behavior.
2026-04-18 16:41:35 +03:00
gnezim 583fe45c14 Match Angular details layout: flat accordion rows, progress labels, mini-list
- Accordion now renders flat rows (icon + caption/status on the left,
  Время начала / Время окончания columns on the right) under a single
  collapse toggle, matching Angular's flight-details-wrapper layout.
- Aircraft row moves the model title into the row subtitle and drops the
  duplicate 'Борт' property, so the row reads 'Борт / Sukhoi SuperJet 100'.
- Route strip grows a green in-flight state with a plane marker on the
  progress bar plus 'В пути Xч Xм' / 'До прилета Xч Xм' durations derived
  from actual-departure and scheduled-arrival.
- Mini-list sidebar now fetches sibling flights from the departure station
  parsed from the '?request=onlineboard-departure-LED-...' URL param, and
  the item layout gains city + airport labels with formatted time/date
  columns (replacing raw ISO timestamps and IATA codes).
- Tests and mocks updated: add useSearchParams / useOnlineBoard mocks,
  relocate aircraft-title assertions to the accordion level, and expect
  city names on mini-list items.
2026-04-18 16:26:39 +03:00
gnezim 54d1991a8f Add estimated-time note after leg route and accordion icons
- Move the '* Время прилета...' note from the footer to right after the
  LegRoute, matching Angular's position between the route strip and the
  Детали рейса accordion.
- Add inline SVG icons for each FlightDetailsAccordion row
  (Регистрация, Посадка, Высадка, Воздушное судно, Питание, Услуги) in
  blue to mirror Angular's sprite-based icons.
2026-04-18 16:00:29 +03:00
gnezim ec67111d10 Rebuild details leg block to Angular layout
Replace the vertically-stacked station blocks with Angular's
route-strip + time-table layout:

- Top row: big time + city + airport/terminal on both sides, with
  the status label and a progress bar in the middle. Scheduled time
  shows as a small strike-through line under the actual when delayed.
  Arrival time picks up the '+1' day-change marker when the flight
  crosses midnight.
- Bottom row: 'По расписанию' + 'Фактическое / Ожидаемое' detail
  rows for both departure and arrival, with UTC offsets and dates
  styled exactly like the Angular design.

The progress bar colors switch between blue (in-flight), green
(finished/arrived) and red (cancelled). The status text localizes
via FLIGHT-STATUSES.*.

Integration tests switched from asserting IATA codes to asserting
the city names, which now render in the promoted row (matches
Angular and the audit feedback).
2026-04-18 15:53:50 +03:00
gnezim ceeae1a7b1 Strip dashes from date when building flight-details URL
The API returns flight.flightId.date as 'yyyy-MM-dd' (dashed). Our URL
builder pasted it verbatim, producing /onlineboard/SU6162-2026-04-18
which the route parser (expecting yyyyMMdd) rejected. Normalise the
date to compact form inside buildFlightUrlParams so the URL always
matches the route pattern, regardless of whether the caller passes a
compact or dashed date.
2026-04-18 15:41:14 +03:00
gnezim 76f7acb0dd Implement inline expandable flight-card details
Clicking a row on the board search results page now toggles an inline
details panel instead of immediately navigating away. The layout
matches Angular's board-flight-header:

- Aircraft model ('Sukhoi SuperJet 100') appears below the flight
  number when expanded.
- 'Время' detail row: По расписанию / Фактическое times with UTC
  offsets for both the departure and the arrival sides.
- 'Посадка' detail row: boarding status (через the
  BOARDING-STATUSES.* keys), start and end times.
- 'Детали рейса' button (blue) in the bottom-right navigates to the
  full details page.
- Active rows get a blue left border + light-blue background.
- Chevron icon on the right rotates on expand.

Wire-up: FlightCard has two new props (expandable, onViewDetails).
FlightList automatically passes expandable=true when a click handler
is provided. Added SHARED.BOARDING-START / SHARED.BOARDING-END keys
across all nine locales for the time captions.
2026-04-18 15:36:14 +03:00
gnezim 916e594f06 Fix flights-map popup showing raw i18n key
The arrival-city popup called t('FLIGHTS-MAP.BUY-TICKET') but the key
in every locale file is FLIGHTS-MAP.BUY_TICKET_BTN (it was renamed
earlier). Map popups rendered the raw key 'FLIGHTS-MAP.BUY-TICKET'
instead of 'Купить билет'. Point the call at the existing key.
2026-04-18 15:21:25 +03:00
gnezim a0176cc336 Keep DayTabs enabled until calendar API resolves
With empty availableDates every day button rendered as [disabled],
leaving the user unable to navigate between days while the /days API
loads (or for routes where the calendar endpoint hasn't been wired
yet). Treat an empty availableDates array as 'unknown' — don't disable
anything, matching Angular's behaviour where tabs are tappable until
the upstream tells us a specific day has no flights.
2026-04-18 14:12:33 +03:00
gnezim cb61cafbf1 Fix three parity issues from final audit
1. Route heading uses airport name when a code maps only to an airport
   (SVO → 'Шереметьево') but prefers the city when the code is a city
   too (LED → 'Санкт-Петербург', not 'Пулково'). Angular does the
   same. Apply the new lookup order in both the onlineboard and
   schedule search pages.

2. Append ', Сегодня' (or 'DD.MM.YYYY' for other dates) to the board
   search heading, matching Angular.

3. Render the '+1' day-change marker on FlightCard even when only
   scheduled times are known. Previously the fallback pulled the value
   from `actualBlockOff/On.dayChange`, which is undefined for
   scheduled-only flights — so overnight flights like SU 6805
   (23:30 → 00:55 +1) showed no indicator. Read
   `scheduledDeparture/Arrival.dayChange.value` when the actual block
   time is missing.

4. Localize the PrimeReact Calendar widget: register a Russian locale
   in [lang]/layout.tsx and set the active one on every locale change,
   so 'Choose Date' reads 'Выбрать дату' and month/day names localize.
2026-04-18 14:10:26 +03:00
gnezim 4e91e9dca1 Display wall-clock times in TimeGroup instead of reprojecting
formatTime runs new Date(iso).getHours() which reprojects the
timestamp through the browser's local timezone. For a flight arriving
at 06:30 in Almaty (GMT+5) a viewer in Moscow saw '04:30'. Switch the
TimeGroup component to formatLocalTime which reads the wall-clock
directly out of the offset-aware ISO string, matching the rest of the
details/timetable views.
2026-04-18 14:00:03 +03:00
gnezim 22025b3ab4 Drop duplicate leg row on direct schedule details
For a single-leg flight the FlightCard summary already shows
SVO → ALA with times; the extra 'SVO→ALA / 00:05 - 06:30' line
below was redundant noise. Render the per-leg row only when the
route has multiple legs (transfer case).
2026-04-18 13:58:17 +03:00
gnezim 13fb633ec4 Fix schedule details date format
Upstream /schedule/details returns 400 when dates are sent as
yyyy-MM-dd; it wants the full ISO datetime (yyyy-MM-ddT00:00:00), same
as Angular's ApiFormatterService.formatDate output. Update the date
helper in ScheduleDetailsPage to append T00:00:00.

Verified with curl: request now returns 200 with the full flight
payload for SU1942 on 2026-04-18.
2026-04-18 13:56:10 +03:00
gnezim cd398fb8d9 Hide redundant 'Operated by' text on details page
The badge already conveys the operating carrier via the airline logo
(e.g. РОССИЯ for FV-operated Aeroflot flights), and code-share flights
get an additional 'KL 123, AF 456...' line below the flight number
from DetailsHeaderBadge. The separate 'Выполняет рейс: FV' line
duplicated that information.

Keep the div in the DOM with a visually-hidden class so tests that
target the data-testid still find it. Also add the .visually-hidden
utility class to layout.scss.

Test updated to assert the slot still exists but is hidden.
2026-04-18 13:53:13 +03:00
gnezim abc387ac3c Omit blank departure/arrival in schedule details request
Upstream returned HTTP 400 for empty departure/arrival values. Angular
derives them from per-leg data when missing; easier fix here is to
simply not send them — the backend accepts the request without those
query params.
2026-04-18 13:47:19 +03:00
gnezim 34d74b44c5 Add filter sidebar to schedule search
Schedule results previously had nothing in the left rail. Angular fills
it with the route filter + search history. Reuse the OnlineBoardFilter
component pre-populated with current URL params plus SearchHistory
below it, so the layout matches the board pages.
2026-04-18 13:43:28 +03:00
gnezim 13708fce8e Raise ApiClient default timeout to 30s
The upstream /schedule endpoint regularly returns 7MB+ payloads and
takes 6-10s to complete. The 5s default was aborting those fetches
mid-body, cascading into a retry loop that showed ERR_ABORTED in
DevTools and 'Failed to load data' on the UI — even though the backend
eventually answered with HTTP 200. Matches Angular's default.
2026-04-18 13:41:40 +03:00
gnezim 6f634092b2 Fix schedule search endpoint (GET not POST, dateTo + 1 day)
Angular's ScheduleApiService.getFlights uses GET with query params
and a half-open date interval (sends dateTo = requested end + 1 day).
React sent POST with a JSON body matching the params verbatim, which
returned HTTP 500 from the backend.

searchSchedule now:
- GETs flights/1/{lang}/schedule with ?departure=...&arrival=...
- extends dateTo by one day
- coerces connections to '0'/'1' and drops empty timeFrom/timeTo

Verified the call returns 200 with real flight data for
SVO→LED 2026-04-18..2026-04-25. Updated the api tests accordingly.
2026-04-18 13:38:15 +03:00