From 79cf3ca13e33ec8e568e6c3447e990638e695708 Mon Sep 17 00:00:00 2001 From: gauvainboiche Date: Sun, 29 Mar 2026 12:23:57 +0200 Subject: [PATCH] feat: Adding logging solution + alert for mobile phones --- .env.example | 19 +++ .gitignore | 1 + Dockerfile | 16 ++- README.md | 98 ++++++++++++--- docker-compose.yml | 29 ++--- public/index.html | 112 ++++++++++------- public/style.css | 306 ++++++++++++++++++++++++++++----------------- 7 files changed, 383 insertions(+), 198 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..223d05f --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# ── Server ─────────────────────────────────────────────────────────────────── +PORT=8080 + +# ── Security ───────────────────────────────────────────────────────────────── +# Generate with: node -e "console.log(require('crypto').randomBytes(48).toString('hex'))" +JWT_SECRET=CHANGE_ME_TO_A_RANDOM_STRING + +# ── Game database (PostgreSQL) ──────────────────────────────────────────────── +POSTGRES_USER=game +POSTGRES_PASSWORD=CHANGE_ME +POSTGRES_DB=star_wars_grid + +# ── Users database (PostgreSQL) ─────────────────────────────────────────────── +POSTGRES_USERS_USER=users +POSTGRES_USERS_PASSWORD=CHANGE_ME +POSTGRES_USERS_DB=star_wars_users + +# ── CORS ───────────────────────────────────────────────────────────────────── +CORS_ORIGIN=* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1254cea..54dbab5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ data/postgres/ +data/postgres_users/ .env .env.local .env.*.local \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fb1c2bf..9fffce0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,29 @@ FROM node:20-alpine +# Create non-root user/group before switching context +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + WORKDIR /app +# Install dependencies first for better layer caching COPY package.json package-lock.json ./ RUN npm ci --omit=dev +# Copy application source COPY server ./server COPY public ./public COPY config ./config +# Drop to non-root user +USER appuser + +ENV NODE_ENV=production ENV PORT=8080 -ENV CLICK_COOLDOWN_SECONDS=5 EXPOSE 8080 -CMD ["node", "server/index.js"] +# Health-check: lightweight wget is available in node:alpine +HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \ + CMD wget -qO- http://localhost:8080/api/config > /dev/null || exit 1 + +CMD ["node", "server/index.js"] \ No newline at end of file diff --git a/README.md b/README.md index 4d0a7fe..65bd755 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,48 @@ -# Star Wars - Wild Space +# Star Wars – Wild Space Exploitable ring only (outer Ø100, inner Ø60). Planet positions are deterministic from the **server world seed**; **stats stay hidden** until you click a tile. Reveals are **persisted in PostgreSQL**. +> **Desktop only** — the galaxy map is not playable on phones or narrow screens (< 768 px wide). + +## Layout + +The UI is split into two panes: + +| Pane | Content | +|------|---------| +| **Left column** | Title, team scores, player info, cooldown timer, world seed & period countdown, Refresh button, selection details | +| **Right side** | Galaxy map — a perfect square anchored to the right edge of the viewport | + +## Secrets & environment variables + +**Never commit secrets.** All credentials live in **`.env`** (git-ignored). +Copy the template and fill in your values before the first run: + +```powershell +cp .env.example .env +# then edit .env +``` + +| Variable | Description | +|----------|-------------| +| `JWT_SECRET` | Secret used to sign JWT tokens — use a long random string in production | +| `POSTGRES_USER` | Game-DB username (default `game`) | +| `POSTGRES_PASSWORD` | Game-DB password — **change in production** | +| `POSTGRES_DB` | Game-DB name (default `star_wars_grid`) | +| `POSTGRES_USERS_USER` | Users-DB username (default `users`) | +| `POSTGRES_USERS_PASSWORD` | Users-DB password — **change in production** | +| `POSTGRES_USERS_DB` | Users-DB name (default `star_wars_users`) | +| `PORT` | HTTP port exposed by the app (default `8080`) | + ## Runtime config (file + volume) -Edit **`config/game.settings.json`** on the host (mounted into the container at `/app/config/game.settings.json`). The server reloads it when the file’s **mtime** changes, on a schedule controlled by **`configReloadIntervalSeconds`** (minimum 5s), so frequent polling is avoided when nothing changed. +Edit **`config/game.settings.json`** on the host (mounted into the container at `/app/config/game.settings.json`). The server reloads it when the file's **mtime** changes, on a schedule controlled by **`configReloadIntervalSeconds`** (minimum 5 s), so frequent polling is avoided when nothing changed. | Field | Meaning | -|--------|--------| +|-------|---------| | `clickCooldownSeconds` | Cooldown between **new** reveals (same as before). | -| `databaseWipeoutIntervalSeconds` | World period length in **seconds** (default `21600` = 6h). The **world seed** is `swg-` with `slot = floor(UTC unix seconds / this value)`. When the slot changes, **`grid_cells` is truncated** (full wipe). | -| `debugModeForTeams` | If `true` or string `"true"` / `"True"` (case-insensitive), the **Blue / Red** segmented control is shown; if `false`, it is hidden. | +| `databaseWipeoutIntervalSeconds` | World period length in **seconds** (default `21600` = 6 h). The **world seed** is `swg-` with `slot = floor(UTC unix seconds / this value)`. When the slot changes, **`grid_cells` is truncated** (full wipe). | +| `debugModeForTeams` | If `true`, the **Blue / Red** segmented control is shown; if `false`, it is hidden. | | `configReloadIntervalSeconds` | How often the server **checks** the config file (mtime); also used by the client to poll `/api/config`. | `GET /api/config` returns these values plus **`worldSeed`**, **`seedPeriodEndsAtUtc`**, **`seedPeriodStartsAtUtc`**. @@ -18,45 +50,73 @@ Edit **`config/game.settings.json`** on the host (mounted into the container at ## Teams (blue / red) - With **`debugModeForTeams`**: use the **Team** control (top-left) to switch perspective. -- **Your** discovered tiles use your team color (blue: bright cyan; red: red when revealed). Unclaimed ring tiles use the **classic** idle tint for both teams. +- **Your** discovered tiles use your team colour. Unclaimed ring tiles use the classic idle tint for both teams. - Tiles discovered by the **other** team appear **grey**, show **no planet**, and are **not clickable**. First reveal **owns** the tile (`discovered_by`). ## Cooldown -After revealing a **new** tile, a **cooldown** runs (top-right). During cooldown you **cannot reveal additional unseen tiles**, but you can still **click tiles your team already discovered** to view their stats. +After revealing a **new** tile, a **cooldown** runs (left column). During cooldown you **cannot reveal additional unseen tiles**, but you can still **click tiles your team already discovered** to view their stats. ## Run with Docker Compose (Node + PostgreSQL) -From `d:\Users\GaWin\Travaux\Code\star_wars_grid_game`: +### Prerequisites -```powershell -docker compose up --build -``` +1. Copy the environment template and set your secrets: + ```powershell + cp star_wars_grid_game/.env.example star_wars_grid_game/.env + # Edit .env — at minimum change JWT_SECRET, POSTGRES_PASSWORD, POSTGRES_USERS_PASSWORD + ``` -Open `http://localhost:8080`. +2. From `star_wars_grid_game/`: + ```powershell + docker compose up --build + ``` -- **App:** port `8080` -- **Postgres:** port `5432` (user `game`, password `game`, database `star_wars_grid`) -- **Config:** host folder **`./config`** is mounted to **`/app/config`** — edit `game.settings.json` without rebuilding the image. +3. Open `http://localhost:8080`. + +### Ports + +| Service | Port | Notes | +|---------|------|-------| +| App | `8080` | configurable via `PORT` in `.env` | +| Game Postgres | `5432` | user/pass from `.env` | +| Users Postgres | `5433` | user/pass from `.env` | ### Database persistence -PostgreSQL data is under **`./data/postgres`** (bind mount). The **world wipe** only clears `grid_cells` when the UTC period slot changes; it does not delete the Postgres data directory. +PostgreSQL data is under **`./data/postgres`** and **`./data/postgres_users`** (bind mounts). The **world wipe** only clears `grid_cells` when the UTC period slot changes; it does not delete the Postgres data directory. ## Local dev (without Docker) -Requires PostgreSQL and `DATABASE_URL`, then: +Requires PostgreSQL, then: ```powershell +cd star_wars_grid_game +cp .env.example .env # fill in DATABASE_URL etc. npm install -$env:DATABASE_URL="postgres://user:pass@localhost:5432/star_wars_grid" +# export vars or use a tool like dotenv-cli +$env:DATABASE_URL="postgres://game:game@localhost:5432/star_wars_grid" +$env:USERS_DATABASE_URL="postgres://users:users@localhost:5433/star_wars_users" +$env:JWT_SECRET="dev_secret" npm start ``` Ensure `config/game.settings.json` exists (or copy from the repo). +## Docker image notes + +- Base image: `node:20-alpine` +- Runs as a **non-root user** (`appuser`) for security +- `NODE_ENV=production` is set inside the image +- Built-in **health check**: polls `GET /api/config` every 15 s (`wget`) +- Secrets (`JWT_SECRET`, DB passwords) are **never baked into the image** — pass them via `.env` / Compose environment + ## API - `GET /api/config` — cooldown, wipe interval, debug flag, poll interval, `worldSeed`, period timestamps. - `GET /api/grid/:seed` — cells for that seed; **`410`** if `seed` is not the current world seed. -- `POST /api/cell/reveal` — body `{ seed, x, y, team: "blue" \| "red" }` — first reveal wins; **`409`** if the other team owns the tile; **`410`** if seed is stale. +- `POST /api/cell/reveal` — body `{ seed, x, y, team: "blue" | "red" }` — first reveal wins; **`409`** if the other team owns the tile; **`410`** if seed is stale. +- `POST /api/auth/register` — `{ username, email, password, team }` → `{ token, user }`. +- `POST /api/auth/login` — `{ username, password }` → `{ token, user }`. +- `GET /api/auth/me` — Bearer token required → refreshed `{ token, user }`. +- `GET /api/scores` — `{ blue: N, red: N }` tile counts for the current period. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4d47c61..848df35 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,18 @@ -# Keep `db` as the first service: clearer dependency order and predictable compose overrides. +# Secrets come from .env (auto-loaded by Compose) — never hard-code credentials here. +# Copy .env.example to .env and fill in values before running. services: db: image: postgres:16-alpine environment: - POSTGRES_USER: game - POSTGRES_PASSWORD: game - POSTGRES_DB: star_wars_grid + POSTGRES_USER: ${POSTGRES_USER:-game} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB:-star_wars_grid} volumes: - ./data/postgres:/var/lib/postgresql/data ports: - "5432:5432" healthcheck: - test: ["CMD-SHELL", "pg_isready -U game -d star_wars_grid"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-game} -d ${POSTGRES_DB:-star_wars_grid}"] interval: 3s timeout: 5s retries: 15 @@ -20,15 +21,15 @@ services: users_db: image: postgres:16-alpine environment: - POSTGRES_USER: users - POSTGRES_PASSWORD: users - POSTGRES_DB: star_wars_users + POSTGRES_USER: ${POSTGRES_USERS_USER:-users} + POSTGRES_PASSWORD: ${POSTGRES_USERS_PASSWORD} + POSTGRES_DB: ${POSTGRES_USERS_DB:-star_wars_users} volumes: - ./data/postgres_users:/var/lib/postgresql/data ports: - "5433:5432" healthcheck: - test: ["CMD-SHELL", "pg_isready -U users -d star_wars_users"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USERS_USER:-users} -d ${POSTGRES_USERS_DB:-star_wars_users}"] interval: 3s timeout: 5s retries: 15 @@ -37,12 +38,12 @@ services: app: build: . ports: - - "8080:8080" + - "${PORT:-8080}:8080" environment: - DATABASE_URL: postgres://game:game@db:5432/star_wars_grid - USERS_DATABASE_URL: postgres://users:users@users_db:5432/star_wars_users - JWT_SECRET: change_me_in_production_please - PORT: "8080" + DATABASE_URL: "postgres://${POSTGRES_USER:-game}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-star_wars_grid}" + USERS_DATABASE_URL: "postgres://${POSTGRES_USERS_USER:-users}:${POSTGRES_USERS_PASSWORD}@users_db:5432/${POSTGRES_USERS_DB:-star_wars_users}" + JWT_SECRET: ${JWT_SECRET} + PORT: "${PORT:-8080}" CONFIG_FILE_PATH: /app/config/game.settings.json volumes: - ./config:/app/config diff --git a/public/index.html b/public/index.html index 17c9404..73e19b2 100644 --- a/public/index.html +++ b/public/index.html @@ -7,7 +7,20 @@ - + + + +
@@ -73,10 +86,14 @@
+
-
-
-
Star Wars - Wild Space
+ + +
-
-
-
- player - - - - -
-
- Cooldown - - 0 - s - -
-
- clickCooldownSeconds - -
-
- worldSeed - -
-
- next period (UTC) - -
-
- resets in - --:--:-- -
+ +
+
+ Player + + + +
-
- +
+ Cooldown + + 0 + s + +
+
+ clickCooldownSeconds + +
+
+ worldSeed + +
+
+ next period (UTC) + +
+
+ resets in + --:--:--
-
-
-
- -
Click a cell in the ring. Planet stats stay hidden until you reveal a tile.
-
- +
+ + + + +
+ +
Click a cell in the ring. Planet stats stay hidden until you reveal a tile.
+
diff --git a/public/style.css b/public/style.css index 8e4d1d9..3ad06d6 100644 --- a/public/style.css +++ b/public/style.css @@ -1,4 +1,13 @@ -.app { +/* ── Reset & base ─────────────────────────────────────────────────────────── */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji"; color: #e9eef6; @@ -6,8 +15,55 @@ min-height: 100vh; } -body { +/* ── Mobile / small-screen overlay ───────────────────────────────────────── */ + +.mobileOverlay { + display: none; /* hidden on desktop — shown via media query below */ + position: fixed; + inset: 0; + z-index: 200; + background: rgba(5, 8, 18, 0.97); + backdrop-filter: blur(16px); + align-items: center; + justify-content: center; + padding: 24px; + text-align: center; +} + +.mobileOverlayCard { + max-width: 360px; + padding: 40px 28px; + border-radius: 24px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(15, 22, 42, 0.9); + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.7); +} + +.mobileOverlayIcon { + font-size: 52px; + margin-bottom: 16px; +} + +.mobileOverlayTitle { + margin: 0 0 12px; + font-size: 22px; + font-weight: 800; + letter-spacing: 0.2px; + color: rgba(113, 199, 255, 0.95); +} + +.mobileOverlayText { margin: 0; + font-size: 14px; + line-height: 1.7; + color: rgba(233, 238, 246, 0.75); +} + +/* Show overlay on small / narrow screens */ +@media (max-width: 768px), (max-height: 500px) { + .mobileOverlay { + display: flex; + } } /* ── Auth overlay ─────────────────────────────────────────────────────────── */ @@ -272,8 +328,8 @@ body { display: flex; align-items: center; gap: 16px; - padding: 10px 20px; - border-radius: 16px; + padding: 10px 16px; + border-radius: 14px; border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.04); font-size: 22px; @@ -319,79 +375,96 @@ body { font-size: 18px; } -/* ── Top bar ──────────────────────────────────────────────────────────────── */ +/* ── Main app layout ──────────────────────────────────────────────────────── */ -.topbar { +.app { display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - padding: 18px 18px 12px; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(7, 10, 20, 0.65); - backdrop-filter: blur(8px); - position: sticky; - top: 0; - z-index: 2; + flex-direction: row; + align-items: stretch; + height: 100vh; + overflow: hidden; } -.h1 { - font-size: 18px; +/* ── Left information column ──────────────────────────────────────────────── */ + +.infoColumn { + flex: 1 1 0; + min-width: 260px; + max-width: 420px; + display: flex; + flex-direction: column; + gap: 16px; + padding: 22px 18px 18px; + overflow-y: auto; + overflow-x: hidden; + background: rgba(7, 10, 20, 0.55); + border-right: 1px solid rgba(255, 255, 255, 0.07); +} + +/* Scrollbar styling for the info column */ +.infoColumn::-webkit-scrollbar { + width: 5px; +} +.infoColumn::-webkit-scrollbar-track { + background: transparent; +} +.infoColumn::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.12); + border-radius: 4px; +} + +.infoSection--title .h1 { + font-size: 17px; font-weight: 700; letter-spacing: 0.2px; } -.sub { - font-size: 12px; - opacity: 0.75; - margin-top: 3px; +.infoSection--title .sub { + font-size: 11px; + opacity: 0.65; + margin-top: 4px; } -/* ── Top-right table ──────────────────────────────────────────────────────── */ +/* ── Info table (rows of key/value pairs) ─────────────────────────────────── */ -.topRight { +.infoTable { display: flex; flex-direction: column; - align-items: flex-end; - gap: 10px; + gap: 6px; +} + +.infoRow { + display: flex; + align-items: baseline; + gap: 8px; + min-height: 22px; +} + +.infoKey { flex: 0 0 auto; -} - -.topRightTable { - display: table; - border-collapse: separate; - border-spacing: 0 4px; -} - -.topRightRow { - display: table-row; -} - -.trKey { - display: table-cell; + font-size: 11px; + opacity: 0.65; + white-space: nowrap; + min-width: 130px; text-align: right; - padding-right: 8px; - font-size: 11px; - opacity: 0.7; - white-space: nowrap; - vertical-align: middle; } -.trVal { - display: table-cell; - text-align: left; +.infoVal { + flex: 1; font-size: 11px; white-space: nowrap; - vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; } -.trVal code { +.infoVal code { font-size: 11px; - padding: 2px 8px; - border-radius: 8px; + padding: 2px 7px; + border-radius: 7px; background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.12); word-break: break-all; + white-space: normal; } /* Countdown row */ @@ -410,14 +483,16 @@ body { text-transform: uppercase; letter-spacing: 0.06em; color: rgba(255, 193, 113, 0.9); + opacity: 1 !important; } .countdownVal { - display: table-cell; - font-size: 14px; + font-size: 15px; font-weight: 700; color: rgba(255, 193, 113, 1); - vertical-align: middle; + display: flex; + align-items: baseline; + gap: 2px; } .countdown { @@ -429,7 +504,6 @@ body { .countdownUnit { font-size: 12px; opacity: 0.8; - margin-left: 2px; } /* User display */ @@ -459,51 +533,86 @@ body { } .muted { - opacity: 0.7; + opacity: 0.55; } +/* ── Controls ──────────────────────────────────────────────────────────────── */ + .controls { display: flex; - align-items: end; + align-items: center; gap: 10px; } button { - padding: 10px 12px; + padding: 9px 14px; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.14); background: rgba(113, 199, 255, 0.16); color: #e9eef6; + font-size: 13px; cursor: pointer; + transition: background 0.15s; } button:hover { - background: rgba(113, 199, 255, 0.22); + background: rgba(113, 199, 255, 0.24); } -.main { - display: grid; - grid-template-columns: 1fr 360px; - gap: 14px; - padding: 14px 18px 18px; -} +/* ── Selection panel ─────────────────────────────────────────────────────── */ -.board { - position: relative; - border-radius: 16px; - border: 1px solid rgba(255, 255, 255, 0.10); - background: rgba(255, 255, 255, 0.04); +.panel { + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.09); + background: rgba(255, 255, 255, 0.03); + overflow: hidden; + flex: 1 1 auto; + min-height: 80px; +} + +.panelTitle { + padding: 10px 14px; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + opacity: 0.7; + border-bottom: 1px solid rgba(255, 255, 255, 0.07); +} + +.details { + margin: 0; + padding: 12px 14px 14px; + font-size: 12px; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + color: rgba(233, 238, 246, 0.92); +} + +.details--hidden { + color: rgba(233, 238, 246, 0.4); + font-style: italic; +} + +/* ── Galaxy: square, pinned to the right ──────────────────────────────────── */ + +.galaxyMain { + /* Height fills the viewport; aspect-ratio keeps it perfectly square */ + flex: 0 0 auto; + position: relative; + height: 100vh; + aspect-ratio: 1 / 1; + /* Constrain width so the canvas never exceeds available space */ + max-width: calc(100vw - 260px); + background: #000; overflow: hidden; - min-height: 820px; } canvas { display: block; - width: min(820px, calc(100vw - 420px)); - height: min(820px, calc(100vw - 420px)); - max-width: 820px; - max-height: 820px; - margin: 0 auto; + width: 100%; + height: 100%; image-rendering: pixelated; } @@ -515,46 +624,9 @@ canvas { font-size: 12px; opacity: 0.8; border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.10); + border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(7, 10, 20, 0.65); backdrop-filter: blur(6px); -} - -.panel { - border-radius: 16px; - border: 1px solid rgba(255, 255, 255, 0.10); - background: rgba(255, 255, 255, 0.04); - overflow: hidden; - height: fit-content; -} - -.panelTitle { - padding: 12px 14px; - font-weight: 700; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); -} - -.details { - margin: 0; - padding: 12px 14px 14px; - font-size: 12px; - line-height: 1.4; - white-space: pre-wrap; - word-break: break-word; - color: rgba(233, 238, 246, 0.92); -} - -.details--hidden { - color: rgba(233, 238, 246, 0.45); - font-style: italic; -} - -@media (max-width: 1100px) { - .main { - grid-template-columns: 1fr; - } - canvas { - width: min(820px, calc(100vw - 36px)); - height: min(820px, calc(100vw - 36px)); - } + max-width: calc(100% - 28px); + pointer-events: none; } \ No newline at end of file