Deploy to a DigitalOcean droplet: wizwar is on the internet
Production shape: one Node process serves the built client and the websocket on a single port (SPA fallback, same-origin wss in the client), with Caddy terminating auto-TLS in front. The droplet (nyc3, $6/mo) runs it under systemd as an unprivileged user with room files on the persistent disk at /var/lib/wizwar/rooms — deploys and reboots cannot eat a game. deploy/ carries the one-time droplet setup script, the systemd unit, the Caddyfile, an everyday deploy script (build locally, rsync, install, restart), and a README. Live at https://wizwar.104.236.96.198.sslip.io via sslip.io, so TLS needed no DNS setup at all. Verified over the real internet: room created, second player joined, expansion game started, hands dealt, room file persisted. Fixed en route: rsync's unanchored "data" exclude was stripping the engine's card database, and tsx must ship (it is the runtime). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
002dbd2547
commit
23bdd009a0
@@ -11,6 +11,9 @@
|
||||
// {type:"state", view} redacted full view (after every change)
|
||||
// {type:"error", message}
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { extname, join, normalize } from "node:path";
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
import type { Command, PlayerId } from "@wizwar/engine";
|
||||
import {
|
||||
@@ -30,7 +33,33 @@ import {
|
||||
|
||||
const port = Number(process.env.PORT ?? 8787);
|
||||
loadPersistedRooms();
|
||||
const wss = new WebSocketServer({ port });
|
||||
|
||||
// One process serves both the built client and the websocket, so production
|
||||
// needs only a TLS proxy in front (or nothing, on a LAN).
|
||||
const STATIC_DIR = process.env.WIZWAR_STATIC_DIR ?? join(process.cwd(), "..", "web", "dist");
|
||||
const MIME: Record<string, string> = {
|
||||
".html": "text/html", ".js": "text/javascript", ".css": "text/css",
|
||||
".png": "image/png", ".svg": "image/svg+xml", ".ico": "image/x-icon",
|
||||
".woff2": "font/woff2", ".json": "application/json",
|
||||
};
|
||||
const httpServer = createServer((req, res) => {
|
||||
try {
|
||||
const url = (req.url ?? "/").split("?")[0]!;
|
||||
let file = normalize(join(STATIC_DIR, url === "/" ? "index.html" : url));
|
||||
if (!file.startsWith(normalize(STATIC_DIR))) {
|
||||
res.writeHead(403).end();
|
||||
return;
|
||||
}
|
||||
if (!existsSync(file)) file = join(STATIC_DIR, "index.html"); // SPA fallback
|
||||
const body = readFileSync(file);
|
||||
res.writeHead(200, { "content-type": MIME[extname(file)] ?? "application/octet-stream" });
|
||||
res.end(body);
|
||||
} catch {
|
||||
res.writeHead(500).end();
|
||||
}
|
||||
});
|
||||
const wss = new WebSocketServer({ server: httpServer, path: undefined });
|
||||
httpServer.listen(port);
|
||||
|
||||
interface Session {
|
||||
socket: WebSocket;
|
||||
@@ -182,4 +211,4 @@ wss.on("connection", (socket) => {
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`wizwar server listening on ws://localhost:${port}`);
|
||||
console.log(`wizwar serving client + websocket on port ${port}`);
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import type { Command, GameEvent, GameView } from "@wizwar/engine";
|
||||
import { cardDef } from "@wizwar/engine";
|
||||
|
||||
const SERVER_URL = import.meta.env.VITE_WIZWAR_SERVER ?? "ws://localhost:8787";
|
||||
const SERVER_URL =
|
||||
import.meta.env.VITE_WIZWAR_SERVER ??
|
||||
(location.hostname === "localhost"
|
||||
? "ws://localhost:8787"
|
||||
: `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/ws`);
|
||||
|
||||
export function humanize(e: GameEvent): string | null {
|
||||
switch (e.type) {
|
||||
|
||||
Reference in New Issue
Block a user