Wave 5B: Pitcher Arsenal via Baseball Savant (Statcast)
D4 — build the FREE Baseball Savant adapter for pitch-level identity
(mix / velo / usage% / whiff%), the missing layer statsapi doesn't carry.
- savantAdapter.getPitcherArsenal(id|name) — normalizes two public Savant
CSV leaderboards (csv=true, NO parsing dependency): pitch-arsenal-stats
(usage% + whiff% + K%) + pitch-arsenals avg_speed (velo). League-wide,
cached 24h + in-memory mirror, indexed by MLBAM id. Defensive: null on any
unrecognized shape; a missing velo/whiff is ABSENT (null), never 0.
Injectable (fetchImpl/statsCsv/veloCsv/resolveId) → tests hit no network.
Live endpoints VERIFIED (200, exact columns) from the sandbox.
- GET /api/stats/pitcher/:name/arsenal (stats.js) + Next proxy. MLB-only;
an error/miss returns { found:false } so the card self-hides honestly.
- PitcherArsenal.tsx (+ barrel) — the mockup's PITCHER IDENTITY strip:
pitch mix % + velo + whiff%, mono/tabular, ranked by usage, sharpest-whiff
pitch highlighted green. Self-hides (heading included) when arsenal absent.
Mounted on the MLB player profile (a pitcher surface). Context, not a
graded market value.
- Tests: savantAdapter (fake CSV → ranked arsenal; unknown shape/blank cells
→ absent not 0; name→id resolve) + PitcherArsenal source locks (self-hide,
mono/tabular, em-dash-not-zero). +2 suites / +17 tests (3012 → 3029).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000';
|
||||
|
||||
/**
|
||||
* Pitcher arsenal proxy (Wave 5B). Forwards GET
|
||||
* /api/stats/pitcher/:name/arsenal to the Express stats route (Baseball Savant
|
||||
* pitch mix / velo / whiff). Thin pass-through; preserves ?sport=. On any
|
||||
* upstream failure it returns { found:false } so the PitcherArsenal card
|
||||
* self-hides honestly rather than showing an error.
|
||||
*/
|
||||
export async function GET(req: NextRequest, ctx: { params: Promise<{ name: string }> }) {
|
||||
const { name } = await ctx.params;
|
||||
const qs = req.nextUrl.search;
|
||||
try {
|
||||
const upstream = await fetch(
|
||||
`${BACKEND_URL}/api/stats/pitcher/${encodeURIComponent(name)}/arsenal${qs}`,
|
||||
{ method: 'GET', headers: { Accept: 'application/json' } },
|
||||
);
|
||||
const data = await upstream.json().catch(() => ({ found: false }));
|
||||
return NextResponse.json(data, { status: upstream.ok ? 200 : upstream.status });
|
||||
} catch {
|
||||
return NextResponse.json({ found: false }, { status: 200 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user