# P3 — Filter + Validation + Search History + Search Execution + Cancel 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:** Bring §4.1.9 (filter attributes + validation + search history), §4.1.10 (Online-Board search execution + errors), §4.1.11 (Schedule search execution + errors), and §4.1.12 (search cancellation) of TZ РИ-07-2538С into compliance.

**Architecture:** Filter components (`OnlineBoardFilter`, `ScheduleFilter`, `CityAutocomplete`, `CityPickerPopup`) and search hooks already exist from phase-1. P3 is primarily an **audit + gap-fill pass** against TZ Tables 11–18: verify each attribute's placeholder/format/behavior rules, add missing validation, fix today/tomorrow/current-week display substitutions, clean up the clear-button (X) UX, and tighten the «Вы искали» search history to match TZ 4.1.9.5.

**Tech Stack:** TypeScript, React 18, PrimeReact (Calendar, Slider), Vitest, Playwright.

**Parent spec:** `docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md`.

**Rule IDs covered:** §4.1.9-R1 through R-final (to be populated in Task 1), §4.1.10, §4.1.11, §4.1.12.

---

## File Structure

### Files to create
- `src/features/online-board/dateLabels.ts` — helpers `replaceWithTodayTomorrow(yyyymmdd)` + `replaceWithCurrentWeek(dateFrom, dateTo)`. Pure functions returning display strings.
- `src/features/online-board/dateLabels.test.ts`.
- `src/shared/hooks/useSearchHistoryNS.ts` — if current `useSearchHistory` doesn't scope items per section (Board vs Schedule, per TZ 4.1.9.5 + 4.1.8-R5), add a section-scoped wrapper. **Verify first before creating.**
- `tests/e2e/p3-filter-search.spec.ts` — Playwright spec covering filter behaviors + search execution.

### Files to modify
- `src/features/online-board/components/OnlineBoardFilter.tsx` — apply TZ-exact placeholders, today/tomorrow substitution on calendar display, clear-button (X) behavior, time-slider 1h minimum gap.
- `src/features/schedule/components/ScheduleFilter.tsx` — same + current-week substitution on Schedule calendar.
- `src/ui/city-autocomplete/CityAutocomplete.tsx` — audit manual-entry rules per §4.1.9.1 (what happens when user types vs selects).
- `src/ui/city-autocomplete/CityPickerPopup.tsx` — audit dictionary picker per §4.1.9.2 (focus behavior, keyboard nav, close-on-outside-click).
- `src/ui/layout/SearchHistory.tsx` + `src/shared/hooks/useSearchHistory.ts` — verify item limit, ordering, per-item icon, click-rehydrate behavior per §4.1.9.5.
- `src/features/online-board/api.ts` / `src/features/schedule/api.ts` — verify error handling branches per §4.1.10.1 / §4.1.11.1.
- `src/features/online-board/components/OnlineBoardSearchPage.tsx` — verify loader visibility + abort-on-navigate per §4.1.12.
- `src/features/schedule/components/ScheduleSearchPage.tsx` — same.
- `docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md` — populate rules (Task 1), mark Done (Task 11).

### Files reviewed, not modified
- `src/shared/state/crossSectionNavigation.ts` — filter state already handled per P1.

---

## Task 1: Populate rule enumeration for §4.1.9/10/11/12

**Files:**
- Modify: `docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md`

Expand skeleton rows in §4.1.9 (and its 5 subsections) + §4.1.10 / §4.1.11 / §4.1.12 with concrete rule rows.

- [ ] **Step 1.1: Read TZ bodies**

Line ranges in `/tmp/ri_tz_extract/content.txt`:
- §4.1.9 (filter attributes): ~524-680
- §4.1.9.1 (manual city entry): within 524-680
- §4.1.9.2 (dictionary city picker): within 524-680
- §4.1.9.3 (filter validation): within 524-680
- §4.1.9.4 (schedule range+return validation): within 524-680
- §4.1.9.5 (search history "Вы искали"): within 524-680
- §4.1.10 (Online-Board search execution): ~681-723
- §4.1.10.1 (OB errors): within 681-723
- §4.1.11 (Schedule search execution): ~724-756
- §4.1.11.1 (Schedule errors): within 724-756
- §4.1.12 (cancel): ~757-766

