Commit graph

48 commits

Author SHA1 Message Date
4b89138db5 fix(ops): канарейка смоука сработала на мерже PR-D3 — обновляю ожидания вместе с ней
После мержа платёжного роутера (#3231) perimeter-smoke стал красным на проде:

  FAIL: trade-in payments/notify — 401 anonymous (rbac закрыт до PR-D3)
        gendsgn.ru/.../payments/notify -> got '405', expected 401

Это НЕ поломка, а срабатывание канарейки, которую сам скрипт и обещал:
«осознанно станет красной, когда PR-D3 явно откроет эти пути — тогда ожидания
здесь надо обновить вместе с ними». Я смержил PR-D3 и ожидание не обновил.

Что изменилось по существу: /payments/notify теперь в rbac._PUBLIC_PATHS —
вебхук Т-Банка не умеет присылать X-Authenticated-User, аутентификация там
подписью тела. /payments/checkout НЕ открывался и остаётся под 401.

Проверка переведена с GET на POST: GET по POST-only маршруту даёт 405 и не
говорит ничего ни про rbac, ни про флаг. POST с пустым телом проходит rbac
(иначе 401) и упирается в выключенный контур — 503.

Заодно закрыты три дыры в покрытии смоука, все замерены на проде:
· /stats и /showcase — ручки, из которых лэндинг берёт числа. При их отказе
  страница по построению рендерится БЕЗ блоков (честная деградация), то есть
  тихо. Ничем другим это не ловится.
· /estimate и /estimate/read — обязаны быть неотличимы от несуществующего
  пути при выключенном флаге. До #3233 отвечали 422 с перечнем своих полей.

Путь длинный (/trade-in/api/public/...) намеренно: короткого на публичном
домене нет, а по нему ходит фронт — проба обязана идти рабочим трактом.

Следующая канарейка отмечена в комментарии: при включении PAYMENTS_ENABLED
503 станет отказом по подписи, и ожидание надо будет обновить вместе с флагом.

Прогон на проде: ALL CHECKS PASSED.
2026-08-29 20:49:49 +05:00
bot-backend
4b019ebd67 chore(claude): retire autonomous bot-pipeline
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Ролевые окна (claude --strict-mcp-config --mcp-config .claude/mcp/<role>.json)
не используются — отказ по стоимости: слишком много токенов на простых задачах.

Удалено (25 файлов):
- scripts/: start-bot, start-{analyst,backend,frontend,qa,reviewer}, setup-bot-env
- .claude/mcp/: 5 per-role MCP-конфигов
- .claude/commands/: 6 work-as-*
- .claude/agents/: 6 auto-* + _autonomous_pickup

Ссылки в выживших файлах поправлены: CLAUDE.md, README.md, git-pr.md,
deep-review-phases/phase-5-verdict.md, deep-code-reviewer.md,
cleanup-stale-claims.sh. Self-extending guard сохранён — из него убраны
только имена удалённых файлов, сам запрет на self-merge правил пайплайна цел.

Универсальные воркеры не тронуты: backend-engineer, frontend-engineer,
database-expert, devops-engineer, tech-analyst, code-reviewer, qa-tester,
deep-code-reviewer.
2026-08-28 22:27:00 +03:00
bot-backend
031b4559b9 fix(mera/perimeter): www-формы доменов МЕРА обрывали TLS вместо редиректа
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
A-записи www.meraocenka.ru / www.merahome.ru / www.meraotsenka.ru заведены и
указывают на прод, но site-блоков под них в Caddy не было. Caddy матчит строго
по имени хоста и на неизвестное имя сертификата не выпускает, поэтому клиент,
набравший привычное «www.», получал обрыв рукопожатия (TLS alert 80) — для
браузера это «сайт не открывается». В логах доступа при этом ни строки: до
HTTP-слоя запрос не доходил, так что молчали и метрики.

www.gendsgn.ru такой блок имел с самого начала и потому работал — здесь ровно
тот же приём, три отдельных блока с 301 на канонический meraocenka.ru.

Найдено сквозным аудитом периметра 27.08. Проверено: три www-имени резолвятся
в 188.124.37.140, curl до правки возвращал пустой код (000) на всех трёх,
www.gendsgn.ru — 301.

Смоук периметра дополнен проверкой 5b: отсутствие блока проявляется НЕ как
404, а как пустой код ответа, и обычная проверка «спутники отдают 301» такой
регресс не ловила.
2026-08-27 14:17:15 +03:00
bot-backend
de56b8ae01 feat(ops): сторож расхождения «прод ↔ main» — молчаливый недоехавший деплой становится видимым (#3029)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m8s
CI / backend-tests (pull_request) Successful in 17m30s
27.08 прод сутки жил на позавчерашнем коммите, и это нашлось только руками.
Замена IP сервера (#3110) осиротила секрет DEPLOY_HOST: deploy.yml падал на
`dial tcp ***:***: i/o timeout`, при этом CI оставался зелёным, PR
продолжали мержиться, а deploy-infra.yml исправно обновлял Beget и создавал
впечатление, что всё в порядке.

Проверок, которые спрашивают не «прошёл ли прогон», а «доехал ли код», не
было ни одной. Этот сторож — ровно такая: ежечасно читает HEAD с прод-хоста
и сверяет с tip main.

Три исхода вместо двух. «Хост не ответил» (2) отделён от «на хосте не тот
код» (1) — это разные аварии с разной первой командой в разборе, и сегодня
погорели именно на их смешении: недоступность выглядела как обычный красный
прогон. Льготный период 30 минут гасит ложную тревогу на деплое, который
ещё в полёте: ежечасный сторож неизбежно попадёт в окно между мержем и
концом выката, а сторож, которого научились игнорировать, хуже отсутствующего.

Логика вынесена в scripts/check-deploy-drift.sh и не ходит по сети — SSH
живёт в workflow, где секреты. Благодаря этому тест ИСПОЛНЯЕТ настоящий
скрипт, а не пересказывает его: ошибка в самом bash видна только при запуске.

Read-only: один ssh и git rev-parse, ничего не деплоит.
2026-08-27 12:35:43 +03:00
bot-backend
0e4beb8457 chore(hooks): точечный allowlist на три ключа метрик вместо снятия гарда
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Владельцу понадобились креды Grafana (basic_auth Caddy + внутренний вход), а
они лежат в backend/.env.runtime, который check-secret-read.py закрывает
целиком. Снимать гард нельзя: в том же файле prod DB-пароли и токены
Forgejo/GlitchTip.

Вместо этого — ALLOWED_KEYS из трёх ключей (METRICS_UI_PASSWORD,
GRAFANA_ADMIN_USER, GRAFANA_ADMIN_PASSWORD) и разрешение ровно на anchored-греп
по ним. Условия намеренно жёсткие, чтобы «прочитать один ключ» нельзя было
развернуть в «выгрузить файл»: блокируются инверсия (-v / --invert-match),
пайпы, цепочки ; && ||, подстановки $(...) и обратные кавычки, редиректы.

Тест на 10 кейсов: разрешён только anchored-греп по ключу из списка; отбиты
инверсия, пайп, цепочка, редирект, подстановка, чужой ключ, греп без якоря ^ и
обычный cat. Проверяется и функция, и хук end-to-end через настоящий
stdin-payload.

Гард в деле: он же отбил эту самую команду коммита, когда текст сообщения
содержал имя закрытого файла рядом с read-verb.
2026-08-26 13:09:37 +03:00
bot-backend
beafe6925b fix(observability): агент не падает из-за переменной чужой роли (#3078)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m55s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Successful in 17m25s
Джоба agent-apps упала целиком:

    error while interpolating services.postgres-exporter-infra.environment.
    DATA_SOURCE_NAME: required variable INFRA_EXPORTER_DSN is missing a value

INFRA_EXPORTER_DSN нужен экспортеру с profiles: ["infra"], который на
продуктовом хосте не поднимается вовсе. Но compose интерполирует ВЕСЬ файл
до фильтрации по профилям, поэтому `${VAR:?}` роняет команду из-за чужой
переменной. Вместе с агентом не поднялись alloy, node-exporter и cadvisor,
которым никакой DSN не нужен. Симметрично упал бы и инфраструктурный агент -
на двух продуктовых переменных.

Второй дефект в той же цепочке: GENDESIGN_EXPORTER_DSN тоже отсутствовал.
setup-metrics-exporter-dsn.sh читает только runtime-файл окружения бэкенда, а
DATABASE_URL и TRADEIN_DATABASE_URL живут в основном. Значит подстановка
всегда была пустой, add_key печатал "нечем заполнить" и выходил с кодом 0 -
мягкий пропуск встречался с жёстким требованием compose.

Стало:
- compose: `:-` вместо `:?` у трёх DSN. Интерполяция больше не может упасть.
- deploy-metrics.yml: профиль экспортеров включается, только если нужные ЭТОЙ
  роли DSN заполнены; иначе ::warning и агент поднимается без экспортера.
  Громкость не убрана, а перенесена туда, где роль известна. Тот же приём, что
  уже применён к Alertmanager в джобе server.
- setup-metrics-exporter-dsn.sh: читает оба файла окружения (базовый, затем
  runtime - он перекрывает). Пишет по-прежнему только в runtime, лишних копий
  пароля не заводит.

Почему `:-` не ослабление: пустой DATA_SOURCE_NAME поднял бы экспортер,
который молча не отдаёт метрик, - ровно тот тихий отказ, ради которого весь
стек и заводится. Поэтому пустой DSN теперь означает "профиль не включаем",
а не "поднимаем пустым".

Известное следствие, отмеченное в коде: INFRA_EXPORTER_DSN не собирает никто -
скрипт знает только про GENDESIGN_/TRADEIN_ и работает на продуктовом хосте.
Пока это так, инфраструктурный агент будет честно предупреждать, что метрик
Postgres инфры нет, вместо того чтобы падать целиком.

Тесты (3) структурные, проверяют оба конца инварианта: обязательности не
вернулись в compose; каждая DSN-переменная проверяется в деплое; профили не
захардкожены. Фальсификация: на исходных файлах краснеют все три.
2026-08-26 12:45:31 +03:00
1da2eee142 Merge pull request 'fix(observability): стек не поднимался — привилегированную роль спрашиваем у контейнера, а не угадываем (#3078)' (#3105) from fix/3078-metrics-role-discovery into main
Some checks failed
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Metrics / server (push) Failing after 26s
Deploy Metrics / agent-apps (push) Has been skipped
Deploy Metrics / agent-infra (push) Has been skipped
Deploy / build-backend (push) Successful in 37s
Deploy / build-worker (push) Successful in 40s
Deploy / deploy (push) Successful in 1m4s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 10s
Reviewed-on: #3105
2026-08-26 09:27:29 +00:00
bot-backend
33bc7e4acf fix(observability): привилегированную роль спрашиваем у контейнера, а не угадываем (#3078)
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m2s
CI / backend-tests (pull_request) Successful in 17m21s
Стек наблюдаемости не поднялся ни на одном хосте после мержа #3099: джоба
server упала, agent-apps и agent-infra пропустились как зависимые.

Причина (задача 23657, 26.08 08:55):

    err:   ОШИБКА: не нашёл роль с правом CREATE ROLE в gendesign-infra-postgres

setup-metrics-grafana-role.sh искал привилегированную роль перебором трёх
имён - glitchtip, forgejo, postgres. Ни одно не совпадает ни с одним реальным
кластером проекта: infra-postgres -> infra, gendesign-postgres-1 -> gendesign,
tradein-postgres -> tradein. Комментарий над перебором сам предупреждал, что
"угадывать postgres неверно", и дальше шло угадывание.

Замер на живом контейнере 26.08 (read-only, ничего не создавалось):

    POSTGRES_USER изнутри контейнера: infra
    glitchtip - отказ, forgejo - отказ, postgres - отказ, infra - 1

Стало: имя берём из POSTGRES_USER самого контейнера - это та переменная,
которой роль и создана при initdb, то есть источник истины. Прежний список
оставлен ПОСЛЕ него запасным путём для кластера не из образа postgres.

Попутно - глоб в paths деплоя. Воркфлоу запускает ТРИ setup-скрипта, а в
триггере стоял только setup-metrics-secrets.sh: правка двух остальных не
заводила выкат, и на хосте молча оставалась старая версия. Тот же класс, что
#2203 закрыл глобом ops/*.sh. Добавлен тест, который сверяет запускаемые
скрипты с шаблонами paths - на исходном воркфлоу он краснеет, указывая на
setup-metrics-exporter-dsn.sh.

Тесты (6) исполняют РЕАЛЬНЫЙ скрипт с подставным docker и проверяют
фактический выбор роли, а не наличие правильных слов в комментарии.
Фальсификация: на исходном коде краснеют 3 из 5 ролевых тестов; проходят
только те два, что фиксируют сохранённое поведение (запасной перебор и
громкая ошибка при отсутствии привилегий). tests/ops целиком - 28 passed.
2026-08-26 12:08:37 +03:00
bot-backend
46b42c80db ci(caddy): гард — import обязан быть покрыт volume-маунтом
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m10s
CI / openapi-codegen-check (pull_request) Successful in 2m18s
CI / backend-tests (pull_request) Successful in 17m41s
Follow-up к #3103. Прод лёг на ~30 минут потому, что PR завёл
`import ../metrics-*.caddy.snippet` в caddy/sites/infra.caddy, но не добавил
bind-монты этих файлов в docker-compose.prod.yml.

Соседний гард `caddy validate` эту дыру не ловит принципиально: он копирует
каталог caddy/ целиком (`docker cp caddy ...`), а на проде смонтированы только
отдельные файлы плюс два каталога. Расхождение между «что лежит в репозитории»
и «что реально видит контейнер» видно только если сверять с маунтами.

check-caddy-snippet-mounts.py разбирает bind-монты сервиса caddy:, резолвит
каждый `import` в Caddyfile / caddy/sites/*.caddy / caddy/*.caddy.snippet
относительно КОНТЕЙНЕРНОГО пути импортирующего файла и падает, если цель не
покрыта ни одним маунтом. Именованные сниппеты `(name) { }` пропускаются,
для glob/placeholder-импортов (`caddy/sites/{$CADDY_SITES:*}.caddy`)
проверяется каталог. --selftest воспроизводит ровно баг #3102.
2026-08-26 12:07:29 +03:00
bot-backend
309d273f3f feat(observability): стек метрик и логов — Prometheus, Loki, Grafana, агенты на обоих хостах
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Метрик в проекте не было ни одной: ни экспортеров, ни /metrics в бэкендах,
единственный канал наблюдения — journald, единственный сигнал об аварии —
исключение в GlitchTip. Из-за этого целый класс отказов невидим в принципе:
задача рапортует done, строк ноль, исключения нет. Так протухли данные на семь
месяцев (#2998), 34 дня был мёртв house_imv_backfill (#2698), 8 суток писал ноль
newbuilding_enrich (#2767), 91 день копилось раздутие listings (#2992).

Grafana не заменяет GlitchTip: ошибки остаются там. Grafana OSS не принимает
Sentry DSN ни одним компонентом, а скрубберы в before_send — требование 152-ФЗ.
Здесь появляется другой класс данных: ряды и алерты по трендам.

Наблюдатель поставлен у ДРУГОГО провайдера, чем наблюдаемое: серверная сторона
на Beget, рядом с GlitchTip. Если ляжет Poincare, мониторинг должен об этом
сказать, а не лечь вместе с ним.

Транспорт push, а не pull: агент на Poincare шлёт remote_write и логи исходящим
HTTPS, поэтому там не открывается ни одного входящего порта сверх 22/80/443.
При обрыве канала Alloy копит в WAL и досылает — pull-скрейп в той же ситуации
терял бы точки именно в аварии, ради которой мониторинг и нужен.

Два контура доступа с разными учётками. Пароль приёмника по построению лежит
открытым на продуктовом хосте, значит его компрометация неизбежна вместе с
хостом; будь это учётка витрины, утёк бы и доступ к дашбордам.

GlitchTip читается прямым SQL, а не Sentry-плагином: у плагина на 6.1.6
stats_v2 отдаёт 500 (баг GlitchTip #381), Events/Discover — 404 (#416), а в
grafana/sentry-datasource слово glitchtip не встречается ни разу. Схема сверена
на живой базе: колонка времени называется timestamp, а не received, и отдельной
таблицы IssueIndex не существует — агрегаты лежат на самой issue_events_issue.

Алерты за профилем alerts: канал доставки — открытый вопрос #3078, и стек не
должен на нём стоять. Деплой предупреждает, что уведомлять пока некому.

Каждая настройка, способная отказать молча, закрыта явно: ретенция Prometheus
задана и по времени и по размеру, retention_enabled у компактора Loki (без него
retention_period не работает вовсе), путь к журналу и запуск Alloy от root
(иначе агент читает ноль записей без ошибки), проверка Caddy до перезагрузки
(на этом хосте тот же Caddy держит git, errors и obsidian).

Refs #3078
2026-08-26 10:45:54 +03:00
f2945b7157 fix(ci): деплой падает громко, если :latest отстаёт от головы по компоненту (#2950) (#3023)
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 15s
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 57s
Deploy / build-backend (push) Successful in 1m2s
Deploy / build-frontend (push) Successful in 1m8s
Deploy / build-worker (push) Successful in 1m9s
Deploy / deploy (push) Successful in 1m58s
Deploy / deploy-status (push) Successful in 3s
Deploy Trade-In / build-frontend (push) Successful in 3m14s
Deploy / perimeter-smoke (push) Successful in 14s
Deploy Trade-In / test (push) Successful in 4m12s
Deploy Trade-In / build-backend (push) Successful in 33s
Deploy Trade-In / deploy (push) Successful in 2m16s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
2026-08-21 12:19:07 +00:00
e86f0782da ci: смоук периметра МЕРЫ запускается сразу после деплоя (#2917) (#2922)
All checks were successful
Deploy / changes (push) Successful in 10s
perimeter-smoke-mera / smoke (push) Successful in 12s
Deploy Trade-In / changes (push) Successful in 14s
Deploy / build-frontend (push) Successful in 51s
Deploy / build-backend (push) Successful in 53s
Deploy / build-worker (push) Successful in 53s
Deploy Trade-In / build-browser (push) Successful in 40s
Deploy / deploy (push) Successful in 1m33s
Deploy / deploy-status (push) Successful in 2s
Deploy / perimeter-smoke (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Successful in 2m50s
Deploy Trade-In / test (push) Successful in 4m8s
Deploy Trade-In / build-backend (push) Successful in 35s
Deploy Trade-In / deploy (push) Successful in 1m39s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
2026-08-19 08:13:49 +00:00
bot-backend
b1fb7bb055 Merge remote-tracking branch 'forgejo/main' into feat/mera-public-api
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Successful in 46s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m7s
CI Trade-In / backend-tests (pull_request) Successful in 4m45s
# Conflicts:
#	tradein-mvp/backend/app/core/rbac.py
2026-08-16 09:28:41 +03:00
bot-backend
7424c283d5 feat(mera/b2c): отдельный экран оценки на meraocenka.ru и короткие адреса
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 44s
CI Trade-In / frontend-checks (pull_request) Successful in 1m2s
CI Trade-In / backend-tests (pull_request) Successful in 4m41s
## Экран проверки — /estimate

Проверка квартиры вынесена на собственный адрес: там у автокомплита есть
место под список подсказок, а у результата — место рядом с полями. Форма в
герое лэндинга осталась входной точкой и уводит сюда, донося набранное через
sessionStorage (НЕ через query — адрес в URL попал бы в access-лог Caddy рядом
с IP посетителя, а мы на той же странице обещаем ничего не хранить).

Показывает живую пробу покрытия: сколько похожих квартир продаётся рядом и
сколько в среднем висят их объявления. Ни одной рублёвой цифры — цену продаёт
платный шаг. Тексты вердикта вынесены чистой функцией (coverage-copy.ts) и
покрыты тестами: подпись под возрастом обязана говорить «объявление», а не
«продаётся» (выборка цензурирована), при неизвестном возрасте плитки нет
вообще, а пустая когорта объясняется как факт о рынке с подсказкой, что
поменять, — директива «никогда не блокировать вывод».

## Короткие адреса

Человек больше не видит /trade-in/mera-public/... — только /, /estimate,
/oferta, /refund, /privacy. Длинные адреса отдают 301 на короткие: у страницы
один канонический адрес, старые ссылки живы.

Цена решения: ссылки работают только на meraocenka.ru (короткие пути раздаёт
этот хост). Открывать лэндинг для проверки нужно там же, а не с gendsgn.ru.

Ссылки эмитятся обычным <a> (PublicLink) — next/link подставляет basePath, и
href="/estimate" уехал бы на несуществующий /trade-in/estimate.

## Три дефекта, найденных на живом сайте

1. Палитра v3 никуда не доезжала. b2c-tokens.ts не импортировал НИКТО, ни одна
   --b2c-* переменная не объявлялась, каскад молча пропускал такие декларации —
   лэндинг отдавал 200 бесцветным. Добавлен мост b2cVars, гейтом стал тест:
   каждая использованная в CSS переменная обязана быть объявлена.
2. Голый /trade-in/mera-public падал в 404 — матчер был со слэшем и звёздочкой.
   Ровно туда вела «Главная» в подвале.
3. «Для бизнеса» вела на «/» — то есть на сам лэндинг. Теперь абсолютный адрес
   B2B-контура. Пункты «Проверьте себя» и «Продажа под ключ» вели на якоря,
   которых нет нигде: приведены к виду «Статьи» — видны, но не кликабельны.

## Периметр и приватность

Подсказки переведены на POST: access-лог публичного домена пишет URI целиком,
то есть GET с ?q= сохранял бы адрес квартиры в файл. Тело в лог не попадает.
Метод запинен тестом — это часть обещания, а не стиль.

П. 5.4 политики ПДн переписан ВМЕСТЕ с кодом: прежний текст утверждал, что
адрес не покидает браузер, и это перестало быть правдой. Новый говорит точно —
передаётся, используется однократно, в базах не сохраняется. Последнее
проверено по коду: suggest() работает без кэша, проба — один SELECT, аудит
пишет строку только при наличии username. PUBLIC_ESTIMATE_ENABLED сузился до
платного шага (бесплатная проба не хранит ничего, платный расчёт хранит).

## Проверено

vitest 47 passed (9 файлов), tsc, next lint, next build, isolation guard
40 файлов, backend 75 passed, caddy validate = Valid configuration.

Мутации структурных гейтов:
  убрать --b2c-accent-text из b2cVars   → падает тест палитры
  убрать /estimate из @meraPages        → падает тест маршрутов
  добавить импорт next/link             → падает тест basePath
  откат                                 → 14 passed

Caddyfile добавлен в paths-filter фронтового лэйна: его читает тест маршрутов,
и без этой строки правка одного лишь Caddyfile не запускала бы ни один гейт.

Refs #2894, #2895
2026-08-16 09:27:04 +03:00
bot-backend
d046eaada2 feat(mera/b2c): публичный API МЕРЫ — /api/public/mera/{suggest,coverage}
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m49s
Первый шаг к отдельному B2C-интерфейсу оценки на meraocenka.ru: домен получает
собственную поверхность бэкенда вместо того, чтобы тянуть куски закрытого
контура.

## Отдельный префикс, а не проброс кусков /api/v1/*

На meraocenka.ru действует allowlist-by-default. Открыть там API можно было
двумя способами: перечислить нужные v1-пути поимённо — или завести префикс, под
которым по определению не лежит ничего закрытого. Выбран второй: при первом
одна опечатка в матчере (`/trade-in/api/*` вместо точного пути) открывает
наружу весь v1 — ~20 ручек, включая PDF расчётов, фотографии и админку. Цена
ошибки, а не удобство.

Добавить сюда приватную ручку теперь нужно СПЕЦИАЛЬНО — положив файл в
app/api/public/. Случайно нельзя.

## Ноль записей в БД

Обе ручки только читают: /coverage — один SELECT, /suggest — прокси
автокомплита. Это условие, при котором публичная форма работает ДО контура
согласия 152-ФЗ (#2895: сегодня адрес физлица попадает в trade_in_estimates
раньше согласия, а пути удаления в бэкенде нет). Платный расчёт, который писать
будет, открывается только вместе с ним.

## Делегирование, а не копии

Обе ручки вызывают те же функции, что обслуживают закрытый контур
(v1.geocode.suggest_addresses, v1.trade_in.coverage_probe). Разбор #2894
показал, чем кончается вторая копия когорты: проба отвечает «данные есть» там,
где платный расчёт видит ноль. Публичный ответ переиспользует
CoverageProbeResponse — на нём уже стоит гейт «ни одного price-подобного поля».

## Бюджеты

Общего 300/60с мало: /suggest через DaData-тир — платный внешний вызов, абуз
стоит денег. Свои per-IP окна: 40/мин на подсказки (человек с debounce'ом
тратит единицы на адрес), 15/мин на пробу.

## Проверено

11 тестов, из них структурные: набор ручек под /api/public проверяется на
РАВЕНСТВО (третья, добавленная без правки теста, роняет сборку) и сверяется с
rbac._PUBLIC_PATHS в обе стороны — чтобы не осталось открытого пути-призрака.
Рядом висит закрытый маршрут-двойник: без него «аноним получает 200» одинаково
зелёный и когда исключение точечное, и когда auth-гейт снят целиком.

Мутации:
  убрать пути из rbac._PUBLIC_PATHS → 7 failed / 4 passed
  снять бюджет с /coverage         → 2 failed / 9 passed
  откат                            → 11 passed

Плюс 72 passed на связке rbac + coverage + version, `caddy validate` = Valid
configuration, ruff чист.

Смоук периметра дополнен парой, которую нельзя разделять: публичные ручки
отвечают 200 анонимно И /trade-in/api/v1/* на этом домене по-прежнему 404.
Зелёная только первая проверка = API открыт целиком, а тест этого не заметил.

Refs #2894, #2895
2026-08-15 22:32:53 +03:00
bot-backend
a8fa7364ae merge(tradein/payments): влить main в feat/tradein-payments-perimeter-hardening
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m56s
Слияние main принесло собственные Sentry-скрубберы (redact_telegram_bot_token +
stabilize_retry_error_fingerprint) в app/main.py и app/scheduler_main.py — конфликт
разрешён композицией, а не выбором стороны: обработчик перед отправкой в GlitchTip
теперь прогоняет событие через всю цепочку в указанном порядке:
scrub_payment_request_body → scrub_pii_event → redact_telegram_bot_token →
stabilize_retry_error_fingerprint (main.py), и без redact_telegram_bot_token в
scheduler_main.py (тот процесс не держит TelegramClient) — оба канала,
before_send и before_send_transaction, используют один и тот же обработчик.

tests/test_sentry_scrub.py: тесты обеих сторон объединены без потерь — PR-D2
платёжный composed-тест (body-wipe + PII-scrub + token-redaction) и весь блок
RetryError fingerprint-стабилизации из main сосуществуют в одном файле.
2026-08-15 22:32:28 +03:00
bot-backend
c6243b166f feat(mera): оферта, политика возврата и утверждённая политика ПДн + реквизиты
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 56s
Юридический блокер публичного B2C-запуска (G6 в mera-b2c-paid-flow-decision.md:
`LEGAL_ENTITY == null` → нет реквизитов, нет продажи) и входное требование
модерации эквайера: оферты не было вообще, а privacy-страница в собственной
шапке писала, что она НЕ утверждённая политика по ст. 18.1 152-ФЗ.

Что сделано:
- content.ts: `LEGAL_ENTITY` заполнен (ООО «ПРОЕКТ ФЛЭТ», ИНН/КПП/ОГРН, адреса,
  директор, банковские реквизиты), добавлены `SUPPORT_EMAIL`,
  `SERVICE_PRICE_RUB`, пути и короткие публичные URL документов. Единый
  источник — подвал, оферта, возврат и ПДн рендерят эти поля, а не повторяют
  строки. ОГРН сверен с открытыми данными ЕГРЮЛ (в исходном сообщении владельца
  был с опечаткой …028028, верный …028281).
- Новые страницы /mera-public/oferta и /mera-public/refund — редакции владельца
  от 13.08.2026 дословно, плейсхолдер `[адрес электронной почты]` заменён на
  support@meraocenka.ru.
- privacy/page.tsx переписана на утверждённую редакцию с оператором и
  реквизитами приказа. Сохранены оба инварианта честности: раздел про страницу
  ввода адреса условен по `PUBLIC_ESTIMATE_ENABLED` (п. 5.4 — пока публичный
  расчёт выключен, адрес не покидает браузер), срок хранения оплаченного отчёта
  рендерится из `PAID_REPORT_RETENTION_MONTHS`, а не числом в тексте
  (test_paid_retention_text_consistency.py).
- Caddyfile: короткие адреса /oferta, /refund, /privacy → rewrite на поддерево
  лэндинга. Именно они напечатаны внутри документов и уйдут в заявку эквайеру.
  Пути перечислены поимённо — allowlist-by-default периметра не ослаблен.
- smoke-mera-perimeter.sh: три новые проверки на короткие адреса.

Публикация оферты НЕ включает приём оплаты: платёжного контура в коде нет,
`PUBLIC_ESTIMATE_ENABLED` по-прежнему false. Оферта публикуется раньше кнопки
намеренно — без неё эквайер не примет заявку.

Проверено локально: tsc, next lint, vitest (29), isolation guard, next build
(три страницы пререндерены), pytest test_paid_retention_text_consistency (3),
caddy validate + adapt (rewrite на месте), рендер всех трёх страниц через
next start — реквизиты, почта и цена на месте.
2026-08-13 20:10:38 +03:00
bot-backend
1f85ef7d4e fix(tradein/payments): тело нотификации не течёт в мониторинг и аудит, повторы банка не отбиваются лимитом
All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Successful in 3m50s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
PR-D2 платёжного контура МЕРЫ — закрывает утечки до открытия публичных путей
(PR-D3/D4), сам ничего не открывает: _PUBLIC_PATHS (rbac.py), Caddyfile,
roles.yaml, auth_session.py не тронуты.

- sentry_scrub.py: новая scrub_payment_request_body — вырезает
  event.request.data целиком для /api/v1/trade-in/payments/* (sentry_sdk 2.64
  кладёт полное тело запроса в request.data, send_default_pii=False это НЕ
  гейтит — тот флаг управляет только куками). Плюс расширен _PII_KEYS:
  customer_email/customer_phone/pan/expdate/cardid/rebillid/token/terminalkey.
- main.py, scheduler_main.py, tgbot_main.py (все 3 точки инициализации
  sentry_sdk.init в проекте) — тот же обработчик проведён в ОБА канала,
  before_send и before_send_transaction. Мотивирующий инцидент: на соседнем
  продукте вчера закрыли только error-канал, transaction остался без
  обработчика вообще.
- ratelimit.py: точный путь notify — свой щедрый SlidingWindowLimiter
  (3000/60с per-IP, идиома support.py) вместо общего лимитера, но НЕ полное
  отключение — backstop против шторма запросов остаётся, подпись проверяется
  уже после разбора тела (PR-D3). Только notify, не checkout (тот с сессией).
- request_audit.py: notify — в audit skip-набор (defense-in-depth: middleware
  внешний относительно rbac_guard и читает сырой X-Authenticated-User —
  спуфнутый заголовок иначе писал бы фальшивые события с атрибуцией admin).
- smoke-mera-perimeter.sh: негативные проверки-канарейки — notify/checkout
  сейчас закрыты 404 (meraocenka.ru, Caddy не проксирует) и 401
  (gendsgn.ru, rbac ещё не открыл) с обеих сторон периметра.

Тесты: scrub на произвольной глубине + payment-path body-wipe, AST-разбор
(не substring — комментарии в этих же файлах сами упоминают
before_send_transaction) на проводку обоих каналов во всех точках
инициализации, 400 запросов notify без единого 429 + контроль что общий
лимитер по-прежнему активен на других путях, notify вне user_events даже со
спуфнутым X-Authenticated-User: admin.
2026-08-07 16:17:59 +03:00
482deb4864 fix(migrations): закрепить lock_timeout для блокирующего DDL и ловить невалидные индексы (#2791)
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-frontend (push) Successful in 43s
Deploy Trade-In / build-browser (push) Successful in 43s
Deploy / build-frontend (push) Successful in 47s
Deploy / build-worker (push) Successful in 49s
Deploy / build-backend (push) Successful in 50s
Deploy / deploy (push) Successful in 1m12s
Deploy Trade-In / test (push) Successful in 3m25s
Deploy Trade-In / build-backend (push) Successful in 30s
Deploy Trade-In / deploy (push) Successful in 1m50s
2026-08-07 11:21:28 +00:00
d605f0b938 ci: гейт против публикации занятых на VPS портов в workflow (#2757) (#2759) 2026-08-06 21:40:05 +00:00
55254b43e4 feat(mera/b2c): публичный периметр на отдельном домене meraocenka.ru — этап 1 из 8 (#2545)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Successful in 30s
Deploy / build-backend (push) Successful in 32s
Deploy / build-worker (push) Successful in 32s
Deploy / deploy (push) Successful in 1m15s
Открывает публичный site-блок meraocenka.ru с allowlist-by-default: разрешены только корень лэндинга, его поддерево и _next/static, всё остальное 404. B2B-контур gendsgn.ru не затронут.

DNS A-записи для всех трёх доменов на 46.173.16.127 подтверждены до мержа.
2026-08-02 12:51:46 +00:00
0658399f01 fix(devops): cleanup-merged-worktrees.sh — 3 бага, найденные вживую на 69 забытых worktree (#2418) 2026-07-04 13:00:14 +00:00
b7d35cfee0 chore(claude): security hardening — dangerous-commands hook, settings в git, restricted DB, throttle (#2150) 2026-07-02 15:23:11 +00:00
845f699176 chore(claude-config): secret-read + psycopg2 hooks, tradein.md rule, tradein-mvp globs (#1975) 2026-06-27 10:03:19 +00:00
8a3bae0cda feat(analyze): per-category OSRM routing (foot vs driving) (#39 A3)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m46s
CI / backend-tests (pull_request) Successful in 11m55s
Walk-relevant POIs (school/shop/park/kindergarten/pharmacy/stops) now route
via a FOOT OSRM graph (osrm-walk service), car-relevant POIs (mall/hospital +
unknown) keep the DRIVING graph. Validation showed driving overstated
pedestrian-proximity distance — median 1.6-2.9x straight-line (#39).

- config: osrm_walk_local_url + osrm_walk_categories (frozenset, 9 walk cats)
- osrm_client_local: base_url override on get_road_distances_m (default unchanged)
- _apply_osrm_road_distances: split POIs by category, per-group OSRM call with
  independent graceful fallback (one server down -> its group keeps straight-line),
  in-place write-back by original index; never raises; flag-OFF byte-identical
- docker-compose: osrm-walk service (foot graph, internal, mem_limit 1.5g)
- build_osrm.sh: CAR=0 gate for foot-only refresh (doesn't touch live car graph)
- tests: per-category split, per-group fallback, asymmetric intra-group write-back

Still flag-gated (use_osrm_distances OFF) — enabling is a product decision.
Refs #39
2026-06-27 04:30:25 +05:00
4fe20a9cbd feat(devops): self-hosted OSRM routing engine for site-finder (#39) (#1929)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Successful in 31s
Deploy / build-backend (push) Successful in 33s
Deploy / build-worker (push) Successful in 46s
Deploy / deploy (push) Successful in 1m18s
2026-06-26 19:18:21 +00:00
86e9ea2937 fix(week-review): автофиксы код-ревью — 169 issue (label «week ревью 1»)
Многоагентный аудит + имплементация: один воркер на файл, точечные правки.
Верификация: py_compile (47/47 .py) + tsc --noEmit (0 ошибок). Unit-тесты
не прогонялись (окружение не поднято: rollup native dep / нет pytest-venv).

Полностью исправлено (169): #1336, #1337, #1339, #1340, #1341, #1342, #1343, #1345, #1346, #1348, #1349, #1350, #1351, #1354, #1356, #1358, #1359, #1360, #1362, #1364, #1365, #1366, #1367, #1368, #1369, #1370, #1371, #1372, #1373, #1374, #1375, #1376, #1377, #1378, #1379, #1380, #1381, #1382, #1384, #1385, #1386, #1387, #1388, #1389, #1390, #1391, #1392, #1394, #1395, #1396, #1397, #1399, #1400, #1401, #1402, #1403, #1404, #1408, #1409, #1410, #1411, #1412, #1413, #1414, #1415, #1416, #1417, #1418, #1420, #1423, #1425, #1426, #1427, #1428, #1429, #1430, #1431, #1432, #1433, #1434, #1435, #1437, #1438, #1439, #1440, #1441, #1442, #1443, #1444, #1445, #1446, #1447, #1448, #1449, #1450, #1451, #1452, #1453, #1454, #1455, #1456, #1457, #1458, #1459, #1460, #1461, #1462, #1463, #1464, #1465, #1466, #1467, #1468, #1469, #1471, #1472, #1473, #1474, #1476, #1478, #1479, #1481, #1482, #1483, #1484, #1485, #1487, #1488, #1489, #1490, #1491, #1492, #1493, #1494, #1495, #1496, #1497, #1499, #1500, #1501, #1502, #1504, #1505, #1506, #1507, #1510, #1514, #1515, #1516, #1517, #1518, #1519, #1521, #1522, #1523, #1524, #1525, #1526, #1527, #1528, #1529, #1531, #1532, #1533, #1534, #1535, #1536, #1537, #1538

Частично (9, in-file часть, остаток cross-file): #1361, #1419, #1422, #1424, #1470, #1475, #1477, #1480, #1498
Требуют cross-file (3, не тронуты): #1338, #1363, #1421
Пропущено (1): #1539

Не входило в партию: 22 needs-Leha issue (нужны решения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:21:11 +05:00
6883d14177 fix(ops): repair broken main-DB backup + harden auth scripts/docs (#71 #427 #429 #428)
Some checks failed
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
#71 (CRITICAL): backup.sh committed 100644 → git reset --hard on deploy
re-asserts non-exec mode → raw-path cron fails Permission denied (last good
dump 2026-05-27, no S3). Commit 100755 + chmod ops/*.sh in deploy.yml +
size sanity-check (never prune good dumps for a truncated one) + keep-N
retention + optional S3 (redacted /etc/default template). Modeled on the
working backup-tradein-db.sh.

#427: widen basic_auth username regex ^[a-z][a-z0-9_.-]{1,62}$ + literal-escape
  dotted names in grep probes.
#429: replace list_users.sh false-positive grep with anchored awk over basic_auth block.
#428: PILOT_ACCESS.md support email → pilot@gendsgn.ru.

Closes #71
Closes #427
Closes #429
Closes #428
2026-06-13 20:13:04 +05:00
6e0039dd47 chore(agents): reviewer back to Opus (revert model-part of aa3d012)
Per user decision: reviewer = Opus (merge-authority needs strong reasoning on verdict);
остальные loop-роли остаются Sonnet. Реверсит только model-часть aa3d012 (reviewer→sonnet).
- start-bot.ps1: $model = reviewer? opus : sonnet + --model $model
- auto-code-reviewer.md: frontmatter model: opus + doc reviewer на Opus
2026-05-31 17:16:57 +03:00
5fa9588935 chore(agents): bot harness tuning — per-role model, forgejo MCP deferral, vault capture, #893 curl hardening
- start-bot.ps1: --model per-role (sonnet; opus только reviewer); CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=60
- .claude/mcp/*.json: forgejo (~90 tools) deferred во всех ролях; postgres-gendesign deferred в backend/reviewer
- auto-analyst.md: knowledge-capture step (status/done → inbox draft → vault-overlord) — закрывает 'волт не обновляется'
- forgejo ops через mcp__forgejo__* вместо сырого curl: auto-code-reviewer/backend/qa workflow + _autonomous_pickup contract
- #893 guard: бан curl→/tmp→python3 (Windows 404+FileNotFoundError); deferred→ToolSearch; stale tool-name cleanup (deep-reviewer phases)
2026-05-31 16:26:17 +03:00
aa3d012fdf chore(agents): bot-окна на Sonnet (start-bot --model) + context-hygiene + reviewer/resolver→sonnet (weekly Opus-пул) 2026-05-31 15:39:42 +03:00
5cfab0b23e fix(bots): подключить postgres-tradein MCP в analyst/backend bot-окна (#857) 2026-05-31 06:44:46 +00:00
lekss361
2df8f78747 chore(bots): per-window MCP isolation via --strict-mcp-config
Каждое bot-окно грузит ТОЛЬКО нужные роли серверы (не все 7+ в каждом окне).
Выигрыш: не спавнить heavy-серверы зря (docker postgres в frontend/qa), right-size
eager-набор, чистый tool-surface на роль. (Контекст tool-search и так бережёт —
deferred ≈ бесплатно; это про процессы + eager + ясность.)

- .claude/mcp/{analyst,backend,frontend,reviewer,qa}.json — self-contained per-role
  конфиги. Секреты НЕ инлайнятся: ${OBSIDIAN_API_KEY}/${GENDESIGN_DB_URI}/
  ${GLITCHTIP_TOKEN} через env-подстановку (.mcp.json env expansion, подтв. в доках).
  Матрица: forgejo+obsidian+context7 eager везде; pg-gendesign eager у backend/reviewer,
  deferred у analyst; UI (playwright/a11y/lighthouse/shadcn) только frontend/qa; fetch+
  glitchtip у backend; glitchtip у qa.
- start-bot.ps1: извлекает MCP-секреты из ~/.claude.json (единый источник) → export →
  запускает `claude --strict-mcp-config --mcp-config .claude/mcp/<role>.json`.

Main/интерактивное окно (без start-bot.ps1) по-прежнему видит полный .mcp.json+user.
2026-05-30 11:05:24 +03:00
lekss361
edc5823622 chore(bots): per-role launcher scripts + move bot pipeline to forgejo MCP
(a) Launcher scripts — больше не экспортировать токен руками:
- scripts/start-bot.ps1 <role> — выставляет per-bot identity + токены (FORGEJO_ACCESS_TOKEN
  для forgejo MCP + FORGEJO_TOKEN для curl-fallback), git-identity, forgejo-bot remote,
  verify, затем запускает claude.
- scripts/start-{analyst,backend,frontend,reviewer,qa}.ps1 — тонкие врапперы.

(b) Bots on forgejo MCP (goern) вместо curl:
- _autonomous_pickup.md: новая секция «Forgejo операции — mcp__forgejo__* (PRIMARY)» с полным
  curl→MCP mapping (list_repo_issues / create_pull_request / merge_pull_request /
  get_pull_request_diff / create_pull_review / add+remove_issue_labels / update_issue /
  issue_state_change / create_issue_comment). curl_forgejo помечен FALLBACK.
- 5 auto-*.md + 5 work-as-*.md: директива «Forgejo API → mcp__forgejo__* tools (mapping в
  _autonomous_pickup); curl только fallback» + указатель на start-bot.ps1.

Gotcha задокументирован: include_org_labels:false на user-репо (lekss361 не org).
Требует: рестарт Claude Code (forgejo MCP eager из .mcp.json) + FORGEJO_ACCESS_TOKEN per-window
(делает start-bot.ps1).
2026-05-30 10:38:10 +03:00
lekss361
5673dd6298 chore(claude): extend self-extending tripwire + pause-bots guard for stale-claims
Two safety follow-ups к autonomous-pipeline (flagged в multiagent-аудите):

- Self-extending guard расширен: reviewer-bot NEVER merge'ит PR, меняющий
  _autonomous_pickup.md (claim/kill-switch/merge-FSM contract) или любой
  work-as-*.md (persona activation) — раньше защищались только git-pr.md
  Auto-merge policy + CLAUDE.md + auto-code-reviewer.md. Закрывает дыру, где
  bot мог изменить claim/kill-switch logic без human. Правки в git-pr.md,
  auto-code-reviewer.md, work-as-reviewer.md.
- cleanup-stale-claims.sh: pause-bots early-exit. Без него cron освобождал
  wip-claim worker'а, приостановленного mid-work (он держит claim до un-pause
  per _autonomous_pickup.md), теряя его работу.
2026-05-29 17:36:20 +03:00
lekss361
8c9d1344cc fix(stale-claims): avoid SIGPIPE 141 — pass file path не pipe
Root cause: `cat $ISSUES_TMP | python3 <<EOF ... sys.exit(0)` — Python exits
0 early, cat получает SIGPIPE, set -o pipefail → script exits 141 = failure.

Fix: передача file path через env var вместо stdin pipe. Python читает файл
напрямую. No pipe → no SIGPIPE.

Verified локально на VPS с bot-qa token: exit 0, '[OK] 0 wip issues —
nothing to do'.

Also remove diagnostic steps from workflow (added в PR #611, нужно для
поиска root cause, теперь убираем).

Refs: PR #611 (diagnostic), runs #1514-1520 failures.
2026-05-28 01:05:56 +03:00
lekss361
e4dc866e20 fixup(claude): address review-bot blockers on PR #610
🔴 Critical (3):

1. cleanup-stale-claims.sh: export CUTOFF + STALE_HOURS
   Без `export` Python heredoc child process получал "0" вместо реального
   cutoff → cron всегда видел все issues как fresh → ничего не освобождал.
   Plus added abort при CUTOFF=0 в Python heredoc для defense-in-depth.

2. cleanup-stale-claims.sh: datetime.UTC → datetime.timezone.utc
   datetime.UTC требует Python 3.11+. Forgejo runner ubuntu-latest имеет 3.10.
   datetime.timezone.utc works в 3.8+.

3. cleanup-stale-claims.sh: invert label order — DELETE wip FIRST, потом
   POST ready. Если step 2 (add ready) fails — issue в neutral state без
   status/*, менее опасно чем оба status/ready+status/wip одновременно.

🟠 + 🟡 + 🟢 (5):

4. Added pagination loop (defensive cap 10 pages × 50 = 500 issues), но
   реализована через temp file + Python parsing (не bash JSON concat).

5. Removed dead code: iso_to_epoch() shell helper не использовался.

6. _autonomous_pickup.md: fix broken `Out-Null | if (-not $?)` pattern.
   GetEnvironmentVariable НЕ throws при missing — возвращает $null. $? у
   Get* всегда $true. Заменено на прямую проверку результата.

7. stale-claims.yml: добавлен concurrency group чтобы overlapping runs
   не stomp'ились (хотя 5-min timeout делает overlap unlikely, defensive).

8. Documented updated_at != heartbeat assumption в header script'а.

Не сделано (low/optional):

- Setup script tokens в memory (UX trade-off — не critical)
- Token suffix в console output (минор info leak в shell history)

Refs: PR #610 review by review-bot (sha=297eb29, verdict=changes)
2026-05-28 00:33:27 +03:00
lekss361
297eb29d65 feat(claude): env-vars refactor + stale-claim cron (Phase 2 finalization)
#11: slash-commands и _autonomous_pickup.md теперь читают persistent User-scope
env vars напрямую (FORGEJO_TOKEN_<ROLE>) вместо file-based из ~/.claude/secrets/.

Один раз: scripts/setup-bot-env.ps1 (запускается user'ом локально, кладёт
токены из vault в Windows User registry). После этого окна grab credentials
через [System.Environment]::GetEnvironmentVariable() — никаких файлов.

Все 5 work-as-*.md обновлены (analyst/backend/frontend/reviewer/qa).

#12: stale-claim cleanup — cron каждые 30 минут.

- scripts/cleanup-stale-claims.sh — bash, parses Forgejo API + Python для JSON
- .forgejo/workflows/stale-claims.yml — cron schedule `*/30 * * * *`

Освобождает issues застрявшие в status/wip >4h:
  - assignees=[]
  - +status/ready -status/wip
  - comment "stale claim released, worker likely crashed"

Использует secret FORGEJO_BOT_QA_TOKEN (минимум прав = write:issue).

Setup: secret нужно завести в Forgejo Actions UI после merge —
git.gendsgn.ru/lekss361/gendesign/-/settings/actions/secrets.

Refs: PRs #608, #609. Closes TODO с PR #608 (stale-claim cron).
2026-05-28 00:24:45 +03:00
d954be1331 fix(auth): scripts/auth/*.sh — правильный SNIPPET path (#589) 2026-05-26 08:39:24 +00:00
03ab1faa7c chore(claude): aggressive worktree cleanup via Forgejo PR API (#498) 2026-05-24 09:34:48 +00:00
998c8a17a8 chore(claude): agents view readiness — cleanup, .worktreeinclude, workflow (#497) 2026-05-24 09:20:26 +00:00
73bdff41a7 chore(claude-config): align with 2026 best practices (#495) 2026-05-24 08:48:42 +00:00
cc2d148967 feat(security): закрыть gendsgn.ru basic_auth gate (multi-user, 11 users) (#426)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Successful in 30s
Deploy / build-frontend (push) Successful in 28s
Deploy / build-backend (push) Successful in 31s
Deploy / deploy (push) Successful in 53s
Pilot demo 28.05.2026: closed gendsgn.ru/* (включая trade-in/) basic_auth gate. /health и /preview/* остаются public через route { } wrapper (Caddy 2.x directive ordering fix).

11 users (admin + user1..user10), bcrypt cost=14, snippet в git (audit trail). Scripts: scripts/auth/{add,remove,list}_user.sh.

Fixup PR #426 (route{} wrap + log rotation + base64 busybox-compat + runbook audit log fix).
2026-05-23 08:16:10 +00:00
021b0589b2 fix(infra): bootstrap_glitchtip — \gexec instead of DO block (psql var trap) (#219) 2026-05-16 15:54:29 +00:00
d3aa96336b feat(infra): GlitchTip self-hosted error tracking (#204 devops) (#205)
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
2026-05-16 15:08:50 +00:00
lekss361
7c43bc5496
feat(infra): auto-apply data/sql/*.sql migrations in deploy pipeline (#150) (#151)
* fix(frontend): apiFetchWithStatus body stream double-read crash

Per user report 2026-05-14: 'Failed to execute text on Response: body stream
already read' при analyze с non-JSON error response.

apiFetchWithStatus делал .json() (consumes stream), потом .text() в catch
(FAILS — stream already consumed). Fetch API: body — ReadableStream,
consumable один раз.

Fix: read text() once → JSON.parse on string. На fail (HTML / non-JSON)
оборачиваем в {detail: rawText}. Никаких double-reads.

Site-finder cad search больше не крашится на error pages.

* feat(infra): auto-apply data/sql/*.sql migrations in deploy pipeline (#150)

Закрывает root cause production 500 на /api/v1/admin/site-finder/weight-profiles
(а также 2 unapplied migrations: #89/#91).

## Why это нужно

После audit обнаружили что:
- backend/alembic/versions/ пустой (Alembic configured но не используется)
- deploy.yml не имеет migration step
- 47+ raw SQL files накопилось без auto-apply
- 3 файла unapplied: 87 (engineering), 89 (drop brin), 90 (weight profiles)

User report: GET weight-profiles → 500 потому что user_weight_profiles
table не существует на prod.

## Changes

### deploy.yml +39 lines
- Path trigger расширен на data/sql/*.sql
- New step 'Apply DB migrations' между compose pull и compose up -d:
  - Idempotent CREATE TABLE _schema_migrations
  - Loop по data/sql/*.sql sorted → skip applied → psql with ON_ERROR_STOP=on
  - Record applied filename в tracking table
  - Exit 1 на failure → containers НЕ обновляются (no partial state)

### NN collision fix
- 87_engineering_networks_191fz.sql → 91_engineering_networks_191fz.sql
  (collision с 87_on_demand_indexes.sql, мой renumber 85→87 был неудачным)

### CLAUDE.md +32 lines
- Subsection 'Auto-apply' под Migration pattern
- Bootstrap procedure docs
- Idempotency requirement
- Failure recovery

### scripts/bootstrap_schema_migrations.sh +100 lines
- One-time seed _schema_migrations с 48 already-applied files
- 3 файла intentionally NOT seeded: 89/90/91 (auto-apply на first deploy)
- Usage docs + verification query

## Vault

domains/infra/Runbook_Auto_Apply_Migrations.md NEW
infra-MOC.md updated

## Required manual steps ДО merge

1. SSH gendesign (tunnel)
2. POSTGRES creds export
3. bash scripts/bootstrap_schema_migrations.sh — seed 48 already-applied files
4. Verify: SELECT COUNT(*) FROM _schema_migrations → 48
5. После merge → first deploy auto-apply #89/#90/#91 → 500 closes

Closes #150

---------

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 08:06:54 +03:00
lekss361
769b16df47 fix(worker): port objective_etl from psycopg2 → psycopg v3
Worker крашился на старте с `ModuleNotFoundError: No module named 'psycopg2'`
при импорте app/workers/tasks/objective_etl, потому что app/services/objective_etl
писал на psycopg2 API, а в pyproject.toml есть только psycopg[binary]>=3.2.0.

Изменения:
- psycopg2 → psycopg (v3)
- psycopg2.extras.execute_values → _bulk_upsert helper, эмулирующий тот же
  behavior через cur.executemany (в psycopg v3 это pipeline-mode, overhead
  на сетевые round-trips минимален)
- psycopg.connect совместим с тем же connection string

Тесты: AST parse ✓, ruff ✓, import ✓
2026-05-11 12:07:57 +03:00
lekss361
b0f5c38894 feat(deploy): split obsidian stack from main + path-filtered GHA
Obsidian Self-hosted LiveSync (CouchDB) выведен в отдельный docker-compose
stack и GHA workflow. Любой стек теперь редеплоится независимо.

* docker-compose.obsidian.yml: только couchdb в shared external network
* docker-compose.prod.yml: убрана couchdb-секция; caddy подключён к
  shared network для маршрута obsidian.gendsgn.ru → couchdb:5984
* .github/workflows/deploy-obsidian.yml: новый workflow с path-filters
  (триггерится только при изменениях obsidian-related файлов)
* .github/workflows/deploy.yml: + path-filters (триггерится только при
  main-related файлах) + создание shared network в bootstrap
* docs/obsidian-livesync.md: обновлены инструкции под split-архитектуру

После push:
1. ssh gendesign 'docker network create gendesign_shared'  (один раз)
2. Прописать COUCHDB_USER/PASSWORD в backend/.env.runtime
3. Добавить DNS A-record obsidian.gendsgn.ru → IP VPS
4. git push → GHA задеплоит обе части независимо

Преимущества:
- Изменения main не разрывают LiveSync клиентов
- Изменения obsidian не рестартуют main
- Падение obsidian-стека не влияет на основное приложение
2026-05-11 09:51:31 +03:00