diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 02574451..e228cbbd 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -110,7 +110,7 @@ jobs: # format-pass = future enhancement. `ruff check .` сейчас green. run: uv run ruff check . - - name: Test (pytest) + - name: Test (pytest + coverage gate) # --ignore=tests/smoke: prod-smoke бьёт по live https://gendsgn.ru # (помечены @pytest.mark.prod_smoke; pyproject addopts уже их deselect'ит, # но --ignore — belt-and-suspenders на случай сбора фикстур). @@ -119,7 +119,35 @@ jobs: # tests/sql/ mv_layout self-skip'ается через Postgres-connectivity probe # (5432 недоступен в этом mock-lane) → SKIP. Это intended. # PDF-тесты ИДУТ (libpango выше). Target: 0 failed, skips OK. - run: uv run pytest -q --ignore=tests/smoke + # + # Coverage-gate (#68): --cov=app меряет покрытие пакета app/. + # --cov-fail-under=65 → job RED если покрытие упало ниже baseline + # (измерено 2026-06: mock-lane сьют ~71%, см. [tool.coverage] в pyproject; + # 65 = floor с запасом, не flaky). На CI PDF-тесты РЕАЛЬНО идут (libpango), + # поэтому реальное CI-покрытие ≥ локально-измеренного floor. + # coverage.xml — артефакт для будущего Codecov/Coveralls upload (#68 badge). + # term-missing → видно непокрытые строки прямо в job-логе. + run: | + uv run pytest -q --ignore=tests/smoke \ + --cov=app \ + --cov-report=term-missing:skip-covered \ + --cov-report=xml:coverage.xml \ + --cov-fail-under=65 + + - name: Coverage summary → job output + # Дешёвый human-readable итог. Бежит даже если gate упал (if: always) — + # чтобы было видно НАСКОЛЬКО просело покрытие, а не только "fail". + # GITHUB_STEP_SUMMARY поддержан не во всех версиях Forgejo act_runner → + # если переменная пустая/файла нет, печатаем в обычный лог (fallback). + if: always() + run: | + [ -f coverage.xml ] || { echo "coverage.xml отсутствует — пропускаю summary"; exit 0; } + report="$(uv run coverage report --skip-covered --sort=cover | tail -40)" + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + { echo '```'; echo "$report"; echo '```'; } >> "$GITHUB_STEP_SUMMARY" + else + echo "$report" + fi frontend-tests: runs-on: ubuntu-latest @@ -152,3 +180,95 @@ jobs: # `npm run test` = `vitest run` (single-shot, не watch). Только тесты — # `next build` НАМЕРЕННО не здесь (тяжёлый, verified в deploy.yml build). run: npm run test + + # OpenAPI → TS codegen drift-gate (#69). Защищает фронт от молчаливого + # рассинхрона типов: если backend изменил OpenAPI-схему, а + # frontend/src/lib/api-types.ts не перегенерён (`npm run codegen`) — job RED. + # + # Бежит когда поменялся backend ИЛИ frontend (backend-change может застейлить + # типы даже без правок во frontend/). Отдельный job (не внутри frontend-tests), + # чтобы vitest и codegen-gate скейлились независимо. + # + # WHY no running server: `npm run codegen` бьёт по http://localhost:8000/openapi.json, + # но схема = app.openapi() — её можно сдампить из python БЕЗ uvicorn/DB + # (app.main импортируется под TESTING=1 со stub-DSN, как в backend-tests). + # Это убирает flaky port-wait. openapi-typescript v7 принимает локальный файл. + # + # WHY prettier: committed api-types.ts форматируется prettier'ом через + # pre-commit hook (mirrors-prettier, no config → defaults, 2-space). Raw + # openapi-typescript отдаёт 4-space → diff-шум. Прогоняем prettier (defaults) + # на regen, чтобы сравнивать ТОЛЬКО контент, не форматирование. + openapi-codegen-check: + runs-on: ubuntu-latest + needs: changes + if: | + needs.changes.outputs.backend == 'true' || + needs.changes.outputs.frontend == 'true' + env: + # Те же stub-переменные, что backend-tests: psycopg требует parseable URL + # на импорте; реального коннекта нет (схему дампим, не обслуживаем запросы). + TESTING: "1" + DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test + REDIS_URL: redis://localhost:6379/0 + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Install system deps for geo + WeasyPrint + # app.main транзитивно тянет geo/PDF-модули. На macOS-dev импорт схемы + # проходит и без этих либ, но на ubuntu ставим как backend-tests + # (belt-and-suspenders, чтобы import app.main точно не падал). + run: | + sudo apt-get update + sudo apt-get install -y libpq-dev libgdal-dev libproj-dev libgeos-dev \ + libcairo2 libpango-1.0-0 libpangoft2-1.0-0 + + - name: Install backend deps (uv sync --frozen) + working-directory: backend + run: uv sync --frozen + + - name: Install frontend deps (npm ci) + working-directory: frontend + run: npm ci --legacy-peer-deps --no-audit --no-fund + + - name: Dump OpenAPI schema from app (no server) + working-directory: backend + run: uv run python -c "import json; from app.main import app; print(json.dumps(app.openapi()))" > /tmp/openapi.json + + - name: Regenerate api-types.ts + format (prettier defaults) + working-directory: frontend + # 1) openapi-typescript из дампнутого файла (эквивалент `npm run codegen`, + # который читает ту же схему по URL). 2) prettier (defaults, как + # pre-commit hook) → формат совпадает с committed-файлом. + run: | + npx openapi-typescript /tmp/openapi.json -o src/lib/api-types.ts + npx prettier --write src/lib/api-types.ts + + - name: Assert api-types.ts is up-to-date + working-directory: frontend + # git diff --exit-code: 0 если файл не изменился (типы актуальны), + # 1 если regen дал другой результат (типы устарели → fail с подсказкой). + run: | + if ! git diff --exit-code -- src/lib/api-types.ts; then + echo "::error::frontend/src/lib/api-types.ts устарел относительно backend OpenAPI." \ + "Запусти: cd frontend && npm run codegen (с backend на :8000), затем закоммить." \ + "Pre-commit prettier отформатирует автоматически." + exit 1 + fi + echo "✓ api-types.ts актуален относительно backend OpenAPI-схемы." diff --git a/Caddyfile b/Caddyfile index 257bd081..7911a163 100644 --- a/Caddyfile +++ b/Caddyfile @@ -149,6 +149,25 @@ errors.gendsgn.ru { } } +# Uptime Kuma — self-hosted uptime monitoring + public status page (#75 B6-1). +# DNS: A-record status.gendsgn.ru → IP VPS (добавить перед деплоем стека). +# Контейнер из docker-compose.uptime.yml (project gendesign-uptime) на shared +# gendesign_shared network. Если стек не запущен — Caddy отдаёт 502 ТОЛЬКО на +# этом домене, main-сайт не страдает (как obsidian.gendsgn.ru). +# +# ВНИМАНИЕ: status-page НАМЕРЕННО публичен (trust-building для пилотов, issue #75). +# Admin-панель Kuma (/dashboard, /manage-*) защищена собственным логином Kuma — +# НЕ кладём её за caddy/users.caddy.snippet, иначе double-auth сломает setup. +status.gendsgn.ru { + encode zstd gzip + + reverse_proxy uptime-kuma:3001 + + log { + output file /var/log/caddy/status.gendsgn.ru.log + } +} + # Forgejo — self-hosted git (migration 2026-05-16). # DNS: A-record git.gendsgn.ru → IP VPS. # Forgejo container из forgejo-migration/docker-compose.yml на shared diff --git a/backend/pyproject.toml b/backend/pyproject.toml index bf825af7..cd271da7 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ dev = [ "pytest>=8.0.0", "pytest-asyncio>=0.23.0", + "pytest-cov>=5.0.0", # coverage gate в CI (#68): pytest --cov=app --cov-fail-under "ruff>=0.5.0", "mypy>=1.10.0", "types-redis>=4.6.0", @@ -101,9 +102,35 @@ ignore_missing_imports = true [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "auto" +# --cov НАМЕРЕННО НЕ в addopts: локальный `pytest` остаётся быстрым/без coverage. +# Coverage-gate включается ТОЛЬКО в CI явными флагами +# (`--cov=app --cov-fail-under=65`, см. .forgejo/workflows/ci.yml backend-tests). addopts = ["-m", "not prod_smoke"] markers = [ "slow: marks tests as slow (need real network, deselect with -m 'not slow')", "prod_smoke: production smoke tests against live https://gendsgn.ru (run only post-deploy with -m prod_smoke)", "integration: phantom column gate tests requiring TEST_DATABASE_URL (SSH tunnel to prod Postgres)", ] + +[tool.coverage.run] +# Меряем покрытие пакета app/. branch=true — учитываем непокрытые ветки, не только строки. +source = ["app"] +branch = true +omit = [ + "app/**/__main__.py", + "tests/*", +] + +[tool.coverage.report] +# Baseline gate = 65% (измерено: mock-lane CI-сьют даёт ~71% на 2026-06; 65 = floor +# с запасом под libpango-skip дельту и PR-to-PR дрейф, не ratchet'ит слишком туго). +# Поднимать постепенно по мере роста сьюта (#68 acceptance: "постепенно поднимать"). +fail_under = 65 +show_missing = true +skip_covered = true +exclude_also = [ + "if TYPE_CHECKING:", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "@(abc\\.)?abstractmethod", +] diff --git a/backend/uv.lock b/backend/uv.lock index 22ff9a98..12a58632 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -380,6 +380,90 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" }, + { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" }, + { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" }, + { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" }, + { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" }, + { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" }, + { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" }, + { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" }, + { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" }, + { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" }, + { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" }, + { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" }, + { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" }, + { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" }, + { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" }, + { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" }, + { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" }, + { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" }, + { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, +] + [[package]] name = "cryptography" version = "47.0.0" @@ -615,6 +699,7 @@ dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-cov" }, { name = "ruff" }, { name = "types-redis" }, ] @@ -663,6 +748,7 @@ dev = [ { name = "pre-commit", specifier = ">=3.7.0" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-asyncio", specifier = ">=0.23.0" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, { name = "ruff", specifier = ">=0.5.0" }, { name = "types-redis", specifier = ">=4.6.0" }, ] @@ -1921,6 +2007,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" diff --git a/docker-compose.uptime.yml b/docker-compose.uptime.yml new file mode 100644 index 00000000..91bb92a0 --- /dev/null +++ b/docker-compose.uptime.yml @@ -0,0 +1,57 @@ +# Uptime Kuma — self-hosted uptime monitoring + alerting (#75 B6-1). +# +# Деплоится ОТДЕЛЬНО от main-стека (свой compose-проект `gendesign-uptime`), +# по образцу docker-compose.obsidian.yml. НЕ входит в deploy.yml path-фильтры — +# main-pipeline (project `gendesign`, -f docker-compose.prod.yml) его НЕ трогает +# и НЕ пересоздаёт. Запуск/обновление — вручную (Anton), см. инструкцию ниже. +# +# docker network create gendesign_shared # один раз, обычно уже создан main-стеком +# docker compose -p gendesign-uptime -f docker-compose.uptime.yml up -d +# +# Caddy в main-стеке маршрутизирует status.gendsgn.ru → uptime-kuma:3001 через +# external network gendesign_shared (Caddyfile, блок status.gendsgn.ru). +# +# WHY отдельный контейнер, а не VPS: issue допускает доп-контейнер. Trade-off +# принят сознательно — если упадёт весь VPS, Kuma упадёт вместе с сайтом и не +# отправит алерт. Митигируется внешним «watchdog» (healthchecks.io / UptimeRobot +# free tier), который пингует https://gendsgn.ru/health извне — см. README-блок +# «External watchdog» ниже. Kuma покрывает per-endpoint/SSL/latency-мониторинг +# и self-hosted status-page; внешний пинг — last-resort «весь хост лёг». +# +# Лёгкий: образ ~250 МБ, idle RAM ~80-120 МБ. mem_limit держит его в узде. + +services: + uptime-kuma: + image: louislam/uptime-kuma:1 + container_name: gendesign-uptime-kuma + restart: unless-stopped + # expose (НЕ ports) — наружу не публикуем, фронтит Caddy через shared-сеть. + # Если нужен прямой доступ для первичной настройки без Caddy — временно + # раскомментируй ports ниже (127.0.0.1 only) и ходи через SSH-туннель. + expose: + - "3001" + # ports: + # - "127.0.0.1:3001:3001" + volumes: + # Persistent: SQLite-база Kuma (мониторы, история, настройки, нотификации). + - uptime_kuma_data:/app/data + healthcheck: + # Встроенный extra/healthcheck в образе louislam/uptime-kuma. + test: ["CMD", "extra/healthcheck"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 30s + networks: + - shared + mem_limit: 256m + +volumes: + uptime_kuma_data: + +networks: + # Та же external-сеть, что у main-стека (Caddy) и obsidian-стека. + # Создаётся вне compose: docker network create gendesign_shared + shared: + external: true + name: gendesign_shared diff --git a/frontend/src/lib/api-types.ts b/frontend/src/lib/api-types.ts index 44bf1ea1..9a177f01 100644 --- a/frontend/src/lib/api-types.ts +++ b/frontend/src/lib/api-types.ts @@ -17,9 +17,12 @@ export interface paths { * Create Concept * @description Generate 3 building variants for the given parcel polygon. * - * Stage 1a: returns stub with 3 empty variants. - * Stage 1b: real greedy placement. - * Stage 1c: TEAP + financial model attached. + * Stage 1a: Shapely parse + buildable area (setback) + placement grid. + * Stage 1b: greedy section placement with STRtree collisions (3 strategies). + * Stage 1c: real ТЭП + financial model attached to each variant. + * + * A degenerate parcel (setback consumes everything, malformed geometry) yields a + * 422 rather than empty variants — that is a bad request, not a valid empty result. */ post: operations["create_concept_api_v1_concepts_post"]; delete?: never; @@ -28,6 +31,57 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/chat/ask": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Ask + * @description Ответить на вопрос по §22-форсайту участка. + * + * Отчёта нет → 200 + report_status='pending' + RU-подсказка «запустите анализ» + * (READ-ONLY, ничего не считаем). Иначе ветка по `settings.llm_enabled`: + * • False → детерминированный Step-1 ответ (intent → render_answer), llm_used=False; + * • True → LLM tool-loop (orchestrate_chat через run_in_threadpool); при сбое LLM + * оркестратор сам отдаёт детерминированный ответ (llm_used=False + fallback_reason). + */ + post: operations["ask_api_v1_chat_ask_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/by-bbox": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Parcels By Bbox + * @description GET /parcels/by-bbox — вернуть участки внутри bounding box для карты. + * + * Использует GIST-индекс на cad_parcels.geom (ST_Intersects). + * Если передан user_id — добавляет статус из parcel_user_status (overlay). + * last_analysis_date — реальная дата последнего анализа из v_analysis_runs_latest + * (#994, миграция 127): один batch-запрос на весь bbox, участки без рана → None. + */ + get: operations["get_parcels_by_bbox_api_v1_parcels_by_bbox_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/parcels/search": { parameters: { query?: never; @@ -50,7 +104,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/parcels/{parcel_id}": { + "/api/v1/parcels/runs/{run_id}": { parameters: { query?: never; header?: never; @@ -58,10 +112,371 @@ export interface paths { cookie?: never; }; /** - * Get Parcel - * @description TODO Stage 2b: fetch parcel by id from DB. + * Get Analysis Run + * @description Полная строка одного рана анализа по id (включая `result`-блоб) — #994. + * + * Для re-open / детального просмотра сохранённого анализа. `result` отдаём как + * есть (форма analyze-1.0 ParcelAnalysis или §22 "1.0" SiteFinderReport — модель + * истории её не навязывает). + * + * 404 (graceful HTTPException), если рана с таким id нет. */ - get: operations["get_parcel_api_v1_parcels__parcel_id__get"]; + get: operations["get_analysis_run_api_v1_parcels_runs__run_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/runs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Analysis Runs + * @description Недавние раны анализа на участок, newest-first (LIGHT-список) — #994. + * + * Облегчённый список истории анализов: метаданные ранов БЕЗ тяжёлого `result`- + * блоба (его отдаёт GET /runs/{run_id}). Пустой список (200, НЕ 404), если ранов + * на участок ещё не было. + */ + get: operations["list_analysis_runs_api_v1_parcels__cad_num__runs_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/fetch-status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Fetch Status + * @description Polling endpoint для on-demand cadastre fetch (см. issue #93). + * + * Frontend polling каждые 2с после 202 Accepted из /analyze. + * + * Returns: + * { + * "status": "ready" | "fetching" | "not_in_nspd" | "failed" | "invalid_format", + * "job_id": int | None, + * "error_msg": str | None, + * "eta_seconds": int | None, + * } + * + * Frontend поведение: + * - "ready" → автоматически re-trigger POST /analyze + * - "fetching" → continue polling + * - "not_in_nspd" → показать пользователю «cad не найден в НСПД» + * - "failed" → retry button + retry-after message + * - "invalid_format" → подсказка формата + */ + get: operations["get_fetch_status_api_v1_parcels__cad_num__fetch_status_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/forecast": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Parcel Forecast + * @description Read-only fetch §22-форсайта участка (3b-ii, #995). + * + * Клиент поллит после POST /analyze (тот fire-and-forget enqueue'ит фон-таску, + * считающую §22 SiteFinderReport ~30-180s). Читает ПОСЛЕДНИЙ ран schema_version + * "1.0" (§22-форсайт, НЕ analyze-1.0 inline-dict) через schema-filtered + * `latest_run_for` (3b-i seam). + * + * Returns: + * • 200 {"status": "ready", "run_id", "created_at", "report"} — §22 готов + * (report = SiteFinderReport.as_dict() из run.result). + * • 202 {"status": "pending"} — ещё не посчитан, клиент продолжает поллить. + * + * Graceful: нет рана → 202 pending (НЕ 404/500 на happy "not yet" path); + * ошибка БД → 202 pending + warning (клиент ретраит, не видит 500). + */ + get: operations["get_parcel_forecast_api_v1_parcels__cad_num__forecast_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/forecast/export": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Export Parcel Forecast + * @description Экспорт §22-форсайта — Markdown / JSON / DOCX / PPTX (файл) или TG-сводка (#959). + * + * Читает ПОСЛЕДНИЙ §22-ран schema_version "1.0" (тот же блоб, что отдаёт GET + * /{cad}/forecast inline) и отдаёт его в нужной форме. + * • format=md → `render_report_markdown(run.result)` — attachment (.md, чистый Markdown). + * • format=json → сырой `run.result` — attachment (.json, download-вариант inline-чтения). + * • format=docx → `render_report_docx(run.result)` — attachment (.docx, Word-документ, + * зеркало содержания md/pdf; python-docx). + * • format=pptx → `render_report_pptx(run.result)` — attachment (.pptx, презентация, + * сжатое зеркало содержания docx; python-pptx). + * • format=tg → `render_report_telegram_summary(run.result)` — КРАТКАЯ plain-text сводка + * для копипаста в Telegram, INLINE (без Content-Disposition: это сниппет читать/копировать + * и отправить, а не файл-download). + * + * В отличие от read-only поллинга GET /{cad}/forecast (нет рана → 202 pending), это + * export-эндпоинт: нет рана → 404 (graceful HTTPException «прогноз ещё не посчитан», + * НЕ 500). 3-сегментный путь (`/{cad}/forecast/export`) не конфликтует с 2-сегментными + * `/{cad}/forecast`, `/{cad}/runs`, `/runs/{run_id}` и 1-сегментным `/{parcel_id}`. + * + * Args: + * cad_num: кадастровый номер участка (в имени файла `:` → `_`). + * format: "md" (default) | "json" | "tg" | "docx" | "pptx". + * + * Returns: + * Response — для md/json/docx/pptx attachment с Content-Disposition (имя + * `gendesign_forecast__.`); для tg — inline text/plain сводка. + */ + get: operations["export_parcel_forecast_api_v1_parcels__cad_num__forecast_export_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/analyze": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Analyze Parcel + * @description Анализ участка: близость к социалке + district context + конкуренты. + * + * Порядок поиска геометрии: cad_quarters_geom → cad_buildings → cad_parcels_geom. + * + * Issue #93 — Graceful fallback при отсутствии geometry: + * - Не возвращаем 404 сразу. Вместо: enqueue NSPD on-demand fetch, ждём + * inline до _INLINE_FETCH_WAIT_S (~15с). Если за это время геометрия + * появилась в БД — продолжаем analyze (fast path). + * - Иначе → 202 Accepted + {status, job_id, eta_seconds} для polling + * через GET /fetch-status. + * - Дедупликация (через `find_active_on_demand_job`): параллельные запросы + * на тот же cad → один Celery job, оба клиента ждут. + * + * §22-форсайт (3b-ii, #995): после успешного persist рана best-effort enqueue'им + * `forecast_site_finder_report.delay(cad_num, horizon, ...)` — fire-and-forget, + * провал Celery/Redis НЕ валит ответ (он уже успешен). `horizon` ∈ {6, 12, 18, 24} + * (ТЗ §12.1; иначе 422). Результат добавляется в ответ как `forecast` stub (additive); сам + * §22-отчёт клиент забирает через GET /{cad}/forecast (202 pending → 200 ready). + */ + post: operations["analyze_parcel_api_v1_parcels__cad_num__analyze_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/connection-points": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Точки подключения к инженерным сетям + охранные зоны (issue #115) + * @description Инженерные структуры (ТП, ЦТП, ЛЭП) и охранные зоны коммуникаций вблизи участка. + * + * Источник данных: nspd_quarter_dumps (НСПД cat 36328 / 37578). + * Если дамп для квартала ещё не загружен → dump_available=false, + * пустые массивы (harvest запускается автоматически). + * Если участок не найден в БД (нет geom) → 404. + * + * Query params: + * radius_m: радиус поиска инженерных структур, 50–2000 м (default 500). + */ + get: operations["get_parcel_connection_points_api_v1_parcels__cad_num__connection_points_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/isochrones": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Isochrones + * @description Изохроны доступности от центроида участка через OpenRouteService. + * + * Modes: foot-walking | cycling-regular | driving-car. + * times_min — список минут (1-60), например ?times_min=10×_min=15. + * Возвращает GeoJSON FeatureCollection для рендера на карте. + */ + get: operations["get_isochrones_api_v1_parcels__cad_num__isochrones_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/poi-score": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * POI weighted top-7 (B6) + * @description Вернуть top-7 ближайших POI для участка. + * + * decay=straight (default, B6): weight = (1 / (distance_m + 100)) * category_weight. + * decay=routing (#41): время пешком (ORS /matrix) + per-category radius (минут) + + * per-category decay-кривая. За порогом доступности категории POI выбывает. + * ORS недоступен → graceful fallback на straight-line оценку времени. + * + * POI берутся из osm_poi_ekb в заданном радиусе (default 2000м), отсортированы + * по weight DESC. + */ + get: operations["get_poi_score_api_v1_parcels__cad_num__poi_score_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/competitors": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Parcel Competitors + * @description Активные конкуренты ЖК в радиусе от участка (Issue #112). + * + * Возвращает список ЖК из domrf_kn_objects в радиусе radius_km от центроида + * участка с рассчитанным velocity_per_month за указанный time_window. + */ + post: operations["get_parcel_competitors_api_v1_parcels__cad_num__competitors_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/best-layouts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Parcel Best Layouts + * @description Top layouts (rooms × area_bin) у конкурентов с ranking по velocity. + * + * Issue #113 Phase 2.1: "Анализ лучших планировок конкурентов → ТЗ на проектирование". + * Reads from mv_layout_velocity (auto-populated via objective_corpus_room_month + * × objective_complex_mapping). + */ + post: operations["get_parcel_best_layouts_api_v1_parcels__cad_num__best_layouts_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/best-layouts/pdf": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Parcel Best Layouts Pdf + * @description ТЗ на проектирование (PDF) — генерируется из /best-layouts данных. + * + * Issue #113 Phase 2.1: data-driven unit-mix recommendation для тендера. + */ + post: operations["get_parcel_best_layouts_pdf_api_v1_parcels__cad_num__best_layouts_pdf_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/parcels/{cad_num}/snapshot.pdf": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * 1-page PDF snapshot участка (НСПД + POI + конкуренты) + * @description Генерирует одностраничный PDF-снимок участка (A4). + * + * Содержимое: + * - Header: кадастровый номер, адрес, район, площадь + * - Block 1: 5 KPI (площадь, кадастровая стоимость, категория, ВРИ, дата обновления) + * - Block 2: Топ-7 POI по взвешенному баллу (из osm_poi_ekb, радиус 1 км) + * - Block 3: Топ-5 конкурентов (из domrf_kn_objects, радиус 3 км) + * - Footer: gendsgn.ru + дата генерации + * + * Не является официальной выпиской ЕГРН — только аналитические данные НСПД. + * Генерация <2 сек. Открывается в Adobe Reader / Chrome. + */ + get: operations["parcel_snapshot_pdf_api_v1_parcels__cad_num__snapshot_pdf_get"]; put?: never; post?: never; delete?: never; @@ -344,6 +759,131 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/analytics/object/{obj_id}/full": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Object Full Detail + * @description Extended object info — все 30+ новых полей (22begh: specs/accessibility/metro/scores). + */ + get: operations["object_full_detail_api_v1_analytics_object__obj_id__full_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/analytics/object/{obj_id}/quartirography": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Object Quartirography + * @description Квартирография ЖК: группировка flats по комнатности с count/area/price. + */ + get: operations["object_quartirography_api_v1_analytics_object__obj_id__quartirography_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/analytics/object/{obj_id}/checks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Object Checks + * @description 6 проверок «Проверено на наш.дом.рф» (22f). + */ + get: operations["object_checks_api_v1_analytics_object__obj_id__checks_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/analytics/object/{obj_id}/documents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Object Documents + * @description PDF документы из domrf_kn_documents (22i): декларации, разрешения, отчётность. + */ + get: operations["object_documents_api_v1_analytics_object__obj_id__documents_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/analytics/object/{obj_id}/buildings": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Object Buildings + * @description Список кадастровых зданий ЖК из cad_buildings. + */ + get: operations["object_buildings_api_v1_analytics_object__obj_id__buildings_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/analytics/velocity-alerts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Velocity Alerts + * @description ЖК с statistical-significance провалом темпа продаж vs своя история. + * + * Источник: domrf_kn_sale_graph (monthly realised). Детерминированная + * z-score детекция: recent 3-mo mean vs trailing prior-period mean. + * Алерт когда z <= -min_zscore И drop_pct <= -min_drop_pct (двойной gate + * снижает false-positive на шумных low-volume ЖК). severity high/medium. + */ + get: operations["velocity_alerts_api_v1_analytics_velocity_alerts_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/analytics/prinzip/insights": { parameters: { query?: never; @@ -425,6 +965,33 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/market/ddu-indicator": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Ddu Indicator + * @description ARN-style ценовой индикатор первичного рынка (ДДУ) по кварталам × площади. + * + * Принимает то же body, что ARN/НСПД (camelCase). MVP: period_type=Q, + * calc methods 1 (базисный) / 2 (предыдущий), субъект 66. Неподдержанные + * ARN-опции (M/H/Y, method=3, иные субъекты, cad_quarter) не отвергаются — + * отражаются в `notes`. Источник: mv_ddu_price_indicator (migration 152). + * + * Killer-USP: «у нас первичка, у НСПД вторичка» — обе нужны девелоперу. + */ + post: operations["ddu_indicator_api_v1_market_ddu_indicator_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/admin/scrape/kn": { parameters: { query?: never; @@ -554,6 +1121,101 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/admin/scrape/noise-sync": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Trigger Noise Sync + * @description Manual trigger для синхронизации OSM шумовых источников ЕКБ из Overpass API. + * + * Обычно запускается еженедельно через beat (понед 3:30 МСК). + * Этот endpoint — для ad-hoc запуска (например после первого деплоя или + * после добавления миграции 84_osm_noise_sources_ekb). + */ + post: operations["trigger_noise_sync_api_v1_admin_scrape_noise_sync_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/scrape/nspd/harvest-quarter": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Trigger Harvest Quarter + * @description Manual trigger одного quarter harvest (для testing / конкретного квартала). + * + * Запускает harvest_quarter Celery task и возвращает task_id. + * Для получения результата — poll через Celery inspect или нождите строку + * в nspd_quarter_dumps WHERE quarter_cad = payload.quarter_cad. + */ + post: operations["trigger_harvest_quarter_api_v1_admin_scrape_nspd_harvest_quarter_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/scrape/pzz-sync": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Trigger Pzz Sync + * @description Manual trigger для импорта ПЗЗ территориальных зон ЕКБ из Росреестр PKK6. + * + * Запускать после деплоя миграции 85_pzz_zones_ekb.sql и при необходимости + * обновить данные о зонировании (обычно раз в несколько месяцев). + */ + post: operations["trigger_pzz_sync_api_v1_admin_scrape_pzz_sync_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/scrape/poi-sync": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Trigger Poi Sync + * @description Manual trigger для синхронизации OSM POI ЕКБ из Overpass API. + * + * Обычно запускается еженедельно через beat (понед 3:00 МСК). + * Этот endpoint — для ad-hoc запуска (например после первого деплоя + * или при создании нового тестового участка). + */ + post: operations["trigger_poi_sync_api_v1_admin_scrape_poi_sync_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/admin/scrape/objective": { parameters: { query?: never; @@ -610,7 +1272,7 @@ export interface paths { * * В отличие от ETL (POST /objective) — это вызовы api.objctv.ru напрямую, * с парсингом payload через app.workers.tasks.scrape_objective. - * Полезно когда нужно расширить покрытие за пределы Антоновского ЕКБ + * Полезно когда нужно расширить покрытие за пределов Антоновского ЕКБ * (Свердл.обл целиком + Челябинск + Тюмень + Пермь). */ post: operations["trigger_objective_sync_our_api_v1_admin_scrape_objective_sync_our_post"]; @@ -698,6 +1360,31 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/admin/scrape/geo/bulk": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Bulk Enqueue Geo + * @description Разбить pending cad-номера на N чанков и запустить N×len(thematic_ids) geo-jobs. + * + * source=rosreestr_pending: для каждого thematic_id выбирает cad из rosreestr_deals + * которых нет в соответствующей geo-таблице. + * source=all_in_region: UNION из rosreestr_deals + cad_buildings + complexes — один + * набор для всех thematic_ids (каждый thematic_id обрабатывает весь список). + */ + post: operations["bulk_enqueue_geo_api_v1_admin_scrape_geo_bulk_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/admin/scrape/geo/jobs/{job_id}/cancel": { parameters: { query?: never; @@ -738,6 +1425,29 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/admin/scrape/newbuilding-crossload": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Trigger Newbuilding Crossload + * @description Ручной запуск cross-load ETL tradein.houses → newbuilding_listings (#976). + * + * Обычно запускается ночью через beat (03:30 МСК). + * Если TRADEIN_DATABASE_URL не задан в settings — возвращает 503. + */ + post: operations["trigger_newbuilding_crossload_api_v1_admin_scrape_newbuilding_crossload_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/admin/scrape/all/runs": { parameters: { query?: never; @@ -781,6 +1491,37 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/admin/scrape/freshness": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Scrape Freshness + * @description Свежесть данных по каждому источнику (для dashboard + alerting). + * + * Для каждого источника один проход по его run-таблице агрегирует: + * - last_success_at — max(COALESCE(finished_at, started_at)) WHERE status='done' + * - last_attempt_at — max(started_at) (или created_at fallback) по всем прогонам + * - last_status — status последнего прогона + * - objects_updated_24h / _7d — сумма run-счётчика по done-прогонам в окне + * - age_days — возраст last_success в днях (NULL если успехов не было) + * - freshness — fresh / stale / critical / unknown (по порогам источника) + * - status — ok / stale / failed (для UI-светофора) + * + * `overall_status` агрегирует худший статус по критичным источникам. + */ + get: operations["scrape_freshness_api_v1_admin_scrape_freshness_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/admin/scrape/runs": { parameters: { query?: never; @@ -798,6 +1539,111 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/admin/scrape/ekburg-permits": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Trigger Ekburg Permits + * @description Manual trigger для синхронизации РНС/РВЭ ЕКБ из екатеринбург.рф (xlsx). + * + * Обычно запускается раз в месяц через beat (1-го числа 02:00 UTC). + * Этот endpoint — для ad-hoc запуска (после первого деплоя или для smoke-теста). + * + * - year=None → refresh_all: скачивает 5 xlsx (2022-2026), ~3-8 мин. + * - year=2026 → refresh_year(2026): один файл, ~1 мин. + */ + post: operations["trigger_ekburg_permits_api_v1_admin_scrape_ekburg_permits_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/scrape/kn-catalog-objects": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Trigger Kn Catalog Objects + * @description Manual trigger для catalog-OBJECT scraper (заполняет wall_type, energy_eff, + * ceiling_height_m, parking_*, playground_*, scores из SSR __NEXT_DATA__). + * + * Beat schedule: Tuesday 04:00 UTC, batch 300/run. Этот endpoint — для ad-hoc + * запуска (smoke-тест после деплоя или повторный pass для свежесозданных + * объектов до next beat fire). + * + * - max_objects=None → дефолтный лимит таска (300). + * - max_objects=3 → smoke-тест. + * - force=True → "Загрузить все": игнорирует skip-today, грузит всё подряд. + */ + post: operations["trigger_kn_catalog_objects_api_v1_admin_scrape_kn_catalog_objects_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/jobs/settings": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Job Settings + * @description Список всех job_settings (все job_type). + */ + get: operations["list_job_settings_api_v1_admin_jobs_settings_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/jobs/settings/{job_type}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Job Setting + * @description Настройки одного job_type. Возвращает fallback defaults если строки нет в БД. + */ + get: operations["get_job_setting_api_v1_admin_jobs_settings__job_type__get"]; + /** + * Update Job Setting + * @description PATCH-style update: передавать только изменяемые поля. + * + * После изменения cron_schedule нужен restart beat-контейнера. + * Остальные поля (rate_ms, extra_config, enabled) применяются динамически. + * + * cron_schedule='' устанавливает NULL (manual-only режим). + * rate_ms=0 устанавливает NULL (no throttle). + */ + put: operations["update_job_setting_api_v1_admin_jobs_settings__job_type__put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/admin/leads/": { parameters: { query?: never; @@ -915,6 +1761,67 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/admin/site-finder/weight-profiles": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List User Profiles + * @description Список всех weight profiles для заданного user_id. Default-профиль первым. + * + * При include_system=true добавляются системные presets (user_id='__system__'): + * Эконом, Комфорт, Бизнес — готовые шаблоны весов POI. + */ + get: operations["list_user_profiles_api_v1_admin_site_finder_weight_profiles_get"]; + put?: never; + /** + * Create User Profile + * @description Создать новый weight profile. + * + * При is_default=True снимает is_default у всех существующих профилей + * пользователя в одной транзакции. + */ + post: operations["create_user_profile_api_v1_admin_site_finder_weight_profiles_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/site-finder/weight-profiles/{profile_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get User Profile + * @description Получить один weight profile по id (scoped к user_id). + */ + get: operations["get_user_profile_api_v1_admin_site_finder_weight_profiles__profile_id__get"]; + /** + * Update User Profile + * @description PATCH-style обновление weight profile. + * + * Передавай только изменяемые поля. Не переданные поля остаются прежними. + * При is_default=True снимает is_default у остальных профилей пользователя. + */ + put: operations["update_user_profile_api_v1_admin_site_finder_weight_profiles__profile_id__put"]; + post?: never; + /** + * Delete User Profile + * @description Удалить weight profile. 404 если не найден. + */ + delete: operations["delete_user_profile_api_v1_admin_site_finder_weight_profiles__profile_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/photos/{obj_id}/{file_id}": { parameters: { query?: never; @@ -932,6 +1839,534 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/custom-pois": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Pois + * @description Список кастомных POI пользователя. + * + * Если parcel_cad задан — возвращает global (parcel_cad IS NULL) + parcel-specific POI. + */ + get: operations["list_pois_api_v1_custom_pois_get"]; + put?: never; + /** + * Create Poi + * @description Создать кастомную POI точку. + * + * Возвращает 201 с созданным объектом. X-Session-Id response header содержит + * актуальный session_id (пригодится frontend для localStorage). + */ + post: operations["create_poi_api_v1_custom_pois_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/custom-pois/{poi_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete Poi + * @description Удалить кастомную POI. 404 если не найдена. + */ + delete: operations["delete_poi_api_v1_custom_pois__poi_id__delete"]; + options?: never; + head?: never; + /** + * Patch Poi + * @description Обновить поля кастомной POI (PATCH-style). 404 если не найдена. + */ + patch: operations["patch_poi_api_v1_custom_pois__poi_id__patch"]; + trace?: never; + }; + "/api/v1/locations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Locations Endpoint + * @description Все локации (районы) с district-level индексами. + */ + get: operations["list_locations_endpoint_api_v1_locations_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/locations/{district_name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Location Endpoint + * @description Вернуть одну локацию по district_name. 404 если не найдена. + */ + get: operations["get_location_endpoint_api_v1_locations__district_name__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/insights": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Insights Endpoint + * @description Список insight'ов с фильтрами + пагинацией. + */ + get: operations["list_insights_endpoint_api_v1_insights_get"]; + put?: never; + /** + * Create Insight Endpoint + * @description Создать insight. created_by = X-Authenticated-User. + */ + post: operations["create_insight_endpoint_api_v1_insights_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/insights/{insight_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Insight Endpoint + * @description Вернуть один insight. 404 если не найден. + */ + get: operations["get_insight_endpoint_api_v1_insights__insight_id__get"]; + /** + * Update Insight Endpoint + * @description Partial update insight. 404 если не найден. + */ + put: operations["update_insight_endpoint_api_v1_insights__insight_id__put"]; + post?: never; + /** + * Delete Insight Endpoint + * @description Удалить insight. 404 если не найден. + */ + delete: operations["delete_insight_endpoint_api_v1_insights__insight_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/own-projects": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Own Projects Endpoint + * @description Список планируемых проектов с фильтрами + пагинацией. + */ + get: operations["list_own_projects_endpoint_api_v1_own_projects_get"]; + put?: never; + /** + * Create Own Project Endpoint + * @description Создать планируемый проект. created_by = X-Authenticated-User. + */ + post: operations["create_own_project_endpoint_api_v1_own_projects_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/own-projects/{project_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Own Project Endpoint + * @description Вернуть один планируемый проект. 404 если не найден. + */ + get: operations["get_own_project_endpoint_api_v1_own_projects__project_id__get"]; + /** + * Update Own Project Endpoint + * @description Partial update планируемого проекта. 404 если не найден. + */ + put: operations["update_own_project_endpoint_api_v1_own_projects__project_id__put"]; + post?: never; + /** + * Delete Own Project Endpoint + * @description Удалить планируемый проект. 404 если не найден. + */ + delete: operations["delete_own_project_endpoint_api_v1_own_projects__project_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/cadastre/jobs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Cadastre Jobs + * @description Список последних cadastre harvest jobs. + */ + get: operations["list_cadastre_jobs_api_v1_admin_cadastre_jobs_get"]; + put?: never; + /** + * Create Cadastre Job + * @description Создать bulk cadastre harvest job и поставить в очередь. + * + * scope=pilot → первые 50 ЕКБ кварталов из cad_quarters_geom + * scope=ekb_full → все ~2408 ЕКБ кварталов + * scope=manual_list → body.quarters (явный список) + * + * Возвращает: {job_id, targets_total, estimate_minutes} + */ + post: operations["create_cadastre_job_api_v1_admin_cadastre_jobs_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/cadastre/jobs/{job_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Cadastre Job + * @description Детали одного cadastre harvest job. + */ + get: operations["get_cadastre_job_api_v1_admin_cadastre_jobs__job_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/cadastre/jobs/{job_id}/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel Cadastre Job + * @description Отменить job. Воркеры увидят status='cancelled' при следующей итерации и skip. + */ + post: operations["cancel_cadastre_job_api_v1_admin_cadastre_jobs__job_id__cancel_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/cadastre/jobs/{job_id}/resume": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Resume Cadastre Job + * @description Возобновить paused/failed job. Re-enqueue enqueue_cadastre_harvest. + */ + post: operations["resume_cadastre_job_api_v1_admin_cadastre_jobs__job_id__resume_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/etl/objective-backfill": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Run Objective Backfill + * @description Запустить backfill objective_complex_mapping + опционально REFRESH MV. + * + * Ищет DOM.РФ комплексы (is_ekb=true) без mapping и применяет fuzzy match + * к project_name из objective_corpus_room_month через pg_trgm similarity. + * + * Режим v1 (default, ?v2=false): + * - score >= 0.85 (AUTO_ACCEPT_THRESHOLD): auto-insert, match_method='fuzzy_trgm' + * - score >= 0.6 (REVIEW_THRESHOLD) и < 0.85: только счётчик review_queue + * + * Режим v2 (?v2=true, task #44 coverage expansion): + * - score >= 0.80 (AUTO_ACCEPT_THRESHOLD_V2): auto-insert, match_method='fuzzy_v2' + * - is_reviewed=false — требует ручной проверки (false positives вероятны) + * - Целевой прирост: +~47-80 строк, coverage 8.5% → ~17% + * + * Returns dict: + * auto_accepted: сколько строк вставлено + * review_queue: сколько кандидатов ниже порога auto-accept + * skipped: ON CONFLICT + ошибки + * mv_rows_after_refresh: строк в MV после REFRESH (0 если refresh_mv=False) + * threshold_used: фактический порог (float) + * match_method_used: match_method в БД (str) + */ + post: operations["run_objective_backfill_api_v1_admin_etl_objective_backfill_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/admin/etl/nspd-denorm-backfill": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Run Nspd Denorm Backfill + * @description Запустить Celery backfill: denormalize nspd_quarter_dumps → nspd_parcels/nspd_buildings. + * + * Задача идемпотентна (ON CONFLICT DO UPDATE). Безопасно запускать повторно. + * Возвращает Celery task_id — статус через /flower или celery inspect. + * + * Args: + * limit: если задан — обработать только первые N кварталов ORDER BY quarter_cad. + */ + post: operations["run_nspd_denorm_backfill_api_v1_admin_etl_nspd_denorm_backfill_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/trade-in/estimate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Estimate + * @description MOCK реализация оценки квартиры для Trade-In. + * + * TODO TI-1b: заменить на реальный SQL aggregation из objective_lots + * после OBJ-1/2 merge (issue #314). + */ + post: operations["estimate_api_v1_trade_in_estimate_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/trade-in/estimate/{estimate_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Estimate + * @description Получить сохранённую оценку по UUID (для генерации PDF). + * + * Возвращает 404 если оценка не найдена или TTL истёк. + */ + get: operations["get_estimate_api_v1_trade_in_estimate__estimate_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/trade-in/estimate/{estimate_id}/pdf": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Estimate Pdf + * @description Скачать 4-страничный PDF-отчёт для оценки trade-in. + * + * Возвращает application/pdf attachment. + * 404 — оценка не найдена. + * 410 — оценка просрочена (TTL 24ч). + */ + get: operations["estimate_pdf_api_v1_trade_in_estimate__estimate_id__pdf_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/landing/stats": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Landing Stats + * @description 5 KPI для hero-секции лендинга. + * + * Поля ответа: + * - zk_total: количество ЖК DOM.РФ в ЕКБ + * - deals_total: суммарное кол-во ДДУ-сделок в индексе Росреестра + * - price_coverage_pct: % объектов objective_lots с ценой + * - mapping_coverage_pct: % маппинга objective → domrf_kn_objects (ЕКБ) + * - last_data_update: дата последнего обновления данных (YYYY-MM-DD) + * - paradox: строка-парадокс портфеля для hero CTA + * - stale: true если данные недоступны (DB ошибка или пустая БД) + */ + get: operations["landing_stats_api_v1_landing_stats_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/pilot/request": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create Pilot Request + * @description Сохраняет заявку на пилот в pilot_requests. + */ + post: operations["create_pilot_request_api_v1_pilot_request_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/users/me/recent-parcels": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Recent Parcels + * @description STUB. Real impl после NextAuth (Wave 3) — будет читать parcel_analysis_history table. + */ + get: operations["get_recent_parcels_api_v1_users_me_recent_parcels_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Me + * @description Return the current user's RBAC scope (role + allowed/deny paths). + */ + get: operations["me_api_v1_me_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/ping": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Ping */ + get: operations["ping_api_v1_ping_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/health": { parameters: { query?: never; @@ -953,6 +2388,728 @@ export interface paths { export type webhooks = Record; export interface components { schemas: { + /** AggregatedEstimate */ + AggregatedEstimate: { + /** + * Estimate Id + * Format: uuid + */ + estimate_id: string; + /** Median Price Rub */ + median_price_rub: number; + /** Range Low Rub */ + range_low_rub: number; + /** Range High Rub */ + range_high_rub: number; + /** Median Price Per M2 */ + median_price_per_m2: number; + /** + * Confidence + * @enum {string} + */ + confidence: "low" | "medium" | "high"; + /** N Analogs */ + n_analogs: number; + /** Period Months */ + period_months: number; + /** Analogs */ + analogs: components["schemas"]["AnalogLot"][]; + /** Actual Deals */ + actual_deals: components["schemas"]["AnalogLot"][]; + /** + * Expires At + * Format: date-time + */ + expires_at: string; + }; + /** AnalogLot */ + AnalogLot: { + /** Address */ + address: string; + /** Area M2 */ + area_m2: number; + /** Rooms */ + rooms: number; + /** Floor */ + floor: number | null; + /** Total Floors */ + total_floors: number | null; + /** Price Rub */ + price_rub: number; + /** Price Per M2 */ + price_per_m2: number; + /** Listing Date */ + listing_date: string | null; + /** Days On Market */ + days_on_market: number | null; + /** Photo Url */ + photo_url?: string | null; + }; + /** + * AnalysisRunDetail + * @description Полная строка одного рана (включая `result`) для re-open / детального view. + * + * `result` — JSONB-блоб analyze (schema_version "analyze-1.0", форма ParcelAnalysis) + * ИЛИ §22 SiteFinderReport ("1.0") — отдаём как есть (dict), форму не навязываем + * (модель истории, не контракт analyze). Источник: repository.get_run (full SELECT). + */ + AnalysisRunDetail: { + /** Id */ + id: number; + /** Cad Num */ + cad_num: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Status */ + status?: string | null; + /** Schema Version */ + schema_version?: string | null; + /** District */ + district?: string | null; + /** Confidence */ + confidence?: string | null; + /** Created By */ + created_by?: string | null; + /** Advisory */ + advisory?: boolean | null; + /** Params */ + params?: { + [key: string]: unknown; + } | null; + /** Segment */ + segment?: { + [key: string]: unknown; + } | null; + /** Result */ + result?: { + [key: string]: unknown; + } | null; + }; + /** + * AnalysisRunListResponse + * @description Ответ GET /parcels/{cad_num}/runs — список (пустой, если ранов нет). + */ + AnalysisRunListResponse: { + /** Runs */ + runs: components["schemas"]["AnalysisRunSummary"][]; + }; + /** + * AnalysisRunSummary + * @description Облегчённая карточка одного рана для списка истории по участку. + * + * БЕЗ тяжёлого `result`-блоба (~90 ключей) — только метаданные для UI-списка + * «история анализов». Источник колонок: repository.list_runs_for (LIGHT SELECT). + */ + AnalysisRunSummary: { + /** Id */ + id: number; + /** Cad Num */ + cad_num: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Status */ + status?: string | null; + /** Schema Version */ + schema_version?: string | null; + /** District */ + district?: string | null; + /** Confidence */ + confidence?: string | null; + /** Created By */ + created_by?: string | null; + }; + /** + * AnalyzeRequest + * @description Опциональное тело запроса POST /analyze. + * + * Позволяет передать inline POI-веса напрямую в запросе без сохранения + * профиля. Если задан weights — применяется с наивысшим приоритетом + * (выше profile_id и user default). + */ + AnalyzeRequest: { + /** + * Weights + * @description Inline POI weights override (категория → weight). Если задан — применяется к scoring, без обязательного profile save. Validated против ALLOWED_CATEGORIES + MIN_WEIGHT/MAX_WEIGHT. + */ + weights?: { + [key: string]: number; + } | null; + }; + /** + * AnalyzeResponse + * @description Типизированный контракт ответа POST /parcels/{cad_num}/analyze (#992). + * + * КРИТИЧЕСКИЕ инварианты (EPIC 961 — наивысший blast radius PR): + * + * 1) `extra="allow"` — ЛЮБОЙ ключ из `result_payload` (parcels.py), НЕ перечисленный + * здесь явно, ПОПАДАЕТ в ответ без изменений (Pydantic v2: уходит в + * __pydantic_extra__ → включается в model_dump → FastAPI отдаёт в body). + * Это backstop против silent-drop: даже если новый ключ добавят в payload и + * забудут сюда — frontend его получит. Без этого response_model молча выкинул + * бы неперечисленные ключи и сломал Site Finder. + * + * 2) ВСЕ поля Optional с дефолтом — модель НИЧЕГО не требует. Эндпоинт имеет + * graceful 202 fetch-stub ({status:"fetching", job_id, eta_seconds, message}, + * без score/competitors/...). Если бы тут были required-поля, FastAPI словил бы + * ValidationError → 500 при сериализации 202-стаба через response_model. Все + * поля nullable → 202-стаб проходит без ошибок (недостающие → null, additive, + * frontend ветвится по HTTP-коду 202, не по телу — useSiteAnalysis.ts). + * + * Перечень ниже документирует ~90 известных top-level ключей (зеркало + * frontend ParcelAnalysis + result_payload) для OpenAPI / codegen — но НЕ + * ограничивает ответ (extra="allow" + Optional). Источник истины формы — всё + * ещё result_payload в parcels.py; модель его описывает, а не сужает. + */ + AnalyzeResponse: { + /** Cad Num */ + cad_num?: string | null; + /** Source */ + source?: string | null; + /** Geom Geojson */ + geom_geojson?: { + [key: string]: unknown; + } | null; + /** District */ + district?: { + [key: string]: unknown; + } | null; + /** Score */ + score?: number | null; + /** Score Without Center */ + score_without_center?: number | null; + /** Score Label */ + score_label?: string | null; + /** Score Max Reference */ + score_max_reference?: number | null; + /** Score Explanation */ + score_explanation?: string | null; + /** Score Breakdown */ + score_breakdown?: { + [key: string]: unknown; + } | null; + /** Score Breakdown Detailed */ + score_breakdown_detailed?: + | { + [key: string]: unknown; + }[] + | null; + /** Score Top 3 Positives */ + score_top_3_positives?: + | { + [key: string]: unknown; + }[] + | null; + /** Score Top 3 Negatives */ + score_top_3_negatives?: + | { + [key: string]: unknown; + }[] + | null; + /** Score By Group */ + score_by_group?: + | { + [key: string]: unknown; + }[] + | null; + /** Poi Count */ + poi_count?: number | null; + /** Location */ + location?: { + [key: string]: unknown; + } | null; + /** Competitors */ + competitors?: + | { + [key: string]: unknown; + }[] + | null; + /** Market Pulse */ + market_pulse?: { + [key: string]: unknown; + } | null; + /** Market Avg Price Per M2 */ + market_avg_price_per_m2?: number | null; + /** Market Data Coverage Pct */ + market_data_coverage_pct?: number | null; + /** Pipeline 24Mo */ + pipeline_24mo?: { + [key: string]: unknown; + } | null; + /** Velocity */ + velocity?: { + [key: string]: unknown; + } | null; + /** Market Trend */ + market_trend?: { + [key: string]: unknown; + } | null; + /** Market Price */ + market_price?: { + [key: string]: unknown; + } | null; + /** Noise */ + noise?: { + [key: string]: unknown; + } | null; + /** Air Quality */ + air_quality?: { + [key: string]: unknown; + } | null; + /** Weather */ + weather?: { + [key: string]: unknown; + } | null; + /** Seasonal Weather */ + seasonal_weather?: { + [key: string]: unknown; + } | null; + /** Wind */ + wind?: { + [key: string]: unknown; + } | null; + /** Geology */ + geology?: { + [key: string]: unknown; + } | null; + /** Hydrology */ + hydrology?: { + [key: string]: unknown; + } | null; + /** Utilities */ + utilities?: { + [key: string]: unknown; + } | null; + /** Geotech Risk */ + geotech_risk?: { + [key: string]: unknown; + } | null; + /** Geometry Suitability */ + geometry_suitability?: { + [key: string]: unknown; + } | null; + /** Neighbors Summary */ + neighbors_summary?: { + [key: string]: unknown; + } | null; + /** Parcel Meta */ + parcel_meta?: { + [key: string]: unknown; + } | null; + /** Recent Permits In Quarter */ + recent_permits_in_quarter?: + | { + [key: string]: unknown; + }[] + | null; + /** Permits Summary */ + permits_summary?: { + [key: string]: unknown; + } | null; + /** Zoning */ + zoning?: { + [key: string]: unknown; + } | null; + /** Success Recommendation */ + success_recommendation?: { + [key: string]: unknown; + } | null; + /** Isochrones Available */ + isochrones_available?: boolean | null; + /** Confidence */ + confidence?: number | null; + /** Confidence Label */ + confidence_label?: string | null; + /** Confidence Breakdown */ + confidence_breakdown?: { + [key: string]: unknown; + } | null; + /** Confidence Caveats */ + confidence_caveats?: string[] | null; + /** Nspd Zoning */ + nspd_zoning?: { + [key: string]: unknown; + } | null; + /** Nspd Zouit Overlaps */ + nspd_zouit_overlaps?: + | { + [key: string]: unknown; + }[] + | null; + /** Nspd Engineering Nearby */ + nspd_engineering_nearby?: + | { + [key: string]: unknown; + }[] + | null; + /** Nspd Risk Zones */ + nspd_risk_zones?: components["schemas"]["RiskZone"][] | null; + /** Nspd Opportunity Parcels */ + nspd_opportunity_parcels?: + | components["schemas"]["OpportunityParcel"][] + | null; + /** Nspd Red Lines */ + nspd_red_lines?: components["schemas"]["RedLine"][] | null; + /** Nspd Dump */ + nspd_dump?: { + [key: string]: unknown; + } | null; + /** Gate Verdict */ + gate_verdict?: { + [key: string]: unknown; + } | null; + /** Weights Profile */ + weights_profile?: { + [key: string]: unknown; + } | null; + /** Custom Poi Score Items */ + custom_poi_score_items?: + | { + [key: string]: unknown; + }[] + | null; + /** Egrn */ + egrn?: { + [key: string]: unknown; + } | null; + /** Encumbrance */ + encumbrance?: { + [key: string]: unknown; + } | null; + /** Red Lines */ + red_lines?: { + [key: string]: unknown; + } | null; + /** Metro */ + metro?: { + [key: string]: unknown; + } | null; + /** District Price Per M2 Min */ + district_price_per_m2_min?: number | null; + /** District Price Per M2 Max */ + district_price_per_m2_max?: number | null; + /** District Price Per M2 Median */ + district_price_per_m2_median?: number | null; + /** District Price Sample Size */ + district_price_sample_size?: number | null; + /** Risks */ + risks?: { + [key: string]: unknown; + } | null; + /** Forecast */ + forecast?: { + [key: string]: unknown; + } | null; + /** Ird */ + ird?: { + [key: string]: unknown; + } | null; + /** Developer Attribution */ + developer_attribution?: { + [key: string]: unknown; + } | null; + /** Status */ + status?: string | null; + /** Job Id */ + job_id?: number | null; + /** Eta Seconds */ + eta_seconds?: number | null; + /** Message */ + message?: string | null; + } & { + [key: string]: unknown; + }; + /** + * BestLayoutsRequest + * @description Параметры запроса top-планировок в радиусе вокруг участка. + */ + BestLayoutsRequest: { + /** + * Radius Km + * @default 1 + */ + radius_km: number; + /** + * Time Window + * @default last_quarter + * @enum {string} + */ + time_window: "last_month" | "last_quarter" | "last_year"; + /** Filter Competitor Obj Ids */ + filter_competitor_obj_ids?: number[] | null; + /** Exclude Competitor Obj Ids */ + exclude_competitor_obj_ids?: number[]; + /** + * Min Velocity Per Month + * @default 0.5 + */ + min_velocity_per_month: number; + /** Obj Class Filter */ + obj_class_filter?: ("economy" | "comfort" | "business") | null; + /** Target Total Flats */ + target_total_flats?: number | null; + }; + /** + * BestLayoutsResponse + * @description Ответ /best-layouts endpoint'а. + */ + BestLayoutsResponse: { + /** Top Layouts */ + top_layouts: components["schemas"]["TopLayoutRow"][]; + recommendation_for_tz: components["schemas"]["LayoutTzRecommendation"]; + data_quality: components["schemas"]["LayoutDataQuality"]; + }; + /** + * BulkGeoEnqueueRequest + * @description Параметры для параллельного backfill geo по Свердловской обл. + */ + BulkGeoEnqueueRequest: { + /** + * Parallelism + * @default 5 + */ + parallelism: number; + /** + * Thematic Ids + * @description Список thematic_id: 1=parcel, 2=quarter, 5=building. Можно несколько. + * @default [ + * 2 + * ] + */ + thematic_ids: number[]; + /** + * Region Codes + * @description Список region кодов + * @default [ + * 66 + * ] + */ + region_codes: number[]; + /** + * Only Ddu + * @description True = только ДДУ (002001003000); False = все cad-кварталы + * @default false + */ + only_ddu: boolean; + /** + * Source + * @description rosreestr_pending — cad-номера из rosreestr_deals которых нет в geo-таблице; all_in_region — UNION всех cad из rosreestr_deals + cad_buildings + complexes + * @default rosreestr_pending + */ + source: string; + }; + /** + * ChatAskRequest + * @description Запрос вопроса по §22-форсайту участка. + * + * `cad_num` + `message` обязательны. `intent` — опциональный явный intent (если задан, + * роутер его уважает и не угадывает по тексту). `history` — опциональная история + * диалога: принимается для совместимости контракта, но в Step 1 НЕ используется + * (LLM-контекст — Step 2); длину кэпируем (последние _HISTORY_MAX_TURNS ходов). + */ + ChatAskRequest: { + /** + * Cad Num + * @description Кадастровый номер участка + */ + cad_num: string; + /** + * Message + * @description Вопрос пользователя (RU, free-form) + */ + message: string; + /** @description Явный intent — если задан, роутер его уважает (не угадывает по тексту) */ + intent?: components["schemas"]["ChatIntent"] | null; + /** + * History + * @description История диалога (Step 1: принимается, НЕ используется; LLM-контекст — Step 2) + */ + history?: components["schemas"]["ChatTurn"][] | null; + }; + /** + * ChatAskResponse + * @description Ответ чата по отчёту участка (детерминированный, шаблонный RU-текст). + * + * `answer` — готовый RU-ответ с числами ВЕРБАТИМ из отчёта (никогда не фабрикуем) + + * advisory-оговорка. `grounded_in` — привязка к рану/секциям (None если pending). + * `llm_used` ВСЕГДА False в Step 1. `fallback_reason` — почему ответ деградировал + * (отчёта нет / intent не распознан / секция отсутствует). `advisory` зеркалит + * report.advisory (пока всегда True). `report_status` — 'ready' (отчёт прочитан) или + * 'pending' (отчёта ещё нет — запустите анализ участка). + */ + ChatAskResponse: { + /** + * Answer + * @description Готовый RU-ответ (числа из отчёта + advisory-оговорка) + */ + answer: string; + /** @description Привязка к рану/схеме/секциям (None если отчёта ещё нет) */ + grounded_in?: components["schemas"]["GroundedIn"] | null; + /** + * Llm Used + * @description Использовался ли LLM (Step 1 — ВСЕГДА False; LLM — Step 2) + * @default false + */ + llm_used: boolean; + /** + * Fallback Reason + * @description Почему ответ деградировал (нет отчёта / intent unknown / нет секции) + */ + fallback_reason?: string | null; + /** + * Advisory + * @description Отчёт советующий (зеркало report.advisory — пока всегда True) + * @default true + */ + advisory: boolean; + /** + * Report Status + * @description Статус отчёта: 'ready' (прочитан) | 'pending' (ещё нет) + * @enum {string} + */ + report_status: "ready" | "pending"; + }; + /** + * ChatIntent + * @description Поддерживаемые типы вопросов по отчёту (детерминированный роутинг §22-чата). + * + * StrEnum → значение члена и есть строка (сериализуется как plain-строка в JSON- + * ответе, сравнимо со строкой). `unknown` — graceful-фолбэк: вопрос не распознан, + * отдаём меню поддерживаемых тем. + * @enum {string} + */ + ChatIntent: + | "summary" + | "what_to_build" + | "why_forecast" + | "risks" + | "scenarios" + | "unknown"; + /** + * ChatTurn + * @description Один ход диалога (роль + текст). Принимается, но в Step 1 НЕ используется. + * + * Форвардится в LLM-контекст только на Step 2 (#957). frozen — это входной факт. + */ + ChatTurn: { + /** + * Role + * @description Роль хода: 'user' | 'assistant' + */ + role: string; + /** + * Content + * @description Текст хода диалога + */ + content: string; + }; + /** Competitor */ + Competitor: { + /** Obj Id */ + obj_id: number; + /** Comm Name */ + comm_name: string | null; + /** Dev Name */ + dev_name: string | null; + /** Obj Class */ + obj_class: string | null; + /** Distance M */ + distance_m: number; + /** Lat */ + lat: number; + /** Lng */ + lng: number; + /** Stage */ + stage: string | null; + /** Site Status */ + site_status?: string | null; + /** Ready Dt */ + ready_dt?: string | null; + /** Flats Total */ + flats_total: number | null; + /** Flats Sold */ + flats_sold: number | null; + /** Sold Pct */ + sold_pct: number | null; + /** Velocity Per Month */ + velocity_per_month: number; + /** Avg Price Per M2 */ + avg_price_per_m2: number | null; + /** Is Active */ + is_active: boolean; + /** Relevance Weight */ + relevance_weight?: number | null; + /** Relevance Breakdown */ + relevance_breakdown?: { + [key: string]: number; + } | null; + }; + /** CompetitorsRequest */ + CompetitorsRequest: { + /** + * Radius Km + * @default 1 + */ + radius_km: number; + /** + * Time Window + * @default last_quarter + * @enum {string} + */ + time_window: "last_month" | "last_quarter" | "last_year"; + /** Obj Class Filter */ + obj_class_filter?: ("economy" | "comfort" | "business") | null; + /** Exclude Obj Ids */ + exclude_obj_ids?: number[]; + /** + * Horizon Months + * @default 12 + */ + horizon_months: number; + }; + /** CompetitorsResponse */ + CompetitorsResponse: { + /** Competitors */ + competitors: components["schemas"]["Competitor"][]; + summary: components["schemas"]["CompetitorsSummary"]; + }; + /** CompetitorsSummary */ + CompetitorsSummary: { + /** Total Competitors */ + total_competitors: number; + /** Active Count */ + active_count: number; + /** Weighted Avg Velocity */ + weighted_avg_velocity: number; + /** Radius Km */ + radius_km: number; + /** Time Window */ + time_window: string; + }; + /** ComplexBuilding */ + ComplexBuilding: { + /** Cad Num */ + cad_num: string; + /** Floors */ + floors: number | null; + /** Area */ + area: number | null; + /** Purpose */ + purpose: string | null; + /** Building Name */ + building_name: string | null; + /** Readable Address */ + readable_address: string | null; + /** Geom Geojson */ + geom_geojson: { + [key: string]: unknown; + } | null; + }; /** * ConceptInput * @description Stage 1a — input contract. Frozen interface for frontend codegen. @@ -1007,6 +3164,213 @@ export interface components { teap: components["schemas"]["TEAP"]; financial: components["schemas"]["FinancialModel"]; }; + /** ConnectionPointsResponse */ + ConnectionPointsResponse: { + /** Engineering Structures */ + engineering_structures: components["schemas"]["EngineeringStructure"][]; + /** Zouit Engineering Overlaps */ + zouit_engineering_overlaps: components["schemas"]["ZouitOverlap"][]; + summary: components["schemas"]["ConnectionPointsSummary"]; + /** Dump Available */ + dump_available: boolean; + /** Dump Fetched At */ + dump_fetched_at: string | null; + }; + /** ConnectionPointsSummary */ + ConnectionPointsSummary: { + /** Nearest Structure Distance M */ + nearest_structure_distance_m: number | null; + /** In Protection Zone */ + in_protection_zone: boolean; + /** Protection Zones Intersecting */ + protection_zones_intersecting: number; + /** Total Structures In Radius */ + total_structures_in_radius: number; + }; + /** + * CreateCadastreJobRequest + * @description Параметры создания bulk cadastre job. + */ + CreateCadastreJobRequest: { + /** + * Scope + * @description pilot=50 кварталов ЕКБ, ekb_full=все 2408, manual_list=явный список + */ + scope: string; + /** + * Quarters + * @description Список кварталов для scope=manual_list + */ + quarters?: string[] | null; + /** + * Limit + * @description Ограничить количество кварталов (override для pilot/ekb_full) + */ + limit?: number | null; + /** Name */ + name?: string | null; + /** + * Rate Ms + * @description Задержка между запросами в мс (333мс = ~3 req/s) + * @default 333 + */ + rate_ms: number; + }; + /** CustomPoiCreate */ + CustomPoiCreate: { + /** + * Name + * @description Отображаемое название точки + */ + name: string; + /** + * Category + * @description Произвольная категория + */ + category?: string | null; + /** + * Weight + * @description Вес точки в scoring [-5, 5] + */ + weight: number; + /** + * Lon + * @description Долгота WGS-84 + */ + lon: number; + /** + * Lat + * @description Широта WGS-84 + */ + lat: number; + /** + * Parcel Cad + * @description Кадастровый номер участка; NULL = глобальная для пользователя + */ + parcel_cad?: string | null; + /** + * Notes + * @description Примечания + */ + notes?: string | null; + }; + /** CustomPoiOut */ + CustomPoiOut: { + /** Id */ + id: number; + /** User Id */ + user_id: string; + /** Parcel Cad */ + parcel_cad: string | null; + /** Name */ + name: string; + /** Category */ + category: string | null; + /** Weight */ + weight: number; + /** Lon */ + lon: number; + /** Lat */ + lat: number; + /** Notes */ + notes: string | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** + * Updated At + * Format: date-time + */ + updated_at: string; + }; + /** CustomPoiUpdate */ + CustomPoiUpdate: { + /** Name */ + name?: string | null; + /** Category */ + category?: string | null; + /** Weight */ + weight?: number | null; + /** Lon */ + lon?: number | null; + /** Lat */ + lat?: number | null; + /** Parcel Cad */ + parcel_cad?: string | null; + /** Notes */ + notes?: string | null; + }; + /** + * DduIndicatorRequest + * @description ARN-shaped request body. Unsupported options are reported in `notes` + * of the response rather than rejected (MVP supports Q / methods 1,2 / subj 66). + */ + DduIndicatorRequest: { + /** Indicators */ + indicators?: string[] | null; + /** + * Calculationmethod + * @default 2 + */ + calculationMethod: number; + /** Periodfrom */ + periodFrom?: string | null; + /** Periodto */ + periodTo?: string | null; + /** Federaldistrict */ + federalDistrict?: number[] | null; + /** Federalsubject */ + federalSubject?: string[] | null; + /** Cadastralquarter */ + cadastralQuarter?: string[] | null; + /** Arearanges */ + areaRanges?: number[] | null; + }; + /** + * DduIndicatorResponse + * @description Free-form ARN-mirror response (table + graph + meta + notes). + */ + DduIndicatorResponse: { + /** Meta */ + meta: { + [key: string]: unknown; + }; + /** Table */ + table: { + [key: string]: unknown; + }[]; + /** Graph */ + graph: { + [key: string]: unknown; + }[]; + /** Notes */ + notes: string[]; + }; + /** EngineeringStructure */ + EngineeringStructure: { + /** Name */ + name: string | null; + /** Type */ + type: string | null; + /** Cad Num */ + cad_num: string | null; + /** Distance To Boundary M */ + distance_to_boundary_m: number; + /** Geometry Geojson */ + geometry_geojson: { + [key: string]: unknown; + }; + /** Readable Address */ + readable_address: string | null; + /** Raw Props */ + raw_props: { + [key: string]: unknown; + }; + /** Source */ + source: string; + }; /** * EnqueueGeoJobRequest * @description Создать новый bulk geo-job. @@ -1054,11 +3418,394 @@ export interface components { /** Irr */ irr: number; }; + /** + * GroundedIn + * @description Привязка ответа к источнику — какой ран/схема/секции легли в основу ответа. + * + * Делает ответ explainable: фронт может показать «основано на ране N, секции X, Y». + * None в ответе, когда отчёта ещё нет (report_status='pending'). + */ + GroundedIn: { + /** + * Run Id + * @description ID персистентного analysis_run, давшего отчёт + */ + run_id: number; + /** + * Schema Version + * @description Версия схемы отчёта (контракт as_dict) + */ + schema_version: string; + /** + * Sections + * @description Секции отчёта, использованные для ответа (explainability) + */ + sections?: string[]; + }; /** HTTPValidationError */ HTTPValidationError: { /** Detail */ detail?: components["schemas"]["ValidationError"][]; }; + /** + * HarvestQuarterRequest + * @description Параметры ручного запуска harvest одного квартала. + */ + HarvestQuarterRequest: { + /** + * Quarter Cad + * @description 3-сегментный кадастровый номер квартала, например '66:41:0204016' + */ + quarter_cad: string; + /** + * Region Code + * @default 66 + */ + region_code: number; + /** + * Include Zouit + * @default true + */ + include_zouit: boolean; + /** + * Include Risks + * @default false + */ + include_risks: boolean; + }; + /** InsightCreate */ + InsightCreate: { + /** + * Title + * @description Заголовок заметки + */ + title: string; + /** + * Body + * @description Тело заметки (intel) + */ + body: string; + /** + * Category + * @description Категория: competition|permitting|demand|risk|other + */ + category?: + | ("competition" | "permitting" | "demand" | "risk" | "other") + | null; + /** + * Is Confidential + * @description «Непублично» (§7.13): маркировка чувствительной заметки + * @default false + */ + is_confidential: boolean; + /** + * District + * @description Location ref (район); Part B промоутит в Location FK + */ + district?: string | null; + /** + * Cad Num + * @description Опциональный кадастровый номер участка + */ + cad_num?: string | null; + /** + * Lon + * @description Долгота WGS-84 (map-pin) + */ + lon?: number | null; + /** + * Lat + * @description Широта WGS-84 (map-pin) + */ + lat?: number | null; + /** + * Source + * @description Источник intel + */ + source?: string | null; + }; + /** + * InsightList + * @description Ответ list-эндпоинта: total + страница строк (как admin_leads). + */ + InsightList: { + /** Total */ + total: number; + /** Limit */ + limit: number; + /** Offset */ + offset: number; + /** Rows */ + rows: components["schemas"]["InsightOut"][]; + }; + /** InsightOut */ + InsightOut: { + /** Id */ + id: number; + /** Created By */ + created_by: string; + /** Title */ + title: string; + /** Body */ + body: string; + /** Category */ + category: string | null; + /** Is Confidential */ + is_confidential: boolean; + /** District */ + district: string | null; + /** Cad Num */ + cad_num: string | null; + /** Lon */ + lon: number | null; + /** Lat */ + lat: number | null; + /** Source */ + source: string | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** + * Updated At + * Format: date-time + */ + updated_at: string; + }; + /** + * InsightUpdate + * @description Partial update (PUT) — переданные поля заменяются, отсутствующие не трогаются. + */ + InsightUpdate: { + /** Title */ + title?: string | null; + /** Body */ + body?: string | null; + /** Category */ + category?: + | ("competition" | "permitting" | "demand" | "risk" | "other") + | null; + /** Is Confidential */ + is_confidential?: boolean | null; + /** District */ + district?: string | null; + /** Cad Num */ + cad_num?: string | null; + /** Lon */ + lon?: number | null; + /** Lat */ + lat?: number | null; + /** Source */ + source?: string | null; + }; + /** JobSettingRead */ + JobSettingRead: { + /** Job Type */ + job_type: string; + /** Enabled */ + enabled: boolean; + /** Queue Name */ + queue_name: string; + /** Cron Schedule */ + cron_schedule: string | null; + /** Rate Ms */ + rate_ms: number | null; + /** Max Retries */ + max_retries: number; + /** Max Concurrency */ + max_concurrency: number; + /** Extra Config */ + extra_config: { + [key: string]: unknown; + }; + /** + * Updated At + * Format: date-time + */ + updated_at: string; + /** Updated By */ + updated_by: string | null; + /** Description */ + description: string | null; + }; + /** + * JobSettingUpdate + * @description PATCH-style: передавать только изменяемые поля. Непереданные — без изменений. + */ + JobSettingUpdate: { + /** Enabled */ + enabled?: boolean | null; + /** Queue Name */ + queue_name?: string | null; + /** Cron Schedule */ + cron_schedule?: string | null; + /** Rate Ms */ + rate_ms?: number | null; + /** Max Retries */ + max_retries?: number | null; + /** Max Concurrency */ + max_concurrency?: number | null; + /** Extra Config */ + extra_config?: { + [key: string]: unknown; + } | null; + }; + /** LandingStatsOut */ + LandingStatsOut: { + /** Zk Total */ + zk_total: number; + /** Deals Total */ + deals_total: number; + /** Price Coverage Pct */ + price_coverage_pct: number; + /** Mapping Coverage Pct */ + mapping_coverage_pct: number; + /** Last Data Update */ + last_data_update: string; + /** Paradox */ + paradox: string; + /** + * Stale + * @default false + */ + stale: boolean; + }; + /** + * LayoutDataQuality + * @description Метаданные качества данных (coverage). + */ + LayoutDataQuality: { + /** Objects With Velocity Data */ + objects_with_velocity_data: number; + /** Objects Total In Radius */ + objects_total_in_radius: number; + /** Velocity Coverage Pct */ + velocity_coverage_pct: number; + /** + * Confidence + * @enum {string} + */ + confidence: "high" | "medium" | "low"; + }; + /** + * LayoutTzMixRow + * @description Строка рекомендации unit-mix для ТЗ. + */ + LayoutTzMixRow: { + /** Room Bucket */ + room_bucket: string; + /** Pct */ + pct: number; + /** Abs Units */ + abs_units: number | null; + /** Avg Target Area M2 */ + avg_target_area_m2: number | null; + }; + /** + * LayoutTzRecommendation + * @description Рекомендация для ТЗ на проектирование. + */ + LayoutTzRecommendation: { + /** Rationale Text */ + rationale_text: string; + /** Mix */ + mix: components["schemas"]["LayoutTzMixRow"][]; + /** Weighted Avg Price Per M2 Rub */ + weighted_avg_price_per_m2_rub: number | null; + /** Based On Obj Count */ + based_on_obj_count: number; + /** Based On Total Deals */ + based_on_total_deals: number; + /** + * Data Window Start + * Format: date + */ + data_window_start: string; + /** + * Data Window End + * Format: date + */ + data_window_end: string; + /** + * Cap Skipped + * @default false + */ + cap_skipped: boolean; + }; + /** + * LocationList + * @description Ответ list-эндпоинта: total + страница строк (зеркало InsightList). + */ + LocationList: { + /** Total */ + total: number; + /** Rows */ + rows: components["schemas"]["LocationOut"][]; + }; + /** + * LocationOut + * @description Локация (район) с district-level индексами (#948 Part B, §8.2). + */ + LocationOut: { + /** Id */ + id: number; + /** + * District Name + * @description Логический ключ района (8 админ-районов ЕКБ) + */ + district_name: string; + /** + * Region + * @description Регион (NULL = ЕКБ/Свердл.) + */ + region?: string | null; + /** + * Lon + * @description Долгота центроида района (WGS-84), None если нет + */ + lon?: number | null; + /** + * Lat + * @description Широта центроида района (WGS-84), None если нет + */ + lat?: number | null; + /** + * Infra Index + * @description Инфра-индекс 0..1 (POI района / POI города); None=нет данных + */ + infra_index?: number | null; + /** + * Competition Index + * @description Индекс конкуренции 0..1 (доступное/затоваренное конкурирующее предложение = overstock; выше = больше конкур. давления; ортогонален demand); None=нет данных + */ + competition_index?: number | null; + /** + * Demand Index + * @description Индекс спроса 0..1 (saturated velocity); None=нет данных + */ + demand_index?: number | null; + /** + * Future Supply Index + * @description Индекс будущего предложения 0..1; None=нет данных + */ + future_supply_index?: number | null; + /** + * Indices Computed At + * @description Когда индексы последний раз пересчитаны (None до первого refresh) + */ + indices_computed_at?: string | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** + * Updated At + * Format: date-time + */ + updated_at: string; + }; /** * ObjectiveConfigUpdateRequest * @description PATCH-style — обновляет только переданные поля. @@ -1081,27 +3828,167 @@ export interface components { /** Retries */ retries?: number | null; }; - /** ParcelDetail */ - ParcelDetail: { + /** + * OpportunityParcel + * @description TIER 4: opportunity ЗУ вблизи участка (auction/scheme/free/future/oopt). + * + * Поля: + * layer: тип opportunity — "auction_parcels" | "scheme_parcels" | + * "free_parcels" | "future_parcels" | "oopt" + * cad_num: кадастровый номер ЗУ (если доступен в NSPD properties) + * distance_m: расстояние от centroid анализируемого участка до ЗУ, м + * geom_wkt: WKT геометрии ЗУ в EPSG:4326 + */ + OpportunityParcel: { + /** Layer */ + layer: string; + /** Cad Num */ + cad_num: string | null; + /** Distance M */ + distance_m: number | null; + /** Geom Wkt */ + geom_wkt: string | null; + }; + /** OwnPlannedProjectCreate */ + OwnPlannedProjectCreate: { + /** + * Name + * @description Название проекта/ЖК + */ + name: string; + /** + * District + * @description Район (location ref) + */ + district?: string | null; + /** + * Obj Class + * @description Класс: Стандарт|Комфорт|Бизнес|Премиум + */ + obj_class?: ("Стандарт" | "Комфорт" | "Бизнес" | "Премиум") | null; + /** + * Planned Release Month + * @description Планируемый месяц выхода в продажу (нормализуется к 1-му числу) + */ + planned_release_month?: string | null; + /** + * Price Min Per M2 + * @description Нижняя граница цены, ₽/м² (≥0) + */ + price_min_per_m2?: number | null; + /** + * Price Max Per M2 + * @description Верхняя граница цены, ₽/м² (≥0) + */ + price_max_per_m2?: number | null; + /** + * Unit Mix + * @description Квартирография: доли по форматам {"studio":0.3,"1k":0.4,...} (каждая в [0,1]) + */ + unit_mix?: { + [key: string]: number; + } | null; + /** + * Lon + * @description Долгота WGS-84 (центроид) + */ + lon?: number | null; + /** + * Lat + * @description Широта WGS-84 (центроид) + */ + lat?: number | null; + }; + /** + * OwnPlannedProjectList + * @description Ответ list-эндпоинта: total + страница строк (как insight). + */ + OwnPlannedProjectList: { + /** Total */ + total: number; + /** Limit */ + limit: number; + /** Offset */ + offset: number; + /** Rows */ + rows: components["schemas"]["OwnPlannedProjectOut"][]; + }; + /** OwnPlannedProjectOut */ + OwnPlannedProjectOut: { /** Id */ - id: string; - /** Cadastral Number */ - cadastral_number: string; - /** Area Sqm */ - area_sqm: number; - /** Vri */ - vri: string; - /** Address */ - address: string | null; - score: components["schemas"]["ScoreBreakdown"]; - /** Geometry Geojson */ - geometry_geojson: { - [key: string]: unknown; - }; - /** Enrichment */ - enrichment: { - [key: string]: unknown; - }; + id: number; + /** Created By */ + created_by: string; + /** Name */ + name: string; + /** District */ + district: string | null; + /** Obj Class */ + obj_class: string | null; + /** Planned Release Month */ + planned_release_month: string | null; + /** Price Min Per M2 */ + price_min_per_m2: number | null; + /** Price Max Per M2 */ + price_max_per_m2: number | null; + /** Unit Mix */ + unit_mix: { + [key: string]: number; + } | null; + /** Lon */ + lon: number | null; + /** Lat */ + lat: number | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** + * Updated At + * Format: date-time + */ + updated_at: string; + }; + /** + * OwnPlannedProjectUpdate + * @description Partial update (PUT) — переданные поля заменяются, отсутствующие не трогаются. + */ + OwnPlannedProjectUpdate: { + /** Name */ + name?: string | null; + /** District */ + district?: string | null; + /** Obj Class */ + obj_class?: ("Стандарт" | "Комфорт" | "Бизнес" | "Премиум") | null; + /** Planned Release Month */ + planned_release_month?: string | null; + /** Price Min Per M2 */ + price_min_per_m2?: number | null; + /** Price Max Per M2 */ + price_max_per_m2?: number | null; + /** Unit Mix */ + unit_mix?: { + [key: string]: number; + } | null; + /** Lon */ + lon?: number | null; + /** Lat */ + lat?: number | null; + }; + /** + * ParcelBboxResponse + * @description Ответ GET /parcels/by-bbox. + */ + ParcelBboxResponse: { + /** Parcels */ + parcels: components["schemas"]["ParcelMapMarker"][]; + /** Count */ + count: number; + /** Limit */ + limit: number; + /** Bbox Area Km2 */ + bbox_area_km2: number; }; /** ParcelFilter */ ParcelFilter: { @@ -1120,6 +4007,29 @@ export interface components { /** Text Search */ text_search?: string | null; }; + /** + * ParcelMapMarker + * @description Один маркер участка на карте — облегчённый ответ для bbox-запроса. + * + * status берётся из parcel_user_status если передан user_id, иначе null. + * last_analysis_date — placeholder до реализации B2 auth. + */ + ParcelMapMarker: { + /** Cad Num */ + cad_num: string; + /** Centroid Lat */ + centroid_lat: number; + /** Centroid Lon */ + centroid_lon: number; + /** Area M2 */ + area_m2: number | null; + /** Land Category */ + land_category: string | null; + /** Status */ + status: string | null; + /** Last Analysis Date */ + last_analysis_date: string | null; + }; /** ParcelSearchRequest */ ParcelSearchRequest: { filters?: components["schemas"]["ParcelFilter"]; @@ -1151,6 +4061,50 @@ export interface components { address: string | null; score: components["schemas"]["ScoreBreakdown"]; }; + /** PilotRequestInput */ + PilotRequestInput: { + /** Name */ + name: string; + /** Phone */ + phone?: string | null; + /** Email */ + email?: string | null; + /** Company */ + company?: string | null; + /** Message */ + message?: string | null; + /** + * Source + * @default landing + * @enum {string} + */ + source: "landing" | "analyze_page" | "other"; + }; + /** + * PoiScoreItem + * @description Один POI в ranked-ответе. + */ + PoiScoreItem: { + /** Name */ + name: string | null; + /** Category */ + category: string; + /** Distance M */ + distance_m: number; + /** Weight */ + weight: number; + /** Address */ + address: string | null; + }; + /** PoiScoreResponse */ + PoiScoreResponse: { + /** Cad Num */ + cad_num: string; + /** Radius M */ + radius_m: number; + /** Top Poi */ + top_poi: components["schemas"]["PoiScoreItem"][]; + }; /** RecommendBucket */ RecommendBucket: { /** Bucket */ @@ -1177,6 +4131,21 @@ export interface components { velocity_per_month?: number | null; /** Months To Sellout */ months_to_sellout?: number | null; + /** Elasticity */ + elasticity?: number | null; + /** Elasticity R2 */ + elasticity_r2?: number | null; + /** Elasticity N */ + elasticity_n?: number | null; + /** Elasticity Source */ + elasticity_source?: string | null; + /** Velocity Source */ + velocity_source?: string | null; + /** + * Is Top Success + * @default false + */ + is_top_success: boolean; }; /** RecommendComparable */ RecommendComparable: { @@ -1192,6 +4161,14 @@ export interface components { flat_count?: number | null; /** Sold Perc */ sold_perc?: number | null; + /** Cad Quarter */ + cad_quarter?: string | null; + /** Lat */ + lat?: number | null; + /** Lon */ + lon?: number | null; + /** Buildings N */ + buildings_n?: number | null; }; /** RecommendMixInput */ RecommendMixInput: { @@ -1213,6 +4190,10 @@ export interface components { price_factor: number; /** Target Months */ target_months?: number | null; + /** Horizon Months */ + horizon_months?: number | null; + /** Cad Num */ + cad_num?: string | null; }; /** RecommendMixOutput */ RecommendMixOutput: { @@ -1229,6 +4210,29 @@ export interface components { /** Comparables */ comparables: components["schemas"]["RecommendComparable"][]; }; + /** + * RedLine + * @description TIER 4: красная линия застройки (NSPD layer 879243). + * + * Поля: + * geom_wkt: WKT геометрии линии в EPSG:4326 + * intersection_length_m: длина пересечения линии с участком, м + * (null если только nearby, не intersect) + * distance_m: расстояние от ближайшей точки линии до участка, м + * (null если линия пересекает участок) + * + * UI-семантика: + * intersection_length_m != null → красная линия ПЕРЕСЕКАЕТ участок (ALERT) + * distance_m != null → красная линия рядом (warning) + */ + RedLine: { + /** Geom Wkt */ + geom_wkt: string | null; + /** Intersection Length M */ + intersection_length_m: number | null; + /** Distance M */ + distance_m: number | null; + }; /** ReleaseLockRequest */ ReleaseLockRequest: { /** Region Code */ @@ -1246,6 +4250,20 @@ export interface components { */ terminate: boolean; }; + /** + * RiskZone + * @description Одна риск-зона НСПД (TIER 3), пересекающая участок. + */ + RiskZone: { + /** Layer */ + layer: string; + /** Subtype */ + subtype: string | null; + /** Geom Wkt */ + geom_wkt: string | null; + /** Intersection Area Sqm */ + intersection_area_sqm: number | null; + }; /** ScoreBreakdown */ ScoreBreakdown: { /** Total */ @@ -1293,6 +4311,87 @@ export interface components { /** Parking Spaces */ parking_spaces: number; }; + /** + * TopLayoutRow + * @description Одна строка top-planirovok ranking'а. + */ + TopLayoutRow: { + /** Rank */ + rank: number; + /** Room Bucket */ + room_bucket: string; + /** Area Bin */ + area_bin: string; + /** Signature */ + signature: string; + /** Competitor Obj Ids */ + competitor_obj_ids: number[]; + /** Competitor Count */ + competitor_count: number; + /** Total Sold In Window */ + total_sold_in_window: number; + /** Velocity Per Month */ + velocity_per_month: number; + /** Avg Price Per M2 Rub */ + avg_price_per_m2_rub: number | null; + /** Avg Area M2 */ + avg_area_m2: number; + /** Supply Units In Radius */ + supply_units_in_radius: number; + /** Sold Pct Of Supply */ + sold_pct_of_supply: number | null; + /** Is Oversold */ + is_oversold: boolean; + }; + /** TradeInEstimateInput */ + TradeInEstimateInput: { + /** Address */ + address: string; + /** Area M2 */ + area_m2: number; + /** Rooms */ + rooms: number; + /** Floor */ + floor: number; + /** Total Floors */ + total_floors: number; + /** Year Built */ + year_built?: number | null; + /** House Type */ + house_type?: + | ("panel" | "brick" | "monolith" | "monolith_brick" | "other") + | null; + /** Repair State */ + repair_state?: + | ("needs_repair" | "standard" | "good" | "excellent") + | null; + /** Has Balcony */ + has_balcony?: boolean | null; + }; + /** TriggerEkburgPermitsRequest */ + TriggerEkburgPermitsRequest: { + /** + * Year + * @description Если None — refresh_all (все 5 лет 2022-2026). Иначе refresh_year(year) для конкретного года. + */ + year?: number | null; + }; + /** TriggerKnCatalogObjectsRequest */ + TriggerKnCatalogObjectsRequest: { + /** + * Region Code + * @default 66 + */ + region_code: number; + /** Max Objects */ + max_objects?: number | null; + /** + * Force + * @description True — игнорировать фильтр 'уже сегодня обновлён' и грузить ВСЕ объекты последнего snapshot ('Загрузить все'). По умолчанию пропускает то, что уже скраплено сегодня. + * @default false + */ + force: boolean; + }; /** TriggerKnRequest */ TriggerKnRequest: { /** Region Code */ @@ -1339,6 +4438,23 @@ export interface components { */ groups?: string[] | null; }; + /** + * UserScope + * @description Возвращается /me — фронт может использовать allowed_paths для UI gating. + */ + UserScope: { + /** Username */ + username: string; + /** + * Role + * @enum {string} + */ + role: "admin" | "pilot" | "analyst"; + /** Allowed Paths */ + allowed_paths: string[]; + /** Deny Paths */ + deny_paths: string[]; + }; /** ValidationError */ ValidationError: { /** Location */ @@ -1352,6 +4468,88 @@ export interface components { /** Context */ ctx?: Record; }; + /** WeightProfile */ + WeightProfile: { + /** Profile Name */ + profile_name: string; + /** Weights */ + weights?: { + [key: string]: number; + }; + /** + * Is Default + * @default false + */ + is_default: boolean; + /** Description */ + description?: string | null; + /** Id */ + id: number; + /** User Id */ + user_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** + * Updated At + * Format: date-time + */ + updated_at: string; + }; + /** WeightProfileCreate */ + WeightProfileCreate: { + /** Profile Name */ + profile_name: string; + /** Weights */ + weights?: { + [key: string]: number; + }; + /** + * Is Default + * @default false + */ + is_default: boolean; + /** Description */ + description?: string | null; + /** User Id */ + user_id: string; + }; + /** WeightProfileUpdate */ + WeightProfileUpdate: { + /** Profile Name */ + profile_name?: string | null; + /** Weights */ + weights?: { + [key: string]: number; + } | null; + /** Is Default */ + is_default?: boolean | null; + /** Description */ + description?: string | null; + }; + /** ZouitOverlap */ + ZouitOverlap: { + /** Reg Numb Border */ + reg_numb_border: string | null; + /** Type Zone */ + type_zone: string | null; + /** Subcategory */ + subcategory: number | null; + /** Intersects Parcel */ + intersects_parcel: boolean; + /** Geometry Geojson */ + geometry_geojson: { + [key: string]: unknown; + }; + /** Raw Props */ + raw_props: { + [key: string]: unknown; + }; + /** Source */ + source: string; + }; }; responses: never; parameters: never; @@ -1394,6 +4592,80 @@ export interface operations { }; }; }; + ask_api_v1_chat_ask_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ChatAskRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ChatAskResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_parcels_by_bbox_api_v1_parcels_by_bbox_get: { + parameters: { + query: { + /** @description Южная граница bbox */ + min_lat: number; + /** @description Западная граница bbox */ + min_lon: number; + /** @description Северная граница bbox */ + max_lat: number; + /** @description Восточная граница bbox */ + max_lon: number; + limit?: number; + /** @description user_id для overlay статуса */ + user_id?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ParcelBboxResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; search_parcels_api_v1_parcels_search_post: { parameters: { query?: never; @@ -1427,12 +4699,12 @@ export interface operations { }; }; }; - get_parcel_api_v1_parcels__parcel_id__get: { + get_analysis_run_api_v1_parcels_runs__run_id__get: { parameters: { query?: never; header?: never; path: { - parcel_id: string; + run_id: number; }; cookie?: never; }; @@ -1444,7 +4716,430 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ParcelDetail"]; + "application/json": components["schemas"]["AnalysisRunDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_analysis_runs_api_v1_parcels__cad_num__runs_get: { + parameters: { + query?: { + /** @description Сколько недавних ранов вернуть (newest-first) */ + limit?: number; + }; + header?: never; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnalysisRunListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_fetch_status_api_v1_parcels__cad_num__fetch_status_get: { + parameters: { + query?: never; + header?: never; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_parcel_forecast_api_v1_parcels__cad_num__forecast_get: { + parameters: { + query?: never; + header?: { + /** @description Идентичность из Caddy basic_auth (#994, nullable, read-only здесь) */ + "X-Authenticated-User"?: string | null; + }; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + export_parcel_forecast_api_v1_parcels__cad_num__forecast_export_get: { + parameters: { + query?: { + /** @description Формат выгрузки: md (Markdown) | json (сырой отчёт) | tg (сводка) | docx (Word-документ) | pptx (презентация) */ + format?: "md" | "json" | "tg" | "docx" | "pptx"; + }; + header?: never; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + analyze_parcel_api_v1_parcels__cad_num__analyze_post: { + parameters: { + query?: { + /** @description Переопределить веса POI через конкретный weight profile */ + profile_id?: number | null; + /** @description user_id для fallback на default-профиль пользователя */ + profile_user_id?: string | null; + /** @description Горизонт прогноза, мес (#995, ТЗ §12.1): один из {6, 12, 18, 24} */ + horizon?: number; + }; + header?: { + /** @description Session ID пользователя для custom POI scoring (#254) */ + "x-session-id"?: string | null; + /** @description Идентичность из Caddy basic_auth → created_by рана (#994, nullable) */ + "X-Authenticated-User"?: string | null; + }; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["AnalyzeRequest"] | null; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnalyzeResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_parcel_connection_points_api_v1_parcels__cad_num__connection_points_get: { + parameters: { + query?: { + radius_m?: number; + }; + header?: never; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConnectionPointsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_isochrones_api_v1_parcels__cad_num__isochrones_get: { + parameters: { + query?: { + mode?: string; + times_min?: number[]; + }; + header?: never; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_poi_score_api_v1_parcels__cad_num__poi_score_get: { + parameters: { + query?: { + radius_m?: number; + decay?: string; + }; + header?: never; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PoiScoreResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_parcel_competitors_api_v1_parcels__cad_num__competitors_post: { + parameters: { + query?: never; + header?: never; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CompetitorsRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CompetitorsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_parcel_best_layouts_api_v1_parcels__cad_num__best_layouts_post: { + parameters: { + query?: never; + header?: never; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BestLayoutsRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BestLayoutsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_parcel_best_layouts_pdf_api_v1_parcels__cad_num__best_layouts_pdf_post: { + parameters: { + query?: never; + header?: never; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BestLayoutsRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + parcel_snapshot_pdf_api_v1_parcels__cad_num__snapshot_pdf_get: { + parameters: { + query?: never; + header?: never; + path: { + cad_num: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; }; }; /** @description Validation Error */ @@ -1883,6 +5578,206 @@ export interface operations { }; }; }; + object_full_detail_api_v1_analytics_object__obj_id__full_get: { + parameters: { + query?: never; + header?: never; + path: { + obj_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + object_quartirography_api_v1_analytics_object__obj_id__quartirography_get: { + parameters: { + query?: never; + header?: never; + path: { + obj_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }[]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + object_checks_api_v1_analytics_object__obj_id__checks_get: { + parameters: { + query?: never; + header?: never; + path: { + obj_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }[]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + object_documents_api_v1_analytics_object__obj_id__documents_get: { + parameters: { + query?: never; + header?: never; + path: { + obj_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }[]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + object_buildings_api_v1_analytics_object__obj_id__buildings_get: { + parameters: { + query?: never; + header?: never; + path: { + obj_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ComplexBuilding"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + velocity_alerts_api_v1_analytics_velocity_alerts_get: { + parameters: { + query?: { + region_code?: number; + lookback_months?: number; + min_zscore?: number; + min_drop_pct?: number; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; prinzip_insights_api_v1_analytics_prinzip_insights_get: { parameters: { query?: never; @@ -1982,12 +5877,43 @@ export interface operations { }; }; }; + ddu_indicator_api_v1_market_ddu_indicator_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DduIndicatorRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DduIndicatorResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; trigger_kn_sweep_api_v1_admin_scrape_kn_post: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2022,9 +5948,7 @@ export interface operations { queue_status_api_v1_admin_scrape_queue_get: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2041,23 +5965,12 @@ export interface operations { }; }; }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; }; }; release_lock_api_v1_admin_scrape_release_lock_post: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2092,9 +6005,7 @@ export interface operations { revoke_task_api_v1_admin_scrape_revoke_post: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2132,9 +6043,7 @@ export interface operations { run_id?: number | null; limit?: number; }; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2169,9 +6078,7 @@ export interface operations { since_id?: number | null; limit?: number; }; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2199,12 +6106,111 @@ export interface operations { }; }; }; + trigger_noise_sync_api_v1_admin_scrape_noise_sync_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + }; + }; + trigger_harvest_quarter_api_v1_admin_scrape_nspd_harvest_quarter_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["HarvestQuarterRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + trigger_pzz_sync_api_v1_admin_scrape_pzz_sync_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + }; + }; + trigger_poi_sync_api_v1_admin_scrape_poi_sync_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + }; + }; trigger_objective_etl_api_v1_admin_scrape_objective_post: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2241,9 +6247,7 @@ export interface operations { query?: { limit?: number; }; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2274,9 +6278,7 @@ export interface operations { trigger_objective_sync_our_api_v1_admin_scrape_objective_sync_our_post: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2311,9 +6313,7 @@ export interface operations { get_objective_config_api_v1_admin_scrape_objective_config_get: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2330,23 +6330,12 @@ export interface operations { }; }; }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; }; }; update_objective_config_api_v1_admin_scrape_objective_config_put: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2381,9 +6370,7 @@ export interface operations { objective_coverage_api_v1_admin_scrape_objective_coverage_get: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2400,15 +6387,6 @@ export interface operations { }; }; }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; }; }; list_geo_jobs_api_v1_admin_scrape_geo_jobs_get: { @@ -2416,9 +6394,7 @@ export interface operations { query?: { limit?: number; }; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2449,9 +6425,7 @@ export interface operations { enqueue_geo_job_api_v1_admin_scrape_geo_jobs_post: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2483,12 +6457,45 @@ export interface operations { }; }; }; + bulk_enqueue_geo_api_v1_admin_scrape_geo_bulk_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BulkGeoEnqueueRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; cancel_geo_job_api_v1_admin_scrape_geo_jobs__job_id__cancel_post: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path: { job_id: number; }; @@ -2521,9 +6528,7 @@ export interface operations { resume_geo_job_api_v1_admin_scrape_geo_jobs__job_id__resume_post: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path: { job_id: number; }; @@ -2553,15 +6558,35 @@ export interface operations { }; }; }; + trigger_newbuilding_crossload_api_v1_admin_scrape_newbuilding_crossload_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + }; + }; list_all_runs_api_v1_admin_scrape_all_runs_get: { parameters: { query?: { scraper_type?: string | null; limit?: number; }; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2596,9 +6621,7 @@ export interface operations { run_id?: number | null; limit?: number; }; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2626,14 +6649,34 @@ export interface operations { }; }; }; + scrape_freshness_api_v1_admin_scrape_freshness_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + }; + }; list_runs_api_v1_admin_scrape_runs_get: { parameters: { query?: { limit?: number; }; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2661,6 +6704,162 @@ export interface operations { }; }; }; + trigger_ekburg_permits_api_v1_admin_scrape_ekburg_permits_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TriggerEkburgPermitsRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + trigger_kn_catalog_objects_api_v1_admin_scrape_kn_catalog_objects_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TriggerKnCatalogObjectsRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_job_settings_api_v1_admin_jobs_settings_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JobSettingRead"][]; + }; + }; + }; + }; + get_job_setting_api_v1_admin_jobs_settings__job_type__get: { + parameters: { + query?: never; + header?: never; + path: { + job_type: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JobSettingRead"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_job_setting_api_v1_admin_jobs_settings__job_type__put: { + parameters: { + query?: never; + header?: never; + path: { + job_type: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["JobSettingUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JobSettingRead"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; list_leads_api_v1_admin_leads__get: { parameters: { query?: { @@ -2677,9 +6876,7 @@ export interface operations { limit?: number; offset?: number; }; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2712,9 +6909,7 @@ export interface operations { query?: { months?: number; }; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2747,9 +6942,7 @@ export interface operations { query?: { months?: number; }; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2782,9 +6975,7 @@ export interface operations { query?: { months?: number; }; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2815,9 +7006,7 @@ export interface operations { funnel_by_object_api_v1_admin_leads_funnel_by_object_get: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2834,23 +7023,12 @@ export interface operations { }[]; }; }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; }; }; list_sources_api_v1_admin_leads_sources_get: { parameters: { query?: never; - header?: { - "X-Admin-Token"?: string | null; - }; + header?: never; path?: never; cookie?: never; }; @@ -2865,6 +7043,168 @@ export interface operations { "application/json": string[]; }; }; + }; + }; + list_user_profiles_api_v1_admin_site_finder_weight_profiles_get: { + parameters: { + query: { + /** @description user_id для фильтра профилей */ + user_id: string; + /** @description Включить системные preset-профили (Эконом/Комфорт/Бизнес) */ + include_system?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WeightProfile"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_user_profile_api_v1_admin_site_finder_weight_profiles_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["WeightProfileCreate"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WeightProfile"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_user_profile_api_v1_admin_site_finder_weight_profiles__profile_id__get: { + parameters: { + query: { + /** @description Владелец профиля */ + user_id: string; + }; + header?: never; + path: { + profile_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WeightProfile"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_user_profile_api_v1_admin_site_finder_weight_profiles__profile_id__put: { + parameters: { + query: { + /** @description Владелец профиля */ + user_id: string; + }; + header?: never; + path: { + profile_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["WeightProfileUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WeightProfile"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_user_profile_api_v1_admin_site_finder_weight_profiles__profile_id__delete: { + parameters: { + query: { + /** @description Владелец профиля */ + user_id: string; + }; + header?: never; + path: { + profile_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; /** @description Validation Error */ 422: { headers: { @@ -2910,6 +7250,1019 @@ export interface operations { }; }; }; + list_pois_api_v1_custom_pois_get: { + parameters: { + query?: { + /** @description Фильтр по кадастровому номеру участка (возвращает global + parcel POI) */ + parcel_cad?: string | null; + }; + header?: { + "x-session-id"?: string | null; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CustomPoiOut"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_poi_api_v1_custom_pois_post: { + parameters: { + query?: never; + header?: { + "x-session-id"?: string | null; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CustomPoiCreate"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CustomPoiOut"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_poi_api_v1_custom_pois__poi_id__delete: { + parameters: { + query?: never; + header?: { + "x-session-id"?: string | null; + }; + path: { + poi_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + patch_poi_api_v1_custom_pois__poi_id__patch: { + parameters: { + query?: never; + header?: { + "x-session-id"?: string | null; + }; + path: { + poi_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CustomPoiUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CustomPoiOut"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_locations_endpoint_api_v1_locations_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LocationList"]; + }; + }; + }; + }; + get_location_endpoint_api_v1_locations__district_name__get: { + parameters: { + query?: never; + header?: never; + path: { + district_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LocationOut"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_insights_endpoint_api_v1_insights_get: { + parameters: { + query?: { + /** @description Фильтр по району */ + district?: string | null; + /** @description Фильтр по кадастровому номеру */ + cad_num?: string | null; + /** @description Фильтр по категории */ + category?: + | ("competition" | "permitting" | "demand" | "risk" | "other") + | null; + /** @description Фильтр по пометке «непублично» */ + is_confidential?: boolean | null; + /** @description Фильтр по автору */ + created_by?: string | null; + limit?: number; + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InsightList"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_insight_endpoint_api_v1_insights_post: { + parameters: { + query?: never; + header?: { + "x-authenticated-user"?: string | null; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["InsightCreate"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InsightOut"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_insight_endpoint_api_v1_insights__insight_id__get: { + parameters: { + query?: never; + header?: never; + path: { + insight_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InsightOut"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_insight_endpoint_api_v1_insights__insight_id__put: { + parameters: { + query?: never; + header?: { + "x-authenticated-user"?: string | null; + }; + path: { + insight_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["InsightUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InsightOut"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_insight_endpoint_api_v1_insights__insight_id__delete: { + parameters: { + query?: never; + header?: { + "x-authenticated-user"?: string | null; + }; + path: { + insight_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_own_projects_endpoint_api_v1_own_projects_get: { + parameters: { + query?: { + /** @description Фильтр по району */ + district?: string | null; + /** @description Фильтр по классу */ + obj_class?: ("Стандарт" | "Комфорт" | "Бизнес" | "Премиум") | null; + /** @description Фильтр по автору */ + created_by?: string | null; + limit?: number; + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OwnPlannedProjectList"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_own_project_endpoint_api_v1_own_projects_post: { + parameters: { + query?: never; + header?: { + "x-authenticated-user"?: string | null; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OwnPlannedProjectCreate"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OwnPlannedProjectOut"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_own_project_endpoint_api_v1_own_projects__project_id__get: { + parameters: { + query?: never; + header?: never; + path: { + project_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OwnPlannedProjectOut"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_own_project_endpoint_api_v1_own_projects__project_id__put: { + parameters: { + query?: never; + header?: { + "x-authenticated-user"?: string | null; + }; + path: { + project_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OwnPlannedProjectUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OwnPlannedProjectOut"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_own_project_endpoint_api_v1_own_projects__project_id__delete: { + parameters: { + query?: never; + header?: { + "x-authenticated-user"?: string | null; + }; + path: { + project_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_cadastre_jobs_api_v1_admin_cadastre_jobs_get: { + parameters: { + query?: { + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }[]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_cadastre_job_api_v1_admin_cadastre_jobs_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateCadastreJobRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_cadastre_job_api_v1_admin_cadastre_jobs__job_id__get: { + parameters: { + query?: never; + header?: never; + path: { + job_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + cancel_cadastre_job_api_v1_admin_cadastre_jobs__job_id__cancel_post: { + parameters: { + query?: never; + header?: never; + path: { + job_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + resume_cadastre_job_api_v1_admin_cadastre_jobs__job_id__resume_post: { + parameters: { + query?: never; + header?: never; + path: { + job_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + run_objective_backfill_api_v1_admin_etl_objective_backfill_post: { + parameters: { + query?: { + /** @description Preview без insertions */ + dry_run?: boolean; + /** @description REFRESH mv_layout_velocity после backfill */ + refresh_mv?: boolean; + /** @description v2 mode: threshold=0.80, match_method='fuzzy_v2'. Запускать после v1 run — только для unmapped объектов. */ + v2?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + run_nspd_denorm_backfill_api_v1_admin_etl_nspd_denorm_backfill_post: { + parameters: { + query?: { + /** @description Максимум кварталов для обработки (None = все) */ + limit?: number | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + estimate_api_v1_trade_in_estimate_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TradeInEstimateInput"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AggregatedEstimate"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_estimate_api_v1_trade_in_estimate__estimate_id__get: { + parameters: { + query?: never; + header?: never; + path: { + estimate_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AggregatedEstimate"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + estimate_pdf_api_v1_trade_in_estimate__estimate_id__pdf_get: { + parameters: { + query?: never; + header?: never; + path: { + estimate_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + landing_stats_api_v1_landing_stats_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LandingStatsOut"]; + }; + }; + }; + }; + create_pilot_request_api_v1_pilot_request_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PilotRequestInput"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_recent_parcels_api_v1_users_me_recent_parcels_get: { + parameters: { + query?: { + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + me_api_v1_me_get: { + parameters: { + query?: never; + header?: { + "X-Authenticated-User"?: string | null; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserScope"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + ping_api_v1_ping_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: boolean; + }; + }; + }; + }; + }; health_health_get: { parameters: { query?: never; diff --git a/ops/gendesign-uptime.default.example b/ops/gendesign-uptime.default.example new file mode 100644 index 00000000..db99c75f --- /dev/null +++ b/ops/gendesign-uptime.default.example @@ -0,0 +1,28 @@ +# Environment file for ops/uptime-healthcheck.sh (external uptime watchdog, #75). +# +# Install as a ROOT-OWNED, chmod-600 file that is NOT in git, on whatever host +# runs the cron (ideally a host OTHER than the prod VM, so it survives a full +# VPS outage): +# sudo cp ops/gendesign-uptime.default.example /etc/default/gendesign-uptime +# sudo chmod 600 /etc/default/gendesign-uptime +# sudo $EDITOR /etc/default/gendesign-uptime # fill in real Telegram creds +# +# uptime-healthcheck.sh sources this file if present. With NO Telegram vars set, +# it still logs up/down but sends no alert (useful for a dry run first). +# +# Get a bot token from @BotFather; get your chat_id by messaging the bot then +# GET https://api.telegram.org/bot/getUpdates and reading message.chat.id. + +# --- Telegram alerting (both required to enable alerts) --- +#TELEGRAM_BOT_TOKEN=123456789:AA-REPLACE_WITH_REAL_BOT_TOKEN +#TELEGRAM_CHAT_ID=123456789 + +# --- optional overrides (defaults are sensible; uncomment only to change) --- +#BASE_URL=https://gendsgn.ru +#STATE_FILE=/var/tmp/gendesign-uptime-state +#CURL_TIMEOUT=15 +#RETRIES=2 +#RETRY_SLEEP=5 +# Custom check list (newline-separated "label|path|expected_status"): +#CHECKS="health|/health|200 +#market-pulse|/api/v1/analytics/market-pulse|200" diff --git a/ops/uptime-healthcheck.sh b/ops/uptime-healthcheck.sh new file mode 100755 index 00000000..97468d89 --- /dev/null +++ b/ops/uptime-healthcheck.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# External uptime watchdog for gendesign (#75 B6-1, lightweight fallback). +# +# WHY THIS EXISTS alongside Uptime Kuma (docker-compose.uptime.yml): Kuma runs +# ON the prod VM, so if the whole VM dies it can't alert. This script is meant to +# run from cron on a DIFFERENT host (your laptop, a tiny free-tier box, Beget +# shared-host cron) and hit the PUBLIC URLs over the internet — last-resort +# "весь хост лёг" detection. Kuma covers rich per-endpoint/SSL/latency monitoring; +# this covers the case Kuma structurally can't. +# +# Self-contained: only needs `curl` + `bash`. No docker, no repo checkout. +# State (last-known status per check) lives in a file so we alert on TRANSITIONS +# (up→down, down→up) — not every run — to avoid Telegram spam. +# +# Usage (cron — note `bash`, not a bare path, so a missing +x bit can't break it): +# * * * * * bash /path/to/uptime-healthcheck.sh >> /var/log/gendesign-uptime.log 2>&1 +# +# Telegram alerting — set these in an env file (NOT in git, chmod 600): +# TELEGRAM_BOT_TOKEN=123456:ABC... +# TELEGRAM_CHAT_ID=123456789 +# Default env path: /etc/default/gendesign-uptime (override via UPTIME_ENV_FILE). +# A redacted template lives at ops/gendesign-uptime.default.example. +# Without a token set, the script still logs up/down but sends no alert. + +set -euo pipefail + +# --- config (env-overridable) --- +UPTIME_ENV_FILE="${UPTIME_ENV_FILE:-/etc/default/gendesign-uptime}" +# shellcheck source=/dev/null +[[ -f "$UPTIME_ENV_FILE" ]] && source "$UPTIME_ENV_FILE" + +BASE_URL="${BASE_URL:-https://gendsgn.ru}" +STATE_FILE="${STATE_FILE:-/var/tmp/gendesign-uptime-state}" +CURL_TIMEOUT="${CURL_TIMEOUT:-15}" # seconds per request (connect+read) +RETRIES="${RETRIES:-2}" # extra attempts before declaring DOWN +RETRY_SLEEP="${RETRY_SLEEP:-5}" # seconds between attempts +TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}" +TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}" + +# Checks to probe. Format per line: "