M1b screen 01: the flat EDGE BOARD — Design's mobile board IA

The one genuinely-new mobile screen. Design's mobile BOARD is a FLAT edge-ranked
list (all graded props across every game on one list, sorted by edge) — not the
desktop's game-grouped cards. Implemented to the drawing with REAL snapshot data:
- slateAdapter.flattenToEdgeBoard(cards) — pure transform of the assembled
  GameCardData[] (grade→game join already done) into ranked rows, edge desc.
  STRICT null edge sorts LAST (never 0-coerced to the top — Data Semantics Rule).
  Threaded edge_pct through buildPlayerStripsFromProps (was dropped). 6 unit tests.
- MobileEdgeBoard component — Design's exact screen-01 rows: rank (green #1),
  player + prop, matchup sub-line with TeamChips + live-dot, tier grade chip,
  and the edge% as the one bold mono hero (green +, red −). Ranked opacity ramp
  (1 → .55) + green inset border on the top reads. Breadth strip EDGES/AVG CLV/
  GAMES — CLV honest '—' (per-slate CLV isn't computed; never fabricated).
- Slate: <768px renders the flat board, ≥768px keeps game cards (same data,
  toggled by width). Ungraded slate still shows game cards on phones (no blank).

Built to Design's screen-01 drawing, VISUALLY UNVERIFIED at 390px — the core
mobile screen, top of the master-audit list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-16 21:46:47 -04:00
parent f02dc4a6d5
commit 8a24ac9129
5 changed files with 298 additions and 11 deletions
+69
View File
@@ -382,6 +382,9 @@ function buildPlayerStripsFromProps(gameProps, gradeIndex, deltaIndex, now = Dat
line: rec.line,
side,
grade: rec.grade,
// Rev 3 flat edge-board (mobile screen 01) — the ranked hero figure.
// Strict null (never 0-coerced) so an absent edge sorts last, not top.
edge: (rec.edge_pct != null && Number.isFinite(Number(rec.edge_pct))) ? Number(rec.edge_pct) : null,
// A1 S3 — the prop's own book + the best available price across the
// game's book rows for the graded side (null unless ≥2 books at the
// same current line disagree — see detectBestBook).
@@ -595,6 +598,69 @@ function pitchersForGameTeams(awayTeam, homeTeam, pitcherMap) {
return { away: one(a), home: one(h) };
}
// ── Rev 3 — flat EDGE BOARD (mobile screen 01) ──────────────────────────
// Design's mobile board is a FLAT, edge-ranked list (all graded props across
// every game, sorted by edge), not game-grouped cards. This flattens the
// GameCardData[] the Slate already assembled (grade→game join already done)
// into ranked rows. Pure + testable. Sort: edge desc (STRICT null sorts LAST —
// never 0-coerced to the top), grade rank as tiebreak.
const EDGE_GRADE_RANK = { 'A+': 0, A: 1, 'A-': 2, 'B+': 3, B: 4, 'B-': 5, C: 6, D: 7, F: 8 };
function edgeGradeRank(g) {
return EDGE_GRADE_RANK[g] != null ? EDGE_GRADE_RANK[g] : 9;
}
function flattenToEdgeBoard(cards) {
const rows = [];
for (const c of Array.isArray(cards) ? cards : []) {
const away = (c.away && c.away.abbr) || '';
const home = (c.home && c.home.abbr) || '';
for (const strip of (c.playerStrips || [])) {
for (const p of (strip.props || [])) {
if (!p || !p.grade || p.awaiting || p.dead) continue; // only live graded rows
rows.push({
player: strip.player,
team: strip.team || '',
stat: p.stat,
statType: p.statType,
line: p.line,
side: p.side || 'O',
grade: p.grade,
edge: (p.edge != null && Number.isFinite(p.edge)) ? p.edge : null,
sport: c.sport || '',
away,
home,
live: !!c.live,
outcome: p.outcome || null,
movement: p.movement || null,
playerId: strip.playerId,
espnId: strip.espnId,
headshotUrl: strip.headshotUrl,
});
}
}
}
rows.sort((a, b) => {
const ea = a.edge == null ? -Infinity : a.edge;
const eb = b.edge == null ? -Infinity : b.edge;
if (eb !== ea) return eb - ea;
return edgeGradeRank(a.grade) - edgeGradeRank(b.grade);
});
return rows.map((r, i) => ({ ...r, rank: i + 1 }));
}
/** Breadth-strip numbers for the mobile board header (Design screen 01):
* EDGES = count of A/B-tier graded rows · GAMES = games with ≥1 graded row.
* AVG CLV is NOT computed here (comes from /api/accuracy; honest '—' when
* absent — Design draws it but the row-level model doesn't produce per-slate
* CLV). Never fabricates. */
function edgeBoardBreadth(rows, games) {
const list = Array.isArray(rows) ? rows : [];
const edges = list.filter((r) => {
const t = String(r.grade || '')[0];
return t === 'A' || t === 'B';
}).length;
return { edges, games: Array.isArray(games) ? games.length : 0 };
}
module.exports = {
parseAmericanOdds,
detectBestLines,
@@ -611,6 +677,9 @@ module.exports = {
statShort,
gradedAgo,
buildPlayerStripsFromProps,
flattenToEdgeBoard,
edgeBoardBreadth,
edgeGradeRank,
buildPitcherMap,
pitchersForGameTeams,
// DS2 — dashboard rebuild engine.