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).