Files
rf4-spotter/docs/PROJECT_AUDIT_2026-09-22.md
T
ik 104d5803fb
CI / backend-and-migrations (push) Waiting to run
CI / astro-build (push) Waiting to run
CI / dependency-audit (push) Waiting to run
CI / compose-e2e (push) Waiting to run
docs: audit project and reopen incomplete roadmap gates
2026-09-22 07:33:51 +07:00

245 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Аудит RF4 Spotter — 22 сентября 2026
Проверенный исходный commit: `25b6f39`. Аудит охватывает API, Astro UI,
исследовательские CLI, media, миграции, локальную БД, CI и эксплуатационные
сценарии. Изменения этого прохода — отчёт и актуализация плана, не исправление
продуктового кода. Внешние источники не запрашивались.
## Вывод
Основа приложения работоспособна: production build собирается, OpenAPI
синхронизирован, media audit чистый, локальные контейнеры healthy. Однако
состояние «все локальные задачи закончились» неверно. Есть проблемы сохранности
media-решений, конкурентных операций, навигации и достоверности UI. Несколько
пунктов закрыли по проверке наличия элементов или пустой страницы, хотя их
критерии включают наполненные состояния и визуальную приёмку.
## Исполняемые проверки
| Проверка | Результат текущего прохода |
|---|---|
| `.venv/bin/pytest -q` | 228 passed, 1 failed, 1 skipped; 90 dependency deprecation warnings |
| `npm --prefix apps/web run test:unit` | FAIL; прямой запуск файла уточнил: 14 passed, 1 failed |
| `npm --prefix apps/web run build` | PASS; Astro: 87 файлов, 0 errors, 0 warnings, 2 hints по `execCommand` |
| `PYTHONPATH=apps/api .venv/bin/python apps/api/export_openapi.py --check` | PASS, 45 paths |
| `media_cli --audit` | 704 записи: 466 approved, 226 superseded, 1 duplicate, 11 invalid; issues/orphans пусты |
| `media_cli --quality-report` | Среди 253 опубликованных fish-файлов один 48×48, известных альтернатив нет |
| `docker compose ps` | API, web, PostgreSQL, MinIO healthy |
| `docker compose exec -T api alembic current` | `0022 (head)` |
| SQL, транзакция `BEGIN READ ONLY` | waterbody: 21, source_external_id: 19; tackle_item: 0; rig: 0 |
Python failure: `tests/test_community_cli.py:73` ожидает старый ответ reserve-only
без `reserved_at`. Web failure: `apps/web/tests/unit/presentation.test.ts:99`
ожидает «Схема монтажа», тогда как продукт правильно показывает «Схема сборки».
Это рассогласование тестов с изменёнными контрактами, не основание откатывать
терминологию или идентификатор резерва.
Live E2E, новый screenshot/axe-прогон, production bootstrap, restore drill,
нагрузка и проверка CVE в этом проходе не выполнялись. Уже запущенные Docker
образы не пересобирались; их health не доказывает соответствие текущему коду.
Старые browser-отчёты — исторические доказательства с указанными в них границами.
Выводы о гонках ниже основаны на коде; конкурентные production-запросы не выполнялись.
## Ошибки и недоработки
### R01 · P1 · Сохранность media-решений
`apps/api/Dockerfile:9` копирует `data/media` в образ, а сервис api в
`compose.production.yaml` не имеет persistent media volume. Admin publish/rollback
записывают manifest в этот каталог. Пересоздание контейнера вернёт состояние
из образа. `deploy/backup.sh` сохраняет PostgreSQL и MinIO, но не этот manifest.
Нужен единый долговременный writable store с bootstrap из Git, backup/restore и
правилом обновления release. Критерий: решение переживает recreate, обновление
образа и восстановление, не теряет provenance и fallback. Без cooldown.
### R02 · P1 · Область публикации и конкурентная запись manifest
Кнопка `apps/web/src/pages/admin/media.astro:57` появляется на отфильтрованной
странице, но отправляет только note. Endpoint `routers/admin.py:64` вызывает
`publish_quality_upgrades`, который выбирает ВСЕ `upgrade_stored`
(`rf4_research/media_assets.py:205`). Проверенная видимая выборка и область
действия не связаны. Использование общего `.tmp` и отсутствие lock/version
дополнительно допускают конфликт publish/rollback/CLI: atomic replace не
защищает read-modify-write от потери чужого изменения.
Нужны выбранные asset IDs, preview набора, версия manifest, блокировка и
проверка реальных файлов перед продвижением. Критерий: скрытый кандидат не
публикуется, устаревшее решение возвращает конфликт, одновременные операции
сохраняют корректный JSON и обе истории решений. Без cooldown.
### R03 · P1 · Идемпотентность отправки улова
`routers/submissions.py:77` после `IntegrityError` возвращает winner без
повторной проверки payload hash и времени действия ключа. Уникальность ключа
хранится дольше пяти минут, поэтому путь достижим и при повторе старого ключа.
Проверка replay token в обычной ветке также перестаёт проходить после успешного
upload, который очищает token hash. Нужен единый replay-контракт для всех веток,
включая завершённый upload, и различение конфликтов idempotency/Spot/Bait.
Критерий: одинаковый запрос возвращает тот же результат, другой payload — 409;
конкурентное создание одной точки/приманки не превращается в необработанный 500.
Без cooldown; конкурентная проверка в изолированном PostgreSQL.
### R04 · P1 · Скриншот: повтор, гонка и восстановление формы
`add_screenshot` читает report через `db.get`, без row lock: два запроса могут
пройти проверку пустого screenshot_key и записать два объекта. После успешного
upload повтор уже получает 401 из-за очищенного token hash. Если клиент потерял
ответ, он не может отличить успех от неудачи. Кроме того,
`pages/api/report-screenshot.ts:15` удаляет только upload-cookie, сохраняя
старый `rf4-idempotency-key` после восстановления; следующий новый улов может
конфликтовать с предыдущим. Нужны одноразовая атомарная операция, повторяемый
ответ и завершение состояния формы. Критерий: lost response + retry успешно
восстанавливаются, новый улов отправляется, лишние S3-объекты не остаются.
### R05 · P1 · Пагинация и семантика каталога снастей
`pages/tackle/index.astro:18` использует общий offset для предметов и сборок,
а Pagination в конце зависит только от `result.total`. При 0 предметов и 49
сборках вторую страницу сборок нельзя открыть через UI. Сбой предметов скрывает
и успешно загруженные сборки. Неверная category или слишком длинный q дают API
422, но UI называет это временной недоступностью 503. Empty search описан как
незаполненный каталог. Нужны отдельные пагинации/состояния и валидация параметров.
Критерий: доступны все 49+ сборок независимо от предметов, фильтры сохраняются,
ошибка ввода отличается от сбоя API и отсутствия данных. Без cooldown.
Дополнительно карточки этого каталога обёрнуты в `<a>`, а вложенный
`DataPassport` через `SourceBadge` создаёт ещё один `<a>` при source_url.
Это недопустимое вложение интерактивных ссылок; браузер может перестроить DOM.
Сделать отдельные ссылки названия и источника, проверить наполненную карточку
клавиатурой и по итоговому DOM. На пустом каталоге дефект не виден.
### R06 · P1 · Импорт и срок годности сохранённого плана
`pages/plan.astro:45` автоматически заменяет localStorage при открытии share URL.
Параметр остаётся в URL: удаление точки или очистка отменяются при reload.
Свежесть показывается как «Свежесть сохранена» без пересчёта времени, а ссылки
строятся из query JSON. Поэтому текст «Данные не отправляются на сервер»
некорректен для открытия share URL: query приходит серверу.
Нужны preview/merge/replace, однократный импорт с удалением параметра,
восстановление предыдущего плана, обработка storage errors и вычисляемый возраст.
Рассмотреть fragment для переносимого payload. Критерий: чужая ссылка не стирает
план без выбора, reload не воскрешает удалённое, старые сведения явно устаревшие.
### R07 · P1 · Координатная схема
`components/ActivityMap.astro` рисует все переданные items; `index.astro:100`
не ограничивает их одним водоёмом или точностью координат. На одной сетке
смешиваются независимые системы координат. Несколько элементов одной grid cell
накладываются, минимальная ширина маркера превышает ширину ячейки; overflow hidden
может обрезать содержимое без document overflow. Нужны группировка по водоёму,
явные approximate/area-состояния, обработка совпадений и доступный список.
Критерий: все точки различимы и достижимы мышью/клавиатурой на 320 px,
водоём виден в подписи, схемы разных водоёмов не смешаны. Без cooldown.
### R08 · P1 · Достоверность рекомендаций и стоимость аналитики
`routers/analytics.py:32` разрешает публичному клиенту снизить min_samples и
min_players до 1 и получить `recommendation`, хотя UI обещает минимум 3/2.
Группировка идёт по raw строке, а не canonical identity; один подтверждённый ID
может дать ссылку всей группе, содержащей unresolved-строки. Данные выбираются
по reported_at: старый улов, импортированный сегодня, выглядит свежим.
Все отчёты и компоненты окна загружаются в Python без лимита ответа.
Нужны неизменяемый публичный порог, явное время улова/получения, conservative
identity-группировка и измерение фактического endpoint на больших fixtures.
Критерий: 1/1 никогда не рекомендация, unresolved не наследует подтверждение,
старый улов не представлен свежим; ограниченный ответ и документированный бюджет.
### R09 · P1 · Полнота удаления и retention
`tackle_components.py:35` копирует source_url/raw_payload в дочерние записи.
`retention.py:94` и `routers/admin.py` при удалении очищают только родительский
CatchReport. Дочерние копии provenance остаются. Нужно определить поля,
содержащие личные сведения, и каскадно очищать их с сохранением разрешённых
агрегатов. Удаление S3 до commit БД также требует повторяемой процедуры при
отказе транзакции. Критерий: повторная очистка безопасна, чувствительные копии
не остаются в дочерних payload, ошибки между S3/БД восстанавливаются. Без cooldown.
### R10 · P1 · Согласованная модель cooldown
`community_cli.py:385` освобождает резерв при любом URLError кроме HTTPError.
После ответа-редиректа DNS-ошибка следующего hop тоже попадает сюда, хотя первый
сервер уже получил запрос. Community CLI и media CLI используют общий helper
и JSON-состояние при одинаковом state-file, scheduler — отдельную историю БД.
Media CLI не применяет новую отмену резерва при локальном сбое. Единого
координирующего хранилища и одинаковой классификации исходов у всех путей нет.
Нужны признаки стадии запроса и единая координация запусков; отменять резерв
только при доказанном отсутствии обращения. Критерий: DNS до первого запроса
освобождает свой резерв, сбой после redirect — нет; CLI и scheduler не расходуют
одно окно одновременно. Проверять offline, без запросов к RF4DB.
### R11 · P2 · Media: подписи, происхождение и размеры
`EntityMedia.astro:16` при любом размере <256 заявляет, что источник не содержит
проверенной крупной версии; размеры сами этого не доказывают. Новая подпись не
проверена в compact/light/dark после добавления. `media_catalog.py:40` считает
всякий неизвестный URL официальным RF4 и распознаёт домен подстрокой.
Нужны unknown-источник по умолчанию, точное сопоставление hostname, отдельное
подтверждение отсутствия альтернатив, перенос длинных подписей и review низкого
разрешения по всем типам assets. Для revoked media отдельно определить TTL:
годовой immutable cache позволяет старому клиенту показывать ранее выданный файл.
Критерий: неизвестный источник не «официальный», компактная подпись не обрезана,
обещания отзыва соответствуют фактической cache-политике. Без загрузок.
### R12 · P2 · Честная визуальная и административная приёмка
`docs/ux-reference-review-2026-09-21.md` описывает representative light/mobile
review и прямо оставляет production CLS/INP и хранение references открытыми.
Поэтому фраза D07 «подтверждены WCAG AA, отсутствие CLS» не доказана. Последняя
проверка G07 видела только empty-state (в БД нет снастей/сборок), U06 — DOM
одной точки. Наличие кнопки не подтверждает её работу. Административная форма
входа не заменяет authenticated review очереди и конфликтов.
Критерий: привязанные к revision screenshots light/dark на mobile/desktop,
наполненные, длинные, ошибочные состояния, keyboard, contrast, theme без потери
ввода и реальные действия на изолированных fixtures. Production metrics остаются
отдельным gate. Это локальная приёмка, cooldown не нужен.
### R13 · P2 · Вернуть зелёные проверки и устойчивые CI artifacts
Исправить две устаревшие проверки, описанные выше, добавить адресные regressions
для R01–R12 после исправления функций. CI есть в `.gitea/workflows/`, а не в
`.github`: Python/миграции, Astro, Compose E2E и pip-audit уже настроены.
Нужны сохранение успешных visual reports, явный Node/Python baseline,
проверка web dependency advisories и версий контейнеров. Текущий аудит не
подтверждает отсутствие CVE; внешнюю проверку зависимостей выполнять отдельно.
## Улучшения продукта и эксплуатации
- **R14 · P2 · Разделение подтверждённого и демонстрационного каталога.**
`seed.py` создаёт справочники, точки и описания до проверки `seed_demo_data`.
При production false демо-уловы выключены, но редакционные описания точек
остаются. Отделить обязательный справочник от demo seed, показывать полноту
реального каталога/unknown отдельно от числа загруженных элементов. Счётчик
media-файлов не означает наличие canonical карточек. Критерий: чистый production
не создаёт неподтверждённых описаний, local fixtures явно обозначены.
- **R15 · P2 · Восстановление пользовательских действий.** Сохранение формы при
422/429/timeout, понятный конфликт повторной отправки, undo удаления из плана,
общие utility для clipboard/storage. Критерий: пользователь не вводит весь
улов заново и различает «улов сохранён, фото не загружено» от полного отказа.
- **R16 · P2 · Измеряемая производительность.** Media manifest перечитывается и
сканируется для каждого изображения (`published_file`); аналитика грузит ORM
объекты целиком. Измерить реальный API, RSS, число SQL и количество reads,
затем добавить индекс digest/mtime cache и bounded aggregation. SQL-only
query-plan drill не подменяет endpoint measurement. Redis заранее не нужен.
- **R17 · P2 · Публичная навигация и SEO снастей.** `sitemap.xml.ts` перечисляет
рыбу/водоёмы/точки, но не каталог и карточки снастей. Добавить только реально
подтверждённые индексируемые карточки; единый переход из каталога к снастям,
объяснение справочника и аналитики, корректные пустые/недоступные состояния.
- **R18 · P1 · Release gate и инфраструктурные границы.** Dev Compose публикует
API, web и MinIO console на всех интерфейсах; defaults известны. Привязать
dev-порты к loopback либо явно выделить opt-in LAN. Проверить production
proxy/client-IP цепочку и rate-limit на двух клиентах. Секреты, DNS/TLS,
внешние backup/alerts, права источников и пилот остаются отдельными блокерами.
## Состояние данных и корректировка плана
В `.cache/waterbodies/rf4db-level_019_american_pond-20260921.json` уже есть
сохранённый snapshot: 19 видов, 2 URL изображений, description null, point_urls
пустой. Старое утверждение ROADMAP о текущей Cloudflare-странице устарело.
Это подтверждение локального файла, не нового сетевого запроса и не импорта.
Подготовка его versioned provenance и offline import может выполняться без
cooldown; полнота всех detail-страниц W02 всё ещё не достигнута.
Повторно открываются A04/B23 (media mutation), G06/G07, U04/U05,
B25/D07/A06/U06. Остальные ранее реализованные функции не объявляются
несуществующими. Новые критерии и порядок работ — в ROADMAP; полный пункт
закрывается только после устранения дефекта и адресной приёмки.