Compare commits
1 Commits
burn/1339-
...
nexusburn/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09d3d949d4 |
@@ -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)"
|
||||
@@ -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"
|
||||
|
||||
108
DEPLOYMENT.md
108
DEPLOYMENT.md
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
6
app.js
@@ -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);
|
||||
|
||||
57
deploy.sh
57
deploy.sh
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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 $?
|
||||
62
nginx.conf
62
nginx.conf
@@ -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
16
run.sh
Executable 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
233
server.py
@@ -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)
|
||||
|
||||
120
setup-vps.sh
120
setup-vps.sh
@@ -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 ""
|
||||
Reference in New Issue
Block a user