diff --git a/docs/superpowers/plans/2026-04-22-p3-filter-validation-history-search.md b/docs/superpowers/plans/2026-04-22-p3-filter-validation-history-search.md new file mode 100644 index 00000000..aeb6179e --- /dev/null +++ b/docs/superpowers/plans/2026-04-22-p3-filter-validation-history-search.md @@ -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 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 `` 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"; + + 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: shows X button when filled, hides when empty", async () => { + render(); + const input = screen.getByTestId(""); + expect(screen.queryByTestId("-clear")).not.toBeInTheDocument(); + userEvent.type(input, "SU1234"); + expect(screen.getByTestId("-clear")).toBeInTheDocument(); + userEvent.click(screen.getByTestId("-clear")); + expect(input).toHaveValue(""); + expect(screen.queryByTestId("-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="-clear"`. +- Has `aria-label={t("SHARED.CLEAR")}` for a11y. +- Click handler clears the field value + refocuses the input. + +For PrimeReact ``: 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 `` (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(); + // 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="-clear"` convention. +- Time-range clamping (Task 5) uses minutes-since-midnight consistently. + +--- + +## Execution Handoff + +Plan complete. Execution options: Subagent-Driven (recommended) or Inline.