Files
flights_web/docs/superpowers/plans/2026-04-14-phase-2-online-board-master.md
T

44 KiB

Phase 2 — Online Board MASTER Plan

This document is a plan INDEX, not an executable plan. It lists the Phase 2 sub-plans, their dependency order, the contracts each sub-plan exports for downstream sub-plans to consume, and the shared files that cross sub-plan boundaries.

Do not execute this document directly. Each sub-plan is a separate file under docs/superpowers/plans/ with its own TDD-granular tasks. They are written on demand by re-invoking the superpowers:writing-plans skill with a sub-plan-specific prompt.

Goal of Phase 2: Port the Online Board feature from Angular to React, achieving URL parity (100% against the Phase 0 corpus), SEO parity (enhanced with JSON-LD Flight + ItemList schemas), and live SignalR updates via the TrackerHub. Online Board is the hardest feature (SignalR live data + deep-linked search + flight details + real-time UI + SEO) and is deliberately first so its risk surfaces early.

Phase 2 exit gate (must pass before Phase 3 starts):

  • URL parity 100% verified against the Phase 0 prod-access-log corpus — every onlineboard/* URL shape round-trips through parseOnlineBoardUrl / buildOnlineBoardUrl identically to Angular.
  • SEO parity: canonical, hreflang (9 langs + x-default), OG tags, JSON-LD (Flight for details, ItemList of Flight for search results) — validated by SSR render + cheerio parse + schema-dts type check.
  • Playwright integration tests passing (4 ported Cypress scenarios + SignalR mock server + error cases).
  • VRT within threshold for all online-board routes x 3 viewports (375px, 768px, 1440px) x 2 languages (ru, en).
  • Load test at 150 RPS passes on online-board routes (50% headroom above the 100 RPS requirement).
  • All Phase 1 exit gates still green (regression gate).
  • WCAG AA violations block (upgraded from Phase 1's warn-only).
  • Real analytics vendors (Yandex.Metrica, CTM, Variocube, Dynatrace) emitting in testing + staging environments.

Reference spec: docs/superpowers/specs/2026-04-14-aeroflot-flights-react-rewrite-design.md — Phase 2 implements §9.2 (Phase 2 scope), §3.3-3.5 (routing + URL parity), §4.4 (SignalR), §5 (UI adapter), §6.5-6.8 (SEO/JSON-LD/OG/hreflang).

Phase 1 prerequisite: All Phase 1 exit gates must be green before Phase 2 starts. Phase 2 consumes the following Phase 1 contracts: ApiClient + CachedApiClient (1D), SignalRConnection + useLiveFlights (1E), SeoHead + buildHreflangSet + JsonLdRenderer (1F-seo), createI18nInstance + useTranslation (1C), Logger + useLogger (1G-logger), Analytics + useAnalytics (1G-analytics), ErrorBoundary + errorToResponse (1F-layout), getEnv (1A-1), root + locale layouts (1F-layout).


Sub-plan inventory

ID Sub-plan Estimated size File
2A UI adapter layer (src/ui/flights/) Large (20-30 tasks) 2026-04-14-phase-2a-ui-flights.md (TBW)
2B URL serializer/parser (src/features/online-board/url.ts) Small (5-10 tasks) 2026-04-14-phase-2b-url-serializer.md (TBW)
2C API client + hooks (src/features/online-board/api.ts, hooks) Medium (10-20 tasks) 2026-04-14-phase-2c-api-hooks.md (TBW)
2D SignalR wiring Medium (10-15 tasks) 2026-04-14-phase-2d-signalr-wiring.md (TBW)
2E Routes + pages Medium (15-20 tasks) 2026-04-14-phase-2e-routes-pages.md (TBW)
2F SEO + JSON-LD Small (8-12 tasks) 2026-04-14-phase-2f-seo-jsonld.md (TBW)
2G Parity harnesses Medium (10-15 tasks) 2026-04-14-phase-2g-parity-harnesses.md (TBW)
2H Integration tests Medium (10-15 tasks) 2026-04-14-phase-2h-integration-tests.md (TBW)

Sizes: Small = 5-12 tasks, Medium = 10-20 tasks, Large = 20-30 tasks.


Dependency graph

           ┌───────────────────┐
           │  2A  UI adapter   │  (flight-display components needed by pages)
           │  src/ui/flights/  │
           └────────┬──────────┘
                    │
    ┌───────────────┼───────────────┐
    │               │               │
    ▼               ▼               │
┌──────────┐  ┌───────────┐        │
│ 2B  URL  │  │ 2C  API   │        │
│ serializer│  │ client +  │        │
│ (indepen-│  │ hooks     │        │
│  dent)   │  │ (uses 2A  │        │
│          │  │  for types)│        │
└────┬─────┘  └─────┬─────┘        │
     │              │               │
     │              ▼               │
     │        ┌───────────┐        │
     │        │ 2D SignalR │        │
     │        │ wiring     │        │
     │        │ (consumes  │        │
     │        │  2C hooks) │        │
     │        └─────┬──────┘        │
     │              │               │
     └──────────────┼───────────────┘
                    ▼
           ┌───────────────────┐
           │  2E  Routes +     │
           │  pages            │
           │  (consumes 2A +   │
           │   2B + 2C + 2D)   │
           └────────┬──────────┘
                    │
                    ▼
           ┌───────────────────┐
           │  2F  SEO +        │
           │  JSON-LD          │
           │  (consumes 2E     │
           │   route context)  │
           └────────┬──────────┘
                    │
                    ▼
           ┌───────────────────┐
           │  2G  Parity       │
           │  harnesses        │
           │  (consumes 2E +   │
           │   2F for baselines)│
           └────────┬──────────┘
                    │
                    ▼
           ┌───────────────────┐
           │  2H  Integration  │
           │  tests            │
           │  (consumes all)   │
           └───────────────────┘

Execution order

Serial (1 engineer): 2A → 2B → 2C → 2D → 2E → 2F → 2G → 2H.

Rationale:

  • 2A must come first: the UI flight-display components are needed by every page. This is the src/ui/flights/ population step from design spec §5.
  • 2B (URL serializer) is logically independent of 2A but ordered after it in serial because 2E needs both and 2A is larger / higher risk.
  • 2C depends on 2A for data model types (flight types used in hooks).
  • 2D depends on 2C (wires SignalR push events into the same hooks/state that 2C creates).
  • 2E depends on 2A + 2B + 2C + 2D — it's the integration point (pages import UI components, use URL parsing, call API hooks, wire live data).
  • 2F depends on 2E (SEO builders need the route context and data shapes that pages define).
  • 2G depends on 2E + 2F (parity harnesses test the rendered pages + SEO output).
  • 2H depends on everything (Playwright integration tests exercise the full feature).

Parallel (2+ engineers): After 2A ships:

  • Engineer 1: 2B (URL serializer, fully independent)
  • Engineer 2: 2C (API hooks, needs 2A types)
  • Then 2D follows 2C; 2E follows 2B + 2C + 2D; rest is serial.

Critical path

2A → 2C → 2D → 2E → 2F → 2G → 2H is the critical path. 2B sits off the critical path (it's small and independent) and can be slotted alongside 2C.


Contracts — what each sub-plan exports

2A — UI adapter layer contracts

Scope: Port the subset of Angular FlightsModule shared components that the Online Board feature uses. These land in src/ui/flights/ per the design spec §5 location rule ("does more than one feature use it?"). Feature-specific components land in src/features/online-board/components/ in sub-plan 2E.

Exports:

  • src/ui/flights/FlightCard.tsx — single flight row in search results. Displays carrier logo, flight number, departure/arrival airports and times, status badge, aircraft type. Props-driven, no data fetching.
  • src/ui/flights/FlightList.tsx — scrollable list of FlightCard items with empty-state and loading skeleton.
  • src/ui/flights/FlightDetails.tsx — expanded flight details view: route map placeholder, status timeline, departure/arrival info, aircraft info, codeshare info.
  • src/ui/flights/StatusBadge.tsx — flight status indicator (on time, delayed, cancelled, landed, etc.) with color-coded styling.
  • src/ui/flights/AirportDisplay.tsx — airport name + IATA code display with optional city name.
  • src/ui/flights/TimeDisplay.tsx — time formatting component (scheduled vs actual, with delay indicator).
  • src/ui/flights/SearchForm.tsx — online board search form with flight number input, airport autocomplete (PrimeReact Autocomplete), date picker (PrimeReact Calendar), and search type selector.
  • src/ui/flights/CalendarStrip.tsx — horizontal date selector showing available search dates from the calendar API.
  • src/ui/flights/FlightDetailsSkeleton.tsx — Suspense fallback skeleton for the details page.
  • src/ui/flights/FlightListSkeleton.tsx — Suspense fallback skeleton for search result pages.
  • src/ui/flights/ConnectionStatusBadge.tsx — SignalR connection status indicator ("live", "reconnecting", "offline").
  • src/ui/flights/Breadcrumbs.tsx — breadcrumb navigation for online board routes.

Porting workflow: per design spec §5.4 — read Angular source, translate template to JSX preserving DOM + class names, translate logic to hooks/props, port SCSS to .module.scss, write Vitest test, capture VRT baseline.

SCSS files: Each component has a co-located .module.scss file ported from the Angular component's SCSS. Class names preserved for VRT pixel parity.

TypeScript contracts (data model types in src/ui/flights/types.ts):

/** Simplified flight record for list/card display */
export interface ISimpleFlight {
  id: string;                          // unique flight identifier
  flightNumber: string;                // e.g. "SU 100"
  carrier: string;                     // IATA carrier code, e.g. "SU"
  carrierName: string;                 // localized carrier name
  departure: IAirportTime;
  arrival: IAirportTime;
  status: FlightStatus;
  aircraftType?: string;
  codeshares?: string[];
  distance?: number;
}

