Skip to content

About

Self-hosted BattleBit Remastered stats tracker

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

BattleBit Stats

A self-hosted stats tracker for BattleBit Remastered. It polls the public API, keeps a time series of every leaderboard player and clan, browses the live server list, and charts all of it on a dark interface.

Everything it shows is real data from publicapi.battlebit.cloud. Nothing is simulated, and where the upstream data can't support a claim the UI says so instead of guessing.

Live: https://bbrstats.duckdns.org

Overview — live population, board leaders, fastest gainers, and where people are playing

Player profile — totals, linked value and rank charts, per-board standing

What it does

  • Leaderboards — all nine player boards plus the clan board, searchable and paginated, with per-row value gain and rank movement.
  • Player profiles — lifetime totals per board, rank and percentile, an estimated level, and two linked charts (value over time, rank over time) that re-render when you pick a different board or time range.
  • Server browser — live list with region / mode / map facets, plus a detail page showing population over time, map rotation, and round boundaries.
  • Overview — current population, who leads each board, who is gaining fastest, and where people are actually playing.
  • Search — ⌘K (or /, or the button) across players, clans and servers.
  • BattleBit Arena — an archive of the community 1v1 ladder at bbrarena.gg: every match, every kill, the weapon used and where it happened, plus a ladder, head-to-head records and per-player weapon breakdowns. Clearly labelled as a scraped archive throughout, and kept in separate tables from the live-API data.

Documentation

  • docs/DEPLOY.md — running it locally or on a VPS, resetting the data, and the disk-growth knobs
  • SECURITY.md — threat model, hardening, and what an active sweep actually found
  • docs/ROADMAP.md — what's next and why
  • docs/CLAN-PAGES.md — design for clan-maintained rosters, and what adding a write surface would cost

Quick start

docker compose up -d
# http://localhost:8106

The first poll runs at boot: the server list lands in under a second and the full leaderboard (~45,000 observations) in about three. History accumulates from there — charts that need two points will say so until they have them.

Without Docker:

cd backend
npm install          # one dependency: express
npm start            # serves the API and the SPA on :8090, ingest included

Node 22 or newer is required — the app uses the built-in node:sqlite, so there is no native module to compile and no build step for the frontend.

How the storage works

The previous version stored everything as JSON documents that were rewritten in full on every poll cycle. At ~23,000 players across nine boards that produced a 292 MB players.json and a 535 MB ranks.json.

This version writes only what moved:

  • stat_current holds one row per player per board — the latest value and rank. Leaderboard reads hit this and nothing else.
  • stat_history gains a row only when a value actually changes. A player who hasn't played since the last poll costs one updated_at write.
  • A rank that drifts without the underlying value moving (someone else passed you) is throttled to at most one point an hour, measured against the last logged rank so a change during the quiet window is deferred, not lost.

The same 23,575 players and 5,000 clans occupy 7.7 MB. A second poll one minute later wrote zero history rows.

Before After
Store JSON documents, full rewrite per cycle SQLite, append-on-change
Size at ~23k players 827 MB 7.7 MB
Full leaderboard ingest — ~3 s
Runtime dependencies express, discord.js express

API

Every endpoint is read-only.

Endpoint Returns
GET /api/overview live population, counts, leaders, movers, distributions
GET /api/leaderboard/:category one board — ?page, ?perPage, ?q
GET /api/player/:name profile: per-board value, rank, percentile, level, activity
GET /api/player/:name/series time series — ?category, ?range=24h|7d|30d|all
GET /api/compare?names=a,b up to four players on one board
GET /api/servers live list with facets — ?region, ?gamemode, ?map, ?q, ?sort
GET /api/server/:id samples, map rotation, round boundaries
GET /api/clans, GET /api/clan/:id clan board and clan detail
GET /api/search?q= players, clans and servers
GET /api/arena/overview Arena archive totals, activity, top weapons, ladder
GET /api/arena/ladder Arena ladder — ?sort, ?q, ?min, ?page
GET /api/arena/matches Arena matches — ?player, ?weapon, ?page
GET /api/arena/match/:id full kill log, weapons and arena polygon
GET /api/arena/player/:name record, weapons used, head-to-head, rating history
GET /api/arena/weapons weapon usage across the whole archive
GET /api/health counts and the last ten ingest runs
POST /api/refresh force an ingest; 404s unless ADMIN_TOKEN is set

There is no login, because there is nothing to protect — the app has no write surface unless you deliberately enable ADMIN_TOKEN.

Reads are rate limited per IP (240 requests/minute by default, RATE_MAX and RATE_WINDOW_MS to tune). Static assets are exempt — throttling those would break page loads for anyone sharing a public IP. The limiter lives in the app rather than the proxy so it travels with the deployment.

Upstream data limits

