Taskmaster Scoreboard Generator
A static scoreboard (index.html) with an optional phone remote control (control.html) for entering scores from another device. No build step — everything is plain HTML/CSS/JS, served as-is.
Prerequisites:
- Python 3 — runs the local dev server (
serve.py). Check withpython3 --version. - Node.js 18+ — only needed to run the test suite, not for the app itself. Check with
node --version. - A Firebase project — only needed for the remote-control feature (below); the display page works fine without it.
Clone the repo:
git clone https://github.com/Unicycle77/tm-scoreboard.git
cd tm-scoreboard
npm start
Serves the site at http://localhost:8420. That's all index.html needs on its own — the remote-control feature additionally requires a Firebase project (below).
The display and control pages sync over a Firebase Realtime Database. Each person running their own copy of this app sets up their own free Firebase project — nobody shares a database with anyone else's copy of the app. To use this feature (locally or when you deploy your own copy):
- Sign in to Firebase. Go to the Firebase console with any Google account (create one first if you don't have one).
- Create a project. Click "Add project" / "Create a project", give it any name, and click through the wizard — you can turn off Google Analytics, it isn't used by this app. Everything here fits in Firebase's free Spark plan; no credit card or billing setup is required.
- Create the Realtime Database. In the left sidebar: Build → Realtime Database → Create Database. Pick any region. When it asks about security rules, choose "Start in test mode" — you'll lock it down properly in step 6, so don't skip that step (test-mode rules auto-expire after 30 days).
- Register a web app. Click the gear icon → Project settings → General tab → scroll to "Your apps" → click the
</>(web) icon. Give it any nickname, and you don't need to check "Also set up Firebase Hosting". After clicking "Register app", Firebase shows afirebaseConfigobject — keep that page open, you'll copy values from it next. - Configure this app. Copy
js/firebase-config.example.jstojs/firebase-config.js(gitignored — never commit this file, since it's specific to your own Firebase project) and fill in the values from thefirebaseConfigobject shown in the previous step:window.TM_FIREBASE_CONFIG = { apiKey: "...", authDomain: "...", databaseURL: "...", projectId: "..." };
- Lock down the database rules. Back in the console: Build → Realtime Database → Rules tab, replace the contents with:
This scopes all access to
{ "rules": { "sessions": { "$code": { ".read": true, ".write": true } } } }sessions/<code>— anything else in the database is denied by default. Anyone who knows a specific session's code can read/write it (same trust model as the pairing code itself); nothing else is exposed. Click Publish.
That's it — npm start, open index.html, click "Remote" to get a pairing code/QR, and open control.html (or scan the code) on another device. Seeing "1 device connected" after opening both confirms it's wired up correctly.
The rules above mean a session's code is its access control — no login, no per-user permissions. That's intentionally lightweight for a casual game night, but it's weaker than "someone has to see your screen or scan your QR code" implies: Firebase Realtime Database enforces no rate limiting and the rules don't require authentication, so a script can guess codes directly against Firebase's API without a human ever seeing one.
Codes are 6 characters from a 31-character alphabet (~888 million possible codes, CODE_LENGTH in js/sync.js) specifically to make that impractical — it was originally 4 characters (~924k codes), which was a small enough space to make untargeted brute-forcing a real, if low-stakes, possibility rather than a purely theoretical one. 6 characters raises the bar significantly; it doesn't make brute-forcing impossible against a well-resourced, targeted attacker, just no longer realistic as an idle/opportunistic attack. Given there's nothing sensitive in a scoreboard and sessions are short-lived, that trade-off is accepted rather than going further. If you want it hardened further:
- Even longer codes are the cheapest next step — bump
CODE_LENGTHinjs/sync.js. No new dependencies or infrastructure, at the cost of a code that's more tedious to read aloud or type by hand (QR scanning is unaffected). - Firebase App Check is the thorough fix. Attaches an attestation token (via reCAPTCHA v3, free) to every request and lets you require a valid token in the RTDB rules, which blocks scripts talking to Firebase directly outside a real instance of this app in a browser — regardless of code length. Bigger lift: register a reCAPTCHA v3 site, add the App Check SDK to
js/vendor/, and initialize it before other Firebase calls in bothindex.jsandcontrol.js. - Security-rules-based rate limiting doesn't help here — rules can throttle repeated writes to a known path, but can't limit how many different codes a client tries, since each guess targets an unrelated path with no shared state to check against.
Requires Node 18+ (check with node --version; if you're on an older default, nvm install 20 && nvm use 20 works well).
npm install
npm test # unit + e2e
npm run test:unit # pure scoring/task logic only, no browser or network needed
npm run test:e2e # full browser flows via Playwright
The e2e tests drive real pages against your actual Firebase project from step above (js/firebase-config.js must exist), so run the setup section first. Each test cleans up the session it creates afterward.