SHEGOTGAME

What's new

Changelog

Every release, every fix, every model tune-up. Currently running v11.9.26 (f4693ba). Full source on GitHub.

  1. 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_ledger mirror 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.
        • v1.3.12

          Meet The GM's Read™ — the head coach behind every pick

            Added

            • New /methodology page 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.
          • 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. See docs/postmortems/2026-07-11-pick-grades-duplicates.md.

              Changed

              • /api/version now reports 11.2.4.
            • 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-record surfaces 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/version now correctly reports 11.2.3 matching the backend release. The v11.2.3 backend release PR only bumped the repo-root package.json; because the version endpoint imports web/package.json, the deployed app kept reporting 11.2.2 until this hotfix. See docs/CHANGELOG.md v11.2.3.
            • v1.3.8

              Added

              • /track-record now 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 from v_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_7d is now available for the web app to read. Not yet wired into any UI; when the dial strip on /track-record adds a leg-level view, it can source from this view. Backend-only in this release. See docs/CHANGELOG.md v11.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.md v11.2.2.
            • v1.3.6

              Fixed

              • Public model-transparency dials now read clean data. The /track-record and 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 in user_slips. All synthetic rows were moved to a dedicated synthetic_pregame_snapshots table; the views now filter to the real-user allow-list source IN ('parlay_hub','auto_generated') OR source IS NULL. Post-fix hit rates verified beating_vig across 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. See docs/incidents/2026-07-10-synthetic-pregame-dial-contamination.md.
            • v1.3.5

              Added

              • 2-leg parlay tracking end-to-end. The user_slips.slip_size check constraint has been relaxed from BETWEEN 3 AND 5 to BETWEEN 2 AND 5 (migration 20260707_relax_slip_size_min.sql). TICKET_BENCHMARKS and the v_slip_hit_rate_by_leg_count_v1 view already published a 2-leg break-even (0.275 = 0.524^2); those rows will now actually get populated instead of reporting insufficient_sample forever.
              • ParlayHub size picker now includes a 2 button. slipSize state widened to 2 | 3 | 4 | 5.
              • Auto-Build (buildAutoParlays) default legCounts widened 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_BENCHMARKS in web/lib/api.ts has no 6-leg row published, so 6-leg tracking would show insufficient_sample forever. The fantasy_ledger payout 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.
            • v1.3.4

              Added

              • /how-we-measure transparency 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 /parlays and /stats.
              • /api/data?action=hit-rate-by-leg-count — read-only endpoint that surfaces v_slip_hit_rate_by_leg_count_v1 for the transparency page. 5-minute Cache-Control.
              • v_slip_hit_rate_by_leg_count_v1 SQL view (migration 20260706_hit_rate_by_leg_count.sql) — rolls up graded user_slips into per-leg-count hit rates over 7d / 30d / season windows joined against the TICKET_BENCHMARKS reference (2:0.275, 3:0.144, 4:0.075, 5:0.039), with a classification column: beating_vig / watch / below_benchmark / insufficient_sample. Buckets with fewer than 5 graded slips return insufficient_sample to suppress early-season noise.
              • /api/admin/parlay-drift-alert (root api/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. Missing SLACK_WEBHOOK_URL falls back to console.error (mirrors scripts/prod-drift-monitor.js). Auth via DASHBOARD_ADMIN_TOKEN or Vercel cron header.
              • Cron entry in web/vercel.json: 35 13 * * 1 — fires 5 min after the existing snapshot-model-health cron.

              Notes

              • No new tables or writes. Everything reads from user_slips, which already ships graded results via lib/grade-user-slips.js. Zero net-new schema.
              • Benchmarks in the SQL view are kept in lock-step with TICKET_BENCHMARKS in web/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_CODES alongside UD/PP/FD/DK, but the generic buildAutoParlays() filters the picks-pool by p.platform === platform and picks-pool only tags UD/PP/FD/DK — never POTD — so the beam was always empty and every size skipped with insufficient valid legs. Even if the filter matched, capturing POTD here would duplicate the real POTD tracking that already flows through api/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.
            • v1.3.2

              Fixed

              • Auto-capture parlay pipeline was silently dead-lettered. The Vercel cron in web/vercel.json pointed 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-scheduler was never being invoked. Zero source='auto_generated' rows landed in user_slips since the 2026-07-04 migration. Updated the cron entry to /api/admin/wave-scheduler so 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-in x-vercel-cron=1 header — no env changes required.
              • Companion Supabase migration fix_auto_capture_unique_index dropped the partial unique index uq_user_slips_auto_slot (which had WHERE 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 partial WHERE clause, so every capture attempt was returning "no unique or exclusion constraint matching the ON CONFLICT specification". Parlay_hub rows are unaffected because they have wave_index=NULL and 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.
            • 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 (propline PyPI, 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 despite PROPLINE_WEBHOOK_SECRET being set). The breakage was masked earlier because the legacy wnba-v700 endpoint had no secret set and accepted everything unsigned. - lib/propline-webhook-verify.jsverifyProplineSignature / computeProplineSignature now take a timestamp argument and compute the HMAC over ${timestamp}.${rawBody}. A missing/empty timestamp returns false (no signature can match without it). Still fail-closed on an unset secret and timing-safe. - web/app/api/propline-webhook/route.ts — reads X-PropLine-Timestamp and passes it to the verifier; returns 401 {error:"Missing timestamp"} when the header is absent. Also handles the X-PropLine-Event: test / {"event_type":"test"} connectivity ping as a 200 {ok:true,test:true} no-op (no Supabase writes), and logs X-PropLine-Delivery for future idempotency dedupe. - api/propline-webhook.js (legacy Pages-Router handler, still live on wnba-v700 as a rollback safety net) — same signature-format fix for parity (reads x-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 --noEmit clean; next build clean.
              • Security/correctness fix — no visual/Lighthouse/axe surface. /api/version to report 1.3.1 post-deploy; live PropLine test delivery verified post-merge.
            • v1.3.0

              Added

              • Full legacy endpoint port to shegotgame.app — the remaining repo-root api/*.js handlers 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 shared web/lib/pagesRouterAdapter.ts (same proven pattern as /api/data and /api/bdl-stories), preserving each handler's contract byte-for-byte: - web/app/api/auto-grade/route.ts — POST-only pick_grades upsert (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; the scorecard contract is consumed by web/app/track-record/page.tsx. - web/app/api/game-aware-ingest/route.ts — GET pre-tip ingest with optional CRON_SECRET guard (401 on mismatch). - web/app/api/prizepicks/route.ts — GET PrizePicks proxy (L1 mem + L2 pp_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 when PROPLINE_API_KEY is unset. - web/app/api/bdl/route.ts — GET BDL ?action=… proxy; 200 {advanced_disabled:true} when BDL_API_KEY is 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 into npm run test:regression.

              Notes / follow-ups

              • api/parlay-recap.js intentionally NOT ported as a route — it has no default HTTP handler; it is a library module exporting buildRecap() / sendRecap(), already reachable via api/bdl-stories.js?action=recap and imported by scripts/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/*.js files are left in place (a separate cleanup PR will retire them). This PR only adds the App Router shims.
              • api/game-aware-ingest.js claims a Vercel */15 cron in its header, but no such cron is registered in web/vercel.json (crons there target /api/data). Registering the cron on shegotgame-web is a follow-up, out of scope for this byte-for-byte port.
              • Pre-existing legacy quirks preserved as-is (flagged, not fixed): auto-grade checks Supabase env before validating the body; bdl returns 200 advanced_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 --noEmit clean; next build clean with all 8 new routes registered.
              • API-only change; no visual/Lighthouse/axe surface. /api/version to report 1.3.0 post-deploy.
            • v1.2.35

              Added

              • PropLine webhook ported to shegotgame.app (web/app/api/propline-webhook/route.ts) — App Router port of the legacy repo-root api/propline-webhook.js (Webhook ID 23) so PropLine Streaming Lite events land on the primary shegotgame.app deploy instead of the legacy wnba-v700.vercel.app host. Handles line_movement (upsert propline_line_cache), steam (insert propline_steam_alerts) and resolution (insert propline_resolutions), filtered to basketball_wnba. Non-WNBA events are acked 200 {ignored:true} with no writes so PropLine does not retry.
              • Fail-closed HMAC auth (lib/propline-webhook-verify.js) — raw body is read via req.text() before parsing, verified with a timing-safe HMAC-SHA256 compare. Unlike the legacy handler, a missing PROPLINE_WEBHOOK_SECRET now returns 401 (never accepts unsigned requests); missing/mismatched signatures also return 401. Supabase write failures return 500 (not swallowed) so PropLine retries. New regression suite tests/regression/propline-webhook-verify.test.mjs (9 cases) wired into npm run test:regression.

              Fixed

              • Legacy wnba-v700.vercel.app hardcoded URLs replaced with env-driven URL defaulting to https://shegotgame.app in api/health.js, api/beehiiv.js (fixes outbound recap-email footer links + display text) and api/model-transparency.js. Legacy api/propline-webhook.js is 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/version to report 1.2.35.
            • v1.2.35

              Added

              • /api/version now 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 fetched slip_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. slip 50eafed8 sat at legs_pending=3 on a lost slip, hiding one true WIN). The slipsUrl filter now also picks up any terminally-graded slip with legs_pending>0, and graded_at is 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.json crons — 11 stale cron entries at repo root pointing at legacy wnba-v700.vercel.app handlers 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 in web/vercel.json (*/5 scan-user-slip-dnps + snapshot-pretip) plus .github/workflows/*.yml. Root file retained for ignoreCommand and rewrites only.

              Notes — team-abbrev join audit

              • Grader's boxScoreMap keys on normPlayer(player_name) only — no team join, safe.
              • bdl-context.js handles GS↔GSV and WSH↔WAS via inline TEAM_ALIASES; slip-side aliases LVA/NYL/PHO never hit a read-path team join.
              • scan-user-slip-dnps.js joins wnba_schedule.home_abbr/away_abbr against ESPN_PLAYER_MAP.team; both use ESPN convention (LV/NY/PHX/WSH) so they match natively. No bypass fix needed.
            • v1.2.28

              Changed

              • BDL daily ingest resilience — the daily BDL Daily Ingest job no longer reds when the non-critical BallDontLie /player_injuries endpoint fails. Core player_game_stats / team_game_stats writes land *first*; the injury sync now runs from scripts/sync-bdl-injuries.py, which retries 3× with exponential backoff (5s/15s/45s), treats a persistent 401 as non-fatal (rotated/expired key), and exits 0 so 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.
            • v1.2.27

              Fixed

              • fantasy_bankroll migration never applied to prod (silent-fail bugfix). The 20260703_fantasy_bankroll.sql migration existed in the repo but had never been recorded in supabase_migrations.schema_migrations — because two type mismatches would have made it error on first apply: 1. fantasy_ledger.slip_id BIGINT — but user_slips.id is UUID. 2. Trigger multiplied NEW.payout_multiplier as numeric — but the column is stored as TEXT (e.g. "20x", "5.0").
              • slip_alerts had no anon/authenticated SELECT policy. Only slip_alerts_service_all for service_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>0 because 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 bypass normalizeTeam is 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 when window_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.)
            • v1.2.25

              Added

              • T-60 pre-tip PropLine snapshot writer. New /api/data?action=snapshot-pretip endpoint + Vercel cron every 5 min. Queries wnba_schedule for 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) to pre_tip_propline_snapshots. Auth: x-vercel-cron:1 header or ?token=CRON_TOKEN. (Commit 0551748.)
            • v1.2.23

              Fixed

              • Canonical slip_result enum (silent-data-loss bugfix). Three conflicting label conventions existed simultaneously: 1. lib/grade-user-slips.js used .toLowerCase() on an internal WIN|LOSS|PUSH|PENDING enum — 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 on slip_result IN ('won','lost','push') (plural). 3. slip_alerts.projected_result CHECK required 'pending'|'won'|'lost'|'push'. 4. 20260703_fantasy_bankroll.sql required UPPERCASE WIN|LOSS|PUSH.
            • v1.2.22

              Added

              • Portland Fire (POR) ESPN player-map coveragelib/espn-player-map.js now 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 backendweb/vercel.json now runs /api/data?action=scan-user-slip-dnps every 5 minutes (*/5 * * * *). Retires the hourly pplx-tool cron f61b1388 that was standing in during the shim migration. Backend cron fires against the live shegotgame.app route 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 updates projected_result on user_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).
            • v1.2.21

              Changed

              • components/ScheduleStrip.tsx polling now reduces Vercel edge load. - Tab-visibility gating: the schedule only polls while document.visibilityState === 'visible'. When the tab is hidden the interval is paused; when it becomes visible again the strip refetches immediately and resumes polling (via a visibilitychange listener 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 existing lib/scheduleView.mjs view (view.live) via a new pure pollIntervalMs(view) helper — no duplicated status logic.

              Added

              • scripts/vercel-ignore-build.sh + ignoreCommand in root vercel.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: ScheduleStrip ordered purely by the server bucket field, and the wnba_schedule_landing SQL view derives that bucket from an ET day-string (game_date). Late-night games whose UTC game_date rolls into "today" were bucketed today and floated to the top even after going FINAL — a day-string comparison bug.

              Changed

              • components/ScheduleStrip.tsx is now a client component that orders and filters the schedule from absolute UTC timestamps (never day strings), using a new pure helper lib/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 payload end_time when present, otherwise falls back to start_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_poll StoryStats 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 at tests/regression/schedule-view.test.mjs (8 cases: all-upcoming, live+upcoming, FINAL <3h shown-below, FINAL >3h filtered, empty schedule, explicit end_time cutoff, stable ordering, constants).
              • Optional end_time field on the ScheduleGame type for the FINAL cutoff.

              QA

              • node tests/regression/schedule-view.test.mjs8/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 to master and must pass there before promotion. No baseline snapshot updates were needed (card markup unchanged; only ordering/visibility logic changed).
            • 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 existing computeSlipProbability so Auto-Build ranking matches the sticky slip bar. Exports buildAutoParlays, getPlatformDeepLink (UTM-tagged pick-a-builder link per book, Underdog via affiliate URL), and formatAutoParlayText (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.md v9.9.15.6): OddsAPI-scored legs top out at ~62 confidence vs PP/UD's ~91, so the global slice(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.
            • 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: ellipsis so 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 — wraps api/data.js (17 actions, 3300 lines)
                  • app/api/bdl-stories/route.ts — wraps api/bdl-stories.js (games, pregame, live_poll, postgame, feed, recap, send_recap, recap_sweep)
                  • app/api/dashboard/route.ts — wraps api/dashboard.js
                  • app/api/odds/route.ts — wraps api/odds.js
                  • lib/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.ts no longer defaults to wnba-v700.vercel.app. Server-side fetches fall back to https://shegotgame.app; client-side fetches use relative URLs. NEXT_PUBLIC_API_BASE / API_BASE still honored for split-deploy testing.
                  • web/lib/analytics.ts TRACK_ENDPOINT, SAVE_SLIP_ENDPOINT, and USER_SLIP_STATS_ENDPOINT changed 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_grades and parlay_performance_log because 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.json 1.2.9 while the root parlay engine advances from v9.9.14.1 to v9.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_version column) plus a compact signal_snapshot JSONB 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_version instead of guessing from timestamps. Straddled-deploy days resolve the parlay's logged version by majority-vote across its legs.
                  • Customer-visible endpoints unchanged. /api/version still reports the root engine version (now 9.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 at shegotgame.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.
                • v1.2.8

                  Added — Public changelog page + version badge link

                  • app/changelog/page.tsx — new public route at /changelog. Reads web/CHANGELOG.md at 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-DD release headers and ### section headers, escapes HTML then re-applies a safelisted inline subset (code, strong, http/https links only). Rejected adding remark/marked/gray-matter since 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, LIVE badge on the top entry, and mobile-first spacing. Release cards get a subtle gold ring when they match the running version so the footer’s v number 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 from player_game_stats (players with min_played = 0 on slip_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 because propline_resolutions still shows actual_value = 0. The DNP check runs *before* the resolution lookup so it wins the tiebreak.
                  • Grader response now returns dnp_players count 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 every user_slips row whose slip_result is still pending on the target date, resolves each leg by matching (player, market_key) against propline_resolutions and comparing actual_value to line + direction. Aggregates into legs_won / legs_lost / legs_push / legs_pending and final slip_result (won, lost, push, or pending). Idempotent: re-running only updates rows that gained a new resolution.
                  • api/data.jsgrade-pipeline action now also invokes runUserSlipGrader() 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-stats and 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 — mounts UserSlipHitRateStrip directly under the hero band, above the live-strip and StoryStats feed. First-party pipeline end-to-end: user builds slip → handleSaveSlip persists → nightly grader resolves → view rolls up → strip shows real user hit rate.
                  • supabase/migrations/20260702_v126_view_realign.sql applied to production. Realigns v_user_slip_hit_rate_7d column names + result-label vocabulary to match the grader (won/lost/push/pending, lowercase). View now exposes platform, 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.js so 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, .shareBtn styles; .slipActions now 1fr 1fr 1.2fr for 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() uses navigator.sendBeacon with a fetch keepalive fallback so events survive the page tearing down on "Open in Platform". Session id persists in localStorage as sgg_session_id (never PII). Typed SggEvent union 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 → Supabase app_events — no third-party data leak, no GA4, no cookie banner needed.
                  • api/data.js — new handleTrack handler on the wnba-v700 backend. Inserts events into app_events with 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 — new handleSaveSlip handler (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 — new handleUserSlipStats handler reading v_user_slip_hit_rate_7d view for the upcoming homepage tile.
                  • web/components/ParlayHub.tsx — wires persistSlip() into copy / share / open / debounced-finalize. Uses a slipIdCacheRef map so the same unique leg-set only creates one row, and subsequent actions PATCH flags. Every persist is keepalive: true fire-and-forget — slip persistence errors never surface to the UX.
                  • supabase/migrations/20260702_analytics_and_slips.sql applied to production. Creates app_events + user_slips tables with RLS-deny-anon policies (service key only), plus the v_user_slip_hit_rate_7d view 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 — extended Content-Security-Policy to 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 blocking pagead2.googlesyndication.com, so no ads would ever render and Google's site verification would fail silently in the browser console.
                  • script-src now also allows pagead2.googlesyndication.com, *.googlesyndication.com, *.google.com, *.googletagservices.com, adservice.google.com.
                  • connect-src widened for AdSense telemetry beacons.
                  • frame-src now 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 /parlays page hero: ParlayHub component 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-safe window.open fallback — 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-pool on wnba-v700 monolith returns a broad candidate pool bucketed per platform, with query params platform, market, minConfidence, size (max 200).
                  • Response shape includes book_line, book_price, game_time, opponent, and a stable-hash id per 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) in lib/api.ts filters by game_date against a UTC-anchored 7-day window (avoids timezone drift).

                  Added — Google AdSense (pub-3418444283543157)

                  • Set NEXT_PUBLIC_ADSENSE_CLIENT_ID = ca-pub-3418444283543157 on all three Vercel targets (production/preview/development). Existing AdSenseScript loader 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.ts centralizes 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 ParlayPick of the Day + Builder (metadata, <title>, OpenGraph, Twitter card, and H1). The consensus slip has always been a POTD product; the label now matches.
                • v1.2.2

                  Fixed — PWA external links

                  • Read the full story CTA now opens in system browser instead of trapping inside the installed PWA's own webview. Extracted the link into a client component ExternalSourceLink.tsx that explicitly calls window.open(url, '_blank', 'noopener,noreferrer') with a location.href fallback for popup-blocked scenarios. Modifier-clicks (cmd/ctrl/shift/middle) still use browser-native behavior.
                  • Manifest hardened for external navigation (public/manifest.json): added explicit scope: '/', capture_links: 'none', and launch_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 restoredalternates.canonical on root layout + /parlays + /storystats + /storystats/[slug]. All resolve to https://shegotgame.app via metadataBase. Fixes duplicate-content ambiguity between shegotgame.app and wnba-full.vercel.app.
                  • Host consolidation middleware (middleware.ts) — 308 permanent redirects from wnba-full.vercel.app/* to shegotgame.app/* with X-Robots-Tag: noindex, follow belt-and-suspenders header. Consolidates PageRank + inbound links onto the canonical host.
                  • JSON-LD structured data (components/seo/JsonLd.tsx): - WebApplication schema in root layout (all routes) with SportsApplication category, free Offer, publisher Organization, SearchAction, sameAs socials. - NewsArticle schema on /storystats/[slug] with headline, datePublished, articleSection, keywords from card tags, publisher logo. - BreadcrumbList schema 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=N window (default 1h, max 24h). Externally callable by scheduled tasks to submit fresh slugs to Google Indexing API.
                  • Google Site Verification meta restored via verification.google in root metadata.
                  • Route-specific OG + Twitter for /parlays and /storystats — fixes metadata drift where these routes were leaking the root layout's OG title.
                • v1.2.0

                  Added — Monetization layer (env-gated, invisible until keys set)

                  • Tip Jar — global TipJarButton in TopBar opens TipJarModal with Cash App, Venmo, and PayPal targets. Also TipJarInline blocks on /, /parlays, and /storystats/[slug]. All handles set to the locked SheGotGameSystems (Cash App / Venmo / paypal.me/SheGotGameSystems).
                  • AdSense scaffolding<AdSenseScript /> client component wired into root layout.tsx; <AdSlot /> placeholders on home, parlays, and StoryStats detail. Rendering is env-gated by NEXT_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 stripAffiliateStrip on /parlays links to https://play.underdogsports.com/vgwg/eoqxki8x (the locked, unchanged Underdog affiliate URL).
                  • Contact emailwill@shegotgame.app surfaced 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 to storystats_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.
                • v1.1.1

                  Fixed — Accessibility (Full SOP QA follow-ups)

                  • heading-order (axe moderate, Lighthouse a11y hit) — StoryStatsCard headline changed from <h3> to <h2> so page heading hierarchy is h1 (hero) → h2 (card) on / and /storystats. Previously skipped h2.
                  • label-content-name-mismatch on StoryStatsCard — removed the redundant aria-label={card.headline} on the outer <a>. The card's visible inner text is already a meaningful accessible name.
                  • label-content-name-mismatch on ParlayLegCard toggle button — replaced synthesized aria-label with a visually-hidden .sr-only span prefix ("Add to slip: " / "Remove from slip: "). Accessible name now matches the visible content.
                  • image-redundant-alt on TopBar logo — set alt="" on the decorative <Image> since the sibling wordmark and the link's aria-label already name the link. The logo image is purely decorative.
                  • Added .sr-only utility class to globals.css for 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, /parlays 100, /storystats 100, /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: - ParlayBuilder client 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 - ParlayLegCard with 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.tsgetTodaysParlay(), getTrackRecord() bridges to the monolith with full TypeScript types (ParlayLeg, ParlayResponse, TrackRecordPick, TrackRecordResponse).
                  • Version endpoint at /api/version returning { 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_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY values that had KEY= prefix pasted into the value field. Build-time SSG now succeeds for all /storystats/[slug] routes.

                  Removed

                  • /legacy middleware rewrite. The old monolith remains live at wnba-v700.vercel.app as an untouched instant-rollback backup — no back-door URL on the new site. Rollback = one DNS change on shegotgame.app.
                • v1.0.0

                  Initial Next.js 14 App Router migration. See earlier commits for details.

                  Added

                  • /web scaffolded with App Router, dark forest + cream themes, Satoshi + Cabinet Grotesk from Fontshare.
                  • /storystats feed + /storystats/[slug] detail (Server Components + ISR).
                  • Editorial homepage with hero StoryStats + live strip + gauges.
                  • /admin/model-trends behind 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