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

23 KiB
Raw Blame History

Аудит 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; полный пункт закрывается только после устранения дефекта и адресной приёмки.