# Flights Mini-List Sidebar (B.2) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a left sidebar on the Online Board details page showing all flights sharing the same flight number, with click-to-navigate and auto-scroll-into-view for the selected flight, wrapped in the shared `PageLayout`.

**Architecture:** Three changes in order: (1) extend `useFlightDetails` to expose the full `routes[]` array as `allFlights`; (2) add `FlightsMiniList` + `FlightsMiniListItem` components with `scrollIntoView` behavior; (3) refactor `OnlineBoardDetailsPage` to use `PageLayout` with the mini-list in `contentLeft`.

**Tech Stack:** React 18, React Router v6 (Modern.js), Vitest + React Testing Library, existing `buildOnlineBoardUrl` helper, existing `PageLayout` component.

---

## File Structure

### New files
- `src/features/online-board/components/FlightsMiniList/FlightsMiniList.tsx` — Container (returns null for ≤1 flight, manages scroll)
- `src/features/online-board/components/FlightsMiniList/FlightsMiniList.scss`
- `src/features/online-board/components/FlightsMiniList/FlightsMiniList.test.tsx`
- `src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx` — Single flight row
- `src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.test.tsx`
- `src/features/online-board/components/FlightsMiniList/index.ts` — Re-exports

### Modified files
- `src/features/online-board/hooks/useFlightDetails.ts` — Return `allFlights` in addition to `flight`
- `src/features/online-board/components/OnlineBoardDetailsPage.tsx` — Wrap content in `PageLayout`, pass mini-list to `contentLeft`
- `src/features/online-board/components/OnlineBoardDetailsPage.test.tsx` — Update existing tests for new layout

### No changes needed
- `src/features/online-board/url.ts` — Existing `buildOnlineBoardUrl({ type: "details", ... })` already produces the correct URL
- `src/ui/layout/PageLayout.tsx` — Already accepts `contentLeft`

---

### Task 1: Extend `useFlightDetails` to Return `allFlights`

**Files:**
- Modify: `src/features/online-board/hooks/useFlightDetails.ts`

- [ ] **Step 1: Write failing test**

Create `src/features/online-board/hooks/useFlightDetails.test.ts`:

```typescript
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook, waitFor } from "@testing-library/react";
import React, { type ReactNode } from "react";
import { useFlightDetails } from "./useFlightDetails.js";
import { ApiContext } from "@/shared/api/provider.js";
import type { ApiClient } from "@/shared/api/client.js";
import type { IBoardResponse } from "../types.js";

function makeFlight(id: string) {
  return {
    id,
    routeType: "Direct" as const,
    flyingTime: "1h",
    status: "Scheduled" as const,
    flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date: "20260416" },
    operatingBy: {},
    leg: {
      arrival: {
        scheduled: { airport: "", airportCode: "", city: "", cityCode: "", countryCode: "" },
        latest: { airport: "", airportCode: "", city: "", cityCode: "", countryCode: "" },
        dispatch: "", gate: "", terminal: "",
        times: { scheduledArrival: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: 0, utc: "" } },
      },
      dayChange: 0,
      departure: {
        scheduled: { airport: "", airportCode: "", city: "", cityCode: "", countryCode: "" },
        latest: { airport: "", airportCode: "", city: "", cityCode: "", countryCode: "" },
        dispatch: "", gate: "", terminal: "", checkingStatus: "Scheduled", parkingStand: "",
        times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: 0, utc: "" } },
      },
      equipment: {},
      flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
      flyingTime: "1h",
      index: 0,
      operatingBy: {},
      status: "Scheduled" as const,
      updated: "",
    },
  };
}

function makeWrapper(client: ApiClient) {
  return ({ children }: { children: ReactNode }) => React.createElement(
    ApiContext.Provider,
    { value: client },
    children,
  );
}

describe("useFlightDetails", () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  it("returns first flight and full array in allFlights", async () => {
    const response: IBoardResponse = {
      data: {
        partners: [],
        routes: [makeFlight("SU0022-20260416"), makeFlight("SU0022-20260417"), makeFlight("SU0022-20260418")],
        daysOfFlight: [],
      },
    };
    const client = {
      get: vi.fn().mockResolvedValue(response),
    } as unknown as ApiClient;

    const { result } = renderHook(
      () => useFlightDetails({ flights: "SU0022", dates: "2026-04-16" }),
      { wrapper: makeWrapper(client) },
    );

    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(result.current.flight?.id).toBe("SU0022-20260416");
    expect(result.current.allFlights).toHaveLength(3);
    expect(result.current.allFlights[0]?.id).toBe("SU0022-20260416");
  });

  it("returns empty allFlights array when response has no routes", async () => {
    const response: IBoardResponse = {
      data: { partners: [], routes: [], daysOfFlight: [] },
    };
    const client = {
      get: vi.fn().mockResolvedValue(response),
    } as unknown as ApiClient;

    const { result } = renderHook(
      () => useFlightDetails({ flights: "SU0022", dates: "2026-04-16" }),
      { wrapper: makeWrapper(client) },
    );

    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(result.current.flight).toBeNull();
    expect(result.current.allFlights).toEqual([]);
  });
});
```