These are properties of the public API, surfaced in the UI rather than papered over:

  • No player IDs. Rows are keyed by display name, so a rename starts a fresh history under the new name.
  • Names are not unique. A single board carries 25–40 repeated display names — two different players both called "Harry" at different ranks. Each board is collapsed to one row per name, keeping the best-ranked occurrence, and the profile discloses the collision count instead of blending the histories.
  • Boards are truncated to roughly the top 5,000, so "percentile" means position within the published board, not within the whole player base.
  • Level is estimated. The API publishes no level field; the curve here is an approximation and is labelled as such everywhere it appears.
  • No per-match data. Server "rounds" are inferred from map changes between consecutive polls, and clan membership is inferred from a (TAG) prefix in a player's name — both are labelled as inferred.

Clan self-service

A clan can maintain its own roster. The operator mints a PIN for whoever runs the clan; the first time they use it they set a password, and the PIN stops working. From then on they sign in on their clan's page and can write a description, add social links, and add/edit/reorder/remove members with free-form roles.

Links are allowlisted to http/https, and the YouTube / Twitch / Discord icon is derived from the parsed hostname on the server — a link cannot wear a badge it has not earned, and every link shows its real destination.

Everything else on the site stays a read. This is the only place a visitor can write, so it carries scrypt passwords, server-side sessions whose tokens are stored only as hashes, HttpOnly + SameSite=Strict cookies, CSRF tokens on every write, a per-credential lockout, and authorisation taken from the session rather than the URL.

→ docs/DEPLOY.md · SECURITY.md

Operator dashboard

A local-only control panel at /admin, on the tailnet and localhost but never on the public domain. Seven pages: Overview, Cadence, Traffic, Arena, Clan access, Settings, Maintenance.

The Traffic page carries an offline world map of where readers connect from, the full visitor log (sortable, searchable, filterable, paged), request history, and a manual blocklist. Geolocation is a local table — no address ever leaves the machine. Install it with npm run geoip:update; without it the map says so and everything else carries on.

→ docs/DEPLOY.md

Runtime settings

Poll cadence, rate limits and feature switches live in the database, not only in the environment. They are edited from the operator dashboard at /admin and take effect on the next poll or the next request — no restart. An environment variable of the same name is the default for a setting that has never been changed, so a fresh clone behaves exactly as documented and stays tunable afterwards.

Fourteen settings across four groups: ingest cadence, Arena schedule, public limits, feature switches. Each is range-checked; a key that is not in the registry cannot be written at all.

Configuration

Everything is optional; see .env.example and docs/DEPLOY.md for the full table.

Resetting the data is an operator command, not a web endpoint:

docker compose exec battlebit-stats node admin.js reseed   # wipe and pull fresh
docker compose exec battlebit-stats node admin.js status   # size and growth
docker compose exec battlebit-stats node admin.js backup   # safe on a live db

Granting a clan access to their page, and archiving the Arena ladder, are also operator commands — see docs/DEPLOY.md:

docker compose exec battlebit-stats node admin.js clan-grant 5001 --discord "name"
docker compose exec battlebit-stats node admin.js clan-grants
docker compose exec battlebit-stats node admin.js clan-revoke 5001
docker compose exec battlebit-stats node admin.js arena-scrape
docker compose exec battlebit-stats node admin.js arena-update

There is also a local-only operator dashboard at /admin for archive status, Arena updates and minting clan PINs. It is served on localhost and the tailnet only — the public domain returns 404 for it, and it is deliberately not on a Tailscale Funnel. See docs/DEPLOY.md.

It asks you to type a confirmation word first. Nothing destructive is reachable over HTTP — see SECURITY.md.

Design

The interface follows the Hallmark Midnight theme in the atmospheric genre: near-black paper tinted toward the anchor hue rather than #000, a single cool indigo accent, Space Grotesk over Inter with JetBrains Mono for figures. Fonts are self-hosted — the app ships a strict CSP (default-src 'self') and fetches nothing from a CDN at runtime.

Charts are hand-built SVG with no charting library. The categorical palette was validated with the data-viz palette checker against this app's actual dark surface and passes on lightness band, chroma floor, colour-vision separation, normal-vision separation and contrast. Distribution bars deliberately use a single hue: they compare one measure across labelled categories, so a second colour channel would encode nothing.

Layout tokens live in frontend/css/tokens.css and are portable.

Development

cd backend
npm test                  # append-on-change rules, profile maths, sampling, rate limiting
npm run ingest:once       # one live poll, exits non-zero if a feed failed

The frontend has no build step. frontend/js/core/ holds a ~70-line signal implementation, a hash router and the fetch layer; frontend/js/charts/ holds the chart primitives; frontend/js/views/ holds one file per route.

Roadmap

Product features live in docs/FEATURE-ROADMAP.md; hosting and operations in docs/ROADMAP.md.

docs/ROADMAP.md — five phases. Phase 4 (a dedicated public DuckDNS domain) is live. The measured finding driving the rest: history grows ~615,000 rows/day, and 76 % of those rows are rank drift rather than anyone actually playing — bounding that, plus backups, is the next real work.

Licence

MIT. Not affiliated with BattleBit Remastered or its developers. Map thumbnails under frontend/maps/ ship with the repository.

About

Self-hosted BattleBit Remastered stats tracker

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages