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

View File

@@ -0,0 +1,92 @@
from django.db import models
from simple_history.models import HistoricalRecords
class Conference(models.Model):
"""
Conference within a sport (e.g., Eastern, Western for NBA).
"""
sport = models.ForeignKey(
'core.Sport',
on_delete=models.CASCADE,
related_name='conferences'
)
canonical_id = models.CharField(
max_length=100,
blank=True,
db_index=True,
help_text='Canonical ID from bootstrap JSON (e.g., nba_eastern)'
)
name = models.CharField(max_length=50)
short_name = models.CharField(
max_length=10,
blank=True,
help_text='Short name (e.g., East, West)'
)
order = models.PositiveSmallIntegerField(
default=0,
help_text='Display order'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Audit trail
history = HistoricalRecords()
class Meta:
ordering = ['sport', 'order', 'name']
unique_together = ['sport', 'name']
verbose_name = 'Conference'
verbose_name_plural = 'Conferences'
def __str__(self):
return f"{self.sport.short_name} - {self.name}"
class Division(models.Model):
"""
Division within a conference (e.g., Atlantic, Central for NBA East).
"""
conference = models.ForeignKey(
Conference,
on_delete=models.CASCADE,
related_name='divisions'
)
canonical_id = models.CharField(
max_length=100,
blank=True,
db_index=True,
help_text='Canonical ID from bootstrap JSON (e.g., nba_southeast)'
)
name = models.CharField(max_length=50)
short_name = models.CharField(
max_length=10,
blank=True,
help_text='Short name'
)
order = models.PositiveSmallIntegerField(
default=0,
help_text='Display order'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Audit trail
history = HistoricalRecords()
class Meta:
ordering = ['conference', 'order', 'name']
unique_together = ['conference', 'name']
verbose_name = 'Division'
verbose_name_plural = 'Divisions'
def __str__(self):
return f"{self.conference.sport.short_name} - {self.conference.name} - {self.name}"
@property
def sport(self):
return self.conference.sport