Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
9cc6dfc
Removed git add from husky
justwickedcode Mar 9, 2026
e997cda
Implemented the scraper for multipla languages and a basic api
justwickedcode Sep 15, 2026
20f06b6
Add the quotes-api service, 12 more Wikiquote language editions, and …
justwickedcode Sep 15, 2026
89623c0
Add backend/ws: real-time multiplayer typing races over WebSockets
justwickedcode Sep 15, 2026
b0ad43a
Fix conflict resolution: drop pre-origin/main useTypingRace.ts hunk a…
justwickedcode Sep 15, 2026
07d0e38
Remove dead Nuxt scaffold file (frontend/app/app.vue)
justwickedcode Sep 15, 2026
f1e12b6
Remove unused cn import from TypingRace.tsx
justwickedcode Sep 15, 2026
02d5b9b
Wire the real multiplayer backend into the actual frontend routes
justwickedcode Sep 16, 2026
6c61285
Reskin theme to shadcn's neutral black-and-white palette
justwickedcode Sep 16, 2026
6e8ee79
Fix broken scrolling and vertically-centered content pages
justwickedcode Sep 16, 2026
8904669
Implement the Instrument Cluster redesign: analog gauges, sidebar rai…
justwickedcode Sep 16, 2026
1c376fc
Implemented scraper, api, ws and improved frontend
justwickedcode Sep 16, 2026
8881d4d
Fixed prod log spam
justwickedcode Sep 28, 2026
25b8706
Fixed prod log spam
justwickedcode Sep 28, 2026
eee41b7
Fix Wikiquote crawler getting rate-limited across all editions
justwickedcode Sep 28, 2026
5cbc182
Use a Wikimedia-policy-compliant User-Agent for Wikiquote requests
justwickedcode Sep 28, 2026
56d1881
Revert global Wikiquote rate limiter — it was self-inflicted queueing…
justwickedcode Sep 28, 2026
f42b800
Clamp discovery cursor's CategoryIndex on the dead-streak skip path too
justwickedcode Sep 28, 2026
9bae79c
Fix all three Wikiquote parsers missing every quote on every page
justwickedcode Sep 28, 2026
f078569
Fix citation-link discovery rejecting every link on every page
justwickedcode Sep 28, 2026
c10c7f2
Fix two real indexing/query-scaling gaps found auditing production re…
justwickedcode Sep 28, 2026
2bacd7a
Batch quote saves into one INSERT per page instead of one per quote
justwickedcode Sep 28, 2026
9011176
Add game_unsuitable quote flag, calibrated against the live 752K-row …
justwickedcode Sep 28, 2026
87b3efa
Default LOG_LEVEL to error, not warn, for production
justwickedcode Sep 28, 2026
07f45e4
Reject MediaWiki red links before queueing, not after a guaranteed 404
justwickedcode Sep 29, 2026
1266832
Exclude game_unsuitable quotes from what players actually get served
justwickedcode Sep 29, 2026
c06d2b8
Preserve real line breaks in poem-shaped quotes instead of destroying…
justwickedcode Sep 29, 2026
45710f2
Preserve line breaks through MakeTypingSafe instead of flattening them
justwickedcode Sep 29, 2026
ca6b0ba
Revert "Preserve line breaks through MakeTypingSafe instead of flatte…
justwickedcode Sep 29, 2026
12053a1
Flag poem-shaped (multi-line) quotes as game_unsuitable
justwickedcode Sep 29, 2026
a6fdcde
Gate live racing to desktop and fix a stats-page hydration flash
justwickedcode Sep 29, 2026
002f7ce
Fix /guide's word drill stacking one word per line instead of scrolling
justwickedcode Sep 29, 2026
9250a6e
Fold the /play mode picker into the dashboard, removing the extra hop
justwickedcode Sep 29, 2026
1f258d7
Expand achievements from 6 to 23 and reorganize the dashboard layout
justwickedcode Sep 29, 2026
f456942
Rebuild the daily challenge: start button, countdown, history, streaks
justwickedcode Sep 29, 2026
f298929
Replace hand-drawn car SVGs with real top-down sprites; add Upgrades …
justwickedcode Sep 29, 2026
921b0cc
Consolidate Garage into one page, drop livery/credits, gate cosmetics…
justwickedcode Sep 29, 2026
a6a004e
Add a confetti/flash celebration when the player crosses the finish line
justwickedcode Sep 29, 2026
849871e
Add solo-race powerups (boost/shell) and unlockable trail effects
justwickedcode Sep 29, 2026
9b22f6d
Make trail/powerup/underglow independently selectable in the Garage
justwickedcode Sep 29, 2026
70cbd1d
Fix app-wide "selected" border/bg not rendering, add Trail live previews
justwickedcode Sep 29, 2026
f90c503
Add 7 Fleet vehicles, drop car-body recolor, fix inconsistent recolor…
justwickedcode Sep 29, 2026
dacd15f
Unlock everything for now, preview trails on the main car, add 3 trai…
justwickedcode Sep 29, 2026
ff97663
Add 11 more vehicles (Pixel + Aircraft categories), remove sidebar di…
justwickedcode Sep 29, 2026
ad48a08
Fix vehicle roster issues: drop trailer, fix rally/police lookalikes,…
justwickedcode Sep 29, 2026
fb596e5
Replace gunship with helicopter, add Heavy/Street categories, fix SUV…
justwickedcode Sep 29, 2026
b5f2ebe
Drop Heavy/Street/Aircraft vehicles, keep only Lambo (moved into Cars)
justwickedcode Sep 29, 2026
83e7462
Add 3 more supercars from the Turbo Developement Team pack
justwickedcode Sep 29, 2026
8ee7d1b
Add 3 more cars from looneybits' 2D Sport Cars pack
justwickedcode Sep 29, 2026
b7813dd
Redesign trail effects to be bigger and more aggressive
justwickedcode Sep 29, 2026
4bd82d7
Fix trail regressions: thunderbolt orientation, smoke visibility, sho…
justwickedcode Sep 29, 2026
9742d03
Toughen up trail visuals: solid fills instead of soft gradient fades
justwickedcode Sep 29, 2026
6114415
Toughen up the thunderbolt trail
justwickedcode Sep 29, 2026
0771f4e
Multiple thunderbolts + bigger/thicker trails across the board
justwickedcode Sep 29, 2026
79744c2
Thunderbolt: 3 scattered bolts at angles, spread further from the car
justwickedcode Sep 29, 2026
0aa1b51
Thunderbolt: stack the 3 bolts as a column instead of a scattered line
justwickedcode Sep 29, 2026
1ae60a0
Thunderbolt: vary bolt lengths, kill the merged-glow background box
justwickedcode Sep 29, 2026
74cc0fe
Add 3 more powerups with activation animations, fix ready-banner visi…
justwickedcode Sep 30, 2026
467c8bc
Sidebar polish: drop the best-wpm block, color the logo, roomier spacing
justwickedcode Sep 30, 2026
ca927a7
Fix sidebar over-spacing and a stray glow bug from the last pass
justwickedcode Sep 30, 2026
0f48d75
Skip already-queued red-link URLs instead of fetching them to a 404
justwickedcode Sep 30, 2026
fe72e1f
Sidebar: bigger home icon, even icon spacing, collapse pinned to bottom
justwickedcode Sep 30, 2026
dda96f2
Log the running total quote count every 1000 quotes saved
justwickedcode Sep 30, 2026
c3012f5
Discover Goodreads tag links from each quote, not just a fixed 19-tag…
justwickedcode Sep 30, 2026
1ef159e
Add a random-page dead-end fallback for Goodreads, mirroring Wikiquote's
justwickedcode Sep 30, 2026
e25b6b1
Log a source/language breakdown every 10k quotes, not just the runnin…
justwickedcode Oct 1, 2026
621b539
Log the source/language summary at startup too, with comma-formatted …
justwickedcode Oct 1, 2026
6403fa0
Render the source/language summary as an aligned table, sorted by cou…
justwickedcode Oct 1, 2026
0511661
Delay a hard-failed URL's retry instead of stalling the whole worker
justwickedcode Oct 1, 2026
345d47b
Wire backend/ws into production deployment alongside api and crawler
justwickedcode Oct 1, 2026
79e5fb1
Wire solo-race quotes to the real backend/api corpus
justwickedcode Oct 1, 2026
7025592
Add a Dockerfile for the frontend's production deployment
justwickedcode Oct 1, 2026
1740522
Document deploying the frontend as its own Dokploy App
justwickedcode Oct 1, 2026
c0857c6
Log api/ws connectivity at startup and WebSocket lifecycle events
justwickedcode Oct 1, 2026
051aaba
Make VITE_API_URL/VITE_WS_URL settable as runtime env vars, not build…
justwickedcode Oct 1, 2026
4ad0280
Take api off the public internet entirely, proxied through the frontend
justwickedcode Oct 1, 2026
4e75cb8
Add a compose file for deploying crawler/api/ws/frontend together aga…
justwickedcode Oct 1, 2026
5706f3f
Attach the app-stack compose project to Dokploy's shared dokploy-network
justwickedcode Oct 1, 2026
36ab35b
Drop host port publishing from frontend/ws in the app-stack compose file
justwickedcode Oct 1, 2026
1c5bd13
Fix frontend/ws healthchecks hitting IPv6 loopback with nothing liste…
justwickedcode Oct 1, 2026
ef97f53
Consolidate to a single docker-compose.yml matching the real deployment
justwickedcode Oct 1, 2026
f05175c
Add a roadmap phase for CI and frontend test coverage
justwickedcode Oct 1, 2026
8cb16e3
api: own users/race_results tables, add account persistence endpoints
justwickedcode Oct 2, 2026
6f4144b
frontend: Casdoor OIDC login (/login, /auth/callback, /logout)
justwickedcode Oct 2, 2026
353509c
Fix OIDC issuer mismatch via real discovery, add visible login UI
justwickedcode Oct 2, 2026
a001d16
auth: make the OIDC integration provider-agnostic, drop Casdoor naming
justwickedcode Oct 3, 2026
130e5e3
frontend: add a dedicated /account page
justwickedcode Oct 3, 2026
35d883f
auth: fetch userinfo endpoint too, don't depend on id_token alone
justwickedcode Oct 3, 2026
370bf1f
auth: force the provider chooser after logout with prompt=select_account
justwickedcode Oct 3, 2026
8054af0
auth: prompt=login too, select_account alone didn't force re-auth
justwickedcode Oct 3, 2026
1f20fb6
auth: add OIDC_ORG_ID to scope the login flow to the right Zitadel org
justwickedcode Oct 3, 2026
3c99fb7
auth: fix discovery URL dropping the issuer's path (Keycloak realm 404)
justwickedcode Oct 3, 2026
603370a
docs: document the OIDC/Keycloak auth setup in PRODUCTION.md
justwickedcode Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copy this to .env.prod and fill in real values — never reuse dev placeholder values here.
# Used by:
# docker compose --env-file .env.prod up -d --build
#
# This stack deploys against Postgres/Redis that already exist elsewhere (no postgres/redis
# service blocks in docker-compose.yml) — point these at your real instances.