Use `sed -n '524,766p' /tmp/ri_tz_extract/content.txt` to dump the entire §4.1.9-4.1.12 range.

- [ ] **Step 1.2: Enumerate rules**

For each subsection, enumerate every concrete assertion:
- §4.1.9 Tables 11/12/13/14/15: placeholder + format + behavior per attribute per mode per geo-state.
- §4.1.9.1: manual-entry behavior (filter strip diacritics, case-insensitive, highlight match, no-match state, backspace).
- §4.1.9.2: picker behavior (opens on focus, keyboard arrows, Enter selects, Escape closes, click-outside closes, tab order).
- §4.1.9.3: per-attribute validation rules, error-tooltip placement, submit-blocking vs inline.
- §4.1.9.4: `Показать расписание на` range ≤ 7 days, `Дата обратного рейса` after outbound date.
- §4.1.9.5: «Вы искали» — max items, dedup, ordering, icon per kind (Board/Schedule), click rehydrates filter + navigates, cleared-on-session-end.
- §4.1.10 / §4.1.11: explicit-submit, loader visibility, endpoint routing per mode, URL-first navigation, no search on attribute change.
- §4.1.10.1 / §4.1.11.1: network timeout, 4xx/5xx handling, empty result state, retry affordance.
- §4.1.12: AbortController cancels in-flight, Escape cancels, navigate/tab-switch cancels.

Target rule count: §4.1.9 ≥60 rules, §4.1.10 ≥10, §4.1.11 ≥10, §4.1.12 ≥5. Total P3 additions ~85–100 rules.

- [ ] **Step 1.3: Update Coverage summary**

Bump `Total rules extracted` by the added count.

- [ ] **Step 1.4: Commit**

```bash
git add docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md
git commit -m "Populate rule rows for P3 subsections 4.1.9/10/11/12 in TZ audit spec"
```

---

## Task 2: Today / Tomorrow date-label substitution (Online-Board)

**Files:**
- Create: `src/features/online-board/dateLabels.ts`
- Create: `src/features/online-board/dateLabels.test.ts`
- Modify: `src/features/online-board/components/OnlineBoardFilter.tsx` (use the helper to display Calendar value)

Per TZ Table 11/12: when the selected date is today, the calendar input should display `"Сегодня"` (localized). When tomorrow, `"Завтра"`. Otherwise `DD.MM.YYYY`.

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

```ts
// src/features/online-board/dateLabels.test.ts
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { formatDateWithTodayTomorrow } from "./dateLabels.js";

describe("4.1.9-R: formatDateWithTodayTomorrow (Board calendar display)", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date(2026, 4, 15, 12, 0, 0));
  });
  afterEach(() => { vi.useRealTimers(); });

  it("returns 'Сегодня' for today's date", () => {
    const t = (k: string) => (k === "SHARED.TODAY" ? "Сегодня" : k === "SHARED.TOMORROW" ? "Завтра" : k);
    expect(formatDateWithTodayTomorrow(new Date(2026, 4, 15), t)).toBe("Сегодня");
  });

  it("returns 'Завтра' for tomorrow's date", () => {
    const t = (k: string) => (k === "SHARED.TODAY" ? "Сегодня" : k === "SHARED.TOMORROW" ? "Завтра" : k);
    expect(formatDateWithTodayTomorrow(new Date(2026, 4, 16), t)).toBe("Завтра");
  });

  it("returns dd.MM.yyyy for other dates", () => {
    const t = (k: string) => k;
    expect(formatDateWithTodayTomorrow(new Date(2026, 4, 17), t)).toBe("17.05.2026");
    expect(formatDateWithTodayTomorrow(new Date(2026, 11, 31), t)).toBe("31.12.2026");
  });

  it("returns dd.MM.yyyy for yesterday (not 'Вчера')", () => {
    // TZ only has Today/Tomorrow substitutions, not Yesterday.
    const t = (k: string) => k;
    expect(formatDateWithTodayTomorrow(new Date(2026, 4, 14), t)).toBe("14.05.2026");
  });

  it("returns empty string for null/undefined", () => {
    expect(formatDateWithTodayTomorrow(null, () => "")).toBe("");
    expect(formatDateWithTodayTomorrow(undefined, () => "")).toBe("");
  });
});
```

