Courtside Analytics
NBA team, player and schedule analytics for front-office decisions, refreshed from nba_api.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗A live-updating college football rankings site for you and your friends.
Everyone builds their own Top 25 ballot by dragging team helmets into place, and all ballots are averaged into the combined TNT Ranking — recalculated live the moment anyone changes their ballot.
TNT #3 ▲8 = we have them eight spots higher).
The 2026 preseason Coaches Poll is loaded; the AP preseason poll isn't out
until Aug 17, so it says so until you add it. These polls are typed in by
hand, not scraped — add a week by dropping an entry into
public/polls.js (the format is documented at the top of the file).public/heisman.js and
is hand-entered, so it's easy to update). Your picks stay
completely private — nobody can see anyone else's until they all lock the
moment the first game kicks off (Aug 29, 2026). Until then a live countdown
shows how long is left and who has already locked in (names only, never their
picks); once the clock hits zero everyone's predictions are revealed side by
side.npm install
npm start
Then open http://localhost:3000. Set PORT to change the port.
Locally, everything is stored in data/store.json (created automatically), so
back that file up if you care about your poll history.
The site is built to run on Vercel: public/ is served as static files and
each file in api/ becomes a serverless function. Pushing to the connected
branch deploys it.
One setup step is required, once. Serverless functions have no disk, so
data/store.json has nowhere to live. Worse than losing data on redeploy:
Vercel runs several function instances, each with its own memory, so without a
shared database different people can be served different ballots. Pick either
option below — the app detects whichever one you configure.
Needs no account beyond the GitHub one this repo already lives in, and has nothing to pay for or run out of.
tnt-store.json containing {}. The gist's ID is the last part of its URL.gist scope ticked.GIST_ID and
GIST_TOKEN.Every save writes a gist revision, so its history grows one entry per ballot
change — harmless, just busy-looking. Optional: GIST_FILE to use a filename
other than tnt-store.json.
FIREBASE_DB_URL to the Vercel project's environment variables
(e.g. https://your-project-default-rtdb.firebaseio.com), plus one of:
FIREBASE_DB_SECRET — Project Settings → Service Accounts → Database
secrets. One value, quickest to set up.FIREBASE_SERVICE_ACCOUNT — the whole service-account JSON, pasted in.
This is Google's current supported path; the app signs a JWT with it and
exchanges that for a short-lived access token, cached until it expires.".read": false,
".write": false). Only the server talks to Firebase, and it authenticates,
so no public access is needed — see the note below.Optional: FIREBASE_DB_PATH to store somewhere other than tnt/store.
Either connect one through the Vercel dashboard (Storage → Create
Database → Redis), which injects KV_REST_API_URL and KV_REST_API_TOKEN
for you, or make a database at https://upstash.com directly and paste those
two values in yourself. Then redeploy. The app can't tell the difference.
If something doesn't save, open /api/health on the deployed site. It
reports which backend is actually in use, whether it can be read, and the
current revision — no credentials, just the diagnosis. backend: "memory"
means none of the variables below are reaching the app. Add ?write=1 to
round-trip a write and prove it survives.
Set exactly one of the three. They're checked Firebase → Redis → Gist, so a half-finished migration resolves to a single store rather than quietly splitting the data across two. To confirm it took, check the deployment logs — with none configured the app still runs but logs a loud warning that data is being held in memory.
Why the browser never talks to the database directly. It would be tempting to use a Firebase client SDK and get real-time push for free. But the predictions rule — nobody sees anyone else's picks until the Aug 29 lock — is enforced server-side in
lib/core.js, and it only holds because reads go through the server, which sends other people's picks to nobody. Pointing the client straight at Firebase would put every prediction in the browser and leave security rules as the only thing standing between them, which this app can't write meaningfully because it has no per-user authentication.
npm start runs the same code against data/store.json, so any ordinary
Node host (Render, Railway, Fly.io, a VPS, a Raspberry Pi) works too — just
put data/ on a persistent disk. No Redis needed there.
public/teams.js — add or recolor teams there.api/sync (the single read the
client polls) plus api/ballot and api/predictions for writes. All the
actual rules live in lib/core.js, which both the serverless functions and
the local Express server call into, so there's one implementation regardless
of where it's running.lib/store.js picks its backend automatically — Firebase
Realtime Database, Redis, or a GitHub gist when credentials are present
(Vercel), a JSON file otherwise (local), memory as a last resort. The whole
store is a single JSON document, so each backend is just a get and a put;
adding another one means implementing those two functions. Saving is a
read-modify-write, so writes are conditional: each one only lands if the
document is still what was read, and store.update() re-reads and retries
when it isn't. Without that, two people saving in the same moment would each
write back a document based on what they read before the other's save, and
the later write would silently erase the earlier ballot — while telling both
of them "Saved". Redis does the check with a small EVAL script and Firebase
with an ETag; gists have no conditional update, so there they're only
serialised within one instance.public/net.js polls and presents the same on/emit
interface the app was originally written against, so the app code doesn't
know or care that there's no socket underneath. Polling is deliberate:
serverless functions can't push to other people's browsers, which is exactly
what a shared ranking needs. The loop is adaptive — every 3s while something
is happening or you're touching the page, backing off to 15s when the page
is just sitting there, and stopping entirely on a hidden tab. Everything the
loop needs comes from a single /api/sync call so one tick costs one
function invocation and one storage read. That matters: polling is the
app's steady-state traffic, and a Redis free tier is priced per command.lib/core.js. Before the
lock, other people's picks are never sent to your browser at all, so there's
nothing to find in dev tools.