Add P3 implementation plan: filter + validation + search history + search execution

This commit is contained in:
2026-04-21 19:40:04 +03:00
parent 268205fc2f
commit 3b32233b88
@@ -0,0 +1,683 @@
# 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 1118: 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 ~85100 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 210 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.