Files
rf4-spotter/docs/PROJECT_AUDIT_2026-09-08.md
T
ik 3e70b0f387
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s
docs: audit production readiness UX identity and SEO
2026-09-08 16:43:33 +07:00

104 lines
26 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 — 8 сентября 2026
База проверки: commit `486b4e9`. Аудит охватывает production-конфигурацию, API, импорт и публикацию данных, Astro, пользовательские сценарии, визуальную идентичность и SEO. Это отчёт о текущем состоянии, а не подтверждение готовности к запуску. Исправления приложения в этот пакет не входят.
## Вывод
Стек соответствует продукту: Astro SSR + FastAPI + PostgreSQL + MinIO + Caddy. Перенос на Next.js/Vinext, добавление SPA-фреймворка, Redis или микросервисов сейчас не обоснованы. Есть полезная основа: миграции, модерация, provenance, проверка изображений, Docker, backup/restore, SEO-разметка и выразительный рыболовный визуальный язык.
Открывать production пока рано. Подтверждены три блокирующих противоречия конфигурации: маршрутизация формы, несовместимые схемы admin-аутентификации и отсутствие внешней сети у community scheduler. Успешная локальная проверка напрямую через Astro/API эти дефекты не обнаруживает. Главный продуктовый риск — фактическое наполнение и достоверность времени/оценок, а не недостаток декоративных элементов.
## Метод и границы
- Прочитаны конфигурации Compose/Caddy/Docker/CI, ключевые обработчики API и Astro, код агрегации, scheduler, research CLI, staging/review/retention, seed, компоненты и стили, текущие планы и ограничения.
- Встроенный браузер: главная текущего локального стенда, desktop 1280×1000 и mobile 390×844; оценены композиция, меню и фильтры. Предыдущий прогон 320/390/768/1280 проверял прежде всего переполнение. Его нельзя считать полноценной визуальной приёмкой всех состояний.
- `astro check`: 40 файлов, 0 ошибок, 0 предупреждений. Целевые тесты CLI/scheduler/activity: 16 passed. Общий pytest был начат, но завершённый результат в этом прогоне не получен; прежние общие результаты не выдаются за новый полный прогон.
- Внешние парсеры не запускались, 30-минутный лимит не расходован. Актуальность HTML источников, CVE всех зависимостей, нагрузка, TLS/DNS, реальный backup на сервере и поисковая индексация повторно не проверялись.
- Проверены официальные документы Docker, Caddy и Google. Подтверждение по коду отделено ниже от дизайнерских предложений и будущих проверок.
Приоритеты: **P0** — блокирует production; **P1** — нужно до открытой альфы; **P2** — улучшение качества/масштабирования; **P3** — после обратной связи пилота.
## Техническая часть и эксплуатация
| ID | Приоритет | Подтверждение и последствие | Критерий исправления |
|---|---|---|---|
| T01 | P0 | `deploy/Caddyfile`: `/api/*` уходит в FastAPI, но `/api/report` и `/api/report-screenshot` реализованы в Astro. Production-форма попадает в отсутствующий маршрут. | Через Caddy отправка и повтор загрузки достигают Astro и возвращают ожидаемый 303; `/api/v1/*` достигает FastAPI. |
| T02 | P0 | Caddy `basic_auth` на `/api/v1/admin/*` требует Basic в `Authorization`; admin JS посылает Bearer, `_admin` требует именно Bearer. Два слоя используют один несовместимый контракт. | Выбрать согласованную схему; доказать успешные чтение/модерацию через proxy и отказ неавторизованному клиенту. |
| T03 | P0 | `compose.production.yaml`: community-scheduler только в `backend`, сеть `internal: true`. Нет внешней сети для HTTPS источников. | Разрешён исходящий доступ scheduler; БД/MinIO остаются без внешних портов. Проверить локальным контролируемым HTTP-источником. |
| T04 | P1 | Astro POST не передаёт идентичность клиента, FastAPI rate limit использует `request.client.host`. Заявки через web объединяются под его адресом. Доверенные proxy явно не настроены. | Два клиента имеют независимый лимит; поддельный forwarded header его не обходит; проверка через всю цепочку Caddy → Astro → API. |
| T05 | P1 | В Astro `request.formData()` выполняется до try, POST fetch без timeout; Caddy не задаёт лимит тела. Лимит 8 МБ в Python применяется после приёма multipart. | Ограничить входной body до буферизации, обработать повреждённый multipart, задать время ожидания и понятные 413/429/ошибки формы. |
| T06 | P1 | Bootstrap запускает db/minio/api/web, без proxy и community-scheduler; CI проверяет локальный Compose. `monitor.sh` не включает community-scheduler, `/ready` знает лишь официальный импорт. | Один production-contract прогон через proxy, проверка scheduler heartbeat/последнего успеха и уведомление о зависании/остановке. |
| T07 | P1 | `GET /api/v1/imports` публично возвращает `ImportRunOut`, включая `error_summary` и `source_url`. | Публичный DTO содержит только необходимые безопасные статусы; детали ошибок доступны администратору. |
| T08 | P2 | Python фиксирует прямые зависимости, но не транзитивные; Docker base tags изменяемы. В CI нет web unit job (`test:unit`) и отдельного dependency audit. | Воспроизводимый Python lock/constraints, поддерживаемые runtime-версии и регулярная проверка зависимостей; подключить существующие unit-тесты. CVE здесь не утверждаются. |
| T09 | P2 | `activity_rows` загружает все уловы окна и агрегирует Python-списками до пагинации; на карточке точки также читается история. Public cache локален процессу. | Измерить память/p95 на представительном объёме; оптимизировать запросы по результату. Сохранить документированный TTL 20 с до необходимости общей инвалидации. |
| T10 | P2 | API/web стартуют с миграциями/seed; MinIO app policy `readwrite` шире одного bucket, общие admin credentials. | Оформить отдельный release/migration шаг, bucket policy и план персонального доступа при росте команды; не добавлять identity provider до потребности. |
Семантика изоляции подтверждена [Docker networks](https://docs.docker.com/reference/compose-file/networks/#internal). Конфликт заголовков T02 следует из кода и [контракта Caddy Basic Auth](https://caddyserver.com/docs/caddyfile/directives/basic_auth).
## Парсеры и достоверность данных
| ID | Приоритет | Подтверждение и последствие | Критерий исправления |
|---|---|---|---|
| D01 | P1 | `oldest_site_source` включает выключенные адаптеры. Если самым старым RF4-STAT endpoint окажется disabled, включённый сосед будет постоянно пропускаться. | Ротация только enabled-кандидатов; тест выключенного соседа, включения обратно, ошибки и двух конкурирующих запусков. |
| D02 | P1 | Research CLI делает check/write раздельно без межпроцессного lock, повреждённый state трактует как пустой; старые source-key записи после перехода к доменам не учитываются. Автономный state не связан с PostgreSQL. | Атомарное резервирование с fail-closed, миграция состояния; production сбор через единый механизм, offline parsing отделён. Нельзя заявлять общую гарантию только на основании предупреждения README. |
| D03 | P1 | `fetch_html` допускает произвольный override URL, автоматические redirects, неограниченный `read()`. `fetch_site_key` различает `download.rf4db.com` и `rf4db.com`. | Единый реестр разрешённых площадок/хостов, проверка URL и redirects до HTTP, лимит ответа, корректные 429/Retry-After в рамках ≥30 минут. |
| D04 | P1 | RF4-STAT fishing выдаёт `waterbody_external_id=None`. Mapping сохраняет fallback alias по имени, но `_auto_publish` требует оба external ID. Ранее подтверждённое имя не включает автопубликацию следующей полной записи. | Использовать единый безопасный контракт алиасов для suggestion/manual/auto; не разрешать неподтверждённое fuzzy-сопоставление. |
| D05 | P1 | Scheduler RF4MAP/RF4 Posts опрашивает по одному фиксированному detail URL. Seed даёт только 2 рыбы/2 водоёма; на локальной главной также только они и 0 активных точек. «Все парсеры включены» не означает полный охват. | Реестр задач/очередь разрешённых URL и пополнение канонического каталога; видимые счётчики fetched/staged/mapped/published/age; приёмочный набор из каждого адаптера. |
| D06 | P1 | Score зависит от количества сообщений одного ника; текст главной «Один игрок не может искусственно поднять уверенность» неверен. Имена без аккаунтов не доказывают независимость. | Исправить обещание, определить ограничения вклада одного игрока и неизвестных авторов; воспроизводимый пример спама не изображает независимые подтверждения. |
| D07 | P1 | В activity окно/затухание по `reported_at`, подпись свежести по `caught_at or reported_at`; community publisher записывает `published_at` также в `caught_at`. В feed «Получено» — `last_seen_at`, который обновляется каждым опросом. | Разделить время улова, публикации, первого получения и последнего обнаружения; старый повторно найденный материал не выглядит новым уловом. |
| D08 | P1 | На `/spots/[id]` score берётся из первых 100 глобальных activity-строк и только по spot_id, хотя индекс рассчитан на spot+fish. Возможны ложное «данных нет» и оценка одной рыбы под видом всей точки. | Прямой запрос оценки точки с явным контекстом рыбы; тест >100 комбинаций и нескольких рыб на точке. |
| D09 | P2 | Staging и autopublish коммитятся отдельными операциями; ошибка поздней записи может оставить часть публикаций при failed journal. Изменение сравнивает весь payload; полная история редакций отсутствует. | Явная семантика partial success, идемпотентный retry, сравнение значимых полей и история редакций/удалений; отсутствие записи в очередном списке само по себе не означает удаление. |
| D10 | P2 | `source_status` анализирует последние 20 запусков, running может считаться healthy; backoff смешан для домена, успешный сосед может сбросить ошибки endpoint. | Раздельные domain cooldown и endpoint backoff, stalled/running статус, последний успех вне ограничения 20 строк. |
Обнаруженная особенность sitemap **не является ошибкой пагинации**: API `public-spot-pages` выдаёт два пути на строку БД, поэтому `limit=500` и остановка при `<1000` путях согласованы. Нужен контрактный тест и явная документация, а не механическая замена на `<500`.
## UI/UX
| ID | Приоритет | Наблюдение | Критерий/решение |
|---|---|---|---|
| U01 | P1 | На screenshot 1280×1000 период и сортировка стоят вертикально; фильтры занимают около 196 px. Используется `details`/`display:contents`, результат не соответствует пятиколоночной задумке. | Устойчивая desktop-сетка и мобильное раскрытие, проверка реального layout браузеров; отсутствие overflow недостаточно. |
| U02 | P1 | Главная передаёт waterbody/fish/hours только в activity; feed получает лишь limit. После выбора водоёма снизу остаются чужие сигналы. | Фильтровать оба блока согласованно либо явно подписать независимый общий feed. URL и сброс сохраняют понятную область действия. |
| U03 | P1 | Главная показывает первые 20 activity, records — 50, каталоги — 100 activity; навигации по остальным нет. Счётчик `items.length` выглядит общим числом. | «Показано N из M», пагинация/следующая страница с сохранением фильтров; отдельно считать уникальные точки и комбинации точка+рыба. |
| U04 | P1 | Ошибки 422/429/5xx формы сводятся к «Проверьте поля»; нет ожидания/защиты от повторной отправки. Сохранение текста через sessionStorage не гарантировано при его недоступности; файл не восстанавливается. | Разные полезные сообщения, Retry-After для 429, блокировка повторного submit, честная подсказка о файле, server fallback. |
| U05 | P2 | В 390×844 результаты начинаются около y=700, mobile меню скрывает последний пункт; при 0 точек зелёный «пульс» всё ещё изображает живую активность. | Компактный первый экран, заметный доступ к скрытой навигации, отдельные empty/stale/error состояния, прямые действия «72 часа», «Сбросить», «Добавить улов». |
| U06 | P2 | Карточки многократно повторяют качество/доверие; «Не рассчитана», «сырые данные», «сюжеты» описывают реализацию. Текстовые badges часто 9–11 px. | Иерархия рыба → точка → приманка → возраст → источник; понятные статусы без выдуманных процентов, крупнее значимые подписи. |
| U07 | P2 | Предыдущая визуальная приёмка недостаточна: главная была пустой. | Матрица empty/1/many/long names/error; desktop/mobile, клавиатура, 200% zoom, раскрытые фильтры и форма; проверить также каталоги, detail и admin. Отдельно повторить axe, не приравнивая его к UX-аудиту. |
## Визуальная идентичность
Сильная основа: тёмный хвойный фон, лаймовый акцент, спокойная бумажная подложка, контрастная антиква, озеро, крючок, поплавок, рябь и координатный радар. Это стоит сохранить. Следующие пункты — дизайнерские предложения, не программные дефекты.
| ID | Приоритет | Предложение | Проверяемый результат |
|---|---|---|---|
| V01 | P2 | Утвердить RF4 Spotter как основное имя, «Ни хвоста, ни чешуи» как слоган либо явно выбрать обратную иерархию. | Одинаковое узнаваемое имя в шапке, favicon/manifest, title, OG и footer. |
| V02 | P2 | Свести размеры текста, отступы, радиусы, тени, focus и семантические цвета в небольшой набор CSS-токенов. | Компонентные состояния document/demo; источник и качество различаются текстом/формой, не только цветом. Разделить монолитный минифицированный global.css по компонентам. |
| V03 | P2 | Сделать паспорт точки компактным «полевым журналом»: координатная метка с копированием, линия времени, SVG рыбы, легенда шкалы. | Один основной визуальный акцент на карточку; индекс не выглядит вероятностью поимки, радар не выдаётся за настоящую карту. |
| V04 | P2 | Проверить маленькие знаки 16/32 px, sharpness app icons и maskable safe area; точность OG alt. | Чёткие отдельные SVG/PNG варианты; OG обещает реальный продукт и имеет осмысленную подпись изображения. |
| V05 | P3 | Отдельные обложки водоёмов и тематические OG для рыбы/точки. | Только после согласования прав и измерения веса; не препятствуют чтению карточек и загрузке. |
## SEO и производительность
SSR, русский `lang`, canonical, OG/Twitter, JSON-LD, sitemap, slug URL и 301 со старых UUID уже есть. Недостающие метатеги не являются главным препятствием: сейчас важнее корректные ответы, полнота каталога и полезный постоянный контент.
| ID | Приоритет | Подтверждение/риск | Критерий исправления |
|---|---|---|---|
| S01 | P1 | index/records/report/status при сбое API отображают ошибку с HTTP 200; в detail возможен частично заполненный spot и `noindex={!spot}`. | Единая таблица 200/404/422/503, no-store/Retry-After и политика индексации ошибок. Не выдавать пустую аварийную страницу за нормальный результат. |
| S02 | P1 | Sitemap собирает полный справочник, но detail ищет slug только в первых 500 элементах; формы загружают первые 200. При росте появятся sitemap URL с ложным 404. | Прямые slug endpoints и пагинация каталогов; все sitemap URL разрешаются независимо от позиции в справочнике. |
| S03 | P2 | canonical убирает все query; structured data содержит жёсткий домен, `PUBLIC_SITE_URL` не передаётся в Docker build. Отдельная политика trailing slash/пагинации не оформлена. | Единый origin, политика вариантов URL и страниц пагинации; внутренние ссылки используют конечные canonical URL (в sidebar ещё UUID redirect). |
| S04 | P2 | Постоянные страницы рыба+водоём живут в sitemap, но контент ограничен 72 часами и быстро превращается в шаблон «данных нет». | Полезный подтверждённый справочный контент, дата обновления/источники, архив последних известных наблюдений с явной давностью, видимые breadcrumbs. Решение об индексации пустых комбинаций. |
| S05 | P2 | Sitemap lastGood хранится в памяти без максимального срока fallback; >49000 путей вызывает ошибку вместо index. | Документированный срок fallback, sitemap index при необходимости, корректный lastmod только по реальным изменениям. |
| S06 | P2 | Hero уже WebP ~71 КБ, но не включён в Caddy matcher brandAssets; старый LCP 9,3 с не является текущим измерением. | Проверить фактический Cache-Control hero; свежий mobile/desktop performance baseline после P0/U01, затем production Core Web Vitals. Не обещать ускорение в секундах без измерения. |
| S07 | P2 | robots содержит обычные правила и не закрывает весь staging; поисковые сервисы/превью ещё не проверены на рабочем домене. | Закрытие тестового окружения, проверка Google Search Console и Яндекс Вебмастера после DNS/TLS, sitemap/JSON-LD/OG validation и наблюдение за индексацией. |
Политика HTTP основана на [Google: HTTP status codes](https://developers.google.com/crawling/docs/troubleshooting/http-status-codes), а согласование URL — на [Google: canonical URLs](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls). Наличие schema/сайткарты само по себе не обещает индексацию или позиции.
## Порядок реализации
1. T01T06: production маршруты, auth, сеть, клиентский лимит и реальная приёмка через Caddy.
2. D01D08, T07: лимиты/публикация, наполнение, честное время и оценка конкретной точки.
3. U01U04, S01–S02: основные сценарии, пагинация, ошибки и доступность индексируемых страниц.
4. V01V04, U05U07, S03S06: визуальная система, понятность и поисковое качество.
5. T08T10, D09D10 и внешний launch checklist; S07 на рабочем домене. V05 после пилота.
Тестировать по риску: адресные unit/contract проверки, один общий proxy/Compose-прогон для инфраструктурного пакета, браузер для визуального пакета. Документация и небольшие стилевые правки не требуют пересборки всех контейнеров. Статус выполнения ведётся в ROADMAP по ID этого отчёта.