What's new
Changelog
Every release, every fix, every model tune-up. Currently running v11.9.26 (f4693ba). Full source on GitHub.
v1.3.15
Cleaner Stats page — no more stuck 62% parlay dials
v1.3.14
Transparency report card now shows *your* account only
v1.3.13
Slip cards now show your money — stake, payout, and every leg
Fixed
- P&L / bankroll / leaderboard were stale. All fantasy P&L reads flow through the
fantasy_ledgermirror of your graded slips, and its settle trigger had frozen each slip at its first (often provisional) grade — so re-grades from a live game never updated your record. Corrected the trigger to re-settle on re-grade and reconciled 85 stale rows. Your record is now 74-268 with +$1,880 lifetime P&L and a $380 biggest hit, matching the true graded results behind the transparency report. Backend postmortem:docs/incidents/2026-07-12-fantasy-ledger-regrade-freeze.md.
Added
- Account slip cards surface
Bet,Won/Lost/To win, and a per-leg hit/miss/push/pending breakdown, all sourced from the settled ledger row.
- P&L / bankroll / leaderboard were stale. All fantasy P&L reads flow through the
v1.3.12
Meet The GM's Read™ — the head coach behind every pick
Added
- New
/methodologypage surfacing all seven proprietary models — The GM's Read™ plus SSI™, Jet Lag Index™, Gas Tank Index™, Mental Load™, Mind Lock-in Score™, and BURST™ — with a front-office explainer of how they work together.
Changed
- Home hero, parlays fine print, subscribe blurb, and site meta all rebranded around The GM's Read™ with consistent unicode ™ treatment. Copy-only; no changes to picks, scoring, or grading.
- New
v1.3.11
Version-sync guard + deploy-gate regex tightening
v1.3.10
Fixed
- Hit-rate dials now show accurate numbers. Investigation revealed the underlying grading table had duplicate rows per pick (up to 22× for one Arike Ogunbowale line on 2026-07-10), inflating all hit-rate denominators. Backend now dedups within-platform on
(player, market_norm, line, direction, game_date, platform). Track record, model transparency confidence dials, and Dial Truth surfaces now reflect real per-pick-event hit-rates. Seedocs/postmortems/2026-07-11-pick-grades-duplicates.md.
Changed
/api/versionnow reports 11.2.4.
- Hit-rate dials now show accurate numbers. Investigation revealed the underlying grading table had duplicate rows per pick (up to 22× for one Arike Ogunbowale line on 2026-07-10), inflating all hit-rate denominators. Backend now dedups within-platform on
v1.3.9
Added
- Six new Supabase calibration views are available for the web app to read. Backend-only in this release — no UI wired yet. When the model-transparency or
/track-recordsurfaces add a by-market or platform-crosstab dial, they can source from these views: -v_hit_rate_by_market_7d— leg hit-rate by market shape (points, rebounds, ...) -v_hit_rate_by_platform_market_7d— platform × market crosstab -v_hit_rate_by_confidence_7d— confidence-decile calibration -v_hit_rate_by_model_version_30d— per-model-version regression detection -v_hit_rate_daily_30d— daily time series -v_hit_rate_platform_wide_v1— single-row 7d/30d/all summary dial
Fixed
/api/versionnow correctly reports 11.2.3 matching the backend release. The v11.2.3 backend release PR only bumped the repo-rootpackage.json; because the version endpoint importsweb/package.json, the deployed app kept reporting 11.2.2 until this hotfix. Seedocs/CHANGELOG.mdv11.2.3.
- Six new Supabase calibration views are available for the web app to read. Backend-only in this release — no UI wired yet. When the model-transparency or
v1.3.8
Added
/track-recordnow shows a Sportsbook Calibration (7d) section. A new public block below the platform/tier breakdown renders per-platform leg hit-rate, W-L-P, and coverage % for DK / FD / PP / UD, sourced fromv_platform_leg_hit_rate_7d. Coverage badges use a traffic-light (≥80% green, 75–80% amber, <75% red) so ingest gaps surface immediately. New backend endpoint:GET /api/data?action=platform-calibration-7d(5-minute Cache-Control; refreshes only after the grader runs).
v1.3.7
Added
- New Supabase view
v_platform_leg_hit_rate_7dis now available for the web app to read. Not yet wired into any UI; when the dial strip on/track-recordadds a leg-level view, it can source from this view. Backend-only in this release. Seedocs/CHANGELOG.mdv11.2.2 for full detail.
Changed
- No user-visible changes in this release. All work is in backend analytics and CI hardening. See root
docs/CHANGELOG.mdv11.2.2.
- New Supabase view
v1.3.6
Fixed
- Public model-transparency dials now read clean data. The
/track-recordand platform dial strips (7d/30d/season) were misrepresenting model performance because the underlying views (v_slip_hit_rate_by_leg_count_v1,v_user_slip_hit_rate_7d,v_slip_hit_rate_7d) aggregated across 284,344 synthetic Mode 2 rows co-mingled with 333 real user slips inuser_slips. All synthetic rows were moved to a dedicatedsynthetic_pregame_snapshotstable; the views now filter to the real-user allow-listsource IN ('parlay_hub','auto_generated') OR source IS NULL. Post-fix hit rates verifiedbeating_vigacross sizes 2–5 (2-leg 44.4%, 3-leg 21.6%, 4-leg 13.9%, 5-leg 5.9%). No web app source changed; this line documents the DB-side change that fixed the dials the app renders. Seedocs/incidents/2026-07-10-synthetic-pregame-dial-contamination.md.
- Public model-transparency dials now read clean data. The
v1.3.5
Added
- 2-leg parlay tracking end-to-end. The
user_slips.slip_sizecheck constraint has been relaxed fromBETWEEN 3 AND 5toBETWEEN 2 AND 5(migration20260707_relax_slip_size_min.sql).TICKET_BENCHMARKSand thev_slip_hit_rate_by_leg_count_v1view already published a 2-leg break-even (0.275 = 0.524^2); those rows will now actually get populated instead of reportinginsufficient_sampleforever. - ParlayHub size picker now includes a
2button.slipSizestate widened to2 | 3 | 4 | 5. - Auto-Build (
buildAutoParlays) defaultlegCountswidened to[2, 3, 4, 5], so the model-driven top-parlays surface offers 2-leg suggestions alongside 3/4/5. - Auto-capture cron (
/api/admin/auto-capture-parlays) now iterates[2, 3, 4, 5]. The Underdog payout tier ladder was extended with 2-leg =3x(matches UD's standard 2-pick payout). - Drift alert scan (
/api/admin/parlay-drift-alert) now iterates[2, 3, 4, 5]so 2-leg streaks below benchmark also trigger Slack alerts.
Notes
- Max slip size stays at 5 —
TICKET_BENCHMARKSinweb/lib/api.tshas no 6-leg row published, so 6-leg tracking would showinsufficient_sampleforever. Thefantasy_ledgerpayout CASE already supports slip_size=6 (35x), but no consumer benchmark exists for it yet. - The migration is reversible — see the ROLLBACK comment block at the bottom of
20260707_relax_slip_size_min.sql.
- 2-leg parlay tracking end-to-end. The
v1.3.4
Added
/how-we-measuretransparency page — documents the 52.4% break-even at -110, why 3/4/5-leg parlays compound the bar (0.524^N), and renders live SGG performance per leg count with pass/fail classification against the benchmark. Linked from the badge sections on/parlaysand/stats./api/data?action=hit-rate-by-leg-count— read-only endpoint that surfacesv_slip_hit_rate_by_leg_count_v1for the transparency page. 5-minute Cache-Control.v_slip_hit_rate_by_leg_count_v1SQL view (migration20260706_hit_rate_by_leg_count.sql) — rolls up gradeduser_slipsinto per-leg-count hit rates over 7d / 30d / season windows joined against theTICKET_BENCHMARKSreference (2:0.275, 3:0.144, 4:0.075, 5:0.039), with aclassificationcolumn:beating_vig/watch/below_benchmark/insufficient_sample. Buckets with fewer than 5 graded slips returninsufficient_sampleto suppress early-season noise./api/admin/parlay-drift-alert(rootapi/admin/*.js) — Monday scheduled scan that compares this-week vs last-week hit rate per leg count and posts a Slack message when any bucket sits below its benchmark for two consecutive weeks. MissingSLACK_WEBHOOK_URLfalls back toconsole.error(mirrorsscripts/prod-drift-monitor.js). Auth viaDASHBOARD_ADMIN_TOKENor Vercel cron header.- Cron entry in
web/vercel.json:35 13 * * 1— fires 5 min after the existingsnapshot-model-healthcron.
Notes
- No new tables or writes. Everything reads from
user_slips, which already ships graded results vialib/grade-user-slips.js. Zero net-new schema. - Benchmarks in the SQL view are kept in lock-step with
TICKET_BENCHMARKSinweb/lib/api.ts. Any change there must be mirrored in the migration.
v1.3.3
Fixed
- Auto-capture endpoint no longer attempts to build a POTD slip. POTD was in
PLATFORM_CODESalongside UD/PP/FD/DK, but the genericbuildAutoParlays()filters the picks-pool byp.platform === platformand picks-pool only tags UD/PP/FD/DK — never POTD — so the beam was always empty and every size skipped withinsufficient valid legs. Even if the filter matched, capturing POTD here would duplicate the real POTD tracking that already flows throughapi/data.js → handleGeneratePotd → parlay_of_day → pick_grades / parlay_scorecard → platform_report_card. Auto-capture is now scoped to the four partner books; POTD tracking is unchanged and continues through its own pipeline.
- Auto-capture endpoint no longer attempts to build a POTD slip. POTD was in
v1.3.2
Fixed
- Auto-capture parlay pipeline was silently dead-lettered. The Vercel cron in
web/vercel.jsonpointed at/api/data?action=snapshot-pretip, an action that does not exist on the vanilla-JS side; the actual wave scheduler at/api/admin/wave-schedulerwas never being invoked. Zerosource='auto_generated'rows landed inuser_slipssince the 2026-07-04 migration. Updated the cron entry to/api/admin/wave-schedulerso wave 1 (pre-tip, T-45 to T-75 min) and wave N+1 (post-game-final) snapshots fire automatically on the correct 5-min cadence. Vercel cron auth uses the built-inx-vercel-cron=1header — no env changes required. - Companion Supabase migration
fix_auto_capture_unique_indexdropped the partial unique indexuq_user_slips_auto_slot(which hadWHERE source = 'auto_generated') and rebuilt it as a full unique index on(slip_date, platform, slip_size, wave_index). The Supabase JS.upsert({ onConflict })helper cannot emit the partialWHEREclause, so every capture attempt was returning"no unique or exclusion constraint matching the ON CONFLICT specification". Parlay_hub rows are unaffected because they havewave_index=NULLand Postgres treats NULLs as distinct in unique indexes. - Manual backfill of tonight's IND @ LV wave 1 captured 12 slips (UD/PP/FD/DK × 3/4/5 legs). POTD skipped all sizes with
insufficient valid legs— that builder needs a separate look but is outside this hotfix.
- Auto-capture parlay pipeline was silently dead-lettered. The Vercel cron in
v1.3.1
Fixed
- PropLine webhook signature verification computed HMAC over the wrong bytes — rejecting 100% of real deliveries with 401. Our verifier hashed the raw request body alone, but PropLine's SDK (
proplinePyPI,client.py::verify_signature) signs the timestamp + literal.+ body: ``python message = f"{timestamp}.".encode() + body expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()`So every signed delivery mismatched and returned 401 (evidence: webhook ID 32, delivery 5858273 returned 401 despitePROPLINE_WEBHOOK_SECRETbeing set). The breakage was masked earlier because the legacywnba-v700endpoint had no secret set and accepted everything unsigned. -lib/propline-webhook-verify.js—verifyProplineSignature/computeProplineSignaturenow take atimestampargument and compute the HMAC over${timestamp}.${rawBody}. A missing/empty timestamp returnsfalse(no signature can match without it). Still fail-closed on an unset secret and timing-safe. -web/app/api/propline-webhook/route.ts— readsX-PropLine-Timestampand passes it to the verifier; returns 401{error:"Missing timestamp"}when the header is absent. Also handles theX-PropLine-Event: test/{"event_type":"test"}connectivity ping as a 200{ok:true,test:true}no-op (no Supabase writes), and logsX-PropLine-Deliveryfor future idempotency dedupe. -api/propline-webhook.js(legacy Pages-Router handler, still live onwnba-v700as a rollback safety net) — same signature-format fix for parity (readsx-propline-timestamp, signs${timestamp}.${rawBody}`).
QA
- Regression:
npm run test:regression— 56/56 pass (model-constants 20, parlay-correlation 12, propline-webhook-verify 12 [was 9: added missing timestamp, old-format-rejected, and an exact PropLine-SDK test vector], endpoint-port-contract 12). tsc --noEmitclean;next buildclean.- Security/correctness fix — no visual/Lighthouse/axe surface.
/api/versionto report 1.3.1 post-deploy; live PropLine test delivery verified post-merge.
- PropLine webhook signature verification computed HMAC over the wrong bytes — rejecting 100% of real deliveries with 401. Our verifier hashed the raw request body alone, but PropLine's SDK (
v1.3.0
Added
- Full legacy endpoint port to shegotgame.app — the remaining repo-root
api/*.jshandlers fell outside the App Router build boundary after the 2026-07-02 migration and returned 404 on shegotgame.app. Eight are now reachable again via thin App Router shims that delegate to the untouched legacy handlers through the sharedweb/lib/pagesRouterAdapter.ts(same proven pattern as/api/dataand/api/bdl-stories), preserving each handler's contract byte-for-byte: -web/app/api/auto-grade/route.ts— POST-onlypick_gradesupsert (405 on GET, 500 on missing Supabase env, 502 on upstream fail). -web/app/api/grade-parlays/route.ts— GET?action=grade-now|scorecard| grade-propline-resolutions|backfill-by-market; thescorecardcontract is consumed byweb/app/track-record/page.tsx. -web/app/api/game-aware-ingest/route.ts— GET pre-tip ingest with optionalCRON_SECRETguard (401 on mismatch). -web/app/api/prizepicks/route.ts— GET PrizePicks proxy (L1 mem + L2pp_snapshot+ L3 503). -web/app/api/underdog/route.ts— GET Underdog proxy (90s in-mem cache, stale-on-error). -web/app/api/propline/route.ts— GET PropLine read proxy (distinct from the already-shipped/api/propline-webhook); 500 whenPROPLINE_API_KEYis unset. -web/app/api/bdl/route.ts— GET BDL?action=…proxy; 200{advanced_disabled:true}whenBDL_API_KEYis unset, 400 on unknown action. -web/app/api/storystats/route.ts— GET/POST StoryStats?action=…engine (distinct from the existing/api/storystats/indexing-ping); 400 on unknown action. - Regression suite
tests/regression/endpoint-port-contract.test.mjs(12 cases: importability + method-mismatch/OPTIONS/auth/missing-config/unknown-action contract on no-network paths) wired intonpm run test:regression.
Notes / follow-ups
api/parlay-recap.jsintentionally NOT ported as a route — it has no default HTTP handler; it is a library module exportingbuildRecap()/sendRecap(), already reachable viaapi/bdl-stories.js?action=recapand imported byscripts/bdl-stories-recap-sweep.js. Its legacy "500" was merely Vercel having no handler to invoke, not a real endpoint contract; porting it would invent new behavior, so it is left as-is.- Legacy
api/*.jsfiles are left in place (a separate cleanup PR will retire them). This PR only adds the App Router shims. api/game-aware-ingest.jsclaims a Vercel*/15cron in its header, but no such cron is registered inweb/vercel.json(crons there target/api/data). Registering the cron onshegotgame-webis a follow-up, out of scope for this byte-for-byte port.- Pre-existing legacy quirks preserved as-is (flagged, not fixed):
auto-gradechecks Supabase env before validating the body;bdlreturns 200advanced_disabled(not 500) when the key is unset per the[FIX 2026-07-01].
QA
- Regression:
npm run test:regression— 53/53 pass (model-constants 20, parlay-correlation 12, propline-webhook-verify 9, endpoint-port-contract 12). tsc --noEmitclean;next buildclean with all 8 new routes registered.- API-only change; no visual/Lighthouse/axe surface.
/api/versionto report 1.3.0 post-deploy.
- Full legacy endpoint port to shegotgame.app — the remaining repo-root
v1.2.35
Added
- PropLine webhook ported to shegotgame.app (
web/app/api/propline-webhook/route.ts) — App Router port of the legacy repo-rootapi/propline-webhook.js(Webhook ID 23) so PropLine Streaming Lite events land on the primary shegotgame.app deploy instead of the legacywnba-v700.vercel.apphost. Handlesline_movement(upsertpropline_line_cache),steam(insertpropline_steam_alerts) andresolution(insertpropline_resolutions), filtered tobasketball_wnba. Non-WNBA events are acked200 {ignored:true}with no writes so PropLine does not retry. - Fail-closed HMAC auth (
lib/propline-webhook-verify.js) — raw body is read viareq.text()before parsing, verified with a timing-safe HMAC-SHA256 compare. Unlike the legacy handler, a missingPROPLINE_WEBHOOK_SECRETnow returns 401 (never accepts unsigned requests); missing/mismatched signatures also return 401. Supabase write failures return 500 (not swallowed) so PropLine retries. New regression suitetests/regression/propline-webhook-verify.test.mjs(9 cases) wired intonpm run test:regression.
Fixed
- Legacy
wnba-v700.vercel.apphardcoded URLs replaced with env-driven URL defaulting tohttps://shegotgame.appinapi/health.js,api/beehiiv.js(fixes outbound recap-email footer links + display text) andapi/model-transparency.js. Legacyapi/propline-webhook.jsis intentionally retained live until Webhook ID 23 is re-registered in the PropLine portal.
QA
- Regression:
npm run test:regression— propline-webhook-verify 9/9 pass. - API-only + legacy-URL changes; no visual/Lighthouse/axe surface change. Post-deploy manual checklist in PR (401 no-sig / 401 wrong-sig / 200 valid + Supabase row / 200 non-WNBA ignored).
/api/versionto report 1.2.35.
- PropLine webhook ported to shegotgame.app (
v1.2.35
Added
/api/versionnow reports the transaction-monitor version (monitor: "wnba-transactions@1.0.0"). The customer-facing web app is otherwise unchanged; the WNBA season-long transaction monitor itself is a backend cron job documented in the repo-root changelog (docs/CHANGELOG.md).
v1.2.29
Fixed
- Grader re-visits stuck slips (
lib/grade-user-slips.js) — the grader previously only fetchedslip_result=eq.pending, so once a slip had gone terminal (won/lost/push) it was never re-graded even if late-arriving box score data would have resolved additional legs (e.g. slip50eafed8sat atlegs_pending=3on a lost slip, hiding one true WIN). The slipsUrl filter now also picks up any terminally-graded slip withlegs_pending>0, andgraded_atis only stamped on the FIRST pending→terminal transition so time-to-grade analytics stay honest. - Slip 50eafed8 backfilled — manual PATCH counted Kahleah Copper's 30-pt OVER 18.5 as WIN (legs_won 1→2, legs_pending 3→2); slip_result stays 'lost' because the Alyssa Thomas UNDER 6.5 leg had already lost. The other three pending legs (Stewart NYL / Young LVA / Wilson LVA) remain unresolved because those teams did not play 07-03 and never will — they are genuine no-game voids, not a normalization bug.
Removed
- Zombie root
vercel.jsoncrons — 11 stale cron entries at repo root pointing at legacywnba-v700.vercel.apphandlers that no longer exist in the codebase (ingest-resolutions,track-record,generate-potd,refresh-correlations,push-dispatch-line-moves,bdl-stories/*,grade-recap-email,wave-scheduler). All real production crons live inweb/vercel.json(*/5scan-user-slip-dnps + snapshot-pretip) plus.github/workflows/*.yml. Root file retained forignoreCommandandrewritesonly.
Notes — team-abbrev join audit
- Grader's
boxScoreMapkeys onnormPlayer(player_name)only — no team join, safe. bdl-context.jshandlesGS↔GSVandWSH↔WASvia inlineTEAM_ALIASES; slip-side aliasesLVA/NYL/PHOnever hit a read-path team join.scan-user-slip-dnps.jsjoinswnba_schedule.home_abbr/away_abbragainstESPN_PLAYER_MAP.team; both use ESPN convention (LV/NY/PHX/WSH) so they match natively. No bypass fix needed.
- Grader re-visits stuck slips (
v1.2.28
Changed
- BDL daily ingest resilience — the daily
BDL Daily Ingestjob no longer reds when the non-critical BallDontLie/player_injuriesendpoint fails. Coreplayer_game_stats/team_game_statswrites land *first*; the injury sync now runs fromscripts/sync-bdl-injuries.py, which retries 3× with exponential backoff (5s/15s/45s), treats a persistent401as non-fatal (rotated/expired key), and exits0so a fresh warehouse day stays green. - On-call Slack alerting — any persistent BDL/Supabase failure now DMs the on-call user via
chat.postMessage(SLACK_BOT_TOKEN). Core stats-write failures additionally exit non-zero so real breakage still pages, while a flaky injuries endpoint no longer produces daily false alarms.
Notes
- Upsert-count observability (surfacing rows-written per run) is a separate follow-up tracked in PR #15.
- BDL daily ingest resilience — the daily
v1.2.27
Fixed
fantasy_bankrollmigration never applied to prod (silent-fail bugfix). The20260703_fantasy_bankroll.sqlmigration existed in the repo but had never been recorded insupabase_migrations.schema_migrations— because two type mismatches would have made it error on first apply: 1.fantasy_ledger.slip_id BIGINT— butuser_slips.idis UUID. 2. Trigger multipliedNEW.payout_multiplieras numeric — but the column is stored as TEXT (e.g."20x","5.0").slip_alertshad no anon/authenticated SELECT policy. Onlyslip_alerts_service_allforservice_role— meaning the moment the scanner exited shadow mode and the frontend tried to render alerts, anon and signed-in users would get[]with no error (same silent-fail class as the enum mismatch).
Notes
- Horizon-scan audit completed for 9 latent-bug dimensions. Clean: player apostrophes, platform IDs (UD/PP/FD/DK/POTD), slip_size CHECK (3-5), scanner DST/null-start-time. Real bugs: fantasy_bankroll missing table + slip_alerts RLS gap (both fixed in this release). Semantic issue flagged: 1 graded slip has
legs_pending>0because the grader terminates on first miss — leg-resolution policy pending decision. - Team abbreviation drift (GSV/LVA/NYL/PHO/WAS vs GS/LV/NY/PHX/WSH) still present in raw data but
lib/team-map.js::normalizeTeam()handles it in code paths that use it. Audit of joins that bypassnormalizeTeamis a follow-up.
v1.2.26
Fixed
- Picks-pool window-clamp: mirror clamp on OddsAPI fallback path. The A clamp only applied to the PropLine event filter; the OddsAPI fallback (used when PropLine is short) was still using raw
PROP_WINDOW_FUTURE_MS(36h), leaking tomorrow's slate back into the pool whenwindow_mode='next_game_auto'. Live evidence at T-84min to SEA@POR: events_in_window=2 but the response contained 131 picks across 6 teams including tomorrow's TOR/DAL and LV/IND. (Commit 6dd974b.)
- Picks-pool window-clamp: mirror clamp on OddsAPI fallback path. The A clamp only applied to the PropLine event filter; the OddsAPI fallback (used when PropLine is short) was still using raw
v1.2.25
Added
- T-60 pre-tip PropLine snapshot writer. New
/api/data?action=snapshot-pretipendpoint + Vercel cron every 5 min. Querieswnba_schedulefor games in T-55..T-65 window, skips events already snapshotted, matches ESPN games to PropLine by team name + ±30min commence_time, fetches full player-prop board across 6 books, upserts one row per (event×player×market×bookmaker) topre_tip_propline_snapshots. Auth:x-vercel-cron:1header or?token=CRON_TOKEN. (Commit 0551748.)
- T-60 pre-tip PropLine snapshot writer. New
v1.2.23
Fixed
- Canonical
slip_resultenum (silent-data-loss bugfix). Three conflicting label conventions existed simultaneously: 1.lib/grade-user-slips.jsused.toLowerCase()on an internalWIN|LOSS|PUSH|PENDINGenum — writing'loss'(singular) to the DB. 2. Every analytics view (v_user_slip_hit_rate_7d,20260702_analytics_and_slips.sql,20260704_auto_capture_parlays.sql) filtered onslip_result IN ('won','lost','push')(plural). 3.slip_alerts.projected_resultCHECK required'pending'|'won'|'lost'|'push'. 4.20260703_fantasy_bankroll.sqlrequired UPPERCASEWIN|LOSS|PUSH.
- Canonical
v1.2.22
Added
- Portland Fire (POR) ESPN player-map coverage —
lib/espn-player-map.jsnow includes all 14 POR roster entries with ESPN athlete IDs (Sarah Ashlee Barker, Frieda Buhner, Bridget Carleton, Emily Engstler, Sania Feagin, Luisa Geiselsoder, Megan Gustafson, Jordan Harrison, Carla Leite, Teja Oblak, Nyadiew Puoch, Karlie Samuelson, Serah Williams, Holly Winterburn). POR previously had zero coverage; 6 of tonight's 20 pending user_slips carry a Carla Leite leg and would have been invisible to the T-30 DNP scanner. Map is now 154 players covering all pending-slip legs.
Changed
- Real-cadence scanner cron on web backend —
web/vercel.jsonnow runs/api/data?action=scan-user-slip-dnpsevery 5 minutes (*/5 * * * *). Retires the hourly pplx-tool cronf61b1388that was standing in during the shim migration. Backend cron fires against the liveshegotgame.approute so we get a real 5-minute pulse against pre-tip roster status.
Notes
- Shadow mode still active — scanner writes to
slip_alerts.channel='shadow'and updatesprojected_resultonuser_slips; no user-facing notifications yet. First real T-30 exercise is tonight 7:31 PM CDT for POR@SEA (tip 8:00 PM CDT).
- Portland Fire (POR) ESPN player-map coverage —
v1.2.21
Changed
components/ScheduleStrip.tsxpolling now reduces Vercel edge load. - Tab-visibility gating: the schedule only polls whiledocument.visibilityState === 'visible'. When the tab is hidden the interval is paused; when it becomes visible again the strip refetches immediately and resumes polling (via avisibilitychangelistener that is removed on cleanup). Backgrounded tabs no longer hammer the edge. - Dynamic cadence: poll every 60s when a game in the current view is LIVE (live scores stay fresh — preserves the LIVE refresh contract) and 120s otherwise (upcoming-only / FINAL-visible-only). LIVE detection is single-sourced from the existinglib/scheduleView.mjsview (view.live) via a new purepollIntervalMs(view)helper — no duplicated status logic.
Added
scripts/vercel-ignore-build.sh+ignoreCommandin rootvercel.json. Vercel skips Preview builds when a commit's diff (git diff --name-only HEAD^ HEAD) touches only docs-type files (**/*.md,**/*.mdx,CHANGELOG.md,docs/,.github/,LICENSE). Follows Vercel's exit-code protocol (exit 0 → skip, exit 1 → build). Production builds ($VERCEL_ENV == "production") always build; the docs-only skip applies to Preview only.
v1.2.15
Fixed
- StoryStats ESPN schedule strip showed stale "yesterday FINAL" cards above tonight's real game. Screenshots showed two finished games (DAL @ CON FINAL 86-83, SEA @ PHX FINAL 67-90) rendering above the actually-scheduled MIN @ NY 7:30 PM matchup. Root cause:
ScheduleStripordered purely by the serverbucketfield, and thewnba_schedule_landingSQL view derives that bucket from an ET day-string (game_date). Late-night games whose UTCgame_daterolls into "today" were bucketedtodayand floated to the top even after going FINAL — a day-string comparison bug.
Changed
components/ScheduleStrip.tsxis now a client component that orders and filters the schedule from absolute UTC timestamps (never day strings), using a new pure helperlib/scheduleView.mjs(computeScheduleView). Rules: - Stable order: LIVE (start asc) → UPCOMING (start asc) → FINAL within 3h (end desc). Position #1 is always the next relevant game; a LIVE game wins, else the earliest upcoming game. - A FINAL card is visible only for 3h after game end. End time uses the payloadend_timewhen present, otherwise falls back tostart_time + 2.5h(our schedule view exposes no end column). FINAL cards always render below live/upcoming, never above. - Recomputes on every render and refreshes every 60s while the tab is open (re-runs ordering so a FINAL drops off / an upcoming flips to LIVE, and refetches the schedule from Supabase). - Day label + ordering use the viewer's local timezone; cutoffs use absolute timestamps. - Friendly empty state ("No games right now — check back soon") when there are no live games, no upcoming games in the next ~48h, and no FINAL games within the last 3h — instead of stale cards.- Visual design of the cards is unchanged; only data selection + ordering changed. The BDL
live_pollStoryStats pipeline is untouched — this hotfix is scoped to the ESPN schedule component only.
Added
lib/scheduleView.mjs— pure, DOM-free ordering/visibility helper, unit tested attests/regression/schedule-view.test.mjs(8 cases: all-upcoming, live+upcoming, FINAL <3h shown-below, FINAL >3h filtered, empty schedule, explicitend_timecutoff, stable ordering, constants).- Optional
end_timefield on theScheduleGametype for the FINAL cutoff.
QA
node tests/regression/schedule-view.test.mjs— 8/8 pass (new).node tests/regression/model-constants.test.mjs,node tests/regression/parlay-correlation.test.mjs— pass (existing, no regression).- Playwright real-browser QA (mobile + desktop), Playwright visual regression, Lighthouse, and axe-core require
npm install+ a running server / live URL, which are not available in this sandbox. These run in the GitHub Actions Production Deploy Gate on push tomasterand must pass there before promotion. No baseline snapshot updates were needed (card markup unchanged; only ordering/visibility logic changed).
- StoryStats ESPN schedule strip showed stale "yesterday FINAL" cards above tonight's real game. Screenshots showed two finished games (DAL @ CON FINAL 86-83, SEA @ PHX FINAL 67-90) rendering above the actually-scheduled MIN @ NY 7:30 PM matchup. Root cause:
v1.2.14
Added
lib/parlay/auto-builder.ts: pure TS module that surfaces the top-5 model-preferred parlays per leg count (3, 4, 5) for a target platform. Scoring uses the existingcomputeSlipProbabilityso Auto-Build ranking matches the sticky slip bar. ExportsbuildAutoParlays,getPlatformDeepLink(UTM-tagged pick-a-builder link per book, Underdog via affiliate URL), andformatAutoParlayText(clipboard-ready summary).- Adj Prob + correlation delta now visible in the sticky slip bar. Fifth stat cell shows correlation-adjusted probability against the shipped phi table; ▲/▼ chip surfaces the delta vs the naive compound. First time SGG's phi math is exposed to users.
- 'Why this pick' signal chips under each pool row (usage%, streak, hit rate, opponent def rank, weak-D flag, ESPN form trend).
- Confidence-tier tinted borders on pool + ticket rows: LOCKED IN gold, HIGH green, EDGE blue, VALUE gray.
- Correlated-leg dot on ticket rows participating in |ρ| ≥ 0.30 pair.
- DK/FD depth: platform quotas bumped to parity with PP/UD (20/20/20/20), default pool size expanded to 80. Users now see 20 candidate rows for each of the four supported books.
v1.2.13
Fixed
- DK/FD pool visibility. The picks-pool endpoint has been returning 0 rows for DraftKings and FanDuel since the OddsAPI path shipped. Root cause was server-side (see root
CHANGELOG.mdv9.9.15.6): OddsAPI-scored legs top out at ~62 confidence vs PP/UD's ~91, so the globalslice(0, 60)cap always starved DK/FD out of the pool. Server now applies per-platform quotas (PP 20 / UD 20 / DK 10 / FD 10) before the global cap. No client changes required — users will now see DK and FD options in the parlay hub picks list automatically.
- DK/FD pool visibility. The picks-pool endpoint has been returning 0 rows for DraftKings and FanDuel since the OddsAPI path shipped. Root cause was server-side (see root
v1.2.12
Added — Auto-Build button on every parlay picker (v9.9.15.4)
Changed — Mobile ticket polish (v9.9.15.5)
- Ticket rows now include a per-leg remove (×) button so users can drop a single leg without scrolling back to the pool list. 28×28 tap target.
- Sticky slip bar honors
env(safe-area-inset-bottom)on iOS so the action buttons no longer sit under the home indicator. - Slip stats grid uses
minmax(0, 1fr)+text-overflow: ellipsisso long payout multipliers never wrap. Dedicated 320px breakpoint for iPhone SE. - Auto-Build button moved to its own row on <480px and given a pill treatment; pool checkmarks bumped to 24px (WCAG minimum).
v1.2.11
Fixed — Env-var alias shim: canonical names now hydrated from NEXT_PUBLIC_ variants
v1.2.10
Fixed — P0 outage: /api/data + /api/bdl-stories + /api/dashboard + /api/odds all returned 404
- All 7 Vercel crons have been running against 404s since 2026-07-02: ingest-resolutions (05:30 UTC), track-record (07:00 UTC), generate-potd (16:30 UTC), bdl-stories/games (12:00 UTC), bdl-stories/live_poll (every 2 min), bdl-stories/postgame (hourly), bdl-stories/recap_sweep (:15 past hourly).
- Homepage hit-rate gauges (7d / 30d / season) rendered null.
- Parlays page games-today strip rendered null.
- Pick of the Day panel rendered null.
- Save-slip and slip analytics events silently dropped client-side.
app/api/data/route.ts— wrapsapi/data.js(17 actions, 3300 lines)app/api/bdl-stories/route.ts— wrapsapi/bdl-stories.js(games, pregame, live_poll, postgame, feed, recap, send_recap, recap_sweep)app/api/dashboard/route.ts— wrapsapi/dashboard.jsapp/api/odds/route.ts— wrapsapi/odds.jslib/pagesRouterAdapter.ts— shared adapter with idle-timeout and exception fail-safes (returns 504 / 500 with detail instead of dangling)
Changed — Web frontend repointed to same-origin
web/lib/api.tsno longer defaults townba-v700.vercel.app. Server-side fetches fall back tohttps://shegotgame.app; client-side fetches use relative URLs.NEXT_PUBLIC_API_BASE/API_BASEstill honored for split-deploy testing.web/lib/analytics.tsTRACK_ENDPOINT,SAVE_SLIP_ENDPOINT, andUSER_SLIP_STATS_ENDPOINTchanged from absolute wnba-v700 URLs to relative/api/data?action=...paths.
Changed — Root parlay engine advanced to `v9.9.15.1`
- Corresponds to root engine
v9.9.15.1. No scoring changes in this bump — the engine version tick is purely for delivery/routing. - v9.9.15.0's model-version tracking (added earlier today) will now actually flow into
pick_gradesandparlay_performance_logbecause the pipelines can finally run again.
v1.2.9
Changed — Root parlay engine advanced to `v9.9.15.0` (later same day)
- No web-app code changed in this bump. Web app stays at
web/package.json1.2.9 while the root parlay engine advances fromv9.9.14.1tov9.9.15.0(model-version tracking foundation). - Every pick, grade, and parlay logged from this point forward is stamped with the exact engine version that produced it (
model_versioncolumn) plus a compactsignal_snapshotJSONB of the marginals/CLV/BDL context used at scoring time. Historical rows are backfilled to'pre-tracking'(110 picks, 19 pick_grades, 22 parlay_performance_log). - Unlocks version-attributed retro grading: future model changes can be A/B'd against past cohorts by filtering on
model_versioninstead of guessing from timestamps. Straddled-deploy days resolve the parlay's logged version by majority-vote across its legs. - Customer-visible endpoints unchanged.
/api/versionstill reports the root engine version (now9.9.15.0) and short commit hash.
Added — `/api/model-transparency` route (App Router shim)
app/api/model-transparency/route.ts— App Router route that ports the legacy repo-root Pages-Router handler into the Next.js build boundary so the endpoint is finally reachable in prod atshegotgame.app/api/model-transparency. Same query contract (player,market,platform,slipContext), same signals schema, same fail-safe payload. Surfaces the Tier 4.2 per-player phi correlation source & N introduced by root v9.9.14.0.- Corresponds to root parlay-engine
v9.9.14.1. No engine changes; this is a delivery/routing patch.
- No web-app code changed in this bump. Web app stays at
v1.2.8
Added — Public changelog page + version badge link
app/changelog/page.tsx— new public route at/changelog. Readsweb/CHANGELOG.mdat build time and renders it as a chronological timeline of releases. Fully static (force-static), zero client JS, single file fetch at build — no bundle size impact.lib/changelog.ts— tiny in-house Keep-a-Changelog parser. Splits the markdown on## [x.y.z] — YYYY-MM-DDrelease headers and###section headers, escapes HTML then re-applies a safelisted inline subset (code,strong, http/https links only). Rejected addingremark/marked/gray-mattersince the format is stable enough that 60 lines of parsing is cheaper than 40 KB of dependencies.components/BuildBadge.tsx— the footer version pill now links to/changelog(in-app release notes) while the commit SHA still links to GitHub. Tap the version on the installed PWA to see what shipped.app/changelog/page.module.css— dark-theme timeline UI with gold accent for the current release,LIVEbadge on the top entry, and mobile-first spacing. Release cards get a subtle gold ring when they match the running version so the footer’svnumber and the page’s "you are here" indicator always agree.
Notes
- No API changes, no schema changes, no monetization changes. The changelog page is fully static and safe to cache aggressively.
- Kept
alternates.canonical: '/changelog'on the page metadata and standard OG/Twitter cards so the page can be shared externally.
v1.2.7
Fixed — DNP handling in user-slip grader
lib/grade-user-slips.js— grader now fetches the day's DNP roster fromplayer_game_stats(players withmin_played = 0onslip_date) and treats those legs as PUSH, matching how book platforms void inactive-player props. Previously an OVER on a scratched player would grade as LOSS and an UNDER as WIN becausepropline_resolutionsstill showsactual_value = 0. The DNP check runs *before* the resolution lookup so it wins the tiebreak.- Grader response now returns
dnp_playerscount alongside existing fields. - Pick grading is untouched — this only affects user-built slips.
v1.2.6
Added — User-slip grading + homepage tile
lib/grade-user-slips.js— new grader module. For everyuser_slipsrow whoseslip_resultis stillpendingon the target date, resolves each leg by matching (player, market_key) againstpropline_resolutionsand comparingactual_valuetoline + direction. Aggregates intolegs_won / legs_lost / legs_push / legs_pendingand finalslip_result(won,lost,push, orpending). Idempotent: re-running only updates rows that gained a new resolution.api/data.js—grade-pipelineaction now also invokesrunUserSlipGrader()after the standard pick pipeline. Failures inside the user-slip grader are logged and returned in the response but never break pick grading (pick grading is the source of truth for our internal model).web/components/UserSlipHitRateStrip.tsx— new homepage strip that reads/api/data?action=user-slip-statsand renders per-platform hit rates (UD / PP / FD / DK) for slips users actually built in the last 7 days. Silently hides itself when nothing has been graded yet, so the first week after launch there's no empty tile. Mobile: 2×2 grid.web/app/page.tsx— mountsUserSlipHitRateStripdirectly under the hero band, above the live-strip and StoryStats feed. First-party pipeline end-to-end: user builds slip →handleSaveSlippersists → nightly grader resolves → view rolls up → strip shows real user hit rate.supabase/migrations/20260702_v126_view_realign.sqlapplied to production. Realignsv_user_slip_hit_rate_7dcolumn names + result-label vocabulary to match the grader (won/lost/push/pending, lowercase). View now exposesplatform, total_slips, won, lost, push, hit_rate— a stable contract for both the API handler and the strip component.
Design decision — slip vs. pick grading isolation
- User-slip grading is a strict superset of pick grading: it depends on propline_resolutions but adds no new source of truth. Kept the module separate from
lib/grade-pipeline.jsso a bug in slip grading can never corrupt the internal model's hit rate history. The grade-pipeline action runs both back-to-back, but they don't share transactions or state.
v1.2.5
Added — Slip ticket preview, share button, first-party analytics + slip persistence
web/components/ParlayHub.tsx— platform tabs now render a live "ticket preview" listing each selected leg (player · direction · line · market · conf%), with an empty-state nudge when the user hasn't selected any legs yet. Adds a third action button — Share — that fires the native Web Share sheet on mobile (iMessage, X, DMs, etc.) and falls back to clipboard on desktop.web/components/ParlayHub.module.css— new.slipCard,.ticket,.ticketRow,.shareBtnstyles;.slipActionsnow1fr 1fr 1.2frfor the Copy / Share / Open trio. Mobile-first: preview compresses to two lines per row under 380px so it fits above the fold on iPhone SE.web/lib/analytics.ts— new first-party event helper.trackEvent()usesnavigator.sendBeaconwith afetch keepalivefallback so events survive the page tearing down on "Open in Platform". Session id persists inlocalStorageassgg_session_id(never PII). TypedSggEventunion covers:slip_leg_toggled,slip_finalized(debounced 5s after target size reached),slip_copied,slip_shared,platform_opened,tab_switched,market_filter_changed,slip_size_changed. All events POST to/api/data?action=track→ Supabaseapp_events— no third-party data leak, no GA4, no cookie banner needed.api/data.js— newhandleTrackhandler on the wnba-v700 backend. Inserts events intoapp_eventswith a salted IP-hash fingerprint (16 hex chars, non-reversible) so we can dedup abusive traffic without storing raw IPs. Never hard-fails — analytics returns 200 even on DB error.api/data.js— newhandleSaveSliphandler (v1.2.6 groundwork). Two modes: create (session_id, platform, pick_ids, legs → returns row id) and update (id + was_copied/was_shared/was_opened → PATCH flags). Lets us record every finalized slip a user builds — not just the ones they act on — so the v1.2.6 grader can compute real user-slip hit rates.api/data.js— newhandleUserSlipStatshandler readingv_user_slip_hit_rate_7dview for the upcoming homepage tile.web/components/ParlayHub.tsx— wirespersistSlip()into copy / share / open / debounced-finalize. Uses aslipIdCacheRefmap so the same unique leg-set only creates one row, and subsequent actions PATCH flags. Every persist iskeepalive: truefire-and-forget — slip persistence errors never surface to the UX.supabase/migrations/20260702_analytics_and_slips.sqlapplied to production. Createsapp_events+user_slipstables with RLS-deny-anon policies (service key only), plus thev_user_slip_hit_rate_7dview for the homepage rollup tile.
Rejected — GA4 integration
- Considered adding Google Analytics 4 for these events; chose first-party Supabase pipeline instead. Slip data is the moat — shipping it to Google would leak the exact leg mix competitors need to reverse-engineer our edge. Zero CSP changes, zero cookie banner, faster than gtag.
v1.2.4
Fixed — CSP blocked AdSense loader
web/vercel.json— extendedContent-Security-Policyto allow Google AdSense to load. The v1.2.3 script/img tags were shipped correctly but the existing strict CSP (script-src 'self' 'unsafe-inline' https://vercel.live) was blockingpagead2.googlesyndication.com, so no ads would ever render and Google's site verification would fail silently in the browser console.script-srcnow also allowspagead2.googlesyndication.com,*.googlesyndication.com,*.google.com,*.googletagservices.com,adservice.google.com.connect-srcwidened for AdSense telemetry beacons.frame-srcnow allows*.googlesyndication.com,*.google.com,*.doubleclick.net(ads render inside cross-origin iframes).- No change to
img-src— already permissive (https:). - Everything else (Supabase, Fontshare, wnba-v700 API, Vercel Live) unchanged.
v1.2.3
Added — Tabbed Parlay Hub (Pick of the Day + platform builders)
- New
/parlayspage hero:ParlayHubcomponent with tabs[Pick of the Day] [Underdog] [PrizePicks] [FanDuel] [DraftKings]. Each platform tab is brand-color themed (SGG gold for POTD+UD, PrizePicks purple#7B4DFF, FanDuel blue#1493FF, DraftKings green#4CBB17). All tabs hydrate from a single server-side fetch — zero API round-trip on tab switch (critical for the mobile PWA UX). - POTD tab renders the existing consensus slip with a distinct gold-accented
houseCard(radial gold gradient, SGG badge, gold title color) so it's obviously our proprietary pick. - Platform tabs show a filtered pick pool with a stat-category chip row (All / Points / Rebounds / Assists / 3PM / PRA / P+A / P+R / R+A / Steals / Blocks), a 3–4–5 slip-size picker, live-updating slip metrics (legs, avg conf, compound %, payout multiplier), and two actions:
Copy Slip(formatted clipboard text) +Open in [Platform](deep-link with PWA-safewindow.openfallback — same pattern as ExternalSourceLink). - State machine: platform switch clears selections (different books = different lines); market chip changes preserve selections; reducing slip size drops overflow legs (keeps highest confidence first).
Added — Backend picks-pool endpoint
- New action
GET /api/data?action=picks-poolon wnba-v700 monolith returns a broad candidate pool bucketed per platform, with query paramsplatform,market,minConfidence,size(max 200). - Response shape includes
book_line,book_price,game_time,opponent, and a stable-hashidper pick for React key stability. - Reuses the same PropLine fetch pipeline +
scoreServerLeg()signals used by POTD (rolling projections, BDL usage/pace/zone, dynamic caps, retro hit rates). No MAX_LEGS or market-diversity cap — returns the whole pool. - Cache header
s-maxage=180, stale-while-revalidate=60(3-min edge). - DraftKings + FanDuel lines pulled from The Odds API (bookmakers
draftkings,fanduel) since PropLine's WNBA feed doesn't include those books. OddsAPI errors are logged but never fail the whole endpoint.
Changed — Track Record (Graded Legs)
- Was:
Last 10 graded picks(fixed count cap). - Now:
Last 7 days · N graded picks(rolling day window, no cap). - Implementation:
getTrackRecord(days = 7)inlib/api.tsfilters bygame_dateagainst a UTC-anchored 7-day window (avoids timezone drift).
Added — Google AdSense (pub-3418444283543157)
- Set
NEXT_PUBLIC_ADSENSE_CLIENT_ID = ca-pub-3418444283543157on all three Vercel targets (production/preview/development). ExistingAdSenseScriptloader now mounts unconditionally site-wide, letting Google begin site verification / auto-ads review immediately. - Slot IDs (
NEXT_PUBLIC_AD_SLOT_*) remain empty until the user creates slots in the AdSense dashboard — progressive rollout.
Added — Platform metadata module
- New
lib/platforms.tscentralizes platform codes, brand colors (with 20%-alpha variants for backgrounds), deep-link URLs, and the market chip list. Single source of truth so rebranding a platform is a one-file change.
Renamed — Page title
Today's Parlay→Pick of the Day + Builder(metadata,<title>, OpenGraph, Twitter card, and H1). The consensus slip has always been a POTD product; the label now matches.
- New
v1.2.2
Fixed — PWA external links
Read the full storyCTA now opens in system browser instead of trapping inside the installed PWA's own webview. Extracted the link into a client componentExternalSourceLink.tsxthat explicitly callswindow.open(url, '_blank', 'noopener,noreferrer')with alocation.hreffallback for popup-blocked scenarios. Modifier-clicks (cmd/ctrl/shift/middle) still use browser-native behavior.- Manifest hardened for external navigation (
public/manifest.json): added explicitscope: '/',capture_links: 'none', andlaunch_handler.client_mode: 'navigate-existing'. Tells Android/iOS that URLs outside the SGG domain are not part of the app and should never be captured back into the PWA. - Fixes report from installed-PWA users: Yahoo/ESPN/The Athletic articles were opening in-app instead of the system browser.
v1.2.1
Added — SEO recovery (audit findings from SEO_AUDIT_2026-07-02.md)
- Canonical links restored —
alternates.canonicalon root layout +/parlays+/storystats+/storystats/[slug]. All resolve tohttps://shegotgame.appviametadataBase. Fixes duplicate-content ambiguity between shegotgame.app and wnba-full.vercel.app. - Host consolidation middleware (
middleware.ts) — 308 permanent redirects fromwnba-full.vercel.app/*toshegotgame.app/*withX-Robots-Tag: noindex, followbelt-and-suspenders header. Consolidates PageRank + inbound links onto the canonical host. - JSON-LD structured data (
components/seo/JsonLd.tsx): -WebApplicationschema in root layout (all routes) with SportsApplication category, free Offer, publisher Organization, SearchAction, sameAs socials. -NewsArticleschema on/storystats/[slug]with headline, datePublished, articleSection, keywords from card tags, publisher logo. -BreadcrumbListschema on/storystats/[slug]for SERP hierarchy display. - RSS 2.0 feed at
/rss.xml— 100 most-recent StoryStats cards, RSS autodiscovery<link rel="alternate">in root layout. Cached s-maxage=300 stale-while-revalidate=3600. - Indexing ping API at
POST /api/storystats/indexing-ping— token-gated endpoint returning URLs of cards published in the last?hours=Nwindow (default 1h, max 24h). Externally callable by scheduled tasks to submit fresh slugs to Google Indexing API. - Google Site Verification meta restored via
verification.googlein root metadata. - Route-specific OG + Twitter for
/parlaysand/storystats— fixes metadata drift where these routes were leaking the root layout's OG title.
- Canonical links restored —
v1.2.0
Added — Monetization layer (env-gated, invisible until keys set)
- Tip Jar — global
TipJarButtoninTopBaropensTipJarModalwith Cash App, Venmo, and PayPal targets. AlsoTipJarInlineblocks on/,/parlays, and/storystats/[slug]. All handles set to the lockedSheGotGameSystems(Cash App / Venmo / paypal.me/SheGotGameSystems). - AdSense scaffolding —
<AdSenseScript />client component wired into rootlayout.tsx;<AdSlot />placeholders on home, parlays, and StoryStats detail. Rendering is env-gated byNEXT_PUBLIC_ADSENSE_CLIENT_ID— until it's set, no scripts load and no slots appear. Ready for AdSense approval without shipping any user-visible ad surface yet. - Underdog affiliate strip —
AffiliateStripon/parlayslinks tohttps://play.underdogsports.com/vgwg/eoqxki8x(the locked, unchanged Underdog affiliate URL). - Contact email —
will@shegotgame.appsurfaced in footer + "Get in touch" block on every page. - Legal footer — Affiliate Disclosure, Responsible Gambling (with 1-800-522-4700 hotline), Terms of Use links.
Added — StoryStats content pipeline (no more "thin" feed)
- Manual backfill — 12 hand-crafted cards from real BDL data across Jun 14–20, 2026 (A'ja Wilson 33/10, Sonia Citron/Mabrey 37/9-3PM, Malonga 28/11, Clark 26/7, Miles 31, Reese 21/11, Williams 21/12/5, plus prop-movers, four-factors, shot-diet, clutch-report weekly cards).
- Auto-generator — pure Postgres functions + pg_cron. No Edge Function, no service-role key exposure. Runs every 30 min: -
storystats_gen_postgame(max_cards)— top PRA lines, last 3 days -storystats_gen_prop_movers()— weekly overs vs season baseline -storystats_gen_four_factors()— weekly top team by point diff -storystats_gen_shot_diet()— league 3PT rate + top 3PT/rim shooters -storystats_generate_all()— orchestrator, logs tostorystats_run_log-storystats_cleanup_old()— archives cards >30 days (daily 04:00 UTC) - Idempotent via
ON CONFLICT (slug) DO NOTHING; ISO-week slugs keep weekly cards uniquely dated (prop-movers-week-2026-25, etc.). - Cron jobs registered:
storystats-generate(*/30 * * * *) +storystats-cleanup(0 4 * * *). Latest run made 5 postgame cards in 33ms; DB now holds 25 total cards (17 postgame, 3 four_factors, 2 prop_movers, 2 shot_diet, 1 clutch_report).
QA — Full SOP gate passed
- Playwright interaction (mobile 375×812 + desktop 1440×900): 33/33 pass, 0 fail, 0 warn. TipJar modal opens, ESC closes, all three handles visible, Underdog URL present, AdSense correctly not loaded (env-gated), all three legal footer links present.
- axe-core WCAG 2.1 AA on
/,/parlays,/storystats,/storystats/[slug]— 0 critical, 0 serious, 0 moderate on every route. - Lighthouse (all 4 routes × mobile+desktop, 8 runs): - Home mobile: perf 91 / a11y 95 / bp 100 / seo 100 - Home desktop: perf 100 / a11y 100 / bp 100 / seo 100 - Parlays mobile: perf 98 / a11y 95 / bp 100 / seo 100 - Parlays desktop: perf 100 / a11y 100 / bp 100 / seo 100 - StoryStats mobile: perf 98 / a11y 95 / bp 100 / seo 100 - StoryStats desktop: perf 100 / a11y 100 / bp 100 / seo 100 - StoryStats detail mobile: perf 98 / a11y 93 / bp 100 / seo 100 - StoryStats detail desktop: perf 100 / a11y 98 / bp 100 / seo 100 - All ≥ 90 across every category — SOP gate passed.
- Tip Jar — global
v1.1.1
Fixed — Accessibility (Full SOP QA follow-ups)
heading-order(axe moderate, Lighthouse a11y hit) —StoryStatsCardheadline changed from<h3>to<h2>so page heading hierarchy ish1 (hero) → h2 (card)on/and/storystats. Previously skipped h2.label-content-name-mismatchonStoryStatsCard— removed the redundantaria-label={card.headline}on the outer<a>. The card's visible inner text is already a meaningful accessible name.label-content-name-mismatchonParlayLegCardtoggle button — replaced synthesizedaria-labelwith a visually-hidden.sr-onlyspan prefix ("Add to slip: " / "Remove from slip: "). Accessible name now matches the visible content.image-redundant-alton TopBar logo — setalt=""on the decorative<Image>since the sibling wordmark and the link'saria-labelalready name the link. The logo image is purely decorative.- Added
.sr-onlyutility class toglobals.cssfor future WCAG-safe visually-hidden text.
QA artifacts (this release)
- Playwright interaction + visual regression: 35/39 pass, 4 informational (see
qa/REPORT.md). - Lighthouse mobile:
/99,/parlays100,/storystats100,/storystats/[slug]100. Desktop: 92-94 (LCP text-node bound; no image hero to prioritize). - axe-core: 0 critical, 0 serious, 0 moderate after fixes.
v1.1.0
Added
- Real She Got Game logo in TopBar (was placeholder "SGG" wordmark), with hover animation and drop shadow. Same file (
/sgg-icon-192.png) is already wired to favicon, apple-touch-icon, PWA manifest, and OG image. - NewsTicker — sitewide sticky horizontal marquee under TopBar. Pulls the 20 most recent StoryStats headlines from Supabase. Auto-scrolls left with pause-on-hover. Live-pulse gold indicator. Respects
prefers-reduced-motion. Renders nothing if feed is empty. - Full parlay builder port at
/parlays: -ParlayBuilderclient component with sticky slip summary (compound probability, avg confidence, payout multiplier, HIGH tag count) - Toggle-any-leg-in-or-out with live slip recalculation - Copy-to-clipboard for finalized slip in a shareable text format -ParlayLegCardwith confidence stripe, direction chip, HIGH/EDGE/LEAN badges, expandable "Why this pick" reveal (blurb + BDL usage/pace/zone) - Track record table showing last 10 graded picks with HIT/MISS pills lib/api.ts—getTodaysParlay(),getTrackRecord()bridges to the monolith with full TypeScript types (ParlayLeg,ParlayResponse,TrackRecordPick,TrackRecordResponse).- Version endpoint at
/api/versionreturning{ version, commit, branch, deployed_at, environment }. - BuildBadge footer on every page — tiny commit SHA link to GitHub.
Changed
- StoryStats env vars fixed — corrected corrupted
NEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_ANON_KEYvalues that hadKEY=prefix pasted into the value field. Build-time SSG now succeeds for all/storystats/[slug]routes.
Removed
/legacymiddleware rewrite. The old monolith remains live atwnba-v700.vercel.appas an untouched instant-rollback backup — no back-door URL on the new site. Rollback = one DNS change onshegotgame.app.
- Real She Got Game logo in TopBar (was placeholder "SGG" wordmark), with hover animation and drop shadow. Same file (
v1.0.0
Initial Next.js 14 App Router migration. See earlier commits for details.
Added
/webscaffolded with App Router, dark forest + cream themes, Satoshi + Cabinet Grotesk from Fontshare./storystatsfeed +/storystats/[slug]detail (Server Components + ISR).- Editorial homepage with hero StoryStats + live strip + gauges.
/admin/model-trendsbehind ADMIN_TOKEN cookie/query auth.- PWA manifest, sitemap, robots, security headers, tight CSP.
- Bottom tab bar navigation, mobile-first design.
Build stats
- 94.5 KB First Load JS across all routes
- 96% reduction from prior 2.3 MB monolith