{"openapi":"3.1.0","info":{"title":"Пробиваем","description":"Независимая проверка товаров на маркетплейсах","version":"0.1.0"},"paths":{"/api/analyze":{"post":{"tags":["analyze"],"summary":"Analyze","operationId":"analyze_api_analyze_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyzeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisResult"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/ingest":{"post":{"tags":["analyze"],"summary":"Ingest","description":"Accept a pre-scraped card + reviews from the browser extension.\n\nThe extension has already done the parsing on the user's IP (works around\nOzon / Megamarket WAFs that block our server). We just run the same\ndetector -> LLM -> cache pipeline as /api/analyze.","operationId":"ingest_api_ingest_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisResult"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/recheck":{"post":{"tags":["analyze"],"summary":"Recheck","description":"Smart recheck: WB → force-fresh; non-WB → cache-fresh-or-queue.\n\nReturns:\n  {status: \"fresh\"|\"cached\"|\"queued\"|\"no_extension\",\n   analysis_id, short_id, url, marketplace, ...}\n\nStatus semantics:\n  - \"fresh\"  → 200, AnalysisResult dict в `result`, попытка списана.\n  - \"cached\" → 200, AnalysisResult dict в `result`, попытка НЕ списана\n               (cache_age_min < 60), `cache_age_min` = возраст в минутах.\n  - \"queued\" → 202, task_id для polling /api/recheck/status/{task_id};\n               попытка спишется при successful submit.\n  - \"no_extension\" → 200 (не ошибка для UI, чтобы фронт показал модалку);\n               `extension_url` для CTA. Попытка не списывается.\n\nHTTP codes:\n  402 — квота исчерпана (через require_check_quota_dry).\n  429 — rate-limit (1 recheck/30s per (user,analysis_id)).\n  400 — нет ни analysis_id ни url.\n  404 — analysis_id передан, но в БД нет такой записи.","operationId":"recheck_api_recheck_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RecheckRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/recheck/status/{task_id}":{"get":{"tags":["analyze"],"summary":"Recheck Status","description":"Polling endpoint для status='queued' рецепкa.\n\nВозвращает:\n  - {status: \"pending\"} — task ещё в очереди (или processing).\n  - {status: \"done\", result: {...AnalysisResult...}, quota_used: 1} —\n    extension прислал payload, мы прогнали analysis pipeline, списали попытку.\n  - {status: \"errored\", error: \"...\"} — extension вернул error / task expired.\n  - {status: \"expired\"} — task_id не найден (TTL 5 минут истёк).\n\nFrontend должен polling'овать каждые 2с. Тщательно: caller не должен\nблокироваться долго — если result ещё не готов, мы возвращаем pending\nмоментально (без BRPOP).","operationId":"recheck_status_api_recheck_status__task_id__get","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string","title":"Task Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/health":{"get":{"tags":["analyze"],"summary":"Health","operationId":"health_api_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health Api Health Get"}}}}}}},"/api/v1/capabilities":{"get":{"tags":["Monitoring v1"],"summary":"Capabilities","operationId":"capabilities_api_v1_capabilities_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"402":{"description":"PRO or analysis quota required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"403":{"description":"Account or origin denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"409":{"description":"Idempotency conflict or slot limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"422":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"429":{"description":"Rate or queue limit (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"500":{"description":"Temporary server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"503":{"description":"Dependency unavailable (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}}}}},"/api/v1/monitoring/products":{"get":{"tags":["Monitoring v1"],"summary":"Products","operationId":"products_api_v1_monitoring_products_get","security":[{"ProbiwaemBearer":[]},{"ProbiwaemKey":[]}],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Cursor"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}},{"name":"role","in":"query","required":false,"schema":{"anyOf":[{"enum":["watch","own","competitor"],"type":"string"},{"type":"null"}],"title":"Role"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductList"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"402":{"description":"PRO or analysis quota required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"403":{"description":"Account or origin denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"409":{"description":"Idempotency conflict or slot limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"422":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"429":{"description":"Rate or queue limit (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"500":{"description":"Temporary server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"503":{"description":"Dependency unavailable (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}}}}},"/api/v1/monitoring/imports":{"post":{"tags":["Monitoring v1"],"summary":"Create Import","operationId":"create_import_api_v1_monitoring_imports_post","security":[{"ProbiwaemBearer":[]},{"ProbiwaemKey":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128,"pattern":"^[A-Za-z0-9._:-]+$","title":"Idempotency-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportBody"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJob"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"402":{"description":"PRO or analysis quota required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"403":{"description":"Account or origin denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"409":{"description":"Idempotency conflict or slot limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"422":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"429":{"description":"Rate or queue limit (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"500":{"description":"Temporary server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"503":{"description":"Dependency unavailable (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"200":{"description":"Existing job for this idempotency key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJob"}}}}}}},"/api/v1/monitoring/imports/{job_id}":{"get":{"tags":["Monitoring v1"],"summary":"Import Status","operationId":"import_status_api_v1_monitoring_imports__job_id__get","security":[{"ProbiwaemBearer":[]},{"ProbiwaemKey":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJob"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"402":{"description":"PRO or analysis quota required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"403":{"description":"Account or origin denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"409":{"description":"Idempotency conflict or slot limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"422":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"429":{"description":"Rate or queue limit (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"500":{"description":"Temporary server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"503":{"description":"Dependency unavailable (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}}}}},"/api/v1/monitoring/products/{product_id}":{"patch":{"tags":["Monitoring v1"],"summary":"Update Product","operationId":"update_product_api_v1_monitoring_products__product_id__patch","security":[{"ProbiwaemBearer":[]},{"ProbiwaemKey":[]}],"parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"integer","title":"Product Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductUpdated"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"402":{"description":"PRO or analysis quota required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"403":{"description":"Account or origin denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"409":{"description":"Idempotency conflict or slot limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"422":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"429":{"description":"Rate or queue limit (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"500":{"description":"Temporary server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"503":{"description":"Dependency unavailable (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}}}},"delete":{"tags":["Monitoring v1"],"summary":"Remove Product","operationId":"remove_product_api_v1_monitoring_products__product_id__delete","security":[{"ProbiwaemBearer":[]},{"ProbiwaemKey":[]}],"parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"integer","title":"Product Id"}}],"responses":{"204":{"description":"Successful Response"},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"402":{"description":"PRO or analysis quota required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"403":{"description":"Account or origin denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"409":{"description":"Idempotency conflict or slot limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"422":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"429":{"description":"Rate or queue limit (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"500":{"description":"Temporary server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}},"503":{"description":"Dependency unavailable (Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationError"}}}}}}},"/api/v1/reviews/workbench":{"post":{"tags":["Private review workbench"],"summary":"Analyze Import","operationId":"analyze_import_api_v1_reviews_workbench_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewImport"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkbenchReport"}}}},"413":{"description":"Maximum 1.1 MB request body"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ProbiwaemBearer":[]},{"ProbiwaemKey":[]}]}},"/api/admin/ops/security/status":{"get":{"tags":["admin-security"],"summary":"Status","operationId":"status_api_admin_ops_security_status_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/security/incidents":{"get":{"tags":["admin-security"],"summary":"Incidents","operationId":"incidents_api_admin_ops_security_incidents_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/security/incidents/{incident_id}/decision":{"post":{"tags":["admin-security"],"summary":"Decision","operationId":"decision_api_admin_ops_security_incidents__incident_id__decision_post","parameters":[{"name":"incident_id","in":"path","required":true,"schema":{"type":"string","title":"Incident Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/security/test":{"post":{"tags":["admin-security"],"summary":"Test Delivery","operationId":"test_delivery_api_admin_ops_security_test_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/security/deliveries/{delivery_id}/retry":{"post":{"tags":["admin-security"],"summary":"Retry Delivery","operationId":"retry_delivery_api_admin_ops_security_deliveries__delivery_id__retry_post","parameters":[{"name":"delivery_id","in":"path","required":true,"schema":{"type":"string","title":"Delivery Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/security/support":{"get":{"tags":["admin-security"],"summary":"Support","operationId":"support_api_admin_ops_security_support_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/security/support/{ticket_id}/reply":{"post":{"tags":["admin-security"],"summary":"Reply","operationId":"reply_api_admin_ops_security_support__ticket_id__reply_post","parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","title":"Ticket Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplyBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/visitors":{"get":{"tags":["admin-visitors"],"summary":"Visitors","operationId":"visitors_api_admin_ops_visitors_get","parameters":[{"name":"minutes","in":"query","required":false,"schema":{"type":"integer","maximum":30,"minimum":1,"default":5,"title":"Minutes"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":100,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/anon-checks":{"get":{"tags":["public"],"summary":"Public Anon Checks","description":"v0.23.6: Список последних анонимных проверок текущего pb_anon cookie.\n\nИспользуется на лендинге (index.html) — показывает юзеру: «Сегодня вы\nпроверили N товаров» с 3-5 миниатюрами. Цель — мотивировать\nрегистрироваться, чтобы история не потерялась.\n\nБез auth — наоборот, специально для НЕзалогиненных. Залогиненные\nюзеры читают свою историю через /api/my/* (там user_id-based access).\n\nReturns:\n    {\n        \"checks\": [{short_id, title, marketplace, verdict, suspicion_pct,\n                    image_url, created_at_iso, ago_human}, ...],\n        \"total_24h\": int,                  # счётчик за last 24h\n        \"has_anon\": bool,                  # есть ли вообще pb_anon cookie\n    }\n\nБД может вернуть пусто (юзер только что зашёл) — это нормально.","operationId":"public_anon_checks_api_public_anon_checks_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":5,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Anon Checks Api Public Anon Checks Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/stats":{"get":{"tags":["public"],"summary":"Public Stats","description":"Агрегаты проверок: total / today / last_7d / verdict_breakdown.\n\nЕсли БД не настроена (database_url пустой) или падает — возвращаем\nfallback с round-number, чтобы UI не моргал пустотой.","operationId":"public_stats_api_public_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Stats Api Public Stats Get"}}}}}}},"/api/public/live-stats":{"get":{"tags":["public"],"summary":"Public Live Stats","description":"Live-stats ticker — agg + recent_avoids в одном ответе.\n\nReturns:\n    {\n        \"checks_today\": int,            # SUM count за UTC-сутки\n        \"avoids_today\": int,            # subset с verdict='avoid'\n        \"fakes_found_24h\": int,         # last 24h, avoid AND suspicion>=60\n        \"recent_avoids\": [{\n            \"short_id\": \"aBc2K\",\n            \"product_name_truncated\": str,  # first 40 chars\n            \"marketplace\": \"wildberries\",\n            \"suspicion_pct\": int,\n            \"minutes_ago\": int,\n        }, ...up to 5],\n        \"ts\": \"ISO-8601 UTC\",\n    }","operationId":"public_live_stats_api_public_live_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Live Stats Api Public Live Stats Get"}}}}}}},"/api/public/recent-analyses":{"get":{"tags":["public"],"summary":"Public Recent Analyses","description":"Публичная лента — все проверки которые админ не скрыл.\n\nv0.12.5: убран TTL 48ч. Теперь лента показывает ВСЁ что юзеры пробивали,\nотсортировано по created_at DESC. Старые карточки контролирует админ через\n`/admin/feed` — кнопкой «Скрыть» переводит is_hidden=True, и они выпадают\nиз ленты. Это даёт лендингу больше «социального доказательства» (бесконечная\nистория проверок) + админу полный контроль над тем что висит публично.\n\nЕсли проверка устарела фактически (цена, рейтинг изменились) — админ её\nскрывает руками. Автоматический TTL не использовался эвристически\nхорошо: товары которые «протухают» за 48ч редкие, а нормальные —\nостаются актуальными месяцами (накрутка отзывов держится годами).\n\nv0.14.16: optional фильтр `verdict=trusted|caution|avoid` для UI-табов\n«Можно брать / Осторожно / Не брать» на /recent.","operationId":"public_recent_analyses_api_public_recent_analyses_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":12,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}},{"name":"verdict","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Recent Analyses Api Public Recent Analyses Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/recent-timeline":{"get":{"tags":["public"],"summary":"Public Recent Timeline","description":"Бакеты проверок за указанное окно.\n\n`window` ∈ {\"24h\",\"48h\",\"7d\"}. Для 24h/48h — часовые бакеты, для 7d —\nдневные. Всегда возвращаем ровно `span` элементов (бакеты без данных\nполучают count=0), самый левый — самый старый.\nДополнительно `total` — суммарное количество проверок за окно, чтобы\nфронту было проще решать о fallback'ах без повторного счёта.","operationId":"public_recent_timeline_api_public_recent_timeline_get","parameters":[{"name":"window","in":"query","required":false,"schema":{"type":"string","default":"24h","title":"Window"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Recent Timeline Api Public Recent Timeline Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/featured":{"get":{"tags":["public"],"summary":"Public Featured","description":"Последние N featured-проверок за 7 дней.\n\nTTL 7 дней — чтобы старая «рекомендация редакции» не висела месяцами, но\nадмин видит полный список без TTL на /admin/featured. Лимит 1-12 (больше\nне нужно, грид на главной отображает 6).","operationId":"public_featured_api_public_featured_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":6,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Featured Api Public Featured Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/demo-urls":{"get":{"tags":["public"],"summary":"Public Demo Urls","description":"N свежих URL'ов из недавних проверок с verdict in (red, yellow).\n\nИспользуется кнопкой «Попробовать на примере» на главной. Берём\nподозрительные товары за 14 дней — на них Score низкий, демонстрация\nполучается показательной (если бы давали зелёные товары — юзер бы\nподумал «ну и что тут полезного»).\n\nv0.14.57 fix: было `verdict in ('red', 'yellow')` — но VerdictEnum\nиспользует `trusted / caution / avoid / unknown`. Запрос всегда возвращал\nпусто, фронт фолбэчился на захардкоженные WB-URL.\n\nv0.14.70 fix: фильтр по marketplace IN (wildberries, yandex_market).\nOzon и Мегамаркет на сайте НЕ ПРОВЕРЯЮТСЯ (антибот блокирует), а\nкнопка «Попробовать на примере» работает именно через сайт. Юзер\nкликал и получал Ozon-URL, который висел и ничего не показывал.\n\nКэш на 5 мин (RANDOM() ORDER BY дорогой запрос); один кэш на ВСЕ limit'ы,\nподрезаем при выдаче.","operationId":"public_demo_urls_api_public_demo_urls_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":5,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Demo Urls Api Public Demo Urls Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/check-by-url":{"get":{"tags":["public"],"summary":"Public Check By Url","description":"Проверяет, есть ли свежий (≤48ч) публичный анализ для данного URL\nтовара. Возвращает `{found, analysis_id, created_at, hours_ago, result}`.\n\nИспользуется фронтом на главной: перед вызовом тяжёлого `/api/analyze`\nбыстро узнать, что товар уже пробит — показать юзеру с бейджем\n«проверено N часов назад» + кнопка «пробить заново».\n\nДля Ozon variant-товаров: если по точному external_id не нашли, пробуем\nпо **slug-base** (cлаг карточки без хвоста-id). Разные цвета/размеры\nодной карточки дают общий slug, а reviews на них идут из одного пула\n(с reviewsVariantMode=2), значит кеш применим.\n\nv0.6.23 (extension): добавлен `request: Request` чтобы извлечь pb_session\ncookie / X-Ext-Token и подмешать current `user_tier` в response.\nРаньше: `result.user_tier` отдавался из row.result_json — там лежал тариф\nПЕРВОГО проверявшего юзера (обычно free), и Pro-юзеры видели расширение\nкак Free на cached товарах. Теперь резолвим current user'а из request.","operationId":"public_check_by_url_api_public_check_by_url_get","parameters":[{"name":"url","in":"query","required":true,"schema":{"type":"string","title":"Url"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Check By Url Api Public Check By Url Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/bulk-check-by-url":{"post":{"tags":["public"],"summary":"Public Bulk Check By Url","description":"Батч-вариант `/check-by-url` для расширения: принимает до 20 URL,\nвозвращает массив `{url, found, analysis_id, pct, verdict}`.\n\nЦель — extension в каталоге: мелкий % рядом с карточкой товара, только\nдля уже пробитых. Ничего не скрейпит, никаких тяжёлых вызовов.\n\nAnti-abuse (v0.16+): отдельный per-IP rate-limit 60 batch'ей/час\n(поверх middleware-квоты).","operationId":"public_bulk_check_by_url_api_public_bulk_check_by_url_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCheckRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Bulk Check By Url Api Public Bulk Check By Url Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/check/{analysis_id}":{"get":{"tags":["public"],"summary":"Public Analysis Detail","description":"Полный результат одной проверки для публичной share-страницы.\n\nВозвращаем тот же AnalysisResult-shape что /api/analyze.\n\nv0.12.17: убрали фильтр `created_at >= cutoff` (раньше отрезал всё старше 48ч).\nЭто была рассинхронизация с `public_recent_analyses` (там фильтр тоже убрали),\nиз-за чего юзер видел свою же старую проверку из ленты как «скрыта/удалена».\nТеперь отдаём любой age. Скрытые (is_hidden=True) — 404 с meta для UI\n(товар можно открыть на маркетплейсе или перепробить).","operationId":"public_analysis_detail_api_public_check__analysis_id__get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Analysis Detail Api Public Check  Analysis Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/check/{analysis_id}/og.png":{"get":{"tags":["public"],"summary":"Public Check Og","description":"Legacy OG-PNG endpoint. v0.16.2: 301-редиректим на /p/{short}/og.png —\nканонический рендер живёт там (Pillow + product image overlay), здесь\nтолько устаревший renderer с overlap'ом текста.\n\nTG/WhatsApp/Slack кэшируют og:image агрессивно — старые шары до сих пор\nтыкают в /api/public/check/{id}/og.png. Редирект 301 заставит их\nпересохранить новый URL и подгрузить актуальную карточку.\n\nЕсли short_id не получится сэнкодить (теоретически невозможно для\nположительного int) — fallback на статичный `/static/og-home.png`.","operationId":"public_check_og_api_public_check__analysis_id__og_png_get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"tags":["public"],"summary":"Public Check Og","description":"Legacy OG-PNG endpoint. v0.16.2: 301-редиректим на /p/{short}/og.png —\nканонический рендер живёт там (Pillow + product image overlay), здесь\nтолько устаревший renderer с overlap'ом текста.\n\nTG/WhatsApp/Slack кэшируют og:image агрессивно — старые шары до сих пор\nтыкают в /api/public/check/{id}/og.png. Редирект 301 заставит их\nпересохранить новый URL и подгрузить актуальную карточку.\n\nЕсли short_id не получится сэнкодить (теоретически невозможно для\nположительного int) — fallback на статичный `/static/og-home.png`.","operationId":"public_check_og_api_public_check__analysis_id__og_png_get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/check/{analysis_id}/pdf":{"get":{"tags":["public"],"summary":"Public Check Pdf","description":"PDF-разбор товара (attachment). Redis-кеш 1 ч. 404 если hidden/нет.\n\nv0.20.x: убран 48h cutoff (синхронно с /check/{id} — см. v0.12.17).\nРаньше юзер открывал /p/{short} (там cutoff'а нет), кликал «Скачать PDF» —\nи получал 404, потому что Analysis старше 48h. Теперь PDF доступен\nстолько же, сколько живёт сама share-страница: пока не is_hidden=True.\n\nContent-Disposition: attachment — скачивание, а не inline-просмотр.\nНа mobile inline в Chrome открывается криво (PDF.js не везде есть);\nTelegram-share отправляет файлом тоже корректнее с attachment.\n\nSecurity audit fix (2026-05-03): эндпоинт раньше был полностью публичный —\nUI скрывал ссылку для не-Pro юзеров через {% if is_user_pro %}, но любой\nмог curl'нуть `/api/public/check/{id}/pdf` и получить PDF без подписки.\nТеперь явно гейтим Pro: 401 если нет сессии, 402 (pro_required) если\nне Pro. Платный контент должен быть платным.","operationId":"public_check_pdf_api_public_check__analysis_id__pdf_get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/seller/{seller_id}":{"get":{"tags":["public"],"summary":"Public Seller Dossier","description":"Расширенное досье продавца (публично, без auth).\n\nPath:\n    /api/public/seller/{seller_id}?marketplace=wildberries\n\nПараметр `marketplace` опционален. Если задан — фильтруем только этим\nМП (полезно когда тот же seller_id может встречаться на разных МП).\nЕсли не задан — агрегируем по всем МП.\n\nReturns:\n    Структура из app.services.seller_dossier.build_seller_dossier\n    (см. модуль docstring сервиса).","operationId":"public_seller_dossier_api_public_seller__seller_id__get","parameters":[{"name":"seller_id","in":"path","required":true,"schema":{"type":"string","title":"Seller Id"}},{"name":"marketplace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketplace"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Seller Dossier Api Public Seller  Seller Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/check/{aid}/seller-summary":{"get":{"tags":["public"],"summary":"Public Check Seller Summary","description":"Pro-only — краткое summary продавца товара из проверки aid.\n\nResolve чейна:\n    analyses[aid].result_json → product → seller_id  →\n        services.seller_dossier.build_seller_dossier\n        (тот же сервис что /api/public/seller/{seller_id})\n\nКонтракт ответа:\n    {\n      \"analysis_id\": 142,\n      \"seller_name\": \"Fitness Shock\",\n      \"seller_id\": \"12345\",\n      \"marketplace\": \"wildberries\",\n      \"total_products\": 23,           # сколько товаров продавца у нас\n      \"avg_score\": 67,                # средний Probiwaem Score (0-100)\n      \"avoid_pct\": 13,                # % verdict4=\"avoid\"\n      \"checked_count\": 8              # кол-во НЕ-avoid (доступных к покупке)\n    }\n\nEdge cases:\n  • aid не найден / hidden → 404.\n  • result_json без seller_id (старая запись, или маркет не отдал) →\n    404 «Продавец неизвестен» — UI покажет плейсхолдер.\n  • Не-Pro / аноним → 401.","operationId":"public_check_seller_summary_api_public_check__aid__seller_summary_get","parameters":[{"name":"aid","in":"path","required":true,"schema":{"type":"integer","title":"Aid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Check Seller Summary Api Public Check  Aid  Seller Summary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/alternatives/{analysis_id}":{"get":{"tags":["public"],"summary":"Public Alternatives","description":"Топ-N альтернатив с лучшим Probiwaem Score.\n\nPublic endpoint. Free-юзер получает одну карточку + paywall-hint\n(«Ещё N альтернатив в Pro»). Pro-юзер — все.\n\nResponse:\n    {\n      \"items\": [\n        {analysis_id, name, image_url, pb_score, verdict4_label,\n         url, score_delta}, ...\n      ],\n      \"total_found\": int,         # сколько нашли всего (для hint'а)\n      \"is_pro\": bool,\n      \"paywall_hint\": str | null, # текст «Ещё N в Pro» если Free\n    }","operationId":"public_alternatives_api_public_alternatives__analysis_id__get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":3,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Alternatives Api Public Alternatives  Analysis Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/mirrors/{analysis_id}":{"get":{"tags":["public"],"summary":"Public Mirrors","description":"Зеркала текущего товара на других маркетплейсах.\n\nВозвращает {\"items\": [...], \"source\": {...}} где каждый item:\n    marketplace, url, pb_score, verdict, similarity_score, matched_by, name.\n\nЕсли БД недоступна, analysis старше 48ч / hidden — 404.\nЕсли у товара не извлечён brand/name — пустой items (искать нечем).","operationId":"public_mirrors_api_public_mirrors__analysis_id__get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Mirrors Api Public Mirrors  Analysis Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/leads/seller":{"post":{"tags":["public"],"summary":"Create Seller Lead","description":"Принять заявку с лендинга /for-sellers.\n\nДелает: валидация → INSERT в seller_leads → fire-and-forget уведомления\n(email + TG). Возвращает {ok, message} — фронт показывает success-toast.\n\nЗащита от спама:\nv0.12.10: middleware /api/public/* в exempt_prefixes (см. config.py) —\nто есть TrafficAndBanMiddleware НЕ срабатывает на эту ручку. Без локального\nrate-limit спам-бот может слить базу seller_leads и забить SMTP/TG.\nПоэтому здесь явный per-IP лимит через Redis: 5 заявок/час, 20/сутки.","operationId":"create_seller_lead_api_public_leads_seller_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SellerLeadRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Create Seller Lead Api Public Leads Seller Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/headline-stats":{"get":{"tags":["public"],"summary":"Public Headline Stats","description":"3-5 готовых строк для top-bar банера с реальными числами из БД.\n\nВозвращает {stats: [{icon, text, tone}], ts}. Если данных мало (<100\nпроверок всего) — stats: [] и фронт прячет банер. Кешируется в Redis\nна 5 мин — агрегаты не меняются от запроса к запросу значимо.","operationId":"public_headline_stats_api_public_headline_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Headline Stats Api Public Headline Stats Get"}}}}}}},"/api/public/search":{"get":{"tags":["public"],"summary":"Public Search","description":"Поиск товаров на маркетплейсе с overlay из наших вердиктов.\n\nArgs:\n    q: Текстовый запрос (2-100 chars).\n    mp: Маркетплейс. Сейчас только \"wb\" (Wildberries). Я.Маркет — TODO.\n    limit: Сколько товаров вернуть. Cap = 20.\n\nReturns:\n    {\n        \"query\": str,                    # нормализованный q\n        \"marketplace\": \"wb\",\n        \"items\": [\n            {\n                \"id\": \"wb_12345\",\n                \"external_id\": \"12345\",\n                \"name\": str,\n                \"brand\": str,\n                \"price_kopecks\": int,\n                \"rating\": float,         # 0..5\n                \"review_count\": int,\n                \"image_url\": str,        # WB CDN, может быть \"\"\n                \"marketplace_url\": str,  # https://www.wildberries.ru/catalog/12345/detail.aspx\n                \"probiwaem\": null | {\n                    \"short_id\": str,\n                    \"verdict\": \"trusted|caution|avoid|unknown\",\n                    \"suspicion_pct\": int,\n                    \"checked_at\": str,   # ISO-8601 UTC\n                }\n            }, ...\n        ],\n        \"cached\": bool,                  # был ли cache-hit\n        \"ts\": str,                       # ISO-8601 UTC\n    }\n\nErrors:\n    400 — невалидный query (короткий/длинный/пустой) или mp не \"wb\".\n    429 — rate-limit превышен.\n    503 — WB не отвечает (timeout/non-200/невалидный JSON).","operationId":"public_search_api_public_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","title":"Q"}},{"name":"mp","in":"query","required":false,"schema":{"type":"string","default":"wb","title":"Mp"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Public Search Api Public Search Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/ext-uninstall-feedback":{"post":{"tags":["public"],"summary":"Ext Uninstall Feedback","operationId":"ext_uninstall_feedback_api_public_ext_uninstall_feedback_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtUninstallFeedback"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Ext Uninstall Feedback Api Public Ext Uninstall Feedback Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/shorts/feed":{"get":{"tags":["shorts"],"summary":"Shorts Feed","description":"Curated shorts feed.","operationId":"shorts_feed_api_shorts_feed_get","parameters":[{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"Cursor"}},{"name":"seen","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Seen"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":20,"minimum":1,"default":7,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Shorts Feed Api Shorts Feed Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/shorts/discover":{"get":{"tags":["shorts"],"summary":"Shorts Discover","description":"Grid-page feed (фильтры/сортировка/cursor pagination). Anon-friendly.","operationId":"shorts_discover_api_shorts_discover_get","parameters":[{"name":"marketplace","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Marketplace"}},{"name":"verdict","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Verdict"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","maxLength":24,"default":"recent","title":"Sort"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"Cursor"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":24,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Shorts Discover Api Shorts Discover Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/shorts/me/interactions":{"get":{"tags":["shorts"],"summary":"Shorts Me Interactions","description":"Batch lookup like/save state. Anon → пустой ответ (без 401).","operationId":"shorts_me_interactions_api_shorts_me_interactions_get","parameters":[{"name":"short_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Short Ids"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Shorts Me Interactions Api Shorts Me Interactions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/shorts/me/saved":{"get":{"tags":["shorts"],"summary":"Shorts Me Saved","description":"Paginated лента сохранённых shorts. Anon → 401.","operationId":"shorts_me_saved_api_shorts_me_saved_get","parameters":[{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"Cursor"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Shorts Me Saved Api Shorts Me Saved Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/shorts/event":{"post":{"tags":["shorts"],"summary":"Shorts Event","description":"Analytics event log + Redis aggregate counters (views, shares).","operationId":"shorts_event_api_shorts_event_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Shorts Event Api Shorts Event Post"}}}}}}},"/api/shorts/{short_id}/counters":{"get":{"tags":["shorts"],"summary":"Shorts Counters","description":"Aggregate likes/views/shares счётчики. Anon-friendly.\n\nv0.25.67: URL принимает base62 short_id → strict decode.","operationId":"shorts_counters_api_shorts__short_id__counters_get","parameters":[{"name":"short_id","in":"path","required":true,"schema":{"type":"string","title":"Short Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Shorts Counters Api Shorts  Short Id  Counters Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/shorts/{short_id}/like":{"post":{"tags":["shorts"],"summary":"Shorts Like Toggle","description":"Toggle like для залогиненного юзера. Anon → 401.","operationId":"shorts_like_toggle_api_shorts__short_id__like_post","parameters":[{"name":"short_id","in":"path","required":true,"schema":{"type":"string","title":"Short Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Shorts Like Toggle Api Shorts  Short Id  Like Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/shorts/{short_id}/save":{"post":{"tags":["shorts"],"summary":"Shorts Save Toggle","description":"Toggle save. Anon → 401.","operationId":"shorts_save_toggle_api_shorts__short_id__save_post","parameters":[{"name":"short_id","in":"path","required":true,"schema":{"type":"string","title":"Short Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Shorts Save Toggle Api Shorts  Short Id  Save Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/shorts/{short_id}":{"get":{"tags":["shorts"],"summary":"Shorts Single","description":"Single short payload для deep-link `/shorts/{short_id}`.","operationId":"shorts_single_api_shorts__short_id__get","parameters":[{"name":"short_id","in":"path","required":true,"schema":{"type":"string","title":"Short Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Shorts Single Api Shorts  Short Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/ext/heartbeat":{"post":{"tags":["ext-telemetry"],"summary":"Heartbeat","description":"Идемпотентный upsert строки `ext_devices` по `device_uuid`.\n\nВозвращает `{ok, throttled, device_id}`. `throttled=True` означает что\nзапрос пришёл до истечения rate-limit окна — отвечаем 200 чтобы\nклиент не ретраил; состояние в БД при этом не меняется.\n\nv0.14.9: если device_uuid в device_ban — возвращаем 403, расширение\nв этом устройстве полностью отключается (background.js видит 403 →\nпереходит в disabled-state).","operationId":"heartbeat_api_ext_heartbeat_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartbeatBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/ext/events":{"post":{"tags":["ext-telemetry"],"summary":"Ext Events","description":"Батч funnel-событий. Всегда 200, fire-and-forget на стороне клиента.","operationId":"ext_events_api_ext_events_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtEventsBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/ext/error":{"post":{"tags":["ext-telemetry"],"summary":"Ext Error","description":"Принимает отчёт об ошибке расширения. Всегда отвечает 200 если\nзапрос не явный мусор — лучше сохранить «слегка кривой» payload, чем\n422-ить и потерять данные о баге.\n\nSprint-1 hotfix (agent-A): см. ExtErrorBody docstring — за 12 дней\n422 потеряли 90+ отчётов. Теперь:\n- схема расслаблена (все Optional);\n- дефолты безопасные ('unknown' для scope/message);\n- при любой ошибке БД возвращаем {ok:true,saved:false} вместо 500\n  (extension всё равно fire-and-forget, но 5xx раздували алерты).","operationId":"ext_error_api_ext_error_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtErrorBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/ext/scan/next-task":{"get":{"tags":["ext-scan"],"summary":"Next Task","description":"Возвращает {task_id, url, marketplace} или 204 No Content.\n\nHeader X-Device-Id обязателен.","operationId":"next_task_api_ext_scan_next_task_get","parameters":[{"name":"marketplaces","in":"query","required":false,"schema":{"type":"string","default":"ozon,megamarket,yandex_market","title":"Marketplaces"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/ext/scan/{task_id}/result":{"post":{"tags":["ext-scan"],"summary":"Submit Task Result","operationId":"submit_task_result_api_ext_scan__task_id__result_post","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string","title":"Task Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ResultBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/ext/scan/{task_id}/error":{"post":{"tags":["ext-scan"],"summary":"Submit Task Error","operationId":"submit_task_error_api_ext_scan__task_id__error_post","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string","title":"Task Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ErrorBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/subscribe":{"post":{"tags":["billing"],"summary":"Subscribe","description":"Создать подписку и вернуть PaymentURL для редиректа в T-Bank.\n\nOwner = current_user (из pb_session cookie). Email берём из user.email,\nНЕ из тела запроса — раньше юзер мог оформить подписку на чужой email.\n\nИдемпотентно: повторный вызов от того же user'а с PENDING-статусом\nпереиспользует Subscription (но создаёт новый Payment с новым OrderId).","operationId":"subscribe_api_billing_subscribe_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscribeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscribeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/extend":{"post":{"tags":["billing"],"summary":"Extend Subscription","operationId":"extend_subscription_api_billing_extend_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtendRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscribeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/webhook/tbank":{"post":{"tags":["billing"],"summary":"Tbank Webhook","description":"Callback от T-Bank.\n\nФормат: JSON в body, Token внутри. Проверка подписи — через billing.handle_notification.\n\nВсегда возвращает `\"OK\"` со статусом 200 — даже при невалидной подписи, чтобы:\n1. T-Bank не ретраил 30 дней (они ретраят при не-200)\n2. Атакующий не мог по HTTP-коду различить валидный/невалидный webhook\n\nЗащита от replay/spoof'инга:\n- IP whitelist (если settings.tbank_webhook_strict_ip = True) — отклоняем\n  запросы не из известных T-Bank диапазонов. По дефолту off для dev.\n- Replay-window: если за <5 секунд по тому же Payment мы уже получили\n  webhook, считаем дублём (T-Bank часто ретраит при сетевых таймаутах,\n  и наша БД уже зафиксировала результат).\n\nТелеметрия (валидный/нет) идёт в структурированные логи.","operationId":"tbank_webhook_api_billing_webhook_tbank_post","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/billing/cancel":{"post":{"tags":["billing"],"summary":"Cancel","description":"Отменить авто-продление текущего юзера. Доступ сохраняется до expires_at.\n\nOwner lookup по session user_id — раньше (до auth-gate) можно было передать\nчужой email в body и отменить чужую подписку.","operationId":"cancel_api_billing_cancel_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Cancel Api Billing Cancel Post"}}}}}}},"/api/billing/reactivate":{"post":{"tags":["billing"],"summary":"Reactivate","description":"Включить авто-продление обратно для ACTIVE (но auto_renew=false) подписки.\n\nOwner lookup по session user_id. 404 если подписки нет или она не в\nACTIVE/PAST_DUE.","operationId":"reactivate_api_billing_reactivate_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Reactivate Api Billing Reactivate Post"}}}}}}},"/api/billing/account":{"get":{"tags":["billing"],"summary":"Account","description":"Унифицированный статус подписки текущего юзера.\n\nAuth: читаем pb_session cookie. Если нет — возвращаем Free-ответ\n(не 401), чтобы extension popup мог без логина показать «Free тариф,\nвойди через Яндекс/VK для Pro».\n\nOwner lookup ВСЕГДА по user_id из session. email=? query больше не\nподдерживается (раньше была PII-leak: перебором можно было узнать\nчей тариф/expiry).","operationId":"account_api_billing_account_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}}}},"/api/billing/check-packs":{"get":{"tags":["billing"],"summary":"Check Packs","description":"Список доступных пакетов проверок для UI.\n\nБез auth — UI должен мочь показать карточки и неавторизованному юзеру.\nПокупка (`/check-packs/buy`) уже требует логин.\n\nВозвращает: ``{\"items\": [{...starter...}, {...pro...}, {...power...}]}``\nв фиксированном порядке возрастания объёма (см. `all_packs()`).","operationId":"check_packs_api_billing_check_packs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Check Packs Api Billing Check Packs Get"}}}}}}},"/api/billing/check-packs/buy":{"post":{"tags":["billing"],"summary":"Buy Pack","description":"Создать покупку пакета и вернуть PaymentURL для редиректа в T-Bank.\n\nOwner = current_user.id из сессии. receipt_email — только контакт для чека;\npack_id валидируется в `init_check_pack_purchase`.\n\nReturns: ``{\"payment_url\": \"https://securepay.tinkoff.ru/...\", \"purchase_id\": ...}``.\n\nЕсли billing выключен — `init_check_pack_purchase` вернёт fail-stub URL,\nэндпоинт всё равно отдаёт 200 (UI редиректит, юзер видит «оплата временно\nнедоступна» вместо 5xx-ошибки).","operationId":"buy_pack_api_billing_check_packs_buy_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuyPackRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Buy Pack Api Billing Check Packs Buy Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/balance":{"get":{"tags":["billing"],"summary":"My Balance","description":"Текущий баланс проверок + breakdown по 3 пулам.\n\nv0.14.64: вместо одного `balance` числа отдаём прозрачный breakdown:\n    • balance              — `pack_balance` (для backward-compat, deprecated)\n    • pack_balance         — купленные пакеты + перенос с прошлого месяца\n    • monthly_used         — потрачено из квоты в текущем календарном месяце\n    • monthly_quota        — 300 для Pro, null для Free\n    • monthly_remaining    — 300 - monthly_used\n    • monthly_bonus_balance — отдельный пул накопленных +100 бонусов\n    • period_start         — ISO date начала текущего периода (для UI)\n    • period_end           — ISO date следующего сброса квоты (1-го числа)\n    • total_available      — сумма всех пулов\n    • is_pro / used_last_48h / next_free_at — старые поля (compat).\n\nSprint-1 hardening (agent-A): три раза за 12 дней падало с\nUndefinedColumnError \"user_id\" — миграционный drift / частичный\nrollout. Теперь каждый из трёх SQL-блоков обёрнут try/except и при\nсхеме-сбое отдаём максимально честный fallback (нули + is_pro=false)\nвместо 500. Это позволяет /account и popup'у extension'а рендерить\n«0 проверок» вместо ошибки — юзер сможет купить пакет даже если\nbreakdown временно сломан.","operationId":"my_balance_api_billing_balance_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Balance Api Billing Balance Get"}}}}}}},"/api/billing/anon-quota":{"get":{"tags":["billing"],"summary":"Anon Quota","description":"Сколько бесплатных проверок осталось у анонимного юзера за 24ч.\n\nБез auth — наоборот, специально для НЕзалогиненных. Используется на\nлендинге quota_hint.js: показать «X из 10 проверок без регистрации»\nрядом с формой пробива.\n\nВозвращает:\n    - quota         — лимит anon-проверок в сутки (из settings.anon_checks_per_24h).\n    - used          — сколько потрачено за последние 24ч под этим pb_anon cookie.\n    - remaining     — max(0, quota - used).\n    - wait_until    — ISO-время освобождения первого слота (oldest + 24h),\n                      либо null если ещё есть запас.\n    - has_cookie    — есть ли вообще pb_anon (false → юзер только что зашёл).","operationId":"anon_quota_api_billing_anon_quota_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Anon Quota Api Billing Anon Quota Get"}}}}}}},"/api/billing/referral/code":{"get":{"tags":["billing"],"summary":"My Referral Code","description":"Реферальный код юзера + share-URL. Создаёт код если ещё нет.\n\nReturns: {\"code\": \"AB12CD34\", \"share_url\": \"https://probiwaem.ru/?ref=AB12CD34\"}","operationId":"my_referral_code_api_billing_referral_code_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Referral Code Api Billing Referral Code Get"}}}}}}},"/api/billing/referral/stats":{"get":{"tags":["billing"],"summary":"My Referral Stats","description":"Статистика приглашений: сколько друзей, сколько проверок заработал.\n\nReturns:\n  {\n    \"code\": str, \"share_url\": str,\n    \"invited_count\": int,\n    \"bonus_earned_total\": int,  # сумма bonus_referrer\n    \"recent_invites\": [{\"created_at\": iso, \"bonus\": int}, ...]  # до 10\n  }","operationId":"my_referral_stats_api_billing_referral_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Referral Stats Api Billing Referral Stats Get"}}}}}}},"/api/billing/status":{"get":{"tags":["billing"],"summary":"Status","description":"Legacy alias на /account — оставлен для старых клиентов.\n\nТребует auth как и все остальные billing ручки. email=? query больше\nне поддерживается (PII-leak через enumeration).","operationId":"status_api_billing_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionStatusResponse"}}}}}}},"/api/billing/purchases":{"get":{"tags":["billing"],"summary":"My Purchases","description":"Список покупок юзера: подписочные платежи (Payment) + пакеты\n(CheckPackPurchase), отсортированы по дате убыв.\n\nВозвращает:\n    items: [\n        { kind: 'subscription' | 'pack',\n          id: int,\n          title: str,             # «Подписка Продвинутый» / «Пакет 50 проверок»\n          amount_rubles: int,\n          status: 'confirmed'|'rejected'|'refunded'|'new'|'created',\n          created_at: ISO8601,\n          order_id: str,           # для саппорта\n          meta: { tier: 'pro'?, is_annual: bool?, pack_id: str?, checks: int? }\n        }, ...\n    ]\n    count: int","operationId":"my_purchases_api_billing_purchases_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Purchases Api Billing Purchases Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/redeem-promo":{"post":{"tags":["billing"],"summary":"Redeem Promo","description":"Юзер вводит промо-код. Применяет один из бонусов:\n  • free_checks: +N проверок к балансу\n  • pro_days: +N дней Pro\nИдемпотентно: повторный apply того же кода = «уже применён», без double-bonus.","operationId":"redeem_promo_api_billing_redeem_promo_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Redeem Promo Api Billing Redeem Promo Post"}}}}}}},"/api/billing/partner-dashboard":{"get":{"tags":["billing"],"summary":"Partner Dashboard","description":"v0.25.83: дашборд партнёра в /account.\nВозвращает все коды юзера (если он owner_user_id) + агрегаты + recent.\nЕсли у юзера нет связанных кодов — `{\"is_partner\": false}`.","operationId":"partner_dashboard_api_billing_partner_dashboard_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Partner Dashboard Api Billing Partner Dashboard Get"}}}}}}},"/api/billing/my-applied-promos":{"get":{"tags":["billing"],"summary":"My Applied Promos","description":"v0.25.86: список промокодов, которые юзер применял (для секции\n«Промокоды» в /account). Показываем последние 20 — для большинства\nхватит, никто не применяет >5 кодов реалистично.","operationId":"my_applied_promos_api_billing_my_applied_promos_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Applied Promos Api Billing My Applied Promos Get"}}}}}}},"/api/billing/activate-trial":{"post":{"tags":["billing"],"summary":"Activate Trial","description":"Активировать триал Продвинутого в один клик (без карты).\n\nv0.29.9 (want-to-buy момент 402): юзер упёрся в лимит → в paywall-модале\nкнопка «3 дня Продвинутого бесплатно». grant_trial_days сам считает\nlifetime cap (30 дней на юзера навсегда) — злоупотребить нельзя.\nАктивная платная подписка → 409 (триал не нужен).","operationId":"activate_trial_api_billing_activate_trial_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Activate Trial Api Billing Activate Trial Post"}}}}}}},"/auth/yandex/login":{"get":{"tags":["auth"],"summary":"Yandex Login","operationId":"yandex_login_auth_yandex_login_get","parameters":[{"name":"next","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/yandex/callback":{"get":{"tags":["auth"],"summary":"Yandex Callback","operationId":"yandex_callback_auth_yandex_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/vk/login":{"get":{"tags":["auth"],"summary":"Vk Login","operationId":"vk_login_auth_vk_login_get","parameters":[{"name":"next","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/vk/callback":{"get":{"tags":["auth"],"summary":"Vk Callback","operationId":"vk_callback_auth_vk_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"device_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Id"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/logout":{"post":{"tags":["auth"],"summary":"Logout","operationId":"logout_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Logout Auth Logout Post"}}}}}}},"/auth/verify-email":{"get":{"tags":["auth"],"summary":"Verify Email","description":"Подтверждение email через magic-link из письма.\n\nЮзер кликает по ссылке `/verify-email?t=<token>` → если token валиден,\nставим `email_verified_at=now`, обнуляем token, редиректим в /account\nс success-флагом.\n\nНа invalid/expired token — редиректим на /account с error-флагом\n(UI покажет «ссылка устарела, запросите новое письмо»).","operationId":"verify_email_auth_verify_email_get","parameters":[{"name":"t","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"T"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/me":{"get":{"tags":["auth"],"summary":"Me","operationId":"me_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/extension-token":{"get":{"tags":["auth"],"summary":"Extension Token","description":"Выдаёт HMAC-подписанный extension-token для текущего залогиненного юзера.\n\nТребования:\n  - Юзер залогинен (pb_session cookie). Без cookie → 401.\n  - Никаких CSRF-проверок: GET, idempotent, и токен бесполезен без знания\n    нашего secret'а (его невозможно reverse-engineer'ить из самого токена).\n\nResponse 200:\n    {\"token\": \"pb_ext.v1.<...>\", \"exp\": <unix_ts>, \"email\": \"alice@x.ru\"}\n\nResponse 401:\n    {\"error\": \"auth_required\", \"login_url\": \"/login\"}\n\nРасширение кэширует token + exp в chrome.storage.local и обновляет за\n24 часа до exp (защита от race с серверным истечением).","operationId":"extension_token_auth_extension_token_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/my/price-history/{analysis_id}":{"get":{"tags":["my"],"summary":"My Price History","description":"Временной ряд цен товара за `days` дней. Pro-only.\n\nResponse:\n    {\n      \"analysis_id\": 42,\n      \"days\": 30,\n      \"current_price_kopecks\": 119000,   # из последней точки\n      \"min_price_kopecks\": 79000,        # минимум за окно\n      \"max_price_kopecks\": 149000,       # максимум за окно\n      \"points\": [\n         {\"captured_at\": \"2026-04-01T...\", \"price_kopecks\": 129000, \"old_price_kopecks\": 149000}\n         ...\n      ]\n    }\n\nЕсли у товара меньше 2 точек (новая карточка, её ещё никто не проверял\nдважды) — возвращаем points=[] и нули, UI покажет «недостаточно истории».","operationId":"my_price_history_api_my_price_history__analysis_id__get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Price History Api My Price History  Analysis Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/price-history/by-product/{marketplace}/{external_id}":{"get":{"tags":["my"],"summary":"My Price History By Product","description":"То же, что /price-history/{analysis_id}, но по (marketplace, external_id).\n\nНужно расширению: /api/ingest не возвращает analysis_id, но product.marketplace\nи product.external_id — возвращает всегда. Тот же shape ответа.","operationId":"my_price_history_by_product_api_my_price_history_by_product__marketplace___external_id__get","parameters":[{"name":"marketplace","in":"path","required":true,"schema":{"type":"string","title":"Marketplace"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Price History By Product Api My Price History By Product  Marketplace   External Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/price-history/batch":{"post":{"tags":["my"],"summary":"My Price History Batch","description":"Батч-версия /price-history/{aid} для watchlist-спарклайнов.\n\nResponse: {[aid: str]: {points, current_price_kopecks, min_..., max_..., days}}.\nОдин WHERE IN по (marketplace, external_id) — вместо N+1.","operationId":"my_price_history_batch_api_my_price_history_batch_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceHistoryBatchBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Price History Batch Api My Price History Batch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/price-alerts":{"get":{"tags":["my"],"summary":"List My Price Alerts","description":"Все алерты юзера + текущая цена по каждому товару (из price_history).\n\nFree-юзер видит свои старые алерты (если оформлял в Pro и downgrade'нулся),\nможет их удалить или дождаться повторной активации Pro.","operationId":"list_my_price_alerts_api_my_price_alerts_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response List My Price Alerts Api My Price Alerts Get"}}}}}},"post":{"tags":["my"],"summary":"Create Price Alert","description":"Создать или обновить алерт на цену. Pro-only.\n\nИдемпотентно по UniqueConstraint(user_id, analysis_id) — повторный POST\nс другим `threshold_kopecks` UPDATEит существующий и сбрасывает\n`triggered_at` (юзер фактически создал «новый» алерт, cooldown не должен\nунаследоваться от старого).\n\nЛимит: 100 алертов на юзера (только активные считаются).","operationId":"create_price_alert_api_my_price_alerts_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceAlertCreateBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/price-alerts/by-analysis/{analysis_id}":{"get":{"tags":["my"],"summary":"Get My Price Alert By Analysis","description":"Один алерт по analysis_id (или 404 если у юзера такого нет).\n\nИспользуется UI: «у вас уже есть алерт на этот товар» — фронт прячет\nкнопку «создать» и показывает текущий threshold.","operationId":"get_my_price_alert_by_analysis_api_my_price_alerts_by_analysis__analysis_id__get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Get My Price Alert By Analysis Api My Price Alerts By Analysis  Analysis Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/price-alerts/{alert_id}":{"delete":{"tags":["my"],"summary":"Delete My Price Alert","description":"Удалить алерт. Доступно всем авторизованным — downgrade'нувшийся юзер\nдолжен уметь убрать свои старые записи.","operationId":"delete_my_price_alert_api_my_price_alerts__alert_id__delete","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"integer","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/similar-clean/{analysis_id}":{"get":{"tags":["my"],"summary":"My Similar Clean","description":"Похожие товары того же маркетплейса с низкой накруткой.\n\nFree-доступна (но без price-фильтра — это УКАЗАНО в ТЗ как\n«маркетинг-фича: показываем value»).\n\nResponse:\n    {\n      \"analysis_id\": 42,\n      \"is_pro\": true,\n      \"items\": [\n        {\"analysis_id\":..., \"name\":..., \"price_rub\":..., \"verdict\":...,\n         \"suspicion_pct\":..., \"reviews_count\":..., \"image_url\":..., \"url\":...}\n      ],\n      \"query_terms\": [\"samsung\", \"galaxy\", \"a54\"]\n    }","operationId":"my_similar_clean_api_my_similar_clean__analysis_id__get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10,"minimum":1,"default":5,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Similar Clean Api My Similar Clean  Analysis Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/seller/{marketplace}/{seller_id}":{"get":{"tags":["my"],"summary":"My Seller Dossier","description":"Сводка по продавцу + TOP-10 последних товаров. Pro-only.\n\nPath:\n    /api/my/seller/wildberries/12345\n\nResponse:\n    {\n      \"seller_id\": \"12345\", \"marketplace\": \"wildberries\",\n      \"seller_name\": \"ООО Электронмаркет\",\n      \"total_products_checked\": 47,\n      \"with_accrual_suspicion\": 18,  # suspicion > 50\n      \"avg_suspicion_pct\": 32.4,\n      \"red_flag_share\": 0.38,        # доля verdict=avoid\n      \"verdict_counts\": {\"trusted\":12, \"caution\":20, \"avoid\":15},\n      \"products\": [\n         {\"analysis_id\":..., \"name\":..., \"suspicion_pct\":..., \"verdict\":...,\n          \"created_at\":..., \"image_url\":..., \"price_rub\":..., \"reviews_count\":...}\n         // 10 последних\n      ]\n    }","operationId":"my_seller_dossier_api_my_seller__marketplace___seller_id__get","parameters":[{"name":"marketplace","in":"path","required":true,"schema":{"type":"string","title":"Marketplace"}},{"name":"seller_id","in":"path","required":true,"schema":{"type":"string","title":"Seller Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Seller Dossier Api My Seller  Marketplace   Seller Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/seller/{marketplace}/{seller_id}/products":{"get":{"tags":["my"],"summary":"My Seller Products","description":"Полный список товаров продавца с пагинацией (для /seller/.../...).\n\nОтдельный endpoint от /seller/.../: dossier-агрегаты кешируются агрессивно,\nа полный список может быть длиннее и нужен только на странице /seller/*.","operationId":"my_seller_products_api_my_seller__marketplace___seller_id__products_get","parameters":[{"name":"marketplace","in":"path","required":true,"schema":{"type":"string","title":"Marketplace"}},{"name":"seller_id","in":"path","required":true,"schema":{"type":"string","title":"Seller Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Seller Products Api My Seller  Marketplace   Seller Id  Products Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/similar-clean/by-product/{marketplace}/{external_id}":{"get":{"tags":["my"],"summary":"My Similar Clean By Product","description":"Тот же similar-clean, но ключ — (marketplace, external_id).\n\nНужен расширению: /api/ingest не возвращает analysis_id, но product\nточно известен. Внутри просто резолвит analysis_id и переиспользует\nосновной алгоритм.","operationId":"my_similar_clean_by_product_api_my_similar_clean_by_product__marketplace___external_id__get","parameters":[{"name":"marketplace","in":"path","required":true,"schema":{"type":"string","title":"Marketplace"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10,"minimum":1,"default":5,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Similar Clean By Product Api My Similar Clean By Product  Marketplace   External Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/feed/recommended":{"get":{"tags":["my"],"summary":"My Recommended Feed","description":"v0.24.4: Personal feed для главной — рекомендации на основе истории.\n\nВозвращает:\n    {\n      \"has_data\": True,\n      \"items\": [\n        {\n          \"analysis_id\": 42, \"short_id\": \"Abc\",\n          \"name\": \"...\", \"marketplace\": \"wildberries\",\n          \"verdict\": \"trusted\", \"score\": 78,\n          \"image_url\": \"...\", \"category_label\": \"Электроника\",\n          \"url\": \"/p/Abc\"\n        },\n        ...\n      ],\n      \"categories\": [\"cat:электроника\", \"brand:samsung\", ...],\n    }\nЕсли у юзера нет истории / недостаточно категорий —\n    {\"has_data\": False, \"items\": [], \"categories\": []}\nФронт в этом случае скрывает секцию (graceful) и показывает обычный\ndemo_example.","operationId":"my_recommended_feed_api_my_feed_recommended_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":16,"minimum":1,"default":8,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Recommended Feed Api My Feed Recommended Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/tools-summary":{"get":{"tags":["my"],"summary":"My Tools Summary","description":"Счётчики для блока «Мои Pro-инструменты» в /account.\n\nWatchlist — count по БД.\nPDF downloads — пока 0 (не логируем в таблицу, Redis-кеш эфемерный).\nSeller dossiers — unique viewed кладём в redis set `pb:my:seller_viewed:{user}`\nс TTL 30д; считаем его cardinality. Метрика грязная (TTL эфемерный),\nно MVP-ready: юзер видит «я смотрел 12 продавцов», можно итерировать.","operationId":"my_tools_summary_api_my_tools_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Tools Summary Api My Tools Summary Get"}}}}}}},"/api/my/ping":{"get":{"tags":["my"],"summary":"My Ping","description":"Возвращает email + tier — полезно для смоук-тестов /account.","operationId":"my_ping_api_my_ping_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Ping Api My Ping Get"}}}}}}},"/api/my/account-progress":{"get":{"tags":["my"],"summary":"My Account Progress","description":"v0.23.9: Progress-strip для /account dashboard'а.\n\nПоказывает юзеру чек-лист «используй «Пробиваем» на 100%»:\n  - first_check  — есть ли первая запись в user_checks\n  - watchlist    — есть ли хотя бы 1 товар на отслеживании\n  - folder       — есть ли созданная папка\n  - all_done     — все три ✓ → strip коллапсится в «🎉 всё подключено»\n\nЦель: новый юзер регистрируется → видит 0/3 → понимает что делать\nдальше по шагам. После того как все ✓ — strip автоматически прячется.\n\nВозвращает:\n    {\n        \"first_check\": bool,\n        \"watchlist\": bool,\n        \"folder\": bool,\n        \"all_done\": bool,\n        \"completed_count\": int,   # 0..3 — для UI прогресс-бара\n        \"total_steps\": 3,\n    }\n\nPerformance: 3 EXISTS-запроса в одной session, ms-уровень.","operationId":"my_account_progress_api_my_account_progress_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Account Progress Api My Account Progress Get"}}}}}}},"/api/my/checks":{"get":{"tags":["my"],"summary":"List My Checks","description":"Постраничная история проверок текущего юзера.\n\nПараметры:\n- `limit`  — 1..100, default 20\n- `offset` — >=0, default 0\n- `pinned=1` — только избранное (is_pinned=true)\n- `verdict` — \"avoid\" | \"caution\" | \"trusted\" | None\n- `search` — подстрока в названии товара (ILIKE по product.name + url)\n- `folder_id` — фильтр по папке (для UI subnav-tab «Папка X»)\n- `no_folder=1` — карточки без папки (UI subnav-tab «Без папки»)\n\nFree-лимит: режем до FREE_HISTORY_LIMIT. `total` — реальное количество\n(для плашки «старше — в Pro»), `items` — не больше лимита.","operationId":"list_my_checks_api_my_checks_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pinned","in":"query","required":false,"schema":{"type":"integer","maximum":1,"minimum":0,"default":0,"title":"Pinned"}},{"name":"verdict","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Search"}},{"name":"folder_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Folder Id"}},{"name":"no_folder","in":"query","required":false,"schema":{"type":"integer","maximum":1,"minimum":0,"default":0,"title":"No Folder"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response List My Checks Api My Checks Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/checks/{check_id}/pin":{"post":{"tags":["my"],"summary":"Pin My Check","description":"Toggle `is_pinned`. Pro-only: Free получает 402 `pin_is_pro`.","operationId":"pin_my_check_api_my_checks__check_id__pin_post","parameters":[{"name":"check_id","in":"path","required":true,"schema":{"type":"integer","title":"Check Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/folders":{"get":{"tags":["my"],"summary":"List My Folders","description":"Список папок юзера + count закреплённых карточек в каждой.\n\nFree-юзер тоже может вызвать (но папок у него ноль — POST/folders под\nPro-чеком). Возвращаем пустой массив без 402, чтобы UI не падал.","operationId":"list_my_folders_api_my_folders_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response List My Folders Api My Folders Get"}}}}}},"post":{"tags":["my"],"summary":"Create My Folder","description":"Создать новую папку. Лимит 20 на юзера. Pro-only.","operationId":"create_my_folder_api_my_folders_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderCreateBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/folders/{folder_id}":{"patch":{"tags":["my"],"summary":"Patch My Folder","description":"Переименовать / сменить цвет / переместить (position). Pro-only.","operationId":"patch_my_folder_api_my_folders__folder_id__patch","parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"integer","title":"Folder Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderPatchBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["my"],"summary":"Delete My Folder","description":"Удалить папку. user_checks.pinned_folder_id занулится (FK SET NULL),\nкарточки выпадут в «Без папки», но не пропадут.","operationId":"delete_my_folder_api_my_folders__folder_id__delete","parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"integer","title":"Folder Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/checks/{check_id}/move-to-folder":{"post":{"tags":["my"],"summary":"Move Check To Folder","description":"Переместить проверку в папку (или null = убрать из папки).\n\nPro-only. Если folder_id указан — папка должна принадлежать тому же\nюзеру, иначе 404. При перемещении в папку авто-пиним (если ещё не).","operationId":"move_check_to_folder_api_my_checks__check_id__move_to_folder_post","parameters":[{"name":"check_id","in":"path","required":true,"schema":{"type":"integer","title":"Check Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveToFolderBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/checks/{check_id}/note":{"post":{"tags":["my"],"summary":"Note My Check","description":"Set/clear note. Pro-only: Free получает 402 `note_is_pro`.\n\nПустая строка = удалить заметку (храним NULL).","operationId":"note_my_check_api_my_checks__check_id__note_post","parameters":[{"name":"check_id","in":"path","required":true,"schema":{"type":"integer","title":"Check Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/checks/{check_id}":{"delete":{"tags":["my"],"summary":"Delete My Check","description":"Убирает запись из user_checks (Analysis не трогаем).\n\nПубличная share-страница /check/{analysis_id} продолжает работать —\nиз личной ленты просто пропадает. Доступно всем авторизованным.","operationId":"delete_my_check_api_my_checks__check_id__delete","parameters":[{"name":"check_id","in":"path","required":true,"schema":{"type":"integer","title":"Check Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/checks/export.pdf":{"get":{"tags":["my"],"summary":"Export My Checks Pdf","description":"Многостраничный PDF со всей историей юзера (Pro-only).\n\nКаждая страница = render_analysis_pdf на отдельной Analysis-row,\nстраницы склеиваются через pypdf. Лимит 1000 записей — защита от\nмегабайтных PDF; в реальности в истории редко > 200 проверок.","operationId":"export_my_checks_pdf_api_my_checks_export_pdf_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/my/watchlist":{"get":{"tags":["my"],"summary":"List My Watchlist","description":"Все товары юзера под наблюдением. Доступно всем авторизованным\n(Free юзер увидит пустой список или те, что добавил когда-то в Pro).","operationId":"list_my_watchlist_api_my_watchlist_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response List My Watchlist Api My Watchlist Get"}}}}}},"post":{"tags":["my"],"summary":"Add To My Watchlist","description":"Добавить товар; те же лимиты и блокировка, что в API v1.","operationId":"add_to_my_watchlist_api_my_watchlist_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchlistAddBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/watchlist/{watch_id}":{"delete":{"tags":["my"],"summary":"Remove From My Watchlist","description":"Убрать из watchlist. Доступно всем авторизованным (чтобы юзер,\ndowngrade'нувшийся с Pro на Free, мог удалить ставшие ему ненужными записи).","operationId":"remove_from_my_watchlist_api_my_watchlist__watch_id__delete","parameters":[{"name":"watch_id","in":"path","required":true,"schema":{"type":"integer","title":"Watch Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/watchlist/{watch_id}/toggle-notify":{"post":{"tags":["my"],"summary":"Toggle Watchlist Notify","description":"Включить / отключить email-алерты по конкретной подписке.\n\nБез 402 — юзер даже Free должен уметь отключить уведомления на свои\nстарые записи. Алерты всё равно посылаются только Pro-юзерам (cron-side\ncheck тарифа).","operationId":"toggle_watchlist_notify_api_my_watchlist__watch_id__toggle_notify_post","parameters":[{"name":"watch_id","in":"path","required":true,"schema":{"type":"integer","title":"Watch Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/watchlist/{watch_id}/target":{"post":{"tags":["my"],"summary":"Set Watchlist Target","description":"Задать целевую цену «не дороже» для подписки.\n\ntarget_rubles=0 → снять цель (NULL в БД). Иначе — конвертим в копейки и\nкладём в watchlist.target_price_kopecks. watchlist_price_check читает это\nполе и шлёт WATCHLIST_PRICE_DROP при cur ≤ target.\n\nv0.28.11: с открытием watchlist для free (2 слота) target-цена остаётся\nPro-дифференциатором: free получает стандартные алерты (просадка ≥10%,\nволна накрутки), «алерт на МОЮ цену» — Продвинутый.","operationId":"set_watchlist_target_api_my_watchlist__watch_id__target_post","parameters":[{"name":"watch_id","in":"path","required":true,"schema":{"type":"integer","title":"Watch Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchlistTargetBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/watchlist/{watch_id}/snooze":{"post":{"tags":["my"],"summary":"Snooze Watchlist","description":"Поставить watchlist-row на паузу на N дней (или снять).\n\ndays=0 → paused_until=NULL (снять). Иначе — paused_until = now()+N дней.\nCron'ы фильтруют paused_until IS NULL OR paused_until <= NOW(): row\nсохраняется, история/baseline/snapshot не теряются, юзер просто перестаёт\nполучать алерты на этот период.","operationId":"snooze_watchlist_api_my_watchlist__watch_id__snooze_post","parameters":[{"name":"watch_id","in":"path","required":true,"schema":{"type":"integer","title":"Watch Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchlistSnoozeBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/referral":{"get":{"tags":["my"],"summary":"Get Referral","description":"Мой реферальный код + статистика приглашений.\n\nВозвращает:\n  - `code` / `url` — что шарить;\n  - `invited_count` — скольких удалось зарегистрировать по ссылке;\n  - `converted_count` — сколько из них оформили Pro (активная подписка);\n  - `bonus_days_earned` — накопленный бонус.","operationId":"get_referral_api_my_referral_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/my/notifications":{"get":{"tags":["my"],"summary":"List My Notifications","description":"Последние уведомления юзера + счётчик непрочитанных.\n\nВозвращает:\n  items: [{id, kind, title, body, icon, link_url, link_label,\n           is_read, read_at, created_at, meta, created_by_admin_id}]\n  total: число всех уведомлений (с учётом фильтра unread_only)\n  unread: сколько непрочитанных (всегда, независимо от фильтра)","operationId":"list_my_notifications_api_my_notifications_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"unread_only","in":"query","required":false,"schema":{"type":"integer","maximum":1,"minimum":0,"default":0,"title":"Unread Only"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response List My Notifications Api My Notifications Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/notifications/{notification_id}/read":{"post":{"tags":["my"],"summary":"Mark Notification Read","description":"Пометить одно уведомление прочитанным. Идемпотентно.","operationId":"mark_notification_read_api_my_notifications__notification_id__read_post","parameters":[{"name":"notification_id","in":"path","required":true,"schema":{"type":"integer","title":"Notification Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Mark Notification Read Api My Notifications  Notification Id  Read Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/notifications/read-all":{"post":{"tags":["my"],"summary":"Mark All Notifications Read","description":"Пометить все уведомления юзера прочитанными. Возвращает N.","operationId":"mark_all_notifications_read_api_my_notifications_read_all_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Mark All Notifications Read Api My Notifications Read All Post"}}}}}}},"/api/my/notifications/unread-count":{"get":{"tags":["my"],"summary":"Notifications Unread Count","description":"Лёгкий endpoint для бейджа — только число, без списка.\n\nИспользует partial index ix_user_notifications_unread → дешёвый COUNT.","operationId":"notifications_unread_count_api_my_notifications_unread_count_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"integer"},"type":"object","title":"Response Notifications Unread Count Api My Notifications Unread Count Get"}}}}}}},"/api/my/support/threads":{"get":{"tags":["my"],"summary":"List My Support Threads","description":"Список «мои обращения» + counter непрочитанных для бейджа.\n\nPreview — первые 100 символов последнего НЕ-internal сообщения\n(internal-заметки админа юзер не видит вообще).","operationId":"list_my_support_threads_api_my_support_threads_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response List My Support Threads Api My Support Threads Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["my"],"summary":"Create My Support Thread","description":"Открыть новое обращение. Rate-limit: 3 / 24h / user.\n\nВозвращает {thread_id}. UI редиректит на /support/{thread_id}.","operationId":"create_my_support_thread_api_my_support_threads_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SupportCreateBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Create My Support Thread Api My Support Threads Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/support/threads/{thread_id}":{"get":{"tags":["my"],"summary":"Get My Support Thread","description":"Полная ветка переписки текущего юзера. Anti-IDOR: WHERE user_id=user.id.\n\nФильтруем internal-notes — юзер не должен их видеть НИКОГДА.\nПобочный эффект: mark_seen_by_user → сброс бейджа unread.","operationId":"get_my_support_thread_api_my_support_threads__thread_id__get","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"integer","title":"Thread Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Get My Support Thread Api My Support Threads  Thread Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/support/threads/{thread_id}/messages":{"post":{"tags":["my"],"summary":"Post My Support Message","description":"Юзер отвечает в свой тред. Rate-limit: 20 / час / user (анти-флуд).\n\nЕсли тред closed → 409 (реактивация только через админа).","operationId":"post_my_support_message_api_my_support_threads__thread_id__messages_post","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"integer","title":"Thread Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SupportMessageBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Post My Support Message Api My Support Threads  Thread Id  Messages Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/support/threads/{thread_id}/close":{"post":{"tags":["my"],"summary":"Close My Support Thread","description":"Юзер сам помечает тред как `resolved` (например, разобрался).\n\nОтличие от admin-close: юзер ставит RESOLVED, не CLOSED — чтобы при\nнеобходимости мог сам же и реактивировать (пишет новое сообщение).\n409 если уже resolved/closed.","operationId":"close_my_support_thread_api_my_support_threads__thread_id__close_post","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"integer","title":"Thread Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Close My Support Thread Api My Support Threads  Thread Id  Close Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/support/unread-count":{"get":{"tags":["my"],"summary":"My Support Unread Count","description":"Лёгкий endpoint — число непрочитанных тредов. Для бейджа в ЛК.","operationId":"my_support_unread_count_api_my_support_unread_count_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"integer"},"type":"object","title":"Response My Support Unread Count Api My Support Unread Count Get"}}}}}}},"/api/my/notifications/digest-status":{"get":{"tags":["my"],"summary":"My Digest Status","description":"Возвращает {opt_in, last_sent_at}. UI читает для заполнения чекбокса.","operationId":"my_digest_status_api_my_notifications_digest_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Digest Status Api My Notifications Digest Status Get"}}}}}}},"/api/my/notifications/digest-opt-out":{"post":{"tags":["my"],"summary":"My Digest Opt Out","description":"Отписаться от еженедельного дайджеста. Идемпотентно.","operationId":"my_digest_opt_out_api_my_notifications_digest_opt_out_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Digest Opt Out Api My Notifications Digest Opt Out Post"}}}}}}},"/api/my/notifications/digest-opt-in":{"post":{"tags":["my"],"summary":"My Digest Opt In","description":"Подписаться на еженедельный дайджест обратно.","operationId":"my_digest_opt_in_api_my_notifications_digest_opt_in_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Digest Opt In Api My Notifications Digest Opt In Post"}}}}}}},"/api/my/notifications/low-balance-status":{"get":{"tags":["my"],"summary":"My Low Balance Status","description":"Возвращает {opt_in, last_sent_at}. UI читает для заполнения чекбокса.","operationId":"my_low_balance_status_api_my_notifications_low_balance_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Low Balance Status Api My Notifications Low Balance Status Get"}}}}}}},"/api/my/notifications/low-balance-opt-out":{"post":{"tags":["my"],"summary":"My Low Balance Opt Out","description":"Отписаться от уведомлений «низкий баланс». Идемпотентно.","operationId":"my_low_balance_opt_out_api_my_notifications_low_balance_opt_out_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Low Balance Opt Out Api My Notifications Low Balance Opt Out Post"}}}}}}},"/api/my/notifications/low-balance-opt-in":{"post":{"tags":["my"],"summary":"My Low Balance Opt In","description":"Подписаться на уведомления «низкий баланс» обратно.","operationId":"my_low_balance_opt_in_api_my_notifications_low_balance_opt_in_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Low Balance Opt In Api My Notifications Low Balance Opt In Post"}}}}}}},"/api/my/export.xlsx":{"get":{"tags":["my"],"summary":"Export Xlsx","description":"Экспортирует все проверки юзера в Excel-файл (Pro-only).\n\nОдин лист «Probiwaem Checks», шапка с фирменным цветом, conditional\nformatting на колонке Probiwaem Score (0=красный, 50=жёлтый, 100=зелёный).\nAuto-width до 60 символов на колонку — длинные URL не разъезжаются.\n\nЛимит: первая 10000 строк (хватает с запасом — рекордсмены едва дотягивают\nдо 1000). Защита от ОЗУ-съедания на патологических аккаунтах.","operationId":"export_xlsx_api_my_export_xlsx_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/my/api-keys":{"get":{"tags":["my"],"summary":"List My Api Keys","description":"Список API-ключей юзера. БЕЗ raw_key — только prefix и метаданные.\n\nv0.14.5: возвращаем max_keys и rate_limits для UI (чтобы юзер видел\nлимиты без хардкода в template'е).","operationId":"list_my_api_keys_api_my_api_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response List My Api Keys Api My Api Keys Get"}}}}}},"post":{"tags":["my"],"summary":"Create My Api Key","description":"Создаёт новый API-ключ. Возвращает raw_key ОДИН раз.\n\nUI обязан показать модал с предупреждением «сохрани сейчас, больше\nне покажем». На повторный GET /api-keys приходит только prefix.\n\nv0.14.5: cap MAX_ACTIVE_KEYS_PER_USER. При превышении возвращаем 429\nс понятной ошибкой — фронт показывает toast «у вас уже N ключей,\nотзовите старый».","operationId":"create_my_api_key_api_my_api_keys_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Create My Api Key Api My Api Keys Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/api-keys/{key_id}":{"delete":{"tags":["my"],"summary":"Revoke My Api Key","description":"Отзыв (soft-delete) API-ключа. Идемпотентно.","operationId":"revoke_my_api_key_api_my_api_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"integer","title":"Key Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Revoke My Api Key Api My Api Keys  Key Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/cross-marketplace/{analysis_id}":{"get":{"tags":["my"],"summary":"My Cross Marketplace","description":"Cross-marketplace-таблица: тот же товар на 4 маркетплейсах + pb_score.\n\nPro-only. Free-юзеру 402 с preview/mock_rows для рендера блюра.\n\nPath:\n    /api/my/cross-marketplace/{analysis_id}\n\nPro response:\n    {\n      \"source\": {marketplace, marketplace_label, url, name, ...},\n      \"mirrors\": [\n        {marketplace, marketplace_label, url, price_rub, price_diff_pct,\n         pb_score, verdict, match_confidence, needs_check, ...},\n        ...\n      ],\n      \"is_pro\": true,\n      \"fetched_at\": \"2026-05-02T20:00:00Z\"\n    }\n\nFree response (HTTP 402):\n    {is_pro: false, preview: true, mock_rows: [...],\n     cta_text: \"...\", feature_name: \"...\"}","operationId":"my_cross_marketplace_api_my_cross_marketplace__analysis_id__get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response My Cross Marketplace Api My Cross Marketplace  Analysis Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/savings":{"get":{"tags":["my"],"summary":"Get Savings","description":"Копилка сэкономленного — «Пробиваем сэкономил вам ≈N ₽».\n\nv0.30.0 (ценность до оплаты): накопительный счётчик денег, которые юзер\nоценочно сберёг, НЕ купив товары с вердиктом avoid (накрутка/подделка) из\nсвоей истории проверок. Эвристика — та же, что в глобальном stats-виджете\n(public_routes.py): sum(price)×0.5 по avoid-проверкам. Консистентность\nцифр, честно помечаем is_estimate=true.\n\nLogin-only (НЕ Pro): free видит «вот сколько уже помогли — оформи»,\nPro — аргумент продлить. Anti-IDOR: WHERE uc.user_id = current_user.id.","operationId":"get_savings_api_my_savings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Get Savings Api My Savings Get"}}}}}}},"/api/my/watchers/brand":{"get":{"tags":["watchers"],"summary":"List Brand Watchers","description":"Все brand-подписки юзера. Доступно всем авторизованным — Free может\nувидеть свои подписки и удалить их (но не добавить новые).","operationId":"list_brand_watchers_api_my_watchers_brand_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response List Brand Watchers Api My Watchers Brand Get"}}}}}},"post":{"tags":["watchers"],"summary":"Add Brand Watcher","description":"Подписаться на бренд. Pro-only.\n\nUPSERT по (user_id, brand_name, marketplace) — повторный POST с теми же\nпараметрами вернёт существующую запись (200, already=True), не плодит дубли.\nЛимит MAX_BRAND_WATCHERS_PER_USER на юзера.","operationId":"add_brand_watcher_api_my_watchers_brand_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandWatcherCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/watchers/brand/{watcher_id}":{"delete":{"tags":["watchers"],"summary":"Remove Brand Watcher","description":"Отписаться от бренда. Доступно всем авторизованным.","operationId":"remove_brand_watcher_api_my_watchers_brand__watcher_id__delete","parameters":[{"name":"watcher_id","in":"path","required":true,"schema":{"type":"integer","title":"Watcher Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/watchers/seller":{"get":{"tags":["watchers"],"summary":"List Seller Watchers","description":"Все seller-подписки юзера.","operationId":"list_seller_watchers_api_my_watchers_seller_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response List Seller Watchers Api My Watchers Seller Get"}}}}}},"post":{"tags":["watchers"],"summary":"Add Seller Watcher","description":"Подписаться на продавца. Pro-only.\n\nUPSERT по (user_id, marketplace, seller_id). Если запись уже есть —\nобновляем seller_name_snapshot (имя могло обновиться) и возвращаем\nalready=True.","operationId":"add_seller_watcher_api_my_watchers_seller_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SellerWatcherCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/watchers/seller/{watcher_id}":{"delete":{"tags":["watchers"],"summary":"Remove Seller Watcher","description":"Отписаться от продавца.","operationId":"remove_seller_watcher_api_my_watchers_seller__watcher_id__delete","parameters":[{"name":"watcher_id","in":"path","required":true,"schema":{"type":"integer","title":"Watcher Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cart/check":{"post":{"tags":["cart"],"summary":"Cart Check","description":"Массовая проверка URL-ов корзины. См. модуль-doc выше.","operationId":"cart_check_api_cart_check_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartCheckRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartCheckResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cart/import-from-url":{"post":{"tags":["cart"],"summary":"Cart Import From Url","description":"Best-effort импорт публичного WB-списка по URL.\n\nПоток:\n  1. Auth (как и /check — корзина для зарегистрированных).\n  2. Rate-limit: 5 req/min/IP (Redis NX EX, in-memory fallback).\n  3. Проверка хоста — только wildberries.ru.\n  4. httpx GET с timeout=10s, наш UA, max-bytes=2MB.\n  5. Regex по HTML → SKU → /catalog/{sku}/detail.aspx URL'ы.\n  6. Если 0 SKU — `error=\"Не нашли товаров...\"`, НЕ 500.","operationId":"cart_import_from_url_api_cart_import_from_url_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportFromUrlRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportFromUrlResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/cart/report.pdf":{"post":{"tags":["cart"],"summary":"Cart Pdf Report","description":"Pro-only PDF-отчёт о проверке корзины (8-10 страниц).\n\nAuth: 401 без сессии. Pro: 402 (pro_required) для не-Pro юзеров —\nupsell в UI ведёт на /pricing.\n\nBody — те же URL'ы что и /cart/check, не более _MAX_URLS.\nВозвращает: application/pdf, attachment с именем-датой.","operationId":"cart_pdf_report_api_cart_report_pdf_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartPdfRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/notifications/prefs":{"get":{"tags":["notifications"],"summary":"Get Prefs","description":"Снапшот настроек уведомлений юзера для всех известных событий.\n\nResponse shape:\n    {\n      \"events\": [\n        {\n          \"event\": \"watchlist_price_drop\",\n          \"channel_tg\": true,\n          \"channel_browser_push\": false,\n          \"channel_email\": false,\n          \"channel_in_app\": true,\n          \"default_channels\": {\"channel_tg\": true, ...}\n        },\n        ...\n      ]\n    }\n\nЕсли записи в БД для события нет — возвращаем _defaults_for(event).\nBackward-compat: legacy `enabled` поле читается тоже — если строка есть,\nно channel_tg явно не выставлен (старый бот /settings обновлял только\nenabled), используем `enabled` как channel_tg.","operationId":"get_prefs_api_my_notifications_prefs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Get Prefs Api My Notifications Prefs Get"}}}}}},"post":{"tags":["notifications"],"summary":"Update Prefs","description":"UPSERT per-event-channel toggles.\n\nПринимает один event за раз — UI отдельным запросом тоггл'ит каждое\nизменение (нагрузка минимальная — десяток событий, юзер тапает редко).\nBatch-режим (массовый PATCH всех событий) добавим если потребуется.\n\nBackward-compat для tg_notifier: одновременно с channel_tg пишем `enabled`,\nчтобы старая ручка `is_event_enabled` (читает `enabled`) выдавала\nсвежее значение.","operationId":"update_prefs_api_my_notifications_prefs_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrefsUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/notifications/push/subscribe":{"post":{"tags":["notifications"],"summary":"Push Subscribe","description":"Сохранить Web Push подписку Service Worker'а.\n\nТело — стандартный PushSubscription JSON:\n    {endpoint, keys: {p256dh, auth}}\n\nПоведение:\n  • UPSERT по (user_id, endpoint). При повторной подписке с того же\n    устройства — обновляем p256dh/auth (юзер мог пере-сгенерить ключи\n    при clear-data) и сбрасываем failure_count в 0 (старые 410 могли\n    быть результатом одноразового сетевого сбоя).\n  • last_used_at не трогаем здесь — это реально «использовалась» (т.е.\n    push успешно отправлен), а не «зарегистрирована».\n  • Если settings.notifications_vapid_public_key пустой — возвращаем 503\n    (browser push не настроен на сервере). Этим UI понимает что нужно\n    показать «Push временно недоступны».\n\nSECURITY: endpoint от FCM/Apple/Mozilla формально публичный (содержится\nв нём токен подписки), но мы не позволяем юзеру подписать чей-то чужой\nendpoint — UNIQUE (user_id, endpoint) гарантирует, что один и тот же\nendpoint у двух разных юзеров будет двумя разными строками. Это не\nутечка — push-сервис всё равно проверяет VAPID-подпись и шифрование.","operationId":"push_subscribe_api_my_notifications_push_subscribe_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushSubscriptionPayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["notifications"],"summary":"Push Unsubscribe","description":"Удалить подписку по endpoint'у.\n\nЮзер unsubscribed через `subscription.unsubscribe()` в SW → шлёт DELETE\nс тем же endpoint. Удаляем строго свою (user_id-фильтр) — спуфер не\nсможет удалить чужую подписку через known endpoint, потому что в WHERE\nесть user_id.\n\nЕсли ничего не удалили — возвращаем ok:true (идемпотентно). Не отдаём\n404 чтобы фронт не пытался retry'ить.","operationId":"push_unsubscribe_api_my_notifications_push_subscribe_delete","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushUnsubscribeBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/notifications/in-app":{"get":{"tags":["notifications"],"summary":"Get In App Queue","description":"Прочитать последние N in-app уведомлений из Redis.\n\nКлюч: `pb:notif:in_app:<user_id>` (Redis LIST). push_dispatcher делает\nLPUSH сериализованного JSON при отправке события (если у юзера\nchannel_in_app=true). Этот endpoint дёргает фронт /account через\npoll или WebSocket-fallback.\n\nЕсли Redis недоступен — возвращаем пустой список (graceful degradation,\nin-app — best-effort канал; основные уведомления всё равно остаются\nв `user_notifications`-таблице через notify_user).","operationId":"get_in_app_queue_api_my_notifications_in_app_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Get In App Queue Api My Notifications In App Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/notifications/in-app/mark-read":{"post":{"tags":["notifications"],"summary":"Mark In App Read","description":"Пометить in-app уведомления как прочитанные.\n\nСтратегия: переписываем содержимое LIST'а, удаляя элементы с переданными\nid. Альтернатива (флаг `read=true` в payload) дороже — нужно decode/encode\nкаждого элемента. LRANGE → filter → DEL+RPUSH делается транзакционно через\nMULTI/EXEC.\n\nЕсли ids пустой — no-op (фронт может звонить как «всё прочитано»; но это\nлучше делать через явный DEL key).","operationId":"mark_in_app_read_api_my_notifications_in_app_mark_read_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InAppMarkReadBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/vapid-public-key":{"get":{"tags":["notifications"],"summary":"Get Vapid Public Key","description":"Public VAPID-ключ для applicationServerKey в pushManager.subscribe.\n\nЕсли ключи не настроены — 503. UI/SW понимает «browser push отключён».","operationId":"get_vapid_public_key_api_auth_vapid_public_key_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Get Vapid Public Key Api Auth Vapid Public Key Get"}}}}}}},"/api/telegram/me":{"get":{"tags":["telegram"],"summary":"Telegram Me","description":"Состояние привязки. Используется фронтом для polling'а после\nклика «Привязать» — пока бот не отметит linked=true, фронт\nпродолжает опрос.\n\n`bot_blocked` (миграция 041): True если tg_notifier недавно получил 403\nForbidden от TG (юзер заблокировал бота). При True telegram_user_id\nобнулён, но фронт показывает специальную плашку «Вы заблокировали\nбота — разблокируйте, чтобы получать пуши» вместо обычной кнопки\n«Привязать».","operationId":"telegram_me_api_telegram_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Telegram Me Api Telegram Me Get"}}}}}}},"/api/telegram/link-token":{"post":{"tags":["telegram"],"summary":"Create Link Token","description":"Создаёт одноразовый токен и возвращает deep-link на бота.\n\nЕсли юзер уже привязан — возвращаем 409 с подсказкой «сначала отвяжите».\nЭто не критично для security (на привязанном юзере /start <token> просто\nперезапишет username), но защищает от случайной перепривязки на чужой\ntg-аккаунт.","operationId":"create_link_token_api_telegram_link_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Create Link Token Api Telegram Link Token Post"}}}}}}},"/api/telegram/unlink":{"post":{"tags":["telegram"],"summary":"Unlink Telegram","description":"Обнуляет telegram_user_id / username / linked_at у текущего юзера.\n\nИдемпотентна: повторный вызов при уже отвязанном юзере вернёт ok=True\n(не палим состояние через 404 — у /account этого знания и не нужно).","operationId":"unlink_telegram_api_telegram_unlink_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Unlink Telegram Api Telegram Unlink Post"}}}}}}},"/api/admin/tg-callback":{"post":{"tags":["admin-tg"],"summary":"Admin Tg Callback","operationId":"admin_tg_callback_api_admin_tg_callback_post","parameters":[{"name":"x-telegram-bot-api-secret-token","in":"header","required":false,"schema":{"type":"string","default":"","title":"X-Telegram-Bot-Api-Secret-Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Admin Tg Callback Api Admin Tg Callback Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/telegram-webapp":{"post":{"summary":"Авторизация через Telegram WebApp initData","operationId":"telegram_webapp_auth_api_auth_telegram_webapp_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_TgAuthBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/tg-app":{"get":{"summary":"Telegram WebApp launcher (одноразовая страница для авторизации)","description":"Лендинг для KeyboardButton(web_app=WebAppInfo(...)). Сам не делает\nничего — отдаёт HTML который JS'ом постит initData в auth-эндпоинт и\nредиректит в /account.\n\nКэшировать НЕ надо: это live-page (initData мы не хотим класть в\nproxy-cache; auth-обмен должен быть свежим).","operationId":"tg_app_launcher_tg_app_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/banners":{"get":{"tags":["banners"],"summary":"Get Banner","description":"Вернуть подходящий баннер для placement + текущей audience.\n\n`dismissed` — comma-separated список slug'ов которые юзер уже закрыл.\nВиджет читает из cookie `pb_dismissed_banners` и подмешивает в запрос.\nСервер исключает их из выборки (никогда не вернёт уже dismissed-баннер).\n\nЕсли ни один не подходит — `{\"banner\": null, \"html\": \"\"}`.","operationId":"get_banner_api_banners_get","parameters":[{"name":"placement","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":24,"title":"Placement"}},{"name":"dismissed","in":"query","required":false,"schema":{"type":"string","maxLength":2000,"default":"","title":"Dismissed"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/banners/{slug}/impression":{"post":{"tags":["banners"],"summary":"Banner Impression","description":"Атомарный +1 к impressions_count. Виджет дедуплицирует per-session.","operationId":"banner_impression_api_banners__slug__impression_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/banners/{slug}/click":{"post":{"tags":["banners"],"summary":"Banner Click","description":"Атомарный +1 к clicks_count при клике на CTA.","operationId":"banner_click_api_banners__slug__click_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/banners/{slug}/dismiss":{"post":{"tags":["banners"],"summary":"Banner Dismiss","description":"Атомарный +1 к dismissals_count. Cookie ставит сам виджет.","operationId":"banner_dismiss_api_banners__slug__dismiss_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/find":{"post":{"tags":["public","finder"],"summary":"Public Find","description":"Подбор товаров по свободному описанию (WB MVP).\n\nFlow:\n    1. parse_intent(query) — детерминированный разбор (БЕЗ LLM).\n    2. search_wb — публичный WB v4 search (free, server-side, кэш в Redis).\n    3. DB honesty-overlay — batched SELECT по нашим вердиктам.\n    4. rank_candidates — ранжирование по честности + релевантности + цене.\n\n503 если фича выключена (kill-switch). 429 при rate-limit. Любая\nвнутренняя ошибка → 200 с пустыми results + мягкая note (страницу не валим).","operationId":"public_find_api_public_find_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public/build/templates":{"get":{"tags":["public","builds"],"summary":"Build Templates","description":"Список сборок для карточек выбора (рендерит build.js).","operationId":"build_templates_api_public_build_templates_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplatesResponse"}}}}}}},"/api/public/build":{"post":{"tags":["public","builds"],"summary":"Public Build","description":"Собрать комплект: цель → шаблон → параллельный подбор по слотам.","operationId":"public_build_api_public_build_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/ext/ozon-debug-dump":{"post":{"tags":["ext-debug"],"summary":"Marketplace Debug Dump","operationId":"marketplace_debug_dump_api_ext_ozon_debug_dump_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/ext/marketplace-debug-dump":{"post":{"tags":["ext-debug"],"summary":"Marketplace Debug Dump","operationId":"marketplace_debug_dump_api_ext_marketplace_debug_dump_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/ext/user-report":{"post":{"tags":["ext-debug"],"summary":"Ext User Report","description":"User-initiated bug report из кнопки «Сообщить о проблеме» в popup'е.\n\nПубличный endpoint, не требует auth (аноним тоже может репортить).\nЕсли юзер залогинен на probiwaem.ru — подхватываем email из pb_session\ncookie автоматически, чтобы админ мог ему ответить.\n\nRate-limit: 5 reports / hour / device_uuid ИЛИ IP.","operationId":"ext_user_report_api_ext_user_report_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtUserReportBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/ext/latest-version":{"get":{"tags":["ext-debug"],"summary":"Ext Latest Version","description":"Возвращает актуальную версию расширения + ссылки на скачивание.\n\nПубличный endpoint, не требует auth. Кешируется Cloudflare'ом на edge\n(Cache-Control: public, max-age=600) — 10 минут — чтобы 10тыс юзеров\nне забомбили нас на каждый браузерный рестарт.","operationId":"ext_latest_version_api_ext_latest_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/login":{"post":{"tags":["admin"],"summary":"Admin Login","operationId":"admin_login_api_admin_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminLoginBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/logout":{"post":{"tags":["admin"],"summary":"Admin Logout","operationId":"admin_logout_api_admin_logout_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/change-password":{"post":{"tags":["admin"],"summary":"Admin Change Password","description":"Смена пароля залогиненным админом. Требует текущий пароль + TOTP.","operationId":"admin_change_password_api_admin_change_password_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/me":{"get":{"tags":["admin"],"summary":"Admin Me","operationId":"admin_me_api_admin_me_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/totp/setup":{"post":{"tags":["admin"],"summary":"Totp Setup","description":"Step 1. Verify password; return a *pending* secret + QR.\n\nThe secret is NOT stored yet — the client must echo it back on /totp/verify\ntogether with a valid 6-digit code before we accept it.","operationId":"totp_setup_api_admin_totp_setup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TotpSetupBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/totp/verify":{"post":{"tags":["admin"],"summary":"Totp Verify","description":"Step 2. Confirm the pending secret with a real code, then persist it.","operationId":"totp_verify_api_admin_totp_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TotpVerifyBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/metrics":{"get":{"tags":["admin"],"summary":"Metrics","operationId":"metrics_api_admin_metrics_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/analyses":{"get":{"tags":["admin"],"summary":"List Analyses","operationId":"list_analyses_api_admin_analyses_get","parameters":[{"name":"marketplace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketplace"}},{"name":"verdict","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/analyses/{analysis_id}":{"get":{"tags":["admin"],"summary":"Get Analysis","operationId":"get_analysis_api_admin_analyses__analysis_id__get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/flags-stats":{"get":{"tags":["admin"],"summary":"Flags Stats","description":"Aggregate top_flags counts across all analyses in the window.","operationId":"flags_stats_api_admin_flags_stats_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":7,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/dashboard":{"get":{"tags":["admin-ops"],"summary":"Dashboard","operationId":"dashboard_api_admin_ops_dashboard_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"default":24,"title":"Hours"}},{"name":"trends","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Trends"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/throttled":{"get":{"tags":["admin-ops"],"summary":"Throttled Ips","description":"Топ IP по количеству 429 за окно. Админ видит кого рейтлимитит —\nи может быстро добавить override в /admin/quotas если это легит.\n\nВозвращает:\n  - total_hits: общее число 429 за окно\n  - unique_ips: уникальных IP\n  - top: [{client_ip, count, last_ts, paths[3]}, …]","operationId":"throttled_ips_api_admin_ops_throttled_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"default":24,"title":"Hours"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/recent":{"get":{"tags":["admin-ops"],"summary":"Recent Requests","operationId":"recent_requests_api_admin_ops_recent_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"since_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Since Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stream/activity":{"get":{"tags":["admin-ops"],"summary":"Stream Activity","description":"SSE stream of recent RequestLog inserts. Polls DB every 2s and emits\nnew rows. Single-process safe. If client disconnects, generator exits.\n\nЗащита от pool-exhaustion: лимит одновременных стримов per-IP +\nжёсткий max-age в 30 мин на сам stream. После max-age клиенту\nотправляется `event: close` и stream завершается — клиент сам\nреконнектится.","operationId":"stream_activity_api_admin_ops_stream_activity_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/clients":{"get":{"tags":["admin-ops"],"summary":"List Clients","operationId":"list_clients_api_admin_ops_clients_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"marketplace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketplace"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/clients/{ip}":{"get":{"tags":["admin-ops"],"summary":"Client Detail","operationId":"client_detail_api_admin_ops_clients__ip__get","parameters":[{"name":"ip","in":"path","required":true,"schema":{"type":"string","title":"Ip"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":2160,"minimum":1,"default":168,"title":"Hours"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/bans":{"get":{"tags":["admin-ops"],"summary":"List Bans","operationId":"list_bans_api_admin_ops_bans_get","parameters":[{"name":"include_inactive","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Inactive"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":500,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["admin-ops"],"summary":"Create Ban","operationId":"create_ban_api_admin_ops_bans_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBanBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/bans/{ban_id}/lift":{"post":{"tags":["admin-ops"],"summary":"Lift Ban","operationId":"lift_ban_api_admin_ops_bans__ban_id__lift_post","parameters":[{"name":"ban_id","in":"path","required":true,"schema":{"type":"integer","title":"Ban Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/quotas":{"get":{"tags":["admin-ops"],"summary":"List Quotas","operationId":"list_quotas_api_admin_ops_quotas_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":500,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["admin-ops"],"summary":"Upsert Quota","operationId":"upsert_quota_api_admin_ops_quotas_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertQuotaBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/quotas/{quota_id}":{"delete":{"tags":["admin-ops"],"summary":"Delete Quota","operationId":"delete_quota_api_admin_ops_quotas__quota_id__delete","parameters":[{"name":"quota_id","in":"path","required":true,"schema":{"type":"integer","title":"Quota Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/logs":{"get":{"tags":["admin-ops"],"summary":"List Logs","operationId":"list_logs_api_admin_ops_logs_get","parameters":[{"name":"status_min","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":999,"minimum":100},{"type":"null"}],"title":"Status Min"}},{"name":"status_max","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":999,"minimum":100},{"type":"null"}],"title":"Status Max"}},{"name":"errors_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Errors Only"}},{"name":"path","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path"}},{"name":"ip","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip"}},{"name":"marketplace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketplace"}},{"name":"endpoint_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Endpoint Type"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":2160,"minimum":1,"default":24,"title":"Hours"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/audit":{"get":{"tags":["admin-ops"],"summary":"List Audit","operationId":"list_audit_api_admin_ops_audit_get","parameters":[{"name":"action","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action"}},{"name":"admin_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Admin Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/settings":{"get":{"tags":["admin-ops"],"summary":"Get Settings","operationId":"get_settings_api_admin_ops_settings_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["admin-ops"],"summary":"Update Settings","operationId":"update_settings_api_admin_ops_settings_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSettingsBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/webmaster/connect":{"get":{"tags":["admin-ops"],"summary":"Webmaster Connect Start","description":"Кнопка «Подключить Вебмастер»: 302 на Яндекс через приложение входа.\n\nИтог придёт на /auth/yandex/callback (state с префиксом «wm.») и вернётся\nв настройки с ?wm=<код>. Защита от подделки: services/webmaster_connect.py.","operationId":"webmaster_connect_start_api_admin_ops_webmaster_connect_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses.csv":{"get":{"tags":["admin-ops"],"summary":"Export Analyses Csv","operationId":"export_analyses_csv_api_admin_ops_analyses_csv_get","parameters":[{"name":"marketplace","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketplace"}},{"name":"verdict","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict"}},{"name":"min_suspicion","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Min Suspicion"}},{"name":"max_suspicion","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Max Suspicion"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses/{analysis_id}/edit":{"post":{"tags":["admin-ops"],"summary":"Edit Analysis","operationId":"edit_analysis_api_admin_ops_analyses__analysis_id__edit_post","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisEditBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses/{analysis_id}/hide":{"post":{"tags":["admin-ops"],"summary":"Hide Analysis","operationId":"hide_analysis_api_admin_ops_analyses__analysis_id__hide_post","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses/bulk-hide":{"post":{"tags":["admin-ops"],"summary":"Bulk Hide Analyses","description":"Массовое скрытие проверок по фильтру. is_hidden=False → True.\n\nНе удаляет — soft-hide, можно вернуть через unhide. Безопасно даже на\nтысячах строк (single UPDATE), но требует `confirm=true` чтобы случайно\nне убрать всю ленту.","operationId":"bulk_hide_analyses_api_admin_ops_analyses_bulk_hide_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkHideRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses/{analysis_id}/feature":{"post":{"tags":["admin-ops"],"summary":"Feature Analysis","operationId":"feature_analysis_api_admin_ops_analyses__analysis_id__feature_post","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses/{analysis_id}/unfeature":{"post":{"tags":["admin-ops"],"summary":"Unfeature Analysis","operationId":"unfeature_analysis_api_admin_ops_analyses__analysis_id__unfeature_post","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/featured":{"get":{"tags":["admin-ops"],"summary":"List Featured","description":"Все featured за всё время (не TTL-ограничено, в отличие от публичного).\n\nИспользуется на /admin/featured — страница где мы unfeature'им устаревшее\nили возвращаем скрытое обратно.","operationId":"list_featured_api_admin_ops_featured_get","parameters":[{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Search"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/dashboard-timeline":{"get":{"tags":["admin-ops"],"summary":"Dashboard Timeline","operationId":"dashboard_timeline_api_admin_ops_dashboard_timeline_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"default":24,"title":"Hours"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/tg-test":{"post":{"tags":["admin-ops"],"summary":"Tg Test Alert","operationId":"tg_test_alert_api_admin_ops_tg_test_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses/{analysis_id}/unhide":{"post":{"tags":["admin-ops"],"summary":"Unhide Analysis","operationId":"unhide_analysis_api_admin_ops_analyses__analysis_id__unhide_post","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses/{analysis_id}":{"delete":{"tags":["admin-ops"],"summary":"Delete Analysis","operationId":"delete_analysis_api_admin_ops_analyses__analysis_id__delete","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"force","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Force"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["admin-ops"],"summary":"Update Analysis","operationId":"update_analysis_api_admin_ops_analyses__analysis_id__put","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisUpdateBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/feed":{"get":{"tags":["admin-ops"],"summary":"Admin Feed","description":"Карточная модерационная лента.\n\n`since` ∈ {today, 24h, 7d, all} — меняет только выдачу items.\nHeader-summary всегда считается за 24h независимо от фильтров, чтобы\nсверху не скакали цифры при переключении фильтра.","operationId":"admin_feed_api_admin_ops_feed_get","parameters":[{"name":"mp","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mp"}},{"name":"verdict","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict"}},{"name":"since","in":"query","required":false,"schema":{"type":"string","default":"24h","title":"Since"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Search"}},{"name":"include_hidden","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Hidden"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":96,"minimum":1,"default":24,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/ozon-debug/latest":{"get":{"tags":["admin-ops"],"summary":"Marketplace Debug Latest","operationId":"marketplace_debug_latest_api_admin_ops_ozon_debug_latest_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"marketplace","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Marketplace"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/marketplace-debug/latest":{"get":{"tags":["admin-ops"],"summary":"Marketplace Debug Latest","operationId":"marketplace_debug_latest_api_admin_ops_marketplace_debug_latest_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"marketplace","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Marketplace"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/ozon-debug/{dump_id}":{"get":{"tags":["admin-ops"],"summary":"Marketplace Debug Get","operationId":"marketplace_debug_get_api_admin_ops_ozon_debug__dump_id__get","parameters":[{"name":"dump_id","in":"path","required":true,"schema":{"type":"integer","title":"Dump Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["admin-ops"],"summary":"Marketplace Debug Delete","operationId":"marketplace_debug_delete_api_admin_ops_ozon_debug__dump_id__delete","parameters":[{"name":"dump_id","in":"path","required":true,"schema":{"type":"integer","title":"Dump Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/marketplace-debug/{dump_id}":{"get":{"tags":["admin-ops"],"summary":"Marketplace Debug Get","operationId":"marketplace_debug_get_api_admin_ops_marketplace_debug__dump_id__get","parameters":[{"name":"dump_id","in":"path","required":true,"schema":{"type":"integer","title":"Dump Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["admin-ops"],"summary":"Marketplace Debug Delete","operationId":"marketplace_debug_delete_api_admin_ops_marketplace_debug__dump_id__delete","parameters":[{"name":"dump_id","in":"path","required":true,"schema":{"type":"integer","title":"Dump Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/devices/stats":{"get":{"tags":["admin-ops"],"summary":"Devices Stats","operationId":"devices_stats_api_admin_ops_devices_stats_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/parsing-health":{"get":{"tags":["admin-ops"],"summary":"Parsing Health","description":"Здоровье серверных парсеров за последние 24 часа.\n\nИдея: владелец открывает /admin/marketplace-debug и за 5 секунд видит\n«парсер X справляется на 92%, парсер Y лежит». Никаких графиков —\nодна строка на маркет, цвет = severity.","operationId":"parsing_health_api_admin_ops_parsing_health_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/health-summary":{"get":{"tags":["admin-ops"],"summary":"Health Summary","description":"Top-bar health badge: agg redis/db/cron/glitchtip/workers статус.\n\nCalled on every admin page load + every 60s. Must NOT crash on Redis\nbeing down — это и есть то, что мы отчитываем. Short-lived selects only.","operationId":"health_summary_api_admin_ops_health_summary_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/sentry-summary":{"get":{"tags":["admin-ops"],"summary":"Sentry Summary","description":"Сводка по GlitchTip: открытые issue + топ-5 по двум проектам (api+ext).\n\nНа любой сбой — `available=false` + текстовый error, UI показывает\nзаглушку и не валится. ext=null если проект не настроен/404.","operationId":"sentry_summary_api_admin_ops_sentry_summary_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses/search-full":{"get":{"tags":["admin-ops"],"summary":"Analyses Search Full","description":"Расширенный поиск анализов: q ищет в url + product_name + product.seller,\nip — exact match по client_ip.\n\nВозвращает облегчённую структуру (без image_url/summary) — для таблицы\nв /admin/analyses сверху над основной лентой.","operationId":"analyses_search_full_api_admin_ops_analyses_search_full_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Q"}},{"name":"ip","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Ip"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/active-now":{"get":{"tags":["admin-ops"],"summary":"Active Now","description":"Сколько устройств активны прямо сейчас (5 мин) и за последний час.\n\n`online_extensions` = `active_5min`, продублирован под понятным именем\nдля дашбордной KPI-карточки «🟢 N онлайн».","operationId":"active_now_api_admin_ops_active_now_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/devices/{device_uuid}/export.json":{"get":{"tags":["admin-ops"],"summary":"Device Export Json","description":"Полная выгрузка устройства: ExtDevice + active DeviceBan + последние\nсобытия телеметрии (ext_events). Возвращается как JSON-файл с\nContent-Disposition: attachment, чтобы браузер сразу скачивал.","operationId":"device_export_json_api_admin_ops_devices__device_uuid__export_json_get","parameters":[{"name":"device_uuid","in":"path","required":true,"schema":{"type":"string","title":"Device Uuid"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/clients/bulk-ban":{"post":{"tags":["admin-ops"],"summary":"Clients Bulk Ban","description":"Массовый бан списка IP'ов одной причиной (для жёсткого ответа на\ncoordinated abuse). expires_at=NULL — бан навсегда (admin может снять\nточечно через /admin/bans).","operationId":"clients_bulk_ban_api_admin_ops_clients_bulk_ban_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkBanBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/devices":{"get":{"tags":["admin-ops"],"summary":"Devices List","description":"v0.14.9: возвращаем также `device_uuid_full` (для ban-кнопки в UI) +\n`is_banned`/`ban_reason`/`ban_expires_at` если устройство забанено.\nПолный uuid допустимо отдавать админу — это random-UUID без PII.","operationId":"devices_list_api_admin_ops_devices_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"version","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Version"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/devices/{device_uuid}/details":{"get":{"tags":["admin-ops"],"summary":"Device Details","description":"v0.14.16: расширенная инфа об устройстве — ДО решения банить.\n\nЮзер: «зачем сразу даёт уведомление, я хочу инфу посмотреть».\nВозвращаем:\n  - Базовый ExtDevice (всё что было в /devices list)\n  - Активный ban (если есть)\n  - История последних 20 ошибок из ext_errors (scope, message, when)\n  - Последние 20 событий из ext_events (event-type, when)\n  - Аналитика: total errors / unique scopes / errors_24h","operationId":"device_details_api_admin_ops_devices__device_uuid__details_get","parameters":[{"name":"device_uuid","in":"path","required":true,"schema":{"type":"string","title":"Device Uuid"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/devices/{device_uuid}/ban":{"post":{"tags":["admin-ops"],"summary":"Device Ban","description":"Забанить устройство по device_uuid. Идемпотентно: повторный ban\nобновляет reason/expires + сбрасывает lifted_at если был unban.","operationId":"device_ban_api_admin_ops_devices__device_uuid__ban_post","parameters":[{"name":"device_uuid","in":"path","required":true,"schema":{"type":"string","title":"Device Uuid"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceBanBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/devices/{device_uuid}/unban":{"post":{"tags":["admin-ops"],"summary":"Device Unban","description":"Снять бан: устанавливаем lifted_at = NOW(). Запись остаётся\nисторической (видна в /devices/banned с фильтром «Включая снятые»).","operationId":"device_unban_api_admin_ops_devices__device_uuid__unban_post","parameters":[{"name":"device_uuid","in":"path","required":true,"schema":{"type":"string","title":"Device Uuid"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/devices/banned":{"get":{"tags":["admin-ops"],"summary":"Devices Banned","description":"Список забаненных устройств. По умолчанию только активные банны\n(где lifted_at IS NULL и expires_at не истёк).","operationId":"devices_banned_api_admin_ops_devices_banned_get","parameters":[{"name":"include_lifted","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Lifted"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/ozon-debug":{"delete":{"tags":["admin-ops"],"summary":"Marketplace Debug Clear","operationId":"marketplace_debug_clear_api_admin_ops_ozon_debug_delete","parameters":[{"name":"marketplace","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Marketplace"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/marketplace-debug":{"delete":{"tags":["admin-ops"],"summary":"Marketplace Debug Clear","operationId":"marketplace_debug_clear_api_admin_ops_marketplace_debug_delete","parameters":[{"name":"marketplace","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Marketplace"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/images/stats":{"get":{"tags":["admin-ops"],"summary":"Image Health Stats","description":"v0.17: KPI «битые картинки» по всем маркетплейсам.\n\nJSONB-фильтрация: image_url достаём из result_json->'product'->>'image_url',\nстатус health-check'а — из result_json->'_image_check'->>'status'.\n\nPerformance: на 100k analyses query идёт через partial scan (where created_at)\n+ JSONB extract. На текущих объёмах — ~150ms; если станет тяжело, можно\nдобавить индекс на (created_at, ((result_json->'product'->>'image_url')='')).","operationId":"image_health_stats_api_admin_ops_images_stats_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":1,"default":30,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/ext-errors/stats":{"get":{"tags":["admin-ops"],"summary":"Ext Errors Stats","operationId":"ext_errors_stats_api_admin_ops_ext_errors_stats_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":720,"minimum":1,"default":168,"title":"Hours"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/ext-errors":{"get":{"tags":["admin-ops"],"summary":"Ext Errors List","operationId":"ext_errors_list_api_admin_ops_ext_errors_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"scope","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Scope"}},{"name":"version","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Version"}},{"name":"marketplace","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Marketplace"}},{"name":"device_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Device Id"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":720,"minimum":1,"default":168,"title":"Hours"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["admin-ops"],"summary":"Ext Errors Clear","description":"Чистка записей. Без параметров — удаляет ВСЁ (требует подтверждения на UI).\nС `older_than_hours` — удаляет только старше N часов (ротация).\nС `scope` — ограничивает по scope.","operationId":"ext_errors_clear_api_admin_ops_ext_errors_delete","parameters":[{"name":"scope","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Scope"}},{"name":"older_than_hours","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":8760,"minimum":1},{"type":"null"}],"title":"Older Than Hours"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/tg-stats":{"get":{"tags":["admin-ops"],"summary":"Tg Bot Stats","operationId":"tg_bot_stats_api_admin_ops_tg_stats_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/auth-log":{"get":{"tags":["admin-ops"],"summary":"List Auth Log","operationId":"list_auth_log_api_admin_ops_auth_log_get","parameters":[{"name":"event_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event Type"}},{"name":"provider","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"}},{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Search"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":7,"title":"Days"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users":{"get":{"tags":["admin-ops"],"summary":"List Users","operationId":"list_users_api_admin_ops_users_get","parameters":[{"name":"tier","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tier"}},{"name":"blocked","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Blocked"}},{"name":"provider","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Search"}},{"name":"days","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":365,"minimum":1},{"type":"null"}],"title":"Days"}},{"name":"show","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"trash | active (default)","title":"Show"},"description":"trash | active (default)"},{"name":"include_extras","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Extras"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users/{user_id}":{"get":{"tags":["admin-ops"],"summary":"Get User Detail","operationId":"get_user_detail_api_admin_ops_users__user_id__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"history_limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"History Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["admin-ops"],"summary":"Patch User","operationId":"patch_user_api_admin_ops_users__user_id__patch","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPatchBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["admin-ops"],"summary":"Delete User","operationId":"delete_user_api_admin_ops_users__user_id__delete","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users/{user_id}/grant-pro":{"post":{"tags":["admin-ops"],"summary":"Grant Pro","operationId":"grant_pro_api_admin_ops_users__user_id__grant_pro_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantProBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users/{user_id}/grant-checks":{"post":{"tags":["admin-ops"],"summary":"Grant Checks","description":"v0.12.18: начислить юзеру N проверок с пояснением.\n\nUse-case: компенсация когда что-то сломалось у нас (упал GigaChat,\nпарсер вернул мусор, юзер заплатил но webhook не дошёл, ...).\nАльтернатива email-CS-обращению с ручным SQL-INSERT'ом.\n\nАтомарно: add_balance в users.checks_balance + audit-log + notif юзеру.\nПроверки НЕ сгорают (контракт пакетов), идут поверх Free-квоты и\n1000/мес Pro-квоты — тратятся первыми.\n\nЗащита:\n- admin-only (TOTP через get_current_admin middleware)\n- audit-trail в admin_audit_log (видно кто/когда/кому/сколько)\n- max 10000 за раз (Field validator)\n- reason обязателен 3..200 символов — fraud-protection: чтобы случайно\n  кликнутая кнопка требовала explicit заполнение","operationId":"grant_checks_api_admin_ops_users__user_id__grant_checks_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantChecksBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users/{user_id}/notify":{"post":{"tags":["admin-ops"],"summary":"Admin Notify User","description":"Отправить персональное in-app уведомление юзеру.","operationId":"admin_notify_user_api_admin_ops_users__user_id__notify_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotifyUserBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/notifications/broadcast":{"post":{"tags":["admin-ops"],"summary":"Admin Notify Broadcast","description":"Разослать уведомление подгруппе юзеров (all/pro/free).\n\nВозвращает число отправленных. Commit делается здесь — rowcount точный.","operationId":"admin_notify_broadcast_api_admin_ops_notifications_broadcast_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotifyBroadcastBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/notifications/recent":{"get":{"tags":["admin-ops"],"summary":"Admin Notifications Recent","description":"Последние уведомления для overview в /admin/notify — видеть, что\nюзеры/система присылают (последние broadcast, direct, auto).","operationId":"admin_notifications_recent_api_admin_ops_notifications_recent_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/funnel":{"get":{"tags":["admin-ops"],"summary":"Stats Funnel","description":"Воронка конверсии за последние `days` дней.\n\nСтупени — свободно-верная оценка, не точный трекинг. Для uniq_visitors\nиспользуем DISTINCT client_ip по RequestLog (исключая админский трафик\nи 404/5xx boot-ноиз). `did_check` считаем по DISTINCT client_ip в\nAnalysis (только успешные проверки, уже дедуплицированные). Логин,\nclicked-pro, started_payment, paid — каждый следующий шаг считается\nнезависимо, поэтому сумма шагов ≠ визитам; это нормально для такой\nприкидочной воронки.","operationId":"stats_funnel_api_admin_ops_stats_funnel_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":7,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/daily":{"get":{"tags":["admin-ops"],"summary":"Stats Daily","description":"Daily-stacked чарт: visitors / anon_checks / new_users / paid.","operationId":"stats_daily_api_admin_ops_stats_daily_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":30,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/cohort":{"get":{"tags":["admin-ops"],"summary":"Stats Cohort","description":"Недельные когорты удержания логинов.\n\nСтрока — неделя регистрации, столбцы — W0..W(N-1). Активность юзера\nна неделе = есть запись в user_checks. Без UserCheck у нас нет другого\nсигнала активности (last_login_at хранит только последнюю дату,\nагрегировать по неделям нельзя).","operationId":"stats_cohort_api_admin_ops_stats_cohort_get","parameters":[{"name":"weeks","in":"query","required":false,"schema":{"type":"integer","maximum":12,"minimum":3,"default":6,"title":"Weeks"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/revenue":{"get":{"tags":["admin-ops"],"summary":"Stats Revenue","description":"Выручка: MRR, сегодня, месяц, прогноз +30д.","operationId":"stats_revenue_api_admin_ops_stats_revenue_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/llm":{"get":{"tags":["admin-ops"],"summary":"Stats Llm","description":"Оценка стоимости LLM-вызовов (GigaChat Lite).\n\nТочный учёт токенов появится с таблицей `llm_calls` (TODO). До тех пор\n— прикидка: 2 вызова на один Analysis, ~1200 input + ~350 output токенов\nкаждый, тариф 0.2₽/1K input + 0.6₽/1K output.","operationId":"stats_llm_api_admin_ops_stats_llm_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/referral":{"get":{"tags":["admin-ops"],"summary":"Stats Referral","description":"Viral-loop: daily new users (referral vs organic) + K-factor + top referrers.\n\n- **Daily split** — за `days` дней, регистрации с referred_by vs без.\n- **K-factor** — (invited за 7 дней) / (active users за 7 дней).\n  Active user = тот, кто делал проверку (UserCheck.created_at >= 7d).\n  Цифра >1.0 — exponential growth; 0.1-0.3 — типичный здоровый запуск.\n- **Top referrers** — топ-10 по числу приглашённых за всё время.","operationId":"stats_referral_api_admin_ops_stats_referral_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":7,"default":30,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/signup-sources":{"get":{"tags":["admin-ops"],"summary":"Stats Signup Sources","description":"Breakdown signup_source за последние `days` дней.\n\nResponse:\n    {\n      \"days\": 30,\n      \"total_signups\": 312,\n      \"total_pro_signups\": 28,\n      \"sources\": [\n         {\"source\": \"share\",   \"signups\": 180, \"pro_signups\": 14},\n         {\"source\": \"direct\",  \"signups\": 80,  \"pro_signups\": 6},\n         {\"source\": \"vk_ads\",  \"signups\": 30,  \"pro_signups\": 5},\n         ...\n      ]\n    }","operationId":"stats_signup_sources_api_admin_ops_stats_signup_sources_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/anonymous":{"get":{"tags":["admin-ops"],"summary":"Stats Anonymous","description":"KPI «Анонимная аудитория» за выбранное окно.\n\nResponse:\n    {\n      \"window\": \"7d\",\n      \"since\": \"...ISO...\",\n      \"unique_anons\": 1842,\n      \"total_anon_checks\": 7320,\n      \"median_checks_per_anon\": 2,\n      \"p90_checks_per_anon\": 8,\n      \"max_checks_per_anon\": 14,\n      \"exhausted_count\": 38,           # кандидаты на регистрацию\n      \"exhausted_share_pct\": 2.06,\n      \"registered_after_anon\": 12,\n      \"conversion_pct\": 0.65,\n      \"top_ips\": [{\"ip\": \"1.2.3.4\", \"anons\": 4, \"checks\": 31}, ...]\n    }","operationId":"stats_anonymous_api_admin_ops_stats_anonymous_get","parameters":[{"name":"window","in":"query","required":false,"schema":{"type":"string","pattern":"^(24h|7d|30d)$","default":"7d","title":"Window"}},{"name":"trend","in":"query","required":false,"schema":{"type":"integer","maximum":60,"minimum":0,"default":0,"title":"Trend"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/ext-reports/latest":{"get":{"tags":["admin-ops"],"summary":"Admin Ext Reports Latest","description":"Список последних user-report'ов. Фильтры по статусу и типу опциональны.","operationId":"admin_ext_reports_latest_api_admin_ops_ext_reports_latest_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Status"}},{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Kind"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/ext-reports/{report_id}":{"get":{"tags":["admin-ops"],"summary":"Admin Ext Reports Detail","operationId":"admin_ext_reports_detail_api_admin_ops_ext_reports__report_id__get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"integer","title":"Report Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["admin-ops"],"summary":"Admin Ext Reports Update","description":"Обновить статус и/или admin_note. Body: {status?, admin_note?}.","operationId":"admin_ext_reports_update_api_admin_ops_ext_reports__report_id__patch","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"integer","title":"Report Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/support/threads":{"get":{"tags":["admin-ops"],"summary":"Admin List Support Threads","description":"Список всех тредов. Фильтры по статусу и user_id. Сортировка —\nlast_message_at DESC (самые свежие сверху — что нужно ответить).","operationId":"admin_list_support_threads_api_admin_ops_support_threads_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/support/unread-count":{"get":{"tags":["admin-ops"],"summary":"Admin Support Unread Count","description":"Лёгкий endpoint для бейджа в сайдбаре — сколько тредов ждёт\nответа админа. Используется polling'ом каждые 30с в admin.js.","operationId":"admin_support_unread_count_api_admin_ops_support_unread_count_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/support/threads/{thread_id}":{"get":{"tags":["admin-ops"],"summary":"Admin Get Support Thread","description":"Полная ветка + снапшот юзера (email/tier/кол-во проверок).\n\nВозвращает ВСЕ сообщения, включая internal_note — админ должен видеть\nвнутренние заметки коллег. Помечает mark_seen_by_admin.","operationId":"admin_get_support_thread_api_admin_ops_support_threads__thread_id__get","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"integer","title":"Thread Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/support/threads/{thread_id}/messages":{"post":{"tags":["admin-ops"],"summary":"Admin Post Support Message","description":"Админский ответ или internal-заметка. При non-internal — notify_user\nшлёт юзеру ping «Ответ от поддержки» с link_url=/support/{tid}.","operationId":"admin_post_support_message_api_admin_ops_support_threads__thread_id__messages_post","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"integer","title":"Thread Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SupportReplyBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/support/threads/{thread_id}/status":{"post":{"tags":["admin-ops"],"summary":"Admin Change Support Status","description":"Меняем статус треда. resolved/closed → проставляем closed_at+admin.","operationId":"admin_change_support_status_api_admin_ops_support_threads__thread_id__status_post","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"integer","title":"Thread Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SupportStatusBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/support/threads/{thread_id}/assign":{"post":{"tags":["admin-ops"],"summary":"Admin Assign Support","description":"Назначаем ответственного админа. admin_id=null — снять assign.","operationId":"admin_assign_support_api_admin_ops_support_threads__thread_id__assign_post","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"integer","title":"Thread Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SupportAssignBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/support/threads/{thread_id}/priority":{"post":{"tags":["admin-ops"],"summary":"Admin Set Support Priority","description":"Выставить priority=high на критичные треды (оплата не прошла и т.п.).","operationId":"admin_set_support_priority_api_admin_ops_support_threads__thread_id__priority_post","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"integer","title":"Thread Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_SupportPriorityBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/alerts":{"get":{"tags":["admin-ops"],"summary":"Admin Alerts","description":"Агрегированный счётчик для алерт-бара наверху каждой admin-страницы.\n\nСчитает три категории:\n  * support — unread support-threads\n  * ext_reports — ExtUserReport со статусом `new`\n  * payment_failed_24h — платежи с status=rejected за 24ч\n\nv0.27.37: использует _run_db_shielded вместо Depends(get_db) — это самый\nчастый auto-poll endpoint, главный источник connection-leak'а #37. Shield\nгарантирует возврат коннекта в пул даже при client-disconnect.","operationId":"admin_alerts_api_admin_ops_alerts_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/events-stream":{"get":{"tags":["admin-ops"],"summary":"Admin Events Stream","description":"Merged stream последних N событий требующих внимания.\n\nОбъединяет:\n  * support-threads (OPEN / PENDING_ADMIN) — ждут ответа\n  * ext-reports status=new\n  * payments status=rejected за 24ч\n  * admin.user_grant_pro за 7 дней (аудит — видеть действия коллег)\n\nФормат единый: {type, ts, title, subtitle, link, icon, severity}.\nseverity: \"warn\" | \"info\" | \"error\" — UI красит акцент.","operationId":"admin_events_stream_api_admin_ops_events_stream_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/cron-health":{"get":{"tags":["admin-ops"],"summary":"Admin Cron Health","description":"v0.12.21: Состояние всех cron-сервисов через Redis heartbeat.\n\nВозвращает list[{name, expected_interval_s, last_seen, age_seconds,\nstatus=\"ok\"|\"stale\"|\"dead\"|\"never\", last_status}].\n\nЕсли cron не писал heartbeat дольше 1.5× expected_interval — `stale`,\nдольше 3× — `dead`. Это важно для visibility «Pro-bonus 1-го числа\nне пришёл — почему?».","operationId":"admin_cron_health_api_admin_ops_cron_health_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/pro-quota-usage":{"get":{"tags":["admin-ops"],"summary":"Admin Stats Pro Quota Usage","description":"v0.12.21: Распределение Pro-юзеров по used_in_last_30d.\n\nВозвращает:\n    active_pro_total — сколько активных Pro\n    at_quota_now    — сколько уже исчерпали 1000/мес\n    near_quota      — used >= 800 (не исчерпали, но скоро)\n    buckets         — гистограмма [0-200, 200-500, 500-800, 800-1000, 1000+]\n    top_users       — топ-10 Pro-юзеров по used_30d (для отдельной admin-страницы)\n\nИспользуется в admin dashboard KPI «Pro at quota X / Y».","operationId":"admin_stats_pro_quota_usage_api_admin_ops_stats_pro_quota_usage_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/daily-verdicts":{"get":{"tags":["admin-ops"],"summary":"Stats Daily Verdicts","description":"Stacked распределение verdict'ов по дням.\n\nДля stacked-area в /admin/stats. Бакеты дней — МСК (как в /stats/daily).","operationId":"stats_daily_verdicts_api_admin_ops_stats_daily_verdicts_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"default":30,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users/{user_id}/support-threads":{"get":{"tags":["admin-ops"],"summary":"Admin User Support Threads","description":"Последние N support-тредов конкретного юзера + total count.\n\nОбёртка над list_threads_for_admin — UI получает семантический endpoint.","operationId":"admin_user_support_threads_api_admin_ops_users__user_id__support_threads_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":20,"minimum":1,"default":3,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/leads":{"get":{"tags":["admin-ops"],"summary":"List Seller Leads","description":"Список B2B-заявок. Сортировка: status='new' first, затем created_at DESC.","operationId":"list_seller_leads_api_admin_ops_leads_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/leads/{lead_id}/status":{"post":{"tags":["admin-ops"],"summary":"Update Seller Lead Status","description":"Поменять статус заявки. Контрол на 4 значения.","operationId":"update_seller_lead_status_api_admin_ops_leads__lead_id__status_post","parameters":[{"name":"lead_id","in":"path","required":true,"schema":{"type":"integer","title":"Lead Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeadStatusBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/leads/{lead_id}":{"delete":{"tags":["admin-ops"],"summary":"Delete Seller Lead","description":"Удалить заявку (например, спам). Hard-delete — заявок мало, soft-delete не нужен.","operationId":"delete_seller_lead_api_admin_ops_leads__lead_id__delete","parameters":[{"name":"lead_id","in":"path","required":true,"schema":{"type":"integer","title":"Lead Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/billing/payments":{"get":{"tags":["admin-ops"],"summary":"Admin Billing Payments","description":"Список платежей с фильтрами. Используется в /admin/billing.","operationId":"admin_billing_payments_api_admin_ops_billing_payments_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"description":"email / order_id / tbank_payment_id / user_id","title":"Q"},"description":"email / order_id / tbank_payment_id / user_id"},{"name":"has_extend","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Только advance-extend","title":"Has Extend"},"description":"Только advance-extend"},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/billing/payments/{payment_id}/sync":{"post":{"tags":["admin-ops"],"summary":"Admin Billing Payment Sync","description":"Manual T-Bank GetState для конкретного платежа.\n\nUse-case: webhook потерялся (sweeper не дотянулся, network glitch),\nюзер пишет в support «оплатил, доступа нет». Админ открывает\n/admin/billing, видит NEW-платёж, жмёт «Sync» → мы делаем GetState\nв T-Bank → если CONFIRMED → запускаем _on_payment_confirmed вручную.\nИдемпотентно: для уже CONFIRMED — noop.","operationId":"admin_billing_payment_sync_api_admin_ops_billing_payments__payment_id__sync_post","parameters":[{"name":"payment_id","in":"path","required":true,"schema":{"type":"integer","title":"Payment Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/billing/bulk-grant-checks":{"post":{"tags":["admin-ops"],"summary":"Admin Bulk Grant Checks","description":"v0.12.22: bulk-выдача проверок (компенсация инцидента).\n\ntarget:\n  • all_pro          — все юзеры с активной Pro-подпиской\n  • active_users_14d — юзеры пробившие хотя бы 1 проверку за последние 14д\n  • by_user_ids      — точечно по списку user_ids (max 1000)\n\nЗащита:\n- admin-only TOTP (через get_current_admin)\n- dry_run по дефолту → возвращает кол-во target'ов БЕЗ начисления\n- max 1000 проверок на юзера, max 1000 user_ids\n- explicit reason ≥3 символа (видно в notif)\n- audit-лог с count/target/dry_run","operationId":"admin_bulk_grant_checks_api_admin_ops_billing_bulk_grant_checks_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkGrantChecksBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/billing/bulk-grant-history":{"get":{"tags":["admin-ops"],"summary":"Admin Bulk Grant History","description":"v0.14.9: история bulk-grant'ов из admin_audit_log.\n\nЧитает action='admin.bulk_grant_checks' с details JSONB. Без отдельной\nтаблицы — переиспользуем уже существующий audit. Ниже видно:\nкогда / кто из админов / target / count / reason / итог granted/failed.","operationId":"admin_bulk_grant_history_api_admin_ops_billing_bulk_grant_history_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/bulk-grant/preview-count":{"get":{"tags":["admin-ops"],"summary":"Bulk Grant Preview Count","description":"v0.21: live-preview подсчёт для cohort builder'а.\n\nПринимает cohort-фильтры в query string. Возвращает {count, sample_emails}.","operationId":"bulk_grant_preview_count_api_admin_ops_bulk_grant_preview_count_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/bulk-grant":{"post":{"tags":["admin-ops"],"summary":"Bulk Grant V2","description":"v0.21: единый endpoint cohort builder'а с расписанием и dry-run.\n\nЛогика:\n  • dry_run=True            — считаем, отдаём preview без записи.\n  • dry_run=False, mode=now — выполняем грант inline.\n  • dry_run=False, mode≠now — записываем в bulk_grant_jobs, cron исполнит.\n\nЗащиты:\n  • cohort > 10000 без user_ids — отклоняем (только через dry-run + split).\n  • cohort пустая — отклоняем.\n  • count капается Pydantic'ом 1..10000.","operationId":"bulk_grant_v2_api_admin_ops_bulk_grant_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkGrantBodyV2"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/bulk-grant/cancel":{"post":{"tags":["admin-ops"],"summary":"Bulk Grant Cancel","description":"Отменяет pending job.\n\nRace-condition: cron берёт FOR UPDATE SKIP LOCKED → если совпало,\ncron первым переключит pending→executing, наш UPDATE … WHERE\nstatus='pending' просто не найдёт строки и вернёт 409.","operationId":"bulk_grant_cancel_api_admin_ops_bulk_grant_cancel_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkGrantCancelBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/bulk-grant/undo":{"post":{"tags":["admin-ops"],"summary":"Bulk Grant Undo","description":"v0.21: откат последнего ручного bulk-grant.\n\nРезолвим юзеров по cohort из audit-row и списываем -count с checks_balance\n(но не уходим в минус — GREATEST(0, balance - count)). 24-часовое окно.","operationId":"bulk_grant_undo_api_admin_ops_bulk_grant_undo_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkGrantUndoBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/bulk-grant/history":{"get":{"tags":["admin-ops"],"summary":"Bulk Grant History V2","description":"v0.21: объединённая история — bulk_grant_jobs ∪ admin_audit_log.\n\nStatus filter:\n  pending|executing|done|cancelled|failed — фильтр jobs.\n  legacy — только audit-записи (v0.14.x bulk_grant_checks).\n  пусто — всё подряд.","operationId":"bulk_grant_history_v2_api_admin_ops_bulk_grant_history_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/bulk-grant/detail":{"get":{"tags":["admin-ops"],"summary":"Bulk Grant Detail","description":"v0.21: модалка деталей. source=job|audit.","operationId":"bulk_grant_detail_api_admin_ops_bulk_grant_detail_get","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"integer","minimum":1,"title":"Id"}},{"name":"source","in":"query","required":false,"schema":{"type":"string","default":"job","title":"Source"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/search":{"get":{"tags":["admin-ops"],"summary":"Admin Global Search","description":"Глобальный поиск. Ищет в users, payments, analyses, support по q.","operationId":"admin_global_search_api_admin_ops_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":200,"title":"Q"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/scan-network/stats":{"get":{"tags":["admin-ops"],"summary":"Scan Network Stats","description":"Stats для admin-widget: online ext-devices, queue depth, throughput, contributors.\n\nВозвращает:\n    {\n      online_devices: int,\n      queue_depth: int,\n      last_24h: { enqueued, completed, expired, error_returned, dedup_hits, cache_hit, cache_miss, by_mp: {...} },\n      hourly: [ {hour, data}, ... ] (last 24 buckets),\n      top_contributors: [ {device_uuid, scrape_count, last_seen}, ... ]\n    }","operationId":"scan_network_stats_api_admin_ops_scan_network_stats_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/revenue/timeline":{"get":{"tags":["admin-ops"],"summary":"Stats Revenue Timeline","description":"Daily revenue + MRR timeline + 30d projection.\n\nИспользуется в /admin/stats для трёх графиков под revenue-KPI:\n  - daily stacked-bar (revenue по дням, разделён по tier'ам)\n  - donut active subs by tier (вычисляется на /stats/revenue, не здесь)\n  - line MRR + projection","operationId":"stats_revenue_timeline_api_admin_ops_stats_revenue_timeline_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":7,"default":90,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/llm/timeline":{"get":{"tags":["admin-ops"],"summary":"Stats Llm Timeline","description":"Daily оценка стоимости LLM с low/mid/high-полосами.\n\nСчитаем количество анализов в день × коэффициент токенов × тариф.\nЭто та же оценка, что в /stats/llm — здесь распределена по дням.","operationId":"stats_llm_timeline_api_admin_ops_stats_llm_timeline_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":7,"default":60,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/marketplace-coverage":{"get":{"tags":["admin-ops"],"summary":"Stats Marketplace Coverage","description":"Распределение проверок по маркетплейсам + дневной timeline.\n\nДля donut + line на /admin/stats: «новый» card.","operationId":"stats_marketplace_coverage_api_admin_ops_stats_marketplace_coverage_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":1,"default":30,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/billing/stats":{"get":{"tags":["admin-ops"],"summary":"Billing Stats","description":"Чарты на /admin/billing: daily revenue, payment-status donut,\nARPU trend (90d weekly), stuck-payments daily.","operationId":"billing_stats_api_admin_ops_billing_stats_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":7,"default":30,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/leads/stats":{"get":{"tags":["admin-ops"],"summary":"Leads Stats","description":"B2B-leads chart-data: funnel + weekly conversion + sku-bucket donut.","operationId":"leads_stats_api_admin_ops_leads_stats_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":7,"default":90,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/tg-stats/trends":{"get":{"tags":["admin-ops"],"summary":"Tg Stats Trends","description":"DAU/WAU/MAU timeline для tg-stats.\n\nDAU за каждый день (количество уникальных user_id с событием в этот день),\nWAU/MAU вычисляются как rolling-окна (распределённый join по дням).","operationId":"tg_stats_trends_api_admin_ops_tg_stats_trends_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":7,"default":60,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/devices/trends":{"get":{"tags":["admin-ops"],"summary":"Devices Trends","description":"Phase 2A: DAU/WAU/MAU 60d + ежедневные новые установки 30d.\n\nДля line + bar на /admin/devices.","operationId":"devices_trends_api_admin_ops_devices_trends_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/ext-errors/timeline":{"get":{"tags":["admin-ops"],"summary":"Ext Errors Timeline","description":"Phase 2A: 30d total errors + unique devices line-chart.","operationId":"ext_errors_timeline_api_admin_ops_ext_errors_timeline_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":7,"default":30,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/auth-log/heatmap":{"get":{"tags":["admin-ops"],"summary":"Auth Log Heatmap","description":"Phase 2A: 7×24 матрица failed-логинов + success rate timeline 30d.\n\nHeatmap (день недели × час) → видно паттерны брутфорса.","operationId":"auth_log_heatmap_api_admin_ops_auth_log_heatmap_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/cron-health/history":{"get":{"tags":["admin-ops"],"summary":"Cron Health History","description":"Phase 2A: hourly статус-история cron-сервисов (для heatmap).\n\nСейчас в Redis хранится только LAST heartbeat — историю не восстанавливаем.\nПоэтому отдаём snapshot реплицированный по часам (placeholder), чтобы UI\nнарисовал что-то осмысленное. Это позволит юзеру увидеть структуру\nграфика, а после внедрения persist-таблицы реальные данные подставятся.\n\nTODO: persist heartbeat log в Postgres (cron_heartbeat_log table) с\nINSERT каждый раз при record_heartbeat().","operationId":"cron_health_history_api_admin_ops_cron_health_history_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":720,"minimum":24,"default":168,"title":"Hours"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/stats/pro-quota-usage/history":{"get":{"tags":["admin-ops"],"summary":"Stats Pro Quota Usage History","description":"Phase 2A: Pro at-quota timeline (90d daily).\n\nДля каждого дня считаем сколько активных Pro исчерпали квоту\n(used_30d ≥ pro_quota). Snapshot-приближение через user_checks.","operationId":"stats_pro_quota_usage_history_api_admin_ops_stats_pro_quota_usage_history_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":7,"default":90,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/clients/{ip}/timeline":{"get":{"tags":["admin-ops"],"summary":"Client Timeline","description":"Phase 2A: hourly status-breakdown за last N hours для client_detail.\n\nОтдаёт c2xx/c4xx/c5xx по часам — для stacked-bar-чарта.","operationId":"client_timeline_api_admin_ops_clients__ip__timeline_get","parameters":[{"name":"ip","in":"path","required":true,"schema":{"type":"string","title":"Ip"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":720,"minimum":24,"default":168,"title":"Hours"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users/bulk-block":{"post":{"tags":["admin-ops"],"summary":"Bulk Block Users","description":"Phase 2C: массово заблокировать N юзеров.\n\nВозвращает per-id результат: ok / error / not_found, чтобы UI показал\nдетально что прошло, а что нет.","operationId":"bulk_block_users_api_admin_ops_users_bulk_block_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkBlockBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users/bulk-grant-pro":{"post":{"tags":["admin-ops"],"summary":"Bulk Grant Pro Users","description":"Phase 2C: массово выдать Pro/Expert на N дней группе юзеров.\n\nИдёт через ту же логику что и `/users/{id}/grant-pro` — продлевает\nсуществующую подписку или создаёт новую с auto_renew=False.","operationId":"bulk_grant_pro_users_api_admin_ops_users_bulk_grant_pro_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkGrantProBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users/bulk-notify":{"post":{"tags":["admin-ops"],"summary":"Bulk Notify Users","description":"Phase 2C: массовое in-app уведомление выбранным юзерам.","operationId":"bulk_notify_users_api_admin_ops_users_bulk_notify_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkNotifyBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users/{user_id}/soft-delete":{"post":{"tags":["admin-ops"],"summary":"Soft Delete User","description":"Phase 2C: soft-delete юзера. Помечаем deleted_at = NOW() — юзер\nстановится невидимым продукту, но запись остаётся в БД 30 дней\n(или пока админ не сделает hard-delete из trash-вью).\n\nЮзер при попытке логина получит блок (is_blocked также взводим, чтобы\nauth_routes тут же отказывал).","operationId":"soft_delete_user_api_admin_ops_users__user_id__soft_delete_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users/{user_id}/restore":{"post":{"tags":["admin-ops"],"summary":"Restore User","description":"Phase 2C: вернуть soft-deleted юзера в активные.\n\nСнимаем deleted_at и is_blocked одновременно — иначе юзер по-прежнему\nне сможет войти.","operationId":"restore_user_api_admin_ops_users__user_id__restore_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/users.csv":{"get":{"tags":["admin-ops"],"summary":"Export Users Csv","description":"Phase 2C: CSV-экспорт списка юзеров с теми же фильтрами что /users.","operationId":"export_users_csv_api_admin_ops_users_csv_get","parameters":[{"name":"tier","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tier"}},{"name":"blocked","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Blocked"}},{"name":"provider","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Search"}},{"name":"days","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":365,"minimum":1},{"type":"null"}],"title":"Days"}},{"name":"show","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Show"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/feed/bulk-hide":{"post":{"tags":["admin-ops"],"summary":"Feed Bulk Hide","description":"Phase 2C: bulk-hide выбранных карточек ленты по id.\n\nВ отличие от /analyses/bulk-hide (фильтр-based), здесь ids — конкретный\nсписок выбранных чекбоксами.","operationId":"feed_bulk_hide_api_admin_ops_feed_bulk_hide_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedBulkIdsBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/feed/bulk-unhide":{"post":{"tags":["admin-ops"],"summary":"Feed Bulk Unhide","description":"Phase 2C: bulk-unhide — возвращаем скрытые карточки в ленту.","operationId":"feed_bulk_unhide_api_admin_ops_feed_bulk_unhide_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedBulkIdsBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/feed/bulk-feature":{"post":{"tags":["admin-ops"],"summary":"Feed Bulk Feature","description":"Phase 2C: добавить выбранные карточки в подборку редакции.","operationId":"feed_bulk_feature_api_admin_ops_feed_bulk_feature_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedBulkIdsBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/feed/bulk-unfeature":{"post":{"tags":["admin-ops"],"summary":"Feed Bulk Unfeature","description":"Phase 2C: убрать выбранные карточки из подборки.","operationId":"feed_bulk_unfeature_api_admin_ops_feed_bulk_unfeature_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedBulkIdsBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/leads.csv":{"get":{"tags":["admin-ops"],"summary":"Export Leads Csv","description":"Phase 2C: CSV-экспорт B2B заявок.","operationId":"export_leads_csv_api_admin_ops_leads_csv_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/audit.csv":{"get":{"tags":["admin-ops"],"summary":"Export Audit Csv","description":"Phase 2C: CSV-экспорт audit-log.","operationId":"export_audit_csv_api_admin_ops_audit_csv_get","parameters":[{"name":"action","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action"}},{"name":"admin_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Admin Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/logs.csv":{"get":{"tags":["admin-ops"],"summary":"Export Logs Csv","description":"Phase 2C: CSV-экспорт request_log с теми же фильтрами что /logs.","operationId":"export_logs_csv_api_admin_ops_logs_csv_get","parameters":[{"name":"errors_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Errors Only"}},{"name":"path","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path"}},{"name":"ip","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip"}},{"name":"endpoint_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Endpoint Type"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":2160,"minimum":1,"default":24,"title":"Hours"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/bans.csv":{"get":{"tags":["admin-ops"],"summary":"Export Bans Csv","description":"Phase 2C: CSV-экспорт списка банов.","operationId":"export_bans_csv_api_admin_ops_bans_csv_get","parameters":[{"name":"include_inactive","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Inactive"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses/{analysis_id}/siblings":{"get":{"tags":["admin-ops"],"summary":"Analysis Siblings","description":"Phase 2C: вернуть prev/next id для детальной страницы анализа.\n\nСортировка совпадает с /admin/analyses (created_at DESC) — «вперёд»\nозначает «более старая запись», «назад» — «более новая».","operationId":"analysis_siblings_api_admin_ops_analyses__analysis_id__siblings_get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/ext-reports/{report_id}/siblings":{"get":{"tags":["admin-ops"],"summary":"Ext Report Siblings","description":"Phase 2C: prev/next для /admin/ext-reports/{id}.","operationId":"ext_report_siblings_api_admin_ops_ext_reports__report_id__siblings_get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"integer","title":"Report Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/marketplace-debug/{dump_id}/siblings":{"get":{"tags":["admin-ops"],"summary":"Marketplace Debug Siblings","description":"Phase 2C: prev/next для /admin/marketplace-debug/{id}.","operationId":"marketplace_debug_siblings_api_admin_ops_marketplace_debug__dump_id__siblings_get","parameters":[{"name":"dump_id","in":"path","required":true,"schema":{"type":"integer","title":"Dump Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses/{analysis_id}/similar":{"get":{"tags":["admin-ops"],"summary":"Analysis Similar","description":"P2.8: вернуть до 5 разборов того же товара для боковой секции.","operationId":"analysis_similar_api_admin_ops_analyses__analysis_id__similar_get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/support/templates":{"get":{"tags":["admin-ops"],"summary":"List Support Templates","description":"Phase 2C: список canned-replies для /admin/support.","operationId":"list_support_templates_api_admin_ops_support_templates_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["admin-ops"],"summary":"Create Support Template","description":"Phase 2C: добавить canned-reply.","operationId":"create_support_template_api_admin_ops_support_templates_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/support/templates/{template_id}":{"put":{"tags":["admin-ops"],"summary":"Update Support Template","description":"Phase 2C: изменить canned-reply.","operationId":"update_support_template_api_admin_ops_support_templates__template_id__put","parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"integer","title":"Template Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["admin-ops"],"summary":"Delete Support Template","description":"Phase 2C: удалить canned-reply.","operationId":"delete_support_template_api_admin_ops_support_templates__template_id__delete","parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"integer","title":"Template Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/analyses/search":{"get":{"tags":["admin-ops"],"summary":"Analyses Autocomplete Search","description":"Phase 2C: autocomplete-поиск анализов по тексту/URL для добавления\nв подборку редакции.\n\nИспользуется на /admin/featured — кнопка «+ Добавить» открывает поле,\nюзер пишет, мы возвращаем 10 совпадений.","operationId":"analyses_autocomplete_search_api_admin_ops_analyses_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":200,"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":30,"minimum":1,"default":10,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/sprint-metrics":{"get":{"tags":["admin-ops"],"summary":"Admin Sprint Metrics","description":"Sprint-1..3 KPI: trials, recurring-discount, cross-mp, anon-claim, image-health.\n\nВозвращает JSON с пятью секциями + alerts. Каждая секция изолирована\nв try/except: если sub-блок (cron-status, Redis-counter) упал —\nотдаём `{...status: \"unknown\"}` вместо 500 на весь endpoint.\n\nКэш 5 минут: дашборд авто-обновляется раз в 60с, но реально пересчитывать\nтяжёлый distinct(user_id) WHERE EXISTS(...) каждую минуту смысла нет.","operationId":"admin_sprint_metrics_api_admin_ops_sprint_metrics_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/antirating/post-now":{"post":{"tags":["admin-ops"],"summary":"Antirating Post Now","description":"Ручной триггер постинга «Антирейтинг недели» в TG-канал.\n\nВызывает `build_weekly_antirating(session)` + `post_antirating_to_channel(items)`\nсинхронно. Возвращает подробный результат для дебага в админке.\n\nЕсли канал не сконфигурирован (settings.telegram_antirating_channel_id\nпуст) — items всё равно соберутся, но posted=False; админ увидит что\nвыборка работает, и сможет дописать ENV до запуска cron'а в понедельник.","operationId":"antirating_post_now_api_admin_ops_antirating_post_now_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/promo":{"get":{"tags":["admin-ops"],"summary":"Admin Promo List","description":"Список всех партнёрских промо-кодов + статистика (применений, покупок,\nrevenue, payout owed). Один запрос с JOIN+GROUP BY, не N+1.","operationId":"admin_promo_list_api_admin_ops_promo_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Admin Promo List Api Admin Ops Promo Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["admin-ops"],"summary":"Admin Promo Create","description":"Создаёт новый промо-код.","operationId":"admin_promo_create_api_admin_ops_promo_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PartnerCodeCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Admin Promo Create Api Admin Ops Promo Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/promo/totals":{"get":{"tags":["admin-ops"],"summary":"Admin Promo Totals","description":"Сводная статистика по всем партнёрским кодам для админ-баннера/виджета.\n\nSingle SQL с GROUP BY + второй запрос для top-3. Кешировать не нужно —\n/admin/promo и dashboard опрашивают это редко (только при загрузке/reload),\nа данных мало (десятки кодов максимум).","operationId":"admin_promo_totals_api_admin_ops_promo_totals_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Admin Promo Totals Api Admin Ops Promo Totals Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/promo/{code_id}":{"patch":{"tags":["admin-ops"],"summary":"Admin Promo Patch","description":"Обновление существующего кода. Сам код менять нельзя — это сломает\nуже-распространённые реф-ссылки. Чтобы «переименовать» — деактивируйте\nи создайте новый.","operationId":"admin_promo_patch_api_admin_ops_promo__code_id__patch","parameters":[{"name":"code_id","in":"path","required":true,"schema":{"type":"integer","title":"Code Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PartnerCodePatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Admin Promo Patch Api Admin Ops Promo  Code Id  Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/promo/{code_id}/redemptions":{"get":{"tags":["admin-ops"],"summary":"Admin Promo Redemptions","description":"Детальный лог применений конкретного кода для drawer'а в админке.\n\nСодержит: redeemed_at, user_email, payment_id (если купил), revenue,\npayout owed.","operationId":"admin_promo_redemptions_api_admin_ops_promo__code_id__redemptions_get","parameters":[{"name":"code_id","in":"path","required":true,"schema":{"type":"integer","title":"Code Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":200,"title":"Limit"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Admin Promo Redemptions Api Admin Ops Promo  Code Id  Redemptions Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/promo/{code_id}/payouts/mark-paid":{"post":{"tags":["admin-ops"],"summary":"Admin Promo Mark Paid","description":"Помечает все unpaid payout'ы по коду как выплаченные (payout_paid_at=NOW).","operationId":"admin_promo_mark_paid_api_admin_ops_promo__code_id__payouts_mark_paid_post","parameters":[{"name":"code_id","in":"path","required":true,"schema":{"type":"integer","title":"Code Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Admin Promo Mark Paid Api Admin Ops Promo  Code Id  Payouts Mark Paid Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/monitoring/errors":{"get":{"tags":["admin-monitoring"],"summary":"Monitoring Errors","description":"Сводка ошибок за 24ч для /admin/dashboard.\n\nResponse:\n    {\n      \"ok\": true,\n      \"count_total\": 42,\n      \"count_last_hour\": 3,\n      \"last_seen\": \"2026-04-24T14:00:00+00:00\",\n      \"error\": null,           # или \"glitchtip_not_configured\" и т.п.\n      \"ui_url\": \"https://errors.probiwaem.ru/…/issues/\"\n    }","operationId":"monitoring_errors_api_admin_monitoring_errors_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Monitoring Errors Api Admin Monitoring Errors Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/monitoring/alert-test":{"post":{"tags":["admin-monitoring"],"summary":"Monitoring Alert Test","description":"Ручной триггер error_alerter — удобно проверить, что TG-канал жив.\n\nВсегда шлёт ОДНО тестовое сообщение в telegram_admin_chat_id, минуя\nburst-порог и cooldown. Возвращает {sent: bool, reason: str}.","operationId":"monitoring_alert_test_api_admin_monitoring_alert_test_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Monitoring Alert Test Api Admin Monitoring Alert Test Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/debug/raise-error":{"get":{"tags":["admin-monitoring"],"summary":"Debug Raise Error","description":"Кидает тестовый ValueError — проверяет, что Sentry ingest ловит ошибки.\n\nРаботает только при `settings.debug_raise_enabled=True`. Иначе 404 —\nтихо, чтобы внешний сканер не увидел в списке endpoint'ов ручку,\nкоторая что-то делает. Обязательно отключить после подтверждения работы.","operationId":"debug_raise_error_api_admin_debug_raise_error_get","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Debug Raise Error Api Admin Debug Raise Error Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/banners":{"get":{"tags":["admin-banners"],"summary":"List Banners","description":"Список баннеров с фильтрами + пагинацией.","operationId":"list_banners_api_admin_ops_banners_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"placement","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Placement"}},{"name":"audience","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audience"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":0,"default":0,"title":"Offset"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["admin-banners"],"summary":"Create Banner","description":"Создать новый баннер. slug должен быть уникален.","operationId":"create_banner_api_admin_ops_banners_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BannerCreateBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/banners/{banner_id}":{"get":{"tags":["admin-banners"],"summary":"Get Banner","operationId":"get_banner_api_admin_ops_banners__banner_id__get","parameters":[{"name":"banner_id","in":"path","required":true,"schema":{"type":"integer","title":"Banner Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["admin-banners"],"summary":"Update Banner","description":"Частичное обновление. None в поле = не трогать.","operationId":"update_banner_api_admin_ops_banners__banner_id__put","parameters":[{"name":"banner_id","in":"path","required":true,"schema":{"type":"integer","title":"Banner Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BannerBaseBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["admin-banners"],"summary":"Delete Banner","operationId":"delete_banner_api_admin_ops_banners__banner_id__delete","parameters":[{"name":"banner_id","in":"path","required":true,"schema":{"type":"integer","title":"Banner Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/banners/{banner_id}/toggle":{"post":{"tags":["admin-banners"],"summary":"Toggle Banner","description":"Inverts is_active. Удобный 1-click для admin-UI.","operationId":"toggle_banner_api_admin_ops_banners__banner_id__toggle_post","parameters":[{"name":"banner_id","in":"path","required":true,"schema":{"type":"integer","title":"Banner Id"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/banners/preview":{"post":{"tags":["admin-banners"],"summary":"Preview Banner","description":"Renderим _banner.html partial по переданному body. Никаких записей.","operationId":"preview_banner_api_admin_ops_banners_preview_post","parameters":[{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BannerPreviewBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/ops/banners/{banner_id}/timeline":{"get":{"tags":["admin-banners"],"summary":"Banner Timeline","description":"Аналитика по баннеру.\n\nTODO v0.16: per-hour timeline требует отдельной таблицы `banner_events`\n(impression/click/dismiss с timestamp). Пока что отдаём только агрегаты\n+ пустой timeline-массив, чтобы admin UI не падал.","operationId":"banner_timeline_api_admin_ops_banners__banner_id__timeline_get","parameters":[{"name":"banner_id","in":"path","required":true,"schema":{"type":"integer","title":"Banner Id"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":2160,"minimum":1,"default":168,"title":"Hours"}},{"name":"pb_admin_token","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pb Admin Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Index","operationId":"index__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Index","operationId":"index__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/privacy":{"get":{"summary":"Privacy","operationId":"privacy_privacy_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/recent":{"get":{"summary":"Public Recent Page","description":"Публичная страница со всей лентой последних проверок.\n\nv0.16.2: SSR-prelude с initial_items (первые 12) — соцсети-боты не\nвыполняют JS, без SSR они видели пустой <div> и думали «ничего нет».\nКлиент потом всё равно дофетчит /api/public/recent-analyses для\nбесконечной прокрутки/обновления, но первый paint и crawler-индексация\nидут уже с готовым content.","operationId":"public_recent_page_recent_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Public Recent Page","description":"Публичная страница со всей лентой последних проверок.\n\nv0.16.2: SSR-prelude с initial_items (первые 12) — соцсети-боты не\nвыполняют JS, без SSR они видели пустой <div> и думали «ничего нет».\nКлиент потом всё равно дофетчит /api/public/recent-analyses для\nбесконечной прокрутки/обновления, но первый paint и crawler-индексация\nидут уже с готовым content.","operationId":"public_recent_page_recent_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/shorts":{"get":{"summary":"Shorts Feed Page","description":"SSR shell для /shorts feed.\n\nPre-renders первые 1-2 articles + JSON bootstrap для всего batch'а (4-7).\nFrontend (feed.js) hydrate'ит Embla carousel virtualization из bootstrap.","operationId":"shorts_feed_page_shorts_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Shorts Feed Page","description":"SSR shell для /shorts feed.\n\nPre-renders первые 1-2 articles + JSON bootstrap для всего batch'а (4-7).\nFrontend (feed.js) hydrate'ит Embla carousel virtualization из bootstrap.","operationId":"shorts_feed_page_shorts_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/shorts/discover":{"get":{"summary":"Shorts Discover Page","description":"SSR shell для /shorts/discover — TikTok-style grid всех расследований.\n\nServer-renders первые 20 cards (для SEO + LCP), bootstrap JSON для\nfrontend hydration (filter/sort/search/load-more).","operationId":"shorts_discover_page_shorts_discover_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Shorts Discover Page","description":"SSR shell для /shorts/discover — TikTok-style grid всех расследований.\n\nServer-renders первые 20 cards (для SEO + LCP), bootstrap JSON для\nfrontend hydration (filter/sort/search/load-more).","operationId":"shorts_discover_page_shorts_discover_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/shorts/{short_id}":{"get":{"summary":"Shorts Single Page","description":"SSR deep-link /shorts/{short_id} — открывает feed с этим short в фокусе.\n\nOG-meta указывают на этот конкретный short (для VK/TG/TikTok preview).\nBootstrap содержит этот short как первый item + следующие из feed_get_one.","operationId":"shorts_single_page_shorts__short_id__get","parameters":[{"name":"short_id","in":"path","required":true,"schema":{"type":"string","title":"Short Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Shorts Single Page","description":"SSR deep-link /shorts/{short_id} — открывает feed с этим short в фокусе.\n\nOG-meta указывают на этот конкретный short (для VK/TG/TikTok preview).\nBootstrap содержит этот short как первый item + следующие из feed_get_one.","operationId":"shorts_single_page_shorts__short_id__get","parameters":[{"name":"short_id","in":"path","required":true,"schema":{"type":"string","title":"Short Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/compare":{"get":{"summary":"Public Compare Page","description":"Side-by-side сравнение до 3-х проверок.\n\nv0.14.62: ?ids=12,15,21 (csv int analysis_id'ов). Рендерит таблицу\n«бренд | оценка | вердикт | подозрит. | флаги» — юзер выбирает лучший\nпо столбцу.\n\nИспользование:\n  • Юзер копирует ссылки 2-3 товаров с маркетплейса, вставляет в форму\n    compare-режима в /account → редирект сюда с `?ids=...`.\n  • SEO: Google индексирует, потенциально trafic-magnet\n    («сравнить iphone 15 wb vs ozon»).\n\nЕсли ids невалидные → пустой шаблон с подсказкой.","operationId":"public_compare_page_compare_get","parameters":[{"name":"ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ids"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Public Compare Page","description":"Side-by-side сравнение до 3-х проверок.\n\nv0.14.62: ?ids=12,15,21 (csv int analysis_id'ов). Рендерит таблицу\n«бренд | оценка | вердикт | подозрит. | флаги» — юзер выбирает лучший\nпо столбцу.\n\nИспользование:\n  • Юзер копирует ссылки 2-3 товаров с маркетплейса, вставляет в форму\n    compare-режима в /account → редирект сюда с `?ids=...`.\n  • SEO: Google индексирует, потенциально trafic-magnet\n    («сравнить iphone 15 wb vs ozon»).\n\nЕсли ids невалидные → пустой шаблон с подсказкой.","operationId":"public_compare_page_compare_get","parameters":[{"name":"ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ids"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/share-target":{"get":{"summary":"Web Share Target","description":"Endpoint для Web Share Target API. См. site.webmanifest::share_target.","operationId":"web_share_target_share_target_get","parameters":[{"name":"url","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},{"name":"text","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"}},{"name":"title","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/top":{"get":{"summary":"Public Top Page","description":"Публичная страница «Самое грязное на маркетплейсах».\n\nv0.14.61: SEO-bait + social-proof. Сразу SSR-render с реальными данными\nиз БД — Google индексирует контент, юзеры из поиска приходят на готовую\nстатистику. Кэш 30 мин (SQL-агрегации дорогие).\nСекции:\n  • Маркетплейс-сравнение — кто чаще всего «не брать» (count + %).\n  • Худшие товары за 7 дней — avoid с максимальной suspicion_pct.\n  • Топ-10 продавцов с самой плохой репутацией (avg suspicion ≥ 60).","operationId":"public_top_page_top_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Public Top Page","description":"Публичная страница «Самое грязное на маркетплейсах».\n\nv0.14.61: SEO-bait + social-proof. Сразу SSR-render с реальными данными\nиз БД — Google индексирует контент, юзеры из поиска приходят на готовую\nстатистику. Кэш 30 мин (SQL-агрегации дорогие).\nСекции:\n  • Маркетплейс-сравнение — кто чаще всего «не брать» (count + %).\n  • Худшие товары за 7 дней — avoid с максимальной suspicion_pct.\n  • Топ-10 продавцов с самой плохой репутацией (avg suspicion ≥ 60).","operationId":"public_top_page_top_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/methodology":{"get":{"summary":"Methodology","description":"Публичная страница — как работает детектор накрутки отзывов.\n\nНужна, чтобы юзеры не думали «89% это точно фейки», а понимали\nчто это эвристика с конкретными флагами и весами.","operationId":"methodology_methodology_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Methodology","description":"Публичная страница — как работает детектор накрутки отзывов.\n\nНужна, чтобы юзеры не думали «89% это точно фейки», а понимали\nчто это эвристика с конкретными флагами и весами.","operationId":"methodology_methodology_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/find":{"get":{"summary":"Public Find Page","description":"«Помоги подобрать» — публичный подбор товаров по свободному описанию.\n\nServer-rendered shell (форма + платформ-селектор + контейнер результатов).\nСам подбор делает find.js → POST /api/public/find (WB MVP, server-side,\nБЕЗ расширения). Поиск БЕСПЛАТНЫЙ; непроверенные кандидаты фронт предлагает\n«пробить» через существующий /api/analyze.\n\nСтраница рендерится всегда. Фича-флаг settings.pomogi_podobrat_enabled\nпрокидываем в контекст: при OFF API отдаёт 503, а find.js покажет мягкую\nзаметку. Никакого упоминания AI/ИИ/нейросетей — подбор детерминированный.","operationId":"public_find_page_find_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Public Find Page","description":"«Помоги подобрать» — публичный подбор товаров по свободному описанию.\n\nServer-rendered shell (форма + платформ-селектор + контейнер результатов).\nСам подбор делает find.js → POST /api/public/find (WB MVP, server-side,\nБЕЗ расширения). Поиск БЕСПЛАТНЫЙ; непроверенные кандидаты фронт предлагает\n«пробить» через существующий /api/analyze.\n\nСтраница рендерится всегда. Фича-флаг settings.pomogi_podobrat_enabled\nпрокидываем в контекст: при OFF API отдаёт 503, а find.js покажет мягкую\nзаметку. Никакого упоминания AI/ИИ/нейросетей — подбор детерминированный.","operationId":"public_find_page_find_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/build":{"get":{"summary":"Public Build Page","description":"«Сборки» — готовые комплекты из проверенных товаров.\n\nSSR-shell: hero + карточки шаблонов + контейнер результата. Подбор делает\nbuild.js → POST /api/public/build (кураторские шаблоны + конвейер /find,\nБЕЗ LLM). Тот же kill-switch, что у /find.","operationId":"public_build_page_build_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Public Build Page","description":"«Сборки» — готовые комплекты из проверенных товаров.\n\nSSR-shell: hero + карточки шаблонов + контейнер результата. Подбор делает\nbuild.js → POST /api/public/build (кураторские шаблоны + конвейер /find,\nБЕЗ LLM). Тот же kill-switch, что у /find.","operationId":"public_build_page_build_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/check/{analysis_id}":{"get":{"summary":"Public Check Page","description":"Legacy share-URL path. v0.16.2: 301-редиректим на /p/{short_id} —\n/p/ имеет правильную SSR-страницу с полным OG-meta и dynamic PNG-карточкой,\nа /check/{id} использовал устаревший renderer с overlap'ом текста + генерик\nOG для всех товаров. Все существующие TG-шары не сломаются (TG/WhatsApp/\nSlack honor 301 при первом fetch и обновляют preview-cache).\n\nЕсли short_id не резолвится (старая запись удалена) — возвращаем 410 Gone.","operationId":"public_check_page_check__analysis_id__get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Public Check Page","description":"Legacy share-URL path. v0.16.2: 301-редиректим на /p/{short_id} —\n/p/ имеет правильную SSR-страницу с полным OG-meta и dynamic PNG-карточкой,\nа /check/{id} использовал устаревший renderer с overlap'ом текста + генерик\nOG для всех товаров. Все существующие TG-шары не сломаются (TG/WhatsApp/\nSlack honor 301 при первом fetch и обновляют preview-cache).\n\nЕсли short_id не резолвится (старая запись удалена) — возвращаем 410 Gone.","operationId":"public_check_page_check__analysis_id__get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/p/{short_id}":{"get":{"summary":"Public Share Page","description":"Pretty-странчиа проверки, оптимизированная под шар в мессенджеры.\n\nРендерит SSR-блок с верcdiктом, score, top-flags, complaints summary +\nOG-теги для preview в TG/WhatsApp. Если проверка скрыта (is_hidden) →\n410 Gone (сообщаем что страница есть, но её админ убрал).\n\nPrivacy: НЕ показываем client_ip, user_agent, anon_uuid, edited_by_admin_id.\nТолько публичные поля карточки.","operationId":"public_share_page_p__short_id__get","parameters":[{"name":"short_id","in":"path","required":true,"schema":{"type":"string","title":"Short Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Public Share Page","description":"Pretty-странчиа проверки, оптимизированная под шар в мессенджеры.\n\nРендерит SSR-блок с верcdiктом, score, top-flags, complaints summary +\nOG-теги для preview в TG/WhatsApp. Если проверка скрыта (is_hidden) →\n410 Gone (сообщаем что страница есть, но её админ убрал).\n\nPrivacy: НЕ показываем client_ip, user_agent, anon_uuid, edited_by_admin_id.\nТолько публичные поля карточки.","operationId":"public_share_page_p__short_id__get","parameters":[{"name":"short_id","in":"path","required":true,"schema":{"type":"string","title":"Short Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/seller/{marketplace}/{seller_id}":{"get":{"summary":"Seller Dossier Page","description":"Pro-страница «Досье продавца»: все товары этого продавца на МП с\nвердиктами и накруткой. Содержимое грузится клиентом через\n/api/my/seller/{mp}/{seller_id} — если Free, увидит paywall inline.","operationId":"seller_dossier_page_seller__marketplace___seller_id__get","parameters":[{"name":"marketplace","in":"path","required":true,"schema":{"type":"string","title":"Marketplace"}},{"name":"seller_id","in":"path","required":true,"schema":{"type":"string","title":"Seller Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/terms":{"get":{"summary":"Terms","operationId":"terms_terms_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/whats-new":{"get":{"summary":"Whats New","description":"Публичный changelog. Ведём честно — что добавили, что починили, куда\nдвижемся. Обновляется при каждом релизе. Шаблон `whats_new.html` содержит\nзахардкоженные секции (версия → bullets), это специально: дешевле чем\nхранить в БД + идёт в SEO.","operationId":"whats_new_whats_new_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Whats New","description":"Публичный changelog. Ведём честно — что добавили, что починили, куда\nдвижемся. Обновляется при каждом релизе. Шаблон `whats_new.html` содержит\nзахардкоженные секции (версия → bullets), это специально: дешевле чем\nхранить в БД + идёт в SEO.","operationId":"whats_new_whats_new_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/pricing":{"get":{"summary":"Pricing","description":"Публичная страница тарифов (Free / Pro / Эксперт).\n\nОбязательна по требованиям Т-Банка к сайту с эквайрингом — должны быть\nуказаны цены, порядок оплаты, условия оказания услуги. Цены должны 1-в-1\nсовпадать с /offer и с PRICE_KOPECKS в app/services/billing.py.","operationId":"pricing_pricing_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Pricing","description":"Публичная страница тарифов (Free / Pro / Эксперт).\n\nОбязательна по требованиям Т-Банка к сайту с эквайрингом — должны быть\nуказаны цены, порядок оплаты, условия оказания услуги. Цены должны 1-в-1\nсовпадать с /offer и с PRICE_KOPECKS в app/services/billing.py.","operationId":"pricing_pricing_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/monitoring":{"get":{"summary":"Monitoring Page","operationId":"monitoring_page_monitoring_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Monitoring Page","operationId":"monitoring_page_monitoring_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/review-workbench":{"get":{"summary":"Review Workbench Page","operationId":"review_workbench_page_review_workbench_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Review Workbench Page","operationId":"review_workbench_page_review_workbench_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api-guide":{"get":{"summary":"Api Guide Page","operationId":"api_guide_page_api_guide_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Api Guide Page","operationId":"api_guide_page_api_guide_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/for-sellers":{"get":{"summary":"For Sellers Page","description":"B2B-лендинг для продавцов маркетплейсов.\n\nТариф «Аналитик» 4 990 ₽/мес. Self-serve checkout'а нет — лид-форма\nPOST /api/public/leads/seller, дальше менеджер созванивается и\nактивирует доступ вручную.","operationId":"for_sellers_page_for_sellers_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"For Sellers Page","description":"B2B-лендинг для продавцов маркетплейсов.\n\nТариф «Аналитик» 4 990 ₽/мес. Self-serve checkout'а нет — лид-форма\nPOST /api/public/leads/seller, дальше менеджер созванивается и\nактивирует доступ вручную.","operationId":"for_sellers_page_for_sellers_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/wb-check":{"get":{"summary":"Wb Check Page","operationId":"wb_check_page_wb_check_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Wb Check Page","operationId":"wb_check_page_wb_check_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/ozon-check":{"get":{"summary":"Ozon Check Page","operationId":"ozon_check_page_ozon_check_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Ozon Check Page","operationId":"ozon_check_page_ozon_check_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/yandex-market-check":{"get":{"summary":"Yandex Market Check Page","operationId":"yandex_market_check_page_yandex_market_check_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Yandex Market Check Page","operationId":"yandex_market_check_page_yandex_market_check_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/megamarket-check":{"get":{"summary":"Megamarket Check Page","operationId":"megamarket_check_page_megamarket_check_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Megamarket Check Page","operationId":"megamarket_check_page_megamarket_check_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/category/{cat_slug}":{"get":{"summary":"Category Check Page","description":"Один dispatch-route на все 7 cluster pages. Slug не из _CATEGORY_DATA → 404.\n\n/category/ namespace выбран чтобы не конфликтовать с legacy\n/check/{analysis_id:int} (FastAPI пытается распарсить slug как int\nи валится 422 не fall-through-я на наш route).","operationId":"category_check_page_category__cat_slug__get","parameters":[{"name":"cat_slug","in":"path","required":true,"schema":{"type":"string","title":"Cat Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"summary":"Category Check Page","description":"Один dispatch-route на все 7 cluster pages. Slug не из _CATEGORY_DATA → 404.\n\n/category/ namespace выбран чтобы не конфликтовать с legacy\n/check/{analysis_id:int} (FastAPI пытается распарсить slug как int\nи валится 422 не fall-through-я на наш route).","operationId":"category_check_page_category__cat_slug__get","parameters":[{"name":"cat_slug","in":"path","required":true,"schema":{"type":"string","title":"Cat Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/subscribe":{"get":{"summary":"Subscribe Page","description":"Страница оформления подписки. Требует логин.\n\nДо миграции 018 принимали любой email в body — юзер мог оплатить на\nчужой email. Теперь /subscribe server-side редиректит на /login?next=...\nесли нет pb_session. subscribe.html фетчит POST /api/billing/subscribe\nбез email (он берётся из cookie).","operationId":"subscribe_page_subscribe_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/checks-pack":{"get":{"summary":"Checks Pack Page","description":"Страница покупки пакета проверок (v0.8.0).\n\nПубличный шаблон — список доступных пакетов тянется JS'ом из\nGET /api/billing/check-packs. Кнопка «Купить» делает POST на\n/api/billing/check-packs/buy и редиректит на T-Bank PaymentURL.\n\nЛогин не требуется на этом этапе — POST-эндпоинт сам редиректит\nна /login?next=/checks-pack если 401.","operationId":"checks_pack_page_checks_pack_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/cart":{"get":{"summary":"Cart Page","description":"Страница «Проверка корзины» — Pro-фича (v0.9.0, Agent K).\n\nJS делает POST /api/cart/check со списком URL. Бэкенд:\n- Pro → cache-lookup для всех URL (max 30); cache-miss → needs_extension\n- Free → берёт только первые 3 как preview, рисует paywall\n- Анон → 401, JS редиректит на /login?next=/cart\n\nШаблон сам не делает auth-check на сервере — POST/GET cookie проверится\nв `/api/cart/check`. Так юзер увидит форму и поймёт ценность фичи\nещё до логина.","operationId":"cart_page_cart_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Cart Page","description":"Страница «Проверка корзины» — Pro-фича (v0.9.0, Agent K).\n\nJS делает POST /api/cart/check со списком URL. Бэкенд:\n- Pro → cache-lookup для всех URL (max 30); cache-miss → needs_extension\n- Free → берёт только первые 3 как preview, рисует paywall\n- Анон → 401, JS редиректит на /login?next=/cart\n\nШаблон сам не делает auth-check на сервере — POST/GET cookie проверится\nв `/api/cart/check`. Так юзер увидит форму и поймёт ценность фичи\nещё до логина.","operationId":"cart_page_cart_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/billing/success":{"get":{"summary":"Billing Success Page","description":"Лендинг после успешной оплаты. Т-Банк редиректит сюда с ?OrderId=...\n\nv0.14.27: разруливаем pack vs subscription по префиксу OrderId.\n  • OrderId=\"pack_<id>_<ts>\"  → CheckPackPurchase, рендерим pack-страницу\n  • OrderId=\"pb_<sub_id>_<ts>\" → Subscription, рендерим sub-страницу\n  • Без OrderId / fallback → если есть активная подписка — показываем её.\n\nДо этого fix'а в любом случае рендерили подписку, и юзер после покупки\nпакета видел «Подписка Продвинутый активна» — wat?","operationId":"billing_success_page_billing_success_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/billing/fail":{"get":{"summary":"Billing Fail Page","description":"Лендинг после неудачной оплаты (недостаточно средств, 3DS fail и т.п.).","operationId":"billing_fail_page_billing_fail_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/login":{"get":{"summary":"Login Page","description":"Страница входа — кнопки «Войти через Яндекс / VK».\n\nv0.28.8 (fix пустого обещания): CTA «Получить 3 дня бесплатно» на /p ведёт\nсюда с ?from=p_share_promo, но login.html прокидывает в OAuth только `next`\n— `from` терялся, и auth-callback НИКОГДА не выдавал обещанный триал.\nТеперь помечаем источник серверной ephemeral-cookie: _finalize_login\n(auth_routes.py) увидит её и вызовет grant_trial_days для нового юзера.\nCookie переживает OAuth-roundtrip независимо от JS на странице.","operationId":"login_page_login_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/goodbye":{"get":{"summary":"Goodbye Page","description":"Опрос причины удаления расширения (v0.29.1).\n\nchrome.runtime.setUninstallURL ведёт сюда с ?from=ext&v=<версия>.\nОтвет уходит в POST /api/public/ext-uninstall-feedback → structured-лог.","operationId":"goodbye_page_goodbye_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/support":{"get":{"summary":"Support Page","description":"Общая страница техподдержки юзера. JS читает /auth/me и либо\nредиректит на /login, либо рендерит список обращений + кнопку\n«Новое обращение».","operationId":"support_page_support_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/support/{tid}":{"get":{"summary":"Support Thread Page","description":"Конкретная ветка переписки. Тот же шаблон, что и /support —\nSPA-подобное поведение, JS сам решает режим (список vs чат) по URL.","operationId":"support_thread_page_support__tid__get","parameters":[{"name":"tid","in":"path","required":true,"schema":{"type":"integer","title":"Tid"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account":{"get":{"summary":"Account Page","description":"Личный кабинет. JS проверяет /auth/me и редиректит на /login если не вошёл.","operationId":"account_page_account_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/offer":{"get":{"summary":"Offer","description":"Публичная оферта на платные тарифы. Нужна для корректного приёма\nB2C-платежей через Т-Банк Acquiring — кнопка «Оплатить» = акцепт.","operationId":"offer_offer_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin":{"get":{"summary":"Admin Root","operationId":"admin_root_admin_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/login":{"get":{"summary":"Admin Login Page","operationId":"admin_login_page_admin_login_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/setup":{"get":{"summary":"Admin Setup Page","operationId":"admin_setup_page_admin_setup_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/analyses":{"get":{"summary":"Admin Analyses Page","operationId":"admin_analyses_page_admin_analyses_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/featured":{"get":{"summary":"Admin Featured Page","operationId":"admin_featured_page_admin_featured_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/feed":{"get":{"summary":"Admin Feed Page","operationId":"admin_feed_page_admin_feed_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/leads":{"get":{"summary":"Admin Leads Page","operationId":"admin_leads_page_admin_leads_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/stats":{"get":{"summary":"Admin Stats Page","operationId":"admin_stats_page_admin_stats_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/users":{"get":{"summary":"Admin Users Page","operationId":"admin_users_page_admin_users_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/users/{user_id}":{"get":{"summary":"Admin User Detail Page","operationId":"admin_user_detail_page_admin_users__user_id__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/auth-log":{"get":{"summary":"Admin Auth Log Page","operationId":"admin_auth_log_page_admin_auth_log_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/change-password":{"get":{"summary":"Admin Change Password Page","operationId":"admin_change_password_page_admin_change_password_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/analysis/{analysis_id}":{"get":{"summary":"Admin Analysis Detail Page","operationId":"admin_analysis_detail_page_admin_analysis__analysis_id__get","parameters":[{"name":"analysis_id","in":"path","required":true,"schema":{"type":"integer","title":"Analysis Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/clients":{"get":{"summary":"Admin Clients Page","operationId":"admin_clients_page_admin_clients_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/clients/{cidr}":{"get":{"summary":"Admin Client Detail Page","operationId":"admin_client_detail_page_admin_clients__cidr__get","parameters":[{"name":"cidr","in":"path","required":true,"schema":{"type":"string","title":"Cidr"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/bans":{"get":{"summary":"Admin Bans Page","operationId":"admin_bans_page_admin_bans_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/visitors":{"get":{"summary":"Admin Visitors Page","operationId":"admin_visitors_page_admin_visitors_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/security":{"get":{"summary":"Admin Security Page","operationId":"admin_security_page_admin_security_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/quotas":{"get":{"summary":"Admin Quotas Page","operationId":"admin_quotas_page_admin_quotas_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/logs":{"get":{"summary":"Admin Logs Page","operationId":"admin_logs_page_admin_logs_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/audit":{"get":{"summary":"Admin Audit Page","operationId":"admin_audit_page_admin_audit_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/cron-health":{"get":{"summary":"Admin Cron Health Page","operationId":"admin_cron_health_page_admin_cron_health_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/pro-quota":{"get":{"summary":"Admin Pro Quota Page","operationId":"admin_pro_quota_page_admin_pro_quota_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/billing":{"get":{"summary":"Admin Billing Page","operationId":"admin_billing_page_admin_billing_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/banners":{"get":{"summary":"Admin Banners Page","operationId":"admin_banners_page_admin_banners_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/search":{"get":{"summary":"Admin Search Page","operationId":"admin_search_page_admin_search_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/promo":{"get":{"summary":"Admin Promo Page","description":"v0.25.81: партнёрские промо-коды (для стримеров/блогеров).\nСоздание, редактирование, статистика, payout-tracking.\nСм. app/services/partner_codes.py + миграция 058.","operationId":"admin_promo_page_admin_promo_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/bulk-grant":{"get":{"summary":"Admin Bulk Grant Page","operationId":"admin_bulk_grant_page_admin_bulk_grant_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/settings":{"get":{"summary":"Admin Settings Page","operationId":"admin_settings_page_admin_settings_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/marketplace-debug":{"get":{"summary":"Admin Marketplace Debug Page","operationId":"admin_marketplace_debug_page_admin_marketplace_debug_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/devices":{"get":{"summary":"Admin Devices Page","operationId":"admin_devices_page_admin_devices_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/ext-errors":{"get":{"summary":"Admin Ext Errors Page","operationId":"admin_ext_errors_page_admin_ext_errors_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/ext-reports":{"get":{"summary":"Admin Ext Reports Page","operationId":"admin_ext_reports_page_admin_ext_reports_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/ext-reports/{report_id}":{"get":{"summary":"Admin Ext Reports Detail Page","operationId":"admin_ext_reports_detail_page_admin_ext_reports__report_id__get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"integer","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/tg-stats":{"get":{"summary":"Admin Tg Stats Page","operationId":"admin_tg_stats_page_admin_tg_stats_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/notify":{"get":{"summary":"Admin Notify Page","operationId":"admin_notify_page_admin_notify_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/support":{"get":{"summary":"Admin Support Page","description":"Inbox техподдержки в стиле Telegram Desktop (v0.6.0).\n\nДвухпанельный SPA: слева список тредов, справа открытый чат.\nБез выбранного треда — empty-state. См. admin/support.html.","operationId":"admin_support_page_admin_support_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/support/{thread_id}":{"get":{"summary":"Admin Support Detail Page","description":"Тот же шаблон, что и /admin/support — JS на загрузке открывает\nуказанный тред в правой панели. Переключение между тредами после\nэтого идёт через history.pushState (без F5).","operationId":"admin_support_detail_page_admin_support__thread_id__get","parameters":[{"name":"thread_id","in":"path","required":true,"schema":{"type":"integer","title":"Thread Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/marketplace-debug/{dump_id}":{"get":{"summary":"Admin Marketplace Debug Detail Page","operationId":"admin_marketplace_debug_detail_page_admin_marketplace_debug__dump_id__get","parameters":[{"name":"dump_id","in":"path","required":true,"schema":{"type":"integer","title":"Dump Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/robots.txt":{"get":{"summary":"Robots","operationId":"robots_robots_txt_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}},"head":{"summary":"Robots","operationId":"robots_robots_txt_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/sitemap.xml":{"get":{"summary":"Sitemap","operationId":"sitemap_sitemap_xml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"summary":"Sitemap","operationId":"sitemap_sitemap_xml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/yandex-feed.xml":{"get":{"summary":"Yandex Feed","description":"v0.14.52: RSS-feed УБРАН. Яндекс.Webmaster парсит этот URL как YML\n(категория «Исполнители») и валит errors на каждый RSS-тег\n(`<link>`, `<language>`, `<item>` — unknown for YML schema).\nВозвращаем 410 Gone чтоб они перестали ломиться.\n\nАктивные feeds:\n  • /sitemap.xml      — для search-индексации (стандарт sitemaps.org)\n  • /yandex-yml.xml   — для Я.Бизнес (категория «Исполнители»)","operationId":"yandex_feed_yandex_feed_xml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"summary":"Yandex Feed","description":"v0.14.52: RSS-feed УБРАН. Яндекс.Webmaster парсит этот URL как YML\n(категория «Исполнители») и валит errors на каждый RSS-тег\n(`<link>`, `<language>`, `<item>` — unknown for YML schema).\nВозвращаем 410 Gone чтоб они перестали ломиться.\n\nАктивные feeds:\n  • /sitemap.xml      — для search-индексации (стандарт sitemaps.org)\n  • /yandex-yml.xml   — для Я.Бизнес (категория «Исполнители»)","operationId":"yandex_feed_yandex_feed_xml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/yandex-yml.xml":{"get":{"summary":"Yandex Yml","description":"YML-feed для Яндекс.Бизнеса (категория «Исполнители»).\n\nv0.14.30: отдельный от /yandex-feed.xml endpoint — это РАЗНЫЕ форматы.\n  • /yandex-feed.xml — RSS 2.0 для Яндекс.Webmaster (поисковая индексация\n    свежих анализов).\n  • /yandex-yml.xml  — YML 2.0 каталог услуг для Яндекс.Бизнеса (карточка\n    исполнителя в Яндекс.Картах + контекст реклама).\n\nv0.27.25 (Я.Бизнес validation FIX): добавлены обязательные элементы\nкатегории «Исполнители» (исходя из errors-all-feed CSV из Я.Бизнес панели):\n  • <set-ids>           — группировка предложений по типу услуги (subscription/pack)\n  • <param name=\"Рейтинг\">       — числовая оценка исполнителя (1-5)\n  • <param name=\"Число отзывов\"> — int, количество отзывов о нас (берём live из БД)\n  • <param name=\"Годы опыта\">    — int, лет работы (1 = launched 2025-2026)\n  • <param name=\"Регион\">        — где работает исполнитель (Россия, мы онлайн)\n  • <param name=\"Конверсия\">     — % конверсии (1-100, расчётное значение)\nТакже fix <name>: должен начинаться с имени ИП/организации (требование Я.Бизнес).\nReviews count — фиксируем в app.state cache на 1 ч чтоб не дёргать БД на каждый\nYML-request (Я.Бизнес crawler фетчит ~1 раз в сутки, кеш с запасом).\n\nУслуги probiwaem:\n  • Подписка Продвинутый — 990 ₽/мес\n  • Пакет 50 проверок — 149 ₽\n  • Пакет 200 проверок — 399 ₽\n  • Пакет 1000 проверок — 1290 ₽","operationId":"yandex_yml_yandex_yml_xml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"summary":"Yandex Yml","description":"YML-feed для Яндекс.Бизнеса (категория «Исполнители»).\n\nv0.14.30: отдельный от /yandex-feed.xml endpoint — это РАЗНЫЕ форматы.\n  • /yandex-feed.xml — RSS 2.0 для Яндекс.Webmaster (поисковая индексация\n    свежих анализов).\n  • /yandex-yml.xml  — YML 2.0 каталог услуг для Яндекс.Бизнеса (карточка\n    исполнителя в Яндекс.Картах + контекст реклама).\n\nv0.27.25 (Я.Бизнес validation FIX): добавлены обязательные элементы\nкатегории «Исполнители» (исходя из errors-all-feed CSV из Я.Бизнес панели):\n  • <set-ids>           — группировка предложений по типу услуги (subscription/pack)\n  • <param name=\"Рейтинг\">       — числовая оценка исполнителя (1-5)\n  • <param name=\"Число отзывов\"> — int, количество отзывов о нас (берём live из БД)\n  • <param name=\"Годы опыта\">    — int, лет работы (1 = launched 2025-2026)\n  • <param name=\"Регион\">        — где работает исполнитель (Россия, мы онлайн)\n  • <param name=\"Конверсия\">     — % конверсии (1-100, расчётное значение)\nТакже fix <name>: должен начинаться с имени ИП/организации (требование Я.Бизнес).\nReviews count — фиксируем в app.state cache на 1 ч чтоб не дёргать БД на каждый\nYML-request (Я.Бизнес crawler фетчит ~1 раз в сутки, кеш с запасом).\n\nУслуги probiwaem:\n  • Подписка Продвинутый — 990 ₽/мес\n  • Пакет 50 проверок — 149 ₽\n  • Пакет 200 проверок — 399 ₽\n  • Пакет 1000 проверок — 1290 ₽","operationId":"yandex_yml_yandex_yml_xml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AccountResponse":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"receipt_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Receipt Email"},"tier":{"type":"string","title":"Tier","default":"free"},"is_pro":{"type":"boolean","title":"Is Pro","default":false},"status":{"type":"string","title":"Status","default":"none"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"},"auto_renew":{"type":"boolean","title":"Auto Renew","default":false},"next_charge_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Charge At"},"is_annual":{"type":"boolean","title":"Is Annual","default":false},"is_trial":{"type":"boolean","title":"Is Trial","default":false},"failed_charge_count":{"type":"integer","title":"Failed Charge Count","default":0},"has_rebill":{"type":"boolean","title":"Has Rebill","default":false},"limits":{"type":"object","title":"Limits"}},"type":"object","title":"AccountResponse","description":"Унифицированный ответ для extension — всё что нужно отрендерить в popup.\n\nv0.7.2: добавлены поля для UI карточки авто-продления в /account#section=\nbilling: next_charge_at (когда будет следующее списание), is_annual\n(месяц или год → нужно показать «9 990 ₽/год» vs «990 ₽/мес»),\nfailed_charge_count (для индикатора «карта не прошла, retry M/3»),\nhas_rebill (true если у нас есть сохранённая карта — иначе «авто-\nпродление недоступно, нужно оформить заново»)."},"AdminLoginBody":{"properties":{"username":{"type":"string","maxLength":64,"minLength":1,"title":"Username"},"password":{"type":"string","maxLength":200,"minLength":1,"title":"Password"},"totp_code":{"type":"string","maxLength":10,"minLength":6,"title":"Totp Code"}},"type":"object","required":["username","password","totp_code"],"title":"AdminLoginBody"},"AnalysisEditBody":{"properties":{"verdict":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict"},"suspicion_pct":{"anyOf":[{"type":"number","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Suspicion Pct"},"real_complaints_summary":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Real Complaints Summary"},"verdict_reasoning":{"anyOf":[{"type":"string","maxLength":800},{"type":"null"}],"title":"Verdict Reasoning"},"reason":{"type":"string","maxLength":500,"title":"Reason","default":""}},"type":"object","title":"AnalysisEditBody"},"AnalysisResult":{"properties":{"product":{"$ref":"#/components/schemas/Product"},"data_source":{"type":"string","enum":["unknown","server","browser"],"title":"Data Source","default":"unknown"},"reviews_fetched":{"type":"integer","title":"Reviews Fetched"},"suspicion_percentage":{"type":"number","maximum":100.0,"minimum":0.0,"title":"Suspicion Percentage"},"fake_like_count":{"type":"integer","title":"Fake Like Count"},"suspicious_count":{"type":"integer","title":"Suspicious Count"},"real_count":{"type":"integer","title":"Real Count"},"top_flags":{"additionalProperties":{"type":"integer"},"type":"object","title":"Top Flags"},"real_complaints_summary":{"type":"string","title":"Real Complaints Summary"},"verdict":{"$ref":"#/components/schemas/Verdict"},"verdict_reasoning":{"type":"string","title":"Verdict Reasoning"},"generated_at":{"type":"string","format":"date-time","title":"Generated At"},"user_tier":{"type":"string","title":"User Tier","default":"free"},"saved_to_history":{"type":"boolean","title":"Saved To History","default":false},"analysis_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Analysis Id"},"distribution":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Distribution"},"reviews_summary":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Reviews Summary"},"complaints_items_rich":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Complaints Items Rich"},"review_insights":{"anyOf":[{"$ref":"#/components/schemas/ReviewInsights"},{"type":"null"}]},"counterfeit_risk":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Counterfeit Risk"},"short_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Short Id"},"unique_suspicious_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Unique Suspicious Count"},"total_flag_hits":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Flag Hits"},"antivirus":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Antivirus"},"grade":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade"},"adjusted_rating":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Adjusted Rating"}},"type":"object","required":["product","reviews_fetched","suspicion_percentage","fake_like_count","suspicious_count","real_count","top_flags","real_complaints_summary","verdict","verdict_reasoning"],"title":"AnalysisResult"},"AnalysisUpdateBody":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":400},{"type":"null"}],"title":"Name"},"summary":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Summary"},"suspicion_pct":{"anyOf":[{"type":"number","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Suspicion Pct"},"verdict":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict"},"is_hidden":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Hidden"},"is_featured":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Featured"}},"type":"object","title":"AnalysisUpdateBody","description":"Любое поле опционально; переданные — применяются, остальные не трогаем."},"AnalyzeRequest":{"properties":{"url":{"type":"string","maxLength":2000,"minLength":10,"title":"Url"},"payload":{"anyOf":[{"$ref":"#/components/schemas/MarketplaceProductPayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["url"],"title":"AnalyzeRequest","description":"POST /api/analyze body.\n\nDefault: `{url: \"...\"}` — серверный парсер сам тянет карточку и отзывы.\nE2F-flow (v0.6.11+ extension): `{url, payload: MarketplaceProductPayload}` —\nрасширение уже пробило карточку с user-IP/cookies (обход WAF), сервер\nпропускает свой fetch, валидирует payload и запускает antifraud сразу\nна присланных reviews. См. routes.analyze().\n\nAnti-abuse (v0.16+): URL должен быть на один из 4 поддерживаемых маркетов\n(WB / Ozon / Я.Маркет / Megamarket). Иначе 422 ДО запуска парсера/LLM."},"BannerBaseBody":{"properties":{"slug":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Slug"},"title":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Title"},"body":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Body"},"variant":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Variant"},"placement":{"anyOf":[{"type":"string","maxLength":24},{"type":"null"}],"title":"Placement"},"audience":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Audience"},"starts_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Starts At"},"ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ends At"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"dismissible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Dismissible"},"cta_label":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Cta Label"},"cta_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Cta Url"},"icon":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Icon"},"bg_color":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Bg Color"},"fg_color":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Fg Color"},"priority":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Priority"},"meta":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Meta"}},"type":"object","title":"BannerBaseBody","description":"Общие поля для create/update/preview. Все опциональны на update."},"BannerCreateBody":{"properties":{"slug":{"type":"string","maxLength":64,"title":"Slug"},"title":{"type":"string","maxLength":200,"minLength":1,"title":"Title"},"body":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Body"},"variant":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Variant"},"placement":{"type":"string","maxLength":24,"title":"Placement"},"audience":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Audience"},"starts_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Starts At"},"ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ends At"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"dismissible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Dismissible"},"cta_label":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Cta Label"},"cta_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Cta Url"},"icon":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Icon"},"bg_color":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Bg Color"},"fg_color":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Fg Color"},"priority":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Priority"},"meta":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Meta"}},"type":"object","required":["slug","title","placement"],"title":"BannerCreateBody","description":"Для create — slug+title+placement обязательны."},"BannerPreviewBody":{"properties":{"slug":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Slug"},"title":{"type":"string","maxLength":200,"minLength":1,"title":"Title"},"body":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Body"},"variant":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Variant"},"placement":{"anyOf":[{"type":"string","maxLength":24},{"type":"null"}],"title":"Placement"},"audience":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Audience"},"starts_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Starts At"},"ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ends At"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"dismissible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Dismissible"},"cta_label":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Cta Label"},"cta_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Cta Url"},"icon":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Icon"},"bg_color":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Bg Color"},"fg_color":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Fg Color"},"priority":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Priority"},"meta":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Meta"}},"type":"object","required":["title"],"title":"BannerPreviewBody","description":"Для preview — slug может отсутствовать (используется временный)."},"BrandWatcherCreate":{"properties":{"brand_name":{"type":"string","maxLength":200,"minLength":1,"title":"Brand Name"},"marketplace":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Marketplace"}},"additionalProperties":false,"type":"object","required":["brand_name"],"title":"BrandWatcherCreate","description":"Body для POST /api/my/watchers/brand."},"BuildRequest":{"properties":{"goal":{"type":"string","maxLength":200,"minLength":1,"title":"Goal","description":"Цель сборки — свободный текст или slug шаблона"}},"type":"object","required":["goal"],"title":"BuildRequest","description":"Тело POST /api/public/build."},"BuildResponse":{"properties":{"template":{"$ref":"#/components/schemas/TemplateOut"},"slots":{"items":{"$ref":"#/components/schemas/SlotOut"},"type":"array","title":"Slots"},"totals":{"$ref":"#/components/schemas/BuildTotals"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","required":["template","slots","totals"],"title":"BuildResponse"},"BuildTotals":{"properties":{"sum_kopecks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sum Kopecks"},"checked_count":{"type":"integer","title":"Checked Count","default":0},"slots_total":{"type":"integer","title":"Slots Total","default":0},"avoid_filtered":{"type":"integer","title":"Avoid Filtered","default":0}},"type":"object","title":"BuildTotals"},"BulkBanBody":{"properties":{"ips":{"items":{"type":"string"},"type":"array","maxItems":500,"minItems":1,"title":"Ips"},"reason":{"type":"string","maxLength":256,"minLength":3,"title":"Reason"}},"type":"object","required":["ips","reason"],"title":"BulkBanBody"},"BulkBlockBody":{"properties":{"user_ids":{"items":{"type":"integer"},"type":"array","maxItems":1000,"minItems":1,"title":"User Ids"},"reason":{"type":"string","maxLength":200,"title":"Reason","default":""}},"type":"object","required":["user_ids"],"title":"BulkBlockBody"},"BulkCheckRequest":{"properties":{"urls":{"items":{"type":"string"},"type":"array","maxItems":20,"minItems":1,"title":"Urls"}},"type":"object","required":["urls"],"title":"BulkCheckRequest"},"BulkGrantBodyV2":{"properties":{"cohort":{"type":"object","title":"Cohort","description":"cohort builder snapshot"},"count":{"type":"integer","maximum":10000.0,"minimum":1.0,"title":"Count"},"reason":{"type":"string","maxLength":500,"minLength":3,"title":"Reason"},"notification":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Notification"},"schedule":{"type":"object","title":"Schedule"},"dry_run":{"type":"boolean","title":"Dry Run","default":false}},"type":"object","required":["cohort","count","reason"],"title":"BulkGrantBodyV2","description":"v0.21: новый payload bulk-grant с cohort + schedule + dry_run."},"BulkGrantCancelBody":{"properties":{"job_id":{"type":"integer","minimum":1.0,"title":"Job Id"}},"type":"object","required":["job_id"],"title":"BulkGrantCancelBody"},"BulkGrantChecksBody":{"properties":{"target":{"type":"string","title":"Target","description":"all_pro | active_users_14d | by_user_ids | free_active_7d | unknown_verdict_7d"},"user_ids":{"anyOf":[{"items":{"type":"integer"},"type":"array","maxItems":1000},{"type":"null"}],"title":"User Ids"},"count":{"type":"integer","maximum":1000.0,"minimum":1.0,"title":"Count","description":"Сколько проверок на каждого (1..1000)"},"reason":{"type":"string","maxLength":200,"minLength":3,"title":"Reason"},"dry_run":{"type":"boolean","title":"Dry Run","description":"Без dry_run = реально начисляем","default":true}},"type":"object","required":["target","count","reason"],"title":"BulkGrantChecksBody","description":"v0.14.9: массовое начисление проверок по фильтру.\n\nTargets:\n  • all_pro            — все активные Pro\n  • active_users_14d   — кто пробил ≥1 проверку за 14 дней\n  • by_user_ids        — точечно по списку user_ids\n  • free_active_7d     — Free-юзеры с ≥1 проверкой за 7 дней (re-engagement)\n  • unknown_verdict_7d — юзеры с проверками unknown-вердикта за 7д (компенсация)"},"BulkGrantProBody":{"properties":{"user_ids":{"items":{"type":"integer"},"type":"array","maxItems":1000,"minItems":1,"title":"User Ids"},"days":{"type":"integer","maximum":365.0,"minimum":1.0,"title":"Days","default":30},"tier":{"type":"string","title":"Tier","default":"pro"}},"type":"object","required":["user_ids"],"title":"BulkGrantProBody"},"BulkGrantUndoBody":{"properties":{"audit_id":{"type":"integer","minimum":1.0,"title":"Audit Id"}},"type":"object","required":["audit_id"],"title":"BulkGrantUndoBody"},"BulkHideRequest":{"properties":{"mp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mp"},"verdict":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict"},"older_than_days":{"anyOf":[{"type":"integer","maximum":3650.0,"minimum":1.0},{"type":"null"}],"title":"Older Than Days"},"only_unknown_verdict":{"type":"boolean","title":"Only Unknown Verdict","default":false},"only_zero_reviews":{"type":"boolean","title":"Only Zero Reviews","default":false},"confirm":{"type":"boolean","title":"Confirm","default":false}},"type":"object","title":"BulkHideRequest"},"BulkNotifyBody":{"properties":{"user_ids":{"items":{"type":"integer"},"type":"array","maxItems":1000,"minItems":1,"title":"User Ids"},"title":{"type":"string","maxLength":200,"minLength":1,"title":"Title"},"body":{"type":"string","maxLength":4000,"title":"Body","default":""},"link_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Link Url"},"link_label":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Link Label"}},"type":"object","required":["user_ids","title"],"title":"BulkNotifyBody"},"BuyPackRequest":{"properties":{"pack_id":{"type":"string","title":"Pack Id","description":"starter | pro | power"},"feature":{"anyOf":[{"type":"string","enum":["monitoring","sellers","compare","checks","cart"]},{"type":"null"}],"title":"Feature"},"product_url":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Product Url"},"receipt_email":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Receipt Email"}},"type":"object","required":["pack_id"],"title":"BuyPackRequest","description":"POST /check-packs/buy — выбор пакета.\n\npack_id строго один из 'starter' | 'pro' | 'power' (см. check_packs.PACKS)."},"CartAlternative":{"properties":{"short_id":{"type":"string","title":"Short Id"},"name":{"type":"string","title":"Name"},"price":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Price"},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url"},"marketplace":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketplace"},"score":{"type":"integer","title":"Score"}},"type":"object","required":["short_id","name","score"],"title":"CartAlternative","description":"Проверенный аналог для AVOID-товара. UI показывает топ-3.\n\nИспользуется для embedded recommendations (SPEC #3): когда у юзера\nв корзине плохой товар, мы прямо в карточке предлагаем альтернативу\nс Probiwaem-Score значимо выше — чтобы конверсия в Pro росла за\nсчёт actionable-рекомендаций, а не просто негативного вердикта."},"CartCheckRequest":{"properties":{"urls":{"items":{"type":"string"},"type":"array","maxItems":30,"minItems":1,"title":"Urls"}},"type":"object","required":["urls"],"title":"CartCheckRequest"},"CartCheckResponse":{"properties":{"is_pro":{"type":"boolean","title":"Is Pro"},"preview_only":{"type":"boolean","title":"Preview Only"},"summary":{"$ref":"#/components/schemas/CartSummary"},"items":{"items":{"$ref":"#/components/schemas/CartItem"},"type":"array","title":"Items"}},"type":"object","required":["is_pro","preview_only","summary","items"],"title":"CartCheckResponse"},"CartItem":{"properties":{"url":{"type":"string","title":"Url"},"marketplace":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketplace"},"status":{"type":"string","title":"Status"},"pb_score":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pb Score"},"verdict4_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict4 Label"},"product_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Product Name"},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url"},"analysis_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Analysis Id"},"overpayment_kopecks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Overpayment Kopecks","default":0},"fake_discount":{"type":"boolean","title":"Fake Discount","default":false},"alternatives":{"anyOf":[{"items":{"$ref":"#/components/schemas/CartAlternative"},"type":"array"},{"type":"null"}],"title":"Alternatives"},"price_kopecks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Price Kopecks"}},"type":"object","required":["url","status"],"title":"CartItem"},"CartPdfRequest":{"properties":{"urls":{"items":{"type":"string"},"type":"array","maxItems":30,"minItems":1,"title":"Urls"}},"type":"object","required":["urls"],"title":"CartPdfRequest"},"CartSummary":{"properties":{"total":{"type":"integer","title":"Total"},"can_buy":{"type":"integer","title":"Can Buy","default":0},"caution":{"type":"integer","title":"Caution","default":0},"avoid":{"type":"integer","title":"Avoid","default":0},"skipped":{"type":"integer","title":"Skipped","default":0},"fake_discount_count":{"type":"integer","title":"Fake Discount Count","default":0},"total_overpayment_kopecks":{"type":"integer","title":"Total Overpayment Kopecks","default":0},"total_risk_kopecks":{"type":"integer","title":"Total Risk Kopecks","default":0},"risk_count":{"type":"integer","title":"Risk Count","default":0}},"type":"object","required":["total"],"title":"CartSummary"},"ChangePasswordBody":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","title":"New Password"},"totp_code":{"type":"string","title":"Totp Code"}},"type":"object","required":["current_password","new_password","totp_code"],"title":"ChangePasswordBody"},"ClientProduct":{"properties":{"marketplace":{"$ref":"#/components/schemas/Marketplace"},"external_id":{"type":"string","maxLength":20,"pattern":"^[0-9]{1,20}$","title":"External Id"},"url":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Url"},"name":{"type":"string","maxLength":1000,"minLength":1,"title":"Name"},"brand":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Brand"},"seller":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Seller"},"seller_id":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Seller Id"},"price":{"anyOf":[{"type":"number","maximum":1000000000.0,"minimum":0.0},{"type":"null"}],"title":"Price"},"old_price":{"anyOf":[{"type":"number","maximum":1000000000.0,"minimum":0.0},{"type":"null"}],"title":"Old Price"},"rating":{"anyOf":[{"type":"number","maximum":5.0,"minimum":0.0},{"type":"null"}],"title":"Rating"},"review_count":{"anyOf":[{"type":"integer","maximum":1000000000.0,"minimum":0.0},{"type":"null"}],"title":"Review Count"},"image_url":{"anyOf":[{"type":"string","maxLength":2000,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Image Url"},"characteristics":{"anyOf":[{"items":{"additionalProperties":{"type":"string"},"type":"object"},"type":"array","maxItems":100},{"type":"null"}],"title":"Characteristics"}},"type":"object","required":["marketplace","external_id","url","name"],"title":"ClientProduct","description":"Bounds for untrusted input; internal parser models remain unchanged.\n\nExtra DOM diagnostics from older extensions are ignored for compatibility.\nThey never become verdict fields or persisted product metadata."},"ClientReview":{"properties":{"id":{"type":"string","maxLength":256,"minLength":1,"title":"Id"},"rating":{"type":"integer","maximum":5.0,"minimum":1.0,"title":"Rating"},"text":{"type":"string","maxLength":20000,"title":"Text"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"has_photo":{"type":"boolean","title":"Has Photo","default":false},"author_name":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Author Name"},"pickup_point":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Pickup Point"}},"additionalProperties":false,"type":"object","required":["id","rating","text"],"title":"ClientReview"},"CreateApiKeyRequest":{"properties":{"label":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Label"}},"type":"object","title":"CreateApiKeyRequest"},"CreateBanBody":{"properties":{"cidr":{"type":"string","maxLength":64,"minLength":1,"title":"Cidr"},"reason":{"type":"string","maxLength":256,"title":"Reason","default":""},"hours":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Hours"}},"type":"object","required":["cidr"],"title":"CreateBanBody"},"DecisionBody":{"properties":{"action":{"type":"string","enum":["restrict","ban24","dismiss","lift"],"title":"Action"},"confirm":{"type":"boolean","title":"Confirm","default":false}},"additionalProperties":false,"type":"object","required":["action"],"title":"DecisionBody"},"DeviceBanBody":{"properties":{"reason":{"type":"string","maxLength":256,"minLength":3,"title":"Reason"},"expires_in_hours":{"anyOf":[{"type":"integer","maximum":8760.0,"minimum":1.0},{"type":"null"}],"title":"Expires In Hours"}},"type":"object","required":["reason"],"title":"DeviceBanBody"},"E2FReview":{"properties":{"id":{"anyOf":[{"type":"string","maxLength":256,"minLength":1},{"type":"null"}],"title":"Id"},"author":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Author"},"rating":{"type":"integer","maximum":5.0,"minimum":1.0,"title":"Rating"},"text":{"type":"string","maxLength":20000,"title":"Text"},"date":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Date"},"has_photo":{"type":"boolean","title":"Has Photo","default":false},"pickup_point":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Pickup Point"}},"additionalProperties":false,"type":"object","required":["rating","text"],"title":"E2FReview","description":"Review shape that the extension's E2F fetchers ship in /api/analyze.payload.\n\nЧуть проще чем `Review`: оба `id` опционален (некоторые маркеты не отдают\nстабильный id), `rating` называется `rating`, дата — `date`. Сервер\nсам приведёт к `Review` через `to_review()`."},"ErrorDetail":{"properties":{"code":{"type":"string","title":"Code"},"message":{"type":"string","title":"Message"},"retryable":{"type":"boolean","title":"Retryable"},"request_id":{"type":"string","title":"Request Id"},"details":{"type":"object","title":"Details"}},"type":"object","required":["code","message","retryable","request_id"],"title":"ErrorDetail"},"ExtErrorBody":{"properties":{"device_uuid":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Device Uuid"},"ext_version":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Ext Version"},"marketplace":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Marketplace"},"scope":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Scope"},"message":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Message"},"stack":{"anyOf":[{"type":"string","maxLength":8000},{"type":"null"}],"title":"Stack"},"product_url":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Product Url"},"chrome_major":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Chrome Major"},"platform":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Platform"}},"additionalProperties":true,"type":"object","title":"ExtErrorBody","description":"Очень терпимая схема — лучше принять «слегка кривой» payload, чем\n422-ить и потерять отчёт о баге.\n\nSprint-1 hotfix (agent-A): за 12 дней потеряно 90+ отчётов из-за того,\nчто extension'у удавалось прислать `scope=\"\"` или `message=\"\"` (например,\nкогда ловится исключение без `.message` — `String(err || \"unknown\")`\nможет дать пустую строку при некоторых типах ошибок). Раньше\n`Field(min_length=1)` бил 422 и мы НЕ ВИДЕЛИ что у юзеров реально\nломается.\n\nТеперь:\n- ВСЕ поля Optional (включая scope/message);\n- device_uuid тоже Optional (расширение может не успеть прочитать\n  chrome.storage.local при ранних SW-ошибках);\n- extra='allow' — если в payload есть лишние ключи (новые версии\n  extension'а добавляют поля), не валим запрос, складываем «как есть»\n  в логи / БД (мы храним только нужные поля);\n- дефолты безопасные: пустой scope → 'unknown', пустой message →\n  'unknown error'. В обработчике обрезаем длину защитно."},"ExtEventItem":{"properties":{"event":{"type":"string","maxLength":48,"minLength":1,"title":"Event"},"marketplace":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Marketplace"},"feature":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Feature"},"value":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Value"}},"type":"object","required":["event"],"title":"ExtEventItem"},"ExtEventsBody":{"properties":{"device_uuid":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Device Uuid"},"ext_version":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Ext Version"},"chrome_major":{"anyOf":[{"type":"integer","maximum":999.0,"minimum":0.0},{"type":"null"}],"title":"Chrome Major"},"platform":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Platform"},"events":{"items":{"$ref":"#/components/schemas/ExtEventItem"},"type":"array","maxItems":20,"title":"Events"}},"type":"object","title":"ExtEventsBody"},"ExtUninstallFeedback":{"properties":{"reason":{"type":"string","maxLength":32,"minLength":1,"title":"Reason"},"comment":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Comment"},"version":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Version"}},"type":"object","required":["reason"],"title":"ExtUninstallFeedback"},"ExtUserReportBody":{"properties":{"device_uuid":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Device Uuid"},"ext_version":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Ext Version"},"user_message":{"type":"string","maxLength":4000,"title":"User Message","default":""},"diagnostics":{"type":"object","title":"Diagnostics"},"kind":{"type":"string","maxLength":16,"title":"Kind","default":"bug"},"hp":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Hp"}},"type":"object","title":"ExtUserReportBody"},"ExtendRequest":{"properties":{"months":{"type":"integer","maximum":12.0,"minimum":1.0,"title":"Months","description":"На сколько месяцев продлить (1..12)"}},"type":"object","required":["months"],"title":"ExtendRequest","description":"Продление существующей подписки на N месяцев одним платежом.\n\nPydantic Field validates 1..12 inclusive — это гарантия защиты на уровне\nсхемы, в дополнение к сервисной логике. Запрашивать 13+ или 0 нет смысла:\nбольше года вперёд хранить не хотим (валюта может скакнуть, цены поменяться),\nменьше 1 — это не extend."},"FeedBulkIdsBody":{"properties":{"ids":{"items":{"type":"integer"},"type":"array","maxItems":1000,"minItems":1,"title":"Ids"},"reason":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Reason"}},"type":"object","required":["ids"],"title":"FeedBulkIdsBody"},"FindRequest":{"properties":{"query":{"type":"string","maxLength":200,"minLength":1,"title":"Query","description":"Свободное описание товара"},"marketplaces":{"items":{"type":"string"},"type":"array","title":"Marketplaces","description":"Площадки для поиска. MVP: только 'wb'."}},"type":"object","required":["query"],"title":"FindRequest","description":"Тело запроса POST /api/public/find."},"FindResponse":{"properties":{"intent":{"$ref":"#/components/schemas/IntentOut"},"count":{"type":"integer","title":"Count","default":0},"results":{"items":{"$ref":"#/components/schemas/FindResultOut"},"type":"array","title":"Results"},"marketplaces_note":{"type":"string","title":"Marketplaces Note","default":"Живой поиск: Wildberries. Ozon, Я.Маркет и Мегамаркет: из товаров, которые здесь уже проверяли."},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","title":"FindResponse","description":"Ответ POST /api/public/find."},"FindResultOut":{"properties":{"external_id":{"type":"string","title":"External Id"},"name":{"type":"string","title":"Name"},"brand":{"type":"string","title":"Brand","default":""},"price_kopecks":{"type":"integer","title":"Price Kopecks","default":0},"url":{"type":"string","title":"Url"},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url"},"honesty":{"$ref":"#/components/schemas/HonestyOut"},"rank_score":{"type":"number","title":"Rank Score","default":0.0},"relevance":{"type":"number","title":"Relevance","default":0.0},"marketplace":{"type":"string","title":"Marketplace","default":"wildberries"}},"type":"object","required":["external_id","name","url"],"title":"FindResultOut","description":"Один товар в выдаче подбора."},"FolderCreateBody":{"properties":{"name":{"type":"string","maxLength":64,"title":"Name"},"color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Color"}},"type":"object","required":["name"],"title":"FolderCreateBody"},"FolderPatchBody":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Name"},"color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Color"},"position":{"anyOf":[{"type":"integer","maximum":10000.0,"minimum":0.0},{"type":"null"}],"title":"Position"}},"type":"object","title":"FolderPatchBody"},"GrantChecksBody":{"properties":{"count":{"type":"integer","maximum":10000.0,"minimum":1.0,"title":"Count","description":"Сколько проверок начислить (1..10000)"},"reason":{"type":"string","maxLength":200,"minLength":3,"title":"Reason","description":"Причина — видна юзеру в нотификации"}},"type":"object","required":["count","reason"],"title":"GrantChecksBody","description":"v0.12.18: ручное начисление проверок админом (компенсация за инцидент,\nбонус, акция). Атомарно через add_balance(source='admin'). Записываем\nв audit-log + шлём юзеру notif с пояснением."},"GrantProBody":{"properties":{"days":{"type":"integer","maximum":365.0,"minimum":1.0,"title":"Days","default":30},"tier":{"type":"string","title":"Tier","default":"pro"}},"type":"object","title":"GrantProBody"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HeartbeatBody":{"properties":{"device_uuid":{"type":"string","maxLength":64,"minLength":32,"title":"Device Uuid"},"ext_version":{"type":"string","maxLength":16,"title":"Ext Version"},"chrome_major":{"anyOf":[{"type":"integer","maximum":999.0,"minimum":0.0},{"type":"null"}],"title":"Chrome Major"},"platform":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Platform"},"last_marketplace":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Last Marketplace"},"scraped_this_session":{"type":"integer","maximum":50.0,"minimum":0.0,"title":"Scraped This Session","default":0}},"type":"object","required":["device_uuid","ext_version"],"title":"HeartbeatBody"},"HonestyOut":{"properties":{"checked":{"type":"boolean","title":"Checked","default":false},"pb_score":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pb Score"},"grade":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade"},"grade_color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade Color"},"verdict":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verdict"},"suspicion_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Suspicion Pct"},"short_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Short Id"}},"type":"object","title":"HonestyOut","description":"Блок «честности» товара. checked=false для непроверенных кандидатов."},"ImportBody":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ImportItem"},"type":"array","maxItems":30,"minItems":1,"title":"Items"}},"additionalProperties":false,"type":"object","required":["items"],"title":"ImportBody"},"ImportFromUrlRequest":{"properties":{"url":{"type":"string","maxLength":2000,"minLength":8,"title":"Url"}},"type":"object","required":["url"],"title":"ImportFromUrlRequest"},"ImportFromUrlResponse":{"properties":{"urls":{"items":{"type":"string"},"type":"array","title":"Urls"},"count":{"type":"integer","title":"Count","default":0},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","title":"ImportFromUrlResponse"},"ImportItem":{"properties":{"url":{"type":"string","maxLength":2000,"minLength":10,"title":"Url"},"role":{"type":"string","enum":["watch","own","competitor"],"title":"Role","default":"watch"},"group_name":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Group Name"}},"additionalProperties":false,"type":"object","required":["url"],"title":"ImportItem"},"ImportJob":{"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","enum":["queued","running","completed"],"title":"Status"},"total":{"type":"integer","title":"Total"},"processed":{"type":"integer","title":"Processed"},"results":{"items":{"type":"object"},"type":"array","title":"Results"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["id","status","total","processed","results","created_at"],"title":"ImportJob"},"ImportedReview":{"properties":{"product_id":{"type":"string","maxLength":128,"minLength":1,"title":"Product Id","default":"item"},"product_name":{"type":"string","maxLength":160,"minLength":1,"title":"Product Name","default":"Товар"},"marketplace":{"type":"string","maxLength":60,"minLength":1,"title":"Marketplace","default":"Выгрузка"},"rating":{"type":"integer","maximum":5.0,"minimum":1.0,"title":"Rating"},"text":{"type":"string","maxLength":4000,"minLength":1,"title":"Text"}},"additionalProperties":false,"type":"object","required":["rating","text"],"title":"ImportedReview"},"InAppMarkReadBody":{"properties":{"ids":{"items":{"type":"string"},"type":"array","maxItems":200,"title":"Ids"}},"additionalProperties":false,"type":"object","title":"InAppMarkReadBody","description":"{ids: [\"uuid-1\", \"uuid-2\", ...]}.\n\nids — те значения, которые лежали в Redis-payload'е под ключом id/notif_id."},"IngestRequest":{"properties":{"marketplace":{"$ref":"#/components/schemas/Marketplace"},"product":{"$ref":"#/components/schemas/ClientProduct"},"reviews":{"items":{"$ref":"#/components/schemas/ClientReview"},"type":"array","maxItems":5500,"title":"Reviews"}},"additionalProperties":false,"type":"object","required":["marketplace","product"],"title":"IngestRequest","description":"Structured product+reviews payload coming from the browser extension.\n\nThe extension scrapes an Ozon / Megamarket product page from the user's\nown IP (so no WAF on us) and ships it here. We treat it like the tail end\nof /api/analyze: detector -> GigaChat -> cache -> AnalysisResult.\n\nClient identity and input bounds are checked before processing. A valid\nshape does not prove marketplace provenance; browser results are not\npublished to the shared product database or cache."},"IntegrationError":{"properties":{"error":{"$ref":"#/components/schemas/ErrorDetail"}},"type":"object","required":["error"],"title":"IntegrationError"},"IntentOut":{"properties":{"raw_query":{"type":"string","title":"Raw Query","default":""},"keywords":{"items":{"type":"string"},"type":"array","title":"Keywords"},"brands":{"items":{"type":"string"},"type":"array","title":"Brands"},"price_min_kopecks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Price Min Kopecks"},"price_max_kopecks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Price Max Kopecks"},"confidence":{"type":"number","title":"Confidence","default":0.0}},"type":"object","title":"IntentOut","description":"Разобранное намерение пользователя (для прозрачности/отладки на фронте)."},"IssueEvidence":{"properties":{"text":{"type":"string","title":"Text"},"rating":{"type":"integer","title":"Rating"}},"type":"object","required":["text","rating"],"title":"IssueEvidence"},"LeadStatusBody":{"properties":{"status":{"type":"string","maxLength":20,"title":"Status"}},"type":"object","required":["status"],"title":"LeadStatusBody"},"Marketplace":{"type":"string","enum":["wildberries","ozon","yandex_market","megamarket"],"title":"Marketplace"},"MarketplaceProductPayload":{"properties":{"marketplace":{"$ref":"#/components/schemas/Marketplace"},"external_id":{"type":"string","maxLength":20,"pattern":"^[0-9]{1,20}$","title":"External Id"},"url":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Url"},"name":{"type":"string","maxLength":1000,"minLength":1,"title":"Name"},"image_url":{"anyOf":[{"type":"string","maxLength":2000,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Image Url"},"price_kopecks":{"anyOf":[{"type":"integer","maximum":100000000000.0,"minimum":0.0},{"type":"null"}],"title":"Price Kopecks"},"old_price_kopecks":{"anyOf":[{"type":"integer","maximum":100000000000.0,"minimum":0.0},{"type":"null"}],"title":"Old Price Kopecks"},"seller_name":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Seller Name"},"seller_id":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Seller Id"},"rating":{"anyOf":[{"type":"number","maximum":5.0,"minimum":0.0},{"type":"null"}],"title":"Rating"},"reviews_count":{"anyOf":[{"type":"integer","maximum":1000000000.0,"minimum":0.0},{"type":"null"}],"title":"Reviews Count"},"reviews":{"items":{"$ref":"#/components/schemas/E2FReview"},"type":"array","maxItems":5500,"title":"Reviews"},"characteristics":{"anyOf":[{"items":{"additionalProperties":{"type":"string"},"type":"object"},"type":"array","maxItems":100},{"type":"null"}],"title":"Characteristics"}},"additionalProperties":false,"type":"object","required":["marketplace","external_id","url","name"],"title":"MarketplaceProductPayload","description":"Pre-scraped product card + reviews from the browser extension's E2F flow.\n\nРасширение тянет mobile-API маркетплейса (Ozon composer, MM mobile, YM\nmobile) с user-IP/cookies (обходим WAF) и присылает сюда вместо того\nчтобы сервер сам шёл парсить. См. `extension/lib/marketplace_fetchers.js`.\n\nЦены — в копейках (integer), чтоб избежать float-rounding в transit.\n`to_product()` / `to_reviews()` приводят к каноническим `Product` / `Review`."},"MonitoringProduct":{"properties":{"id":{"type":"integer","title":"Id"},"analysis_id":{"type":"integer","title":"Analysis Id"},"url":{"type":"string","title":"Url"},"analysis_url":{"type":"string","title":"Analysis Url"},"role":{"type":"string","enum":["watch","own","competitor"],"title":"Role"},"group_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Group Name"},"marketplace":{"type":"string","enum":["wildberries","ozon","yandex_market","megamarket"],"title":"Marketplace"},"external_id":{"type":"string","title":"External Id"},"name":{"type":"string","title":"Name"},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url"},"rating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rating"},"reviews_sample_size":{"type":"integer","title":"Reviews Sample Size"},"suspicion_pct":{"type":"number","title":"Suspicion Pct"},"complaints":{"items":{"anyOf":[{"type":"string"},{"type":"object"}]},"type":"array","title":"Complaints"},"complaints_summary":{"type":"string","title":"Complaints Summary"},"review_insights":{"anyOf":[{"$ref":"#/components/schemas/ReviewInsights"},{"type":"null"}]},"analysis_updated_at":{"type":"string","format":"date-time","title":"Analysis Updated At"},"price_kopecks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Price Kopecks"},"price_change_7d_kopecks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Price Change 7D Kopecks"},"price_observation":{"$ref":"#/components/schemas/PriceObservation"},"target_price_kopecks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Target Price Kopecks"},"notify_on_burst":{"type":"boolean","title":"Notify On Burst"},"paused_until":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Paused Until"}},"type":"object","required":["id","analysis_id","url","analysis_url","role","group_name","marketplace","external_id","name","image_url","rating","reviews_sample_size","suspicion_pct","complaints","complaints_summary","analysis_updated_at","price_kopecks","price_change_7d_kopecks","price_observation","target_price_kopecks","notify_on_burst","paused_until"],"title":"MonitoringProduct"},"MoveToFolderBody":{"properties":{"folder_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Folder Id"}},"type":"object","title":"MoveToFolderBody"},"NoteBody":{"properties":{"note":{"type":"string","maxLength":500,"title":"Note","default":""}},"type":"object","title":"NoteBody"},"NotifyBroadcastBody":{"properties":{"title":{"type":"string","maxLength":200,"minLength":1,"title":"Title"},"body":{"type":"string","maxLength":4000,"title":"Body","default":""},"link_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Link Url"},"link_label":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Link Label"},"target":{"type":"string","title":"Target","default":"all"}},"type":"object","required":["title"],"title":"NotifyBroadcastBody"},"NotifyUserBody":{"properties":{"title":{"type":"string","maxLength":200,"minLength":1,"title":"Title"},"body":{"type":"string","maxLength":4000,"title":"Body","default":""},"link_url":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Link Url"},"link_label":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Link Label"}},"type":"object","required":["title"],"title":"NotifyUserBody"},"PrefsUpdate":{"properties":{"event":{"type":"string","maxLength":64,"minLength":1,"title":"Event"},"channel_tg":{"type":"boolean","title":"Channel Tg","default":true},"channel_browser_push":{"type":"boolean","title":"Channel Browser Push","default":false},"channel_email":{"type":"boolean","title":"Channel Email","default":false},"channel_in_app":{"type":"boolean","title":"Channel In App","default":true}},"additionalProperties":false,"type":"object","required":["event"],"title":"PrefsUpdate","description":"Body для POST /prefs — UPSERT каналов одного события."},"PriceAlertCreateBody":{"properties":{"analysis_id":{"type":"integer","minimum":1.0,"title":"Analysis Id"},"threshold_kopecks":{"type":"integer","maximum":10000000000.0,"exclusiveMinimum":0.0,"title":"Threshold Kopecks"}},"type":"object","required":["analysis_id","threshold_kopecks"],"title":"PriceAlertCreateBody"},"PriceHistoryBatchBody":{"properties":{"aids":{"items":{"type":"integer"},"type":"array","maxItems":100,"minItems":1,"title":"Aids"},"days":{"type":"integer","maximum":90.0,"minimum":1.0,"title":"Days","default":7}},"type":"object","required":["aids"],"title":"PriceHistoryBatchBody"},"PriceObservation":{"properties":{"status":{"type":"string","enum":["fresh","stale","pending","unavailable","paused"],"title":"Status"},"last_success_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Success At"},"last_attempt_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Attempt At"},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code"}},"type":"object","required":["status","last_success_at","last_attempt_at","error_code"],"title":"PriceObservation"},"Product":{"properties":{"marketplace":{"$ref":"#/components/schemas/Marketplace"},"external_id":{"type":"string","title":"External Id"},"url":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Url"},"name":{"type":"string","title":"Name"},"brand":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Brand"},"seller":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seller"},"seller_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seller Id"},"price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Price"},"old_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Old Price"},"rating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rating"},"review_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Review Count"},"image_url":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Image Url"},"characteristics":{"anyOf":[{"items":{"additionalProperties":{"type":"string"},"type":"object"},"type":"array"},{"type":"null"}],"title":"Characteristics"}},"type":"object","required":["marketplace","external_id","url","name"],"title":"Product"},"ProductList":{"properties":{"items":{"items":{"$ref":"#/components/schemas/MonitoringProduct"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"limits":{"additionalProperties":{"type":"integer"},"type":"object","title":"Limits"},"coverage":{"type":"string","title":"Coverage"}},"type":"object","required":["items","next_cursor","limits","coverage"],"title":"ProductList"},"ProductPatch":{"properties":{"role":{"anyOf":[{"type":"string","enum":["watch","own","competitor"]},{"type":"null"}],"title":"Role"},"group_name":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Group Name"},"target_price_kopecks":{"anyOf":[{"type":"integer","maximum":1000000000.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Target Price Kopecks"},"notify_on_burst":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Notify On Burst"}},"additionalProperties":false,"type":"object","title":"ProductPatch"},"ProductUpdated":{"properties":{"id":{"type":"integer","title":"Id"},"updated":{"type":"boolean","title":"Updated"}},"type":"object","required":["id","updated"],"title":"ProductUpdated"},"PushSubscriptionKeys":{"properties":{"p256dh":{"type":"string","maxLength":512,"minLength":20,"title":"P256Dh"},"auth":{"type":"string","maxLength":128,"minLength":10,"title":"Auth"}},"additionalProperties":false,"type":"object","required":["p256dh","auth"],"title":"PushSubscriptionKeys"},"PushSubscriptionPayload":{"properties":{"endpoint":{"type":"string","maxLength":2000,"minLength":20,"title":"Endpoint"},"keys":{"$ref":"#/components/schemas/PushSubscriptionKeys"}},"type":"object","required":["endpoint","keys"],"title":"PushSubscriptionPayload","description":"W3C PushSubscription JSON, как его генерит pushManager.subscribe()."},"PushUnsubscribeBody":{"properties":{"endpoint":{"type":"string","maxLength":2000,"minLength":20,"title":"Endpoint"}},"additionalProperties":false,"type":"object","required":["endpoint"],"title":"PushUnsubscribeBody"},"ReplyBody":{"properties":{"body":{"type":"string","maxLength":4000,"minLength":1,"title":"Body"},"request_id":{"type":"string","format":"uuid","title":"Request Id"}},"additionalProperties":false,"type":"object","required":["body","request_id"],"title":"ReplyBody"},"ReviewImport":{"properties":{"reviews":{"items":{"$ref":"#/components/schemas/ImportedReview"},"type":"array","maxItems":1000,"minItems":1,"title":"Reviews"}},"additionalProperties":false,"type":"object","required":["reviews"],"title":"ReviewImport"},"ReviewInsights":{"properties":{"version":{"type":"integer","title":"Version","default":1},"source":{"type":"string","enum":["server_sample","browser_sample","private_import"],"title":"Source"},"sample_size":{"type":"integer","title":"Sample Size"},"unique_texts":{"type":"integer","title":"Unique Texts"},"low_rated_texts":{"type":"integer","title":"Low Rated Texts"},"duplicate_texts":{"type":"integer","title":"Duplicate Texts"},"excluded_suspicious":{"type":"integer","title":"Excluded Suspicious","default":0},"matched_reviews":{"type":"integer","title":"Matched Reviews"},"issues":{"items":{"$ref":"#/components/schemas/ReviewIssue"},"type":"array","title":"Issues"},"caveat":{"type":"string","title":"Caveat"}},"type":"object","required":["source","sample_size","unique_texts","low_rated_texts","duplicate_texts","matched_reviews","caveat"],"title":"ReviewInsights"},"ReviewIssue":{"properties":{"key":{"type":"string","title":"Key"},"label":{"type":"string","title":"Label"},"count":{"type":"integer","minimum":1.0,"title":"Count"},"repeated":{"type":"boolean","title":"Repeated"},"buyer_action":{"type":"string","title":"Buyer Action"},"seller_action":{"type":"string","title":"Seller Action"},"examples":{"items":{"$ref":"#/components/schemas/IssueEvidence"},"type":"array","title":"Examples"}},"type":"object","required":["key","label","count","repeated","buyer_action","seller_action","examples"],"title":"ReviewIssue"},"SellerLeadRequest":{"properties":{"name":{"type":"string","maxLength":100,"minLength":2,"title":"Name"},"email":{"type":"string","maxLength":200,"minLength":5,"title":"Email"},"telegram":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Telegram"},"sku_count":{"type":"string","maxLength":20,"title":"Sku Count"},"marketplaces":{"items":{"type":"string"},"type":"array","maxItems":8,"title":"Marketplaces"},"message":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Message"}},"type":"object","required":["name","email","sku_count"],"title":"SellerLeadRequest","description":"Лид-форма с лендинга /for-sellers."},"SellerWatcherCreate":{"properties":{"marketplace":{"type":"string","maxLength":32,"minLength":1,"title":"Marketplace"},"seller_id":{"type":"string","maxLength":64,"minLength":1,"title":"Seller Id"},"seller_name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Seller Name"}},"additionalProperties":false,"type":"object","required":["marketplace","seller_id"],"title":"SellerWatcherCreate","description":"Body для POST /api/my/watchers/seller."},"SlotOut":{"properties":{"name":{"type":"string","title":"Name"},"picks":{"items":{"$ref":"#/components/schemas/FindResultOut"},"type":"array","title":"Picks"}},"type":"object","required":["name","picks"],"title":"SlotOut"},"SubscribeRequest":{"properties":{"tier":{"type":"string","title":"Tier","description":"pro | expert"},"is_annual":{"type":"boolean","title":"Is Annual","default":false},"device_uuid":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Device Uuid"},"feature":{"anyOf":[{"type":"string","enum":["monitoring","sellers","compare","checks","cart"]},{"type":"null"}],"title":"Feature"},"product_url":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Product Url"},"receipt_email":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Receipt Email"}},"type":"object","required":["tier"],"title":"SubscribeRequest","description":"Владелец — user_id из сессии; receipt_email — только контакт для чека."},"SubscribeResponse":{"properties":{"payment_url":{"type":"string","title":"Payment Url"},"order_id":{"type":"string","title":"Order Id"},"amount_rub":{"type":"number","title":"Amount Rub"}},"type":"object","required":["payment_url","order_id","amount_rub"],"title":"SubscribeResponse"},"SubscriptionStatusResponse":{"properties":{"found":{"type":"boolean","title":"Found"},"tier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tier"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"},"auto_renew":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Auto Renew"},"next_charge_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Charge At"}},"type":"object","required":["found"],"title":"SubscriptionStatusResponse"},"TemplateBody":{"properties":{"label":{"type":"string","maxLength":80,"minLength":1,"title":"Label"},"body":{"type":"string","maxLength":4000,"minLength":1,"title":"Body"}},"type":"object","required":["label","body"],"title":"TemplateBody"},"TemplateOut":{"properties":{"slug":{"type":"string","title":"Slug"},"title":{"type":"string","title":"Title"},"tagline":{"type":"string","title":"Tagline"},"emoji":{"type":"string","title":"Emoji","default":""},"slots_count":{"type":"integer","title":"Slots Count"},"preview_sum_kopecks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Preview Sum Kopecks"},"preview_images":{"items":{"type":"string"},"type":"array","title":"Preview Images"}},"type":"object","required":["slug","title","tagline","slots_count"],"title":"TemplateOut"},"TemplatesResponse":{"properties":{"templates":{"items":{"$ref":"#/components/schemas/TemplateOut"},"type":"array","title":"Templates"}},"type":"object","required":["templates"],"title":"TemplatesResponse"},"TestBody":{"properties":{"request_id":{"type":"string","format":"uuid","title":"Request Id"}},"additionalProperties":false,"type":"object","required":["request_id"],"title":"TestBody"},"TotpSetupBody":{"properties":{"username":{"type":"string","title":"Username"},"password":{"type":"string","title":"Password"}},"type":"object","required":["username","password"],"title":"TotpSetupBody"},"TotpVerifyBody":{"properties":{"username":{"type":"string","title":"Username"},"password":{"type":"string","title":"Password"},"pending_secret":{"type":"string","title":"Pending Secret"},"totp_code":{"type":"string","title":"Totp Code"},"backup_codes":{"items":{"type":"string"},"type":"array","title":"Backup Codes"}},"type":"object","required":["username","password","pending_secret","totp_code"],"title":"TotpVerifyBody"},"UpdateSettingsBody":{"properties":{"values":{"type":"object","title":"Values"}},"type":"object","required":["values"],"title":"UpdateSettingsBody"},"UpsertQuotaBody":{"properties":{"cidr":{"type":"string","maxLength":64,"minLength":1,"title":"Cidr"},"quota_per_hour":{"type":"integer","maximum":1000000.0,"minimum":0.0,"title":"Quota Per Hour"},"quota_per_day":{"type":"integer","maximum":10000000.0,"minimum":0.0,"title":"Quota Per Day"},"notes":{"type":"string","maxLength":256,"title":"Notes","default":""}},"type":"object","required":["cidr","quota_per_hour","quota_per_day"],"title":"UpsertQuotaBody"},"UserPatchBody":{"properties":{"is_blocked":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Blocked"},"display_name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Display Name"}},"type":"object","title":"UserPatchBody"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"Verdict":{"type":"string","enum":["trusted","caution","avoid","unknown"],"title":"Verdict"},"WatchlistAddBody":{"properties":{"analysis_id":{"type":"integer","minimum":1.0,"title":"Analysis Id"}},"type":"object","required":["analysis_id"],"title":"WatchlistAddBody"},"WatchlistSnoozeBody":{"properties":{"days":{"type":"integer","maximum":365.0,"minimum":0.0,"title":"Days"}},"type":"object","required":["days"],"title":"WatchlistSnoozeBody"},"WatchlistTargetBody":{"properties":{"target_rubles":{"type":"integer","maximum":10000000.0,"minimum":0.0,"title":"Target Rubles"}},"type":"object","required":["target_rubles"],"title":"WatchlistTargetBody"},"WorkbenchProduct":{"properties":{"product_id":{"type":"string","title":"Product Id"},"product_name":{"type":"string","title":"Product Name"},"marketplace":{"type":"string","title":"Marketplace"},"insights":{"$ref":"#/components/schemas/ReviewInsights"}},"type":"object","required":["product_id","product_name","marketplace","insights"],"title":"WorkbenchProduct"},"WorkbenchReport":{"properties":{"source":{"type":"string","title":"Source","default":"private_import"},"persisted":{"type":"boolean","title":"Persisted","default":false},"input_reviews":{"type":"integer","title":"Input Reviews"},"products":{"items":{"$ref":"#/components/schemas/WorkbenchProduct"},"type":"array","title":"Products"},"notice":{"type":"string","title":"Notice","default":"Разбор вашей выгрузки. Подлинность и полнота отзывов не проверены; отчёт не публикуется и не сохраняется в общую базу."}},"type":"object","required":["input_reviews","products"],"title":"WorkbenchReport"},"_ErrorBody":{"properties":{"error":{"type":"string","maxLength":300,"minLength":1,"title":"Error"}},"type":"object","required":["error"],"title":"_ErrorBody"},"_PartnerCodeCreate":{"properties":{"code":{"type":"string","maxLength":32,"minLength":2,"title":"Code"},"partner_name":{"type":"string","maxLength":120,"minLength":1,"title":"Partner Name"},"partner_contact":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Partner Contact"},"partner_url":{"anyOf":[{"type":"string","maxLength":300},{"type":"null"}],"title":"Partner Url"},"bonus_type":{"type":"string","title":"Bonus Type","default":"free_checks"},"bonus_value":{"type":"integer","maximum":100000.0,"minimum":0.0,"title":"Bonus Value","default":10},"revenue_share_pct":{"type":"number","maximum":100.0,"minimum":0.0,"title":"Revenue Share Pct","default":30.0},"notes":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Notes"},"owner_email":{"anyOf":[{"type":"string","maxLength":254},{"type":"null"}],"title":"Owner Email"}},"type":"object","required":["code","partner_name"],"title":"_PartnerCodeCreate"},"_PartnerCodePatch":{"properties":{"partner_name":{"anyOf":[{"type":"string","maxLength":120,"minLength":1},{"type":"null"}],"title":"Partner Name"},"partner_contact":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Partner Contact"},"partner_url":{"anyOf":[{"type":"string","maxLength":300},{"type":"null"}],"title":"Partner Url"},"bonus_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bonus Type"},"bonus_value":{"anyOf":[{"type":"integer","maximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"Bonus Value"},"revenue_share_pct":{"anyOf":[{"type":"number","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Revenue Share Pct"},"notes":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Notes"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"owner_email":{"anyOf":[{"type":"string","maxLength":254},{"type":"null"}],"title":"Owner Email"}},"type":"object","title":"_PartnerCodePatch"},"_RecheckRequest":{"properties":{"analysis_id":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Analysis Id"},"url":{"anyOf":[{"type":"string","maxLength":2000,"minLength":10},{"type":"null"}],"title":"Url"}},"type":"object","title":"_RecheckRequest","description":"POST /api/recheck body. Можно передать analysis_id ИЛИ url.\n\nanalysis_id — приоритет, чтобы найти marketplace + URL в БД (избегает\nситуации «юзер прислал чужой URL по своему analysis_id»). Если БД\nнедоступна или запись не найдена — fallback на url."},"_ResultBody":{"properties":{"payload":{"$ref":"#/components/schemas/MarketplaceProductPayload"}},"type":"object","required":["payload"],"title":"_ResultBody"},"_SupportAssignBody":{"properties":{"admin_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Admin Id"}},"type":"object","title":"_SupportAssignBody"},"_SupportCreateBody":{"properties":{"subject":{"type":"string","maxLength":200,"minLength":3,"title":"Subject"},"message":{"type":"string","maxLength":4000,"minLength":3,"title":"Message"}},"type":"object","required":["subject","message"],"title":"_SupportCreateBody"},"_SupportMessageBody":{"properties":{"body":{"type":"string","maxLength":4000,"title":"Body","default":""},"attachments":{"items":{"$ref":"#/components/schemas/app__api__my_routes___SupportAttachment"},"type":"array","maxItems":3,"title":"Attachments"}},"type":"object","title":"_SupportMessageBody"},"_SupportPriorityBody":{"properties":{"priority":{"type":"string","pattern":"^(normal|high)$","title":"Priority"}},"type":"object","required":["priority"],"title":"_SupportPriorityBody"},"_SupportReplyBody":{"properties":{"body":{"type":"string","maxLength":4000,"title":"Body","default":""},"is_internal_note":{"type":"boolean","title":"Is Internal Note","default":false},"attachments":{"items":{"$ref":"#/components/schemas/app__api__admin_ops_routes___SupportAttachment"},"type":"array","maxItems":3,"title":"Attachments"}},"type":"object","title":"_SupportReplyBody"},"_SupportStatusBody":{"properties":{"status":{"type":"string","pattern":"^(resolved|open|pending_user|closed)$","title":"Status"}},"type":"object","required":["status"],"title":"_SupportStatusBody"},"_TgAuthBody":{"properties":{"init_data":{"type":"string","maxLength":4096,"minLength":1,"title":"Init Data"}},"type":"object","required":["init_data"],"title":"_TgAuthBody"},"app__api__admin_ops_routes___SupportAttachment":{"properties":{"name":{"type":"string","maxLength":200,"title":"Name","default":"image.png"},"type":{"type":"string","maxLength":64,"title":"Type","default":"image/png"},"size":{"type":"integer","maximum":6291456.0,"minimum":0.0,"title":"Size","default":0},"data_url":{"type":"string","maxLength":8388608,"title":"Data Url","default":""}},"type":"object","title":"_SupportAttachment","description":"Картинка-вложение в support-сообщении. См. user-side в my_routes.py.\ndata_url — base64 inline (data:image/...;base64,...). Каждая ≤6 MB.\nХранится в JSONB-колонке messages.attachments как list[dict]."},"app__api__my_routes___SupportAttachment":{"properties":{"name":{"type":"string","maxLength":200,"title":"Name","default":"image.png"},"type":{"type":"string","maxLength":64,"title":"Type","default":"image/png"},"size":{"type":"integer","maximum":6291456.0,"minimum":0.0,"title":"Size","default":0},"data_url":{"type":"string","maxLength":8388608,"title":"Data Url","default":""}},"type":"object","title":"_SupportAttachment","description":"Описание прикреплённой картинки. v0.6: храним сам контент в data_url\n(base64 inline) — без отдельного S3/blob storage. Это OK для саппорт-\nскриншотов: ≤3 шт × ≤5 MB = 15 MB max на сообщение, в JSONB-колонке.\nЕсли объёмы вырастут — переедем на R2/S3 c прозрачным API."}},"securitySchemes":{"ProbiwaemBearer":{"type":"http","scheme":"bearer"},"ProbiwaemKey":{"type":"apiKey","in":"header","name":"X-API-Key"}}}}