fix(tradein/admin): убрать показатели, которые не могут быть ненулевыми, и брать список источников из данных (#2674) #2684

Merged
bot-backend merged 2 commits from fix/2674-admin-metrics-honesty into main 2026-08-05 23:01:31 +00:00
Collaborator

Summary

Четыре находки #2674 одного класса: админка показывает числа, которые никогда не бывают ненулевыми, и подаёт это как результат. Ноль читается оператором как «всё чисто», а не как «мы это не считаем» — такой показатель хуже отсутствующего, поэтому по каждой находке выбор был «заполнить или честно убрать», а не «как бы не удалять код».

Числа с прода (tradein-postgres, 2026-08-06, до фикса).


1. «Помечено выбросов» — УБРАН (v_data_quality.outliers_flagged + колонка listings.is_outlier)

Было: is_outlier = true у 0 из 93 408 объявлений, NULL — у 0. То есть в колонке только DEFAULT false из миграции 002 и ни одного UPDATE за всю историю; писателя нет нигде.

Почему убрать, а не реализовать. Механизм не «не доделан» — он невыразим в этой колонке. «Выброс» у эстиматора считается Tukey-фильтром по конкретной подборке аналогов (estimator._filter_outliers, k=1.5 / 1.0 на малых выборках) и живёт ровно один запрос: один и тот же лот — выброс для оценки соседней однушки и нормальный аналог для оценки трёшки этажом выше. Persist-флаг на объявлении такое отношение выразить не может в принципе. Комментарий в 002 («помечено модели после aggregation») описывает отменённую затею batch-разметки, которую вытеснила фильтрация во время запроса.

Проверено, что нигде не читается: во всём репозитории outliers_flagged / v_data_quality встречаются только в четырёх data/sql/*.sql; ни один Python/TS не читает ни колонку, ни view (сам view — hand-tool под psql: его COMMENT ссылается на /api/v1/admin/data-quality, но эндпоинт давно переписан и view не трогает). View сохранён, из него убрана одна строка.

2. Счётчики HTTP-запросов / ошибок / вернувшихся / исчезнувших — УБРАНЫ (4 колонки scrape_runs)

Было: http_requests, http_errors, returning_count, disappeared_count0 во всех 3244 прогонах с миграции 015, писателя нет вообще, и ни API, ни UI их даже не отдают.

Почему убрать, включая «дешёвые в заполнении»:

  • http_requests — HTTP-запросы не считает ни один фетчер (curl_cffi / BrowserFetcher). Заполнить нечем без сквозной инструментации всех путей сбора — это проект, а не фикс, и заказчика у числа нет.
  • http_errors — то, что оператор реально хочет («сколько ошибок»), уже пишется тем, кто их считает: errors_count в counters jsonb (579 прогонов), и RunsTable его уже показывает. Отдельная колонка была бы вторым, иначе определённым счётчиком того же слова.
  • returning_count / disappeared_count — та же история: deactivate_stale_* пишет deactivated (187) и revived (169) в counters, вычисляя их там, где данные есть. Дублировать это колонкой, которая ненулевая у 3 источников из 53, — сделать хуже.

Сигнал не потерян: он живёт в counters jsonb у джоба, который его знает.

3. Тип прогона run_typeУБРАН (API + таблица админки + колонка)

Было: 3244 строки, ровно одно значение 'city_sweep'DEFAULT из миграции 051; ни одно место кода run_type не задаёт. Колонка «Тип» в UI подписывала «city_sweep» прогоны, которые никаким sweep не были: proxy_healthcheck (1630 шт.), deactivate_stale_*, sber_index_pull, rosreestr_dkp_import.

Почему убрать: вид прогона честно называет source — вторая, вырожденная ось не нужна. Колонка «Тип» в таблице заменена на «Источник» (r.source) — то же место на экране, но со значением, которое различает строки. Симметрично снят SELECT run_type в обеих копиях list_all (app.services.scrape_runs и scraper_kit.orchestration.runs) — иначе после DROP админка отдаёт 500.

4. Фильтр источников — из данных, а не из литерала

Было: RUN_SOURCE_FILTERS = ["", "avito", "cian", "yandex"], а list_all сравнивает точно (source = :source). Строк с source = 'avito'/'cian'/'yandex' в таблице 0 — то есть каждый пункт фильтра давал пустую выдачу, и пустой выбор «Все» тоже: он молча подставлял source вкладки. В данных 53 источника; 2466 прогонов (76%) вне трёх площадок, включая весь Домклик (domclick_city_sweep 46, domclick_detail_backfill 30, deactivate_stale_domklik 25).

Стало: GET /api/v1/admin/scrape/runs/sourcesSELECT DISTINCT source ORDER BY source (фильтры прогонов игнорирует — иначе выбор источника вырезал бы из списка остальные), отдельный query-key на фронте (список не схлопывается при смене фильтра), «Все» = буквально все. Новый источник появляется в фильтре сам, без правки кода.


Тесты

tradein-mvp/backend/tests/test_2674_dead_admin_metrics.py (8 тестов) + правка test_scraper_admin_apis.py.

Фальсификация патч-методом (impl откачен, тесты оставлены) — красные все 9, зелёных без фикса нет:

FAILED test_sources_endpoint_returns_whatever_the_table_holds
FAILED test_sources_query_ignores_run_filters
FAILED test_frontend_source_filter_has_no_hardcoded_list
FAILED test_unified_run_row_has_no_run_type
FAILED test_list_all_selects_no_dropped_columns[app.services]
FAILED test_list_all_selects_no_dropped_columns[scraper_kit]
FAILED test_migration_drops_every_dead_column
FAILED test_latest_v_data_quality_no_longer_reports_outliers
FAILED test_scraper_admin_apis.py::test_unified_runs_returns_rows_and_total

Тест на «новый источник появляется сам» кормит фейковую БД заведомо несуществующим newmarket_2031_sweep — если он доезжает до ответа, список действительно из данных. Тест test_latest_v_data_quality_no_longer_reports_outliers смотрит не на конкретный файл, а на действующее определение view (последняя миграция, которая его создаёт) — поймает и повторное добавление показателя.

Полный прогон: 3487 passed, 9 skipped (--deselect tests/test_search_api.py::test_search_cache_hit, как в CI). Фронт: tsc --noEmit чистый.

Test plan

  • Деплой применил 214_drop_dead_run_metrics.sql (strict): \d scrape_runs — нет run_type, http_requests, http_errors, returning_count, disappeared_count; \d listings — нет is_outlier; SELECT * FROM v_data_quality — нет колонки outliers_flagged и view не сломан.
  • docker exec tradein-backend curl -s -H "X-Authenticated-User: admin" localhost:8000/api/v1/admin/scrape/runs/sources — 53 источника, среди них domclick_city_sweep.
  • .../scrape/runs?limit=5 — 200, в строках нет run_type.
  • Страница /scrapers, любая вкладка: «Источник» в фильтре — длинный список реальных значений; выбор domclick_city_sweep даёт непустую выдачу; при «Все» таблица не пустая (раньше была пустой на всех вкладках); колонка «Источник» в таблице различает строки.
  • Скрейпер жив после DROP: новый прогон завершается в done, counters/total_seen пишутся как раньше (окно между применением SQL и подъёмом нового образа — админский /scrape/runs может кратко отдать 500 на старом коде, самоустраняется).

Refs #2674

## Summary Четыре находки #2674 одного класса: админка показывает числа, которые **никогда не бывают ненулевыми**, и подаёт это как результат. Ноль читается оператором как «всё чисто», а не как «мы это не считаем» — такой показатель хуже отсутствующего, поэтому по каждой находке выбор был «заполнить или честно убрать», а не «как бы не удалять код». Числа с прода (`tradein-postgres`, 2026-08-06, до фикса). --- ### 1. «Помечено выбросов» — **УБРАН** (`v_data_quality.outliers_flagged` + колонка `listings.is_outlier`) **Было:** `is_outlier = true` у **0** из 93 408 объявлений, `NULL` — у 0. То есть в колонке только `DEFAULT false` из миграции 002 и ни одного `UPDATE` за всю историю; писателя нет нигде. **Почему убрать, а не реализовать.** Механизм не «не доделан» — он невыразим в этой колонке. «Выброс» у эстиматора считается Tukey-фильтром по **конкретной подборке аналогов** (`estimator._filter_outliers`, k=1.5 / 1.0 на малых выборках) и живёт ровно один запрос: один и тот же лот — выброс для оценки соседней однушки и нормальный аналог для оценки трёшки этажом выше. Persist-флаг на объявлении такое отношение выразить не может в принципе. Комментарий в 002 («помечено модели после aggregation») описывает отменённую затею batch-разметки, которую вытеснила фильтрация во время запроса. **Проверено, что нигде не читается:** во всём репозитории `outliers_flagged` / `v_data_quality` встречаются **только** в четырёх `data/sql/*.sql`; ни один Python/TS не читает ни колонку, ни view (сам view — hand-tool под psql: его COMMENT ссылается на `/api/v1/admin/data-quality`, но эндпоинт давно переписан и view не трогает). View сохранён, из него убрана одна строка. ### 2. Счётчики HTTP-запросов / ошибок / вернувшихся / исчезнувших — **УБРАНЫ** (4 колонки `scrape_runs`) **Было:** `http_requests`, `http_errors`, `returning_count`, `disappeared_count` — **0 во всех 3244 прогонах** с миграции 015, писателя нет вообще, и ни API, ни UI их даже не отдают. **Почему убрать, включая «дешёвые в заполнении»:** - `http_requests` — HTTP-запросы не считает **ни один** фетчер (`curl_cffi` / `BrowserFetcher`). Заполнить нечем без сквозной инструментации всех путей сбора — это проект, а не фикс, и заказчика у числа нет. - `http_errors` — то, что оператор реально хочет («сколько ошибок»), **уже пишется** тем, кто их считает: `errors_count` в `counters` jsonb (579 прогонов), и `RunsTable` его уже показывает. Отдельная колонка была бы вторым, иначе определённым счётчиком того же слова. - `returning_count` / `disappeared_count` — та же история: `deactivate_stale_*` пишет `deactivated` (187) и `revived` (169) в `counters`, вычисляя их там, где данные есть. Дублировать это колонкой, которая ненулевая у 3 источников из 53, — сделать хуже. Сигнал не потерян: он живёт в `counters` jsonb у джоба, который его знает. ### 3. Тип прогона `run_type` — **УБРАН** (API + таблица админки + колонка) **Было:** 3244 строки, **ровно одно значение** `'city_sweep'` — `DEFAULT` из миграции 051; ни одно место кода `run_type` не задаёт. Колонка «Тип» в UI подписывала «city_sweep» прогоны, которые никаким sweep не были: `proxy_healthcheck` (1630 шт.), `deactivate_stale_*`, `sber_index_pull`, `rosreestr_dkp_import`. **Почему убрать:** вид прогона честно называет `source` — вторая, вырожденная ось не нужна. Колонка «Тип» в таблице заменена на «**Источник**» (`r.source`) — то же место на экране, но со значением, которое различает строки. Симметрично снят `SELECT run_type` в обеих копиях `list_all` (`app.services.scrape_runs` и `scraper_kit.orchestration.runs`) — иначе после DROP админка отдаёт 500. ### 4. Фильтр источников — **из данных**, а не из литерала **Было:** `RUN_SOURCE_FILTERS = ["", "avito", "cian", "yandex"]`, а `list_all` сравнивает **точно** (`source = :source`). Строк с `source` = `'avito'`/`'cian'`/`'yandex'` в таблице **0** — то есть каждый пункт фильтра давал пустую выдачу, и пустой выбор «Все» тоже: он молча подставлял `source` вкладки. В данных **53** источника; 2466 прогонов (**76%**) вне трёх площадок, включая весь Домклик (`domclick_city_sweep` 46, `domclick_detail_backfill` 30, `deactivate_stale_domklik` 25). **Стало:** `GET /api/v1/admin/scrape/runs/sources` → `SELECT DISTINCT source ORDER BY source` (фильтры прогонов игнорирует — иначе выбор источника вырезал бы из списка остальные), отдельный query-key на фронте (список не схлопывается при смене фильтра), «Все» = буквально все. **Новый источник появляется в фильтре сам, без правки кода.** --- ## Тесты `tradein-mvp/backend/tests/test_2674_dead_admin_metrics.py` (8 тестов) + правка `test_scraper_admin_apis.py`. Фальсификация патч-методом (impl откачен, тесты оставлены) — **красные все 9**, зелёных без фикса нет: ``` FAILED test_sources_endpoint_returns_whatever_the_table_holds FAILED test_sources_query_ignores_run_filters FAILED test_frontend_source_filter_has_no_hardcoded_list FAILED test_unified_run_row_has_no_run_type FAILED test_list_all_selects_no_dropped_columns[app.services] FAILED test_list_all_selects_no_dropped_columns[scraper_kit] FAILED test_migration_drops_every_dead_column FAILED test_latest_v_data_quality_no_longer_reports_outliers FAILED test_scraper_admin_apis.py::test_unified_runs_returns_rows_and_total ``` Тест на «новый источник появляется сам» кормит фейковую БД заведомо несуществующим `newmarket_2031_sweep` — если он доезжает до ответа, список действительно из данных. Тест `test_latest_v_data_quality_no_longer_reports_outliers` смотрит не на конкретный файл, а на **действующее** определение view (последняя миграция, которая его создаёт) — поймает и повторное добавление показателя. Полный прогон: `3487 passed, 9 skipped` (`--deselect tests/test_search_api.py::test_search_cache_hit`, как в CI). Фронт: `tsc --noEmit` чистый. ## Test plan - [ ] Деплой применил `214_drop_dead_run_metrics.sql` (strict): `\d scrape_runs` — нет `run_type`, `http_requests`, `http_errors`, `returning_count`, `disappeared_count`; `\d listings` — нет `is_outlier`; `SELECT * FROM v_data_quality` — нет колонки `outliers_flagged` и view не сломан. - [ ] `docker exec tradein-backend curl -s -H "X-Authenticated-User: admin" localhost:8000/api/v1/admin/scrape/runs/sources` — 53 источника, среди них `domclick_city_sweep`. - [ ] `.../scrape/runs?limit=5` — 200, в строках **нет** `run_type`. - [ ] Страница `/scrapers`, любая вкладка: «Источник» в фильтре — длинный список реальных значений; выбор `domclick_city_sweep` даёт непустую выдачу; при «Все» таблица не пустая (раньше была пустой на всех вкладках); колонка «Источник» в таблице различает строки. - [ ] Скрейпер жив после DROP: новый прогон завершается в `done`, `counters`/`total_seen` пишутся как раньше (окно между применением SQL и подъёмом нового образа — админский `/scrape/runs` может кратко отдать 500 на старом коде, самоустраняется). Refs #2674
bot-backend added 1 commit 2026-08-05 22:36:30 +00:00
fix(tradein/admin): убрать показатели, которые не могут быть ненулевыми, и брать список источников из данных (#2674)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / 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 / frontend-checks (pull_request) Successful in 1m1s
CI Trade-In / backend-tests (pull_request) Successful in 2m57s
4d0795ae7a
Четыре находки одного класса: админка показывает числа, которые никогда не
бывают ненулевыми, и подаёт это как результат. Ноль читается оператором как
«всё чисто», а не как «мы это не считаем» — такой показатель хуже отсутствующего.

1. «Помечено выбросов» (v_data_quality.outliers_flagged) — УБРАН вместе с
   колонкой listings.is_outlier. Механизм не «не доделан»: «выброс» у эстиматора
   вычисляется Tukey-фильтром по КОНКРЕТНОЙ подборке аналогов и живёт один
   запрос — один и тот же лот выброс для одной оценки и нормальный аналог для
   соседней. Persist-флаг на объявлении такое отношение выразить не может,
   реализовать пометку нечем.

2. http_requests / http_errors / returning_count / disappeared_count — УБРАНЫ.
   HTTP-запросы не считает ни один фетчер (заполнить нечем без сквозной
   инструментации). Ошибки и «пропало/вернулось» уже считает тот, кто их знает,
   и кладёт в counters jsonb: errors_count у pipeline, deactivated/revived у
   deactivate_stale_*. Отдельные колонки были бы вторым определением того же.

3. run_type — УБРАН из API, из таблицы админки и из схемы. Ни одно место кода
   его не задавало; DEFAULT из 051 подписывал 'city_sweep' даже proxy_healthcheck.
   Колонка «Тип» в UI заменена на «Источник» — там осмысленное значение.

4. Фильтр источников — теперь из данных (GET /scrape/runs/sources, SELECT
   DISTINCT source). Захардкоженная тройка не просто была неполной: сравнение
   точное, а строк с source='avito'/'cian'/'yandex' в таблице нет вообще, то
   есть каждый пункт фильтра давал пустую выдачу, и пустой выбор («Все») тоже —
   он молча подставлял source вкладки. Новый источник появляется в списке сам.

Числа с прода (tradein-postgres, 2026-08-06): is_outlier=true у 0 из 93 408
listings (NULL у 0 — только DEFAULT); четыре счётчика = 0 во всех 3244 прогонах
с миграции 015; run_type — одно значение на 3244 строки; 53 реальных источника,
2466 прогонов (76%) вне трёх площадок, включая весь Домклик.

Миграция 214 идемпотентна; v_data_quality пересоздан тем же DDL минус
outliers_flagged (порядок DROP VIEW → DROP COLUMN → CREATE как в 095).
Light1YT added 1 commit 2026-08-05 22:57:57 +00:00
fix(tradein/admin): гейт отмены по источнику, честный комментарий view, лимит 50 (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 / frontend-checks (pull_request) Successful in 1m2s
CI Trade-In / backend-tests (pull_request) Successful in 2m58s
3c5f535e6c
Ревью PR #2684 — четыре MINOR.

1. Починка фильтра открыла кнопку отмены на все 53 источника. Раньше таблица была
   пуста на каждой вкладке, поэтому кнопка не рендерилась НИ РАЗУ и дыра не
   проявлялась: ручки отмены source не проверяют вовсе. Оператор на вкладке Авито
   мог бы «отменить» refresh_search_matview — задача продолжила бы работать под
   статусом 'cancelled' (ещё один врущий статус ровно в тот день, когда их
   вычищаем), а has_running_run перестал бы держать single-run guard, который
   существует из-за инцидента с двойным свипом и баном (2026-05-31).

   Гейт поставлен на общем узле всех пяти ручек — scrape_runs.honors_cancel +
   отказ в mark_cancelled, — а не в UI: иначе ручной POST по-прежнему снимал бы
   guard. Флаг cancellable отдаётся в строке, UI по нему прячет кнопку.
   Состав набора выведен из call-site'ов runs.is_cancelled: city-sweep'ы (все
   площадки и города), full-load'ы, avito_newbuilding_sweep, rosreestr_dkp_import.
   Правило НЕ «любой *_sweep»: yandex_newbuilding_sweep отмену не опрашивает.

2. Комментарий пересозданного v_data_quality утверждал, что его обновляет
   /api/v1/admin/data-quality. Читателей у view нет ни одного — живая ручка строит
   свой запрос. PR с тезисом «ложный показатель хуже отсутствующего» не имеет права
   переносить в прод ложное утверждение о читателе.

3. Лимит выдачи 20 → 50: первые 20 строк по started_at на три четверти —
   сердцебиение proxy_healthcheck (1631 из 3245), часовой сбор мог не поместиться.
   Привязка к вкладке НЕ возвращается.

4. Тест «действующее определение view» искал маркер подстрокой с OR REPLACE —
   миграция с обычным CREATE VIEW или парой DROP+CREATE была бы невидима, и тест
   проверял бы 214, пока показатель уже вернулся. Заменено регуляркой на обе формы.

Фальсификация трёх новых тестов патч-методом — все три красные. Полный прогон
3490 passed / 9 skipped, tsc --noEmit чистый.
Author
Collaborator

Второй коммит 3c5f535e — по всем четырём пунктам ревью.

1. Кнопка отмены — гард, не отдельная задача. Поставил его в бэкенде, а не в UI, потому что дыра шире, чем рендер кнопки: ручки отмены source не проверяют вовсе, и ручной POST снимал бы has_running_run-guard даже при спрятанной кнопке. Гейт — на общем узле всех пяти ручек: scrape_runs.honors_cancel(source) + отказ в mark_cancelled (лог + False, без вранья в статусе). Флаг cancellable отдаётся в строке, UI по нему прячет кнопку.

Состав набора вывел из call-site'ов runs.is_cancelled, а не из слова «свип»: *city_sweep* (все площадки и города, включая _nizhniy_tagil), *full_load* (включая _exhaustive), avito_newbuilding_sweep, rosreestr_dkp_import (он тоже опрашивает — в scheduler.py, per-batch). yandex_newbuilding_sweep в набор НЕ входит — его таск отмену не опрашивает, поэтому правило «любой *_sweep» было бы ровно тем же багом с другой стороны. Это зафиксировано тестом.

2. Комментарий view — заменил на честный: «KPI-снимок для РУЧНЫХ psql-запросов, читателей в коде нет (проверено #2674): /api/v1/admin/scraper/data-quality считает свои метрики сам и этот view не трогает».

3. Лимит 20 → 50 в useScraperRuns + подпись под заголовком. Привязку к вкладке не возвращал.

4. Тест «действующее определение» — маркер стал регуляркой CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+v_data_quality\b, ловит и CREATE VIEW, и пару DROP+CREATE.

Тесты. Три новых (honors_cancel набор, cancellable в строке, отказ mark_cancelled) — фальсификация патч-методом, все три красные без фикса. Правил один существующий: test_city_sweep.py::test_scrape_runs_mark_cancelled_returns_bool кормил голый MagicMock(), у которого .source — тоже мок; теперь source="avito_city_sweep" с комментарием про новую предпосылку. Полный прогон 3490 passed, 9 skipped, tsc --noEmit чистый.

Отдельно, для протокола: npm run lint локально не запускается — @eslint/eslintrc отсутствует в node_modules и в главном дереве тоже (состояние pre-existing, не от этого PR). В CI ставится заново, там и проверится.

Второй коммит `3c5f535e` — по всем четырём пунктам ревью. **1. Кнопка отмены — гард, не отдельная задача.** Поставил его в бэкенде, а не в UI, потому что дыра шире, чем рендер кнопки: ручки отмены source не проверяют вовсе, и ручной POST снимал бы `has_running_run`-guard даже при спрятанной кнопке. Гейт — на общем узле всех пяти ручек: `scrape_runs.honors_cancel(source)` + отказ в `mark_cancelled` (лог + `False`, без вранья в статусе). Флаг `cancellable` отдаётся в строке, UI по нему прячет кнопку. Состав набора вывел из call-site'ов `runs.is_cancelled`, а не из слова «свип»: `*city_sweep*` (все площадки и города, включая `_nizhniy_tagil`), `*full_load*` (включая `_exhaustive`), `avito_newbuilding_sweep`, `rosreestr_dkp_import` (он тоже опрашивает — в `scheduler.py`, per-batch). **`yandex_newbuilding_sweep` в набор НЕ входит** — его таск отмену не опрашивает, поэтому правило «любой `*_sweep`» было бы ровно тем же багом с другой стороны. Это зафиксировано тестом. **2. Комментарий view** — заменил на честный: «KPI-снимок для РУЧНЫХ psql-запросов, читателей в коде нет (проверено #2674): `/api/v1/admin/scraper/data-quality` считает свои метрики сам и этот view не трогает». **3. Лимит 20 → 50** в `useScraperRuns` + подпись под заголовком. Привязку к вкладке не возвращал. **4. Тест «действующее определение»** — маркер стал регуляркой `CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+v_data_quality\b`, ловит и `CREATE VIEW`, и пару DROP+CREATE. **Тесты.** Три новых (`honors_cancel` набор, `cancellable` в строке, отказ `mark_cancelled`) — фальсификация патч-методом, все три красные без фикса. Правил один существующий: `test_city_sweep.py::test_scrape_runs_mark_cancelled_returns_bool` кормил голый `MagicMock()`, у которого `.source` — тоже мок; теперь `source="avito_city_sweep"` с комментарием про новую предпосылку. Полный прогон `3490 passed, 9 skipped`, `tsc --noEmit` чистый. **Отдельно, для протокола:** `npm run lint` локально не запускается — `@eslint/eslintrc` отсутствует в node_modules и в главном дереве тоже (состояние pre-existing, не от этого PR). В CI ставится заново, там и проверится.
bot-backend merged commit 0ed0f97ae2 into main 2026-08-05 23:01:31 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2684
No description provided.