manage.py: show commands before confirms, fold schema/fn into step 1, nginx guard

- Show exact commands that will be run before each confirm prompt
- Step 1 dialog now offers schema and function deployment after writing .env
- Steps 2/3 relabeled as 'Redeploy only' for standalone use
- Option 5 (nginx) detects existing config and warns before overwriting
- Option 1 menu label clarified as 'Database configuration and deployment dialog'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-04-05 17:28:26 -04:00
parent a3c7be61d0
commit 1edb998487

View File

@ -59,6 +59,13 @@ def confirm(label, default_yes=True):
return default_yes
return val.startswith('y')
def show_commands(cmds):
label = 'Command that will be run:' if len(cmds) == 1 else 'Commands that will be run:'
print(f' {label}')
for cmd in cmds:
print(f' {dim(" ".join(str(c) for c in cmd))}')
print()
def pause():
input(f'\n {dim("Press Enter to continue...")}')
@ -226,10 +233,10 @@ def show_status(cfg):
def action_configure(cfg):
"""Write or update .env with database connection details."""
if cfg:
header(f'Edit database connection settings in {ENV_FILE}')
header(f'Database configuration and deployment dialog — editing {ENV_FILE}')
print(f' Current settings will be shown as defaults.')
else:
header(f'Create {ENV_FILE} with database connection settings')
header(f'Database configuration and deployment dialog — creating {ENV_FILE}')
print(f' {ENV_FILE} does not exist yet.')
print(f' If the target database does not exist or the user cannot connect,')
@ -255,6 +262,10 @@ def action_configure(cfg):
ok(f'Successfully connected to database "{new_cfg["DB_NAME"]}" on {new_cfg["DB_HOST"]}')
else:
warn(f'Cannot connect to database "{new_cfg["DB_NAME"]}" on {new_cfg["DB_HOST"]} with the provided credentials.')
show_commands([
['psql', '-U', '<admin>', '-h', new_cfg['DB_HOST'], '-p', new_cfg['DB_PORT'], '-d', 'postgres', '-c', f"CREATE USER {new_cfg['DB_USER']} ... (if user does not exist)"],
['psql', '-U', '<admin>', '-h', new_cfg['DB_HOST'], '-p', new_cfg['DB_PORT'], '-d', 'postgres', '-c', f"CREATE DATABASE {new_cfg['DB_NAME']} ... (if database does not exist)"],
])
if not confirm(f'Use PostgreSQL admin credentials to create the database user and/or database?', default_yes=False):
info(f'{ENV_FILE} was not written — no changes made')
return cfg
@ -310,6 +321,46 @@ def action_configure(cfg):
print()
write_env(new_cfg)
ok(f'Settings written to {ENV_FILE}')
db_location = f'database "{new_cfg["DB_NAME"]}" on {new_cfg["DB_HOST"]}:{new_cfg["DB_PORT"]}'
schema_file = ROOT / 'database' / 'schema.sql'
functions_file = ROOT / 'database' / 'functions.sql'
# Offer schema deployment
print()
sd = schema_deployed(new_cfg)
if sd:
warn(f'"dataflow" schema already exists in {db_location}.')
show_commands([['psql', '-U', new_cfg['DB_USER'], '-h', new_cfg['DB_HOST'], '-p', new_cfg['DB_PORT'], '-d', new_cfg['DB_NAME'], '-f', str(schema_file)]])
if confirm(f'Redeploy "dataflow" schema? (will reset all data)', default_yes=False):
print(f' Running {schema_file} against {db_location}...')
r = psql_file(new_cfg, schema_file)
if r.returncode == 0:
ok(f'"dataflow" schema redeployed into {db_location}')
else:
err(f'Schema deployment failed:\n{r.stderr}')
else:
show_commands([['psql', '-U', new_cfg['DB_USER'], '-h', new_cfg['DB_HOST'], '-p', new_cfg['DB_PORT'], '-d', new_cfg['DB_NAME'], '-f', str(schema_file)]])
if confirm(f'Deploy "dataflow" schema into {db_location}?', default_yes=False):
print(f' Running {schema_file} against {db_location}...')
r = psql_file(new_cfg, schema_file)
if r.returncode == 0:
ok(f'"dataflow" schema deployed into {db_location}')
else:
err(f'Schema deployment failed:\n{r.stderr}')
return new_cfg
# Offer function deployment
print()
show_commands([['psql', '-U', new_cfg['DB_USER'], '-h', new_cfg['DB_HOST'], '-p', new_cfg['DB_PORT'], '-d', new_cfg['DB_NAME'], '-f', str(functions_file)]])
if confirm(f'Deploy SQL functions into {db_location}?', default_yes=False):
print(f' Running {functions_file} against {db_location}...')
r = psql_file(new_cfg, functions_file)
if r.returncode == 0:
ok(f'SQL functions deployed into {db_location}')
else:
err(f'Function deployment failed:\n{r.stderr}')
return new_cfg
@ -324,12 +375,17 @@ def action_deploy_schema(cfg):
print(f' Source file : {schema_file}')
print(f' Target : "dataflow" schema in {db_location}')
print(f' Scope : creates the "dataflow" schema and tables inside the existing')
print(f' database — does NOT create the database or PostgreSQL user.')
print(f' Run option 1 first if the database or user does not exist yet.')
print()
if not can_connect(cfg):
err(f'Cannot connect to {db_location} — check credentials in {ENV_FILE}')
return
show_commands([['psql', '-U', cfg['DB_USER'], '-h', cfg['DB_HOST'], '-p', cfg['DB_PORT'], '-d', cfg['DB_NAME'], '-q', '-f', str(schema_file)]])
if schema_deployed(cfg):
warn(f'"dataflow" schema already exists in {db_location}.')
warn(f'Redeploying will DROP and recreate the schema, deleting all data.')
@ -337,7 +393,7 @@ def action_deploy_schema(cfg):
info('Cancelled — no changes made')
return
else:
if not confirm(f'Deploy "dataflow" schema from {schema_file} into {db_location}?', default_yes=False):
if not confirm(f'Deploy "dataflow" schema into {db_location}?', default_yes=False):
info('Cancelled — no changes made')
return
@ -372,7 +428,8 @@ def action_deploy_functions(cfg):
info('Cancelled — no changes made')
return
if not confirm(f'Deploy SQL functions from {functions_file} into {db_location}?', default_yes=False):
show_commands([['psql', '-U', cfg['DB_USER'], '-h', cfg['DB_HOST'], '-p', cfg['DB_PORT'], '-d', cfg['DB_NAME'], '-q', '-f', str(functions_file)]])
if not confirm(f'Deploy SQL functions into {db_location}?', default_yes=False):
info('Cancelled — no changes made')
return
@ -397,6 +454,7 @@ def action_build_ui():
err(f'{ui_dir}/package.json not found — is the ui directory present?')
return
show_commands([['npm', 'run', 'build', f' (in {ui_dir})']])
if not confirm(f'Build UI from {ui_dir} into {out_dir}?', default_yes=False):
info('Cancelled — no changes made')
return
@ -417,6 +475,16 @@ def action_setup_nginx(cfg):
port = cfg.get('API_PORT', '3020') if cfg else '3020'
existing_domain = nginx_domain(port)
if existing_domain:
warn(f'nginx is already configured for this service.')
info(f' Current config: {existing_domain} → localhost:{port}')
print()
if not confirm(f'Reconfigure nginx (overwrite existing config for {existing_domain})?', default_yes=False):
info('Cancelled — no changes made')
return
print()
print(f' This will write an nginx site config and reload nginx (requires sudo).')
print(f' The site will proxy incoming HTTP requests to the dataflow API on localhost:{port}.')
print()
@ -485,6 +553,12 @@ server {{
f.write(conf)
tmp = f.name
show_commands([
['sudo', 'cp', '<config>', str(conf_path)],
['sudo', 'chmod', '644', str(conf_path)],
['sudo', 'nginx', '-t'],
['sudo', 'systemctl', 'reload', 'nginx'],
])
if not confirm(f'Write nginx config to {conf_path} and reload nginx (requires sudo)?', default_yes=False):
os.unlink(tmp)
info('Cancelled — no changes made')
@ -512,6 +586,7 @@ server {{
if not cert_exists:
warn(f'No SSL certificate found for {domain} — site is HTTP only.')
if confirm(f'Run certbot to obtain an SSL certificate for {domain} and switch to HTTPS?'):
show_commands([['sudo', 'certbot', '--nginx', '-d', domain, '--non-interactive', '--agree-tos', '--redirect', '-m', f'admin@{domain}']])
print(f' Running certbot for {domain}...')
r = sudo_run(['certbot', '--nginx', '-d', domain,
'--non-interactive', '--agree-tos', '--redirect',
@ -538,6 +613,11 @@ def action_install_service():
err(f'Service unit file not found: {SERVICE_SRC}')
return
show_commands([
['sudo', 'cp', str(SERVICE_SRC), str(SERVICE_FILE)],
['sudo', 'systemctl', 'daemon-reload'],
['sudo', 'systemctl', 'enable', 'dataflow'],
])
if not confirm(f'Copy {SERVICE_SRC.name} to {SERVICE_FILE} and enable it with systemd (requires sudo)?', default_yes=False):
info('Cancelled — no changes made')
return
@ -573,9 +653,9 @@ def action_restart_service():
print(f' Service file : {SERVICE_FILE}')
print(f' Current state : {current_state}')
print(f' Action : sudo systemctl {action} dataflow')
print()
show_commands([['sudo', 'systemctl', action, 'dataflow']])
if not confirm(f'{action.capitalize()} dataflow.service?', default_yes=False):
info('Cancelled — no changes made')
return
@ -597,13 +677,13 @@ def action_stop_service():
header('Stop dataflow.service')
print(f' Service file : {SERVICE_FILE}')
print(f' Action : sudo systemctl stop dataflow')
print()
if not service_running():
info('dataflow.service is not currently running — nothing to stop')
return
show_commands([['sudo', 'systemctl', 'stop', 'dataflow']])
if not confirm('Stop dataflow.service?', default_yes=False):
info('Cancelled — no changes made')
return
@ -616,9 +696,9 @@ def action_stop_service():
# ── Main menu ─────────────────────────────────────────────────────────────────
MENU = [
('Configure database connection settings (.env)', action_configure),
('Deploy "dataflow" schema (database/schema.sql)', action_deploy_schema),
('Deploy SQL functions (database/functions.sql)', action_deploy_functions),
('Database configuration and deployment dialog (.env)', action_configure),
('Redeploy "dataflow" schema only (database/schema.sql)', action_deploy_schema),
('Redeploy SQL functions only (database/functions.sql)', action_deploy_functions),
('Build UI (ui/ → public/)', action_build_ui),
('Set up nginx reverse proxy', action_setup_nginx),
('Install dataflow systemd service unit', action_install_service),