PREVIEW · read-only · draft

Player Stats desktop v2: proof

Branch claude/stats-desktop-v2 @ 646bb617 (after) vs #1312 @ 02267c9a (before). Nothing is merged or deployed. Screenshots: headless Chrome, 1000 px tall, dark theme, the built preview pages served locally with every API answer replayed from production's own 2025 answers (read by GET on 10-15) and #1314's stored 2025 boards — zero production reads. Real Safari and authenticated MFL were not used.

← back to the preview

1. Before and after, at each desktop width

WR · 2025 · MFL Total. Left: #1312 as it is. Right: v2. Same data in both.

1024 px

Before · Fantasy points · 1024
Before · Fantasy points · 1024
After · Scoring · 1024
After · Scoring · 1024
Before · Schedule context (Sched Δ + Opp ±%) · 1024
Before · Schedule context (Sched Δ + Opp ±%) · 1024
After · Schedule (Opp. allowed only) · 1024
After · Schedule (Opp. allowed only) · 1024

1280 px

Before · Fantasy points · 1280
Before · Fantasy points · 1280
After · Scoring · 1280
After · Scoring · 1280
Before · Schedule context (Sched Δ + Opp ±%) · 1280
Before · Schedule context (Sched Δ + Opp ±%) · 1280
After · Schedule (Opp. allowed only) · 1280
After · Schedule (Opp. allowed only) · 1280

1440 px

Before · Fantasy points · 1440
Before · Fantasy points · 1440
After · Scoring · 1440
After · Scoring · 1440
Before · Schedule context (Sched Δ + Opp ±%) · 1440
Before · Schedule context (Sched Δ + Opp ±%) · 1440
After · Schedule (Opp. allowed only) · 1440
After · Schedule (Opp. allowed only) · 1440

2. Inside the MFL frame

MFL's header, then the Stats message slot loaded by that version's own loader (the page in an iframe); 2025 chosen inside the frame. The frame opens on QB, the page's default.

1024 px

Before in the frame · 1024
Before in the frame · 1024
After in the frame · 1024
After in the frame · 1024

1280 px

Before in the frame · 1280
Before in the frame · 1280
After in the frame · 1280
After in the frame · 1280

1440 px

Before in the frame · 1440
Before in the frame · 1440
After in the frame · 1440
After in the frame · 1440

3. The new views and controls (after)

1024 px

Heatmap · 1024
Heatmap · 1024
Scatter · 1024
Scatter · 1024
About these numbers (dialog) · 1024
About these numbers (dialog) · 1024
More filters open · 1024
More filters open · 1024
Status banner → See why: a server on the older Sched version (never called 'not published') · 1024
Status banner → See why: a server on the older Sched version (never called 'not published') · 1024
Before: notes above the table, About at the bottom · 1024
Before: notes above the table, About at the bottom · 1024

1280 px

Heatmap · 1280
Heatmap · 1280
Scatter · 1280
Scatter · 1280
About these numbers (dialog) · 1280
About these numbers (dialog) · 1280
More filters open · 1280
More filters open · 1280
Status banner → See why: a server on the older Sched version (never called 'not published') · 1280
Status banner → See why: a server on the older Sched version (never called 'not published') · 1280
Before: notes above the table, About at the bottom · 1280
Before: notes above the table, About at the bottom · 1280

1440 px

Heatmap · 1440
Heatmap · 1440
Scatter · 1440
Scatter · 1440
About these numbers (dialog) · 1440
About these numbers (dialog) · 1440
More filters open · 1440
More filters open · 1440
Status banner → See why: a server on the older Sched version (never called 'not published') · 1440
Status banner → See why: a server on the older Sched version (never called 'not published') · 1440
Before: notes above the table, About at the bottom · 1440
Before: notes above the table, About at the bottom · 1440

Player drawer · 1280

