BikeControl (SwiftControl)
Zwift Click과 Shimano 컨트롤러를 MyWhoosh와 Rouvy와 Zwift에서 쓴다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 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.