export interface IAirportTime {
  airport: string;                     // IATA code
  airportName: string;                 // localized name
  cityName: string;                    // localized city name
  scheduled: string;                   // ISO 8601 datetime
  actual?: string;                     // ISO 8601 datetime (if available)
  terminal?: string;
  gate?: string;
}

export type FlightStatus =
  | "scheduled"
  | "delayed"
  | "departed"
  | "in_flight"
  | "landed"
  | "arrived"
  | "cancelled"
  | "diverted"
  | "unknown";

/** Parsed flight identifier from URL */
export interface IParsedFlightId {
  carrier: string;                     // e.g. "SU"
  flightNumber: string;               // e.g. "100"
  suffix?: string;                     // optional flight suffix
  date: string;                        // yyyyMMdd
}

/** Request type discriminator for online board search */
export type FlightRequestType =
  | "flight"                           // search by flight number
  | "departure"                        // search by departure airport
  | "arrival"                          // search by arrival airport
  | "route";                           // search by departure + arrival airports

Package additions (2A): primereact (PrimeReact component library — specific components: Calendar, Autocomplete, Tooltip), clsx (conditional class names).

Exit gate for 2A:

  • Every UI component has a Vitest test rendering it with representative props and asserting key DOM structure.
  • SCSS modules compile with no errors.
  • No direct primereact/* imports outside src/ui/ (enforced by 1A-3 ESLint boundary rules).
  • VRT baselines captured for each component at 375px, 768px, 1440px viewports.
  • pnpm typecheck and pnpm lint green.

2B — URL serializer/parser contracts

Scope: TDD port of the Angular URL builder/parser for all 6 online board route shapes. Byte-exact parity with Angular's OnlineBoardFlightNumberUrlParamsResolver, OnlineBoardDepartureUrlParamsResolver, OnlineBoardArrivalUrlParamsResolver, OnlineBoardRouteUrlParamsResolver. The Phase 0 URL corpus fixtures are the test oracle.

Exports (src/features/online-board/url.ts):

import type { FlightRequestType, IParsedFlightId } from "@/ui/flights/types";

/** Discriminated union of all parsed online board URL parameter shapes */
export type OnlineBoardParams =
  | { type: "start" }
  | { type: "flight"; carrier: string; flightNumber: string; suffix?: string; date: string }
  | { type: "departure"; station: string; date: string; timeFrom?: string; timeTo?: string }
  | { type: "arrival"; station: string; date: string; timeFrom?: string; timeTo?: string }
  | { type: "route"; departure: string; arrival: string; date: string; timeFrom?: string; timeTo?: string }
  | { type: "details"; carrier: string; flightNumber: string; suffix?: string; date: string };

/**
 * Parse a raw URL path segment into typed online board params.
 * Returns null if the path does not match any known online board URL shape.
 *
 * @param routeType - the route prefix ("flight", "departure", "arrival", "route", or "" for details)
 * @param params - the raw URL parameter string (e.g. "SU100-20250115")
 */
export function parseOnlineBoardUrl(routeType: string, params: string): OnlineBoardParams | null;

/**
 * Build a URL path segment from typed online board params.
 * Output is byte-exact match with Angular's URL builder.
 *
 * @returns the path segment without leading slash (e.g. "flight/SU100-20250115")
 */
export function buildOnlineBoardUrl(params: OnlineBoardParams): string;

/**
 * Parse a flight URL parameter string into its constituent parts.
 * Handles format: {carrier}{flightNumber}{suffix?}-{yyyyMMdd}
 */
export function parseFlightUrlParams(raw: string): IParsedFlightId | null;

/**
 * Build a flight URL parameter string from parts.
 * Output format: {carrier}{flightNumber}{suffix?}-{yyyyMMdd}
 */
export function buildFlightUrlParams(id: IParsedFlightId): string;

/**
 * Parse a station URL parameter string.
 * Handles format: {station}-{yyyyMMdd}[-{timeFrom}{timeTo}]
 */
export function parseStationUrlParams(raw: string): {
  station: string;
  date: string;
  timeFrom?: string;
  timeTo?: string;
} | null;

/**
 * Parse a route URL parameter string.
 * Handles format: {dep}-{arr}-{yyyyMMdd}[-{timeFrom}{timeTo}]
 */
export function parseRouteUrlParams(raw: string): {
  departure: string;
  arrival: string;
  date: string;
  timeFrom?: string;
  timeTo?: string;
} | null;

Test strategy:

  1. Table-driven tests against the Phase 0 URL corpus (every real URL from prod access logs).
  2. Round-trip property tests: buildOnlineBoardUrl(parseOnlineBoardUrl(type, params)) === originalParams for all valid inputs.
  3. fast-check fuzz tests: random carrier codes (2 chars), flight numbers (1-4 digits), IATA codes (3 chars), dates (valid yyyyMMdd range), optional suffixes.
  4. Edge cases: missing optional time range, malformed dates, unknown carriers, extra hyphens.

Exit gate for 2B:

  • 100% of Phase 0 URL corpus fixtures pass round-trip parity.
  • Fuzz tests with fast-check find no serialization asymmetry.
  • parseOnlineBoardUrl returns null for invalid inputs (never throws).
  • Zero any types in the module.

2C — API client + hooks contracts

Scope: Online board REST endpoints wrapped in typed functions + React hooks. Consumes ApiClient / CachedApiClient from Phase 1's 1D.

Exports (src/features/online-board/api.ts):

import type { ISimpleFlight, IParsedFlightId, FlightRequestType } from "@/ui/flights/types";

/** Response shape from GET /board */
export interface BoardResponse {
  flights: ISimpleFlight[];
  totalCount: number;
  date: string;
  requestType: FlightRequestType;
}

/** Full flight details response from GET /onlineboard/details */
export interface FlightDetailsResponse {
  flight: ISimpleFlight;
  route: IRoutePoint[];
  codeshares: ICodeshare[];
  statusHistory: IStatusHistoryEntry[];
}

export interface IRoutePoint {
  airport: string;
  airportName: string;
  cityName: string;
  scheduledTime: string;
  actualTime?: string;
  terminal?: string;
  gate?: string;
}

export interface ICodeshare {
  carrier: string;
  carrierName: string;
  flightNumber: string;
}

export interface IStatusHistoryEntry {
  status: string;
  timestamp: string;
}

/** Calendar days response from GET /v1/days/.../board/ */
export type CalendarDaysResponse = string[];   // array of "yyyy-MM-dd" date strings

/**
 * Search flights on the online board.
 * Maps to: GET /board?type={type}&station={station}&date={date}&...
 */
export function searchFlights(
  client: ApiClient,
  params: {
    type: FlightRequestType;
    date: string;
    station?: string;
    departure?: string;
    arrival?: string;
    carrier?: string;
    flightNumber?: string;
    timeFrom?: string;
    timeTo?: string;
  },
): Promise<BoardResponse>;

/**
 * Get flight details.
 * Maps to: GET /onlineboard/details?flightId={carrier}{number}&date={date}
 */
export function getFlightDetails(
  client: ApiClient,
  id: IParsedFlightId,
): Promise<FlightDetailsResponse>;

/**
 * Get available calendar days for a given search context.
 * Maps to: GET /v1/days/{station|route}/board/
 */
export function getCalendarDays(
  client: ApiClient,
  params: {
    type: FlightRequestType;
    station?: string;
    departure?: string;
    arrival?: string;
  },
): Promise<CalendarDaysResponse>;

Exports (src/features/online-board/hooks/useOnlineBoard.ts):

import type { BoardResponse } from "../api";
import type { OnlineBoardParams } from "../url";

export interface UseOnlineBoardResult {
  data: BoardResponse;
  calendarDays: string[];
  isRefetching: boolean;
  error: Error | null;
  refetch: () => void;
}

/**
 * Hook for online board search pages.
 * SSR: receives initialData from the loader.
 * Client: re-fetches on param change, receives live updates from SignalR (via 2D).
 */
export function useOnlineBoard(
  params: OnlineBoardParams,
  initialData: BoardResponse,
  initialCalendarDays: string[],
): UseOnlineBoardResult;

Exports (src/features/online-board/hooks/useFlightDetails.ts):

import type { FlightDetailsResponse } from "../api";
import type { IParsedFlightId } from "@/ui/flights/types";

export interface UseFlightDetailsResult {
  data: FlightDetailsResponse;
  isRefetching: boolean;
  error: Error | null;
  refetch: () => void;
}

/**
 * Hook for the flight details page.
 * SSR: receives initialData from the loader.
 * Client: re-fetches on param change, receives live updates from SignalR (via 2D).
 */
export function useFlightDetails(
  id: IParsedFlightId,
  initialData: FlightDetailsResponse,
): UseFlightDetailsResult;

Caching strategy (per design spec §4.2):

  • searchFlights: 30s TTL (live data), server LRU + client memory cache via CachedApiClient.
  • getFlightDetails: 30s TTL (live data), same caching layers.
  • getCalendarDays: 5 min TTL (static reference data).

Exit gate for 2C:

  • Vitest tests cover: successful API call + response deserialization for all three endpoints; error mapping (404 → ApiHttpError, timeout → ApiTimeoutError); cache hit for repeated identical queries; hooks render with initial data and update on refetch.
  • useOnlineBoard and useFlightDetails hooks tested with @testing-library/react-hooks for SSR initial data pass-through and client-side refetch behavior.
  • Zero any types.

2D — SignalR wiring contracts

Scope: Connect the generic useLiveFlights hook from Phase 1's 1E to the real TrackerHub channels used by Online Board. Wire into the search and details hooks from 2C.

Exports (src/features/online-board/hooks/useLiveBoard.ts):

import type { ConnectionStatus } from "@/shared/signalr/connection";
import type { BoardResponse } from "../api";

export interface UseLiveBoardResult {
  data: BoardResponse;
  connectionStatus: ConnectionStatus;
}

/**
 * Wraps useLiveFlights with Online Board-specific channel configuration.
 * Channel: SubscribeDate(date, departure?, arrival?)
 * On RefreshDate push: triggers silent re-fetch of searchFlights().
 */
export function useLiveBoard(
  params: { date: string; departure?: string; arrival?: string },
  initialData: BoardResponse,
): UseLiveBoardResult;

Exports (src/features/online-board/hooks/useLiveFlightDetails.ts):

import type { ConnectionStatus } from "@/shared/signalr/connection";
import type { FlightDetailsResponse } from "../api";
import type { IParsedFlightId } from "@/ui/flights/types";

export interface UseLiveFlightDetailsResult {
  data: FlightDetailsResponse;
  connectionStatus: ConnectionStatus;
}

/**
 * Wraps useLiveFlights with flight details-specific channel configuration.
 * Channel: Subscribe(flightId@date)
 * On push: triggers silent re-fetch of getFlightDetails().
 */
export function useLiveFlightDetails(
  id: IParsedFlightId,
  initialData: FlightDetailsResponse,
): UseLiveFlightDetailsResult;

TrackerHub channel mapping:

  • Search pages: SubscribeDate(date, departure?, arrival?) — server pushes RefreshDate when any flight matching the query updates.
  • Details page: Subscribe({carrier}{flightNumber}@{date}) — server pushes updates when the specific flight changes.
  • On push: the hook triggers a silent re-fetch of the corresponding REST endpoint (no full-page reload, no flash). The re-fetched data replaces the current state atomically.

Integration with 2C hooks: useOnlineBoard (2C) internally delegates to useLiveBoard (2D) for its live data; useFlightDetails (2C) internally delegates to useLiveFlightDetails (2D). The 2C hooks are the public API; 2D hooks are internal wiring.

