StatsHub Docs

statshub-config

What every board shows, what it starts as, and who may see it — as JSON both apps read.

Two questions, one package:

  • Does this exist, and who may see it?app.json and each feature's entries.
  • And what does it look like? — each feature's filters, table and card.

Plain JSON, so an admin screen can change a starting odds range or a default column without a deploy, and so web and native read one file rather than each keeping a list that drifts.

Layout

app.json
defaults.json
screener.json
value-bets-v2.json
value-bets-v3.json
player-trends.json
index.ts

index.ts imports every file statically. That is not tidiness — Metro cannot resolve a require() built at run time, so a feature added here has to be named there too.

defaults.json — what is true until a feature disagrees

KeyHolds
paginationRows per request, mobile and desktop
dataSWR dedupe window, poll interval, focus revalidation, retries
oddsThe slider's absolute range, step and decimals
hitRateThe windows offered (L5/L10/L15) and which is default
limitsCeilings — saved views, folders, search results shown, chips in a rail
brandName, domain, social handle, support URL
complianceResponsible-gambling marks and the minimum age

odds here is the slider's floor, not a board's starting point

They differ, and conflating them is a bug that has already shipped: the slider runs from 1.01, Prop Screener starts at 1.30, and a control comparing its value against its own minimum read that gap as a filter the reader had applied — so an untouched board reported "Clear 1". A board's zero point belongs in its own filters.defaults.

features/<id>.json — one board

{
  "entries": [  ],           // subroutes: state, access, platform paths

  "filters": {
    "defaults": {  },        // the board's ZERO POINT
    "important": ["league"],  // which also appear on the bar
    "groups": [               // panel order, and what is in each group
      { "label": "Leagues", "filters": ["league", "selectedLeagueOnly"] }
    ]
  },

  "table": {
    "sort": { "column": "L10", "direction": "desc" },
    "columns": [ { "id": "player", "label": "Player", "visible": true } ],
    "pageSize": { "mobile": 100, "desktop": 250 }   // overrides the global pair
  },

  "card": {
    "rows": [["player", "team"], ["market", "line", "odds"]]
  }
}

Three things the shape encodes:

  • Order is array order — for entries, groups, columns and card.rows. Move an item to move it in the UI. There is no order field to keep in sync.
  • visible: false ships a column but leaves it off, so the column menu has something to offer. Deleting the entry removes it entirely.
  • card.rows is the mobile rendering of one row — each array is a line, each id a field on that line, left to right. A field in no row is not shown on mobile at all.
  • A group's label resolves its icon, through the same table the filter controls use — so renaming a group changes its glyph. See icons and imagery.

Reading it

import {
  appDefaults,
  featureColumns,
  featureFilterDefaults,
  featurePageSize,
} from "statshub-config";

featurePageSize("screener", isMobile); // board override, else the global pair
featureColumns("screener", true); // only the columns on by default
featureFilterDefaults("screener").oddsRange; // { from: 1.3, to: 10 }
appDefaults().hitRate.windows; // ["L5", "L10", "L15"]

featurePageSize takes the breakpoint rather than reading it, so the caller's own useIsMobile stays the single source of that answer.

What is not in here, and why

  • Copy. Labels are translated; they belong in public/locales. A label in this package is an id a developer reads, not a string a user sees.
  • Secrets and endpoints. Environment, not configuration — this file is readable by the client.
  • Anything derived. If it can be computed from the data, compute it. A config value that duplicates a query result is a value that goes stale.

Still hardcoded

The schema covers the whole shape, but only Prop Screener's page size reads from it today. Migrating a board is: move its DEFAULT_FILTERS into filters.defaults, its column list into table.columns, and its mobile card fields into card.rows, then delete the constants. Do one board at a time and check the board still starts where it did.

On this page