Files
google-labs-jules[bot] 28d5ef301b feat(core): implement Admin Analytics dashboard (#120)
- Create `templates/admin/index.html` extending Unfold base layout to display custom dashboard.
- Add Chart.js visualization for "Bandwidth Saved" metric.
- Display key stats: CPU, RAM, Bandwidth Saved, Revenue, Anime/Episode counts.
- Utilize existing `core.dashboard.dashboard_callback` logic.
- Ensure safe data passing using `json_script`.
- Verified with unit tests in `core/tests/test_dashboard.py`.

Current Focus Mode: C (The Architect)
Changes Made:
- Added `templates/admin/index.html` with Chart.js integration.
- Fixed template to use `json_script` for safe JSON handling.
- Verified backend logic in `core/dashboard.py`.
Impact on System:
- Admins now see a visual dashboard with real-time stats and bandwidth savings graph upon login.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-01-22 13:22:22 +00:00

125 lines
6.7 KiB
HTML

{% extends 'unfold/layouts/base.html' %}
{% load i18n %}
{% block content %}
<div class="p-4">
<h1 class="text-3xl font-bold mb-6 text-gray-800 dark:text-gray-200">{% trans "Dashboard" %}</h1>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- CPU Usage -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border-l-4 border-blue-500">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 dark:bg-blue-900 text-blue-500">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path></svg>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500 dark:text-gray-400">CPU Usage</p>
<p class="text-2xl font-bold text-gray-700 dark:text-white">{{ dashboard_stats.cpu }}%</p>
</div>
</div>
</div>
<!-- RAM Usage -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border-l-4 border-purple-500">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 dark:bg-purple-900 text-purple-500">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500 dark:text-gray-400">RAM Usage</p>
<p class="text-2xl font-bold text-gray-700 dark:text-white">{{ dashboard_stats.ram }}%</p>
</div>
</div>
</div>
<!-- Bandwidth Saved -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border-l-4 border-green-500">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 dark:bg-green-900 text-green-500">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path></svg>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Bandwidth Saved</p>
<p class="text-2xl font-bold text-gray-700 dark:text-white">{{ dashboard_stats.bandwidth_saved_gb }} GB</p>
</div>
</div>
</div>
<!-- Total Revenue -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border-l-4 border-yellow-500">
<div class="flex items-center">
<div class="p-3 rounded-full bg-yellow-100 dark:bg-yellow-900 text-yellow-500">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Total Revenue</p>
<p class="text-2xl font-bold text-gray-700 dark:text-white">${{ dashboard_stats.total_revenue }}</p>
</div>
</div>
</div>
</div>
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Bandwidth Chart -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-4">{% trans "Bandwidth Savings (Last 7 Days)" %}</h3>
<canvas id="bandwidthChart"></canvas>
</div>
<!-- Content Stats -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-4">{% trans "Content Overview" %}</h3>
<div class="space-y-4">
<div class="flex justify-between items-center border-b border-gray-200 dark:border-gray-700 pb-2">
<span class="text-gray-600 dark:text-gray-400">Total Anime</span>
<span class="font-bold text-gray-800 dark:text-white">{{ dashboard_stats.anime_count }}</span>
</div>
<div class="flex justify-between items-center border-b border-gray-200 dark:border-gray-700 pb-2">
<span class="text-gray-600 dark:text-gray-400">Total Episodes</span>
<span class="font-bold text-gray-800 dark:text-white">{{ dashboard_stats.episode_count }}</span>
</div>
</div>
</div>
</div>
</div>
{{ dashboard_stats.bandwidth_chart|json_script:"bandwidth-chart-data" }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('bandwidthChart').getContext('2d');
// dashboard_stats.bandwidth_chart is a JSON string generated by python's json.dumps.
// When passed to json_script, it's treated as a string value, so the script tag contains "{\"labels\": ...}".
// First parse gets the string.
const rawData = JSON.parse(document.getElementById('bandwidth-chart-data').textContent);
// Second parse gets the object (if it's a string).
const chartData = (typeof rawData === 'string') ? JSON.parse(rawData) : rawData;
new Chart(ctx, {
type: 'line',
data: chartData,
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'GB Saved'
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
});
</script>
{% endblock %}