Match Angular's flight-actions layout — schedule rows now show the
orange Buy and outlined Status рейса buttons inline at the right edge
of the row instead of inside the expanded panel.
Companion markdown to the comparison-report/visual/report.html with
the same coverage matrix and per-page findings. Useful for git-based
review without serving the HTML.
Also adds AGENTS.md (subagent role definitions for future sessions)
and the modernjs-v3-upgrade plan stub from the earlier scoping.
The mobile day-select dropdown was rendering as an empty <select>
on detail pages where the calendar API hasn't shipped any usable
days for that view. The empty box took layout space and looked
broken. Match Angular: don't render the picker when there's
nothing to pick.
ScheduleStartPage now starts dateFrom/dateTo as null so the input
shows the `ДД.ММ.ГГГГ - ДД.ММ.ГГГГ` placeholder Angular ships
instead of pre-filling with the current week. The submit handler
defaults to current-week range when the user submits without
picking dates, preserving the legacy "find this week" UX.
Same pattern as the onlineboard date fix.
- ScheduleSearchPage: H1 now reads `Расписание по маршруту: …`
using SCHEDULE.SCHEDULE-BY-ROUTE — the existing onlineboard
variant was leaking through. Matches Angular's schedule-search
title-bar verbatim.
- DayGroupedFlightList: render a non-sortable column header bar
above the list with `Рейс / Авиакомпания, борт / Вылет / Время
в пути / Прилет`. Mirrors Angular's schedule-list-flight-header
column row. Sort arrows still TBD.
- New i18n keys: SCHEDULE.COL-FLIGHT, COL-AIRLINE, COL-DEPARTURE,
COL-DURATION, COL-ARRIVAL (RU + EN both filled).
Schedule-route mismatch now 11.99% (was 12.47% pre-heading fix).
Paint 3 known noise regions white in both screenshots before pixel-
matching:
- top-left ~200×90 (debug counter + orange `Тестовая версия` badge)
- top-right ~240×50 (build tag like `rc/2026-04-06`)
- bottom-right ~90×90 (chat-widget bubble)
These show only on the deployed Angular test env, not in the React
dev build, and were inflating every parity score by ~1-2pp.
Mismatch deltas vs prior run:
en-onlineboard-route 4.62% → 4.45%
flight-details 11.24% → 10.82%
mobile-flight-details 17.92% → 16.58%
mobile-onlineboard-start 20.37% → 18.66%
onlineboard-arrival 4.63% → 4.46%
onlineboard-departure 5.03% → 4.86%
onlineboard-route 4.78% → 4.60%
onlineboard-start 14.52% → 13.77%
schedule-route 12.47% → 11.87%
schedule-start 13.39% → 12.86%
flights-map 37.28% → 36.40%
- FlightCard: when the flight is multi-leg, render one OperatorLogo
per leg in the header so code-share / multi-carrier journeys show
both airline brands (Angular's `operator-logo-and-model x N` row).
Direct flights keep the single logo.
- FlightCard: add an orange "Купить" (buy ticket) link rendered next
to "Детали рейса" when the card is in the schedule context. Links
to aeroflot.ru's booking flow per Angular's flight-actions wiring.
- Reverted earlier per-leg flight-number stack — IFlightLeg in React
doesn't carry a per-leg flightId, so the parent SU number is the
authoritative label. The Angular dual-number stack belongs to the
ConnectingFlight shape (separate from MultiLeg) which the React
code already renders flat.
- flights-map: default departure to Москва (MOW) when geolocation
doesn't yield a city. Mirrors Angular which seeds the orange
marker on Moscow regardless of geo permission. Hook now has two
effects — a synchronous MOW fallback that fires once dictionaries
load, and the existing geo callback that may upgrade to a closer
city when permission is granted.
- Schedule: introduce DayGroupedFlightList. Buckets the flat result
list by scheduled-departure date and renders each group under a
`Воскресенье 19 Апреля`-style header (Intl-driven, weekday +
genitive month). Single-day result skips the grouping noise.
- Schedule: introduce WeekTabs. Replaces the daily DayTabs in the
schedule sticky-content with Monday-anchored 7-day windows like
`13 апр - 19 апр`, matching Angular's week-tabs component.
handleWeekChange recomputes both dateFrom (Monday) and dateTo
(Sunday) when the tab changes.
- Schedule: aircraft model now visible in the collapsed FlightCard
row when `direction === "schedule"` (Sukhoi SuperJet 100 / Airbus
A321 etc., per Angular's operator-logo-and-model column).
- FlightCard / FlightList: extend `direction` union with `"schedule"`.
Tests updated: useGeolocationDefault tests now assert the MOW
fallback fires when permission is denied / API missing / arrival
already set (was previously expected to no-op).
- OperatorLogo: accept BCP-47 codes (`ru-ru`) by trimming to first 2
chars before picking the en/ru asset variant. Fixes the Russian
flight-details page rendering ROSSIYA (Latin) instead of РОССИЯ.
- FlightCard / FlightList: thread `direction` from the search page so
arrival results show Высадка (deboarding) instead of Посадка
(boarding) — Angular parity. The arrival side reads from
arrivalLeg.transition.deboarding when direction === 'arrival'.
- OnlineBoardFilter:
- Дата рейса starts blank with `ДД.ММ.ГГГГ` placeholder; submit
handler defaults to today on empty.
- Город вылета / Город прилета placeholders flip to
`Все направления` when the opposite-direction field is filled.
- Filter content row now flows with $space-l vertical gap to match
Angular's accordion-content rhythm (was ~6 px tighter).
- FlightsMiniList: `display: none` on mobile. Avoids the duplicate
summary card that was floating above the main details on small
viewports — Angular hides the sidebar mini-list there.
- FlightsMap calendar trigger: override PrimeReact's filled-blue
button to a transparent outline so it reads as a glyph (matches
Angular's outline calendar icon).
Pixel-mismatch results (re-diffed via scripts/visual-diff.mjs):
en-onlineboard-route 5.50% → 4.62%
onlineboard-arrival 5.53% → 4.63%
onlineboard-departure 5.92% → 5.03%
onlineboard-route 5.16% → 4.78%
mobile-onlineboard-start 23.51% → 20.37%
mobile-flight-details 18.82% → 17.92%
flight-details carrier-logo verified visually; pixel
count unchanged (height delta dominates)
onlineboard-start 14.56% → 14.52%
Larger remaining mismatches (schedule-route 14%, flights-map 34%,
flight-details 11%) are dominated by structural Angular features the
React port doesn't yet ship (day grouping, code-share bundling on
schedule; geo-driven origin marker on map; height-delta on details).
Tracked as P1 follow-ups in the comparison report.
Angular's LocalizationService reads `Country = baseHref[1..3]` and
`Language = baseHref[4..6]` — both halves are the same 2-letter
language code (`/ru-ru/`, `/en-en/`, `/zh-zh/`, …), confirmed by
the spec fixtures using `/en-en/onlineboard/...`. The previous
shipping codes mixed in IETF region codes (`en-us`, `ja-jp`, `ko-kr`,
`zh-cn`) which do not match the customer's URL surface.
Renamed:
en-us → en-en
ja-jp → ja-ja
ko-kr → ko-ko
zh-cn → zh-zh
The `LANGUAGE_TO_LOCALE_CODE` table now mirrors Angular exactly.
Resolver/hreflang tests + layout 404 message updated.
- URL surface now matches Angular: `/ru-ru/`, `/en-us/`, `/zh-cn/`, …
(BCP-47). Bare short codes still work — the [lang]/layout auto-
promotes them with a replace navigation. Internally everything that
needs the short language (i18n file lookup, API path segment,
Accept-Language header, dictionary `title[lang]` key, Intl
formatters) reads it through the new `useLocale()` hook, which
returns both `locale` (BCP-47) and `language` (short).
- ApiClient.locale is now mutable and is updated from the [lang]
layout whenever the URL locale changes — was hard-coded to "ru" in
the root layout before, so backend responses for /en/... still came
back in Russian. Cities / airports / flight statuses now arrive in
the active language.
- All 21 empty EN translation keys filled in (AIRPLANE.*, BOARD.
PREVIOUS-FLIGHT, SCHEDULE.FILE-NAME, SEO.SCHEDULE.*, SEO.FLIGHTS-
MAP.*, SHARED.FLIGHT-TRANSFER-PLURAL-*, SHARED.WEEK_FORMAT-WRONG)
so /en-us renders without falling back to raw keys.
- Added BOARD.LOAD-FAILED-TITLE / -MESSAGE keys (RU + EN) and removed
the three hardcoded Russian error strings from the search-page
error card.
- FlightStatus now reads `FLIGHT-STATUSES.{Status}` from i18n instead
of hardcoding the Russian labels.
- FlightCard's OperatorLogo now picks the en/ru carrier-logo variant
from `useLocale().language` instead of always passing "ru" — the
Aeroflot/Rossiya logos display in the active language where
variants exist.
- registerPrimeLocales(): all 9 supported languages get a PrimeReact
`addLocale` entry at module load (RU + EN hand-curated, others built
from Intl). Calendar/AutoComplete widgets switch with the URL.
- ErrorBoundary catches outside the i18n provider, so it now ships
its own minimal localised string table keyed off the URL locale —
no more "Something went wrong" leaking on the Russian site.
- Hreflang URLs now emit BCP-47 (`/en-us/...`) while `hreflang="en"`
stays the short Google-friendly form.
- Datetime helpers accept either short or BCP-47 locale (`isRussianLocale`)
so callers can pass through whatever the route hands them.
- Drop the React-only standalone /popular route (and its e2e
smoketest). Angular returns 404 for /ru-ru/popular; popular
requests are surfaced inline on onlineboard/schedule start pages
via PopularRequestsPanel (which stays). Matching the URL surface
is a contractual requirement for the MF remote.
- Replace ?tab/?departure/?arrival/?return query-string prefill on
the onlineboard and schedule start pages with a sessionStorage
transient slot. Mirrors Angular's OnlineBoardFiltersStateService /
ScheduleFiltersStateService cross-page singletons: URLs stay
clean of query strings, the start-page form still seeds itself
from a popular-request click, and a fresh page reload (which
bypasses the in-memory state in Angular) lands on a pristine form.
Same-page popular clicks remount the filter via key bump so the
useState initializers pick up the new prefill.
- SharePanel: fix wrong i18n key (SHARED.COPY → SHARE.COPY) and switch
to the brand-icon-on-top + translated-label layout that Angular uses
(renders as untranslated raw key + plain text list before).
- LastUpdate: stamp now reflects when the client received the data, not
the API record's mutation timestamp — Angular sets `flight.lastUpdate
= new Date()` in populate.logic.ts; we mirror that behavior so users
no longer see stale 'updated' values from cached API rows.
- FlightCard: keep operator logo + plane icon + status text on mobile
(previously hidden via display:none); regrid to 3-col layout so the
card mirrors Angular's mobile pattern. Boarding status row gains the
leading colour-coded dot Angular ships ('Уточняется' grey).
- OnlineBoardSearchPage: H1 for /flight/... search now reads
'Рейс: SU 6497, Сегодня' instead of 'Номер рейса: SU6497' (matches
Angular's title.service); add the '* Время в системе - МЕСТНОЕ.'
footer note Angular's <page-footer-notes> renders.
- FlightsMap filter: drop the React-only 'Найдите свой маршрут'
header; replace the horizontal swap glyph with vertical blue arrows
(Angular rotates the same SVG 90deg); add Leaflet city-tooltip
styling so labels render text-only with a white text-shadow halo
rather than as PrimeReact-default white pills.
- DayQuickPick: new mobile-only 3-day quick-pick row above the manual
date input on both onlineboard and flights-map filters, mirroring
Angular's calendar-input.component .calendar--mobile block. Uses
Intl.DateTimeFormat formatToParts to get the genitive month form.
Angular's sprite has the plane nose-right, but the inline 24×24 path
bundled with FlightStatus is nose-up. Transform-rotate matches the
Angular direction without swapping the SVG asset.
Angular's crumb trail ends at 'Онлайн-Табло'; the route description
lives only in the h1 below. React was repeating the heading as a third
crumb, doubling up the text.
Angular's expanded FlightCard bottom bar has a share button on the
left next to the 'Детали рейса' action on the right. Render an icon
button backed by /assets/img/share.svg with the same
justify-content: space-between layout. Click uses the Web Share API
when available and falls back to writing the current URL to the
clipboard.
Render the latest time on top (30px/light/#333) with the crossed-out
scheduled time below (12px/#f37b09/line-through), mirroring Angular's
'time' vs 'oldTime' pattern instead of the inverted order React had.
Drop the local .flight-card__time font-size override so TimeGroup owns
its own typography.
In the expanded panel, show both scheduled and latest time columns.
Angular falls back to estimatedBlockOff/On when actual is absent and
labels the column 'Ожидаемое' vs 'Фактическое' accordingly — mirror
that logic so flights with only an ETA surface it.
Drop the 8px margin below the day-tabs strip so the sticky card touches
the results frame, matching Angular's layout.
Add page-layout__scroll-overlay — a fixed 25px dark-blue strip across
the top of the viewport — so flight rows scrolling past the 20px-sticky
date row don't peek through the gap above it.
Realign DayTabs range to match Angular's boardSearchFrom=1 /
boardSearchTo=7 defaults (today-1 through today+7). Previous 2/14
window left today off-center in the 7-tab page.
Rebuild DayTabs to mirror Angular's flat single-line tab strip: one label
per tab ('17 апр.' siblings, '19 апреля' active), 48px tall, 7 tabs per
page, brand blue label on light-blue background with white active cell.
Single Intl.DateTimeFormat call keeps Russian months in genitive case.
Drop the 60px sticky-content offset to 20px so the date strip aligns
with the left filter column (both already sticky at top:20px).
Correct SEO.FLIGHTS_MAP translation key to SEO.FLIGHTS-MAP.MAIN — the
underscored path never existed in the locale files, so the browser tab
title on /{lang}/flights-map fell back to the raw key.
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.
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.
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.
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.
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).
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.
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.
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.
- 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.
- 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.
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.
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.
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.
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().
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.
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.
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.
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.
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.
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.
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.
Angular's captioned-time-group renders 'UTC {{ 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.
- 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.
- 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.
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.
- 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.
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.