docs: audit project and reopen incomplete roadmap gates
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
# Аудит 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; полный пункт
|
||||
закрывается только после устранения дефекта и адресной приёмки.
|
||||
Reference in New Issue
Block a user