# Full connection strings for your existing Postgres/Redis — not split into user/password/host
# parts, since docker-compose.yml passes these straight through as-is to crawler/api/ws.
DATABASE_URL=postgres://USER:PASSWORD@HOST:5432/DBNAME?sslmode=disable
REDIS_ADDR=redis://default:PASSWORD@HOST:6379

# The real, deployed frontend URL — not localhost. This is the one origin the api and ws
# services will both accept cross-origin / WebSocket-upgrade requests from. (api's own copy of
# this is effectively vestigial now that it has no public domain — see PRODUCTION.md's
# "Isolating api behind the frontend" — kept only as harmless defense-in-depth.)
CORS_ALLOWED_ORIGIN=https://your-frontend-domain.example.com

# ws's real public wss:// domain — a full URL, not a path under the frontend's or api's own
# domain. Baked into the frontend's client bundle at container startup (see
# frontend/docker-entrypoint.sh); the browser connects to this directly. There's no equivalent
# var for api anymore — it's reached only through the frontend's own internal proxy, configured
# via API_INTERNAL_URL inside docker-compose.yml itself, not a secret worth putting here.
VITE_WS_URL=wss://your-ws-domain.example.com
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ frontend/dist/
logs/
*.log

# Go (scraper)
# Go (scraper + api + ws)
# NOTE: go.sum must NEVER be ignored — it locks dependency checksums and is required for
# reproducible builds. A prior "*.sum" rule here silently excluded both backend/scraper/go.sum
# and backend/api/go.sum from every commit ever made in this repo; removed, and both files are
# now tracked for real.
backend/scraper/vendor/
*.sum
*.exe
backend/api/quotes-api
backend/ws/quotes-ws
372 changes: 372 additions & 0 deletions PRODUCTION.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1423,6 +1423,18 @@ one to already exist for what it delivers to matter.
- [ ] §6.4: `/leaderboard` endpoint + `leaderboard` view.
- [ ] §11.1: `/leaderboard`, `/login`, `/register` routes.

