feat: refractor tier-specific card art rendering #179
No reviewers
Labels
No Label
ai-changes-requested
ai-failed
ai-merged
ai-pr-opened
ai-reviewed
ai-reviewing
ai-reviewing
ai-working
bug
enhancement
evolution
performance
phase-0
phase-1a
phase-1b
phase-1c
phase-1d
security
tech-debt
todo
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/paper-dynasty-database#179
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/refractor-card-art"
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?
Summary
tier_style.htmltemplate with per-tier CSS: borders, header backgrounds, column header gradients, inset glow, diamond indicator, corner accentsresolve_refractor_tier()helper: pure-math tier lookup from variant hash (no DB query)Design Decisions
RefractorCosmetictable wiring — tier styling embedded directly in CSS template for simplicitycard_creation.pychanges — presentation logic stays in templates/routerposition: relative; overflow: hiddenon#fullCard#ACE6FF/#EAA49Cnever overriddenTier Visual Summary
Deploy Notes
One-time cache bust needed before deploy:
Test Plan
?html=True)python -m pytestpasses (no model changes)Relates-to: initiative #19
🤖 Generated with Claude Code
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_tierlogic is sound. The function iterates tiers 1–4, callscompute_variant_hash(player_id, tier), and compares against the incomingvariant. This correctly reverse-maps the hash. Variant 0 is explicitly caught first and returns T0 before the loop, which matches the reserved-variant convention incompute_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_tieris injected beforerequestin both batter and pitcher branches. Both code paths inget_batter_cardsetcard_data["refractor_tier"]thencard_data["request"], then callTemplateResponse. 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-botoverride (3px) but T2 does not. Instyle.html,.border-botisborder-bottom: 3px solid black. T2 overrides.border-botglobally to 4px, which will also apply to#resultHeader. T1 uses 4px globally then narrows#resultHeader.border-botback 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::afterwithwidth: 200%. Withoverflow: hiddenon#header(set in the T4 block), the overflowing overlay is clipped. The animation rests attranslateX(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 thez-index: 1prismatic::afteroverlay. Selector is appropriately tight to direct children.CSS Specificity
T2 double
.border-right-thickrules.#resultHeader .border-right-thick(specificity 0,2,0) setsborder-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
#headerblocks. Lines 98–101 set the background; line 126 addsoverflow: hidden; position: relative;. Two#headerblocks in the same tier — both apply cleanly since there's no property overlap. No conflict, but could be consolidated (see suggestions).tier_style.htmlloads afterstyle.htmlin<head>. All tier overrides win specificity ties by source order. No!importantanywhere. Clean cascade.Body colors (
#ACE6FF,#EAA49C) are not referenced anywhere intier_style.html. Design constraint upheld.Style & Conventions
compute_variant_hashimport is top-level at line 35 with the other module imports. Matches the project convention against mid-file lazy imports.resolve_refractor_tieris a plaindef, notasync. 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-shadowis repeated verbatim on all four quad elements. Functionally correct; a CSS class would be cleaner (see suggestions).Security
No issues.
refractor_tieris 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-testedcompute_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)
Consolidate the duplicate
#headerblock in T3. Mergeoverflow: hidden; position: relative;into the first#headerblock at the top of the T3 section.Extract repeated diamond quad
box-shadowto a CSS rule. Adding.diamond-quad.filled { box-shadow: inset 0 1px 2px ...; }intier_style.htmlremoves the four identical inlinestyle=attributes fromplayer_card.html.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.
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 atvariant == 0(T0 fast path) and falls back to T0 for unrecognized variant hashes — graceful degradation confirmed.refractor_tieris set incard_dataon both the batting and pitching code paths, beforetemplates.TemplateResponse(...)is called. ✅html=False) bypasses rendering entirely —refractor_tieris never computed in that branch, which is correct since the PNG is already stored. The deploy cache-bust is appropriately documented. ✅resolve_refractor_tier()does at most 4 SHA-256 hashes per render, negligible versus Playwright overhead. ✅Security
player_idandvariantare typed integers from path params. No user data is interpolated into CSS or HTML template strings.Style & Conventions
#headerrules intier_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.tier_style.html(\ No newline at end of filein diff). Minor convention issue.compute_variant_hashis correctly placed at top-level, following the CLAUDE.md "never add lazy imports to middle of file" rule. ✅Suggestions
tier_style.html: mergeoverflow: hidden; position: relativeinto the primary#headerblock for T3 rather than adding a second#headerrule after the animation keyframe comment. Purely cosmetic — not worth a follow-up issue.{% if refractor_tier is defined and ... %}guard in both templates is a reasonable safety net for template reuse, but sinceresolve_refractor_tier()always sets the key to an int (0–4), theis definedcheck 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
#headerrule, 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
Approved via pd-pr
Approved via pd-pr