**Note:** If `ApiContext` isn't exported from `@/shared/api/provider.js`, check how existing hook tests wire up the API client. If there's no `ApiContext`, wrap the hook with whatever provider the existing `useApiClient()` expects. Adjust imports to match.

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run src/features/online-board/hooks/useFlightDetails.test.ts`

Expected: FAIL — `allFlights` property does not exist on the return type.

- [ ] **Step 3: Update the hook**

Edit `src/features/online-board/hooks/useFlightDetails.ts`. Replace the entire file content with:

```typescript
/**
 * React hook for the flight details page.
 *
 * Calls `getFlightDetails` on param change, manages loading/error/data state.
 * Returns both the first flight (for primary display) and the full routes
 * array (for the mini-list sidebar).
 *
 * @module
 */

import { useState, useEffect, useRef } from "react";
import { useApiClient } from "@/shared/api/provider.js";
import { getFlightDetails } from "../api.js";
import type { FlightDetailsParams } from "../api.js";
import type { ISimpleFlight } from "../types.js";
import type { ApiError } from "@/shared/api/errors.js";

export interface UseFlightDetailsResult {
  flight: ISimpleFlight | null;
  allFlights: ISimpleFlight[];
  loading: boolean;
  error: ApiError | null;
}

/**
 * Hook for the flight details page. Fetches the details response and exposes
 * both the first route (primary flight) and the full routes array.
 */
export function useFlightDetails(
  params: FlightDetailsParams,
): UseFlightDetailsResult {
  const client = useApiClient();
  const [allFlights, setAllFlights] = useState<ISimpleFlight[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<ApiError | null>(null);

  const paramsRef = useRef(params);
  paramsRef.current = params;

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    setError(null);

    getFlightDetails(client, paramsRef.current)
      .then((response) => {
        if (!cancelled) {
          setAllFlights(response.data.routes);
          setLoading(false);
        }
      })
      .catch((err: ApiError) => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });

    return () => {
      cancelled = true;
    };
  }, [client, params.flights, params.dates]);

  return {
    flight: allFlights[0] ?? null,
    allFlights,
    loading,
    error,
  };
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run src/features/online-board/hooks/useFlightDetails.test.ts`

Expected: PASS — both tests pass.

- [ ] **Step 5: Check for regressions**

Run: `pnpm typecheck`

Expected: Existing consumers of `useFlightDetails` still compile (they only destructure `flight`, `loading`, `error`; adding `allFlights` doesn't break them).

Run: `pnpm test`

Expected: No new regressions. `OnlineBoardDetailsPage.test.tsx` should still pass.

- [ ] **Step 6: Commit**

```bash
git add src/features/online-board/hooks/useFlightDetails.ts src/features/online-board/hooks/useFlightDetails.test.ts
git commit -m "Expose allFlights array from useFlightDetails for mini-list sidebar"
```

---

### Task 2: FlightsMiniListItem Component

**Files:**
- Create: `src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx`
- Create: `src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.test.tsx`
- Create: `src/features/online-board/components/FlightsMiniList/FlightsMiniList.scss`

- [ ] **Step 1: Write failing tests**

Create `src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.test.tsx`:

```tsx
// @vitest-environment jsdom
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "@modern-js/runtime/router";
import { FlightsMiniListItem } from "./FlightsMiniListItem.js";
import type { ISimpleFlight } from "../../types.js";

vi.mock("@/i18n/provider.js", () => ({
  useTranslation: () => ({ t: (k: string) => k }),
}));

function makeDirectFlight(overrides: Partial<ISimpleFlight> = {}): ISimpleFlight {
  return {
    id: "SU0022-20260416",
    routeType: "Direct" as const,
    flyingTime: "1h 30m",
    status: "Scheduled" as const,
    flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date: "20260416" },
    operatingBy: {},
    leg: {
      arrival: {
        scheduled: { airport: "Pulkovo", airportCode: "LED", city: "St Petersburg", cityCode: "LED", countryCode: "RU" },
        latest: { airport: "Pulkovo", airportCode: "LED", city: "St Petersburg", cityCode: "LED", countryCode: "RU" },
        dispatch: "", gate: "", terminal: "",
        times: { scheduledArrival: { dayChange: { value: 0, title: "" }, local: "12:30", localTime: "12:30", tzOffset: 3, utc: "09:30" } },
      },
      dayChange: 0,
      departure: {
        scheduled: { airport: "Sheremetyevo", airportCode: "SVO", city: "Moscow", cityCode: "MOW", countryCode: "RU" },
        latest: { airport: "Sheremetyevo", airportCode: "SVO", city: "Moscow", cityCode: "MOW", countryCode: "RU" },
        dispatch: "", gate: "", terminal: "", checkingStatus: "Scheduled", parkingStand: "",
        times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "10:00", localTime: "10:00", tzOffset: 3, utc: "07:00" } },
      },
      equipment: {},
      flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
      flyingTime: "1h 30m",
      index: 0,
      operatingBy: {},
      status: "Scheduled" as const,
      updated: "",
    },
    ...overrides,
  } as ISimpleFlight;
}

