From bd9cc92766d5faac722309de1e827a9d206bf1d8 Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 15 Apr 2026 07:46:33 +0300 Subject: [PATCH] Add Phase 2 Online Board master plan with 8 sub-plans --- .../2026-04-14-phase-2-online-board-master.md | 896 ++++++++++++++++++ 1 file changed, 896 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-14-phase-2-online-board-master.md diff --git a/docs/superpowers/plans/2026-04-14-phase-2-online-board-master.md b/docs/superpowers/plans/2026-04-14-phase-2-online-board-master.md new file mode 100644 index 00000000..fe43d57c --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-phase-2-online-board-master.md @@ -0,0 +1,896 @@ +# 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`):** + +```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`):** + +```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`):** + +```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; + +/** + * Get flight details. + * Maps to: GET /onlineboard/details?flightId={carrier}{number}&date={date} + */ +export function getFlightDetails( + client: ApiClient, + id: IParsedFlightId, +): Promise; + +/** + * 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; +``` + +**Exports (`src/features/online-board/hooks/useOnlineBoard.ts`):** + +```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`):** + +```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`):** + +```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`):** + +```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 `RefreshDate` → `useLiveBoard` 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):** + +```tsx +// 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(); + return ( + <> + + }> + + + + ); +} +``` + +**Feature barrel (`src/features/online-board/index.ts`) — populated in 2E:** + +```ts +// 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:** + +```tsx +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 ( + Loading...}> + + + ); +} +``` + +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()` + `` 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`):** + +```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; // 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`: +```json +{ + "@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 ``, `<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:** + +```ts +// 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.