Files
pg-stand-sync/CLAUDE.md
T
mikhail 9fdb02c72a TUI-пульт на Textual вместо текстового меню
Списки баз стенда и локальных дампов на одном экране, фильтр сужает их
на каждом набранном символе. Enter по базе открывает окно параметров
дампа с галочкой автоподнятия: снята — только dump, поднята — dump плюс
restore. Правки полей уезжают в копию конфига, config.json не трогается.

Ядро получило четыре хука для UI (лог, границы шагов, пароль, отмена),
run() читает вывод утилит построчно через Popen, поэтому журнал
наполняется во время работы pg_dump. Текстовый режим, CLI и --dry-run
ведут себя как раньше; без textual или вне tty пульт не запускается.

Журнал разворачивается на весь экран по F4 и сам на старте операции.
Раскладка адаптивная: narrow ниже 100 колонок, short ниже 32 строк.
Ctrl-хоткеи продублированы кириллицей — русская раскладка их не глушит.

Запуск через run.ps1 (venv, UTF-8-консоль, -Setup и -Tests).
Проверка — tests_tui.py: 46 тестов, TUI гоняется headless через Pilot.
2026-08-20 05:21:52 +03:00

127 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Что это
`pg_stand_sync.py` переносит базы PostgreSQL со стенда на локальный сервер через
`pg_dump -Fc``DROP/CREATE DATABASE``pg_restore -j`. Ядро (CLI и текстовое меню) —
только stdlib Python 3.10+; TUI-пульт лежит в `pg_stand_sync_tui.py` и требует `textual`
(`requirements.txt`). Без textual и вне tty всё работает по-старому текстом.
Утилиты берутся не из PATH, а из поставки pgAdmin 4 (`pg_bin_dir` в конфиге, psql 18) —
локальный сервер при этом PG 16, поэтому дамп со стенда новее 16 может частично
не примениться.
## Команды
```bash
python pg_stand_sync.py # TUI-пульт (основной режим)
python pg_stand_sync.py --no-tui # то же текстовым меню
python pg_stand_sync.py list # базы стенда — самая быстрая проверка связи
python pg_stand_sync.py --dry-run sync DB # печатает все команды, к серверам не ходит
```
Проверка изменений — `tests_tui.py` (pytest, TUI гоняется headless через `App.run_test()`
и `Pilot`, к стенду никто не ходит), плюс `--dry-run` для сверки состава командных строк и
`list` для реального подключения:
```bash
.venv/Scripts/python.exe -m pytest tests_tui.py -q
```
⚠️ Запускать TUI в терминале агента нельзя — сессия заблокируется. Всё проверяется только
через Pilot и `--dry-run`. Pylance в строгом режиме шумит десятками «частично неизвестных
типов» — это ожидаемо, аннотации намеренно нестрогие.
Стенд — `postgresql.lan` (`postgres.lan` не резолвится), PG 16; локальный сервер — PG 15,
утилиты из pgAdmin — 18, отсюда шум `unrecognized configuration parameter` при restore
(объясняется строкой `NEWER_CLIENT_HINT`, данные не страдают).
Пароль стенда лежит в `config.json` (файл в `.gitignore`), поэтому неинтерактивные проверки
проходят молча. Если его там нет — задать `$env:STAND_PGPASSWORD`: без пароля и без tty
`getpass` подвесил бы скрипт.
## Устройство скрипта
Слои, снизу вверх, — правки почти всегда касаются одного из них:
```
conn_env(node,label,db) libpq-переменные: пароль только через PGPASSWORD, никогда в argv
└─ resolve_password() config.password → password_env → хук UI → getpass (кэш на процесс)
run() / capture() Popen + построчное чтение; убиваемы через request_cancel()
no_prompt(env) --no-password, когда пароля нет: иначе psql ждёт ввода вечно
dump / recreate_target / restore / post_sql шаги, все принимают dry_run
do_dump / do_restore / do_sync сценарии, ими пользуются оба режима
pick() + interactive() текстовый режим; CLI-ветка в main() вызывает те же do_*
tui_enabled(args) единственная точка решения о режиме
└─ pg_stand_sync_tui.run_tui() пульт на Textual, те же do_*
```
Четыре необязательных хука — всё, что ядро знает об UI. Пока они не установлены, текстовый
вывод не отличается от прежнего ни на символ:
```ini
set_sink(fn) ; fn(kind, text), kind = log | warn | out; иначе print в stdout
set_progress(fn) ; fn(event, what, elapsed), event = start | done | fail
set_password_prompt(fn) ; fn(label, node) вместо getpass; вернул None — Cancelled
request_cancel() ; terminate + taskkill /T /F дерева; это kill, а не rollback
matches(text, query) ; общий фильтр для pick() и обоих списков TUI
```
Ключевые инварианты:
- Диалог `DbOpModal` — единственный вход в операции над базой: галочка автоподнятия решает
`kind` (`dump` или `sync`), а правки полей уезжают в `OpSpec.overrides` и накладываются
копией конфига в `op_cfg()`. Файл `config.json` пульт не переписывает, значения живут
до выхода в `DashboardScreen.op_defaults`.
- Имя базы — параметр, а не поле конфига. `source` описывает только сервер
(`maintenance_database` — куда подключаться, чтобы прочитать `pg_database`),
`target.database: null` означает «локально как на стенде»; переопределяется `--target-db`.
- Любая новая операция добавляется как `do_*` и подключается в оба режима — в `interactive()`
и в `main()`. Дублировать логику в ветке меню нельзя.
- `dry_run` протаскивается до `run()`, который в этом режиме только печатает команду. Новый
вызов внешней утилиты обязан идти через `run`/`capture`, иначе `--dry-run` соврёт.
- `pick()` — способ выбора из списка в текстовом режиме: текст фильтрует, число выбирает,
`multi=True` разрешает `2,5,7`, `q` поднимает `Cancelled` (в меню — возврат, не выход).
Fallback-выбор в CLI-командах (`sync` без имени базы) остаётся текстовым даже при
установленном textual: TUI посреди CLI-команды сломал бы pipe-сценарии.
- Текст `StepError``«{what} завершился с кодом {N}»` — менять нельзя: на него завязано
глушение ошибок `pg_restore`.
- Новый `Ctrl`-хоткей добавляется только через `with_ru_layout([...])`: при русской раскладке
терминал присылает `ctrl+в` вместо `ctrl+d`, и голый биндинг молча не срабатывает. Полноту
дублей проверяет тест, перебирающий `PgSyncApp.BINDINGS`.
- В TUI любой `Ctrl+…`-хоткей объявляется `priority=True`: `Input` биндит `ctrl+a`, `ctrl+d`,
`ctrl+u`, `ctrl+x`, `ctrl+c` — без приоритета действия молча не сработают в фильтре.
`Ctrl+C` в Textual 8 по умолчанию копирует текст, а не выходит.
- Отмена (`Ctrl+X`) — это kill дочерних процессов, отката нет: если `DROP DATABASE` уже
прошёл, локальная база останется пустой, и UI обязан сказать это прямым текстом.
`request_cancel()` зовётся только из потока (в цикле событий он вешает UI на секунды),
а флаг отмены снимается только там, где воркер точно не в ядре, — в `begin()` и в
`on_op_done()`. В `on_unmount()` его снимать нельзя: при выходе рабочий поток ещё жив и со
снятым флагом спокойно доработает следующий `DROP DATABASE` уже без UI.
- `target_db=None` в TUI не значит «как на стенде»: `do_sync` считает
`target_db or cfg.target.database or dbname`. Поэтому sync нескольких баз запрещён,
когда `target.database` задан, — иначе они слились бы в одну.
- Строки списков строятся через `room_for()`, который вычитает `SCROLLBAR_ROOM = 2`: сам
скроллбар в `content_size` не учтён, и строка ровно по ширине переносит хвост («1277 MB»
рвётся на «1277» и «MB»). Проверяется тестом со `scrollbar-gutter: stable`.
- Раскладка: `narrow` (<100 колонок) и `short` (<32 строк) вешаются руками в `on_resize`;
`Horizontal` со сменой `layout: vertical` не раздаёт детям ни `fr`, ни проценты по высоте,
поэтому в узком режиме правая колонка задана в строках. `.zoom-log #bottom` требует
`!important` — равная по специфичности `.short` иначе побеждает.
- `F4` разворачивает журнал; `begin()` делает это сам и помечает `_zoom_auto`, чтобы
`on_op_done` свернул обратно. Ручной `F4` снимает флаг — дальше раскладкой владеет человек.
- Счётчик «показано/всего» пишется в `border_subtitle` панели, а не списка: у `OptionList`
в CSS `border: none`, а Textual рисует подпись только в строке рамки. Ширина строк списков
и плана считается от `content_size` — фиксированные колонки переносятся и рвут вёрстку.
- `pg_restore` без `--exit-on-error` возвращает 1 на игнорируемых ошибках, поэтому его
`StepError` глотается и логируется как предупреждение — не «чинить» это молча.
- Имя файла дампа `<db>-YYYYmmdd-HHMMSS.dump`: по нему `db_from_dump_name()` восстанавливает
имя базы для `restore` без `--target-db`. Формат менять только вместе с `STAMP_RE`.
- Реконфигурация stdout/stderr в UTF-8 в начале файла нужна для русского лога в cp866-консоли
Windows; вывод дочерних процессов читается с `errors="replace"`.
`config.json` (рабочий, с паролями) в `.gitignore` — структура правится в
`config.example.json`, оба файла держать в согласии.