feat: add Django web app, CloudKit sync, dashboard, and game_datetime_utc export

Adds the full Django application layer on top of sportstime_parser:
- core: Sport, Team, Stadium, Game models with aliases and league structure
- scraper: orchestration engine, adapter, job management, Celery tasks
- cloudkit: CloudKit sync client, sync state tracking, sync jobs
- dashboard: staff dashboard for monitoring scrapers, sync, review queue
- notifications: email reports for scrape/sync results
- Docker setup for deployment (Dockerfile, docker-compose, entrypoint)

Game exports now use game_datetime_utc (ISO 8601 UTC) instead of
venue-local date+time strings, matching the canonical format used
by the iOS app.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-19 14:04:27 -06:00
parent 4353d5943c
commit 63acf7accb
114 changed files with 13070 additions and 887 deletions

109
core/models/stadium.py Normal file
View File

@@ -0,0 +1,109 @@
from django.db import models
from simple_history.models import HistoricalRecords
class Stadium(models.Model):
"""
Stadium/Arena/Venue model.
"""
SURFACE_CHOICES = [
('grass', 'Natural Grass'),
('turf', 'Artificial Turf'),
('ice', 'Ice'),
('hardwood', 'Hardwood'),
('other', 'Other'),
]
ROOF_TYPE_CHOICES = [
('dome', 'Dome (Closed)'),
('retractable', 'Retractable'),
('open', 'Open Air'),
]
id = models.CharField(
max_length=100,
primary_key=True,
help_text='Canonical ID (e.g., stadium_nba_los_angeles_lakers)'
)
sport = models.ForeignKey(
'core.Sport',
on_delete=models.CASCADE,
related_name='stadiums'
)
name = models.CharField(
max_length=200,
help_text='Current stadium name'
)
city = models.CharField(max_length=100)
state = models.CharField(
max_length=100,
blank=True,
help_text='State/Province (blank for international)'
)
country = models.CharField(
max_length=100,
default='USA'
)
latitude = models.DecimalField(
max_digits=9,
decimal_places=6,
null=True,
blank=True
)
longitude = models.DecimalField(
max_digits=9,
decimal_places=6,
null=True,
blank=True
)
capacity = models.PositiveIntegerField(
null=True,
blank=True,
help_text='Seating capacity'
)
surface = models.CharField(
max_length=20,
choices=SURFACE_CHOICES,
blank=True
)
roof_type = models.CharField(
max_length=20,
choices=ROOF_TYPE_CHOICES,
blank=True
)
opened_year = models.PositiveSmallIntegerField(
null=True,
blank=True,
help_text='Year stadium opened'
)
timezone = models.CharField(
max_length=50,
blank=True,
help_text='IANA timezone (e.g., America/Los_Angeles)'
)
image_url = models.URLField(
blank=True,
help_text='URL to stadium image'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Audit trail
history = HistoricalRecords()
class Meta:
ordering = ['sport', 'city', 'name']
verbose_name = 'Stadium'
verbose_name_plural = 'Stadiums'
def __str__(self):
return f"{self.name} ({self.city})"
@property
def location(self):
"""Return city, state/country string."""
if self.state:
return f"{self.city}, {self.state}"
return f"{self.city}, {self.country}"