{
    "openapi": "3.0.1",
    "info": {
        "title": "Datahub API",
        "version": "0.2",
        "description": "Сбор данных из открытых источников: публикации государственных органов и региональных СМИ, статистика Росстата, вакансии HH."
    },
    "servers": [
        {
            "url": "/",
            "description": "Тот же хост, что отдал эту страницу"
        }
    ],
    "tags": [
        {
            "name": "contract",
            "description": "Контракт SOW: ленты по возрастающему курсору `since_id`"
        },
        {
            "name": "worker",
            "description": "Inbound API внешнего парсера (`ymaps_parser`). Право `/api/datahub/worker/*`, не DATAHUB_API_TOKEN."
        },
        {
            "name": "hh",
            "description": "Национальный рынок HH (ClickHouse + карточки работодателей). Право `hh`."
        }
    ],
    "security": [
        {
            "ApiKeyAuth": []
        },
        {
            "TokenAuth": []
        }
    ],
    "paths": {
        "/api/datahub/articles": {
            "get": {
                "tags": [
                    "contract"
                ],
                "summary": "Публикации в контракте SOW",
                "description": "Основной поток для Event Builder: актуальные ревизии публикаций по возрастанию `document_id`.\n\nРевизия приходит как запись с тем же `article_id` и другим `content_hash`. Что с ней делать, решает потребитель: слияние и дедупликация между источниками по SOW не задача модуля.\n\nПагинация: повторять запрос с `since_id` из `next_since_id`, пока `count` больше нуля.",
                "operationId": "datahub_articles",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/since_id"
                    },
                    {
                        "$ref": "#/components/parameters/source_code"
                    },
                    {
                        "$ref": "#/components/parameters/group_code"
                    },
                    {
                        "$ref": "#/components/parameters/doc_kind"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Страница публикаций",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "ok": {
                                            "type": "boolean",
                                            "example": true
                                        },
                                        "count": {
                                            "type": "integer",
                                            "description": "Строк на этой странице",
                                            "example": 100
                                        },
                                        "next_since_id": {
                                            "type": "integer",
                                            "description": "Курсор для следующего запроса",
                                            "example": 4821
                                        },
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/sowArticle"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/forbidden"
                    },
                    "429": {
                        "$ref": "#/components/responses/tooManyRequests"
                    }
                }
            }
        },
        "/api/datahub/observations": {
            "get": {
                "tags": [
                    "contract"
                ],
                "summary": "Наблюдения статистики",
                "description": "Показатель × территория × период × значение, с происхождением значения (`extraction_method`, `cell_locator`, `confidence`) — чтобы любую цифру можно было открыть в исходном файле и проверить.\n\nРегион: `territory_code=98` (Якутия, ОКАТО) или `territory_id` из `/api/datahub/status`. Пагинация: повторять с `since_id` из `next_since_id`, пока `count` больше нуля.",
                "operationId": "datahub_observations",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/since_id"
                    },
                    {
                        "$ref": "#/components/parameters/dataset_code"
                    },
                    {
                        "$ref": "#/components/parameters/indicator_code"
                    },
                    {
                        "$ref": "#/components/parameters/period_from"
                    },
                    {
                        "$ref": "#/components/parameters/territory_id"
                    },
                    {
                        "$ref": "#/components/parameters/territory_code"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Страница наблюдений",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "ok": {
                                            "type": "boolean",
                                            "example": true
                                        },
                                        "count": {
                                            "type": "integer",
                                            "example": 100
                                        },
                                        "next_since_id": {
                                            "type": "integer",
                                            "example": 190324
                                        },
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/sowObservation"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/forbidden"
                    },
                    "429": {
                        "$ref": "#/components/responses/tooManyRequests"
                    }
                }
            }
        },
        "/api/datahub/records": {
            "get": {
                "tags": [
                    "contract"
                ],
                "summary": "Сырые записи внешних API",
                "description": "Полный `payload` записи внешнего API без удаления полей, с ревизиями по `payload_hash`. Пагинация: `since_id` / `next_since_id`.",
                "operationId": "datahub_records",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/since_id"
                    },
                    {
                        "$ref": "#/components/parameters/record_type"
                    },
                    {
                        "$ref": "#/components/parameters/source_code"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Страница записей",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "ok": { "type": "boolean", "example": true },
                                        "count": { "type": "integer" },
                                        "next_since_id": { "type": "integer" },
                                        "data": { "type": "array", "items": { "type": "object" } }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/datahub/hh/areas": {
            "get": {
                "tags": ["hh"],
                "summary": "Справочник area (РФ и субъекты)",
                "description": "Имена, не id. Порядок: Россия, Москва, Санкт-Петербург, затем по алфавиту. Право `hh`.",
                "operationId": "datahub_hh_areas",
                "responses": {
                    "200": {
                        "description": "Список area",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "ok": { "type": "boolean" },
                                        "count": { "type": "integer" },
                                        "data": { "type": "array", "items": { "type": "object" } }
                                    }
                                }
                            }
                        }
                    },
                    "403": { "$ref": "#/components/responses/forbidden" }
                }
            }
        },
        "/api/datahub/hh/roles": {
            "get": {
                "tags": ["hh"],
                "summary": "Профессиональные роли",
                "description": "Листья `/professional_roles` с именами. Право `hh`.",
                "operationId": "datahub_hh_roles",
                "responses": {
                    "200": {
                        "description": "Список ролей",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "ok": { "type": "boolean" },
                                        "count": { "type": "integer" },
                                        "data": { "type": "array", "items": { "type": "object" } }
                                    }
                                }
                            }
                        }
                    },
                    "403": { "$ref": "#/components/responses/forbidden" }
                }
            }
        },
        "/api/datahub/hh/counters": {
            "get": {
                "tags": ["hh"],
                "summary": "Счётчики фильтра HH",
                "description": "Ряд `datahub_hh_filter_counts` за день. `area_id=0` — субъекты, не сумма РФ. Право `hh`.",
                "operationId": "datahub_hh_counters",
                "parameters": [
                    { "$ref": "#/components/parameters/limit" },
                    { "name": "day", "in": "query", "schema": { "type": "string" }, "description": "`YYYY-MM-DD` или `YYYYMMDD`" },
                    { "name": "area_id", "in": "query", "schema": { "type": "integer" }, "description": "113 — РФ, 0 — все субъекты" },
                    { "name": "axis", "in": "query", "schema": { "type": "string", "default": "professional_role" } },
                    { "name": "search_period", "in": "query", "schema": { "type": "integer", "default": 0 } }
                ],
                "responses": {
                    "200": {
                        "description": "Ряд счётчиков",
                        "content": { "application/json": { "schema": { "type": "object" } } }
                    },
                    "403": { "$ref": "#/components/responses/forbidden" }
                }
            }
        },
        "/api/datahub/hh/vacancies": {
            "get": {
                "tags": ["hh"],
                "summary": "Вакансии HH",
                "description": "Карточки из ClickHouse по возрастанию `vacancy_id`. Пагинация: `since_id` / `next_since_id`. Право `hh`.",
                "operationId": "datahub_hh_vacancies",
                "parameters": [
                    { "$ref": "#/components/parameters/since_id" },
                    { "$ref": "#/components/parameters/limit" },
                    { "name": "area_id", "in": "query", "schema": { "type": "integer" } },
                    { "name": "role_id", "in": "query", "schema": { "type": "integer" } },
                    { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["pending", "active", "archived", "gone", "forbidden", "redirect"] } }
                ],
                "responses": {
                    "200": {
                        "description": "Страница вакансий",
                        "content": { "application/json": { "schema": { "type": "object" } } }
                    },
                    "403": { "$ref": "#/components/responses/forbidden" }
                }
            }
        },
        "/api/datahub/hh/events": {
            "get": {
                "tags": ["hh"],
                "summary": "События жизни вакансии",
                "description": "opened / reopened / archived / deleted / hidden за день. Курсор `before` = `event_at` последней строки. Право `hh`.",
                "operationId": "datahub_hh_events",
                "parameters": [
                    { "$ref": "#/components/parameters/limit" },
                    { "name": "day", "in": "query", "schema": { "type": "string" } },
                    { "name": "event_type", "in": "query", "schema": { "type": "string", "enum": ["opened", "reopened", "archived", "deleted", "hidden"] } },
                    { "name": "before", "in": "query", "schema": { "type": "string" }, "description": "`event_at` последней отданной строки" }
                ],
                "responses": {
                    "200": {
                        "description": "Страница событий",
                        "content": { "application/json": { "schema": { "type": "object" } } }
                    },
                    "403": { "$ref": "#/components/responses/forbidden" }
                }
            }
        },
        "/api/datahub/hh/employers": {
            "get": {
                "tags": ["hh"],
                "summary": "Работодатели HH",
                "description": "Актуальные `hh_employer` из Postgres. Пагинация: `since_id` / `next_since_id`. Право `hh`.",
                "operationId": "datahub_hh_employers",
                "parameters": [
                    { "$ref": "#/components/parameters/since_id" },
                    { "$ref": "#/components/parameters/limit" },
                    { "name": "employer_id", "in": "query", "schema": { "type": "string" } }
                ],
                "responses": {
                    "200": {
                        "description": "Страница работодателей",
                        "content": { "application/json": { "schema": { "type": "object" } } }
                    },
                    "403": { "$ref": "#/components/responses/forbidden" }
                }
            }
        },
        "/api/datahub/files": {
            "get": {
                "tags": [
                    "contract"
                ],
                "summary": "Исходные файлы",
                "description": "Метаданные неизменённых исходников (XLSX Росстата и т.п.). Байты — `GET /api/datahub/files/{file_id}/content`.\n\nПагинация: `since_id` / `next_since_id`.",
                "operationId": "datahub_files",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/since_id"
                    },
                    {
                        "$ref": "#/components/parameters/dataset_code"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Страница файлов",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "ok": { "type": "boolean", "example": true },
                                        "count": { "type": "integer" },
                                        "next_since_id": { "type": "integer" },
                                        "data": {
                                            "type": "array",
                                            "items": { "$ref": "#/components/schemas/sowFile" }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/forbidden"
                    }
                }
            }
        },
        "/api/datahub/files/{file_id}": {
            "get": {
                "tags": [
                    "contract"
                ],
                "summary": "Метаданные исходного файла",
                "description": "Одна ревизия файла по `file_id` из наблюдения. Право то же, что у `/api/datahub/files`.",
                "operationId": "datahub_file",
                "parameters": [
                    {
                        "name": "file_id",
                        "in": "path",
                        "required": true,
                        "schema": { "type": "integer", "example": 20 }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Метаданные файла",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "ok": { "type": "boolean", "example": true },
                                        "data": { "$ref": "#/components/schemas/sowFile" }
                                    }
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/forbidden"
                    },
                    "404": {
                        "description": "Файла с таким id нет"
                    }
                }
            }
        },
        "/api/datahub/files/{file_id}/content": {
            "get": {
                "tags": [
                    "contract"
                ],
                "summary": "Скачать исходный файл",
                "description": "Байты из объектного хранилища, как скачали с источника. `Content-Disposition: attachment`.",
                "operationId": "datahub_file_content",
                "parameters": [
                    {
                        "name": "file_id",
                        "in": "path",
                        "required": true,
                        "schema": { "type": "integer", "example": 20 }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Тело файла",
                        "content": {
                            "application/octet-stream": {
                                "schema": { "type": "string", "format": "binary" }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/forbidden"
                    },
                    "404": {
                        "description": "Файла нет в реестре или в хранилище"
                    }
                }
            }
        },
        "/api/datahub/runs": {
            "get": {
                "tags": [
                    "contract"
                ],
                "summary": "Прогоны за сутки",
                "description": "Прогоны сбора со счётчиками и статусами. Счётчики приходят из результата записи, а не из числа элементов, которые «должны были сохраниться»: заявленный `items_stored` обязан подтверждаться строками в `datahub_documents` по этому `run_id`.\n\nВремена дублируются в ISO (`started_at_iso`, `finished_at_iso`): unix-секунды остаются для машин, читаемая форма — чтобы дашборд не изобретал своё форматирование.",
                "operationId": "datahub_runs",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/day"
                    },
                    {
                        "$ref": "#/components/parameters/source_code"
                    },
                    {
                        "$ref": "#/components/parameters/group_code"
                    },
                    {
                        "name": "status",
                        "in": "query",
                        "description": "Фильтр по статусу прогона",
                        "schema": {
                            "$ref": "#/components/schemas/runStatus"
                        }
                    },
                    {
                        "name": "limit",
                        "in": "query",
                        "description": "Размер страницы",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 1000,
                            "default": 200
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Прогоны за сутки",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "ok": {
                                            "type": "boolean",
                                            "example": true
                                        },
                                        "day": {
                                            "type": "integer",
                                            "description": "Бакет суток YYYYMMDD",
                                            "example": 20260824
                                        },
                                        "count": {
                                            "type": "integer",
                                            "example": 12
                                        },
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/run"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/forbidden"
                    },
                    "429": {
                        "$ref": "#/components/responses/tooManyRequests"
                    }
                }
            }
        },
        "/api/datahub/status": {
            "get": {
                "tags": [
                    "contract"
                ],
                "summary": "Сводка сбора",
                "description": "Всё, что нужно дашборду одним запросом: здоровье, прогоны за сутки, суммы счётчиков, больные источники, дерево навигации, таблица источников с методологией извлечения и справочник территорий.\n\n`ok` — «запрос удался», а состояние сбора отдельным полем `collection_ok`: иначе прогоны со статусом `error` читались бы как поломка API.",
                "operationId": "datahub_status",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/day"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Сводка за сутки",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/status"
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/forbidden"
                    },
                    "429": {
                        "$ref": "#/components/responses/tooManyRequests"
                    }
                }
            }
        },
        "/api/datahub/worker/claim": {
            "post": {
                "tags": ["worker"],
                "summary": "Забрать заказ города",
                "operationId": "datahub_worker_claim",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": ["parser_name", "host"],
                                "properties": {
                                    "parser_name": { "type": "string", "example": "ymaps" },
                                    "host": { "type": "string", "example": "ymaps-1" },
                                    "parser_version": { "type": "string", "example": "0.1.0" }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": { "description": "`data` — спецификация прогона либо null, если работы нет" },
                    "403": { "$ref": "#/components/responses/forbidden" }
                }
            }
        },
        "/api/datahub/worker/runs/{run_id}/known-ids": {
            "get": {
                "tags": ["worker"],
                "summary": "Известные external_id источника прогона",
                "operationId": "datahub_worker_known_ids",
                "parameters": [
                    { "name": "run_id", "in": "path", "required": true, "schema": { "type": "integer" } },
                    { "name": "since", "in": "query", "schema": { "type": "integer", "default": 0 } },
                    { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 5000 } }
                ],
                "responses": {
                    "200": { "description": "`ids` и `next_since_id` (пусто — конец)" },
                    "403": { "$ref": "#/components/responses/forbidden" },
                    "404": { "description": "прогон не найден" }
                }
            }
        },
        "/api/datahub/worker/runs/{run_id}/heartbeat": {
            "post": {
                "tags": ["worker"],
                "summary": "Пульс воркера",
                "operationId": "datahub_worker_heartbeat",
                "parameters": [
                    { "name": "run_id", "in": "path", "required": true, "schema": { "type": "integer" } }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "host": { "type": "string" },
                                    "parser_version": { "type": "string" }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": { "description": "`data.status`" },
                    "403": { "$ref": "#/components/responses/forbidden" },
                    "409": { "description": "прогон закрыт или чужой" }
                }
            }
        },
        "/api/datahub/worker/runs/{run_id}/progress": {
            "post": {
                "tags": ["worker"],
                "summary": "Прогресс без финализации",
                "operationId": "datahub_worker_progress",
                "parameters": [
                    { "name": "run_id", "in": "path", "required": true, "schema": { "type": "integer" } }
                ],
                "responses": {
                    "200": { "description": "ok" },
                    "403": { "$ref": "#/components/responses/forbidden" },
                    "409": { "description": "прогон закрыт" }
                }
            }
        },
        "/api/datahub/worker/runs/{run_id}/events": {
            "post": {
                "tags": ["worker"],
                "summary": "События прогона",
                "operationId": "datahub_worker_events",
                "parameters": [
                    { "name": "run_id", "in": "path", "required": true, "schema": { "type": "integer" } }
                ],
                "responses": {
                    "200": { "description": "ok" },
                    "403": { "$ref": "#/components/responses/forbidden" },
                    "409": { "description": "прогон закрыт" }
                }
            }
        },
        "/api/datahub/worker/runs/{run_id}/records": {
            "post": {
                "tags": ["worker"],
                "summary": "Батч карточек ym_org",
                "operationId": "datahub_worker_records",
                "parameters": [
                    { "name": "run_id", "in": "path", "required": true, "schema": { "type": "integer" } }
                ],
                "responses": {
                    "200": { "description": "`accepted` и `states` из save()" },
                    "403": { "$ref": "#/components/responses/forbidden" },
                    "409": { "description": "прогон закрыт" }
                }
            }
        },
        "/api/datahub/worker/runs/{run_id}/finish": {
            "post": {
                "tags": ["worker"],
                "summary": "Конец обхода: сверка и статус",
                "operationId": "datahub_worker_finish",
                "parameters": [
                    { "name": "run_id", "in": "path", "required": true, "schema": { "type": "integer" } }
                ],
                "responses": {
                    "200": { "description": "`data.status` считает datahub" },
                    "403": { "$ref": "#/components/responses/forbidden" },
                    "409": { "description": "прогон закрыт или чужой; finish не повторять" }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "TokenAuth": {
                "type": "apiKey",
                "description": "Токен в query-параметре",
                "name": "syla_token",
                "in": "query"
            },
            "ApiKeyAuth": {
                "type": "apiKey",
                "description": "Токен в заголовке",
                "name": "X-SYLA",
                "in": "header"
            }
        },
        "parameters": {
            "since_id": {
                "name": "since_id",
                "in": "query",
                "description": "Курсор: отдавать строки с PK больше этого значения. `0` либо отсутствие — с начала",
                "schema": {
                    "type": "integer",
                    "minimum": 0,
                    "default": 0
                }
            },
            "limit": {
                "name": "limit",
                "in": "query",
                "description": "Размер страницы. Значения выше 1000 срезаются: защита и от случайного `limit=1000000`, и от таймаута",
                "schema": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 1000,
                    "default": 100
                }
            },
            "source_code": {
                "name": "source_code",
                "in": "query",
                "description": "Код источника из реестра",
                "schema": {
                    "type": "string",
                    "example": "sk_chel"
                }
            },
            "group_code": {
                "name": "group_code",
                "in": "query",
                "description": "Код группы источников. Группа без источников даёт пустую страницу, а не ошибку",
                "schema": {
                    "type": "string",
                    "example": "sk"
                }
            },
            "doc_kind": {
                "name": "doc_kind",
                "in": "query",
                "description": "Вид документа",
                "schema": {
                    "type": "string",
                    "enum": [
                        "news",
                        "press_release",
                        "operational_report",
                        "telegram_post",
                        "blog",
                        "other"
                    ]
                }
            },
            "record_type": {
                "name": "record_type",
                "in": "query",
                "description": "Тип сырой записи",
                "schema": {
                    "type": "string",
                    "enum": [
                        "hh_vacancy",
                        "hh_employer",
                        "vk_post",
                        "cbr_rate",
                        "rosstat_section",
                        "ym_org"
                    ],
                    "example": "ym_org"
                }
            },
            "dataset_code": {
                "name": "dataset_code",
                "in": "query",
                "description": "Код датасета из реестра",
                "schema": {
                    "type": "string",
                    "example": "rosstat_population"
                }
            },
            "indicator_code": {
                "name": "indicator_code",
                "in": "query",
                "description": "Код показателя",
                "schema": {
                    "type": "string"
                }
            },
            "period_from": {
                "name": "period_from",
                "in": "query",
                "description": "Нижняя граница `period_start` в формате YYYYMMDD (не unix): например `20200101`. Отдаются наблюдения с периодом не раньше этой даты",
                "schema": {
                    "type": "integer",
                    "example": 20200101
                }
            },
            "territory_id": {
                "name": "territory_id",
                "in": "query",
                "description": "Внутренний id территории. На разных стендах разный — для субъекта берите `territory_code`",
                "schema": {
                    "type": "integer",
                    "example": 24
                }
            },
            "territory_code": {
                "name": "territory_code",
                "in": "query",
                "description": "Код субъекта в справочнике ОКАТО (2 знака). Якутия — `98`. Неизвестный код даёт пустую страницу, не 404",
                "schema": {
                    "type": "string",
                    "example": "98"
                }
            },
            "day": {
                "name": "day",
                "in": "query",
                "description": "Бакет суток YYYYMMDD. По умолчанию — сегодня",
                "schema": {
                    "type": "integer",
                    "example": 20260824
                }
            }
        },
        "responses": {
            "forbidden": {
                "description": "Токен не передан либо недействителен, или у пользователя нет права на этот путь",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/error"
                        },
                        "example": {
                            "ok": false,
                            "error": "токен не передан либо недействителен"
                        }
                    }
                }
            },
            "tooManyRequests": {
                "description": "Превышена квота. Режет только при включённом `DATAHUB_API_RATELIMIT_ENABLED`",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/error"
                        }
                    }
                }
            },
            "notImplemented": {
                "description": "Метод ещё не реализован: обработчик бросает исключение",
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "object",
                            "properties": {
                                "ok": {
                                    "type": "boolean",
                                    "example": false
                                },
                                "errors": {
                                    "type": "object",
                                    "description": "Описание исключения",
                                    "additionalProperties": true
                                }
                            }
                        }
                    }
                }
            }
        },
        "schemas": {
            "error": {
                "type": "object",
                "properties": {
                    "ok": {
                        "type": "boolean",
                        "example": false
                    },
                    "error": {
                        "type": "string",
                        "description": "Причина отказа"
                    }
                }
            },
            "health": {
                "type": "object",
                "properties": {
                    "ok": {
                        "type": "boolean",
                        "description": "PostgreSQL и S3 доступны, схемы применены",
                        "example": true
                    },
                    "module": {
                        "type": "string",
                        "example": "datahub"
                    },
                    "checked_at": {
                        "type": "string",
                        "format": "date-time",
                        "example": "2026-08-24T16:00:00+03:00"
                    },
                    "php": {
                        "type": "object",
                        "properties": {
                            "ok": {
                                "type": "boolean",
                                "example": true
                            },
                            "version": {
                                "type": "string",
                                "example": "8.3.14"
                            },
                            "sapi": {
                                "type": "string",
                                "example": "fpm-fcgi"
                            },
                            "hostname": {
                                "type": "string"
                            },
                            "extensions": {
                                "type": "object",
                                "description": "Наличие обязательных расширений",
                                "additionalProperties": {
                                    "type": "boolean"
                                },
                                "example": {
                                    "pdo_pgsql": true,
                                    "curl": true,
                                    "intl": true
                                }
                            }
                        }
                    },
                    "postgres": {
                        "type": "object",
                        "properties": {
                            "ok": {
                                "type": "boolean",
                                "example": true
                            },
                            "source": {
                                "type": "string",
                                "example": "default"
                            },
                            "host": {
                                "type": "string",
                                "example": "127.0.0.1:5432"
                            },
                            "database": {
                                "type": "string"
                            },
                            "schema": {
                                "type": "string",
                                "example": "datahub_smoke"
                            },
                            "rows": {
                                "type": "integer",
                                "description": "Строк в смоук-таблице"
                            },
                            "uncompiled": {
                                "type": "array",
                                "description": "Схемы модуля, не доехавшие до корневой `schemas/`. Присутствует только когда такие есть",
                                "items": {
                                    "type": "string"
                                }
                            },
                            "error": {
                                "type": "string",
                                "description": "Присутствует только при ошибке"
                            }
                        }
                    },
                    "s3": {
                        "type": "object",
                        "properties": {
                            "ok": {
                                "type": "boolean",
                                "example": true
                            },
                            "endpoint": {
                                "type": "string"
                            },
                            "bucket": {
                                "type": "string"
                            },
                            "objects": {
                                "type": "integer",
                                "description": "Объектов в первой сотне листинга"
                            },
                            "error": {
                                "type": "string",
                                "description": "Присутствует только при ошибке"
                            }
                        }
                    }
                }
            },
            "sowArticle": {
                "type": "object",
                "description": "Публикация в контракте SOW. Ключ есть всегда, значение может быть `null`: это объединение полей всех SOW, а не пересечение — у одних источников нет `region_code`, у других `video` или `author`.",
                "properties": {
                    "article_id": {
                        "type": "string",
                        "description": "UID публикации (`document_uid`). Ревизия приходит с тем же `article_id` и другим `content_hash`",
                        "example": "sk_chel:9f2c1a7e4b"
                    },
                    "source_code": {
                        "type": "string",
                        "example": "sk_chel"
                    },
                    "source_layer": {
                        "type": "string",
                        "description": "Слой источника из реестра: федеральный, региональный и т.п.",
                        "nullable": true
                    },
                    "source_name": {
                        "type": "string",
                        "nullable": true
                    },
                    "region_code": {
                        "type": "string",
                        "nullable": true
                    },
                    "region_name": {
                        "type": "string",
                        "nullable": true
                    },
                    "source_url": {
                        "type": "string",
                        "description": "URL публикации на сайте источника",
                        "example": "https://example.gov.ru/news/12345"
                    },
                    "published_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "Дата публикации. `null`, если разобрать не удалось: отсутствие даты честнее выдуманной",
                        "nullable": true,
                        "example": "2026-08-24T09:15:00+00:00"
                    },
                    "pub_date_estimated": {
                        "type": "boolean",
                        "description": "Дата не взята из вёрстки, а оценена",
                        "example": false
                    },
                    "title": {
                        "type": "string",
                        "nullable": true
                    },
                    "description": {
                        "type": "string",
                        "nullable": true
                    },
                    "full_text": {
                        "type": "string",
                        "nullable": true
                    },
                    "images": {
                        "type": "array",
                        "description": "Изображения публикации. Пусто — `[]`",
                        "items": {
                            "type": "object",
                            "additionalProperties": true
                        }
                    },
                    "attachments": {
                        "type": "array",
                        "description": "Вложения публикации. Пусто — `[]`",
                        "items": {
                            "type": "object",
                            "additionalProperties": true
                        }
                    },
                    "video": {
                        "type": "string",
                        "nullable": true
                    },
                    "author": {
                        "type": "string",
                        "nullable": true
                    },
                    "tags": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "content_hash": {
                        "type": "string",
                        "description": "Хеш содержимого: смена хеша при том же `article_id` и есть ревизия"
                    },
                    "parser_version": {
                        "type": "string",
                        "nullable": true
                    },
                    "parsed_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "Когда собрано (`collected_at`)",
                        "nullable": true
                    }
                }
            },
            "sowObservation": {
                "type": "object",
                "description": "Наблюдение: показатель × территория × период × значение, вместе с происхождением значения.",
                "properties": {
                    "observation_id": {
                        "type": "integer",
                        "example": 190324
                    },
                    "source_code": {
                        "type": "string",
                        "example": "rosstat"
                    },
                    "dataset_id": {
                        "type": "integer",
                        "nullable": true
                    },
                    "dataset_code": {
                        "type": "string",
                        "nullable": true
                    },
                    "indicator_code": {
                        "type": "string",
                        "nullable": true
                    },
                    "indicator_label": {
                        "type": "string",
                        "nullable": true
                    },
                    "territory_id": {
                        "type": "integer",
                        "description": "Территория из справочника. `null`, когда название не сматчилось: коллизию алиаса не угадываем",
                        "nullable": true
                    },
                    "territory_code_raw": {
                        "type": "string",
                        "nullable": true
                    },
                    "territory_name_raw": {
                        "type": "string",
                        "description": "Название территории как в источнике",
                        "nullable": true
                    },
                    "period_type": {
                        "type": "string",
                        "nullable": true,
                        "enum": [
                            "year",
                            "halfyear",
                            "quarter",
                            "month",
                            "week",
                            "day",
                            "point",
                            "range",
                            null
                        ]
                    },
                    "period_start": {
                        "type": "integer",
                        "description": "Начало периода, YYYYMMDD (не unix). Январь 2024 — `20240101`",
                        "example": 20240101,
                        "nullable": true
                    },
                    "period_end": {
                        "type": "integer",
                        "description": "Конец периода, YYYYMMDD (не unix). 2024 год — `20241231`",
                        "example": 20241231,
                        "nullable": true
                    },
                    "period_year": {
                        "type": "integer",
                        "nullable": true
                    },
                    "period_label": {
                        "type": "string",
                        "description": "Период как в источнике",
                        "nullable": true
                    },
                    "value_numeric": {
                        "type": "number",
                        "description": "Числовое значение. `null`, когда значения нет или оно не число",
                        "nullable": true
                    },
                    "value_raw": {
                        "type": "string",
                        "description": "Значение как в ячейке источника",
                        "nullable": true
                    },
                    "value_flag": {
                        "type": "string",
                        "nullable": true,
                        "enum": [
                            "ok",
                            "missing",
                            "confidential",
                            "not_applicable",
                            "rounded",
                            "estimated",
                            null
                        ]
                    },
                    "unit_raw": {
                        "type": "string",
                        "nullable": true
                    },
                    "unit_code": {
                        "type": "string",
                        "nullable": true
                    },
                    "unit_scale": {
                        "type": "number",
                        "description": "Множитель единицы измерения",
                        "nullable": true
                    },
                    "dimensions": {
                        "type": "object",
                        "description": "Разрезы наблюдения. У выгрузки Росстата обычно один ключ `series` — полный текст ряда (территория, пол, возраст, город/село)",
                        "additionalProperties": true,
                        "example": {
                            "series": "Численность населения …: Республика Саха (Якутия): Все население: мужчины и женщины"
                        }
                    },
                    "extraction_method": {
                        "type": "string",
                        "description": "Чем извлечено значение",
                        "nullable": true,
                        "enum": [
                            "sdmx",
                            "csv",
                            "xlsx_structural",
                            "xlsx_llm",
                            "pdf_ocr",
                            "api",
                            "manual",
                            null
                        ]
                    },
                    "file_id": {
                        "type": "integer",
                        "description": "Ревизия исходного файла в `GET /api/datahub/files/{file_id}`. `null`, если наблюдение пришло не из файла",
                        "example": 20,
                        "nullable": true
                    },
                    "cell_locator": {
                        "type": "object",
                        "description": "Координата ячейки в исходном файле, чтобы цифру можно было проверить. Пусто — `null`, а не `[]`: это координата, а не список",
                        "nullable": true,
                        "additionalProperties": true
                    },
                    "confidence": {
                        "type": "number",
                        "nullable": true
                    },
                    "collected_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    }
                }
            },
            "sowFile": {
                "type": "object",
                "description": "Метаданные неизменённого исходного файла. Байты — `GET /api/datahub/files/{file_id}/content`.",
                "properties": {
                    "file_id": { "type": "integer", "example": 20 },
                    "file_uid": { "type": "string" },
                    "rev": { "type": "integer", "description": "Номер ревизии файла. Первая загрузка — 1, смена содержимого (sha256) — +1", "example": 1 },
                    "source_code": { "type": "string", "example": "rosstat_emiss" },
                    "dataset_id": { "type": "integer", "nullable": true },
                    "url": { "type": "string", "description": "URL, с которого скачали" },
                    "title": { "type": "string", "nullable": true },
                    "detected_format": { "type": "string", "nullable": true, "example": "xls" },
                    "sha256": { "type": "string", "nullable": true },
                    "byte_size": { "type": "integer", "nullable": true },
                    "content_type": { "type": "string", "nullable": true },
                    "period_from": { "type": "integer", "nullable": true, "description": "YYYYMMDD" },
                    "period_to": { "type": "integer", "nullable": true, "description": "YYYYMMDD" },
                    "downloaded_at": { "type": "string", "format": "date-time", "nullable": true }
                }
            },
            "runStatus": {
                "type": "string",
                "description": "Статус прогона. Вычисляется по фактам: `ok` недостижим без прошедшей post-run сверки счётчиков",
                "enum": [
                    "running",
                    "ok",
                    "partial",
                    "blocked",
                    "error",
                    "cancelled"
                ]
            },
            "run": {
                "type": "object",
                "properties": {
                    "run_id": {
                        "type": "integer",
                        "example": 1284
                    },
                    "run_uid": {
                        "type": "string",
                        "description": "UUID прогона"
                    },
                    "source_code": {
                        "type": "string",
                        "nullable": true
                    },
                    "group_code": {
                        "type": "string",
                        "nullable": true
                    },
                    "mode": {
                        "type": "string",
                        "enum": [
                            "full",
                            "incremental",
                            "backfill",
                            "listing_only",
                            "probe",
                            "reparse"
                        ]
                    },
                    "status": {
                        "$ref": "#/components/schemas/runStatus"
                    },
                    "stopped_reason": {
                        "type": "string",
                        "description": "Почему прогон остановился",
                        "nullable": true
                    },
                    "started_at": {
                        "type": "integer",
                        "description": "Unix-время",
                        "nullable": true
                    },
                    "finished_at": {
                        "type": "integer",
                        "description": "Unix-время. `null`, пока прогон идёт",
                        "nullable": true
                    },
                    "started_at_iso": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "finished_at_iso": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "duration_ms": {
                        "type": "integer"
                    },
                    "items_pending": {
                        "type": "integer",
                        "description": "Сколько элементов ждёт обработки в очереди"
                    },
                    "pages_processed": {
                        "type": "integer"
                    },
                    "items_found": {
                        "type": "integer"
                    },
                    "items_new": {
                        "type": "integer"
                    },
                    "items_stored": {
                        "type": "integer",
                        "description": "Подтверждается строками в `datahub_documents` по этому `run_id`"
                    },
                    "items_revised": {
                        "type": "integer"
                    },
                    "items_duplicate": {
                        "type": "integer"
                    },
                    "items_invalid": {
                        "type": "integer"
                    },
                    "items_without_date": {
                        "type": "integer",
                        "description": "Публикации, у которых дату разобрать не удалось"
                    },
                    "items_without_territory": {
                        "type": "integer"
                    },
                    "http_requests": {
                        "type": "integer"
                    },
                    "http_errors": {
                        "type": "integer"
                    },
                    "parse_errors": {
                        "type": "integer"
                    },
                    "files_downloaded": {
                        "type": "integer"
                    },
                    "bytes_downloaded": {
                        "type": "integer"
                    },
                    "observations_stored": {
                        "type": "integer"
                    },
                    "robots_blocked": {
                        "type": "integer"
                    }
                }
            },
            "status": {
                "type": "object",
                "properties": {
                    "ok": {
                        "type": "boolean",
                        "description": "Запрос удался. Состояние сбора — в `collection_ok`",
                        "example": true
                    },
                    "collection_ok": {
                        "type": "boolean",
                        "description": "Нет ни одного прогона со статусом `error` за эти сутки",
                        "example": true
                    },
                    "day": {
                        "type": "integer",
                        "example": 20260824
                    },
                    "health": {
                        "$ref": "#/components/schemas/health"
                    },
                    "runs": {
                        "type": "integer",
                        "description": "Прогонов за сутки"
                    },
                    "statuses": {
                        "type": "object",
                        "description": "Прогоны по статусам",
                        "additionalProperties": {
                            "type": "integer"
                        },
                        "example": {
                            "ok": 9,
                            "partial": 2,
                            "error": 1
                        }
                    },
                    "counters": {
                        "type": "object",
                        "description": "Суммы канонических счётчиков по прогонам за сутки",
                        "additionalProperties": {
                            "type": "integer"
                        }
                    },
                    "duration_ms": {
                        "type": "integer",
                        "description": "Суммарная длительность прогонов"
                    },
                    "unhealthy_sources": {
                        "type": "array",
                        "description": "Источники с серией отказов или без успешного прогона за сутки",
                        "items": {
                            "type": "object",
                            "additionalProperties": true
                        }
                    },
                    "last_runs": {
                        "type": "array",
                        "description": "До 50 последних прогонов за сутки",
                        "items": {
                            "$ref": "#/components/schemas/run"
                        }
                    },
                    "documents": {
                        "type": "integer",
                        "description": "Всего актуальных публикаций"
                    },
                    "counts": {
                        "type": "object",
                        "description": "Пилюли дашборда: за сутки и всего",
                        "properties": {
                            "runs_day": {
                                "type": "integer"
                            },
                            "runs_total": {
                                "type": "integer"
                            },
                            "documents_day": {
                                "type": "integer"
                            },
                            "documents_total": {
                                "type": "integer"
                            },
                            "datasets_day": {
                                "type": "integer"
                            },
                            "datasets_total": {
                                "type": "integer"
                            },
                            "groups_active": {
                                "type": "integer"
                            },
                            "sources_active": {
                                "type": "integer"
                            },
                            "unhealthy": {
                                "type": "integer"
                            },
                            "events_day": {
                                "type": "integer"
                            },
                            "events_total": {
                                "type": "integer"
                            }
                        }
                    },
                    "sources": {
                        "type": "array",
                        "description": "Таблица источников. Датасеты Росстата подмешаны строками с `row_kind=dataset`",
                        "items": {
                            "$ref": "#/components/schemas/statusSource"
                        }
                    },
                    "territories": {
                        "type": "array",
                        "description": "Справочник территорий для фильтра, от страны к населённым пунктам",
                        "items": {
                            "type": "object",
                            "properties": {
                                "territory_id": {
                                    "type": "integer"
                                },
                                "code": {
                                    "type": "string",
                                    "description": "Код в системе справочника. У субъектов РФ — ОКАТО, Якутия — `98`"
                                },
                                "name": {
                                    "type": "string"
                                },
                                "level": {
                                    "type": "string",
                                    "enum": [
                                        "country",
                                        "federal_district",
                                        "region",
                                        "municipality",
                                        "settlement"
                                    ]
                                }
                            }
                        }
                    },
                    "nav": {
                        "type": "object",
                        "description": "Дерево навигации: реализованные источники и датасеты, у которых уже есть наблюдения",
                        "properties": {
                            "groups": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "group_code": {
                                            "type": "string"
                                        },
                                        "title": {
                                            "type": "string"
                                        },
                                        "sources": {
                                            "type": "array",
                                            "items": {
                                                "type": "object",
                                                "properties": {
                                                    "source_code": {
                                                        "type": "string"
                                                    },
                                                    "source_name": {
                                                        "type": "string"
                                                    },
                                                    "docs_count": {
                                                        "type": "integer"
                                                    },
                                                    "last_ok_at": {
                                                        "type": "string",
                                                        "format": "date-time",
                                                        "nullable": true
                                                    },
                                                    "fail_streak": {
                                                        "type": "integer"
                                                    }
                                                }
                                            }
                                        },
                                        "datasets": {
                                            "type": "array",
                                            "items": {
                                                "type": "object",
                                                "properties": {
                                                    "dataset_code": {
                                                        "type": "string"
                                                    },
                                                    "title": {
                                                        "type": "string"
                                                    },
                                                    "indicator_code": {
                                                        "type": "string",
                                                        "nullable": true
                                                    },
                                                    "last_loaded_at": {
                                                        "type": "string",
                                                        "format": "date-time",
                                                        "nullable": true
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "statusSource": {
                "type": "object",
                "properties": {
                    "row_kind": {
                        "type": "string",
                        "enum": [
                            "source",
                            "dataset"
                        ]
                    },
                    "source_code": {
                        "type": "string"
                    },
                    "source_name": {
                        "type": "string",
                        "nullable": true
                    },
                    "group_code": {
                        "type": "string",
                        "nullable": true
                    },
                    "source_kind": {
                        "type": "string",
                        "nullable": true
                    },
                    "source_layer": {
                        "type": "string",
                        "nullable": true
                    },
                    "enabled": {
                        "type": "integer",
                        "enum": [
                            0,
                            1
                        ]
                    },
                    "lifecycle_state": {
                        "type": "string",
                        "description": "Реализован ли адаптер источника",
                        "nullable": true
                    },
                    "base_url": {
                        "type": "string",
                        "nullable": true
                    },
                    "parser_profile": {
                        "type": "string",
                        "nullable": true
                    },
                    "region_code": {
                        "type": "string",
                        "nullable": true
                    },
                    "region_name": {
                        "type": "string",
                        "nullable": true
                    },
                    "docs_count": {
                        "type": "integer",
                        "nullable": true
                    },
                    "fail_streak": {
                        "type": "integer",
                        "description": "Отказов подряд"
                    },
                    "current_delay_ms": {
                        "type": "integer",
                        "description": "Текущая задержка между запросами к источнику"
                    },
                    "last_ok_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "cooldown_until": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "last_403_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "last_429_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "access": {
                        "type": "object",
                        "description": "Условия доступа: robots.txt, лимиты, требования источника",
                        "additionalProperties": true
                    },
                    "methodology": {
                        "type": "object",
                        "description": "Как именно извлекаются поля этого источника — из живого профиля парсера, а не из описания в документации. Правка профиля меняет ответ на следующем запросе",
                        "nullable": true,
                        "properties": {
                            "title": {
                                "type": "string"
                            },
                            "summary": {
                                "type": "string"
                            },
                            "steps": {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                }
                            },
                            "fields": {
                                "type": "array",
                                "description": "Поле и способ его извлечения. Пустая цепочка селекторов означает «не извлекаем: в вёрстке нет», а не пропуск поля",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "name": {
                                            "type": "string"
                                        },
                                        "how": {
                                            "type": "string"
                                        }
                                    }
                                }
                            },
                            "refuses": {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                }
                            },
                            "limits": {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                }
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "kind": {
                                        "type": "string"
                                    },
                                    "profile_code": {
                                        "type": "string",
                                        "nullable": true
                                    },
                                    "profile_rev": {
                                        "type": "integer",
                                        "nullable": true
                                    },
                                    "timezone": {
                                        "type": "string",
                                        "nullable": true
                                    },
                                    "parser_version": {
                                        "type": "string",
                                        "nullable": true
                                    }
                                }
                            }
                        }
                    },
                    "dataset_code": {
                        "type": "string",
                        "description": "Только у строк `row_kind=dataset`",
                        "nullable": true
                    },
                    "frequency": {
                        "type": "string",
                        "description": "Только у датасетов",
                        "nullable": true
                    },
                    "channel": {
                        "type": "string",
                        "description": "Только у датасетов",
                        "nullable": true
                    },
                    "unit_expected": {
                        "type": "string",
                        "description": "Только у датасетов",
                        "nullable": true
                    },
                    "priority": {
                        "type": "string",
                        "description": "Только у датасетов",
                        "nullable": true
                    },
                    "id_status": {
                        "type": "string",
                        "description": "Только у датасетов",
                        "nullable": true
                    },
                    "sow_ref": {
                        "type": "string",
                        "description": "Ссылка на пункт SOW. Только у датасетов",
                        "nullable": true
                    },
                    "notes": {
                        "type": "string",
                        "description": "Только у датасетов",
                        "nullable": true
                    },
                    "parent_source": {
                        "type": "string",
                        "description": "Источник-родитель датасета",
                        "nullable": true
                    },
                    "indicator_code": {
                        "type": "string",
                        "description": "Только у датасетов",
                        "nullable": true
                    }
                }
            }
        }
    }
}
