diff --git a/CLAUDE.md b/CLAUDE.md index bd5ec4b..2783720 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,6 +49,17 @@ The system's most distinctive property: highlighting in blind mode reveals **zer - **Health:** `curl https://chess.sethpc.xyz/api/health` - **Deploy update:** `pnpm -r build` → `pnpm --filter @blind-chess/server deploy --prod --legacy .deploy-server` → rsync server bundle to `/opt/blind-chess/server/` and client `dist/` to `/opt/blind-chess/client/dist/` → `chown -R blindchess:blindchess /opt/blind-chess` → `systemctl restart blind-chess`. Server restart drops in-memory games (acceptable for MVP). +### Local instance — `chess.local` on VDJ-RIG + +A second, **LAN-only** deploy on VDJ-RIG (192.168.0.143), fully independent of the CT 690 / chess.sethpc.xyz instance (separate in-memory state). Serves on **port 80**, reached at `http://chess.local` via an mDNS alias — no Caddy, no TLS. + +- **Artifacts** (`deploy/`): `blind-chess-local.service` (server unit; binds port 80 as the non-root `blindchess` user via `CAP_NET_BIND_SERVICE`), `chess-mdns-alias` + `chess-mdns-alias.service` (publishes the `chess.local` mDNS name with `avahi-publish -a -R` — `-R` avoids a reverse-PTR collision with the host's own `.local` name), `install-local.sh` (root-side installer). +- **On the rig:** tree at `/opt/blind-chess/{server,client/dist}`, units `blind-chess.service` + `chess-mdns-alias.service`, Node 22 via NodeSource. +- **Logs:** `ssh vdj-rig 'journalctl -u blind-chess -f'` +- **Restart:** `ssh vdj-rig 'sudo systemctl restart blind-chess'` +- **Health:** `curl http://chess.local/api/health` +- **Redeploy:** on steel141 `pnpm -r build` + `pnpm --filter @blind-chess/server deploy --prod --legacy .deploy-server`; rsync `.deploy-server/` → rig `~/blind-chess-stage/server/`, `packages/client/dist/` → `~/blind-chess-stage/client-dist/`, and the `deploy/` files → `~/blind-chess-stage/`; then `sudo bash ~/blind-chess-stage/install-local.sh`. + ## Conventions - Inherits global homelab conventions from `~/bin/CLAUDE.md` (gitea CLI, conventional commits, `.claude/handoffs/` for session state). diff --git a/deploy/blind-chess-local.service b/deploy/blind-chess-local.service new file mode 100644 index 0000000..59d0a8d --- /dev/null +++ b/deploy/blind-chess-local.service @@ -0,0 +1,36 @@ +[Unit] +Description=blind_chess server — local LAN instance (chess.local) +Documentation=https://git.sethpc.xyz/Seth/blind_chess +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=blindchess +Group=blindchess +WorkingDirectory=/opt/blind-chess/server +ExecStart=/usr/bin/node /opt/blind-chess/server/dist/server.js +Environment=NODE_ENV=production +Environment=PORT=80 +Environment=HOST=0.0.0.0 +Environment=STATIC_DIR=/opt/blind-chess/client/dist +Environment=PUBLIC_BASE=http://chess.local +Environment=LOG_LEVEL=info +Restart=always +RestartSec=2s +StandardOutput=journal +StandardError=journal + +# Bind privileged port 80 as a non-root user +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE + +# Hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/blind-chess + +[Install] +WantedBy=multi-user.target diff --git a/deploy/chess-mdns-alias b/deploy/chess-mdns-alias new file mode 100644 index 0000000..e4a05e9 --- /dev/null +++ b/deploy/chess-mdns-alias @@ -0,0 +1,21 @@ +#!/bin/bash +# Publish "chess.local" as an mDNS alias for this host's primary IPv4 address. +# Invoked by chess-mdns-alias.service (blind_chess local deploy). +# +# avahi-daemon already advertises the host's own .local; this adds +# the friendly "chess.local" name pointing at the same machine. Runs in the +# foreground holding the registration until the service is stopped. +set -euo pipefail + +IP="$(hostname -I | awk '{print $1}')" +if [ -z "$IP" ]; then + echo "chess-mdns-alias: no IPv4 address found" >&2 + exit 1 +fi + +echo "chess-mdns-alias: publishing chess.local -> $IP" +# -R/--no-reverse: skip the reverse (PTR) record. avahi-daemon already owns the +# PTR for this IP via the host's own .local, so publishing chess.local +# for the same address *with* a reverse entry collides ("Local name collision"). +# Clients only need the forward A record, which -a still publishes. +exec avahi-publish -a -R chess.local "$IP" diff --git a/deploy/chess-mdns-alias.service b/deploy/chess-mdns-alias.service new file mode 100644 index 0000000..e3deeb2 --- /dev/null +++ b/deploy/chess-mdns-alias.service @@ -0,0 +1,14 @@ +[Unit] +Description=Publish chess.local mDNS alias for the blind_chess local deploy +Requires=avahi-daemon.service +After=avahi-daemon.service network-online.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/chess-mdns-alias +Restart=always +RestartSec=5s + +[Install] +WantedBy=multi-user.target diff --git a/deploy/install-local.sh b/deploy/install-local.sh new file mode 100644 index 0000000..1181772 --- /dev/null +++ b/deploy/install-local.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# blind_chess — local (chess.local) deploy installer. +# +# Run as root ON THE TARGET HOST. Expects a staging directory containing: +# server/ pnpm-deploy bundle (dist/ + node_modules/) +# client-dist/ vite build output +# chess-mdns-alias mDNS alias helper script +# blind-chess-local.service systemd unit for the server +# chess-mdns-alias.service systemd unit for the mDNS alias +# +# Usage: sudo bash install-local.sh [STAGE_DIR] +# (STAGE_DIR defaults to the directory containing this script) +set -euo pipefail + +STAGE="${1:-$(cd "$(dirname "$0")" && pwd)}" +echo "=== blind_chess local install (staging: $STAGE) ===" + +# --- Node.js 22 (Debian trixie ships only 20; blind_chess needs >=22) --- +need_node=1 +if command -v node >/dev/null 2>&1; then + major="$(node -e 'process.stdout.write(String(process.versions.node.split(".")[0]))' 2>/dev/null || echo 0)" + if [ "${major:-0}" -ge 22 ] 2>/dev/null; then need_node=0; fi +fi +if [ "$need_node" -eq 1 ]; then + echo "--- installing Node.js 22 via NodeSource ---" + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + apt-get install -y -o DPkg::Lock::Timeout=600 nodejs +fi +echo "node: $(node --version)" + +# --- avahi-utils provides avahi-publish --- +command -v avahi-publish >/dev/null 2>&1 || \ + apt-get install -y -o DPkg::Lock::Timeout=600 avahi-utils + +# --- dedicated unprivileged service user --- +getent passwd blindchess >/dev/null 2>&1 || \ + useradd --system --user-group --no-create-home --shell /usr/sbin/nologin blindchess + +# --- deploy tree under /opt/blind-chess --- +install -d /opt/blind-chess +rm -rf /opt/blind-chess/server /opt/blind-chess/client +cp -a "$STAGE/server" /opt/blind-chess/server +install -d /opt/blind-chess/client +cp -a "$STAGE/client-dist" /opt/blind-chess/client/dist +chown -R blindchess:blindchess /opt/blind-chess + +# --- mDNS alias helper --- +install -m 0755 "$STAGE/chess-mdns-alias" /usr/local/bin/chess-mdns-alias + +# --- systemd units (the server unit installs under the canonical name) --- +install -m 0644 "$STAGE/blind-chess-local.service" /etc/systemd/system/blind-chess.service +install -m 0644 "$STAGE/chess-mdns-alias.service" /etc/systemd/system/chess-mdns-alias.service +systemctl daemon-reload +systemctl enable --now chess-mdns-alias.service +systemctl enable --now blind-chess.service + +echo "=== install complete ===" +systemctl --no-pager --lines=0 status blind-chess.service chess-mdns-alias.service || true