Import verified AzerothAI snapshot from GitHub
Core tests / core (push) Successful in 9m57s
Full verification / application (push) Failing after 5m16s
Full verification / local-brain (push) Failing after 6m15s
Realm simulation / simulated-live-world (push) Failing after 5m32s
WoW integration lab / real-worldserver (push) Canceled after 17m23s

Source branch: feature/ai-brain-router
Source commit: c2a09e0460465201a58aaab56c71424e9a96405e
This commit is contained in:
Richard Clark
2026-09-22 13:12:54 -04:00
commit 5f0fc86cf9
175 changed files with 25807 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
# Temporary Windows PC + Ubuntu VM Lab
This is the short-lived lab profile for running AzerothAI on a Windows gaming PC until the dedicated mini PC is ready.
## Layout
Windows gaming PC:
- WoW 3.3.5a client
- Ollama
- Qwen3.5 4B on the RTX GPU
- Ubuntu Server VM containing AzerothCore, Playerbots, ALE, MySQL, AzerothAI, and World Monitor
The Windows install stays intact. Do not install Proxmox on the gaming PC for this temporary lab.
## VM settings
Create an Ubuntu Server VM in VMware with:
- 4 vCPUs
- 8 GB RAM
- 90 GB dynamically allocated disk
- NAT networking for the temporary Wi-Fi lab
- no desktop environment required
Ubuntu 24.04 LTS is the reference host, but newer Ubuntu hosts are acceptable because the AzerothCore Docker build itself uses its pinned container base. The bootstrap automatically expands free LVM space and handles the VMware NAT path to Windows-hosted Ollama.
With NAT, Windows can reach the VM directly on VMnet8. Phone/iPad access is optional and can be added later with bridged networking or a tunnel.
If free disk space is tight, 70 GB can work for a short test, but 90 GB leaves more breathing room for AzerothCore build output and extracted client data.
## Step 1 — Windows host
Open PowerShell as Administrator in the AzerothAI checkout.
Read-only check:
powershell -ExecutionPolicy Bypass -File .\deploy\pc-lab\host-setup.ps1
When ready to set up the model host:
powershell -ExecutionPolicy Bypass -File .\deploy\pc-lab\host-setup.ps1 -InstallOllama -StartOllama -OpenFirewall
That checks RAM, free space, CPU virtualization and GPU; installs Ollama through winget if requested; binds Ollama to TCP 11434; pulls qwen3.5:4b; opens TCP 11434 on Windows Private networks only; and prints the Windows LAN IPv4 address.
Keep that IPv4 address.
## Step 2 — Create Ubuntu VM
Install Ubuntu Server normally. For this temporary Wi-Fi setup, use VMware NAT.
After installation:
ip addr
Keep the VM's LAN IPv4 address too.
## Step 3 — Clone AzerothAI in Ubuntu
sudo apt-get update
sudo apt-get install -y git
git clone https://github.com/Endami-backup/AzerothAI.git
cd AzerothAI
git checkout feature/ai-brain-router
bash deploy/pc-lab/vm-bootstrap.sh
The bootstrap first tries to discover the Windows VMnet8/Ollama endpoint automatically. If it cannot, enter the VMware NAT host IPv4 printed by host-setup.ps1.
On the first run the script may install Docker and ask you to log out/back in before rerunning. That is expected.
The bootstrap verifies the VM can reach Qwen on Windows, calls the real playable-realm bootstrap, builds AzerothCore + Playerbots + ALE, imports the realm databases/client data, starts AzerothAI, points AzerothAI at the Windows Ollama host, and prints the WoW realm and dashboard addresses.
## Step 4 — Create your WoW GM account
Inside Ubuntu:
cd ~/azerothai-pc-lab/azerothcore
docker attach ac-worldserver
At the worldserver console:
account create YOURNAME YOURPASSWORD
account set gmlevel YOURNAME 3 -1
Detach without killing worldserver with Ctrl-P, then Ctrl-Q.
Then save those same credentials for AzerothAI's localhost-only SOAP GM channel from the AzerothAI checkout:
bash deploy/playable/set-gm-credentials.sh YOURNAME YOURPASSWORD
Restart the AzerothAI runtime if the helper tells you to.
## Step 5 — Connect WoW from Windows
Edit:
<WoW 3.3.5a folder>\Data\enUS\realmlist.wtf
Use the Ubuntu VM IP:
set realmlist 192.168.x.x
Launch WoW and sign in. The realm name is AzerothAI Lab.
## Step 6 — Open World Monitor
From Windows, iPhone, or iPad on the same network:
http://<ubuntu-vm-ip>:8787
Open World Monitor. It shows Zaku's location/state/home/goal/last decision, active zones, population bands, Hardcore deaths, authoritative ALE events, server/model health, and Population Director activity.
## Step 7 — Test connectivity from Windows
powershell -ExecutionPolicy Bypass -File .\deploy\pc-lab\check-lab.ps1 -VmIp 192.168.x.x
It checks World Monitor on TCP 8787, WoW authserver on 3724, worldserver on 8085, and the AzerothAI health API.
## What still needs real-server proof
The simulated ALE/live-world path is green in CI. This VM gives us the physical environment to validate:
- real Playerbot character GUID binding
- PlayerBotAI execution of AzerothAI intents
- all real damage paths for Zaku's 1 HP clamp
- actual quest/travel/social behavior
- real WoW client login/play
If the live server differs from the simulator, the worldserver is authoritative and we fix the adapter.
## Stop the lab
From Ubuntu:
export AZEROTHAI_PLAYABLE_ROOT="$HOME/azerothai-pc-lab"
bash deploy/playable/stop.sh
The database and realm files remain on disk.
## Thursday migration
Before deleting the VM, create a private migration archive:
cd ~/AzerothAI
export AZEROTHAI_PLAYABLE_ROOT="$HOME/azerothai-pc-lab"
bash deploy/pc-lab/export-for-mini-pc.sh
The archive contains the AzerothCore auth/characters/world databases, AzerothAI local state, generated configuration, and runtime secrets. It is created with owner-only permissions and must be treated as sensitive.
This VM is disposable, but Zaku is not. On the mini PC we should import this state so Zaku remains the same persistent identity rather than being rebuilt as a new character.
## Remote dashboard over Tailscale
If the dashboard HTML loads over the raw Tailscale IP but CSS/JS assets do not, use the built-in proxy helper:
```bash
cd ~/AzerothAI
git pull
bash deploy/pc-lab/tailscale-dashboard.sh
```
It serves built client assets directly with nginx, proxies API/SSR requests back to the AzerothAI runtime on port 8787, and publishes the result privately with Tailscale Serve over HTTPS. Use the `https://...ts.net/` URL it prints instead of raw `http://100.x.x.x:8787`.
+48
View File
@@ -0,0 +1,48 @@
param(
[Parameter(Mandatory=$true)]
[string]$VmIp,
[int]$DashboardPort = 8787,
[int]$AuthPort = 3724,
[int]$WorldPort = 8085
)
$ErrorActionPreference = "Continue"
function Test-Port([string]$HostName,[int]$Port,[string]$Label) {
$result = Test-NetConnection -ComputerName $HostName -Port $Port -WarningAction SilentlyContinue
if ($result.TcpTestSucceeded) {
Write-Host "[OK] $Label ${HostName}:$Port" -ForegroundColor Green
return $true
}
Write-Host "[FAIL] $Label ${HostName}:$Port" -ForegroundColor Red
return $false
}
Write-Host "AzerothAI PC Lab connectivity check" -ForegroundColor Cyan
Write-Host "VM: $VmIp"
Write-Host ""
$ok = $true
$ok = (Test-Port $VmIp $DashboardPort "World Monitor") -and $ok
$ok = (Test-Port $VmIp $AuthPort "WoW authserver") -and $ok
$ok = (Test-Port $VmIp $WorldPort "WoW worldserver") -and $ok
try {
$health = Invoke-RestMethod -Uri "http://${VmIp}:$DashboardPort/api/health" -TimeoutSec 5
Write-Host "[OK] AzerothAI health API" -ForegroundColor Green
$health | ConvertTo-Json -Depth 5
} catch {
Write-Host "[FAIL] AzerothAI health API" -ForegroundColor Red
$ok = $false
}
if (-not $ok) {
Write-Host ""
Write-Host "One or more checks failed. Verify the VM is using Bridged networking and the Ubuntu firewall is not blocking these ports." -ForegroundColor Yellow
exit 1
}
Write-Host ""
Write-Host "All network checks passed." -ForegroundColor Green
Write-Host "World Monitor: http://${VmIp}:$DashboardPort"
Write-Host "WoW realmlist: set realmlist $VmIp"
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB_ROOT="${AZEROTHAI_PLAYABLE_ROOT:-$HOME/azerothai-pc-lab}"
ACORE_ROOT="$LAB_ROOT/azerothcore"
RUNTIME_ENV="$LAB_ROOT/runtime.env"
OUT_DIR="${1:-$HOME}"
STAMP="$(date +%Y%m%d-%H%M%S)"
WORK="$LAB_ROOT/export-$STAMP"
ARCHIVE="$OUT_DIR/azerothai-pc-lab-$STAMP.tar.gz"
mkdir -p "$WORK/mysql" "$WORK/azerothai"
if [[ ! -f "$RUNTIME_ENV" ]]; then
echo "Missing $RUNTIME_ENV. This does not look like an initialized PC lab." >&2
exit 2
fi
# shellcheck disable=SC1090
source "$RUNTIME_ENV"
: "${DOCKER_DB_ROOT_PASSWORD:?Missing database password in runtime.env}"
if ! docker inspect ac-database >/dev/null 2>&1; then
echo "The AzerothCore database container is not present." >&2
exit 3
fi
echo "==> Dumping AzerothCore databases"
for db in acore_auth acore_characters acore_world; do
docker exec ac-database mysqldump -uroot -p"$DOCKER_DB_ROOT_PASSWORD" --single-transaction --routines --events --triggers "$db" > "$WORK/mysql/$db.sql"
done
echo "==> Preserving AzerothAI state and runtime configuration"
cp "$RUNTIME_ENV" "$WORK/runtime.env"
chmod 600 "$WORK/runtime.env"
if [[ -d "$REPO_ROOT/.wrangler/state" ]]; then
tar -C "$REPO_ROOT" -czf "$WORK/azerothai/wrangler-state.tar.gz" .wrangler/state
fi
if [[ -d "$LAB_ROOT/etc" ]]; then
tar -C "$LAB_ROOT" -czf "$WORK/azerothcore-etc.tar.gz" etc
fi
cat > "$WORK/MANIFEST.txt" <<EOF
AzerothAI temporary PC lab migration export
Created: $(date -Is)
Source branch: $(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
Source commit: $(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || echo unknown)
Contains:
- acore_auth SQL
- acore_characters SQL
- acore_world SQL
- AzerothAI local D1/SQLite state, when present
- AzerothCore generated configuration, when present
- runtime.env including bridge/operator/database secrets
Treat this archive as sensitive because it contains credentials.
EOF
echo "==> Creating migration archive"
tar -C "$WORK" -czf "$ARCHIVE" .
chmod 600 "$ARCHIVE"
rm -rf "$WORK"
echo
echo "Migration export created:"
echo " $ARCHIVE"
echo
echo "Keep this file private. It contains realm data and runtime credentials."
+174
View File
@@ -0,0 +1,174 @@
param(
[switch]$InstallOllama,
[switch]$StartOllama,
[switch]$OpenFirewall,
[string]$Model = "qwen3.5:4b"
)
$ErrorActionPreference = "Stop"
function Section([string]$Text) {
Write-Host ""
Write-Host "==> $Text" -ForegroundColor Cyan
}
function Get-LanIPv4 {
$candidates = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object {
$_.IPAddress -notlike "127.*" -and
$_.IPAddress -notlike "169.254.*" -and
$_.InterfaceAlias -notmatch "vEthernet|VirtualBox|VMware|Loopback|Tailscale"
} |
Sort-Object InterfaceMetric
return ($candidates | Select-Object -First 1 -ExpandProperty IPAddress)
}
function Get-VMwareNatInfo {
$addr = Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias "*VMware*VMnet8*" -ErrorAction SilentlyContinue |
Select-Object -First 1
if (-not $addr) { return $null }
$octets = $addr.IPAddress.Split('.')
if ($octets.Count -ne 4) { return $null }
$subnet = "$($octets[0]).$($octets[1]).$($octets[2]).0/$($addr.PrefixLength)"
return [pscustomobject]@{
IP = $addr.IPAddress
PrefixLength = $addr.PrefixLength
Subnet = $subnet
}
}
Section "AzerothAI temporary Windows host check"
$lanIp = Get-LanIPv4
$vmnet8 = Get-VMwareNatInfo
if (-not $lanIp) {
Write-Warning "Could not auto-detect a LAN IPv4 address."
} else {
Write-Host "Windows LAN IPv4: $lanIp"
}
if ($vmnet8) {
Write-Host "VMware NAT host IPv4: $($vmnet8.IP)"
Write-Host "VMware NAT subnet: $($vmnet8.Subnet)"
}
$free = Get-PSDrive -Name C
$freeGB = [math]::Round($free.Free / 1GB, 1)
Write-Host "Free space on C:: $freeGB GB"
if ($freeGB -lt 70) {
Write-Warning "Less than 70 GB is free. The temporary VM may be tight once AzerothCore and client data are built."
}
$ram = Get-CimInstance Win32_ComputerSystem
$totalGB = [math]::Round($ram.TotalPhysicalMemory / 1GB, 1)
Write-Host "Installed RAM: $totalGB GB"
if ($totalGB -lt 14) {
Write-Warning "This PC has less memory than expected for an 8 GB VM plus Windows."
}
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
Write-Host "CPU: $($cpu.Name)"
Write-Host "Logical processors: $($cpu.NumberOfLogicalProcessors)"
$gpu = Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name
Write-Host "GPU(s): $($gpu -join ', ')"
$virt = (Get-CimInstance Win32_Processor | Select-Object -First 1).VirtualizationFirmwareEnabled
Write-Host "Firmware virtualization enabled: $virt"
if (-not $virt) {
Write-Warning "CPU virtualization appears disabled. Enable SVM/AMD-V in BIOS before creating the VM."
}
$ollamaCommand = Get-Command ollama -ErrorAction SilentlyContinue
$ollamaDefault = Join-Path $env:LOCALAPPDATA "Programs\Ollama\ollama.exe"
$ollamaExe = if ($ollamaCommand) { $ollamaCommand.Source } elseif (Test-Path $ollamaDefault) { $ollamaDefault } else { $null }
if ($InstallOllama -and -not $ollamaExe) {
Section "Installing Ollama"
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
throw "winget is not available. Install Ollama manually, then rerun this script."
}
winget install --id Ollama.Ollama -e --accept-source-agreements --accept-package-agreements
if (Test-Path $ollamaDefault) {
$ollamaExe = $ollamaDefault
} else {
$ollamaCommand = Get-Command ollama -ErrorAction SilentlyContinue
if ($ollamaCommand) { $ollamaExe = $ollamaCommand.Source }
}
}
if ($ollamaExe) {
Section "Ollama"
Write-Host "Ollama detected: $ollamaExe"
if ($StartOllama) {
[Environment]::SetEnvironmentVariable("OLLAMA_HOST", "0.0.0.0:11434", "User")
$env:OLLAMA_HOST = "0.0.0.0:11434"
Get-Process -Name "ollama" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
Start-Process -FilePath $ollamaExe -ArgumentList "serve" -WindowStyle Hidden
Start-Sleep -Seconds 3
Write-Host "Pulling $Model ..."
& $ollamaExe pull $Model
try {
Invoke-RestMethod -Uri "http://127.0.0.1:11434/api/tags" -TimeoutSec 5 | Out-Null
Write-Host "Ollama API: OK"
} catch {
Write-Warning "Ollama did not answer on port 11434 yet."
}
} else {
Write-Host "Use -StartOllama when you want this script to expose Ollama to the Ubuntu VM and pull $Model."
}
} else {
Write-Warning "Ollama is not installed. Re-run with -InstallOllama -StartOllama, or install it manually."
}
if ($OpenFirewall) {
Section "Windows firewall"
$profiles = Get-NetConnectionProfile -ErrorAction SilentlyContinue
$activeProfiles = $profiles | Where-Object { $_.IPv4Connectivity -ne "Disconnected" }
foreach ($profile in $activeProfiles) {
Write-Host "Network profile: $($profile.InterfaceAlias) -> $($profile.NetworkCategory)"
if ($profile.NetworkCategory -eq "Public") {
Write-Warning "This active network is Public. The AzerothAI firewall rule is Private-only and will not accept VM traffic until the network is trusted/Private or you deliberately change the rule."
}
}
$ruleName = "AzerothAI Ollama from VM"
$existing = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if ($existing) {
Remove-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
}
if ($vmnet8) {
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow -Protocol TCP -LocalPort 11434 -RemoteAddress $vmnet8.Subnet -Profile Any | Out-Null
Write-Host "Opened TCP 11434 only to the VMware NAT subnet $($vmnet8.Subnet)."
} else {
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow -Protocol TCP -LocalPort 11434 -Profile Private | Out-Null
Write-Host "VMnet8 was not detected, so TCP 11434 was opened on Private networks only."
}
}
Section "Temporary VM recommendation"
Write-Host "Ubuntu Server 24.04 LTS"
Write-Host "vCPU: 4"
Write-Host "RAM: 8 GB"
Write-Host "Disk: 90 GB dynamically allocated"
Write-Host "Network: NAT recommended for this temporary Wi-Fi lab"
Write-Host ""
Write-Host "After Ubuntu boots, clone AzerothAI and run:"
Write-Host " bash deploy/pc-lab/vm-bootstrap.sh"
Write-Host ""
if ($vmnet8) {
Write-Host "The Ubuntu NAT VM should use this Windows Ollama endpoint:"
Write-Host " http://$($vmnet8.IP):11434"
} elseif ($lanIp) {
Write-Host "Fallback Windows Ollama endpoint:"
Write-Host " http://${lanIp}:11434"
}
Write-Host ""
Write-Host "Nothing in this script installs Proxmox or modifies your Windows boot configuration."
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB_ROOT="${AZEROTHAI_PLAYABLE_ROOT:-$HOME/azerothai-pc-lab}"
DEV_PORT="${AZEROTHAI_REMOTE_UI_DEV_PORT:-5173}"
PROXY_PORT="${AZEROTHAI_TAILNET_PROXY_PORT:-8788}"
APP_PORT="${AZEROTHAI_APP_PORT:-8787}"
PID_FILE="$LAB_ROOT/azerothai-ui-dev.pid"
LOG_FILE="$LAB_ROOT/azerothai-ui-dev.log"
NGINX_SITE="/etc/nginx/sites-available/azerothai-tailnet"
NGINX_LINK="/etc/nginx/sites-enabled/azerothai-tailnet"
need(){ command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; }
need curl
need tailscale
need pnpm
TAILSCALE_IP="$(tailscale ip -4 2>/dev/null | head -n1 || true)"
if [[ -z "$TAILSCALE_IP" ]]; then
echo "No active Tailscale IPv4 address found." >&2
exit 2
fi
DNS_NAME="$(tailscale status --json 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); print((d.get("Self",{}).get("DNSName") or "").rstrip("."))' 2>/dev/null || true)"
echo "==> Rebuilding live AzerothAI backend with current source"
cd "$REPO_ROOT"
pnpm build
if [[ -f "$LAB_ROOT/azerothai.pid" ]]; then
OLD_APP_PID="$(cat "$LAB_ROOT/azerothai.pid" 2>/dev/null || true)"
if [[ -n "$OLD_APP_PID" ]]; then kill "$OLD_APP_PID" 2>/dev/null || true; fi
fi
pkill -f "wrangler.*8787" 2>/dev/null || true
pkill -f "pnpm.*start:lan" 2>/dev/null || true
sleep 2
nohup pnpm start:lan >"$LAB_ROOT/azerothai.log" 2>&1 &
echo $! > "$LAB_ROOT/azerothai.pid"
echo "==> Waiting for live AzerothAI backend"
for _ in {1..90}; do
if curl -fsS "http://127.0.0.1:$APP_PORT/api/health" >/dev/null 2>&1; then break; fi
sleep 1
done
curl -fsS "http://127.0.0.1:$APP_PORT/api/health" >/dev/null
echo "==> Restarting autonomous high-level service if this lab is configured"
RUNTIME_ENV="$LAB_ROOT/runtime.env"
if [[ -f "$RUNTIME_ENV" ]] && grep -q '^AUTONOMY_TOKEN=' "$RUNTIME_ENV"; then
bash "$REPO_ROOT/deploy/playable/install-autonomy-service.sh"
fi
echo "==> Ensuring durable ALE database relay is active"
if [[ -f "$RUNTIME_ENV" ]] && grep -q '^ALE_BRIDGE_TOKEN=' "$RUNTIME_ENV" && grep -q '^DOCKER_DB_ROOT_PASSWORD=' "$RUNTIME_ENV"; then
bash "$REPO_ROOT/deploy/playable/install-ale-relay-service.sh"
fi
echo "==> Starting hydration-safe dashboard UI"
mkdir -p "$LAB_ROOT"
if [[ -f "$PID_FILE" ]]; then
OLD_PID="$(cat "$PID_FILE" 2>/dev/null || true)"
if [[ -n "$OLD_PID" ]]; then kill "$OLD_PID" 2>/dev/null || true; fi
fi
pkill -f "vinext.*$DEV_PORT" 2>/dev/null || true
cd "$REPO_ROOT"
nohup pnpm dev >"$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
for _ in {1..90}; do
if curl -fsS "http://127.0.0.1:$DEV_PORT/" >/dev/null 2>&1; then
break
fi
sleep 1
done
if ! curl -fsS "http://127.0.0.1:$DEV_PORT/" >/dev/null 2>&1; then
echo "Dashboard UI dev server did not start." >&2
tail -80 "$LOG_FILE" >&2 || true
exit 3
fi
echo "==> Installing nginx if needed"
if ! command -v nginx >/dev/null 2>&1; then
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nginx
fi
echo "==> Wiring UI to the live AzerothAI API"
sudo tee "$NGINX_SITE" >/dev/null <<EOF
server {
listen 127.0.0.1:$PROXY_PORT;
server_name _;
location ^~ /api/ {
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_set_header Host \$http_host;
proxy_set_header X-Forwarded-Host \$http_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 https;
proxy_pass http://127.0.0.1:$APP_PORT;
add_header Cache-Control "no-store" always;
}
location / {
proxy_http_version 1.1;
proxy_set_header Host 127.0.0.1:$DEV_PORT;
proxy_set_header X-Forwarded-Host \$http_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 https;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:$DEV_PORT;
add_header Cache-Control "no-store" always;
}
}
EOF
sudo ln -sfn "$NGINX_SITE" "$NGINX_LINK"
sudo nginx -t
sudo systemctl enable --now nginx
sudo systemctl reload nginx
echo "==> Verifying dashboard and live API through proxy"
VERIFY_HOST="${DNS_NAME:-$TAILSCALE_IP}"
curl -fsS -H "Host: $VERIFY_HOST" "http://127.0.0.1:$PROXY_PORT/" >/dev/null
curl -fsS -H "Host: $VERIFY_HOST" "http://127.0.0.1:$PROXY_PORT/api/state" >/dev/null
ORIGIN="https://$VERIFY_HOST"
ACTION_BODY="$(mktemp)"
ACTION_CODE="$(curl -sS -o "$ACTION_BODY" -w '%{http_code}' -X POST -H "Host: $VERIFY_HOST" -H "Origin: $ORIGIN" -H "Content-Type: application/json" --data '{}' "http://127.0.0.1:$PROXY_PORT/api/actions" || true)"
if [[ "$ACTION_CODE" == "403" ]] && grep -q "Cross-origin action rejected" "$ACTION_BODY"; then
echo "Remote action origin check is still rejecting the trusted tailnet host." >&2
cat "$ACTION_BODY" >&2
rm -f "$ACTION_BODY"
exit 47
fi
rm -f "$ACTION_BODY"
echo "Remote action origin check: accepted (HTTP $ACTION_CODE expected for empty test action)"
echo "==> Publishing privately with Tailscale Serve"
sudo tailscale serve --bg "$PROXY_PORT"
STAMP="$(date +%s)"
echo
echo "============================================================"
echo " AzerothAI remote dashboard is ready"
echo "============================================================"
if [[ -n "$DNS_NAME" ]]; then
echo "Open this fresh URL:"
echo " https://$DNS_NAME/?v=$STAMP"
else
echo "Run: tailscale serve status"
fi
echo
echo "UI log: $LOG_FILE"
echo "Live API remains on port $APP_PORT."
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB_ROOT="${AZEROTHAI_PLAYABLE_ROOT:-$HOME/azerothai-pc-lab}"
cd "$REPO_ROOT"
echo "==> Rebuilding AzerothAI so server HTML and client bundles match"
pnpm build
echo "==> Restarting AzerothAI runtime"
if [[ -f "$LAB_ROOT/azerothai.pid" ]]; then
PID="$(cat "$LAB_ROOT/azerothai.pid" 2>/dev/null || true)"
if [[ -n "$PID" ]]; then kill "$PID" 2>/dev/null || true; fi
sleep 1
fi
nohup pnpm start:lan >"$LAB_ROOT/azerothai.log" 2>&1 &
echo $! > "$LAB_ROOT/azerothai.pid"
echo "==> Waiting for dashboard health"
for _ in {1..90}; do
if curl -fsS http://127.0.0.1:8787/api/health >/dev/null 2>&1; then
break
fi
sleep 1
done
curl -fsS http://127.0.0.1:8787/api/health >/dev/null
echo "==> Rebuilding remote Tailscale dashboard proxy"
bash "$REPO_ROOT/deploy/pc-lab/tailscale-dashboard.sh"
DNS_NAME="$(tailscale status --json 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); print((d.get("Self",{}).get("DNSName") or "").rstrip("."))' 2>/dev/null || true)"
STAMP="$(date +%s)"
echo
echo "Remote dashboard repair complete."
if [[ -n "$DNS_NAME" ]]; then
echo "Open this fresh URL:"
echo " https://$DNS_NAME/?v=$STAMP"
fi
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
STATIC_ROOT="$REPO_ROOT/dist/client"
PUBLIC_ROOT="/var/www/azerothai"
NGINX_SITE="/etc/nginx/sites-available/azerothai-tailnet"
NGINX_LINK="/etc/nginx/sites-enabled/azerothai-tailnet"
LOCAL_PROXY_PORT="${AZEROTHAI_TAILNET_PROXY_PORT:-8788}"
APP_PORT="${AZEROTHAI_APP_PORT:-8787}"
need(){ command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; }
if [[ ! -d "$STATIC_ROOT" ]]; then
echo "Missing built client assets at $STATIC_ROOT" >&2
echo "Run: cd $REPO_ROOT && pnpm build" >&2
exit 1
fi
need tailscale
need curl
TAILSCALE_IP="$(tailscale ip -4 2>/dev/null | head -n1 || true)"
if [[ -z "$TAILSCALE_IP" ]]; then
echo "No active Tailscale IPv4 address found. Run: sudo tailscale up --hostname=azerothai-lab" >&2
exit 2
fi
echo "==> Checking AzerothAI backend"
curl -fsS "http://127.0.0.1:$APP_PORT/api/health" >/dev/null || {
echo "AzerothAI is not responding on 127.0.0.1:$APP_PORT" >&2
exit 1
}
echo "==> Installing nginx if needed"
if ! command -v nginx >/dev/null 2>&1; then
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nginx
fi
echo "==> Publishing built client assets to an nginx-readable directory"
sudo rm -rf "$PUBLIC_ROOT"
sudo mkdir -p "$PUBLIC_ROOT"
sudo cp -a "$STATIC_ROOT"/. "$PUBLIC_ROOT"/
sudo find "$PUBLIC_ROOT" -type d -exec chmod 755 {} +
sudo find "$PUBLIC_ROOT" -type f -exec chmod 644 {} +
echo "==> Configuring a static-asset-safe reverse proxy"
sudo tee "$NGINX_SITE" >/dev/null <<EOF
server {
listen 127.0.0.1:$LOCAL_PROXY_PORT;
server_name _;
root $PUBLIC_ROOT;
location ~* \\.(?:css|js|mjs|svg|png|ico|woff|woff2)$ {
try_files \$uri @azerothai;
access_log off;
add_header Cache-Control "no-store" always;
}
location / {
add_header Cache-Control "no-store" always;
proxy_http_version 1.1;
proxy_set_header Host \$http_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 https;
proxy_pass http://127.0.0.1:$APP_PORT;
}
location @azerothai {
proxy_http_version 1.1;
proxy_set_header Host \$http_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 https;
proxy_pass http://127.0.0.1:$APP_PORT;
}
}
EOF
sudo ln -sfn "$NGINX_SITE" "$NGINX_LINK"
sudo nginx -t
sudo systemctl enable --now nginx
sudo systemctl reload nginx
echo "==> Verifying proxied page and repairing asset paths if needed"
PAGE="$(curl -fsS -H "Host: $TAILSCALE_IP:$LOCAL_PROXY_PORT" "http://127.0.0.1:$LOCAL_PROXY_PORT/")"
map_asset_path(){
local raw="$1" clean target base match srcdir dstdir
clean="${raw%%\?*}"
[[ "$clean" == /* ]] || return 0
target="$PUBLIC_ROOT$clean"
[[ -f "$target" ]] && return 0
base="$(basename "$clean")"
match="$(find "$PUBLIC_ROOT" "$REPO_ROOT/dist" "$REPO_ROOT/.next" -type f -name "$base" -print -quit 2>/dev/null || true)"
[[ -n "$match" ]] || return 0
dstdir="$(dirname "$target")"
sudo mkdir -p "$dstdir"
sudo cp -a "$match" "$target"
}
while IFS= read -r asset; do
[[ -n "$asset" ]] || continue
map_asset_path "$asset"
done < <(printf '%s' "$PAGE" | grep -oE '(href|src)="[^"]+\.(css|js|mjs|svg|png|ico|woff|woff2)(\?[^"]*)?"' | cut -d'"' -f2 | sort -u || true)
CSS_PATH="$(printf '%s' "$PAGE" | grep -oE 'href="[^"]+\.css[^"]*"' | head -n1 | cut -d'"' -f2 || true)"
if [[ -n "$CSS_PATH" ]]; then
case "$CSS_PATH" in
http://*|https://*) CSS_URL="$CSS_PATH" ;;
/*) CSS_URL="http://127.0.0.1:$LOCAL_PROXY_PORT$CSS_PATH" ;;
*) CSS_URL="http://127.0.0.1:$LOCAL_PROXY_PORT/$CSS_PATH" ;;
esac
if curl -fsS -H "Host: $TAILSCALE_IP:$LOCAL_PROXY_PORT" "$CSS_URL" >/dev/null; then
echo "Stylesheet reachable through proxy: $CSS_PATH"
else
echo "Stylesheet still returned 404: $CSS_PATH" >&2
echo "Built CSS files found:" >&2
find "$PUBLIC_ROOT" -type f -name '*.css' -print | head -20 >&2 || true
exit 44
fi
else
echo "Warning: no stylesheet link found in HTML." >&2
fi
echo "==> Verifying all JavaScript bundles through proxy"
JS_FAILURES=0
while IFS= read -r script; do
[[ -n "$script" ]] || continue
case "$script" in
http://*|https://*) JS_URL="$script" ;;
/*) JS_URL="http://127.0.0.1:$LOCAL_PROXY_PORT$script" ;;
*) JS_URL="http://127.0.0.1:$LOCAL_PROXY_PORT/$script" ;;
esac
CODE="$(curl -sS -o /dev/null -w '%{http_code}' -H "Host: $TAILSCALE_IP:$LOCAL_PROXY_PORT" "$JS_URL" || true)"
echo "$CODE $script"
if [[ "$CODE" != "200" ]]; then JS_FAILURES=$((JS_FAILURES+1)); fi
done < <(printf '%s' "$PAGE" | grep -oE 'src="[^"]+\\.(js|mjs)(\\?[^"]*)?"' | cut -d'"' -f2 | sort -u || true)
STATE_CODE="$(curl -sS -o /dev/null -w '%{http_code}' -H "Host: $TAILSCALE_IP:$LOCAL_PROXY_PORT" "http://127.0.0.1:$LOCAL_PROXY_PORT/api/state" || true)"
echo "$STATE_CODE /api/state"
if [[ "$STATE_CODE" != "200" ]]; then
echo "Dashboard API state is not reachable through the proxy." >&2
exit 45
fi
if [[ "$JS_FAILURES" -ne 0 ]]; then
echo "$JS_FAILURES JavaScript bundle(s) are still unavailable." >&2
exit 46
fi
echo "==> Publishing privately with Tailscale Serve"
sudo tailscale serve --bg "$LOCAL_PROXY_PORT"
DNS_NAME="$(tailscale status --json 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); print((d.get("Self",{}).get("DNSName") or "").rstrip("."))' 2>/dev/null || true)"
echo
echo "============================================================"
echo " AzerothAI private dashboard proxy is ready"
echo "============================================================"
if [[ -n "$DNS_NAME" ]]; then
echo "Open on any device in your tailnet:"
echo " https://$DNS_NAME/"
else
echo "Run: tailscale serve status"
echo "Then open the HTTPS URL it prints."
fi
echo
echo "Direct raw-IP access to :8787 can still be used for diagnostics,"
echo "but the Tailscale Serve HTTPS URL is the recommended remote dashboard."
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if [[ "$(id -u)" -eq 0 ]]; then
echo "Run this as your normal Ubuntu user, not root." >&2
exit 2
fi
discover_windows_host(){
local src prefix network candidate
src="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')"
prefix="$(ip -4 -o addr show scope global | awk 'NR==1{print $4}' | cut -d/ -f2)"
if [[ -n "$src" && -n "$prefix" && "$prefix" -ge 24 ]]; then
network="$(echo "$src" | awk -F. '{print $1"."$2"."$3}')"
candidate="$network.1"
if curl -fsS --connect-timeout 2 "http://$candidate:11434/api/tags" >/dev/null 2>&1; then
printf '%s' "$candidate"
return
fi
fi
local gw
gw="$(ip route | awk '/default/ {print $3; exit}')"
if [[ -n "$gw" ]] && curl -fsS --connect-timeout 2 "http://$gw:11434/api/tags" >/dev/null 2>&1; then
printf '%s' "$gw"
fi
}
if [[ -z "${WINDOWS_HOST_IP:-}" ]]; then
WINDOWS_HOST_IP="$(discover_windows_host || true)"
fi
if ! sudo -n true 2>/dev/null; then
echo "This setup needs sudo for Docker/packages. Ubuntu may ask for your password."
fi
echo "==> Installing Ubuntu prerequisites"
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl git jq openssl python3 unzip lvm2
echo "==> Expanding Ubuntu root LVM to use free virtual-disk space"
ROOT_DEV="$(findmnt -n -o SOURCE / || true)"
if [[ "$ROOT_DEV" == /dev/mapper/* ]] && command -v lvs >/dev/null 2>&1; then
FREE_BYTES="$(sudo vgs --noheadings --units b --nosuffix -o vg_free 2>/dev/null | awk '{sum+=$1} END{printf "%.0f",sum}')"
if [[ "${FREE_BYTES:-0}" -gt 1073741824 ]]; then
sudo lvextend -l +100%FREE -r "$ROOT_DEV"
fi
fi
df -h /
if ! command -v node >/dev/null 2>&1 || [[ "$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)" -lt 22 ]]; then
echo "==> Installing Node.js 22"
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs
fi
if ! command -v pnpm >/dev/null 2>&1; then
echo "==> Installing pnpm"
sudo corepack enable
sudo corepack prepare pnpm@11.25.0 --activate
fi
echo "Node: $(node --version)"
echo "pnpm: $(pnpm --version)"
if ! command -v docker >/dev/null 2>&1; then
echo "==> Installing Docker Engine"
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
echo
echo "Docker was installed and your user was added to the docker group."
echo "Log out of Ubuntu and back in, then rerun this script."
exit 20
fi
if ! docker info >/dev/null 2>&1; then
echo "Docker exists but this user cannot access it yet."
echo "Log out of Ubuntu and back in, then rerun this script."
exit 21
fi
echo "==> Checking Windows-hosted Ollama"
OLLAMA_AVAILABLE=0
if [[ -n "${WINDOWS_HOST_IP:-}" ]] && curl -fsS --connect-timeout 4 "http://$WINDOWS_HOST_IP:11434/api/tags" >/tmp/ollama-tags.json; then
OLLAMA_AVAILABLE=1
echo "Ollama reachable at http://$WINDOWS_HOST_IP:11434"
else
echo "Windows Ollama is not reachable yet. Continuing with deterministic AzerothAI decisions."
echo "The game realm can boot first; Qwen can be connected afterward without rebuilding AzerothCore."
fi
echo "==> Bootstrapping playable AzerothAI realm"
export AZEROTHAI_PLAYABLE_ROOT="${AZEROTHAI_PLAYABLE_ROOT:-$HOME/azerothai-pc-lab}"
bash "$REPO_ROOT/deploy/playable/bootstrap.sh"
LAB_ROOT="$AZEROTHAI_PLAYABLE_ROOT"
if [[ "$OLLAMA_AVAILABLE" -eq 1 ]]; then
echo "==> Pointing AzerothAI at Qwen on the Windows RTX host"
DEV_VARS="$REPO_ROOT/.dev.vars"
python3 - "$DEV_VARS" "http://$WINDOWS_HOST_IP:11434" <<'PY'
from pathlib import Path
import sys
p=Path(sys.argv[1])
value=sys.argv[2]
lines=p.read_text().splitlines() if p.exists() else []
out=[]
found=False
for line in lines:
if line.startswith("LLM_URL="):
out.append("LLM_URL="+value)
found=True
else:
out.append(line)
if not found:
out.append("LLM_URL="+value)
p.write_text("\n".join(out).rstrip()+"\n")
PY
chmod 600 "$DEV_VARS"
echo "==> Restarting AzerothAI runtime with host Ollama endpoint"
if [[ -f "$LAB_ROOT/azerothai.pid" ]]; then
pid="$(cat "$LAB_ROOT/azerothai.pid")"
kill "$pid" 2>/dev/null || true
sleep 1
fi
cd "$REPO_ROOT"
nohup pnpm start:lan >"$LAB_ROOT/azerothai.log" 2>&1 &
echo $! > "$LAB_ROOT/azerothai.pid"
for _ in {1..60}; do
if curl -fsS http://127.0.0.1:8787/api/health >/tmp/azerothai-health.json 2>/dev/null; then break; fi
sleep 1
done
fi
VM_IP="$(hostname -I | awk '{print $1}')"
echo
echo "============================================================"
echo " TEMPORARY PC LAB READY"
echo "============================================================"
echo "Ubuntu VM IP : $VM_IP"
echo "Windows host : ${WINDOWS_HOST_IP:-not connected yet}"
if [[ "$OLLAMA_AVAILABLE" -eq 1 ]]; then
echo "Ollama : http://$WINDOWS_HOST_IP:11434"
else
echo "Ollama : not connected yet (realm still usable)"
fi
echo
echo "Next:"
echo " 1. Create your WoW/GM account using the worldserver console."
echo " 2. Put the Ubuntu VM IP into Windows WoW realmlist.wtf."
echo " 3. Open AzerothAI World Monitor from Windows/iPad:"
echo " http://$VM_IP:8787"
echo
echo "Check everything:"
echo " AZEROTHAI_PLAYABLE_ROOT=$AZEROTHAI_PLAYABLE_ROOT bash deploy/playable/status.sh"