mnswpr/scripts/seed-dev-scores.js
Ayo f019afd7c8 feat: reusable, time-windowed leaderboard (@cozy-games/leaderboard)
Extract the leaderboard into a generic, backend-agnostic package and add
rolling Today/Week/Month/All-Time windows, a web component, and local-first
development against the Firestore emulator.

Package (leaderboard/):
- LeaderBoardService: backend-agnostic core via a storage-adapter seam, with
  Firebase and Supabase adapters (Supabase client injected, no added dep)
- Rolling time windows (last 24h / 7d / 30d) with hover tooltips; top-N by score
- <cozy-leaderboard> web component built on web-component-base: compose the UI
  in HTML, configure the backend once in JS
- Every user-facing string is configurable (labels, tooltips, empty/loading/
  error messages, anonymous name) so i18n lives in the app; README + CONFIGURATION

App (mnswpr.com):
- Compose the board declaratively in index.html via <cozy-leaderboard>
- Nickname + randomized greeting bar; score submission through the element
- Legends: the current all-time leaders frozen into a static /legends page
- Firebase config and leaderboard namespace via Vite env vars; emulator-first
  local dev (VITE_FIRESTORE_EMULATOR)

Firebase schema-as-code:
- firebase.json, .firebaserc, firestore.rules (public reads, create-only scores,
  no client updates/deletes, namespace-generalized), empty indexes (rolling
  windows need none)
- prod (mw-*) vs dev/test (mw-test-*) separation by collection namespace
- emulator config, seed script, and docs (firebase-leaderboards.md,
  leaderboard-env-migration.md, AYO.md)

Utils/tests: UTC date-bucket helper (retained as metadata) with Vitest coverage.
2026-07-03 13:39:44 +02:00

104 lines
3.8 KiB
JavaScript

/**
* Seed the DEVELOPMENT leaderboard with sample scores so the boards aren't empty
* while developing. Writes to the `mw-test-*` namespace by default.
*
* Two ways to run it:
*
* 1. Against the local Firestore EMULATOR (recommended — no cloud, no deploy):
* pnpm emulators # in one terminal
* pnpm seed:emulator # in another
* (seed:emulator sets FIRESTORE_EMULATOR_HOST so writes hit the emulator.)
*
* 2. Against the real cloud dev database: REQUIRES the generalized
* firestore.rules to be deployed first (they permit writes to
* `mw-test-scores`), else every write returns permission-denied:
* npx firebase deploy --only firestore:rules --project dev
* (cd app && node ../scripts/seed-dev-scores.js)
*
* `firebase` is an app-workspace dependency, so run it from the app directory so
* Node can resolve it. Optional namespace override: LB_NAMESPACE=mw-test.
*/
import { initializeApp } from 'firebase/app'
import { getFirestore, connectFirestoreEmulator, doc, collection, setDoc } from 'firebase/firestore/lite'
import { buckets } from '../utils/date-bucket/date-bucket.js'
const firebaseConfig = {
apiKey: 'AIzaSyCTi_5Sm5dHFNf0d_Gn0MNWmlGheFBf6MQ',
authDomain: 'moment-188701.firebaseapp.com',
databaseURL: 'https://moment-188701.firebaseio.com',
projectId: 'secure-moment-188701',
storageBucket: 'secure-moment-188701.firebasestorage.app',
messagingSenderId: '113827947104',
appId: '1:113827947104:web:b176f746d8358302c51905'
}
const namespace = process.env.LB_NAMESPACE || 'mw-test'
// Per-level rough time ranges (ms) so sample times look believable.
const LEVELS = {
beginner: [1800, 12000],
intermediate: [28000, 95000],
expert: [90000, 260000],
nightmare: [180000, 620000]
}
const NAMES = [
'Ayo', 'Sweeper', 'MineWizard', 'jenjereren', 'wasd', 'kaboom',
'flagella', 'ClickClick', 'Torpedo', 'S-Davies', 'Anonymous', 'zero-day',
'BoomBot', 'gridlock', 'NoNiner', 'chord-lord', 'tenten', 'quicksilver'
]
// Ages (days ago) spread so Today / Week / Month / All Time all get entries.
const AGE_DAYS = [0, 0, 0, 1, 2, 4, 6, 10, 18, 27, 45, 80]
const now = Date.now()
const rand = (min, max) => Math.floor(min + Math.random() * (max - min))
const pick = arr => arr[Math.floor(Math.random() * arr.length)]
const store = getFirestore(initializeApp(firebaseConfig))
// Point at a local Firestore emulator when FIRESTORE_EMULATOR_HOST is set
// (e.g. "127.0.0.1:8080"). Writes then hit the local emulator, not the cloud.
if (process.env.FIRESTORE_EMULATOR_HOST) {
const [host, port] = process.env.FIRESTORE_EMULATOR_HOST.split(':')
connectFirestoreEmulator(store, host, Number(port))
console.log(`Using Firestore emulator at ${process.env.FIRESTORE_EMULATOR_HOST}`)
}
let ok = 0
let failed = 0
let firstError
for (const [level, [min, max]] of Object.entries(LEVELS)) {
for (let i = 0; i < AGE_DAYS.length; i++) {
const stamp = new Date(now - AGE_DAYS[i] * 86400000 - rand(0, 3600000))
const bucket = buckets(stamp)
const entry = {
name: pick(NAMES),
playerId: `seed-${level}-${i}`,
score: rand(min, max),
category: level,
time_stamp: stamp,
day: bucket.day,
week: bucket.week,
month: bucket.month,
meta: { isMobile: Math.random() < 0.3, seed: true }
}
try {
await setDoc(doc(collection(store, `${namespace}-scores`, level, 'games')), entry)
ok++
} catch (error) {
failed++
firstError = firstError || (error.code || error.message)
}
}
console.log(`${level}: seeded`)
}
console.log(`\nDone. Wrote ${ok} sample score(s) to ${namespace}-scores, ${failed} failed.`)
if (failed) {
console.error(`\nWrites failed (${firstError}). If this is 'permission-denied', deploy the rules first:`)
console.error(' npx firebase deploy --only firestore:rules --project dev')
process.exit(1)
}