diff --git a/.gitignore b/.gitignore index 2725e46..583b76f 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,8 @@ pyenv.cfg pyvenv.cfg docker-compose.override.yml docker-compose.*.yml +.run-local.pid +.env.local *.db venv .claude/ diff --git a/app/routers_v2/players.py b/app/routers_v2/players.py index 7ebd1b5..0f0d380 100644 --- a/app/routers_v2/players.py +++ b/app/routers_v2/players.py @@ -32,6 +32,7 @@ from ..db_engine import ( ) from ..db_helpers import upsert_players from ..dependencies import oauth2_scheme, valid_token +from ..services.refractor_boost import compute_variant_hash # --------------------------------------------------------------------------- # Persistent browser instance (WP-02) @@ -132,6 +133,19 @@ def normalize_franchise(franchise: str) -> str: return FRANCHISE_NORMALIZE.get(titled, titled) +def resolve_refractor_tier(player_id: int, variant: int) -> int: + """Determine the refractor tier (0-4) from a player's variant hash. + + Pure math — no DB query needed. Returns 0 for base cards or unknown variants. + """ + if variant == 0: + return 0 + for tier in range(1, 5): + if compute_variant_hash(player_id, tier) == variant: + return tier + return 0 + + router = APIRouter(prefix="/api/v2/players", tags=["players"]) @@ -723,6 +737,9 @@ async def get_batter_card( variant: int = 0, d: str = None, html: Optional[bool] = False, + tier: Optional[int] = Query( + None, ge=0, le=4, description="Override refractor tier for preview (dev only)" + ), ): try: this_player = Player.get_by_id(player_id) @@ -786,6 +803,9 @@ async def get_batter_card( card_data["cardset_name"] = this_player.cardset.name else: card_data["cardset_name"] = this_player.description + card_data["refractor_tier"] = ( + tier if tier is not None else resolve_refractor_tier(player_id, variant) + ) card_data["request"] = request html_response = templates.TemplateResponse("player_card.html", card_data) @@ -823,6 +843,9 @@ async def get_batter_card( card_data["cardset_name"] = this_player.cardset.name else: card_data["cardset_name"] = this_player.description + card_data["refractor_tier"] = ( + tier if tier is not None else resolve_refractor_tier(player_id, variant) + ) card_data["request"] = request html_response = templates.TemplateResponse("player_card.html", card_data) diff --git a/run-local.sh b/run-local.sh new file mode 100755 index 0000000..df7b0ad --- /dev/null +++ b/run-local.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# run-local.sh — Spin up the Paper Dynasty Database API locally for testing. +# +# Connects to the dev PostgreSQL on the homelab (10.10.0.42) so you get real +# card data for rendering. Playwright Chromium must be installed locally +# (it already is on this workstation). +# +# Usage: +# ./run-local.sh # start on default port 8000 +# ./run-local.sh 8001 # start on custom port +# ./run-local.sh --stop # kill a running instance +# +# Card rendering test URLs (after startup): +# HTML preview: http://localhost:8000/api/v2/players/{id}/battingcard/{date}/{variant}?html=True +# PNG render: http://localhost:8000/api/v2/players/{id}/battingcard/{date}/{variant} +# API docs: http://localhost:8000/api/docs + +set -euo pipefail +cd "$(dirname "$0")" + +PORT="${1:-8000}" +PIDFILE=".run-local.pid" +LOGFILE="logs/database/run-local.log" + +# ── Stop mode ──────────────────────────────────────────────────────────────── +if [[ "${1:-}" == "--stop" ]]; then + if [[ -f "$PIDFILE" ]]; then + pid=$(cat "$PIDFILE") + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" + echo "Stopped local API (PID $pid)" + else + echo "PID $pid not running (stale pidfile)" + fi + rm -f "$PIDFILE" + else + echo "No pidfile found — nothing to stop" + fi + exit 0 +fi + +# ── Pre-flight checks ─────────────────────────────────────────────────────── +if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "Already running (PID $(cat "$PIDFILE")). Use './run-local.sh --stop' first." + exit 1 +fi + +# Check Python deps are importable +python -c "import fastapi, peewee, playwright" 2>/dev/null || { + echo "Missing Python dependencies. Install with: pip install -r requirements.txt" + exit 1 +} + +# Check Playwright Chromium is available +python -c " +from playwright.sync_api import sync_playwright +p = sync_playwright().start() +b = p.chromium.launch(args=['--no-sandbox']) +b.close() +p.stop() +" 2>/dev/null || { + echo "Playwright Chromium not installed. Run: playwright install chromium" + exit 1 +} + +# Check dev DB is reachable +python -c " +import socket +s = socket.create_connection(('10.10.0.42', 5432), timeout=3) +s.close() +" 2>/dev/null || { + echo "Cannot reach dev PostgreSQL at 10.10.0.42:5432 — is the homelab up?" + exit 1 +} + +# ── Ensure directories exist ──────────────────────────────────────────────── +mkdir -p logs/database +mkdir -p storage/cards + +# ── Launch ─────────────────────────────────────────────────────────────────── +echo "Starting Paper Dynasty Database API on http://localhost:${PORT}" +echo " DB: paperdynasty_dev @ 10.10.0.42" +echo " Logs: ${LOGFILE}" +echo "" + +# Load .env, then .env.local overrides (for passwords not in version control) +set -a +# shellcheck source=/dev/null +[[ -f .env ]] && source .env +[[ -f .env.local ]] && source .env.local +set +a + +# Override DB host to point at the dev server's IP (not Docker network name) +export DATABASE_TYPE=postgresql +export POSTGRES_HOST="${POSTGRES_HOST_LOCAL:-10.10.0.42}" +export POSTGRES_PORT="${POSTGRES_PORT:-5432}" +export POSTGRES_DB="${POSTGRES_DB:-paperdynasty_dev}" +export POSTGRES_USER="${POSTGRES_USER:-sba_admin}" +export LOG_LEVEL=INFO +export TESTING=True + +if [[ -z "${POSTGRES_PASSWORD:-}" || "$POSTGRES_PASSWORD" == "your_production_password" ]]; then + echo "ERROR: POSTGRES_PASSWORD not set or is the placeholder value." + echo "Create .env.local with: POSTGRES_PASSWORD=" + exit 1 +fi + +uvicorn app.main:app \ + --host 0.0.0.0 \ + --port "$PORT" \ + --reload \ + --reload-dir app \ + --reload-dir storage/templates \ + 2>&1 | tee "$LOGFILE" & + +echo $! >"$PIDFILE" +sleep 2 + +if kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "" + echo "API running (PID $(cat "$PIDFILE"))." + echo "" + echo "Quick test URLs:" + echo " API docs: http://localhost:${PORT}/api/docs" + echo " Health: curl -s http://localhost:${PORT}/api/v2/players/1/battingcard?html=True" + echo "" + echo "Stop with: ./run-local.sh --stop" +else + echo "Failed to start — check ${LOGFILE}" + rm -f "$PIDFILE" + exit 1 +fi diff --git a/storage/templates/player_card.html b/storage/templates/player_card.html index 9cdf814..81f522b 100644 --- a/storage/templates/player_card.html +++ b/storage/templates/player_card.html @@ -2,9 +2,26 @@ {% include 'style.html' %} + {% include 'tier_style.html' %}
+ {%- set diamond_colors = { + 1: {'color': '#1a6b1a', 'highlight': '#40b040'}, + 2: {'color': '#2070b0', 'highlight': '#50a0e8'}, + 3: {'color': '#a82020', 'highlight': '#e85050'}, + 4: {'color': '#6b2d8e', 'highlight': '#a060d0'}, + } -%} + {% if refractor_tier is defined and refractor_tier > 0 %} + {%- set dc = diamond_colors[refractor_tier] -%} + {%- set filled_bg = 'linear-gradient(135deg, ' ~ dc.highlight ~ ' 0%, ' ~ dc.color ~ ' 50%, ' ~ dc.color ~ ' 100%)' -%} +
+
= 2 %}style="background: {{ filled_bg }};"{% endif %}>
+
= 1 %}style="background: {{ filled_bg }};"{% endif %}>
+
= 3 %}style="background: {{ filled_bg }};"{% endif %}>
+
= 4 %}style="background: {{ filled_bg }};"{% endif %}>
+
+ {% endif %}