function renderWithRouter(ui: React.ReactElement) {
  return render(<MemoryRouter>{ui}</MemoryRouter>);
}

describe("FlightsMiniListItem", () => {
  it("renders flight number", () => {
    const flight = makeDirectFlight();
    renderWithRouter(<FlightsMiniListItem flight={flight} isSelected={false} lang="ru" />);
    expect(screen.getByText("SU 0022")).toBeTruthy();
  });

  it("renders departure and arrival times", () => {
    const flight = makeDirectFlight();
    renderWithRouter(<FlightsMiniListItem flight={flight} isSelected={false} lang="ru" />);
    expect(screen.getByText("10:00")).toBeTruthy();
    expect(screen.getByText("12:30")).toBeTruthy();
  });

  it("renders departure and arrival station codes", () => {
    const flight = makeDirectFlight();
    renderWithRouter(<FlightsMiniListItem flight={flight} isSelected={false} lang="ru" />);
    expect(screen.getByText("SVO")).toBeTruthy();
    expect(screen.getByText("LED")).toBeTruthy();
  });

  it("has data-testid based on flight id", () => {
    const flight = makeDirectFlight();
    renderWithRouter(<FlightsMiniListItem flight={flight} isSelected={false} lang="ru" />);
    expect(screen.getByTestId("mini-list-item-SU0022-20260416")).toBeTruthy();
  });

  it("applies selected modifier when isSelected", () => {
    const flight = makeDirectFlight();
    renderWithRouter(<FlightsMiniListItem flight={flight} isSelected={true} lang="ru" />);
    const item = screen.getByTestId("mini-list-item-SU0022-20260416");
    expect(item.className).toMatch(/--selected/);
  });

  it("does not apply selected modifier when not selected", () => {
    const flight = makeDirectFlight();
    renderWithRouter(<FlightsMiniListItem flight={flight} isSelected={false} lang="ru" />);
    const item = screen.getByTestId("mini-list-item-SU0022-20260416");
    expect(item.className).not.toMatch(/--selected/);
  });

  it("link points to the flight details URL", () => {
    const flight = makeDirectFlight();
    renderWithRouter(<FlightsMiniListItem flight={flight} isSelected={false} lang="ru" />);
    const link = screen.getByTestId("mini-list-item-SU0022-20260416") as HTMLAnchorElement;
    expect(link.getAttribute("href")).toBe("/ru/onlineboard/SU0022-20260416");
  });

  it("uses first leg departure and last leg arrival for multi-leg flights", () => {
    const multiLeg: ISimpleFlight = {
      ...makeDirectFlight(),
      routeType: "MultiLeg" as const,
      legs: [
        {
          ...(makeDirectFlight().leg!),
          departure: {
            scheduled: { airport: "JFK", airportCode: "JFK", city: "New York", cityCode: "NYC", countryCode: "US" },
            latest: { airport: "JFK", airportCode: "JFK", city: "New York", cityCode: "NYC", countryCode: "US" },
            dispatch: "", gate: "", terminal: "", checkingStatus: "Scheduled", parkingStand: "",
            times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "08:00", localTime: "08:00", tzOffset: 0, utc: "" } },
          },
        },
        {
          ...(makeDirectFlight().leg!),
          arrival: {
            scheduled: { airport: "CDG", airportCode: "CDG", city: "Paris", cityCode: "PAR", countryCode: "FR" },
            latest: { airport: "CDG", airportCode: "CDG", city: "Paris", cityCode: "PAR", countryCode: "FR" },
            dispatch: "", gate: "", terminal: "",
            times: { scheduledArrival: { dayChange: { value: 0, title: "" }, local: "14:00", localTime: "14:00", tzOffset: 0, utc: "" } },
          },
        },
      ],
    } as unknown as ISimpleFlight;
    delete (multiLeg as { leg?: unknown }).leg;
    renderWithRouter(<FlightsMiniListItem flight={multiLeg} isSelected={false} lang="ru" />);
    expect(screen.getByText("JFK")).toBeTruthy();
    expect(screen.getByText("CDG")).toBeTruthy();
  });
});
```

**Note:** If `MemoryRouter` is not exported from `@modern-js/runtime/router`, check how other tests render components with routing. Common alternatives include wrapping with a custom router provider. Adjust imports to match existing patterns.

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.test.tsx`

Expected: FAIL — module not found.

- [ ] **Step 3: Create the shared stylesheet**

Create `src/features/online-board/components/FlightsMiniList/FlightsMiniList.scss`:

