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

11 KiB
Raw Blame History

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 -FcDROP/CREATE DATABASEpg_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 может частично не примениться.

Команды

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 для реального подключения:

.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. Пока они не установлены, текстовый вывод не отличается от прежнего ни на символ:

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, оба файла держать в согласии.