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 default_yes
return val.startswith('y') 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(): def pause():
input(f'\n {dim("Press Enter to continue...")}') input(f'\n {dim("Press Enter to continue...")}')
@ -226,10 +233,10 @@ def show_status(cfg):
def action_configure(cfg): def action_configure(cfg):
"""Write or update .env with database connection details.""" """Write or update .env with database connection details."""
if cfg: 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.') print(f' Current settings will be shown as defaults.')
else: 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' {ENV_FILE} does not exist yet.')
print(f' If the target database does not exist or the user cannot connect,') 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"]}') ok(f'Successfully connected to database "{new_cfg["DB_NAME"]}" on {new_cfg["DB_HOST"]}')
else: else:
warn(f'Cannot connect to database "{new_cfg["DB_NAME"]}" on {new_cfg["DB_HOST"]} with the provided credentials.') 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): 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') info(f'{ENV_FILE} was not written — no changes made')
return cfg return cfg
@ -310,6 +321,46 @@ def action_configure(cfg):
print() print()
write_env(new_cfg) write_env(new_cfg)
ok(f'Settings written to {ENV_FILE}') 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 return new_cfg
@ -324,12 +375,17 @@ def action_deploy_schema(cfg):
print(f' Source file : {schema_file}') print(f' Source file : {schema_file}')
print(f' Target : "dataflow" schema in {db_location}') 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() print()
if not can_connect(cfg): if not can_connect(cfg):
err(f'Cannot connect to {db_location} — check credentials in {ENV_FILE}') err(f'Cannot connect to {db_location} — check credentials in {ENV_FILE}')
return 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): if schema_deployed(cfg):
warn(f'"dataflow" schema already exists in {db_location}.') warn(f'"dataflow" schema already exists in {db_location}.')
warn(f'Redeploying will DROP and recreate the schema, deleting all data.') 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') info('Cancelled — no changes made')
return return
else: 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') info('Cancelled — no changes made')
return return
@ -372,7 +428,8 @@ def action_deploy_functions(cfg):
info('Cancelled — no changes made') info('Cancelled — no changes made')
return 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') info('Cancelled — no changes made')
return return
@ -397,6 +454,7 @@ def action_build_ui():
err(f'{ui_dir}/package.json not found — is the ui directory present?') err(f'{ui_dir}/package.json not found — is the ui directory present?')
return 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): if not confirm(f'Build UI from {ui_dir} into {out_dir}?', default_yes=False):
info('Cancelled — no changes made') info('Cancelled — no changes made')
return return
@ -417,6 +475,16 @@ def action_setup_nginx(cfg):
port = cfg.get('API_PORT', '3020') if cfg else '3020' 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' 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(f' The site will proxy incoming HTTP requests to the dataflow API on localhost:{port}.')
print() print()
@ -485,6 +553,12 @@ server {{
f.write(conf) f.write(conf)
tmp = f.name 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): if not confirm(f'Write nginx config to {conf_path} and reload nginx (requires sudo)?', default_yes=False):
os.unlink(tmp) os.unlink(tmp)
info('Cancelled — no changes made') info('Cancelled — no changes made')
@ -512,6 +586,7 @@ server {{
if not cert_exists: if not cert_exists:
warn(f'No SSL certificate found for {domain} — site is HTTP only.') 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?'): 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}...') print(f' Running certbot for {domain}...')
r = sudo_run(['certbot', '--nginx', '-d', domain, r = sudo_run(['certbot', '--nginx', '-d', domain,
'--non-interactive', '--agree-tos', '--redirect', '--non-interactive', '--agree-tos', '--redirect',
@ -538,6 +613,11 @@ def action_install_service():
err(f'Service unit file not found: {SERVICE_SRC}') err(f'Service unit file not found: {SERVICE_SRC}')
return 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): 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') info('Cancelled — no changes made')
return return
@ -573,9 +653,9 @@ def action_restart_service():
print(f' Service file : {SERVICE_FILE}') print(f' Service file : {SERVICE_FILE}')
print(f' Current state : {current_state}') print(f' Current state : {current_state}')
print(f' Action : sudo systemctl {action} dataflow')
print() print()
show_commands([['sudo', 'systemctl', action, 'dataflow']])
if not confirm(f'{action.capitalize()} dataflow.service?', default_yes=False): if not confirm(f'{action.capitalize()} dataflow.service?', default_yes=False):
info('Cancelled — no changes made') info('Cancelled — no changes made')
return return
@ -597,13 +677,13 @@ def action_stop_service():
header('Stop dataflow.service') header('Stop dataflow.service')
print(f' Service file : {SERVICE_FILE}') print(f' Service file : {SERVICE_FILE}')
print(f' Action : sudo systemctl stop dataflow')
print() print()
if not service_running(): if not service_running():
info('dataflow.service is not currently running — nothing to stop') info('dataflow.service is not currently running — nothing to stop')
return return
show_commands([['sudo', 'systemctl', 'stop', 'dataflow']])
if not confirm('Stop dataflow.service?', default_yes=False): if not confirm('Stop dataflow.service?', default_yes=False):
info('Cancelled — no changes made') info('Cancelled — no changes made')
return return
@ -616,9 +696,9 @@ def action_stop_service():
# ── Main menu ───────────────────────────────────────────────────────────────── # ── Main menu ─────────────────────────────────────────────────────────────────
MENU = [ MENU = [
('Configure database connection settings (.env)', action_configure), ('Database configuration and deployment dialog (.env)', action_configure),
('Deploy "dataflow" schema (database/schema.sql)', action_deploy_schema), ('Redeploy "dataflow" schema only (database/schema.sql)', action_deploy_schema),
('Deploy SQL functions (database/functions.sql)', action_deploy_functions), ('Redeploy SQL functions only (database/functions.sql)', action_deploy_functions),
('Build UI (ui/ → public/)', action_build_ui), ('Build UI (ui/ → public/)', action_build_ui),
('Set up nginx reverse proxy', action_setup_nginx), ('Set up nginx reverse proxy', action_setup_nginx),
('Install dataflow systemd service unit', action_install_service), ('Install dataflow systemd service unit', action_install_service),