```scss
.mini-list {
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 170px);
  overflow-y: auto;
  background: #fff;
  border-radius: 8px;

  &__item {
    padding: 12px;
    border-bottom: 1px solid #e0e0e0;
    text-decoration: none;
    color: inherit;
    display: block;

    &:last-child {
      border-bottom: none;
    }

    &:hover {
      background: #f8f9fa;
    }

    &--selected {
      border: 2px solid #2060c0;
      border-radius: 4px;
    }
  }

  &__flight-number {
    font-size: 12px;
    color: #666;
    margin-bottom: 4px;
  }

  &__content {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    grid-template-rows: auto auto;
    gap: 8px 12px;
  }

  &__dep-time,
  &__arr-time {
    font-size: 16px;
    font-weight: 500;
    color: #1a3a5c;
  }

  &__arr-time {
    text-align: right;
  }

  &__status-icon {
    grid-column: 2;
    grid-row: 1;
    align-self: center;
  }

  &__dep-station {
    grid-column: 1;
    grid-row: 2;
    font-size: 14px;
    color: #333;
  }

  &__arr-station {
    grid-column: 3;
    grid-row: 2;
    text-align: right;
    font-size: 14px;
    color: #333;
  }
}
```

- [ ] **Step 4: Create FlightsMiniListItem.tsx**

Create `src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx`:

```tsx
import { forwardRef } from "react";
import { Link } from "@modern-js/runtime/router";
import type { ISimpleFlight, IFlightLeg } from "../../types.js";
import { buildOnlineBoardUrl } from "../../url.js";
import "./FlightsMiniList.scss";

export interface FlightsMiniListItemProps {
  flight: ISimpleFlight;
  isSelected: boolean;
  lang: string;
}

/**
 * Extract first-leg departure and last-leg arrival for display.
 * Direct flights: just use the single leg.
 * MultiLeg flights: first leg's departure, last leg's arrival.
 */
function getEndpoints(flight: ISimpleFlight): { dep: IFlightLeg["departure"]; arr: IFlightLeg["arrival"] } {
  if (flight.routeType === "Direct") {
    return { dep: flight.leg.departure, arr: flight.leg.arrival };
  }
  const firstLeg = flight.legs[0]!;
  const lastLeg = flight.legs[flight.legs.length - 1]!;
  return { dep: firstLeg.departure, arr: lastLeg.arrival };
}

function getDepTime(dep: IFlightLeg["departure"]): string {
  return dep.times.actualBlockOff?.local ?? dep.times.scheduledDeparture.local;
}

function getArrTime(arr: IFlightLeg["arrival"]): string {
  return arr.times.actualBlockOn?.local ?? arr.times.scheduledArrival.local;
}

export const FlightsMiniListItem = forwardRef<HTMLAnchorElement, FlightsMiniListItemProps>(
  ({ flight, isSelected, lang }, ref) => {
    const { dep, arr } = getEndpoints(flight);
    const href = `/${lang}/${buildOnlineBoardUrl({
      type: "details",
      carrier: flight.flightId.carrier,
      flightNumber: flight.flightId.flightNumber,
      ...(flight.flightId.suffix ? { suffix: flight.flightId.suffix } : {}),
      date: flight.flightId.date,
    })}`;

    const className = `mini-list__item${isSelected ? " mini-list__item--selected" : ""}`;

    return (
      <Link
        ref={ref}
        to={href}
        className={className}
        data-testid={`mini-list-item-${flight.id}`}
      >
        <div className="mini-list__flight-number">
          {flight.flightId.carrier} {flight.flightId.flightNumber}
        </div>
        <div className="mini-list__content">
          <span className="mini-list__dep-time">{getDepTime(dep)}</span>
          <span className="mini-list__status-icon" aria-label={flight.status}>
            ✈
          </span>
          <span className="mini-list__arr-time">{getArrTime(arr)}</span>
          <span className="mini-list__dep-station">{dep.scheduled.airportCode}</span>
          <span className="mini-list__arr-station">{arr.scheduled.airportCode}</span>
        </div>
      </Link>
    );
  },
);

FlightsMiniListItem.displayName = "FlightsMiniListItem";
```

- [ ] **Step 5: Run test to verify pass**

Run: `pnpm vitest run src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.test.tsx`

Expected: PASS — all 8 tests pass.

- [ ] **Step 6: Commit**

```bash
git add src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.test.tsx src/features/online-board/components/FlightsMiniList/FlightsMiniList.scss
git commit -m "Add FlightsMiniListItem component with Link navigation"
```

---

### Task 3: FlightsMiniList Container

**Files:**
- Create: `src/features/online-board/components/FlightsMiniList/FlightsMiniList.tsx`
- Create: `src/features/online-board/components/FlightsMiniList/FlightsMiniList.test.tsx`
- Create: `src/features/online-board/components/FlightsMiniList/index.ts`

- [ ] **Step 1: Write failing tests**

Create `src/features/online-board/components/FlightsMiniList/FlightsMiniList.test.tsx`:

