admin
The internal admin surface, on port 3003.
apps/statshub-admin is the data-ops console behind the product: where the feeds
are loaded, the corrections are made, the bot is configured and a running
backfill is watched. It is a view over api and nothing
else — no database client, no API client of its own.
bun run dev:admin # http://localhost:3003
bun run build:adminWhat it does
Loaders and backfills
Pull leagues, tournaments, events, shotmaps and images in, and watch a
backfill job while it runs. src/components/dashboard is one file per
control, which is why the list of them reads like the list of jobs.
Data quality rules
Stats rules, position-change avoid rules, player-assignment maintenance and team-name management — the corrections that keep a feed's idea of a player and ours pointing at the same person.
The Telegram bot
Routing, tournament scope, alerts, custom messages, and a test send. What the bot posts is decided here rather than in its own config.
Models and prompts
Model management and an OpenRouter prompt editor, so a prompt change does not need a deploy.
Daily spreadsheet
The prop screener as a sheet, for the people who work in one.
Social export and widgets
/social-export renders posts as images; /widget-showcase is every rich
response the agent can return, rendered on one page so a widget can be
checked without asking a model for it.
How it is built
Every /api request is a rewrite
There is no API client here and no database client either. next.config.mjs
rewrites /api/:path* to API_ORIGIN server-side, so the browser stays on
the admin origin and the Supabase session cookie rides along unchanged.
Except /api/health
That one is the container's liveness probe and answers from the filesystem
route. afterFiles rewrites — which is where rewrites() puts these —
never reach a route that already exists, which is exactly why it works.
Nothing here is indexed
X-Robots-Tag: noindex, nofollow on /:path*, set in the config rather
than per page, so a new route cannot forget it.
API_ORIGIN picks the target
It decides which deployment you are administering and defaults to
http://127.0.0.1:8080. The dashboard is full of destructive buttons and
none of them ask twice — check what the API is attached to first.
Types, not a generated client
src/types/api.ts holds the response shapes read from the Go API, hand-kept
against the reference. The screener domain types sit
beside them because in legacy they were exported from the screener page,
so getting a type meant importing a page component.
SWR everywhere
No server components fetching, no React Query. Every panel polls or revalidates on focus, which is the behaviour you want from a console watching a job that is still running.
Layout
| Directory | What is in it |
|---|---|
src/app | App Router routes |
src/types | Response shapes read from the Go API, and the screener domain types |
src/server | Server-only helpers |
src/components, src/hooks, src/contexts | App-local UI |
Environment
apps/statshub-admin/.env.example lists what it needs. Like web, it starts without
those values and fails at request time rather than at boot.
It reads whatever the API points at
API_ORIGIN decides which deployment admin is administering, and it defaults to
http://127.0.0.1:8080. Start the API against the local Postgres
(bun run db:dev:up, then bun run dev:api) before trying anything
destructive.