Compare commits
3
Commits
0f9c255596
...
f04dbabb3c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f04dbabb3c | ||
|
|
fa3b24fb1f | ||
|
|
6477681e41 |
@@ -42,4 +42,6 @@ BACKUP_ROOT=/srv/rf4-backups
|
||||
MONITOR_DISK_MAX_PERCENT=85
|
||||
MONITOR_TLS_MIN_DAYS=14
|
||||
MONITOR_BACKUP_MAX_HOURS=26
|
||||
MONITOR_DB_MAX_MB=2048
|
||||
MONITOR_MINIO_MAX_MB=10240
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
@@ -10,7 +10,7 @@ RF4 Spotter — неофициальный сервис свежих точек
|
||||
|
||||
RF4DB/RF4-STAT/RF4MAP/RF4 Posts сначала принимаются в изолированный staging. Полные записи с ранее подтверждёнными алиасами источника публикуются автоматически; новые соответствия и неполные записи остаются на ручной проверке. Для разрешённых community-источников действует интервал не менее 30 минут на источник. Открытая альфа не использует продуктовый allowlist: интерфейс показывает весь корректно загруженный разрешённый каталог, сохраняя требования полноты и модерации.
|
||||
|
||||
На сайте у каждой записи отображается источник, а у агрегированной активности — все вошедшие в расчёт источники. Неполные community-наблюдения публикуются сразу в отдельной ленте «Полевые сигналы» с предупреждением и перечнем отсутствующих полей; до подтверждения полноты они не влияют на индекс клёва. Лента раскрывается серверной кнопкой «Показать ещё», сохраняет выбранные фильтры и ограничена 48 сигналами на страницу. Совпадающие рыба, водоём, координаты и вес объединяются визуально, при этом карточка сохраняет все уникальные ссылки на исходные наблюдения.
|
||||
На сайте у каждой записи отображается источник, а у агрегированной активности — все вошедшие в расчёт источники. Неполные community-наблюдения публикуются сразу в отдельной ленте «Полевые сигналы» с предупреждением и перечнем отсутствующих полей; до подтверждения полноты они не влияют на индекс клёва. Лента раскрывается серверной кнопкой «Показать ещё», сохраняет выбранные фильтры и ограничена 48 сигналами на страницу. Совпадающие рыба, водоём, координаты и вес объединяются визуально, при этом карточка сохраняет все уникальные ссылки на исходные наблюдения. Sidebar лидера скрывается при единственном результате, чтобы не повторять ту же карточку.
|
||||
|
||||
Базовый SEO-контур готов для `rf4spotter.ru`: страницы имеют уникальные метаданные, canonical, Open Graph/Twitter Card, фирменное изображение 1200×630 и JSON-LD; доступны динамические `/robots.txt` и `/sitemap.xml`, административные и ошибочные страницы закрыты от индексации, добавлена собственная страница 404. Индексируемые каталоги рыб и водоёмов, detail-страницы и сочетания водоём + рыба строятся из актуального разрешённого справочника и включаются в sitemap. Подключены фирменные favicon/app icons, web manifest и production-кэширование статических ресурсов. Карточки активности показывают единый паспорт данных: источники, свежесть, полноту и уровень доверия. На `/status` опубликована легенда цветов всех источников и статусов качества.
|
||||
|
||||
@@ -26,7 +26,7 @@ Production-контур для домена `rf4spotter.ru`, TLS, секреты
|
||||
|
||||
Политика минимизации данных и ежедневная dry-run-first очистка описаны в [`docs/data-retention.md`](docs/data-retention.md).
|
||||
|
||||
Host-side мониторинг контейнеров, readiness, диска, резервных копий и TLS описан в [`docs/production-monitoring.md`](docs/production-monitoring.md).
|
||||
Host-side мониторинг контейнеров, readiness, диска, объёма PostgreSQL/MinIO, резервных копий и TLS описан в [`docs/production-monitoring.md`](docs/production-monitoring.md).
|
||||
Ежедневный systemd timer создаёт проверяемую копию до retention-очистки, а production Compose ограничивает рост JSON-логов контейнеров.
|
||||
|
||||
Фактическое состояние DNS/TLS домена и серверный чек-лист ведутся в [`docs/deployment-status.md`](docs/deployment-status.md).
|
||||
@@ -88,7 +88,7 @@ docker compose up --build
|
||||
|
||||
Контейнер API сам выполняет `alembic upgrade head`, затем идемпотентный seed. PostgreSQL хранит данные в именованном volume `postgres_data`, а MinIO — в `minio_data`. Compose ожидает readiness PostgreSQL и MinIO перед API, а API-контейнер проверяет `/ready`. Версия и commit SHA задаются через `APP_VERSION`/`APP_REVISION`; те же значения доступны администратору в `/api/v1/admin/diagnostics`. Официальный импорт по умолчанию необязателен; при включённом scheduler установите `OFFICIAL_IMPORT_REQUIRED=true`, тогда отсутствующий, неуспешный или просроченный запуск сделает readiness отрицательным.
|
||||
|
||||
API и scheduler пишут по одной JSON-записи на событие. HTTP-лог содержит только сгенерированный `request_id`, метод, путь без query string, статус и длительность; IP, заголовок авторизации и пользовательский payload не журналируются. `X-Request-ID` возвращается клиенту. Стандартный access-log Uvicorn отключён. Уровень управляется `LOG_LEVEL`.
|
||||
API и scheduler пишут по одной JSON-записи на событие. HTTP-лог содержит только сгенерированный `request_id`, метод, путь без query string, статус и длительность; IP, заголовок авторизации и пользовательский payload не журналируются. `X-Request-ID` возвращается клиенту. Стандартный access-log Uvicorn отключён. Уровень управляется `LOG_LEVEL`. Защищённый `/api/v1/admin/diagnostics` скачивает JSON только с идентификатором сборки и агрегированными счётчиками, без имён игроков, исходных URL, payload и ошибок парсеров.
|
||||
|
||||
Остановка:
|
||||
|
||||
|
||||
+20
-2
@@ -258,8 +258,26 @@ def _admin(authorization: Annotated[str | None, Header()] = None) -> str:
|
||||
|
||||
|
||||
@app.get("/api/v1/admin/diagnostics")
|
||||
def admin_diagnostics(_: Annotated[str, Depends(_admin)]) -> dict[str, str]:
|
||||
return {"version": settings.app_version, "revision": settings.app_revision, "environment": settings.deployment_environment}
|
||||
def admin_diagnostics(db: Db, _: Annotated[str, Depends(_admin)]) -> JSONResponse:
|
||||
report_counts = {status.value: count for status, count in db.execute(
|
||||
select(CatchReport.moderation_status, func.count()).group_by(CatchReport.moderation_status)
|
||||
)}
|
||||
observation_counts = {status: count for status, count in db.execute(
|
||||
select(ExternalObservation.status, func.count()).group_by(ExternalObservation.status)
|
||||
)}
|
||||
payload = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"build": {"version": settings.app_version, "revision": settings.app_revision, "environment": settings.deployment_environment},
|
||||
"counts": {
|
||||
"catch_reports": report_counts,
|
||||
"external_observations": observation_counts,
|
||||
"data_sources": db.scalar(select(func.count()).select_from(DataSource)) or 0,
|
||||
"enabled_data_sources": db.scalar(select(func.count()).select_from(DataSource).where(DataSource.enabled.is_(True))) or 0,
|
||||
"official_import_runs": db.scalar(select(func.count()).select_from(OfficialRecordImport)) or 0,
|
||||
"community_import_runs": db.scalar(select(func.count()).select_from(CommunityImportRun)) or 0,
|
||||
},
|
||||
}
|
||||
return JSONResponse(payload, headers={"Content-Disposition": "attachment; filename=rf4spotter-diagnostics.json"})
|
||||
|
||||
|
||||
@app.get("/api/v1/imports", response_model=list[ImportRunOut])
|
||||
|
||||
@@ -89,7 +89,13 @@ def test_admin_diagnostics_exposes_build_identity_only_to_admin() -> None:
|
||||
assert client.get("/api/v1/admin/diagnostics").status_code == 401
|
||||
response = client.get("/api/v1/admin/diagnostics", headers={"Authorization": "Bearer change-me-in-production"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"version": "0.1.0", "revision": "dev", "environment": "development"}
|
||||
payload = response.json()
|
||||
assert payload["build"] == {"version": "0.1.0", "revision": "dev", "environment": "development"}
|
||||
assert set(payload) == {"generated_at", "build", "counts"}
|
||||
assert response.headers["Content-Disposition"] == "attachment; filename=rf4spotter-diagnostics.json"
|
||||
serialized = response.text.lower()
|
||||
for forbidden in ("player_name", "source_url", "error_summary", "raw_payload", "admin_token", "s3_"):
|
||||
assert forbidden not in serialized
|
||||
|
||||
|
||||
def test_spot_detail_and_catches() -> None:
|
||||
|
||||
@@ -10,6 +10,7 @@ import "../styles/fish-silhouette.css";
|
||||
import "../styles/empty-states.css";
|
||||
import "../styles/alpha-banner.css";
|
||||
import "../styles/signal-pagination.css";
|
||||
import "../styles/dashboard-polish.css";
|
||||
import FishingIcon from "../components/FishingIcon.astro";
|
||||
import AlphaBanner from "../components/AlphaBanner.astro";
|
||||
const {
|
||||
|
||||
@@ -27,7 +27,7 @@ try {
|
||||
items = await api<Activity[]>(`/api/v1/activity?${query}`);
|
||||
}
|
||||
} catch { unavailable = true; }
|
||||
const leaderLevel = items[0] ? activityLevel(items[0].activity_score) : null;
|
||||
const leaderLevel = items.length > 1 && items[0] ? activityLevel(items[0].activity_score) : null;
|
||||
const selectedWaterbody = waterbodies.find(item => item.slug === waterbody)?.name_ru ?? "Все водоёмы";
|
||||
const selectedFish = fishes.find(item => item.slug === fish)?.name_ru ?? "Любая рыба";
|
||||
const periodLabel = { "6": "6 часов", "12": "12 часов", "24": "24 часа", "72": "72 часа" }[hours] ?? hours;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.dashboard:has(>.results-column:only-child){grid-template-columns:1fr}
|
||||
+7
-1
@@ -42,6 +42,12 @@ curl -fsS https://rf4spotter.ru/health
|
||||
curl -fsS https://rf4spotter.ru/ready
|
||||
```
|
||||
|
||||
Безопасный диагностический снимок для администратора:
|
||||
|
||||
```bash
|
||||
curl -fsS -H "Authorization: Bearer $ADMIN_TOKEN" -o rf4spotter-diagnostics.json https://rf4spotter.ru/api/v1/admin/diagnostics
|
||||
```
|
||||
|
||||
После обновления DNS и получения сертификатов выполните внешний этап той же проверки:
|
||||
|
||||
```bash
|
||||
@@ -145,7 +151,7 @@ systemctl list-timers rf4spotter-maintenance.timer
|
||||
./deploy/monitor.sh
|
||||
```
|
||||
|
||||
Проверка контролирует контейнеры, `/ready`, диск, свежесть backup и срок TLS. Установка systemd timer, пороги и порядок реакции описаны в [`docs/production-monitoring.md`](../docs/production-monitoring.md). До подключения реального канала уведомлений одного журнала systemd недостаточно.
|
||||
Проверка контролирует контейнеры, `/ready`, диск, объём PostgreSQL/MinIO, свежесть backup и срок TLS. Установка systemd timer, пороги и порядок реакции описаны в [`docs/production-monitoring.md`](../docs/production-monitoring.md). До подключения реального канала уведомлений одного журнала systemd недостаточно.
|
||||
|
||||
## 9. Что ещё блокирует публичное открытие альфы
|
||||
|
||||
|
||||
+18
-2
@@ -20,10 +20,14 @@ backup_root=${BACKUP_ROOT:-$(read_env BACKUP_ROOT)}
|
||||
disk_max=${MONITOR_DISK_MAX_PERCENT:-$(read_env MONITOR_DISK_MAX_PERCENT)}
|
||||
tls_days=${MONITOR_TLS_MIN_DAYS:-$(read_env MONITOR_TLS_MIN_DAYS)}
|
||||
backup_hours=${MONITOR_BACKUP_MAX_HOURS:-$(read_env MONITOR_BACKUP_MAX_HOURS)}
|
||||
db_max_mb=${MONITOR_DB_MAX_MB:-$(read_env MONITOR_DB_MAX_MB)}
|
||||
minio_max_mb=${MONITOR_MINIO_MAX_MB:-$(read_env MONITOR_MINIO_MAX_MB)}
|
||||
disk_max=${disk_max:-85}
|
||||
tls_days=${tls_days:-14}
|
||||
backup_hours=${backup_hours:-26}
|
||||
for value in "$disk_max" "$tls_days" "$backup_hours"; do
|
||||
db_max_mb=${db_max_mb:-2048}
|
||||
minio_max_mb=${minio_max_mb:-10240}
|
||||
for value in "$disk_max" "$tls_days" "$backup_hours" "$db_max_mb" "$minio_max_mb"; do
|
||||
case "$value" in ''|*[!0-9]*) printf 'CRITICAL rf4spotter failures="monitor-threshold-invalid"\n'; exit 2 ;; esac
|
||||
done
|
||||
base_url=${MONITOR_BASE_URL:-https://$site_domain}
|
||||
@@ -48,6 +52,18 @@ case "$disk_used" in
|
||||
*) [ "$disk_used" -lt "$disk_max" ] || fail "disk:${disk_used}%" ;;
|
||||
esac
|
||||
|
||||
db_bytes=$($compose exec -T db sh -c 'psql -At -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "select pg_database_size(current_database())"' 2>/dev/null) || db_bytes=""
|
||||
case "$db_bytes" in
|
||||
''|*[!0-9]*) db_mb="unknown"; fail "postgresql-size:unknown" ;;
|
||||
*) db_mb=$(((db_bytes + 1048575) / 1048576)); [ "$db_mb" -lt "$db_max_mb" ] || fail "postgresql-size:${db_mb}MB" ;;
|
||||
esac
|
||||
|
||||
minio_kb=$($compose exec -T minio du -sk /data 2>/dev/null | awk 'NR==1 {print $1}')
|
||||
case "$minio_kb" in
|
||||
''|*[!0-9]*) minio_mb="unknown"; fail "minio-size:unknown" ;;
|
||||
*) minio_mb=$(((minio_kb + 1023) / 1024)); [ "$minio_mb" -lt "$minio_max_mb" ] || fail "minio-size:${minio_mb}MB" ;;
|
||||
esac
|
||||
|
||||
if [ -z "$backup_root" ] || [ ! -d "$backup_root" ]; then
|
||||
fail "backup:directory"
|
||||
else
|
||||
@@ -77,4 +93,4 @@ if [ -n "$failures" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'OK rf4spotter timestamp=%s disk_used=%s%% backup_max_age=%sh tls_min=%sd\n' "$timestamp" "$disk_used" "$backup_hours" "$tls_days"
|
||||
printf 'OK rf4spotter timestamp=%s disk_used=%s%% postgresql=%sMB minio=%sMB backup_max_age=%sh tls_min=%sd\n' "$timestamp" "$disk_used" "$db_mb" "$minio_mb" "$backup_hours" "$tls_days"
|
||||
|
||||
+3
-2
@@ -123,6 +123,7 @@
|
||||
|
||||
### Графические элементы и объяснение данных
|
||||
|
||||
- [x] Не показывать дублирующий sidebar лидера при единственной найденной точке (7 сентября 2026).
|
||||
- [x] Подготовить фирменный знак «крючок + поплавок + координатная сетка» и согласованный набор favicon/app icons (7 сентября 2026).
|
||||
- [x] Добавить честный координатный радар без имитации отсутствующей карты на страницу точки (7 сентября 2026).
|
||||
- [x] Добавить временную шкалу активности 72 часа с 12-часовым шагом в виде лески с поплавками (7 сентября 2026).
|
||||
@@ -143,8 +144,8 @@
|
||||
- [x] Настроить долгий immutable cache для хешированных assets и разумный cache для изображений/favicon (7 сентября 2026).
|
||||
- [x] Добавить серверное «Показать ещё» для публичной ленты полевых сигналов с сохранением фильтров и пределом 48 записей (7 сентября 2026).
|
||||
- [x] Объединять одинаковые полевые сигналы в сюжеты без потери уникальных ссылок provenance и показывать число совпадений (7 сентября 2026).
|
||||
- [ ] Добавить безопасный административный экспорт диагностики без персональных данных.
|
||||
- [ ] Контролировать рост PostgreSQL и MinIO с порогами предупреждений.
|
||||
- [x] Добавить защищённый JSON-экспорт диагностики со сборкой и агрегированными счётчиками без персональных данных, URL и ошибок источников (7 сентября 2026).
|
||||
- [x] Контролировать рост PostgreSQL и MinIO host-monitor'ом с настраиваемыми порогами и runbook реакции (7 сентября 2026).
|
||||
- [ ] Добавить фоновую проверку битых исходных ссылок с соблюдением лимитов источников.
|
||||
- [x] Показывать версию и commit SHA в readiness и защищённой административной диагностике (7 сентября 2026).
|
||||
- [x] Добавить публичную `/status` без внутренних адресов, секретов и текстов ошибок (7 сентября 2026).
|
||||
|
||||
+2
-2
@@ -73,9 +73,9 @@
|
||||
|
||||
- [x] Заменить slug-input фильтрами из справочников и добавить сброс.
|
||||
- [x] Сделать мобильную выдачу рекордов самодостаточной с подписями значений.
|
||||
- [ ] Убрать CLI-команду из публичного empty state.
|
||||
- [x] Убрать CLI-команду из публичного empty state.
|
||||
- [ ] Добавить к уловам detail-страницы дату/свежесть и происхождение, если API их отдаёт.
|
||||
- [ ] Устранить дублирование карточки и sidebar на малой выборке.
|
||||
- [x] Устранить дублирование карточки и sidebar на малой выборке: блок лидера показывается только при двух и более результатах (7 сентября 2026).
|
||||
|
||||
Критерий: фильтрация не требует технических терминов; каждая мобильная запись читается без заголовка таблицы; источник и свежесть понятны.
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
- контейнеры `proxy`, `db`, `minio`, `api`, `web` находятся в running;
|
||||
- публичный `https://rf4spotter.ru/ready` сообщает `ready`;
|
||||
- файловая система проекта заполнена менее чем на 85%;
|
||||
- база PostgreSQL занимает менее 2 ГБ, а данные MinIO — менее 10 ГБ;
|
||||
- последняя полная резервная копия не старше 26 часов и проходит проверку `SHA256SUMS`;
|
||||
- TLS-сертификат домена действует ещё минимум 14 дней.
|
||||
|
||||
Пороги задаются в `.env.production`: `BACKUP_ROOT`, `MONITOR_DISK_MAX_PERCENT`, `MONITOR_BACKUP_MAX_HOURS`, `MONITOR_TLS_MIN_DAYS`. Проверка TLS использует SNI и поэтому обнаруживает как истечение, так и выдачу сертификата для неверного домена.
|
||||
Пороги задаются в `.env.production`: `BACKUP_ROOT`, `MONITOR_DISK_MAX_PERCENT`, `MONITOR_DB_MAX_MB`, `MONITOR_MINIO_MAX_MB`, `MONITOR_BACKUP_MAX_HOURS`, `MONITOR_TLS_MIN_DAYS`. Значения объёма задаются в МБ и для открытой альфы по умолчанию равны 2048/10240. Проверка TLS использует SNI и поэтому обнаруживает как истечение, так и выдачу сертификата для неверного домена.
|
||||
|
||||
## Ручная проверка
|
||||
|
||||
@@ -43,5 +44,7 @@ Monitor timer записывает результат в journal. Maintenance ti
|
||||
1. `service:*` — посмотреть `docker compose ... ps` и логи конкретного контейнера; не выполнять `down -v`.
|
||||
2. `readiness` — проверить `/ready`, PostgreSQL и MinIO; на время сбоя остановить приём пользовательских заявок.
|
||||
3. `disk:*` — сначала перенести старые backup во внешнее хранилище; не удалять Docker volumes вслепую.
|
||||
4. `backup:*` — запустить `deploy/backup.sh`, проверить `SHA256SUMS` и устранить причину пропуска расписания.
|
||||
5. `tls:*` — проверить DNS, доступность портов 80/443 и логи Caddy; не отключать проверку сертификата.
|
||||
4. `postgresql-size:*` — проверить рост таблиц и выполнение retention; перед очисткой сделать backup и не выполнять ручное удаление строк без плана.
|
||||
5. `minio-size:*` — сверить объекты с заявками и политикой retention; не удалять volume или бакет целиком.
|
||||
6. `backup:*` — запустить `deploy/backup.sh`, проверить `SHA256SUMS` и устранить причину пропуска расписания.
|
||||
7. `tls:*` — проверить DNS, доступность портов 80/443 и логи Caddy; не отключать проверку сертификата.
|
||||
|
||||
Reference in New Issue
Block a user