feat: refractor tier-specific card art rendering #179

Merged
cal merged 3 commits from feature/refractor-card-art into main 2026-04-04 17:33:38 +00:00
Owner

Summary

  • Add tier-aware visual styling to Playwright card PNG rendering (T0-T4)
  • New tier_style.html template with per-tier CSS: borders, header backgrounds, column header gradients, inset glow, diamond indicator, corner accents
  • Diamond tier indicator: CSS 2x2 grid at header/result boundary, progressive fill (1B→2B→3B→Home) with approved tier colors (T1 green, T2 blue, T3 red, T4 purple)
  • resolve_refractor_tier() helper: pure-math tier lookup from variant hash (no DB query)
  • T3/T4 CSS animations defined but paused for static PNG capture (APNG follow-up)

Design Decisions

  • No RefractorCosmetic table wiring — tier styling embedded directly in CSS template for simplicity
  • No card_creation.py changes — presentation logic stays in templates/router
  • T0 cards unchanged — tier_style emits only position: relative; overflow: hidden on #fullCard
  • Body colors sacred#ACE6FF / #EAA49C never overridden

Tier Visual Summary

Tier Borders Header Col Headers Diamond Special
T0 black white blue/red none
T1 silver silver gradient blue/red 1 green
T2 blue-silver iridescent blue-purple/red-magenta 2 blue inset glow
T3 gold gold gradient gold 3 red gold inset glow, shimmer (paused)
T4 bold gold white + prismatic gold 4 purple + glow dual glow, corners, prismatic (paused)

Deploy Notes

One-time cache bust needed before deploy:

find storage/cards -name '*-v[^0]*.png' -delete

Test Plan

  • T0 card renders identically to current output (?html=True)
  • T1-T4 variant cards show correct tier styling in HTML output
  • Diamond quad count matches tier (1-4)
  • Diamond colors match approved palette
  • T4 has corner accents in HTML
  • Unknown variant falls back to T0 gracefully
  • python -m pytest passes (no model changes)

Relates-to: initiative #19

🤖 Generated with Claude Code

## Summary - Add tier-aware visual styling to Playwright card PNG rendering (T0-T4) - New `tier_style.html` template with per-tier CSS: borders, header backgrounds, column header gradients, inset glow, diamond indicator, corner accents - Diamond tier indicator: CSS 2x2 grid at header/result boundary, progressive fill (1B→2B→3B→Home) with approved tier colors (T1 green, T2 blue, T3 red, T4 purple) - `resolve_refractor_tier()` helper: pure-math tier lookup from variant hash (no DB query) - T3/T4 CSS animations defined but paused for static PNG capture (APNG follow-up) ## Design Decisions - **No `RefractorCosmetic` table wiring** — tier styling embedded directly in CSS template for simplicity - **No `card_creation.py` changes** — presentation logic stays in templates/router - **T0 cards unchanged** — tier_style emits only `position: relative; overflow: hidden` on `#fullCard` - **Body colors sacred** — `#ACE6FF` / `#EAA49C` never overridden ## Tier Visual Summary | Tier | Borders | Header | Col Headers | Diamond | Special | |------|---------|--------|-------------|---------|---------| | T0 | black | white | blue/red | none | — | | T1 | silver | silver gradient | blue/red | 1 green | — | | T2 | blue-silver | iridescent | blue-purple/red-magenta | 2 blue | inset glow | | T3 | gold | gold gradient | gold | 3 red | gold inset glow, shimmer (paused) | | T4 | bold gold | white + prismatic | gold | 4 purple + glow | dual glow, corners, prismatic (paused) | ## Deploy Notes One-time cache bust needed before deploy: ```bash find storage/cards -name '*-v[^0]*.png' -delete ``` ## Test Plan - [ ] T0 card renders identically to current output (`?html=True`) - [ ] T1-T4 variant cards show correct tier styling in HTML output - [ ] Diamond quad count matches tier (1-4) - [ ] Diamond colors match approved palette - [ ] T4 has corner accents in HTML - [ ] Unknown variant falls back to T0 gracefully - [ ] `python -m pytest` passes (no model changes) Relates-to: initiative #19 🤖 Generated with [Claude Code](https://claude.com/claude-code)
cal added 1 commit 2026-04-04 05:15:07 +00:00
Implement tier-aware visual styling for card PNG rendering (T0-T4).
Each refractor tier gets distinct borders, header backgrounds, column
header gradients, diamond tier indicators, and decorative effects.