```tsx
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "@modern-js/runtime/router";
import { FlightsMiniList } from "./FlightsMiniList.js";
import type { ISimpleFlight } from "../../types.js";

vi.mock("@/i18n/provider.js", () => ({
  useTranslation: () => ({ t: (k: string) => k }),
}));

// Mock scrollIntoView (not implemented in jsdom)
beforeEach(() => {
  Element.prototype.scrollIntoView = vi.fn();
});

function makeFlight(id: string, carrier = "SU", flightNumber = "0022", date = "20260416"): ISimpleFlight {
  return {
    id,
    routeType: "Direct" as const,
    flyingTime: "1h",
    status: "Scheduled" as const,
    flightId: { carrier, flightNumber, suffix: "", date },
    operatingBy: {},
    leg: {
      arrival: {
        scheduled: { airport: "LED", airportCode: "LED", city: "SPB", cityCode: "LED", countryCode: "RU" },
        latest: { airport: "LED", airportCode: "LED", city: "SPB", cityCode: "LED", countryCode: "RU" },
        dispatch: "", gate: "", terminal: "",
        times: { scheduledArrival: { dayChange: { value: 0, title: "" }, local: "12:30", localTime: "", tzOffset: 0, utc: "" } },
      },
      dayChange: 0,
      departure: {
        scheduled: { airport: "SVO", airportCode: "SVO", city: "MOW", cityCode: "MOW", countryCode: "RU" },
        latest: { airport: "SVO", airportCode: "SVO", city: "MOW", cityCode: "MOW", countryCode: "RU" },
        dispatch: "", gate: "", terminal: "", checkingStatus: "Scheduled", parkingStand: "",
        times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "10:00", localTime: "", tzOffset: 0, utc: "" } },
      },
      equipment: {},
      flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
      flyingTime: "1h",
      index: 0,
      operatingBy: {},
      status: "Scheduled" as const,
      updated: "",
    },
  };
}

function renderWithRouter(ui: React.ReactElement) {
  return render(<MemoryRouter>{ui}</MemoryRouter>);
}

describe("FlightsMiniList", () => {
  it("returns null when flights array is empty", () => {
    const current = makeFlight("SU0022-20260416");
    const { container } = renderWithRouter(
      <FlightsMiniList flights={[]} currentFlight={current} lang="ru" />,
    );
    expect(container.firstChild).toBeNull();
  });

  it("returns null when flights array has only one flight", () => {
    const current = makeFlight("SU0022-20260416");
    const { container } = renderWithRouter(
      <FlightsMiniList flights={[current]} currentFlight={current} lang="ru" />,
    );
    expect(container.firstChild).toBeNull();
  });

  it("renders one item per flight when multiple flights", () => {
    const flights = [
      makeFlight("SU0022-20260416", "SU", "0022", "20260416"),
      makeFlight("SU0022-20260417", "SU", "0022", "20260417"),
      makeFlight("SU0022-20260418", "SU", "0022", "20260418"),
    ];
    renderWithRouter(
      <FlightsMiniList flights={flights} currentFlight={flights[0]!} lang="ru" />,
    );
    expect(screen.getByTestId("mini-list-item-SU0022-20260416")).toBeTruthy();
    expect(screen.getByTestId("mini-list-item-SU0022-20260417")).toBeTruthy();
    expect(screen.getByTestId("mini-list-item-SU0022-20260418")).toBeTruthy();
  });

  it("highlights the item matching currentFlight.id", () => {
    const flights = [
      makeFlight("SU0022-20260416", "SU", "0022", "20260416"),
      makeFlight("SU0022-20260417", "SU", "0022", "20260417"),
    ];
    renderWithRouter(
      <FlightsMiniList flights={flights} currentFlight={flights[1]!} lang="ru" />,
    );
    const selected = screen.getByTestId("mini-list-item-SU0022-20260417");
    const notSelected = screen.getByTestId("mini-list-item-SU0022-20260416");
    expect(selected.className).toMatch(/--selected/);
    expect(notSelected.className).not.toMatch(/--selected/);
  });

  it("calls scrollIntoView on the selected item after mount", async () => {
    const flights = [
      makeFlight("SU0022-20260416", "SU", "0022", "20260416"),
      makeFlight("SU0022-20260417", "SU", "0022", "20260417"),
    ];
    renderWithRouter(
      <FlightsMiniList flights={flights} currentFlight={flights[1]!} lang="ru" />,
    );
    // Wait a microtask for useEffect
    await Promise.resolve();
    expect(Element.prototype.scrollIntoView).toHaveBeenCalledWith(
      expect.objectContaining({ block: "center", behavior: "smooth" }),
    );
  });

  it("has data-testid on the container", () => {
    const flights = [
      makeFlight("SU0022-20260416", "SU", "0022", "20260416"),
      makeFlight("SU0022-20260417", "SU", "0022", "20260417"),
    ];
    renderWithRouter(
      <FlightsMiniList flights={flights} currentFlight={flights[0]!} lang="ru" />,
    );
    expect(screen.getByTestId("flights-mini-list")).toBeTruthy();
  });
});
```

- [ ] **Step 2: Run test to verify fail**

Run: `pnpm vitest run src/features/online-board/components/FlightsMiniList/FlightsMiniList.test.tsx`

Expected: FAIL — module not found.

- [ ] **Step 3: Create FlightsMiniList.tsx**

Create `src/features/online-board/components/FlightsMiniList/FlightsMiniList.tsx`:

```tsx
import { type FC, useEffect, useRef } from "react";
import type { ISimpleFlight } from "../../types.js";
import { FlightsMiniListItem } from "./FlightsMiniListItem.js";
import "./FlightsMiniList.scss";

export interface FlightsMiniListProps {
  flights: ISimpleFlight[];
  currentFlight: ISimpleFlight;
  lang: string;
}

export const FlightsMiniList: FC<FlightsMiniListProps> = ({
  flights,
  currentFlight,
  lang,
}) => {
  const itemRefs = useRef<Map<string, HTMLAnchorElement>>(new Map());

  useEffect(() => {
    const selected = itemRefs.current.get(currentFlight.id);
    if (selected) {
      selected.scrollIntoView({ block: "center", behavior: "smooth" });
    }
  }, [currentFlight.id]);

  if (flights.length <= 1) {
    return null;
  }

  return (
    <section className="mini-list" data-testid="flights-mini-list">
      {flights.map((flight) => (
        <FlightsMiniListItem
          key={flight.id}
          ref={(el) => {
            if (el) {
              itemRefs.current.set(flight.id, el);
            } else {
              itemRefs.current.delete(flight.id);
            }
          }}
          flight={flight}
          isSelected={flight.id === currentFlight.id}
          lang={lang}
        />
      ))}
    </section>
  );
};
```

- [ ] **Step 4: Create the index barrel**

Create `src/features/online-board/components/FlightsMiniList/index.ts`:

```typescript
export { FlightsMiniList } from "./FlightsMiniList.js";
export type { FlightsMiniListProps } from "./FlightsMiniList.js";
export { FlightsMiniListItem } from "./FlightsMiniListItem.js";
export type { FlightsMiniListItemProps } from "./FlightsMiniListItem.js";
```

- [ ] **Step 5: Run test to verify pass**

Run: `pnpm vitest run src/features/online-board/components/FlightsMiniList/FlightsMiniList.test.tsx`

Expected: PASS — all 6 tests pass.

- [ ] **Step 6: Commit**

```bash
git add src/features/online-board/components/FlightsMiniList/
git commit -m "Add FlightsMiniList container with scroll-into-view behavior"
```

---

### Task 4: Integrate Mini-List into OnlineBoardDetailsPage

**Files:**
- Modify: `src/features/online-board/components/OnlineBoardDetailsPage.tsx`
- Modify: `src/features/online-board/components/OnlineBoardDetailsPage.test.tsx`

- [ ] **Step 1: Add failing integration tests**

Append these tests to the existing `OnlineBoardDetailsPage.test.tsx` (add a new `describe` block inside the outer describe):

```tsx
describe("mini-list integration", () => {
  it("renders mini-list when multiple flights are returned", () => {
    // The existing test file uses a mockState variable. Mutate it to provide multiple flights.
    // Assumes `makeFlight(id, date)` helper exists or can be adapted from existing mocks.
    const first = mockFlight;
    const second = { ...mockFlight, id: "SU0022-20260417" };
    mockState = { flight: first, allFlights: [first, second], loading: false, error: null };
    render(
      <MemoryRouter>
        <OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />
      </MemoryRouter>,
    );
    expect(screen.getByTestId("flights-mini-list")).toBeTruthy();
  });

  it("does not render mini-list when only one flight is returned", () => {
    mockState = { flight: mockFlight, allFlights: [mockFlight], loading: false, error: null };
    render(
      <MemoryRouter>
        <OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />
      </MemoryRouter>,
    );
    expect(screen.queryByTestId("flights-mini-list")).toBeNull();
  });

  it("renders inside PageLayout (has page-layout class)", () => {
    mockState = { flight: mockFlight, allFlights: [mockFlight], loading: false, error: null };
    const { container } = render(
      <MemoryRouter>
        <OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />
      </MemoryRouter>,
    );
    expect(container.querySelector(".page-layout")).toBeTruthy();
  });
});
```

**Adaptation note:** Read the existing `OnlineBoardDetailsPage.test.tsx` to see how it mocks `useFlightDetails`. The existing test uses a `mockState` variable. Add `allFlights` to every existing `mockState` assignment so the hook returns the new shape. Also add `MemoryRouter` to the existing `render` calls if they aren't already wrapped (the mini-list uses `<Link>` which needs router context).

- [ ] **Step 2: Run tests to verify failures**

Run: `pnpm vitest run src/features/online-board/components/OnlineBoardDetailsPage.test.tsx`

Expected: The 3 new tests fail; existing tests might also fail if `allFlights` isn't set in their mock states (fix those as part of this step).

- [ ] **Step 3: Update all existing mockState assignments**

Find every `mockState = { ... }` or similar in `OnlineBoardDetailsPage.test.tsx`. For each, add `allFlights` to match `flight` semantics:

- When `flight: mockFlight` → add `allFlights: [mockFlight]`
- When `flight: null` → add `allFlights: []`
- When mocking multiple flights → pass them all in `allFlights`

- [ ] **Step 4: Refactor the component to use PageLayout**

Edit `src/features/online-board/components/OnlineBoardDetailsPage.tsx`. Near the top add imports:

