Compare commits

..

1 Commits

Author SHA1 Message Date
Timmy (NEXUSBURN)
09d3d949d4 feat: unified HTTP+WS server for proper URL deployment
Some checks failed
CI / test (pull_request) Failing after 47s
Review Approval Gate / verify-review (pull_request) Failing after 8s
CI / validate (pull_request) Failing after 50s
Closes #1339. The Nexus Three.js app required HTTP serving for module
imports to work (file:// and raw Forge URLs break). This change:

- server.py: dual-purpose async server — HTTP on :8080 (static files)
  and WebSocket on :8765 (broadcast gateway), single process
- Dockerfile: exposes both 8080 (HTTP) and 8765 (WS), includes all
  frontend assets (boot.js, bootstrap.mjs, gofai_worker.js, etc.)
- docker-compose.yml: maps HTTP :8080/:8081 + WS :8765/:8766
- deploy.sh: updated for new port layout
- run.sh: standalone no-Docker launcher
- app.js: WS URL derives from HTTP port (8080->8765, 8081->8766)
- deploy.yml workflow: docker compose on remote host

To deploy: `./deploy.sh` (Docker) or `./run.sh` (bare metal).
Open http://HOST:8080 in a browser — Three.js modules load correctly.
2026-04-13 18:24:59 -04:00
14 changed files with 213 additions and 613 deletions

View File

@@ -1,78 +0,0 @@
name: Deploy Nexus (Nginx)
on:
push:
branches:
- main
paths:
- 'index.html'
- 'app.js'
- 'style.css'
- 'nginx.conf'
- 'Dockerfile.nginx'
- 'docker-compose.nginx.yml'
- 'server.py'
- 'requirements.txt'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preflight secrets check
env:
H: ${{ secrets.DEPLOY_HOST }}
U: ${{ secrets.DEPLOY_USER }}
K: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
if [ -z "$H" ] || [ -z "$U" ] || [ -z "$K" ]; then
echo "ERROR: Missing deploy secret. Configure DEPLOY_HOST/DEPLOY_USER/DEPLOY_SSH_KEY in Settings → Actions → Secrets"
exit 1
fi
- name: Deploy to VPS via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
echo "Deploying Nexus with nginx..."
# Clone or update repo
if [ ! -d ~/the-nexus ]; then
git clone http://143.198.27.163:3000/Timmy_Foundation/the-nexus.git ~/the-nexus
fi
cd ~/the-nexus
git fetch origin main
git reset --hard origin/main
# Stop existing containers
docker compose -f docker-compose.nginx.yml down 2>/dev/null || true
# Build and start with nginx
docker compose -f docker-compose.nginx.yml build
docker compose -f docker-compose.nginx.yml up -d
# Verify deployment
sleep 5
if curl -s http://localhost/health | grep -q "OK"; then
echo "✅ Nexus deployed successfully with nginx"
echo "🌐 Access at: http://$(hostname -I | awk '{print $1}')"
else
echo "❌ Deployment failed - health check failed"
exit 1
fi
notify:
needs: deploy
runs-on: ubuntu-latest
steps:
- name: Notify on success
if: success()
run: |
echo "Nexus deployed successfully with nginx!"
echo "URL: http://nexus.alexanderwhitestone.com (if DNS configured)"

View File

@@ -27,8 +27,10 @@ jobs:
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd ~/the-nexus || git clone http://143.198.27.163:3000/Timmy_Foundation/the-nexus.git ~/the-nexus
cd ~/the-nexus || git clone https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus.git ~/the-nexus
cd ~/the-nexus
git fetch origin main
git reset --hard origin/main
./deploy.sh main
docker compose build nexus-main
docker compose up -d --force-recreate nexus-main
echo "Nexus deployed — HTTP :8080, WS :8765"

View File

@@ -1,108 +0,0 @@
# Nexus Deployment Guide
## Quick Start
### Option 1: Deploy with Nginx (Recommended)
```bash
# Deploy main (port 80)
./deploy.sh
# Deploy staging (port 8080)
./deploy.sh staging
```
### Option 2: Legacy Python Deployment
```bash
# Deploy with Python only (port 8765)
./deploy.sh legacy
```
## URL Configuration
### Local Development
- **Nginx Main:** http://localhost
- **Nginx Staging:** http://localhost:8080
- **Legacy Python:** ws://localhost:8765
### Production (Ezra VPS)
- **Main Site:** http://nexus.alexanderwhitestone.com (after DNS setup)
- **Direct IP:** http://143.198.27.163
## DNS Setup
To point a domain to the Nexus:
1. Create an A record:
```
nexus.alexanderwhitestone.com → 143.198.27.163
```
2. (Optional) Set up SSL with Let's Encrypt:
```bash
sudo certbot --nginx -d nexus.alexanderwhitestone.com
```
## Manual VPS Deployment
1. SSH into Ezra VPS:
```bash
ssh root@143.198.27.163
```
2. Clone and deploy:
```bash
cd ~
git clone http://143.198.27.163:3000/Timmy_Foundation/the-nexus.git
cd the-nexus
./deploy.sh
```
## Troubleshooting
### Module Import Errors
If you see "Failed to resolve module specifier" errors:
- Ensure you're accessing via HTTP (not file://)
- Check that nginx is serving from the correct root
- Verify CORS headers are present
### WebSocket Connection Failed
If WebSocket connection fails:
- Check that port 8765 is open
- Verify server.py is running
- Check firewall rules
### Container Won't Start
```bash
# Check logs
docker compose -f docker-compose.nginx.yml logs
# Rebuild
docker compose -f docker-compose.nginx.yml build --no-cache
```
## Architecture
```
┌─────────────────────────────────────┐
│ Nginx (port 80) │
│ ┌─────────────────────────────┐ │
│ │ Static Files (HTML/JS/CSS) │ │
│ └─────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Python WebSocket Server │ │
│ │ (port 8765) │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
```
## Files
- `nginx.conf` — Nginx configuration
- `Dockerfile.nginx` — Multi-stage build (nginx + Python)
- `docker-compose.nginx.yml` — Docker Compose for nginx deployment
- `docker-entrypoint.sh` — Starts both nginx and Python server
- `.gitea/workflows/deploy-nginx.yml` — CI/CD workflow

View File

@@ -11,11 +11,16 @@ COPY nexus/ nexus/
COPY server.py ./
# Frontend assets referenced by index.html
COPY index.html help.html style.css app.js service-worker.js manifest.json ./
COPY index.html help.html style.css app.js boot.js bootstrap.mjs gofai_worker.js mempalace.js service-worker.js manifest.json ./
# Config/data
COPY portals.json vision.json robots.txt ./
# Icons
COPY icons/ icons/
# Expose HTTP (static) and WebSocket
EXPOSE 8080
EXPOSE 8765
CMD ["python3", "server.py"]

View File

@@ -1,56 +0,0 @@
# Multi-stage build: Python backend + Nginx frontend
FROM python:3.11-slim AS backend
WORKDIR /app
# Install Python deps
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Backend
COPY nexus/ nexus/
COPY server.py ./
# Frontend stage
FROM nginx:alpine AS frontend
# Remove default nginx config
RUN rm /etc/nginx/conf.d/default.conf
# Copy our nginx config
COPY nginx.conf /etc/nginx/conf.d/nexus.conf
# Copy frontend assets
COPY index.html help.html style.css app.js service-worker.js manifest.json /usr/share/nginx/html/
COPY portals.json vision.json robots.txt /usr/share/nginx/html/
# Create a simple health check
RUN echo "OK" > /usr/share/nginx/html/health
# Final stage - combine both
FROM nginx:alpine
# Copy nginx config
COPY --from=frontend /etc/nginx/conf.d/nexus.conf /etc/nginx/conf.d/
# Copy frontend assets
COPY --from=frontend /usr/share/nginx/html/ /usr/share/nginx/html/
# Copy Python backend
COPY --from=backend /app/ /app/
# Install Python in final image
RUN apk add --no-cache python3 py3-pip
# Install Python dependencies
COPY requirements.txt /app/
RUN cd /app && pip3 install --no-cache-dir -r requirements.txt
# Copy entrypoint script
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
EXPOSE 80 8765
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

6
app.js
View File

@@ -2188,7 +2188,11 @@ function connectHermes() {
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/world/ws`;
// WS gateway runs on HTTP port - 115 (8080->8765, 8081->8766)
const wsHost = window.location.hostname;
const httpPort = parseInt(window.location.port) || 8080;
const wsPort = httpPort === 8081 ? 8766 : 8765;
const wsUrl = `${protocol}//${wsHost}:${wsPort}/api/world/ws`;
console.log(`Connecting to Hermes at ${wsUrl}...`);
hermesWs = new WebSocket(wsUrl);

View File

@@ -1,54 +1,25 @@
#!/usr/bin/env bash
# deploy.sh — deploy the Nexus to production or staging
# Usage: ./deploy.sh — deploy main with nginx (port 80)
# ./deploy.sh staging — deploy staging with nginx (port 8080)
# ./deploy.sh legacy — deploy with Python only (port 8765)
# ./deploy.sh --help — show help
# deploy.sh — spin up (or update) the Nexus staging environment
# Usage: ./deploy.sh — rebuild and restart nexus-main (HTTP :8080, WS :8765)
# ./deploy.sh staging — rebuild and restart nexus-staging (HTTP :8081, WS :8766)
set -euo pipefail
SERVICE="${1:-main}"
USE_NGINX=true
SERVICE="${1:-nexus-main}"
case "$SERVICE" in
--help|-h)
echo "Usage: $0 [main|staging|legacy]"
echo ""
echo "Options:"
echo " main (default) — Deploy main with nginx (port 80)"
echo " staging — Deploy staging with nginx (port 8080)"
echo " legacy — Deploy with Python only (port 8765)"
echo " --help — Show this help"
exit 0
;;
staging)
SERVICE="nexus-staging"
PORT="8080"
;;
legacy)
SERVICE="nexus-main"
USE_NGINX=false
PORT="8765"
;;
main|*)
SERVICE="nexus-main"
PORT="80"
;;
staging) SERVICE="nexus-staging" ;;
main) SERVICE="nexus-main" ;;
esac
echo "==> Deploying $SERVICE ..."
echo "==> Deploying $SERVICE "
docker compose build "$SERVICE"
docker compose up -d --force-recreate "$SERVICE"
if [ "$USE_NGINX" = true ]; then
echo "==> Using nginx deployment..."
docker compose -f docker-compose.nginx.yml build "$SERVICE"
docker compose -f docker-compose.nginx.yml up -d --force-recreate "$SERVICE"
echo "==> Deployed with nginx on port $PORT"
echo "==> Access at: http://localhost:$PORT"
if [ "$SERVICE" = "nexus-main" ]; then
echo "==> HTTP: http://localhost:8080"
echo "==> WS: ws://localhost:8765"
else
echo "==> Using legacy Python deployment..."
docker compose build "$SERVICE"
docker compose up -d --force-recreate "$SERVICE"
echo "==> Deployed with Python on port $PORT"
echo "==> WebSocket at: ws://localhost:$PORT"
echo "==> HTTP: http://localhost:8081"
echo "==> WS: ws://localhost:8766"
fi
echo "==> Done. Container: $SERVICE"

