Predict football scores, earn points for how close you get, and out-foresee your friends on the global ranking.
  • TypeScript 69.8%
  • Vue 26.7%
  • JavaScript 2.9%
  • CSS 0.4%
  • Dockerfile 0.2%
Find a file
2026-06-08 09:35:49 +02:00
.github test(harness)+refactor(god-components): Nuxt component-test harness (@nuxt/test-utils, two vitest projects: node 'unit' gate + 'nuxt' env for *.nuxt.test.ts via mountSuspended) 2026-06-08 07:10:28 +02:00
app fix(admin): invalidate the vue-query cache after import/sync 2026-06-08 09:32:36 +02:00
db feat(crowd+brand+404): 'Show everyone's totals' account preference (showCrowd field + migration 0008, /api/predictions/crowd sums via groupBy, useCrowdTotals composable, totals under prediction inputs on fixtures/match/My Picks, verified 7-2 semantics in test + live); LogoMark reworked per feedback - pedestal restored, glow is now paint-bucket section fills (5 panels + core, staggered), title untouched; 404 trail retracts and dims with the star, nothing lingers during the pre-shot pause 2026-06-08 03:51:46 +02:00
drizzle feat(crowd+brand+404): 'Show everyone's totals' account preference (showCrowd field + migration 0008, /api/predictions/crowd sums via groupBy, useCrowdTotals composable, totals under prediction inputs on fixtures/match/My Picks, verified 7-2 semantics in test + live); LogoMark reworked per feedback - pedestal restored, glow is now paint-bucket section fills (5 panels + core, staggered), title untouched; 404 trail retracts and dims with the star, nothing lingers during the pre-shot pause 2026-06-08 03:51:46 +02:00
i18n/locales fix(about): tech-stack cards corrupted every 3rd entry - nested anchors (card <a> + license badge <a>) made Firefox split the parse and leak the badge's monospace/10px into sibling cards; card is now a div with a stretched project link + sibling badge link (verified all titles 16px). also: rarityTip helper restored (anchor drift had no-op'd it, 500ing user pages), prediction breakdown shows +N rarity and a ×N joker/final badge so points reconcile with the total 2026-06-08 05:15:40 +02:00
lib refactor(TIER3/4 core): predictionHits dedup (was open-coded 3x in the engine); AppStage const helpers (isSingleMatchStage/countsDouble/isKnockout) replacing scattered FINAL/THIRD_PLACE literals across server + client; Tier4 minors - seed championBonus (was masked by column default), 2FA-delete hard-fails on missing auth secret instead of decrypting against '', preferences toggle uses async/await not .then, encrypted-adapter no longer treats a corrupt sealed envelope as legacy plaintext (decrypt error now surfaces) 2026-06-08 06:04:55 +02:00
mise-tasks refactor(mise): make create-admin and release direct node file tasks (#!/usr/bin/env -S node + //MISE//USAGE) 2026-06-08 09:08:43 +02:00
public feat(game+ux): single-match rounds drop the joker - the FINAL counts double for everyone (engine forceJoker, finalize passes stage, setJoker rejects FINAL/THIRD_PLACE, x2 badge in fixtures + picks lists, tested); stats skeleton reordered under the real possession bar (venue skeleton above, fake possession removed); champion flag gap widened; ranking showcase screenshot retaken with crowned champion flags 2026-06-08 04:47:26 +02:00
scripts refactor(mise): make create-admin and release direct node file tasks (#!/usr/bin/env -S node + //MISE//USAGE) 2026-06-08 09:08:43 +02:00
server fix(uefa): correct goal assists and own-goal detection in match events 2026-06-08 09:32:36 +02:00
shared/types refactor(TIER3/4 core): predictionHits dedup (was open-coded 3x in the engine); AppStage const helpers (isSingleMatchStage/countsDouble/isKnockout) replacing scattered FINAL/THIRD_PLACE literals across server + client; Tier4 minors - seed championBonus (was masked by column default), 2FA-delete hard-fails on missing auth secret instead of decrypting against '', preferences toggle uses async/await not .then, encrypted-adapter no longer treats a corrupt sealed envelope as legacy plaintext (decrypt error now surfaces) 2026-06-08 06:04:55 +02:00
tests fix(h2h): head-to-head spans all competitions (was same-tournament only, where two teams almost never meet twice) - code-based pairing, competition name on each row, links route to the right competition (verified: euro QF ESP-GER surfaces WC2022 ESP 1-1 GER) 2026-06-08 01:58:23 +02:00
.dockerignore refactor(mise): make create-admin and release direct node file tasks (#!/usr/bin/env -S node + //MISE//USAGE) 2026-06-08 09:08:43 +02:00
.env.dev.example feat(security+infra): 2FA-gated account termination (server-enforced via beforeDelete + x-totp-code, verified both ways); passkey registration behind sudo mode (password+2FA confirm -> 5-min reauth cookie, server middleware guards register endpoints, sign-in stays open, verified); compose split: pinned images (postgres:17.10-alpine, maildev:2.2.1, app:local), dev extras in compose.dev.yaml with optional .env.dev, .mise.toml task shortcuts 2026-06-07 22:18:26 +02:00
.env.example chore(config): drop dead runtimeConfig + env vars (NUXT_MATCH_PROVIDER, NUXT_FIFA_SEASON_ID, NUXT_WC_SEASON) 2026-06-08 08:01:55 +02:00
.gitignore chore(security): remove accidentally-committed curl cookie jar (adm.jar) 2026-06-08 07:51:24 +02:00
.mise.toml refactor(mise): convert the arg-taking tasks (create-admin, release) to file tasks with #USAGE specs 2026-06-08 09:02:46 +02:00
.nuxtrc test(harness)+refactor(god-components): Nuxt component-test harness (@nuxt/test-utils, two vitest projects: node 'unit' gate + 'nuxt' env for *.nuxt.test.ts via mountSuspended) 2026-06-08 07:10:28 +02:00
CHANGELOG.md chore(release): 0.14.0 2026-06-08 09:35:49 +02:00
compose.dev.yaml fix(compose+docker): db port off the prod base (app talks to it in-network as db:5432); host access moved to the dev overlay on 127.0.0.1; app bound to 127.0.0.1:3000 (reverse-proxy in front). .dockerignore excludes non-build files (compose, Dockerfile, scripts, tests, *.test.ts, README, .mise.toml) so editing them no longer busts the pnpm build cache - verified CACHED on a README touch. NOTE kept LICENSE + CHANGELOG.md in context (imported ?raw by license/about pages). 2026-06-08 08:56:52 +02:00
compose.yaml fix(compose+docker): db port off the prod base (app talks to it in-network as db:5432); host access moved to the dev overlay on 127.0.0.1; app bound to 127.0.0.1:3000 (reverse-proxy in front). .dockerignore excludes non-build files (compose, Dockerfile, scripts, tests, *.test.ts, README, .mise.toml) so editing them no longer busts the pnpm build cache - verified CACHED on a README touch. NOTE kept LICENSE + CHANGELOG.md in context (imported ?raw by license/about pages). 2026-06-08 08:56:52 +02:00
Dockerfile build: containerize with multi-stage Dockerfile + compose.yaml; bump pnpm to 11.5.1 2026-06-05 02:13:29 +02:00
drizzle.config.ts feat: set up Drizzle ORM + Postgres with docker-compose dev database 2026-06-05 00:08:59 +02:00
LICENSE feat: UEFA Euro 2024 via UEFA's public match API (51 fixtures, shootouts exact, provider+tests); WTFPL license; keepachangelog CHANGELOG backfilled; /about page (stack grid, OSS acknowledgements w/ licenses, AI note, data credits, rendered changelog); landing FAQ (8 Q&A x4 locales) + footer (c) line; coverage badge generator + README badges; SMTP e2e script (pnpm e2e:smtp / mise run e2e-smtp); built output verified under Bun 2026-06-07 22:55:14 +02:00
nuxt.config.ts chore(config): drop dead runtimeConfig + env vars (NUXT_MATCH_PROVIDER, NUXT_FIFA_SEASON_ID, NUXT_WC_SEASON) 2026-06-08 08:01:55 +02:00
package.json chore(release): 0.14.0 2026-06-08 09:35:49 +02:00
pnpm-lock.yaml test(harness)+refactor(god-components): Nuxt component-test harness (@nuxt/test-utils, two vitest projects: node 'unit' gate + 'nuxt' env for *.nuxt.test.ts via mountSuspended) 2026-06-08 07:10:28 +02:00
pnpm-workspace.yaml build: containerize with multi-stage Dockerfile + compose.yaml; bump pnpm to 11.5.1 2026-06-05 02:13:29 +02:00
README.md fix(compose+docker): db port off the prod base (app talks to it in-network as db:5432); host access moved to the dev overlay on 127.0.0.1; app bound to 127.0.0.1:3000 (reverse-proxy in front). .dockerignore excludes non-build files (compose, Dockerfile, scripts, tests, *.test.ts, README, .mise.toml) so editing them no longer busts the pnpm build cache - verified CACHED on a README touch. NOTE kept LICENSE + CHANGELOG.md in context (imported ?raw by license/about pages). 2026-06-08 08:56:52 +02:00
tsconfig.json feat(types+validation): TIER1#3 + TIER2 keystone 2026-06-08 05:59:45 +02:00
uno.config.ts feat(ui): rebrand to Nostragoalus; dark mode, redesigned layout + cards, competition switcher, match view 2026-06-05 10:56:21 +02:00
vitest.config.ts test(harness)+refactor(god-components): Nuxt component-test harness (@nuxt/test-utils, two vitest projects: node 'unit' gate + 'nuxt' env for *.nuxt.test.ts via mountSuspended) 2026-06-08 07:10:28 +02:00

Nostragoalus - the football oracle

Nostragoalus

Coverage License: WTFPL

A football score-prediction game: friends predict match scores and earn points by how close they get, ranked per competition and on a global leaderboard. Ships with the FIFA World Cup 2026 (default), World Cup 2022, and UEFA Euro 2024.

Source: https://git.arzaroth.com/Arzaroth/Nostragoalus

Runtimes

Built and tested on Node.js 22 and Bun:

pnpm build && node .output/server/index.mjs   # or: bun .output/server/index.mjs

Features

  • Score predictions with closeness-tiered points, a rarity bonus, and one ×2 joker per round
  • Optional crowd totals under every prediction (everyone's picks combined), updated live over WebSocket
  • Transparent scoring: base + rarity bonus + joker/final ×2 broken out on every pick, with the full formula in the FAQ
  • Champion pick bonus, locked at the first kickoff and shown beside every name on the rankings
  • Per-competition and global rankings with movement arrows; browse other players' (locked) predictions
  • Live scores over WebSocket with a pixel-art goal celebration; match view with possession, per-team match stats, goal timeline with cards (incl. touchline bookings) and substitutions, all-time head-to-head and cross-competition form (friendlies included, causally cut off at kickoff), penalty shootouts, and each team's top scorer / top assister
  • Per-team pages: official squads with positions, manager, season stats, competition switcher
  • Knockout bracket and an interactive world map (Leaflet / OpenStreetMap)
  • Auth: email + password (HIBP-checked), 2FA (TOTP, email codes, single-use backup codes, trusted devices), passkeys (sudo-gated registration), runtime-configurable SSO (OIDC / SAML / Google) with envelope-encrypted secrets, admin user management
  • Four languages (EN / FR / TH / tlh), light/dark/system themes saved per account
  • Auto-generated API docs at /docs/api (OpenAPI + Scalar)

Stack

See the in-app About page for the full annotated list with licenses. Highlights:

  • Nuxt 4 + Vue 3 + TypeScript (Nitro node-server), PrimeVue v4, UnoCSS, VueUse, motion-v, Nuxt I18n
  • TanStack Vue Query (client) + Nuxt useFetch (SSR)
  • better-auth (sessions, 2FA, passkeys, SSO, admin)
  • Drizzle ORM + PostgreSQL (PGlite for hermetic tests)
  • Provider-agnostic match data: keyless FIFA and UEFA public APIs
  • In-process scheduled tasks (Croner) for fixtures / live scores / finalize

Scoring

Tiered base points (exact 3 / goal-difference 2 / outcome 1 / miss 0) + a rarity bonus + one ×2 joker per round, plus a champion-pick bonus. Penalty shootouts decide who advances, never your points.

Running with Docker

compose.yaml is the prod-shaped base (pinned Postgres + the multi-stage app image); compose.dev.yaml overlays dev extras (maildev SMTP catcher, hot-reload container, .env.dev). Migrations apply automatically on startup (RUN_MIGRATIONS=true). With mise:

cp .env.example .env   # set BETTER_AUTH_SECRET, NUXT_ADMIN_EMAILS, ...
mise run up            # prod-like: app + db
mise run dev           # HMR dev server + db + maildev (inbox UI on :1080)
mise run preview       # built app + db + maildev
mise run down          # stop everything

(Equivalent raw commands live in .mise.toml.)

Local development

docker compose -f compose.yaml -f compose.dev.yaml up -d db   # Postgres (loopback :5432)
pnpm install
cp .env.example .env        # then fill in secrets
pnpm db:migrate             # apply migrations
pnpm dev                    # http://localhost:3000
pnpm typecheck              # strict vue-tsc gate
pnpm test:coverage          # logic unit tests (>=98% branch coverage enforced)
pnpm test:components        # component/composable tests (Nuxt runtime)
pnpm e2e:smtp               # email-OTP end-to-end (needs mise run dev)
pnpm badge                  # refresh the coverage badge from the last run

mise run check runs the full gate (typecheck + coverage + component tests).

First admin

There's no default admin password. Either add your email to NUXT_ADMIN_EMAILS and sign up normally, or provision one directly (stack must be up):

mise run create-admin you@example.com "Your Name"

It prompts for the password (hidden, never in shell history), signs up through better-auth (HIBP-checked + hashed), and sets the DB role to admin. Idempotent - re-running just promotes an existing account.