Commit graph

7 commits

Author SHA1 Message Date
f7e8228550 fix(ptica): свежесть data-table источника считается по успешным строкам (#2956) (#2957)
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 2m28s
Deploy / build-worker (push) Successful in 5m26s
Deploy / deploy (push) Successful in 1m25s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 9s
2026-08-20 08:36:05 +00:00
2e20b6307b fix(tradein): гейт номеров миграций берёт эталон из git, ручной манифест удалён
Some checks failed
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
CI Trade-In / backend-tests (pull_request) Failing after 21s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 55s
CI Trade-In / frontend-checks (pull_request) Successful in 1m26s
CI / openapi-codegen-check (pull_request) Successful in 2m7s
CI / backend-tests (pull_request) Successful in 16m53s
_manifest_applied.txt по построению не мог покраснеть. Тест считал «новым»
любой файл, которого нет в списке, а новые файлы от списка освобождены
(докстринг test_manifest_covers_all_but_new_files: «НЕ требует, чтобы новый
файл уже был в manifest»). Забытое имя и новая миграция PR для гейта — одно и
то же, поэтому дрейф был не пропуском проверки, а её штатным исключением.
Замер на main 2026-08-07: 15 имён не дописано, все четыре теста зелёные —
через сутки после того, как #2692 догнал список руками.

Список при этом был лишь копией того, что git и так знает: deploy-tradein.yml
применяет КАЖДЫЙ data/sql/*.sql из main под ON_ERROR_STOP, то есть «файл
доехал до main» и есть «имя закреплено на проде». Ведём эталон в git — и
дрейфовать становится нечему.

Кросс-ветковая дыра закрыта тем же ходом: номер нового файла сверяется с
ПОЛНЫМ origin/main, а не с рабочим деревом, поэтому коллизия с миграцией,
смерженной после ветвления, находится. Проверено на живом PR #2754
(234_trade_in_estimates_retain_until против 234_scrape_runs_ban_kind_unknown
из main): старый гейт зелёный, новый красный.

Удаление/переименование применённой миграции сверяется с ТОЧКОЙ ВЕТВЛЕНИЯ, а
не с origin/main: иначе ветка недельной давности краснела бы за чужие
миграции. Проверено — ветка от 2026-07-30 при +43 миграциях в main зелёная.

CI: checkout переведён на fetch-depth 0 + отдельный fetch main. Этот Forgejo
не публикует refs/pull/N/merge (1620 */head, ноль */merge), а на depth=1 нет
ни origin/main, ни общего предка — без этого гейту не с чем сверять, и он
намеренно красный, а не тихо пропущенный.

Контракт сведён к одной формулировке — докстринг test_migration_numbering.py;
шапка манифеста, правило 3, хвост манифеста и рецепт из .claude/rules
удалены или заменены ссылкой. Заодно исправлен сам рецепт: `git ls-tree` без
`-r` печатает каталог, а не файлы.

Refs #2683
2026-08-07 14:35:28 +05:00
b5976c0cc9 feat(auth): роли и трёхзначное состояние доступа в БД auth [PR-2a/6]
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m47s
CI / backend-tests (pull_request) Successful in 15m48s
Схема под решения владельца от 2026-07-31 по эпику «единый вход».
Python-кода нет, поведение прода не меняется — в БД auth пока никто не ходит.

Развилка А закрыта в пользу ПОЛНОГО переезда: tradein_users (БД tradein) в
итоге удаляется, auth.users становится единственным реестром людей. Значит
role и manager_id переезжают сюда — это отменяет решение 001:15-19 («ролей
здесь нет — сознательно»), что зафиксировано в шапке файла и переписанным
COMMENT ON TABLE, а не оставлено расходиться молча.

Развилка Б закрыта в пользу трёх состояний: is_active заменён на access_state
(active / trial_expired / disabled). Булев флаг схлопывал «пускаем, но
объясняем» и «не пускаем вовсе» в одно значение — trial-экран исчезал бы
без падения тестов. Семантика зафиксирована в COMMENT: trial_expired при
ВЕРНОМ пароле даёт 403 с отдельным кодом и НЕ выдаёт сессию, disabled —
generic 401; неверный пароль в любом состоянии остаётся generic 401, то есть
защита от перечисления логинов сохраняется. user2 («Брусника») → trial_expired.

Колонки role/manager_id зеркалят м.192 побуквенно (CHECK ролей, иерархический
CHECK, partial index, self-FK ON DELETE SET NULL), чтобы код «Меры» переехал
на auth.users без правок. Добавлен users_manager_not_self_ck — на уровне БД
самоназначение менеджером иначе проходит, а второй потребитель (Птица)
валидации «Меры» не имеет.

