Add golf ball logo with card suits and fix server shutdown hang

- Add SVG golf ball logo with dimples and card suit symbols (♣♦♠♥)
- Place logo to the left of the golfer emoji in header
- Fix server shutdown hanging by removing custom signal handlers
  that intercepted SIGINT/SIGTERM without triggering uvicorn shutdown

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee 2026-01-27 12:31:21 -05:00
parent 546e63ffed
commit d2e78da7d2
4 changed files with 112 additions and 12 deletions

101
client/golfball-logo.svg Normal file
View File

@ -0,0 +1,101 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<defs>
<!-- Gradient for 3D ball effect -->
<radialGradient id="ballGradient" cx="30%" cy="25%" r="65%" fx="25%" fy="20%">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="50%" stop-color="#f5f5f5"/>
<stop offset="80%" stop-color="#e0e0e0"/>
<stop offset="100%" stop-color="#c8c8c8"/>
</radialGradient>
<!-- Dimple shading gradient -->
<radialGradient id="dimpleGrad" cx="40%" cy="35%" r="60%">
<stop offset="0%" stop-color="#d0d0d0"/>
<stop offset="100%" stop-color="#b8b8b8"/>
</radialGradient>
<!-- Clip for dimples to stay within ball -->
<clipPath id="ballClip">
<circle cx="50" cy="50" r="45"/>
</clipPath>
</defs>
<!-- Main ball base -->
<circle cx="50" cy="50" r="46" fill="url(#ballGradient)"/>
<!-- Dimples - arranged in organic pattern -->
<g clip-path="url(#ballClip)" fill="url(#dimpleGrad)" opacity="0.5">
<!-- Ring 1 - outer edge dimples -->
<circle cx="50" cy="8" r="3.5"/>
<circle cx="67" cy="12" r="3.5"/>
<circle cx="81" cy="23" r="3.5"/>
<circle cx="89" cy="40" r="3.5"/>
<circle cx="90" cy="58" r="3.5"/>
<circle cx="83" cy="75" r="3.5"/>
<circle cx="70" cy="87" r="3.5"/>
<circle cx="50" cy="92" r="3.5"/>
<circle cx="30" cy="87" r="3.5"/>
<circle cx="17" cy="75" r="3.5"/>
<circle cx="10" cy="58" r="3.5"/>
<circle cx="11" cy="40" r="3.5"/>
<circle cx="19" cy="23" r="3.5"/>
<circle cx="33" cy="12" r="3.5"/>
<!-- Ring 2 -->
<circle cx="50" cy="18" r="3.2"/>
<circle cx="64" cy="22" r="3.2"/>
<circle cx="75" cy="32" r="3.2"/>
<circle cx="80" cy="47" r="3.2"/>
<circle cx="78" cy="63" r="3.2"/>
<circle cx="70" cy="76" r="3.2"/>
<circle cx="57" cy="83" r="3.2"/>
<circle cx="43" cy="83" r="3.2"/>
<circle cx="30" cy="76" r="3.2"/>
<circle cx="22" cy="63" r="3.2"/>
<circle cx="20" cy="47" r="3.2"/>
<circle cx="25" cy="32" r="3.2"/>
<circle cx="36" cy="22" r="3.2"/>
<!-- Ring 3 - mid area (avoiding center for suits) -->
<circle cx="50" cy="27" r="2.8"/>
<circle cx="62" cy="32" r="2.8"/>
<circle cx="70" cy="42" r="2.8"/>
<circle cx="72" cy="58" r="2.8"/>
<circle cx="66" cy="70" r="2.8"/>
<circle cx="34" cy="70" r="2.8"/>
<circle cx="28" cy="58" r="2.8"/>
<circle cx="30" cy="42" r="2.8"/>
<circle cx="38" cy="32" r="2.8"/>
<!-- Scattered small dimples in gaps -->
<circle cx="57" cy="15" r="2.5"/>
<circle cx="43" cy="15" r="2.5"/>
<circle cx="84" cy="50" r="2.5"/>
<circle cx="16" cy="50" r="2.5"/>
<circle cx="76" cy="68" r="2.5"/>
<circle cx="24" cy="68" r="2.5"/>
<circle cx="76" cy="32" r="2.5"/>
<circle cx="24" cy="32" r="2.5"/>
<circle cx="40" cy="80" r="2.5"/>
<circle cx="60" cy="80" r="2.5"/>
</g>
<!-- Subtle inner shadow for depth -->
<circle cx="50" cy="50" r="45" fill="none" stroke="#a0a0a0" stroke-width="1" opacity="0.3"/>
<!-- Outer edge highlight -->
<circle cx="50" cy="50" r="46" fill="none" stroke="#ffffff" stroke-width="0.5" opacity="0.5"/>
<!-- Card suits - 2x2 grid in center -->
<!-- Club (black) - top left -->
<text x="38" y="50" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#1a1a1a" text-anchor="middle">&#9827;</text>
<!-- Diamond (red) - top right -->
<text x="62" y="50" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#cc0000" text-anchor="middle">&#9830;</text>
<!-- Spade (black) - bottom left -->
<text x="38" y="68" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#1a1a1a" text-anchor="middle">&#9824;</text>
<!-- Heart (red) - bottom right -->
<text x="62" y="68" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#cc0000" text-anchor="middle">&#9829;</text>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -16,7 +16,7 @@
<!-- Lobby Screen --> <!-- Lobby Screen -->
<div id="lobby-screen" class="screen active"> <div id="lobby-screen" class="screen active">
<h1><span class="golfer-swing">🏌️</span><span class="kicked-ball"></span> <span class="golf-title">Golf</span></h1> <h1><img src="golfball-logo.svg" alt="" class="golfball-logo"><span class="golfer-swing">🏌️</span><span class="kicked-ball"></span> <span class="golf-title">Golf</span></h1>
<p class="subtitle">6-Card Golf Card Game <button id="rules-btn" class="btn btn-small btn-rules">Rules</button> <button id="leaderboard-btn" class="btn btn-small leaderboard-btn">Leaderboard</button></p> <p class="subtitle">6-Card Golf Card Game <button id="rules-btn" class="btn btn-small btn-rules">Rules</button> <button id="leaderboard-btn" class="btn btn-small leaderboard-btn">Leaderboard</button></p>
<!-- Auth buttons for guests --> <!-- Auth buttons for guests -->

