Squad rail becomes a league-wide captaincy board, star-rated #31
No reviewers
Labels
No labels
architecture
cleanup
dependencies
performance
priority: high
priority: medium
reliability
security
testing
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
solvreven/FPL!31
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/captaincy-board"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stacked on #30 → #29 → #28. Merge in that order and this reduces to its own commit.
Asked for directly: "on the right side in the squad tab, to the right of the pitch, I want removed. I want top 5 captaincy picks for every week, sorted by 1-5 stars ... It can even have a half star aswell."
What it does
/api/captain-picksranks the whole pool for one gameweek, not the squad — the question is "who is worth captaining", which is also how a transfer gets argued for. It calls the samerank_captaincythe armband uses rather than a second ranking that would have to be kept in step: two surfaces naming different best captains is a defect this app has already shipped once, and the fix then was to remove one of the computations.Eligibility is a presence test, never a default. A player needs a row for the round, a measured rate and an appearance probability; missing any, he is absent from the board rather than ranked on a substitute.
Stars are absolute, not relative to the week's field. A relative scale always awards five stars to the best available option, so a blank-heavy round would flatter a mediocre captain on the one decision that doubles the return. Calibrated against the real distribution rather than invented — across GW3–5 the maximum was 4.68–5.26 and the median 2.05 — so one star per expected point, to the nearest half spans the field without bunching, and a weak round honestly shows no five-star pick.
The half star is geometry, not a glyph: a filled row overlaid on an outline row, clipped to
rating/5. Measured live at 55.8 of 62.0px for 4.5. A future 4.25 would render with no new case. The stars spend no data channel — neutral white at two weights, the same reasoning the C badge already carries, so they cannot collide with the--mag-*or--fx-*ramps.Two defects found by looking at the running page
Neither was visible in the code.
The armband named B.Fernandes while the board topped out on Virgil. Both correct — your fifteen versus all 406, and Virgil is unowned — but that is not visible at a glance, and it is the "two captains on one screen" shape a reader caught here before. The ownership chip (
your captain/in your squad) makes the panels visibly agree instead of apparently contradicting.The heading was derived from the pager's gameweek, not the data's. They agree today because the fetch is keyed on the pager, but a label taken from anything other than the rows beneath it is "Match predictions · GW1" waiting for the two to drift. It reads
d.gwnow.What removing the rail cost
Recorded rather than buried, because it is a real loss and the reviewer should weigh it:
5/5/5/5/5/5and one of0/0/0/15/15/0are indistinguishable in a total and want opposite decisions.railYourXiHtmlwas kept on purpose. It exists because a reader reported the pitch drawing an XI they never chose; sweeping it up with the rail would have silently un-fixed that.railOutlookHtml/Lines/Bench/Shakynow have no caller. Kept rather than deleted — each is still exercised by its own tests and none computes a discredited answer, unlikecaptainCandidates, which was deleted for ranking by a quantity we had just shown to be wrong. The disconnection is stated at the definition, because dead code whose tests still pass is this repo's mechanism-that-is-not-connected shape. Happy to delete them and reworktest_phase_d.py/test_bench_contribution.pyif you'd rather.Tests
Twelve tests for the removed cards deleted. One of them,
test_shaky_starts_is_absent_when_nobody_is_shaky, was still passing — it asserts the block is absent and the block can no longer render at all. A test that cannot fail is not a test, so it went with its branch rather than being left as a green line.test_rail_content_is_reachable_at_every_breakpointwas re-pointed, not deleted: the responsive rule is unchanged, only the content it guards.14 new tests. Nine cuts, nine REDs — but three survived the first pass, and each was a hole in my own tests:
railShakyHtmlrenders nothing for a squad with no shaky player — the check could not failAll three now bite, and the third is what surfaced the
d.gwdefect above.Verified on the live page, not from the DOM alone: screenshotted and read back.
🤖 Generated with Claude Code
https://claude.ai/code/session_014adnmRztkdAPwinWXnNQ7N
Removes the six-book consensus from the served surfaces and makes NT the market for match odds, anytime goalscorer and anytime assist. The consensus files stay on disk and score_goalscorer.py still scores both every round, so this is a code change, not a lost comparison. NT does not open its markets all at once -- measured 2026-09-01/02, goals open ~4 days out and assists ~2, per fixture -- so a single pre-deadline pull could never carry both. nt_watch in chores.py polls on COVERAGE ("does the newest banked run carry both markets for every fixture of the next round", answered from the manifests on disk) rather than on a clock or a completion marker, so a pull someone ran by hand counts. The denominator is the round's fixture count from FPL, never the run's. Four producer-without-a-consumer defects surfaced while wiring it, three created by the NT-only switch itself: disagreements and fixtures_without_market were computed and discarded by main.matches; gw{n}_market_nt.json was written by nt_resolve with nothing rebuilding the cache to read it; and n_fixtures counted match_odds while the table was drawn from a different subset. _unpriced_but_ours no longer returns one flat list. "No book priced X -- the omission is the bookmakers' saying he is unlikely to feature" was printed over five players in two fixtures NT had not opened at all, which is a verdict reported where the book had said nothing. It returns {"declined": [...], "unopened": [...]} keyed on the FPL team id, because the two need opposite responses. pre_deadline_steps is now pull -> resolve -> rebuild -> snapshot, so the frozen cache is the one built from the frozen prices. snapshot_pit.odds_for takes both market files; the raw NT run is zipped rather than pointed at, since prune_nt_runs can delete the directory. Retires _books_expect_him: the coverage-share gate cannot be computed from one book. Reported rather than patched around -- raising our own start_prob floor would let the weakest number in the system decide membership. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014adnmRztkdAPwinWXnNQ7NAdds `understat_watch` to chores.py, beside `nt_watch`. It asks "does the banked current-season file reach the round that has been played" and answers it FROM THE FILE, so a fetch someone ran by hand counts and the ledger is never the evidence -- a completion marker is not evidence of what was completed. NOT folded into after_gameweek.py, and the reason is measured rather than assumed. Understat had GW2 by 48h after that round's last kickoff, while after_gameweek fires the moment the round ends. Chaining it there would fetch before Understat published and leave us permanently one round behind: self-healing in principle, never actually caught up. One observation bounds the lag at <=48h and cannot pin it tighter, which is the argument for keying on coverage rather than on a clock -- the design does not need the number. THE FETCH IS ALWAYS FOLLOWED BY A REBUILD. foreign_rates.json and the current-season file are read at CACHE BUILD time and nowhere else, so a fetch with no rebuild behind it refreshes a file no projection ever sees. That is the shape nt_resolve sat in for a week, writing gw{n}_market_nt.json that only a rebuild reads. No season is written down: the season id and the output filename are both DERIVED from the label FPL reports (understat_season_arg, understat_out_path). A BUG THAT WAS LIVE FOR ABOUT A MINUTE, and the sharpest thing here. Coverage counted all five leagues. Ligue 1 had played 3 rounds while the PL had played 2, so it returned 3, never fell below the denominator, and the watch would have been PERMANENTLY SATISFIED without ever firing once. The denominator is the FPL gameweek, so the numerator has to be EPL rows -- counting one population while describing another is the "20 fixtures" coverage line in a different file. Pinned by test_coverage_counts_only_the_league_the_denominator_belongs_to. 8 tests here, 21 across the three Understat files. Every cut RED, including two on the WIRING rather than the mechanism: removing the planner hook goes red while every helper test stays green, and making the watch fire unconditionally goes red too. A mechanism nothing calls is a comment. Verified at commit time: the 21 targeted tests pass and all mutation cuts go RED. The full suite was still running; its result is reported separately rather than asserted here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014adnmRztkdAPwinWXnNQ7NView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.