From 8d7fab97b9cb7b930f85903044defa4fe4a30513 Mon Sep 17 00:00:00 2001 From: IK Date: Sat, 12 Sep 2026 21:03:37 +0700 Subject: [PATCH] ci: lock generated OpenAPI contract --- .gitea/workflows/ci.yml | 2 + README.md | 2 + apps/api/export_openapi.py | 34 + apps/api/openapi.json | 3278 ++++++++++++++++++++++++++++++++++++ docs/ROADMAP.md | 2 +- docs/api-contract.md | 13 + 6 files changed, 3330 insertions(+), 1 deletion(-) create mode 100644 apps/api/export_openapi.py create mode 100644 apps/api/openapi.json create mode 100644 docs/api-contract.md diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4496e09..eb16d6e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -41,6 +41,8 @@ jobs: alembic current - name: Run Python tests run: pytest -q + - name: Verify generated OpenAPI contract + run: python apps/api/export_openapi.py --check astro-build: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 85ba6bc..5e52166 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ Production release отделяет Alembic от runtime: одноразовый Тяжёлый production bootstrap вынесен в отдельный ручной/еженедельный CI workflow с 30-минутным timeout и сохраняемыми diagnostics; обычный push по-прежнему использует быстрый Compose E2E. +Публичный API зафиксирован генерируемым [OpenAPI-контрактом](docs/api-contract.md): CI сравнивает `apps/api/openapi.json` с фактической схемой FastAPI, поэтому рефакторинг routers не может незаметно изменить URL, параметры или response models. + После повторных ошибок scheduler увеличивает паузу экспоненциально до 24 часов и возвращается к 30 минутам после успеха. Публичная страница `/status` показывает свежесть и состояние источников без URL запросов, внутренних ошибок и другой диагностической информации. Подробный план и актуальные чекбоксы находятся в [`docs/ROADMAP.md`](docs/ROADMAP.md). Результаты проверки интерфейса и пять приоритетных UX-пакетов описаны в [`docs/UI_UX_AUDIT.md`](docs/UI_UX_AUDIT.md). diff --git a/apps/api/export_openapi.py b/apps/api/export_openapi.py new file mode 100644 index 0000000..e3b1c70 --- /dev/null +++ b/apps/api/export_openapi.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from app.main import app + + +TARGET = Path(__file__).with_name("openapi.json") + + +def rendered_contract() -> str: + return json.dumps(app.openapi(), ensure_ascii=False, indent=2, sort_keys=True) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Generate or verify the RF4 Spotter OpenAPI contract") + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + rendered = rendered_contract() + if args.check: + if not TARGET.exists() or TARGET.read_text(encoding="utf-8") != rendered: + parser.exit(1, "OpenAPI contract is stale; run apps/api/export_openapi.py\n") + print(f"OpenAPI contract is current: {len(app.openapi()['paths'])} paths") + return 0 + TARGET.write_text(rendered, encoding="utf-8") + print(f"Wrote {TARGET}: {len(app.openapi()['paths'])} paths") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/apps/api/openapi.json b/apps/api/openapi.json new file mode 100644 index 0000000..9a9800c --- /dev/null +++ b/apps/api/openapi.json @@ -0,0 +1,3278 @@ +{ + "components": { + "schemas": { + "ActivityOut": { + "properties": { + "activity_score": { + "title": "Activity Score", + "type": "integer" + }, + "average_weight_g": { + "title": "Average Weight G", + "type": "integer" + }, + "best_bait": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Best Bait" + }, + "catches": { + "title": "Catches", + "type": "integer" + }, + "confidence_score": { + "title": "Confidence Score", + "type": "integer" + }, + "explanation": { + "title": "Explanation", + "type": "string" + }, + "fish": { + "title": "Fish", + "type": "string" + }, + "fish_slug": { + "title": "Fish Slug", + "type": "string" + }, + "last_confirmed_at": { + "format": "date-time", + "title": "Last Confirmed At", + "type": "string" + }, + "max_weight_g": { + "title": "Max Weight G", + "type": "integer" + }, + "sources": { + "items": { + "type": "string" + }, + "title": "Sources", + "type": "array" + }, + "spot_id": { + "format": "uuid", + "title": "Spot Id", + "type": "string" + }, + "unique_players": { + "title": "Unique Players", + "type": "integer" + }, + "waterbody": { + "title": "Waterbody", + "type": "string" + }, + "waterbody_slug": { + "title": "Waterbody Slug", + "type": "string" + }, + "x": { + "title": "X", + "type": "integer" + }, + "y": { + "title": "Y", + "type": "integer" + } + }, + "required": [ + "spot_id", + "waterbody_slug", + "waterbody", + "fish_slug", + "fish", + "x", + "y", + "best_bait", + "catches", + "unique_players", + "average_weight_g", + "max_weight_g", + "last_confirmed_at", + "activity_score", + "confidence_score", + "explanation", + "sources" + ], + "title": "ActivityOut", + "type": "object" + }, + "AdminCatchReportOut": { + "properties": { + "bait": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Bait" + }, + "comment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Comment" + }, + "coordinates": { + "title": "Coordinates", + "type": "string" + }, + "fish": { + "title": "Fish", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "moderation_status": { + "title": "Moderation Status", + "type": "string" + }, + "player_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Player Name" + }, + "reported_at": { + "format": "date-time", + "title": "Reported At", + "type": "string" + }, + "screenshot_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Screenshot Url" + }, + "waterbody": { + "title": "Waterbody", + "type": "string" + }, + "weight_g": { + "title": "Weight G", + "type": "integer" + } + }, + "required": [ + "id", + "fish", + "waterbody", + "coordinates", + "weight_g", + "bait", + "player_name", + "reported_at", + "moderation_status", + "comment", + "screenshot_url" + ], + "title": "AdminCatchReportOut", + "type": "object" + }, + "BaitOut": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "kind": { + "title": "Kind", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "normalized_name": { + "title": "Normalized Name", + "type": "string" + } + }, + "required": [ + "id", + "name", + "normalized_name", + "kind" + ], + "title": "BaitOut", + "type": "object" + }, + "Body_add_screenshot_api_v1_catch_reports__report_id__screenshot_post": { + "properties": { + "screenshot": { + "format": "binary", + "title": "Screenshot", + "type": "string" + } + }, + "required": [ + "screenshot" + ], + "title": "Body_add_screenshot_api_v1_catch_reports__report_id__screenshot_post", + "type": "object" + }, + "CatchOut": { + "properties": { + "bait": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Bait" + }, + "caught_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Caught At" + }, + "fish": { + "title": "Fish", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "player_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Player Name" + }, + "reported_at": { + "format": "date-time", + "title": "Reported At", + "type": "string" + }, + "retrieve_method": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Retrieve Method" + }, + "retrieve_speed": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Retrieve Speed" + }, + "source_system": { + "title": "Source System", + "type": "string" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url" + }, + "weight_g": { + "title": "Weight G", + "type": "integer" + } + }, + "required": [ + "id", + "fish", + "weight_g", + "bait", + "player_name", + "caught_at", + "reported_at", + "retrieve_method", + "retrieve_speed", + "source_system", + "source_url" + ], + "title": "CatchOut", + "type": "object" + }, + "CatchReportAccepted": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "idempotent": { + "default": false, + "title": "Idempotent", + "type": "boolean" + }, + "moderation_status": { + "title": "Moderation Status", + "type": "string" + }, + "screenshot_upload_token": { + "title": "Screenshot Upload Token", + "type": "string" + } + }, + "required": [ + "id", + "moderation_status", + "screenshot_upload_token" + ], + "title": "CatchReportAccepted", + "type": "object" + }, + "CatchReportCreate": { + "properties": { + "bait_name": { + "anyOf": [ + { + "maxLength": 200, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Bait Name" + }, + "caught_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Caught At" + }, + "comment": { + "anyOf": [ + { + "maxLength": 1000, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Comment" + }, + "fish_slug": { + "title": "Fish Slug", + "type": "string" + }, + "fishing_method": { + "anyOf": [ + { + "maxLength": 50, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Fishing Method" + }, + "player_name": { + "anyOf": [ + { + "maxLength": 100, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Player Name" + }, + "retrieve_method": { + "anyOf": [ + { + "maxLength": 100, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Retrieve Method" + }, + "retrieve_speed": { + "anyOf": [ + { + "maximum": 100.0, + "minimum": 0.0, + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Retrieve Speed" + }, + "rig_type": { + "anyOf": [ + { + "maxLength": 100, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Rig Type" + }, + "source_url": { + "anyOf": [ + { + "maxLength": 1000, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url" + }, + "waterbody_slug": { + "title": "Waterbody Slug", + "type": "string" + }, + "website": { + "default": "", + "maxLength": 200, + "title": "Website", + "type": "string" + }, + "weight_g": { + "exclusiveMinimum": 0.0, + "maximum": 3000000.0, + "title": "Weight G", + "type": "integer" + }, + "x": { + "maximum": 10000.0, + "minimum": -10000.0, + "title": "X", + "type": "integer" + }, + "y": { + "maximum": 10000.0, + "minimum": -10000.0, + "title": "Y", + "type": "integer" + } + }, + "required": [ + "fish_slug", + "waterbody_slug", + "x", + "y", + "weight_g" + ], + "title": "CatchReportCreate", + "type": "object" + }, + "CatchReportCreated": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "moderation_status": { + "title": "Moderation Status", + "type": "string" + } + }, + "required": [ + "id", + "moderation_status" + ], + "title": "CatchReportCreated", + "type": "object" + }, + "ExternalAliasSuggestionOut": { + "properties": { + "fish_slug": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Fish Slug" + }, + "requires_confirmation": { + "default": true, + "title": "Requires Confirmation", + "type": "boolean" + }, + "waterbody_slug": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Waterbody Slug" + } + }, + "required": [ + "fish_slug", + "waterbody_slug" + ], + "title": "ExternalAliasSuggestionOut", + "type": "object" + }, + "ExternalObservationDecision": { + "properties": { + "reason": { + "maxLength": 1000, + "minLength": 1, + "title": "Reason", + "type": "string" + } + }, + "required": [ + "reason" + ], + "title": "ExternalObservationDecision", + "type": "object" + }, + "ExternalObservationMapping": { + "properties": { + "fish_slug": { + "title": "Fish Slug", + "type": "string" + }, + "note": { + "anyOf": [ + { + "maxLength": 1000, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Note" + }, + "waterbody_slug": { + "title": "Waterbody Slug", + "type": "string" + } + }, + "required": [ + "fish_slug", + "waterbody_slug" + ], + "title": "ExternalObservationMapping", + "type": "object" + }, + "ExternalObservationOut": { + "properties": { + "catch_report_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Catch Report Id" + }, + "fish_external_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Fish External Id" + }, + "fish_name": { + "title": "Fish Name", + "type": "string" + }, + "fish_slug": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Fish Slug" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "last_seen_at": { + "format": "date-time", + "title": "Last Seen At", + "type": "string" + }, + "published_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Published At" + }, + "review_note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Review Note" + }, + "source_external_id": { + "title": "Source External Id", + "type": "string" + }, + "source_system": { + "title": "Source System", + "type": "string" + }, + "source_url": { + "title": "Source Url", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "waterbody_external_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Waterbody External Id" + }, + "waterbody_name": { + "title": "Waterbody Name", + "type": "string" + }, + "waterbody_slug": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Waterbody Slug" + }, + "weight_g": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Weight G" + }, + "x": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "X" + }, + "y": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Y" + } + }, + "required": [ + "id", + "source_system", + "source_external_id", + "source_url", + "fish_name", + "fish_external_id", + "waterbody_name", + "waterbody_external_id", + "x", + "y", + "weight_g", + "published_at", + "last_seen_at", + "status", + "fish_slug", + "waterbody_slug", + "catch_report_id", + "review_note" + ], + "title": "ExternalObservationOut", + "type": "object" + }, + "ExternalObservationPublished": { + "properties": { + "catch_report_id": { + "format": "uuid", + "title": "Catch Report Id", + "type": "string" + }, + "observation_id": { + "format": "uuid", + "title": "Observation Id", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "observation_id", + "catch_report_id", + "status" + ], + "title": "ExternalObservationPublished", + "type": "object" + }, + "FishOut": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "name_ru": { + "title": "Name Ru", + "type": "string" + }, + "slug": { + "title": "Slug", + "type": "string" + }, + "trophy_weight_g": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Trophy Weight G" + } + }, + "required": [ + "id", + "slug", + "name_ru", + "trophy_weight_g" + ], + "title": "FishOut", + "type": "object" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array" + } + }, + "title": "HTTPValidationError", + "type": "object" + }, + "ImportRunOut": { + "properties": { + "error_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Summary" + }, + "finished_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Finished At" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "not_modified": { + "title": "Not Modified", + "type": "boolean" + }, + "response_bytes": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Response Bytes" + }, + "response_content_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Response Content Type" + }, + "response_etag": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Response Etag" + }, + "response_last_modified": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Response Last Modified" + }, + "response_status": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Response Status" + }, + "rows_created": { + "title": "Rows Created", + "type": "integer" + }, + "rows_seen": { + "title": "Rows Seen", + "type": "integer" + }, + "rows_updated": { + "title": "Rows Updated", + "type": "integer" + }, + "source_url": { + "title": "Source Url", + "type": "string" + }, + "started_at": { + "format": "date-time", + "title": "Started At", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "id", + "started_at", + "finished_at", + "status", + "source_url", + "rows_seen", + "rows_created", + "rows_updated", + "error_summary", + "response_status", + "response_etag", + "response_last_modified", + "response_content_type", + "response_bytes", + "not_modified" + ], + "title": "ImportRunOut", + "type": "object" + }, + "ImportRunPublicOut": { + "properties": { + "finished_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Finished At" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "not_modified": { + "title": "Not Modified", + "type": "boolean" + }, + "rows_created": { + "title": "Rows Created", + "type": "integer" + }, + "rows_seen": { + "title": "Rows Seen", + "type": "integer" + }, + "rows_updated": { + "title": "Rows Updated", + "type": "integer" + }, + "started_at": { + "format": "date-time", + "title": "Started At", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "id", + "started_at", + "finished_at", + "status", + "rows_seen", + "rows_created", + "rows_updated", + "not_modified" + ], + "title": "ImportRunPublicOut", + "type": "object" + }, + "ModerationStatus": { + "enum": [ + "pending", + "approved", + "rejected" + ], + "title": "ModerationStatus", + "type": "string" + }, + "ModerationUpdate": { + "properties": { + "reason": { + "anyOf": [ + { + "maxLength": 1000, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reason" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "status" + ], + "title": "ModerationUpdate", + "type": "object" + }, + "OfficialRecordOut": { + "properties": { + "bait": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Bait" + }, + "category": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Category" + }, + "fish": { + "title": "Fish", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "player_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Player Name" + }, + "record_date": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Record Date" + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Region" + }, + "source_system": { + "default": "rf4-official", + "title": "Source System", + "type": "string" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url" + }, + "waterbody": { + "title": "Waterbody", + "type": "string" + }, + "weight_g": { + "title": "Weight G", + "type": "integer" + } + }, + "required": [ + "id", + "fish", + "weight_g", + "waterbody", + "bait", + "player_name", + "record_date", + "category", + "region", + "source_url" + ], + "title": "OfficialRecordOut", + "type": "object" + }, + "PaginatedActivityOut": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/ActivityOut" + }, + "title": "Items", + "type": "array" + }, + "limit": { + "title": "Limit", + "type": "integer" + }, + "offset": { + "title": "Offset", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "PaginatedActivityOut", + "type": "object" + }, + "PaginatedOfficialRecordOut": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/OfficialRecordOut" + }, + "title": "Items", + "type": "array" + }, + "limit": { + "title": "Limit", + "type": "integer" + }, + "offset": { + "title": "Offset", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "PaginatedOfficialRecordOut", + "type": "object" + }, + "PublicObservationOut": { + "properties": { + "fish_name": { + "title": "Fish Name", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "last_seen_at": { + "format": "date-time", + "title": "Last Seen At", + "type": "string" + }, + "missing_fields": { + "items": { + "type": "string" + }, + "title": "Missing Fields", + "type": "array" + }, + "quality": { + "title": "Quality", + "type": "string" + }, + "source_name": { + "title": "Source Name", + "type": "string" + }, + "source_system": { + "title": "Source System", + "type": "string" + }, + "source_url": { + "title": "Source Url", + "type": "string" + }, + "waterbody_name": { + "title": "Waterbody Name", + "type": "string" + }, + "weight_g": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Weight G" + }, + "x": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "X" + }, + "y": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Y" + } + }, + "required": [ + "id", + "source_system", + "source_name", + "source_url", + "fish_name", + "waterbody_name", + "x", + "y", + "weight_g", + "last_seen_at", + "missing_fields", + "quality" + ], + "title": "PublicObservationOut", + "type": "object" + }, + "SourceStatusOut": { + "properties": { + "last_started_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Started At" + }, + "last_success_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Success At" + }, + "name": { + "title": "Name", + "type": "string" + }, + "observations": { + "title": "Observations", + "type": "integer" + }, + "source_system": { + "title": "Source System", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "source_system", + "name", + "status", + "last_started_at", + "last_success_at", + "observations" + ], + "title": "SourceStatusOut", + "type": "object" + }, + "SpotOut": { + "properties": { + "catches_24h": { + "title": "Catches 24H", + "type": "integer" + }, + "catches_3d": { + "title": "Catches 3D", + "type": "integer" + }, + "catches_7d": { + "title": "Catches 7D", + "type": "integer" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "top_baits": { + "items": { + "type": "string" + }, + "title": "Top Baits", + "type": "array" + }, + "waterbody": { + "title": "Waterbody", + "type": "string" + }, + "waterbody_slug": { + "title": "Waterbody Slug", + "type": "string" + }, + "x": { + "title": "X", + "type": "integer" + }, + "y": { + "title": "Y", + "type": "integer" + } + }, + "required": [ + "id", + "waterbody_slug", + "waterbody", + "x", + "y", + "description", + "catches_24h", + "catches_3d", + "catches_7d", + "top_baits" + ], + "title": "SpotOut", + "type": "object" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "title": "Location", + "type": "array" + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + }, + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError", + "type": "object" + }, + "WaterbodyOut": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "name_ru": { + "title": "Name Ru", + "type": "string" + }, + "slug": { + "title": "Slug", + "type": "string" + }, + "unlock_level": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Unlock Level" + } + }, + "required": [ + "id", + "slug", + "name_ru", + "unlock_level" + ], + "title": "WaterbodyOut", + "type": "object" + } + } + }, + "info": { + "title": "RF4 Spotter API", + "version": "0.1.0" + }, + "openapi": "3.1.0", + "paths": { + "/api/v1/activity": { + "get": { + "operationId": "activity_api_v1_activity_get", + "parameters": [ + { + "in": "query", + "name": "hours", + "required": false, + "schema": { + "default": 24, + "title": "Hours", + "type": "integer" + } + }, + { + "in": "query", + "name": "waterbody", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Waterbody" + } + }, + { + "in": "query", + "name": "fish", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Fish" + } + }, + { + "in": "query", + "name": "method", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Method" + } + }, + { + "in": "query", + "name": "sort", + "required": false, + "schema": { + "default": "activity", + "enum": [ + "activity", + "confidence", + "freshness" + ], + "title": "Sort", + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 20, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedActivityOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Activity" + } + }, + "/api/v1/admin/catch-reports": { + "get": { + "operationId": "admin_reports_api_v1_admin_catch_reports_get", + "parameters": [ + { + "in": "query", + "name": "status", + "required": false, + "schema": { + "$ref": "#/components/schemas/ModerationStatus", + "default": "pending" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 50, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AdminCatchReportOut" + }, + "title": "Response Admin Reports Api V1 Admin Catch Reports Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Admin Reports" + } + }, + "/api/v1/admin/catch-reports/{report_id}": { + "delete": { + "operationId": "delete_report_api_v1_admin_catch_reports__report_id__delete", + "parameters": [ + { + "in": "path", + "name": "report_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Report Id", + "type": "string" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Delete Report" + }, + "patch": { + "operationId": "moderate_report_api_v1_admin_catch_reports__report_id__patch", + "parameters": [ + { + "in": "path", + "name": "report_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Report Id", + "type": "string" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModerationUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatchReportCreated" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Moderate Report" + } + }, + "/api/v1/admin/diagnostics": { + "get": { + "operationId": "admin_diagnostics_api_v1_admin_diagnostics_get", + "parameters": [ + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Admin Diagnostics" + } + }, + "/api/v1/admin/external-observations": { + "get": { + "operationId": "admin_external_observations_api_v1_admin_external_observations_get", + "parameters": [ + { + "in": "query", + "name": "status", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "staged", + "mapped", + "ready", + "published", + "rejected", + "review" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "in": "query", + "name": "source_system", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source System" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 50, + "maximum": 200, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ExternalObservationOut" + }, + "title": "Response Admin External Observations Api V1 Admin External Observations Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Admin External Observations" + } + }, + "/api/v1/admin/external-observations/{observation_id}/alias-suggestions": { + "get": { + "operationId": "admin_external_alias_suggestions_api_v1_admin_external_observations__observation_id__alias_suggestions_get", + "parameters": [ + { + "in": "path", + "name": "observation_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Observation Id", + "type": "string" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalAliasSuggestionOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Admin External Alias Suggestions" + } + }, + "/api/v1/admin/external-observations/{observation_id}/mapping": { + "patch": { + "operationId": "admin_map_external_observation_api_v1_admin_external_observations__observation_id__mapping_patch", + "parameters": [ + { + "in": "path", + "name": "observation_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Observation Id", + "type": "string" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalObservationMapping" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalObservationOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Admin Map External Observation" + } + }, + "/api/v1/admin/external-observations/{observation_id}/publish": { + "post": { + "operationId": "admin_publish_external_observation_api_v1_admin_external_observations__observation_id__publish_post", + "parameters": [ + { + "in": "path", + "name": "observation_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Observation Id", + "type": "string" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalObservationPublished" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Admin Publish External Observation" + } + }, + "/api/v1/admin/external-observations/{observation_id}/reject": { + "patch": { + "operationId": "admin_reject_external_observation_api_v1_admin_external_observations__observation_id__reject_patch", + "parameters": [ + { + "in": "path", + "name": "observation_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Observation Id", + "type": "string" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalObservationDecision" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalObservationOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Admin Reject External Observation" + } + }, + "/api/v1/admin/imports": { + "get": { + "operationId": "admin_imports_api_v1_admin_imports_get", + "parameters": [ + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 20, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ImportRunOut" + }, + "title": "Response Admin Imports Api V1 Admin Imports Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Admin Imports" + } + }, + "/api/v1/admin/imports/official-records": { + "post": { + "operationId": "admin_start_official_import_api_v1_admin_imports_official_records_post", + "parameters": [ + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportRunOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Admin Start Official Import" + } + }, + "/api/v1/baits": { + "get": { + "operationId": "baits_api_v1_baits_get", + "parameters": [ + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 200, + "maximum": 500, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BaitOut" + }, + "title": "Response Baits Api V1 Baits Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Baits" + } + }, + "/api/v1/catch-reports": { + "post": { + "operationId": "create_catch_report_api_v1_catch_reports_post", + "parameters": [ + { + "in": "header", + "name": "idempotency-key", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Idempotency-Key" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatchReportCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatchReportAccepted" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Create Catch Report" + } + }, + "/api/v1/catch-reports/{report_id}/screenshot": { + "post": { + "operationId": "add_screenshot_api_v1_catch_reports__report_id__screenshot_post", + "parameters": [ + { + "in": "path", + "name": "report_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Report Id", + "type": "string" + } + }, + { + "in": "header", + "name": "X-Upload-Token", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Upload-Token" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_add_screenshot_api_v1_catch_reports__report_id__screenshot_post" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Add Screenshot" + } + }, + "/api/v1/community-observations": { + "get": { + "operationId": "community_observations_api_v1_community_observations_get", + "parameters": [ + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 12, + "maximum": 50, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + }, + { + "in": "query", + "name": "waterbody", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Waterbody" + } + }, + { + "in": "query", + "name": "fish", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Fish" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PublicObservationOut" + }, + "title": "Response Community Observations Api V1 Community Observations Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Community Observations" + } + }, + "/api/v1/fishes": { + "get": { + "operationId": "fishes_api_v1_fishes_get", + "parameters": [ + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 200, + "maximum": 500, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/FishOut" + }, + "title": "Response Fishes Api V1 Fishes Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Fishes" + } + }, + "/api/v1/imports": { + "get": { + "operationId": "imports_api_v1_imports_get", + "parameters": [ + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 20, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ImportRunPublicOut" + }, + "title": "Response Imports Api V1 Imports Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Imports" + } + }, + "/api/v1/public-spot-pages": { + "get": { + "operationId": "public_spot_pages_api_v1_public_spot_pages_get", + "parameters": [ + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 500, + "maximum": 500, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "title": "Response Public Spot Pages Api V1 Public Spot Pages Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Public Spot Pages" + } + }, + "/api/v1/records": { + "get": { + "operationId": "records_api_v1_records_get", + "parameters": [ + { + "in": "query", + "name": "fish", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Fish" + } + }, + { + "in": "query", + "name": "waterbody", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Waterbody" + } + }, + { + "in": "query", + "name": "category", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Category" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 50, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedOfficialRecordOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Records" + } + }, + "/api/v1/source-status": { + "get": { + "operationId": "source_status_api_v1_source_status_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/SourceStatusOut" + }, + "title": "Response Source Status Api V1 Source Status Get", + "type": "array" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Source Status" + } + }, + "/api/v1/spots/resolve": { + "get": { + "operationId": "resolve_spot_api_v1_spots_resolve_get", + "parameters": [ + { + "in": "query", + "name": "waterbody", + "required": true, + "schema": { + "title": "Waterbody", + "type": "string" + } + }, + { + "in": "query", + "name": "x", + "required": true, + "schema": { + "maximum": 10000, + "minimum": -10000, + "title": "X", + "type": "integer" + } + }, + { + "in": "query", + "name": "y", + "required": true, + "schema": { + "maximum": 10000, + "minimum": -10000, + "title": "Y", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SpotOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Resolve Spot" + } + }, + "/api/v1/spots/{spot_id}": { + "get": { + "operationId": "spot_detail_api_v1_spots__spot_id__get", + "parameters": [ + { + "in": "path", + "name": "spot_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Spot Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SpotOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Spot Detail" + } + }, + "/api/v1/spots/{spot_id}/catches": { + "get": { + "operationId": "spot_catches_api_v1_spots__spot_id__catches_get", + "parameters": [ + { + "in": "path", + "name": "spot_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Spot Id", + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 50, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/CatchOut" + }, + "title": "Response Spot Catches Api V1 Spots Spot Id Catches Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Spot Catches" + } + }, + "/api/v1/spots/{spot_id}/timeline": { + "get": { + "operationId": "spot_timeline_api_v1_spots__spot_id__timeline_get", + "parameters": [ + { + "in": "path", + "name": "spot_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Spot Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Response Spot Timeline Api V1 Spots Spot Id Timeline Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Spot Timeline" + } + }, + "/api/v1/waterbodies": { + "get": { + "operationId": "waterbodies_api_v1_waterbodies_get", + "parameters": [ + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 200, + "maximum": 500, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/WaterbodyOut" + }, + "title": "Response Waterbodies Api V1 Waterbodies Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Waterbodies" + } + }, + "/health": { + "get": { + "operationId": "health_health_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "title": "Response Health Health Get", + "type": "object" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Health" + } + }, + "/ready": { + "get": { + "operationId": "ready_ready_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + } + }, + "summary": "Ready" + } + } + } +} diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d2ef93d..63ef68c 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -56,7 +56,7 @@ - [x] **Q13 · Production bootstrap в CI.** Отдельный workflow запускает `deploy/test-production-bootstrap.sh` вручную или раз в неделю, а не на каждом push. Вывод bootstrap всегда сохраняется 14 дней; при падении добавляются Compose status и Playwright diagnostics. - [ ] **Q14 · Полная CSP.** Расширить текущую CSP (`frame-ancestors`, `base-uri`, `object-src`) до `default-src`, `script-src`, `style-src`, `img-src`, `connect-src` и `form-action`. Сначала инвентаризировать inline scripts/styles Astro, затем внедрить nonce/hash или безопасное вынесение; проверить report/admin/OG без ослабления до произвольных внешних origin. - [ ] **Q15 · Частичная деградация главной.** Разделить получение activity, community signals и справочников так, чтобы отказ одного источника не превращал всю главную в общий 503. Для SSR не добавлять искусственный client-side loading/optimistic UI; показывать независимые `StatePanel` и корректный HTTP/cache статус. -- [ ] **Q16 · Контракт OpenAPI.** Генерировать OpenAPI artifact из приложения в CI и проверять осознанные изменения контракта при декомпозиции API; не поддерживать вручную редактируемую копию. +- [x] **Q16 · Контракт OpenAPI.** `apps/api/openapi.json` детерминированно генерируется из FastAPI; CI проверяет его актуальность после backend suite. Изменение artifact обязательно рассматривается вместе с реализацией, а ручное редактирование не используется. - [ ] **Q17 · Эксплуатационные документы.** Добавить короткие ADR по Astro/FastAPI/PostgreSQL, локальному cache и стратегии scheduler, а также incident runbook для заполнения диска/PostgreSQL, отказа MinIO, зависших импортов, ошибок миграции и компрометации секретов. - [ ] **Q18 · Минимальная observability.** До открытой альфы определить дешёвые метрики request count/latency/error rate, глубины moderation/staging и возраста последнего успешного импорта. Формат и exporter выбрать после выбора мониторинга сервера; полноценный tracing не внедрять без подтверждённой потребности. diff --git a/docs/api-contract.md b/docs/api-contract.md new file mode 100644 index 0000000..23da77f --- /dev/null +++ b/docs/api-contract.md @@ -0,0 +1,13 @@ +# Контракт API + +`apps/api/openapi.json` — автоматически сгенерированный снимок фактического FastAPI-контракта. Он фиксирует пути, методы, параметры, response schemas и security metadata перед декомпозицией routers. Файл нельзя править вручную. + +После осознанного изменения API: + +```bash +.venv/bin/python apps/api/export_openapi.py +git diff -- apps/api/openapi.json +.venv/bin/python apps/api/export_openapi.py --check +``` + +CI выполняет режим `--check` после тестов и падает, если код и committed artifact расходятся. Изменение снимка должно находиться в том же коммите, что реализация и тесты нового контракта. Перестановка routers без изменения публичного поведения не должна менять artifact.