View File

@@ -1,42 +0,0 @@
version: "3.9"
services:
nexus-main:
build:
context: .
dockerfile: Dockerfile.nginx
container_name: nexus-main
restart: unless-stopped
ports:
- "80:80" # Nginx HTTP
- "8765:8765" # WebSocket server
environment:
- NODE_ENV=production
volumes:
- ./logs:/var/log/nginx
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
nexus-staging:
build:
context: .
dockerfile: Dockerfile.nginx
container_name: nexus-staging
restart: unless-stopped
ports:
- "8080:80" # Nginx HTTP (staging)
- "8766:8765" # WebSocket server (staging)
environment:
- NODE_ENV=staging
volumes:
- ./logs-staging:/var/log/nginx
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s

View File

@@ -6,10 +6,12 @@ services:
container_name: nexus-main
restart: unless-stopped
ports:
- "8080:8080"
- "8765:8765"
nexus-staging:
build: .
container_name: nexus-staging
restart: unless-stopped
ports:
- "8766:8765"
- "8081:8080"
- "8766:8765"

View File

@@ -1,31 +0,0 @@
#!/bin/sh
set -e
echo "Starting Nexus deployment..."
# Start nginx in background
echo "Starting nginx on port 80..."
nginx -g "daemon off;" &
NGINX_PID=$!
# Start Python WebSocket server in background
echo "Starting WebSocket server on port 8765..."
cd /app && python3 server.py &
PYTHON_PID=$!
# Function to handle shutdown
shutdown() {
echo "Shutting down..."
kill $NGINX_PID 2>/dev/null
kill $PYTHON_PID 2>/dev/null
exit 0
}
# Trap SIGTERM and SIGINT
trap shutdown SIGTERM SIGINT
# Wait for any process to exit
wait -n
# Exit with status of process that exited first
exit $?

