---
name: kodoceh_web_admins_cabinets
description: kodoceh-web — где кабинеты и админки 3 продуктов и как закрыт доступ не-админам (проверять перед правками админок/кабинетов); + где история Автероа
metadata: 
  node_type: memory
  type: project
  originSessionId: b11e39bd-4f6f-47c9-ba7b-680ccf041a5b
---

**Кабинеты** (фронт kodoceh-web на mini, Next.js, `npm start` детачем по портам studio 20410 / Гаечкин 20411 / ВакХантер 20412 / Автеро 20413; перезапуск: `kill $(pgrep -f apps/<app>)` + `nohup npm start`, НЕ `pkill next-server` — убьёт zemlya/tbr):
- **ВакХантер** `apps/vakhunter/app/app/page.tsx` — **IA упрощена 2026-06-26 (D34): верхнее меню 3 вкладки** Лента · Помощник · Кабинет (+Админ). Избранное/Отклики = чипы внутри Ленты (`Feed` mode all/favorites/applied); Поиски(Subscriptions)/Новости — подвкладки Кабинета; Способы входа+Уведомления слиты в «Настройки». **Новичок (пустой `specialty`) → компонент `Setup`** (пошагово имя→**рассказ о себе**→специальность→город→формат/зп; шаг «о себе» = текст/голос → `/ai/resume` предзаполняет поля, юзер правит), лента закрыта до specialty; gate в `Shell` по `me.specialty`. Имя в шапке = БД (`auth_me` first_name-first, D35), не кука. **UX-проход по аудиту 2026-06-26 (D40, kodoceh-web `8a2f301`; 134 находки, выбран «полный проход»; только apps/vakhunter, общий preset НЕ тронут):** верхних вкладок 4 — Лента·Помощник·**Кабинет**·[Админ] (Кабинет стал ВИДИМОЙ вкладкой, был спрятан за аватаром/недостижим на мобиле; аватар = дубль-шорткат). Подтабы Кабинета → **подчёркивание** (не вторые pill): Профиль·**Оповещения**(ex-«Поиски»)·**Тариф**(ex-«Подписка»)·Друзья·Новости·Настройки. **«Выйти» одним кликом** у бейджа тарифа. Контраст: токен `--accent-text` (тёмная бирюза light/светлая dark) для всего бирюзового ТЕКСТА (фон кнопок остаётся `#1F97A6`) + затемнён `--muted/--muted-soft` + global `:focus-visible`; единый бренд-цвет (зелёный `#2E8C66` убран из vakhunter `globals.css`/PDF). Глитчи: фильтры единообразны (debounce q/город/зп), 401 на звезде, HotToast `dismissed` в `useRef`+`onPatch`, таймаут поллинга входа, `confirm` на удаление промокода/оповещения/бан, profile-save ловит ошибку. Drawer: Esc+`role=dialog`+aria. Мобайл: тост «Горячие» стартует свёрнутой пилюлей, тап-зоны крупнее. Словарь: «алерты»→«уведомления», «Откликнулся?»→«Я откликнулся ✓», «Открыть на источнике»→«Откликнуться на сайте», «По навыкам»→«Мне подходят», человеческие ошибки промокода/входа. Лендинг: «отклик в один тап»→правда. Осознанно НЕ сделано: нижний таб-бар мобайла (взяты верхние вкладки), полный focus-trap, сплошная токенизация хексов/радиусов, сырой формат зарплаты (данные парсера). Синхрон терминов бота — отдельным шагом. **Помощник: карточка «Резюме»** — `/api/ai/resume_build` (сильные/слабые + сборка) → PDF (print-окно). **Голос — `@cbr/ui VoiceInput`** (браузерный Web Speech, ноль серверной нагрузки, D36; в полях гайда/резюме/ассистента); серверного STT в проектах НЕТ. **Лента (ред. 2026-06-26):** `VacancyRow` (карточки 2 колонки, ⭐+«Подробнее») + `VacancyDrawer` (slide-in справа, на мобиле fullscreen) со всеми действиями+ИИ; фильтры свёрнуты под кнопку «Фильтры»; `SavedList` тоже на rows+drawer. **«Выйти»** — Кабинет→Настройки (`/auth/logout`). **Релевантность ленты (2026-06-26, D37):** `/api/vacancies` персонализируется по профилю движком бота `matches_vacancy_relevance` + СКОРИНГ — хит термина профиля обязан быть в заголовке/навыках (описание-онли = шум, не в счёт; иначе общий токен «прием» цеплял чужих). **Без списка профессий** (открыто: незнакомая спец-ть всплывает по своим терминам); `scope=all` отключает, `empty_personalized`→«показать все». **`/api/hot`** — релевантные с зарплатой строго выше `desired_salary`, top-8, `above_by`; фронт `HotToast` (slide-in снизу-справа, «⚡ ЗАРПЛАТА ВЫШЕ ВАШЕЙ»+«↗+N₽», клик→drawer, монтаж в Shell). **Фикс `database/db.py` (общий с ботом):** нормализатор зарплаты терял «50 000» (разделитель тысяч ломал `\d+`)→схлопываем пробел/NBSP/точку; бэкфилл сделан. Проверка 8 профессий: рабочие/медики/риелторы 90-100%, бухгалтер 65% (неоднозначность «1С», не трогаем). Сидинг ~390 реальных вакансий через trudvsem. **Обучаемость (2026-06-26, D38):** (1) **самосев** — при тонкой выдаче (<8) фоном `search_profile_sources`(trudvsem+hh+avito)→`add_vacancy`, дебаунс 6ч/профессию (таблица `seed_demand`), `seeding=true`→фронт плашка+перезапрос 12с (проверено: «стропальщик» 0→20 за ~14с); (2) **skills_focus=1** (тумблер «По навыкам», localStorage) — буквальные токены навыка (`token_set`, НЕ build_profile_terms — иначе раздувается домен) в title/skills/desc, сужает (python+k8s 20→4); (3) **обучение** — веб-действия каждые 10 шт зовут `rebuild_user_keyword_preferences` + learned-нудж в скоринге (как у бота, раньше шли мимо). **Зарплата «выше вашей» (D39):** `/api/hot` сравнивает желаемую с ГАРАНТИРОВАННЫМ полом (`_salary_floor` из строки): «от 140k»→140k, диапазон «140–250»→140k (не 250!), фикс→сумма, «до Y»→пола нет→не в горячих. Общий нормализатор НЕ трогаем. **Скоринг-гейт в боте (D39):** `filter_vacancies(require_title_skills=True)` только в `search_service.filter_results_for_profile` (интерактивный поиск); подписки/дайджесты/алерты НЕ трогаем (recall > precision). Бот e2e (skill vakhunter-e2e) 6/6. **Фильтры ленты (проверено 12/12 + город):** `q/source/employment_type/salary_min(мягкий)/with_salary/kind(order|job через is_freelance_source)/skills_focus/city/scope`; **город** `?city=` через `matches_vacancy_location` (пусто→все, удалёнка проходит, синонимы/регионы; поле в UI предзаполнено из профиля) — раньше города в ленте НЕ было вовсе (D40-ish); **пустой фильтр = все** (каждый — `if q.get()` в WHERE). gorodrabot починен (путь принимает 1 токен → берём первое значимое слово).
- **Автеро** `apps/avtero/app/app/page.tsx` — иконка-человечек → `Cabinet`; `/admin` отдельно. **Вход обязателен** (анону нет CTA/гаража). **Гараж (2026-06-26):** в `Wizard` выпадающий «Выбрать авто из гаража» (localStorage `pk_garage`, до 6) → подставляет VIN.
- **Гаечкин** `apps/gaechkin/app/cabinet/page.tsx` — sidebar **5 пунктов (2026-06-26, D34): Дашборд/Записи/Клиенты/Выручка + «Настройки сервиса»** (подвкладки Услуги/Мастера/Витрина); `/admin` отдельно.

**Админки — доступ не-админам ЗАКРЫТ (серверный гейт 403 + UI), проверено Playwright:**
- **ВакХантер** (aiohttp `web/webapp.py`): каждый `/api/admin/*` за `_is_admin(_user(request))`, id из `bot.config.admin_ids`. Drill-down: `GET /api/admin/users/{uid}/activity`.
- **Автеро** (FastAPI `app/main.py`): блок `/api/admin/*` за `_is_pk_admin` (tg_id ∈ env `BOT_ADMIN_USER_IDS`=1005954706); overview/users/users/{id}/history/feedback.
- **Гаечкин** (FastAPI `gaechkin/api/cabinet.py`, `cab_admin_router` prefix `/api/admin`): за `_is_admin`, id из `settings.admin_tg_ids`={1005954706, 8688355181}.

**Отзывы (footer-форма, контакт обязателен):** ВакХантер `web_feedback`, Автеро `web_feedback` (lazy), Гаечкин `gk_web_feedback` (lazy `CREATE TABLE IF NOT EXISTS`, Postgres, мимо alembic — см. [[obsidian_documentation_rule]] Decisions D25). Все видны в админ-вкладке «Обратная связь».

**Связывание учёток (2026-06-26):** ВакХантер — полное слияние (канон=Telegram, D21). **Автеро** — кабинет «Способы входа» привязывает почту/Яндекс/Mail/Telegram к текущему аккаунту; `accounts.merge_accounts`/`resolve_link` сливают при коллизии (канон=текущая сессия, квоту не суммируем, необратимо); TG-привязка из веба = deep-link `link_<token>` + бот `_handle_web_link` → `/api/auth/telegram/link/confirm`. Гаечкин — только привязка незанятых способов (owner-merge НЕ делаем, риск tenant).

