{"openapi":"3.1.0","info":{"title":"Куда дальше? API","version":"0.1.0","description":"API текущего MVP. GET /api/catalog публичный; фильтрация и пагинация пока выполняются на фронтенде. Админские операции используют HttpOnly cookie rc_session, полученную через POST /api/admin/login (срок 12 часов). Для всех POST заголовок Origin должен совпадать с SITE_URL. В браузере сначала войдите на /admin, затем откройте /api-docs: cookie отправляется автоматически. В CLI сохраните cookie через --cookie-jar и передавайте её через --cookie вместе с Origin. Нельзя вручную установить Cookie из браузерного JavaScript. Сохранённый список хранится в localStorage и не имеет API. Сервер не гарантирует точность условий найма."},"servers":[{"url":"/","description":"Текущий сервер портала"}],"tags":[{"name":"Каталог"},{"name":"Авторизация"},{"name":"Управление"},{"name":"Документация"}],"paths":{"/api/catalog":{"get":{"tags":["Каталог"],"operationId":"getCatalog","summary":"Получить компании и активные IT-вакансии","description":"Возвращает весь каталог без query-параметров. Админские поля board и notes компаний не публикуются. Неизвестные условия отмечены unknown. Поле hiring содержит автоматическую классификацию по географии и явным условиям. У компании — сводка по текущим ролям; неполный, устаревший или ошибочный сбор даёт UNKNOWN. TRUE_GLOBAL не подтверждает найм из РФ или отсутствие требования английского.","responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Catalog"}}}},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/admin/login":{"post":{"tags":["Авторизация"],"operationId":"loginAdmin","summary":"Войти в админку","parameters":[{"$ref":"#/components/parameters/Origin"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginInput"}}}},"responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}},"headers":{"Set-Cookie":{"description":"rc_session: HttpOnly, SameSite=Strict, Path=/, Max-Age=43200. Secure зависит от COOKIE_SECURE.","schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/api/admin/logout":{"post":{"tags":["Авторизация"],"operationId":"logoutAdmin","summary":"Завершить сессию","description":"Удаляет текущую сессию и cookie. Допустимо без действующей сессии.","parameters":[{"$ref":"#/components/parameters/Origin"}],"responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/admin/session":{"get":{"security":[{"adminSession":[]}],"tags":["Авторизация"],"operationId":"getAdminSession","summary":"Проверить текущую сессию","responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Session"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/admin/companies":{"get":{"security":[{"adminSession":[]}],"tags":["Управление"],"operationId":"getAdminCompanies","summary":"Получить компании с настройками сбора","responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminCompany"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"security":[{"adminSession":[]}],"tags":["Управление"],"operationId":"saveCompany","summary":"Добавить или изменить компанию","description":"Без id создаёт компанию с новым UUID; с id изменяет существующую. Для manual автосбор принудительно выключается. Для остальных ATS нужен board: токен доски, а не полный URL.","parameters":[{"$ref":"#/components/parameters/Origin"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyInput"},"example":{"name":"Example","website":"https://example.com/","careers":"https://example.com/careers","category":"IT","ats":"manual","enabled":false,"notes":""}}}},"responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanySaved"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/admin/review":{"post":{"security":[{"adminSession":[]}],"tags":["Управление"],"operationId":"reviewJob","summary":"Сохранить проверенные условия вакансии","description":"Нужны HTTPS-ссылка и непустое пояснение. Условия сохраняются для одной вакансии, а не компании в целом.","parameters":[{"$ref":"#/components/parameters/Origin"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewInput"}}}},"responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/admin/sync":{"post":{"security":[{"adminSession":[]}],"tags":["Управление"],"operationId":"syncCompanies","summary":"Обновить включённые источники","description":"С id обновляет одну включённую компанию; без id — все включённые. Неизвестная или выключенная компания даёт пустой массив. Запрос выполняется синхронно и может быть длительным. Ошибка отдельного источника возвращается в элементе массива при HTTP 200; предыдущие данные сохраняются. Повторный запуск в течение минуты отклоняется. Параллельный импорт может вернуть HTTP 500. Подозрительный ноль или падение более чем на 70% сохраняет предыдущие данные и возвращает ok=false. Отсутствующие вакансии полного источника закрываются после двух подтверждений с интервалом не менее 6 часов; RSS их не закрывает.","parameters":[{"$ref":"#/components/parameters/Origin"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncInput"},"example":{}}}},"responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SyncResult"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/openapi.json":{"get":{"tags":["Документация"],"operationId":"getOpenApi","summary":"Получить OpenAPI-спецификацию","responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"type":"object","description":"Документ OpenAPI 3.1.0"}}}}}}},"/api/logos/{id}":{"get":{"operationId":"getCompanyLogo","summary":"Сохранённый логотип компании","description":"Возвращает проверенный значок из сборки для inDrive и airSlate; для остальных компаний загружает favicon сайта и сохраняет на сервере. Изображение кэшируется на 30 дней; при ошибке обновления возвращается сохранённая версия.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"If-None-Match","in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"Логотип компании","headers":{"Cache-Control":{"schema":{"type":"string"}},"ETag":{"schema":{"type":"string"}}},"content":{"image/x-icon":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/gif":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"image/svg+xml":{"schema":{"type":"string","format":"binary"}}}},"304":{"description":"Изображение не изменилось"},"404":{"description":"Компания или логотип не найдены"}}}}},"components":{"securitySchemes":{"adminSession":{"type":"apiKey","in":"cookie","name":"rc_session","description":"Непрозрачная HttpOnly-сессия администратора. Получается при входе; передаётся автоматически в браузере."}},"parameters":{"Origin":{"name":"Origin","in":"header","required":true,"description":"Точное значение origin из SITE_URL, например http://localhost:3200. Браузер устанавливает этот заголовок сам.","schema":{"type":"string","format":"uri"}}},"schemas":{"Company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"website":{"type":"string","format":"uri"},"careers":{"type":"string","format":"uri"},"category":{"type":"string"},"ats":{"type":"string","enum":["manual","greenhouse","lever","lever_eu","ashby","recruitee","workable","teamtailor","breezy","huntflow","pinpoint","custom"]},"enabled":{"type":"boolean"},"last_sync":{"type":["string","null"],"format":"date-time"},"sync_error":{"type":["string","null"]},"hiring":{"type":"object","required":["model","reason","counts","checked_at","fresh","locations"],"properties":{"model":{"type":"string","enum":["TRUE_GLOBAL","RELOCATION_HUB","LOCAL_ONLY","UNKNOWN"]},"reason":{"type":"string"},"counts":{"type":"object","additionalProperties":{"type":"integer"}},"checked_at":{"type":["string","null"],"format":"date-time"},"fresh":{"type":"boolean","description":"Источник подключён, обновление успешно и не старше 48 часов. Счётчики при false могут быть устаревшими."},"locations":{"type":"object","description":"Уникальные локации из полей активных вакансий, сгруппированные по модели. Включают страны, города и регионы, без вывода о праве на работу.","additionalProperties":{"type":"array","items":{"type":"string"}}}}},"description":{"type":"string","maxLength":1200,"description":"Краткое публичное описание деятельности компании."},"description_url":{"type":"string","description":"HTTPS-ссылка на источник описания; пустая строка, если не указана."},"source_status":{"type":"string","enum":["needs_adapter","not_found"],"description":"Результат исследования неподключённого источника. Подключение и ошибки определяются отдельно."}},"required":["id","name","website","careers","category","ats","enabled","last_sync","sync_error"]},"AdminCompany":{"allOf":[{"$ref":"#/components/schemas/Company"},{"type":"object","properties":{"board":{"type":"string"},"notes":{"type":"string"},"last_attempt":{"type":["string","null"],"format":"date-time"},"last_source_count":{"type":["integer","null"]},"last_job_count":{"type":["integer","null"]}},"required":["board","notes"]}]},"Job":{"type":"object","properties":{"id":{"type":"string"},"company_id":{"type":"string"},"company_name":{"type":"string"},"source_id":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"location":{"type":"string"},"role":{"type":"string","enum":["IT","QA"]},"remote":{"type":"string","enum":["remote","worldwide","hybrid","onsite","unknown"]},"russian":{"type":"string","enum":["unknown","required","mentioned"]},"english":{"type":"string","enum":["unknown","A2","B1","B2","C1","C2"]},"rf":{"type":"string","enum":["unknown","allowed","excluded"]},"evidence_url":{"type":"string"},"evidence_note":{"type":"string"},"reviewed_at":{"type":["string","null"],"format":"date-time"},"active":{"type":"boolean"},"first_seen":{"type":"string","format":"date-time"},"last_seen":{"type":"string","format":"date-time"},"hiring":{"type":"object","required":["model","reason","geography","source","relocation"],"properties":{"model":{"type":"string","enum":["TRUE_GLOBAL","RELOCATION_HUB","LOCAL_ONLY","UNKNOWN"]},"reason":{"type":"string"},"geography":{"type":"string"},"source":{"type":"string","format":"uri"},"relocation":{"type":"object","description":"Поддержка переезда оценивается отдельно от географии. Страна сама по себе не подтверждает релокацию.","required":["status","evidence"],"properties":{"status":{"type":"string","enum":["supported","excluded","unknown"]},"evidence":{"type":"string","description":"Фрагмент источника; пустая строка при отсутствии явных условий."}}}}},"company_website":{"type":"string","format":"uri","description":"Официальный сайт компании для логотипа."}},"required":["id","company_id","company_name","source_id","title","url","location","role","remote","russian","english","rf","evidence_url","evidence_note","reviewed_at","active","first_seen","last_seen"]},"Catalog":{"type":"object","properties":{"companies":{"type":"array","items":{"$ref":"#/components/schemas/Company"}},"jobs":{"type":"array","items":{"$ref":"#/components/schemas/Job"}}},"required":["companies","jobs"]},"LoginInput":{"type":"object","properties":{"password":{"type":"string","maxLength":256,"writeOnly":true}},"required":["password"]},"CompanyInput":{"type":"object","properties":{"id":{"type":"string","description":"Идентификатор существующей компании для редактирования."},"name":{"type":"string","minLength":1,"maxLength":120},"website":{"type":"string","format":"uri","pattern":"^https://","description":"HTTPS URL без учётных данных."},"careers":{"type":"string","format":"uri","pattern":"^https://","description":"HTTPS URL без учётных данных."},"category":{"type":"string","maxLength":120,"default":""},"ats":{"type":"string","enum":["manual","greenhouse","lever","lever_eu","ashby","recruitee","workable","teamtailor","breezy","huntflow","pinpoint","custom"]},"board":{"type":"string","description":"Токен публичной доски, не API-ключ. Novakid: novakid; Workable: slug; Teamtailor: slug, специальные домены replika/ecommpay/sumsub/osome разрешены явно. Custom: только miro, evil_martians, plata, aviasales, gaijin, dodo, mygames, uzum, muse, softswiss, tangem, groupib, luxoft и spotware; адреса и обработчики фиксированы. Breezy/Huntflow/Pinpoint: slug публичной карьерной доски."},"enabled":{"type":"boolean","default":false},"notes":{"type":"string","maxLength":2000,"default":""},"description":{"type":"string","maxLength":1200,"description":"Краткое публичное описание деятельности компании."},"description_url":{"type":"string","description":"HTTPS-ссылка на источник описания; пустая строка, если не указана."},"source_status":{"type":"string","enum":["needs_adapter","not_found"],"description":"Результат исследования неподключённого источника. Подключение и ошибки определяются отдельно."}},"required":["name","website","careers","ats"],"allOf":[{"if":{"properties":{"ats":{"not":{"const":"manual"}}},"required":["ats"]},"then":{"required":["board"],"properties":{"board":{"type":"string","maxLength":100,"pattern":"^[a-zA-Z0-9_-]+(?:\\.[a-zA-Z0-9_-]+)*$"}}}}]},"ReviewInput":{"type":"object","properties":{"id":{"type":"string"},"russian":{"type":"string","enum":["unknown","required","mentioned"]},"english":{"type":"string","enum":["unknown","A2","B1","B2","C1","C2"]},"rf":{"type":"string","enum":["unknown","allowed","excluded"]},"evidence_url":{"type":"string","format":"uri","pattern":"^https://"},"evidence_note":{"type":"string","minLength":1,"maxLength":1500,"description":"Не допускается строка только из пробелов."}},"required":["id","russian","english","rf","evidence_url","evidence_note"]},"SyncInput":{"type":"object","properties":{"id":{"type":"string"}},"example":{}},"SyncResult":{"oneOf":[{"type":"object","properties":{"company":{"type":"string"},"ok":{"const":true},"count":{"type":"integer","minimum":0},"complete":{"type":"boolean","description":"false для RSS: добавление/обновление без автоматического закрытия отсутствующих вакансий."}},"required":["company","ok","count","complete"]},{"type":"object","properties":{"company":{"type":"string"},"ok":{"const":false},"error":{"type":"string","maxLength":240}},"required":["company","ok","error"]}]},"Ok":{"type":"object","properties":{"ok":{"const":true}},"required":["ok"]},"Session":{"type":"object","properties":{"authenticated":{"const":true}},"required":["authenticated"]},"CompanySaved":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"Error":{"type":"object","properties":{"statusCode":{"type":"integer"},"statusMessage":{"type":"string"},"message":{"type":"string"},"error":{"type":"boolean"},"url":{"type":"string"}},"required":["statusCode"],"description":"Стандартный ответ ошибки H3/Nitro. Дополнительные поля зависят от окружения."}},"responses":{"Unauthorized":{"description":"Нет действующей сессии или неверный пароль.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Origin не совпадает с SITE_URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InvalidInput":{"description":"Некорректные данные.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Компания или вакансия не найдена.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Лимит: вход — 10 попыток за 15 минут на IP; обновление — не чаще раза в минуту на процесс.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unavailable":{"description":"Пароль администратора не настроен.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InternalError":{"description":"Ошибка сервера или параллельный импорт.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}