View File

@@ -1,62 +0,0 @@
server {
listen 80;
server_name nexus.alexanderwhitestone.com;
root /usr/share/nginx/html;
index index.html;
# Enable gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
gzip_comp_level 6;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Handle SPA routing - serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
}
# WebSocket proxy for server.py (if needed)
location /ws {
proxy_pass http://localhost:8765;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# CORS headers for development
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type" always;
}
# Redirect HTTP to HTTPS (if SSL is configured)
server {
listen 443 ssl http2;
server_name nexus.alexanderwhitestone.com;
# SSL configuration (commented out for now)
# ssl_certificate /etc/nginx/ssl/nexus.crt;
# ssl_certificate_key /etc/nginx/ssl/nexus.key;
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers HIGH:!aNULL:!MD5;
# For now, redirect to HTTP
return 301 http://$server_name$request_uri;
}

16
run.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# run.sh — run Nexus locally without Docker
# Usage: ./run.sh — HTTP :8080, WS :8765
# NEXUS_HTTP_PORT=9090 ./run.sh — custom HTTP port
set -euo pipefail
cd "$(dirname "$0")"
# Install deps if missing
if ! python3 -c "import websockets" 2>/dev/null; then
echo "==> Installing dependencies..."
pip3 install -r requirements.txt
fi
echo "==> Starting Nexus server..."
exec python3 server.py