**Оплата ВакХантера (важно):** возврат на `return_url=?paid=1` ≠ факт оплаты. Грант тарифа ТОЛЬКО вебхуком Platega при `CONFIRMED` (`bot/main_ptb.py` `platega_webhook`, verify_callback fail-closed). Фронт сверяет `GET /api/payment/last` и не показывает «оплата получена» без `status=succeeded`. См. Decisions D27.

**TG-дубль отзывов:** все 3 продукта (`POST /api/feedback` → `_notify_*_admins` бот-токеном+прокси, best-effort) + отзыв в админ-вкладке.

**Уведомления о новых веб-юзерах в TG (2026-06-26):** тем же каналом, что дубль отзывов (`_notify_*_admins`, бот-токен+прокси). ВакХантер — `_notify_new_user` в `_create_web_user`; Автеро — `accounts.identity_exists()` перед `get_or_create_for_identity` в 3 точках (Telegram poll/OAuth/email), **resolve_link не считается новым**; Гаечкин — `web_seen`+`mark_web_seen()` в `cab_email.py`, сигнал = первый веб-вход владельца. Доставка всех 3 проверена живьём (см. [[obsidian_documentation_rule]] D30).

**Учёт посещений (2026-06-26):** единый стор `~/Services/shared/visits.db` (`visits(site,day,path,vhash,ts)`; vhash=sha256(ip|ua|day|site), без сырого IP). `@cbr/ui VisitBeacon` в 4 layout → `navigator.sendBeacon` (text/plain, без CORS) на `https://vakhunter.codoceh.ru/api/vh/visit` (rewrite `/api/vh/*`→backend; только прод). Коллектор `POST /api/visit` (204), дашборд `GET /api/admin/visits` (за `_is_admin`) → под-вкладка «Посещения» ТОЛЬКО в админке ВакХантера (все 4 сайта; pk/gk-зеркала позже). D29.