- [ ] **Step 2.2: Run — FAIL**

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

- [ ] **Step 2.3: Implement**

```ts
// src/features/online-board/dateLabels.ts
/**
 * Board calendar date-label substitution per TZ §4.1.9 Tables 11 + 12.
 * Today → "Сегодня", tomorrow → "Завтра", otherwise dd.MM.yyyy.
 *
 * Only substitutes Today and Tomorrow per TZ — yesterday stays dd.MM.yyyy.
 */

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TFunction = (key: string, opts?: any) => string;

export function formatDateWithTodayTomorrow(
  date: Date | null | undefined,
  t: TFunction,
): string {
  if (!date) return "";
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const d = new Date(date);
  d.setHours(0, 0, 0, 0);
  const deltaDays = Math.round((d.getTime() - today.getTime()) / 86_400_000);
  if (deltaDays === 0) return t("SHARED.TODAY");
  if (deltaDays === 1) return t("SHARED.TOMORROW");
  const day = String(d.getDate()).padStart(2, "0");
  const month = String(d.getMonth() + 1).padStart(2, "0");
  return `${day}.${month}.${d.getFullYear()}`;
}
```

- [ ] **Step 2.4: Run — PASS**

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

- [ ] **Step 2.5: Wire into OnlineBoardFilter**

Find where the PrimeReact `<Calendar>` component is rendered in `OnlineBoardFilter.tsx` (flight-number mode + route mode). Find its `dateFormat` or `formatDateTime` / `value` prop. Add a `formatDateTime` callback that uses `formatDateWithTodayTomorrow`:

```tsx
import { formatDateWithTodayTomorrow } from "../dateLabels.js";

<Calendar
  value={date}
  onChange={(e) => setDate(e.value as Date)}
  // ...
  formatDateTime={(date) => formatDateWithTodayTomorrow(date, t)}
/>
```

(PrimeReact API: check if `formatDateTime` is the right prop name for the current version. If not, use `value={formatDateWithTodayTomorrow(date, t)}` with a custom Input approach. Adapt to the actual PrimeReact version in the project.)

- [ ] **Step 2.6: Add component test**

In `OnlineBoardFilter.test.tsx` (or create if missing), add a test asserting the input reads "Сегодня" when today's date is selected with a frozen clock.

- [ ] **Step 2.7: Run all Online-Board tests**

Run: `pnpm vitest run src/features/online-board/`
Expected: PASS.

- [ ] **Step 2.8: Commit**

```bash
git add src/features/online-board/dateLabels.ts src/features/online-board/dateLabels.test.ts src/features/online-board/components/OnlineBoardFilter.tsx
git commit -m "Add Today/Tomorrow label substitution to Online-Board date picker per TZ 4.1.9 Tables 11+12"
```

---

## Task 3: Current-Week label substitution (Schedule)

**Files:**
- Create: `src/features/schedule/dateLabels.ts`
- Create: `src/features/schedule/dateLabels.test.ts`
- Modify: `src/features/schedule/components/ScheduleFilter.tsx`

Per TZ Table 14 (Schedule `Показать расписание на`): if the selected date range corresponds to the current week (Mon-Sun), display `"Текущая неделя"` instead of `DD.MM.YYYY-DD.MM.YYYY`.

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

