Files
lego-instructions-manager/app/templates/admin/dashboard.html
T
jessikitty 89f65e2d74 Add automatic instruction fetcher for official sets
Downloads official instruction PDFs from LEGO, located via the Brickset API, one set per interval (10 minutes by default) so neither API gets hammered. Official sets only; MOCs are excluded throughout.

- New admin page at /admin/instruction-fetch: queue builder, pacing controls, live countdown and per-set job history

- Database-backed queue (instruction_fetch_jobs) and last-run timestamp, so pacing survives restarts

- Background worker thread, plus 'flask instructions queue/run/status' for Task Scheduler or cron

- SHA-256 dedupe (instructions.sha256), PDF header check, size cap and streaming downloads via temp files

- brickset_api: getInstructions2 by set number and getInstructions by setID (the old code sent a set number to the setID method); getSets params now proper JSON; getKeyUsageStats added

- Schema top-ups applied automatically at boot: new table plus instructions.sha256/source

- Docker: fetcher env vars passed through compose, no worker thread in the entrypoint bootstrap
2026-09-26 13:43:17 +10:00

290 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}Admin Dashboard - {{ app_name }}{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col">
<h1>
<i class="bi bi-shield-lock"></i> Admin Dashboard
</h1>
<p class="text-muted">System overview and management</p>
</div>
</div>
<!-- Quick Stats -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card bg-primary text-white h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-subtitle mb-2 text-white-50">Total Users</h6>
<h2 class="mb-0">{{ total_users }}</h2>
</div>
<i class="bi bi-people display-4 opacity-50"></i>
</div>
</div>
<div class="card-footer bg-primary bg-opacity-75">
<a href="{{ url_for('admin.users') }}" class="text-white text-decoration-none">
Manage Users <i class="bi bi-arrow-right"></i>
</a>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-success text-white h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-subtitle mb-2 text-white-50">Total Sets</h6>
<h2 class="mb-0">{{ total_sets }}</h2>
</div>
<i class="bi bi-box-seam display-4 opacity-50"></i>
</div>
</div>
<div class="card-footer bg-success bg-opacity-75">
<a href="{{ url_for('admin.sets') }}" class="text-white text-decoration-none">
View All Sets <i class="bi bi-arrow-right"></i>
</a>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-warning text-dark h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-subtitle mb-2">MOC Builds</h6>
<h2 class="mb-0">{{ total_mocs }}</h2>
</div>
<i class="bi bi-star-fill display-4 opacity-50"></i>
</div>
</div>
<div class="card-footer bg-warning bg-opacity-75">
<a href="{{ url_for('admin.sets', type='mocs') }}" class="text-dark text-decoration-none">
View MOCs <i class="bi bi-arrow-right"></i>
</a>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-info text-white h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-subtitle mb-2 text-white-50">Instructions</h6>
<h2 class="mb-0">{{ total_instructions }}</h2>
<small class="text-white-50">{{ total_storage_mb }} MB</small>
</div>
<i class="bi bi-file-pdf display-4 opacity-50"></i>
</div>
</div>
<div class="card-footer bg-info bg-opacity-75">
<span class="text-white">Total Storage Used</span>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Recent Users -->
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-person-plus"></i> Recent Users</h5>
<a href="{{ url_for('admin.users') }}" class="btn btn-sm btn-outline-primary">View All</a>
</div>
<div class="card-body p-0">
{% if recent_users %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Username</th>
<th>Email</th>
<th>Joined</th>
<th>Admin</th>
</tr>
</thead>
<tbody>
{% for user in recent_users %}
<tr>
<td>
<i class="bi bi-person-circle"></i> {{ user.username }}
</td>
<td>{{ user.email }}</td>
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
<td>
{% if user.is_admin %}
<span class="badge bg-danger">Admin</span>
{% else %}
<span class="badge bg-secondary">User</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-center text-muted my-4">No users yet</p>
{% endif %}
</div>
</div>
</div>
<!-- Top Contributors -->
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-trophy"></i> Top Contributors</h5>
</div>
<div class="card-body p-0">
{% if top_contributors %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>User</th>
<th>Sets Added</th>
</tr>
</thead>
<tbody>
{% for user, count in top_contributors %}
<tr>
<td>
<i class="bi bi-person-circle"></i> {{ user.username }}
</td>
<td>
<span class="badge bg-success">{{ count }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-center text-muted my-4">No data yet</p>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<!-- Popular Themes -->
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-bar-chart"></i> Popular Themes</h5>
</div>
<div class="card-body">
{% if theme_stats %}
{% for theme, count in theme_stats %}
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<span>{{ theme }}</span>
<span class="badge bg-primary">{{ count }}</span>
</div>
<div class="progress" style="height: 20px;">
{% set percentage = (count / total_sets * 100) | int %}
<div class="progress-bar" role="progressbar"
style="width: {{ percentage }}%"
aria-valuenow="{{ percentage }}"
aria-valuemin="0"
aria-valuemax="100">
{{ percentage }}%
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center text-muted">No theme data yet</p>
{% endif %}
</div>
</div>
</div>
<!-- Recent Sets -->
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Recently Added Sets</h5>
<a href="{{ url_for('admin.sets') }}" class="btn btn-sm btn-outline-primary">View All</a>
</div>
<div class="card-body p-0">
{% if recent_sets %}
<div class="list-group list-group-flush">
{% for set in recent_sets[:5] %}
<a href="{{ url_for('sets.view_set', set_id=set.id) }}"
class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>{{ set.set_number }}</strong>
{% if set.is_moc %}
<span class="badge bg-warning text-dark ms-1">
<i class="bi bi-star-fill"></i>
</span>
{% endif %}
<br>
<small class="text-muted">{{ set.set_name }}</small>
</div>
<span class="badge bg-secondary">{{ set.theme }}</span>
</div>
</a>
{% endfor %}
</div>
{% else %}
<p class="text-center text-muted my-4">No sets yet</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-gear"></i> Quick Actions</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-2">
<a href="{{ url_for('instruction_fetch.index') }}" class="btn btn-outline-warning w-100">
<i class="bi bi-cloud-arrow-down"></i> Instruction Fetcher
</a>
</div>
<div class="col-md-3 mb-2">
<a href="{{ url_for('admin.users') }}" class="btn btn-outline-primary w-100">
<i class="bi bi-people"></i> Manage Users
</a>
</div>
<div class="col-md-3 mb-2">
<a href="{{ url_for('admin.sets') }}" class="btn btn-outline-success w-100">
<i class="bi bi-box-seam"></i> Manage Sets
</a>
</div>
<div class="col-md-3 mb-2">
<a href="{{ url_for('admin.bulk_import') }}" class="btn btn-outline-info w-100">
<i class="bi bi-cloud-upload"></i> Bulk Import
</a>
</div>
<div class="col-md-3 mb-2">
<a href="{{ url_for('admin.site_settings') }}" class="btn btn-outline-secondary w-100">
<i class="bi bi-sliders"></i> Site Settings
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}