Add two endpoints for reading EvolutionCardState:
GET /api/v2/teams/{team_id}/evolutions
- Optional filters: card_type, tier
- Pagination: page / per_page (default 10, max 100)
- Joins EvolutionTrack so card_type filter is a single query
- Returns {count, items} with full card state + threshold context
GET /api/v2/evolution/cards/{card_id}
- Resolves card_id -> (player_id, team_id) via Card table
- Duplicate cards for same player+team share one state row
- Returns 404 when card missing or has no evolution state
Both endpoints:
- Require bearer token auth (valid_token dependency)
- Embed the EvolutionTrack in each item (not just the FK id)
- Compute next_threshold: threshold for tier above current (null at T4)
- Share _build_card_state_response() helper in evolution.py
Also cleans up 30 pre-existing ruff violations in teams.py that were
blocking the pre-commit hook: F541 bare f-strings, E712 boolean
comparisons (now noqa where Peewee ORM requires == False/True),
and F841 unused variable assignments.
Tests: tests/test_evolution_state_api.py — 10 integration tests that
skip automatically without POSTGRES_HOST, following the same pattern as
test_evolution_track_api.py.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename _CareerTotals.k → .strikeouts to match formula engine's
stats.strikeouts Protocol
- Update test stubs: TrackStub fields t1→t1_threshold etc. to match
EvolutionTrack model
- Fix fully_evolved logic: derive from post-max current_tier, not
new_tier (prevents contradictory state on tier regression)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes#73
Adds POST /api/v2/evolution/cards/{card_id}/evaluate — force-recalculates
a card's evolution state from career totals (SUM across all
player_season_stats rows for the player-team pair).
Changes:
- app/services/evolution_evaluator.py: evaluate_card() function that
aggregates career stats, delegates to formula engine for value/tier
computation, updates evolution_card_state with no-regression guarantee
- app/routers_v2/evolution.py: POST /cards/{card_id}/evaluate endpoint
plus existing GET /tracks and GET /tracks/{id} endpoints (WP-06)
- tests/test_evolution_evaluator.py: 15 unit tests covering tier
assignment, advancement, partial progress, idempotency, fully evolved,
no regression, multi-season aggregation, missing state error, and
return shape
- tests/__init__.py, tests/conftest.py: shared test infrastructure
All 15 tests pass. Models and formula engine are lazily imported so
this module is safely importable before WP-01/WP-05/WP-07/WP-09 merge.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
formula_engine.tier_from_value read track.t1/t2/t3/t4 but the
EvolutionTrack model defines t1_threshold/t2_threshold/etc. Updated
both the function and test fixtures to use the _threshold suffix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Mirror the batter_id is None guard in _build_pitching_groups() so that
a StratPlay row with a null pitcher_id is skipped rather than creating
a None key in the groups dict (which would fail on the NOT NULL FK
constraint during upsert).
- Revert Dockerfile to the next-release base: drop the COPY path change
and CMD addition that were already merged in PR #101 and are unrelated
to the ProcessedGame ledger feature.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#75.
New file app/services/evolution_init.py:
- _determine_card_type(player): pure fn mapping pos_1 to 'batter'/'sp'/'rp'
- initialize_card_evolution(player_id, team_id, card_type): get_or_create
EvolutionCardState with current_tier=0, current_value=0.0, fully_evolved=False
- Safe failure: all exceptions caught and logged, never raises
- Idempotent: duplicate calls for same (player_id, team_id) are no-ops
and do NOT reset existing evolution progress
Modified app/routers_v2/cards.py:
- Add WP-10 hook after Card.bulk_create in the POST endpoint
- For each card posted, call _determine_card_type + initialize_card_evolution
- Wrapped in try/except so evolution failures cannot block pack opening
- Fix pre-existing lint violations (unused lc_id, bare f-string, unused e)
New file tests/test_evolution_init.py (16 tests, all passing):
- Unit: track assignment for batter / SP / RP / CP positions
- Integration: first card creates state with zeroed fields
- Integration: duplicate card is a no-op (progress not reset)
- Integration: different players on same team get separate states
- Integration: card_type routes to correct EvolutionTrack
- Integration: missing track returns None gracefully
Fix tests/test_evolution_models.py: correct PlayerSeasonStats import/usage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#105
Replace the last_game FK guard in update_season_stats() with an atomic
INSERT into a new processed_game ledger table. The old guard only blocked
same-game immediate replay; it was silently bypassed if game G+1 was
processed first (last_game already overwritten). The ledger is keyed on
game_id so any re-delivery — including out-of-order — is caught reliably.
Changes:
- app/db_engine.py: add ProcessedGame model (game FK PK + processed_at)
- app/services/season_stats.py: replace last_game check with
ProcessedGame.get_or_create(); import ProcessedGame; update docstrings
- migrations/2026-03-18_add_processed_game.sql: CREATE TABLE IF NOT EXISTS
processed_game with FK to stratgame ON DELETE CASCADE
- tests/conftest.py: add ProcessedGame to imports and _TEST_MODELS list
- tests/test_season_stats_update.py: add test_out_of_order_replay_prevented;
update test_double_count_prevention docstring
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Correct idempotency guard docstring in update_season_stats() to
accurately describe the last_game FK check limitation: only detects
replay of the most-recently-processed game; out-of-order re-delivery
(game G after G+1) bypasses the guard. References issue #105 for the
planned ProcessedGame ledger fix.
- Fix migration card_type comment: 'batting' or 'pitching' → 'batter',
'sp', or 'rp' to match actual seeded values.
- Remove local rarity fixture in test_season_stats_update.py that
shadowed the conftest.py fixture; remove unused rarity parameter from
player_batter and player_pitcher fixtures.
- Update test_double_count_prevention docstring to note the known
out-of-order re-delivery limitation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CRITICAL: Fix migration FK refs player(id) → player(player_id)
- Remove dead is_start flag from pitching groups (no starts column)
- Fix hr → homerun in test make_play helper
- Add explanatory comment to ruff.toml
- Replace print() with logging in seed script
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JSON definitions and idempotent seed function for the 3 universal
evolution tracks (Batter, Starting Pitcher, Relief Pitcher) with
locked threshold values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implement update_season_stats(game_id) in app/services/season_stats.py.
Aggregates StratPlay batting/pitching stats and Decision win/loss/save
data into PlayerSeasonStats with idempotency guard and dual-backend
upsert (PostgreSQL EXCLUDED increments, SQLite read-modify-write).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add EvolutionTrack, EvolutionCardState, EvolutionTierBoost, and
EvolutionCosmetic models to db_engine.py with composite unique indexes
and create_tables blocks. Also includes PlayerSeasonStats (WP-02).
Add ruff.toml to suppress pre-existing F403/F405 from intentional
`from peewee import *` wildcard import pattern in db_engine.py.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolve conflicts in app/main.py and app/routers_v2/players.py:
keep main's render pipeline optimization (Phase 0) with asyncio.Lock,
error-tolerant shutdown, and --no-sandbox launch args. The next-release
browser code was an earlier version of the same feature.
Add evolution router import and inclusion from next-release.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract shared pitcher value computation into _pitcher_value() helper.
Consolidate duplicated column lists and index helper in season stats tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Separate batting and pitching into distinct tables with descriptive column
names. Eliminates naming collisions (so/k ambiguity) and column mismatches
between the ORM model and raw SQL. Each table now covers all aggregatable
fields from its source (BattingStat/PitchingStat) including sac, ibb, gidp,
earned_runs, runs_allowed, wild_pitches, balks, and games_started.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The single-letter `k` field was ambiguous and too short for comfortable use.
Rename to `so_pitcher` for clarity, and `so` to `so_batter` to distinguish
batting strikeouts from pitching strikeouts in the same model.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace per-request Chromium launch/teardown with a module-level
persistent browser. get_browser() lazy-initializes with is_connected()
auto-reconnect; shutdown_browser() is wired into FastAPI lifespan for
clean teardown. Pages are created per-request and closed in a finally
block to prevent leaks.
Also fixed pre-existing ruff errors in staged files (E402 noqa comments,
F541 f-string prefix removal, F841 unused variable rename) that were
blocking the pre-commit hook.
Closes#89
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Address two review findings from PR #94:
1. Race condition: concurrent requests could both launch Chromium when
_browser is None. Wrap the init check in asyncio.Lock so only one
coroutine creates the browser process.
2. Font duplication: the WOFF2 files are variable fonts covering all
needed weights. Consolidate 5 @font-face blocks (3x Open Sans,
2x Source Sans 3) into 2 using CSS font-weight range syntax,
saving ~163KB of redundant base64 per render.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes#71
Adds GET /api/v2/evolution/tracks and GET /api/v2/evolution/tracks/{track_id}
endpoints for browsing evolution tracks and their thresholds. Both endpoints
require Bearer token auth and return a track dict with formula and t1-t4
threshold fields. The card_type query param filters the list endpoint.
EvolutionTrack is lazy-imported inside each handler so the app can start
before WP-01 (EvolutionTrack model) is merged into next-release.
Also suppresses pre-existing E402/F541 ruff warnings in app/main.py via
pyproject.toml per-file-ignores so the pre-commit hook does not block
unrelated future commits to that file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#74
Adds app/services/formula_engine.py with three pure formula functions
(compute_batter_value, compute_sp_value, compute_rp_value), a dispatch
helper (compute_value_for_track), and a tier classifier (tier_from_value).
Tier boundaries and thresholds match the locked seed data from WP-03.
Note: pitcher formulas use stats.k (not stats.so) to match the
PlayerSeasonStats model field name introduced in WP-02.
19 unit tests in tests/test_formula_engine.py — all pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Evolution models (EvolutionTrack, EvolutionCardState, EvolutionTierBoost,
EvolutionCosmetic), their re-export module, and tests were included in
this PR without disclosure. Removed to keep this PR scoped to
PlayerSeasonStats (WP-02) only per review feedback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When no position filters are applied, `final_players` is a lazy Peewee queryset
with `ORDER BY RANDOM() LIMIT n`. Iterating it twice (once to build player_ids,
once for the response loop) executes two separate DB queries with different random
seeds, causing dex_by_player to be built for a different player set than returned,
silently producing empty paperdex for all players.
Add `final_players = list(final_players)` before building player_ids to ensure
both iterations operate on the same materialized result. Also fix pre-existing
syntax error in import statement and minor ruff lint issues in the same file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Starters face both LHH and RHH, so the OPS aggregation formula should
penalise the weaker platoon split (higher OPS allowed) rather than
reward the stronger one. Changed min(ops_vl, ops_vr) → max(ops_vl, ops_vr)
in both get_total_ops (line 621) and sort_starters (line 703) and
replaced the TODO comment with an explanatory note.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The /live-update/pitching POST endpoint was a placeholder that only
validated auth and returned the input unchanged. No pitching processing
logic existed anywhere in the codebase. Removed the dead endpoint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace per-player/card Paperdex.select().where() calls with a single
batched query grouped by player_id. Eliminates N+1 queries in:
- players list endpoint (get_players, with inc_dex flag)
- players by team endpoint
- cards list endpoint (also materializes query to avoid double count())
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove card_1..card_26 FK columns from Roster ORM model
- Add RosterSlot model with (roster, slot, card) and a unique index on (roster, slot)
- Activate get_cards() helper on Roster using the new junction table
- Register RosterSlot in create_tables for SQLite dev environments
- Add migrations/migrate_roster_junction_table.py to backfill existing data
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The endpoint iterated over `files.vl_basic` (a string, not parsed CSV),
causing it to loop over individual characters. The body contained only
`pass` with TODO comments and no running stats logic. Removed the
endpoint entirely along with the dead commented-out csv helper code.
The `BattingFiles` model is retained as it is still used by
`live_update_pitching`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace 71 broad `except Exception` blocks in 19 router files with the
specific `peewee.DoesNotExist` exception. GET endpoints that call
`Model.get_by_id()` now only catch the expected DoesNotExist error,
allowing real DB failures (connection errors, etc.) to propagate as
500s rather than being masked as 404s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace per-player get_or_none() calls in get_bratings() with two bulk
SELECT queries before the position loop, keyed by player_id and card+hand.
This reduces DB round trips from O(3N) to O(2) for all lineup difficulties.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Support the Discord bot's new scouting feature where players can scout
cards from other teams' opened packs. Stores opportunities with expiry
timestamps and tracks which teams claim which cards.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace all logging.warning(f'Bad Token: {token}') calls with
logging.warning('Bad Token: [REDACTED]') across 30 router files.
Full bearer tokens were being written to log files on auth failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reuse the result of get_or_none instead of discarding it and calling
get_by_id again, eliminating one unnecessary round-trip per request.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
return_val was assigned from DataFrame(data_list).to_csv() before the
player data row was appended to data_list, so the CSV response contained
only the header row. Moved the to_csv() call to after the append.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add None checks for vlval/vrval in get_total_ops inside sort_pitchers()
and sort_starters(). Returns float("inf") when ratings are missing so
pitchers without ratings sort to the end rather than raising AttributeError.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add explanatory comment clarifying that synchronous=OFF is a dev-only
trade-off (production uses PostgreSQL), and describing the crash-corruption
risk and how WAL mode partially mitigates it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moved logging.basicConfig() to app/main.py as the single source of truth.
Removed duplicate (no-op) calls from app/db_engine.py, app/dependencies.py,
and all 30 router files in app/routers_v2/. Removed the now-unused LOG_DATA
dict and date/log_level locals from dependencies.py and db_engine.py.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace two get_or_none calls per row in sort_pitchers and sort_starters
with a single batched SELECT for all card IDs, reducing N*2 queries to 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed 55 unused imports across 26 router files. Most were `db` imports
left over after the db.close() removal in the previous commit, plus
additional stale imports (scipy.stats, chunked, copy, base64, Html2Image,
pandas.DataFrame, pydantic.validator, etc.) that were already unused.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add db_session_middleware to main.py that opens the connection at the
start of each request and closes it in a try/finally block, ensuring
connections are always returned even on uncaught exceptions.
Remove all individual db.close() calls from 30 router files in
app/routers_v2/ — the middleware now handles all code paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pydantic evaluates bare `random.randint(1, 3)` once at class definition
time, so every PlayerModel instance shared the same value. Replaced with
`pydantic.Field(default_factory=...)` so a new random value is generated
per instance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Card model has no roster1/2/3 fields. Accessing them would raise
AttributeError at runtime. Removed the non-existent columns from
the CSV header and data row.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change `== 'False'` to `== 'True'` so TESTING=True routes to dev URL
- Fix leading space on TESTING=TRUE in .env so the var is actually set
- Update .env comment to match corrected logic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`all_teams.where(Team.is_ai)` always filtered for AI teams regardless
of the caller's intent. Match the existing has_guide pattern and use
explicit boolean comparison so False is handled correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added missing 'human' key to gauntlet-9 cardset configuration.
This was causing 500 errors when players tried to start gauntlet
games because the legal-check endpoint couldn't validate cards.
Error: KeyError: 'human' at app/routers_v2/cards.py:242
when checking CARDSETS[rarity_name]['human']
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add explicit ORDER BY id to all queries for consistent results across SQLite and PostgreSQL
- PostgreSQL does not guarantee row order without ORDER BY, unlike SQLite
- Skip table creation when DATABASE_TYPE=postgresql (production tables already exist)
- Fix datetime handling in notifications (PostgreSQL native datetime vs SQLite timestamp)
- Fix grouped query count() calls that don't work in PostgreSQL
- Update .gitignore to include storage/templates/ directory
This completes the PostgreSQL migration compatibility layer while maintaining
backwards compatibility with SQLite for local development.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
PostgreSQL does not guarantee row order without ORDER BY, unlike SQLite
which implicitly returned rows by rowid. This caused bugs where queries
returned results in unexpected order (e.g., get_team_by_owner returning
gauntlet team instead of main team).
Override select() in BaseModel to add default ordering by id. Explicit
.order_by() calls will override this default.
Also mark legacy db_engine.py as deprecated.
- Fix batstats.py and pitstats.py POST handlers to convert timestamps
- Fix Pydantic model defaults from *100000 to *1000 (wrong multiplier)
Found during second-pass audit.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Convert milliseconds to datetime for created filter in batstats.py
and pitstats.py GET endpoints.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Convert milliseconds to datetime for GET filters (created_after/before, ended_after/before)
- Fix is_active filter to use is_null() instead of comparing to 0
- Fix PATCH to use datetime.now() instead of int timestamps
- Fix POST to convert timestamps and use None for nullable ended field
- Update Pydantic model defaults to None instead of int
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Convert milliseconds timestamps to datetime for created_after filter
in notifications and created_after/before filters in paperdex.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Convert milliseconds timestamps from Discord bot to datetime objects
for PostgreSQL DateTimeField columns in notifications, packs, paperdex,
and rewards routers. Also fix rewards GET created_after filter.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix upsert_many() to use column_name for EXCLUDED references
(ForeignKeyField columns end in _id, e.g., batter -> batter_id)
- Add null checks in batting/pitching CSV output for player, team, game
fields to prevent 'NoneType' not subscriptable errors
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 2018 Promos (14) and 2022 Promos (4) to flashback mode legal cardsets
- Convert Unix timestamps to datetime in rewards POST/PATCH for PostgreSQL
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix /legal-check endpoint to handle card_ids passed as stringified list
- Add compose.production.yml for akamai deployment (pd_api container)
- Add migrate_missing_data.py script for filling gaps from initial migration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PostgreSQL requires GROUP BY for all non-aggregated columns when using
aggregate functions. Added group_by(pitcher, game) to the StratPlay query
that calculates pitcher innings in the /decisions/rest endpoint.
- Fix NULL handling for FK checks in stratplays.py: use x.field_id instead
of x.field to avoid triggering FK lookups on potentially missing rows
- Cast boolean is_start to integer for SUM() - PostgreSQL cannot sum booleans
- Add missing GROUP BY clause to Decision aggregate query
- Add Case import for boolean-to-integer casting
- Update migration script with boolean/datetime column mappings
- Exclude legacy battingstat/pitchingstat tables from migration
- Add comprehensive POSTGRES_MIGRATION_GUIDE.md documentation
Tested: /plays/batting and /plays/pitching endpoints work with group_by=player
- Add SQL migration script to update all franchise values
- Change AI roster queries from Team.lname to Team.sname
- Add FRANCHISE_NORMALIZE helper for bulk imports
- Update St Louis Cardinals hardcoded fix
Enables cross-era player matching for AI rosters (fixes Oakland Athletics issue)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Refactor get_batting_totals() to conditionally build SELECT fields based on group_by parameter
- Refactor get_pitching_totals() with same pattern
- Ensures all non-aggregated SELECT fields are included in GROUP BY clause
- Based on successful Major Domo migration pattern
- Add environment-based PostgreSQL configuration to db_engine.py
- Add table_name to all 30 models (Meta class)
- Update db_migrations.py to auto-select migrator based on DB type
- Add comprehensive PostgreSQL migration plan document