Click a row: the drawer (unchanged; its cost is #1315)
Click a row: the drawer (unchanged; its cost is #1315)

4. Wireframes

Table
Table
Heatmap
Heatmap
Scatter
Scatter

5. Checks on every screenshot

Page-level sideways scroll (scrollWidth > clientWidth), rows drawn, and the status banner's first line. Writes refused: 0. Requests to production: 0 (every worker and data-file request was answered from recorded answers; only Google Fonts loaded from outside). Page errors: 0.

ScreenshotSideways scrollRowsBanner
before_wr_scoring_1024no239—
before_wr_schedule_1024no239—
before_bottom_about_1024no239—
after_wr_scoring_1024no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_wr_schedule_1024no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_heatmap_1024no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_scatter_1024no0—
after_about_1024no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_more_1024no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_banner_version_1024no239⚠ Schedule stats unavailable — the stats server is older than this page See why +1 more ·
mflframe_before_1024no78—
mflframe_after_1024no78ⓘ Playoff-week MFL points are stored for rostered players only See why
before_wr_scoring_1280no239—
before_wr_schedule_1280no239—
before_bottom_about_1280no239—
after_wr_scoring_1280no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_wr_schedule_1280no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_heatmap_1280no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_scatter_1280no0—
after_about_1280no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_more_1280no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_banner_version_1280no239⚠ Schedule stats unavailable — the stats server is older than this page See why +1 more ·
mflframe_before_1280no78—
mflframe_after_1280no78ⓘ Playoff-week MFL points are stored for rostered players only See why
before_wr_scoring_1440no239—
before_wr_schedule_1440no239—
before_bottom_about_1440no239—
after_wr_scoring_1440no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_wr_schedule_1440no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_heatmap_1440no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_scatter_1440no0—
after_about_1440no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_more_1440no239ⓘ Playoff-week MFL points are stored for rostered players only See why
after_banner_version_1440no239⚠ Schedule stats unavailable — the stats server is older than this page See why +1 more ·
mflframe_before_1440no78—
mflframe_after_1440no78ⓘ Playoff-week MFL points are stored for rostered players only See why
after_drawer_1280no78ⓘ Playoff-week MFL points are stored for rostered players only See why

6. Live check of this deployed preview (one load, 9:36 PM ET)

After page, WR · 2025 · MFL Total, loaded once from this site in headless Chrome. The player list came from #1314's stored file (lb1/6b8fbdf84c3e8a17/2025/skill_1-17.json.gz, HTTP 200): 0 D1 rows. 12 requests on load, all GET; then Heatmap → Scatter → Table → Fantasy team = Free agents made 0 more. 239 players listed, 143 free agents. Writes blocked: 0. Page errors: 0.

Live: Free agents chosen, no reload · 1280
Live: Free agents chosen, no reload · 1280

7. The review (docs/STATS_DESKTOP_REVIEW.md)

Desktop Player Stats — first-principles review (Keith, 2026-10-15)

The goal is an easy, uncluttered desktop page, not keeping every feature. This review covers the page as #1312 left it (on top of #1305) and proposes what an owner sees instead. It is a DRAFT for review: nothing merges or deploys from it.

The owner's questions (what the page is for)

  1. Who has performed best?
  2. How much has he played?
  3. Is it sustainable or driven by opportunity?
  4. How does he compare with others at his position?
  5. What schedule has he already faced?
  6. What changed over the selected weeks?
  7. Should I open his detailed view?

Each question gets ONE place to answer it — a view, a stat group, or the player drawer — never all seven as simultaneous columns.

QuestionWhere it is answered
1 Best performersTable · Scoring (the default)
2 How much he playedTable · Opportunity
3 Sustainable or opportunity?Scatter (volume × efficiency) · Table · Efficiency · Consistency
4 Compared with his positionHeatmap (percentile profile) · PPG Rk
5 Schedule facedTable · Schedule (one measure)
6 What changed in these weeksWindow (presets / custom weeks) · drawer By week
7 Open the detailClick the row → player drawer (By week · By season · Full profile)

1. Inventory of the current page (#1312) and what happens to each item

Classification: Keep (primary) · View (dedicated view) · More (More filters) · Drawer · Combine · Remove.

Header and navigation

ItemNowDecisionWhy
"UPS · Advanced Stats" eyebrow + large titletopCombine → one compact title rowthe hero title costs ~90 px above every table
Standfirst paragraph ("Every NFL player at a position…")under titleRemove (into About)permanent text an owner reads once
Search players (whole league, /)top rightKeepfinds anyone, incl. players not in this list
Light/dark toggletop rightKeep (small)the MFL frame is dark; some owners print light
View tabs: Player Stats · Fantasy Points Against · ADP · Vegas · Pace · Schedulebelow titleKeep (label "Pts Agst")separate boards; out of scope here
About these numbersbottom of the page (<details>)Keep → top, a button beside the title opening a compact modalKeith item 1

Primary controls

ItemNowDecisionWhy
Season (single; "2024 + 2025" when seasons are added)row 1KeepQ1–Q6
Window (MFL Reg · Playoffs · Total · Full + NFL playoffs · Custom weeks…)row 1Keep; custom week pair inline only when CustomQ6
Position (QB RB WR TE Flex · DL LB DB IDP Flex · PK PN)row 1Keepevery question is per position
DE/DT and CB/S sub-tabsinside DL/DBKeep, unchanged: they appear only inside the chosen DL or DB buttonno extra row; IDP owners do split edge from interior
Fantasy team (multi-select, ⌘-click)More filtersKeep → primary, a single select: All players · Rostered · Free agents · each teamowners ask "best free agents?" constantly; multi-team ⌘-click is unusable
Stat group chips (8–10 per position)row 2Keep, fewer and renamed (below)Table only
View switch Table · Heatmap · Scatter— (Scatter/Heat hidden in More → Tools)New, primaryKeith item 2
Find in this listrow 3Keep (start of row 3)
Sort by select + High/low buttonrow 3Removeheaders already sort (click / Enter), with the arrow showing
Player count ("226 players")row 3 rightKeep (row 3 right, with the window and freshness)
Resetinside More filtersKeep → small text button at the end of row 3Sched pattern: available, not loud

More filters panel

ItemDecisionWhy
Add seasons (multi-select)More (as "Add seasons")multi-season totals are niche
Value filters (per-column min/max)Morepower use; shown as "Filters in use" chips
Column sets (Standard · Advanced · Analytics · Custom) + saved sets + the 41-group custom pickerMore, one entry "Choose columns…"keeps every column reachable without 4 presets competing with stat groups
Select players to compare (checkbox column + compare bar + compare drawer)Moreoccasional
Scatter, Heat buttonsRemove from here → View switchKeith item 2
Download CSVMore

Notes, warnings and the band

ItemDecisionWhy
Context line "WR · 2025 · MFL Total · Sources: … · read 8:45 PM"Combine → muted "Wks 1–17 · final · read 8:45 PM" at the end of the filters line; sources → About
"No data reported for these columns…"Remove → status banner (cause-specific) + empty columns hiddenKeith item 3
"Sample too small for…"Combine → banner ("small sample") + ·small n on the header
"The list is the stats source's top 500 …"Remove → banner only when the worker says the list was cut (next_offset), cause "source limit"stored boards (#1314) aren't cut at all
"No stats for 2019 … pick an earlier one"Remove → banner with the real cause (no players match / not published / request failed / stored copy unusable)it was shown for failures that weren't "no stats"
Sched Δ / Opp ±% explanation paragraphRemove → About (Schedule group) + banner only if unavailable
Ownership note ("Couldn't read all of MFL's rosters…")Combine → banner, cause "request failure (MFL rosters)"
Source-freshness noteCombine → banner when stale / partial; otherwise the muted freshness text
EPA mixed-definition / RZ mixed notesCombine → banner cause "source definitions differ"

Table

ItemDecision
Frozen # and Player (name + "TEAM · Owner")Keep
Group header row, heat classes on cellsRemove from Table (Heatmap view does color)
Header sort, aria-sortKeep
Contained scrolling (table scrolls inside its box, header row sticky)Keep
Columns with no data for the selectionHide and say so in the banner
Compare checkbox columnMore (only while compare is on)
Sparkline (cons_spark)Drawer (weekly chart already there)

Stat groups (Table view)

NowDecision
Fantasy points (PPG Rk, Pts, PPG, G)Keep as Scoring (default)
UsageKeep as Opportunity (snaps %, carries/targets, shares)
Passing / Rushing / ReceivingKeep as box-score volume only
Next GenCombine with the per-play columns → Efficiency (Y/A, Y/Tgt, Catch %, aDOT, separation, RYOE…)
Red zoneKeep
IDP / Kicking / PuntingKeep
Boom/BustKeep as Consistency (floor, ceiling, boom %, bust %, recent vs window)
Schedule context (Pts, Sched Δ, Opp ±%, G)Replace with Schedule (MFL Pts, G, one measure — see §5)
Custom columnsMore → "Choose columns…"
xFP (2025 only), PFF separation (2025 only), market, dynasty profile, FTN chartingMore (custom columns) — not in any default group

Views and tools

ItemDecision
Scatter (usage × efficiency, axis pickers)View — "Scatter" with a per-position question preset; custom axes in the view
Heat (percentile cell color)View — "Heatmap": a fixed per-position profile (8 measures), not color on every column
Compare drawerMore
CSVMore

Player drawer

ItemDecision
Row/name click → drawer (By week · By season · Full profile ↗)Keep
By season (all years)Keep, but fix cost first (#1315: ~1.8M D1 rows per tap today)
Weekly chartKeep

2. Recommended page structure (top → bottom)

  1. Title row: "Player Stats" · [ⓘ About these numbers] · … · [⌕ Search players /] [◐]
  2. View tabs: Player Stats · Pts Agst · ADP · Vegas · Pace · Schedule
  3. Filter row (one line at 1024+): Season · Window (+ weeks when Custom) · Position (DE/DT · CB/S inside DL/DB) · Fantasy team
  4. View row: [Table | Heatmap | Scatter] · then, for the chosen view only: Table → stat group chips · Heatmap → color legend · Scatter → question
  5. Find row: Find in this list · [More filters · n] · "In use:" chips (only when More filters are set) · … · "226 players · Wks 1–17 · final · read 9:06 PM ET" · Reset
  6. Status banner (only when something is limited or failed): one line, "See why"
  7. The view (table box / heatmap box / scatter box), scrolling inside itself
  8. Drawer on row/dot click

Switching Table ↔ Heatmap ↔ Scatter re-renders the rows already loaded: no request, no reset.

3. The status banner — real causes only

CauseDetected byExample text
Server/client version mismatchan answer in the older shape (ok but no by_mfl) or a missing field the page needs"Schedule stats unavailable — the stats server is older than this page"
Source hasn't published itthe column's source has no row for this season/window (season before coverage, or not yet published)"Next Gen isn't published for 2015 (starts 2016)"
Stored copy missing or invalidstored_board_unavailable (#1314)"The stored 2019 board couldn't be read (checksum mismatch)"
Filters legitimately return no playersrows loaded, filters leave 0"No players match Free agents + min 8 games"
Player omitted by a source limitthe worker says more exists (next_offset)"List stops at 500 players (source limit)"
Request failureHTTP / network error"Couldn't load Consistency (HTTP 503) — Retry"

"See why" opens: cause · what it affects · what it is NOT · what to do. Never "not published" for a version mismatch.

4. Wireframes

See wire_table.png, wire_heatmap.png, wire_scatter.png (1280 px) in the review page.

5. Sched Δ and Opp ±%

Opp ±%Sched Δ
Owner question"Did the defenses he faced give up more or less than usual to his position?""How many points would he have scored against an average schedule?"
Calculation (#1305 engine)For each of his final games, the opponent is rated by what it allowed to the OTHER players at his position ÷ those players' usual (his games left out); Opp = mean rating. +5% = allowed 5% more than usualSched Δ = his points ÷ Opp − his points
DifferenceThe schedule itself, independent of the playerThe same number restated in his points: same sign meaning reversed (Pickens 2025: Opp −5%, Sched Δ +17.1)
Understood without a note?Mostly, with one sentenceNo — needs the division explained, and the sign confuses
Changes a real comparison? (2025, starters, Wks 1–17)Small over a full season: 80% of starters within about −4% to +4%; median 0.1–0.7 PPG; median 1 rank place; flips 10–27% of near-ties (within 1.0 PPG)identical information
Over short windows (Wks 1–6 / 7–12 / 13–17)Meaningful: QB/TE 80% band ±9–25%; median 0.4–2.9 PPGidentical information
AvailabilityEvery season 2011–2025 (stored Pts Agst inputs) and the current season (live); any window. Older playoff weeks are thin (#1307)same
CostFinished seasons: 0 D1 (stored); current season: one ~60k-row answer per season/windowsame answer

Recommendation (built in the preview): remove both from the general table. Keep ONE measure — "Opp. allowed" (header; its definition reads "Opponents allowed vs usual", i.e. Opp ±% with a plain name) — in the Schedule stat group with MFL Pts and G, described as the schedule already faced, not a forecast. It appears nowhere else, including Custom columns. Remove Sched Δ from the owner page (keep it in the API for research). It earns its place mainly for short windows; over a full season it rarely moves a comparison, and the About text says so.

6. Data and cost dependencies

7. Changes to #1305 and #1312

#1305 (Sched Δ data): keep the engine, the stored 2011–2025 inputs and /api/sos-adjusted-points unchanged. Its page changes are replaced: no Sched Δ column on the owner page; Opp ±% appears only in the Schedule group as "Opp. allowed"; the Sched explanation paragraph goes to About.

#1312 (desktop redesign): superseded by this branch (claude/stats-desktop-v2, stacked on it): About moves to a top modal; the filter row gains Fantasy team (single select); Find, More filters and a small Reset share one row; a Table · Heatmap · Scatter switch; the Sort-by select goes (headers sort); More filters keeps Add seasons, value filters, Choose columns…, Compare, CSV; the band's paragraphs become one cause-specific status banner; empty columns are hidden; stat groups become Scoring · Opportunity · [box score] · Efficiency · Red zone · Consistency · Schedule; docs/STATS_UI_CONTRACT.md is rewritten to match.

#1312's MFL frame fix (the full-height pane) is kept as is.