**SMTP (2026-06-26):** релей УЖЕ есть — туннель `local.codoceh-smtp-tunnel` (mini `127.0.0.1:2526`→VPS Postfix `:25`, STARTTLS). Гаечкин по дефолту на него (`GAECHKIN_SMTP_HOST=127.0.0.1:2526`). Автеро (`AVTERO_SMTP_*`, дефолт-порт 2525!) и ВакХантер — НЕ перенаправлены. Гейт = деливерабилити Gmail (SPF/DKIM/PTR), подтверждает юзер. D32.

**OAuth — БОЕВОЙ (2026-06-26):** Яндекс + Mail.ru подключены на всех 3 продуктах (6 приложений). Креды: ВакХантер — `~/Services/VakHunter/.env` (`VAKHUNTER_OAUTH_{YANDEX,MAILRU}_CLIENT_ID/_SECRET`); Автеро/Гаечкин — launchd plist EnvironmentVariables (`AVTERO_OAUTH_*` / `GAECHKIN_OAUTH_*`). Кнопки env-gated (фронт читает `/api/auth/providers`→`configured_providers()`, рантайм, без пересборки). Redirect-URI: `https://<домен>/api/<vh|pk|gk>/auth/oauth/<yandex|mailru>/callback` (полный гайд: Obsidian `OAuth-setup.md`). **VK НЕ реализован** (email у VK в ответе токена, не в userinfo — отдельная задача). Edge-хардненг отложен (нет root на VPS, только tunnel@) D33.

**Минт тест-сессий** (для проверки): ВакХантер `web.webapp.make_session(uid,name)`→cookie `vh_session` (setup_users.py: FULL_UID=-77001); Автеро `app.webauth.make_session({'tg_id':..})`→`pk_session`; Гаечкин `gaechkin.api.cab_session.make_session({'tg_id':..})`→`gk_session`. **Гаечкин тест-сервис «Тестовый автосервис» (tenant id 1) owner_tg_id=1005954706** (НЕ 8688355181 — у того нет tenant → кабинет «Нет автосервиса»); tenants по `Tenant.owner_tg_id`, БД через `gaechkin.db.base.get_session_factory()()`.

**UX-проходы по живой обратной связи (2026-06-26, D41/D42/D43; метод: синк→Playwright overflow-детектор `scratchpad/audit/{overflow_probe,pk_probe,gk_probe}.py`→фан-аут аудит-агента→фикс→живой прогон→сборка→деплой→доки):**
- **ВакХантер** (`979263a`, бот `cace6ed`, D41): «Открыть ленту»→«Открыть приложение», таб «Лента»→«Вакансии» (словарь, синхрон с ботом); **мобайл-overflow фикс** = базовый `grid-cols-1` (карточки раздувались до max-content и обрезались); **тип занятости в оповещениях** (канон-токены, паритет с ботом, scheduler.py honors employment+work_format); быстрые кнопки на карточке Откликнуться·Открыть·Скрыть.
- **Автеро** (`32ba269`, D42): сторож зависаний SSE-подбора (Отмена + 75с тишина→ошибка с сохранением формы); честный копирайт (убрано «наличие в РФ» — данных нет); чипы-тогглы (был дубль); OEM-копирование «Скопировать»; break-words на именах деталей; consent-подсказка+OAuth href.
- **Гаечкин** (`08db284`, D43): **мобильная нижняя таб-панель** кабинета (sidebar вываливался над контентом; sidebar теперь `hidden lg:block`); словарь Дашборд→Сводка; consent-подсказка+OAuth href; QR убран из обещаний витрины.
- Общие инварианты всех трёх: базовый `grid-cols-1` (см. [[kodoceh_web_build_gotcha]]), consent-подсказка при пустом чекбоксе + OAuth `<a>` всегда с `href`, честный копирайт (не обещать чего нет на экране).

Связано: [[avtero_access_gate]], [[kodoceh_web_build_gotcha]], [[kodoceh_web_overview]], [[vakhunter_overview]].
