277 lines
12 KiB
HTML
277 lines
12 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% block title %}Admin Panel{% endblock %}
|
|
{% block page_title %}Admin Panel{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
<!-- Stats Bar -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-icon bg-primary">
|
|
<i class="fas fa-users"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>{{ stats.total_users }}</h3>
|
|
<p>Total Users</p>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon bg-success">
|
|
<i class="fas fa-user"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>{{ stats.total_players }}</h3>
|
|
<p>Players</p>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon bg-warning">
|
|
<i class="fas fa-shield-alt"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>{{ stats.total_org_teams }}</h3>
|
|
<p>Org Teams</p>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon bg-info">
|
|
<i class="fas fa-calendar-alt"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>{{ stats.active_tryouts }}</h3>
|
|
<p>Active Tryouts</p>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon bg-secondary">
|
|
<i class="fas fa-clock"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>{{ stats.upcoming_tryouts }}</h3>
|
|
<p>Upcoming</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-grid">
|
|
<!-- Left column -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-toggle-{% if settings.tryouts_open %}on{% else %}off{% endif %}"></i> Tryouts Access</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<p>
|
|
Status:
|
|
<span class="badge badge-{% if settings.tryouts_open %}success{% else %}danger{% endif %}">
|
|
{% if settings.tryouts_open %}OPEN{% else %}CLOSED{% endif %}
|
|
</span>
|
|
</p>
|
|
<p class="text-muted" style="font-size: 0.85rem; margin: 8px 0;">
|
|
{% if settings.tryouts_open %}
|
|
Coaches and managers can create and modify tryouts.
|
|
{% else %}
|
|
Only admins can create or modify tryouts. Coaches and managers are locked out.
|
|
{% endif %}
|
|
</p>
|
|
<form method="POST" action="{{ url_for('admin.toggle_tryouts') }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<button type="submit" class="btn btn-{% if settings.tryouts_open %}warning{% else %}success{% endif %}">
|
|
{% if settings.tryouts_open %}Close Tryouts{% else %}Open Tryouts{% endif %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-calendar-check"></i> Season Management</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<p>
|
|
Season:
|
|
<span class="badge badge-{% if settings.season_active %}success{% else %}info{% endif %}">
|
|
{% if settings.season_active %}ACTIVE{% else %}INACTIVE{% endif %}
|
|
</span>
|
|
</p>
|
|
<p><strong>Name:</strong> {{ settings.season_name }}</p>
|
|
<p><strong>Start:</strong> {{ settings.season_start }}</p>
|
|
<p><strong>End:</strong> {{ settings.season_end }}</p>
|
|
|
|
{% if not settings.season_active %}
|
|
<hr style="margin: 12px 0;">
|
|
<form method="POST" action="{{ url_for('admin.start_season') }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<div class="form-group">
|
|
<label>Season Name</label>
|
|
<input type="text" name="season_name" class="form-input" placeholder="e.g. Fall 2026" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Start Date</label>
|
|
<input type="date" name="season_start" class="form-input" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-success">Begin Season</button>
|
|
</form>
|
|
{% else %}
|
|
<hr style="margin: 12px 0;">
|
|
<form method="POST" action="{{ url_for('admin.end_season') }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<div class="form-group">
|
|
<label>End Date</label>
|
|
<input type="date" name="season_end" class="form-input" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-warning">End Season</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Wipe Teams -->
|
|
<div class="card" style="border-left: 4px solid var(--danger);">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-trash-alt" style="color: var(--danger);"></i> Wipe Teams for New Season</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="text-muted" style="font-size: 0.85rem; margin-bottom: 12px;">
|
|
This removes all players from organization teams and deletes all regular-season matches.
|
|
Team structures, coaches, and managers are preserved. A safety backup is created automatically.
|
|
</p>
|
|
<form method="POST" action="{{ url_for('admin.wipe_teams') }}" onsubmit="return confirmWipe()">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<div class="form-group">
|
|
<label>Type <strong>WIPE</strong> to confirm:</label>
|
|
<input type="text" name="confirm" class="form-input" placeholder="WIPE" autocomplete="off" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-danger">Wipe Team Rosters</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manual Backup -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-database"></i> Manual Backup</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="text-muted" style="font-size: 0.85rem; margin-bottom: 12px;">
|
|
Creates a full PostgreSQL database dump (.sql file). Stored on the server.
|
|
</p>
|
|
<form method="POST" action="{{ url_for('admin.create_backup') }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<div class="form-group">
|
|
<label>Notes (optional)</label>
|
|
<input type="text" name="notes" class="form-input" placeholder="What's this backup for?">
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-save"></i> Create Backup Now
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup History -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-history"></i> Backup History & Restore</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if backups %}
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Filename</th>
|
|
<th>Size</th>
|
|
<th>Type</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for b in backups %}
|
|
<tr>
|
|
<td>{{ b.created_at.strftime('%Y-%m-%d %H:%M') if b.created_at else '—' }}</td>
|
|
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="{{ b.filename }}">{{ b.filename }}</td>
|
|
<td>{{ (b.size_bytes / 1024)|round(1) }} KB</td>
|
|
<td><span class="badge badge-info">{{ b.backup_type }}</span></td>
|
|
<td style="white-space: nowrap;">
|
|
<a href="{{ url_for('admin.download_backup', backup_id=b.id) }}" class="btn btn-sm btn-primary" title="Download">
|
|
<i class="fas fa-download"></i>
|
|
</a>
|
|
<form method="POST" action="{{ url_for('admin.restore_backup', backup_id=b.id) }}" class="inline-form"
|
|
onsubmit="return confirm('Restore backup {{ b.filename }}? This will overwrite all current data. A safety backup will be made first.');">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<button type="submit" class="btn btn-sm btn-warning" title="Restore">
|
|
<i class="fas fa-undo"></i>
|
|
</button>
|
|
</form>
|
|
<form method="POST" action="{{ url_for('admin.delete_backup', backup_id=b.id) }}" class="inline-form"
|
|
onsubmit="return confirm('Delete backup {{ b.filename }}?');">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<button type="submit" class="btn btn-sm btn-danger" title="Delete">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted" style="font-size: 0.85rem;">No backups yet. Create your first backup above.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit Log -->
|
|
<div class="card mt-4">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-clipboard-list"></i> Audit Log (Recent)</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if audit_logs %}
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>User</th>
|
|
<th>Action</th>
|
|
<th>Details</th>
|
|
<th>IP</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for entry in audit_logs %}
|
|
<tr>
|
|
<td>{{ entry.created_at.strftime('%Y-%m-%d %H:%M') if entry.created_at else '—' }}</td>
|
|
<td>{{ entry.user.username if entry.user else 'System' }}</td>
|
|
<td><span class="badge badge-info">{{ entry.action }}</span></td>
|
|
<td style="max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ entry.details or '—' }}</td>
|
|
<td>{{ entry.ip_address or '—' }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted" style="font-size: 0.85rem;">No audit log entries yet.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function confirmWipe() {
|
|
var input = document.querySelector('input[name="confirm"]');
|
|
if (input.value.trim() !== 'WIPE') {
|
|
alert('You must type WIPE to confirm.');
|
|
return false;
|
|
}
|
|
return confirm('This will remove ALL players from organization teams and delete ALL regular-season matches. A safety backup will be created automatically. Are you ABSOLUTELY sure?');
|
|
}
|
|
</script>
|
|
{% endblock %} |