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>
189 lines
6.8 KiB
HTML
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 %}
|