233
server.py
View File

@@ -1,92 +1,190 @@
#!/usr/bin/env python3
"""
The Nexus WebSocket Gateway — Robust broadcast bridge for Timmy's consciousness.
This server acts as the central hub for the-nexus, connecting the mind (nexus_think.py),
the body (Evennia/Morrowind), and the visualization surface.
The Nexus — Unified HTTP + WebSocket server.
Serves static frontend files (Three.js app) over HTTP on port 8080
and runs the WebSocket gateway on port 8765.
Single-process, single-command deployment — no nginx required.
"""
import asyncio
import json
import logging
import mimetypes
import os
import signal
import sys
from http import HTTPStatus
from pathlib import Path
from typing import Set
# Branch protected file - see POLICY.md
import websockets
import websockets.asyncio.server
# ---------------------------------------------------------------------------
# Configuration
PORT = 8765
HOST = "0.0.0.0" # Allow external connections if needed
# ---------------------------------------------------------------------------
HTTP_PORT = int(os.environ.get("NEXUS_HTTP_PORT", "8080"))
WS_PORT = int(os.environ.get("NEXUS_WS_PORT", "8765"))
HOST = os.environ.get("NEXUS_HOST", "0.0.0.0")
ROOT = Path(__file__).resolve().parent
# Logging setup
# Static file extensions we're willing to serve
SAFE_SUFFIXES = {
".html", ".htm", ".css", ".js", ".mjs", ".json",
".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico",
".woff", ".woff2", ".ttf", ".eot",
".txt", ".xml", ".webmanifest",
}
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("nexus-gateway")
log = logging.getLogger("nexus")
# State
clients: Set[websockets.WebSocketServerProtocol] = set()
# ---------------------------------------------------------------------------
# HTTP — static file server
# ---------------------------------------------------------------------------
async def http_handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
"""Minimal async HTTP/1.1 server for static files."""
try:
# Read request line + headers (max 8 KB)
data = b""
while b"\r\n\r\n" not in data and len(data) < 8192:
chunk = await asyncio.wait_for(reader.read(4096), timeout=10)
if not chunk:
return
data += chunk
header_text = data.split(b"\r\n\r\n", 1)[0].decode("utf-8", errors="replace")
lines = header_text.split("\r\n")
request_line = lines[0]
parts = request_line.split(" ", 2)
if len(parts) < 2:
writer.close()
return
method, raw_path = parts[0], parts[1]
async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
"""Handles individual client connections and message broadcasting."""
clients.add(websocket)
addr = websocket.remote_address
logger.info(f"Client connected from {addr}. Total clients: {len(clients)}")
if method not in ("GET", "HEAD"):
_write_response(writer, HTTPStatus.METHOD_NOT_ALLOWED, b"Method Not Allowed")
return
# Normalise path — prevent directory traversal
safe_path = raw_path.split("?", 1)[0].split("#", 1)[0]
safe_path = os.path.normpath(safe_path).lstrip("/")
if not safe_path:
safe_path = "index.html"
file_path = ROOT / safe_path
# Reject traversal
if not str(file_path.resolve()).startswith(str(ROOT)):
_write_response(writer, HTTPStatus.FORBIDDEN, b"Forbidden")
return
if not file_path.exists() or file_path.is_dir():
# Try index.html for directories
if file_path.is_dir() and (file_path / "index.html").exists():
file_path = file_path / "index.html"
else:
_write_response(writer, HTTPStatus.NOT_FOUND, b"Not Found")
return
suffix = file_path.suffix.lower()
if suffix not in SAFE_SUFFIXES:
_write_response(writer, HTTPStatus.FORBIDDEN, b"Forbidden")
return
content_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
body = file_path.read_bytes()
headers = {
"Content-Type": content_type,
"Content-Length": str(len(body)),
"Cache-Control": "no-cache",
"Access-Control-Allow-Origin": "*",
}
_write_response(writer, HTTPStatus.OK, body, headers, method == "HEAD")
except (asyncio.TimeoutError, ConnectionError):
pass
except Exception as exc:
log.warning("HTTP handler error: %s", exc)
finally:
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
def _write_response(writer, status, body, headers=None, head_only=False):
line = f"HTTP/1.1 {status.value} {status.phrase}\r\n"
hdr = ""
if headers:
for k, v in headers.items():
hdr += f"{k}: {v}\r\n"
response = (line + hdr + "\r\n").encode()
if not head_only:
response += body if isinstance(body, bytes) else body.encode()
writer.write(response)
# ---------------------------------------------------------------------------
# WebSocket — broadcast gateway
# ---------------------------------------------------------------------------
ws_clients: Set[websockets.asyncio.server.ServerConnection] = set()
async def ws_handler(websocket: websockets.asyncio.server.ServerConnection):
ws_clients.add(websocket)
log.info("WS client connected from %s. Total: %d", websocket.remote_address, len(ws_clients))
try:
async for message in websocket:
# Parse for logging/validation if it's JSON
try:
data = json.loads(message)
msg_type = data.get("type", "unknown")
# Optional: log specific important message types
if msg_type in ["agent_register", "thought", "action"]:
logger.debug(f"Received {msg_type} from {addr}")
if msg_type in ("agent_register", "thought", "action"):
log.debug("WS %s from %s", msg_type, websocket.remote_address)
except (json.JSONDecodeError, TypeError):
pass
# Broadcast to all OTHER clients
if not clients:
continue
disconnected = set()
# Create broadcast tasks, tracking which client each task targets
task_client_pairs = []
for client in clients:
for client in ws_clients:
if client != websocket and client.open:
task = asyncio.create_task(client.send(message))
task_client_pairs.append((task, client))
try:
await client.send(message)
except Exception:
disconnected.add(client)
ws_clients.difference_update(disconnected)
if task_client_pairs:
tasks = [pair[0] for pair in task_client_pairs]
results = await asyncio.gather(*tasks, return_exceptions=True)
for i, result in enumerate(results):
if isinstance(result, Exception):
target_client = task_client_pairs[i][1]
logger.error(f"Failed to send to client {target_client.remote_address}: {result}")
disconnected.add(target_client)
if disconnected:
clients.difference_update(disconnected)
except websockets.exceptions.ConnectionClosed:
logger.debug(f"Connection closed by client {addr}")
except Exception as e:
logger.error(f"Error handling client {addr}: {e}")
pass
except Exception as exc:
log.error("WS handler error for %s: %s", websocket.remote_address, exc)
finally:
clients.discard(websocket)
logger.info(f"Client disconnected {addr}. Total clients: {len(clients)}")
ws_clients.discard(websocket)
log.info("WS client disconnected. Total: %d", len(ws_clients))
# ---------------------------------------------------------------------------
# Main — run both servers concurrently
# ---------------------------------------------------------------------------
async def main():
"""Main server loop with graceful shutdown."""
logger.info(f"Starting Nexus WS gateway on ws://{HOST}:{PORT}")
# Set up signal handlers for graceful shutdown
# HTTP server
http_server = await asyncio.start_server(http_handler, HOST, HTTP_PORT)
log.info("HTTP server listening on http://%s:%d", HOST, HTTP_PORT)
# WebSocket server
ws_server = await websockets.asyncio.server.serve(ws_handler, HOST, WS_PORT)
log.info("WebSocket server listening on ws://%s:%d", HOST, WS_PORT)
log.info("Nexus is live — open http://%s:%d in a browser", HOST, HTTP_PORT)
# Graceful shutdown
loop = asyncio.get_running_loop()
stop = loop.create_future()
def shutdown():
if not stop.done():
stop.set_result(None)
@@ -95,29 +193,28 @@ async def main():
try:
loop.add_signal_handler(sig, shutdown)
except NotImplementedError:
# Signal handlers not supported on Windows
pass
async with websockets.serve(broadcast_handler, HOST, PORT):
logger.info("Gateway is ready and listening.")
await stop
logger.info("Shutting down Nexus WS gateway...")
# Close any remaining client connections (handlers may have already cleaned up)
remaining = {c for c in clients if c.open}
await stop
log.info("Shutting down...")
http_server.close()
await http_server.wait_closed()
ws_server.close()
await ws_server.wait_closed()
remaining = {c for c in ws_clients if c.open}
if remaining:
logger.info(f"Closing {len(remaining)} active connections...")
close_tasks = [client.close() for client in remaining]
await asyncio.gather(*close_tasks, return_exceptions=True)
clients.clear()
logger.info("Shutdown complete.")
await asyncio.gather(*(c.close() for c in remaining), return_exceptions=True)
ws_clients.clear()
log.info("Shutdown complete.")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
except Exception as e:
logger.critical(f"Fatal server error: {e}")
except Exception as exc:
log.critical("Fatal: %s", exc)
sys.exit(1)

