Session 50: Complete Parlay Lab (2215 tests)

Correlation-aware combined parlay grading — the Desk-tier differentiator.

- Correlation model (parlayService.js, added to S28 funcs): correlationScore
  (game-aware 0.7/0.4/0.2/0.0), combinedGrade (avg penalized by avgCorr*0.5),
  estimatedPayout (fair-odds product * (1-avgCorr) discount), correlationWarning,
  gradeParlay.
- POST /api/parlay/grade (public, 2-6 legs) -> {combined,correlation,payout,legs}.
  Fixed the Next proxy (was forwarding to /api/scan/parlay).
- ParlayContext: legs gained team/game/archetype; tier-aware maxLegs; auto-grades
  the slip (debounced) when legs>=2 -> live combined/correlation/payout; hasLeg/
  legKey/atCap.
- "+" button on every graded prop: StatStrip onAddLeg/isLegActive, wired by
  vyndr/GameCard via useParlay (builds leg w/ team + game). GradeResultCard feeds
  the same context from the scan page.
- ParlayPanel (replaces legacy ParlayTray): bottom slide-up w/ legs, combined
  grade, correlation warning, est payout, CLEAR ALL + floating leg-count badge.
  Tier-gated: free 2 legs (payout blurred -> Desk upsell), Analyst 4, Desk 6.

Backend 2185 -> 2215 tests (+30), 187 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-19 11:25:14 -04:00
parent 3b47b783dc
commit f1956dc953
14 changed files with 706 additions and 48 deletions
+16
View File
@@ -33,6 +33,22 @@ router.post('/calculate', (req, res) => {
}
});
// POST /api/parlay/grade (Session 50) — the Parlay Lab. Returns
// { combined, correlation, payout, legs }. Public + stateless (the lightweight
// math); the Lab UI is tier-gated on the frontend. 26 legs.
router.post('/grade', (req, res) => {
const legs = req.body?.legs;
if (!Array.isArray(legs) || legs.length < 2 || legs.length > 6) {
return res.status(400).set(MISSION_HEADER).json({ error: 'A parlay needs 26 legs.' });
}
try {
const betAmount = Number(req.body?.betAmount) || 10;
return res.set(MISSION_HEADER).json(parlayService.gradeParlay(legs, betAmount));
} catch (err) {
return res.status(400).set(MISSION_HEADER).json({ error: err.message });
}
});
router.post('/suggestions', (req, res) => {
try {
const { props, legs, max } = req.body || {};