```ts
// src/features/schedule/dateLabels.test.ts
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { formatScheduleDateRangeWithCurrentWeek } from "./dateLabels.js";

describe("4.1.9-R: formatScheduleDateRangeWithCurrentWeek", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date(2026, 4, 15, 12, 0, 0)); // Fri 2026-05-15
  });
  afterEach(() => { vi.useRealTimers(); });

  it("returns 'Текущая неделя' when range = Mon-Sun of current week", () => {
    const t = (k: string) => (k === "SCHEDULE.CURRENT-WEEK" ? "Текущая неделя" : k);
    expect(
      formatScheduleDateRangeWithCurrentWeek(
        new Date(2026, 4, 11), // Mon
        new Date(2026, 4, 17), // Sun
        t,
      ),
    ).toBe("Текущая неделя");
  });

  it("returns dd.MM.yyyy-dd.MM.yyyy for other ranges", () => {
    const t = (k: string) => k;
    expect(
      formatScheduleDateRangeWithCurrentWeek(
        new Date(2026, 4, 18), // Mon next week
        new Date(2026, 4, 24), // Sun next week
        t,
      ),
    ).toBe("18.05.2026-24.05.2026");
  });

  it("returns dd.MM.yyyy-dd.MM.yyyy for partial current week (not full Mon-Sun)", () => {
    const t = (k: string) => k;
    expect(
      formatScheduleDateRangeWithCurrentWeek(
        new Date(2026, 4, 13), // Wed
        new Date(2026, 4, 17), // Sun
        t,
      ),
    ).toBe("13.05.2026-17.05.2026");
  });

  it("returns empty string for null inputs", () => {
    expect(formatScheduleDateRangeWithCurrentWeek(null, null, () => "")).toBe("");
  });
});
```

- [ ] **Step 3.2: Run — FAIL**

- [ ] **Step 3.3: Implement**

```ts
// src/features/schedule/dateLabels.ts
/**
 * Schedule range-calendar label substitution per TZ §4.1.9 Table 14.
 * Current week Mon-Sun → "Текущая неделя", otherwise dd.MM.yyyy-dd.MM.yyyy.
 */

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TFunction = (key: string, opts?: any) => string;

function toYmd(d: Date): string {
  const day = String(d.getDate()).padStart(2, "0");
  const month = String(d.getMonth() + 1).padStart(2, "0");
  return `${day}.${month}.${d.getFullYear()}`;
}

function mondayOfWeek(base: Date): Date {
  const d = new Date(base);
  d.setHours(0, 0, 0, 0);
  const offset = (d.getDay() + 6) % 7; // Mon = 0, Sun = 6
  d.setDate(d.getDate() - offset);
  return d;
}

export function formatScheduleDateRangeWithCurrentWeek(
  dateFrom: Date | null | undefined,
  dateTo: Date | null | undefined,
  t: TFunction,
): string {
  if (!dateFrom || !dateTo) return "";

  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const thisMon = mondayOfWeek(today);
  const thisSun = new Date(thisMon);
  thisSun.setDate(thisSun.getDate() + 6);

  const from = new Date(dateFrom);
  from.setHours(0, 0, 0, 0);
  const to = new Date(dateTo);
  to.setHours(0, 0, 0, 0);

  if (from.getTime() === thisMon.getTime() && to.getTime() === thisSun.getTime()) {
    return t("SCHEDULE.CURRENT-WEEK");
  }
  return `${toYmd(from)}-${toYmd(to)}`;
}
```

- [ ] **Step 3.4: Run — PASS**

- [ ] **Step 3.5: Wire into ScheduleFilter**

Find the calendar render in `ScheduleFilter.tsx` and apply the formatter to display the selected range.

- [ ] **Step 3.6: i18n key**

Ensure `SCHEDULE.CURRENT-WEEK` exists in `ru-ru` locale = `"Текущая неделя"` and in `en-us` = `"Current week"`. Other 7 locales: empty string per project pattern.

- [ ] **Step 3.7: Run tests**

Run: `pnpm vitest run src/features/schedule/`
Expected: PASS.

- [ ] **Step 3.8: Commit**

```bash
git add src/features/schedule/dateLabels.ts src/features/schedule/dateLabels.test.ts src/features/schedule/components/ScheduleFilter.tsx src/i18n/locales/
git commit -m "Add Current-Week label substitution to Schedule date-range picker per TZ 4.1.9 Table 14"
```

---

## Task 4: Clear-button (X) behavior on filter fields

**Files:**
- Modify: `src/features/online-board/components/OnlineBoardFilter.tsx`
- Modify: `src/features/schedule/components/ScheduleFilter.tsx`
- Modify: `src/ui/city-autocomplete/CityAutocomplete.tsx` (if the X is handled there)

Per TZ Tables 11/12/14 and Table 13: every filter attribute with a value must show a clear-button (X). Clicking X empties the attribute and displays its placeholder. When empty, X is hidden.

- [ ] **Step 4.1: Inspect current state**

