diff --git a/App plan.md b/App plan.md new file mode 100644 index 0000000..f6904ce --- /dev/null +++ b/App plan.md @@ -0,0 +1,314 @@ +Here’s a concrete, **handoff-ready plan** you can give to another AI to implement a first version (v1) of a **Spelljammer Ship Tracker** with **multi-ship CRUD, constrained editing, SQLite persistence, and <1s live updates**. + +--- + +## 0) Product goal (v1) + +A small phone-friendly web app where your group can: + +* Create/select/delete ships (delete requires confirm modal) +* View & edit **core ship stats** + **current weapons on the ship** +* See changes propagate to all connected clients within ~1s +* Persist “real data” server-side in **SQLite** (ship data + weapons + optional reference content) +* Show a **read-only Rules/Reference** screen for battle lookups + +No authentication. Anyone can edit anything. + +--- + +## 1) Recommended stack (simple + robust) + +**Option A (recommended): Node.js + Fastify + Socket.IO + SQLite (better-sqlite3) + React (Vite)** + +* Fast to implement, great websocket support, easy Docker build. +* Socket.IO simplifies reconnects & room-based updates (per ship). +* better-sqlite3 is reliable and fast for low-concurrency apps. + +**Option B:** Node.js + Express + ws + SQLite +(also fine, but more manual reconnection & rooms logic) + +Either way: **single Docker Compose** with one container; SQLite is a mounted file. + +--- + +## 2) Architecture + +### Backend + +* REST API for CRUD and initial state fetch. +* WebSocket (Socket.IO) for real-time updates: + + * Clients join a “ship room” when viewing a ship. + * When any field changes, server: + + 1. validates & persists to SQLite + 2. broadcasts an update event to that room + +### Frontend + +Phone-first layout, 3–4 screens: + +1. **Ship List** (create/select/delete) +2. **Ship Dashboard** (core stats + weapons list) +3. **Edit Weapon** (or inline editing) +4. **Rules/Reference** (read-only) + +Use constrained controls: + +* numeric steppers/sliders for numbers +* dropdowns for enums (size category, maneuverability class, etc.) +* toggles for booleans +* “Add weapon” form with select + numeric inputs + +--- + +## 3) Data model (SQLite) + +Implement migrations on startup. + +### Tables + +**ships** + +* `id` TEXT (uuid, PK) +* `name` TEXT (unique-ish, not required but recommended) +* Core stats (examples; adjust to your desired fields): + + * `hull_current` INTEGER NOT NULL + * `hull_max` INTEGER NOT NULL + * `armor_current` INTEGER NOT NULL + * `armor_max` INTEGER NOT NULL + * `ac` INTEGER NOT NULL + * `con_save` INTEGER NULL (or `con_score`) + * `speed` INTEGER NULL + * `maneuver_class` TEXT NULL (enum: A/B/C/D/E/F/S per your rules) + * `size_category` TEXT NULL +* `notes` TEXT NULL +* `updated_at` INTEGER (unix ms) + +**weapons** + +* `id` TEXT (uuid, PK) +* `ship_id` TEXT (FK -> ships.id, indexed) +* `name` TEXT NOT NULL +* `type` TEXT NULL (optional enum: siege/arcane/etc.) +* `attack_mod` INTEGER NULL +* `damage` TEXT NULL (freeform like “3d10”) +* `range` TEXT NULL +* `ammo_current` INTEGER NULL +* `ammo_max` INTEGER NULL +* `status` TEXT NULL (enum: ok/damaged/disabled) +* `notes` TEXT NULL +* `sort_order` INTEGER NOT NULL default 0 +* `updated_at` INTEGER + +**rules_pages** (optional; for read-only reference) + +* `id` TEXT PK (e.g., “battle”, “ship-stats”) +* `title` TEXT +* `content_markdown` TEXT +* `updated_at` INTEGER + +> Keep rules content as markdown stored in DB **or** ship it as static markdown files in the frontend. For v1, static markdown is simplest. + +--- + +## 4) API design (REST) + +### Ships + +* `GET /api/ships` → list ships `{id, name, updated_at}` +* `POST /api/ships` → create `{name, initialStats...}` → returns full ship +* `GET /api/ships/:id` → full ship + weapons +* `PATCH /api/ships/:id` → update core stats (validated + constrained) +* `DELETE /api/ships/:id` → delete ship + weapons + +### Weapons + +* `POST /api/ships/:id/weapons` → add weapon +* `PATCH /api/weapons/:weaponId` → update weapon fields +* `DELETE /api/weapons/:weaponId` → delete weapon + +Validation rules (server-enforced): + +* current values must be `0 <= current <= max` +* max values must be `>= 0` +* enums must be in allowed set +* name length limits + +--- + +## 5) WebSocket events (Socket.IO) + +### Client → Server + +* `ship:join` `{shipId}` +* `ship:leave` `{shipId}` +* `ship:update` `{shipId, patch}` // core stats patch +* `weapon:create` `{shipId, weapon}` +* `weapon:update` `{weaponId, patch}` +* `weapon:delete` `{weaponId}` + +### Server → Client (broadcast to ship room) + +* `ship:state` `{ship, weapons}` // on join, or after major changes +* `ship:patched` `{shipId, patch, updated_at}` +* `weapon:created` `{shipId, weapon}` +* `weapon:patched` `{shipId, weaponId, patch, updated_at}` +* `weapon:deleted` `{shipId, weaponId}` + +Implementation approach: + +* On any mutation, server writes to DB then emits event. +* On join, server emits current `ship:state`. +* Clients apply patches locally; on reconnect, re-join and refresh state. + +Conflict model: + +* **Last write wins** is fine (your requirement). Keep it simple. + +--- + +## 6) Frontend UI details (phone-first) + +### Screen 1: Ship List + +* List cards: name + “last updated” +* Buttons: + + * “Create ship” (modal with name + optional initial stats) + * Tap card to open + * Delete icon → confirm modal (“Delete ship X?”) + +### Screen 2: Ship Dashboard + +Sections: + +1. **Vitals** + + * Hull: current/max (stepper) + * Armor: current/max + * AC (stepper) +2. **Mobility** + + * Speed (stepper) + * Maneuver class (dropdown) + * Size category (dropdown) +3. **Weapons** + + * List weapons with quick fields (ammo, status) + * Tap weapon to open “Weapon details” + * “Add weapon” button +4. **Notes** + + * Multi-line text + +Include a top bar: + +* Ship name +* “Rules” link/button +* “Back to ships” + +### Screen 3: Weapon Details (or modal) + +Constrained inputs for ammo/status/attack_mod. +Freeform for damage/range/notes. + +### Screen 4: Rules/Reference + +* Static markdown viewer with headings + quick links +* Optional “search within rules” (client-side filter) + +--- + +## 7) Persistence + backups + +* SQLite file at `/data/spelljammer.sqlite` +* Docker volume mount that path. +* Optional: periodic backup script/cron outside app (copy DB file nightly). + +--- + +## 8) Docker Compose (single service) + +* Build a single image that serves both: + + * backend API + websockets + * frontend static files +* Expose one port (e.g., 3000) to Caddy reverse proxy. + +Example structure (for the implementing AI): + +* `/server` Fastify app +* `/web` React app build output served by server (or Caddy, but keep simple) + +--- + +## 9) Implementation steps (what the AI should do in order) + +1. Create repo scaffold with `/server` and `/web` +2. Implement SQLite schema + migrations +3. Implement REST endpoints + validation +4. Add Socket.IO with rooms: + + * join/leave + * mutation events that persist + broadcast +5. Build React UI: + + * routing for screens + * state store (zustand or simple context) + * socket connection + reconnection handling +6. Wire CRUD flows: + + * list → select → join room → render state + * edit fields → emit update → optimistic UI +7. Add delete confirmation modal +8. Add rules markdown page (static) +9. Dockerize + compose + test behind Caddy + +--- + +## 10) Acceptance criteria checklist + +* ✅ Create ship, appears in list, persists after restart +* ✅ Select ship, see dashboard with core stats + weapons +* ✅ Delete ship requires confirm, removes weapons too +* ✅ All edits propagate to another browser within <1s +* ✅ App is usable on phone (single column layout, large touch targets) +* ✅ SQLite file persists via mounted volume +* ✅ WebSocket reconnect works (refresh state on reconnect) +* ✅ No auth, no user accounts + +--- + +## 11) “Reference guide” integration (v1) + +Since you want read-only rules “for during battle”: +Start with a **curated markdown** page that copies only the snippets you frequently need (e.g., maneuverability classes, hull/armor notes, weapon usage, combat steps). You can refine later. + +(If you want, I can extract a concise “Battle Reference” page from your PDF and format it as markdown for the AI to drop into `/web/src/rules/battle.md`.) + +--- + +## 12) v1 Implementation Status + +**Status: v1 COMPLETE** (implemented 2026-02-19) + +All 14 commits have been implemented: + +1. ✅ Project scaffold (Fastify + Vite + TypeScript) +2. ✅ SQLite connection + migration system +3. ✅ Ship REST API with Zod validation +4. ✅ Weapon REST API with validation +5. ✅ Socket.IO with rooms + real-time events +6. ✅ React routing + layout (mobile-first dark theme) +7. ✅ Zustand stores + Socket.IO client +8. ✅ Ship List page with create modal +9. ✅ Ship Dashboard (vitals + mobility) +10. ✅ Weapons section (add/edit/detail modals + notes) +11. ✅ Delete confirmation polish +12. ✅ Rules/Reference page (collapsible battle reference) +13. ✅ Dockerfile + docker-compose +14. ✅ README with testing + deployment docs + +See `README.md` for full setup, testing, and deployment instructions. diff --git a/README.md b/README.md index f2da043..28cd62c 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,190 @@ A phone-friendly web app for tracking Spelljammer ships during D&D sessions. Sup See `App plan.md` for the full product spec. -## Quick Start (Development) +## 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 -npm run install:all -npm run dev -# Open http://localhost:5173 +# 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 ``` -## Quick Start (Docker) +### 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 ``` -*Full setup, testing, and deployment docs will be added once implementation is complete.* +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. diff --git a/server/test-api.sh b/server/test-api.sh new file mode 100644 index 0000000..7640626 --- /dev/null +++ b/server/test-api.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# API test script for Spelljammer Ship Tracker +# Run with: bash server/test-api.sh +# Requires: curl, jq (optional, for pretty output) + +BASE=http://localhost:3000/api +JQ="cat" +command -v jq > /dev/null 2>&1 && JQ="jq ." + +echo "=== Health Check ===" +curl -s $BASE/health | $JQ +echo "" + +echo "=== Create Ship ===" +SHIP=$(curl -s -X POST $BASE/ships \ + -H 'Content-Type: application/json' \ + -d '{"name":"Astral Clipper","hull_max":100,"hull_current":100,"armor_max":50,"armor_current":50,"ac":15,"speed":8,"maneuver_class":"B"}') +echo "$SHIP" | $JQ +SHIP_ID=$(echo "$SHIP" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) +echo "Ship ID: $SHIP_ID" +echo "" + +echo "=== List Ships ===" +curl -s $BASE/ships | $JQ +echo "" + +echo "=== Get Ship (with weapons) ===" +curl -s $BASE/ships/$SHIP_ID | $JQ +echo "" + +echo "=== Update Ship (take 15 hull damage) ===" +curl -s -X PATCH $BASE/ships/$SHIP_ID \ + -H 'Content-Type: application/json' \ + -d '{"hull_current":85}' | $JQ +echo "" + +echo "=== Add Weapon: Ballista ===" +WEAPON=$(curl -s -X POST $BASE/ships/$SHIP_ID/weapons \ + -H 'Content-Type: application/json' \ + -d '{"name":"Ballista","attack_mod":6,"damage":"3d10","range":"120/480","ammo_max":10,"ammo_current":10}') +echo "$WEAPON" | $JQ +WEAPON_ID=$(echo "$WEAPON" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) +echo "Weapon ID: $WEAPON_ID" +echo "" + +echo "=== Add Weapon: Spellcannon ===" +curl -s -X POST $BASE/ships/$SHIP_ID/weapons \ + -H 'Content-Type: application/json' \ + -d '{"name":"Spellcannon","attack_mod":8,"damage":"4d8","range":"60/240","ammo_max":5,"ammo_current":5}' | $JQ +echo "" + +echo "=== Update Weapon (fire, reduce ammo) ===" +curl -s -X PATCH $BASE/weapons/$WEAPON_ID \ + -H 'Content-Type: application/json' \ + -d '{"ammo_current":9}' | $JQ +echo "" + +echo "=== Get Ship (verify weapons attached) ===" +curl -s $BASE/ships/$SHIP_ID | $JQ +echo "" + +echo "=== Delete Weapon ===" +curl -s -X DELETE $BASE/weapons/$WEAPON_ID | $JQ +echo "" + +echo "=== Delete Ship (cascades to remaining weapons) ===" +curl -s -X DELETE $BASE/ships/$SHIP_ID | $JQ +echo "" + +echo "=== List Ships (should be empty or minus the deleted one) ===" +curl -s $BASE/ships | $JQ +echo "" + +echo "=== All tests completed ==="