- New tier_style.html template: per-tier CSS overrides (borders, headers,
  gradients, inset glow, diamond positioning, corner accents)
- Diamond indicator: 2x2 CSS grid rotated 45deg at header/result boundary,
  progressive fill (1B→2B→3B→Home) with tier-specific colors
- T4 Superfractor: bold gold borders, dual gold-teal glow, corner accents,
  purple diamond with glow pulse animation
- resolve_refractor_tier() helper: pure-math tier lookup from variant hash
- T3/T4 animations defined but paused for static PNG capture (APNG follow-up)

Relates-to: initiative #19

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude added the
ai-reviewing
label 2026-04-04 05:15:26 +00:00
cal reviewed 2026-04-04 05:17:22 +00:00
cal left a comment
Author
Owner

AI Code Review

Files Reviewed

  • app/routers_v2/players.py (modified)
  • storage/templates/player_card.html (modified)
  • storage/templates/tier_style.html (added)

Findings

Correctness

resolve_refractor_tier logic is sound. The function iterates tiers 1–4, calls compute_variant_hash(player_id, tier), and compares against the incoming variant. This correctly reverse-maps the hash. Variant 0 is explicitly caught first and returns T0 before the loop, which matches the reserved-variant convention in compute_variant_hash.

Unknown variant falls back to T0 gracefully. If no tier 1–4 hash matches, the function returns 0. The template gates all tier markup behind refractor_tier > 0, so T0 cards render identically to the pre-PR baseline. This is the correct safe default.

refractor_tier is injected before request in both batter and pitcher branches. Both code paths in get_batter_card set card_data["refractor_tier"] then card_data["request"], then call TemplateResponse. Consistent across both branches.

Diamond quad fill ordering. The four quads are assigned: 2B-position filled if tier >= 2, 1B-position filled if tier >= 1, 3B-position filled if tier >= 3, Home filled if tier >= 4. This produces counts of 1/2/3/4 filled quads for T1/T2/T3/T4 respectively. Correct.

Edge Cases

Variant in DB but not matching any tier hash. The function exhausts all 4 tiers, finds no match, and returns 0. T0 rendering applies — correct and safe.

T1 has a #resultHeader.border-bot override (3px) but T2 does not. In style.html, .border-bot is border-bottom: 3px solid black. T2 overrides .border-bot globally to 4px, which will also apply to #resultHeader. T1 uses 4px globally then narrows #resultHeader.border-bot back to 3px — a deliberate design subtlety. Worth a visual check on T2's result header thickness, but not a logic bug.

T3/T4 animations paused for static PNG. Playwright screenshots the initial paint at the 0% keyframe, where both shimmer and prismatic sweep are transparent. No visible artifact bleeds into the PNG. Correct approach.

T4 #header::after with width: 200%. With overflow: hidden on #header (set in the T4 block), the overflowing overlay is clipped. The animation rests at translateX(0%) for the static capture — the left half of the gradient is visible, which at the stated opacity values is a subtle tint. Acceptable.

#header > * { position: relative; z-index: 2; } (T4 only). Correctly layers all header children above the z-index: 1 prismatic ::after overlay. Selector is appropriately tight to direct children.

CSS Specificity

T2 double .border-right-thick rules. #resultHeader .border-right-thick (specificity 0,2,0) sets border-right-width: 6px; then .border-right-thick (0,1,0) sets the color. The more-specific rule wins on width inside #resultHeader. Intended behavior, no conflict.

T3 duplicate #header blocks. Lines 98–101 set the background; line 126 adds overflow: hidden; position: relative;. Two #header blocks in the same tier — both apply cleanly since there's no property overlap. No conflict, but could be consolidated (see suggestions).

tier_style.html loads after style.html in <head>. All tier overrides win specificity ties by source order. No !important anywhere. Clean cascade.

