Compare commits

...
3 Commits
Author SHA1 Message Date
ik f04dbabb3c fix: avoid duplicate leader panel
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s
2026-09-07 18:51:22 +07:00
ik fa3b24fb1f feat: add safe diagnostics export 2026-09-07 18:49:53 +07:00
ik 6477681e41 ops: monitor database and object storage growth 2026-09-07 18:46:34 +07:00
12 changed files with 71 additions and 17 deletions
+2
View File
@@ -42,4 +42,6 @@ BACKUP_ROOT=/srv/rf4-backups
MONITOR_DISK_MAX_PERCENT=85 MONITOR_DISK_MAX_PERCENT=85
MONITOR_TLS_MIN_DAYS=14 MONITOR_TLS_MIN_DAYS=14
MONITOR_BACKUP_MAX_HOURS=26 MONITOR_BACKUP_MAX_HOURS=26
MONITOR_DB_MAX_MB=2048
MONITOR_MINIO_MAX_MB=10240
LOG_LEVEL=INFO LOG_LEVEL=INFO
+3 -3
View File
@@ -10,7 +10,7 @@ RF4 Spotter — неофициальный сервис свежих точек
RF4DB/RF4-STAT/RF4MAP/RF4 Posts сначала принимаются в изолированный staging. Полные записи с ранее подтверждёнными алиасами источника публикуются автоматически; новые соответствия и неполные записи остаются на ручной проверке. Для разрешённых community-источников действует интервал не менее 30 минут на источник. Открытая альфа не использует продуктовый allowlist: интерфейс показывает весь корректно загруженный разрешённый каталог, сохраняя требования полноты и модерации. 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` опубликована легенда цветов всех источников и статусов качества. Базовый 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). Политика минимизации данных и ежедневная 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-логов контейнеров. Ежедневный systemd timer создаёт проверяемую копию до retention-очистки, а production Compose ограничивает рост JSON-логов контейнеров.
Фактическое состояние DNS/TLS домена и серверный чек-лист ведутся в [`docs/deployment-status.md`](docs/deployment-status.md). Фактическое состояние 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 сам выполняет `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
View File
@@ -258,8 +258,26 @@ def _admin(authorization: Annotated[str | None, Header()] = None) -> str:
@app.get("/api/v1/admin/diagnostics") @app.get("/api/v1/admin/diagnostics")
def admin_diagnostics(_: Annotated[str, Depends(_admin)]) -> dict[str, str]: def admin_diagnostics(db: Db, _: Annotated[str, Depends(_admin)]) -> JSONResponse:
return {"version": settings.app_version, "revision": settings.app_revision, "environment": settings.deployment_environment} 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]) @app.get("/api/v1/imports", response_model=list[ImportRunOut])
+7 -1
View File
@@ -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 assert client.get("/api/v1/admin/diagnostics").status_code == 401
response = client.get("/api/v1/admin/diagnostics", headers={"Authorization": "Bearer change-me-in-production"}) response = client.get("/api/v1/admin/diagnostics", headers={"Authorization": "Bearer change-me-in-production"})
assert response.status_code == 200 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: def test_spot_detail_and_catches() -> None:
+1
View File
@@ -10,6 +10,7 @@ import "../styles/fish-silhouette.css";
import "../styles/empty-states.css"; import "../styles/empty-states.css";
import "../styles/alpha-banner.css"; import "../styles/alpha-banner.css";
import "../styles/signal-pagination.css"; import "../styles/signal-pagination.css";
import "../styles/dashboard-polish.css";
import FishingIcon from "../components/FishingIcon.astro"; import FishingIcon from "../components/FishingIcon.astro";
import AlphaBanner from "../components/AlphaBanner.astro"; import AlphaBanner from "../components/AlphaBanner.astro";
const { const {
+1 -1
View File
@@ -27,7 +27,7 @@ try {
items = await api<Activity[]>(`/api/v1/activity?${query}`); items = await api<Activity[]>(`/api/v1/activity?${query}`);
} }
} catch { unavailable = true; } } 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 selectedWaterbody = waterbodies.find(item => item.slug === waterbody)?.name_ru ?? "Все водоёмы";
const selectedFish = fishes.find(item => item.slug === fish)?.name_ru ?? "Любая рыба"; const selectedFish = fishes.find(item => item.slug === fish)?.name_ru ?? "Любая рыба";
const periodLabel = { "6": "6 часов", "12": "12 часов", "24": "24 часа", "72": "72 часа" }[hours] ?? hours; const periodLabel = { "6": "6 часов", "12": "12 часов", "24": "24 часа", "72": "72 часа" }[hours] ?? hours;
+1
View File
@@ -0,0 +1 @@
.dashboard:has(>.results-column:only-child){grid-template-columns:1fr}
+7 -1
View File
@@ -42,6 +42,12 @@ curl -fsS https://rf4spotter.ru/health
curl -fsS https://rf4spotter.ru/ready 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 и получения сертификатов выполните внешний этап той же проверки: После обновления DNS и получения сертификатов выполните внешний этап той же проверки:
```bash ```bash
@@ -145,7 +151,7 @@ systemctl list-timers rf4spotter-maintenance.timer
./deploy/monitor.sh ./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. Что ещё блокирует публичное открытие альфы ## 9. Что ещё блокирует публичное открытие альфы
+18 -2
View File
@@ -20,10 +20,14 @@ backup_root=${BACKUP_ROOT:-$(read_env BACKUP_ROOT)}
disk_max=${MONITOR_DISK_MAX_PERCENT:-$(read_env MONITOR_DISK_MAX_PERCENT)} disk_max=${MONITOR_DISK_MAX_PERCENT:-$(read_env MONITOR_DISK_MAX_PERCENT)}
tls_days=${MONITOR_TLS_MIN_DAYS:-$(read_env MONITOR_TLS_MIN_DAYS)} tls_days=${MONITOR_TLS_MIN_DAYS:-$(read_env MONITOR_TLS_MIN_DAYS)}
backup_hours=${MONITOR_BACKUP_MAX_HOURS:-$(read_env MONITOR_BACKUP_MAX_HOURS)} 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} disk_max=${disk_max:-85}
tls_days=${tls_days:-14} tls_days=${tls_days:-14}
backup_hours=${backup_hours:-26} 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 case "$value" in ''|*[!0-9]*) printf 'CRITICAL rf4spotter failures="monitor-threshold-invalid"\n'; exit 2 ;; esac
done done
base_url=${MONITOR_BASE_URL:-https://$site_domain} 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}%" ;; *) [ "$disk_used" -lt "$disk_max" ] || fail "disk:${disk_used}%" ;;
esac 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 if [ -z "$backup_root" ] || [ ! -d "$backup_root" ]; then
fail "backup:directory" fail "backup:directory"
else else
@@ -77,4 +93,4 @@ if [ -n "$failures" ]; then
exit 1 exit 1
fi 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
View File
@@ -123,6 +123,7 @@
### Графические элементы и объяснение данных ### Графические элементы и объяснение данных
- [x] Не показывать дублирующий sidebar лидера при единственной найденной точке (7 сентября 2026).
- [x] Подготовить фирменный знак «крючок + поплавок + координатная сетка» и согласованный набор favicon/app icons (7 сентября 2026). - [x] Подготовить фирменный знак «крючок + поплавок + координатная сетка» и согласованный набор favicon/app icons (7 сентября 2026).
- [x] Добавить честный координатный радар без имитации отсутствующей карты на страницу точки (7 сентября 2026). - [x] Добавить честный координатный радар без имитации отсутствующей карты на страницу точки (7 сентября 2026).
- [x] Добавить временную шкалу активности 72 часа с 12-часовым шагом в виде лески с поплавками (7 сентября 2026). - [x] Добавить временную шкалу активности 72 часа с 12-часовым шагом в виде лески с поплавками (7 сентября 2026).
@@ -143,8 +144,8 @@
- [x] Настроить долгий immutable cache для хешированных assets и разумный cache для изображений/favicon (7 сентября 2026). - [x] Настроить долгий immutable cache для хешированных assets и разумный cache для изображений/favicon (7 сентября 2026).
- [x] Добавить серверное «Показать ещё» для публичной ленты полевых сигналов с сохранением фильтров и пределом 48 записей (7 сентября 2026). - [x] Добавить серверное «Показать ещё» для публичной ленты полевых сигналов с сохранением фильтров и пределом 48 записей (7 сентября 2026).
- [x] Объединять одинаковые полевые сигналы в сюжеты без потери уникальных ссылок provenance и показывать число совпадений (7 сентября 2026). - [x] Объединять одинаковые полевые сигналы в сюжеты без потери уникальных ссылок provenance и показывать число совпадений (7 сентября 2026).
- [ ] Добавить безопасный административный экспорт диагностики без персональных данных. - [x] Добавить защищённый JSON-экспорт диагностики со сборкой и агрегированными счётчиками без персональных данных, URL и ошибок источников (7 сентября 2026).
- [ ] Контролировать рост PostgreSQL и MinIO с порогами предупреждений. - [x] Контролировать рост PostgreSQL и MinIO host-monitor'ом с настраиваемыми порогами и runbook реакции (7 сентября 2026).
- [ ] Добавить фоновую проверку битых исходных ссылок с соблюдением лимитов источников. - [ ] Добавить фоновую проверку битых исходных ссылок с соблюдением лимитов источников.
- [x] Показывать версию и commit SHA в readiness и защищённой административной диагностике (7 сентября 2026). - [x] Показывать версию и commit SHA в readiness и защищённой административной диагностике (7 сентября 2026).
- [x] Добавить публичную `/status` без внутренних адресов, секретов и текстов ошибок (7 сентября 2026). - [x] Добавить публичную `/status` без внутренних адресов, секретов и текстов ошибок (7 сентября 2026).
+2 -2
View File
@@ -73,9 +73,9 @@
- [x] Заменить slug-input фильтрами из справочников и добавить сброс. - [x] Заменить slug-input фильтрами из справочников и добавить сброс.
- [x] Сделать мобильную выдачу рекордов самодостаточной с подписями значений. - [x] Сделать мобильную выдачу рекордов самодостаточной с подписями значений.
- [ ] Убрать CLI-команду из публичного empty state. - [x] Убрать CLI-команду из публичного empty state.
- [ ] Добавить к уловам detail-страницы дату/свежесть и происхождение, если API их отдаёт. - [ ] Добавить к уловам detail-страницы дату/свежесть и происхождение, если API их отдаёт.
- [ ] Устранить дублирование карточки и sidebar на малой выборке. - [x] Устранить дублирование карточки и sidebar на малой выборке: блок лидера показывается только при двух и более результатах (7 сентября 2026).
Критерий: фильтрация не требует технических терминов; каждая мобильная запись читается без заголовка таблицы; источник и свежесть понятны. Критерий: фильтрация не требует технических терминов; каждая мобильная запись читается без заголовка таблицы; источник и свежесть понятны.
+6 -3
View File
@@ -7,10 +7,11 @@
- контейнеры `proxy`, `db`, `minio`, `api`, `web` находятся в running; - контейнеры `proxy`, `db`, `minio`, `api`, `web` находятся в running;
- публичный `https://rf4spotter.ru/ready` сообщает `ready`; - публичный `https://rf4spotter.ru/ready` сообщает `ready`;
- файловая система проекта заполнена менее чем на 85%; - файловая система проекта заполнена менее чем на 85%;
- база PostgreSQL занимает менее 2 ГБ, а данные MinIO — менее 10 ГБ;
- последняя полная резервная копия не старше 26 часов и проходит проверку `SHA256SUMS`; - последняя полная резервная копия не старше 26 часов и проходит проверку `SHA256SUMS`;
- TLS-сертификат домена действует ещё минимум 14 дней. - 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`. 1. `service:*` — посмотреть `docker compose ... ps` и логи конкретного контейнера; не выполнять `down -v`.
2. `readiness` — проверить `/ready`, PostgreSQL и MinIO; на время сбоя остановить приём пользовательских заявок. 2. `readiness` — проверить `/ready`, PostgreSQL и MinIO; на время сбоя остановить приём пользовательских заявок.
3. `disk:*` — сначала перенести старые backup во внешнее хранилище; не удалять Docker volumes вслепую. 3. `disk:*` — сначала перенести старые backup во внешнее хранилище; не удалять Docker volumes вслепую.
4. `backup:*` — запустить `deploy/backup.sh`, проверить `SHA256SUMS` и устранить причину пропуска расписания. 4. `postgresql-size:*` — проверить рост таблиц и выполнение retention; перед очисткой сделать backup и не выполнять ручное удаление строк без плана.
5. `tls:*`проверить DNS, доступность портов 80/443 и логи Caddy; не отключать проверку сертификата. 5. `minio-size:*`сверить объекты с заявками и политикой retention; не удалять volume или бакет целиком.
6. `backup:*` — запустить `deploy/backup.sh`, проверить `SHA256SUMS` и устранить причину пропуска расписания.
7. `tls:*` — проверить DNS, доступность портов 80/443 и логи Caddy; не отключать проверку сертификата.