Files
social-media-automation/dashboard/templates/analytics.html
Consultoría AS e32885afc5 fix: Fix YAML syntax errors and validator prompt formatting
- Fix YAML files with unquoted strings containing quotes
- Export singleton instances in ai/__init__.py
- Fix validator scoring prompt to use replace() instead of format()
  to avoid conflicts with JSON curly braces

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 21:13:58 +00:00

257 lines
11 KiB
HTML

{% extends "base.html" %}
{% block title %}Analytics{% endblock %}
{% block extra_head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock %}
{% block content %}
<div class="animate-fade-in">
<!-- Header -->
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-3xl font-bold">Analytics</h1>
<p class="text-gray-400 mt-1">Métricas y rendimiento de tus publicaciones</p>
</div>
<div class="flex gap-3">
<select id="periodSelect" onchange="loadDashboard()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
<option value="7">Últimos 7 días</option>
<option value="30" selected>Últimos 30 días</option>
<option value="90">Últimos 90 días</option>
</select>
<select id="platformSelect" onchange="loadDashboard()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
<option value="">Todas las plataformas</option>
<option value="x">X (Twitter)</option>
<option value="facebook">Facebook</option>
<option value="instagram">Instagram</option>
<option value="threads">Threads</option>
</select>
</div>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="stat-card card rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
<span class="text-2xl">📝</span>
</div>
</div>
<p class="text-3xl font-bold" id="stat-posts">-</p>
<p class="text-gray-400 text-sm mt-1">Posts Totales</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-green-500/20 flex items-center justify-center">
<span class="text-2xl">👁️</span>
</div>
</div>
<p class="text-3xl font-bold" id="stat-impressions">-</p>
<p class="text-gray-400 text-sm mt-1">Impresiones</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-purple-500/20 flex items-center justify-center">
<span class="text-2xl">❤️</span>
</div>
</div>
<p class="text-3xl font-bold" id="stat-engagements">-</p>
<p class="text-gray-400 text-sm mt-1">Interacciones</p>
</div>
<div class="stat-card card rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 rounded-xl bg-yellow-500/20 flex items-center justify-center">
<span class="text-2xl">📊</span>
</div>
</div>
<p class="text-3xl font-bold" id="stat-rate">-</p>
<p class="text-gray-400 text-sm mt-1">Engagement Rate</p>
</div>
</div>
<!-- Charts -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="card rounded-2xl p-6">
<h3 class="font-semibold mb-4">Engagement por Plataforma</h3>
<canvas id="platformChart" height="200"></canvas>
</div>
<div class="card rounded-2xl p-6">
<h3 class="font-semibold mb-4">Rendimiento por Tipo de Contenido</h3>
<canvas id="contentChart" height="200"></canvas>
</div>
</div>
<!-- Top Posts -->
<div class="card rounded-2xl p-6 mb-8">
<h3 class="font-semibold mb-4">Mejores Posts</h3>
<div id="top-posts" class="space-y-4">
<div class="text-center py-8 text-gray-500">Cargando...</div>
</div>
</div>
<!-- Optimal Times -->
<div class="card rounded-2xl p-6">
<h3 class="font-semibold mb-4">Mejores Horarios para Publicar</h3>
<div id="optimal-times" class="grid grid-cols-7 gap-2">
<div class="text-center py-8 text-gray-500 col-span-7">Cargando...</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
let platformChart, contentChart;
async function loadDashboard() {
const period = document.getElementById('periodSelect').value;
const platform = document.getElementById('platformSelect').value;
const params = new URLSearchParams({ days: period });
if (platform) params.append('platform', platform);
try {
const statsRes = await fetch(`/api/analytics/dashboard?${params}`);
const stats = await statsRes.json();
document.getElementById('stat-posts').textContent = stats.total_posts || 0;
document.getElementById('stat-impressions').textContent = formatNumber(stats.total_impressions || 0);
document.getElementById('stat-engagements').textContent = formatNumber(stats.total_engagements || 0);
document.getElementById('stat-rate').textContent = (stats.avg_engagement_rate || 0).toFixed(2) + '%';
updatePlatformChart(stats.platform_breakdown || {});
updateContentChart(stats.content_breakdown || {});
loadTopPosts(period, platform);
loadOptimalTimes(platform);
} catch (error) {
console.error('Error loading dashboard:', error);
}
}
function formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}
function updatePlatformChart(data) {
const ctx = document.getElementById('platformChart').getContext('2d');
const labels = Object.keys(data).length ? Object.keys(data) : ['Sin datos'];
const values = Object.keys(data).length ? Object.values(data) : [0];
if (platformChart) platformChart.destroy();
platformChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: values,
backgroundColor: ['#6366f1', '#8b5cf6', '#ec4899', '#f59e0b'],
borderWidth: 0
}]
},
options: {
responsive: true,
plugins: {
legend: { position: 'bottom', labels: { color: '#9ca3af' } }
}
}
});
}
function updateContentChart(data) {
const ctx = document.getElementById('contentChart').getContext('2d');
const labels = Object.keys(data).length ? Object.keys(data) : ['Sin datos'];
const values = Object.keys(data).length ? Object.values(data) : [0];
if (contentChart) contentChart.destroy();
contentChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Posts',
data: values,
backgroundColor: '#6366f1',
borderRadius: 8
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
y: { grid: { color: '#334155' }, ticks: { color: '#9ca3af' } },
x: { grid: { display: false }, ticks: { color: '#9ca3af' } }
}
}
});
}
async function loadTopPosts(period, platform) {
const container = document.getElementById('top-posts');
try {
const params = new URLSearchParams({ days: period, limit: 5 });
if (platform) params.append('platform', platform);
const response = await fetch(`/api/analytics/top-posts?${params}`);
const data = await response.json();
if (data.posts && data.posts.length) {
container.innerHTML = data.posts.map(post => `
<div class="bg-dark-800/50 rounded-xl p-4">
<p class="text-sm text-gray-300 mb-2">${post.content?.substring(0, 100) || 'Sin contenido'}...</p>
<div class="flex items-center justify-between text-xs text-gray-500">
<span>${post.platforms?.join(', ') || '-'}</span>
<span>Engagement: ${(post.engagement_rate || 0).toFixed(2)}%</span>
</div>
</div>
`).join('');
} else {
container.innerHTML = '<div class="text-center py-8 text-gray-500">No hay datos suficientes</div>';
}
} catch (error) {
container.innerHTML = '<div class="text-center py-8 text-red-400">Error cargando datos</div>';
}
}
async function loadOptimalTimes(platform) {
const container = document.getElementById('optimal-times');
try {
const params = platform ? `?platform=${platform}` : '';
const response = await fetch(`/api/analytics/optimal-times${params}`);
const data = await response.json();
const days = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
if (data.optimal_times && Object.keys(data.optimal_times).length) {
container.innerHTML = days.map((day, i) => {
const times = data.optimal_times[i] || [];
return `
<div class="bg-dark-800/50 rounded-xl p-3 text-center">
<p class="font-medium text-sm mb-2">${day}</p>
<div class="space-y-1">
${times.length ? times.slice(0, 3).map(t => `<span class="text-xs bg-primary/20 text-primary px-2 py-1 rounded-full block">${t}:00</span>`).join('') : '<span class="text-xs text-gray-500">-</span>'}
</div>
</div>
`;
}).join('');
} else {
container.innerHTML = '<div class="text-center py-8 text-gray-500 col-span-7">No hay datos suficientes para calcular horarios óptimos</div>';
}
} catch (error) {
container.innerHTML = '<div class="text-center py-8 text-red-400 col-span-7">Error cargando datos</div>';
}
}
// Load on page load
loadDashboard();
</script>
{% endblock %}