Exit gate for 2D:

  • Vitest: mock SignalR hub pushes RefreshDateuseLiveBoard triggers re-fetch and returns updated data.
  • Vitest: mock SignalR hub pushes flight update → useLiveFlightDetails triggers re-fetch.
  • Strict Mode double-mount: exactly one HubConnection.start() call (inherits from 1E's guarantee).
  • Disconnect scenario: connectionStatus transitions to "offline", data remains last-known-good.
  • SSR: returns { data: initialData, connectionStatus: "idle" } without importing @microsoft/signalr.

2E — Routes + pages contracts

Scope: All src/routes/[lang]/onlineboard/* route files with loaders, Suspense, React.lazy. Also includes feature-specific components in src/features/online-board/components/ that are not shared across features.

Route files:

Route file URL pattern Angular equivalent
src/routes/[lang]/onlineboard/page.tsx /{lang}/onlineboard Start page (search form)
src/routes/[lang]/onlineboard/flight/[params]/page.tsx /{lang}/onlineboard/flight/SU100-20250115 Flight number search
src/routes/[lang]/onlineboard/departure/[params]/page.tsx /{lang}/onlineboard/departure/SVO-20250115 Departure station search
src/routes/[lang]/onlineboard/arrival/[params]/page.tsx /{lang}/onlineboard/arrival/JFK-20250115 Arrival station search
src/routes/[lang]/onlineboard/route/[params]/page.tsx /{lang}/onlineboard/route/SVO-JFK-20250115 Route search
src/routes/[lang]/onlineboard/[params]/page.tsx /{lang}/onlineboard/SU100-20250115 Flight details

Feature-specific components (src/features/online-board/components/):

  • OnlineBoardStart.tsx — start page with search form, search history, and popular routes.
  • OnlineBoardSearch.tsx — search results page (shared by flight/departure/arrival/route searches) with flight list, calendar strip, connection status badge, and filter controls.
  • OnlineBoardDetails.tsx — flight details page with full flight info, status timeline, route points, codeshare info, and connection status badge.

Loader pattern (per design spec §3.4):

// Example: routes/[lang]/onlineboard/departure/[params]/page.tsx
import { lazy, Suspense } from "react";
const OnlineBoardSearch = lazy(() =>
  import("@/features/online-board").then(m => ({ default: m.OnlineBoardSearch }))
);

export async function loader({ params }: { params: { lang: string; params: string } }) {
  const parsed = parseOnlineBoardUrl("departure", params.params);
  if (!parsed || parsed.type !== "departure") throw new ApiHttpError("Not found", 404);
  const [data, calendarDays] = await Promise.all([
    searchFlights(apiClient, { type: "departure", station: parsed.station, date: parsed.date, timeFrom: parsed.timeFrom, timeTo: parsed.timeTo }),
    getCalendarDays(apiClient, { type: "departure", station: parsed.station }),
  ]);
  const seo = buildOnlineBoardSeo(parsed, cityNames);
  return { data, calendarDays, seo, parsed };
}

export default function Page() {
  const { data, calendarDays, seo, parsed } = useLoaderData<typeof loader>();
  return (
    <>
      <SeoHead {...seo} />
      <Suspense fallback={<FlightListSkeleton />}>
        <OnlineBoardSearch initialData={data} initialCalendarDays={calendarDays} params={parsed} />
      </Suspense>
    </>
  );
}

Feature barrel (src/features/online-board/index.ts) — populated in 2E:

// Public surface of the online-board feature
export { OnlineBoardStart } from "./components/OnlineBoardStart";
export { OnlineBoardSearch } from "./components/OnlineBoardSearch";
export { OnlineBoardDetails } from "./components/OnlineBoardDetails";
export { parseOnlineBoardUrl, buildOnlineBoardUrl } from "./url";
export { searchFlights, getFlightDetails, getCalendarDays } from "./api";
export { buildOnlineBoardSeo } from "./seo";
export type { BoardResponse, FlightDetailsResponse, CalendarDaysResponse } from "./api";
export type { OnlineBoardParams } from "./url";

MF expose (src/mf/expose/OnlineBoard.tsx) — updated from stub to real in 2E:

import { lazy, Suspense } from "react";
import type { HostContract } from "@/host-contract";

const OnlineBoardFeature = lazy(() =>
  import("@/features/online-board").then(m => ({ default: m.OnlineBoardRoot }))
);

export default function OnlineBoard({ hostContract }: { hostContract: HostContract }) {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <OnlineBoardFeature hostContract={hostContract} />
    </Suspense>
  );
}

This requires an OnlineBoardRoot component exported from the feature barrel that wraps the internal router for embedded MF usage.

Exit gate for 2E:

  • All 6 routes render via SSR with correct loader data.
  • URL parameters parsed correctly for all route shapes.
  • Invalid URL parameters return 404 (via errorToResponse from 1F-layout).
  • React.lazy() + <Suspense> pattern verified on every page.
  • Feature barrel exports only the public surface (no internal component leakage).
  • MF expose wrapper renders the feature root in a test host.
  • pnpm typecheck and pnpm lint green.

2F — SEO + JSON-LD contracts

Scope: Build the buildOnlineBoardSeo() function for each online board route type. Produce JSON-LD schemas (Flight for details, ItemList of Flight for search results). Consume SeoHead, buildHreflangSet, and JsonLdRenderer from Phase 1's 1F-seo.

Exports (src/features/online-board/seo.ts):

import type { SeoHeadProps } from "@/ui/seo/SeoHead";
import type { OnlineBoardParams } from "./url";
import type { ISimpleFlight, FlightDetailsResponse } from "./api";

/**
 * Build SeoHead props for any online board route.
 * Produces: title, description, canonical, hreflang, OG tags, JSON-LD.
 *
 * JSON-LD schemas:
 *   - Flight details → schema.org/Flight
 *   - Search results → schema.org/ItemList containing Flight items
 *   - Start page → schema.org/WebPage with SearchAction
 */
export function buildOnlineBoardSeo(
  params: OnlineBoardParams,
  context: {
    cityNames: Record<string, string>;  // IATA code → localized city name
    locale: string;
    canonicalOrigin: string;
    flights?: ISimpleFlight[];
    flightDetails?: FlightDetailsResponse;
  },
): SeoHeadProps;

/**
 * Build a JSON-LD Flight object from flight details data.
 * Uses schema-dts types for type safety.
 */
export function buildFlightJsonLd(
  flight: FlightDetailsResponse,
  locale: string,
): import("schema-dts").Flight;

/**
 * Build a JSON-LD ItemList of Flight objects from search results.
 */
export function buildFlightSearchResultsJsonLd(
  flights: ISimpleFlight[],
  params: OnlineBoardParams,
  locale: string,
): import("schema-dts").ItemList;

Title/description strategy: Ported from Angular's translation keys. Pattern:

  • Start page: t("onlineboard.seo.start.title") / t("onlineboard.seo.start.description")
  • Flight search: t("onlineboard.seo.flight.title", { flightNumber }) / t("onlineboard.seo.flight.description", { flightNumber, date })
  • Departure: t("onlineboard.seo.departure.title", { cityName }) / t("onlineboard.seo.departure.description", { cityName, date })
  • Arrival: t("onlineboard.seo.arrival.title", { cityName }) / etc.
  • Route: t("onlineboard.seo.route.title", { departureCity, arrivalCity }) / etc.
  • Details: t("onlineboard.seo.details.title", { flightNumber, carrier }) / etc.

JSON-LD schema details:

Flight details page emits schema.org/Flight:

{
  "@context": "https://schema.org",
  "@type": "Flight",
  "flightNumber": "SU 100",
  "provider": { "@type": "Airline", "iataCode": "SU", "name": "Aeroflot" },
  "departureAirport": { "@type": "Airport", "iataCode": "SVO", "name": "Sheremetyevo", "address": { "@type": "PostalAddress", "addressCountry": "RU" } },
  "arrivalAirport": { "@type": "Airport", "iataCode": "JFK", ... },
  "departureTime": "2025-01-15T10:00:00+03:00",
  "arrivalTime": "2025-01-15T14:30:00-05:00",
  "flightDistance": { "@type": "Distance", "value": "9200 km" }
}

Search results pages emit schema.org/ItemList containing Flight items.

OG images: Phase 2 enhancement per design spec §6.7 — dynamic per-flight OG images via Satori, served from routes/og/flight/[params]/image.tsx, cached s-maxage=86400. Falls back to the static default OG image from Phase 1 if Satori generation fails.

Exit gate for 2F:

  • buildOnlineBoardSeo produces valid SeoHeadProps for all 6 route types.
  • JSON-LD output validates against schema-dts types at compile time.
  • CI JSON-LD validation job passes (structured data validator against fixture renders).
  • buildHreflangSet produces correct 9-language + x-default set for each route.
  • OG tags present and correct for each route type.
  • SSR render + cheerio parse asserts <title>, <meta name="description">, <link rel="canonical">, <link rel="alternate" hreflang>, <meta property="og:*">, <script type="application/ld+json">.

2G — Parity harnesses contracts

Scope: Build the URL parity harness and SEO parity harness that were deferred from Phase 1 (the "deferred 1J" mentioned in the Phase 1 master plan). These harnesses test against the real Online Board feature, not the synthetic smoke route. Also establish VRT baselines for all online board routes.

Exports:

  • tests/parity/url-parity.test.ts — table-driven Vitest test that reads the Phase 0 URL corpus from tests/fixtures/phase-0/url-corpus-onlineboard.json and asserts that parseOnlineBoardUrl + buildOnlineBoardUrl produce byte-exact matches. Merge-blocking CI gate.

  • tests/parity/seo-parity.test.ts — Vitest test that SSR-renders each online board route type, parses the HTML with cheerio, and compares the SEO elements against Phase 0 baselines from tests/fixtures/phase-0/seo-baselines-onlineboard.json. Checks: <title>, <meta name="description">, <link rel="canonical">, hreflang set, OG tags. JSON-LD presence and schema type checked (content differs from Angular since Angular had no JSON-LD — this is an enhancement, not parity).

  • tests/vrt/onlineboard/ — Playwright VRT baseline screenshots for all 6 route types at 3 viewports (375px, 768px, 1440px) x 2 languages (ru, en). Total: 36 baseline images. Threshold: 0.1% pixel diff (configurable). Committed under tests/fixtures/phase-2/vrt/.

  • scripts/parity/run-url-parity.ts — standalone script to run URL parity checks outside of CI (for local development). Reads the corpus, runs both parse and build, reports mismatches with diffs.

Exit gate for 2G:

  • URL parity: 100% of the Phase 0 corpus passes.
  • SEO parity: all baseline comparisons pass (with documented exceptions for JSON-LD enhancement).
  • VRT baselines committed and CI gate functional (diff threshold violation blocks merge).
  • Parity harness documentation in tests/parity/README.md explains how to add new test URLs and update baselines.

2H — Integration tests contracts

Scope: Playwright end-to-end tests covering the full Online Board feature in standalone SSR mode. Ported from the 4 passing Cypress scenarios + new tests for SignalR, error cases, and edge cases.

Test inventory:

  1. Start page — navigates to /{lang}/onlineboard, verifies search form renders, submits a flight number search, arrives at the correct search results URL.
  2. Departure search — navigates to a departure URL, verifies flight list renders with correct airport, verifies calendar strip shows available dates, clicks a different date and verifies URL update.
  3. Flight details — navigates to a details URL, verifies full flight info renders (status, route, codeshares), verifies breadcrumbs.
  4. Language switch — navigates to /ru/onlineboard/..., switches language to en, verifies URL updates to /en/onlineboard/... with same parameters, verifies content is in English.
  5. SignalR live update — mocked SignalR hub pushes a RefreshDate event, verifies the flight list updates without page reload, verifies connection status badge shows "live".
  6. SignalR disconnect — mocked SignalR hub disconnects, verifies "offline" badge appears, verifies last-known data remains displayed.
  7. 404 handling — navigates to an invalid onlineboard URL, verifies 404 page renders with correct HTTP status.
  8. API error handling — mocked API returns 500, verifies error UI renders (not a white screen), verifies "Retry" button triggers re-fetch.
  9. Empty results — search returns zero flights, verifies empty-state UI renders.
  10. Responsive — verifies all pages render correctly at 375px, 768px, 1440px without horizontal scroll.

SignalR mock server:

// tests/mocks/signalr-mock-server.ts
export class MockSignalRServer {
  constructor(port: number);
  start(): Promise<void>;
  stop(): Promise<void>;
  pushRefreshDate(date: string, departure?: string, arrival?: string): void;
  pushFlightUpdate(flightId: string, date: string): void;
  getSubscriptions(): { channel: string; args: unknown[] }[];
}

The mock server implements the TrackerHub protocol: accepts SubscribeDate and Subscribe invocations, records them, and allows tests to programmatically push events.

Exit gate for 2H:

  • All 10 Playwright test scenarios pass.
  • SignalR mock server correctly simulates push events and disconnect.
  • Tests run in CI in under 5 minutes.
  • No flaky tests (3 consecutive CI runs green).

Shared files — cross-sub-plan modification table

File Primary owner Also modified by What the modifiers add
src/features/online-board/index.ts 1A-1 (empty barrel) 2E Populated with all public exports (components, URL functions, API functions, SEO builders, types)
src/mf/expose/OnlineBoard.tsx 1A-2 (stub) 2E Updated from stub to real: imports OnlineBoardRoot from feature barrel, renders with React.lazy + Suspense
src/ui/flights/types.ts 2A 2B, 2C, 2D, 2E, 2F Consumed (read-only) by all downstream sub-plans for type imports
src/observability/analytics/adapters/*.ts 1G-analytics (stubs) 2E Real vendor script loading wired (Yandex.Metrica, CTM, Variocube, Dynatrace) — replaces structured stubs with real implementations when A7 is resolved
package.json 1A-1 2A primereact, clsx added
2B fast-check (dev dep)
2H @playwright/test (if not already from Phase 1)

Modification protocol — same as Phase 1: downstream sub-plan tasks explicitly reference the primary owner, quote the pre-modification state, show the full post-modification file, and re-run the owner's exit-gate tests.


Spec-coverage matrix

Spec section Topic Sub-plan(s)
§3.3 Routing — file-based, precedence 2E
§3.4 Loaders, Suspense, React.lazy 2E
§3.5 URL parity — ported serializers 2B
§3.6 Canonical, hreflang, redirects 2F
§4.4 SignalR wrapper + hook 2D
§4.5 State management (hooks, reducers) 2C, 2D
§4.6 Error handling path 2C, 2E
§5.1 UI adapter boundary 2A
§5.2 SCSS Modules 2A
§5.3 PrimeReact theming 2A
§5.4 Per-component porting workflow 2A
§5.7 Responsiveness 2A, 2H
§6.5 <SeoHead> usage 2F
§6.6 JSON-LD schema coverage (Flight, ItemList) 2F
§6.7 OG images (dynamic per-flight) 2F
§6.8 Canonical + hreflang correctness 2F, 2G
§8.4 Testing strategy (URL parity, VRT, Playwright) 2G, 2H
§9.2 Phase 2 scope + exit gate All

Phase 2 global exit gate — checklist

  • 2A: All UI flight-display components have Vitest tests + VRT baselines; no direct primereact/* imports outside src/ui/; SCSS compiles clean.
  • 2B: 100% of Phase 0 URL corpus passes round-trip parity; fast-check fuzz tests green; parseOnlineBoardUrl returns null (never throws) on invalid input.
  • 2C: API client functions tested for all three endpoints; hooks tested with initial data + refetch; cache behavior verified; zero any types.
  • 2D: SignalR wiring tested: push → re-fetch → updated data; Strict Mode safe; disconnect → offline badge + last-known data; SSR returns idle.
  • 2E: All 6 routes render SSR with correct loaders; invalid params → 404; React.lazy + Suspense on every page; feature barrel exports only public surface; MF expose wrapper functional.
  • 2F: buildOnlineBoardSeo produces valid output for all 6 route types; JSON-LD validates against schema-dts; hreflang reciprocal across 9 languages; OG tags correct; CI JSON-LD validator passes.
  • 2G: URL parity 100% against Phase 0 corpus; SEO parity baselines pass; 36 VRT baselines committed and CI gate functional.
  • 2H: All 10 Playwright scenarios pass; SignalR mock server functional; 3 consecutive CI runs green (no flakes); tests complete in under 5 minutes.
  • Load test: Online board routes sustain 150 RPS with p95 latency under 500ms.
  • WCAG AA: @axe-core/playwright violations block (upgraded from Phase 1 warn-only).
  • Analytics: Real vendor scripts (Yandex.Metrica, CTM, Variocube, Dynatrace) emitting in testing + staging.
  • Phase 1 regression: All Phase 1 exit gates still green on main.
  • Security scan: osv-scanner + npm audit green after Phase 2 dependency additions.
  • Bundle size: Online board feature chunk within budget (budget TBD in 2A based on Angular bundle analysis).

Risks + open questions for Phase 2

  1. PrimeReact vs PrimeNG DOM differences (T2). Design spec §5.3 identifies Calendar, Autocomplete, DataTable, and Toast as likely failure cases. 2A absorbs this risk in src/ui/primitives/ wrappers + compensating CSS. Unresolved diffs go to docs/visual-parity-exceptions.md.

  2. SignalR TrackerHub protocol undocumented. The Angular source is the only specification. 2D must reverse-engineer the exact channel names and message shapes. Risk: Angular uses implicit conventions that aren't obvious from source reading alone. Mitigation: run the Angular app against a test hub and capture wire traffic during 2D.

  3. Analytics vendor credentials (A7) still unknown. If A7 is unresolved when 2E starts, the analytics adapters stay as structured stubs and the "real vendors in testing/staging" exit gate defers to Phase 3. The rest of Phase 2 is not blocked.

  4. Phase 0 URL corpus coverage. The parity harness is only as good as the corpus. If prod access logs are incomplete (e.g., rare URL shapes not in logs), parity gaps may surface post-cutover. Mitigation: fast-check fuzz tests in 2B supplement the corpus with randomized inputs.

  5. Dynamic OG images via Satori. Satori is a runtime dependency for generating per-flight OG images. If Satori proves unreliable or slow, Phase 2 falls back to the static default OG image and defers dynamic images to a follow-up.

  6. Load test at 150 RPS. Phase 1's smoke route may not be representative of Online Board's SSR cost (which includes API calls, SignalR connection setup, and JSON-LD generation). Load test infrastructure must mock upstream APIs at realistic latencies.


Cutover plan (from design spec §9.2)

  1. Deploy to staging; run full test suite + load test + SEO audit.
  2. Canary 5% of /{lang}/onlineboard/* prod traffic for 24h (request-id hash bucket behind proxy); rest stays on Angular.
  3. Monitor: error rate, p95 latency, flights.react.error, flights.api.error, SignalR health, Web Vitals, Search Console crawl errors.
  4. If clean: 25% → 50% → 100% over 72h, always reversible.
  5. Hold at 100% for 1 week. Then retire (not delete) Angular online-board code.

Cutover is NOT part of the sub-plans — it executes after 2H passes and is tracked as an operational procedure, not a development task.


How to write each sub-plan

When the user is ready to execute a sub-plan, re-invoke superpowers:writing-plans with a specific prompt like:

"Write sub-plan 2A (UI adapter layer) from docs/superpowers/plans/2026-04-14-phase-2-online-board-master.md. Target file: docs/superpowers/plans/2026-04-14-phase-2a-ui-flights.md. Follow the contracts defined in the master plan §2A exactly; reference the design spec §5 as source material and the Angular component inventory from Phase 0."

The sub-plan writer must:

  1. Read this master plan in full for the dependency + contract context.
  2. Read the Phase 1 master plan for the contracts Phase 2 consumes.
  3. Read the relevant design spec sections.
  4. Read any upstream sub-plans that have already been written (their exit gates lock in file/API shapes).
  5. Produce a fully TDD-granular plan at the shape of the Phase 1 sub-plan format.
  6. Match the contracts in this master plan byte-for-byte on type signatures. Any contract change requires updating this master plan first.

Self-review

Spec coverage. Every Phase 2-relevant design-spec section (§3.3-3.6, §4.4-4.6, §5, §6.5-6.8, §8.4, §9.2) maps to at least one sub-plan in the spec-coverage matrix.

Placeholder scan. No TBD / TODO / FIXME outside of the "TBW" markers on sub-plan filenames and the bundle-size budget note (which is deliberately deferred to 2A since it depends on Angular bundle analysis).

Internal consistency. Cross-checked: ISimpleFlight + IParsedFlightId + FlightRequestType defined in 2A → consumed by 2B, 2C, 2D, 2E, 2F; OnlineBoardParams defined in 2B → consumed by 2C, 2E, 2F; BoardResponse + FlightDetailsResponse defined in 2C → consumed by 2D, 2E, 2F; SeoHeadProps from 1F-seo → consumed by 2F; useLiveFlights from 1E → consumed by 2D; ApiClient from 1D → consumed by 2C; SignalRConnection from 1E → consumed by 2D.

Phase 1 contract consumption. Every Phase 1 contract used by Phase 2 is listed in the prerequisite section. No Phase 2 sub-plan creates functionality already provided by Phase 1 — it only wires and extends.

Dependency graph acyclicity. The graph is a strict DAG: 2A → 2C → 2D → 2E → 2F → 2G → 2H, with 2B as an independent node feeding into 2E. No cycles.


Next step

  • If you approve this master plan: say so, and I'll write sub-plan 2A (UI adapter layer) in the next session.
  • If you want changes: tell me, I revise.