104 lines
26 KiB
Markdown
104 lines
26 KiB
Markdown
# Аудит 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. T01–T06: production маршруты, auth, сеть, клиентский лимит и реальная приёмка через Caddy.
|
||
2. D01–D08, T07: лимиты/публикация, наполнение, честное время и оценка конкретной точки.
|
||
3. U01–U04, S01–S02: основные сценарии, пагинация, ошибки и доступность индексируемых страниц.
|
||
4. V01–V04, U05–U07, S03–S06: визуальная система, понятность и поисковое качество.
|
||
5. T08–T10, D09–D10 и внешний launch checklist; S07 на рабочем домене. V05 после пилота.
|
||
|
||
Тестировать по риску: адресные unit/contract проверки, один общий proxy/Compose-прогон для инфраструктурного пакета, браузер для визуального пакета. Документация и небольшие стилевые правки не требуют пересборки всех контейнеров. Статус выполнения ведётся в ROADMAP по ID этого отчёта.
|