### Phase 6 — CI and frontend test coverage

- [ ] Add CI (e.g. GitHub Actions) running `go build`/`go test` for all
three backends (`api`, `ws`, `scraper`) and `bun run build`/`bun run
lint` for the frontend on every push/PR — currently these only run
locally, by hand, so nothing stops a broken commit from landing on
`main`.
- [ ] Add frontend test coverage. The three Go backends have real test
suites (18 `_test.go` files across crawler/dedup/fetcher/parser/db/
api/ws); the frontend currently has none — "tested" there means
build+lint clean, not behavior-verified.

---

## 19. Open questions & risks
Expand Down
14 changes: 14 additions & 0 deletions backend/api/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copy this file to .env and fill in real values. .env is gitignored — never commit real
# credentials.

# This API reads the same "quotes" Postgres database backend/scraper writes to — see
# backend/scraper/docker-compose.yml for that database's own container. This API does not run
# its own database and does not run migrations (backend/scraper owns the schema); run the
# scraper against a fresh database at least once before starting this API.
DATABASE_URL=postgres://postgres:postgres@localhost:55432/quotes?sslmode=disable

# Which port this API listens on, and which frontend origin it accepts cross-origin requests
# from. In production, set this to the real deployed frontend URL (e.g.
# https://vroomy.example.com), not localhost.
API_PORT=8080
CORS_ALLOWED_ORIGIN=http://localhost:3000
19 changes: 19 additions & 0 deletions backend/api/Caddyfile.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Example Caddy config for terminating real HTTPS in front of the quotes API — Caddy gets you
# automatic Let's Encrypt certificates (issued and renewed with zero manual cert management)
# for the cost of one file like this. Not included as a service in docker-compose.prod.yml
# by default since it needs a real domain name pointed at this host to issue a certificate for
# — meaningless in a local/test environment, and different for every real deployment.
#
# Usage once you have a domain pointed at this host:
# 1. Copy this file to Caddyfile, replace api.example.com with your real domain.
# 2. Install Caddy on the host (not in Docker, for simplicity — apt/dnf/brew all package it,
# or see https://caddyserver.com/docs/install), or add it as its own compose service if
# you'd rather keep everything containerized.
# 3. sudo caddy run --config Caddyfile
#
# This assumes the api service is reachable at 127.0.0.1:8080 — which is exactly how
# docker-compose.prod.yml publishes it (bound to localhost only, not the public interface).

