Superseded the old login-only TODO: OAuth login, manager-token tier lookup, D1-backed vote casting, the navbar auth dropdown, and the /vote UI are all built and verified against a real guild.host login. What's left is deploy + a couple of known caveats.
HANDOVER.mdis deleted — this file plusDEVELOPMENT.mdare now the source of truth.
- Confirm
EVENT_SLUGinsrc/server/configs/oauth.tsmatches the real event (vdc8dhtoday). npm run db:init:remote— appliesresources/schema.sqlto prod D1 (idempotent).- Seed prod
ticket_tiers(tier name must match guild.host exactly, case-sensitive):Any tier NOT listed here defaults to budget 1 (seeINSERT INTO ticket_tiers (name, budget) VALUES ('Super Early Bird', 2), ('Early Bird', 1);
budgetForTierinsrc/server/repositories/vote.ts) — only add rows for overrides. npm run secret(wrangler secret put) for:GUILD_OAUTH_CLIENT_ID,GUILD_OAUTH_CLIENT_SECRET,SESSION_SECRET,GUILD_ORG_REFRESH_TOKEN.ALLOWED_ORIGIN=https://jsconf.com.br(never*— credentialed cookies need an explicit origin).- Confirm the guild.host OAuth app has
https://api.jsconf.com.br/api/vote/callbackregistered as a redirect URI (in addition to the localhost one used for dev). - Bootstrap
GUILD_ORG_REFRESH_TOKEN: the capture path is gated onenv.ENVIRONMENT === 'development'(fail closed — it is never reachable in prod), so run it from a development environment: localwrangler devwithENVIRONMENT=development, or a throwaway dev deploy. Hit/api/vote/login?manager=1, log in with the org account, and copy the printed refresh token into the prod secret. The token is org-scoped, so where you capture it doesn't matter. After first use, D1 (manager_oauthtable) keeps itself current — guild rotates the refresh token on every use, andmanagerAccessToken()writes the new one back automatically. - Delete the dev-only
?manager=1capture branch inauthLogin/authCallback(src/server/routes/auth.ts) once the prod token is bootstrapped — already inert in production, but worth removing as cleanup. - Merge
voting-system→main. CD (.github/workflows/cd_deploy.yml) builds the worker (tools/prepare-worker.mtsinjects the real D1database_idfrom theWORKER_D1secret) and deploys both the worker and the static site. - Do one real login against prod post-deploy — confirm guild consent → callback → tier resolution → session → vote cast, end to end — before announcing voting is open.
- Budget is baked into the session JWT at login time. Changing a tier's
budgetinticket_tiersdoes NOT retroactively update anyone already logged in — they need to log out and back in. Hit this directly during testing. - Multiple tickets per user isn't handled as "best tier wins."
fetchTicketTier(src/server/helpers/oauth.ts) returns the tier from the FIRST matching attendee node in guild's paginated list, not an aggregate/max. Untested in practice (no real multi-ticket account seen yet) — if it comes up, decide whether to prefer the highest-budget tier instead. - Rate limiter shares one bucket in local dev.
checkRateLimitkeys onCF-Connecting-IP/X-Forwarded-For, absent locally, so ALL local requests (yours + any curl testing) share a single'unknown'bucket (10 req/60s). Not a prod issue — Cloudflare always setsCF-Connecting-IPthere.
Add a votable talk:
INSERT INTO speakers (name, email, phone, city, state, travel_pref, experience, bio)
VALUES (...);
INSERT INTO talks (speaker_id, duration, title, description, audience_level, reason, status)
VALUES (<speaker_id>, <0|1>, '<title>', '<description>', <0-3>, '<reason>', 2);status = 2 is what makes a talk appear on /vote — nothing else to touch.
Hide a talk without losing its votes: UPDATE talks SET status = <anything but 2> WHERE id = <id>;
Change a tier's budget: UPDATE ticket_tiers SET budget = <n> WHERE name = '<exact tier name>';
(remember the JWT caveat above — affected users need to re-login).
Change the voting deadline: edit VOTE_CLOSES_AT in src/server/configs/vote.ts (hardcoded,
requires a deploy — deliberate, since it changes rarely).