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>
This commit is contained in:
@@ -1,182 +1,250 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - Social Media Automation</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #1a1a2e; color: #eee; }
|
||||
.card { background-color: #16213e; border-radius: 12px; }
|
||||
.accent { color: #d4a574; }
|
||||
.btn-primary { background-color: #d4a574; color: #1a1a2e; }
|
||||
.btn-primary:hover { background-color: #c49564; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen">
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="animate-fade-in">
|
||||
<!-- Header -->
|
||||
<header class="bg-gray-900 border-b border-gray-800 px-6 py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">
|
||||
<span class="accent">Consultoría AS</span> - Social Media
|
||||
</h1>
|
||||
<nav class="flex gap-4">
|
||||
<a href="/dashboard" class="px-4 py-2 rounded bg-gray-800">Home</a>
|
||||
<a href="/dashboard/compose" class="px-4 py-2 rounded hover:bg-gray-800 accent">+ Crear Post</a>
|
||||
<a href="/dashboard/posts" class="px-4 py-2 rounded hover:bg-gray-800">Posts</a>
|
||||
<a href="/dashboard/calendar" class="px-4 py-2 rounded hover:bg-gray-800">Calendario</a>
|
||||
<a href="/dashboard/interactions" class="px-4 py-2 rounded hover:bg-gray-800">Interacciones</a>
|
||||
<a href="/logout" class="px-4 py-2 rounded hover:bg-gray-800 text-red-400">Salir</a>
|
||||
</nav>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Dashboard</h1>
|
||||
<p class="text-gray-400 mt-1">Bienvenido, {{ user.username }}</p>
|
||||
</div>
|
||||
</header>
|
||||
<a href="/compose" class="btn-primary px-6 py-3 rounded-xl font-medium flex items-center gap-2 transition-all">
|
||||
<span>✍️</span>
|
||||
<span>Crear Post</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-5 gap-4 mb-8">
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-3xl font-bold accent">{{ stats.posts_today }}</div>
|
||||
<div class="text-gray-400 text-sm">Posts Hoy</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-3xl font-bold accent">{{ stats.posts_week }}</div>
|
||||
<div class="text-gray-400 text-sm">Posts Semana</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-3xl font-bold text-yellow-500">{{ stats.pending_approval }}</div>
|
||||
<div class="text-gray-400 text-sm">Pendientes</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-3xl font-bold text-blue-500">{{ stats.scheduled }}</div>
|
||||
<div class="text-gray-400 text-sm">Programados</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-3xl font-bold text-red-500">{{ stats.interactions_pending }}</div>
|
||||
<div class="text-gray-400 text-sm">Interacciones</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>
|
||||
<span class="text-xs text-gray-500 bg-dark-700 px-2 py-1 rounded-full">Hoy</span>
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ stats.posts_today }}</p>
|
||||
<p class="text-gray-400 text-sm mt-1">Posts publicados</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<!-- Pending Approval -->
|
||||
<div class="card p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Pendientes de Aprobación</h2>
|
||||
<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>
|
||||
<span class="text-xs text-gray-500 bg-dark-700 px-2 py-1 rounded-full">Semana</span>
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ stats.posts_week }}</p>
|
||||
<p class="text-gray-400 text-sm mt-1">Posts esta semana</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>
|
||||
<span class="text-xs text-yellow-400 bg-yellow-500/20 px-2 py-1 rounded-full">Pendiente</span>
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ stats.pending_approval }}</p>
|
||||
<p class="text-gray-400 text-sm mt-1">Por aprobar</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>
|
||||
<span class="text-xs text-green-400 bg-green-500/20 px-2 py-1 rounded-full">Programado</span>
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ stats.scheduled }}</p>
|
||||
<p class="text-gray-400 text-sm mt-1">Posts programados</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Pending Posts -->
|
||||
<div class="card rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-lg font-semibold flex items-center gap-2">
|
||||
<span>⏳</span>
|
||||
<span>Pendientes de Aprobación</span>
|
||||
</h2>
|
||||
<a href="/posts?status=pending" class="text-primary text-sm hover:underline">Ver todos</a>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{% if pending_posts %}
|
||||
{% for post in pending_posts %}
|
||||
<div class="border-b border-gray-700 py-4 last:border-0">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="bg-blue-900 text-blue-300 px-2 py-1 rounded text-xs">
|
||||
{{ post.content_type }}
|
||||
</span>
|
||||
<span class="text-gray-500 text-sm">
|
||||
{{ post.scheduled_at }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-300 mb-3">{{ post.content[:200] }}...</p>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="approvePost({{ post.id }})"
|
||||
class="btn-primary px-3 py-1 rounded text-sm">
|
||||
Aprobar
|
||||
</button>
|
||||
<button onclick="rejectPost({{ post.id }})"
|
||||
class="bg-red-900 text-red-300 px-3 py-1 rounded text-sm">
|
||||
Rechazar
|
||||
</button>
|
||||
<button onclick="editPost({{ post.id }})"
|
||||
class="bg-gray-700 px-3 py-1 rounded text-sm">
|
||||
Editar
|
||||
</button>
|
||||
<div class="bg-dark-800/50 rounded-xl p-4 hover:bg-dark-700/50 transition-colors">
|
||||
<p class="text-sm text-gray-300 line-clamp-2">{{ post.content[:100] }}{% if post.content|length > 100 %}...{% endif %}</p>
|
||||
<div class="flex items-center justify-between mt-3">
|
||||
<div class="flex items-center gap-2">
|
||||
{% for platform in post.platforms %}
|
||||
<span class="text-xs bg-dark-700 px-2 py-1 rounded-full">{{ platform }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">{{ post.created_at[:10] if post.created_at else '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-gray-500">No hay posts pendientes</p>
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<span class="text-4xl mb-2 block">✅</span>
|
||||
<p>No hay posts pendientes</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduled -->
|
||||
<div class="card p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Próximas Publicaciones</h2>
|
||||
<!-- Scheduled Posts -->
|
||||
<div class="card rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-lg font-semibold flex items-center gap-2">
|
||||
<span>🗓️</span>
|
||||
<span>Próximas Publicaciones</span>
|
||||
</h2>
|
||||
<a href="/calendar" class="text-primary text-sm hover:underline">Ver calendario</a>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{% if scheduled_posts %}
|
||||
{% for post in scheduled_posts %}
|
||||
<div class="border-b border-gray-700 py-3 last:border-0 flex items-center gap-4">
|
||||
<div class="text-center">
|
||||
<div class="text-accent font-bold">{{ post.scheduled_at }}</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-xs bg-gray-700 px-2 py-1 rounded">
|
||||
{{ post.content_type }}
|
||||
</span>
|
||||
<p class="text-gray-400 text-sm mt-1">
|
||||
{{ post.content[:100] }}...
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
{% for platform in post.platforms %}
|
||||
<span class="text-xs bg-gray-800 px-2 py-1 rounded">
|
||||
{{ platform }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
<div class="bg-dark-800/50 rounded-xl p-4 hover:bg-dark-700/50 transition-colors">
|
||||
<p class="text-sm text-gray-300 line-clamp-2">{{ post.content[:100] }}{% if post.content|length > 100 %}...{% endif %}</p>
|
||||
<div class="flex items-center justify-between mt-3">
|
||||
<div class="flex items-center gap-2">
|
||||
{% for platform in post.platforms %}
|
||||
<span class="text-xs bg-dark-700 px-2 py-1 rounded-full">{{ platform }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="text-xs text-green-400">{{ post.scheduled_at[:16] if post.scheduled_at else '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-gray-500">No hay posts programados</p>
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<span class="text-4xl mb-2 block">📭</span>
|
||||
<p>No hay posts programados</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Interactions -->
|
||||
<div class="card p-6 mt-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Interacciones Recientes</h2>
|
||||
{% if recent_interactions %}
|
||||
<div class="grid gap-4">
|
||||
<div class="card rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-lg font-semibold flex items-center gap-2">
|
||||
<span>💬</span>
|
||||
<span>Interacciones Recientes</span>
|
||||
</h2>
|
||||
<a href="/interactions" class="text-primary text-sm hover:underline">Ver todas</a>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{% if recent_interactions %}
|
||||
{% for interaction in recent_interactions %}
|
||||
<div class="bg-gray-800 rounded-lg p-4">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<span class="font-semibold">@{{ interaction.author_username }}</span>
|
||||
<span class="text-gray-500 text-sm ml-2">{{ interaction.platform }}</span>
|
||||
<div class="bg-dark-800/50 rounded-xl p-4 hover:bg-dark-700/50 transition-colors">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center text-sm">
|
||||
{{ interaction.author_username[0]|upper if interaction.author_username else '?' }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium">@{{ interaction.author_username or 'Usuario' }}</p>
|
||||
<p class="text-xs text-gray-500">{{ interaction.interaction_type }}</p>
|
||||
</div>
|
||||
<span class="text-gray-500 text-xs">{{ interaction.interaction_at }}</span>
|
||||
</div>
|
||||
<p class="text-gray-300">{{ interaction.content }}</p>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button class="bg-accent text-gray-900 px-3 py-1 rounded text-sm">
|
||||
Responder
|
||||
</button>
|
||||
<button class="bg-gray-700 px-3 py-1 rounded text-sm">
|
||||
Marcar como Lead
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 line-clamp-2">{{ interaction.content[:80] }}{% if interaction.content|length > 80 %}...{% endif %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">No hay interacciones pendientes</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<span class="text-4xl mb-2 block">💤</span>
|
||||
<p>No hay interacciones pendientes</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function approvePost(postId) {
|
||||
const response = await fetch(`/api/posts/${postId}/approve`, { method: 'POST' });
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
<!-- Quick Actions -->
|
||||
<div class="mt-8">
|
||||
<h2 class="text-lg font-semibold mb-4">Acciones Rápidas</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<a href="/compose" class="card rounded-xl p-4 text-center hover:bg-dark-700/50 transition-all group">
|
||||
<span class="text-3xl mb-2 block group-hover:scale-110 transition-transform">✍️</span>
|
||||
<span class="text-sm text-gray-300">Nuevo Post</span>
|
||||
</a>
|
||||
<a href="/compose?type=tip" class="card rounded-xl p-4 text-center hover:bg-dark-700/50 transition-all group">
|
||||
<span class="text-3xl mb-2 block group-hover:scale-110 transition-transform">💡</span>
|
||||
<span class="text-sm text-gray-300">Generar Tip</span>
|
||||
</a>
|
||||
<a href="/analytics" class="card rounded-xl p-4 text-center hover:bg-dark-700/50 transition-all group">
|
||||
<span class="text-3xl mb-2 block group-hover:scale-110 transition-transform">📊</span>
|
||||
<span class="text-sm text-gray-300">Ver Analytics</span>
|
||||
</a>
|
||||
<a href="/settings" class="card rounded-xl p-4 text-center hover:bg-dark-700/50 transition-all group">
|
||||
<span class="text-3xl mb-2 block group-hover:scale-110 transition-transform">⚙️</span>
|
||||
<span class="text-sm text-gray-300">Configuración</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Platform Status -->
|
||||
<div class="mt-8">
|
||||
<h2 class="text-lg font-semibold mb-4">Estado de Plataformas</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4" id="platform-status">
|
||||
<!-- Loaded via JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
async function loadPlatformStatus() {
|
||||
const container = document.getElementById('platform-status');
|
||||
const platforms = ['x', 'facebook', 'instagram', 'threads'];
|
||||
|
||||
container.innerHTML = platforms.map(p => `
|
||||
<div class="card rounded-xl p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xl">${getPlatformIcon(p)}</span>
|
||||
<span class="text-xs bg-dark-700 px-2 py-1 rounded-full">Verificando...</span>
|
||||
</div>
|
||||
<p class="text-sm mt-2 capitalize">${p}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/publish/test');
|
||||
const data = await response.json();
|
||||
|
||||
container.innerHTML = '';
|
||||
for (const [platform, status] of Object.entries(data)) {
|
||||
const isConnected = status.configured && status.connected;
|
||||
container.innerHTML += `
|
||||
<div class="card rounded-xl p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xl">${getPlatformIcon(platform)}</span>
|
||||
<span class="text-xs ${isConnected ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'} px-2 py-1 rounded-full">
|
||||
${isConnected ? 'Conectado' : 'Desconectado'}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm mt-2 capitalize">${platform}</p>
|
||||
${status.details?.username ? `<p class="text-xs text-gray-500">@${status.details.username}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
container.innerHTML = '<div class="col-span-4 text-center text-gray-500 py-4">Error cargando estado</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectPost(postId) {
|
||||
if (confirm('¿Seguro que quieres rechazar este post?')) {
|
||||
const response = await fetch(`/api/posts/${postId}/reject`, { method: 'POST' });
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
function getPlatformIcon(platform) {
|
||||
const icons = { x: '𝕏', facebook: '📘', instagram: '📸', threads: '🧵' };
|
||||
return icons[platform] || '📱';
|
||||
}
|
||||
|
||||
function editPost(postId) {
|
||||
window.location.href = `/posts/${postId}/edit`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
loadPlatformStatus();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user