Replace per-row CardModel.get() in _build_card_state_response with a
bulk prefetch in list_card_states: collect variant player IDs, issue at
most 2 queries (BattingCard + PitchingCard), build a (player_id, variant)
-> image_url map, and pass the resolved value directly to the helper.
The single-card get_card_state path is unchanged and still resolves
image_url inline (one extra query is acceptable for a single-item response).
Closes#199
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reject unknown --flags with error instead of silently treating as commit SHA
- Declare remote_hash as local to prevent stale values across loop iterations
- Use associative array for container names (consistent with DEPLOY_HOST pattern)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The cache hit branch at line 773 still returned image/png, meaning
the MIME type fix was never seen in production since cached responses
dominate. Update it to match the cache miss path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
deploy.sh now checks local vs remote templates via md5sum on every
deploy and warns about drift. Pass --sync-templates to push changed
files. Also reports cached card image counts on the target server.
New clear-card-cache.sh script inspects or clears cached PNG/APNG
card images inside the API container, with --apng-only and --all
modes. Added scripts/README.md documenting all operational scripts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restores the commented-out `?variant=` query filter on GET /api/v2/cards
so callers can pass variant=0 for base cards or variant=N for a specific
refractor variant. Adds variant column to CSV output header and rows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add silver backing div behind diamond so the X gap lines pop
- Darken unfilled quads for better contrast with filled tier color
- Make diamond grid background transparent (backing shows through gaps)
- Deduplicate shared positioning into a combined CSS rule
- Remove dead TIER_DIAMOND_COLORS dict and filled_bg from Python
(template already computes these via Jinja)
NOTE: These are volume-mounted template files, NOT baked into the
Docker image. After merging, manually deploy to each server:
scp storage/templates/tier_style.html <host>:<container-data>/storage/templates/
scp storage/templates/player_card.html <host>:<container-data>/storage/templates/
Hosts:
Dev: ssh pd-database → /home/cal/container-data/dev-pd-database/
Prod: ssh akamai → /root/container-data/paper-dynasty/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The tier_style.html template references {{ filled_bg }} for diamond
quad backgrounds but it was never set in the rendering code, making
the tier indicator invisible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes PR #190 (chore/deploy-script — applied directly due to Gitea rebase conflict from stale branch base)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Guards against Peewee 3.17.9 returning None from .count() on a
complex multi-join query when 0 rows match the filter set.
Closes#183
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dev environment uses sba_postgres container, paperdynasty_dev database,
sba_admin user — not pd_postgres/pd_master as previously documented.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move lazy imports to top level in card_storage.py and players.py (CLAUDE.md violation)
- Use os.environ.get() for S3_BUCKET/S3_REGION to allow dev/prod bucket separation
- Fix test patch targets from app.db_engine to app.services.card_storage (required after top-level import move)
- Fix assert_called_once_with field name: MockBatting.player → MockBatting.player_id
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Pin boto3==1.42.65 to match project convention of exact version pins
- Use player_id (not player) for FK column access in card_storage.py
to match the pattern used throughout the codebase
- Add comment explaining the tier is None guard in S3 upload scheduling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds image_url field to each card state entry in the GET
/api/v2/refractor/cards response. Resolved by looking up the variant
BattingCard/PitchingCard row. Returns null when no image has been
rendered yet.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds BackgroundTasks to the card render endpoint. After rendering a
variant card (variant > 0) where image_url is None, schedules
backfill_variant_image_url to upload the PNG to S3 and populate
image_url on the card row.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New service with S3 upload functions for the refractor card art
pipeline. backfill_variant_image_url reads rendered PNGs from disk,
uploads to S3, and sets image_url on BattingCard/PitchingCard rows.
18 tests covering key construction, URL formatting, upload params,
and error swallowing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dev environment uses sba_postgres container, paperdynasty_dev database,
sba_admin user — not pd_postgres/pd_master as previously documented.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Speculative schema from initial Refractor design that was never used —
boosts are hardcoded in refractor_boost.py and tier visuals are embedded
in CSS templates. Both tables have zero rows on dev and prod.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of failing and requiring manual fix + re-commit, the hook now
runs ruff check --fix first, re-stages the fixed files, then checks
for remaining unfixable issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Skip PNG cache when ?tier= param is set to prevent serving stale T0 images
- Move {% if %} guard before diamond_colors dict in player_card.html
- Extract base #fullCard styles outside refractor conditional in tier_style.html
- Make run-local.sh DB host configurable, clean up Playwright check
Follow-up to PR #179
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move diamond left to align bottom point with center column divider
- Keep all border widths uniform across tiers (remove T4 bold borders)
- Remove corner accents entirely (T4 differentiated by glow + prismatic)
- Fix T4 header z-index: don't override position on absolutely-positioned
topright stat elements (stealing, running, bunting, hit & run)
- Add ?tier= query param for dev preview of tier styling on base cards
- Add run-local.sh for local API testing against dev database
- Add .env.local and .run-local.pid to .gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cards created before the refractor system was deployed have no
RefractorCardState row. Previously evaluate-game silently skipped these
players. Now it calls initialize_card_refractor on-the-fly so any card
used in a game gets refractor tracking regardless of when it was created.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move `import os` from inside evaluate_game() to module top-level imports
(lazy imports are only for circular dependency avoidance)
- Add get_or_none idempotency guard before RefractorBoostAudit.create()
inside db.atomic() to prevent IntegrityError on UNIQUE(card_state, tier)
constraint in PostgreSQL when apply_tier_boost is called twice for the
same tier
- Update atomicity test stub to provide card_state/tier attributes for
the new Peewee expression in the idempotency guard
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a card reaches a new Refractor tier during game evaluation, the
system now creates a boosted variant card with modified ratings. This
connects the Phase 2 Foundation pure functions (PR #176) to the live
evaluate-game endpoint.
Key changes:
- evaluate_card() gains dry_run parameter so apply_tier_boost() is the
sole writer of current_tier, ensuring atomicity with variant creation
- apply_tier_boost() orchestrates the full boost flow: source card
lookup, boost application, variant card + ratings creation, audit
record, and atomic state mutations inside db.atomic()
- evaluate_game() calls evaluate_card(dry_run=True) then loops through
intermediate tiers on tier-up, with error isolation per player
- Display stat helpers compute fresh avg/obp/slg for variant cards
- REFRACTOR_BOOST_ENABLED env var provides a kill switch
- 51 new tests: unit tests for display stats, integration tests for
orchestration, HTTP endpoint tests for multi-tier jumps, pitcher
path, kill switch, atomicity, idempotency, and cross-player isolation
- Clarified all "79-sum" references to note the 108-total card invariant
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename `evolution_tier` parameter to `refractor_tier` in compute_variant_hash()
to match the refractor naming convention established in PR #131
- Update hash input dict key accordingly (safe: function is new, no stored hashes)
- Update test docstrings referencing the old parameter name
- Remove redundant parentheses on boost_delta_json TextField declaration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>