# Spelljammer Ship Tracker A phone-friendly web app for tracking Spelljammer ships during D&D sessions. Supports multi-ship CRUD, real-time sync via WebSocket, and a battle rules reference. See `App plan.md` for the full product spec. ## Tech Stack - **Backend:** Node.js + Fastify + Socket.IO + better-sqlite3 + Zod - **Frontend:** React 19 + Vite + react-router-dom + zustand + socket.io-client - **Language:** TypeScript everywhere - **Database:** SQLite (file-based, zero config) - **Deployment:** Docker + Docker Compose ## Prerequisites - **Node.js** >= 18 (tested with v18.20.8) - **npm** >= 8 - **Docker + Docker Compose** (for production deployment only) ## Development Setup ```bash # Install dependencies for both server and web cd server && npm install cd ../web && npm install cd .. # Or use the convenience script: npm install # installs root deps (concurrently) npm run install:all # installs server + web deps ``` ### Running in Development **Option A: Two terminals** ```bash # Terminal 1 — Backend (port 3000) cd server && npm run dev # Terminal 2 — Frontend (port 5173, proxies API to 3000) cd web && npm run dev ``` **Option B: Single command** ```bash npm run dev ``` Then open **http://localhost:5173** in your browser. The Vite dev server proxies `/api` and `/socket.io` requests to the Fastify server, so everything works from a single URL. ## Production Deployment (Docker) ```bash # Build and start docker compose up -d --build # Open http://localhost:3000 ``` The Docker image: - Multi-stage build (web build → server build → production) - Serves both API and frontend from a single container on port 3000 - SQLite database stored in a Docker volume at `/data/spelljammer.sqlite` ### Stopping ```bash docker compose down ``` ### Database Backup ```bash # Copy the SQLite file out of the container docker cp spelljammer-ships:/data/spelljammer.sqlite ./backup-$(date +%F).sqlite ``` ### Reverse Proxy (Optional) If you want HTTPS, put Caddy or nginx in front: ``` # Example Caddyfile spelljammer.yourdomain.com { reverse_proxy localhost:3000 } ``` ## Testing ### API Tests Start the server, then run the test script: ```bash cd server && npm run dev # In another terminal: bash server/test-api.sh ``` This runs through the full CRUD lifecycle: create ship → update stats → add weapons → fire (reduce ammo) → delete. ### Real-Time Sync Test 1. Open the app in two browser tabs 2. Create a ship in one tab — it should appear in the other when you refresh the list 3. Open the same ship in both tabs 4. Change hull points in one — the other should update within ~1 second 5. Add/edit/delete weapons — verify sync ### Mobile Test - Use Chrome DevTools (F12 → device toggle) to simulate a phone - Or open the URL from your phone on the same network ## Project Structure ``` ├── server/ # Fastify backend │ ├── src/ │ │ ├── index.ts # Entry point (Fastify + Socket.IO bootstrap) │ │ ├── config.ts # Environment config │ │ ├── db/ # SQLite connection + migrations │ │ ├── routes/ # REST API endpoints │ │ ├── services/ # Database query logic │ │ ├── socket/ # Socket.IO event handlers │ │ └── validation/ # Zod schemas │ └── test-api.sh # curl-based API test script ├── web/ # React frontend │ ├── src/ │ │ ├── App.tsx # Router setup │ │ ├── socket.ts # Socket.IO client │ │ ├── store/ # Zustand state stores │ │ ├── pages/ # Route pages │ │ ├── components/ # UI components │ │ ├── rules/ # Battle reference content │ │ └── types/ # TypeScript interfaces │ └── vite.config.ts # Dev proxy config ├── Dockerfile # Multi-stage production build ├── docker-compose.yml # Single-service compose └── App plan.md # Full product specification ``` ## API Reference ### Ships | Method | Endpoint | Description | | -------- | ----------------- | ------------------------ | | `GET` | `/api/ships` | List all ships | | `POST` | `/api/ships` | Create a new ship | | `GET` | `/api/ships/:id` | Get ship with weapons | | `PATCH` | `/api/ships/:id` | Update ship fields | | `DELETE` | `/api/ships/:id` | Delete ship + weapons | ### Weapons | Method | Endpoint | Description | | -------- | --------------------------- | ----------------- | | `POST` | `/api/ships/:id/weapons` | Add weapon to ship| | `PATCH` | `/api/weapons/:weaponId` | Update weapon | | `DELETE` | `/api/weapons/:weaponId` | Delete weapon | ### WebSocket Events | Direction | Event | Purpose | | ---------------- | ----------------- | ---------------------------------- | | Client → Server | `ship:join` | Join a ship's room | | Client → Server | `ship:leave` | Leave a ship's room | | Client → Server | `ship:update` | Update ship stats | | Client → Server | `weapon:create` | Add a weapon | | Client → Server | `weapon:update` | Update a weapon | | Client → Server | `weapon:delete` | Delete a weapon | | Server → Client | `ship:state` | Full ship+weapons state (on join) | | Server → Client | `ship:patched` | Ship field(s) updated | | Server → Client | `weapon:created` | New weapon added | | Server → Client | `weapon:patched` | Weapon field(s) updated | | Server → Client | `weapon:deleted` | Weapon removed | ## Troubleshooting **Server won't start:** Check that port 3000 is free (`lsof -i :3000` or `netstat -an | grep 3000`). **WebSocket not connecting:** In development, make sure Vite proxy is configured (check `web/vite.config.ts`). The proxy forwards `/socket.io` to port 3000. **Database locked:** If the server crashes without clean shutdown, the SQLite WAL/journal files may remain. Delete `data/spelljammer.sqlite-wal` and `data/spelljammer.sqlite-shm`, then restart. **Docker build fails on better-sqlite3:** The `node:18-alpine` image includes prebuilt binaries. If issues persist, switch to `node:18-slim` in the Dockerfile.