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:
188
templates/dashboard/index.html
Normal file
188
templates/dashboard/index.html
Normal file
@@ -0,0 +1,188 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h2>Dashboard</h2>
|
||||
<div>
|
||||
<a href="{% url 'admin:index' %}" class="btn btn-secondary">Admin Panel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overview Stats -->
|
||||
<div class="stat-grid">
|
||||
<div class="stat-card primary">
|
||||
<div class="stat-value">{{ sports_count }}</div>
|
||||
<div class="stat-label">Active Sports</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ teams_count }}</div>
|
||||
<div class="stat-label">Teams</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stadiums_count }}</div>
|
||||
<div class="stat-label">Stadiums</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-value">{{ games_count|floatformat:0 }}</div>
|
||||
<div class="stat-label">Games</div>
|
||||
</div>
|
||||
<div class="stat-card {% if pending_reviews > 0 %}warning{% endif %}">
|
||||
<div class="stat-value">{{ pending_reviews }}</div>
|
||||
<div class="stat-label">Pending Reviews</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2 mt-2">
|
||||
<!-- Sport Stats -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Stats by Sport</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sport</th>
|
||||
<th>Teams</th>
|
||||
<th>Stadiums</th>
|
||||
<th>Games</th>
|
||||
<th>Reviews</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stat in sport_stats %}
|
||||
<tr>
|
||||
<td><strong>{{ stat.sport.short_name }}</strong></td>
|
||||
<td>{{ stat.teams }}</td>
|
||||
<td>{{ stat.stadiums }}</td>
|
||||
<td>{{ stat.games }}</td>
|
||||
<td>
|
||||
{% if stat.pending_reviews > 0 %}
|
||||
<span class="badge badge-warning">{{ stat.pending_reviews }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">0</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-muted">No sports configured</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Recent Scrape Jobs -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Recent Scrape Jobs</h3>
|
||||
<a href="{% url 'dashboard:scraper_status' %}" class="btn btn-primary">View All</a>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sport</th>
|
||||
<th>Status</th>
|
||||
<th>Games</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for job in recent_jobs %}
|
||||
<tr>
|
||||
<td>{{ job.config.sport.short_name }}</td>
|
||||
<td>
|
||||
{% if job.status == 'completed' %}
|
||||
<span class="badge badge-success">Completed</span>
|
||||
{% elif job.status == 'running' %}
|
||||
<span class="badge badge-info">Running</span>
|
||||
{% elif job.status == 'failed' %}
|
||||
<span class="badge badge-danger">Failed</span>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">{{ job.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ job.games_found }}</td>
|
||||
<td class="text-muted">{{ job.created_at|timesince }} ago</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-muted">No recent jobs</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<!-- Recent Syncs -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Recent CloudKit Syncs</h3>
|
||||
<a href="{% url 'dashboard:sync_status' %}" class="btn btn-primary">View All</a>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Records</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for sync in recent_syncs %}
|
||||
<tr>
|
||||
<td>{{ sync.sync_type }}</td>
|
||||
<td>
|
||||
{% if sync.status == 'completed' %}
|
||||
<span class="badge badge-success">Completed</span>
|
||||
{% elif sync.status == 'running' %}
|
||||
<span class="badge badge-info">Running</span>
|
||||
{% elif sync.status == 'failed' %}
|
||||
<span class="badge badge-danger">Failed</span>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">{{ sync.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ sync.records_synced }}</td>
|
||||
<td class="text-muted">{{ sync.created_at|timesince }} ago</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-muted">No recent syncs</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Quick Actions</h3>
|
||||
</div>
|
||||
<div style="display: grid; gap: 1rem;">
|
||||
<a href="{% url 'admin:core_game_changelist' %}" class="btn btn-primary" style="text-align: center;">
|
||||
Manage Games
|
||||
</a>
|
||||
<a href="{% url 'admin:core_team_changelist' %}" class="btn btn-primary" style="text-align: center;">
|
||||
Manage Teams
|
||||
</a>
|
||||
<a href="{% url 'admin:core_stadium_changelist' %}" class="btn btn-primary" style="text-align: center;">
|
||||
Manage Stadiums
|
||||
</a>
|
||||
<a href="{% url 'dashboard:review_queue' %}" class="btn {% if pending_reviews > 0 %}btn-danger{% else %}btn-secondary{% endif %}" style="text-align: center;">
|
||||
Review Queue {% if pending_reviews > 0 %}({{ pending_reviews }}){% endif %}
|
||||
</a>
|
||||
<form method="post" action="{% url 'dashboard:run_sync' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-success" style="width: 100%;">
|
||||
Trigger CloudKit Sync
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user