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.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# 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`, оба файла держать в согласии.
|
||||
Reference in New Issue
Block a user