Sem Panela
Lista de presença, sorteio de times equilibrados e controle de pagamentos da sua pelada.
翻譯準備中
尚未收集儲存庫資訊。
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗A public site for your ESPN league (ID 78683444): standings, weekly matchups, rosters, custom power rankings, and hand-curated league history.
Every page works today with clearly-fake preview data (see data/mock-data.ts).
Nothing breaks or looks empty before you connect ESPN — a small banner just
says "preview data" until the two environment variables below are set.
league-hub) and drag-and-drop
this whole folder's contents in through GitHub's web upload — no local git
needed, same as before.ESPN_S2 — from your browser's ESPN cookies (see below)ESPN_SWID — same place, includes the curly bracesESPN_LEAGUE_ID — 78683444 (already the default if you skip this)ESPN_SEASON_YEAR — e.g. 2027This needs a computer (mobile browsers don't expose this):
https://fantasy.espn.com.espn_s2 and SWID values into Vercel as above.espn_s2 occasionally expires and needs refreshing (no fixed schedule).
SWID tends to be stable for a long time.
The admin area lets you post Weekly Awards, matchup previews/recaps, keepers, and team logos — all from forms, no code editing. Three things to set up in Vercel, all one-time:
ADMIN_PASSWORD with any password you choose. That's what gates /admin.DATABASE_URL
environment variable — no manual setup. The first time any admin page
runs, the app automatically creates the tables it needs.BLOB_READ_WRITE_TOKEN automatically.Then visit yoursite.vercel.app/admin, log in with the password from step 1,
and you're in. There's also a small "Commissioner" link in the site's footer
so you don't have to remember the URL.
ANTHROPIC_API_KEY (see below);
everything else on the site works fine without it./admin/keeper-import)
pulls them in bulk from the Keeper History Google Sheet (one tab per season):
preview first, add-only (keepers already on the site are skipped, so it's safe
to re-run as 2027 keepers come in). It matches the sheet's manager names to
Managers and uses the team linked to them for that season (falling back to their
latest earlier team when the season isn't linked yet). The keeper type and
position are saved in the note. The sheet must be shared as "Anyone with the
link" for the site to read it, otherwise use the paste box. Parsing is in
lib/keeper-import.ts.ANTHROPIC_API_KEY with
that key as the value.Without this set, every other part of the site (including the stat-based "Suggest from stats" button, which needs no AI) works exactly the same — only the "Generate draft" button is disabled until this is added.
Past seasons are found automatically. The site asks ESPN which earlier seasons it has for this league (the last dozen years), so Standings, Matchups, Rosters, the Luck Chart, and League History all offer the same season buttons with no import needed. A season the league didn't play on ESPN (the Fantrax year) simply isn't found there -- it still appears on League History once you write an entry for it.
lib/played-elsewhere.ts.Season labels are ESPN's own season numbers (e.g. "2026"), the same numbering the rest of the site uses.
Entering a season played on another platform (admin > Fantrax Scores). First tag the season "Played on Fantrax" on the League History admin page (this switches ESPN's data for that year off everywhere). Then paste that season's weekly scores, one game per line:
week, home team, home score, away team, away score
(add ,playoff to a playoff game; put a team name in "quotes" if it contains a
comma). A live preview checks the paste as you type -- bad numbers, a team
playing itself, a team in two games in one week, and (as warnings) a missing
game or a misspelled team name -- and shows the standings it would produce so
you can check them against Fantrax before saving. Saving again replaces the
season's games, which is how you correct a mistake. Then link each team to its
manager on the same page.
The site then works that season out from the scores: Standings, Matchups, the Luck Chart (public page and image) and League History all show it, labelled as entered by hand. Rosters aren't tracked for it. Standings are computed from regular-season games (win-loss, then points for), so they should match Fantrax's own unless Fantrax used a different tiebreaker.
Linking managers to teams (admin > Link Managers). The Records page adds up each manager's history, so every past team needs a manager. Pick a year and that year's own team names are listed, each with a manager dropdown that saves the moment you choose. The number on a year is how many of its teams still need a manager (a tick means done). Shortcuts: Copy links from fills this year's unlinked teams from the same team slot last year; fill other years (next to a linked team) uses that manager for every other year where the team is still unlinked; for the Fantrax year, Auto-link teams named after managers does all ten in one tap. Linking only changes the manager -- it never overwrites a saved team name, note, or record.
Playoffs and games that don't count (admin > Playoffs & Rules). For each
finished season you set the week the playoffs began; every game from that week
on is a playoff game and is left out of standings, the Luck Chart, and all-time
totals. This overrides whatever ESPN (or the pasted scores' ,playoff marker)
said, so it doesn't depend on ESPN flagging playoffs correctly; leave it blank to
use the data's own flags. You can also mark individual games (or a whole week) as
"doesn't count" -- for games before the playoffs that shouldn't be counted, e.g.
played after a team was eliminated. Games in playoff weeks are already left out of
the regular season, so those don't need ticking. When a season has any ruling, its
Standings and League History table are recomputed from the games that count
(ESPN's own win-loss totals can't know about your rulings); with no rulings, ESPN's
numbers are used as before. Rulings apply to finished seasons; the current
season keeps using ESPN's live flags. The Matchups page labels playoff games and
games that don't count. Logic: lib/season-rules.ts.
The Records page (public, /records) totals up every finished season -- ESPN and the hand-entered Fantrax one together, regular season only:
Titles and runner-ups come from League History: the champion/runner-up you
type there has to match that season's team name (spelling and capitalization
don't matter, spacing is ignored), otherwise the page says which one it
couldn't match. Any team not yet linked to a manager is left out of the totals,
and the page says how many. The maths lives in lib/records.ts and was
cross-checked against a separate implementation on generated data; the database
functions were run against a real Postgres engine.
The Matchups and Rosters pages both have a season selector (seasons are found automatically -- see above). Picking an older season pulls that season's actual schedule, scores, and rosters straight from ESPN — the same live-fetch approach as the current season, just pointed at a different year. Nothing is copied into the database for this; it's fetched fresh each time (and cached for a day, since past seasons don't change).
Confirmed and fixed. ESPN's roster response only ever contains season-
aggregate stat buckets (scoringPeriodId: 0, distinguished by
statSplitTypeId instead -- season total, last-7-days, etc.) unless the
request explicitly asks for a specific scoringPeriodId. Without it, a
past season's roster fetch contained no real per-week data at all, no
matter which week the code searched for afterward -- confirmed directly
against the live league via the admin Roster Stats Probe page (still there
for future debugging). getWeeklyPlayerStats in lib/espn.ts now always
asks ESPN for the specific week explicitly. This fixes Graphics, the
Weekly Awards "Suggest from stats" button, and Matchup Blurbs' draft
suggestion for any week, current or past. As a side effect it also fixes a
latent caching bug: different weeks of the same past season used to share
one cached response (up to 24 hours) since the week wasn't part of the
request URL at all; each week now gets its own.
Graphics (in the admin area) generates shareable images — 3 Stars,
Top 3 by Position, Team of the Week — from the same real weekly stats as
the Awards "Suggest" button, no admin curation needed, just pick a week.
Matches the visual style approved on the design canvas. Right-click or
press-and-hold an image to save it; there's also a Download link. Uses
next/og (built into Next.js — no extra service or API key).
Player headshots: each player's photo circle uses their real ESPN
headshot when one's on file, and falls back to the same initials-circle
style as before when it isn't (ESPN doesn't have a photo for every player).
The photo is checked for real before the image is generated (with a strict
timeout), so a missing or slow photo can never break the graphic -- worst
case, that one player just shows initials. The URL pattern
(a.espncdn.com/i/headshots/nhl/players/full/{id}.png) is documented and
was verified against a real local test image end-to-end (circular clipping,
crop-not-stretch, colored borders, and the fallback all confirmed working),
but this environment can't reach ESPN's own image servers to test against
them directly, so the very first real check of ESPN's actual photos happens
on a live deployment. Logic lives in lib/headshots.ts (the availability
check) and lib/og-avatar.tsx (the shared avatar element).
All three graphics also take an optional ?season=YYYY (and a matching
field on the admin Graphics page) to pull a past season's already-
completed weeks instead of the current one -- handy before the current
season has any stats posted yet, e.g. to check real players' photos show
up correctly without waiting.
Luck Chart: what each team's record would be if it played every other team every week ("all-play" -- the "expected" record), against the record it actually has. The Luck column is actual win% minus expected win%: green = lucky (won more than the all-play record says), red = unlucky, shaded darker for bigger gaps, and the luckiest and unluckiest team are called out above the table. Also shown: median points per week vs. the league median, and "Chg" (places moved in expected-win% ranking since the week before). Ties count as half a win.
It appears in two places: as a live table on the public Power Rankings
page (season buttons, plus arrows to step through weeks; a past season
opens on its end-of-season chart), and as a shareable image on the admin
Graphics page (type a season there for a past season's final chart).
Past seasons come straight from ESPN, so a season button appears for any
season you've imported records for (or add ?season=2026 to the address).
Only completed regular-season games count -- playoff games and bye
weeks are excluded, since only some teams play them. Playoff games are
recognised by ESPN's playoffTierType field, which is the one thing here
not yet confirmed against a real league: if a past season's chart looks
off (say, an extra week or two of very lopsided results), that's the first
place to look (getMatchups in lib/espn.ts). The math is in
lib/luck.ts and was cross-checked against a separate implementation on
test data.
Player Spotlight: one featured player per week (top scorer by default,
or narrow by position on the Graphics page; ?playerId= picks a specific
player, ?season= a past season) with headshot, points, and stat tiles --
goals/assists/shots for skaters, saves/SV%/GA/wins for goalies. The ESPN stat
ids come from the community espn-api project's hockey map, NOT verified
against this league: open /api/admin/graphics/player-spotlight?week=N&debug=1
(logged in) to see a player's raw stat ids and, if a tile looks wrong, fix the
ids in lib/espn-stats.ts (the only place they live).
Weekly Checklist (/admin/weekly): pick a week and see what's done
(matchup blurbs, awards, writeup) with links to the tool for each step,
including Generate Graphics opened to that week. It's on the admin dashboard.
More graphics (all on the admin Graphics page, all with a download link):
?season=.lib/head-to-head.ts from
regular-season results (the same games the Records page counts) plus this
season's earlier weeks, following the MANAGER so a renamed team keeps its
history. Needs the teams linked to managers under Link Managers; a game with an
unlinked team, or two managers who have never met, just shows "VS".lib/portrait-graphics.tsx (frame()).statLine in lib/espn-stats.ts; same stat ids
as Player Spotlight, so a wrong tile there means a wrong line here).&caption=1
(lib/captions.ts), including the week's dates when the Week Days calendar is saved./api/admin/graphics/player-radar, portrait only): a percentile
radar for one player, current season. Each axis is one of the league's scoring
categories (read from ESPN's scoring settings and labelled in STAT_META,
; skaters and goalies get their own sets). The value is the
player's PER-GAME rate (rate stats like GAA/SV% are used as-is), ranked against
every NHL player at the same position group (forwards / defensemen / goalies,
from ESPN's player list, most-owned first) who has played the minimum games. The
minimum is half of what the leaders have played (1-15 games) unless
is given, so early-season percentiles are noisy -- a small-sample note appears
when the player is under it. A stat the league scores negatively (goals against,
PIM...) is flipped so a bigger point is always better, and doing nothing in a
count stat always scores 0. Pick a player with (the Graphics page has
a name search); without it the week's top scorer () is used.
returns the categories, pool size and every percentile as JSON -- the first thing to
check if an axis looks wrong. Logic in , drawing in .
Card design: big headshot and name, then the radar on a navy panel with a GOLD shape (never red/green, which
are reserved for good/bad numbers). Related categories sit next to each other
( in ); the dashed ring is labelled AVG. It skips the
rink-marking background ( option of ).
Rare stats (shutouts, OT losses, short-handed goals/assists, hat tricks: in
) aren't plotted, because most players have none and the chart would
collapse; they show as season-count tiles in the panel's top-left corner (if that would
leave fewer than 3 axes they stay on the chart). DEF is dropped for forwards. A previous
season: add (needs ): uses that season's ESPN player list and
scoring settings, minimum games capped at 20, labelled with the season. The Player Cards
page has an "Add a previous season" picker ().
Fantasy points table (total and per game): each shown as a percentile (big) with the rank
beside it, vs ALL players (forwards + defensemen + goalies together; fantasy points use the
league's own scoring so they compare across positions) and vs his own group. Each group
applies its own minimum-games cutoff. in ; the
block of shows the numbers. The totals are the big numbers; beside each is the rank
(larger) and the percentile (smaller), e.g. "#24 96th pct" -- no pool size shown. Other layouts
exist behind (percentile only, rank only, percentile big + rank small).Power rankings blend win %, point differential and streak (see
lib/power-rankings.ts), built only from completed regular-season games. Movement
arrows compare the ranking through the latest final week to the week before;
nothing is stored. The admin Formula Test (/admin/formula-test) backtests
the formula against every past season; on this league's 7 seasons nothing beat
the current formula by more than noise, so the weights were left alone.
Weekly stats and the week -> scoring period mapping: this league scores
DAILY (a scoring period is one day) with multi-day matchup weeks, so "week N" is
a group of days. ESPN does NOT say which days belong to a week for this league
(the schedule has no per-day breakdown, and scheduleSettings.matchupPeriods
just maps matchup periods to matchup periods), so the commissioner enters each
week's length once per season at Week Days (/admin/week-days), copied from
ESPN's schedule page. lib/week-calendar.ts turns those lengths into days.
getWeeklyPlayerStats uses ESPN's per-day data if it ever appears, otherwise
the saved week lengths, and sums every day, counting only days a player was in an
active lineup slot (bench/IR skipped). With nothing saved, the player graphics
(3 Stars, Top 3, Team of the Week, Player Spotlight) show an error pointing to
Week Days instead of reporting one night as a week. /api/admin/period-probe
(logged in) shows what ESPN returns, for checking. Empty results show diagnostics.
Admin login: the cookie holds a signed, expiring token, never the password.
Optionally set ADMIN_SESSION_SECRET in Vercel; changing it or ADMIN_PASSWORD
logs everyone out.
AI-drafted blurbs only work for the current season; asking for a past season returns a clear message instead of mixing in current-season data.
ADMIN_SESSION_SECRET in Vercel for extra
hardening; changing it or ADMIN_PASSWORD logs everyone out.lib/espn.ts is the one file that talks to ESPN —
start there.name field (falling back to the older
location + nickname split, then the abbreviation). If a team ever shows
up oddly, it likely hasn't set a custom name in ESPN's own team settings.ANTHROPIC_API_KEY as matchup drafts, grounded in the real compiled
facts for that period).lib/espn-stats.ts&minGames=N&playerId=N&week=N&debug=1lib/radar.tslib/radar-image.tsxAXIS_ORDERlib/radar.tsplainframe()RARE_STATSlib/radar.ts&season=YYYY&playerId=/api/admin/players/seasonspointsComparisonlib/radar.tspoints&debug=1&pts=pct|rank|both/admin/player-cards): pick one player (search any NHL player by
name, or choose a fantasy team and then a player from its roster) and every card for
that player is generated: Player Radar and the weekly Player Spotlight (week box
defaults to the latest completed week). The card list is the cards array in the page,
so a new player card type is one more entry. Uses the shared components/GraphicCard.tsx
(also used by the Graphics page) and /api/admin/players/rosters for the team dropdowns./api/admin/graphics/player-bars, portrait only): the same data as the Player
Radar drawn like Baseball Savant's percentile panel -- one bar per category on a shared
POOR / MEDIAN / GREAT scale, a numbered circle at the end of each bar, the actual per-game value
on the right, categories grouped into SCORING / PHYSICAL (or GOALIE), rare stats as season-count
tiles. Best for goalies and any league with many categories. Both cards share
lib/player-profile.ts (loadProfile) so they always agree, and take the same parameters
(playerId or week, season, minGames, debug, caption). A player below the minimum games is
NOT QUALIFIED: the radar dims, the bars are hatched with no percentiles. Colors use a blue (strong) /
neutral (median) / coral (weak) scale (scaleColor in lib/radar.ts), which stays readable with
common color blindness unlike red/green. The radar's dashed ring is labelled MEDIAN (it is the 50th
percentile of the pool). The Player Cards page shows both cards for a picked player.
The header band of both cards is the PLAYER (photo, name, position/team, games played; the shared
lib/player-header.tsx, passed to frame() as headerContent) rather than a card-type title./api/admin/players/ranks): browse the players in rank order by total
fantasy points, fantasy points per game, or any scoring category per game (lower-is-better categories
sort the other way), among all players or just forwards / defensemen / goalies; "Start at rank" jumps
to a rank. A Season dropdown ranks a previous season's final numbers instead (that season's ESPN player
list and scoring; choosing a player from it also adds that season's cards). Only players who meet the
minimum games count, like the cards; click a name to pick him.immutable), which made
a graphic you had already opened keep showing the old picture even after new stats or a redeploy.
Every image response now sends NO_CACHE (lib/og-theme.ts) and the card fetch uses cache: "no-store"./players, in the main nav): the same Player Cards page without the commissioner
login: search a player, pick from a team, browse rankings, previous seasons; Radar, Bars and the weekly
Spotlight. The page is components/PlayerCardsBrowser.tsx, shared with the admin page (props choose the
endpoints). The public endpoints are /api/cards/* (images) and /api/players/* (search, rosters,
ranks, seasons): thin wrappers around the admin handlers (lib/public-cards.ts), so the two can't
drift apart. Because anyone can open them: answers are cached on Vercel's CDN for 15 minutes, debug
is refused, errors aren't cached, and setting the environment variable PUBLIC_PLAYER_CARDS=off
switches the public cards off (404) without touching the admin ones./admin/import-season, lib/season-import.ts, lib/imported-seasons.ts): for the 2025
season played on Fantrax. Upload the Fantrax player CSV (it has each player's real fantasy points and points per
game, from which games played is recovered, but NO category stats); the site fetches that season's raw stats from the
NHL's public stats API (api.nhle.com/stats/rest/en, skater summary + realtime, goalie summary), matches the lists
by name + position (games played breaks ties for repeated names like the two Elias Pettersons), and recovers the
league's scoring rules by least-squares fitting the NHL stats to the real Fantrax points. The report shows the fit
(share of players within 0.05 points, R2) so it can be checked BEFORE saving. Saving stores one JSON document per
season in imported_seasons. The 2025 Fantrax league scores: skaters G 2, A 1, shootout goals 0.75, short-handed goals 1, SOG 0.1, hits 0.15, blocks 0.15; goalies W 1.5, SO 1.5,
SV 0.1, GA -0.45, OT+shootout losses 0.5, and their OWN goals 2, assists 1, short-handed points 1 (defensemen appear to score G 2.2 / A 1.2 via a position override).
The import models exactly those categories (SKATER_KEYS / GOALIE_KEYS); shootout goals come from the NHL skater/shootout report, goalie goals/assists from
the goalie summary. Forwards, defensemen and goalies are fitted SEPARATELY (the 2025 league paid defensemen more per goal and assist). Leftover names are
matched by last name + a first name that starts the same way (Sam/Samuel) + games played within 3. Names that differ more (Mikey/Michael, John/Jack, Maxim/Maksim) are matched by last name + first letter + games played within 1.
After you press Save, the season is used everywhere a previous season is: the season picker (marked "Fantrax"), the rank
browser, and the Radar and Bars cards. Those cards use Fantrax's REAL fantasy points (not a recomputation) for the points
table, the NHL raw stats for the category percentiles, and the recovered scoring (per position group) to decide which
categories are shown and which direction is good. The "team" line shows the Fantrax owner ("Owned by OAP"). A player is
found by Fantrax id (fid) or by name (the ESPN id only separates two players of the same name); the 5 or so players the
NHL data lacks appear in fantasy-points rankings but have no category cards. Photos: ESPN's if the player is also on the
current ESPN list, else the NHL's headshot for that season, else initials. Shootout goals, short-handed goals and a
goalie's own goals/assists are shown as season-count tiles (ids 900 / 20 / 913 / 914).pts() / ptsComma()
from lib/format.ts for new screens instead of rounding by hand. Percentages and per-game stat rates are not points.components/Nav.tsx, edit the NAV list to change it). The home page no longer repeats
the nav as button cards: under the hero it shows this week's full scoreboard, a power rankings top 6 with movement arrows, and
"Players of the Week" (top 3 scorers of the latest completed week plus the radar card for the best one, from the public
/api/cards/player-radar). That last section streams in (<Suspense>) so a slow ESPN call never delays the page, and it
simply disappears if its data or image can't load. Sections with no data yet (before Week 1 is final) are skipped.HeroTiles in lib/radar-image.tsx). This replaced the small points table and the
?pts= display options. Use ordinal() from lib/format.ts for any "1st / 2nd / 3rd" text.categoriesFor in lib/radar.ts skips it for everyone. It still counts in fantasy points; it just isn't an axis, bar, or rank option.lib/tiers.ts): Superstar (99th percentile and up), Elite (95-98th), Good (75-94th), Depth (50-74th),
Replacement (below 50th), from fantasy points PER GAME against his own position group (forwards / defensemen / goalies), using the
same minimum-games pool as the rest of the card. The badge sits in the PER GAME hero tile (that is the number it is based on); a player
below the games minimum has no tier. tierFor(profile) in lib/player-profile.ts; the tier is also in the Copy caption text and &debug=1.
Small pools mean few top tiers (only one goalie is Superstar), which is correct. A tier-list graphic (players grouped by tier) was
mocked up but not built.?week=N gives the table as of week N, ?season=YYYY a past
season's final table.&format=landscape on any graphic's address) gives the wide version. Portrait
layouts are in lib/portrait-graphics.tsx, landscape ones in the lib/*-image.tsx
files.