OpenTrailPaper
LilyGO T5S3 4.7" E-Paper PRO용 오픈소스 자전거 컴퓨터 펌웨어, 오프라인 지도·GPX 경로·FIT 기록·블루투스 센서 지원
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Live Fantasy Premier League dashboard: xG, bookmaker-implied fixture strength and an expected-points model, rebuilt every three hours
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A live dashboard for a Fantasy Premier League squad and mini-league. It shows the numbers behind the points (expected goals, expected assists, defensive contributions, bookmaker-implied fixture strength) and turns them into decisions: who to captain, who to sell, when to play a chip.
Live: https://fpl-insights-jm.vercel.app (scheduled to rebuild every three hours; actual timing can vary)
| Planning | Mini-league |
|---|---|
FPL's own site tells you what happened (points) but not why, and not whether it will happen again. A defender with 15 points from one lucky goal and a midfielder quietly racking up chances look the same in the points column. I wanted the underlying numbers per match, in one place, next to the decisions they should inform, and I wanted my mini-league's results explained rather than just ranked.
It also became a real exercise in working with messy public data: four sources with no shared keys, an expected-points model built because FPL's own projection is unusable (26 distinct values across 515 players, capped at 4.0), and a pipeline that has to keep running unattended.
Built with Claude Code as a pair programmer.
python cli.py dashboard --open the whole thing as a web page
python cli.py team your 15, with per-match xG/xA
python cli.py league how your rivals are doing, and why
python cli.py player szoboszlai one player, in detail
python -m pytest the tests
config.json holds the FPL ids to report on. The committed one points at my
own team and mini-league, so everything above runs straight after a clone.
To point it at a different team:
{
"entry_id": 3385667,
"league_id": 1453141
}
Your entry_id is the number in the URL when you view your own team
(/entry/<id>/event/1/). The league_id is the number in a mini-league URL.
Both can also be passed as --entry / --league.
Responses are cached in .cache/ for 15 minutes. --ttl 0 forces a refresh.
python cli.py dashboard --open writes dashboard.html and opens it. Re-run it
whenever you want current numbers. --artifact also writes a complete HTML copy,
dashboard-artifact.html, which is what the live site serves.
Four tabs:
The design system is in docs/design.md: FPL purple #37003c,
night #16001a, green #00ff87, white and a paper ground, Noto Sans with
tabular figures, and a generated icon set packed into one embedded sprite
(scripts/pack_icons.py). Nothing on the page means something by hue alone:
gains carry a sign or arrow, states change fill, weight or shape, and fixture
pills print the opponent and the number on a dark-to-green ramp that stays
readable in greyscale.
The dashboard is something you go and look at. The deadline is the moment
that matters, so a companion bot, in a separate private repo
(fpl-lineup-alert), pushes to my phone through ntfy
instead:
sub spence for hall or captain haaland runs a workflow that
makes the change on the real FPL team. Only the repo owner's comments count,
and transfers are deliberately left out because they cost points and cannot
be undone.The two repos share data, not code. Each build here publishes
projections.json (next gameweek's expected points for every player) beside
the page, and the bot fetches it from the live site, falling back to points
per game if it is missing or for the wrong gameweek. The bot is kept private
because it holds an FPL login and can change the team.
fplapi.py FPL API client: retries, backoff, 15-minute disk cache
analysis.py per-player reports, findings, the expected-points model
odds.py Pinnacle odds -> expected goals and clean-sheet chances
pulse.py Premier League Opta stats, joined on Opta id
ffs.py Fantasy Football Scout predicted line-ups
elite.py ownership among the top of the global league
transfers.py transfer and pair suggestions
chips.py chip planner
captaincy.py captaincy model
scout.py the Scout tab's player pools
ticker.py fixture ticker and runs
dashboard.py page assembly: build() gathers data and cards, render() wraps them
cards_*.py one module per tab (squad, planning, league, scout) plus shared helpers
components.py chart-like building blocks shared across cards
dashboard.css/.js the page's styles and script; the other .js files are per-feature
assets/ the packed icon sprite (scripts/pack_icons.py rebuilds it)
cli.py the command-line entry point
Bookmaker odds: Pinnacle's public guest API, no key. Pinnacle runs on low
margins and high limits, so its line is close to a market consensus. The total
line nearest even money gives the match's expected goals, the handicap line
nearest even gives the supremacy, and those two solve for each side's expected
goals. A clean sheet is then the opponent failing to score under a Poisson
model, exp(-m). That last step is an assumption (Poisson slightly understates
0-0 draws), so it is labelled as derived, not quoted, wherever it appears.
Predicted line-ups: Fantasy Football Scout's team-news page. FPL's API says
nothing about who will start. FFS renders each predicted starter with a photo
whose filename is the same id FPL exposes in element["photo"], so the two
join on a number, not a name. If the page fails to parse, the feature reports
nothing rather than marking everyone benched.
Premier League Opta stats: footballapi.pulselive.com, the open API behind
premierleague.com. Big chances created and missed, shots, shots on target,
dribbles and touches, none of which FPL shows. The join is exact: Pulselive
returns altIds: {"opta": "p223094"} and FPL carries the identical string in
element["opta_code"]. Two quirks: ids come back as floats and must be cast to
int for URLs, and the ranked endpoints only include Opta ids when asked with
altIds=true.
Elite ownership: FPL's global league (id 314) is public and ranked, so the
top managers' squads can be read like a mini-league rival's. Ownership among
that group separates "popular" from "popular with people who are winning".
Early in a season, though, the current top of that table is mostly a
survivorship sample: two or three lucky captaincy picks, not evidence of
judgement. So the reference group is "proven" managers: of the current top
250 (--elite N to change it, 0 to skip), only those who finished inside the
top 100,000 last season count. That buys a season-plus of track record at the
cost of a smaller sample, so every ownership figure carries its 95% margin of
error, and a gap smaller than twice that margin is treated as no difference.
Price-change forecasts: FPL's own price_change_percent and
price_change_projections, which the site does not surface. Positive rises,
negative falls, and the change fires at 100.
Sources checked and rejected: FBref (403), FotMob (404), Sofascore (403), and Fantasy Football Scout's projected points and DefCon tables, which show names to anonymous visitors but strip the numbers (that is their paid product).
GitHub Actions builds it (.github/workflows/build.yml) on code changes and
on a three-hour schedule. It runs the regression tests first, then commits
dashboard-artifact.html and projections.json if they changed. Optional source
failures appear in the run log and on the page. Every run ends with a source-health
block, so a broken scrape shows up as a FAIL line rather than quietly vanishing.
Three-hourly rather than twice a day because GitHub's scheduler is best-effort: the very first scheduled run here was skipped outright, with no error, leaving an eight-hour-old page live. Eight chances a day makes a dropped run a non-event, and runs where nothing changed exit without committing. Prices change around 01:30 UK, so early builds catch them; predicted line-ups sharpen through the day, so later builds land before a typical 18:30 deadline.
Vercel deploys it. The Vercel project is linked to this repo, so every build that changes the page triggers a fresh deploy with no extra step. This replaced a second scheduled job that republished the page twice a day and quietly stopped running with no error. Deploying off the git push removes that failure mode rather than monitoring it better. If the GitHub Action stops firing, the page warns once its snapshot is six hours old. Scheduled runs can be delayed or skipped, so three hours is a requested cadence, not a freshness guarantee. Fatal build errors and failed regression tests prevent publication, preserving the last valid page.
The built HTML is committed. A 2.2 MB page eight times a day sounds expensive, but the embedded images are byte-identical between builds, so git deltas them away. Measured across two consecutive builds: zero growth.
Per-match history is current season only. element-summary gives one row
per fixture for this season, but only totals for earlier seasons. Those totals
are used as a baseline while the current season is short. For older per-match
data, github.com/vaastav/Fantasy-Premier-League archives it as CSV per
gameweek from 2016/17.
Defensive contribution. FPL awards 2 points for hitting a threshold in a
match: 10 for defenders (clearances, blocks, interceptions, tackles), 12 for
midfielders and forwards (the same plus recoveries). The API's
defensive_contribution field is already composed correctly per position
(verified against the raw components rather than assumed), so only the
threshold differs.
Early season. Per-90 rates over one or two matches are noise. The report says so when players are under 270 minutes and falls back to last-season baselines.
FPL's xG is Opta's. Understat runs a different model and will not agree: for 2025/26 Haaland was 25.50 xG by FPL and 28.80 by Understat. The two are never mixed in one number.
curl cannot reach the FPL API from my machine: the TLS handshake fails
(curl exit 35) while Python's urllib succeeds against the same host, so
everything uses urllib.Squad, bank and free-transfer estimates describe the last public picks. Moves already made before the next deadline are not visible without signing in. Transfer affordability uses selling values recovered from public purchase history; advice is withheld when those values cannot be recovered. Bench Boost rebuilds show at most one affordable upgrade, checked against the full squad.
The model currently supports single-fixture gameweeks. Chip and transfer advice is withheld if its eight-week planning window contains a double gameweek, and missing projections are reported rather than replaced by repeated market odds. Blank weeks are distinguished from unavailable fixture data in the player profile. The budget-based Free Hit and Wildcard search is approximate; owned-XI selection is exact for a legal fifteen. Projections are estimates, not backtested forecasts.
© 2026 Jonno Millar, all rights reserved. Public to read, not to reuse (see LICENSE).