Гранты. INSERT выдан — без него переезд не состоится (создание сотрудника из
«Команды»). DELETE НЕ выдан: потребителя нет (в team.py только POST и PATCH),
а 002:22-33 отклоняла ровно такие гранты-на-будущее; появится хендлер —
появится строка GRANT в той же миграции. Табличный UPDATE из 002:80 сужен до
column-level: иначе auth_app молча получил бы право писать role и
access_state, и ошибка в PATCH-эндпоинте превращалась бы в тихое повышение до
админа или тихое снятие блокировки. role и manager_id в список не включены —
их сегодня не пишет никто.

Гранта на users_id_seq нет намеренно: для GENERATED ALWAYS AS IDENTITY
PostgreSQL использует NextValueExpr → nextval_internal(check_permissions
:= false), ACL последовательности не проверяется. Утверждение 002:26-27
(«идентичность требует nextval») фактически неверно; проверено обратным
экспериментом — REVOKE, затем INSERT.

Проверено исполнением на postgres:16, не по комментариям:
- чистая сборка 001→002→003→004 — 13 строк, роли admin/manager×2/employee×10,
  user2 = trial_expired, is_active отсутствует, все 6 констрейнтов на месте;
- повторный прогон 004 ×2 идемпотентен;
- ручные прод-правки (user2 → active, user3 → manager) переживают повтор —
  backfill не затирает решения владельца;
- периметр auth_app: INSERT users ✓, UPDATE access_state ✓, INSERT sessions ✓;
  UPDATE role ✗, UPDATE manager_id ✗, DELETE ✗, CREATE TABLE ✗;
- CHECK'и ловят: admin с manager_id, self-manager, access_state вне списка,
  role вне списка, INSERT без role.

Тест: 6 passed. Добавлена проверка запрета CREATE INDEX CONCURRENTLY —
в связке с обязательной обёрткой BEGIN/COMMIT это комбинация, невыполнимая
на проде (25001), а отдельной проверки на неё не было.
2026-08-01 00:28:37 +03:00
bot-backend
bea61f6cd9 feat(auth): отдельная БД auth — фундамент единого входа «Меры» и «Птицы»
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 14s
CI Trade-In / backend-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 3m0s
CI / backend-tests (pull_request) Successful in 15m35s
PR-1 эпика: вся авторизация переезжает на одну нейтральную форму входа, браузерный
popup (Caddy basic_auth) убирается. Этот PR — ТОЛЬКО фундамент, прод работает как
сейчас: в БД gendesign ничего не меняется, новая БД создаётся и наполняется
логинами без паролей, читать её пока некому.

Почему отдельная БД, а не таблица в существующей: хранилище доступов не должно
принадлежать продукту, из которого аккаунты выносятся. Сервер — существующий
gendesign-postgres (новый контейнер не заводим); проверено, что оба бэкенда
сидят в сети gendesign_shared и TCP-достают до него.

Состав:
- data/sql/auth/001-003 — схема (users, sessions), роль приложения, сид 13 логинов.
  password_hash = NULL у ВСЕХ: plaintext и bcrypt-хеши в git запрещены, пароли
  проставляются отдельно на проде (конвенция репы, прецедент tradein м.193).
- ops/db-bootstrap/create_auth_db.sql — CREATE DATABASE через \gexec. Не миграцией:
  CREATE DATABASE запрещён в транзакции, а миграции обязаны быть транзакционными.
- ops/db-bootstrap/set_auth_app_password.sql — пароль роли из env, зеркало
  set_tradein_fdw_password.sql (GUC + \o /dev/null + %L, строго через stdin —
  :'pw' не интерполируется внутри $$...$$, на этом падал деплой 2026-05-24).

Схема лежит в ПОДКАТАЛОГЕ data/sql/auth/ намеренно: основной цикл деплоя использует
`ls -1 data/sql/*.sql`, который в подкаталоги не рекурсирует → эти файлы физически
не могут примениться в БД gendesign. Защита не на дисциплине, а на глобе. Триггер
`data/sql/**` подкаталог при этом покрывает.

Права: владелец БД — суперюзер, а не auth_app (иначе гранты были бы декорацией).
users — только SELECT+UPDATE (INSERT не выдан: создания аккаунтов в этом PR нет, а
снять грант, на который уже опирается прод-код, сложнее чем выдать). REVOKE ALL ON
DATABASE FROM PUBLIC продублирован в bootstrap и в миграции намеренно: bootstrap
гоняется каждый деплой (переприменяемость), миграция — однократно (самодостаточность).

