Initial clean commit
This commit is contained in:
commit
6784d81c2c
141 changed files with 25219 additions and 0 deletions
729
templates/stats.html
Normal file
729
templates/stats.html
Normal file
|
|
@ -0,0 +1,729 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Estadísticas de Actividad{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="stats-header">
|
||||
<h1><i class="fas fa-chart-line"></i> Estadísticas</h1>
|
||||
<p>Actividad de noticias y distribución por categorías</p>
|
||||
</div>
|
||||
|
||||
<!-- Live Stats Banner (Moved from Config) -->
|
||||
<div class="stats-banner">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ translations_per_min }}</div>
|
||||
<div class="stat-label">trad/min</div>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value {% if pending_count > 100 %}stat-warning{% endif %}">{{ pending_count }}</div>
|
||||
<div class="stat-label">pendientes</div>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value stat-processing">{{ processing_count }}</div>
|
||||
<div class="stat-label">procesando</div>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ traducciones_count }}</div>
|
||||
<div class="stat-label">completadas</div>
|
||||
</div>
|
||||
{% if error_count > 0 %}
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value stat-error">{{ error_count }}</div>
|
||||
<div class="stat-label">errores</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Big Stats Section (Moved from Config) -->
|
||||
<div class="big-stats">
|
||||
<div class="big-stat">
|
||||
<div class="big-number">{{ noticias_count|default(0) }}</div>
|
||||
<div class="big-label">Noticias</div>
|
||||
</div>
|
||||
<div class="big-stat">
|
||||
<div class="big-number">{{ traducciones_count|default(0) }}</div>
|
||||
<div class="big-label">Traducidas</div>
|
||||
</div>
|
||||
<div class="big-stat highlight">
|
||||
{% set percent = ((traducciones_count / noticias_count * 100) if noticias_count > 0 else 0)|round(1) %}
|
||||
<div class="big-number">{{ percent }}%</div>
|
||||
<div class="big-label">Completado</div>
|
||||
</div>
|
||||
<div class="big-stat">
|
||||
<div class="big-number">{{ noticias_hoy|default(0) }}</div>
|
||||
<div class="big-label">Noticias Hoy</div>
|
||||
</div>
|
||||
<div class="big-stat">
|
||||
<div class="big-number">{{ noticias_ultima_hora|default(0) }}</div>
|
||||
<div class="big-label">Última Hora</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="card stats-card full-width">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
|
||||
<h3 style="margin:0;">Actividad</h3>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="updateActivityChart('1h', this)">1h</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="updateActivityChart('8h', this)">8h</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="updateActivityChart('24h', this)">24h</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="updateActivityChart('7d', this)">7d</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm active"
|
||||
onclick="updateActivityChart('30d', this)">30d</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="activityChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card stats-card">
|
||||
<h3>Noticias por Categoría</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="categoryChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card stats-card">
|
||||
<h3>Noticias por País</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="countryChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Translations Section -->
|
||||
<div class="card stats-card full-width">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
|
||||
<h3 style="margin:0;">Ritmo de Traducción (TPM)</h3>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm active"
|
||||
onclick="updateRateChart('1h', this)">1h</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="updateRateChart('8h', this)">8h</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="updateRateChart('24h', this)">24h</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="updateRateChart('7d', this)">7d</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="rateChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card stats-card">
|
||||
<h3>Traducciones Diarias</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="translationActivityChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card stats-card">
|
||||
<h3>Idiomas de Origen</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="langChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card stats-card">
|
||||
<h3>Información del Sistema</h3>
|
||||
<ul class="system-stats" id="system-info-list">
|
||||
<li><strong>CPU:</strong> <span id="cpu-load">Cargando...</span></li>
|
||||
<li><strong>Núcleos:</strong> <span id="cpu-cores">-</span></li>
|
||||
<li><strong>GPU:</strong> <span id="gpu-name">-</span></li>
|
||||
<li><strong>GPU Temp:</strong> <span id="gpu-temp">-</span></li>
|
||||
<li><strong>GPU Util:</strong> <span id="gpu-util">-</span></li>
|
||||
<li><strong>GPU Mem:</strong> <span id="gpu-mem">-</span></li>
|
||||
<li><strong>Uptime:</strong> <span id="system-uptime">-</span></li>
|
||||
<li class="last-update">Actualizado: <span id="last-update-time">-</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Activity Chart
|
||||
let activityChart = null;
|
||||
|
||||
window.updateActivityChart = function (range, btn) {
|
||||
// Update active button state
|
||||
if (btn) {
|
||||
// Find buttons specifically for this chart
|
||||
const parent = btn.closest('.btn-group');
|
||||
if (parent) {
|
||||
parent.querySelectorAll('button').forEach(b => b.classList.remove('active'));
|
||||
}
|
||||
btn.classList.add('active');
|
||||
}
|
||||
|
||||
fetch(`/stats/api/activity?range=${range}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const ctx = document.getElementById('activityChart').getContext('2d');
|
||||
|
||||
if (activityChart) {
|
||||
activityChart.destroy();
|
||||
}
|
||||
|
||||
// Dynamic title
|
||||
let titleText = "Noticias Recientes";
|
||||
if (range === '1h') titleText = "Última Hora";
|
||||
if (range === '8h') titleText = "Últimas 8 Horas";
|
||||
if (range === '24h') titleText = "Últimas 24 Horas";
|
||||
if (range === '7d') titleText = "Últimos 7 Días";
|
||||
if (range === '30d') titleText = "Últimos 30 Días";
|
||||
|
||||
// Create gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
|
||||
gradient.addColorStop(0, 'rgba(52, 152, 219, 0.5)'); // Start color
|
||||
gradient.addColorStop(1, 'rgba(52, 152, 219, 0.0)'); // End color
|
||||
|
||||
activityChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: 'Noticias Ingestadas',
|
||||
data: data.data,
|
||||
borderColor: '#3498db',
|
||||
backgroundColor: gradient,
|
||||
borderWidth: 3,
|
||||
tension: 0.4, // Smooth curves
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 6,
|
||||
pointHitRadius: 10
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: {
|
||||
display: true,
|
||||
text: titleText,
|
||||
font: { size: 16, weight: 'normal' },
|
||||
padding: { bottom: 20 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
titleColor: '#2c3e50',
|
||||
bodyColor: '#2c3e50',
|
||||
borderColor: '#e0e0e0',
|
||||
borderWidth: 1,
|
||||
padding: 10,
|
||||
displayColors: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: false }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
border: { display: false },
|
||||
grid: {
|
||||
borderDash: [5, 5],
|
||||
color: '#f0f0f0'
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Miles de noticias'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initial load (30d)
|
||||
updateActivityChart('30d');
|
||||
|
||||
// Category Chart
|
||||
fetch('/stats/api/categories')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const ctx = document.getElementById('categoryChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
data: data.data,
|
||||
backgroundColor: [
|
||||
'#4facfe', '#00f2fe', '#43e97b', '#38f9d7', '#fa709a',
|
||||
'#fee140', '#667eea', '#764ba2', '#ff9a9e', '#a18cd1'
|
||||
],
|
||||
borderWidth: 0,
|
||||
hoverOffset: 10
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutout: '75%', // Thinner ring
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 20,
|
||||
font: { size: 12 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Country Chart
|
||||
fetch('/stats/api/countries')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const ctx = document.getElementById('countryChart').getContext('2d');
|
||||
|
||||
// Gradient for bars
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
|
||||
gradient.addColorStop(0, '#1abc9c');
|
||||
gradient.addColorStop(1, '#16a085');
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: 'Noticias',
|
||||
data: data.data,
|
||||
backgroundColor: gradient,
|
||||
borderRadius: 4, // Rounded bars
|
||||
borderSkipped: false,
|
||||
barPercentage: 0.6,
|
||||
categoryPercentage: 0.8
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
titleColor: '#2c3e50',
|
||||
bodyColor: '#2c3e50',
|
||||
borderColor: '#e0e0e0',
|
||||
borderWidth: 1,
|
||||
displayColors: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: false }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
border: { display: false },
|
||||
grid: {
|
||||
borderDash: [5, 5],
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- NEW TRANSLATION CHARTS ---
|
||||
|
||||
// Translation Rate (TPM)
|
||||
let rateChart = null;
|
||||
|
||||
window.updateRateChart = function (range, btn) {
|
||||
// Update active button state
|
||||
if (btn) {
|
||||
document.querySelectorAll('.btn-group button').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
}
|
||||
|
||||
fetch(`/stats/api/translations/rate?range=${range}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const ctx = document.getElementById('rateChart').getContext('2d');
|
||||
|
||||
if (rateChart) {
|
||||
rateChart.destroy();
|
||||
}
|
||||
|
||||
// Gradient for rate chart
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
|
||||
gradient.addColorStop(0, 'rgba(230, 126, 34, 0.5)');
|
||||
gradient.addColorStop(1, 'rgba(230, 126, 34, 0.0)');
|
||||
|
||||
rateChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: 'Traducciones',
|
||||
data: data.data,
|
||||
borderColor: '#e67e22',
|
||||
backgroundColor: gradient,
|
||||
borderWidth: 3,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 6,
|
||||
pointHitRadius: 10
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: {
|
||||
display: true,
|
||||
text: `Traducciones - Última(s) ${range}`,
|
||||
font: { size: 16, weight: 'normal' },
|
||||
padding: { bottom: 20 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
titleColor: '#2c3e50',
|
||||
bodyColor: '#2c3e50',
|
||||
borderColor: '#e0e0e0',
|
||||
borderWidth: 1,
|
||||
padding: 10,
|
||||
displayColors: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: { grid: { display: false } },
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
border: { display: false },
|
||||
grid: {
|
||||
borderDash: [5, 5],
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initial load (1h)
|
||||
updateRateChart('1h');
|
||||
|
||||
// Translation Activity
|
||||
fetch('/stats/api/translations/activity')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const ctx = document.getElementById('translationActivityChart').getContext('2d');
|
||||
|
||||
// Gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
|
||||
gradient.addColorStop(0, '#9b59b6');
|
||||
gradient.addColorStop(1, '#8e44ad');
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: 'Traducciones Diarias',
|
||||
data: data.data,
|
||||
backgroundColor: gradient,
|
||||
borderRadius: 4,
|
||||
barPercentage: 0.6,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
titleColor: '#2c3e50',
|
||||
bodyColor: '#2c3e50',
|
||||
borderColor: '#e0e0e0',
|
||||
borderWidth: 1,
|
||||
displayColors: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: { grid: { display: false } },
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
border: { display: false },
|
||||
grid: {
|
||||
borderDash: [5, 5],
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Translation Languages
|
||||
fetch('/stats/api/translations/languages')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const ctx = document.getElementById('langChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
data: data.data,
|
||||
backgroundColor: [
|
||||
'#34495e', '#16a085', '#27ae60', '#2980b9', '#8e44ad',
|
||||
'#2c3e50', '#f1c40f', '#e67e22', '#e74c3c', '#ecf0f1'
|
||||
],
|
||||
borderWidth: 0,
|
||||
hoverOffset: 10
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 20,
|
||||
font: { size: 12 }
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Idioma de Origen (Top 10)',
|
||||
padding: { bottom: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// System Info Polling
|
||||
function updateSystemInfo() {
|
||||
fetch('/stats/api/system/info')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
document.getElementById('cpu-load').textContent = data.cpu ? data.cpu.load : 'N/A';
|
||||
document.getElementById('cpu-cores').textContent = data.cpu ? data.cpu.cores : '-';
|
||||
document.getElementById('system-uptime').textContent = data.uptime;
|
||||
document.getElementById('last-update-time').textContent = data.timestamp;
|
||||
|
||||
if (data.gpu) {
|
||||
document.getElementById('gpu-name').textContent = data.gpu.name;
|
||||
document.getElementById('gpu-temp').textContent = data.gpu.temp;
|
||||
document.getElementById('gpu-util').textContent = data.gpu.util;
|
||||
document.getElementById('gpu-mem').textContent = data.gpu.mem;
|
||||
} else {
|
||||
document.getElementById('gpu-name').textContent = 'No detectada';
|
||||
}
|
||||
})
|
||||
.catch(err => console.error("Error polling system info", err));
|
||||
}
|
||||
|
||||
updateSystemInfo();
|
||||
setInterval(updateSystemInfo, 10000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.stats-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
padding: 20px;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.system-stats {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.system-stats li {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.system-stats li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Stats Banner Styles */
|
||||
.stats-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
padding: 1rem 1.5rem;
|
||||
background: #111;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-value.stat-warning {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.stat-value.stat-processing {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.stat-value.stat-error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.7rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.last-update {
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
margin-top: auto;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed var(--border-color);
|
||||
}
|
||||
|
||||
.dark-mode .stats-banner {
|
||||
background: #000;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.stats-banner {
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 0 0 45%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Big Stats */
|
||||
.big-stats {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin: 1rem 0 2rem 0;
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid var(--border-color, #eee);
|
||||
border-bottom: 1px solid var(--border-color, #eee);
|
||||
}
|
||||
|
||||
.big-stat {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.big-stat .big-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
color: #111;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.big-stat .big-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted, #666);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.big-stat.highlight .big-number {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.dark-mode .big-stat .big-number {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.big-stats {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue