Files
flights_web/docs/superpowers/specs/2026-04-03-angular-to-react-migration-design.md
T
gnezim 729603d27c fix: resolve build issues with ModernJS v3 + Module Federation
- 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
2026-04-03 23:34:20 +03:00

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 ./App via mf-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

  1. User selects filters -> Zustand store updates
  2. URL updates to reflect filter state (URL is source of truth)
  3. Route component reads URL params -> passes to TanStack Query hook
  4. Query fetches from REST API, caches result
  5. SignalR subscribes to relevant hub events
  6. 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 IATA
  • isValidFlightNumber(num) -- 1-4 digits + optional letter suffix
  • isValidTimeRange(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.ts exporting a loader function that runs on the server only.
  • Loaders prefetch TanStack Query data with queryClient.prefetchQuery() and return dehydrate(queryClient).
  • Page components wrap content in <HydrationBoundary state={dehydratedState}> -- client picks up cached data without refetch.
  • New QueryClient per request to prevent data leaks between users.
  • staleTime set 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
  • sendBeacon on visibilitychange (when hidden) 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
  • traceId for 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 hydration
  • output.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 to RefreshDate, invalidates flight board query
  • useFlightDetailsRealtime(flightId) -- subscribes to Refresh, 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
  • 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 | IMultiLegFlight
  • RouteType: DIRECT, MULTI_LEG, CONNECTING
  • FlightStatus: SCHEDULED, SENT, IN_FLIGHT, LANDED, ARRIVED, DELAYED, CANCELLED, UNKNOWN
  • RequestMode: 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, mapApiUrl
  • urlForTrackerHub (SignalR hub)
  • urlForChatBot
  • appInsights (instrumentationKey, application, category, env)
  • refreshPauseMin: 15, refreshStopMin: 60
  • boardCalendarDatesEnabledCountBack: 1, boardCalendarDatesEnabledCountForward: 14
  • scheduleCalendarDatesEnabledCountBack: 1, scheduleCalendarDatesEnabledCountForward: 330
  • features: { 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