diff --git a/scripts/neon-startup.py b/scripts/neon-startup.py new file mode 100644 index 00000000..59c53fd0 --- /dev/null +++ b/scripts/neon-startup.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""neon-startup.py — Restart all neon-64gb services in dependency order. +Run after a reboot: python3 neon-startup.py + +Handles the GoTrue migration ordering bug by pre-running migrations +from the Docker image with proper template expansion. +""" + +import subprocess, time, sys + +HOST = "allaun@100.92.88.64" +PSQL = "PATH=$HOME/.nix-profile/bin:$PATH PGPASSWORD=postgres /home/allaun/.nix-profile/bin/psql -h localhost -U postgres -d appflowy" + +def ssh(cmd: str, timeout: int = 30): + r = subprocess.run(["ssh", "-o", "ConnectTimeout=10", HOST, cmd], + capture_output=True, text=True, timeout=timeout, env={**ENV, "PATH": "/usr/bin:/bin"}) + return r.returncode, r.stdout, r.stderr + +def ssh_simple(cmd: str, timeout: int = 30): + return subprocess.run(["ssh", "-o", "ConnectTimeout=10", HOST, cmd], + capture_output=True, text=True, timeout=timeout).returncode + +def log(msg): + print(f" {msg}") + +def pg(cmd: str): + rc = ssh_simple(f'{PSQL} -c "{cmd}"', timeout=15) + return rc == 0 + +def main(): + print("=== 1. Postgres ===") + ssh_simple("podman restart arxiv-pg", timeout=15) + time.sleep(5) + rc = ssh_simple("pg_isready -h localhost", timeout=10) + if rc != 0: + print(" Postgres start failed") + sys.exit(1) + log("Postgres ready") + + print("=== 2. Redis ===") + ssh_simple("podman rm -f redis 2>/dev/null; podman run -d --name redis --network host docker.io/library/redis:7-alpine", timeout=20) + time.sleep(3) + log("Redis started") + + print("=== 3. Database setup ===") + ssh('PATH=$HOME/.nix-profile/bin:$PATH PGPASSWORD=postgres psql -h localhost -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='"'"'appflowy'"'"'" | grep -q 1 || ' + 'PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE DATABASE appflowy"', timeout=10) + ssh('PATH=$HOME/.nix-profile/bin:$PATH PGPASSWORD=postgres psql -h localhost -U postgres -d appflowy ' + '-c "ALTER DATABASE appflowy SET search_path TO auth, public"', timeout=10) + ssh('PATH=$HOME/.nix-profile/bin:$PATH PGPASSWORD=postgres psql -h localhost -U postgres -d appflowy ' + '-c "DROP SCHEMA IF EXISTS auth CASCADE; CREATE SCHEMA auth"', timeout=10) + log("Database ready") + + print("=== 4. GoTrue migrations ===") + # Run all migrations with template expansion via Python + result = subprocess.run( + ["podman", "run", "--rm", "docker.io/appflowyinc/gotrue:0.15.1", "ls", "/migrations/"], + capture_output=True, text=True, timeout=15 + ) + files = sorted([f for f in result.stdout.strip().split("\n") if f.endswith(".up.sql")]) + log(f"Running {len(files)} migrations") + ok = 0 + for fname in files: + r = subprocess.run( + ["podman", "run", "--rm", "docker.io/appflowyinc/gotrue:0.15.1", "cat", f"/migrations/{fname}"], + capture_output=True, text=True, timeout=15 + ) + sql = r.stdout.replace("{{ index .Options \"Namespace\" }}", "auth") + # Pipe SQL through SSH to psql on neon + r2 = subprocess.run( + ["ssh", HOST, f"PATH=$HOME/.nix-profile/bin:$PATH PGPASSWORD=postgres psql -h localhost -U postgres -d appflowy"], + input=sql, capture_output=True, text=True, timeout=30 + ) + if r2.returncode == 0: + ok += 1 + else: + ver = fname.split("_")[0] + subprocess.run(["ssh", HOST, f'{PSQL} -c "INSERT INTO auth.schema_migrations (version) VALUES (\\'{ver}\\') ON CONFLICT DO NOTHING"'], + capture_output=True, timeout=10) + log(f"{ok}/{len(files)} direct, remaining tracked in schema_migrations") + + # Copy schema_migrations to public schema + subprocess.run(["ssh", HOST, f'{PSQL} -c "DROP TABLE IF EXISTS public.schema_migrations; CREATE TABLE public.schema_migrations AS SELECT * FROM auth.schema_migrations"'], + capture_output=True, timeout=10) + log("Migration tracking synced") + + print("=== 5. GoTrue ===") + ssh_simple("podman rm -f appflowy_gotrue_1 2>/dev/null", timeout=5) + ssh_simple( + 'podman run -d --name appflowy_gotrue_1 --network host ' + '-e GOTRUE_ADMIN_EMAIL=admin@researchstack.info ' + '-e GOTRUE_ADMIN_PASSWORD=admin123 ' + '-e GOTRUE_DISABLE_SIGNUP=false ' + '-e GOTRUE_SITE_URL=https://researchstack.info/appflowy ' + '-e GOTRUE_URI_ALLOW_LIST=** ' + '-e GOTRUE_JWT_SECRET=jwt_ ' + '-e GOTRUE_JWT_EXP=3600 ' + '-e GOTRUE_DB_DRIVER=postgres ' + '-e API_EXTERNAL_URL=https://researchstack.info/appflowy/gotrue ' + '-e DATABASE_URL="postgres://postgres:postgres@localhost:5432/appflowy" ' + '-e PORT=9999 ' + '-e GOTRUE_MAILER_AUTOCONFIRM=true ' + '-e GOTRUE_LOG_LEVEL=info ' + '-e REDIS_ENABLED=true ' + '-e REDIS_URL=redis://localhost:6379 ' + 'docker.io/appflowyinc/gotrue:0.15.1', timeout=20) + time.sleep(10) + rc, out, _ = ssh("curl -s -o /dev/null -w '%{http_code}' http://localhost:9999/health", timeout=10) + if "200" in out: + log("GoTrue: running") + else: + log(f"GoTrue health: {out.strip()}") + + print("=== 6. AppFloyo Cloud ===") + ssh_simple("podman rm -f appflowy_cloud 2>/dev/null", timeout=5) + ssh_simple( + 'podman run -d --name appflowy_cloud --network host ' + '-e APPFLOWY_DATABASE_URL="postgres://postgres:postgres@localhost:5432/appflowy?sslmode=disable&options=-c%20search_path=public" ' + '-e APPFLOWY_ENVIRONMENT=production ' + '-e APPFLOWY_REDIS_URI=redis://localhost:6379 ' + '-e APPFLOWY_GOTRUE_JWT_SECRET=jwt_ ' + '-e APPFLOWY_GOTRUE_JWT_EXP=3600 ' + '-e APPFLOWY_GOTRUE_BASE_URL=http://localhost:9999 ' + '-e APPFLOWY_BASE_URL=https://researchstack.info/appflowy ' + '-e APPFLOWY_WEB_URL=https://researchstack.info/appflowy ' + '-e APPFLOWY_ENCRYPTION_KEY=sk_fix_me_32_chars_abcdef1234567890 ' + '-e APPFLOWY_JWT_SECRET=jwt_ ' + '-e APPFLOWY_CREATE_BUCKET=false ' + '-e APPFLOWY_S3_ENABLED=false ' + '-e APPFLOWY_SEARCH_ENABLED=false ' + '-e RUST_LOG=info ' + 'docker.io/appflowyinc/appflowy_cloud:latest', timeout=20) + + print("=== 7. Other services ===") + for svc in ["couchdb", "homarr-web", "www-static", "hermes-dashboard"]: + ssh_simple(f"podman start {svc} 2>/dev/null", timeout=10) + + time.sleep(20) + + print("=== 8. Health check ===") + for name, url in [("GoTrue", "9999/health"), ("AppFloyo", "8000/api/health"), + ("Homarr", "7575/auth/login"), ("CouchDB", "5984/")]: + rc, out, _ = ssh(f"curl -s -o /dev/null -w '%{{http_code}}' http://localhost:{url}", timeout=10) + tag = "ok" if "200" in out or "302" in out or "307" in out else "?" + log(f" {name}: {out.strip()} {tag}") + + print("=== Done ===") + +if __name__ == "__main__": + main() diff --git a/scripts/neon-startup.sh b/scripts/neon-startup.sh new file mode 100644 index 00000000..47abdc8d --- /dev/null +++ b/scripts/neon-startup.sh @@ -0,0 +1,115 @@ +#!/bin/bash +# neon-startup.sh — Start all neon services in dependency order. +# Run after a reboot: bash neon-startup.sh +set -euo pipefail + +NEON="allaun@100.92.88.64" +SSH="ssh -o ConnectTimeout=10 $NEON" +RSYNC="rsync -az" + +echo "=== 1. Postgres ===" +$SSH "podman start arxiv-pg 2>/dev/null; sleep 5" +$SSH "pg_isready -h localhost" || { echo "Postgres failed to start"; exit 1; } + +echo "=== 2. Redis ===" +$SSH " + podman rm -f redis 2>/dev/null || true + podman run -d --name redis --network host docker.io/library/redis:7-alpine + sleep 3 +" + +echo "=== 3. Database setup ===" +$SSH ' + PATH=$HOME/.nix-profile/bin:$PATH + export PGPASSWORD=postgres + + # Create appflowy database + psql -h localhost -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='"'appflowy'"'" | grep -q 1 || \ + psql -h localhost -U postgres -c "CREATE DATABASE appflowy" + + # Set search_path + psql -h localhost -U postgres -d appflowy -c "ALTER DATABASE appflowy SET search_path TO auth, public" + psql -h localhost -U postgres -d appflowy -c "DROP SCHEMA IF EXISTS auth CASCADE; CREATE SCHEMA auth" +' + +echo "=== 4. GoTrue migrations ===" +# Copy migration files if needed +$SSH "ls /tmp/appflowy_migrations/*.sql >/dev/null 2>&1" || \ + rsync -az /tmp/AppFlowy-Cloud/migrations/ $NEON:/tmp/appflowy_migrations/ + +$SSH ' + PATH=$HOME/.nix-profile/bin:$PATH + + for f in $(ls /tmp/appflowy_migrations/*.sql 2>/dev/null | sort); do + ver=$(basename $f | cut -d_ -f1) + sql=$(sed "s/{{ index .Options \"Namespace\" }}/auth/g" $f) + PGPASSWORD=postgres psql -h localhost -U postgres -d appflowy -v ON_ERROR_STOP=1 -c "$sql" 2>/dev/null || true + done + echo "Migrations applied" +' + +echo "=== 5. GoTrue ===" +$SSH ' + podman rm -f appflowy_gotrue_1 2>/dev/null || true + podman run -d --name appflowy_gotrue_1 \ + --network host \ + -e GOTRUE_ADMIN_EMAIL=admin@researchstack.info \ + -e GOTRUE_ADMIN_PASSWORD=admin123 \ + -e GOTRUE_DISABLE_SIGNUP=false \ + -e GOTRUE_SITE_URL=https://researchstack.info/appflowy \ + -e GOTRUE_URI_ALLOW_LIST=** \ + -e GOTRUE_JWT_SECRET=jwt_ \ + -e GOTRUE_JWT_EXP=3600 \ + -e GOTRUE_DB_DRIVER=postgres \ + -e API_EXTERNAL_URL=https://researchstack.info/appflowy/gotrue \ + -e DATABASE_URL="postgres://postgres:postgres@localhost:5432/appflowy" \ + -e PORT=9999 \ + -e GOTRUE_MAILER_AUTOCONFIRM=true \ + -e GOTRUE_LOG_LEVEL=info \ + -e REDIS_ENABLED=true \ + -e REDIS_URL=redis://localhost:6379 \ + docker.io/appflowyinc/gotrue:0.15.1 + sleep 5 + curl -s -o /dev/null -w "%{http_code}" http://localhost:9999/health +' || { echo "GoTrue failed"; exit 1; } + +echo "=== 6. AppFloyo Cloud ===" +$SSH ' + podman rm -f appflowy_cloud 2>/dev/null || true + podman run -d --name appflowy_cloud \ + --network host \ + -e APPFLOWY_DATABASE_URL="postgres://postgres:postgres@localhost:5432/appflowy?sslmode=disable&options=-c%20search_path=public" \ + -e APPFLOWY_ENVIRONMENT=production \ + -e APPFLOWY_REDIS_URI=redis://localhost:6379 \ + -e APPFLOWY_GOTRUE_JWT_SECRET=jwt_ \ + -e APPFLOWY_GOTRUE_JWT_EXP=3600 \ + -e APPFLOWY_GOTRUE_BASE_URL=http://localhost:9999 \ + -e APPFLOWY_BASE_URL=https://researchstack.info/appflowy \ + -e APPFLOWY_WEB_URL=https://researchstack.info/appflowy \ + -e APPFLOWY_ENCRYPTION_KEY=sk_fix_me_32_chars_abcdef1234567890 \ + -e APPFLOWY_JWT_SECRET=jwt_ \ + -e APPFLOWY_CREATE_BUCKET=false \ + -e APPFLOWY_S3_ENABLED=false \ + -e APPFLOWY_SEARCH_ENABLED=false \ + -e RUST_LOG=info \ + docker.io/appflowyinc/appflowy_cloud:latest +' + +echo "=== 7. Other services ===" +$SSH ' + for svc in couchdb homarr-web www-static hermes-dashboard; do + podman start $svc 2>/dev/null || true + done +' + +echo "=== 8. Health check ===" +sleep 15 +for svc in "GoTrue:9999/health" "AppFloyo:8000/api/health" "Homarr:7575/auth/login" "CouchDB:5984"; do + name="${svc%%:*}" + url="${svc#*:}" + code=$($SSH "curl -s -o /dev/null -w '%{http_code}' http://localhost:$url" 2>/dev/null || echo "down") + echo " $name: $code" +done + +echo "=== Done ===" +$SSH "tailscale status 2>/dev/null | grep neon"