diff --git a/scripts/deploy_board_integrated.py b/scripts/deploy_board_integrated.py new file mode 100644 index 0000000..0310bcc --- /dev/null +++ b/scripts/deploy_board_integrated.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Deploy KohakuBoard integrated with KohakuHub (shared database for SSO) + +OPTIONAL: This is only if you want to share database with KohakuHub. +For fully standalone deployment, use deploy_board.py instead. +""" + +import subprocess +import sys + + +def main(): + print("=" * 60) + print("KohakuBoard Integrated Deployment (OPTIONAL)") + print("Shares database with KohakuHub for unified accounts") + print("=" * 60) + + # Check if KohakuHub is running (optional warning, not error) + print("\n[0/3] Checking KohakuHub services...") + result = subprocess.run( + ["docker", "ps", "--filter", "name=postgres", "--format", "{{.Names}}"], + capture_output=True, + text=True, + ) + if "postgres" not in result.stdout: + print("⚠️ Warning: KohakuHub PostgreSQL is not running") + print("\nThis deployment expects to connect to KohakuHub's database.") + print("If you want fully standalone deployment, use:") + print(" python scripts/deploy_board.py") + print("\nIf you want to deploy KohakuHub first:") + print(" 1. cp docker-compose.example.yml docker-compose.yml") + print(" 2. Edit docker-compose.yml (change passwords and secrets)") + print(" 3. ./deploy.sh") + print("\nContinuing anyway (will fail if database is not accessible)...") + else: + print("✓ KohakuHub PostgreSQL is running") + + # Build frontend + print("\n[1/3] Building frontend...") + result = subprocess.run( + ["npm", "install", "--prefix", "./src/kohaku-board-ui"], + check=False, + ) + if result.returncode != 0: + print("✗ Failed to install dependencies") + sys.exit(1) + + result = subprocess.run( + ["npm", "run", "build", "--prefix", "./src/kohaku-board-ui"], + check=False, + ) + if result.returncode != 0: + print("✗ Failed to build frontend") + sys.exit(1) + + print("\n[2/3] Starting KohakuBoard services...") + result = subprocess.run( + [ + "docker-compose", + "-f", + "docker-compose.board-integrated.yml", + "up", + "-d", + "--build", + ], + check=False, + ) + if result.returncode != 0: + print("✗ Failed to start Docker services") + sys.exit(1) + + print("\n" + "=" * 60) + print("✓ KohakuBoard deployed successfully (integrated mode)!") + print("=" * 60) + print(f"\nKohakuHub: http://localhost:28080") + print(f"KohakuBoard: http://localhost:28081") + print(f"\n⚡ SSO enabled: Login works across both systems") + print(f"\n📊 Shared database: Users are unified") + print("\nView logs: docker-compose -f docker-compose.board-integrated.yml logs -f") + print("Stop services: docker-compose -f docker-compose.board-integrated.yml down") + print() + + +if __name__ == "__main__": + main() diff --git a/src/kohakuboard/cli.py b/src/kohakuboard/cli.py index 5f9f136..352d15a 100644 --- a/src/kohakuboard/cli.py +++ b/src/kohakuboard/cli.py @@ -34,8 +34,9 @@ def cli(): @click.argument("folder", type=click.Path(exists=True)) @click.option("--port", default=48889, help="Server port (default: 48889)") @click.option("--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)") +@click.option("--reload", is_flag=True, help="Enable auto-reload for development") @click.option("--no-browser", is_flag=True, help="Do not open browser automatically") -def open(folder, port, host, no_browser): +def open(folder, port, host, reload, no_browser): """Open local board folder in browser Starts a local web server to browse boards in the specified folder. @@ -44,7 +45,7 @@ def open(folder, port, host, no_browser): Examples: kobo open ./kohakuboard kobo open /path/to/experiments --port 8080 - kobo open ./boards --no-browser + kobo open ./boards --reload --no-browser """ folder_path = Path(folder).resolve() @@ -57,6 +58,8 @@ def open(folder, port, host, no_browser): click.echo("🚀 Starting KohakuBoard server (local mode)") click.echo(f"📁 Board directory: {folder_path}") click.echo(f"🌐 Server URL: http://localhost:{port}") + if reload: + click.echo(f"🔄 Auto-reload: Enabled") click.echo() # Open browser after delay @@ -79,7 +82,7 @@ def open(folder, port, host, no_browser): "kohakuboard.main:app", host=host, port=port, - reload=False, + reload=reload, log_level="info", ) except KeyboardInterrupt: @@ -87,6 +90,144 @@ def open(folder, port, host, no_browser): sys.exit(0) +@cli.command() +@click.option("--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)") +@click.option("--port", default=48889, help="Server port (default: 48889)") +@click.option( + "--data-dir", + default="./kohakuboard", + help="Board data directory (default: ./kohakuboard)", +) +@click.option( + "--db", + default="sqlite:///kohakuboard.db", + help="Database URL (default: sqlite:///kohakuboard.db)", +) +@click.option( + "--db-backend", + type=click.Choice(["sqlite", "postgres"]), + default="sqlite", + help="Database backend (default: sqlite)", +) +@click.option( + "--reload", + is_flag=True, + help="Enable auto-reload for development", +) +@click.option( + "--workers", + default=1, + help="Number of worker processes (default: 1, use 4+ for production)", +) +@click.option( + "--session-secret", + help="Session secret for authentication (required in production)", +) +@click.option( + "--no-browser", + is_flag=True, + help="Do not open browser automatically", +) +def serve( + host, port, data_dir, db, db_backend, reload, workers, session_secret, no_browser +): + """Start KohakuBoard server in remote mode with authentication + + Lightweight entry point for both testing and production deployments. + No Docker required - just Python and a database. + + Examples: + # Development with auto-reload (SQLite) + kobo serve --reload + + # Production with PostgreSQL + kobo serve --db postgresql://user:pass@localhost/kohakuboard \\ + --db-backend postgres \\ + --workers 4 \\ + --session-secret $(openssl rand -hex 32) + + # Custom configuration + kobo serve --port 8080 \\ + --data-dir /var/kohakuboard \\ + --db sqlite:///data/board.db \\ + --workers 2 + """ + data_dir_path = Path(data_dir).resolve() + data_dir_path.mkdir(parents=True, exist_ok=True) + + # Set environment for remote mode + os.environ["KOHAKU_BOARD_MODE"] = "remote" + os.environ["KOHAKU_BOARD_BOARD_DATA_DIR"] = str(data_dir_path) + os.environ["KOHAKU_BOARD_PORT"] = str(port) + os.environ["KOHAKU_BOARD_HOST"] = host + os.environ["KOHAKU_BOARD_DB_BACKEND"] = db_backend + os.environ["KOHAKU_BOARD_DATABASE_URL"] = db + os.environ["KOHAKU_BOARD_BASE_URL"] = f"http://localhost:{port}" + + # Session secret (required for production) + if session_secret: + os.environ["KOHAKU_BOARD_AUTH_SESSION_SECRET"] = session_secret + elif workers > 1 and not reload: + click.echo( + "⚠️ Warning: Using default session secret in multi-worker mode", err=True + ) + click.echo(" Generate one with: openssl rand -hex 32", err=True) + click.echo() + + # Auth config defaults (can be overridden with env vars) + os.environ.setdefault("KOHAKU_BOARD_AUTH_REQUIRE_EMAIL_VERIFICATION", "false") + os.environ.setdefault("KOHAKU_BOARD_AUTH_INVITATION_ONLY", "false") + + click.echo("🚀 Starting KohakuBoard server (remote mode)") + click.echo(f"📁 Data directory: {data_dir_path}") + click.echo(f"💾 Database: {db}") + click.echo(f"🌐 Server URL: http://localhost:{port}") + click.echo(f"👥 Authentication: Enabled") + if reload: + click.echo(f"🔄 Auto-reload: Enabled (development)") + if workers > 1: + click.echo(f"⚡ Workers: {workers}") + click.echo() + + # Open browser after delay + if not no_browser: + + def open_browser(): + time.sleep(2) # Wait for server to start + click.echo(f"🔗 Opening browser at http://localhost:{port}") + try: + webbrowser.open(f"http://localhost:{port}") + except Exception as e: + click.echo(f"⚠️ Could not open browser: {e}", err=True) + + thread = threading.Thread(target=open_browser, daemon=True) + thread.start() + + # Run uvicorn + try: + if reload or workers == 1: + # Single worker with optional reload + uvicorn.run( + "kohakuboard.main:app", + host=host, + port=port, + reload=reload, + log_level="info", + ) + else: + # Multi-worker production mode + uvicorn.run( + "kohakuboard.main:app", + host=host, + port=port, + workers=workers, + log_level="info", + ) + except KeyboardInterrupt: + click.echo("\n👋 Server stopped") + sys.exit(0) + + @cli.command() @click.argument("folder", type=click.Path(exists=True)) @click.option(