Body colors (#ACE6FF, #EAA49C) are not referenced anywhere in tier_style.html. Design constraint upheld.

Style & Conventions

compute_variant_hash import is top-level at line 35 with the other module imports. Matches the project convention against mid-file lazy imports.

resolve_refractor_tier is a plain def, not async. Correct — pure CPU math with no I/O.

Template variable guard refractor_tier is defined and refractor_tier > 0. Correct defensive Jinja2 pattern. Covers both a missing key (if another caller path renders the template without setting it) and the T0 case in one guard.

Diamond quad inline box-shadow is repeated verbatim on all four quad elements. Functionally correct; a CSS class would be cleaner (see suggestions).

Security

No issues. refractor_tier is a server-side integer; no user input reaches the template unescaped. No hardcoded credentials.

Test Coverage

No automated tests added for resolve_refractor_tier. The function is a simple loop over the already-tested compute_variant_hash, so risk is low. A parametrized pytest covering T0 passthrough, a valid tier match, and an unrecognized variant would be a worthwhile follow-up.


Suggestions (non-blocking)

  1. Consolidate the duplicate #header block in T3. Merge overflow: hidden; position: relative; into the first #header block at the top of the T3 section.

  2. Extract repeated diamond quad box-shadow to a CSS rule. Adding .diamond-quad.filled { box-shadow: inset 0 1px 2px ...; } in tier_style.html removes the four identical inline style= attributes from player_card.html.

  3. Consider a quick unit test for resolve_refractor_tier. Three parametrize cases cover it fully: variant=0 → T0, compute_variant_hash(player_id, 2) → T2, and an unrecognized variant → T0.


Verdict: APPROVED

The implementation is correct. Tier resolution is pure math with a safe fallback. CSS overrides are properly scoped, load after the base stylesheet, and honor the body-color constraint. T3/T4 animations are correctly paused for static PNG capture. No security issues. The suggestions above are cleanup items only — nothing blocks merge.

Note: Gitea prevents self-approval, so this is posted as COMMENT. Verdict is APPROVED.


Automated review by Claude PR Reviewer

## AI Code Review ### Files Reviewed - `app/routers_v2/players.py` (modified) - `storage/templates/player_card.html` (modified) - `storage/templates/tier_style.html` (added) --- ### Findings #### Correctness **`resolve_refractor_tier` logic is sound.** The function iterates tiers 1–4, calls `compute_variant_hash(player_id, tier)`, and compares against the incoming `variant`. This correctly reverse-maps the hash. Variant 0 is explicitly caught first and returns T0 before the loop, which matches the reserved-variant convention in `compute_variant_hash`. **Unknown variant falls back to T0 gracefully.** If no tier 1–4 hash matches, the function returns 0. The template gates all tier markup behind `refractor_tier > 0`, so T0 cards render identically to the pre-PR baseline. This is the correct safe default. **`refractor_tier` is injected before `request` in both batter and pitcher branches.** Both code paths in `get_batter_card` set `card_data["refractor_tier"]` then `card_data["request"]`, then call `TemplateResponse`. Consistent across both branches. **Diamond quad fill ordering.** The four quads are assigned: 2B-position filled if tier >= 2, 1B-position filled if tier >= 1, 3B-position filled if tier >= 3, Home filled if tier >= 4. This produces counts of 1/2/3/4 filled quads for T1/T2/T3/T4 respectively. Correct. #### Edge Cases **Variant in DB but not matching any tier hash.** The function exhausts all 4 tiers, finds no match, and returns 0. T0 rendering applies — correct and safe. **T1 has a `#resultHeader.border-bot` override (3px) but T2 does not.** In `style.html`, `.border-bot` is `border-bottom: 3px solid black`. T2 overrides `.border-bot` globally to 4px, which will also apply to `#resultHeader`. T1 uses 4px globally then narrows `#resultHeader.border-bot` back to 3px — a deliberate design subtlety. Worth a visual check on T2's result header thickness, but not a logic bug. **T3/T4 animations paused for static PNG.** Playwright screenshots the initial paint at the 0% keyframe, where both shimmer and prismatic sweep are transparent. No visible artifact bleeds into the PNG. Correct approach. **T4 `#header::after` with `width: 200%`.** With `overflow: hidden` on `#header` (set in the T4 block), the overflowing overlay is clipped. The animation rests at `translateX(0%)` for the static capture — the left half of the gradient is visible, which at the stated opacity values is a subtle tint. Acceptable. **`#header > * { position: relative; z-index: 2; }` (T4 only).** Correctly layers all header children above the `z-index: 1` prismatic `::after` overlay. Selector is appropriately tight to direct children. #### CSS Specificity **T2 double `.border-right-thick` rules.** `#resultHeader .border-right-thick` (specificity 0,2,0) sets `border-right-width: 6px`; then `.border-right-thick` (0,1,0) sets the color. The more-specific rule wins on width inside `#resultHeader`. Intended behavior, no conflict. **T3 duplicate `#header` blocks.** Lines 98–101 set the background; line 126 adds `overflow: hidden; position: relative;`. Two `#header` blocks in the same tier — both apply cleanly since there's no property overlap. No conflict, but could be consolidated (see suggestions). **`tier_style.html` loads after `style.html` in `<head>`.** All tier overrides win specificity ties by source order. No `!important` anywhere. Clean cascade. **Body colors (`#ACE6FF`, `#EAA49C`) are not referenced anywhere in `tier_style.html`.** Design constraint upheld. #### Style & Conventions **`compute_variant_hash` import is top-level** at line 35 with the other module imports. Matches the project convention against mid-file lazy imports. **`resolve_refractor_tier` is a plain `def`, not `async`.** Correct — pure CPU math with no I/O. **Template variable guard `refractor_tier is defined and refractor_tier > 0`.** Correct defensive Jinja2 pattern. Covers both a missing key (if another caller path renders the template without setting it) and the T0 case in one guard. **Diamond quad inline `box-shadow` is repeated verbatim on all four quad elements.** Functionally correct; a CSS class would be cleaner (see suggestions). #### Security No issues. `refractor_tier` is a server-side integer; no user input reaches the template unescaped. No hardcoded credentials. #### Test Coverage No automated tests added for `resolve_refractor_tier`. The function is a simple loop over the already-tested `compute_variant_hash`, so risk is low. A parametrized pytest covering T0 passthrough, a valid tier match, and an unrecognized variant would be a worthwhile follow-up. --- ### Suggestions (non-blocking) 1. **Consolidate the duplicate `#header` block in T3.** Merge `overflow: hidden; position: relative;` into the first `#header` block at the top of the T3 section. 2. **Extract repeated diamond quad `box-shadow` to a CSS rule.** Adding `.diamond-quad.filled { box-shadow: inset 0 1px 2px ...; }` in `tier_style.html` removes the four identical inline `style=` attributes from `player_card.html`. 3. **Consider a quick unit test for `resolve_refractor_tier`.** Three parametrize cases cover it fully: `variant=0` → T0, `compute_variant_hash(player_id, 2)` → T2, and an unrecognized variant → T0. --- ### Verdict: APPROVED The implementation is correct. Tier resolution is pure math with a safe fallback. CSS overrides are properly scoped, load after the base stylesheet, and honor the body-color constraint. T3/T4 animations are correctly paused for static PNG capture. No security issues. The suggestions above are cleanup items only — nothing blocks merge. > Note: Gitea prevents self-approval, so this is posted as COMMENT. Verdict is APPROVED. --- *Automated review by Claude PR Reviewer*
Claude reviewed 2026-04-04 05:17:31 +00:00
Claude left a comment
Collaborator

AI Code Review

Files Reviewed

  • app/routers_v2/players.py (modified)
  • storage/templates/player_card.html (modified)
  • storage/templates/tier_style.html (added)

Findings

Correctness

  • resolve_refractor_tier() correctly short-circuits at variant == 0 (T0 fast path) and falls back to T0 for unrecognized variant hashes — graceful degradation confirmed.
  • refractor_tier is set in card_data on both the batting and pitching code paths, before templates.TemplateResponse(...) is called.
  • Diamond fill logic produces the correct count per tier: T1=1, T2=2, T3=3, T4=4. The non-sequential quad ordering (2→1→3→4 in template) is intentional — it maps to the visual 45°-rotated grid positions (right=1B, top=2B, left=3B, bottom=Home).
  • Cache path (file exists + html=False) bypasses rendering entirely — refractor_tier is never computed in that branch, which is correct since the PNG is already stored. The deploy cache-bust is appropriately documented.
  • Performance: resolve_refractor_tier() does at most 4 SHA-256 hashes per render, negligible versus Playwright overhead.

Security

  • No issues. player_id and variant are typed integers from path params. No user data is interpolated into CSS or HTML template strings.

Style & Conventions

  • T3 has two separate #header rules in tier_style.html. The shimmer animation comment block opens a second #header { overflow: hidden; position: relative; } rule rather than merging it with the first. CSS cascade applies both fine (second block adds properties, doesn't override background), but the split is slightly confusing. Not a bug.
  • Missing trailing newline in tier_style.html (\ No newline at end of file in diff). Minor convention issue.
  • Import of compute_variant_hash is correctly placed at top-level, following the CLAUDE.md "never add lazy imports to middle of file" rule.

Suggestions

  • T3 tier_style.html: merge overflow: hidden; position: relative into the primary #header block for T3 rather than adding a second #header rule after the animation keyframe comment. Purely cosmetic — not worth a follow-up issue.
  • The {% if refractor_tier is defined and ... %} guard in both templates is a reasonable safety net for template reuse, but since resolve_refractor_tier() always sets the key to an int (0–4), the is defined check is technically redundant. No action needed.

Verdict: COMMENT

Clean implementation. Logic is correct across all four tiers and the T0 no-op path. The two cosmetic issues (split T3 #header rule, missing trailing newline) don't block merging. The deploy cache-bust procedure is well-documented in the PR body.


Automated review by Claude PR Reviewer

## AI Code Review ### Files Reviewed - `app/routers_v2/players.py` (modified) - `storage/templates/player_card.html` (modified) - `storage/templates/tier_style.html` (added) ### Findings #### Correctness - `resolve_refractor_tier()` correctly short-circuits at `variant == 0` (T0 fast path) and falls back to T0 for unrecognized variant hashes — graceful degradation confirmed. - `refractor_tier` is set in `card_data` on **both** the batting and pitching code paths, before `templates.TemplateResponse(...)` is called. ✅ - Diamond fill logic produces the correct count per tier: T1=1, T2=2, T3=3, T4=4. The non-sequential quad ordering (2→1→3→4 in template) is intentional — it maps to the visual 45°-rotated grid positions (right=1B, top=2B, left=3B, bottom=Home). ✅ - Cache path (file exists + `html=False`) bypasses rendering entirely — `refractor_tier` is never computed in that branch, which is correct since the PNG is already stored. The deploy cache-bust is appropriately documented. ✅ - Performance: `resolve_refractor_tier()` does at most 4 SHA-256 hashes per render, negligible versus Playwright overhead. ✅ #### Security - No issues. `player_id` and `variant` are typed integers from path params. No user data is interpolated into CSS or HTML template strings. #### Style & Conventions - **T3 has two separate `#header` rules** in `tier_style.html`. The shimmer animation comment block opens a second `#header { overflow: hidden; position: relative; }` rule rather than merging it with the first. CSS cascade applies both fine (second block adds properties, doesn't override background), but the split is slightly confusing. Not a bug. - **Missing trailing newline** in `tier_style.html` (`\ No newline at end of file` in diff). Minor convention issue. - Import of `compute_variant_hash` is correctly placed at top-level, following the CLAUDE.md "never add lazy imports to middle of file" rule. ✅ #### Suggestions - T3 `tier_style.html`: merge `overflow: hidden; position: relative` into the primary `#header` block for T3 rather than adding a second `#header` rule after the animation keyframe comment. Purely cosmetic — not worth a follow-up issue. - The `{% if refractor_tier is defined and ... %}` guard in both templates is a reasonable safety net for template reuse, but since `resolve_refractor_tier()` always sets the key to an int (0–4), the `is defined` check is technically redundant. No action needed. ### Verdict: COMMENT Clean implementation. Logic is correct across all four tiers and the T0 no-op path. The two cosmetic issues (split T3 `#header` rule, missing trailing newline) don't block merging. The deploy cache-bust procedure is well-documented in the PR body. --- *Automated review by Claude PR Reviewer*
Claude added
ai-reviewed
and removed
ai-reviewing
labels 2026-04-04 05:17:50 +00:00
cal added 1 commit 2026-04-04 05:43:33 +00:00
- Consolidate T3 duplicate #header rule into single block with overflow/position
- Add explicit T2 #resultHeader border-bottom-width (4px) for clarity
- Move diamond quad filled box-shadow from inline styles to .diamond-quad.filled CSS rule
- Add TestResolveTier: 6 parametrized tests covering tier roundtrip, base card, unknown variant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cal added 1 commit 2026-04-04 16:20:17 +00:00
- 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>
Claude approved these changes 2026-04-04 17:33:20 +00:00
Dismissed
Claude left a comment
Collaborator

Approved via pd-pr

Approved via pd-pr
Claude approved these changes 2026-04-04 17:33:36 +00:00
Claude left a comment
Collaborator

Approved via pd-pr

Approved via pd-pr
cal merged commit 3e84a06b23 into main 2026-04-04 17:33:38 +00:00
cal deleted branch feature/refractor-card-art 2026-04-04 17:33:38 +00:00
Sign in to join this conversation.
No description provided.