```tsx
import { PageLayout } from "@/ui/layout/PageLayout.js";
import { PageTabs } from "@/ui/layout/PageTabs.js";
import { FlightsMiniList } from "./FlightsMiniList/index.js";
```

Then replace the final `return (...)` block (currently starting around line 188 with `<div className="flight-details">`) with:

```tsx
  const legs = getLegs(displayFlight);
  const flightNumber = `${displayFlight.flightId.carrier} ${displayFlight.flightId.flightNumber}`;

  const seoProps = buildFlightDetailsSeo(t, displayFlight, locale, canonicalOrigin);
  const jsonLd = buildFlightJsonLd(displayFlight);

  return (
    <>
      <SeoHead {...seoProps} />
      <JsonLdRenderer data={jsonLd} />
      <PageLayout
        headerLeft={<PageTabs viewType="onlineboard" />}
        title={<h1 className="flight-details__flight-number">{flightNumber}</h1>}
        breadcrumbs={[
          { label: t("BREADCRUMBS.ONLINEBOARD"), href: `/${locale}/onlineboard` },
          { label: flightNumber },
        ]}
        contentLeft={
          <FlightsMiniList
            flights={allFlights}
            currentFlight={displayFlight}
            lang={locale}
          />
        }
      >
        <div className="flight-details" data-testid="flight-details">
          {/* Connection status */}
          <div className="flight-details__status" data-testid="connection-status">
            {connectionStatus === "live" && (
              <span className="connection-badge connection-badge--live">Live</span>
            )}
            {connectionStatus === "reconnecting" && (
              <span className="connection-badge connection-badge--reconnecting">Reconnecting...</span>
            )}
            {connectionStatus === "offline" && (
              <span className="connection-badge connection-badge--offline">Offline</span>
            )}
          </div>

          {/* Flight header (status only; h1 moved to PageLayout title) */}
          <div className="flight-details__header">
            <span className="flight-details__overall-status">{displayFlight.status}</span>
          </div>

          {/* Summary card */}
          <FlightCard flight={displayFlight} />

          {/* Operating carrier */}
          {displayFlight.operatingBy.carrier && (
            <div className="flight-details__operating" data-testid="operating-carrier">
              Operated by: {displayFlight.operatingBy.carrier}
              {displayFlight.operatingBy.flightNumber
                ? ` ${displayFlight.operatingBy.flightNumber}`
                : ""}
            </div>
          )}

          {/* Detailed leg information */}
          <FlightLegs legs={legs} />

          {/* Flying time */}
          <div className="flight-details__flying-time" data-testid="flying-time">
            Total flying time: {displayFlight.flyingTime}
          </div>
        </div>
      </PageLayout>
    </>
  );
```

Also update the hook destructuring near the top:

```tsx
const { flight, allFlights, loading, error } = useFlightDetails(detailsParams);
```

And make sure `allFlights` is in scope where `PageLayout` is rendered. You may need to ensure that loading/error/not-found branches **also** render inside `PageLayout` with the sidebar. For those early-return branches, replace the return with:

```tsx
if (loading) {
  return (
    <PageLayout
      headerLeft={<PageTabs viewType="onlineboard" />}
      breadcrumbs={[{ label: t("BREADCRUMBS.ONLINEBOARD"), href: `/${locale}/onlineboard` }]}
    >
      <FlightListSkeleton count={1} />
    </PageLayout>
  );
}

if (error) {
  return (
    <PageLayout
      headerLeft={<PageTabs viewType="onlineboard" />}
      breadcrumbs={[{ label: t("BREADCRUMBS.ONLINEBOARD"), href: `/${locale}/onlineboard` }]}
    >
      <div className="flight-details flight-details--error">
        Failed to load flight details. Please try again.
      </div>
    </PageLayout>
  );
}

if (!flight) {
  return (
    <PageLayout
      headerLeft={<PageTabs viewType="onlineboard" />}
      breadcrumbs={[{ label: t("BREADCRUMBS.ONLINEBOARD"), href: `/${locale}/onlineboard` }]}
    >
      <div className="flight-details flight-details--not-found">Flight not found.</div>
    </PageLayout>
  );
}
```

- [ ] **Step 5: Check if `BREADCRUMBS.ONLINEBOARD` key exists**

Run: `grep -n "BREADCRUMBS" src/i18n/locales/en/common.json src/i18n/locales/ru/common.json`

If the key doesn't exist, add it to both files. In `src/i18n/locales/en/common.json` find a suitable location (e.g., top level near other similar blocks) and add:

```json
    "BREADCRUMBS": {
        "ONLINEBOARD": "Online Board"
    },
```

And in `src/i18n/locales/ru/common.json`:

```json
    "BREADCRUMBS": {
        "ONLINEBOARD": "Онлайн-табло"
    },
```

- [ ] **Step 6: Run tests to verify pass**

Run: `pnpm vitest run src/features/online-board/components/OnlineBoardDetailsPage.test.tsx`

Expected: All tests pass, including the 3 new mini-list integration tests.

- [ ] **Step 7: Run full suite**

Run: `pnpm test`

