Files
SportstimeAPI/templates/dashboard/index.html
Trey t 63acf7accb 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>
2026-02-19 14:04:27 -06:00

189 lines
6.8 KiB
HTML

{% 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 %}