- Switch from @module-federation/modern-js to @module-federation/modern-js-v3 (v3 compatible) - Rename App.tsx to AppProviders.tsx to avoid hasApp detection that blocks nested route discovery - Move runtime.router config from modern.config.ts to modern.runtime.ts (v3 API) - Fix PostCSS config type annotation - Enable streaming SSR mode successfully
20 KiB
Aeroflot Flights Web: Angular to React Migration -- Design Spec
Overview
Rewrite the Aeroflot Flights Web application from Angular 12 to React 18+, deployed as a Module Federation 2.0 remote micro-frontend. The React app runs standalone (own routing, own layout) and exposes features for the customer's host apps (Web, PWA) to consume via mf-manifest.json.
Technology Stack
| Component | Technology |
|---|---|
| Framework | ModernJS (SSR, streaming mode) |
| Bundler | Rspack (via Rsbuild) |
| Module Federation | MF 2.0 with mf-manifest.json |
| UI Library | React 18+ with Concurrent Mode |
| Component Library | PrimeReact (port of existing PrimeNG look) |
| State (server) | TanStack Query v5 |
| State (client) | Zustand |
| Styling | CSS Modules + postcss-prefix-selector |
| Maps | React-Leaflet |
| i18n | react-i18next (reuse existing JSON files) |
| Real-time | @microsoft/signalr |
| Analytics | analytics (David Wells) with plugins |
1. Project Structure & Module Federation
react-app/
+-- modern.config.ts
+-- module-federation.config.ts
+-- src/
| +-- entry.server.tsx
| +-- entry.client.tsx
| +-- App.tsx
| +-- routes/
| | +-- layout.tsx
| | +-- onlineboard/
| | | +-- page.tsx
| | | +-- flight.[params].tsx
| | | +-- departure.[params].tsx
| | | +-- arrival.[params].tsx
| | | +-- route.[params].tsx
| | | +-- [params].tsx
| | +-- schedule/
| | | +-- page.tsx
| | | +-- [params].tsx
| | +-- flights-map/
| | | +-- page.tsx
| | +-- error/
| | +-- 404.tsx
| +-- features/
| | +-- online-board/
| | | +-- components/
| | | +-- hooks/
| | | +-- services/
| | | +-- types.ts
| | | +-- index.ts
| | +-- schedule/
| | | +-- components/
| | | +-- hooks/
| | | +-- services/
| | | +-- types.ts
| | | +-- index.ts
| | +-- flights-map/
| | | +-- index.ts
| | +-- popular-requests/
| | +-- index.ts
| +-- shared/
| | +-- api/
| | +-- hooks/
| | +-- stores/
| | +-- types/
| | +-- utils/
| | +-- seo/
| | +-- analytics/
| +-- ui/
| | +-- calendar-input/
| | +-- card/
| | +-- date-tabs/
| | +-- time-selector/
| | +-- toggle-switch/
| | +-- icons/
| +-- i18n/
| | +-- config.ts
| | +-- locales/
| +-- styles/
| +-- theme/
| +-- global.module.css
+-- public/
+-- package.json
+-- tsconfig.json
Module Federation Configuration
// module-federation.config.ts
exposes: {
'./App': './src/App.tsx',
'./OnlineBoard': './src/features/online-board/index.ts',
'./Schedule': './src/features/schedule/index.ts',
'./FlightsMap': './src/features/flights-map/index.ts',
'./PopularRequests': './src/features/popular-requests/index.ts',
}
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
'@microsoft/signalr': { singleton: true, requiredVersion: '^9.0.0' },
}
Two Modes of Operation
- Standalone: Full React app with own routing -- used for development, testing, and direct access.
- Remote: Host apps import individual features or
./Appviamf-manifest.json. Each feature is a self-contained React component with props-based API.
Feature Isolation Rule
Features never import from each other. They only import from shared/, ui/, and i18n/. Each feature exposes a single entry component with well-defined props. This makes future MF extraction (breaking a feature into its own remote) a build/deployment change, not a code change.
2. Data Flow & State Architecture
Zustand Stores (client/UI state)
// Per-feature filter stores
useOnlineBoardFilters // flightNumber, suffix, timeRange, date, departure, arrival
useScheduleFilters // departure, arrival, dateRange, directOnly, withReturn
useFlightsMapFilters // departure
// App-level stores
useSettingsStore // environment config, feature flags, refresh intervals
useUserLocationStore // geolocation result
useSearchHistoryStore // persisted search history (localStorage)
TanStack Query (server state)
// Query keys: [feature, entity, params]
useFlightsQuery({ date, departure, arrival })
useFlightDetailsQuery({ flightNumber, date })
useFlightDaysQuery({ date, param, scope })
useScheduleQuery({ departure, arrival, dates })
useScheduleDaysQuery({ date, param })
useDestinationsQuery({ departure })
usePopularRequestsQuery()
TanStack Query replaces Angular's CacheService (response caching), manual _loading flags (loading/error states), and background refetching logic.
Data Flow
- User selects filters -> Zustand store updates
- URL updates to reflect filter state (URL is source of truth)
- Route component reads URL params -> passes to TanStack Query hook
- Query fetches from REST API, caches result
- SignalR subscribes to relevant hub events
- Real-time update arrives -> query invalidated -> silent background refetch -> UI updates
3. Routing & URL Structure
URL Patterns (unchanged from Angular)
/onlineboard/ # Start page
/onlineboard/flight/{flightNumber}-{flightDate} # Flight number search
/onlineboard/departure/{departure}-{date}-{from}{to} # Departure search
/onlineboard/arrival/{arrival}-{date}-{from}{to} # Arrival search
/onlineboard/route/{dep}-{arr}-{date}-{from}{to} # Route search
/onlineboard/{flightNumber}-{flightDate} # Flight details
/schedule/ # Start page
/schedule/{departure}-{arrival}-{dateRange} # Search results
/schedule/{flightNumber}-{flightDate} # Flight details
/flights-map/ # Map view (feature-flagged)
/error/404 # Not found
Language Handling
Language detected from: (1) URL prefix if host provides one (remote mode), (2) browser locale / stored preference (standalone mode), (3) configurable via props.
Language prefixes (/ru, /en, /es, /fr, /it, /ja, /ko, /zh, /de) redirect to /onlineboard.
URL <-> State Synchronization
URL is the source of truth for search state. useUrlParams() parses route params into typed filter objects. useNavigateWithParams() builds URL from filter state and navigates.
Route Validation
Wrapper components validate URL params before rendering (replaces Angular route guards):
isValidDate(date)isValidStation(code)-- 3-letter IATAisValidFlightNumber(num)-- 1-4 digits + optional letter suffixisValidTimeRange(from, to)
Invalid params redirect to /error/404.
Feature Flag Guard
flights-map route is gated by settingsStore.features.flightsMap. If disabled, redirects to /onlineboard.
Settings Resolution
Root layout fetches settings via TanStack Query before rendering any feature (replaces Angular's SettingsResolver).
4. SSR & SEO
SSR Strategy
ModernJS streaming SSR with Data Loader pattern (Remix-inspired):
- Each route defines a
page.data.tsexporting aloaderfunction that runs on the server only. - Loaders prefetch TanStack Query data with
queryClient.prefetchQuery()and returndehydrate(queryClient). - Page components wrap content in
<HydrationBoundary state={dehydratedState}>-- client picks up cached data without refetch. - New
QueryClientper request to prevent data leaks between users. staleTimeset to match cache TTL (10-30s) to prevent immediate client refetch after hydration.- Bot detection: ModernJS detects crawlers and serves full (non-streamed) HTML.
- Fallback: if SSR fails, ModernJS auto-falls back to CSR.
Head Management
React 19 native metadata hoisting -- no library needed. <title>, <meta>, <link> rendered anywhere in the component tree are automatically hoisted to <head>.
When running as MF remote: the remote exposes a metadata contract for the host to read during SSR.
JSON-LD Structured Data
schema.org/Flight with Airline and Airport sub-objects. For list pages, wrapped in ItemList.
Fields: flightNumber, airline (name, iataCode), departureAirport / arrivalAirport (name, iataCode), departureTime, arrivalTime (ISO 8601 with timezone), estimatedFlightDuration, departureTerminal, arrivalTerminal.
Rendered server-side via <script type="application/ld+json">.
OpenGraph
Per-page OG tags: og:title, og:description, og:url, og:type, og:locale, og:locale:alternate (for all 9 languages). Twitter Cards with summary_large_image.
Per-Page SEO
| Page | Title | Structured Data | noRobots |
|---|---|---|---|
| Board start | "Online Timetable -- Aeroflot" | None | No |
| Flight search results | "Flights {dep} -> {arr} -- {date}" | ItemList of Flight | No |
| Flight details | "Flight SU-{num}: {dep} -> {arr}" | Flight | Yes |
| Schedule start | "Flight Schedule -- Aeroflot" | None | No |
| Schedule results | "Schedule {dep} -- {arr}" | ItemList of Flight | No |
| Flights map | "Flight Map -- Aeroflot" | None | No |
5. Analytics, Logging & Monitoring
Analytics Abstraction
analytics library as unified dispatcher with custom plugins per provider. Page views tracked automatically on route changes via useLocation().
Per-System Integration
Yandex.Metrika: Modern-Yandex-Metrika with delay: 'onload' for LCP optimization. Client-only. SPA page views via ym(COUNTER_ID, 'hit', url). Goals via ym(COUNTER_ID, 'reachGoal', 'goalName', params).
Dynatrace (Klyuch-Astrom): Host app injects OneAgent -- remote does NOT load its own copy. OneAgent auto-captures XHR/fetch, DOM mutations, route changes. Custom business actions via dtrum API. TypeScript types via @dynatrace/dtrum-api-types.
Variocube (Variokub): Server-side variant resolution via Varioqub usersplit API in ModernJS loader. Variant resolved before render -- zero layout shift. Variant assignment cached per visitor cookie.
CTM: Async script injection. Re-trigger CallTrackingMetrics.swapNumbers() on route changes. SSR HTML contains original business number (correct for SEO).
Logging
Custom structured logger with batch shipping:
- Batch size: 50 entries, flush every 5 seconds
sendBeacononvisibilitychange(whenhidden) for reliable delivery on page unload- Pluggable format -- customer's log format spec TBD (will be provided by customer separately)
- Sampling: 100% errors, 100% warnings, 20% info in production
traceIdfor correlation with backend OpenTelemetry traces- No PII without explicit consent
Web Vitals
Google's web-vitals v5 library, client-only after hydration. Tracks: LCP, INP, CLS, FCP, TTFB. Reports via sendBeacon to /api/vitals.
6. Style Isolation & Theming
Three-Layer Isolation
Layer 1 -- CSS Modules: All custom components use .module.css files. Class names hashed at build time. Zero runtime cost, SSR-safe.
Layer 2 -- postcss-prefix-selector: All global/third-party CSS (PrimeReact) prefixed with .afl-flights. Every PrimeReact selector becomes .afl-flights .p-button, etc.
Layer 3 -- PrimeReact overlay containment: PrimeReactProvider with appendTo: 'self' globally. All overlays (dropdowns, dialogs, tooltips) render inside the scoped wrapper.
Boundary Reset
Explicitly set inherited properties on boundary element (NOT all: initial which breaks display, box-sizing, etc.):
.afl-flights {
font-family: 'Aeroflot Sans', sans-serif;
font-size: 16px;
line-height: 1.5;
color: var(--afl-text-color, #1a1a1a);
direction: ltr;
text-align: left;
isolation: isolate; /* New stacking context -- prevents z-index conflicts */
}
Responsive Layout: Container Queries
Container queries (not media queries) for all component-level responsiveness. A MF remote doesn't control the viewport -- it might be full-width or in a sidebar. Container queries respond to actual available space.
.afl-flights { container-type: inline-size; container-name: flights-root; }
@container flights-root (min-width: 768px) { ... }
@container flights-root (min-width: 1024px) { ... }
Nested containers for fine-grained responsiveness (e.g., .flightCard as its own container).
Media queries reserved only for: prefers-color-scheme, prefers-reduced-motion, print.
Browser support: 95%+ (Chrome 105+, Firefox 110+, Safari 16+).
Theming: Three-Tier CSS Custom Properties
Tier 1 -- Primitive tokens: --afl-blue-500, --afl-gray-100, etc.
Tier 2 -- Semantic tokens: --afl-primary, --afl-surface, --afl-text-color
Tier 3 -- Component tokens: --afl-btn-bg, --afl-card-bg, --afl-card-shadow
Host app overrides any tier by setting variables on .afl-flights. Theme variants via data-theme attribute (light, dark).
PrimeReact theme integration: map tokens to PrimeReact design token system (--p-primary-color: var(--afl-primary)).
ModernJS/Rspack Config
output.cssModules.localIdentName: '[local]--[hash:base64:5]'-- deterministic for SSR hydrationoutput.cssModules.namedExport: true-- better tree-shaking- Streaming SSR mode (required for MF with ModernJS)
7. SignalR Real-Time Integration
Connection Management
React Context + singleton HubConnection. Dynamic import('@microsoft/signalr') inside useEffect -- the library references XMLHttpRequest/WebSocket at module level, which crashes during SSR.
Reconnection Policy
Custom IRetryPolicy with exponential backoff (1s, 2s, 4s, 8s... capped at 30s), never gives up. The default policy stops after 4 attempts -- unacceptable for a 24/7 flight board.
Event Handling
useSignalREvent hook subscribes to hub events with a ref-based callback to avoid stale closures.
Feature-level hooks:
useFlightBoardRealtime(date, departure?, arrival?)-- subscribes toRefreshDate, invalidates flight board queryuseFlightDetailsRealtime(flightId)-- subscribes toRefresh, invalidates flight details query
Re-Subscribe After Reconnect
SignalR does not persist subscriptions across reconnects. A subscription registry in the provider tracks active subscriptions. onreconnected callback re-invokes all registered subscriptions.
Inactive Tab Handling
Chrome throttles background tabs, which can drop SignalR connections. Rely on withAutomaticReconnect (infinite retry) + TanStack Query's refetchOnWindowFocus: true. When user returns to tab: auto-reconnect fires, subscriptions re-registered, stale data refetched.
TanStack Query Config
Flight queries use staleTime: Infinity -- don't auto-refetch since all updates come via SignalR. refetchOnWindowFocus: true handles tab-return freshness.
Module Federation
Remote owns the connection. @microsoft/signalr marked singleton: true in MF shared config to avoid duplicate bundles.
Connection Status UI
Non-blocking banner: "Real-time updates paused. Reconnecting..." (reconnecting state), "Connection lost. Data may be outdated." (disconnected state).
8. Performance & Scaling
Target: 100 RPS
Three-Tier Caching
Client -> CDN (10s cache + 50s stale-while-revalidate)
| MISS
v
Redis (full-page HTML cache, 10-15s TTL)
| MISS
v
ModernJS SSR (streaming render)
| prefetch
v
TanStack Query server-side (API data cache, 10-30s)
| MISS
v
REST API
With caching, actual SSR renders happen once per 10-15 seconds per unique URL. 100 RPS is met by CDN/Redis alone.
CDN Headers
Cache-Control: public, s-maxage=10, stale-while-revalidate=50, max-age=0
Vary: Accept-Language
Code Splitting
Route-based splitting via ModernJS (not React.lazy() which does not work in SSR). Each route directory = separate chunk. MF remote loading handled by federation runtime.
Bundle Size Budget
| Asset | Budget (gzipped) |
|---|---|
| MF remote entry | < 50 KB |
| Total remote bundle | < 200 KB |
| Per-page JS | < 400 KB |
Optimization: individual PrimeReact imports, lazy-load React-Leaflet behind feature flag, MF 2.0 tree shaking with federationRuntime: 'hoisted', only share react, react-dom, @microsoft/signalr.
Node.js Scaling
- SSR render: ~50ms per request
- Single process: ~20 RPS
- Target: 2 containers x 4 cores = 8 processes -> ~160 RPS (60% headroom)
--max-old-space-size=1536(1.5 GB heap on 2 GB container)- PM2 cluster mode with
--max-memory-restart 1G
Geographic Distribution
App is fully stateless -- no server-side sessions, no sticky sessions. Deploy to multiple regions behind global load balancer. Redis per region for SSR cache.
Performance Monitoring
CI/CD: size-limit for bundle budget, Rspack performance.hints: 'error', Lighthouse CI for Web Vitals.
Production: SSR render time p50/p95/p99 (alert p95 > 200ms), cache hit rate (target > 90%), Web Vitals (LCP <= 2.5s, INP <= 200ms, CLS <= 0.1, TTFB <= 800ms), memory per pod (alert 80%), error rate (alert > 1%).
9. Features to Port
Online Board
- Real-time flight status display with search by flight number, route, departure, arrival
- SignalR integration for live updates
- Flight details view with boarding status, leg information, codeshare data
- Day navigation, time range filtering
- Popular requests widget
Schedule
- Flight schedule search by route over date ranges (up to 330 days)
- Return flight support
- Direct-only filter
- Accordion-based results display
- Flight details view
Flights Map
- Interactive Leaflet map showing available destinations from departure point
- Feature-flag gated (
features.flightsMap) - Destination list with connection details
Popular Requests
- Shared widget showing trending searches across all features
- Generic request components (flight number, route, departure, arrival)
10. Data Types
Port the existing TypeScript types from Angular's /typings/ directory:
IFlight = ISimpleFlight | IConnectingFlight(discriminated union)ISimpleFlight = IDirectFlight | IMultiLegFlightRouteType: DIRECT, MULTI_LEG, CONNECTINGFlightStatus: SCHEDULED, SENT, IN_FLIGHT, LANDED, ARRIVED, DELAYED, CANCELLED, UNKNOWNRequestMode: FLIGHT_NUMBER, ROUTE, SCHEDULE_ROUTE_BOTH_DIRECTIONS, DEPARTURE, ARRIVAL- API response types:
IBoardResponse,IScheduleResponse,IDaysResponse,IDestinationsResponse
11. API Endpoints
All endpoints proxied under /api in development, real URLs in production.
GET /api/flights/v1.1/{lang}/board # Flight board
GET /api/flights/v1.1/{lang}/onlineboard/details # Flight details
GET /api/flights/v1.1/{lang}/days/{date}/31/{param}/board # Available days (board)
GET /api/flights/v1.1/{lang}/schedule/1 # Schedule search
GET /api/flights/v1.1/{lang}/days/{date}/382/{param}/schedule # Available days (schedule)
GET /api/flights/v1.1/{lang}/destinations/1 # Map destinations
GET /api/flights/v1.1/{lang}/days/{date}/200/{param}/flights-map # Map days
GET /api/Requests/1/getpopular # Popular requests
12. i18n
9 languages: ru, en, de, fr, es, it, ja, ko, zh.
Existing JSON translation files reused directly -- simple nested key-value format compatible with react-i18next without reformatting.
Calendar locale data registered per language.
13. Environment Configuration
Per-environment settings (mirroring Angular's environment*.ts):
apiRootUrl,wsRootUrl,mapApiUrlurlForTrackerHub(SignalR hub)urlForChatBotappInsights(instrumentationKey, application, category, env)refreshPauseMin: 15,refreshStopMin: 60boardCalendarDatesEnabledCountBack: 1,boardCalendarDatesEnabledCountForward: 14scheduleCalendarDatesEnabledCountBack: 1,scheduleCalendarDatesEnabledCountForward: 330features: { flightsMap: boolean }- Ticket purchase time windows (prod only)
14. Non-Functional Requirements
| Requirement | Implementation |
|---|---|
| 100 RPS | Three-tier caching (CDN -> Redis -> SSR), horizontal scaling |
| 24/7 availability | Stateless app, geo-distributed VMs, < 6h recovery |
| Security/isolation | CSS Modules + prefix scoping, isolation: isolate, appendTo: 'self' |
| SEO | SSR, JSON-LD, OpenGraph, canonical URLs |
| Responsive | Container queries, fluid layout |
| Logging | Structured frontend logs -> customer's logging system |
| Monitoring | Web Vitals, SSR metrics, error rates -> aggregator |
| Multi-platform | Web + PWA embedding via Module Federation |