View File

@@ -1,120 +0,0 @@
#!/usr/bin/env bash
# setup-vps.sh — Initial setup for Ezra VPS
# Run this once on a fresh VPS to prepare for Nexus deployment
set -euo pipefail
echo "Setting up Ezra VPS for Nexus deployment..."
# Update system
echo "Updating system packages..."
apt-get update && apt-get upgrade -y
# Install Docker
echo "Installing Docker..."
if ! command -v docker &> /dev/null; then
apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io
systemctl enable docker
systemctl start docker
echo "✅ Docker installed"
else
echo "✅ Docker already installed"
fi
# Install Docker Compose
echo "Installing Docker Compose..."
if ! command -v docker-compose &> /dev/null; then
curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
echo "✅ Docker Compose installed"
else
echo "✅ Docker Compose already installed"
fi
# Install nginx (for reverse proxy if needed)
echo "Installing nginx..."
if ! command -v nginx &> /dev/null; then
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
echo "✅ Nginx installed"
else
echo "✅ Nginx already installed"
fi
# Configure firewall
echo "Configuring firewall..."
if command -v ufw &> /dev/null; then
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw allow 8765/tcp # WebSocket
ufw allow 3000/tcp # Gitea
ufw --force enable
echo "✅ Firewall configured"
else
echo "⚠️ ufw not available, skipping firewall configuration"
fi
# Create nexus user (optional)
echo "Creating nexus user..."
if ! id -u nexus &>/dev/null; then
useradd -m -s /bin/bash nexus
usermod -aG docker nexus
echo "✅ nexus user created"
else
echo "✅ nexus user already exists"
fi
# Clone repository
echo "Cloning Nexus repository..."
if [ ! -d /home/nexus/the-nexus ]; then
sudo -u nexus git clone http://143.198.27.163:3000/Timmy_Foundation/the-nexus.git /home/nexus/the-nexus
echo "✅ Repository cloned"
else
echo "✅ Repository already exists"
fi
# Set up nginx reverse proxy (optional)
echo "Setting up nginx reverse proxy..."
cat > /etc/nginx/sites-available/nexus << 'EOF'
server {
listen 80;
server_name nexus.alexanderwhitestone.com;
location / {
proxy_pass http://localhost:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ws {
proxy_pass http://localhost:8765;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOF
ln -sf /etc/nginx/sites-available/nexus /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
echo "✅ Nginx reverse proxy configured"
echo ""
echo "🎉 VPS setup complete!"
echo ""
echo "Next steps:"
echo "1. Point DNS: nexus.alexanderwhitestone.com → $(hostname -I | awk '{print $1}')"
echo "2. Deploy: cd /home/nexus/the-nexus && ./deploy.sh"
echo "3. (Optional) Set up SSL: certbot --nginx -d nexus.alexanderwhitestone.com"
echo ""