Expected: No new failures beyond any pre-existing ones (the previously-skipped feedback button and flight-tab-default tests remain skipped).

- [ ] **Step 8: Run typecheck**

Run: `pnpm typecheck`

Expected: No new errors.

- [ ] **Step 9: Commit**

```bash
git add src/features/online-board/components/OnlineBoardDetailsPage.tsx src/features/online-board/components/OnlineBoardDetailsPage.test.tsx src/i18n/locales/en/common.json src/i18n/locales/ru/common.json
git commit -m "Wire FlightsMiniList into OnlineBoardDetailsPage via PageLayout"
```

---

### Task 5: Manual Browser Verification

**Files:** None — observational.

- [ ] **Step 1: Start both dev servers**

Run in separate terminals (or background):

```bash
cd ClientApp && NODE_OPTIONS=--openssl-legacy-provider npx ng serve --port 4200
pnpm dev:full
```

Wait for both to be ready (Angular on :4200, React on :8080 with proxy).

- [ ] **Step 2: Navigate to a flight details page in the browser**

Open: `http://localhost:8080/ru/onlineboard/SU0022-16042026`

Verify:
- Page loads inside a two-column layout (sidebar + main content)
- Breadcrumbs at top: "Home > Online Board > SU 0022"
- If the real API returns only one flight for this number on this date, the sidebar should be empty (no mini-list)
- If the API returns multiple flights (same number on adjacent dates), the sidebar shows them
- Clicking a different flight item navigates to that flight's URL and the page re-renders
- The selected item has a blue border
- The selected item is visible (scrolled into view)

- [ ] **Step 3: Force multiple flights via Playwright mock**

Since the real dev API may only return one flight per number+date, run this verification script:

```bash
cat << 'SCRIPT' | npx tsx --input-type=module -
import { chromium } from "@playwright/test";
import { mockAngularAPIs } from "./tests/e2e-angular/support/angular-api-mock.js";

const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await mockAngularAPIs(page);

// Inject a mock with multiple flights for SU0022
await page.route("**/onlineboard/details*", (route) => {
  const makeFlight = (dateStr) => ({
    id: `SU0022-${dateStr}`,
    routeType: "Direct",
    flyingTime: "1h 30m",
    status: "Scheduled",
    flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date: dateStr, dateLT: dateStr },
    operatingBy: { carrier: "SU", flightNumber: "0022" },
    leg: {
      index: 0, flyingTime: "1h 30m", status: "Scheduled", updated: "", dayChange: 0,
      flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
      operatingBy: { carrier: "SU", flightNumber: "0022" },
      departure: {
        scheduled: { airport: "Sheremetyevo", airportCode: "SVO", city: "Moscow", cityCode: "MOW", countryCode: "RU" },
        latest: { airport: "Sheremetyevo", airportCode: "SVO", city: "Moscow", cityCode: "MOW", countryCode: "RU" },
        dispatch: "", gate: "", terminal: "", checkingStatus: "Scheduled", parkingStand: "",
        times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "10:00", localTime: "10:00", tzOffset: 3, utc: "07:00" } },
      },
      arrival: {
        scheduled: { airport: "Pulkovo", airportCode: "LED", city: "St Petersburg", cityCode: "LED", countryCode: "RU" },
        latest: { airport: "Pulkovo", airportCode: "LED", city: "St Petersburg", cityCode: "LED", countryCode: "RU" },
        dispatch: "", gate: "", terminal: "",
        times: { scheduledArrival: { dayChange: { value: 0, title: "" }, local: "12:30", localTime: "12:30", tzOffset: 3, utc: "09:30" } },
      },
      equipment: {},
    },
  });
  route.fulfill({
    status: 200,
    contentType: "application/json",
    body: JSON.stringify({
      data: {
        partners: [],
        daysOfFlight: [],
        routes: [
          makeFlight("20260415"),
          makeFlight("20260416"),
          makeFlight("20260417"),
          makeFlight("20260418"),
        ],
      },
    }),
  });
});

await page.goto("http://localhost:8080/ru/onlineboard/SU0022-16042026", { waitUntil: "networkidle" });
await page.waitForTimeout(3000);

const miniList = await page.locator('[data-testid="flights-mini-list"]').count();
console.log("Mini-list container:", miniList);

const items = await page.locator('[data-testid^="mini-list-item-"]').evaluateAll(els =>
  els.map(el => el.getAttribute("data-testid")),
);
console.log("Items:", items);

// Check selected item
const selected = await page.locator('.mini-list__item--selected').evaluate(el =>
  el.getAttribute("data-testid"),
).catch(() => null);
console.log("Selected item:", selected);

await page.screenshot({ path: "/tmp/mini-list-verify.png", fullPage: true });
console.log("Screenshot: /tmp/mini-list-verify.png");

await browser.close();
SCRIPT
```

Expected output:
- Mini-list container: 1
- Items: array of 4 testids (one per date)
- Selected item: `mini-list-item-SU0022-20260416` (the one matching the URL date)
- Screenshot shows sidebar with 4 items, one highlighted with blue border

- [ ] **Step 4: No commit — verification is observational**
