chore: update readme
This commit is contained in:
parent
93e8258270
commit
d5109b8dc9
6 changed files with 257 additions and 82 deletions
|
|
@ -91,7 +91,8 @@ pnpm run db:stop # stop a stray emulator holding :8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Deploy scripts (`deploy:db`, `deploy:site`) also exist but require project
|
Deploy scripts (`deploy:db`, `deploy:site`) also exist but require project
|
||||||
credentials, so they're for maintainers. See [README.md](README.md) and
|
credentials, so they're for maintainers — the first-time setup is documented in
|
||||||
|
[`docs/deployment.md`](docs/deployment.md). See [README.md](README.md) and
|
||||||
[`docs/`](docs/) for the full backend reference.
|
[`docs/`](docs/) for the full backend reference.
|
||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
|
||||||
61
README.md
61
README.md
|
|
@ -14,7 +14,6 @@ The goal is to reveal every safe cell without detonating a mine. Your **first cl
|
||||||
- **Left + right click together** (chording) — reveal the neighbors of a satisfied number
|
- **Left + right click together** (chording) — reveal the neighbors of a satisfied number
|
||||||
- **Touch** — tap to reveal, long-press to flag
|
- **Touch** — tap to reveal, long-press to flag
|
||||||
|
|
||||||
|
|
||||||
## Ways to Use
|
## Ways to Use
|
||||||
|
|
||||||
The web is a wonderful, free, and open platform to create and distribute value. You can use **mnswpr** in different ways:
|
The web is a wonderful, free, and open platform to create and distribute value. You can use **mnswpr** in different ways:
|
||||||
|
|
@ -44,7 +43,7 @@ As of now the tooling I use are:
|
||||||
- [PNPM](https://pnpm.io/installation) for dependency management
|
- [PNPM](https://pnpm.io/installation) for dependency management
|
||||||
- and a bunch of automation using scripts and Continuous Integration actions
|
- and a bunch of automation using scripts and Continuous Integration actions
|
||||||
|
|
||||||
Because a big part of this project's purpose is to track how the software development industry evolves — and because it has come a long way in modernizing along the way — I now also use it as a **playground for coding agents**. It's a small, framework-free, well-scoped codebase, which makes it a great sandbox to see how AI agents read, reason about, and change real code. To help them get their bearings quickly, the repo ships an [`AGENTS.md`](./AGENTS.md) describing the architecture and conventions.
|
Because a big part of this project's purpose is to track how the software development industry evolves, I now also use it as a **playground for coding agents**.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|
@ -67,64 +66,10 @@ pnpm run preview # serve the production build
|
||||||
The game engine and leaderboard are consumed from npm — they live in
|
The game engine and leaderboard are consumed from npm — they live in
|
||||||
[ayo-run/cozy-games](https://github.com/ayo-run/cozy-games), not here.
|
[ayo-run/cozy-games](https://github.com/ayo-run/cozy-games), not here.
|
||||||
|
|
||||||
### Leaderboard (local Firestore emulator)
|
|
||||||
|
|
||||||
The leader board is backed by [Google Firestore](https://firebase.google.com). For local development the app talks to the **Firestore emulator** by default — fully local, no cloud, no deploy. The flag `VITE_FIRESTORE_EMULATOR=1` is already set in `.env.development`.
|
|
||||||
|
|
||||||
`dev` is the whole loop in one command — it wraps Vite in `firebase emulators:exec`, so the Firestore emulator (on :8080, + UI) comes up, gets **seeded with sample scores automatically**, and the app dev server starts against it; everything shuts down when you stop it. (Each `dev` starts a fresh in-memory emulator, so the auto-seed writes exactly one clean set every time — no accumulation.) `firebase-tools` is a pinned **devDependency** of this app (installed by `pnpm install`, invoked as the `firebase` binary), so the only extra prerequisite is **Java** — the Firestore emulator is a Java program (`java -jar cloud-firestore-emulator-*.jar`), and firebase-tools does not bundle a JRE.
|
|
||||||
|
|
||||||
**Usually automatic.** `pnpm install` runs a root `postinstall` ([`scripts/ensure-java.mjs`](../../scripts/ensure-java.mjs)) that installs a user-local Temurin JRE 21 into `~/.local` (no `sudo`) when `java` isn't already on your PATH. It's idempotent and never fails the install, and it skips when `CI` or `SKIP_JRE_SETUP=1` is set, or on unsupported platforms.
|
|
||||||
|
|
||||||
If that skipped and you need Java (or prefer a system-wide install), do it manually — **install a JRE (Java 11+; 21 recommended):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Debian / Ubuntu / Pop!_OS
|
|
||||||
sudo apt update && sudo apt install -y openjdk-21-jre-headless
|
|
||||||
|
|
||||||
# Fedora
|
|
||||||
sudo dnf install -y java-21-openjdk-headless
|
|
||||||
|
|
||||||
# macOS (Homebrew)
|
|
||||||
brew install openjdk@21
|
|
||||||
|
|
||||||
java -version # verify: should print "openjdk 21.x" (or 11+)
|
|
||||||
```
|
|
||||||
|
|
||||||
No `sudo`? Install a JRE into your home directory instead (no root needed):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL -o /tmp/jre21.tgz "https://api.adoptium.net/v3/binary/latest/21/ga/linux/x64/jre/hotspot/normal/eclipse"
|
|
||||||
mkdir -p ~/.local/lib && tar xzf /tmp/jre21.tgz -C ~/.local/lib
|
|
||||||
ln -sf ~/.local/lib/jdk-21*-jre/bin/java ~/.local/bin/java # ~/.local/bin is already on PATH
|
|
||||||
java -version
|
|
||||||
```
|
|
||||||
|
|
||||||
Without Java, `dev` and `db:start` fail with `Could not spawn 'java -version'`. Install it to a permanent location — a JRE unpacked under `/tmp` disappears when the OS cleans temp files. Then just:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm run dev # emulator (:8080 + UI) + auto-seed + app dev server — one command
|
|
||||||
```
|
|
||||||
|
|
||||||
That's the everyday loop. The other DB scripts are for when you want to run pieces separately:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm run db:start # emulator only (stays up across app restarts); pair with dev:no-db
|
|
||||||
pnpm run db:seed # seed a separately-running emulator (what dev does for you)
|
|
||||||
pnpm run db:stop # kill a stray/orphaned emulator holding :8080
|
|
||||||
```
|
|
||||||
|
|
||||||
To skip the emulator entirely — for quick UI-only work, or if you don't have a JDK — run `pnpm run dev:no-db` (plain Vite) and set `VITE_FIRESTORE_EMULATOR=` (empty) in a local, gitignored `.env.local`; the app then uses the cloud `mw-test` namespace instead.
|
|
||||||
|
|
||||||
See [`docs/firebase-leaderboards.md`](./docs/firebase-leaderboards.md) for the full data model, security rules, environments, and deployment.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome! See [`AGENTS.md`](./AGENTS.md) for the architecture, conventions, and release workflow before opening a pull request.
|
Contributions are welcome! See [`AGENTS.md`](./AGENTS.md) for the architecture, conventions, and release workflow before opening a pull request.
|
||||||
|
|
||||||
## You just want to play?
|
|
||||||
|
|
||||||
*👉 The live site is here: [mnswpr.com](https://mnswpr.com)*
|
|
||||||
|
|
||||||
## Background
|
## Background
|
||||||
One day, while working in my home office, I heard loud and fast mouse clicks coming from our bedroom. It's my wife, playing her favorite game (Minesweeper) on a crappy website full of advertisements.
|
One day, while working in my home office, I heard loud and fast mouse clicks coming from our bedroom. It's my wife, playing her favorite game (Minesweeper) on a crappy website full of advertisements.
|
||||||
|
|
||||||
|
|
@ -144,11 +89,9 @@ Can I make a page with complex interactions (more on this later) without any lib
|
||||||
1. Competition motivates users to use your app more ✨
|
1. Competition motivates users to use your app more ✨
|
||||||
1. Hash in bundled filenames helps avoid issues with browser caching (when shipping versions fast) ✨
|
1. Hash in bundled filenames helps avoid issues with browser caching (when shipping versions fast) ✨
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT](../../LICENSE)
|
[MIT](./LICENSE)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
175
docs/deployment.md
Normal file
175
docs/deployment.md
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
# First-time deployment
|
||||||
|
|
||||||
|
Everything needed to take this repo from a fresh clone to a live deploy of
|
||||||
|
[mnswpr.com](https://mnswpr.com). Once it's set up you won't need this again —
|
||||||
|
day to day, `pnpm release` is the whole story.
|
||||||
|
|
||||||
|
Two services are involved: **Netlify** hosts the site, **Firebase** (Firestore)
|
||||||
|
stores the leaderboard. Both are driven entirely from the CLI; nothing here
|
||||||
|
requires a web dashboard except one Netlify setting noted below.
|
||||||
|
|
||||||
|
For the conceptual split between infra *config* (committed, declarative) and
|
||||||
|
infra *tools* (pinned devDependencies), see AGENTS.md → "Infra".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Prerequisites
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm i # installs firebase-tools and netlify-cli as pinned devDependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
Both CLIs are invoked through `pnpm exec`, so you never need a global install.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Netlify
|
||||||
|
|
||||||
|
### Log in and link the repo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec netlify login
|
||||||
|
pnpm exec netlify link # binds this directory to the site; writes .netlify/ (gitignored)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you know the site ID you can skip the prompts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec netlify link --id <site-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
Losing `.netlify/` is harmless — it's local state. Re-run `netlify link`.
|
||||||
|
|
||||||
|
### Set the base directory
|
||||||
|
|
||||||
|
The site's **base directory must be the repo root**. This is the one setting
|
||||||
|
that isn't in a config file — it lives on the site itself (Site configuration →
|
||||||
|
Build & deploy → Base directory) and must be **empty**.
|
||||||
|
|
||||||
|
If it points at a subdirectory that doesn't exist, git-triggered builds fail
|
||||||
|
before they start.
|
||||||
|
|
||||||
|
### Set the environment variables
|
||||||
|
|
||||||
|
This is the step most likely to be missed, because it only affects one of the
|
||||||
|
two deploy paths:
|
||||||
|
|
||||||
|
- **`pnpm run deploy:site`** builds *locally* and uploads `dist/`. It reads your
|
||||||
|
local, gitignored `.env.production` — Netlify's env vars are never consulted.
|
||||||
|
- **`pnpm release`** force-pushes the `release` branch, and **Netlify builds it
|
||||||
|
from git**. That build runs on Netlify's machines with no access to any local
|
||||||
|
file, so it needs the variables set on the site.
|
||||||
|
|
||||||
|
A site that deploys fine via `deploy:site` can still ship a broken build from
|
||||||
|
git if these are missing — the Firebase config comes out `undefined` and the
|
||||||
|
leaderboard fails for every visitor.
|
||||||
|
|
||||||
|
The eight values are the same ones committed in `.env.development`: `prod` and
|
||||||
|
`dev` are the *same* Firebase project (see `firebase/.firebaserc`), separated
|
||||||
|
only by collection namespace. So you can set them straight from that file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -E '^VITE_FIREBASE_' .env.development | while IFS='=' read -r k v; do
|
||||||
|
pnpm exec netlify env:set "$k" "$v"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
That covers:
|
||||||
|
|
||||||
|
```
|
||||||
|
VITE_FIREBASE_API_KEY VITE_FIREBASE_STORAGE_BUCKET
|
||||||
|
VITE_FIREBASE_AUTH_DOMAIN VITE_FIREBASE_MESSAGING_SENDER_ID
|
||||||
|
VITE_FIREBASE_DATABASE_URL VITE_FIREBASE_APP_ID
|
||||||
|
VITE_FIREBASE_PROJECT_ID VITE_FIREBASE_MEASUREMENT_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
Two variables are deliberately **not** in that list:
|
||||||
|
|
||||||
|
- **`VITE_LB_NAMESPACE`** is already set to `mw` in `netlify.toml`, so it ships
|
||||||
|
with the repo. (`src/main.js` defaults to `mw-test` when it's missing, so a
|
||||||
|
misconfigured build writes to the test board, never production.)
|
||||||
|
- **`VITE_FIRESTORE_EMULATOR`** must stay **absent or empty** in production.
|
||||||
|
`src/main.js` treats `1`/`true`/`yes` as "connect to a local emulator", so if
|
||||||
|
it leaks into the site's environment the live leaderboard silently tries to
|
||||||
|
reach `127.0.0.1:8080` and fails for everyone. It belongs only in
|
||||||
|
`.env.development`.
|
||||||
|
|
||||||
|
Verify what's actually set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec netlify env:list
|
||||||
|
```
|
||||||
|
|
||||||
|
> **These are not secrets.** Firebase Web API keys are public by design — they
|
||||||
|
> identify the project, they don't authorize anything. Access is governed by
|
||||||
|
> `firebase/firestore.rules`. They're kept out of git as prod hygiene, not
|
||||||
|
> because exposure is dangerous; the dev values are committed for this reason.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Firebase
|
||||||
|
|
||||||
|
### Log in
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec firebase login
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy the security rules
|
||||||
|
|
||||||
|
**Do this before the first real release.** `firebase/firestore.rules` is the
|
||||||
|
source of truth for who can read and write the leaderboard, but it only takes
|
||||||
|
effect once deployed — a project can be running rules set by hand in the console
|
||||||
|
long ago, which won't match the repo.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run deploy:db # firebase --config firebase/firebase.json deploy --only firestore
|
||||||
|
```
|
||||||
|
|
||||||
|
That deploys rules *and* indexes. Note that `prod` and `dev` are the same
|
||||||
|
Firebase project, so a rules deploy affects both environments at once.
|
||||||
|
|
||||||
|
If the rules are stale, leaderboard writes fail with permission-denied even
|
||||||
|
though the site loads fine — worth confirming after the first deploy by
|
||||||
|
finishing a game and checking the score appears.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Deploy
|
||||||
|
|
||||||
|
Two paths, both supported:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run deploy:site # build locally, upload dist/ via the Netlify CLI
|
||||||
|
```
|
||||||
|
|
||||||
|
or the full release, which is what tags a version and triggers the git build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm release # lint + test, then bumpp, then sync the release branch
|
||||||
|
```
|
||||||
|
|
||||||
|
`pnpm release` runs `scripts/release.js`, which force-pushes `main` to the
|
||||||
|
`release` branch on the `gh` remote. **That push is the deploy** — Netlify
|
||||||
|
watches that branch. It refuses to run from a branch other than `main` or with a
|
||||||
|
dirty working tree.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verifying a first deploy
|
||||||
|
|
||||||
|
1. `pnpm exec netlify env:list` shows all eight `VITE_FIREBASE_*` and no
|
||||||
|
`VITE_FIRESTORE_EMULATOR`.
|
||||||
|
2. The site loads and the version in the heading matches `package.json`.
|
||||||
|
3. Finish a game — your score appears on the board. This is the real end-to-end
|
||||||
|
check: it exercises the env vars, the rules, and the namespace together.
|
||||||
|
4. The leaderboard shows the `mw` namespace, not `mw-test`.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [`firebase-leaderboards.md`](./firebase-leaderboards.md) — data model, rules,
|
||||||
|
environments, deployment details
|
||||||
|
- [`firestore-emulator.md`](./firestore-emulator.md) — running the leaderboard
|
||||||
|
locally
|
||||||
|
- [`leaderboard-env-migration.md`](./leaderboard-env-migration.md) — history of
|
||||||
|
the env-var/namespace migration
|
||||||
|
|
@ -6,9 +6,9 @@ still has to be touched in the Firebase Console.
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Leaderboards are powered by Firestore (`firebase/firestore/lite`) through the
|
Leaderboards are powered by Firestore (`firebase/firestore/lite`) through the
|
||||||
reusable, game-agnostic [`@cozy-games/leaderboard`](../leaderboard/leader-board.js)
|
reusable, game-agnostic [`@cozy-games/leaderboard`](https://github.com/ayo-run/cozy-games)
|
||||||
package. The app wires minesweeper's specifics (finish-time as the `score`,
|
package. The app wires minesweeper's specifics (finish-time as the `score`,
|
||||||
ascending sort, time formatting) in [`app/main.js`](../app/main.js).
|
ascending sort, time formatting) in [`app/main.js`](../src/main.js).
|
||||||
|
|
||||||
The board offers four time windows — **Today** (default), **Week**, **Month**,
|
The board offers four time windows — **Today** (default), **Week**, **Month**,
|
||||||
**All Time** — selected by tabs. Each played game that qualifies is written to a
|
**All Time** — selected by tabs. Each played game that qualifies is written to a
|
||||||
|
|
@ -64,11 +64,11 @@ competition, not a boundary quirk.
|
||||||
|
|
||||||
The Firestore schema (security rules + indexes) lives in the repo:
|
The Firestore schema (security rules + indexes) lives in the repo:
|
||||||
|
|
||||||
- [`firebase.json`](../firebase.json) — points at the rules and indexes files.
|
- [`firebase.json`](../firebase/firebase.json) — points at the rules and indexes files.
|
||||||
- [`.firebaserc`](../.firebaserc) — project aliases: `prod` (default, live site)
|
- [`.firebaserc`](../firebase/.firebaserc) — project aliases: `prod` (default, live site)
|
||||||
and `dev` (`secure-moment-188701`).
|
and `dev` (`secure-moment-188701`).
|
||||||
- [`firestore.rules`](../firestore.rules) — access + validation rules.
|
- [`firestore.rules`](../firebase/firestore.rules) — access + validation rules.
|
||||||
- [`firestore.indexes.json`](../firestore.indexes.json) — **empty**: rolling
|
- [`firestore.indexes.json`](../firebase/firestore.indexes.json) — **empty**: rolling
|
||||||
windows (`time_stamp >=`) and all-time (`orderBy('score')`) use Firestore's
|
windows (`time_stamp >=`) and all-time (`orderBy('score')`) use Firestore's
|
||||||
automatic single-field indexes, so no composite indexes are needed.
|
automatic single-field indexes, so no composite indexes are needed.
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ The Firestore schema (security rules + indexes) lives in the repo:
|
||||||
|
|
||||||
There is **one Firebase project** (`secure-moment-188701`). Production and test
|
There is **one Firebase project** (`secure-moment-188701`). Production and test
|
||||||
data are separated not by project but by **collection namespace**, chosen with
|
data are separated not by project but by **collection namespace**, chosen with
|
||||||
the `VITE_LB_NAMESPACE` env var read in [`app/main.js`](../app/main.js):
|
the `VITE_LB_NAMESPACE` env var read in [`app/main.js`](../src/main.js):
|
||||||
|
|
||||||
| Environment | `VITE_LB_NAMESPACE` | Collections |
|
| Environment | `VITE_LB_NAMESPACE` | Collections |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|
@ -88,7 +88,7 @@ project — so the only difference is the namespace. `app/main.js` defaults to t
|
||||||
**test** namespace, so a missing/misconfigured var can never write into the
|
**test** namespace, so a missing/misconfigured var can never write into the
|
||||||
production board; production must set `VITE_LB_NAMESPACE=mw` explicitly.
|
production board; production must set `VITE_LB_NAMESPACE=mw` explicitly.
|
||||||
|
|
||||||
Dev config lives in the committed [`app/.env.development`](../app/.env.development)
|
Dev config lives in the committed [`app/.env.development`](../.env.development)
|
||||||
(the keys are public). Production sets `VITE_FIREBASE_*` **and**
|
(the keys are public). Production sets `VITE_FIREBASE_*` **and**
|
||||||
`VITE_LB_NAMESPACE=mw` as Netlify environment variables. `.env.production` and
|
`VITE_LB_NAMESPACE=mw` as Netlify environment variables. `.env.production` and
|
||||||
`.env*.local` stay gitignored.
|
`.env*.local` stay gitignored.
|
||||||
|
|
@ -121,7 +121,7 @@ pnpm exec firebase deploy --only firestore:rules,firestore:indexes --project dev
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ Deploying **replaces** whatever rules currently live in the Console. The
|
> ⚠️ Deploying **replaces** whatever rules currently live in the Console. The
|
||||||
> committed [`firestore.rules`](../firestore.rules) is written to cover every
|
> committed [`firestore.rules`](../firebase/firestore.rules) is written to cover every
|
||||||
> collection the app uses, so a deploy is safe — but review it first.
|
> collection the app uses, so a deploy is safe — but review it first.
|
||||||
|
|
||||||
No composite indexes are required — the rolling-window and all-time queries use
|
No composite indexes are required — the rolling-window and all-time queries use
|
||||||
|
|
@ -131,9 +131,9 @@ Firestore's automatic single-field indexes.
|
||||||
|
|
||||||
Local development runs against the **Firebase Local Emulator Suite** by default —
|
Local development runs against the **Firebase Local Emulator Suite** by default —
|
||||||
no cloud, no deploy, no auth, no `permission-denied` — and it loads the committed
|
no cloud, no deploy, no auth, no `permission-denied` — and it loads the committed
|
||||||
[`firestore.rules`](../firestore.rules) and [`firestore.indexes.json`](../firestore.indexes.json)
|
[`firestore.rules`](../firebase/firestore.rules) and [`firestore.indexes.json`](../firebase/firestore.indexes.json)
|
||||||
locally (so you validate them before deploying). The flag
|
locally (so you validate them before deploying). The flag
|
||||||
`VITE_FIRESTORE_EMULATOR=1` is set in [`app/.env.development`](../app/.env.development).
|
`VITE_FIRESTORE_EMULATOR=1` is set in [`app/.env.development`](../.env.development).
|
||||||
|
|
||||||
> Prerequisite: the Firestore emulator is a Java process, so you need a JDK
|
> Prerequisite: the Firestore emulator is a Java process, so you need a JDK
|
||||||
> (11+) installed. `firebase-tools` is a pinned devDependency of this app
|
> (11+) installed. `firebase-tools` is a pinned devDependency of this app
|
||||||
|
|
@ -166,7 +166,7 @@ Everything above is doable via the CLI. If you can't use it:
|
||||||
(If you ever add a query that needs a composite index, Firestore prints a
|
(If you ever add a query that needs a composite index, Firestore prints a
|
||||||
console error with a direct link to create it.)
|
console error with a direct link to create it.)
|
||||||
- **Rules**: edit them directly under **Firestore → Rules** in the Console
|
- **Rules**: edit them directly under **Firestore → Rules** in the Console
|
||||||
(paste from [`firestore.rules`](../firestore.rules)).
|
(paste from [`firestore.rules`](../firebase/firestore.rules)).
|
||||||
- **Server config**: `mw-config/configuration` (`passingStatus`, `message`) is
|
- **Server config**: `mw-config/configuration` (`passingStatus`, `message`) is
|
||||||
**always** managed by hand in the Console — it is read-only to clients and has
|
**always** managed by hand in the Console — it is read-only to clients and has
|
||||||
no code representation. `passingStatus` is the `status` value that makes a game
|
no code representation. `passingStatus` is the `status` value that makes a game
|
||||||
|
|
@ -175,7 +175,7 @@ Everything above is doable via the CLI. If you can't use it:
|
||||||
## Legends (frozen hall of fame)
|
## Legends (frozen hall of fame)
|
||||||
|
|
||||||
The old all-time leaders are preserved as a **fully-rendered static page** —
|
The old all-time leaders are preserved as a **fully-rendered static page** —
|
||||||
[`legends.html`](../legends.html). The records are baked straight into the
|
[`legends.html`](../src/legends.html). The records are baked straight into the
|
||||||
HTML with times pre-formatted; there is **no JavaScript and no Firebase** at page
|
HTML with times pre-formatted; there is **no JavaScript and no Firebase** at page
|
||||||
load. The data never changes.
|
load. The data never changes.
|
||||||
|
|
||||||
|
|
@ -209,7 +209,7 @@ new LeaderBoardService({
|
||||||
|
|
||||||
To run on **Supabase** instead, swap in `SupabaseAdapter` — nothing else in the
|
To run on **Supabase** instead, swap in `SupabaseAdapter` — nothing else in the
|
||||||
game changes. The adapter interface and the Supabase table/SQL schema are
|
game changes. The adapter interface and the Supabase table/SQL schema are
|
||||||
documented in [`leaderboard/README.md`](../leaderboard/README.md).
|
documented in the [@cozy-games/leaderboard README](https://github.com/ayo-run/cozy-games).
|
||||||
|
|
||||||
Then submit `{ name, playerId, score, category, time_stamp, status?, meta? }`
|
Then submit `{ name, playerId, score, category, time_stamp, status?, meta? }`
|
||||||
and render with `render(category, title, duration)`.
|
and render with `render(category, title, duration)`.
|
||||||
|
|
|
||||||
56
docs/firestore-emulator.md
Normal file
56
docs/firestore-emulator.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Leaderboard: the local Firestore emulator
|
||||||
|
|
||||||
|
How to run the leaderboard locally. For the data model, security rules,
|
||||||
|
environments, and deployment, see [`firebase-leaderboards.md`](./firebase-leaderboards.md).
|
||||||
|
|
||||||
|
The leader board is backed by [Google Firestore](https://firebase.google.com). For local development the app talks to the **Firestore emulator** by default — fully local, no cloud, no deploy. The flag `VITE_FIRESTORE_EMULATOR=1` is already set in `.env.development`.
|
||||||
|
|
||||||
|
`dev` is the whole loop in one command — it wraps Vite in `firebase emulators:exec`, so the Firestore emulator (on :8080, + UI) comes up, gets **seeded with sample scores automatically**, and the app dev server starts against it; everything shuts down when you stop it. (Each `dev` starts a fresh in-memory emulator, so the auto-seed writes exactly one clean set every time — no accumulation.) `firebase-tools` is a pinned **devDependency** of this app (installed by `pnpm install`, invoked as the `firebase` binary), so the only extra prerequisite is **Java** — the Firestore emulator is a Java program (`java -jar cloud-firestore-emulator-*.jar`), and firebase-tools does not bundle a JRE.
|
||||||
|
|
||||||
|
## Java
|
||||||
|
|
||||||
|
**Usually automatic.** `pnpm install` runs a root `postinstall` ([`scripts/ensure-java.mjs`](../scripts/ensure-java.mjs)) that installs a user-local Temurin JRE 21 into `~/.local` (no `sudo`) when `java` isn't already on your PATH. It's idempotent and never fails the install, and it skips when `CI` or `SKIP_JRE_SETUP=1` is set, or on unsupported platforms.
|
||||||
|
|
||||||
|
If that skipped and you need Java (or prefer a system-wide install), do it manually — **install a JRE (Java 11+; 21 recommended):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian / Ubuntu / Pop!_OS
|
||||||
|
sudo apt update && sudo apt install -y openjdk-21-jre-headless
|
||||||
|
|
||||||
|
# Fedora
|
||||||
|
sudo dnf install -y java-21-openjdk-headless
|
||||||
|
|
||||||
|
# macOS (Homebrew)
|
||||||
|
brew install openjdk@21
|
||||||
|
|
||||||
|
java -version # verify: should print "openjdk 21.x" (or 11+)
|
||||||
|
```
|
||||||
|
|
||||||
|
No `sudo`? Install a JRE into your home directory instead (no root needed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL -o /tmp/jre21.tgz "https://api.adoptium.net/v3/binary/latest/21/ga/linux/x64/jre/hotspot/normal/eclipse"
|
||||||
|
mkdir -p ~/.local/lib && tar xzf /tmp/jre21.tgz -C ~/.local/lib
|
||||||
|
ln -sf ~/.local/lib/jdk-21*-jre/bin/java ~/.local/bin/java # ~/.local/bin is already on PATH
|
||||||
|
java -version
|
||||||
|
```
|
||||||
|
|
||||||
|
Without Java, `dev` and `db:start` fail with `Could not spawn 'java -version'`. Install it to a permanent location — a JRE unpacked under `/tmp` disappears when the OS cleans temp files.
|
||||||
|
|
||||||
|
## Running it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev # emulator (:8080 + UI) + auto-seed + app dev server — one command
|
||||||
|
```
|
||||||
|
|
||||||
|
That's the everyday loop. The other DB scripts are for when you want to run pieces separately:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run db:start # emulator only (stays up across app restarts); pair with dev:no-db
|
||||||
|
pnpm run db:seed # seed a separately-running emulator (what dev does for you)
|
||||||
|
pnpm run db:stop # kill a stray/orphaned emulator holding :8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Skipping the emulator
|
||||||
|
|
||||||
|
To skip it entirely — for quick UI-only work, or if you don't have a JDK — run `pnpm run dev:no-db` (plain Vite) and set `VITE_FIRESTORE_EMULATOR=` (empty) in a local, gitignored `.env.local`; the app then uses the cloud `mw-test` namespace instead.
|
||||||
|
|
@ -7,7 +7,7 @@ Every environment — a developer running `pnpm dev`, a Netlify preview, and the
|
||||||
live site — reads and writes the same collections (`mw-scores`, `mw-all`,
|
live site — reads and writes the same collections (`mw-scores`, `mw-all`,
|
||||||
`mw-config`, legacy `mw-leaders`) in the single Firebase project
|
`mw-config`, legacy `mw-leaders`) in the single Firebase project
|
||||||
`secure-moment-188701`. `version`/`import.meta.env.MODE` only affects UI text
|
`secure-moment-188701`. `version`/`import.meta.env.MODE` only affects UI text
|
||||||
([lib/mnswpr.js:96](../lib/mnswpr.js:96)); it never changes collection names. So
|
(in the [@cozy-games/mnswpr engine](https://github.com/ayo-run/cozy-games)); it never changes collection names. So
|
||||||
local play pollutes the production leaderboard.
|
local play pollutes the production leaderboard.
|
||||||
|
|
||||||
We keep **one Firebase project** (there is no separate prod project — see
|
We keep **one Firebase project** (there is no separate prod project — see
|
||||||
|
|
@ -27,7 +27,7 @@ Existing production data under `mw-*` is untouched; `mw-test-*` starts empty.
|
||||||
|
|
||||||
| Piece | Now | After |
|
| Piece | Now | After |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Namespace | hardcoded `'mw'` in [app/main.js](../app/main.js) | `import.meta.env.VITE_LB_NAMESPACE` |
|
| Namespace | hardcoded `'mw'` in [app/main.js](../src/main.js) | `import.meta.env.VITE_LB_NAMESPACE` |
|
||||||
| Dev writes | into prod `mw-*` | into `mw-test-*` |
|
| Dev writes | into prod `mw-*` | into `mw-test-*` |
|
||||||
| Rules | only `mw-*` matches | any `mw` / `mw-test` namespace |
|
| Rules | only `mw-*` matches | any `mw` / `mw-test` namespace |
|
||||||
| Indexes | `games` collection group | unchanged (already covers all namespaces) |
|
| Indexes | `games` collection group | unchanged (already covers all namespaces) |
|
||||||
|
|
@ -37,14 +37,14 @@ Existing production data under `mw-*` is untouched; `mw-test-*` starts empty.
|
||||||
|
|
||||||
### 1. Add the namespace env var
|
### 1. Add the namespace env var
|
||||||
|
|
||||||
- [app/.env.development](../app/.env.development): add `VITE_LB_NAMESPACE=mw-test`
|
- [app/.env.development](../.env.development): add `VITE_LB_NAMESPACE=mw-test`
|
||||||
- [app/.env.example](../app/.env.example): document `VITE_LB_NAMESPACE`
|
- [app/.env.example](../.env.example): document `VITE_LB_NAMESPACE`
|
||||||
- Netlify (production build): set `VITE_LB_NAMESPACE=mw` **and** the same
|
- Netlify (production build): set `VITE_LB_NAMESPACE=mw` **and** the same
|
||||||
`VITE_FIREBASE_*` values as dev (same project).
|
`VITE_FIREBASE_*` values as dev (same project).
|
||||||
|
|
||||||
### 2. Read it in the app
|
### 2. Read it in the app
|
||||||
|
|
||||||
In [app/main.js](../app/main.js), replace the hardcoded namespace. Default to the
|
In [app/main.js](../src/main.js), replace the hardcoded namespace. Default to the
|
||||||
**test** namespace so a missing/misconfigured var can never write to production:
|
**test** namespace so a missing/misconfigured var can never write to production:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|
@ -59,7 +59,7 @@ of a missing var is an empty test board, never prod pollution.)
|
||||||
|
|
||||||
### 3. Generalize the security rules
|
### 3. Generalize the security rules
|
||||||
|
|
||||||
Rewrite [firestore.rules](../firestore.rules) so a rule matches by the namespace
|
Rewrite [firestore.rules](../firebase/firestore.rules) so a rule matches by the namespace
|
||||||
*suffix* instead of a literal `mw-` prefix. Firestore ORs all matching rules, so
|
*suffix* instead of a literal `mw-` prefix. Firestore ORs all matching rules, so
|
||||||
one set of generic blocks covers `mw`, `mw-test`, and any future namespace.
|
one set of generic blocks covers `mw`, `mw-test`, and any future namespace.
|
||||||
Note: `{ns}-scores/{cat}/games/{id}`, `{ns}-all/{id}/games/{s}` and
|
Note: `{ns}-scores/{cat}/games/{id}`, `{ns}-all/{id}/games/{s}` and
|
||||||
|
|
@ -117,7 +117,7 @@ The regex `mw(-[a-z]+)?-scores` matches `mw-scores` and `mw-test-scores`
|
||||||
|
|
||||||
Time windows are rolling (`time_stamp >=`) and all-time sorts by `score`, so the
|
Time windows are rolling (`time_stamp >=`) and all-time sorts by `score`, so the
|
||||||
queries use Firestore's automatic single-field indexes.
|
queries use Firestore's automatic single-field indexes.
|
||||||
[firestore.indexes.json](../firestore.indexes.json) is empty — nothing to add for
|
[firestore.indexes.json](../firebase/firestore.indexes.json) is empty — nothing to add for
|
||||||
any namespace.
|
any namespace.
|
||||||
|
|
||||||
### 5. Seed the test config doc
|
### 5. Seed the test config doc
|
||||||
|
|
@ -155,6 +155,6 @@ Both aliases target the one project we actually have:
|
||||||
|
|
||||||
## Rollback
|
## Rollback
|
||||||
|
|
||||||
Set `VITE_LB_NAMESPACE=mw` everywhere (or revert [app/main.js](../app/main.js) to
|
Set `VITE_LB_NAMESPACE=mw` everywhere (or revert [app/main.js](../src/main.js) to
|
||||||
the hardcoded `'mw'`) to return to shared collections. The generalized rules are
|
the hardcoded `'mw'`) to return to shared collections. The generalized rules are
|
||||||
a superset of the old ones, so they stay valid either way.
|
a superset of the old ones, so they stay valid either way.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue