Improve manage.py clarity and verbosity throughout
Every status line, action header, confirm prompt, and ok/err message now names exactly what it refers to — schema name, database, host, file paths, and systemd commands. Menu items include source/target context. No ambiguous shorthand anywhere. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a26a7643e4
commit
b93751e3d1
374
manage.py
374
manage.py
@ -95,7 +95,7 @@ def psql_env(cfg):
|
|||||||
e['PGPASSWORD'] = cfg['DB_PASSWORD']
|
e['PGPASSWORD'] = cfg['DB_PASSWORD']
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def psql_run(cfg, sql, db=None, check=False):
|
def psql_run(cfg, sql, db=None):
|
||||||
db = db or cfg['DB_NAME']
|
db = db or cfg['DB_NAME']
|
||||||
cmd = ['psql', '-U', cfg['DB_USER'], '-h', cfg['DB_HOST'],
|
cmd = ['psql', '-U', cfg['DB_USER'], '-h', cfg['DB_HOST'],
|
||||||
'-p', str(cfg['DB_PORT']), '-d', db, '-tAc', sql]
|
'-p', str(cfg['DB_PORT']), '-d', db, '-tAc', sql]
|
||||||
@ -137,8 +137,7 @@ def service_running():
|
|||||||
return r.stdout.strip() == 'active'
|
return r.stdout.strip() == 'active'
|
||||||
|
|
||||||
def ui_built():
|
def ui_built():
|
||||||
index = ROOT / 'public' / 'index.html'
|
return (ROOT / 'public' / 'index.html').exists()
|
||||||
return index.exists()
|
|
||||||
|
|
||||||
def ui_build_time():
|
def ui_build_time():
|
||||||
index = ROOT / 'public' / 'index.html'
|
index = ROOT / 'public' / 'index.html'
|
||||||
@ -170,203 +169,270 @@ def sudo_run(args, **kwargs):
|
|||||||
# ── Status ────────────────────────────────────────────────────────────────────
|
# ── Status ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def show_status(cfg):
|
def show_status(cfg):
|
||||||
header('Status')
|
header('Current Status')
|
||||||
|
|
||||||
if not cfg:
|
if not cfg:
|
||||||
warn('No .env — not configured')
|
warn(f'Not configured — {ENV_FILE} does not exist')
|
||||||
|
info('Run option 1 to create it.')
|
||||||
|
print()
|
||||||
return
|
return
|
||||||
|
|
||||||
port = cfg.get('API_PORT', '3020')
|
port = cfg.get('API_PORT', '3020')
|
||||||
|
db_conn = f"{cfg['DB_USER']}@{cfg['DB_HOST']}:{cfg['DB_PORT']}/{cfg['DB_NAME']}"
|
||||||
|
db_location = f"database \"{cfg['DB_NAME']}\" on {cfg['DB_HOST']}:{cfg['DB_PORT']}"
|
||||||
|
|
||||||
# Database
|
# Database connection
|
||||||
connected = can_connect(cfg)
|
connected = can_connect(cfg)
|
||||||
db_label = f"{cfg['DB_USER']}@{cfg['DB_HOST']}:{cfg['DB_PORT']}/{cfg['DB_NAME']}"
|
conn_status = green('connected') if connected else red('cannot connect')
|
||||||
status = green('connected') if connected else red('cannot connect')
|
print(f' Database connection {dim(db_conn)} {conn_status}')
|
||||||
print(f' Database {dim(db_label)} {status}')
|
|
||||||
|
|
||||||
|
# Schema and functions (only meaningful if connected)
|
||||||
if connected:
|
if connected:
|
||||||
sd = schema_deployed(cfg)
|
sd = schema_deployed(cfg)
|
||||||
fn = functions_deployed(cfg)
|
fn = functions_deployed(cfg)
|
||||||
print(f' Schema {green("deployed") if sd else red("not deployed")}')
|
schema_status = green('deployed') if sd else red('not deployed')
|
||||||
print(f' Functions {green("deployed") if fn else red("not deployed")}')
|
fn_status = green('deployed') if fn else red('not deployed')
|
||||||
|
print(f' "dataflow" schema {schema_status} {dim(f"in {db_location}")}')
|
||||||
|
print(f' SQL functions {fn_status} {dim(f"in {db_location}")}')
|
||||||
else:
|
else:
|
||||||
print(f' Schema {dim("unknown")}')
|
print(f' "dataflow" schema {dim("unknown — cannot connect to " + db_location)}')
|
||||||
print(f' Functions {dim("unknown")}')
|
print(f' SQL functions {dim("unknown — cannot connect to " + db_location)}')
|
||||||
|
|
||||||
# UI
|
# UI build
|
||||||
|
public_dir = ROOT / 'public'
|
||||||
if ui_built():
|
if ui_built():
|
||||||
print(f' UI {green("built")} {dim(ui_build_time())}')
|
print(f' UI build {green("built")} {dim(f"{public_dir} ({ui_build_time()})")}')
|
||||||
else:
|
else:
|
||||||
print(f' UI {red("not built")}')
|
print(f' UI build {red("not built")} {dim(f"run option 4 to build into {public_dir}")}')
|
||||||
|
|
||||||
# Service
|
# Systemd service
|
||||||
if service_installed():
|
if service_installed():
|
||||||
running = service_running()
|
svc_status = green('running') if service_running() else yellow('stopped')
|
||||||
print(f' Service {green("running") if running else yellow("stopped")}')
|
print(f' dataflow.service {svc_status} {dim(str(SERVICE_FILE))}')
|
||||||
else:
|
else:
|
||||||
print(f' Service {dim("not installed")}')
|
print(f' dataflow.service {red("not installed")} {dim(f"{SERVICE_FILE} does not exist")}')
|
||||||
|
|
||||||
# Nginx
|
# Nginx proxy
|
||||||
domain = nginx_domain(port)
|
domain = nginx_domain(port)
|
||||||
if domain:
|
if domain:
|
||||||
print(f' Nginx {green(domain)}')
|
print(f' Nginx reverse proxy {green("configured")} {dim(f"{domain} → localhost:{port}")}')
|
||||||
else:
|
else:
|
||||||
print(f' Nginx {dim("not configured")}')
|
print(f' Nginx reverse proxy {dim(f"not configured — no site proxying to localhost:{port}")}')
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# ── Actions ───────────────────────────────────────────────────────────────────
|
# ── Actions ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def action_configure(cfg):
|
def action_configure(cfg):
|
||||||
"""Set up or reconfigure .env — handles new and existing databases."""
|
"""Write or update .env with database connection details."""
|
||||||
header('Configure Database')
|
if cfg:
|
||||||
|
header(f'Edit database connection settings in {ENV_FILE}')
|
||||||
|
print(f' Current settings will be shown as defaults.')
|
||||||
|
else:
|
||||||
|
header(f'Create {ENV_FILE} with database connection settings')
|
||||||
|
print(f' {ENV_FILE} does not exist yet.')
|
||||||
|
|
||||||
|
print(f' If the target database does not exist or the user cannot connect,')
|
||||||
|
print(f' you will be prompted for PostgreSQL admin credentials to create them.')
|
||||||
|
print()
|
||||||
|
|
||||||
existing = cfg.copy() if cfg else {}
|
existing = cfg.copy() if cfg else {}
|
||||||
|
|
||||||
print(' Enter connection details. If the database does not exist, you will be')
|
|
||||||
print(' prompted for admin credentials to create it.')
|
|
||||||
print()
|
|
||||||
|
|
||||||
new_cfg = {}
|
new_cfg = {}
|
||||||
new_cfg['DB_HOST'] = prompt('Host', existing.get('DB_HOST', 'localhost'))
|
new_cfg['DB_HOST'] = prompt('PostgreSQL host', existing.get('DB_HOST', 'localhost'))
|
||||||
new_cfg['DB_PORT'] = prompt('Port', existing.get('DB_PORT', '5432'))
|
new_cfg['DB_PORT'] = prompt('PostgreSQL port', existing.get('DB_PORT', '5432'))
|
||||||
new_cfg['DB_NAME'] = prompt('Database', existing.get('DB_NAME', 'dataflow'))
|
new_cfg['DB_NAME'] = prompt('Database name', existing.get('DB_NAME', 'dataflow'))
|
||||||
new_cfg['DB_USER'] = prompt('User', existing.get('DB_USER', 'dataflow'))
|
new_cfg['DB_USER'] = prompt('Database user', existing.get('DB_USER', 'dataflow'))
|
||||||
new_cfg['DB_PASSWORD'] = prompt('Password', existing.get('DB_PASSWORD', ''), secret=True)
|
new_cfg['DB_PASSWORD'] = prompt('Database password', existing.get('DB_PASSWORD', ''), secret=True)
|
||||||
new_cfg['API_PORT'] = prompt('API port', existing.get('API_PORT', '3020'))
|
new_cfg['API_PORT'] = prompt('API port', existing.get('API_PORT', '3020'))
|
||||||
new_cfg['NODE_ENV'] = prompt('Environment', existing.get('NODE_ENV', 'production'))
|
new_cfg['NODE_ENV'] = prompt('Node environment', existing.get('NODE_ENV', 'production'))
|
||||||
|
|
||||||
|
db_conn = f"{new_cfg['DB_USER']}@{new_cfg['DB_HOST']}:{new_cfg['DB_PORT']}/{new_cfg['DB_NAME']}"
|
||||||
print()
|
print()
|
||||||
print(f' Testing connection to {new_cfg["DB_NAME"]}...')
|
print(f' Testing database connection as {db_conn}...')
|
||||||
|
|
||||||
if can_connect(new_cfg):
|
if can_connect(new_cfg):
|
||||||
ok('Connected')
|
ok(f'Successfully connected to database "{new_cfg["DB_NAME"]}" on {new_cfg["DB_HOST"]}')
|
||||||
else:
|
else:
|
||||||
warn('Cannot connect with those credentials.')
|
warn(f'Cannot connect to database "{new_cfg["DB_NAME"]}" on {new_cfg["DB_HOST"]} with the provided credentials.')
|
||||||
if not confirm('Create user/database using admin credentials?', default_yes=False):
|
if not confirm(f'Use PostgreSQL admin credentials to create the database user and/or database?', default_yes=False):
|
||||||
info('Cancelled — .env not written')
|
info(f'{ENV_FILE} was not written — no changes made')
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
print()
|
print()
|
||||||
admin = {}
|
admin = {}
|
||||||
admin['user'] = prompt('Admin username', 'postgres')
|
admin['user'] = prompt('PostgreSQL admin username', 'postgres')
|
||||||
admin['password'] = prompt('Admin password', secret=True)
|
admin['password'] = prompt('PostgreSQL admin password', secret=True)
|
||||||
admin['host'] = new_cfg['DB_HOST']
|
admin['host'] = new_cfg['DB_HOST']
|
||||||
admin['port'] = new_cfg['DB_PORT']
|
admin['port'] = new_cfg['DB_PORT']
|
||||||
|
|
||||||
# Test admin connection
|
print(f' Testing admin connection as {admin["user"]}@{admin["host"]}:{admin["port"]}...')
|
||||||
r = psql_admin(admin, 'SELECT 1')
|
r = psql_admin(admin, 'SELECT 1')
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
err(f'Cannot connect as {admin["user"]}')
|
err(f'Cannot connect to PostgreSQL as admin user "{admin["user"]}" on {admin["host"]}:{admin["port"]}')
|
||||||
return cfg
|
return cfg
|
||||||
|
ok(f'Admin connection successful')
|
||||||
|
|
||||||
# Create user
|
# Create user if needed
|
||||||
r = psql_admin(admin, f"SELECT 1 FROM pg_roles WHERE rolname='{new_cfg['DB_USER']}'")
|
r = psql_admin(admin, f"SELECT 1 FROM pg_roles WHERE rolname='{new_cfg['DB_USER']}'")
|
||||||
if '1' in r.stdout:
|
if '1' in r.stdout:
|
||||||
info(f'User {new_cfg["DB_USER"]} already exists')
|
info(f'PostgreSQL user "{new_cfg["DB_USER"]}" already exists — skipping creation')
|
||||||
else:
|
else:
|
||||||
|
print(f' Creating PostgreSQL user "{new_cfg["DB_USER"]}"...')
|
||||||
r = psql_admin(admin, f"CREATE USER {new_cfg['DB_USER']} WITH PASSWORD '{new_cfg['DB_PASSWORD']}'")
|
r = psql_admin(admin, f"CREATE USER {new_cfg['DB_USER']} WITH PASSWORD '{new_cfg['DB_PASSWORD']}'")
|
||||||
if r.returncode == 0:
|
if r.returncode == 0:
|
||||||
ok(f'User {new_cfg["DB_USER"]} created')
|
ok(f'PostgreSQL user "{new_cfg["DB_USER"]}" created')
|
||||||
else:
|
else:
|
||||||
err(f'Could not create user: {r.stderr.strip()}')
|
err(f'Could not create user "{new_cfg["DB_USER"]}": {r.stderr.strip()}')
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
# Create or grant access to database
|
# Create database or grant access to existing one
|
||||||
r = psql_admin(admin, f"SELECT 1 FROM pg_database WHERE datname='{new_cfg['DB_NAME']}'")
|
r = psql_admin(admin, f"SELECT 1 FROM pg_database WHERE datname='{new_cfg['DB_NAME']}'")
|
||||||
if '1' in r.stdout:
|
if '1' in r.stdout:
|
||||||
info(f'Database {new_cfg["DB_NAME"]} already exists — granting access')
|
print(f' Database "{new_cfg["DB_NAME"]}" already exists — granting CREATE access to "{new_cfg["DB_USER"]}"...')
|
||||||
psql_admin(admin, f"GRANT CREATE ON DATABASE {new_cfg['DB_NAME']} TO {new_cfg['DB_USER']}", db=new_cfg['DB_NAME'])
|
psql_admin(admin, f"GRANT CREATE ON DATABASE {new_cfg['DB_NAME']} TO {new_cfg['DB_USER']}", db=new_cfg['DB_NAME'])
|
||||||
|
ok(f'Access granted on database "{new_cfg["DB_NAME"]}" to user "{new_cfg["DB_USER"]}"')
|
||||||
else:
|
else:
|
||||||
|
print(f' Creating database "{new_cfg["DB_NAME"]}" owned by "{new_cfg["DB_USER"]}"...')
|
||||||
r = psql_admin(admin, f"CREATE DATABASE {new_cfg['DB_NAME']} OWNER {new_cfg['DB_USER']}")
|
r = psql_admin(admin, f"CREATE DATABASE {new_cfg['DB_NAME']} OWNER {new_cfg['DB_USER']}")
|
||||||
if r.returncode == 0:
|
if r.returncode == 0:
|
||||||
ok(f'Database {new_cfg["DB_NAME"]} created')
|
ok(f'Database "{new_cfg["DB_NAME"]}" created on {new_cfg["DB_HOST"]}')
|
||||||
else:
|
else:
|
||||||
err(f'Could not create database: {r.stderr.strip()}')
|
err(f'Could not create database "{new_cfg["DB_NAME"]}": {r.stderr.strip()}')
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
print(f' Verifying connection as {db_conn}...')
|
||||||
if not can_connect(new_cfg):
|
if not can_connect(new_cfg):
|
||||||
err('Still cannot connect after setup — check credentials')
|
err(f'Still cannot connect as {db_conn} after setup — check credentials and PostgreSQL logs')
|
||||||
return cfg
|
return cfg
|
||||||
ok('Connection verified')
|
ok(f'Connection to "{new_cfg["DB_NAME"]}" on {new_cfg["DB_HOST"]} verified')
|
||||||
|
|
||||||
|
print()
|
||||||
write_env(new_cfg)
|
write_env(new_cfg)
|
||||||
ok('.env written')
|
ok(f'Settings written to {ENV_FILE}')
|
||||||
return new_cfg
|
return new_cfg
|
||||||
|
|
||||||
|
|
||||||
def action_deploy_schema(cfg):
|
def action_deploy_schema(cfg):
|
||||||
header('Deploy Schema')
|
header('Deploy "dataflow" schema (database/schema.sql)')
|
||||||
if not cfg:
|
if not cfg:
|
||||||
err('No .env — run Configure first')
|
err(f'{ENV_FILE} not found — run option 1 to configure the database connection first')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
db_location = f'database "{cfg["DB_NAME"]}" on {cfg["DB_HOST"]}:{cfg["DB_PORT"]}'
|
||||||
|
schema_file = ROOT / 'database' / 'schema.sql'
|
||||||
|
|
||||||
|
print(f' Source file : {schema_file}')
|
||||||
|
print(f' Target : "dataflow" schema in {db_location}')
|
||||||
|
print()
|
||||||
|
|
||||||
if not can_connect(cfg):
|
if not can_connect(cfg):
|
||||||
err('Cannot connect to database')
|
err(f'Cannot connect to {db_location} — check credentials in {ENV_FILE}')
|
||||||
return
|
return
|
||||||
|
|
||||||
if schema_deployed(cfg):
|
if schema_deployed(cfg):
|
||||||
warn('Schema already deployed')
|
warn(f'"dataflow" schema already exists in {db_location}.')
|
||||||
if not confirm('Redeploy (this will reset all data)?', default_yes=False):
|
warn(f'Redeploying will DROP and recreate the schema, deleting all data.')
|
||||||
info('Cancelled')
|
if not confirm(f'Drop and redeploy "dataflow" schema in {db_location}?', default_yes=False):
|
||||||
|
info('Cancelled — no changes made')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if not confirm(f'Deploy "dataflow" schema from {schema_file} into {db_location}?', default_yes=False):
|
||||||
|
info('Cancelled — no changes made')
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f' Deploying schema to {cfg["DB_NAME"]}...')
|
print(f' Running {schema_file} against {db_location}...')
|
||||||
r = psql_file(cfg, ROOT / 'database' / 'schema.sql')
|
r = psql_file(cfg, schema_file)
|
||||||
if r.returncode == 0:
|
if r.returncode == 0:
|
||||||
ok('Schema deployed')
|
ok(f'"dataflow" schema deployed into {db_location}')
|
||||||
else:
|
else:
|
||||||
err(f'Failed:\n{r.stderr}')
|
err(f'Schema deployment failed:\n{r.stderr}')
|
||||||
|
|
||||||
|
|
||||||
def action_deploy_functions(cfg):
|
def action_deploy_functions(cfg):
|
||||||
header('Deploy Functions')
|
header('Deploy SQL functions (database/functions.sql)')
|
||||||
if not cfg:
|
if not cfg:
|
||||||
err('No .env — run Configure first')
|
err(f'{ENV_FILE} not found — run option 1 to configure the database connection first')
|
||||||
return
|
|
||||||
if not can_connect(cfg):
|
|
||||||
err('Cannot connect to database')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f' Deploying functions to {cfg["DB_NAME"]}...')
|
db_location = f'database "{cfg["DB_NAME"]}" on {cfg["DB_HOST"]}:{cfg["DB_PORT"]}'
|
||||||
r = psql_file(cfg, ROOT / 'database' / 'functions.sql')
|
functions_file = ROOT / 'database' / 'functions.sql'
|
||||||
|
|
||||||
|
print(f' Source file : {functions_file}')
|
||||||
|
print(f' Target : "dataflow" schema in {db_location}')
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not can_connect(cfg):
|
||||||
|
err(f'Cannot connect to {db_location} — check credentials in {ENV_FILE}')
|
||||||
|
return
|
||||||
|
|
||||||
|
if not schema_deployed(cfg):
|
||||||
|
warn(f'"dataflow" schema not found in {db_location} — deploy schema first (option 2)')
|
||||||
|
if not confirm('Continue anyway?', default_yes=False):
|
||||||
|
info('Cancelled — no changes made')
|
||||||
|
return
|
||||||
|
|
||||||
|
if not confirm(f'Deploy SQL functions from {functions_file} into {db_location}?', default_yes=False):
|
||||||
|
info('Cancelled — no changes made')
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f' Running {functions_file} against {db_location}...')
|
||||||
|
r = psql_file(cfg, functions_file)
|
||||||
if r.returncode == 0:
|
if r.returncode == 0:
|
||||||
ok('Functions deployed')
|
ok(f'SQL functions deployed into {db_location}')
|
||||||
else:
|
else:
|
||||||
err(f'Failed:\n{r.stderr}')
|
err(f'Function deployment failed:\n{r.stderr}')
|
||||||
|
|
||||||
|
|
||||||
def action_build_ui():
|
def action_build_ui():
|
||||||
header('Build UI')
|
header('Build UI (ui/ → public/)')
|
||||||
ui_dir = ROOT / 'ui'
|
ui_dir = ROOT / 'ui'
|
||||||
|
out_dir = ROOT / 'public'
|
||||||
|
|
||||||
|
print(f' Source : {ui_dir} (Vite/React)')
|
||||||
|
print(f' Output : {out_dir}')
|
||||||
|
print()
|
||||||
|
|
||||||
if not (ui_dir / 'package.json').exists():
|
if not (ui_dir / 'package.json').exists():
|
||||||
err('ui/package.json not found')
|
err(f'{ui_dir}/package.json not found — is the ui directory present?')
|
||||||
return
|
return
|
||||||
|
|
||||||
print(' Building...')
|
if not confirm(f'Build UI from {ui_dir} into {out_dir}?', default_yes=False):
|
||||||
r = subprocess.run(['npm', 'run', 'build'], cwd=ui_dir,
|
info('Cancelled — no changes made')
|
||||||
capture_output=True, text=True)
|
return
|
||||||
|
|
||||||
|
print(f' Running npm run build in {ui_dir}...')
|
||||||
|
r = subprocess.run(['npm', 'run', 'build'], cwd=ui_dir, capture_output=True, text=True)
|
||||||
if r.returncode == 0:
|
if r.returncode == 0:
|
||||||
ok('UI built')
|
ok(f'UI built successfully into {out_dir}')
|
||||||
else:
|
else:
|
||||||
err(f'Build failed:\n{r.stderr}')
|
err(f'UI build failed:\n{r.stderr}')
|
||||||
|
|
||||||
|
|
||||||
def action_setup_nginx(cfg):
|
def action_setup_nginx(cfg):
|
||||||
header('Nginx')
|
header('Set up nginx reverse proxy')
|
||||||
if not shutil.which('nginx'):
|
if not shutil.which('nginx'):
|
||||||
err('nginx not found')
|
err('nginx is not installed or not on PATH')
|
||||||
return
|
return
|
||||||
|
|
||||||
port = cfg.get('API_PORT', '3020') if cfg else '3020'
|
port = cfg.get('API_PORT', '3020') if cfg else '3020'
|
||||||
domain = prompt('Domain (e.g. dataflow.example.com)')
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
domain = prompt('Domain name (e.g. dataflow.example.com)')
|
||||||
if not domain:
|
if not domain:
|
||||||
info('No domain — skipped')
|
info('No domain entered — cancelled')
|
||||||
return
|
return
|
||||||
|
|
||||||
conf_name = domain.split('.')[0]
|
conf_name = domain.split('.')[0]
|
||||||
conf_path = NGINX_DIR / conf_name
|
conf_path = NGINX_DIR / conf_name
|
||||||
cert_path = Path(f'/etc/letsencrypt/live/{domain}/fullchain.pem')
|
cert_path = Path(f'/etc/letsencrypt/live/{domain}/fullchain.pem')
|
||||||
|
|
||||||
|
print()
|
||||||
if cert_path.exists():
|
if cert_path.exists():
|
||||||
|
info(f'SSL certificate found at {cert_path} — will configure HTTPS with redirect from HTTP.')
|
||||||
conf = f"""server {{
|
conf = f"""server {{
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
@ -396,6 +462,7 @@ server {{
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
|
info(f'No SSL certificate found at {cert_path} — will configure HTTP only for now.')
|
||||||
conf = f"""server {{
|
conf = f"""server {{
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
@ -406,99 +473,153 @@ server {{
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Write via sudo
|
print(f' Config file : {conf_path}')
|
||||||
|
print(f' Proxy target: localhost:{port}')
|
||||||
|
print()
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
with tempfile.NamedTemporaryFile('w', suffix='.conf', delete=False) as f:
|
with tempfile.NamedTemporaryFile('w', suffix='.conf', delete=False) as f:
|
||||||
f.write(conf)
|
f.write(conf)
|
||||||
tmp = f.name
|
tmp = f.name
|
||||||
|
|
||||||
|
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')
|
||||||
|
return
|
||||||
|
|
||||||
r = sudo_run(['cp', tmp, str(conf_path)])
|
r = sudo_run(['cp', tmp, str(conf_path)])
|
||||||
os.unlink(tmp)
|
os.unlink(tmp)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
err('Could not write nginx config (check sudo)')
|
err(f'Could not write {conf_path} — check sudo permissions')
|
||||||
return
|
return
|
||||||
ok(f'Config written to {conf_path}')
|
ok(f'nginx config written to {conf_path}')
|
||||||
|
|
||||||
|
print(' Testing nginx configuration...')
|
||||||
r = sudo_run(['nginx', '-t'], capture_output=True)
|
r = sudo_run(['nginx', '-t'], capture_output=True)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
err('nginx config invalid — run: sudo nginx -t')
|
err(f'nginx config test failed — run "sudo nginx -t" for details')
|
||||||
return
|
return
|
||||||
ok('Config valid')
|
ok('nginx configuration is valid')
|
||||||
|
|
||||||
|
print(' Reloading nginx...')
|
||||||
sudo_run(['systemctl', 'reload', 'nginx'])
|
sudo_run(['systemctl', 'reload', 'nginx'])
|
||||||
ok('nginx reloaded')
|
ok('nginx reloaded — site is now active')
|
||||||
|
|
||||||
if not cert_path.exists():
|
if not cert_path.exists():
|
||||||
warn(f'No SSL cert for {domain}')
|
warn(f'No SSL certificate found for {domain} — site is HTTP only.')
|
||||||
if confirm('Run certbot now?'):
|
if confirm(f'Run certbot to obtain an SSL certificate for {domain} and switch to HTTPS?'):
|
||||||
|
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',
|
||||||
'-m', f'admin@{domain}'])
|
'-m', f'admin@{domain}'])
|
||||||
if r.returncode == 0:
|
if r.returncode == 0:
|
||||||
ok('SSL configured')
|
ok(f'SSL certificate obtained and HTTPS configured for {domain}')
|
||||||
else:
|
else:
|
||||||
err(f'certbot failed — run manually: sudo certbot --nginx -d {domain}')
|
err(f'certbot failed — run manually: sudo certbot --nginx -d {domain}')
|
||||||
|
|
||||||
|
|
||||||
def action_install_service():
|
def action_install_service():
|
||||||
header('Systemd Service')
|
header(f'Install dataflow systemd service unit')
|
||||||
|
|
||||||
|
print(f' Source : {SERVICE_SRC}')
|
||||||
|
print(f' Target : {SERVICE_FILE}')
|
||||||
|
print()
|
||||||
|
|
||||||
if service_installed():
|
if service_installed():
|
||||||
info('Already installed')
|
info(f'{SERVICE_FILE} already exists — already installed')
|
||||||
return
|
info('Use option 7 to start/restart the service.')
|
||||||
if not SERVICE_SRC.exists():
|
|
||||||
err(f'{SERVICE_SRC} not found')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not SERVICE_SRC.exists():
|
||||||
|
err(f'Service unit file not found: {SERVICE_SRC}')
|
||||||
|
return
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
print(f' Copying {SERVICE_SRC} to {SERVICE_FILE}...')
|
||||||
r = sudo_run(['cp', str(SERVICE_SRC), str(SERVICE_FILE)])
|
r = sudo_run(['cp', str(SERVICE_SRC), str(SERVICE_FILE)])
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
err('Could not install service (check sudo)')
|
err(f'Could not write {SERVICE_FILE} — check sudo permissions')
|
||||||
return
|
return
|
||||||
ok('Service file installed')
|
ok(f'Service unit installed at {SERVICE_FILE}')
|
||||||
|
|
||||||
|
print(' Reloading systemd daemon...')
|
||||||
sudo_run(['systemctl', 'daemon-reload'])
|
sudo_run(['systemctl', 'daemon-reload'])
|
||||||
|
ok('systemd daemon reloaded')
|
||||||
|
|
||||||
|
print(' Enabling dataflow.service to start on boot...')
|
||||||
sudo_run(['systemctl', 'enable', 'dataflow'], capture_output=True)
|
sudo_run(['systemctl', 'enable', 'dataflow'], capture_output=True)
|
||||||
ok('Enabled on boot')
|
ok('dataflow.service enabled on boot')
|
||||||
|
|
||||||
|
info('Run option 7 to start the service now.')
|
||||||
|
|
||||||
|
|
||||||
def action_restart_service():
|
def action_restart_service():
|
||||||
header('API Server')
|
header('Start or restart dataflow.service')
|
||||||
|
|
||||||
if not service_installed():
|
if not service_installed():
|
||||||
err('Service not installed — run Install Service first')
|
err(f'{SERVICE_FILE} not found — run option 6 to install the service first')
|
||||||
return
|
return
|
||||||
|
|
||||||
action = 'restart' if service_running() else 'start'
|
currently_running = service_running()
|
||||||
|
action = 'restart' if currently_running else 'start'
|
||||||
|
current_state = 'currently running' if currently_running else 'currently stopped'
|
||||||
|
|
||||||
|
print(f' Service file : {SERVICE_FILE}')
|
||||||
|
print(f' Current state : {current_state}')
|
||||||
|
print(f' Action : sudo systemctl {action} dataflow')
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not confirm(f'{action.capitalize()} dataflow.service?', default_yes=False):
|
||||||
|
info('Cancelled — no changes made')
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f' Running: sudo systemctl {action} dataflow...')
|
||||||
r = sudo_run(['systemctl', action, 'dataflow'])
|
r = sudo_run(['systemctl', action, 'dataflow'])
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
err(f'Failed — check: journalctl -u dataflow -n 30')
|
err(f'systemctl {action} failed — check logs: journalctl -u dataflow -n 30')
|
||||||
return
|
return
|
||||||
|
|
||||||
import time; time.sleep(1)
|
import time; time.sleep(1)
|
||||||
if service_running():
|
if service_running():
|
||||||
ok(f'Service {action}ed and running')
|
ok(f'dataflow.service {action}ed successfully and is now running')
|
||||||
else:
|
else:
|
||||||
err('Service failed to start — check: journalctl -u dataflow -n 30')
|
err(f'dataflow.service {action}ed but is not running — check logs: journalctl -u dataflow -n 30')
|
||||||
|
|
||||||
|
|
||||||
def action_stop_service():
|
def action_stop_service():
|
||||||
header('Stop Service')
|
header('Stop dataflow.service')
|
||||||
|
|
||||||
|
print(f' Service file : {SERVICE_FILE}')
|
||||||
|
print(f' Action : sudo systemctl stop dataflow')
|
||||||
|
print()
|
||||||
|
|
||||||
if not service_running():
|
if not service_running():
|
||||||
info('Service is not running')
|
info('dataflow.service is not currently running — nothing to stop')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not confirm('Stop dataflow.service?', default_yes=False):
|
||||||
|
info('Cancelled — no changes made')
|
||||||
|
return
|
||||||
|
|
||||||
|
print(' Running: sudo systemctl stop dataflow...')
|
||||||
sudo_run(['systemctl', 'stop', 'dataflow'])
|
sudo_run(['systemctl', 'stop', 'dataflow'])
|
||||||
ok('Service stopped')
|
ok('dataflow.service stopped')
|
||||||
|
|
||||||
|
|
||||||
# ── Main menu ─────────────────────────────────────────────────────────────────
|
# ── Main menu ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
MENU = [
|
MENU = [
|
||||||
('Configure database connection', action_configure),
|
('Configure database connection settings (.env)', action_configure),
|
||||||
('Deploy schema', action_deploy_schema),
|
('Deploy "dataflow" schema (database/schema.sql)', action_deploy_schema),
|
||||||
('Deploy functions', action_deploy_functions),
|
('Deploy SQL functions (database/functions.sql)', action_deploy_functions),
|
||||||
('Build UI', action_build_ui),
|
('Build UI (ui/ → public/)', action_build_ui),
|
||||||
('Set up nginx', action_setup_nginx),
|
('Set up nginx reverse proxy', action_setup_nginx),
|
||||||
('Install service', action_install_service),
|
('Install dataflow systemd service unit', action_install_service),
|
||||||
('Start / restart service', action_restart_service),
|
('Start / restart dataflow.service', action_restart_service),
|
||||||
('Stop service', action_stop_service),
|
('Stop dataflow.service', action_stop_service),
|
||||||
]
|
]
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -510,9 +631,16 @@ def main():
|
|||||||
cfg = load_env()
|
cfg = load_env()
|
||||||
show_status(cfg)
|
show_status(cfg)
|
||||||
|
|
||||||
|
db_target = f'into "{cfg["DB_NAME"]}" on {cfg["DB_HOST"]}' if cfg else '(not configured)'
|
||||||
|
DB_ACTIONS = {
|
||||||
|
'Deploy "dataflow" schema (database/schema.sql)',
|
||||||
|
'Deploy SQL functions (database/functions.sql)',
|
||||||
|
}
|
||||||
|
|
||||||
print(bold('Actions'))
|
print(bold('Actions'))
|
||||||
for i, (label, _) in enumerate(MENU, 1):
|
for i, (label, _) in enumerate(MENU, 1):
|
||||||
print(f' {cyan(str(i))}. {label}')
|
suffix = f' {dim(db_target)}' if label in DB_ACTIONS else ''
|
||||||
|
print(f' {cyan(str(i))}. {label}{suffix}')
|
||||||
print(f' {cyan("q")}. Quit')
|
print(f' {cyan("q")}. Quit')
|
||||||
print()
|
print()
|
||||||
|
|
||||||
@ -526,21 +654,19 @@ def main():
|
|||||||
idx = int(choice) - 1
|
idx = int(choice) - 1
|
||||||
if 0 <= idx < len(MENU):
|
if 0 <= idx < len(MENU):
|
||||||
label, fn = MENU[idx]
|
label, fn = MENU[idx]
|
||||||
# Some actions need cfg, some don't
|
|
||||||
import inspect
|
import inspect
|
||||||
sig = inspect.signature(fn)
|
sig = inspect.signature(fn)
|
||||||
if len(sig.parameters) == 0:
|
if len(sig.parameters) == 0:
|
||||||
result = fn()
|
result = fn()
|
||||||
elif len(sig.parameters) == 1:
|
elif len(sig.parameters) == 1:
|
||||||
result = fn(cfg)
|
result = fn(cfg)
|
||||||
# configure returns updated cfg
|
|
||||||
if label.startswith('Configure') and result is not None:
|
if label.startswith('Configure') and result is not None:
|
||||||
cfg = result
|
cfg = result
|
||||||
pause()
|
pause()
|
||||||
else:
|
else:
|
||||||
warn('Invalid choice')
|
warn('Invalid choice — enter a number from the list above')
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
warn('Invalid choice')
|
warn('Invalid choice — enter a number from the list above')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user