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 thesuperpowers:writing-plansskill 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 throughparseOnlineBoardUrl/buildOnlineBoardUrlidentically to Angular. - SEO parity: canonical, hreflang (9 langs +
x-default), OG tags, JSON-LD (Flightfor details,ItemListofFlightfor search results) — validated by SSR render +cheerioparse +schema-dtstype 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+stagingenvironments.
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 ofFlightCarditems 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 outsidesrc/ui/(enforced by 1A-3 ESLint boundary rules). - VRT baselines captured for each component at 375px, 768px, 1440px viewports.
pnpm typecheckandpnpm lintgreen.
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:
- Table-driven tests against the Phase 0 URL corpus (every real URL from prod access logs).
- Round-trip property tests:
buildOnlineBoardUrl(parseOnlineBoardUrl(type, params)) === originalParamsfor all valid inputs. fast-checkfuzz tests: random carrier codes (2 chars), flight numbers (1-4 digits), IATA codes (3 chars), dates (valid yyyyMMdd range), optional suffixes.- 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-checkfind no serialization asymmetry. parseOnlineBoardUrlreturnsnullfor invalid inputs (never throws).- Zero
anytypes 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 viaCachedApiClient.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. useOnlineBoardanduseFlightDetailshooks tested with@testing-library/react-hooksfor SSR initial data pass-through and client-side refetch behavior.- Zero
anytypes.
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 pushesRefreshDatewhen 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
RefreshDate→useLiveBoardtriggers re-fetch and returns updated data. - Vitest: mock SignalR hub pushes flight update →
useLiveFlightDetailstriggers re-fetch. - Strict Mode double-mount: exactly one
HubConnection.start()call (inherits from 1E's guarantee). - Disconnect scenario:
connectionStatustransitions 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
errorToResponsefrom 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 typecheckandpnpm lintgreen.
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:
buildOnlineBoardSeoproduces validSeoHeadPropsfor all 6 route types.- JSON-LD output validates against
schema-dtstypes at compile time. - CI JSON-LD validation job passes (structured data validator against fixture renders).
buildHreflangSetproduces correct 9-language +x-defaultset for each route.- OG tags present and correct for each route type.
- SSR render +
cheerioparse 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 fromtests/fixtures/phase-0/url-corpus-onlineboard.jsonand asserts thatparseOnlineBoardUrl+buildOnlineBoardUrlproduce 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 withcheerio, and compares the SEO elements against Phase 0 baselines fromtests/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 undertests/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.mdexplains 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:
- Start page — navigates to
/{lang}/onlineboard, verifies search form renders, submits a flight number search, arrives at the correct search results URL. - 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.
- Flight details — navigates to a details URL, verifies full flight info renders (status, route, codeshares), verifies breadcrumbs.
- Language switch — navigates to
/ru/onlineboard/..., switches language toen, verifies URL updates to/en/onlineboard/...with same parameters, verifies content is in English. - SignalR live update — mocked SignalR hub pushes a
RefreshDateevent, verifies the flight list updates without page reload, verifies connection status badge shows "live". - SignalR disconnect — mocked SignalR hub disconnects, verifies "offline" badge appears, verifies last-known data remains displayed.
- 404 handling — navigates to an invalid onlineboard URL, verifies 404 page renders with correct HTTP status.
- API error handling — mocked API returns 500, verifies error UI renders (not a white screen), verifies "Retry" button triggers re-fetch.
- Empty results — search returns zero flights, verifies empty-state UI renders.
- 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 outsidesrc/ui/; SCSS compiles clean. - 2B: 100% of Phase 0 URL corpus passes round-trip parity;
fast-checkfuzz tests green;parseOnlineBoardUrlreturnsnull(never throws) on invalid input. - 2C: API client functions tested for all three endpoints; hooks tested with initial data + refetch; cache behavior verified; zero
anytypes. - 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+Suspenseon every page; feature barrel exports only public surface; MF expose wrapper functional. - 2F:
buildOnlineBoardSeoproduces valid output for all 6 route types; JSON-LD validates againstschema-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/playwrightviolations 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 auditgreen 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
-
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 todocs/visual-parity-exceptions.md. -
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.
-
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.
-
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-checkfuzz tests in 2B supplement the corpus with randomized inputs. -
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.
-
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)
- Deploy to
staging; run full test suite + load test + SEO audit. - Canary 5% of
/{lang}/onlineboard/*prod traffic for 24h (request-id hash bucket behind proxy); rest stays on Angular. - Monitor: error rate, p95 latency,
flights.react.error,flights.api.error, SignalR health, Web Vitals, Search Console crawl errors. - If clean: 25% → 50% → 100% over 72h, always reversible.
- 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:
- Read this master plan in full for the dependency + contract context.
- Read the Phase 1 master plan for the contracts Phase 2 consumes.
- Read the relevant design spec sections.
- Read any upstream sub-plans that have already been written (their exit gates lock in file/API shapes).
- Produce a fully TDD-granular plan at the shape of the Phase 1 sub-plan format.
- 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.