MMA LiveSim
Free UFC DFS optimizer and MMA fight simulator for DraftKings. Live win probabilities for every lineup, plus post-contest EV, ownership, and optimal lineups.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Vertical Meta-glasses volleyball clips, browsable by day and game
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Volleyball clips from Meta glasses for Focus (vbpov.com). Public visitors pick a night, open a session grid, then swipe a TikTok-style 9:16 feed. Signed-in viewers can favorite clips and download. Pro ($5/month, Stripe test mode) gets unlimited original-quality downloads. Public Auth0 sign-ups (email/password + Google) are enabled; only users with the Auth0 admin role on the claim https://vbpov.com/roles can manage venues, tags, games, and uploads.
?ym=YYYY-MM&d=YYYY-MM-DD) lists that day’s sessions (venue, time, type: open gym, league, tournament)./games/{id}/watch/{clipId}. Tag filter is respected. The first clip autoplays muted; tap the sound control (or the video) to unmute. One persistent player element stays mounted and keeps that unlock across swipes (iOS Safari treats unmuted play as per-element). The sound icon follows video.muted. Neighbors preload muted. Admins can set the session pin and play-type tags from a sheet on this screen. Close returns to the grid.users is keyed by Auth0 sub./favorites lists them.vb_vid cookie plus a hashed IP backstop. Already-watched clips replay. Signed-in users and admins are not limited.playback/ copy.uploads/, ~1400×1900 at 120 fps).current_period_end after cancel. Missing Stripe env vars degrade: the pricing page still renders, checkout/portal/webhooks return 503.| Who | Watch | Download |
|---|---|---|
| Guest | 7 unique clips / day | none |
| Free account | unlimited | 3 / rolling week, 1080p |
| Pro | unlimited | unlimited originals |
| Admin | unlimited | unlimited originals |
Hallmark is used for craft only (tokens, type scale, spacing, focus, reduced motion). The look is original: a dark, loud, mobile-first night court. Inspo is the visual source; nothing is cloned, and the site is not meant to resemble Hassan’s own work.
Tokens live in src/app/tokens.css. Display is Tektur, body is Onest. Paper is a red-tinted night (oklch(16% 0.018 18)), accent is electric red used as a fill on actions, spark lime is reserved for pins and Pro.
phone / laptop
│
├─ public Next.js pages ── Neon (venues, games/sessions, clips, tags, pins, users, favorites, watch/download events)
│
├─ Auth0 (public sign-up) ── users.auth0_sub
│ admin role via https://vbpov.com/roles
│
├─ Stripe Checkout + portal + webhooks (TEST)
│
└─ admin (isAdmin only)
│
├─ CRUD APIs ── Neon (venues, tags, games, clip tags, pins)
├─ PATCH /api/admin/games/:id ── venue, starts_at, type
├─ PUT /api/admin/games/:id/pins ── up to 3 pins
├─ DELETE /api/admin/games/:id ── S3 objects first, then game + clips + joins
├─ DELETE /api/admin/clips/:id ── S3 objects first, then clip + joins
└─ POST /api/admin/clips/presign
│
└─ browser PUT ── S3 uploads/{gameId}/{clipId}/…
│
├─ Lambda + ffmpeg layer
│ writes playback/ and posters/
│ POST /api/webhooks/transcode (HMAC)
│
└─ CloudFront (OAC)
posters/* public (Open Graph cards)
playback/* require a CloudFront signed URL when lockPlayback=true
uploads/* require a CloudFront signed URL when lockUploads=true
(same trusted key group; Pro originals and free 1080p
downloads proxy through the app)
src/lib/db/schema.ts.@auth0/nextjs-auth0 v4. Public sign-ups are enabled. Admin access is the Auth0 admin role, delivered on the session as user["https://vbpov.com/roles"] (see Auth0 below).project=volleyball-clips): private S3 bucket, CloudFront with origin access control, transcode Lambda, IAM role for Vercel OIDC.S3 keys:
| Prefix | Purpose |
|---|---|
uploads/{gameId}/{clipId}/… | Original glasses file (Pro download) |
playback/{gameId}/{clipId}/playback.mp4 | 1080-wide H.264 ~5 Mbps / 30 fps, faststart (feed + free download) |
posters/{gameId}/{clipId}/poster.jpg | Still for the grid and player |
The watch page and public APIs never send originalKey or any uploads/ URL. Free downloads never serve uploads/. Pro originals and free 1080p downloads are fetched server-side with a short-lived CloudFront signed URL (CLOUDFRONT_KEY_PAIR_ID + CLOUDFRONT_PRIVATE_KEY) and streamed from /api/clips/:id/download as an attachment.
Feed playback uses a per-clip signed URL (2 hour TTL, refreshed ~10 minutes before expiry), not signed cookies. The CDN is the CloudFront default host (*.cloudfront.net), so the app cannot set cookies for that domain from vbpov.com. POST /api/watch issues the URL only after it allows the view (anonymous 7-a-day cap still applies). Signed-in viewers get start+neighbor URLs on the first HTML response so autoplay and preload stay snappy. When signing env is unset, the app falls back to public cdnUrl() so you can ship the app before lockPlayback=true. Posters stay public. The transcode Lambda reads originals with S3 GetObject, not CloudFront.
Vercel talks to AWS with @vercel/functions/oidc awsCredentialsProvider. The federated role volleyball-clips-vercel-presign can:
s3:PutObject / abort or list multipart uploads on uploads/* (presign)s3:DeleteObject on uploads/*, playback/*, and posters/*It still cannot read objects, list the bucket, or touch Glacier.
Configured on VolleyballClipsStack (not applied until you run cdk deploy):
| Prefix | Storage | Lifecycle |
|---|---|---|
uploads/ | Intelligent-Tiering | Transition at day 0 |
playback/ | Intelligent-Tiering | Transition at day 0 |
posters/ | Standard | No transition (leave as-is) |
| (bucket-wide) | — | Abort incomplete multipart uploads after 7 days |
Intelligent-Tiering Frequent/Infrequent Access stays instantly readable. The stack does not enable archive tiers or Glacier.
Only a session whose https://vbpov.com/roles claim includes admin is authorized (isAdmin in src/lib/roles.ts, used by requireAdminApi / requireAdminPage and src/proxy.ts). Signed-out visitors are sent to login. Signed-in non-admins get 403 on /api/admin/* and the /not-authorized page for /admin. The transcode webhook is unchanged (HMAC, not Auth0).
/games/[id] (admin panel) or /admin/games/[id]/edit: venue from the saved list, date/time, and type. PATCH /api/admin/games/:id.DELETE /api/admin/clips/:id.DELETE /api/admin/games/:id.isAdmin(sessionOrUser) reads user["https://vbpov.com/roles"] and returns true only when that list includes admin. It accepts a session ({ user }) or the user object. Reuse it everywhere — there is no email allowlist. requireAdminPage / requireAdminApi and src/proxy.ts send signed-in non-admins to /not-authorized (or 403 on APIs). The v2 feed tag sheet uses the same check via getViewer().isAdmin.
Add a Post-Login Action on tenant vb-pov.us.auth0.com so roles reach the ID token (v4 drops namespaced claims unless beforeSessionSaved copies them; the app already does that):
exports.onExecutePostLogin = async (event, api) => {
const namespace = "https://vbpov.com";
if (event.authorization?.roles) {
api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
}
};
Create the admin role, assign it to Focus, enable Database + Google sign-up. Do not hardcode Facebook out; it can be added on the tenant later.
users is upserted from the Auth0 sub on first authenticated request.
pnpm install
cp .env.example .env.local
# fill Neon (a branch for schema work) + Auth0 + stack outputs + Stripe test keys
pnpm db:migrate # DATABASE_URL_UNPOOLED, never production when testing
pnpm db:seed
pnpm dev
pnpm db:migrate must use the unpooled Neon URL (DATABASE_URL_UNPOOLED). The app uses the pooled DATABASE_URL. Do not apply new migrations to the production database from an agent. Create a Neon branch, migrate that, then apply production yourself when you are ready.
The first migration also inserts the seed tags; pnpm db:seed is idempotent.
Schema changes: edit src/lib/db/schema.ts, pnpm db:generate, commit drizzle/.
| Variable | Where it comes from |
|---|---|
DATABASE_URL | Neon pooled connection string |
DATABASE_URL_UNPOOLED | Neon direct connection string (migrations / seed) |
AUTH0_SECRET | openssl rand -hex 32 |
APP_BASE_URL | http://localhost:3000 locally; https://vbpov.com in production |
AUTH0_DOMAIN | Auth0 tenant host, no https:// (vb-pov.us.auth0.com) |
AUTH0_CLIENT_ID | Auth0 Regular Web Application |
AUTH0_CLIENT_SECRET | Auth0 Regular Web Application |
APP_TIMEZONE | Optional. Defaults to America/Chicago |
ANON_HASH_SECRET | Optional HMAC key for the watch IP backstop. Falls back to AUTH0_SECRET |
TRANSCODE_WEBHOOK_SECRET | Decrypted SSM /volleyball-clips/transcode-webhook-secret |
CLOUDFRONT_PRIVATE_KEY | Matching private key for CLOUDFRONT_KEY_PAIR_ID. PEM (newlines or \n) or base64. Never commit it |
STRIPE_SECRET_KEY | Stripe test secret (sk_test_…). Optional; app degrades without it |
STRIPE_WEBHOOK_SECRET | Stripe webhook signing secret. Optional |
STRIPE_PRICE_ID | Recurring $5/month test price. Optional |
STRIPE_PORTAL_CONFIG_ID | Optional Customer Portal configuration id (bpc_…). Passed to billingPortal.sessions.create when set |
VolleyballClipsStack outputs| Output / env var | Used for |
|---|---|
AWS_REGION | Always us-east-2 |
AWS_S3_BUCKET | Bucket name for presigned PUTs |
AWS_ROLE_ARN | Vercel OIDC role |
NEXT_PUBLIC_CDN_URL | https://<cloudfront-domain> for playback and posters |
CLOUDFRONT_KEY_PAIR_ID | CloudFront public key id (trusted key group). Required before locking uploads/* or playback/* |
TRANSCODE_WEBHOOK_SECRET_PARAMETER | Name of the SSM SecureString |
Set CLOUDFRONT_PRIVATE_KEY yourself (PEM with real newlines or \n, or base64 of the PEM). Never put it in the repo or in CDK context. The app works without these vars while those prefixes are still public; after a CDK lock, unsigned objects 403 until the vars are set.
Local uploads can use AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY instead of OIDC. Do not set those on Vercel.
Create a Stripe test secret key.
pnpm stripe:setup creates product VB POV Pro and a $5/month price, then prints STRIPE_PRICE_ID.
Or in the Dashboard: Product → recurring $5 USD / month → copy the price id.
Add a webhook endpoint https://<app-host>/api/webhooks/stripe (local: Stripe CLI stripe listen --forward-to localhost:3000/api/webhooks/stripe) for:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paidinvoice.payment_failedSet STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_ID.
Enable the Customer Portal in Stripe for cancel and card updates. Optional: create a portal configuration and set STRIPE_PORTAL_CONFIG_ID so the portal session uses it. Leave it unset to use Stripe’s default.
One more thing: if you will charge US or EU customers later, consider Stripe Tax for recurring payments. This app does not enable automatic_tax.
us-east-2 is fine).DATABASE_URL and DATABASE_URL_UNPOOLED.pnpm db:migrate then optionally pnpm db:seed.The live app uses a dedicated Auth0 tenant (vb-pov.us.auth0.com) with public sign-ups (email/password + Google). Swapping tenant keys in Vercel is enough; the app has no extra Auth0 env vars.
vb-pov.us.auth0.com.http://localhost:3000/auth/callback and https://<app-host>/auth/callback.http://localhost:3000 and https://<app-host>.admin. Assign it only to users who should upload or manage clips. There is no email allowlist in the app.https://vbpov.com/roles. The tenant going live already has this Action; a new tenant must have the same Action or every signed-in user will look like a non-admin.AUTH0_SECRET into env.The app treats someone as an admin only when isAdmin(session) is true — that is, session.user["https://vbpov.com/roles"] includes "admin". @auth0/nextjs-auth0 v4 persists only a default claim subset, so beforeSessionSaved in src/lib/auth0.ts keeps this roles claim on the session user (filterDefaultIdTokenClaims plus an ID-token fallback).
v4 routes are /auth/login, /auth/callback, /auth/logout. Sign-up: /auth/login?screen_hint=signup.
Do not deploy this stack from a coding agent. Focus’s bot deploys AWS. From a machine with AWS credentials:
cd infra
pnpm install
./scripts/prepare-ffmpeg-layer.sh
npx cdk deploy \
-c webhookUrl=https://<app-host>/api/webhooks/transcode \
-c allowedOrigins=http://localhost:3000,https://<app-host> \
-c cloudfrontPublicKeyPath=./keys/cloudfront-public.pem \
-c lockUploads=true \
-c lockPlayback=false
Useful context flags: webhookUrl, allowedOrigins, vercelTeamSlug (focus-otters-projects), vercelProjectName (volleyball-clips), vercelOidcProviderArn, cloudfrontPublicKeyPath or cloudfrontPublicKeyPem, lockUploads (true locks unsigned uploads/*), lockPlayback (true locks unsigned playback/*; default false so the app can ship first). Posters stay on the default behavior and remain public.
Generate the signing keypair on the deploy machine (private key never enters git):
openssl genrsa -out cloudfront-private.pem 2048
openssl rsa -pubout -in cloudfront-private.pem -out cloudfront-public.pem
The stack is VolleyballClipsStack in us-east-2. Every resource is tagged project=volleyball-clips.
After deploy, set AWS_REGION, AWS_S3_BUCKET, AWS_ROLE_ARN, NEXT_PUBLIC_CDN_URL, CLOUDFRONT_KEY_PAIR_ID, CLOUDFRONT_PRIVATE_KEY, and TRANSCODE_WEBHOOK_SECRET on Vercel (preview and production). Then deploy the app, then cdk deploy again with -c lockUploads=true and, when ready, -c lockPlayback=true.
Lockdown check (preview or production):
pnpm exec playwright install chromium
pnpm verify:lockdown -- \
--base-url https://<app-host> \
--cdn-url https://<cloudfront-domain> \
--original-key uploads/<gameId>/<clipId>/<file>.mp4 \
--playback-key playback/<gameId>/<clipId>/playback.mp4 \
--free-cookie '<Cookie header>' \
--pro-cookie '<Cookie header>'
While lockPlayback is still false, set VERIFY_EXPECT_PLAYBACK_UNLOCKED=1. For a Vercel preview behind OIDC, set VERIFY_OIDC_TOKEN. For a signed-in feed, set VERIFY_FEED_COOKIE. The Playwright pass uses domcontentloaded / load (never networkidle) and ignores net::ERR_ABORTED from source switches.
The Lambda is 10 GB RAM, 15 minute timeout, 5 GB ephemeral disk, Node.js 22 + ffmpeg layer. Output is 1080-wide H.264 at ~5 Mbps / 30 fps with +faststart, plus a poster JPG.
SECRET="$(openssl rand -hex 32)"
aws ssm put-parameter \
--name /volleyball-clips/transcode-webhook-secret \
--description "HMAC secret for volleyball-clips transcode webhook" \
--type SecureString \
--value "$SECRET" \
--region us-east-2
The stack output TRANSCODE_WEBHOOK_SECRET_PARAMETER is that name. Do not put the decrypted secret in CloudFormation.
pnpm install, build pnpm build.focus-otters-projects.AWS_REGION=us-east-2.allowedOrigins on the stack.The root layout loads @vercel/analytics and @vercel/speed-insights.
Turn them on in the Vercel dashboard (Hobby can do this):
This project stays on Hobby. Page views work on Hobby. Custom track() events only appear on the Pro plan (Analytics → custom events). The calls are already in the app so they light up if the plan changes. They are fire-and-forget: nothing in the product waits on them, and a failed or ignored call on Hobby does not error.
| Event | When | Where |
|---|---|---|
clip_view | Active clip in the feed changes (throttled). Props: clip_id, game_id | Client |
favorite | Heart succeeds. Prop: clip_id | Client |
download_success | A new row is written to download_events. Props: quality, tier | Server |
download_limit_hit | Free weekly cap, from the download button | Client |
watch_limit_hit | Guest hits the 7-a-day wall | Server |
signup_click | Create-account / sign-in-to-favorite / sign-in-to-download | Client |
sign_in_click | Sign in | Client |
go_pro_click | Pricing links and Pay $5 | Client |
checkout_started | Stripe Checkout URL is in hand | Client |
checkout_success | /pricing?checkout=success | Server |
Hobby source of truth (already in Neon, no Vercel plan needed):
watch_events — guest clip views and the 7-a-day walldownload_events — downloads (quality is playback or original)users.subscription_status (plus current_period_end) — Pro / checkout state from Stripe webhooksFacebook gym links should keep ?utm_source=facebook&utm_campaign=<gym> on the first page. Auth returnTo and in-feed URL updates copy utm_* so a later sign-in does not drop them. Vercel records those query params on the landing page view. UTM breakdowns in the dashboard need Web Analytics Plus; the raw landing URL still has them.
Share previews: metadataBase is https://vbpov.com. Session and clip pages expose Open Graph + Twitter tags (and a 1200×630 opengraph-image) without auth. The 7-a-day wall is client-side after /api/watch; Facebook’s crawler reads the HTML head.
On a phone, the admin session page’s file input opens the camera roll (accept="video/*", multiple). Files upload in parallel (3 at a time). Tag and pin clips on the same page.
Public visitors only see clips in ready.