feat: enable safe community auto publishing

This commit is contained in:
ik
2026-09-07 13:00:58 +07:00
parent c4d1c8b87b
commit 7217356594
10 changed files with 130 additions and 21 deletions
+3 -3
View File
@@ -8,7 +8,7 @@ RF4 Spotter — неофициальный сервис свежих точек
Функциональный MVP и локальный production-контур готовятся к открытой альфе: официальный импорт, пользовательские заявки, модерация, объяснимый индекс, staging внешних источников, адаптивный Astro UI, миграции, резервное копирование, retention, мониторинг и security/accessibility-проверки реализованы. Автоматические импорты внешних источников выключены. Публичный запуск блокируют покупка и настройка сервера, DNS/TLS, реальные секреты, внешний backup, канал уведомлений и публичные страницы правил/privacy. Функциональный MVP и локальный production-контур готовятся к открытой альфе: официальный импорт, пользовательские заявки, модерация, объяснимый индекс, staging внешних источников, адаптивный Astro UI, миграции, резервное копирование, retention, мониторинг и security/accessibility-проверки реализованы. Автоматические импорты внешних источников выключены. Публичный запуск блокируют покупка и настройка сервера, DNS/TLS, реальные секреты, внешний backup, канал уведомлений и публичные страницы правил/privacy.
RF4DB/RF4-STAT/RF4MAP/RF4 Posts принимаются только в изолированный staging и не влияют на индекс без ручного сопоставления и публикации. Для разрешённых community-источников действует интервал не менее 30 минут на источник. Открытая альфа не использует продуктовый allowlist: интерфейс показывает весь корректно загруженный разрешённый каталог, сохраняя требования полноты и модерации. RF4DB/RF4-STAT/RF4MAP/RF4 Posts сначала принимаются в изолированный staging. Полные записи с ранее подтверждёнными алиасами источника публикуются автоматически; новые соответствия и неполные записи остаются на ручной проверке. Для разрешённых community-источников действует интервал не менее 30 минут на источник. Открытая альфа не использует продуктовый allowlist: интерфейс показывает весь корректно загруженный разрешённый каталог, сохраняя требования полноты и модерации.
Подробный план и актуальные чекбоксы находятся в [`docs/ROADMAP.md`](docs/ROADMAP.md). Результаты проверки интерфейса и пять приоритетных UX-пакетов описаны в [`docs/UI_UX_AUDIT.md`](docs/UI_UX_AUDIT.md). Подробный план и актуальные чекбоксы находятся в [`docs/ROADMAP.md`](docs/ROADMAP.md). Результаты проверки интерфейса и пять приоритетных UX-пакетов описаны в [`docs/UI_UX_AUDIT.md`](docs/UI_UX_AUDIT.md).
@@ -112,7 +112,7 @@ docker compose up --build
## Что реализовано ## Что реализовано
- FastAPI и SQLAlchemy 2; - FastAPI и SQLAlchemy 2;
- PostgreSQL 17 и миграции Alembic до `0011`; - PostgreSQL 17 и миграции Alembic до `0012`;
- идемпотентный seed с двумя точками и свежими демо-уловами; - идемпотентный seed с двумя точками и свежими демо-уловами;
- `GET /api/v1/activity` с фильтрами периода, водоёма, рыбы, способа и сортировки; - `GET /api/v1/activity` с фильтрами периода, водоёма, рыбы, способа и сортировки;
- `GET /api/v1/spots/{id}` и `/catches`; - `GET /api/v1/spots/{id}` и `/catches`;
@@ -213,7 +213,7 @@ Production-логи структурированы в JSON и не содерж
## Известные ограничения альфы ## Известные ограничения альфы
- нет пользовательских аккаунтов, OCR, Telegram-бота и уведомлений о клёве; - нет пользовательских аккаунтов, OCR, Telegram-бота и уведомлений о клёве;
- community-источники требуют ручной проверки; неполные наблюдения не публикуются; - community-источники автоматически публикуют только полные наблюдения с подтверждёнными алиасами; новые соответствия требуют ручной проверки, неполные наблюдения не публикуются;
- offset pagination рассчитана на пилотные объёмы, не на бесконечную ленту; - offset pagination рассчитана на пилотные объёмы, не на бесконечную ленту;
- Lighthouse LCP локального прогона — 9,3 с; оптимизация изображения/CDN остаётся после размещения; - Lighthouse LCP локального прогона — 9,3 с; оптимизация изображения/CDN остаётся после размещения;
- один сервер остаётся точкой отказа, поэтому обязательны внешний backup и мониторинг; - один сервер остаётся точкой отказа, поэтому обязательны внешний backup и мониторинг;
@@ -0,0 +1,23 @@
"""Enable all authorized community sources in the staging registry."""
from alembic import op
revision = "0012"
down_revision = "0011"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute("""
INSERT INTO data_source (key, name, base_url, default_confidence, enabled) VALUES
('rf4db', 'RF4DB', 'https://rf4db.com', 70, true),
('rf4stat-fishing', 'RF4-STAT fishing', 'https://rf4-stat.ru/fishing/', 65, true),
('rf4stat-post', 'RF4-STAT posts', 'https://rf4-stat.ru/posts/', 60, true),
('rf4map', 'RF4MAP', 'https://rf4map.ru', 55, true),
('rf4posts-spot', 'RF4 Posts spots', 'https://rf4-posts.com', 50, true)
ON CONFLICT (key) DO UPDATE SET enabled = EXCLUDED.enabled
""")
def downgrade() -> None:
op.execute("UPDATE data_source SET enabled = false WHERE key IN ('rf4db', 'rf4stat-fishing', 'rf4stat-post', 'rf4map', 'rf4posts-spot')")
+39 -2
View File
@@ -8,7 +8,8 @@ from urllib.parse import urlparse
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from .models import DataSource, ExternalObservation from .community_review import publish_observation
from .models import DataSource, ExternalEntityAlias, ExternalObservation
SOURCE_DEFAULTS = { SOURCE_DEFAULTS = {
@@ -36,6 +37,7 @@ def stage_observations(
) -> tuple[int, int]: ) -> tuple[int, int]:
fetched_at = fetched_at or datetime.now(timezone.utc) fetched_at = fetched_at or datetime.now(timezone.utc)
created = updated = 0 created = updated = 0
touched: list[ExternalObservation] = []
for raw in records: for raw in records:
payload = _json_payload(raw) payload = _json_payload(raw)
source_system = _required(payload, "source_system", 50) source_system = _required(payload, "source_system", 50)
@@ -45,7 +47,7 @@ def stage_observations(
source = session.get(DataSource, source_system) source = session.get(DataSource, source_system)
if source is None: if source is None:
name, base_url, confidence = SOURCE_DEFAULTS[source_system] name, base_url, confidence = SOURCE_DEFAULTS[source_system]
source = DataSource(key=source_system, name=name, base_url=base_url, default_confidence=confidence, enabled=False) source = DataSource(key=source_system, name=name, base_url=base_url, default_confidence=confidence, enabled=True)
session.add(source) session.add(source)
session.flush() session.flush()
observation = session.scalar(select(ExternalObservation).where( observation = session.scalar(select(ExternalObservation).where(
@@ -78,10 +80,45 @@ def stage_observations(
for key, value in values.items(): for key, value in values.items():
setattr(observation, key, value) setattr(observation, key, value)
updated += 1 updated += 1
touched.append(observation)
session.commit() session.commit()
for observation in touched:
_auto_publish(session, observation)
return created, updated return created, updated
def _auto_publish(session: Session, observation: ExternalObservation) -> bool:
"""Publish only complete observations covered by previously reviewed aliases."""
if (
observation.status not in {"staged", "mapped", "ready"}
or not observation.source.enabled
or observation.fish_external_id is None
or observation.waterbody_external_id is None
or observation.x is None
or observation.y is None
or observation.weight_g is None
):
return False
fish_alias = session.scalar(select(ExternalEntityAlias).where(
ExternalEntityAlias.source_system == observation.source_system,
ExternalEntityAlias.entity_type == "fish",
ExternalEntityAlias.external_id == observation.fish_external_id,
))
waterbody_alias = session.scalar(select(ExternalEntityAlias).where(
ExternalEntityAlias.source_system == observation.source_system,
ExternalEntityAlias.entity_type == "waterbody",
ExternalEntityAlias.external_id == observation.waterbody_external_id,
))
if fish_alias is None or fish_alias.fish is None or waterbody_alias is None or waterbody_alias.waterbody is None:
return False
observation.fish = fish_alias.fish
observation.waterbody = waterbody_alias.waterbody
observation.status = "ready"
observation.review_note = "Automatically matched by previously reviewed source aliases"
publish_observation(session, observation)
return True
def _json_payload(raw: dict[str, Any]) -> dict[str, Any]: def _json_payload(raw: dict[str, Any]) -> dict[str, Any]:
if not isinstance(raw, dict): if not isinstance(raw, dict):
raise CommunityImportError("each observation must be an object") raise CommunityImportError("each observation must be an object")
+1 -1
View File
@@ -145,7 +145,7 @@ class DataSource(Base):
name: Mapped[str] = mapped_column(String(100)) name: Mapped[str] = mapped_column(String(100))
base_url: Mapped[str] = mapped_column(Text) base_url: Mapped[str] = mapped_column(Text)
default_confidence: Mapped[int] default_confidence: Mapped[int]
enabled: Mapped[bool] = mapped_column(default=False) enabled: Mapped[bool] = mapped_column(default=True)
class ExternalObservation(Base): class ExternalObservation(Base):
+53 -5
View File
@@ -9,7 +9,7 @@ from sqlalchemy.orm import Session
from app.community_importer import CommunityImportError, stage_observations from app.community_importer import CommunityImportError, stage_observations
from app.database import Base from app.database import Base
from app.models import DataSource, ExternalObservation from app.models import CatchReport, DataSource, ExternalEntityAlias, ExternalObservation, Fish, Waterbody
from rf4_research.community_sources import parse_rf4db_catches, parse_rf4map_point, parse_rf4posts_spot from rf4_research.community_sources import parse_rf4db_catches, parse_rf4map_point, parse_rf4posts_spot
@@ -64,7 +64,7 @@ def test_staging_is_idempotent_and_preserves_first_seen(db: Session) -> None:
assert item.last_seen_at.replace(tzinfo=timezone.utc) == second assert item.last_seen_at.replace(tzinfo=timezone.utc) == second
assert db.scalar(select(func.count()).select_from(ExternalObservation)) == 1 assert db.scalar(select(func.count()).select_from(ExternalObservation)) == 1
source = db.get(DataSource, "rf4db") source = db.get(DataSource, "rf4db")
assert source is not None and source.enabled is False assert source is not None and source.enabled is True
def test_external_ids_are_isolated_by_source(db: Session) -> None: def test_external_ids_are_isolated_by_source(db: Session) -> None:
@@ -73,12 +73,60 @@ def test_external_ids_are_isolated_by_source(db: Session) -> None:
assert (created, updated) == (2, 0) assert (created, updated) == (2, 0)
def test_research_sources_can_enter_disabled_staging(db: Session) -> None: def test_research_sources_enter_enabled_staging(db: Session) -> None:
created, updated = stage_observations(db, [record("rf4map"), record("rf4posts-spot")]) created, updated = stage_observations(db, [record("rf4map"), record("rf4posts-spot")])
assert (created, updated) == (2, 0) assert (created, updated) == (2, 0)
assert db.get(DataSource, "rf4map").enabled is False assert db.get(DataSource, "rf4map").enabled is True
assert db.get(DataSource, "rf4posts-spot").enabled is False assert db.get(DataSource, "rf4posts-spot").enabled is True
def test_complete_observation_with_reviewed_aliases_is_published(db: Session) -> None:
source = DataSource(key="rf4db", name="RF4DB", base_url="https://rf4db.com", default_confidence=70, enabled=True)
fish = Fish(slug="pike", name_ru="Щука")
waterbody = Waterbody(slug="test-lake", name_ru="Тестовое озеро")
db.add_all([source, fish, waterbody])
db.flush()
now = datetime.now(timezone.utc)
db.add_all([
ExternalEntityAlias(source_system="rf4db", entity_type="fish", external_id="pike", external_name="Щука", fish=fish, updated_at=now),
ExternalEntityAlias(source_system="rf4db", entity_type="waterbody", external_id="test-lake", external_name="Тестовое озеро", waterbody=waterbody, updated_at=now),
])
db.commit()
assert stage_observations(db, [record() | {"weight_g": 5_000}]) == (1, 0)
item = db.scalar(select(ExternalObservation))
assert item is not None
assert item.status == "published"
assert item.catch_report is not None
assert item.catch_report.fish_id == fish.id
assert item.catch_report.waterbody_id == waterbody.id
assert db.scalar(select(func.count()).select_from(CatchReport)) == 1
assert stage_observations(db, [record() | {"weight_g": 5_000}]) == (0, 1)
db.refresh(item)
assert item.status == "published"
assert db.scalar(select(func.count()).select_from(CatchReport)) == 1
def test_auto_publication_requires_enabled_source(db: Session) -> None:
source = DataSource(key="rf4db", name="RF4DB", base_url="https://rf4db.com", default_confidence=70, enabled=False)
fish = Fish(slug="pike", name_ru="Щука")
waterbody = Waterbody(slug="test-lake", name_ru="Тестовое озеро")
db.add_all([source, fish, waterbody])
db.flush()
now = datetime.now(timezone.utc)
db.add_all([
ExternalEntityAlias(source_system="rf4db", entity_type="fish", external_id="pike", external_name="Щука", fish=fish, updated_at=now),
ExternalEntityAlias(source_system="rf4db", entity_type="waterbody", external_id="test-lake", external_name="Тестовое озеро", waterbody=waterbody, updated_at=now),
])
db.commit()
stage_observations(db, [record() | {"weight_g": 5_000}])
item = db.scalar(select(ExternalObservation))
assert item is not None and item.status == "staged" and item.catch_report is None
def test_parser_json_can_be_staged_without_losing_provenance(db: Session) -> None: def test_parser_json_can_be_staged_without_losing_provenance(db: Session) -> None:
+1 -1
View File
@@ -26,7 +26,7 @@ curl -fsS "http://127.0.0.1:$BOOTSTRAP_API_PORT/ready" >/dev/null
curl -fsS "http://127.0.0.1:$BOOTSTRAP_WEB_PORT/" >/dev/null curl -fsS "http://127.0.0.1:$BOOTSTRAP_WEB_PORT/" >/dev/null
curl -fsS -D - -o /dev/null "http://127.0.0.1:$BOOTSTRAP_API_PORT/health" | grep -qi '^x-frame-options: DENY' curl -fsS -D - -o /dev/null "http://127.0.0.1:$BOOTSTRAP_API_PORT/health" | grep -qi '^x-frame-options: DENY'
curl -fsS -D - -o /dev/null "http://127.0.0.1:$BOOTSTRAP_API_PORT/health" | grep -qi '^cross-origin-opener-policy: same-origin' curl -fsS -D - -o /dev/null "http://127.0.0.1:$BOOTSTRAP_API_PORT/health" | grep -qi '^cross-origin-opener-policy: same-origin'
test "$($compose exec -T db psql -At -U rf4 -d rf4_spotter -c 'select version_num from alembic_version')" = "0011" test "$($compose exec -T db psql -At -U rf4 -d rf4_spotter -c 'select version_num from alembic_version')" = "0012"
index_count=$($compose exec -T db psql -At -U rf4 -d rf4_spotter -c "select count(*) from pg_indexes where schemaname = 'public' and indexname in ('ix_catch_report_activity_lookup','ix_catch_report_spot_feed','ix_catch_report_moderation_queue','ix_catch_report_official_records','ix_official_import_source_status_started','ix_external_observation_review_queue','ix_submission_attempt_client_created','ix_moderation_event_created_at')") index_count=$($compose exec -T db psql -At -U rf4 -d rf4_spotter -c "select count(*) from pg_indexes where schemaname = 'public' and indexname in ('ix_catch_report_activity_lookup','ix_catch_report_spot_feed','ix_catch_report_moderation_queue','ix_catch_report_official_records','ix_official_import_source_status_started','ix_external_observation_review_queue','ix_submission_attempt_client_created','ix_moderation_event_created_at')")
test "$index_count" = "8" test "$index_count" = "8"
test "$($compose exec -T db psql -At -U rf4 -d rf4_spotter -c 'select count(*) from fish')" = "2" test "$($compose exec -T db psql -At -U rf4 -d rf4_spotter -c 'select count(*) from fish')" = "2"
+1
View File
@@ -65,6 +65,7 @@
- [x] Сделать seed устойчивым к частично заполненной БД: справочники досеиваются независимо, демо-уловы идемпотентны и принудительно отключены в production; повторный/частичный запуск покрыт конфигурационными и интеграционными проверками. - [x] Сделать seed устойчивым к частично заполненной БД: справочники досеиваются независимо, демо-уловы идемпотентны и принудительно отключены в production; повторный/частичный запуск покрыт конфигурационными и интеграционными проверками.
- [x] Проверить списочные API по требованию раздела 12: все выдачи имеют ограниченные `limit`/`offset`, детерминированный tie-breaker и типизированные фильтры; фильтр категории рекордов перенесён до пагинации. - [x] Проверить списочные API по требованию раздела 12: все выдачи имеют ограниченные `limit`/`offset`, детерминированный tie-breaker и типизированные фильтры; фильтр категории рекордов перенесён до пагинации.
- [x] Добавить составные индексы PostgreSQL для activity, модерации, официальных рекордов, staging, импорта, аудита и очистки rate limit (миграция `0011`); бюджет и процедура проверки планов зафиксированы в `docs/query-performance.md`. - [x] Добавить составные индексы PostgreSQL для activity, модерации, официальных рекордов, staging, импорта, аудита и очистки rate limit (миграция `0011`); бюджет и процедура проверки планов зафиксированы в `docs/query-performance.md`.
- [x] Включить все пять разрешённых community-источников и автоматическую публикацию полных наблюдений по ранее подтверждённым алиасам без межисточникового склеивания (миграция `0012`, 7 сентября 2026).
- [x] Провести security-проверку admin-аутентификации, CORS, headers, загрузок, контейнерных пользователей и секретов: двойная защита admin web/API, constant-time token, no-store, non-root API/web и отдельные MinIO root/app credentials; остаточные ограничения записаны в `docs/security-review.md`. - [x] Провести security-проверку admin-аутентификации, CORS, headers, загрузок, контейнерных пользователей и секретов: двойная защита admin web/API, constant-time token, no-store, non-root API/web и отдельные MinIO root/app credentials; остаточные ограничения записаны в `docs/security-review.md`.
- [x] Проверить авторизацию повторной загрузки скриншота: используется отдельный одноразовый случайный токен, в БД хранится только SHA-256, UUID заявки недостаточно. - [x] Проверить авторизацию повторной загрузки скриншота: используется отдельный одноразовый случайный токен, в БД хранится только SHA-256, UUID заявки недостаточно.
- [x] Определить сроки хранения ников, исходных payload, staging-наблюдений, moderation events и submission attempts; добавлены настраиваемая dry-run-first очистка, тест и `docs/data-retention.md`. - [x] Определить сроки хранения ников, исходных payload, staging-наблюдений, moderation events и submission attempts; добавлены настраиваемая dry-run-first очистка, тест и `docs/data-retention.md`.
+5 -5
View File
@@ -64,14 +64,14 @@ Detail-страница дополнительно содержит ветер,
- тесты всех извлекаемых полей, locked-координат и отказа на постороннем HTML; - тесты всех извлекаемых полей, locked-координат и отказа на постороннем HTML;
- живой контрольный прогон без сохранения персональных данных и изображений в репозиторий. - живой контрольный прогон без сохранения персональных данных и изображений в репозиторий.
RF4MAP и RF4 Posts остаются исследовательскими: на живых HTML-снимках получены соответственно 30 индивидуальных наблюдений и 6 видов рыб одной точки. Оба результата укладываются в nullable-контракт, имеют отдельные пространства ID и не смешиваются с RF4DB/RF4-STAT. Разрешение и минимальный интервал 30 минут подтверждены; источники разрешены для staging, но остаются выключенными. RF4 Posts трактуется как агрегированная точка, а не индивидуальный улов. На живых HTML-снимках RF4MAP и RF4 Posts получены соответственно 30 индивидуальных наблюдений и 6 видов рыб одной точки. Оба результата укладываются в nullable-контракт, имеют отдельные пространства ID и не смешиваются с RF4DB/RF4-STAT. Разрешение и минимальный интервал 30 минут подтверждены; все источники включены миграцией `0012`. RF4 Posts трактуется как агрегированная точка, а не индивидуальный улов.
## Перед продуктивным импортом ## Перед продуктивным импортом
1. Сохранить подтверждение разрешения и согласованные лимиты запросов. 1. Сохранить подтверждение разрешения и согласованные лимиты запросов.
2. Расширять добавленные алиасы рыб и водоёмов по мере ручной проверки новых значений. 2. Расширять добавленные алиасы рыб и водоёмов по мере ручной проверки новых значений.
3. Согласовать начальные уровни доверия для RF4DB и двух каналов RF4-STAT (в staging записаны консервативные значения 70/65/60, но источники выключены). 3. При необходимости пересмотреть консервативные начальные уровни доверия 70/65/60/55/50.
4. Проверить правила публикации на небольшой вручную подтверждённой выборке. 4. Расширять контрольную выборку автопубликации по мере появления полных наблюдений.
5. Не хранить и не проксировать изображения без отдельного условия разрешения; на первом этапе достаточно исходной ссылки. 5. Не хранить и не проксировать изображения без отдельного условия разрешения; на первом этапе достаточно исходной ссылки.
## Staging ## Staging
@@ -80,6 +80,6 @@ RF4MAP и RF4 Posts остаются исследовательскими: на
Загрузка принимает только известные источники, HTTPS-ссылки соответствующего домена и значения в допустимых диапазонах. Все записи получают статус `staged`; таблица не связана с расчётом активности. Тест доказывает путь `HTML → ExternalCatch → JSON → external_observation` без потери provenance. Загрузка принимает только известные источники, HTTPS-ссылки соответствующего домена и значения в допустимых диапазонах. Все записи получают статус `staged`; таблица не связана с расчётом активности. Тест доказывает путь `HTML → ExternalCatch → JSON → external_observation` без потери provenance.
Контрольная загрузка в PostgreSQL создала 139 staging-записей: 23 `rf4db` с координатами, 100 `rf4stat-fishing` с весом и 16 `rf4stat-post` с весом. Немедленный повтор дал `created=0, updated=139`, подтвердив идемпотентность на реальной БД. Все три источника остались `enabled=false`. Контрольная загрузка в PostgreSQL создала 139 staging-записей: 23 `rf4db` с координатами, 100 `rf4stat-fishing` с весом и 16 `rf4stat-post` с весом. Немедленный повтор дал `created=0, updated=139`, подтвердив идемпотентность на реальной БД. Миграция `0012` включает все пять разрешённых источников.
Миграция `0009` добавляет устойчивые алиасы внешних рыб и водоёмов, канонические ссылки и состояние ручной проверки. Страница `/admin/external-sources` позволяет сопоставить, отклонить или явно опубликовать наблюдение. Публикация создаёт одобренный `catch_report` с исходной ссылкой и полным provenance, но только если одновременно известны канонические сущности, координаты и вес. Поэтому текущие 139 неполных записей после миграции остались в staging. Миграция `0009` добавляет устойчивые алиасы внешних рыб и водоёмов, канонические ссылки и состояние ручной проверки. Страница `/admin/external-sources` позволяет сопоставить, отклонить или явно опубликовать наблюдение. После миграции `0012` повторный импорт автоматически публикует запись только при одновременном наличии подтверждённых алиасов конкретного источника, координат и веса; остальные записи остаются в staging. Поэтому текущие 139 неполных записей не опубликованы.
+3 -3
View File
@@ -10,7 +10,7 @@
- Изображения: не скачиваются, не проксируются и не архивируются без отдельного явно зафиксированного условия. - Изображения: не скачиваются, не проксируются и не архивируются без отдельного явно зафиксированного условия.
- Атрибуция: опубликованный улов обязан сохранять `source_system`, внешний ID и прямой `source_url` в provenance. - Атрибуция: опубликованный улов обязан сохранять `source_system`, внешний ID и прямой `source_url` в provenance.
- Объединение источников: автоматическое склеивание RF4DB и RF4-STAT запрещено, пока нет надёжного общего идентификатора. - Объединение источников: автоматическое склеивание RF4DB и RF4-STAT запрещено, пока нет надёжного общего идентификатора.
- Публикация: только вручную, после канонического сопоставления; обязательны рыба, водоём, координаты и вес. - Публикация: автоматически по ранее вручную подтверждённым алиасам источника либо вручную; обязательны рыба, водоём, координаты и вес.
## RF4MAP и RF4 Posts ## RF4MAP и RF4 Posts
@@ -19,7 +19,7 @@
- Частота: не чаще одного получения страницы каждого источника раз в 30 минут. - Частота: не чаще одного получения страницы каждого источника раз в 30 минут.
- Разрешённый контур: публичные HTML-страницы, нормализация в `ExternalCatch` и закрытый staging; запрещённые в `robots.txt` API не запрашиваются. - Разрешённый контур: публичные HTML-страницы, нормализация в `ExternalCatch` и закрытый staging; запрещённые в `robots.txt` API не запрашиваются.
- Изображения: сохраняются только исходные URL доказательств; файлы не скачиваются и не проксируются. - Изображения: сохраняются только исходные URL доказательств; файлы не скачиваются и не проксируются.
- Публикация: только вручную и только при выполнении общего требования рыба + водоём + координаты + вес. Текущие записи RF4MAP/RF4 Posts не имеют веса и остаются в staging. - Публикация: автоматически по подтверждённым алиасам либо вручную и только при выполнении общего требования рыба + водоём + координаты + вес. Текущие записи RF4MAP/RF4 Posts не имеют веса и остаются в staging.
- Семантика RF4 Posts: несколько видов одного поста остаются связанными с одним UUID точки и не считаются отдельными подтверждёнными взвешиваниями. - Семантика RF4 Posts: несколько видов одного поста остаются связанными с одним UUID точки и не считаются отдельными подтверждёнными взвешиваниями.
## Лимиты запросов ## Лимиты запросов
@@ -30,7 +30,7 @@
- RF4DB: один запрос списка за ручной запуск; detail-страницы запрашиваются только адресно, без массового обхода. - RF4DB: один запрос списка за ручной запуск; detail-страницы запрашиваются только адресно, без массового обхода.
- RF4MAP: не чаще одного запроса раз в 30 минут. - RF4MAP: не чаще одного запроса раз в 30 минут.
- RF4 Posts: не чаще одного запроса раз в 30 минут. - RF4 Posts: не чаще одного запроса раз в 30 минут.
- регулярные scheduler-задачи для обоих источников выключены; - все пять разрешённых community-адаптеров включены в реестре staging; регулярные scheduler-задачи остаются выключены;
- при `429`, `403`, изменении DOM или неожиданном типе ответа сбор прекращается без попытки обхода ограничения. - при `429`, `403`, изменении DOM или неожиданном типе ответа сбор прекращается без попытки обхода ограничения.
## Что ещё требуется зафиксировать ## Что ещё требуется зафиксировать
+1 -1
View File
@@ -14,7 +14,7 @@ RF4 Spotter обрабатывает официальные публичные
- IP не хранится: rate limit использует HMAC-отпечаток с секретом сервера; - IP не хранится: rate limit использует HMAC-отпечаток с секретом сервера;
- из изображений удаляются EXIF и прочие метаданные; - из изображений удаляются EXIF и прочие метаданные;
- query string, authorization headers и пользовательские payload не попадают в HTTP-лог; - query string, authorization headers и пользовательские payload не попадают в HTTP-лог;
- community-наблюдения сначала поступают в изолированный staging и публикуются только после ручной проверки. - community-наблюдения сначала поступают в изолированный staging; полные записи автоматически публикуются только по ранее вручную подтверждённым алиасам источника, остальные требуют ручной проверки.
## Источники и атрибуция ## Источники и атрибуция