api.example.com {
reverse_proxy 127.0.0.1:8080
}
16 changes: 16 additions & 0 deletions backend/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Multi-stage build: compile with the full Go toolchain, ship only the static binary in a
# minimal runtime image.
FROM golang:1.26-alpine AS build
WORKDIR /src

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /api .

FROM alpine:3.20
WORKDIR /app
COPY --from=build /api .
EXPOSE 8080
ENTRYPOINT ["./api"]
68 changes: 60 additions & 8 deletions backend/api/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,65 @@
# Rest API
# quotes-api

## For Max:
A small read-only Go HTTP API serving quotes from the `backend/scraper` crawler's Postgres database, currently used for one thing: feeding the frontend's typing-race game real passages instead of its static placeholder pool (see `frontend/src/lib/typing/sentences.ts`).

- I was thinking about using the one of the next frameworks:
- [Elysia](https://elysiajs.com/)
- [Hono](https://hono.dev/)
## Why Go, not Elysia/Hono

You can also look into others that you like. I am personally familiar with [NestJS](https://nestjs.com/), but I think it's better to get something new for everyone.
The original plan here (see git history) was a TypeScript API using Elysia or Hono. That changed once the actual use case became concrete: this API only needs to read quotes the scraper already collected, using a schema and word-count filtering logic the scraper's own Go code already defines. Rebuilding that in a second language/runtime — a new Postgres client, a new understanding of what makes a quote "typing-race-eligible" — would have been pure duplication for zero benefit, since this service does nothing beyond query and reshape data the scraper already owns.

- They are the new "hot stuff in the town" and I taught it would be fun to try them. I personally prefer Elysia
If a broader REST API (auth, other resources, a real API for the whole app) becomes needed later, that's a fair reason to revisit the stack — Elysia/Hono are still perfectly reasonable choices for that. This one just didn't need it.

- To spin up a db, you can use the `docker-compose` file I gave you. Just run `docker-compose up -d`. To turn it off, `docker-compose down -v`. (-v = delete volume / delete the data you stored in the db already)
## Relationship to backend/scraper

This is a **separate Go module** (its own `go.mod`), not a package inside `backend/scraper` — Go's `internal/` visibility rules mean it couldn't import `backend/scraper`'s internal packages even if it wanted to, and it doesn't need to: it only needs a Postgres connection and a couple of read queries, both trivial to have directly.

- **Database**: points at the _same_ Postgres instance `backend/scraper` writes to (see `.env` — `DATABASE_URL`), not a separate one. Run `backend/scraper`'s own `docker-compose.yml` to get that database running.
- **Schema/migrations**: owned entirely by `backend/scraper`. This API never runs migrations and never writes — run the scraper at least once against a fresh database before starting this API, so the `quotes` table (and its `word_count` column, which this API's filtering depends on) actually exists.
- **`word_count`**: a column `backend/scraper`'s `db.SaveQuote` computes and stores on every quote at write time (indexed alongside `language`) — this API filters on it directly rather than re-splitting every candidate row's text on every request.

## Running it

```bash
go run .
```

Reads `.env` for `DATABASE_URL`, `API_PORT` (default `8080`), and `CORS_ALLOWED_ORIGIN` (default `http://localhost:3000`, i.e. the frontend's dev server). `.env` is optional — if it's just not present (e.g. running in a container, where config comes from the environment directly), that's not an error; only a genuinely malformed `.env` file that does exist is fatal.

For a production deployment (Docker, a reverse proxy for real HTTPS, backups of the shared database) see `../../PRODUCTION.md` at the repo root.

## Rate limiting

`GET /api/quotes/random` is limited to 30 requests/minute per IP (burst of 10) — see `ratelimit.go`. This API has no accounts or API keys (it's called directly from any visitor's browser), so per-IP limiting is the only practical abuse control available; it's sized generously for real gameplay (one new sentence per race, roughly every 10-60+ seconds) and exists to stop a script hammering the endpoint, not to throttle real players. `/health` is exempt, since an orchestrator's own healthcheck polling it every few seconds shouldn't be able to trip a limit meant for abuse.

Prefers `X-Forwarded-For`'s first entry when present (set this correctly in your reverse proxy — see `PRODUCTION.md`), falling back to the direct connection's address for local dev where there's no proxy in front.

## Endpoints

### `GET /api/quotes/random`

Returns one random quote suitable for a typing race.

Query params (all optional):

| Param | Default | Notes |
| ---------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
| `language` | `en` | Matches `quotes.language` (`en` or `de` currently) |
| `minWords` | `25` | Matches the word-count floor the frontend's placeholder pool was tuned for — short passages make WPM numerically unstable |
| `maxWords` | `60` | Upper bound, same reasoning |
| `exclude` | — | Exact text to exclude, so the next race doesn't repeat the passage just shown |

```json
{
"text": "Racing against the clock is the only way to know how fast your fingers really are...",
"author": "Mark Twain",
"source": "wikiquote-en",
"language": "en"
}
```

`404` (`{"error": "no quote matches the requested filters"}`) if nothing matches — a real, expected outcome (an unusual word-count range, a language with a small corpus), not a server error. The frontend should fall back to its static placeholder pool in this case rather than treating it as broken.

Text returned here has already been run through `MakeTypingSafe` — em/en dashes, curly quotes, and ellipsis characters are normalized to plain-ASCII equivalents (checked live: roughly a quarter of typing-eligible quotes contain one of these) so the game's exact character-match input handling doesn't punish a player for punctuation they have no reasonable way to type. Accented letters (names, borrowed words) are left as-is — typeable, just with an extra keystroke on some layouts, and not worth stripping legitimate content over.

### `GET /health`

`200 {"status": "ok"}` if Postgres is reachable, `503` otherwise.
15 changes: 0 additions & 15 deletions backend/api/docker-compose.yml

This file was deleted.

21 changes: 21 additions & 0 deletions backend/api/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module quotes-api

go 1.26.8

require (
github.com/jackc/pgx/v5 v5.11.0
github.com/joho/godotenv v1.5.1
github.com/pressly/goose/v3 v3.27.0
golang.org/x/time v0.16.0
)

require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.34.0 // indirect
)
60 changes: 60 additions & 0 deletions backend/api/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM=
github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
37 changes: 37 additions & 0 deletions backend/api/internal/db/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Package db owns api's own tables (users, race_results) — distinct from backend/scraper's
// migrations, which own quotes/url_frontier in the same physical Postgres database. goose's
// migration-version tracking defaults to one shared "goose_db_version" table regardless of
// which Go binary calls it, so without SetTableName below, api's and scraper's independent
// migration histories would land in the same tracking table and get mixed together (not
// corrupted — the version numbers wouldn't collide since the two services' migration
// timestamps differ — just confusing bookkeeping). SetTableName gives api's own migrations a
// separate "api_goose_db_version" table, keeping each service's history clean.
package db

import (
"embed"

"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
)

//go:embed migrations/*.sql
var migrations embed.FS

func Migrate(pool *pgxpool.Pool) error {
sqlDB := stdlib.OpenDBFromPool(pool)

goose.SetBaseFS(migrations)
goose.SetTableName("api_goose_db_version")

if err := goose.SetDialect("postgres"); err != nil {
return err
}

if err := goose.Up(sqlDB, "migrations"); err != nil {
return err
}

return nil
}
19 changes: 19 additions & 0 deletions backend/api/internal/db/migrations/20261003000000_create_users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- +goose Up
-- id is Casdoor's own "sub" claim — a stable, globally unique string identity — not a
-- locally-generated serial. api never issues its own user ids; Casdoor already owns identity.
CREATE TABLE users (
id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
email TEXT,
avatar_url TEXT,
car_model VARCHAR(32) NOT NULL DEFAULT 'sport',
underglow BOOLEAN NOT NULL DEFAULT false,
underglow_color VARCHAR(16) NOT NULL DEFAULT '#0284c7',
trail VARCHAR(32) NOT NULL DEFAULT 'nitro',
equipped_powerup VARCHAR(32) NOT NULL DEFAULT 'boost',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- +goose Down
DROP TABLE users;
Loading