From 1edb99848724fa3e14bc339e2de94e9ba9f9adba Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sun, 5 Apr 2026 17:28:26 -0400 Subject: [PATCH] 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 --- manage.py | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 9 deletions(-) diff --git a/manage.py b/manage.py index eba10bb..ca4bc47 100755 --- a/manage.py +++ b/manage.py @@ -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', '', '-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', '', '-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', '', 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),