Проверено ИСПОЛНЕНИЕМ на postgis/postgis:16-3.4 (тот же образ, что на проде):
двойной прогон всех файлов идемпотентен; COALESCE-защита сида не затирает вручную
проставленные пароль/имя (проверено живьём); ASCII-CHECK отклоняет кириллицу;
auth_app коннектится, посторонняя роль → permission denied; битая миграция даёт
exit 1 и НЕ пишется в _schema_migrations, т.е. деплой прервётся до подъёма кода;
пароль с кавычками и бэкслешем не ломает %L и не печатается в stdout.

Тест backend/tests/sql/test_auth_sql_migrations.py: имена, транзакционность, наличие
wiring в deploy.yml и детектор паролей/хешей, покрывающий И data/sql/auth, И
ops/db-bootstrap — единственное место в репе с ALTER ROLE ... PASSWORD.
Детектор проверен на живучесть: подложенный bcrypt-хеш роняет тест.

Открытые развилки зафиксированы комментариями в коде, решаются в PR-2/3:
судьба tradein_users/tradein_sessions (два одинаковых по схеме хранилища) и
expired != disabled (trial-экран не выражается булевым is_active).

NB: переменную AUTH_DB_PASSWORD нужно завести вручную в runtime-env бэкенда на VPS.
Пока пусто — шаг ALTER ROLE пропускается с warning'ом, деплой не падает.
2026-07-31 21:47:33 +03:00
658d724075 feat(analytics): velocity-anomaly alerts (#17) + ARN ДДУ price indicator (#99)
Some checks failed
CI / changes (push) Successful in 8s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 7m37s
CI / backend-tests (pull_request) Successful in 7m42s
Deploy / deploy (push) Blocked by required conditions
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 / changes (push) Has been cancelled
#17: detect_velocity_anomalies + GET /analytics/velocity-alerts — z-score drop-detection
на domrf_kn_sale_graph (double-gate z≤-2 AND drop≤-30%, starvation-guards). Snapshot=richest
(не MAX — prod новейшие частичные), lookback anchored на latest report_month (scrape лаг ~4мес).
Prod: ЖК Центральный Парк -69%, ~14ms.

#99: mv_ddu_price_indicator (миграция 152) + POST /market/ddu-indicator — ARN-mirror
ценовой индекс per quarter×area_bucket из rosreestr_deals (ДДУ регион 66). MVP: subject-level,
period Q, window 2025-Q2+, methods 1/2 (basis/previous, prev_period_value honesty). Q1-2026
headline 1.0185 vs ARN 1.03 (±5%). Method 3 blocked (нет pre-2025-Q2 данных) — задокументировано.

Closes #17
Closes #99
2026-06-13 22:04:49 +05:00
e9585bcd3b test(suite): re-align drifted tests with current code (CI-rehab 2/3)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 36s
Deploy / build-worker (push) Successful in 1m8s
Deploy / deploy (push) Successful in 1m13s
Suite was uncollectable for a while → code evolved, mocks didn't (42 failed /
5 errors → 1687 passed / 0 failed / 0 errors). TEST-ONLY, no app code changed,
no real bugs found (code-reviewed  — no test weakened to fake-green).

- best_layouts: sum_deals→deals_window rename; velocity-scaling direction for
  new SF-01 divisors (last_month=1.0/last_year=12.0)
- quarter_dump_lookup: 7→10-tuple mock rows (risks/opportunity/red_lines cols);
  fix early-exit call-counts for the cad_zouit fallback (#232)
- cadastre_bulk: add xmin/ymin/xmax/ymax to harvest-quarter db mock
- nspd_client: add required QuarterDump.opportunity
- TopLayoutRow: add required is_oversold
- analyze_{market_price,recent_permits,inline_weights}: SQL-signature dispatch
  instead of fragile positional db.execute indices (SF-B5 query reorder; also
  fixes a double-POST geom-starvation false-negative)
- custom_pois: move centroid >15km so center_bonus=0 isolates custom-POI delta
- layout/report PDF: runtime WeasyPrint probe (skip macOS dev, RUN on CI)
- mv_layout SQL: normalize +psycopg DSN + connectivity-probe skipif
- admin token tests: skip (X-Admin-Token gate removed #437/#426; protection is
  Caddy basic_auth + RBAC, covered by test_rbac)

Refs #944.
2026-06-03 19:05:43 +05:00
07106d744a chore(mv): regression test + sql rule for weighted AVG (#295) (#626)
Some checks failed
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 33s
Deploy / build-backend (push) Successful in 37s
Deploy / deploy (push) Has been cancelled
2026-05-28 13:49:54 +00:00