About
Nostragoalus is an open-source football prediction game: call the scores, earn points for how close you get, and out-foresee your friends.
Built with AI
This entire application - code, design, translations and even the artwork pipeline - was built in collaboration with Claude (Anthropic), with a human football fan directing every decision. The AI wrote the code; the humans own the opinions.
Read the sourceTech stack
The open-source software this app stands on. Every card links to the project; the badge links to its license.
Frontend
Backend
node-qrcodeMITThe 2FA enrollment QR codes
Tooling & infrastructure
Data sources
Match data comes from publicly accessible FIFA and UEFA endpoints (unofficial, read-only, politely rate-limited). Map tiles by OpenStreetMap contributors. This project is not affiliated with or endorsed by FIFA or UEFA.
Changelog
0.14.02026-06-08
Added
- `mise run create-admin <email> [name]` provisions an admin on demand: prompts for the password (hidden, never in shell history or the process list), signs up via better-auth (HIBP-checked + hashed), then sets the DB role; idempotent. No default admin password exists - this or NUXT_ADMIN_EMAILS bootstraps the first admin.
Fixed
- UEFA match assists showed the beaten goalkeeper instead of the assister (a goal event's secondaryActor is the keeper); real assists are separate ASSIST events, now paired to goals by minute. Penalties correctly show no assist.
- UEFA own goals were never detected (marked as a GOAL with subType 'OWN', not type OWN_GOAL) - 0 recorded across Euro 2024 and each miscredited to the scorer's team; now detected, credited to the beneficiary, with the forcing player's assist.
- Admin import/sync now invalidates the client query cache, so a previously-loaded (e.g. empty) competition no longer keeps showing stale data until a manual refresh.
Changed
- Dropped dead config (NUXT_MATCH_PROVIDER, NUXT_FIFA_SEASON_ID, NUXT_WC_SEASON): provider and season are per-competition (DB / live FIFA seasons API), the env vars were never read.
Security / ops
- Postgres no longer publishes a host port in the prod compose base (the app reaches it in-network); host access for local dev moved to the dev overlay, bound to loopback. The app binds to 127.0.0.1 (put a reverse proxy in front).
- Slimmed the Docker build context so editing compose files, docs, scripts or tests no longer busts the build cache; removed an accidentally-committed curl cookie jar.
0.13.02026-06-08
Fixed
- Crowd totals genuinely refresh on a competition switch now: the three consumers shared one static useFetch key, so Nuxt served the previous competition's cached payload. Rewritten as a plain ref + explicit refetch on (preference, competition) change; locked by a component test.
0.12.02026-06-08
Added
- Component-test harness (@nuxt/test-utils): a `nuxt`-env Vitest project mounts components/composables with auto-imports + PrimeVue via `mountSuspended` (`pnpm test:components`, wired into the release gate and `mise check`).
Changed
- God-components split, logic extracted to tested units. account.vue (558->421 lines): the 2FA enrol/disable/regenerate state machine is now `useTwoFactor` (8 tests) and passkey management `usePasskeys` (5 tests); image resize moved to `app/utils/image.ts`. The match view's timeline assembly and head-to-head tally moved to a pure `app/utils/match-view.ts` (unit-tested). The coverage gate stays on the node logic surface (98.3%); components are covered by their own suite.
0.11.02026-06-08
Added
- `pnpm typecheck` gate (strict vue-tsc) wired into the release gate and `mise run check` - the type-safety net that was configured but never run. Client types (MyPrediction/MatchListItem/LeaderboardRow) now derive from the server query return types (via a `Serialized<>` helper), so they can't drift from the schema.
- Runtime request validation: a `defineValidatedHandler` wrapper (auth guard + Zod body parse + error mapping) on the prediction/joker/champion writes, making the OpenAPI schemas load-bearing (422 on bad input). Handler-level and auth-guard tests added.
Fixed
- The finalize tick is now one atomic transaction (lock/unlock, scoring, champion awards, voids) - a crash mid-tick can no longer zero champion points or half-score a round.
- Several latent bugs the new typecheck surfaced: session `.value` access (the "you" highlight / authed flag were always falsy), predictions never selected penalties (pens never rendered on picks), prediction inputs coerced `undefined`->NaN.
Changed
- Dedup + structure: shared provider stage ladder (one ordered table - fixes the fifa/uefa divergence), shared `getJson` envelope in the FIFA provider, `predictionHits` scoring predicate, `rowFromPerspective` h2h decode, `AppStage` helpers (`isSingleMatchStage`/`countsDouble`), semantic colour tokens (`--ng-star`/`--ng-danger`/`--ng-success`).
- Hardening: 2FA-delete hard-fails on a missing auth secret instead of decrypting against `''`; the encrypted-adapter no longer treats a corrupt sealed envelope as legacy plaintext; the scoring-config seed includes championBonus.
0.10.02026-06-08
Fixed
- Crowd totals refresh when you switch competition (they were stuck showing "–" for the new competition's matches).
- Tech-stack cards mangled every third entry (monospace, tiny text): the card was an anchor with the license badge as a nested anchor - invalid HTML that Firefox split, leaking the badge style. Card is now a div with a stretched project link and a sibling badge link.
Changed
- Prediction points reconcile with the joker/final multiplier: a "+N rarity" chip ("only X% picked this") plus a "×2" badge when the joker or the final doubled the score, so the breakdown matches the total.
Added
- Scoring is now spelled out: predictions show the base points and a separate "+N rarity" chip with an "only X% picked this" tooltip; champion-pick points appear on the leaderboard and player pages; the FAQ carries the full formula in plain notation.
Changed
- A joker can't be placed on a fixture whose teams aren't decided yet (same rule as predicting it), server-enforced and hidden in the UI.
Added
- Champion picks visible at a glance: crowned flag beside each name on the ranking and on player pages; player pages gained a competition switcher and a Global scope.
- Landing showcase is a carousel (circular, autoplay) and the map screenshot now actually shows the map.
Changed
- Single-match rounds have no joker: the final automatically counts double for everyone (badge says so), the third-place play-off scores normally; placing a joker there is rejected server-side.
Fixed
- Stats skeleton no longer fights the already-loaded possession bar (possession sits above the skeleton, which lost its fake bar).
Added
- Landing showcase: six real screenshots (fixtures with crowd totals, match depth, ranking, bracket, map, team page) over a seeded league of two dozen demo oracles; mise tasks seed-demo and shots regenerate everything with headless Firefox.
0.9.02026-06-08
Added
- Crowd totals update live over the WebSocket (anyone saving a prediction refreshes everyone's view, your own saves included) and reserve their line so cards never resize.
- Header crystal ball is bigger, includes the pedestal, and each section glows gold under the cursor (five panels, core, and the orb's outer ring).
- Real 404 page: the shot sails over the bar and becomes a star (clean loop), with a cursor-reactive starfield; the landing starfield got a gravitational lens and the champion pick a holographic hover.
- "Show everyone's totals" preference: under each prediction input, the combined score of all players' predictions (1-1 + 2-1 + 4-0 shows as 7-2) with the prediction count - on fixtures, the match view and My Picks.
- Stats tab shows skeletons while match detail loads.
0.8.02026-06-08
Added
- All-time head-to-head on the match view, sourced from FIFA's full international calendar (World Cups, qualifiers, continental championships, friendlies - back to 1908 where FIFA has it). Works before kickoff, so it doubles as a prediction tool. Tally + goals line + meeting list, linked to our match pages where we hold the fixture.
- Form shows each team's last five results across ALL international football (friendlies and qualifiers included), with competition and date.
- Next lists the team's competition games after the viewed match - results shown form-style for games played since.
- Live-goal celebration: when a live match's score increases, a pixel-art first-person goal animation takes over for three seconds (contributed artwork; reduced-motion respected).
- Match-page dates include the year (head-to-head reaches back decades).
Changed
- Head-to-head, Form and the in-house meeting list all cut off at the viewed match's kickoff - the future never dictates the past.
- The head-to-head tab is always visible; pairs with no recorded meeting get a "first meeting" note instead of a silently missing tab.
- All commit history rewritten to the Arzaroth identity.
Fixed
- Knockout brackets aligned feeder matches under the wrong parents (FIFA lists knockout matches arbitrarily; Morocco and Brazil sat on the wrong sides). A shared ordering pass now walks down from the final for every provider.
- Bracket cards showed (0) penalty scores on matches decided in regulation; pens render as superscripts only for real shootouts.
- The final is pinned to the semis' midline; connector lines merge mid-gap and lead straight into the next fixture; dates centered on every card.
- Euro bracket cards showed "Invalid Date" (UEFA bracket matches lacked kickoff times).
- Match players tab lists contributors only instead of full 26-man rosters of zeros.
- About: Motion's own mark replaces the Framer design-tool logo; official favicons for Nuxt I18n, node-postgres, Nodemailer, maildev.
0.7.12026-06-08
Added
- Euro per-match statistics now come from UEFA's official match-centre feed (possession, passes, crosses, distance covered) with event-stream aggregation as fallback.
- Euro knockout bracket, derived from results (feeders ordered under their parents, champion crowned).
- Full player rankings for Euro (paged; previously cut at 200, hiding most of any squad on match pages).
- Finished matches cache their detail and stats for the process lifetime (live ones still refresh every 5 minutes).
Changed
- Player and coach names render in title case everywhere (FIFA's "Kylian MBAPPÉ" becomes "Kylian Mbappé"; correctly-cased names pass through).
Fixed
- Coach bookings showed "?" - touchline cards (Nagelsmann, Hjulmand) now carry the coach's name and a clipboard marker, on both providers.
- Second yellows on Euro matches were dropped (UEFA encodes them as explicit YELLOW_CARD_SECOND / RED_YELLOW_CARD events).
- Euro matches synced before the stats feed landed had no possession - backfilled.
Added (UX)
- Match view remembers its open tab in the URL (survives refresh, shareable); stats is the default tab when available.
- Substitutions on the match timeline (on/off players, both providers) with persisted toggles to hide subs or bookings.
- Auto-generated API documentation at /docs/api: every route annotated (summaries, descriptions, request bodies, response codes), framework internals filtered out, admin endpoints labeled internal, httpie as the default snippet client; GET responses carry real schemas and examples sampled from the live API.
- Head-to-head tally bar on the match view (wins / draws / wins, shootouts counted as wins).
- Public info pages (About, License) share one shell (starfield + footer); footer and About link the now-public source repository.
- /license page rendering the WTFPL, linked from the footer; the footer (with its own-line "Made with ♥️ from 🇫🇷") is shared with the About page.
- About: VueUse, Nuxt I18n and node-postgres added to the stack.
- Fixture search is accent-insensitive and matches country codes ("Tur" finds Türkiye, "FRA" finds France).
- About: theme-aware mise logo, official TanStack icon, project homepages preferred over GitHub links.
0.7.02026-06-07
Added
- Euro 2024 feature parity with the FIFA competitions: match events (goals with assists, yellow / second-yellow / red cards), per-match stats derived from UEFA's event stream, official squads with positions, season team stats (UEFA-exact), and top scorers / assisters from UEFA's ranking API.
- World Cup 2026 announced squads now show before the tournament (team id resolved from the calendar when no match has been played).
- About page: official logos on every stack card, Bun in the stack.
Changed
- Competitions ordered newest season first everywhere.
- VueUse adopted where it simplifies: reactive QR rendering for 2FA enrollment, clipboard, tickers (countdowns, next-run labels), system dark-mode detection.
Fixed
- Team page competition switcher had no defined order.
0.6.02026-06-07
Added
- UEFA Euro 2024 fixtures and results through UEFA's public match API (groups, knockouts, penalty shootouts).
- Two-factor authentication: TOTP authenticator enrollment with QR + setup key, single-use backup codes (confirmed save step, regenerate on demand), trusted devices with revocation, email codes via SMTP.
- Passkey (WebAuthn) sign-in and management, registration gated behind a fresh password + 2FA confirmation (sudo mode).
- Have-I-Been-Pwned checks rejecting breached passwords at signup and password change.
- Admin: ban/unban users, strip 2FA from an account, per-task last-run/last-failure history in the action tooltips, live next-run countdowns.
- 2FA-gated and last-admin-protected account termination.
- maildev mail catcher in the dev compose overlay; email-OTP end-to-end test (`pnpm e2e:smtp`).
- WTFPL license, coverage badge, this changelog, mise task shortcuts, pinned container images.
Fixed
- `NUXT_CRON_ENABLED=true` was coerced to a boolean by the runtime config and silently disabled live score polling.
0.5.02026-06-07
Added
- Per-user preferences (language, theme incl. system) saved to the account and restored at sign-in; browser/system detection for guests.
- Thai and Klingon translations alongside English and French.
- Brand identity: crystal-ball mark, favicon, full-bleed remastered banner with a scroll-driven intro that docks into a slim pinned bar, animated starfield, oracle-eye default avatars.
- Landing page with feature grid, scoring explainer, and competition showcase.
Fixed
- First-load hydration failure that kept client-only effects (starfield, banner scrub) from running until a client-side navigation.
0.4.02026-06-06
Added
- Competition in the URL (`/world-cup-2026/matches`, …) with a page-title switcher pill; unknown slugs 404.
- Runtime SSO administration (OIDC, SAML, Google) with envelope-encrypted secrets (KEK -> DEK -> AES-256-GCM).
- Admin user management (create, promote/demote, delete) on the better-auth admin plugin.
- Full team pages: official squads with positions and coach, FIFA-exact tournament stats, competition switcher.
- Match view: football-intelligence stat rows (attempts, passes, distance covered, pressures…), chronological laced timeline with yellow/second-yellow/red cards, editable prediction in place, clickable form/next/head-to-head/standings.
- World map: team in the URL, selection surviving competition switches, click-to-center, clickable group standings.
- Personal stats strip on My Picks; leaderboard movement arrows from rank snapshots; kickoff countdowns.
Fixed
- FIFA card codes decoded correctly (2 = straight red, 3 = second yellow).
- Penalty-shootout artifacts (0-0 "shootouts") purged at the source and guarded everywhere.
0.3.02026-06-06
Added
- One ×2 joker per round (movable until kickoff), crowd-rarity bonus, champion pick with country flags.
- Interactive world map (Leaflet + OpenStreetMap) with per-team panels.
- Knockout bracket as a real two-sided tree from FIFA's season bracket.
- Keyless FIFA top scorers, match details (goals, possession, attendance, cards) and live WebSocket score pushes.
0.2.02026-06-05
Added
- Multi-competition schema: World Cup 2026 (default), World Cup 2022, Euro 2024; per-competition and global leaderboards.
- Scheduled tasks: hourly fixture refresh, 2-minute live polling gated on a live window, 5-minute finalize (lock + score).
- i18n (EN/FR), dark mode, redesigned shell.
0.1.02026-06-05
Added
- Nuxt 4 + PrimeVue + UnoCSS scaffold, Drizzle + Postgres, better-auth email/password.
- Closeness-tiered scoring engine (exact 3 / goal difference 2 / outcome 1) with idempotent re-scoring.
- Fixtures, predictions with server-side kickoff locks, leaderboard, admin sync; Vitest suite with a 95% (later 98%) coverage gate.