release-verify.yml: three additions, all targeting the webzavod URL
(no gnerim.ru in this workflow — release-verify e2e runs against the
customer's deployed environment, not our internal preview).
1. Add /etc/hosts entry — flights-ui.devwebzavod.ru has no public DNS.
Operator hosts resolve it via local /etc/hosts to 46.235.186.67.
Without mirroring that on the runner every probe fails with
"Could not resolve host" (runs 537 + 539).
2. Diagnose customer URL reachability — mirrors ci-deploy's tunnel
probe but on the customer URL: surfaces broken /api wiring before
the e2e suite spends 30 minutes hitting it.
3. Pre-warm /api cache — same rationale as ci-deploy: the four
dictionary endpoints are read on every page load, and the upstream
WAF rate-limits per source IP. Warm them once with sleeps so the
e2e suite hits the customer's nginx cache, not the upstream WAF.
schedule-route-buy-button.spec.ts: rewritten for ci-deploy run 538.
The previous version hard-coded the first card on a URL that included
today, hitting the "today's earliest flight is < 2h out, buy button
hides" edge case. Now scans up to 8 cards looking for the buy button
on a fully-future calendar week — proves the strip + button surface
without depending on which specific rows are buyable on the day.
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
Two design pivots discovered during Phase B prerequisites:
Routing: Replace static-route + NAT plan with persistent ssh -L tunnel
from pve-201 to webzavod (deployment/systemd/flights-tim-tunnel.service).
nginx proxies /api/ and /map/api/ to https://127.0.0.1:8443 with SNI/Host
overrides so cert validation still targets the real hostname. No webzavod
kernel changes (no ip_forward/MASQUERADE), no /etc/hosts pin needed.
Workflow B: Drop Jenkins trigger/poll automation (operator lacks Jenkins
job-configure access and user API token access). release.yml now stops
after MR merge with a Telegram message containing the Jenkins job URL.
release-verify.yml (new, workflow_dispatch only) runs the customer-URL
e2e suite once the operator has triggered Jenkins manually and it has
completed.
Other:
- SSR loopback port 8081 -> 3002 (8081 was taken by openwebui on pve-201)
- notify-telegram.sh skips cleanly when TG secrets unset (was: hard-fail)
- README + spec addendum cover the new prereqs and removed steps
The app normalises a short-form /en locale prefix to the BCP-47
form /en-en/ at the router layer, so asserting on the short form is
brittle. Assert a loose /en(-xx)?/onlineboard URL regex instead.
- 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.
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.
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).
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).
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.
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.
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.
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.
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.
Verifies the URL-driven time filter (e.g. -14001800 suffix)
restricts the rendered list and updates the slider label, plus the
slider→submit→URL pipeline persists the chosen range. No code
change required for TIRREDESIGN-11 — adding regression tests.