View File

@ -77,6 +77,15 @@ body {
filter: drop-shadow(1px 1px 1px rgba(0,0,0,0.15)); filter: drop-shadow(1px 1px 1px rgba(0,0,0,0.15));
} }
/* Golf ball logo with card suits */
.golfball-logo {
width: 1.1em;
height: 1.1em;
vertical-align: middle;
margin-right: 0.1em;
filter: drop-shadow(1px 2px 2px rgba(0,0,0,0.25));
}
/* Golfer swing animation */ /* Golfer swing animation */
.golfer-swing { .golfer-swing {
display: inline-block; display: inline-block;

View File

@ -3,7 +3,6 @@
import asyncio import asyncio
import logging import logging
import os import os
import signal
import uuid import uuid
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Optional from typing import Optional
@ -60,22 +59,13 @@ async def _periodic_leaderboard_refresh():
logger.error(f"Leaderboard refresh failed: {e}") logger.error(f"Leaderboard refresh failed: {e}")
async def _initiate_shutdown():
"""Initiate graceful shutdown."""
logger.info("Received shutdown signal")
_shutdown_event.set()
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Application lifespan handler for async service initialization.""" """Application lifespan handler for async service initialization."""
global _user_store, _auth_service, _admin_service, _stats_service, _replay_service global _user_store, _auth_service, _admin_service, _stats_service, _replay_service
global _spectator_manager, _leaderboard_refresh_task, _redis_client, _rate_limiter global _spectator_manager, _leaderboard_refresh_task, _redis_client, _rate_limiter
# Register signal handlers for graceful shutdown # Note: Uvicorn handles SIGINT/SIGTERM and triggers lifespan cleanup automatically
loop = asyncio.get_running_loop()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, lambda: asyncio.create_task(_initiate_shutdown()))
# Initialize Redis client (for rate limiting, health checks, etc.) # Initialize Redis client (for rate limiting, health checks, etc.)
if config.REDIS_URL: if config.REDIS_URL: