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

119
notifications/admin.py Normal file
View File

@@ -0,0 +1,119 @@
from django.contrib import admin
from django.utils.html import format_html
from simple_history.admin import SimpleHistoryAdmin
from .models import EmailConfiguration, EmailLog
@admin.register(EmailConfiguration)
class EmailConfigurationAdmin(SimpleHistoryAdmin):
list_display = [
'name',
'is_enabled_badge',
'recipient_count',
'notify_on_scrape_complete',
'notify_on_scrape_failure',
'notify_on_sync_failure',
]
list_filter = ['is_enabled']
readonly_fields = ['created_at', 'updated_at']
fieldsets = [
(None, {
'fields': ['name', 'is_enabled']
}),
('Recipients', {
'fields': ['recipient_emails']
}),
('Scraper Notifications', {
'fields': [
'notify_on_scrape_complete',
'notify_on_scrape_failure',
'notify_on_new_reviews',
]
}),
('CloudKit Sync Notifications', {
'fields': [
'notify_on_sync_complete',
'notify_on_sync_failure',
]
}),
('Thresholds', {
'fields': ['min_games_for_notification']
}),
('Metadata', {
'fields': ['created_at', 'updated_at'],
'classes': ['collapse']
}),
]
actions = ['send_test_email']
def is_enabled_badge(self, obj):
if obj.is_enabled:
return format_html('<span style="color: green;">● Enabled</span>')
return format_html('<span style="color: gray;">○ Disabled</span>')
is_enabled_badge.short_description = 'Status'
def recipient_count(self, obj):
return len(obj.get_recipients())
recipient_count.short_description = 'Recipients'
@admin.action(description='Send test email')
def send_test_email(self, request, queryset):
from notifications.tasks import send_test_notification
for config in queryset:
send_test_notification.delay(config.id)
self.message_user(request, f'Test emails queued for {queryset.count()} configurations.')
@admin.register(EmailLog)
class EmailLogAdmin(admin.ModelAdmin):
list_display = [
'subject',
'status_badge',
'recipients_display',
'created_at',
]
list_filter = ['status', 'created_at']
search_fields = ['subject', 'recipients']
date_hierarchy = 'created_at'
ordering = ['-created_at']
readonly_fields = [
'configuration',
'subject',
'recipients',
'body_preview',
'status',
'error_message',
'scrape_job',
'sync_job',
'created_at',
]
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def status_badge(self, obj):
colors = {
'sent': '#5cb85c',
'failed': '#d9534f',
}
color = colors.get(obj.status, '#999')
return format_html(
'<span style="background-color: {}; color: white; padding: 3px 8px; '
'border-radius: 3px; font-size: 11px;">{}</span>',
color,
obj.status.upper()
)
status_badge.short_description = 'Status'
def recipients_display(self, obj):
recipients = obj.recipients.split(',')
if len(recipients) > 2:
return f"{recipients[0]}, +{len(recipients)-1} more"
return obj.recipients
recipients_display.short_description = 'Recipients'