aktueller stand

This commit is contained in:
2026-02-03 12:48:43 +01:00
parent e1ecd1a38c
commit b50594dbfa
77 changed files with 19139 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Admin - LinkedIn Posts{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'brand': {
'bg': '#3d4848',
'bg-light': '#4a5858',
'bg-dark': '#2d3838',
'highlight': '#ffc700',
'highlight-dark': '#e6b300',
}
}
}
}
}
</script>
<style>
body { background-color: #3d4848; }
.nav-link.active { background-color: #ffc700; color: #2d3838; }
.nav-link.active svg { stroke: #2d3838; }
.post-content { white-space: pre-wrap; word-wrap: break-word; }
.btn-primary { background-color: #ffc700; color: #2d3838; }
.btn-primary:hover { background-color: #e6b300; }
.sidebar-bg { background-color: #2d3838; }
.card-bg { background-color: #4a5858; border-color: #5a6868; }
.input-bg { background-color: #3d4848; border-color: #5a6868; }
.input-bg:focus { border-color: #ffc700; outline: none; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #3d4848; }
::-webkit-scrollbar-thumb { background: #5a6868; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #6a7878; }
</style>
{% block head %}{% endblock %}
</head>
<body class="text-gray-100 min-h-screen flex">
<!-- Sidebar -->
<aside class="w-64 sidebar-bg border-r border-gray-600 flex flex-col fixed h-full">
<div class="p-4 border-b border-gray-600">
<div class="flex items-center justify-center gap-3">
<div>
<img src="/static/logo.png" alt="Logo" class="h-15 w-auto">
</div>
</div>
<div class="text-center mt-2">
<span class="text-xs text-brand-highlight font-medium px-2 py-1 bg-brand-highlight/20 rounded">ADMIN</span>
</div>
</div>
<nav class="flex-1 p-4 space-y-2">
<a href="/admin" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'home' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
Dashboard
</a>
<a href="/admin/customers/new" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'new_customer' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/></svg>
Neuer Kunde
</a>
<a href="/admin/research" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'research' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
Research Topics
</a>
<a href="/admin/create" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'create' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Post erstellen
</a>
<a href="/admin/posts" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'posts' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
Alle Posts
</a>
<a href="/admin/scraped-posts" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'scraped_posts' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>
Post-Typen
</a>
<a href="/admin/status" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'status' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Status
</a>
</nav>
<div class="p-4 border-t border-gray-600">
<a href="/admin/logout" class="flex items-center gap-2 text-gray-400 hover:text-gray-200 text-sm transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
Logout
</a>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 ml-64">
<div class="p-8">
{% block content %}{% endblock %}
</div>
</main>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,539 @@
{% extends "base.html" %}
{% block title %}Post erstellen - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Post erstellen</h1>
<p class="text-gray-400">Generiere einen neuen LinkedIn Post mit AI</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left: Form -->
<div>
<form id="createPostForm" class="card-bg rounded-xl border p-6 space-y-6">
<!-- Customer Selection -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Kunde auswählen</label>
<select name="customer_id" id="customerSelect" required class="w-full input-bg border rounded-lg px-4 py-3 text-white">
<option value="">-- Kunde wählen --</option>
{% for customer in customers %}
<option value="{{ customer.id }}">{{ customer.name }} - {{ customer.company_name or 'Kein Unternehmen' }}</option>
{% endfor %}
</select>
</div>
<!-- Post Type Selection -->
<div id="postTypeSelectionArea" class="hidden">
<label class="block text-sm font-medium text-gray-300 mb-2">Post-Typ auswählen (optional)</label>
<div id="postTypeCards" class="flex flex-wrap gap-2 mb-2">
<!-- Post type cards will be loaded here -->
</div>
<input type="hidden" id="selectedPostTypeId" value="">
</div>
<!-- Topic Selection -->
<div id="topicSelectionArea" class="hidden">
<label class="block text-sm font-medium text-gray-300 mb-2">Topic auswählen</label>
<div id="topicsList" class="space-y-2 max-h-64 overflow-y-auto">
<p class="text-gray-500">Lade Topics...</p>
</div>
</div>
<!-- Custom Topic -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">
<span>Oder eigenes Topic eingeben</span>
</label>
<div class="space-y-3">
<input type="text" id="customTopicTitle" placeholder="Topic Titel" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
<textarea id="customTopicFact" rows="3" placeholder="Fakt / Kernaussage zum Topic..." class="w-full input-bg border rounded-lg px-4 py-2 text-white"></textarea>
<input type="text" id="customTopicSource" placeholder="Quelle (optional)" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
</div>
<!-- Progress Area -->
<div id="progressArea" class="hidden">
<div class="bg-brand-bg rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span id="progressMessage" class="text-gray-300">Starte Post-Erstellung...</span>
<span id="progressPercent" class="text-gray-400">0%</span>
</div>
<div class="w-full bg-brand-bg-dark rounded-full h-2">
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div id="iterationInfo" class="mt-2 text-sm text-gray-400"></div>
</div>
</div>
<button type="submit" id="submitBtn" class="w-full btn-primary font-medium py-3 rounded-lg transition-colors flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Post generieren
</button>
</form>
{% if not customers %}
<div class="mt-4 bg-yellow-900/30 border border-yellow-600 rounded-lg p-4">
<p class="text-yellow-300">Noch keine Kunden vorhanden. <a href="/admin/customers/new" class="underline">Erstelle zuerst einen Kunden</a>.</p>
</div>
{% endif %}
</div>
<!-- Right: Result -->
<div>
<div id="resultArea" class="card-bg rounded-xl border p-6">
<h3 class="text-lg font-semibold text-white mb-4">Generierter Post</h3>
<!-- Live Versions Display -->
<div id="liveVersions" class="hidden space-y-4 mb-6">
<div class="flex items-center gap-2 mb-2">
<span class="text-sm text-gray-400">Live-Vorschau der Iterationen:</span>
</div>
<div id="versionsContainer" class="space-y-4"></div>
</div>
<div id="postResult">
<p class="text-gray-400">Wähle einen Kunden und ein Topic, dann klicke auf "Post generieren"...</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const form = document.getElementById('createPostForm');
const customerSelect = document.getElementById('customerSelect');
const topicSelectionArea = document.getElementById('topicSelectionArea');
const topicsList = document.getElementById('topicsList');
const submitBtn = document.getElementById('submitBtn');
const progressArea = document.getElementById('progressArea');
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const progressPercent = document.getElementById('progressPercent');
const iterationInfo = document.getElementById('iterationInfo');
const postResult = document.getElementById('postResult');
const liveVersions = document.getElementById('liveVersions');
const versionsContainer = document.getElementById('versionsContainer');
const postTypeSelectionArea = document.getElementById('postTypeSelectionArea');
const postTypeCards = document.getElementById('postTypeCards');
const selectedPostTypeIdInput = document.getElementById('selectedPostTypeId');
let selectedTopic = null;
let currentVersionIndex = 0;
let currentPostTypes = [];
let currentTopics = [];
function renderVersions(versions, feedbackList) {
if (!versions || versions.length === 0) {
liveVersions.classList.add('hidden');
return;
}
liveVersions.classList.remove('hidden');
// Build version tabs and content
let html = `
<div class="flex gap-2 mb-4 flex-wrap">
${versions.map((_, i) => `
<button onclick="showVersion(${i})" id="versionTab${i}"
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}">
V${i + 1}
${feedbackList[i] ? `<span class="ml-1 text-xs opacity-75">(${feedbackList[i].overall_score || '?'})</span>` : ''}
</button>
`).join('')}
</div>
`;
// Show current version
const currentVersion = versions[currentVersionIndex];
const currentFeedback = feedbackList[currentVersionIndex];
html += `
<div class="grid grid-cols-1 ${currentFeedback ? 'lg:grid-cols-2' : ''} gap-4">
<div class="bg-brand-bg/50 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span>
${currentFeedback ? `
<span class="px-2 py-0.5 text-xs rounded ${currentFeedback.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
${currentFeedback.approved ? 'Approved' : `Score: ${currentFeedback.overall_score}/100`}
</span>
` : '<span class="text-xs text-gray-500">Wird bewertet...</span>'}
</div>
<pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-96 overflow-y-auto">${currentVersion}</pre>
</div>
${currentFeedback ? `
<div class="bg-brand-bg/30 rounded-lg p-4 border border-brand-bg-light">
<span class="text-sm font-medium text-gray-300 block mb-2">Kritik</span>
<p class="text-sm text-gray-400 mb-3">${currentFeedback.feedback || 'Keine Kritik'}</p>
${currentFeedback.improvements && currentFeedback.improvements.length > 0 ? `
<div class="mt-2">
<span class="text-xs font-medium text-gray-400">Verbesserungen:</span>
<ul class="mt-1 space-y-1">
${currentFeedback.improvements.map(imp => `
<li class="text-xs text-gray-500 flex items-start gap-1">
<span class="text-yellow-500">•</span> ${imp}
</li>
`).join('')}
</ul>
</div>
` : ''}
${currentFeedback.scores ? `
<div class="mt-3 pt-3 border-t border-brand-bg-light">
<div class="grid grid-cols-3 gap-2 text-xs">
<div class="text-center">
<div class="text-gray-500">Authentizität</div>
<div class="font-medium text-gray-300">${currentFeedback.scores.authenticity_and_style || '?'}/40</div>
</div>
<div class="text-center">
<div class="text-gray-500">Content</div>
<div class="font-medium text-gray-300">${currentFeedback.scores.content_quality || '?'}/35</div>
</div>
<div class="text-center">
<div class="text-gray-500">Technik</div>
<div class="font-medium text-gray-300">${currentFeedback.scores.technical_execution || '?'}/25</div>
</div>
</div>
</div>
` : ''}
</div>
` : ''}
</div>
`;
versionsContainer.innerHTML = html;
}
function showVersion(index) {
currentVersionIndex = index;
// Get cached versions from progress store
const cachedData = window.lastProgressData;
if (cachedData) {
renderVersions(cachedData.versions, cachedData.feedback_list);
}
}
// Load topics and post types when customer is selected
customerSelect.addEventListener('change', async () => {
const customerId = customerSelect.value;
selectedPostTypeIdInput.value = '';
if (!customerId) {
topicSelectionArea.classList.add('hidden');
postTypeSelectionArea.classList.add('hidden');
return;
}
topicSelectionArea.classList.remove('hidden');
topicsList.innerHTML = '<p class="text-gray-500">Lade Topics...</p>';
// Load post types
try {
const ptResponse = await fetch(`/admin/api/customers/${customerId}/post-types`);
const ptData = await ptResponse.json();
if (ptData.post_types && ptData.post_types.length > 0) {
currentPostTypes = ptData.post_types;
postTypeSelectionArea.classList.remove('hidden');
postTypeCards.innerHTML = `
<button type="button" onclick="selectPostTypeForCreate('')" id="ptc_all"
class="px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-highlight/20 border-brand-highlight text-white">
Alle Typen
</button>
` + ptData.post_types.map(pt => `
<button type="button" onclick="selectPostTypeForCreate('${pt.id}')" id="ptc_${pt.id}"
class="px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
${pt.name}
${pt.has_analysis ? '<span class="ml-1 text-green-400 text-xs">*</span>' : ''}
</button>
`).join('');
} else {
postTypeSelectionArea.classList.add('hidden');
}
} catch (error) {
console.error('Failed to load post types:', error);
postTypeSelectionArea.classList.add('hidden');
}
// Load topics
try {
const response = await fetch(`/admin/api/customers/${customerId}/topics`);
const data = await response.json();
if (data.topics && data.topics.length > 0) {
renderTopicsList(data);
} else {
// No topics available - show helpful message
let message = '';
if (data.used_count > 0) {
message = `<div class="text-center py-4">
<p class="text-gray-400 mb-2">Alle ${data.used_count} Topics wurden bereits verwendet.</p>
<a href="/admin/research" class="text-brand-highlight hover:underline">Neue Topics recherchieren</a>
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
</div>`;
} else {
message = `<div class="text-center py-4">
<p class="text-gray-400 mb-2">Keine Topics gefunden.</p>
<a href="/admin/research" class="text-brand-highlight hover:underline">Recherche starten</a>
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
</div>`;
}
topicsList.innerHTML = message;
}
} catch (error) {
topicsList.innerHTML = `<p class="text-red-400">Fehler beim Laden: ${error.message}</p>`;
}
});
// Clear selected topic when custom topic is entered
['customTopicTitle', 'customTopicFact', 'customTopicSource'].forEach(id => {
document.getElementById(id).addEventListener('input', () => {
selectedTopic = null;
document.querySelectorAll('input[name="topic"]').forEach(radio => radio.checked = false);
});
});
function selectPostTypeForCreate(typeId) {
selectedPostTypeIdInput.value = typeId;
// Update card styles
document.querySelectorAll('[id^="ptc_"]').forEach(card => {
if (card.id === `ptc_${typeId}` || (typeId === '' && card.id === 'ptc_all')) {
card.className = 'px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-highlight/20 border-brand-highlight text-white';
} else {
card.className = 'px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white';
}
});
// Optionally reload topics filtered by post type
const customerId = customerSelect.value;
if (customerId) {
loadTopicsForPostType(customerId, typeId);
}
}
async function loadTopicsForPostType(customerId, postTypeId) {
topicsList.innerHTML = '<p class="text-gray-500">Lade Topics...</p>';
try {
let url = `/api/customers/${customerId}/topics`;
if (postTypeId) {
url += `?post_type_id=${postTypeId}`;
}
const response = await fetch(url);
const data = await response.json();
if (data.topics && data.topics.length > 0) {
renderTopicsList(data);
} else {
let message = '';
if (data.used_count > 0) {
message = `<div class="text-center py-4">
<p class="text-gray-400 mb-2">Alle ${data.used_count} Topics wurden bereits verwendet.</p>
<a href="/admin/research" class="text-brand-highlight hover:underline">Neue Topics recherchieren</a>
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
</div>`;
} else {
message = `<div class="text-center py-4">
<p class="text-gray-400 mb-2">Keine Topics gefunden${postTypeId ? ' für diesen Post-Typ' : ''}.</p>
<a href="/admin/research" class="text-brand-highlight hover:underline">Recherche starten</a>
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
</div>`;
}
topicsList.innerHTML = message;
}
} catch (error) {
topicsList.innerHTML = `<p class="text-red-400">Fehler beim Laden: ${error.message}</p>`;
}
}
function renderTopicsList(data) {
// Store topics in global array for safe access
currentTopics = data.topics;
// Reset selected topic when list is re-rendered
selectedTopic = null;
let statsHtml = '';
if (data.used_count > 0) {
statsHtml = `<p class="text-xs text-gray-500 mb-3">${data.available_count} verfügbar · ${data.used_count} bereits verwendet</p>`;
}
topicsList.innerHTML = statsHtml + data.topics.map((topic, i) => `
<label class="flex items-start gap-3 p-3 bg-brand-bg/50 rounded-lg cursor-pointer hover:bg-brand-bg transition-colors border border-transparent hover:border-brand-highlight/30">
<input type="radio" name="topic" value="${i}" class="mt-1 text-brand-highlight" data-topic-index="${i}">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1 flex-wrap">
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-brand-highlight/20 text-brand-highlight rounded">${escapeHtml(topic.category || 'Topic')}</span>
${topic.target_post_type_id ? `<span class="text-xs text-gray-500">Typ-spezifisch</span>` : ''}
${topic.source ? `<span class="text-xs text-gray-500">🔗 ${escapeHtml(topic.source.substring(0, 30))}${topic.source.length > 30 ? '...' : ''}</span>` : ''}
</div>
<p class="font-medium text-white">${escapeHtml(topic.title)}</p>
${topic.angle ? `<p class="text-xs text-brand-highlight/80 mt-1">→ ${escapeHtml(topic.angle)}</p>` : ''}
${topic.hook_idea ? `<p class="text-sm text-gray-300 mt-2 italic border-l-2 border-brand-highlight/30 pl-2">"${escapeHtml(topic.hook_idea.substring(0, 120))}${topic.hook_idea.length > 120 ? '...' : ''}"</p>` : ''}
${topic.key_facts && topic.key_facts.length > 0 ? `
<div class="mt-2 flex flex-wrap gap-1">
${topic.key_facts.slice(0, 2).map(f => `<span class="text-xs bg-brand-bg-dark px-2 py-0.5 rounded text-gray-400">📊 ${escapeHtml(f.substring(0, 40))}${f.length > 40 ? '...' : ''}</span>`).join('')}
</div>
` : (topic.fact ? `<p class="text-sm text-gray-400 mt-1">${escapeHtml(topic.fact.substring(0, 100))}...</p>` : '')}
${topic.why_this_person ? `<p class="text-xs text-gray-500 mt-2">💡 ${escapeHtml(topic.why_this_person.substring(0, 80))}${topic.why_this_person.length > 80 ? '...' : ''}</p>` : ''}
</div>
</label>
`).join('');
// Add event listeners to radio buttons
document.querySelectorAll('input[name="topic"]').forEach(radio => {
radio.addEventListener('change', () => {
const index = parseInt(radio.dataset.topicIndex, 10);
selectedTopic = currentTopics[index];
// Clear custom topic fields
document.getElementById('customTopicTitle').value = '';
document.getElementById('customTopicFact').value = '';
document.getElementById('customTopicSource').value = '';
});
});
}
// Helper function to escape HTML special characters
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const customerId = customerSelect.value;
if (!customerId) {
alert('Bitte wähle einen Kunden aus.');
return;
}
// Get topic (either selected or custom)
let topic;
const customTitle = document.getElementById('customTopicTitle').value.trim();
const customFact = document.getElementById('customTopicFact').value.trim();
if (customTitle && customFact) {
topic = {
title: customTitle,
fact: customFact,
source: document.getElementById('customTopicSource').value.trim() || null,
category: 'Custom'
};
} else if (selectedTopic) {
topic = selectedTopic;
} else {
alert('Bitte wähle ein Topic aus oder gib ein eigenes ein.');
return;
}
submitBtn.disabled = true;
submitBtn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Generiert...';
progressArea.classList.remove('hidden');
postResult.innerHTML = '<p class="text-gray-400">Post wird generiert...</p>';
const formData = new FormData();
formData.append('customer_id', customerId);
formData.append('topic_json', JSON.stringify(topic));
if (selectedPostTypeIdInput.value) {
formData.append('post_type_id', selectedPostTypeIdInput.value);
}
try {
const response = await fetch('/admin/api/posts', {
method: 'POST',
body: formData
});
const data = await response.json();
const taskId = data.task_id;
currentVersionIndex = 0;
window.lastProgressData = null;
const pollInterval = setInterval(async () => {
const statusResponse = await fetch(`/admin/api/tasks/${taskId}`);
const status = await statusResponse.json();
progressBar.style.width = `${status.progress}%`;
progressPercent.textContent = `${status.progress}%`;
progressMessage.textContent = status.message;
if (status.iteration !== undefined) {
iterationInfo.textContent = `Iteration ${status.iteration}/${status.max_iterations}`;
}
// Update live versions display
if (status.versions && status.versions.length > 0) {
window.lastProgressData = status;
// Auto-select latest version
if (status.versions.length > currentVersionIndex + 1) {
currentVersionIndex = status.versions.length - 1;
}
renderVersions(status.versions, status.feedback_list || []);
postResult.innerHTML = '<p class="text-gray-400">Siehe Live-Vorschau oben...</p>';
}
if (status.status === 'completed') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
// Keep live versions visible but update header
const result = status.result;
postResult.innerHTML = `
<div class="space-y-4">
<div class="flex items-center gap-2 text-sm flex-wrap">
<span class="px-2 py-1 rounded ${result.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
${result.approved ? 'Approved' : 'Review needed'}
</span>
<span class="text-gray-400">Score: ${result.final_score}/100</span>
<span class="text-gray-400">Iterations: ${result.iterations}</span>
</div>
<div class="text-sm text-gray-400 mb-2">Finaler Post:</div>
<div class="bg-brand-bg/50 rounded-lg p-4">
<pre class="whitespace-pre-wrap text-gray-200 font-sans">${result.final_post}</pre>
</div>
<div class="flex gap-2">
<button onclick="copyPost()" class="px-4 py-2 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-white transition-colors">
In Zwischenablage kopieren
</button>
<button onclick="toggleVersions()" class="px-4 py-2 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-white transition-colors">
Versionen ${liveVersions.classList.contains('hidden') ? 'anzeigen' : 'ausblenden'}
</button>
</div>
</div>
`;
} else if (status.status === 'error') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
postResult.innerHTML = `<p class="text-red-400">Fehler: ${status.message}</p>`;
}
}, 1000);
} catch (error) {
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
postResult.innerHTML = `<p class="text-red-400">Fehler: ${error.message}</p>`;
}
});
function copyPost() {
const postText = document.querySelector('#postResult pre').textContent;
navigator.clipboard.writeText(postText).then(() => {
alert('Post in Zwischenablage kopiert!');
});
}
function toggleVersions() {
liveVersions.classList.toggle('hidden');
}
</script>
{% endblock %}

View File

@@ -0,0 +1,97 @@
{% extends "base.html" %}
{% block title %}Dashboard - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Dashboard</h1>
<p class="text-gray-400">Willkommen zum LinkedIn Post Creation System</p>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
<strong>Error:</strong> {{ error }}
</div>
{% endif %}
<!-- Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="card-bg rounded-xl border p-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
</div>
<div>
<p class="text-gray-400 text-sm">Kunden</p>
<p class="text-2xl font-bold text-white">{{ customers_count or 0 }}</p>
</div>
</div>
</div>
<div class="card-bg rounded-xl border p-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-green-600/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
</div>
<div>
<p class="text-gray-400 text-sm">Generierte Posts</p>
<p class="text-2xl font-bold text-white">{{ total_posts or 0 }}</p>
</div>
</div>
</div>
<div class="card-bg rounded-xl border p-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
</div>
<div>
<p class="text-gray-400 text-sm">AI Agents</p>
<p class="text-2xl font-bold text-white">5</p>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card-bg rounded-xl border p-6">
<h2 class="text-xl font-semibold text-white mb-4">Schnellaktionen</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<a href="/admin/customers/new" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
</div>
<div>
<p class="font-medium text-white">Neuer Kunde</p>
<p class="text-sm text-gray-400">Setup starten</p>
</div>
</a>
<a href="/admin/research" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
</div>
<div>
<p class="font-medium text-white">Research</p>
<p class="text-sm text-gray-400">Topics finden</p>
</div>
</a>
<a href="/admin/create" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
</div>
<div>
<p class="font-medium text-white">Post erstellen</p>
<p class="text-sm text-gray-400">Content generieren</p>
</div>
</a>
<a href="/admin/posts" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
</div>
<div>
<p class="font-medium text-white">Alle Posts</p>
<p class="text-sm text-gray-400">Übersicht anzeigen</p>
</div>
</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - LinkedIn Posts</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'brand': {
'bg': '#3d4848',
'bg-light': '#4a5858',
'bg-dark': '#2d3838',
'highlight': '#ffc700',
'highlight-dark': '#e6b300',
}
}
}
}
}
</script>
<style>
body { background-color: #3d4848; }
.btn-primary { background-color: #ffc700; color: #2d3838; }
.btn-primary:hover { background-color: #e6b300; }
.card-bg { background-color: #4a5858; border-color: #5a6868; }
.input-bg { background-color: #3d4848; border-color: #5a6868; }
.input-bg:focus { border-color: #ffc700; outline: none; }
</style>
</head>
<body class="text-gray-100 min-h-screen flex items-center justify-center">
<div class="w-full max-w-md">
<div class="card-bg rounded-xl border p-8">
<div class="text-center mb-8">
<img src="/static/logo.png" alt="Logo" class="h-16 w-auto mx-auto mb-4">
<h1 class="text-2xl font-bold text-white mb-2">LinkedIn Posts</h1>
<p class="text-gray-400">Admin Panel</p>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
Falsches Passwort. Bitte versuche es erneut.
</div>
{% endif %}
<form method="POST" action="/admin/login" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Passwort</label>
<input
type="password"
name="password"
required
autofocus
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Passwort eingeben..."
>
</div>
<button
type="submit"
class="w-full btn-primary font-medium py-3 rounded-lg transition-colors"
>
Anmelden
</button>
</form>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,274 @@
{% extends "base.html" %}
{% block title %}Neuer Kunde - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Neuer Kunde</h1>
<p class="text-gray-400">Richte einen neuen Kunden ein und starte das initiale Setup</p>
</div>
<div class="max-w-2xl">
<form id="customerForm" class="card-bg rounded-xl border p-6 space-y-6">
<!-- Basic Info -->
<div>
<h3 class="text-lg font-semibold text-white mb-4">Basis-Informationen</h3>
<div class="grid gap-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Name *</label>
<input type="text" name="name" required class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">LinkedIn URL *</label>
<input type="url" name="linkedin_url" required placeholder="https://www.linkedin.com/in/username" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Firma</label>
<input type="text" name="company_name" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">E-Mail</label>
<input type="email" name="email" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
</div>
</div>
</div>
<!-- Persona -->
<div>
<h3 class="text-lg font-semibold text-white mb-4">Persona & Stil</h3>
<div class="grid gap-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Persona</label>
<textarea name="persona" rows="3" placeholder="Beschreibe die Expertise, Positionierung und den Charakter der Person..." class="w-full input-bg border rounded-lg px-4 py-2 text-white"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Ansprache</label>
<input type="text" name="form_of_address" placeholder="z.B. Duzen (Du/Euch) oder Siezen (Sie)" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Style Guide</label>
<textarea name="style_guide" rows="3" placeholder="Beschreibe den Schreibstil, Tonalität und Richtlinien..." class="w-full input-bg border rounded-lg px-4 py-2 text-white"></textarea>
</div>
</div>
</div>
<!-- Post Types -->
<div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-white">Post-Typen</h3>
<button type="button" id="addPostTypeBtn" class="text-sm text-brand-highlight hover:underline">+ Post-Typ hinzufügen</button>
</div>
<p class="text-sm text-gray-400 mb-4">Definiere verschiedene Arten von Posts (z.B. "Thought Leader", "Case Study", "How-To"). Diese werden zur Kategorisierung und typ-spezifischen Analyse verwendet.</p>
<div id="postTypesContainer" class="space-y-4">
<!-- Post type entries will be added here -->
</div>
</div>
<!-- Progress Area -->
<div id="progressArea" class="hidden">
<div class="bg-brand-bg rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span id="progressMessage" class="text-gray-300">Starte Setup...</span>
<span id="progressPercent" class="text-gray-400">0%</span>
</div>
<div class="w-full bg-brand-bg-dark rounded-full h-2">
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</div>
<!-- Result Area -->
<div id="resultArea" class="hidden">
<div id="successResult" class="hidden bg-green-900/30 border border-green-500 rounded-lg p-4">
<div class="flex items-center gap-3">
<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
<span class="text-green-300">Setup erfolgreich abgeschlossen!</span>
</div>
</div>
<div id="errorResult" class="hidden bg-red-900/30 border border-red-500 rounded-lg p-4">
<div class="flex items-center gap-3">
<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
<span id="errorMessage" class="text-red-300">Fehler beim Setup</span>
</div>
</div>
</div>
<!-- Submit -->
<div class="flex gap-4">
<button type="submit" id="submitBtn" class="flex-1 btn-primary font-medium py-3 rounded-lg transition-colors">
Setup starten
</button>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
const form = document.getElementById('customerForm');
const submitBtn = document.getElementById('submitBtn');
const progressArea = document.getElementById('progressArea');
const resultArea = document.getElementById('resultArea');
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const progressPercent = document.getElementById('progressPercent');
const postTypesContainer = document.getElementById('postTypesContainer');
const addPostTypeBtn = document.getElementById('addPostTypeBtn');
let postTypeIndex = 0;
function createPostTypeEntry() {
const index = postTypeIndex++;
const entry = document.createElement('div');
entry.className = 'bg-brand-bg rounded-lg p-4 border border-brand-bg-light';
entry.id = `postType_${index}`;
entry.innerHTML = `
<div class="flex justify-between items-start mb-3">
<span class="text-sm font-medium text-gray-300">Post-Typ ${index + 1}</span>
<button type="button" onclick="removePostType(${index})" class="text-red-400 hover:text-red-300 text-sm">Entfernen</button>
</div>
<div class="grid gap-3">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-400 mb-1">Name *</label>
<input type="text" data-pt-field="name" data-pt-index="${index}" required placeholder="z.B. Thought Leader" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Beschreibung</label>
<input type="text" data-pt-field="description" data-pt-index="${index}" placeholder="Kurze Beschreibung" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Identifizierende Hashtags (kommagetrennt)</label>
<input type="text" data-pt-field="hashtags" data-pt-index="${index}" placeholder="#ThoughtLeader, #Insight, #Leadership" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Keywords (kommagetrennt)</label>
<input type="text" data-pt-field="keywords" data-pt-index="${index}" placeholder="Erfahrung, Learnings, Meinung" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
<details class="mt-2">
<summary class="text-xs text-gray-400 cursor-pointer hover:text-gray-300">Erweiterte Eigenschaften</summary>
<div class="mt-3 grid gap-3">
<div>
<label class="block text-xs text-gray-400 mb-1">Zweck</label>
<input type="text" data-pt-field="purpose" data-pt-index="${index}" placeholder="z.B. Expertise zeigen, Meinungsführerschaft etablieren" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Typische Tonalität</label>
<input type="text" data-pt-field="tone" data-pt-index="${index}" placeholder="z.B. reflektiert, provokativ, inspirierend" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Zielgruppe</label>
<input type="text" data-pt-field="target_audience" data-pt-index="${index}" placeholder="z.B. Führungskräfte, Entscheider" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
</div>
</details>
</div>
`;
postTypesContainer.appendChild(entry);
}
function removePostType(index) {
const entry = document.getElementById(`postType_${index}`);
if (entry) {
entry.remove();
}
}
function collectPostTypes() {
const postTypes = [];
const entries = postTypesContainer.querySelectorAll('[id^="postType_"]');
entries.forEach(entry => {
const index = entry.id.split('_')[1];
const name = entry.querySelector(`[data-pt-field="name"][data-pt-index="${index}"]`)?.value?.trim();
if (name) {
const hashtagsRaw = entry.querySelector(`[data-pt-field="hashtags"][data-pt-index="${index}"]`)?.value || '';
const keywordsRaw = entry.querySelector(`[data-pt-field="keywords"][data-pt-index="${index}"]`)?.value || '';
postTypes.push({
name: name,
description: entry.querySelector(`[data-pt-field="description"][data-pt-index="${index}"]`)?.value?.trim() || null,
identifying_hashtags: hashtagsRaw.split(',').map(h => h.trim()).filter(h => h),
identifying_keywords: keywordsRaw.split(',').map(k => k.trim()).filter(k => k),
semantic_properties: {
purpose: entry.querySelector(`[data-pt-field="purpose"][data-pt-index="${index}"]`)?.value?.trim() || null,
typical_tone: entry.querySelector(`[data-pt-field="tone"][data-pt-index="${index}"]`)?.value?.trim() || null,
target_audience: entry.querySelector(`[data-pt-field="target_audience"][data-pt-index="${index}"]`)?.value?.trim() || null
}
});
}
});
return postTypes;
}
addPostTypeBtn.addEventListener('click', createPostTypeEntry);
form.addEventListener('submit', async (e) => {
e.preventDefault();
submitBtn.disabled = true;
submitBtn.textContent = 'Wird gestartet...';
progressArea.classList.remove('hidden');
resultArea.classList.add('hidden');
const formData = new FormData(form);
// Add post types as JSON
const postTypes = collectPostTypes();
if (postTypes.length > 0) {
formData.append('post_types_json', JSON.stringify(postTypes));
}
try {
const response = await fetch('/admin/api/customers', {
method: 'POST',
body: formData
});
const data = await response.json();
// Poll for progress
const taskId = data.task_id;
const pollInterval = setInterval(async () => {
const statusResponse = await fetch(`/admin/api/tasks/${taskId}`);
const status = await statusResponse.json();
progressBar.style.width = `${status.progress}%`;
progressPercent.textContent = `${status.progress}%`;
progressMessage.textContent = status.message;
if (status.status === 'completed') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
resultArea.classList.remove('hidden');
document.getElementById('successResult').classList.remove('hidden');
submitBtn.textContent = 'Setup starten';
submitBtn.disabled = false;
form.reset();
postTypesContainer.innerHTML = '';
postTypeIndex = 0;
} else if (status.status === 'error') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
resultArea.classList.remove('hidden');
document.getElementById('errorResult').classList.remove('hidden');
document.getElementById('errorMessage').textContent = status.message;
submitBtn.textContent = 'Setup starten';
submitBtn.disabled = false;
}
}, 1000);
} catch (error) {
progressArea.classList.add('hidden');
resultArea.classList.remove('hidden');
document.getElementById('errorResult').classList.remove('hidden');
document.getElementById('errorMessage').textContent = error.message;
submitBtn.textContent = 'Setup starten';
submitBtn.disabled = false;
}
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
{% extends "base.html" %}
{% block title %}Alle Posts - LinkedIn Posts{% endblock %}
{% block head %}
<style>
.post-card {
background: linear-gradient(135deg, rgba(61, 72, 72, 0.3) 0%, rgba(45, 56, 56, 0.4) 100%);
border: 1px solid rgba(61, 72, 72, 0.6);
transition: all 0.2s ease;
}
.post-card:hover {
border-color: rgba(255, 199, 0, 0.3);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.customer-header {
background: linear-gradient(90deg, rgba(255, 199, 0, 0.1) 0%, transparent 100%);
}
.score-ring {
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.75rem;
}
.score-high { background: rgba(34, 197, 94, 0.2); border: 2px solid rgba(34, 197, 94, 0.5); color: #86efac; }
.score-medium { background: rgba(234, 179, 8, 0.2); border: 2px solid rgba(234, 179, 8, 0.5); color: #fde047; }
.score-low { background: rgba(239, 68, 68, 0.2); border: 2px solid rgba(239, 68, 68, 0.5); color: #fca5a5; }
</style>
{% endblock %}
{% block content %}
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-white mb-2">Alle Posts</h1>
<p class="text-gray-400">{{ total_posts }} generierte Posts</p>
</div>
<a href="/admin/create" class="px-4 py-2.5 btn-primary rounded-lg font-medium flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
Neuer Post
</a>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
<strong>Error:</strong> {{ error }}
</div>
{% endif %}
{% if customers_with_posts %}
<div class="space-y-8">
{% for item in customers_with_posts %}
{% if item.posts %}
<div class="card-bg rounded-xl border overflow-hidden">
<!-- Customer Header -->
<div class="customer-header px-6 py-4 border-b border-brand-bg-light">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl flex items-center justify-center shadow-lg overflow-hidden {{ 'bg-brand-highlight' if not item.profile_picture else '' }}">
{% if item.profile_picture %}
<img src="{{ item.profile_picture }}" alt="{{ item.customer.name }}" class="w-full h-full object-cover" loading="lazy" referrerpolicy="no-referrer">
{% else %}
<span class="text-brand-bg-dark font-bold text-lg">{{ item.customer.name[0] | upper }}</span>
{% endif %}
</div>
<div>
<h3 class="font-semibold text-white text-lg">{{ item.customer.name }}</h3>
<p class="text-sm text-gray-400">{{ item.customer.company_name or 'Kein Unternehmen' }}</p>
</div>
</div>
<div class="flex items-center gap-4">
<span class="px-4 py-1.5 bg-brand-bg rounded-full text-sm text-gray-300 font-medium">
{{ item.post_count }} Post{{ 's' if item.post_count != 1 else '' }}
</span>
</div>
</div>
</div>
<!-- Posts Grid -->
<div class="p-4">
<div class="grid gap-3">
{% for post in item.posts %}
<a href="/admin/posts/{{ post.id }}" class="post-card rounded-xl p-4 block group">
<div class="flex items-center gap-4">
<!-- Score Circle -->
{% if post.critic_feedback and post.critic_feedback | length > 0 %}
{% set score = post.critic_feedback[-1].overall_score %}
<div class="score-ring flex-shrink-0 {{ 'score-high' if score >= 85 else 'score-medium' if score >= 70 else 'score-low' }}">
{{ score }}
</div>
{% else %}
<div class="score-ring flex-shrink-0 bg-brand-bg-dark border-2 border-brand-bg-light text-gray-500">
</div>
{% endif %}
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-3">
<h4 class="font-medium text-white group-hover:text-brand-highlight transition-colors truncate">
{{ post.topic_title or 'Untitled' }}
</h4>
<span class="flex-shrink-0 px-2 py-0.5 text-xs rounded font-medium {{ 'bg-green-600/20 text-green-400 border border-green-600/30' if post.status == 'approved' else 'bg-yellow-600/20 text-yellow-400 border border-yellow-600/30' }}">
{{ post.status | capitalize }}
</span>
</div>
<div class="flex items-center gap-4 mt-1.5 text-sm text-gray-500">
<span class="flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}
</span>
<span class="flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
{{ post.created_at.strftime('%H:%M') if post.created_at else '' }}
</span>
<span class="flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
{{ post.iterations }} Iteration{{ 's' if post.iterations != 1 else '' }}
</span>
</div>
</div>
<!-- Arrow -->
<svg class="w-5 h-5 text-gray-600 group-hover:text-brand-highlight transition-colors flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</a>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="card-bg rounded-xl border p-12 text-center">
<div class="w-20 h-20 bg-brand-bg rounded-2xl flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">Noch keine Posts</h3>
<p class="text-gray-400 mb-6 max-w-md mx-auto">Erstelle deinen ersten LinkedIn Post mit KI-Unterstützung.</p>
<a href="/admin/create" class="inline-flex items-center gap-2 px-6 py-3 btn-primary font-medium rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
Post erstellen
</a>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,215 @@
{% extends "base.html" %}
{% block title %}Research Topics - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Research Topics</h1>
<p class="text-gray-400">Recherchiere neue Content-Themen mit Perplexity AI</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left: Form -->
<div>
<form id="researchForm" class="card-bg rounded-xl border p-6">
<div class="mb-6">
<label class="block text-sm font-medium text-gray-300 mb-2">Kunde auswählen</label>
<select name="customer_id" id="customerSelect" required class="w-full input-bg border rounded-lg px-4 py-3 text-white">
<option value="">-- Kunde wählen --</option>
{% for customer in customers %}
<option value="{{ customer.id }}">{{ customer.name }} - {{ customer.company_name or 'Kein Unternehmen' }}</option>
{% endfor %}
</select>
</div>
<!-- Post Type Selection -->
<div id="postTypeArea" class="mb-6 hidden">
<label class="block text-sm font-medium text-gray-300 mb-2">Post-Typ (optional)</label>
<div id="postTypeCards" class="grid grid-cols-2 gap-2 mb-2">
<!-- Post type cards will be loaded here -->
</div>
<p class="text-xs text-gray-500">Wähle einen Post-Typ für gezielte Recherche oder lasse leer für allgemeine Recherche.</p>
<input type="hidden" name="post_type_id" id="selectedPostTypeId" value="">
</div>
<!-- Progress Area -->
<div id="progressArea" class="hidden mb-6">
<div class="bg-brand-bg rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span id="progressMessage" class="text-gray-300">Starte Recherche...</span>
<span id="progressPercent" class="text-gray-400">0%</span>
</div>
<div class="w-full bg-brand-bg-dark rounded-full h-2">
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</div>
<button type="submit" id="submitBtn" class="w-full btn-primary font-medium py-3 rounded-lg transition-colors flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
Research starten
</button>
</form>
{% if not customers %}
<div class="mt-4 bg-yellow-900/30 border border-yellow-600 rounded-lg p-4">
<p class="text-yellow-300">Noch keine Kunden vorhanden. <a href="/admin/customers/new" class="underline">Erstelle zuerst einen Kunden</a>.</p>
</div>
{% endif %}
</div>
<!-- Right: Results -->
<div>
<div id="resultsArea" class="card-bg rounded-xl border p-6">
<h3 class="text-lg font-semibold text-white mb-4">Gefundene Topics</h3>
<div id="topicsList" class="space-y-4">
<p class="text-gray-400">Starte eine Recherche um Topics zu finden...</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const form = document.getElementById('researchForm');
const submitBtn = document.getElementById('submitBtn');
const progressArea = document.getElementById('progressArea');
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const progressPercent = document.getElementById('progressPercent');
const topicsList = document.getElementById('topicsList');
const customerSelect = document.getElementById('customerSelect');
const postTypeArea = document.getElementById('postTypeArea');
const postTypeCards = document.getElementById('postTypeCards');
const selectedPostTypeId = document.getElementById('selectedPostTypeId');
let currentPostTypes = [];
// Load post types when customer is selected
customerSelect.addEventListener('change', async () => {
const customerId = customerSelect.value;
selectedPostTypeId.value = '';
if (!customerId) {
postTypeArea.classList.add('hidden');
return;
}
try {
const response = await fetch(`/admin/api/customers/${customerId}/post-types`);
const data = await response.json();
if (data.post_types && data.post_types.length > 0) {
currentPostTypes = data.post_types;
postTypeArea.classList.remove('hidden');
postTypeCards.innerHTML = `
<button type="button" onclick="selectPostType('')" id="pt_all"
class="p-3 rounded-lg border text-left transition-colors bg-brand-highlight/20 border-brand-highlight text-white">
<div class="font-medium text-sm">Alle Typen</div>
<div class="text-xs text-gray-400 mt-1">Allgemeine Recherche</div>
</button>
` + data.post_types.map(pt => `
<button type="button" onclick="selectPostType('${pt.id}')" id="pt_${pt.id}"
class="p-3 rounded-lg border text-left transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
<div class="font-medium text-sm">${pt.name}</div>
<div class="text-xs text-gray-400 mt-1">${pt.analyzed_post_count || 0} Posts analysiert</div>
${pt.has_analysis ? '<span class="text-xs text-green-400">Analyse</span>' : ''}
</button>
`).join('');
} else {
postTypeArea.classList.add('hidden');
}
} catch (error) {
console.error('Failed to load post types:', error);
postTypeArea.classList.add('hidden');
}
});
function selectPostType(typeId) {
selectedPostTypeId.value = typeId;
// Update card styles
document.querySelectorAll('[id^="pt_"]').forEach(card => {
if (card.id === `pt_${typeId}` || (typeId === '' && card.id === 'pt_all')) {
card.className = 'p-3 rounded-lg border text-left transition-colors bg-brand-highlight/20 border-brand-highlight text-white';
} else {
card.className = 'p-3 rounded-lg border text-left transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white';
}
});
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const customerId = customerSelect.value;
if (!customerId) return;
submitBtn.disabled = true;
submitBtn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Recherchiert...';
progressArea.classList.remove('hidden');
const formData = new FormData();
formData.append('customer_id', customerId);
if (selectedPostTypeId.value) {
formData.append('post_type_id', selectedPostTypeId.value);
}
try {
const response = await fetch('/admin/api/research', {
method: 'POST',
body: formData
});
const data = await response.json();
const taskId = data.task_id;
const pollInterval = setInterval(async () => {
const statusResponse = await fetch(`/admin/api/tasks/${taskId}`);
const status = await statusResponse.json();
progressBar.style.width = `${status.progress}%`;
progressPercent.textContent = `${status.progress}%`;
progressMessage.textContent = status.message;
if (status.status === 'completed') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
// Display topics
if (status.topics && status.topics.length > 0) {
topicsList.innerHTML = status.topics.map((topic, i) => `
<div class="bg-brand-bg rounded-lg p-4 border border-brand-bg-light">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<span class="inline-block px-2 py-1 text-xs font-medium bg-brand-highlight/20 text-brand-highlight rounded mb-2">${topic.category || 'Topic'}</span>
<h4 class="font-semibold text-white">${topic.title}</h4>
${topic.angle ? `<p class="text-sm text-brand-highlight/80 mt-1">↳ ${topic.angle}</p>` : ''}
${topic.hook_idea ? `<p class="text-sm text-gray-300 mt-2 italic border-l-2 border-brand-highlight/30 pl-2">"${topic.hook_idea.substring(0, 150)}..."</p>` : ''}
<p class="text-gray-400 text-sm mt-2">${topic.fact ? topic.fact.substring(0, 200) + '...' : ''}</p>
${topic.source ? `<p class="text-gray-500 text-xs mt-2">Quelle: ${topic.source}</p>` : ''}
</div>
</div>
</div>
`).join('');
} else {
topicsList.innerHTML = '<p class="text-gray-400">Keine Topics gefunden.</p>';
}
} else if (status.status === 'error') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
topicsList.innerHTML = `<p class="text-red-400">Fehler: ${status.message}</p>`;
}
}, 1000);
} catch (error) {
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
topicsList.innerHTML = `<p class="text-red-400">Fehler: ${error.message}</p>`;
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,571 @@
{% extends "base.html" %}
{% block title %}Gescrapte Posts - LinkedIn Posts{% endblock %}
{% block head %}
<style>
.post-card {
background: linear-gradient(135deg, rgba(61, 72, 72, 0.3) 0%, rgba(45, 56, 56, 0.4) 100%);
border: 1px solid rgba(61, 72, 72, 0.6);
transition: all 0.2s ease;
}
.post-card:hover {
border-color: rgba(255, 199, 0, 0.3);
}
.post-card.selected {
border-color: rgba(255, 199, 0, 0.6);
background: linear-gradient(135deg, rgba(255, 199, 0, 0.05) 0%, rgba(45, 56, 56, 0.4) 100%);
}
.type-badge {
transition: all 0.15s ease;
}
.type-badge:hover {
transform: scale(1.02);
}
.type-badge.active {
background-color: rgba(255, 199, 0, 0.2);
border-color: #ffc700;
}
.post-content-preview {
max-height: 150px;
overflow: hidden;
position: relative;
}
.post-content-preview::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: linear-gradient(transparent, rgba(45, 56, 56, 0.9));
}
.post-content-expanded {
max-height: none;
}
.post-content-expanded::after {
display: none;
}
</style>
{% endblock %}
{% block content %}
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-white mb-2">Gescrapte Posts verwalten</h1>
<p class="text-gray-400">Posts manuell kategorisieren und Post-Typ-Analyse triggern</p>
</div>
</div>
</div>
<!-- Customer Selection -->
<div class="card-bg rounded-xl border p-6 mb-6">
<div class="flex flex-wrap items-end gap-4">
<div class="flex-1 min-w-64">
<label class="block text-sm font-medium text-gray-300 mb-2">Kunde auswählen</label>
<select id="customerSelect" class="w-full input-bg border rounded-lg px-4 py-3 text-white">
<option value="">-- Kunde wählen --</option>
{% for customer in customers %}
<option value="{{ customer.id }}">{{ customer.name }} - {{ customer.company_name or 'Kein Unternehmen' }}</option>
{% endfor %}
</select>
</div>
<div class="flex gap-2">
<button id="classifyAllBtn" class="hidden px-4 py-3 bg-brand-bg hover:bg-brand-bg-light border border-brand-bg-light rounded-lg text-white transition-colors">
<span class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Auto-Klassifizieren
</span>
</button>
<button id="analyzeTypesBtn" class="hidden px-4 py-3 btn-primary rounded-lg font-medium transition-colors">
<span class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Post-Typen analysieren
</span>
</button>
</div>
</div>
</div>
<!-- Progress Area -->
<div id="progressArea" class="hidden card-bg rounded-xl border p-6 mb-6">
<div class="flex items-center justify-between mb-2">
<span id="progressMessage" class="text-gray-300">Arbeite...</span>
<span id="progressPercent" class="text-gray-400">0%</span>
</div>
<div class="w-full bg-brand-bg-dark rounded-full h-2">
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<!-- Stats & Post Types -->
<div id="statsArea" class="hidden mb-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
<div class="card-bg rounded-xl border p-4">
<div class="text-2xl font-bold text-white" id="totalPostsCount">0</div>
<div class="text-sm text-gray-400">Gesamt Posts</div>
</div>
<div class="card-bg rounded-xl border p-4">
<div class="text-2xl font-bold text-green-400" id="classifiedCount">0</div>
<div class="text-sm text-gray-400">Klassifiziert</div>
</div>
<div class="card-bg rounded-xl border p-4">
<div class="text-2xl font-bold text-yellow-400" id="unclassifiedCount">0</div>
<div class="text-sm text-gray-400">Nicht klassifiziert</div>
</div>
<div class="card-bg rounded-xl border p-4">
<div class="text-2xl font-bold text-brand-highlight" id="postTypesCount">0</div>
<div class="text-sm text-gray-400">Post-Typen</div>
</div>
</div>
<!-- Post Type Filter -->
<div class="flex flex-wrap gap-2 mb-4">
<button onclick="filterByType(null)" id="filter_all" class="type-badge px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-highlight/20 border-brand-highlight text-white">
Alle
</button>
<button onclick="filterByType('unclassified')" id="filter_unclassified" class="type-badge px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
Nicht klassifiziert
</button>
<div id="postTypeFilters" class="contents">
<!-- Post type filter buttons will be added here -->
</div>
</div>
</div>
<!-- Posts List -->
<div id="postsArea" class="hidden">
<div id="postsList" class="space-y-4">
<p class="text-gray-400">Lade Posts...</p>
</div>
</div>
<!-- Empty State -->
<div id="emptyState" class="hidden card-bg rounded-xl border p-12 text-center">
<div class="w-20 h-20 bg-brand-bg rounded-2xl flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">Keine gescrapten Posts</h3>
<p class="text-gray-400 mb-6 max-w-md mx-auto">Für diesen Kunden wurden noch keine LinkedIn Posts gescrapet.</p>
</div>
{% if not customers %}
<div class="bg-yellow-900/30 border border-yellow-600 rounded-lg p-4">
<p class="text-yellow-300">Noch keine Kunden vorhanden. <a href="/admin/customers/new" class="underline">Erstelle zuerst einen Kunden</a>.</p>
</div>
{% endif %}
<!-- Post Detail Modal -->
<div id="postModal" class="fixed inset-0 bg-black/70 hidden items-center justify-center z-50 p-4">
<div class="bg-brand-bg-dark rounded-xl border border-brand-bg-light max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col shadow-2xl">
<div class="p-4 border-b border-brand-bg-light flex items-center justify-between bg-brand-bg">
<h3 class="text-lg font-semibold text-white">Post Details</h3>
<button onclick="closeModal()" class="text-gray-400 hover:text-white p-1 hover:bg-brand-bg-light rounded">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1">
<div id="modalContent"></div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const customerSelect = document.getElementById('customerSelect');
const classifyAllBtn = document.getElementById('classifyAllBtn');
const analyzeTypesBtn = document.getElementById('analyzeTypesBtn');
const progressArea = document.getElementById('progressArea');
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const progressPercent = document.getElementById('progressPercent');
const statsArea = document.getElementById('statsArea');
const postsArea = document.getElementById('postsArea');
const postsList = document.getElementById('postsList');
const emptyState = document.getElementById('emptyState');
const postTypeFilters = document.getElementById('postTypeFilters');
const postModal = document.getElementById('postModal');
const modalContent = document.getElementById('modalContent');
let currentPosts = [];
let currentPostTypes = [];
let currentFilter = null;
customerSelect.addEventListener('change', async () => {
const customerId = customerSelect.value;
if (!customerId) {
statsArea.classList.add('hidden');
postsArea.classList.add('hidden');
emptyState.classList.add('hidden');
classifyAllBtn.classList.add('hidden');
analyzeTypesBtn.classList.add('hidden');
return;
}
await loadCustomerData(customerId);
});
async function loadCustomerData(customerId) {
// Load post types
try {
const ptResponse = await fetch(`/admin/api/customers/${customerId}/post-types`);
const ptData = await ptResponse.json();
currentPostTypes = ptData.post_types || [];
// Update post type filters
postTypeFilters.innerHTML = currentPostTypes.map(pt => `
<button onclick="filterByType('${pt.id}')" id="filter_${pt.id}"
class="type-badge px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
${escapeHtml(pt.name)}
<span class="ml-1 text-xs text-gray-400">(${pt.analyzed_post_count || 0})</span>
${pt.has_analysis ? '<span class="ml-1 text-green-400">*</span>' : ''}
</button>
`).join('');
document.getElementById('postTypesCount').textContent = currentPostTypes.length;
} catch (error) {
console.error('Failed to load post types:', error);
}
// Load posts
try {
const response = await fetch(`/admin/api/customers/${customerId}/linkedin-posts`);
const data = await response.json();
console.log('API Response:', data);
if (data.error) {
console.error('API Error:', data.error);
postsList.innerHTML = `<p class="text-red-400">API Fehler: ${escapeHtml(data.error)}</p>`;
postsArea.classList.remove('hidden');
return;
}
currentPosts = data.posts || [];
console.log(`Loaded ${currentPosts.length} posts`);
if (currentPosts.length === 0) {
statsArea.classList.add('hidden');
postsArea.classList.add('hidden');
emptyState.classList.remove('hidden');
classifyAllBtn.classList.add('hidden');
analyzeTypesBtn.classList.add('hidden');
return;
}
// Update stats
const classified = currentPosts.filter(p => p.post_type_id).length;
const unclassified = currentPosts.length - classified;
document.getElementById('totalPostsCount').textContent = currentPosts.length;
document.getElementById('classifiedCount').textContent = classified;
document.getElementById('unclassifiedCount').textContent = unclassified;
statsArea.classList.remove('hidden');
postsArea.classList.remove('hidden');
emptyState.classList.add('hidden');
classifyAllBtn.classList.remove('hidden');
analyzeTypesBtn.classList.remove('hidden');
currentFilter = null;
filterByType(null);
} catch (error) {
console.error('Failed to load posts:', error);
postsList.innerHTML = `<p class="text-red-400">Fehler beim Laden: ${error.message}</p>`;
}
}
function filterByType(typeId) {
currentFilter = typeId;
// Update filter button styles
document.querySelectorAll('.type-badge').forEach(btn => {
const btnId = btn.id.replace('filter_', '');
const isActive = (typeId === null && btnId === 'all') ||
(typeId === 'unclassified' && btnId === 'unclassified') ||
(btnId === typeId);
if (isActive) {
btn.classList.add('active', 'bg-brand-highlight/20', 'border-brand-highlight');
btn.classList.remove('bg-brand-bg', 'border-brand-bg-light');
} else {
btn.classList.remove('active', 'bg-brand-highlight/20', 'border-brand-highlight');
btn.classList.add('bg-brand-bg', 'border-brand-bg-light');
}
});
// Filter posts
let filteredPosts = currentPosts;
if (typeId === 'unclassified') {
filteredPosts = currentPosts.filter(p => !p.post_type_id);
} else if (typeId) {
filteredPosts = currentPosts.filter(p => p.post_type_id === typeId);
}
renderPosts(filteredPosts);
}
function renderPosts(posts) {
if (posts.length === 0) {
postsList.innerHTML = '<p class="text-gray-400 text-center py-8">Keine Posts in dieser Kategorie.</p>';
return;
}
postsList.innerHTML = posts.map((post, index) => {
const postType = currentPostTypes.find(pt => pt.id === post.post_type_id);
const postText = post.post_text || '';
const previewText = postText.substring(0, 300);
return `
<div class="post-card rounded-xl p-4 cursor-pointer" data-post-id="${post.id}" onclick="openPostModal('${post.id}')">
<div class="flex items-start gap-4">
<div class="flex-1 min-w-0">
<!-- Header -->
<div class="flex items-center gap-2 mb-3 flex-wrap">
${postType ? `
<span class="px-2 py-1 text-xs font-medium bg-brand-highlight/20 text-brand-highlight rounded">
${escapeHtml(postType.name)}
</span>
<span class="text-xs text-gray-500">${post.classification_method || 'unknown'} (${Math.round((post.classification_confidence || 0) * 100)}%)</span>
` : `
<span class="px-2 py-1 text-xs font-medium bg-gray-600/30 text-gray-400 rounded">
Nicht klassifiziert
</span>
`}
${post.posted_at ? `
<span class="text-xs text-gray-500">${new Date(post.posted_at).toLocaleDateString('de-DE')}</span>
` : ''}
${post.engagement_score ? `
<span class="text-xs text-gray-500">Engagement: ${post.engagement_score}</span>
` : ''}
</div>
<!-- Content Preview -->
<div class="post-content-preview text-gray-300 text-sm whitespace-pre-wrap mb-3">
${escapeHtml(previewText)}${postText.length > 300 ? '...' : ''}
</div>
<!-- Click hint & Type badges -->
<div class="flex items-center justify-between flex-wrap gap-2" onclick="event.stopPropagation()">
<span class="text-xs text-gray-500 italic">Klicken für Vollansicht</span>
<div class="flex items-center gap-2 flex-wrap">
${currentPostTypes.map(pt => `
<button onclick="event.stopPropagation(); classifyPost('${post.id}', '${pt.id}')"
class="px-2 py-1 text-xs rounded transition-colors ${post.post_type_id === pt.id ? 'bg-brand-highlight/30 text-brand-highlight border border-brand-highlight' : 'bg-brand-bg hover:bg-brand-bg-light text-gray-300 border border-brand-bg-light'}">
${escapeHtml(pt.name)}
</button>
`).join('')}
${post.post_type_id ? `
<button onclick="event.stopPropagation(); classifyPost('${post.id}', null)" class="px-2 py-1 text-xs rounded bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-900/50">
</button>
` : ''}
</div>
</div>
</div>
</div>
</div>
`;
}).join('');
}
async function classifyPost(postId, postTypeId) {
const customerId = customerSelect.value;
if (!customerId) return;
try {
const response = await fetch(`/admin/api/linkedin-posts/${postId}/classify`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ post_type_id: postTypeId })
});
if (!response.ok) {
throw new Error('Failed to classify post');
}
// Update local data
const post = currentPosts.find(p => p.id === postId);
if (post) {
post.post_type_id = postTypeId;
post.classification_method = 'manual';
post.classification_confidence = 1.0;
}
// Update stats
const classified = currentPosts.filter(p => p.post_type_id).length;
document.getElementById('classifiedCount').textContent = classified;
document.getElementById('unclassifiedCount').textContent = currentPosts.length - classified;
// Re-render
filterByType(currentFilter);
} catch (error) {
console.error('Failed to classify post:', error);
alert('Fehler beim Klassifizieren: ' + error.message);
}
}
function openPostModal(postId) {
const post = currentPosts.find(p => p.id === postId);
if (!post) return;
const postType = currentPostTypes.find(pt => pt.id === post.post_type_id);
modalContent.innerHTML = `
<div class="mb-4 flex items-center gap-3 flex-wrap">
${postType ? `
<span class="px-3 py-1.5 text-sm font-medium bg-brand-highlight/20 text-brand-highlight rounded-lg">
${escapeHtml(postType.name)}
</span>
` : `
<span class="px-3 py-1.5 text-sm font-medium bg-gray-600/30 text-gray-400 rounded-lg">
Nicht klassifiziert
</span>
`}
${post.posted_at ? `
<span class="text-sm text-gray-500">${new Date(post.posted_at).toLocaleDateString('de-DE')}</span>
` : ''}
${post.engagement_score ? `
<span class="text-sm text-gray-500">Engagement: ${post.engagement_score}</span>
` : ''}
</div>
<div class="bg-brand-bg rounded-xl p-6 mb-6 border border-brand-bg-light max-h-[50vh] overflow-y-auto">
<div class="whitespace-pre-wrap text-gray-200 font-sans text-base leading-relaxed">${escapeHtml(post.post_text || '')}</div>
</div>
<div class="flex items-center gap-3 flex-wrap border-t border-brand-bg-light pt-4">
<span class="text-sm text-gray-400 font-medium">Typ zuweisen:</span>
${currentPostTypes.map(pt => `
<button onclick="classifyPost('${post.id}', '${pt.id}'); closeModal();"
class="px-4 py-2 text-sm rounded-lg transition-colors ${post.post_type_id === pt.id ? 'bg-brand-highlight text-brand-bg-dark font-medium' : 'bg-brand-bg hover:bg-brand-bg-light text-gray-300 border border-brand-bg-light'}">
${escapeHtml(pt.name)}
</button>
`).join('')}
${post.post_type_id ? `
<button onclick="classifyPost('${post.id}', null); closeModal();" class="px-4 py-2 text-sm rounded-lg bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-900/50">
Klassifizierung entfernen
</button>
` : ''}
</div>
`;
postModal.classList.remove('hidden');
postModal.classList.add('flex');
}
function closeModal() {
postModal.classList.add('hidden');
postModal.classList.remove('flex');
}
// Close modal on backdrop click
postModal.addEventListener('click', (e) => {
if (e.target === postModal) {
closeModal();
}
});
// Close modal on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !postModal.classList.contains('hidden')) {
closeModal();
}
});
// Auto-classify button
classifyAllBtn.addEventListener('click', async () => {
const customerId = customerSelect.value;
if (!customerId) return;
classifyAllBtn.disabled = true;
progressArea.classList.remove('hidden');
try {
const response = await fetch(`/admin/api/customers/${customerId}/classify-posts`, {
method: 'POST'
});
const data = await response.json();
const taskId = data.task_id;
await pollTask(taskId, async () => {
await loadCustomerData(customerId);
});
} catch (error) {
console.error('Classification failed:', error);
alert('Fehler bei der Klassifizierung: ' + error.message);
} finally {
classifyAllBtn.disabled = false;
progressArea.classList.add('hidden');
}
});
// Analyze post types button
analyzeTypesBtn.addEventListener('click', async () => {
const customerId = customerSelect.value;
if (!customerId) return;
analyzeTypesBtn.disabled = true;
progressArea.classList.remove('hidden');
try {
const response = await fetch(`/admin/api/customers/${customerId}/analyze-post-types`, {
method: 'POST'
});
const data = await response.json();
const taskId = data.task_id;
await pollTask(taskId, async () => {
await loadCustomerData(customerId);
alert('Post-Typ-Analyse abgeschlossen! Die Analysen wurden aktualisiert.');
});
} catch (error) {
console.error('Analysis failed:', error);
alert('Fehler bei der Analyse: ' + error.message);
} finally {
analyzeTypesBtn.disabled = false;
progressArea.classList.add('hidden');
}
});
async function pollTask(taskId, onComplete) {
return new Promise((resolve) => {
const interval = setInterval(async () => {
try {
const statusResponse = await fetch(`/admin/api/tasks/${taskId}`);
const status = await statusResponse.json();
progressBar.style.width = `${status.progress}%`;
progressPercent.textContent = `${status.progress}%`;
progressMessage.textContent = status.message;
if (status.status === 'completed') {
clearInterval(interval);
await onComplete();
resolve();
} else if (status.status === 'error') {
clearInterval(interval);
alert('Fehler: ' + status.message);
resolve();
}
} catch (error) {
clearInterval(interval);
console.error('Polling error:', error);
resolve();
}
}, 1000);
});
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}

View File

@@ -0,0 +1,159 @@
{% extends "base.html" %}
{% block title %}Status - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Status</h1>
<p class="text-gray-400">Übersicht über alle Kunden und deren Setup-Status</p>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
<strong>Error:</strong> {{ error }}
</div>
{% endif %}
{% if customer_statuses %}
<div class="grid gap-6">
{% for item in customer_statuses %}
<div class="card-bg rounded-xl border overflow-hidden">
<!-- Customer Header -->
<div class="px-6 py-4 border-b border-brand-bg-light">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-full flex items-center justify-center overflow-hidden {{ 'bg-brand-highlight' if not item.profile_picture else '' }}">
{% if item.profile_picture %}
<img src="{{ item.profile_picture }}" alt="{{ item.customer.name }}" class="w-full h-full object-cover" loading="lazy" referrerpolicy="no-referrer">
{% else %}
<span class="text-brand-bg-dark font-bold text-lg">{{ item.customer.name[0] | upper }}</span>
{% endif %}
</div>
<div>
<h3 class="font-semibold text-white text-lg">{{ item.customer.name }}</h3>
<p class="text-sm text-gray-400">{{ item.customer.company_name or 'Kein Unternehmen' }}</p>
</div>
</div>
<div class="flex items-center gap-2">
{% if item.status.ready_for_posts %}
<span class="px-3 py-1.5 bg-green-600/30 text-green-300 rounded-lg text-sm font-medium flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Bereit für Posts
</span>
{% else %}
<span class="px-3 py-1.5 bg-yellow-600/30 text-yellow-300 rounded-lg text-sm font-medium flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
Setup unvollständig
</span>
{% endif %}
</div>
</div>
</div>
<!-- Status Grid -->
<div class="p-6">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<!-- Scraped Posts -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
{% if item.status.has_scraped_posts %}
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
{% else %}
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
{% endif %}
<span class="text-sm text-gray-400">Scraped Posts</span>
</div>
<p class="text-2xl font-bold text-white">{{ item.status.scraped_posts_count }}</p>
</div>
<!-- Profile Analysis -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
{% if item.status.has_profile_analysis %}
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
{% else %}
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
{% endif %}
<span class="text-sm text-gray-400">Profil Analyse</span>
</div>
<p class="text-lg font-semibold text-white">{{ 'Vorhanden' if item.status.has_profile_analysis else 'Fehlt' }}</p>
</div>
<!-- Research Topics -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
{% if item.status.research_count > 0 %}
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
{% else %}
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
{% endif %}
<span class="text-sm text-gray-400">Research Topics</span>
</div>
<p class="text-2xl font-bold text-white">{{ item.status.research_count }}</p>
</div>
<!-- Generated Posts -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
<span class="text-sm text-gray-400">Generierte Posts</span>
</div>
<p class="text-2xl font-bold text-white">{{ item.status.posts_count }}</p>
</div>
</div>
<!-- Missing Items -->
{% if item.status.missing_items %}
<div class="bg-yellow-900/20 border border-yellow-600/50 rounded-lg p-4">
<h4 class="font-medium text-yellow-300 mb-2 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
Fehlende Elemente
</h4>
<ul class="space-y-1">
{% for item_missing in item.status.missing_items %}
<li class="text-yellow-200/80 text-sm flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
{{ item_missing }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Quick Actions -->
<div class="flex flex-wrap gap-3 mt-4">
{% if not item.status.has_profile_analysis %}
<a href="/admin/customers/new" class="px-4 py-2 btn-primary rounded-lg text-sm transition-colors">
Setup wiederholen
</a>
{% endif %}
{% if item.status.research_count == 0 %}
<a href="/admin/research" class="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg text-sm text-white transition-colors">
Recherche starten
</a>
{% endif %}
{% if item.status.ready_for_posts %}
<a href="/admin/create" class="px-4 py-2 btn-primary rounded-lg text-sm transition-colors">
Post erstellen
</a>
{% endif %}
<a href="/admin/impersonate/{{ item.customer.id }}" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg text-sm text-white transition-colors flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
Als User einloggen
</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="card-bg rounded-xl border p-12 text-center">
<svg class="w-16 h-16 text-gray-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
<h3 class="text-xl font-semibold text-white mb-2">Noch keine Kunden</h3>
<p class="text-gray-400 mb-6">Erstelle deinen ersten Kunden, um den Status zu sehen.</p>
<a href="/admin/customers/new" class="inline-flex items-center gap-2 px-6 py-3 btn-primary font-medium rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
Neuer Kunde
</a>
</div>
{% endif %}
{% endblock %}

103
src/web/templates/base.html Normal file
View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}LinkedIn Posts{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'brand': {
'bg': '#3d4848',
'bg-light': '#4a5858',
'bg-dark': '#2d3838',
'highlight': '#ffc700',
'highlight-dark': '#e6b300',
}
}
}
}
}
</script>
<style>
body { background-color: #3d4848; }
.nav-link.active { background-color: #ffc700; color: #2d3838; }
.nav-link.active svg { stroke: #2d3838; }
.post-content { white-space: pre-wrap; word-wrap: break-word; }
.btn-primary { background-color: #ffc700; color: #2d3838; }
.btn-primary:hover { background-color: #e6b300; }
.sidebar-bg { background-color: #2d3838; }
.card-bg { background-color: #4a5858; border-color: #5a6868; }
.input-bg { background-color: #3d4848; border-color: #5a6868; }
.input-bg:focus { border-color: #ffc700; outline: none; }
/* Scrollbar styling */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #3d4848; }
::-webkit-scrollbar-thumb { background: #5a6868; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #6a7878; }
</style>
{% block head %}{% endblock %}
</head>
<body class="text-gray-100 min-h-screen flex">
<!-- Sidebar -->
<aside class="w-64 sidebar-bg border-r border-gray-600 flex flex-col fixed h-full">
<div class="p-4 border-b border-gray-600">
<div class="flex items-center justify-center gap-3">
<div>
<img src="/static/logo.png" alt="Logo" class="h-15 w-auto">
</div>
</div>
</div>
<nav class="flex-1 p-4 space-y-2">
<a href="/" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'home' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
Dashboard
</a>
<a href="/customers/new" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'new_customer' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/></svg>
Neuer Kunde
</a>
<a href="/research" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'research' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
Research Topics
</a>
<a href="/create" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'create' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Post erstellen
</a>
<a href="/posts" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'posts' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
Alle Posts
</a>
<a href="/scraped-posts" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'scraped_posts' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>
Post-Typen
</a>
<a href="/status" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'status' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Status
</a>
</nav>
<div class="p-4 border-t border-gray-600">
<a href="/logout" class="flex items-center gap-2 text-gray-400 hover:text-gray-200 text-sm transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
Logout
</a>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 ml-64">
<div class="p-8">
{% block content %}{% endblock %}
</div>
</main>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,539 @@
{% extends "base.html" %}
{% block title %}Post erstellen - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Post erstellen</h1>
<p class="text-gray-400">Generiere einen neuen LinkedIn Post mit AI</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left: Form -->
<div>
<form id="createPostForm" class="card-bg rounded-xl border p-6 space-y-6">
<!-- Customer Selection -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Kunde auswählen</label>
<select name="customer_id" id="customerSelect" required class="w-full input-bg border rounded-lg px-4 py-3 text-white">
<option value="">-- Kunde wählen --</option>
{% for customer in customers %}
<option value="{{ customer.id }}">{{ customer.name }} - {{ customer.company_name or 'Kein Unternehmen' }}</option>
{% endfor %}
</select>
</div>
<!-- Post Type Selection -->
<div id="postTypeSelectionArea" class="hidden">
<label class="block text-sm font-medium text-gray-300 mb-2">Post-Typ auswählen (optional)</label>
<div id="postTypeCards" class="flex flex-wrap gap-2 mb-2">
<!-- Post type cards will be loaded here -->
</div>
<input type="hidden" id="selectedPostTypeId" value="">
</div>
<!-- Topic Selection -->
<div id="topicSelectionArea" class="hidden">
<label class="block text-sm font-medium text-gray-300 mb-2">Topic auswählen</label>
<div id="topicsList" class="space-y-2 max-h-64 overflow-y-auto">
<p class="text-gray-500">Lade Topics...</p>
</div>
</div>
<!-- Custom Topic -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">
<span>Oder eigenes Topic eingeben</span>
</label>
<div class="space-y-3">
<input type="text" id="customTopicTitle" placeholder="Topic Titel" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
<textarea id="customTopicFact" rows="3" placeholder="Fakt / Kernaussage zum Topic..." class="w-full input-bg border rounded-lg px-4 py-2 text-white"></textarea>
<input type="text" id="customTopicSource" placeholder="Quelle (optional)" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
</div>
<!-- Progress Area -->
<div id="progressArea" class="hidden">
<div class="bg-brand-bg rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span id="progressMessage" class="text-gray-300">Starte Post-Erstellung...</span>
<span id="progressPercent" class="text-gray-400">0%</span>
</div>
<div class="w-full bg-brand-bg-dark rounded-full h-2">
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div id="iterationInfo" class="mt-2 text-sm text-gray-400"></div>
</div>
</div>
<button type="submit" id="submitBtn" class="w-full btn-primary font-medium py-3 rounded-lg transition-colors flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Post generieren
</button>
</form>
{% if not customers %}
<div class="mt-4 bg-yellow-900/30 border border-yellow-600 rounded-lg p-4">
<p class="text-yellow-300">Noch keine Kunden vorhanden. <a href="/customers/new" class="underline">Erstelle zuerst einen Kunden</a>.</p>
</div>
{% endif %}
</div>
<!-- Right: Result -->
<div>
<div id="resultArea" class="card-bg rounded-xl border p-6">
<h3 class="text-lg font-semibold text-white mb-4">Generierter Post</h3>
<!-- Live Versions Display -->
<div id="liveVersions" class="hidden space-y-4 mb-6">
<div class="flex items-center gap-2 mb-2">
<span class="text-sm text-gray-400">Live-Vorschau der Iterationen:</span>
</div>
<div id="versionsContainer" class="space-y-4"></div>
</div>
<div id="postResult">
<p class="text-gray-400">Wähle einen Kunden und ein Topic, dann klicke auf "Post generieren"...</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const form = document.getElementById('createPostForm');
const customerSelect = document.getElementById('customerSelect');
const topicSelectionArea = document.getElementById('topicSelectionArea');
const topicsList = document.getElementById('topicsList');
const submitBtn = document.getElementById('submitBtn');
const progressArea = document.getElementById('progressArea');
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const progressPercent = document.getElementById('progressPercent');
const iterationInfo = document.getElementById('iterationInfo');
const postResult = document.getElementById('postResult');
const liveVersions = document.getElementById('liveVersions');
const versionsContainer = document.getElementById('versionsContainer');
const postTypeSelectionArea = document.getElementById('postTypeSelectionArea');
const postTypeCards = document.getElementById('postTypeCards');
const selectedPostTypeIdInput = document.getElementById('selectedPostTypeId');
let selectedTopic = null;
let currentVersionIndex = 0;
let currentPostTypes = [];
let currentTopics = [];
function renderVersions(versions, feedbackList) {
if (!versions || versions.length === 0) {
liveVersions.classList.add('hidden');
return;
}
liveVersions.classList.remove('hidden');
// Build version tabs and content
let html = `
<div class="flex gap-2 mb-4 flex-wrap">
${versions.map((_, i) => `
<button onclick="showVersion(${i})" id="versionTab${i}"
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}">
V${i + 1}
${feedbackList[i] ? `<span class="ml-1 text-xs opacity-75">(${feedbackList[i].overall_score || '?'})</span>` : ''}
</button>
`).join('')}
</div>
`;
// Show current version
const currentVersion = versions[currentVersionIndex];
const currentFeedback = feedbackList[currentVersionIndex];
html += `
<div class="grid grid-cols-1 ${currentFeedback ? 'lg:grid-cols-2' : ''} gap-4">
<div class="bg-brand-bg/50 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span>
${currentFeedback ? `
<span class="px-2 py-0.5 text-xs rounded ${currentFeedback.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
${currentFeedback.approved ? 'Approved' : `Score: ${currentFeedback.overall_score}/100`}
</span>
` : '<span class="text-xs text-gray-500">Wird bewertet...</span>'}
</div>
<pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-96 overflow-y-auto">${currentVersion}</pre>
</div>
${currentFeedback ? `
<div class="bg-brand-bg/30 rounded-lg p-4 border border-brand-bg-light">
<span class="text-sm font-medium text-gray-300 block mb-2">Kritik</span>
<p class="text-sm text-gray-400 mb-3">${currentFeedback.feedback || 'Keine Kritik'}</p>
${currentFeedback.improvements && currentFeedback.improvements.length > 0 ? `
<div class="mt-2">
<span class="text-xs font-medium text-gray-400">Verbesserungen:</span>
<ul class="mt-1 space-y-1">
${currentFeedback.improvements.map(imp => `
<li class="text-xs text-gray-500 flex items-start gap-1">
<span class="text-yellow-500">•</span> ${imp}
</li>
`).join('')}
</ul>
</div>
` : ''}
${currentFeedback.scores ? `
<div class="mt-3 pt-3 border-t border-brand-bg-light">
<div class="grid grid-cols-3 gap-2 text-xs">
<div class="text-center">
<div class="text-gray-500">Authentizität</div>
<div class="font-medium text-gray-300">${currentFeedback.scores.authenticity_and_style || '?'}/40</div>
</div>
<div class="text-center">
<div class="text-gray-500">Content</div>
<div class="font-medium text-gray-300">${currentFeedback.scores.content_quality || '?'}/35</div>
</div>
<div class="text-center">
<div class="text-gray-500">Technik</div>
<div class="font-medium text-gray-300">${currentFeedback.scores.technical_execution || '?'}/25</div>
</div>
</div>
</div>
` : ''}
</div>
` : ''}
</div>
`;
versionsContainer.innerHTML = html;
}
function showVersion(index) {
currentVersionIndex = index;
// Get cached versions from progress store
const cachedData = window.lastProgressData;
if (cachedData) {
renderVersions(cachedData.versions, cachedData.feedback_list);
}
}
// Load topics and post types when customer is selected
customerSelect.addEventListener('change', async () => {
const customerId = customerSelect.value;
selectedPostTypeIdInput.value = '';
if (!customerId) {
topicSelectionArea.classList.add('hidden');
postTypeSelectionArea.classList.add('hidden');
return;
}
topicSelectionArea.classList.remove('hidden');
topicsList.innerHTML = '<p class="text-gray-500">Lade Topics...</p>';
// Load post types
try {
const ptResponse = await fetch(`/api/customers/${customerId}/post-types`);
const ptData = await ptResponse.json();
if (ptData.post_types && ptData.post_types.length > 0) {
currentPostTypes = ptData.post_types;
postTypeSelectionArea.classList.remove('hidden');
postTypeCards.innerHTML = `
<button type="button" onclick="selectPostTypeForCreate('')" id="ptc_all"
class="px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-highlight/20 border-brand-highlight text-white">
Alle Typen
</button>
` + ptData.post_types.map(pt => `
<button type="button" onclick="selectPostTypeForCreate('${pt.id}')" id="ptc_${pt.id}"
class="px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
${pt.name}
${pt.has_analysis ? '<span class="ml-1 text-green-400 text-xs">*</span>' : ''}
</button>
`).join('');
} else {
postTypeSelectionArea.classList.add('hidden');
}
} catch (error) {
console.error('Failed to load post types:', error);
postTypeSelectionArea.classList.add('hidden');
}
// Load topics
try {
const response = await fetch(`/api/customers/${customerId}/topics`);
const data = await response.json();
if (data.topics && data.topics.length > 0) {
renderTopicsList(data);
} else {
// No topics available - show helpful message
let message = '';
if (data.used_count > 0) {
message = `<div class="text-center py-4">
<p class="text-gray-400 mb-2">Alle ${data.used_count} Topics wurden bereits verwendet.</p>
<a href="/research" class="text-brand-highlight hover:underline">Neue Topics recherchieren</a>
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
</div>`;
} else {
message = `<div class="text-center py-4">
<p class="text-gray-400 mb-2">Keine Topics gefunden.</p>
<a href="/research" class="text-brand-highlight hover:underline">Recherche starten</a>
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
</div>`;
}
topicsList.innerHTML = message;
}
} catch (error) {
topicsList.innerHTML = `<p class="text-red-400">Fehler beim Laden: ${error.message}</p>`;
}
});
// Clear selected topic when custom topic is entered
['customTopicTitle', 'customTopicFact', 'customTopicSource'].forEach(id => {
document.getElementById(id).addEventListener('input', () => {
selectedTopic = null;
document.querySelectorAll('input[name="topic"]').forEach(radio => radio.checked = false);
});
});
function selectPostTypeForCreate(typeId) {
selectedPostTypeIdInput.value = typeId;
// Update card styles
document.querySelectorAll('[id^="ptc_"]').forEach(card => {
if (card.id === `ptc_${typeId}` || (typeId === '' && card.id === 'ptc_all')) {
card.className = 'px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-highlight/20 border-brand-highlight text-white';
} else {
card.className = 'px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white';
}
});
// Optionally reload topics filtered by post type
const customerId = customerSelect.value;
if (customerId) {
loadTopicsForPostType(customerId, typeId);
}
}
async function loadTopicsForPostType(customerId, postTypeId) {
topicsList.innerHTML = '<p class="text-gray-500">Lade Topics...</p>';
try {
let url = `/api/customers/${customerId}/topics`;
if (postTypeId) {
url += `?post_type_id=${postTypeId}`;
}
const response = await fetch(url);
const data = await response.json();
if (data.topics && data.topics.length > 0) {
renderTopicsList(data);
} else {
let message = '';
if (data.used_count > 0) {
message = `<div class="text-center py-4">
<p class="text-gray-400 mb-2">Alle ${data.used_count} Topics wurden bereits verwendet.</p>
<a href="/research" class="text-brand-highlight hover:underline">Neue Topics recherchieren</a>
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
</div>`;
} else {
message = `<div class="text-center py-4">
<p class="text-gray-400 mb-2">Keine Topics gefunden${postTypeId ? ' für diesen Post-Typ' : ''}.</p>
<a href="/research" class="text-brand-highlight hover:underline">Recherche starten</a>
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
</div>`;
}
topicsList.innerHTML = message;
}
} catch (error) {
topicsList.innerHTML = `<p class="text-red-400">Fehler beim Laden: ${error.message}</p>`;
}
}
function renderTopicsList(data) {
// Store topics in global array for safe access
currentTopics = data.topics;
// Reset selected topic when list is re-rendered
selectedTopic = null;
let statsHtml = '';
if (data.used_count > 0) {
statsHtml = `<p class="text-xs text-gray-500 mb-3">${data.available_count} verfügbar · ${data.used_count} bereits verwendet</p>`;
}
topicsList.innerHTML = statsHtml + data.topics.map((topic, i) => `
<label class="flex items-start gap-3 p-3 bg-brand-bg/50 rounded-lg cursor-pointer hover:bg-brand-bg transition-colors border border-transparent hover:border-brand-highlight/30">
<input type="radio" name="topic" value="${i}" class="mt-1 text-brand-highlight" data-topic-index="${i}">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1 flex-wrap">
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-brand-highlight/20 text-brand-highlight rounded">${escapeHtml(topic.category || 'Topic')}</span>
${topic.target_post_type_id ? `<span class="text-xs text-gray-500">Typ-spezifisch</span>` : ''}
${topic.source ? `<span class="text-xs text-gray-500">🔗 ${escapeHtml(topic.source.substring(0, 30))}${topic.source.length > 30 ? '...' : ''}</span>` : ''}
</div>
<p class="font-medium text-white">${escapeHtml(topic.title)}</p>
${topic.angle ? `<p class="text-xs text-brand-highlight/80 mt-1">→ ${escapeHtml(topic.angle)}</p>` : ''}
${topic.hook_idea ? `<p class="text-sm text-gray-300 mt-2 italic border-l-2 border-brand-highlight/30 pl-2">"${escapeHtml(topic.hook_idea.substring(0, 120))}${topic.hook_idea.length > 120 ? '...' : ''}"</p>` : ''}
${topic.key_facts && topic.key_facts.length > 0 ? `
<div class="mt-2 flex flex-wrap gap-1">
${topic.key_facts.slice(0, 2).map(f => `<span class="text-xs bg-brand-bg-dark px-2 py-0.5 rounded text-gray-400">📊 ${escapeHtml(f.substring(0, 40))}${f.length > 40 ? '...' : ''}</span>`).join('')}
</div>
` : (topic.fact ? `<p class="text-sm text-gray-400 mt-1">${escapeHtml(topic.fact.substring(0, 100))}...</p>` : '')}
${topic.why_this_person ? `<p class="text-xs text-gray-500 mt-2">💡 ${escapeHtml(topic.why_this_person.substring(0, 80))}${topic.why_this_person.length > 80 ? '...' : ''}</p>` : ''}
</div>
</label>
`).join('');
// Add event listeners to radio buttons
document.querySelectorAll('input[name="topic"]').forEach(radio => {
radio.addEventListener('change', () => {
const index = parseInt(radio.dataset.topicIndex, 10);
selectedTopic = currentTopics[index];
// Clear custom topic fields
document.getElementById('customTopicTitle').value = '';
document.getElementById('customTopicFact').value = '';
document.getElementById('customTopicSource').value = '';
});
});
}
// Helper function to escape HTML special characters
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const customerId = customerSelect.value;
if (!customerId) {
alert('Bitte wähle einen Kunden aus.');
return;
}
// Get topic (either selected or custom)
let topic;
const customTitle = document.getElementById('customTopicTitle').value.trim();
const customFact = document.getElementById('customTopicFact').value.trim();
if (customTitle && customFact) {
topic = {
title: customTitle,
fact: customFact,
source: document.getElementById('customTopicSource').value.trim() || null,
category: 'Custom'
};
} else if (selectedTopic) {
topic = selectedTopic;
} else {
alert('Bitte wähle ein Topic aus oder gib ein eigenes ein.');
return;
}
submitBtn.disabled = true;
submitBtn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Generiert...';
progressArea.classList.remove('hidden');
postResult.innerHTML = '<p class="text-gray-400">Post wird generiert...</p>';
const formData = new FormData();
formData.append('customer_id', customerId);
formData.append('topic_json', JSON.stringify(topic));
if (selectedPostTypeIdInput.value) {
formData.append('post_type_id', selectedPostTypeIdInput.value);
}
try {
const response = await fetch('/api/posts', {
method: 'POST',
body: formData
});
const data = await response.json();
const taskId = data.task_id;
currentVersionIndex = 0;
window.lastProgressData = null;
const pollInterval = setInterval(async () => {
const statusResponse = await fetch(`/api/tasks/${taskId}`);
const status = await statusResponse.json();
progressBar.style.width = `${status.progress}%`;
progressPercent.textContent = `${status.progress}%`;
progressMessage.textContent = status.message;
if (status.iteration !== undefined) {
iterationInfo.textContent = `Iteration ${status.iteration}/${status.max_iterations}`;
}
// Update live versions display
if (status.versions && status.versions.length > 0) {
window.lastProgressData = status;
// Auto-select latest version
if (status.versions.length > currentVersionIndex + 1) {
currentVersionIndex = status.versions.length - 1;
}
renderVersions(status.versions, status.feedback_list || []);
postResult.innerHTML = '<p class="text-gray-400">Siehe Live-Vorschau oben...</p>';
}
if (status.status === 'completed') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
// Keep live versions visible but update header
const result = status.result;
postResult.innerHTML = `
<div class="space-y-4">
<div class="flex items-center gap-2 text-sm flex-wrap">
<span class="px-2 py-1 rounded ${result.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
${result.approved ? 'Approved' : 'Review needed'}
</span>
<span class="text-gray-400">Score: ${result.final_score}/100</span>
<span class="text-gray-400">Iterations: ${result.iterations}</span>
</div>
<div class="text-sm text-gray-400 mb-2">Finaler Post:</div>
<div class="bg-brand-bg/50 rounded-lg p-4">
<pre class="whitespace-pre-wrap text-gray-200 font-sans">${result.final_post}</pre>
</div>
<div class="flex gap-2">
<button onclick="copyPost()" class="px-4 py-2 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-white transition-colors">
In Zwischenablage kopieren
</button>
<button onclick="toggleVersions()" class="px-4 py-2 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-white transition-colors">
Versionen ${liveVersions.classList.contains('hidden') ? 'anzeigen' : 'ausblenden'}
</button>
</div>
</div>
`;
} else if (status.status === 'error') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
postResult.innerHTML = `<p class="text-red-400">Fehler: ${status.message}</p>`;
}
}, 1000);
} catch (error) {
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
postResult.innerHTML = `<p class="text-red-400">Fehler: ${error.message}</p>`;
}
});
function copyPost() {
const postText = document.querySelector('#postResult pre').textContent;
navigator.clipboard.writeText(postText).then(() => {
alert('Post in Zwischenablage kopiert!');
});
}
function toggleVersions() {
liveVersions.classList.toggle('hidden');
}
</script>
{% endblock %}

View File

@@ -0,0 +1,97 @@
{% extends "base.html" %}
{% block title %}Dashboard - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Dashboard</h1>
<p class="text-gray-400">Willkommen zum LinkedIn Post Creation System</p>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
<strong>Error:</strong> {{ error }}
</div>
{% endif %}
<!-- Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="card-bg rounded-xl border p-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
</div>
<div>
<p class="text-gray-400 text-sm">Kunden</p>
<p class="text-2xl font-bold text-white">{{ customers_count or 0 }}</p>
</div>
</div>
</div>
<div class="card-bg rounded-xl border p-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-green-600/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
</div>
<div>
<p class="text-gray-400 text-sm">Generierte Posts</p>
<p class="text-2xl font-bold text-white">{{ total_posts or 0 }}</p>
</div>
</div>
</div>
<div class="card-bg rounded-xl border p-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
</div>
<div>
<p class="text-gray-400 text-sm">AI Agents</p>
<p class="text-2xl font-bold text-white">5</p>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card-bg rounded-xl border p-6">
<h2 class="text-xl font-semibold text-white mb-4">Schnellaktionen</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<a href="/customers/new" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
</div>
<div>
<p class="font-medium text-white">Neuer Kunde</p>
<p class="text-sm text-gray-400">Setup starten</p>
</div>
</a>
<a href="/research" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
</div>
<div>
<p class="font-medium text-white">Research</p>
<p class="text-sm text-gray-400">Topics finden</p>
</div>
</a>
<a href="/create" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
</div>
<div>
<p class="font-medium text-white">Post erstellen</p>
<p class="text-sm text-gray-400">Content generieren</p>
</div>
</a>
<a href="/posts" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
</div>
<div>
<p class="font-medium text-white">Alle Posts</p>
<p class="text-sm text-gray-400">Übersicht anzeigen</p>
</div>
</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - LinkedIn Posts</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'brand': {
'bg': '#3d4848',
'bg-light': '#4a5858',
'bg-dark': '#2d3838',
'highlight': '#ffc700',
'highlight-dark': '#e6b300',
}
}
}
}
}
</script>
<style>
body { background-color: #3d4848; }
.btn-primary { background-color: #ffc700; color: #2d3838; }
.btn-primary:hover { background-color: #e6b300; }
.card-bg { background-color: #4a5858; border-color: #5a6868; }
.input-bg { background-color: #3d4848; border-color: #5a6868; }
.input-bg:focus { border-color: #ffc700; outline: none; }
</style>
</head>
<body class="text-gray-100 min-h-screen flex items-center justify-center">
<div class="w-full max-w-md">
<div class="card-bg rounded-xl border p-8">
<div class="text-center mb-8">
<img src="/static/logo.png" alt="Logo" class="h-16 w-auto mx-auto mb-4">
<h1 class="text-2xl font-bold text-white mb-2">LinkedIn Posts</h1>
<p class="text-gray-400">AI Workflow System</p>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
Falsches Passwort. Bitte versuche es erneut.
</div>
{% endif %}
<form method="POST" action="/login" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Passwort</label>
<input
type="password"
name="password"
required
autofocus
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Passwort eingeben..."
>
</div>
<button
type="submit"
class="w-full btn-primary font-medium py-3 rounded-lg transition-colors"
>
Anmelden
</button>
</form>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,274 @@
{% extends "base.html" %}
{% block title %}Neuer Kunde - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Neuer Kunde</h1>
<p class="text-gray-400">Richte einen neuen Kunden ein und starte das initiale Setup</p>
</div>
<div class="max-w-2xl">
<form id="customerForm" class="card-bg rounded-xl border p-6 space-y-6">
<!-- Basic Info -->
<div>
<h3 class="text-lg font-semibold text-white mb-4">Basis-Informationen</h3>
<div class="grid gap-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Name *</label>
<input type="text" name="name" required class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">LinkedIn URL *</label>
<input type="url" name="linkedin_url" required placeholder="https://www.linkedin.com/in/username" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Firma</label>
<input type="text" name="company_name" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">E-Mail</label>
<input type="email" name="email" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
</div>
</div>
</div>
<!-- Persona -->
<div>
<h3 class="text-lg font-semibold text-white mb-4">Persona & Stil</h3>
<div class="grid gap-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Persona</label>
<textarea name="persona" rows="3" placeholder="Beschreibe die Expertise, Positionierung und den Charakter der Person..." class="w-full input-bg border rounded-lg px-4 py-2 text-white"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Ansprache</label>
<input type="text" name="form_of_address" placeholder="z.B. Duzen (Du/Euch) oder Siezen (Sie)" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Style Guide</label>
<textarea name="style_guide" rows="3" placeholder="Beschreibe den Schreibstil, Tonalität und Richtlinien..." class="w-full input-bg border rounded-lg px-4 py-2 text-white"></textarea>
</div>
</div>
</div>
<!-- Post Types -->
<div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-white">Post-Typen</h3>
<button type="button" id="addPostTypeBtn" class="text-sm text-brand-highlight hover:underline">+ Post-Typ hinzufügen</button>
</div>
<p class="text-sm text-gray-400 mb-4">Definiere verschiedene Arten von Posts (z.B. "Thought Leader", "Case Study", "How-To"). Diese werden zur Kategorisierung und typ-spezifischen Analyse verwendet.</p>
<div id="postTypesContainer" class="space-y-4">
<!-- Post type entries will be added here -->
</div>
</div>
<!-- Progress Area -->
<div id="progressArea" class="hidden">
<div class="bg-brand-bg rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span id="progressMessage" class="text-gray-300">Starte Setup...</span>
<span id="progressPercent" class="text-gray-400">0%</span>
</div>
<div class="w-full bg-brand-bg-dark rounded-full h-2">
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</div>
<!-- Result Area -->
<div id="resultArea" class="hidden">
<div id="successResult" class="hidden bg-green-900/30 border border-green-500 rounded-lg p-4">
<div class="flex items-center gap-3">
<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
<span class="text-green-300">Setup erfolgreich abgeschlossen!</span>
</div>
</div>
<div id="errorResult" class="hidden bg-red-900/30 border border-red-500 rounded-lg p-4">
<div class="flex items-center gap-3">
<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
<span id="errorMessage" class="text-red-300">Fehler beim Setup</span>
</div>
</div>
</div>
<!-- Submit -->
<div class="flex gap-4">
<button type="submit" id="submitBtn" class="flex-1 btn-primary font-medium py-3 rounded-lg transition-colors">
Setup starten
</button>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
const form = document.getElementById('customerForm');
const submitBtn = document.getElementById('submitBtn');
const progressArea = document.getElementById('progressArea');
const resultArea = document.getElementById('resultArea');
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const progressPercent = document.getElementById('progressPercent');
const postTypesContainer = document.getElementById('postTypesContainer');
const addPostTypeBtn = document.getElementById('addPostTypeBtn');
let postTypeIndex = 0;
function createPostTypeEntry() {
const index = postTypeIndex++;
const entry = document.createElement('div');
entry.className = 'bg-brand-bg rounded-lg p-4 border border-brand-bg-light';
entry.id = `postType_${index}`;
entry.innerHTML = `
<div class="flex justify-between items-start mb-3">
<span class="text-sm font-medium text-gray-300">Post-Typ ${index + 1}</span>
<button type="button" onclick="removePostType(${index})" class="text-red-400 hover:text-red-300 text-sm">Entfernen</button>
</div>
<div class="grid gap-3">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-400 mb-1">Name *</label>
<input type="text" data-pt-field="name" data-pt-index="${index}" required placeholder="z.B. Thought Leader" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Beschreibung</label>
<input type="text" data-pt-field="description" data-pt-index="${index}" placeholder="Kurze Beschreibung" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Identifizierende Hashtags (kommagetrennt)</label>
<input type="text" data-pt-field="hashtags" data-pt-index="${index}" placeholder="#ThoughtLeader, #Insight, #Leadership" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Keywords (kommagetrennt)</label>
<input type="text" data-pt-field="keywords" data-pt-index="${index}" placeholder="Erfahrung, Learnings, Meinung" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
<details class="mt-2">
<summary class="text-xs text-gray-400 cursor-pointer hover:text-gray-300">Erweiterte Eigenschaften</summary>
<div class="mt-3 grid gap-3">
<div>
<label class="block text-xs text-gray-400 mb-1">Zweck</label>
<input type="text" data-pt-field="purpose" data-pt-index="${index}" placeholder="z.B. Expertise zeigen, Meinungsführerschaft etablieren" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Typische Tonalität</label>
<input type="text" data-pt-field="tone" data-pt-index="${index}" placeholder="z.B. reflektiert, provokativ, inspirierend" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Zielgruppe</label>
<input type="text" data-pt-field="target_audience" data-pt-index="${index}" placeholder="z.B. Führungskräfte, Entscheider" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
</div>
</div>
</details>
</div>
`;
postTypesContainer.appendChild(entry);
}
function removePostType(index) {
const entry = document.getElementById(`postType_${index}`);
if (entry) {
entry.remove();
}
}
function collectPostTypes() {
const postTypes = [];
const entries = postTypesContainer.querySelectorAll('[id^="postType_"]');
entries.forEach(entry => {
const index = entry.id.split('_')[1];
const name = entry.querySelector(`[data-pt-field="name"][data-pt-index="${index}"]`)?.value?.trim();
if (name) {
const hashtagsRaw = entry.querySelector(`[data-pt-field="hashtags"][data-pt-index="${index}"]`)?.value || '';
const keywordsRaw = entry.querySelector(`[data-pt-field="keywords"][data-pt-index="${index}"]`)?.value || '';
postTypes.push({
name: name,
description: entry.querySelector(`[data-pt-field="description"][data-pt-index="${index}"]`)?.value?.trim() || null,
identifying_hashtags: hashtagsRaw.split(',').map(h => h.trim()).filter(h => h),
identifying_keywords: keywordsRaw.split(',').map(k => k.trim()).filter(k => k),
semantic_properties: {
purpose: entry.querySelector(`[data-pt-field="purpose"][data-pt-index="${index}"]`)?.value?.trim() || null,
typical_tone: entry.querySelector(`[data-pt-field="tone"][data-pt-index="${index}"]`)?.value?.trim() || null,
target_audience: entry.querySelector(`[data-pt-field="target_audience"][data-pt-index="${index}"]`)?.value?.trim() || null
}
});
}
});
return postTypes;
}
addPostTypeBtn.addEventListener('click', createPostTypeEntry);
form.addEventListener('submit', async (e) => {
e.preventDefault();
submitBtn.disabled = true;
submitBtn.textContent = 'Wird gestartet...';
progressArea.classList.remove('hidden');
resultArea.classList.add('hidden');
const formData = new FormData(form);
// Add post types as JSON
const postTypes = collectPostTypes();
if (postTypes.length > 0) {
formData.append('post_types_json', JSON.stringify(postTypes));
}
try {
const response = await fetch('/api/customers', {
method: 'POST',
body: formData
});
const data = await response.json();
// Poll for progress
const taskId = data.task_id;
const pollInterval = setInterval(async () => {
const statusResponse = await fetch(`/api/tasks/${taskId}`);
const status = await statusResponse.json();
progressBar.style.width = `${status.progress}%`;
progressPercent.textContent = `${status.progress}%`;
progressMessage.textContent = status.message;
if (status.status === 'completed') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
resultArea.classList.remove('hidden');
document.getElementById('successResult').classList.remove('hidden');
submitBtn.textContent = 'Setup starten';
submitBtn.disabled = false;
form.reset();
postTypesContainer.innerHTML = '';
postTypeIndex = 0;
} else if (status.status === 'error') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
resultArea.classList.remove('hidden');
document.getElementById('errorResult').classList.remove('hidden');
document.getElementById('errorMessage').textContent = status.message;
submitBtn.textContent = 'Setup starten';
submitBtn.disabled = false;
}
}, 1000);
} catch (error) {
progressArea.classList.add('hidden');
resultArea.classList.remove('hidden');
document.getElementById('errorResult').classList.remove('hidden');
document.getElementById('errorMessage').textContent = error.message;
submitBtn.textContent = 'Setup starten';
submitBtn.disabled = false;
}
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
{% extends "base.html" %}
{% block title %}Alle Posts - LinkedIn Posts{% endblock %}
{% block head %}
<style>
.post-card {
background: linear-gradient(135deg, rgba(61, 72, 72, 0.3) 0%, rgba(45, 56, 56, 0.4) 100%);
border: 1px solid rgba(61, 72, 72, 0.6);
transition: all 0.2s ease;
}
.post-card:hover {
border-color: rgba(255, 199, 0, 0.3);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.customer-header {
background: linear-gradient(90deg, rgba(255, 199, 0, 0.1) 0%, transparent 100%);
}
.score-ring {
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.75rem;
}
.score-high { background: rgba(34, 197, 94, 0.2); border: 2px solid rgba(34, 197, 94, 0.5); color: #86efac; }
.score-medium { background: rgba(234, 179, 8, 0.2); border: 2px solid rgba(234, 179, 8, 0.5); color: #fde047; }
.score-low { background: rgba(239, 68, 68, 0.2); border: 2px solid rgba(239, 68, 68, 0.5); color: #fca5a5; }
</style>
{% endblock %}
{% block content %}
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-white mb-2">Alle Posts</h1>
<p class="text-gray-400">{{ total_posts }} generierte Posts</p>
</div>
<a href="/create" class="px-4 py-2.5 btn-primary rounded-lg font-medium flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
Neuer Post
</a>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
<strong>Error:</strong> {{ error }}
</div>
{% endif %}
{% if customers_with_posts %}
<div class="space-y-8">
{% for item in customers_with_posts %}
{% if item.posts %}
<div class="card-bg rounded-xl border overflow-hidden">
<!-- Customer Header -->
<div class="customer-header px-6 py-4 border-b border-brand-bg-light">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl flex items-center justify-center shadow-lg overflow-hidden {{ 'bg-brand-highlight' if not item.profile_picture else '' }}">
{% if item.profile_picture %}
<img src="{{ item.profile_picture }}" alt="{{ item.customer.name }}" class="w-full h-full object-cover" loading="lazy" referrerpolicy="no-referrer">
{% else %}
<span class="text-brand-bg-dark font-bold text-lg">{{ item.customer.name[0] | upper }}</span>
{% endif %}
</div>
<div>
<h3 class="font-semibold text-white text-lg">{{ item.customer.name }}</h3>
<p class="text-sm text-gray-400">{{ item.customer.company_name or 'Kein Unternehmen' }}</p>
</div>
</div>
<div class="flex items-center gap-4">
<span class="px-4 py-1.5 bg-brand-bg rounded-full text-sm text-gray-300 font-medium">
{{ item.post_count }} Post{{ 's' if item.post_count != 1 else '' }}
</span>
</div>
</div>
</div>
<!-- Posts Grid -->
<div class="p-4">
<div class="grid gap-3">
{% for post in item.posts %}
<a href="/posts/{{ post.id }}" class="post-card rounded-xl p-4 block group">
<div class="flex items-center gap-4">
<!-- Score Circle -->
{% if post.critic_feedback and post.critic_feedback | length > 0 %}
{% set score = post.critic_feedback[-1].overall_score %}
<div class="score-ring flex-shrink-0 {{ 'score-high' if score >= 85 else 'score-medium' if score >= 70 else 'score-low' }}">
{{ score }}
</div>
{% else %}
<div class="score-ring flex-shrink-0 bg-brand-bg-dark border-2 border-brand-bg-light text-gray-500">
</div>
{% endif %}
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-3">
<h4 class="font-medium text-white group-hover:text-brand-highlight transition-colors truncate">
{{ post.topic_title or 'Untitled' }}
</h4>
<span class="flex-shrink-0 px-2 py-0.5 text-xs rounded font-medium {{ 'bg-green-600/20 text-green-400 border border-green-600/30' if post.status == 'approved' else 'bg-yellow-600/20 text-yellow-400 border border-yellow-600/30' }}">
{{ post.status | capitalize }}
</span>
</div>
<div class="flex items-center gap-4 mt-1.5 text-sm text-gray-500">
<span class="flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}
</span>
<span class="flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
{{ post.created_at.strftime('%H:%M') if post.created_at else '' }}
</span>
<span class="flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
{{ post.iterations }} Iteration{{ 's' if post.iterations != 1 else '' }}
</span>
</div>
</div>
<!-- Arrow -->
<svg class="w-5 h-5 text-gray-600 group-hover:text-brand-highlight transition-colors flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</a>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="card-bg rounded-xl border p-12 text-center">
<div class="w-20 h-20 bg-brand-bg rounded-2xl flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">Noch keine Posts</h3>
<p class="text-gray-400 mb-6 max-w-md mx-auto">Erstelle deinen ersten LinkedIn Post mit KI-Unterstützung.</p>
<a href="/create" class="inline-flex items-center gap-2 px-6 py-3 btn-primary font-medium rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
Post erstellen
</a>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,215 @@
{% extends "base.html" %}
{% block title %}Research Topics - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Research Topics</h1>
<p class="text-gray-400">Recherchiere neue Content-Themen mit Perplexity AI</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left: Form -->
<div>
<form id="researchForm" class="card-bg rounded-xl border p-6">
<div class="mb-6">
<label class="block text-sm font-medium text-gray-300 mb-2">Kunde auswählen</label>
<select name="customer_id" id="customerSelect" required class="w-full input-bg border rounded-lg px-4 py-3 text-white">
<option value="">-- Kunde wählen --</option>
{% for customer in customers %}
<option value="{{ customer.id }}">{{ customer.name }} - {{ customer.company_name or 'Kein Unternehmen' }}</option>
{% endfor %}
</select>
</div>
<!-- Post Type Selection -->
<div id="postTypeArea" class="mb-6 hidden">
<label class="block text-sm font-medium text-gray-300 mb-2">Post-Typ (optional)</label>
<div id="postTypeCards" class="grid grid-cols-2 gap-2 mb-2">
<!-- Post type cards will be loaded here -->
</div>
<p class="text-xs text-gray-500">Wähle einen Post-Typ für gezielte Recherche oder lasse leer für allgemeine Recherche.</p>
<input type="hidden" name="post_type_id" id="selectedPostTypeId" value="">
</div>
<!-- Progress Area -->
<div id="progressArea" class="hidden mb-6">
<div class="bg-brand-bg rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span id="progressMessage" class="text-gray-300">Starte Recherche...</span>
<span id="progressPercent" class="text-gray-400">0%</span>
</div>
<div class="w-full bg-brand-bg-dark rounded-full h-2">
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</div>
<button type="submit" id="submitBtn" class="w-full btn-primary font-medium py-3 rounded-lg transition-colors flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
Research starten
</button>
</form>
{% if not customers %}
<div class="mt-4 bg-yellow-900/30 border border-yellow-600 rounded-lg p-4">
<p class="text-yellow-300">Noch keine Kunden vorhanden. <a href="/customers/new" class="underline">Erstelle zuerst einen Kunden</a>.</p>
</div>
{% endif %}
</div>
<!-- Right: Results -->
<div>
<div id="resultsArea" class="card-bg rounded-xl border p-6">
<h3 class="text-lg font-semibold text-white mb-4">Gefundene Topics</h3>
<div id="topicsList" class="space-y-4">
<p class="text-gray-400">Starte eine Recherche um Topics zu finden...</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const form = document.getElementById('researchForm');
const submitBtn = document.getElementById('submitBtn');
const progressArea = document.getElementById('progressArea');
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const progressPercent = document.getElementById('progressPercent');
const topicsList = document.getElementById('topicsList');
const customerSelect = document.getElementById('customerSelect');
const postTypeArea = document.getElementById('postTypeArea');
const postTypeCards = document.getElementById('postTypeCards');
const selectedPostTypeId = document.getElementById('selectedPostTypeId');
let currentPostTypes = [];
// Load post types when customer is selected
customerSelect.addEventListener('change', async () => {
const customerId = customerSelect.value;
selectedPostTypeId.value = '';
if (!customerId) {
postTypeArea.classList.add('hidden');
return;
}
try {
const response = await fetch(`/api/customers/${customerId}/post-types`);
const data = await response.json();
if (data.post_types && data.post_types.length > 0) {
currentPostTypes = data.post_types;
postTypeArea.classList.remove('hidden');
postTypeCards.innerHTML = `
<button type="button" onclick="selectPostType('')" id="pt_all"
class="p-3 rounded-lg border text-left transition-colors bg-brand-highlight/20 border-brand-highlight text-white">
<div class="font-medium text-sm">Alle Typen</div>
<div class="text-xs text-gray-400 mt-1">Allgemeine Recherche</div>
</button>
` + data.post_types.map(pt => `
<button type="button" onclick="selectPostType('${pt.id}')" id="pt_${pt.id}"
class="p-3 rounded-lg border text-left transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
<div class="font-medium text-sm">${pt.name}</div>
<div class="text-xs text-gray-400 mt-1">${pt.analyzed_post_count || 0} Posts analysiert</div>
${pt.has_analysis ? '<span class="text-xs text-green-400">Analyse</span>' : ''}
</button>
`).join('');
} else {
postTypeArea.classList.add('hidden');
}
} catch (error) {
console.error('Failed to load post types:', error);
postTypeArea.classList.add('hidden');
}
});
function selectPostType(typeId) {
selectedPostTypeId.value = typeId;
// Update card styles
document.querySelectorAll('[id^="pt_"]').forEach(card => {
if (card.id === `pt_${typeId}` || (typeId === '' && card.id === 'pt_all')) {
card.className = 'p-3 rounded-lg border text-left transition-colors bg-brand-highlight/20 border-brand-highlight text-white';
} else {
card.className = 'p-3 rounded-lg border text-left transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white';
}
});
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const customerId = customerSelect.value;
if (!customerId) return;
submitBtn.disabled = true;
submitBtn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Recherchiert...';
progressArea.classList.remove('hidden');
const formData = new FormData();
formData.append('customer_id', customerId);
if (selectedPostTypeId.value) {
formData.append('post_type_id', selectedPostTypeId.value);
}
try {
const response = await fetch('/api/research', {
method: 'POST',
body: formData
});
const data = await response.json();
const taskId = data.task_id;
const pollInterval = setInterval(async () => {
const statusResponse = await fetch(`/api/tasks/${taskId}`);
const status = await statusResponse.json();
progressBar.style.width = `${status.progress}%`;
progressPercent.textContent = `${status.progress}%`;
progressMessage.textContent = status.message;
if (status.status === 'completed') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
// Display topics
if (status.topics && status.topics.length > 0) {
topicsList.innerHTML = status.topics.map((topic, i) => `
<div class="bg-brand-bg rounded-lg p-4 border border-brand-bg-light">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<span class="inline-block px-2 py-1 text-xs font-medium bg-brand-highlight/20 text-brand-highlight rounded mb-2">${topic.category || 'Topic'}</span>
<h4 class="font-semibold text-white">${topic.title}</h4>
${topic.angle ? `<p class="text-sm text-brand-highlight/80 mt-1">↳ ${topic.angle}</p>` : ''}
${topic.hook_idea ? `<p class="text-sm text-gray-300 mt-2 italic border-l-2 border-brand-highlight/30 pl-2">"${topic.hook_idea.substring(0, 150)}..."</p>` : ''}
<p class="text-gray-400 text-sm mt-2">${topic.fact ? topic.fact.substring(0, 200) + '...' : ''}</p>
${topic.source ? `<p class="text-gray-500 text-xs mt-2">Quelle: ${topic.source}</p>` : ''}
</div>
</div>
</div>
`).join('');
} else {
topicsList.innerHTML = '<p class="text-gray-400">Keine Topics gefunden.</p>';
}
} else if (status.status === 'error') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
topicsList.innerHTML = `<p class="text-red-400">Fehler: ${status.message}</p>`;
}
}, 1000);
} catch (error) {
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
topicsList.innerHTML = `<p class="text-red-400">Fehler: ${error.message}</p>`;
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,571 @@
{% extends "base.html" %}
{% block title %}Gescrapte Posts - LinkedIn Posts{% endblock %}
{% block head %}
<style>
.post-card {
background: linear-gradient(135deg, rgba(61, 72, 72, 0.3) 0%, rgba(45, 56, 56, 0.4) 100%);
border: 1px solid rgba(61, 72, 72, 0.6);
transition: all 0.2s ease;
}
.post-card:hover {
border-color: rgba(255, 199, 0, 0.3);
}
.post-card.selected {
border-color: rgba(255, 199, 0, 0.6);
background: linear-gradient(135deg, rgba(255, 199, 0, 0.05) 0%, rgba(45, 56, 56, 0.4) 100%);
}
.type-badge {
transition: all 0.15s ease;
}
.type-badge:hover {
transform: scale(1.02);
}
.type-badge.active {
background-color: rgba(255, 199, 0, 0.2);
border-color: #ffc700;
}
.post-content-preview {
max-height: 150px;
overflow: hidden;
position: relative;
}
.post-content-preview::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: linear-gradient(transparent, rgba(45, 56, 56, 0.9));
}
.post-content-expanded {
max-height: none;
}
.post-content-expanded::after {
display: none;
}
</style>
{% endblock %}
{% block content %}
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-white mb-2">Gescrapte Posts verwalten</h1>
<p class="text-gray-400">Posts manuell kategorisieren und Post-Typ-Analyse triggern</p>
</div>
</div>
</div>
<!-- Customer Selection -->
<div class="card-bg rounded-xl border p-6 mb-6">
<div class="flex flex-wrap items-end gap-4">
<div class="flex-1 min-w-64">
<label class="block text-sm font-medium text-gray-300 mb-2">Kunde auswählen</label>
<select id="customerSelect" class="w-full input-bg border rounded-lg px-4 py-3 text-white">
<option value="">-- Kunde wählen --</option>
{% for customer in customers %}
<option value="{{ customer.id }}">{{ customer.name }} - {{ customer.company_name or 'Kein Unternehmen' }}</option>
{% endfor %}
</select>
</div>
<div class="flex gap-2">
<button id="classifyAllBtn" class="hidden px-4 py-3 bg-brand-bg hover:bg-brand-bg-light border border-brand-bg-light rounded-lg text-white transition-colors">
<span class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Auto-Klassifizieren
</span>
</button>
<button id="analyzeTypesBtn" class="hidden px-4 py-3 btn-primary rounded-lg font-medium transition-colors">
<span class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Post-Typen analysieren
</span>
</button>
</div>
</div>
</div>
<!-- Progress Area -->
<div id="progressArea" class="hidden card-bg rounded-xl border p-6 mb-6">
<div class="flex items-center justify-between mb-2">
<span id="progressMessage" class="text-gray-300">Arbeite...</span>
<span id="progressPercent" class="text-gray-400">0%</span>
</div>
<div class="w-full bg-brand-bg-dark rounded-full h-2">
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<!-- Stats & Post Types -->
<div id="statsArea" class="hidden mb-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
<div class="card-bg rounded-xl border p-4">
<div class="text-2xl font-bold text-white" id="totalPostsCount">0</div>
<div class="text-sm text-gray-400">Gesamt Posts</div>
</div>
<div class="card-bg rounded-xl border p-4">
<div class="text-2xl font-bold text-green-400" id="classifiedCount">0</div>
<div class="text-sm text-gray-400">Klassifiziert</div>
</div>
<div class="card-bg rounded-xl border p-4">
<div class="text-2xl font-bold text-yellow-400" id="unclassifiedCount">0</div>
<div class="text-sm text-gray-400">Nicht klassifiziert</div>
</div>
<div class="card-bg rounded-xl border p-4">
<div class="text-2xl font-bold text-brand-highlight" id="postTypesCount">0</div>
<div class="text-sm text-gray-400">Post-Typen</div>
</div>
</div>
<!-- Post Type Filter -->
<div class="flex flex-wrap gap-2 mb-4">
<button onclick="filterByType(null)" id="filter_all" class="type-badge px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-highlight/20 border-brand-highlight text-white">
Alle
</button>
<button onclick="filterByType('unclassified')" id="filter_unclassified" class="type-badge px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
Nicht klassifiziert
</button>
<div id="postTypeFilters" class="contents">
<!-- Post type filter buttons will be added here -->
</div>
</div>
</div>
<!-- Posts List -->
<div id="postsArea" class="hidden">
<div id="postsList" class="space-y-4">
<p class="text-gray-400">Lade Posts...</p>
</div>
</div>
<!-- Empty State -->
<div id="emptyState" class="hidden card-bg rounded-xl border p-12 text-center">
<div class="w-20 h-20 bg-brand-bg rounded-2xl flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">Keine gescrapten Posts</h3>
<p class="text-gray-400 mb-6 max-w-md mx-auto">Für diesen Kunden wurden noch keine LinkedIn Posts gescrapet.</p>
</div>
{% if not customers %}
<div class="bg-yellow-900/30 border border-yellow-600 rounded-lg p-4">
<p class="text-yellow-300">Noch keine Kunden vorhanden. <a href="/customers/new" class="underline">Erstelle zuerst einen Kunden</a>.</p>
</div>
{% endif %}
<!-- Post Detail Modal -->
<div id="postModal" class="fixed inset-0 bg-black/70 hidden items-center justify-center z-50 p-4">
<div class="bg-brand-bg-dark rounded-xl border border-brand-bg-light max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col shadow-2xl">
<div class="p-4 border-b border-brand-bg-light flex items-center justify-between bg-brand-bg">
<h3 class="text-lg font-semibold text-white">Post Details</h3>
<button onclick="closeModal()" class="text-gray-400 hover:text-white p-1 hover:bg-brand-bg-light rounded">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1">
<div id="modalContent"></div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const customerSelect = document.getElementById('customerSelect');
const classifyAllBtn = document.getElementById('classifyAllBtn');
const analyzeTypesBtn = document.getElementById('analyzeTypesBtn');
const progressArea = document.getElementById('progressArea');
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const progressPercent = document.getElementById('progressPercent');
const statsArea = document.getElementById('statsArea');
const postsArea = document.getElementById('postsArea');
const postsList = document.getElementById('postsList');
const emptyState = document.getElementById('emptyState');
const postTypeFilters = document.getElementById('postTypeFilters');
const postModal = document.getElementById('postModal');
const modalContent = document.getElementById('modalContent');
let currentPosts = [];
let currentPostTypes = [];
let currentFilter = null;
customerSelect.addEventListener('change', async () => {
const customerId = customerSelect.value;
if (!customerId) {
statsArea.classList.add('hidden');
postsArea.classList.add('hidden');
emptyState.classList.add('hidden');
classifyAllBtn.classList.add('hidden');
analyzeTypesBtn.classList.add('hidden');
return;
}
await loadCustomerData(customerId);
});
async function loadCustomerData(customerId) {
// Load post types
try {
const ptResponse = await fetch(`/api/customers/${customerId}/post-types`);
const ptData = await ptResponse.json();
currentPostTypes = ptData.post_types || [];
// Update post type filters
postTypeFilters.innerHTML = currentPostTypes.map(pt => `
<button onclick="filterByType('${pt.id}')" id="filter_${pt.id}"
class="type-badge px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
${escapeHtml(pt.name)}
<span class="ml-1 text-xs text-gray-400">(${pt.analyzed_post_count || 0})</span>
${pt.has_analysis ? '<span class="ml-1 text-green-400">*</span>' : ''}
</button>
`).join('');
document.getElementById('postTypesCount').textContent = currentPostTypes.length;
} catch (error) {
console.error('Failed to load post types:', error);
}
// Load posts
try {
const response = await fetch(`/api/customers/${customerId}/linkedin-posts`);
const data = await response.json();
console.log('API Response:', data);
if (data.error) {
console.error('API Error:', data.error);
postsList.innerHTML = `<p class="text-red-400">API Fehler: ${escapeHtml(data.error)}</p>`;
postsArea.classList.remove('hidden');
return;
}
currentPosts = data.posts || [];
console.log(`Loaded ${currentPosts.length} posts`);
if (currentPosts.length === 0) {
statsArea.classList.add('hidden');
postsArea.classList.add('hidden');
emptyState.classList.remove('hidden');
classifyAllBtn.classList.add('hidden');
analyzeTypesBtn.classList.add('hidden');
return;
}
// Update stats
const classified = currentPosts.filter(p => p.post_type_id).length;
const unclassified = currentPosts.length - classified;
document.getElementById('totalPostsCount').textContent = currentPosts.length;
document.getElementById('classifiedCount').textContent = classified;
document.getElementById('unclassifiedCount').textContent = unclassified;
statsArea.classList.remove('hidden');
postsArea.classList.remove('hidden');
emptyState.classList.add('hidden');
classifyAllBtn.classList.remove('hidden');
analyzeTypesBtn.classList.remove('hidden');
currentFilter = null;
filterByType(null);
} catch (error) {
console.error('Failed to load posts:', error);
postsList.innerHTML = `<p class="text-red-400">Fehler beim Laden: ${error.message}</p>`;
}
}
function filterByType(typeId) {
currentFilter = typeId;
// Update filter button styles
document.querySelectorAll('.type-badge').forEach(btn => {
const btnId = btn.id.replace('filter_', '');
const isActive = (typeId === null && btnId === 'all') ||
(typeId === 'unclassified' && btnId === 'unclassified') ||
(btnId === typeId);
if (isActive) {
btn.classList.add('active', 'bg-brand-highlight/20', 'border-brand-highlight');
btn.classList.remove('bg-brand-bg', 'border-brand-bg-light');
} else {
btn.classList.remove('active', 'bg-brand-highlight/20', 'border-brand-highlight');
btn.classList.add('bg-brand-bg', 'border-brand-bg-light');
}
});
// Filter posts
let filteredPosts = currentPosts;
if (typeId === 'unclassified') {
filteredPosts = currentPosts.filter(p => !p.post_type_id);
} else if (typeId) {
filteredPosts = currentPosts.filter(p => p.post_type_id === typeId);
}
renderPosts(filteredPosts);
}
function renderPosts(posts) {
if (posts.length === 0) {
postsList.innerHTML = '<p class="text-gray-400 text-center py-8">Keine Posts in dieser Kategorie.</p>';
return;
}
postsList.innerHTML = posts.map((post, index) => {
const postType = currentPostTypes.find(pt => pt.id === post.post_type_id);
const postText = post.post_text || '';
const previewText = postText.substring(0, 300);
return `
<div class="post-card rounded-xl p-4 cursor-pointer" data-post-id="${post.id}" onclick="openPostModal('${post.id}')">
<div class="flex items-start gap-4">
<div class="flex-1 min-w-0">
<!-- Header -->
<div class="flex items-center gap-2 mb-3 flex-wrap">
${postType ? `
<span class="px-2 py-1 text-xs font-medium bg-brand-highlight/20 text-brand-highlight rounded">
${escapeHtml(postType.name)}
</span>
<span class="text-xs text-gray-500">${post.classification_method || 'unknown'} (${Math.round((post.classification_confidence || 0) * 100)}%)</span>
` : `
<span class="px-2 py-1 text-xs font-medium bg-gray-600/30 text-gray-400 rounded">
Nicht klassifiziert
</span>
`}
${post.posted_at ? `
<span class="text-xs text-gray-500">${new Date(post.posted_at).toLocaleDateString('de-DE')}</span>
` : ''}
${post.engagement_score ? `
<span class="text-xs text-gray-500">Engagement: ${post.engagement_score}</span>
` : ''}
</div>
<!-- Content Preview -->
<div class="post-content-preview text-gray-300 text-sm whitespace-pre-wrap mb-3">
${escapeHtml(previewText)}${postText.length > 300 ? '...' : ''}
</div>
<!-- Click hint & Type badges -->
<div class="flex items-center justify-between flex-wrap gap-2" onclick="event.stopPropagation()">
<span class="text-xs text-gray-500 italic">Klicken für Vollansicht</span>
<div class="flex items-center gap-2 flex-wrap">
${currentPostTypes.map(pt => `
<button onclick="event.stopPropagation(); classifyPost('${post.id}', '${pt.id}')"
class="px-2 py-1 text-xs rounded transition-colors ${post.post_type_id === pt.id ? 'bg-brand-highlight/30 text-brand-highlight border border-brand-highlight' : 'bg-brand-bg hover:bg-brand-bg-light text-gray-300 border border-brand-bg-light'}">
${escapeHtml(pt.name)}
</button>
`).join('')}
${post.post_type_id ? `
<button onclick="event.stopPropagation(); classifyPost('${post.id}', null)" class="px-2 py-1 text-xs rounded bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-900/50">
</button>
` : ''}
</div>
</div>
</div>
</div>
</div>
`;
}).join('');
}
async function classifyPost(postId, postTypeId) {
const customerId = customerSelect.value;
if (!customerId) return;
try {
const response = await fetch(`/api/linkedin-posts/${postId}/classify`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ post_type_id: postTypeId })
});
if (!response.ok) {
throw new Error('Failed to classify post');
}
// Update local data
const post = currentPosts.find(p => p.id === postId);
if (post) {
post.post_type_id = postTypeId;
post.classification_method = 'manual';
post.classification_confidence = 1.0;
}
// Update stats
const classified = currentPosts.filter(p => p.post_type_id).length;
document.getElementById('classifiedCount').textContent = classified;
document.getElementById('unclassifiedCount').textContent = currentPosts.length - classified;
// Re-render
filterByType(currentFilter);
} catch (error) {
console.error('Failed to classify post:', error);
alert('Fehler beim Klassifizieren: ' + error.message);
}
}
function openPostModal(postId) {
const post = currentPosts.find(p => p.id === postId);
if (!post) return;
const postType = currentPostTypes.find(pt => pt.id === post.post_type_id);
modalContent.innerHTML = `
<div class="mb-4 flex items-center gap-3 flex-wrap">
${postType ? `
<span class="px-3 py-1.5 text-sm font-medium bg-brand-highlight/20 text-brand-highlight rounded-lg">
${escapeHtml(postType.name)}
</span>
` : `
<span class="px-3 py-1.5 text-sm font-medium bg-gray-600/30 text-gray-400 rounded-lg">
Nicht klassifiziert
</span>
`}
${post.posted_at ? `
<span class="text-sm text-gray-500">${new Date(post.posted_at).toLocaleDateString('de-DE')}</span>
` : ''}
${post.engagement_score ? `
<span class="text-sm text-gray-500">Engagement: ${post.engagement_score}</span>
` : ''}
</div>
<div class="bg-brand-bg rounded-xl p-6 mb-6 border border-brand-bg-light max-h-[50vh] overflow-y-auto">
<div class="whitespace-pre-wrap text-gray-200 font-sans text-base leading-relaxed">${escapeHtml(post.post_text || '')}</div>
</div>
<div class="flex items-center gap-3 flex-wrap border-t border-brand-bg-light pt-4">
<span class="text-sm text-gray-400 font-medium">Typ zuweisen:</span>
${currentPostTypes.map(pt => `
<button onclick="classifyPost('${post.id}', '${pt.id}'); closeModal();"
class="px-4 py-2 text-sm rounded-lg transition-colors ${post.post_type_id === pt.id ? 'bg-brand-highlight text-brand-bg-dark font-medium' : 'bg-brand-bg hover:bg-brand-bg-light text-gray-300 border border-brand-bg-light'}">
${escapeHtml(pt.name)}
</button>
`).join('')}
${post.post_type_id ? `
<button onclick="classifyPost('${post.id}', null); closeModal();" class="px-4 py-2 text-sm rounded-lg bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-900/50">
Klassifizierung entfernen
</button>
` : ''}
</div>
`;
postModal.classList.remove('hidden');
postModal.classList.add('flex');
}
function closeModal() {
postModal.classList.add('hidden');
postModal.classList.remove('flex');
}
// Close modal on backdrop click
postModal.addEventListener('click', (e) => {
if (e.target === postModal) {
closeModal();
}
});
// Close modal on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !postModal.classList.contains('hidden')) {
closeModal();
}
});
// Auto-classify button
classifyAllBtn.addEventListener('click', async () => {
const customerId = customerSelect.value;
if (!customerId) return;
classifyAllBtn.disabled = true;
progressArea.classList.remove('hidden');
try {
const response = await fetch(`/api/customers/${customerId}/classify-posts`, {
method: 'POST'
});
const data = await response.json();
const taskId = data.task_id;
await pollTask(taskId, async () => {
await loadCustomerData(customerId);
});
} catch (error) {
console.error('Classification failed:', error);
alert('Fehler bei der Klassifizierung: ' + error.message);
} finally {
classifyAllBtn.disabled = false;
progressArea.classList.add('hidden');
}
});
// Analyze post types button
analyzeTypesBtn.addEventListener('click', async () => {
const customerId = customerSelect.value;
if (!customerId) return;
analyzeTypesBtn.disabled = true;
progressArea.classList.remove('hidden');
try {
const response = await fetch(`/api/customers/${customerId}/analyze-post-types`, {
method: 'POST'
});
const data = await response.json();
const taskId = data.task_id;
await pollTask(taskId, async () => {
await loadCustomerData(customerId);
alert('Post-Typ-Analyse abgeschlossen! Die Analysen wurden aktualisiert.');
});
} catch (error) {
console.error('Analysis failed:', error);
alert('Fehler bei der Analyse: ' + error.message);
} finally {
analyzeTypesBtn.disabled = false;
progressArea.classList.add('hidden');
}
});
async function pollTask(taskId, onComplete) {
return new Promise((resolve) => {
const interval = setInterval(async () => {
try {
const statusResponse = await fetch(`/api/tasks/${taskId}`);
const status = await statusResponse.json();
progressBar.style.width = `${status.progress}%`;
progressPercent.textContent = `${status.progress}%`;
progressMessage.textContent = status.message;
if (status.status === 'completed') {
clearInterval(interval);
await onComplete();
resolve();
} else if (status.status === 'error') {
clearInterval(interval);
alert('Fehler: ' + status.message);
resolve();
}
} catch (error) {
clearInterval(interval);
console.error('Polling error:', error);
resolve();
}
}, 1000);
});
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}

View File

@@ -0,0 +1,155 @@
{% extends "base.html" %}
{% block title %}Status - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Status</h1>
<p class="text-gray-400">Übersicht über alle Kunden und deren Setup-Status</p>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
<strong>Error:</strong> {{ error }}
</div>
{% endif %}
{% if customer_statuses %}
<div class="grid gap-6">
{% for item in customer_statuses %}
<div class="card-bg rounded-xl border overflow-hidden">
<!-- Customer Header -->
<div class="px-6 py-4 border-b border-brand-bg-light">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-full flex items-center justify-center overflow-hidden {{ 'bg-brand-highlight' if not item.profile_picture else '' }}">
{% if item.profile_picture %}
<img src="{{ item.profile_picture }}" alt="{{ item.customer.name }}" class="w-full h-full object-cover" loading="lazy" referrerpolicy="no-referrer">
{% else %}
<span class="text-brand-bg-dark font-bold text-lg">{{ item.customer.name[0] | upper }}</span>
{% endif %}
</div>
<div>
<h3 class="font-semibold text-white text-lg">{{ item.customer.name }}</h3>
<p class="text-sm text-gray-400">{{ item.customer.company_name or 'Kein Unternehmen' }}</p>
</div>
</div>
<div class="flex items-center gap-2">
{% if item.status.ready_for_posts %}
<span class="px-3 py-1.5 bg-green-600/30 text-green-300 rounded-lg text-sm font-medium flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Bereit für Posts
</span>
{% else %}
<span class="px-3 py-1.5 bg-yellow-600/30 text-yellow-300 rounded-lg text-sm font-medium flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
Setup unvollständig
</span>
{% endif %}
</div>
</div>
</div>
<!-- Status Grid -->
<div class="p-6">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<!-- Scraped Posts -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
{% if item.status.has_scraped_posts %}
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
{% else %}
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
{% endif %}
<span class="text-sm text-gray-400">Scraped Posts</span>
</div>
<p class="text-2xl font-bold text-white">{{ item.status.scraped_posts_count }}</p>
</div>
<!-- Profile Analysis -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
{% if item.status.has_profile_analysis %}
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
{% else %}
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
{% endif %}
<span class="text-sm text-gray-400">Profil Analyse</span>
</div>
<p class="text-lg font-semibold text-white">{{ 'Vorhanden' if item.status.has_profile_analysis else 'Fehlt' }}</p>
</div>
<!-- Research Topics -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
{% if item.status.research_count > 0 %}
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
{% else %}
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
{% endif %}
<span class="text-sm text-gray-400">Research Topics</span>
</div>
<p class="text-2xl font-bold text-white">{{ item.status.research_count }}</p>
</div>
<!-- Generated Posts -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
<span class="text-sm text-gray-400">Generierte Posts</span>
</div>
<p class="text-2xl font-bold text-white">{{ item.status.posts_count }}</p>
</div>
</div>
<!-- Missing Items -->
{% if item.status.missing_items %}
<div class="bg-yellow-900/20 border border-yellow-600/50 rounded-lg p-4">
<h4 class="font-medium text-yellow-300 mb-2 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
Fehlende Elemente
</h4>
<ul class="space-y-1">
{% for item_missing in item.status.missing_items %}
<li class="text-yellow-200/80 text-sm flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
{{ item_missing }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Quick Actions -->
<div class="flex gap-3 mt-4">
{% if not item.status.has_profile_analysis %}
<a href="/customers/new" class="px-4 py-2 btn-primary rounded-lg text-sm transition-colors">
Setup wiederholen
</a>
{% endif %}
{% if item.status.research_count == 0 %}
<a href="/research" class="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg text-sm text-white transition-colors">
Recherche starten
</a>
{% endif %}
{% if item.status.ready_for_posts %}
<a href="/create" class="px-4 py-2 btn-primary rounded-lg text-sm transition-colors">
Post erstellen
</a>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="card-bg rounded-xl border p-12 text-center">
<svg class="w-16 h-16 text-gray-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
<h3 class="text-xl font-semibold text-white mb-2">Noch keine Kunden</h3>
<p class="text-gray-400 mb-6">Erstelle deinen ersten Kunden, um den Status zu sehen.</p>
<a href="/customers/new" class="inline-flex items-center gap-2 px-6 py-3 btn-primary font-medium rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
Neuer Kunde
</a>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anmeldung... - LinkedIn Posts</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { background-color: #3d4848; }
</style>
</head>
<body class="text-gray-100 min-h-screen flex items-center justify-center">
<div class="text-center">
<div class="w-16 h-16 border-4 border-brand-highlight border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p class="text-white text-lg">Anmeldung wird verarbeitet...</p>
<p class="text-gray-400 text-sm mt-2">Du wirst gleich weitergeleitet.</p>
</div>
<script>
// Supabase returns tokens in URL hash fragment
// Extract them and redirect to callback with query params
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const error = params.get('error');
const errorDescription = params.get('error_description');
if (error) {
window.location.href = `/login?error=${encodeURIComponent(error)}`;
} else if (accessToken) {
// Redirect back to callback with tokens in query params
let callbackUrl = `/auth/callback?access_token=${encodeURIComponent(accessToken)}`;
if (refreshToken) {
callbackUrl += `&refresh_token=${encodeURIComponent(refreshToken)}`;
}
window.location.href = callbackUrl;
} else {
// No tokens found, redirect to login
window.location.href = '/login?error=no_tokens';
}
</script>
</body>
</html>

View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}LinkedIn Posts{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'brand': {
'bg': '#3d4848',
'bg-light': '#4a5858',
'bg-dark': '#2d3838',
'highlight': '#ffc700',
'highlight-dark': '#e6b300',
}
}
}
}
}
</script>
<style>
body { background-color: #3d4848; }
.nav-link.active { background-color: #ffc700; color: #2d3838; }
.nav-link.active svg { stroke: #2d3838; }
.post-content { white-space: pre-wrap; word-wrap: break-word; }
.btn-primary { background-color: #ffc700; color: #2d3838; }
.btn-primary:hover { background-color: #e6b300; }
.sidebar-bg { background-color: #2d3838; }
.card-bg { background-color: #4a5858; border-color: #5a6868; }
.input-bg { background-color: #3d4848; border-color: #5a6868; }
.input-bg:focus { border-color: #ffc700; outline: none; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #3d4848; }
::-webkit-scrollbar-thumb { background: #5a6868; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #6a7878; }
</style>
{% block head %}{% endblock %}
</head>
<body class="text-gray-100 min-h-screen flex">
<!-- Sidebar -->
<aside class="w-64 sidebar-bg border-r border-gray-600 flex flex-col fixed h-full">
<div class="p-4 border-b border-gray-600">
<div class="flex items-center justify-center gap-3">
<div>
<img src="/static/logo.png" alt="Logo" class="h-15 w-auto">
</div>
</div>
</div>
<!-- User Profile -->
{% if session %}
<div class="p-4 border-b border-gray-600">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full overflow-hidden bg-brand-highlight flex items-center justify-center">
{% if session.linkedin_picture %}
<img src="{{ session.linkedin_picture }}" alt="{{ session.linkedin_name }}" class="w-full h-full object-cover" referrerpolicy="no-referrer">
{% else %}
<span class="text-brand-bg-dark font-bold">{{ session.customer_name[0] | upper }}</span>
{% endif %}
</div>
<div class="flex-1 min-w-0">
<p class="text-white font-medium text-sm truncate">{{ session.linkedin_name or session.customer_name }}</p>
<p class="text-gray-400 text-xs truncate">{{ session.customer_name }}</p>
</div>
</div>
</div>
{% endif %}
<nav class="flex-1 p-4 space-y-2">
<a href="/" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'home' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
Dashboard
</a>
<a href="/research" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'research' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
Research Topics
</a>
<a href="/create" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'create' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Post erstellen
</a>
<a href="/posts" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'posts' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
Meine Posts
</a>
<a href="/status" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'status' %}active{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Status
</a>
</nav>
<div class="p-4 border-t border-gray-600">
<a href="/logout" class="flex items-center gap-2 text-gray-400 hover:text-gray-200 text-sm transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
Logout
</a>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 ml-64">
<div class="p-8">
{% block content %}{% endblock %}
</div>
</main>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,479 @@
{% extends "base.html" %}
{% block title %}Post erstellen - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Post erstellen</h1>
<p class="text-gray-400">Generiere einen neuen LinkedIn Post mit AI</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left: Form -->
<div>
<form id="createPostForm" class="card-bg rounded-xl border p-6 space-y-6">
<!-- Post Type Selection -->
<div id="postTypeSelectionArea" class="hidden">
<label class="block text-sm font-medium text-gray-300 mb-2">Post-Typ auswählen (optional)</label>
<div id="postTypeCards" class="flex flex-wrap gap-2 mb-2">
<!-- Post type cards will be loaded here -->
</div>
<input type="hidden" id="selectedPostTypeId" value="">
</div>
<!-- Topic Selection -->
<div id="topicSelectionArea">
<label class="block text-sm font-medium text-gray-300 mb-2">Topic auswählen</label>
<div id="topicsList" class="space-y-2 max-h-64 overflow-y-auto">
<p class="text-gray-500">Lade Topics...</p>
</div>
</div>
<!-- Custom Topic -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">
<span>Oder eigenes Topic eingeben</span>
</label>
<div class="space-y-3">
<input type="text" id="customTopicTitle" placeholder="Topic Titel" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
<textarea id="customTopicFact" rows="3" placeholder="Fakt / Kernaussage zum Topic..." class="w-full input-bg border rounded-lg px-4 py-2 text-white"></textarea>
<input type="text" id="customTopicSource" placeholder="Quelle (optional)" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
</div>
<!-- Progress Area -->
<div id="progressArea" class="hidden">
<div class="bg-brand-bg rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span id="progressMessage" class="text-gray-300">Starte Post-Erstellung...</span>
<span id="progressPercent" class="text-gray-400">0%</span>
</div>
<div class="w-full bg-brand-bg-dark rounded-full h-2">
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div id="iterationInfo" class="mt-2 text-sm text-gray-400"></div>
</div>
</div>
<button type="submit" id="submitBtn" class="w-full btn-primary font-medium py-3 rounded-lg transition-colors flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Post generieren
</button>
</form>
</div>
<!-- Right: Result -->
<div>
<div id="resultArea" class="card-bg rounded-xl border p-6">
<h3 class="text-lg font-semibold text-white mb-4">Generierter Post</h3>
<!-- Live Versions Display -->
<div id="liveVersions" class="hidden space-y-4 mb-6">
<div class="flex items-center gap-2 mb-2">
<span class="text-sm text-gray-400">Live-Vorschau der Iterationen:</span>
</div>
<div id="versionsContainer" class="space-y-4"></div>
</div>
<div id="postResult">
<p class="text-gray-400">Wähle ein Topic aus oder gib ein eigenes ein, dann klicke auf "Post generieren"...</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const form = document.getElementById('createPostForm');
const topicSelectionArea = document.getElementById('topicSelectionArea');
const topicsList = document.getElementById('topicsList');
const submitBtn = document.getElementById('submitBtn');
const progressArea = document.getElementById('progressArea');
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const progressPercent = document.getElementById('progressPercent');
const iterationInfo = document.getElementById('iterationInfo');
const postResult = document.getElementById('postResult');
const liveVersions = document.getElementById('liveVersions');
const versionsContainer = document.getElementById('versionsContainer');
const postTypeSelectionArea = document.getElementById('postTypeSelectionArea');
const postTypeCards = document.getElementById('postTypeCards');
const selectedPostTypeIdInput = document.getElementById('selectedPostTypeId');
let selectedTopic = null;
let currentVersionIndex = 0;
let currentPostTypes = [];
let currentTopics = [];
function renderVersions(versions, feedbackList) {
if (!versions || versions.length === 0) {
liveVersions.classList.add('hidden');
return;
}
liveVersions.classList.remove('hidden');
// Build version tabs and content
let html = `
<div class="flex gap-2 mb-4 flex-wrap">
${versions.map((_, i) => `
<button onclick="showVersion(${i})" id="versionTab${i}"
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}">
V${i + 1}
${feedbackList[i] ? `<span class="ml-1 text-xs opacity-75">(${feedbackList[i].overall_score || '?'})</span>` : ''}
</button>
`).join('')}
</div>
`;
// Show current version
const currentVersion = versions[currentVersionIndex];
const currentFeedback = feedbackList[currentVersionIndex];
html += `
<div class="grid grid-cols-1 ${currentFeedback ? 'lg:grid-cols-2' : ''} gap-4">
<div class="bg-brand-bg/50 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span>
${currentFeedback ? `
<span class="px-2 py-0.5 text-xs rounded ${currentFeedback.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
${currentFeedback.approved ? 'Approved' : `Score: ${currentFeedback.overall_score}/100`}
</span>
` : '<span class="text-xs text-gray-500">Wird bewertet...</span>'}
</div>
<pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-96 overflow-y-auto">${currentVersion}</pre>
</div>
${currentFeedback ? `
<div class="bg-brand-bg/30 rounded-lg p-4 border border-brand-bg-light">
<span class="text-sm font-medium text-gray-300 block mb-2">Kritik</span>
<p class="text-sm text-gray-400 mb-3">${currentFeedback.feedback || 'Keine Kritik'}</p>
${currentFeedback.improvements && currentFeedback.improvements.length > 0 ? `
<div class="mt-2">
<span class="text-xs font-medium text-gray-400">Verbesserungen:</span>
<ul class="mt-1 space-y-1">
${currentFeedback.improvements.map(imp => `
<li class="text-xs text-gray-500 flex items-start gap-1">
<span class="text-yellow-500">•</span> ${imp}
</li>
`).join('')}
</ul>
</div>
` : ''}
${currentFeedback.scores ? `
<div class="mt-3 pt-3 border-t border-brand-bg-light">
<div class="grid grid-cols-3 gap-2 text-xs">
<div class="text-center">
<div class="text-gray-500">Authentizität</div>
<div class="font-medium text-gray-300">${currentFeedback.scores.authenticity_and_style || '?'}/40</div>
</div>
<div class="text-center">
<div class="text-gray-500">Content</div>
<div class="font-medium text-gray-300">${currentFeedback.scores.content_quality || '?'}/35</div>
</div>
<div class="text-center">
<div class="text-gray-500">Technik</div>
<div class="font-medium text-gray-300">${currentFeedback.scores.technical_execution || '?'}/25</div>
</div>
</div>
</div>
` : ''}
</div>
` : ''}
</div>
`;
versionsContainer.innerHTML = html;
}
function showVersion(index) {
currentVersionIndex = index;
// Get cached versions from progress store
const cachedData = window.lastProgressData;
if (cachedData) {
renderVersions(cachedData.versions, cachedData.feedback_list);
}
}
// Load topics and post types on page load
async function loadData() {
// Load post types
try {
const ptResponse = await fetch('/api/post-types');
const ptData = await ptResponse.json();
if (ptData.post_types && ptData.post_types.length > 0) {
currentPostTypes = ptData.post_types;
postTypeSelectionArea.classList.remove('hidden');
postTypeCards.innerHTML = `
<button type="button" onclick="selectPostTypeForCreate('')" id="ptc_all"
class="px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-highlight/20 border-brand-highlight text-white">
Alle Typen
</button>
` + ptData.post_types.map(pt => `
<button type="button" onclick="selectPostTypeForCreate('${pt.id}')" id="ptc_${pt.id}"
class="px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
${pt.name}
${pt.has_analysis ? '<span class="ml-1 text-green-400 text-xs">*</span>' : ''}
</button>
`).join('');
} else {
postTypeSelectionArea.classList.add('hidden');
}
} catch (error) {
console.error('Failed to load post types:', error);
postTypeSelectionArea.classList.add('hidden');
}
// Load topics
loadTopics();
}
async function loadTopics(postTypeId = null) {
topicsList.innerHTML = '<p class="text-gray-500">Lade Topics...</p>';
try {
let url = '/api/topics';
if (postTypeId) {
url += `?post_type_id=${postTypeId}`;
}
const response = await fetch(url);
const data = await response.json();
if (data.topics && data.topics.length > 0) {
renderTopicsList(data);
} else {
let message = '';
if (data.used_count > 0) {
message = `<div class="text-center py-4">
<p class="text-gray-400 mb-2">Alle ${data.used_count} Topics wurden bereits verwendet.</p>
<a href="/research" class="text-brand-highlight hover:underline">Neue Topics recherchieren</a>
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
</div>`;
} else {
message = `<div class="text-center py-4">
<p class="text-gray-400 mb-2">Keine Topics gefunden${postTypeId ? ' für diesen Post-Typ' : ''}.</p>
<a href="/research" class="text-brand-highlight hover:underline">Recherche starten</a>
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
</div>`;
}
topicsList.innerHTML = message;
}
} catch (error) {
topicsList.innerHTML = `<p class="text-red-400">Fehler beim Laden: ${error.message}</p>`;
}
}
// Clear selected topic when custom topic is entered
['customTopicTitle', 'customTopicFact', 'customTopicSource'].forEach(id => {
document.getElementById(id).addEventListener('input', () => {
selectedTopic = null;
document.querySelectorAll('input[name="topic"]').forEach(radio => radio.checked = false);
});
});
function selectPostTypeForCreate(typeId) {
selectedPostTypeIdInput.value = typeId;
// Update card styles
document.querySelectorAll('[id^="ptc_"]').forEach(card => {
if (card.id === `ptc_${typeId}` || (typeId === '' && card.id === 'ptc_all')) {
card.className = 'px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-highlight/20 border-brand-highlight text-white';
} else {
card.className = 'px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white';
}
});
// Reload topics filtered by post type
loadTopics(typeId);
}
function renderTopicsList(data) {
// Store topics in global array for safe access
currentTopics = data.topics;
// Reset selected topic when list is re-rendered
selectedTopic = null;
let statsHtml = '';
if (data.used_count > 0) {
statsHtml = `<p class="text-xs text-gray-500 mb-3">${data.available_count} verfügbar · ${data.used_count} bereits verwendet</p>`;
}
topicsList.innerHTML = statsHtml + data.topics.map((topic, i) => `
<label class="flex items-start gap-3 p-3 bg-brand-bg/50 rounded-lg cursor-pointer hover:bg-brand-bg transition-colors border border-transparent hover:border-brand-highlight/30">
<input type="radio" name="topic" value="${i}" class="mt-1 text-brand-highlight" data-topic-index="${i}">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1 flex-wrap">
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-brand-highlight/20 text-brand-highlight rounded">${escapeHtml(topic.category || 'Topic')}</span>
${topic.target_post_type_id ? `<span class="text-xs text-gray-500">Typ-spezifisch</span>` : ''}
${topic.source ? `<span class="text-xs text-gray-500">🔗 ${escapeHtml(topic.source.substring(0, 30))}${topic.source.length > 30 ? '...' : ''}</span>` : ''}
</div>
<p class="font-medium text-white">${escapeHtml(topic.title)}</p>
${topic.angle ? `<p class="text-xs text-brand-highlight/80 mt-1">→ ${escapeHtml(topic.angle)}</p>` : ''}
${topic.hook_idea ? `<p class="text-sm text-gray-300 mt-2 italic border-l-2 border-brand-highlight/30 pl-2">"${escapeHtml(topic.hook_idea.substring(0, 120))}${topic.hook_idea.length > 120 ? '...' : ''}"</p>` : ''}
${topic.key_facts && topic.key_facts.length > 0 ? `
<div class="mt-2 flex flex-wrap gap-1">
${topic.key_facts.slice(0, 2).map(f => `<span class="text-xs bg-brand-bg-dark px-2 py-0.5 rounded text-gray-400">📊 ${escapeHtml(f.substring(0, 40))}${f.length > 40 ? '...' : ''}</span>`).join('')}
</div>
` : (topic.fact ? `<p class="text-sm text-gray-400 mt-1">${escapeHtml(topic.fact.substring(0, 100))}...</p>` : '')}
${topic.why_this_person ? `<p class="text-xs text-gray-500 mt-2">💡 ${escapeHtml(topic.why_this_person.substring(0, 80))}${topic.why_this_person.length > 80 ? '...' : ''}</p>` : ''}
</div>
</label>
`).join('');
// Add event listeners to radio buttons
document.querySelectorAll('input[name="topic"]').forEach(radio => {
radio.addEventListener('change', () => {
const index = parseInt(radio.dataset.topicIndex, 10);
selectedTopic = currentTopics[index];
// Clear custom topic fields
document.getElementById('customTopicTitle').value = '';
document.getElementById('customTopicFact').value = '';
document.getElementById('customTopicSource').value = '';
});
});
}
// Helper function to escape HTML special characters
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Get topic (either selected or custom)
let topic;
const customTitle = document.getElementById('customTopicTitle').value.trim();
const customFact = document.getElementById('customTopicFact').value.trim();
if (customTitle && customFact) {
topic = {
title: customTitle,
fact: customFact,
source: document.getElementById('customTopicSource').value.trim() || null,
category: 'Custom'
};
} else if (selectedTopic) {
topic = selectedTopic;
} else {
alert('Bitte wähle ein Topic aus oder gib ein eigenes ein.');
return;
}
submitBtn.disabled = true;
submitBtn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Generiert...';
progressArea.classList.remove('hidden');
postResult.innerHTML = '<p class="text-gray-400">Post wird generiert...</p>';
const formData = new FormData();
formData.append('topic_json', JSON.stringify(topic));
if (selectedPostTypeIdInput.value) {
formData.append('post_type_id', selectedPostTypeIdInput.value);
}
try {
const response = await fetch('/api/posts', {
method: 'POST',
body: formData
});
const data = await response.json();
const taskId = data.task_id;
currentVersionIndex = 0;
window.lastProgressData = null;
const pollInterval = setInterval(async () => {
const statusResponse = await fetch(`/api/tasks/${taskId}`);
const status = await statusResponse.json();
progressBar.style.width = `${status.progress}%`;
progressPercent.textContent = `${status.progress}%`;
progressMessage.textContent = status.message;
if (status.iteration !== undefined) {
iterationInfo.textContent = `Iteration ${status.iteration}/${status.max_iterations}`;
}
// Update live versions display
if (status.versions && status.versions.length > 0) {
window.lastProgressData = status;
// Auto-select latest version
if (status.versions.length > currentVersionIndex + 1) {
currentVersionIndex = status.versions.length - 1;
}
renderVersions(status.versions, status.feedback_list || []);
postResult.innerHTML = '<p class="text-gray-400">Siehe Live-Vorschau oben...</p>';
}
if (status.status === 'completed') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
// Keep live versions visible but update header
const result = status.result;
postResult.innerHTML = `
<div class="space-y-4">
<div class="flex items-center gap-2 text-sm flex-wrap">
<span class="px-2 py-1 rounded ${result.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
${result.approved ? 'Approved' : 'Review needed'}
</span>
<span class="text-gray-400">Score: ${result.final_score}/100</span>
<span class="text-gray-400">Iterations: ${result.iterations}</span>
</div>
<div class="text-sm text-gray-400 mb-2">Finaler Post:</div>
<div class="bg-brand-bg/50 rounded-lg p-4">
<pre class="whitespace-pre-wrap text-gray-200 font-sans">${result.final_post}</pre>
</div>
<div class="flex gap-2">
<button onclick="copyPost()" class="px-4 py-2 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-white transition-colors">
In Zwischenablage kopieren
</button>
<button onclick="toggleVersions()" class="px-4 py-2 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-white transition-colors">
Versionen ${liveVersions.classList.contains('hidden') ? 'anzeigen' : 'ausblenden'}
</button>
<a href="/posts/${result.post_id}" class="px-4 py-2 bg-brand-highlight hover:bg-brand-highlight/90 rounded-lg text-sm text-brand-bg-dark font-medium transition-colors">
Post öffnen
</a>
</div>
</div>
`;
} else if (status.status === 'error') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
postResult.innerHTML = `<p class="text-red-400">Fehler: ${status.message}</p>`;
}
}, 1000);
} catch (error) {
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
postResult.innerHTML = `<p class="text-red-400">Fehler: ${error.message}</p>`;
}
});
function copyPost() {
const postText = document.querySelector('#postResult pre').textContent;
navigator.clipboard.writeText(postText).then(() => {
alert('Post in Zwischenablage kopiert!');
});
}
function toggleVersions() {
liveVersions.classList.toggle('hidden');
}
// Load data on page load
loadData();
</script>
{% endblock %}

View File

@@ -0,0 +1,76 @@
{% extends "base.html" %}
{% block title %}Dashboard - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Dashboard</h1>
<p class="text-gray-400">Willkommen zurück, {{ session.linkedin_name or session.customer_name }}!</p>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
<strong>Error:</strong> {{ error }}
</div>
{% endif %}
<!-- Stats -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="card-bg rounded-xl border p-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-green-600/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
</div>
<div>
<p class="text-gray-400 text-sm">Generierte Posts</p>
<p class="text-2xl font-bold text-white">{{ total_posts or 0 }}</p>
</div>
</div>
</div>
<div class="card-bg rounded-xl border p-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
</div>
<div>
<p class="text-gray-400 text-sm">AI Agents</p>
<p class="text-2xl font-bold text-white">5</p>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card-bg rounded-xl border p-6">
<h2 class="text-xl font-semibold text-white mb-4">Schnellaktionen</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<a href="/research" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
</div>
<div>
<p class="font-medium text-white">Research</p>
<p class="text-sm text-gray-400">Topics finden</p>
</div>
</a>
<a href="/create" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
</div>
<div>
<p class="font-medium text-white">Post erstellen</p>
<p class="text-sm text-gray-400">Content generieren</p>
</div>
</a>
<a href="/posts" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
</div>
<div>
<p class="font-medium text-white">Meine Posts</p>
<p class="text-sm text-gray-400">Übersicht anzeigen</p>
</div>
</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - LinkedIn Posts</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'brand': {
'bg': '#3d4848',
'bg-light': '#4a5858',
'bg-dark': '#2d3838',
'highlight': '#ffc700',
'highlight-dark': '#e6b300',
},
'linkedin': '#0A66C2'
}
}
}
}
</script>
<style>
body { background-color: #3d4848; }
.btn-linkedin { background-color: #0A66C2; }
.btn-linkedin:hover { background-color: #004182; }
.card-bg { background-color: #4a5858; border-color: #5a6868; }
</style>
</head>
<body class="text-gray-100 min-h-screen flex items-center justify-center">
<div class="w-full max-w-md">
<div class="card-bg rounded-xl border p-8">
<div class="text-center mb-8">
<img src="/static/logo.png" alt="Logo" class="h-16 w-auto mx-auto mb-4">
<h1 class="text-2xl font-bold text-white mb-2">LinkedIn Posts</h1>
<p class="text-gray-400">AI Workflow System</p>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
{% if error == 'access_denied' %}
Zugriff verweigert. Bitte versuche es erneut.
{% elif error == 'unauthorized' %}
Dein LinkedIn-Profil ist nicht autorisiert.
{% else %}
Fehler bei der Anmeldung: {{ error }}
{% endif %}
</div>
{% endif %}
<div class="space-y-6">
<a href="/auth/linkedin" class="w-full btn-linkedin text-white font-medium py-3 px-4 rounded-lg transition-colors flex items-center justify-center gap-3">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
Mit LinkedIn anmelden
</a>
<p class="text-center text-gray-500 text-sm">
Melde dich mit deinem LinkedIn-Konto an, um auf das Dashboard zuzugreifen.
</p>
</div>
</div>
<div class="mt-6 text-center">
<a href="/admin/login" class="text-gray-500 hover:text-gray-300 text-sm">
Admin-Login
</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nicht autorisiert - LinkedIn Posts</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { background-color: #3d4848; }
.card-bg { background-color: #4a5858; border-color: #5a6868; }
</style>
</head>
<body class="text-gray-100 min-h-screen flex items-center justify-center">
<div class="w-full max-w-md">
<div class="card-bg rounded-xl border p-8 text-center">
<div class="w-20 h-20 bg-red-900/30 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<h1 class="text-2xl font-bold text-white mb-4">Nicht autorisiert</h1>
<p class="text-gray-400 mb-6">
Dein LinkedIn-Profil ist nicht mit einem Kundenkonto verknüpft.
Bitte kontaktiere den Administrator, um Zugang zu erhalten.
</p>
<div class="space-y-3">
<a href="/login" class="block w-full bg-brand-bg hover:bg-brand-bg-light text-white font-medium py-3 px-4 rounded-lg transition-colors">
Zurück zur Anmeldung
</a>
<a href="/admin/login" class="block w-full text-gray-400 hover:text-white text-sm transition-colors py-2">
Admin-Login
</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,698 @@
{% extends "base.html" %}
{% block title %}{{ post.topic_title }} - Post Details{% endblock %}
{% block head %}
<style>
.section-card {
background: rgba(61, 72, 72, 0.3);
border: 1px solid rgba(61, 72, 72, 0.6);
}
.title-truncate {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.reference-post {
background: linear-gradient(135deg, rgba(61, 72, 72, 0.4) 0%, rgba(45, 56, 56, 0.6) 100%);
border: 1px solid rgba(255, 199, 0, 0.15);
position: relative;
}
.reference-post::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(180deg, #ffc700 0%, rgba(255, 199, 0, 0.3) 100%);
border-radius: 3px 0 0 3px;
}
.profile-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.profile-item {
background: rgba(45, 56, 56, 0.5);
border-radius: 0.75rem;
padding: 1rem;
border: 1px solid rgba(61, 72, 72, 0.8);
}
.stat-bar {
height: 6px;
background: rgba(45, 56, 56, 0.8);
border-radius: 3px;
overflow: hidden;
}
.stat-bar-fill {
height: 100%;
background: linear-gradient(90deg, #ffc700 0%, #ffdb4d 100%);
border-radius: 3px;
transition: width 0.5s ease;
}
/* LinkedIn Preview styles */
.linkedin-preview {
background: #ffffff;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
color: rgba(0, 0, 0, 0.9);
overflow: hidden;
}
.linkedin-header {
padding: 12px 16px;
display: flex;
align-items: flex-start;
gap: 8px;
}
.linkedin-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #0a66c2 0%, #004182 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 18px;
flex-shrink: 0;
overflow: hidden;
}
.linkedin-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.linkedin-user-info {
flex: 1;
min-width: 0;
}
.linkedin-name {
font-weight: 600;
font-size: 14px;
color: rgba(0, 0, 0, 0.9);
line-height: 1.3;
}
.linkedin-headline {
font-size: 12px;
color: rgba(0, 0, 0, 0.6);
line-height: 1.3;
margin-top: 2px;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.linkedin-timestamp {
font-size: 12px;
color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
gap: 4px;
margin-top: 2px;
}
.linkedin-content {
padding: 0 16px 12px;
font-size: 14px;
line-height: 1.5;
color: rgba(0, 0, 0, 0.9);
white-space: pre-wrap;
word-wrap: break-word;
}
.linkedin-content.collapsed {
max-height: 120px;
overflow: hidden;
position: relative;
}
.linkedin-content.collapsed::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: linear-gradient(transparent, white);
}
.linkedin-see-more {
padding: 0 16px 12px;
font-size: 14px;
color: rgba(0, 0, 0, 0.6);
cursor: pointer;
font-weight: 600;
}
.linkedin-see-more:hover {
color: #0a66c2;
text-decoration: underline;
}
.linkedin-engagement {
padding: 8px 16px;
border-top: 1px solid rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: rgba(0, 0, 0, 0.6);
}
.linkedin-actions {
display: flex;
border-top: 1px solid rgba(0, 0, 0, 0.08);
}
.linkedin-action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 12px 8px;
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.6);
background: transparent;
border: none;
cursor: default;
transition: background 0.15s;
}
.linkedin-action-btn svg {
width: 20px;
height: 20px;
}
.linkedin-more-btn {
padding: 4px;
color: rgba(0, 0, 0, 0.6);
border-radius: 50%;
cursor: default;
}
.view-toggle {
display: inline-flex;
background: rgba(61, 72, 72, 0.5);
border-radius: 8px;
padding: 2px;
}
.view-toggle-btn {
padding: 6px 12px;
font-size: 12px;
font-weight: 500;
border-radius: 6px;
color: #9ca3af;
transition: all 0.2s;
}
.view-toggle-btn.active {
background: rgba(255, 199, 0, 0.2);
color: #ffc700;
}
.view-toggle-btn:hover:not(.active) {
color: #e5e7eb;
}
/* Tab styles */
.tab-btn {
position: relative;
padding: 0.75rem 1.25rem;
font-weight: 500;
color: #9ca3af;
transition: all 0.2s ease;
border-bottom: 2px solid transparent;
}
.tab-btn:hover {
color: #e5e7eb;
}
.tab-btn.active {
color: #ffc700;
border-bottom-color: #ffc700;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: #ffc700;
}
.tab-content {
display: none;
animation: fadeIn 0.2s ease;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
{% endblock %}
{% block content %}
<!-- Breadcrumb & Header -->
<div class="mb-6">
<a href="/posts" class="inline-flex items-center gap-2 text-gray-400 hover:text-brand-highlight transition-colors mb-4">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
Zurück zu meinen Posts
</a>
<div class="flex items-start justify-between gap-4">
<div class="flex-1 min-w-0">
<h1 class="text-2xl font-bold text-white mb-2 title-truncate" title="{{ post.topic_title or 'Untitled Post' }}">
{{ post.topic_title or 'Untitled Post' }}
</h1>
<div class="flex items-center gap-3 text-sm text-gray-400 flex-wrap">
<span>{{ post.created_at.strftime('%d.%m.%Y um %H:%M Uhr') if post.created_at else 'N/A' }}</span>
<span class="text-gray-600">|</span>
<span>{{ post.iterations }} Iteration{{ 's' if post.iterations != 1 else '' }}</span>
</div>
</div>
<div class="flex items-center gap-3 flex-shrink-0">
<span class="px-3 py-1.5 rounded-lg text-sm font-medium {{ 'bg-green-600/30 text-green-300 border border-green-600/50' if post.status == 'approved' else 'bg-yellow-600/30 text-yellow-300 border border-yellow-600/50' }}">
{{ post.status | capitalize }}
</span>
{% if final_feedback %}
<span class="px-3 py-1.5 rounded-lg text-sm font-bold {{ 'bg-green-600/30 text-green-300' if final_feedback.overall_score >= 85 else 'bg-yellow-600/30 text-yellow-300' if final_feedback.overall_score >= 70 else 'bg-red-600/30 text-red-300' }}">
Score: {{ final_feedback.overall_score }}/100
</span>
{% endif %}
</div>
</div>
</div>
<!-- Tab Navigation -->
<div class="mb-6 border-b border-brand-bg-light">
<nav class="flex gap-1" role="tablist">
<button class="tab-btn active" onclick="switchTab('ergebnis')" role="tab" aria-selected="true" data-tab="ergebnis">
<svg class="w-4 h-4 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Ergebnis
</button>
{% if post.writer_versions and post.writer_versions | length > 0 %}
<button class="tab-btn" onclick="switchTab('iterationen')" role="tab" aria-selected="false" data-tab="iterationen">
<svg class="w-4 h-4 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Iterationen
<span class="ml-1 px-1.5 py-0.5 text-xs bg-brand-bg rounded">{{ post.writer_versions | length }}</span>
</button>
{% endif %}
</nav>
</div>
<!-- Tab: Ergebnis (mit Sidebar) -->
<div id="tab-ergebnis" class="tab-content active">
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
<!-- Post Content -->
<div class="xl:col-span-2">
<div class="section-card rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white flex items-center gap-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Finaler Post
</h2>
<div class="flex items-center gap-3">
<!-- View Toggle -->
<div class="view-toggle">
<button onclick="setView('preview')" id="previewToggle" class="view-toggle-btn active">
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
Preview
</button>
<button onclick="setView('raw')" id="rawToggle" class="view-toggle-btn">
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
Raw
</button>
</div>
<button onclick="copyToClipboard()" class="px-3 py-1.5 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-gray-300 transition-colors flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
Kopieren
</button>
</div>
</div>
<!-- LinkedIn Preview View -->
<div id="linkedinPreview" class="linkedin-preview shadow-lg">
<div class="linkedin-header">
<div class="linkedin-avatar">
{% if profile_picture_url %}
<img src="{{ profile_picture_url }}" alt="{{ session.linkedin_name }}" loading="lazy" referrerpolicy="no-referrer">
{% else %}
{{ session.linkedin_name[:2] | upper if session.linkedin_name else 'UN' }}
{% endif %}
</div>
<div class="linkedin-user-info">
<div class="linkedin-name">{{ session.linkedin_name or 'LinkedIn User' }}</div>
<div class="linkedin-headline">{{ session.customer_name or 'LinkedIn Member' }}</div>
<div class="linkedin-timestamp">
<span>Jetzt</span>
<span></span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 1a7 7 0 107 7 7 7 0 00-7-7zM3 8a5 5 0 011-3l.55.55A1.5 1.5 0 015 6.62v1.07a.75.75 0 00.22.53l.56.56a.75.75 0 00.53.22H7v.69a.75.75 0 00.22.53l.56.56a.75.75 0 01.22.53V13a5 5 0 01-5-5zm6.24 4.83l2-2.46a.75.75 0 00.09-.8l-.58-1.16A.76.76 0 0010 8H7v-.19a.51.51 0 01.28-.45l.38-.19a.74.74 0 00.3-1L7.4 5.19a.75.75 0 00-.67-.41H5.67a.75.75 0 01-.44-.14l-.34-.26a5 5 0 017.35 8.44z"/>
</svg>
</div>
</div>
<div class="linkedin-more-btn">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M14 12a2 2 0 11-4 0 2 2 0 014 0zM4 12a2 2 0 11-4 0 2 2 0 014 0zm16 0a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
</div>
</div>
<div id="linkedinContent" class="linkedin-content collapsed">{{ post.post_content }}</div>
<div id="seeMoreBtn" class="linkedin-see-more" onclick="toggleContent()">...mehr anzeigen</div>
<div class="linkedin-engagement">
<svg width="16" height="16" viewBox="0 0 24 24" fill="#0a66c2">
<path d="M19.46 11l-3.91-3.91a7 7 0 01-1.69-2.74l-.49-1.47A2.76 2.76 0 0010.76 1 2.75 2.75 0 008 3.74v1.12a9.19 9.19 0 00.46 2.85L8.89 9H4.12A2.12 2.12 0 002 11.12a2.16 2.16 0 00.92 1.76A2.11 2.11 0 002 14.62a2.14 2.14 0 001.28 2 2 2 0 00-.28 1 2.12 2.12 0 002 2.12v.14A2.12 2.12 0 007.12 22h7.49a8.08 8.08 0 003.58-.84l.31-.16H21V11z"/>
</svg>
<svg width="16" height="16" viewBox="0 0 24 24" fill="#df704d" style="margin-left: -6px;">
<path d="M22.51 11.35a5.84 5.84 0 00-2.53-4.83 5.71 5.71 0 00-5.36-.58 5.64 5.64 0 00-2.62 2.09 5.64 5.64 0 00-2.62-2.09 5.71 5.71 0 00-5.36.58 5.84 5.84 0 00-2.53 4.83 6.6 6.6 0 00.49 2.56c1 2.33 9.91 8.09 9.91 8.09s8.92-5.76 9.91-8.09a6.6 6.6 0 00.71-2.56z"/>
</svg>
<span style="margin-left: 4px;">42</span>
<span style="margin-left: auto;">12 Kommentare • 3 Reposts</span>
</div>
<div class="linkedin-actions">
<button class="linkedin-action-btn">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19.46 11l-3.91-3.91a7 7 0 01-1.69-2.74l-.49-1.47A2.76 2.76 0 0010.76 1 2.75 2.75 0 008 3.74v1.12a9.19 9.19 0 00.46 2.85L8.89 9H4.12A2.12 2.12 0 002 11.12a2.16 2.16 0 00.92 1.76A2.11 2.11 0 002 14.62a2.14 2.14 0 001.28 2 2 2 0 00-.28 1 2.12 2.12 0 002 2.12v.14A2.12 2.12 0 007.12 22h7.49a8.08 8.08 0 003.58-.84l.31-.16H21V11z"/>
</svg>
Gefällt mir
</button>
<button class="linkedin-action-btn">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M7 9h10v1H7zm0 4h7v-1H7zm16-2a6.78 6.78 0 01-2.84 5.61L12 22v-4H8A7 7 0 018 4h8a7 7 0 017 7z"/>
</svg>
Kommentieren
</button>
<button class="linkedin-action-btn">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M13.96 5H6c-.55 0-1 .45-1 1v11H3V6c0-1.66 1.34-3 3-3h7.96L12 0l1.96 5zM17 7h-7c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h7c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2z"/>
</svg>
Reposten
</button>
<button class="linkedin-action-btn">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M21 3L0 10l7.66 4.26L16 8l-6.26 8.34L14 24l7-21z"/>
</svg>
Senden
</button>
</div>
</div>
<!-- Raw View -->
<div id="rawView" class="bg-brand-bg/50 rounded-lg p-5 hidden">
<pre id="finalPostContent" class="whitespace-pre-wrap text-gray-200 font-sans text-sm leading-relaxed">{{ post.post_content }}</pre>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Score Breakdown -->
{% if final_feedback and final_feedback.scores %}
<div class="section-card rounded-xl p-6">
<h3 class="font-semibold text-white mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Score-Aufschlüsselung
</h3>
<div class="space-y-4">
<div>
<div class="flex justify-between text-sm mb-2">
<span class="text-gray-400">Authentizität & Stil</span>
<span class="text-white font-medium">{{ final_feedback.scores.authenticity_and_style }}/40</span>
</div>
<div class="stat-bar">
<div class="stat-bar-fill" style="width: {{ (final_feedback.scores.authenticity_and_style / 40 * 100) | int }}%"></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm mb-2">
<span class="text-gray-400">Content-Qualität</span>
<span class="text-white font-medium">{{ final_feedback.scores.content_quality }}/35</span>
</div>
<div class="stat-bar">
<div class="stat-bar-fill" style="width: {{ (final_feedback.scores.content_quality / 35 * 100) | int }}%"></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm mb-2">
<span class="text-gray-400">Technische Umsetzung</span>
<span class="text-white font-medium">{{ final_feedback.scores.technical_execution }}/25</span>
</div>
<div class="stat-bar">
<div class="stat-bar-fill" style="width: {{ (final_feedback.scores.technical_execution / 25 * 100) | int }}%"></div>
</div>
</div>
<div class="pt-3 border-t border-brand-bg-light">
<div class="flex justify-between">
<span class="text-gray-300 font-medium">Gesamt</span>
<span class="text-brand-highlight font-bold text-lg">{{ final_feedback.overall_score }}/100</span>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Final Feedback Summary -->
{% if final_feedback %}
<div class="section-card rounded-xl p-6">
<h3 class="font-semibold text-white mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg>
Finales Feedback
</h3>
<p class="text-sm text-gray-300 mb-4">{{ final_feedback.feedback }}</p>
{% if final_feedback.strengths %}
<div class="bg-green-900/10 rounded-lg p-3 border border-green-600/20">
<span class="text-xs font-medium text-green-400 block mb-2">Stärken</span>
<ul class="space-y-1">
{% for s in final_feedback.strengths %}
<li class="text-sm text-gray-400 flex items-start gap-2"><span class="text-green-400">+</span> {{ s }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% endif %}
<!-- Quick Actions -->
<div class="section-card rounded-xl p-6">
<h3 class="font-semibold text-white mb-4">Aktionen</h3>
<div class="space-y-2">
<button onclick="copyToClipboard()" class="w-full px-4 py-2.5 bg-brand-highlight hover:bg-brand-highlight/90 text-brand-bg-dark font-medium rounded-lg transition-colors flex items-center justify-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
Post kopieren
</button>
<a href="/create" class="w-full px-4 py-2.5 bg-brand-bg hover:bg-brand-bg-light text-white rounded-lg transition-colors flex items-center justify-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
Neuen Post erstellen
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: Iterationen -->
{% if post.writer_versions and post.writer_versions | length > 0 %}
<div id="tab-iterationen" class="tab-content">
<div class="section-card rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white flex items-center gap-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Iterationen
</h2>
<!-- Pagination Controls -->
<div class="flex items-center gap-2">
<button id="prevVersion" onclick="changeVersion(-1)" class="p-2 rounded-lg bg-brand-bg hover:bg-brand-bg-light text-gray-400 hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
</button>
<span class="text-sm text-gray-400">
<span id="currentVersionNum">1</span> / {{ post.writer_versions | length }}
</span>
<button id="nextVersion" onclick="changeVersion(1)" class="p-2 rounded-lg bg-brand-bg hover:bg-brand-bg-light text-gray-400 hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</button>
</div>
</div>
<!-- Version Content Container -->
{% for i in range(post.writer_versions | length) %}
<div class="version-panel {% if i == 0 %}block{% else %}hidden{% endif %}" data-version="{{ i }}">
<div class="flex items-center justify-between mb-3">
<span class="px-3 py-1 bg-brand-highlight/20 text-brand-highlight rounded-lg text-sm font-medium">Version {{ i + 1 }}</span>
{% if post.critic_feedback and i < post.critic_feedback | length %}
<div class="flex items-center gap-2">
<span class="px-2 py-1 rounded text-xs font-medium {{ 'bg-green-600/30 text-green-300' if post.critic_feedback[i].overall_score >= 85 else 'bg-yellow-600/30 text-yellow-300' }}">
Score: {{ post.critic_feedback[i].overall_score }}/100
</span>
{% if post.critic_feedback[i].approved %}
<span class="px-2 py-1 bg-green-600/30 text-green-300 rounded text-xs">Approved</span>
{% endif %}
</div>
{% endif %}
</div>
<div class="bg-brand-bg/30 rounded-lg p-4 mb-4">
<pre class="whitespace-pre-wrap text-gray-300 font-sans text-sm">{{ post.writer_versions[i] }}</pre>
</div>
{% if post.critic_feedback and i < post.critic_feedback | length %}
{% set fb = post.critic_feedback[i] %}
<div class="bg-brand-bg/20 rounded-lg p-4 border border-brand-bg-light">
<h4 class="text-sm font-medium text-white mb-2 flex items-center gap-2">
<svg class="w-4 h-4 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg>
Critic Feedback
</h4>
<p class="text-sm text-gray-400 mb-3">{{ fb.feedback }}</p>
<div class="grid md:grid-cols-2 gap-4">
{% if fb.strengths %}
<div class="bg-green-900/10 rounded-lg p-3 border border-green-600/20">
<span class="text-xs font-medium text-green-400 block mb-2">Stärken</span>
<ul class="space-y-1 text-sm text-gray-400">
{% for s in fb.strengths %}
<li class="flex items-start gap-2"><span class="text-green-400 mt-0.5">+</span> {{ s }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if fb.improvements %}
<div class="bg-yellow-900/10 rounded-lg p-3 border border-yellow-600/20">
<span class="text-xs font-medium text-yellow-400 block mb-2">Verbesserungen</span>
<ul class="space-y-1 text-sm text-gray-400">
{% for imp in fb.improvements %}
<li class="flex items-start gap-2"><span class="text-yellow-400 mt-0.5">-</span> {{ imp }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
let currentVersion = 0;
const totalVersions = {{ post.writer_versions | length if post.writer_versions else 0 }};
let currentView = 'preview';
let contentExpanded = false;
// LinkedIn Preview functions
function setView(view) {
currentView = view;
const previewEl = document.getElementById('linkedinPreview');
const rawEl = document.getElementById('rawView');
const previewToggle = document.getElementById('previewToggle');
const rawToggle = document.getElementById('rawToggle');
if (view === 'preview') {
previewEl.classList.remove('hidden');
rawEl.classList.add('hidden');
previewToggle.classList.add('active');
rawToggle.classList.remove('active');
} else {
previewEl.classList.add('hidden');
rawEl.classList.remove('hidden');
previewToggle.classList.remove('active');
rawToggle.classList.add('active');
}
}
function toggleContent() {
const contentEl = document.getElementById('linkedinContent');
const seeMoreBtn = document.getElementById('seeMoreBtn');
if (contentExpanded) {
contentEl.classList.add('collapsed');
seeMoreBtn.textContent = '...mehr anzeigen';
seeMoreBtn.style.display = 'block';
} else {
contentEl.classList.remove('collapsed');
seeMoreBtn.textContent = '...weniger anzeigen';
}
contentExpanded = !contentExpanded;
}
function initLinkedInPreview() {
const contentEl = document.getElementById('linkedinContent');
const seeMoreBtn = document.getElementById('seeMoreBtn');
if (contentEl && seeMoreBtn) {
// Check if content is short enough to not need "see more"
const maxCollapsedHeight = 120;
contentEl.classList.remove('collapsed');
const fullHeight = contentEl.scrollHeight;
contentEl.classList.add('collapsed');
if (fullHeight <= maxCollapsedHeight) {
// Content is short, hide "see more" button and remove collapsed class
seeMoreBtn.style.display = 'none';
contentEl.classList.remove('collapsed');
}
}
}
// Tab switching function
function switchTab(tabName) {
// Hide all tab contents
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// Deactivate all tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
btn.setAttribute('aria-selected', 'false');
});
// Show selected tab content
const tabContent = document.getElementById('tab-' + tabName);
if (tabContent) {
tabContent.classList.add('active');
}
// Activate selected tab button
const tabBtn = document.querySelector(`.tab-btn[data-tab="${tabName}"]`);
if (tabBtn) {
tabBtn.classList.add('active');
tabBtn.setAttribute('aria-selected', 'true');
}
}
function copyToClipboard() {
const content = document.getElementById('finalPostContent').textContent;
navigator.clipboard.writeText(content).then(() => {
const btn = event.target.closest('button');
const originalHTML = btn.innerHTML;
btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> Kopiert!';
setTimeout(() => btn.innerHTML = originalHTML, 2000);
});
}
function changeVersion(delta) {
const newVersion = currentVersion + delta;
if (newVersion < 0 || newVersion >= totalVersions) return;
// Hide current
document.querySelector(`.version-panel[data-version="${currentVersion}"]`).classList.add('hidden');
document.querySelector(`.version-panel[data-version="${currentVersion}"]`).classList.remove('block');
// Show new
currentVersion = newVersion;
document.querySelector(`.version-panel[data-version="${currentVersion}"]`).classList.remove('hidden');
document.querySelector(`.version-panel[data-version="${currentVersion}"]`).classList.add('block');
// Update counter
document.getElementById('currentVersionNum').textContent = currentVersion + 1;
// Update button states
document.getElementById('prevVersion').disabled = currentVersion === 0;
document.getElementById('nextVersion').disabled = currentVersion === totalVersions - 1;
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
if (totalVersions > 0) {
document.getElementById('prevVersion').disabled = true;
document.getElementById('nextVersion').disabled = totalVersions <= 1;
}
// Initialize LinkedIn preview
initLinkedInPreview();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,114 @@
{% extends "base.html" %}
{% block title %}Meine Posts - LinkedIn Posts{% endblock %}
{% block head %}
<style>
.post-card {
background: linear-gradient(135deg, rgba(61, 72, 72, 0.3) 0%, rgba(45, 56, 56, 0.4) 100%);
border: 1px solid rgba(61, 72, 72, 0.6);
transition: all 0.2s ease;
}
.post-card:hover {
border-color: rgba(255, 199, 0, 0.3);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.score-ring {
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.75rem;
}
.score-high { background: rgba(34, 197, 94, 0.2); border: 2px solid rgba(34, 197, 94, 0.5); color: #86efac; }
.score-medium { background: rgba(234, 179, 8, 0.2); border: 2px solid rgba(234, 179, 8, 0.5); color: #fde047; }
.score-low { background: rgba(239, 68, 68, 0.2); border: 2px solid rgba(239, 68, 68, 0.5); color: #fca5a5; }
</style>
{% endblock %}
{% block content %}
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-white mb-2">Meine Posts</h1>
<p class="text-gray-400">{{ total_posts }} generierte Posts</p>
</div>
<a href="/create" class="px-4 py-2.5 btn-primary rounded-lg font-medium flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
Neuer Post
</a>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
<strong>Error:</strong> {{ error }}
</div>
{% endif %}
{% if posts %}
<div class="card-bg rounded-xl border overflow-hidden">
<div class="p-4">
<div class="grid gap-3">
{% for post in posts %}
<a href="/posts/{{ post.id }}" class="post-card rounded-xl p-4 block group">
<div class="flex items-center gap-4">
<!-- Score Circle -->
{% if post.critic_feedback and post.critic_feedback | length > 0 %}
{% set score = post.critic_feedback[-1].overall_score %}
<div class="score-ring flex-shrink-0 {{ 'score-high' if score >= 85 else 'score-medium' if score >= 70 else 'score-low' }}">
{{ score }}
</div>
{% else %}
<div class="score-ring flex-shrink-0 bg-brand-bg-dark border-2 border-brand-bg-light text-gray-500">
</div>
{% endif %}
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-3">
<h4 class="font-medium text-white group-hover:text-brand-highlight transition-colors truncate">
{{ post.topic_title or 'Untitled' }}
</h4>
<span class="flex-shrink-0 px-2 py-0.5 text-xs rounded font-medium {{ 'bg-green-600/20 text-green-400 border border-green-600/30' if post.status == 'approved' else 'bg-yellow-600/20 text-yellow-400 border border-yellow-600/30' }}">
{{ post.status | capitalize }}
</span>
</div>
<div class="flex items-center gap-4 mt-1.5 text-sm text-gray-500">
<span class="flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}
</span>
<span class="flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
{{ post.iterations }} Iteration{{ 's' if post.iterations != 1 else '' }}
</span>
</div>
</div>
<!-- Arrow -->
<svg class="w-5 h-5 text-gray-600 group-hover:text-brand-highlight transition-colors flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</a>
{% endfor %}
</div>
</div>
</div>
{% else %}
<div class="card-bg rounded-xl border p-12 text-center">
<div class="w-20 h-20 bg-brand-bg rounded-2xl flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">Noch keine Posts</h3>
<p class="text-gray-400 mb-6 max-w-md mx-auto">Erstelle deinen ersten LinkedIn Post mit KI-Unterstützung.</p>
<a href="/create" class="inline-flex items-center gap-2 px-6 py-3 btn-primary font-medium rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
Post erstellen
</a>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,185 @@
{% extends "base.html" %}
{% block title %}Research Topics - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Research Topics</h1>
<p class="text-gray-400">Recherchiere neue Content-Themen mit Perplexity AI</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left: Form -->
<div>
<form id="researchForm" class="card-bg rounded-xl border p-6">
<!-- Post Type Selection -->
<div id="postTypeArea" class="mb-6">
<label class="block text-sm font-medium text-gray-300 mb-2">Post-Typ (optional)</label>
<div id="postTypeCards" class="grid grid-cols-2 gap-2 mb-2">
<div class="text-gray-500 text-sm">Lade Post-Typen...</div>
</div>
<p class="text-xs text-gray-500">Wähle einen Post-Typ für gezielte Recherche oder lasse leer für allgemeine Recherche.</p>
<input type="hidden" name="post_type_id" id="selectedPostTypeId" value="">
</div>
<!-- Progress Area -->
<div id="progressArea" class="hidden mb-6">
<div class="bg-brand-bg rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span id="progressMessage" class="text-gray-300">Starte Recherche...</span>
<span id="progressPercent" class="text-gray-400">0%</span>
</div>
<div class="w-full bg-brand-bg-dark rounded-full h-2">
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</div>
<button type="submit" id="submitBtn" class="w-full btn-primary font-medium py-3 rounded-lg transition-colors flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
Research starten
</button>
</form>
</div>
<!-- Right: Results -->
<div>
<div id="resultsArea" class="card-bg rounded-xl border p-6">
<h3 class="text-lg font-semibold text-white mb-4">Gefundene Topics</h3>
<div id="topicsList" class="space-y-4">
<p class="text-gray-400">Starte eine Recherche um Topics zu finden...</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const form = document.getElementById('researchForm');
const submitBtn = document.getElementById('submitBtn');
const progressArea = document.getElementById('progressArea');
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const progressPercent = document.getElementById('progressPercent');
const topicsList = document.getElementById('topicsList');
const postTypeCards = document.getElementById('postTypeCards');
const selectedPostTypeId = document.getElementById('selectedPostTypeId');
let currentPostTypes = [];
// Load post types on page load
async function loadPostTypes() {
try {
const response = await fetch('/api/post-types');
const data = await response.json();
if (data.post_types && data.post_types.length > 0) {
currentPostTypes = data.post_types;
postTypeCards.innerHTML = `
<button type="button" onclick="selectPostType('')" id="pt_all"
class="p-3 rounded-lg border text-left transition-colors bg-brand-highlight/20 border-brand-highlight text-white">
<div class="font-medium text-sm">Alle Typen</div>
<div class="text-xs text-gray-400 mt-1">Allgemeine Recherche</div>
</button>
` + data.post_types.map(pt => `
<button type="button" onclick="selectPostType('${pt.id}')" id="pt_${pt.id}"
class="p-3 rounded-lg border text-left transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
<div class="font-medium text-sm">${pt.name}</div>
<div class="text-xs text-gray-400 mt-1">${pt.analyzed_post_count || 0} Posts analysiert</div>
${pt.has_analysis ? '<span class="text-xs text-green-400">Analyse</span>' : ''}
</button>
`).join('');
} else {
postTypeCards.innerHTML = '<p class="text-gray-500 text-sm col-span-2">Keine Post-Typen konfiguriert.</p>';
}
} catch (error) {
console.error('Failed to load post types:', error);
postTypeCards.innerHTML = '<p class="text-red-400 text-sm col-span-2">Fehler beim Laden.</p>';
}
}
function selectPostType(typeId) {
selectedPostTypeId.value = typeId;
document.querySelectorAll('[id^="pt_"]').forEach(card => {
if (card.id === `pt_${typeId}` || (typeId === '' && card.id === 'pt_all')) {
card.className = 'p-3 rounded-lg border text-left transition-colors bg-brand-highlight/20 border-brand-highlight text-white';
} else {
card.className = 'p-3 rounded-lg border text-left transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white';
}
});
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
submitBtn.disabled = true;
submitBtn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Recherchiert...';
progressArea.classList.remove('hidden');
const formData = new FormData();
if (selectedPostTypeId.value) {
formData.append('post_type_id', selectedPostTypeId.value);
}
try {
const response = await fetch('/api/research', {
method: 'POST',
body: formData
});
const data = await response.json();
const taskId = data.task_id;
const pollInterval = setInterval(async () => {
const statusResponse = await fetch(`/api/tasks/${taskId}`);
const status = await statusResponse.json();
progressBar.style.width = `${status.progress}%`;
progressPercent.textContent = `${status.progress}%`;
progressMessage.textContent = status.message;
if (status.status === 'completed') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
if (status.topics && status.topics.length > 0) {
topicsList.innerHTML = status.topics.map((topic, i) => `
<div class="bg-brand-bg rounded-lg p-4 border border-brand-bg-light">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<span class="inline-block px-2 py-1 text-xs font-medium bg-brand-highlight/20 text-brand-highlight rounded mb-2">${topic.category || 'Topic'}</span>
<h4 class="font-semibold text-white">${topic.title}</h4>
${topic.angle ? `<p class="text-sm text-brand-highlight/80 mt-1">↳ ${topic.angle}</p>` : ''}
${topic.hook_idea ? `<p class="text-sm text-gray-300 mt-2 italic border-l-2 border-brand-highlight/30 pl-2">"${topic.hook_idea.substring(0, 150)}..."</p>` : ''}
<p class="text-gray-400 text-sm mt-2">${topic.fact ? topic.fact.substring(0, 200) + '...' : ''}</p>
${topic.source ? `<p class="text-gray-500 text-xs mt-2">Quelle: ${topic.source}</p>` : ''}
</div>
</div>
</div>
`).join('');
} else {
topicsList.innerHTML = '<p class="text-gray-400">Keine Topics gefunden.</p>';
}
} else if (status.status === 'error') {
clearInterval(pollInterval);
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
topicsList.innerHTML = `<p class="text-red-400">Fehler: ${status.message}</p>`;
}
}, 1000);
} catch (error) {
progressArea.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
topicsList.innerHTML = `<p class="text-red-400">Fehler: ${error.message}</p>`;
}
});
// Load post types on page load
loadPostTypes();
</script>
{% endblock %}

View File

@@ -0,0 +1,142 @@
{% extends "base.html" %}
{% block title %}Status - LinkedIn Posts{% endblock %}
{% block content %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Status</h1>
<p class="text-gray-400">Übersicht über deinen Setup-Status</p>
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
<strong>Error:</strong> {{ error }}
</div>
{% endif %}
{% if status %}
<div class="card-bg rounded-xl border overflow-hidden">
<!-- Header -->
<div class="px-6 py-4 border-b border-brand-bg-light">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-full flex items-center justify-center overflow-hidden {{ 'bg-brand-highlight' if not profile_picture else '' }}">
{% if profile_picture %}
<img src="{{ profile_picture }}" alt="{{ customer.name }}" class="w-full h-full object-cover" loading="lazy" referrerpolicy="no-referrer">
{% else %}
<span class="text-brand-bg-dark font-bold text-lg">{{ customer.name[0] | upper }}</span>
{% endif %}
</div>
<div>
<h3 class="font-semibold text-white text-lg">{{ customer.name }}</h3>
<p class="text-sm text-gray-400">{{ customer.company_name or 'Kein Unternehmen' }}</p>
</div>
</div>
<div class="flex items-center gap-2">
{% if status.ready_for_posts %}
<span class="px-3 py-1.5 bg-green-600/30 text-green-300 rounded-lg text-sm font-medium flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Bereit für Posts
</span>
{% else %}
<span class="px-3 py-1.5 bg-yellow-600/30 text-yellow-300 rounded-lg text-sm font-medium flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
Setup unvollständig
</span>
{% endif %}
</div>
</div>
</div>
<!-- Status Grid -->
<div class="p-6">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<!-- Scraped Posts -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
{% if status.has_scraped_posts %}
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
{% else %}
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
{% endif %}
<span class="text-sm text-gray-400">Scraped Posts</span>
</div>
<p class="text-2xl font-bold text-white">{{ status.scraped_posts_count }}</p>
</div>
<!-- Profile Analysis -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
{% if status.has_profile_analysis %}
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
{% else %}
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
{% endif %}
<span class="text-sm text-gray-400">Profil Analyse</span>
</div>
<p class="text-lg font-semibold text-white">{{ 'Vorhanden' if status.has_profile_analysis else 'Fehlt' }}</p>
</div>
<!-- Research Topics -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
{% if status.research_count > 0 %}
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
{% else %}
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
{% endif %}
<span class="text-sm text-gray-400">Research Topics</span>
</div>
<p class="text-2xl font-bold text-white">{{ status.research_count }}</p>
</div>
<!-- Generated Posts -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
<span class="text-sm text-gray-400">Generierte Posts</span>
</div>
<p class="text-2xl font-bold text-white">{{ status.posts_count }}</p>
</div>
</div>
<!-- Missing Items -->
{% if status.missing_items %}
<div class="bg-yellow-900/20 border border-yellow-600/50 rounded-lg p-4">
<h4 class="font-medium text-yellow-300 mb-2 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
Fehlende Elemente
</h4>
<ul class="space-y-1">
{% for item in status.missing_items %}
<li class="text-yellow-200/80 text-sm flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
{{ item }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Quick Actions -->
<div class="flex gap-3 mt-4">
{% if status.research_count == 0 %}
<a href="/research" class="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg text-sm text-white transition-colors">
Recherche starten
</a>
{% endif %}
{% if status.ready_for_posts %}
<a href="/create" class="px-4 py-2 btn-primary rounded-lg text-sm transition-colors">
Post erstellen
</a>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="card-bg rounded-xl border p-12 text-center">
<svg class="w-16 h-16 text-gray-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
<h3 class="text-xl font-semibold text-white mb-2">Status nicht verfügbar</h3>
<p class="text-gray-400 mb-6">Es konnte kein Status geladen werden.</p>
</div>
{% endif %}
{% endblock %}