Read the three filter files. Note which attributes currently have X buttons and which don't. Likely candidates for missing X:
- Flight-number input
- Calendar value (PrimeReact may have `showIcon` but not a clear X)
- Time-range slider (probably doesn't have a clear button — acceptable, slider has no "empty" state per TZ)

Identify gaps.

- [ ] **Step 4.2: Write failing tests per field**

For each filter attribute (flight number, departure city, arrival city, date, schedule range, return date):

```tsx
it("4.1.9-R: <field> shows X button when filled, hides when empty", async () => {
  render(<OnlineBoardFilter ... />);
  const input = screen.getByTestId("<field>");
  expect(screen.queryByTestId("<field>-clear")).not.toBeInTheDocument();
  userEvent.type(input, "SU1234");
  expect(screen.getByTestId("<field>-clear")).toBeInTheDocument();
  userEvent.click(screen.getByTestId("<field>-clear"));
  expect(input).toHaveValue("");
  expect(screen.queryByTestId("<field>-clear")).not.toBeInTheDocument();
});
```

- [ ] **Step 4.3: Run — FAIL on fields missing X**

- [ ] **Step 4.4: Add X button to missing fields**

For each field missing X, add a clear button that:
- Is rendered only when the field has a value (conditional render on `value && value.length > 0`).
- Has `data-testid="<field>-clear"`.
- Has `aria-label={t("SHARED.CLEAR")}` for a11y.
- Click handler clears the field value + refocuses the input.

For PrimeReact `<Calendar>`: use `showButtonBar` + `showIcon` or wrap with a custom clear button overlaid per existing filter styling.

- [ ] **Step 4.5: Run — PASS**

Run: `pnpm vitest run src/features/online-board/ src/features/schedule/`

- [ ] **Step 4.6: Commit**

```bash
git add src/features/online-board/components/OnlineBoardFilter.tsx src/features/schedule/components/ScheduleFilter.tsx src/ui/city-autocomplete/CityAutocomplete.tsx src/i18n/locales/
git commit -m "Add clear-button (X) to filter fields per TZ 4.1.9 Tables 11/12/14"
```

---

## Task 5: Time-slider 1-hour minimum gap

**Files:**
- Modify: `src/features/online-board/components/OnlineBoardFilter.tsx`
- Modify: `src/features/schedule/components/ScheduleFilter.tsx`

Per TZ Table 12/14: `Время рейса` / `Время вылета` slider must enforce `(timeTo - timeFrom) ≥ 60 minutes`. Cannot set `timeTo` before `timeFrom`.

- [ ] **Step 5.1: Inspect current slider setup**

Read the `<Slider>` (PrimeReact) bindings in both filter components. Look for `min`, `max`, `step`, `range` props and the change handler.

- [ ] **Step 5.2: Add failing test**

```tsx
it("4.1.9-R: time slider enforces 1h minimum gap", () => {
  render(<OnlineBoardFilter ... />);
  // Simulate setting timeTo equal to timeFrom (e.g. both at 12:00)
  // Expected: timeTo is auto-adjusted to timeFrom + 60 minutes
});
```

- [ ] **Step 5.3: Implement clamp**

In the slider `onChange` handler, after getting `[from, to]` values, enforce:

```tsx
const onSliderChange = (e: SliderChangeEvent) => {
  const [from, to] = e.value as [number, number];
  const MIN_GAP = 60; // minutes
  let nextTo = to;
  if (to - from < MIN_GAP) {
    // Push `to` forward or `from` back, clamped to bounds.
    nextTo = from + MIN_GAP;
    if (nextTo > 1440) {
      // can't extend — clamp `from` instead
      setRange([1440 - MIN_GAP, 1440]);
      return;
    }
  }
  setRange([from, nextTo]);
};
```

- [ ] **Step 5.4: Run — PASS**

Run: `pnpm vitest run src/features/online-board/ src/features/schedule/`

- [ ] **Step 5.5: Commit**

```bash
git add src/features/online-board/components/OnlineBoardFilter.tsx src/features/schedule/components/ScheduleFilter.tsx
git commit -m "Enforce 1h minimum gap on time-range slider per TZ 4.1.9 Tables 12/14"
```

---

## Task 6: City autocomplete — manual-entry rules (§4.1.9.1)

**Files:**
- Verify: `src/ui/city-autocomplete/CityAutocomplete.tsx`
- Verify: `src/ui/city-autocomplete/searchCities.ts`

Per §4.1.9.1 (read the full text): when user types into the city field:
- Case-insensitive match.
- Accent-insensitive (e.g. "mosco" matches "Москва"? probably language-specific — check TZ).
- Show suggestions in a dropdown.
- No-match state: show "Нет результатов" or similar.
- Typing does NOT submit.
- Exact-match typed value can be auto-committed (already covered by existing commit `9efc76b` per memory).

- [ ] **Step 6.1: Read TZ §4.1.9.1 body** in full.

- [ ] **Step 6.2: Inspect current behavior** — read `CityAutocomplete.tsx` + `searchCities.ts` + tests.

- [ ] **Step 6.3: Enumerate gaps** against TZ.

- [ ] **Step 6.4: Add failing tests for each gap.**

- [ ] **Step 6.5: Fix.**

- [ ] **Step 6.6: Run tests + commit.**

```bash
git add src/ui/city-autocomplete/
git commit -m "Audit CityAutocomplete manual-entry behavior per TZ 4.1.9.1"
```

(Task is inspection-heavy and depends on current state; cannot pre-write exact test code without reading the file first. Implementer subagent will enumerate gaps at kickoff.)

---

## Task 7: City picker popup — dictionary rules (§4.1.9.2)

**Files:**
- Verify: `src/ui/city-autocomplete/CityPickerPopup.tsx`

Per §4.1.9.2:
- Opens on focus / click.
- Keyboard arrow-key navigation.
- Enter commits; Escape closes without commit.
- Click-outside closes.
- List is scrollable; items grouped by country (check TZ Table 13 for exact grouping).
- Selected item highlights.

- [ ] **Step 7.1: Read TZ §4.1.9.2** fully.

- [ ] **Step 7.2: Inspect `CityPickerPopup.tsx`** + tests.

- [ ] **Step 7.3: Gap-list, failing tests, fix, commit.**

```bash
git add src/ui/city-autocomplete/
git commit -m "Audit CityPickerPopup dictionary-picker behavior per TZ 4.1.9.2"
```

---

## Task 8: Filter validation (§4.1.9.3 + §4.1.9.4)

**Files:**
- Modify: `src/features/online-board/components/OnlineBoardFilter.tsx` (validation messages + submit guards)
- Modify: `src/features/schedule/components/ScheduleFilter.tsx`

§4.1.9.3 — per-attribute validation:
- Flight number: format `SU####`, 4 digits, pad as needed.
- Date: within `-1/+14` (Board) or `-1/+330` (Schedule) window.
- City fields: must be selected from dictionary (not arbitrary text) — OR accept exact-match typed value per existing behavior.
- Error tooltip placement: inline below the field or via toast — check TZ mockup.

§4.1.9.4 — Schedule-specific:
- `Показать расписание на` range: max ≤ 7 days (one week).
- `Дата обратного рейса` must be AFTER outbound `dateTo`.
- Return date range: also max 7 days.

- [ ] **Step 8.1: Read TZ §4.1.9.3 + §4.1.9.4** fully.

- [ ] **Step 8.2: Inspect current validation** (`OnlineBoardFilter.tsx` + `ScheduleFilter.tsx`). Look for existing error-state handling.

- [ ] **Step 8.3: Enumerate gaps.**

- [ ] **Step 8.4: Write failing tests per rule.**

- [ ] **Step 8.5: Fix.**

- [ ] **Step 8.6: Commit.**

```bash
git add src/features/online-board/ src/features/schedule/ src/i18n/
git commit -m "Tighten filter validation per TZ 4.1.9.3 + 4.1.9.4"
```

---

## Task 9: Search history «Вы искали» (§4.1.9.5)

**Files:**
- Verify: `src/shared/hooks/useSearchHistory.ts`
- Verify: `src/ui/layout/SearchHistory.tsx`
- Possibly create: `src/shared/hooks/useSearchHistoryNS.ts` (section-namespaced wrapper)

Per §4.1.9.5:
- Recent searches displayed as a list with icons per kind (Board route / Board flight-number / Schedule / etc.).
- Item limit: TZ specifies (check for exact number — typically 5 or 10).
- Ordering: most-recent first.
- Dedup: same search (same cities + date) doesn't appear twice; re-search pushes to top.
- Session-scoped (cleared on page reload per P1 cross-section store semantics, OR persisted — check TZ).
- Click rehydrates filter + navigates to results.
- Per §4.1.8-R5: history is shared between Board and Schedule but NOT Flight-Map.

- [ ] **Step 9.1: Read TZ §4.1.9.5** fully.

- [ ] **Step 9.2: Inspect `useSearchHistory.ts` + `SearchHistory.tsx`** + tests.

- [ ] **Step 9.3: Gap-list.**

- [ ] **Step 9.4: Fix.**

- [ ] **Step 9.5: Commit.**

```bash
git add src/shared/hooks/useSearchHistory.ts src/ui/layout/SearchHistory.tsx
git commit -m "Audit «Вы искали» search history per TZ 4.1.9.5"
```

---

## Task 10: Search execution + cancellation + error handling (§4.1.10, §4.1.11, §4.1.12)

**Files:**
- Verify: `src/features/online-board/api.ts` + `hooks/useOnlineBoard.ts`
- Verify: `src/features/schedule/api.ts` + related hooks
- Verify: `src/ui/errors/*`

§4.1.10: OB search execution — explicit submit, loader visibility, URL-first navigation, endpoint routing per mode. §4.1.10.1: errors (timeout, 4xx, 5xx, empty) each with a specific message.

§4.1.11: same for Schedule.

§4.1.12: cancellation — AbortController on new search, Escape cancels, navigate/tab-switch cancels.

- [ ] **Step 10.1: Read TZ §4.1.10, 4.1.10.1, 4.1.11, 4.1.11.1, 4.1.12** fully.

- [ ] **Step 10.2: Inspect current API + hook files** for AbortController usage + error-state branches.

- [ ] **Step 10.3: Gap-list.**

- [ ] **Step 10.4: Add tests + fixes.**

- [ ] **Step 10.5: Commit.**

```bash
git add src/features/online-board/ src/features/schedule/ src/ui/errors/
git commit -m "Search execution + cancellation + error handling per TZ 4.1.10/11/12"
```

---

## Task 11: Update spec with P3 completion

**Files:**
- Modify: `docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md`

- [ ] **Step 11.1**: Mark each P3 rule `Done` with the relevant commit SHA (from `git log --oneline main..HEAD`).

- [ ] **Step 11.2**: Add any new Conflicts encountered.

- [ ] **Step 11.3**: Append merge-log row.

- [ ] **Step 11.4**: Update coverage summary counts.

- [ ] **Step 11.5**: Commit.

```bash
git add docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md
git commit -m "Mark P3 (filter + validation + history + search) rules Done in TZ audit spec"
```

---

## Task 12: Verify + merge gate

- [ ] **Step 12.1**: `pnpm typecheck && pnpm lint && pnpm vitest run` — all pass.

- [ ] **Step 12.2**: `pnpm test:e2e` — if any P3-specific e2e spec was added.

- [ ] **Step 12.3**: Invoke `superpowers:finishing-a-development-branch`.

---

## Self-Review

**1. Spec coverage.** §4.1.9 (all 5 subsections) + §4.1.10 + §4.1.11 + §4.1.12 — Tasks 2–10 cover the rule clusters. Rule population in Task 1 ensures every rule has an ID.

**2. Placeholder scan.** Tasks 6/7/8/9/10 use "read full TZ body, enumerate gaps, fix" language rather than pre-written code because the existing filter components are large and the TZ-vs-impl gap is unknown without an inspection pass. Each of these tasks will generate focused subtasks during kickoff — this is acceptable for an audit-style plan where the exhaustive detail lives in the audit tables of the spec.

**3. Type consistency.**
- `formatDateWithTodayTomorrow` (Task 2) and `formatScheduleDateRangeWithCurrentWeek` (Task 3) share the same `TFunction` signature.
- Clear-button pattern (Task 4) uses consistent `data-testid="<field>-clear"` convention.
- Time-range clamping (Task 5) uses minutes-since-midnight consistently.

---

## Execution Handoff

Plan complete. Execution options: Subagent-Driven (recommended) or Inline.
