diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index 75d1286..ac83f60 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -19,6 +19,7 @@ export type Catch = { id: string; fish: string; weight_g: number; bait: string | export type DictionaryItem = { id: string; slug: string; name_ru: string; unlock_level?: number | null; fish_species_count?: number | null; source_system?: string | null; source_external_id?: string | null; source_url?: string | null; description?: string | null; source_aliases?: string[] | null; source_fish_species?: string[] | null; source_image_urls?: string[] | null; source_point_urls?: string[] | null; source_checked_at?: string | null }; export type TackleItem = { id: string; name: string; category: string; subcategory: string | null; brand: string | null; family: string | null; unlock_level: number | null; source_system: string | null; source_external_id: string | null; source_url: string | null; source_checked_at: string | null; missing_fields: string[] }; export type PaginatedTackleItems = { items: TackleItem[]; total: number; limit: number; offset: number }; +export type TackleCombination = { role: string; value: string; catches: number; unique_players: number; last_seen_at: string; status: "recommendation" | "insufficient_data"; explanation: string }; export type OfficialRecord = { id: string; fish: string; weight_g: number; waterbody: string; bait: string | null; player_name: string | null; record_date: string | null; category: string | null; region: string | null; source_url: string | null; source_system: string }; export type PaginatedOfficialRecord = { items: OfficialRecord[]; diff --git a/apps/web/src/pages/tackle/analytics.astro b/apps/web/src/pages/tackle/analytics.astro new file mode 100644 index 0000000..c35d4a3 --- /dev/null +++ b/apps/web/src/pages/tackle/analytics.astro @@ -0,0 +1,40 @@ +--- +import Layout from "../../layouts/Layout.astro"; +import PageHero from "../../components/PageHero.astro"; +import StatePanel from "../../components/StatePanel.astro"; +import { ago, api, type DictionaryItem, type TackleCombination } from "../../lib/api"; + +const params = Astro.url.searchParams; +const waterbody = params.get("waterbody") ?? ""; +const fish = params.get("fish") ?? ""; +const method = params.get("method") ?? ""; +const hours = [24, 72, 168].includes(Number(params.get("hours"))) ? Number(params.get("hours")) : 72; +const query = new URLSearchParams({ hours: String(hours), min_samples: "3", min_players: "2" }); +if (waterbody) query.set("waterbody", waterbody); +if (fish) query.set("fish", fish); +if (method) query.set("method", method); +let combinations: TackleCombination[] = [], waters: DictionaryItem[] = [], fishes: DictionaryItem[] = [], unavailable = false; +try { + [combinations, waters, fishes] = await Promise.all([ + api(`/api/v1/analytics/tackle?${query}`), + api("/api/v1/waterbodies?limit=500"), + api("/api/v1/fishes?limit=500"), + ]); +} catch { unavailable = true; } +if (unavailable) { Astro.response.status = 503; Astro.response.headers.set("Retry-After", "60"); Astro.response.headers.set("Cache-Control", "no-store"); } +const methodLabels: Record = { spinning: "Спиннинг", bottom: "Донная", float: "Поплавочная" }; +const roleLabels: Record = { lure: "Приманка", bait: "Наживка", rig: "Монтаж", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", float: "Поплавок", sinker: "Груз" }; +--- + + +
+ + + + + + {(waterbody || fish || method || hours !== 72) && Сбросить} +
+

Порог рекомендации: минимум 3 наблюдения от 2 независимых игроков. Это не рейтинг снасти и не гарантия улова.

+ {unavailable ? : combinations.length ?
{combinations.map(item =>
{roleLabels[item.role] ?? item.role}{item.status === "recommendation" ? "Рекомендация" : "Недостаточно данных"}

{item.value}

Наблюдения
{item.catches}
Игроки
{item.unique_players}
Последнее
{ago(item.last_seen_at)}

{item.explanation}

)}
:
} +
diff --git a/apps/web/src/pages/tackle/index.astro b/apps/web/src/pages/tackle/index.astro index e1757f0..a4beb7d 100644 --- a/apps/web/src/pages/tackle/index.astro +++ b/apps/web/src/pages/tackle/index.astro @@ -35,6 +35,7 @@ if (family) filterParams.set("family", family); {(category || brand || family) && Сбросить} + Сочетания снастей {unavailable ? : result.items.length ?
{result.items.map(item => {categoryLabels[item.category] ?? item.category}{item.name}{[item.brand, item.family].filter(Boolean).join(" · ") || "Характеристики уточняются"}
)}
: } {!unavailable && } diff --git a/apps/web/tests/tackle.spec.ts b/apps/web/tests/tackle.spec.ts index 9be650c..3af0193 100644 --- a/apps/web/tests/tackle.spec.ts +++ b/apps/web/tests/tackle.spec.ts @@ -23,3 +23,12 @@ test("unknown tackle detail has a navigable not-found state", async ({ page }) = await expect(page.getByRole("heading", { name: "Такой карточки нет в каталоге" })).toBeVisible(); await expect(page.getByRole("link", { name: "Открыть каталог" })).toHaveAttribute("href", "/tackle"); }); + +test("tackle analytics keeps an honest empty state", async ({ page }) => { + await page.goto("/tackle/analytics"); + + await expect(page.getByRole("heading", { name: "Сочетания снастей" })).toBeVisible(); + await expect(page.getByLabel("Фильтры сочетаний снастей")).toBeVisible(); + await expect(page.locator("[data-analytics-results]")).toBeVisible(); + await expect(page.getByText(/не рейтинг снасти/i)).toBeVisible(); +}); diff --git a/apps/web/tests/visual-matrix.spec.ts b/apps/web/tests/visual-matrix.spec.ts index 87ec312..824bec6 100644 --- a/apps/web/tests/visual-matrix.spec.ts +++ b/apps/web/tests/visual-matrix.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from "@playwright/test"; const routes = [ - "/", "/spots/vyunok-6331x6332", "/plan", "/waterbodies", "/tackle", "/records", "/report", "/status", "/media", "/rules", + "/", "/spots/vyunok-6331x6332", "/plan", "/waterbodies", "/tackle", "/tackle/analytics", "/records", "/report", "/status", "/media", "/rules", "/waterbodies/р-вьюнок", "/fish/pike", "/admin", "/admin/moderation", "/admin/external-sources", "/admin/media", ]; diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 41ab0d5..689b4fe 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -45,7 +45,7 @@ - [x] **B22 · Сравнение вариантов разных источников.** Offline-команда `--compare-quality-upgrades` сопоставила сохранённые кандидаты с опубликованными fallback по `duplicate_of` и проверила dimensions, размер файла, MIME, прозрачность и aspect ratio. Все 226 кандидатов прошли технические пороги и offline contact-sheet review; источник RF4DB/RF4MAP/официальный RF4 не получал автоматического приоритета. После публикации в manifest не осталось `upgrade_stored`, поэтому текущий повторный запуск команды корректно сообщает `compared: 0`. - [x] **B23 · Безопасное продвижение и provenance.** Связи `supersedes`/`replaced_by`, отдельное решение review, атомарное переключение публичного `entity_key` и явный CLI rollback сохраняют обе исходные ссылки и возможность отката. Публичная карточка показывает источник выбранного изображения, а manifest сохраняет проверенные варианты. Старый Git-файл не удаляется при включении нового. - [x] **B24 · Производные размеры.** После выбора оригиналов генерируются детерминированные WebP/AVIF thumbnails для каталога и отдельный крупный вариант для detail, хэши производных фиксируются в manifest и отдаются через каталог. Маленькие карточки больше не обязаны загружать 1024×1024 оригинал. -- [ ] **B25 · Визуальная приёмка media.** Contact sheets для всех 226 замен собраны и просмотрены offline: явных ложных соответствий, обрезки и искажённых пропорций не обнаружено; одно отличие подписи (`Ерш-носарь`/`Ёрш-носарь`) орфографическое. In-app Chromium проверил 12 комбинаций `/media` (320/390/768/1280 px × light/dark/system): горизонтального overflow нет, у 253 изображений есть `alt`, broken loaded images — `0`, переключение темы корректно меняет `data-theme`; automated media acceptance теперь отдельно проверяет loaded assets и разделы `waterbody/tackle/reference`, keyboard/focus и print-regression проходят, standalone axe — `9/9` маршрутов. Launcher `scripts/capture-visual-matrix.mjs` расширен до матрицы `192` PNG; завершённый capture предыдущего прохода содержит `168` PNG, а representative manual review зафиксирован в `docs/ux-reference-review-2026-09-21.md`. Gate: нет битых файлов, искажённых пропорций, ложных соответствий, обрезанного объекта и изображений ниже 256 px без явной пометки «низкое разрешение». +- [ ] **B25 · Визуальная приёмка media.** Contact sheets для всех 226 замен собраны и просмотрены offline: явных ложных соответствий, обрезки и искажённых пропорций не обнаружено; одно отличие подписи (`Ерш-носарь`/`Ёрш-носарь`) орфографическое. In-app Chromium проверил 12 комбинаций `/media` (320/390/768/1280 px × light/dark/system): горизонтального overflow нет, у 253 изображений есть `alt`, broken loaded images — `0`, переключение темы корректно меняет `data-theme`; automated media acceptance теперь отдельно проверяет loaded assets и разделы `waterbody/tackle/reference`, keyboard/focus и print-regression проходят, standalone axe — `9/9` маршрутов. Launcher `scripts/capture-visual-matrix.mjs` расширен до матрицы `204` PNG; завершённый capture предыдущего прохода содержит `168` PNG, а representative manual review зафиксирован в `docs/ux-reference-review-2026-09-21.md`. Gate: нет битых файлов, искажённых пропорций, ложных соответствий, обрезанного объекта и изображений ниже 256 px без явной пометки «низкое разрешение». ### Каталог водоёмов, карты и координаты @@ -72,7 +72,7 @@ - [ ] **G04 · Crosswalk и нормализация.** Добавлен offline `gear_crosswalk`: нормализация регистра/пробелов/`е/ё`, точное имя или alias плюс совместимая категория, консервативная проверка brand/family. Неоднозначные, несовместимые, брендовые и unmatched-строки получают review-статус без canonical key; исходное значение сохраняется. Остаётся подать реальные RF4DB/RF4MAP/RF4 Posts identities и вручную подтвердить результаты. - [ ] **G05 · Связи с уловами и источниками.** Добавлены `catch_tackle_component` и offline `gear_components`: можно сохранять несколько unresolved/canonical компонентов с ролью, порядком, исходным значением, source identity и `raw_payload`; legacy `bait_id` не меняется. Parser сохраняет порядок оборудования из detail и разделяет bait/rig в catch-полях. Запись компонентов подключена к community import, официальному импорту и пользовательской форме; повторная обработка идемпотентна. Canonical-привязка и безопасный backfill остаются только после подтверждённого crosswalk. - [ ] **G06 · API и публичный каталог.** Добавлены пагинированный `/api/v1/tackle/items` с фильтрами по категории, бренду, семейству и уровню, detail endpoints для предмета и монтажа, а также ordered `tackle_components` в ответе уловов точки. Ответы показывают только канонические характеристики, provenance, timestamp проверки и `missing_fields`; рейтинг эффективности не добавляется. Публичный каталог теперь имеет detail-route `/tackle/items/:id` с 404/unavailable-различением, паспортом данных и явным списком недостающих полей; остаётся подключить канонические ссылки из представлений улова после подтверждения crosswalk. -- [ ] **G07 · Аналитика сочетаний и рекомендации.** Добавлен `/api/v1/analytics/tackle`: approved-наблюдения группируются по роли и исходному компоненту с фильтрами водоёма, рыбы, метода и окна; дубликаты одного улова не увеличивают счётчик, а минимум наблюдений и независимых игроков отделяет факт использования от рекомендации. Пустая или малая выборка получает `insufficient_data`; decay по свежести и отдельные UI-состояния остаются следующим шагом. +- [ ] **G07 · Аналитика сочетаний и рекомендации.** Добавлен `/api/v1/analytics/tackle`: approved-наблюдения группируются по роли и исходному компоненту с фильтрами водоёма, рыбы, метода и окна; дубликаты одного улова не увеличивают счётчик, а минимум наблюдений и независимых игроков отделяет факт использования от рекомендации. Публичный `/tackle/analytics` показывает фильтры, выборку, независимых игроков и раздельные `recommendation`/`insufficient_data` states без рейтинга эффективности. Decay по свежести и дальнейшая визуальная приёмка остаются следующим шагом. - [ ] **G08 · Медиа и качество.** Добавлены отдельные reviewed-роли `tackle_card`, `tackle_detail`, `rig_diagram`, `tackle_screenshot` для `tackle`, а также CLI-параметр `--media-role`; offline audit отклоняет неизвестную роль и несовпадение роли с entity type. Существующие dimensions, MIME, SHA-256, прозрачность, aspect ratio, provenance и атомарное продвижение сохраняются. Остаётся провести реальный contact-sheet review для будущих tackle-кандидатов без автоматической публикации. - [ ] **G09 · Приёмка и эксплуатация.** Добавлены fixture/regression tests для crosswalk, media roles, идемпотентных компонентов, пустых результатов и многокомпонентных наблюдений; каталог `/tackle` включён в visual-matrix, narrow smoke и accessibility routes. Offline catalog/media audits и сохранение старых данных при сбое импорта проходят. TEMP-only query-plan gate для фильтра каталога и группировки сочетаний использует индексы и укладывается в 250 мс. Chromium подтвердил empty-state публичного каталога и отсутствие overflow на 320 px; обычный E2E пропускает bootstrap без явных переменных, отдельный bootstrap с токеном проходит. Остаются HTTP/browser acceptance для неоднозначных и многокомпонентных комплектов, ручной visual review и проверяемый счётчик по категориям либо явный `unknown`. Сетевые тесты не выполнять; импорт оставить opt-in, последовательным и под общим cooldown/backoff. @@ -84,9 +84,9 @@ - [x] **D02 · Системный режим без вспышки.** Первый визит следует `prefers-color-scheme` полностью через CSS: серверный HTML сразу совместим с системной темой, состояние не хранится, inline bootstrap не добавлен и CSP не ослаблена. Явное переопределение появится только вместе с D03–D04. - [x] **D03 · Переключатель темы.** В header добавлен доступный трёхпозиционный выбор «Системная / Светлая / Тёмная»; на узких экранах он сохраняет три понятные иконки и доступные названия. Кнопки работают с клавиатуры, имеют видимый focus и синхронизируют `aria-pressed`. - [x] **D04 · Сохранение и SSR-согласование.** Выбор хранится год в allowlist-cookie `rf4-theme` с `SameSite=Lax` и `Secure` на HTTPS; Astro SSR выставляет `data-theme` до отрисовки, а системный режим оставляет выбор браузеру. Собранный Astro client script переключает тему без inline-кода, localStorage и ослабления CSP. -- [ ] **D05 · Темизация компонентов и графики.** На семантические light/dark-токены переведены базовые и admin-поверхности, header, каталог, breadcrumb-леска, ссылки сущностей, таблицы, формы, moderation-карточки, provenance/quality/status badges, паспорта данных, pagination, empty/loading-состояния, радар, timeline, силуэты и глифы; добавлен forced-colors layer. In-app Chromium подтвердил переключение light/dark/system на медиатеке во всех четырёх ширинах; automated media acceptance отдельно подтверждает light/dark на 320 px, standalone axe проходит `9/9` маршрутов плюс 4 доменных маршрута, forced-colors и print regression подтверждены. Launcher рассчитан на полную screenshot-матрицу `192` PNG без document overflow; representative review тени/градиентов выполнен, а reference-набор остаётся внешним артефактом. Источники и статусы сохраняют текстовые подписи и не различаются только цветом. +- [ ] **D05 · Темизация компонентов и графики.** На семантические light/dark-токены переведены базовые и admin-поверхности, header, каталог, breadcrumb-леска, ссылки сущностей, таблицы, формы, moderation-карточки, provenance/quality/status badges, паспорта данных, pagination, empty/loading-состояния, радар, timeline, силуэты и глифы; добавлен forced-colors layer. In-app Chromium подтвердил переключение light/dark/system на медиатеке во всех четырёх ширинах; automated media acceptance отдельно подтверждает light/dark на 320 px, standalone axe проходит `9/9` маршрутов плюс 4 доменных маршрута, forced-colors и print regression подтверждены. Launcher рассчитан на полную screenshot-матрицу `204` PNG без document overflow; representative review тени/градиентов выполнен, а reference-набор остаётся внешним артефактом. Источники и статусы сохраняют текстовые подписи и не различаются только цветом. - [x] **D06 · Метаданные браузера и CSP.** Добавлены парные `theme-color` для системной light/dark схемы; явный cookie-выбор согласуется с SSR и мгновенно переключает активный meta-тег. PWA manifest использует устойчивый тёмный brand chrome и splash background. Production build сохраняет `inlinedScripts: []`; новые inline script/style/attributes не появились, ослабление CSP не потребовалось. -- [ ] **D07 · Визуальная и accessibility-приёмка.** In-app Chromium подтвердил отсутствие горизонтального scroll на публичных `/`, `/waterbodies`, `/records`, `/report`, `/status`, `/media`, `/rules`, waterbody detail `/waterbodies/р-вьюнок` и fish detail `/fish/pike`, а также admin `/admin`, `/admin/moderation`, `/admin/external-sources`, `/admin/media` в 320/390/768/1280 px; исправлены tablet header и media-grid overflow. Автоматизированный visual-matrix regression и `scripts/capture-visual-matrix.mjs` подтверждают 16 маршрутов × 4 ширины × 3 режима (`192` комбинации), включая spot detail и plan: document overflow отсутствует, везде есть `main`/`h1`, SSR-тема совпадает; launcher настроен на полный внешний capture. Для media и двух detail-страниц light/dark/system корректно переключаются, broken images — `0`; axe `9/9`, отдельная domain accessibility-проверка добавлена для `/waterbodies`, waterbody detail, spot detail и `/media`, keyboard/focus, forced-colors и print-regression проходят. Representative manual review и ограничения reference-хранения зафиксированы в `docs/ux-reference-review-2026-09-21.md`. Для обеих тем обеспечить WCAG AA, отсутствие горизонтального scroll и CLS, reduced motion и переключение без потери введённых данных; сохранить эталонные screenshots и краткий отчёт. +- [ ] **D07 · Визуальная и accessibility-приёмка.** In-app Chromium подтвердил отсутствие горизонтального scroll на публичных `/`, `/waterbodies`, `/records`, `/report`, `/status`, `/media`, `/rules`, waterbody detail `/waterbodies/р-вьюнок` и fish detail `/fish/pike`, а также admin `/admin`, `/admin/moderation`, `/admin/external-sources`, `/admin/media` в 320/390/768/1280 px; исправлены tablet header и media-grid overflow. Автоматизированный visual-matrix regression и `scripts/capture-visual-matrix.mjs` подтверждают 17 маршрутов × 4 ширины × 3 режима (`204` комбинации), включая spot detail, plan и tackle analytics: document overflow отсутствует, везде есть `main`/`h1`, SSR-тема совпадает; launcher настроен на полный внешний capture. Для media и двух detail-страниц light/dark/system корректно переключаются, broken images — `0`; axe `9/9`, отдельная domain accessibility-проверка добавлена для `/waterbodies`, waterbody detail, spot detail и `/media`, keyboard/focus, forced-colors и print-regression проходят. Representative manual review и ограничения reference-хранения зафиксированы в `docs/ux-reference-review-2026-09-21.md`. Для обеих тем обеспечить WCAG AA, отсутствие горизонтального scroll и CLS, reduced motion и переключение без потери введённых данных; сохранить эталонные screenshots и краткий отчёт. ### UX/UI и конкурентное преимущество @@ -98,7 +98,7 @@ - [ ] **U04 · List/map и progressive disclosure.** List остаётся честным базовым режимом: фильтры, сортировка, URL-состояние и evidence-карточки уже работают без имитации координатной карты. Следующий шаг — единый list/map-контракт после подтверждения геометрии; на mobile карта должна открываться отдельным действием. Вторичные raw/provenance-поля не исчезают и раскрываются по запросу. - [ ] **U05 · Mobile-first и сохранённый план рыбалки.** `/plan` поддерживает список до 5 локальных вариантов, удаление, очистку, переход к точке, print/PDF и восстановление из shareable URL; кнопка «Поделиться планом» использует native share или clipboard fallback. Detail-кнопка сохраняет данные с `aria-pressed` и восстанавливается после reload. Print/mobile-контракт проверяет 320/390 px, лимит импорта и отсутствие горизонтального overflow; карточка плана теперь сравнивает подтверждённые метод/проводку и явно показывает риск по выборке/уверенности. - [ ] **U06 · Контентная и визуальная иерархия.** Для detail точки действие «Что взять» выделено отдельным заголовком, provenance и качество собраны в общем паспорте данных, а подтверждённые метод/проводка видны рядом с последними уловами; статусы дополнительно передаются текстом, малая выборка не маркируется как готовая рекомендация, а дублирующие catches/players/confidence убраны из вторичной колонки activity-карточки. Mobile route-contract проверяет первый ответ и действие на home, spot, waterbody, plan и tackle, а task regression требует source/evidence-поля; остаётся ручной review визуальных деталей по [ux-task-review-2026-09-21.md](ux-task-review-2026-09-21.md). -- [ ] **U07 · UX-приёмка и измерения.** Playwright-контракты уже покрывают query journey, сквозной task journey, evidence states, saved plan, share/print, accessibility и visual matrix; unit-контракт добавил детерминированные stale- и blocked-подписи, а insufficient-data теперь отделён от incomplete в публичном паспорте. `/status` явно сообщает, что при ограничении источника опубликованные данные сохранены. Visual-matrix regression теперь проверяет `192` комбинации, включая spot detail и plan; representative review зафиксирован в `docs/ux-reference-review-2026-09-21.md`. Остаётся получить production Lighthouse/CLS/INP/time-to-first-useful-answer после пилота и принять решение о долговременном хранении screenshots. +- [ ] **U07 · UX-приёмка и измерения.** Playwright-контракты уже покрывают query journey, сквозной task journey, evidence states, saved plan, share/print, accessibility и visual matrix; unit-контракт добавил детерминированные stale- и blocked-подписи, а insufficient-data теперь отделён от incomplete в публичном паспорте. `/status` явно сообщает, что при ограничении источника опубликованные данные сохранены. Visual-matrix regression теперь проверяет `204` комбинации, включая spot detail, plan и tackle analytics; representative review зафиксирован в `docs/ux-reference-review-2026-09-21.md`. Остаётся получить production Lighthouse/CLS/INP/time-to-first-useful-answer после пилота и принять решение о долговременном хранении screenshots. - [ ] **Q01 · Документы источников — реестр готов, нужны первичные подтверждения.** Создан единый production-gate с атрибуцией, общим лимитом 30 минут, хранением и процедурой отзыва для RF4DB, RF4-STAT, RF4MAP, RF4 Posts и официального RF4. До открытой публикации приложить устойчивые ссылки/копии первичных разрешений, контакты, даты и отдельно подтвердить право на изображения; пустое поле блокирует соответствующий источник. - [x] **Q02 · Управляемое удаление источника.** Изменение ранее опубликованной записи возвращает её в staging, сбрасывает сопоставление и переводит связанный улов в pending с новой версией решения. Результат проверки хранится как `available`, `missing`, `temporary_error` или `blocked`: только подтверждённый `missing` отзывает публикацию, временная ошибка и блокировка остаются диагностикой. Повторное появление требует ручного подтверждения. Withdrawn-записи исключены из публичной активности, а статус и время проверки доступны в admin provenance и журнале решений. Отдельного сетевого обхода нет: Q03 подключит эту реакцию к разрешённому плановому запросу. @@ -142,7 +142,7 @@ - [x] **A03 · Полный single-owner workflow.** Добавить пагинацию очереди уловов, кнопку запуска официального импорта и отображение результата/истории, безопасные статусы источников с возрастом данных, cooldown/backoff и ручное обновление очередей. Критерий: владелец может пройти путь «импорт → проверка → решение → история» без API/CLI; старые данные сохраняются при сбое импорта. - [x] **A04 · Контур медиа-проверки.** Admin-экран показывает approved/upgrade_queued/upgrade_stored медиа, источник, размеры, производные и provenance. Защищённые действия требуют непустую причину: publish атомарно продвигает только проверенные `upgrade_stored` и сохраняет fallback, rollback принимает только связанную пару `approved`/`superseded`; небезопасные состояния возвращают `409`. API-тесты проверяют auth, валидацию причины и вызов безопасных операций; browser acceptance остаётся в A06. - [ ] **A05 · Многопользовательский доступ.** После пилота заменить общий Bearer-токен персональными аккаунтами и короткими серверными сессиями с отзывом, ролями read-only/moderator/importer/owner, operator ID в аудите и журналом входов. Критерий: минимальные права реально ограничивают действия, logout/revoke инвалидируют сессию на сервере. -- [ ] **A06 · Browser/accessibility acceptance.** In-app Chromium подтвердил `/admin`, moderation, external sources и media в 320/390/768/1280 px без горизонтального overflow; четыре admin-маршрута имеют landmarks, подписанные поля, именованные кнопки и `alt` у изображений. Visual-matrix regression покрывает 16 маршрутов × 4 ширины × 3 режима (`192` комбинации), включая spot detail и plan, без document overflow и с совпадающей SSR-темой. Standalone axe проходит `9/9` маршрутов без serious/critical violations, keyboard/focus regression не оставляет фокус на `BODY`, forced-colors сохраняет видимую границу theme controls, print-regression сохраняет светлую схему и убирает фильтр hero image. Representative screenshots и ручная визуальная проверка shadows/gradients описаны в `docs/ux-reference-review-2026-09-21.md`; headless gate больше не блокируется sandbox при escalated запуске. +- [ ] **A06 · Browser/accessibility acceptance.** In-app Chromium подтвердил `/admin`, moderation, external sources и media в 320/390/768/1280 px без горизонтального overflow; четыре admin-маршрута имеют landmarks, подписанные поля, именованные кнопки и `alt` у изображений. Visual-matrix regression покрывает 17 маршрутов × 4 ширины × 3 режима (`204` комбинации), включая spot detail, plan и tackle analytics, без document overflow и с совпадающей SSR-темой. Standalone axe проходит `9/9` маршрутов без serious/critical violations, keyboard/focus regression не оставляет фокус на `BODY`, forced-colors сохраняет видимую границу theme controls, print-regression сохраняет светлую схему и убирает фильтр hero image. Representative screenshots и ручная визуальная проверка shadows/gradients описаны в `docs/ux-reference-review-2026-09-21.md`; headless gate больше не блокируется sandbox при escalated запуске. - [ ] **A07 · Production gate.** Выполнить preflight с реальными секретами, проверить Caddy Basic + API Bearer, закрытые внутренние порты, backup/restore PostgreSQL и MinIO, readiness, no-store и внешний smoke после деплоя. Локальный чистый-volume bootstrap и оба Playwright-сценария подтверждены; критерий A07 остаётся открытым до acceptance-runbook с реальными секретами, backup/restore, rollback и процедурой отзыва доступа. ## Готовность открытой альфы — требуется сервер или внешний сервис diff --git a/docs/ux-reference-review-2026-09-21.md b/docs/ux-reference-review-2026-09-21.md index 3064c49..619e0fe 100644 --- a/docs/ux-reference-review-2026-09-21.md +++ b/docs/ux-reference-review-2026-09-21.md @@ -10,7 +10,7 @@ node scripts/capture-visual-matrix.mjs \ --output /tmp/rf4-visual-matrix-20260921 ``` -Матрица regression-теста текущего прохода: `192` комбинации — `16` маршрутов × +Матрица regression-теста текущего прохода: `204` комбинации — `17` маршрутов × `4` ширины (`320/390/768/1280`) × `3` режима (`system/light/dark`); тест завершился без overflow. Завершённый standalone capture предыдущего прохода содержит `168` screenshots. Новый launcher уже включает spot detail и plan, но diff --git a/scripts/capture-visual-matrix.mjs b/scripts/capture-visual-matrix.mjs index 98ce5bf..8c5c960 100755 --- a/scripts/capture-visual-matrix.mjs +++ b/scripts/capture-visual-matrix.mjs @@ -6,7 +6,7 @@ import process from "node:process"; import { chromium } from "../apps/web/node_modules/playwright/index.mjs"; const routes = [ - "/", "/spots/vyunok-6331x6332", "/plan", "/waterbodies", "/tackle", "/records", "/report", "/status", "/media", "/rules", + "/", "/spots/vyunok-6331x6332", "/plan", "/waterbodies", "/tackle", "/tackle/analytics", "/records", "/report", "/status", "/media", "/rules", "/waterbodies/р-вьюнок", "/fish/pike", "/admin", "/admin/moderation", "/admin/external-sources", "/admin/media", ];