Add Phase 2 Online Board master plan with 8 sub-plans
This commit is contained in:
@@ -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<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`):**
|
||||
|
||||
```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<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:**
|
||||
|
||||
```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 (
|
||||
<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`):**
|
||||
|
||||
```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`:
|
||||
```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 `<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:**
|
||||
|
||||
```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.
|
||||
Reference in New Issue
Block a user