# Racetimer backend (2026) REST API for the UNLOZE racetimer, reading the `unloze_racetimer_css_2026` database. Java 8+, Jersey (JAX-RS 2.1), deployed as `racetimer_endpoints-1.0.war` on Tomcat 8.5, the same as before. All paths below are under `https:///racetimer_endpoints-1.0/api/`. ## Deploying 1. Build: `mvn package`. The output is `target/racetimer_endpoints-1.0.war`. 2. Update `/opt/tomcat/race_backend_settings.json`. It is the same file the old backend used, with more keys; see `race_backend_settings.example.json`. - `racetimerURL` must now point at the **`unloze_racetimer_css_2026`** database. - Add the `sourcebans*`, `gidStaff` and `gidAdmin` keys. They work like `SBPP_DB_*`, `GID_STAFF` and `GID_ADMIN` in the entwatchbans panel. - Set `jwtSecret` to at least 32 random characters. Without it, admins are signed out on every Tomcat restart. - Set `publicBackendUrl` to this backend's public URL. - Set `frontendUrl` to the React site's URL. Steam sends admins back there after they sign in. - Move the Steam Web API key into `steamApiKey`. The old code had it hardcoded in `Facade.java`, which shipped in the repo, so generate a new key. 3. Deploy the WAR as before. The data loads on startup and reloads every `refreshMinutes` (default 30). The settings path can also be given with `-Dracetimer.settings=/path/file.json` or the `RACETIMER_SETTINGS` environment variable. ## Points Points are calculated per category, from each player's **best valid** time only. Tied times share a position. | Rule | Detail | |---|---| | Who gets points | Everyone while a board has fewer than 200 completions. From 200 on, only the faster half (200 → top 100, 210 → top 105). | | Base points | n − (position − 1): the fastest gets n (the number of completions), each position below gets one less. The slower half gets 0, so on big boards points drop from about n/2 straight to 0 at the cut. | | Bonus multiplier | Only on boards with 100+ completions. 4× at #1, sliding smoothly to 2× at the top-1% mark, then to 1× at the top-5% mark. | | CLASSIC RACETIMER | Final points ÷ 10. | | Invalid category | Still shown with positions, but gives 0 points. | | Invalid record | Listed separately under `invalidated` with no position and 0 points. Only that one run is removed; the player's previous valid time counts again. | | Servers | ZE1 and ZE2 points and ranks are separate. Categories from any other tag (e.g. `dev`) are hidden. | The old flat +2500 bonus for small boards is gone. `GET` responses are computed from an in-memory snapshot. Admin changes rebuild it immediately. ## In-game plugins `racetimer_rank.sp` and `toplvl.sp` keep working unchanged. `player/{steamid}` and `leaderboard/minified/{offset}` still return `PlayerPoints` (and `name`). Without `?server=`, both endpoints use `defaultServerTag` (ze1). On ZE2, add `?server=ze2` to those two URLs so levels come from ZE2 points. Levels drop for CLASSIC data, as intended. ## Endpoints `{steamid}` accepts `STEAM_0:x:y`, `STEAM_1:x:y`, `[U:1:n]` or a SteamID64. `?server=` is `ze1` or `ze2` and defaults to ze1. Errors are JSON: `{"statusCode": 404, "errorMessage": "..."}`. ### Public | Method & path | Returns | |---|---| | `GET timers/leaderboard/{offset}?server=` | 100 players ranked by that server's points. Fields: `steamID`, `steamID64`, `name`, `Avatar`, `Rank`, `PlayerPoints`, `Times`, `UrlBanners`, `server`, `servers` (`{"ze1": {points, rank, times}, "ze2": {...}}`), `badges`. | | `GET timers/leaderboard/minified/{offset}?server=` | `[{name, PlayerPoints}]` (for `toplvl.sp`) | | `GET timers/player/{steamid}?server=` | One player, same fields as the leaderboard. 404 if unknown. | | `GET timers/player/badges/{steamid}` | `{badgesUrls, badges: [{name, url}]}` | | `GET timers/player/maps/{steamid}/{offset}?server=` | 50 rows of the player's best per category. Fields: `recordId`, `categoryId`, `mapName`, `stage`, `categoryNumber`, `serverTag`, `isLegacy`, `categoryInvalid`, `time`, `position`, `completions`, `bonusMultiplier`, `points`, `recordedAt`. Without `server`, both servers are included. | | `GET timers/player/history/{steamid}/{offset}?categoryId=` | 50 improvements, newest first. Fields: `time`, `previousTime`, `improvedBy` (seconds), `recordedAt`, `isInvalid`, `isCurrentBest`, plus the category fields. Legacy records are last, with `recordedAt: null`. | | `GET timers/allmaps` | `[{mapName, allCategoriesInvalid, stages: [{stage, allCategoriesInvalid, categories: [category]}]}]` | | `GET timers/map/{mapname}` | One map in the same shape (case-insensitive). | | `GET timers/category/{id}/{offset}` | `{category, offset, pageSize, entries: [75], invalidated: [...]}` | | `GET timers/searchplayers/{text}?server=` | Up to 100 players, matched on name or Steam ID. | | `GET timers/searchmaps/{text}` | Maps whose name contains the text. | `category` fields: `id`, `categoryNumber` (the same "Category N" as in-game), `mapName`, `stage`, `serverTag`, `serverCvars`, `cvars` (`[{name, value}]`, for showing what differs between categories), `isLegacy`, `isInvalid`, `givesPoints`, `completions`, `fastestTime`. Board `entries` fields: `recordId`, `position`, `steamID`, `steamID64`, `name`, `avatar`, `badgesUrls`, `time`, `points`, `bonusMultiplier`, `recordedAt` (Unix seconds, `null` for legacy), `isLegacy`. ### Admin sign-in (Steam) 1. The site links the admin to `GET auth/steam/login`. 2. After Steam, the backend redirects to `frontendUrl` with either: - `#token=` - or `#loginError=not_admin`, `#loginError=steam_verification_failed`, or `#loginError=admin_check_unavailable` 3. The site sends the token as `Authorization: Bearer `. The old `x-access-token` header also works. 4. `GET auth/me` returns `{steamID, steamID64, name, role, expiresAt}`. - The role is `admin` for `gidAdmin` groups and `staff` for `gidStaff` groups. - A missing or expired token gives 401. ### Admin actions (staff and admin) | Method & path | Body | |---|---| | `PUT admin/records/{recordId}` | `{"invalid": true}` or `{"invalid": false}` | | `PUT admin/categories/{categoryId}` | same | The response is `{id, invalid, changedBy}`. The schema has no audit table, so every change is written to the Tomcat log with the admin's name and Steam ID. ## Removed endpoints `timers/mapsizecache/...` and the old `timers/map/{mapname}/{stage}/{offset}` are gone. Use `category.completions` and `timers/category/{id}/{offset}`. The old username/password `login` endpoint and the JPA entities are gone too. ## Tests `mvn test` runs the tests for the points formula, the snapshot builder, history, Steam IDs, tokens, Steam OpenID checks, and the whole API in memory. The API tests use no database.