homelab-brain/redax-wp/src/templates/index.html
2026-03-03 16:19:53 +07:00

817 lines
39 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Redax-WP — Studio{% endblock %}
{% block extra_head %}
<style>
#wp-preview, #wp-preview p, #wp-preview h1, #wp-preview h2, #wp-preview h3, #wp-preview li, #wp-preview span { color: #0f172a !important; }
#wp-preview a { color: #2563eb !important; }
</style>
{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-6 py-6" data-wp-admin="{{ wp_admin_direct }}" data-wp-url="{{ wp_url }}">
<!-- Status-Bar -->
<div class="flex items-center gap-4 mb-4 text-xs text-slate-500 flex-wrap">
{% if last_published %}
<span>Letzter Post: <span class="text-slate-300">{{ last_published.wp_url[:50] if last_published.wp_url else last_published.title[:40] }}</span> — {{ last_published.published_at[:16] if last_published.published_at else '' }}</span>
{% endif %}
{% if queue_count > 0 %}
<a href="/feeds" class="text-yellow-400 hover:text-yellow-300">📥 {{ queue_count }} RSS-Artikel in Queue</a>
{% endif %}
</div>
<!-- WordPress-Targets (einklappbar) -->
<div class="mb-3">
<button type="button" onclick="var b=document.getElementById('wp-targets-box');b.classList.toggle('hidden');document.getElementById('wp-targets-chevron').textContent=b.classList.contains('hidden')?'▸':'▾'"
class="text-xs text-slate-500 hover:text-slate-300 flex items-center gap-1.5 py-1">
<span id="wp-targets-chevron"></span> 📡 Publish-Ziele
</button>
<div id="wp-targets-box" class="hidden mt-2" style="display:flex;flex-wrap:wrap;gap:0.75rem">
{% for t in wp_targets %}
<div style="background:#1e293b;border:1px solid #334155;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<!-- Checkbox (Primary gesperrt) -->
{% if t.primary %}
<label style="cursor:default;color:#93c5fd;font-size:0.75rem;white-space:nowrap">
<input type="checkbox" checked disabled style="accent-color:#3b82f6"> {{ t.label }}
<span style="opacity:0.5;font-size:0.65rem">(Primary)</span>
</label>
{% else %}
<form method="POST" action="/targets/toggle" style="display:inline;margin:0">
<input type="hidden" name="name" value="{{ t.name }}">
<label style="cursor:pointer;color:{% if t.enabled %}#4ade80{% else %}#64748b{% endif %};font-size:0.75rem;white-space:nowrap">
<input type="checkbox" {% if t.enabled %}checked{% endif %}
onchange="this.form.submit()"
style="accent-color:#22c55e;cursor:pointer"> {{ t.label }}
</label>
</form>
{% endif %}
<!-- Trennlinie -->
<span style="color:#334155;font-size:0.8rem">|</span>
<!-- Website-Button -->
<a href="{{ t.url }}" target="_blank"
style="font-size:0.72rem;padding:3px 10px;border-radius:5px;background:#0f172a;
border:1px solid #334155;color:#94a3b8;text-decoration:none;white-space:nowrap"
title="{{ t.url }}">🌐 Website</a>
<!-- Admin-Button: direkte URL (bypass Cloudflare WAF) -->
<a href="{{ t.admin_url }}" target="_blank"
style="font-size:0.72rem;padding:3px 10px;border-radius:5px;background:#0f172a;
border:1px solid #334155;color:#94a3b8;text-decoration:none;white-space:nowrap"
title="Login: {{ t.username }}">⚙️ WP-Admin</a>
<!-- Login-Info -->
<span style="font-size:0.65rem;color:#475569;white-space:nowrap">
{{ t.username }} / {{ t.admin_pw }}
</span>
</div>
{% endfor %}
</div>
</div>
<div class="space-y-6">
<!-- ═══ STUDIO (oben, volle Breite) ═══ -->
<div class="space-y-4">
<!-- Artikel-Generator -->
<div class="card p-5">
<h2 class="text-base font-semibold text-white mb-4">✍️ Artikel-Studio</h2>
<!-- Quelle + Ton + Prompt -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
<div class="md:col-span-2">
<label class="text-xs text-slate-400 block mb-1">Quelle (URL oder Text)</label>
<input type="text" id="source-input" placeholder="https://... oder Text einfügen" class="w-full">
</div>
<div>
<label class="text-xs text-slate-400 block mb-1">Ton</label>
<select id="tone-select" class="w-full">
<option value="informativ">Informativ</option>
<option value="meinungsstark">Meinungsstark</option>
<option value="reportage">Reportage</option>
</select>
</div>
<div>
<label class="text-xs text-slate-400 block mb-1">Prompt</label>
<select id="prompt-select" class="w-full">
{% for p in prompts %}
<option value="{{ p.id }}" {% if p.is_default %}selected{% endif %}>{{ p.name }}{% if p.is_default %} ✓{% endif %}</option>
{% endfor %}
</select>
</div>
</div>
<!-- Titel -->
<div class="mb-3">
<label class="text-xs text-slate-400 block mb-1">Titel</label>
<input type="text" id="article-title" placeholder="Artikel-Titel" class="w-full">
</div>
<!-- Zwei-Spalten Editor + Vorschau (große Designfläche) -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4" style="min-height: 65vh">
<div class="flex flex-col min-h-0">
<label class="text-xs text-slate-400 block mb-1">Inhalt (HTML)</label>
<textarea id="article-content" placeholder="Artikel-Inhalt..."
oninput="updatePreview()" class="flex-1 min-h-[320px]"
style="min-height: 50vh; resize: vertical"></textarea>
<div class="mt-2 flex gap-2 items-center flex-wrap">
<input type="text" id="inline-image-url" placeholder="Bild-URL für Inhalt..." class="flex-1 min-w-[120px] text-xs">
<button type="button" onclick="insertImageAtCursor()" class="text-xs px-3 py-1.5 rounded bg-slate-700 hover:bg-slate-600 text-slate-200">🖼️ Hier einfügen</button>
</div>
</div>
<div class="flex flex-col min-h-0">
<label class="text-xs text-slate-400 block mb-1">Vorschau</label>
<div id="wp-preview"
class="rounded-lg p-5 overflow-y-auto border border-slate-600 flex-1"
style="min-height: 50vh; max-height: 75vh; font-family: Georgia, serif; line-height: 1.7; font-size: 1rem; background: #f8fafc; color: #0f172a;">
<span class="text-slate-500 italic">Vorschau erscheint beim Tippen...</span>
</div>
<div id="wp-draft-link" class="hidden"></div>
<div id="mirror-status-box" class="hidden mt-2 text-xs space-y-1"></div>
</div>
</div>
<!-- KI-Chat (freie Eingabe) -->
<div class="card p-4 mb-4">
<div class="text-sm font-semibold text-white mb-2 flex items-center gap-2">
<span>💬 KI-Chat</span>
<button type="button" id="chat-toggle" onclick="var b=document.getElementById('chat-box');b.classList.toggle('hidden');this.textContent=b.classList.contains('hidden')?'▸ aufklappen':'▾ einklappen'"
class="text-xs text-slate-500 hover:text-slate-300">▾ einklappen</button>
</div>
<div id="chat-box">
<div id="chat-messages" class="space-y-3 mb-3 max-h-[220px] overflow-y-auto rounded-lg border border-slate-600 p-3 bg-slate-900/50 text-sm"
style="min-height: 80px">
<span class="text-slate-500 italic text-xs">Schreibe eine Nachricht die KI kennt deinen Artikel.</span>
</div>
<div class="flex gap-2">
<input type="text" id="chat-input" placeholder="z.B. Kürze den ersten Absatz, schreib einen Teaser..."
class="flex-1 text-sm" onkeydown="if(event.key==='Enter')sendChat()">
<button type="button" id="btn-chat" onclick="sendChat()" class="btn btn-primary">Senden</button>
</div>
<div id="chat-apply-row" class="hidden mt-2">
<button type="button" onclick="applyChatSuggestion()" class="btn btn-success text-xs">✓ In Editor übernehmen</button>
</div>
</div>
</div>
<!-- SEO-Panel -->
<div class="bg-slate-900/50 border border-slate-700 rounded-lg p-3 mb-4">
<div class="text-xs text-slate-400 font-semibold mb-2">🔍 SEO</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<div>
<label class="text-xs text-slate-500 block mb-1">Meta-Title <span id="seo-title-count" class="text-slate-600">0/60</span></label>
<input type="text" id="seo-title" placeholder="SEO Titel" class="w-full text-xs" oninput="document.getElementById('seo-title-count').textContent=this.value.length+'/60'">
</div>
<div>
<label class="text-xs text-slate-500 block mb-1">Meta-Description <span id="seo-desc-count" class="text-slate-600">0/155</span></label>
<input type="text" id="seo-description" placeholder="Kurzbeschreibung" class="w-full text-xs" oninput="document.getElementById('seo-desc-count').textContent=this.value.length+'/155'">
</div>
<div>
<label class="text-xs text-slate-500 block mb-1">Fokus-Keyword</label>
<input type="text" id="focus-keyword" placeholder="Hauptkeyword" class="w-full text-xs">
</div>
</div>
</div>
<!-- Medien + Kategorie -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
<div>
<label class="text-xs text-slate-400 block mb-1">Featured Image URL</label>
<input type="url" id="featured-image" placeholder="https://..." class="w-full text-xs">
</div>
<div>
<label class="text-xs text-slate-400 block mb-1">Kategorie</label>
<select id="category-select" class="w-full text-xs">
<option value="">— keine —</option>
{% for cat in wp_categories %}
<option value="{{ cat.id }}">{{ cat.name }}</option>
{% endfor %}
</select>
</div>
</div>
<!-- Aktions-Buttons -->
<div class="flex flex-col gap-3">
<div class="flex gap-2 flex-wrap">
<button id="btn-generate" onclick="generateArticle()" class="btn btn-primary">🤖 KI generieren</button>
<button onclick="saveDraft()" class="btn btn-ghost">💾 Entwurf</button>
<button onclick="publishNow()" class="btn btn-success">🚀 Sofort veröffentlichen</button>
<button onclick="toggleSchedulePanel()" class="btn" style="background:#4c1d95;color:#fff">📅 Einplanen</button>
</div>
<div class="flex gap-2 flex-wrap items-center p-3 rounded-lg border border-slate-600 bg-slate-800/60">
<span class="text-sm font-medium text-slate-300">✨ KI verbessern:</span>
<input type="text" id="polish-instruction" placeholder="z.B. kürzer, lockerer — oder: Bild https://... nach Absatz 2 einfügen" class="flex-1 min-w-[200px] text-sm">
<button id="btn-polish" type="button" onclick="polishArticle()" class="btn btn-primary">Verbessern</button>
</div>
</div>
<!-- Einplan-Panel -->
<div id="schedule-panel" class="hidden mt-4 bg-slate-900 border border-slate-700 rounded-lg p-4">
<div class="text-sm text-purple-400 font-semibold mb-3">📅 Artikel einplanen</div>
<div class="flex gap-3 items-end flex-wrap">
<div>
<label class="text-xs text-slate-500 block mb-1">Datum</label>
<input type="date" id="schedule-date" class="text-xs" onchange="checkSlot()">
</div>
<div>
<label class="text-xs text-slate-500 block mb-1">Uhrzeit (15-Min-Slots)</label>
<input type="time" id="schedule-time" value="19:00" step="900" class="text-xs" onchange="checkSlot()">
</div>
<button onclick="confirmSchedule()" id="schedule-confirm-btn"
class="btn btn-primary text-xs">✓ Einplanen</button>
<button onclick="toggleSchedulePanel()" class="text-xs text-slate-500 hover:text-white px-2 py-1.5">Abbrechen</button>
</div>
<div id="slot-status" class="text-xs mt-2 hidden"></div>
</div>
</div>
</div>
<!-- ═══ REDAKTIONSPLAN (unten, volle Breite, scrollen) ═══ -->
<div class="space-y-4">
{% set draft_arts = [] %}
{% for d in plan_days %}
{% for a in plan_articles.get(d, []) %}
{% if a.status == 'draft' %}{% set _ = draft_arts.append(a) %}{% endif %}
{% endfor %}
{% endfor %}
{% set undated_drafts = undated_drafts if undated_drafts is defined else [] %}
{% if undated_drafts %}
<div class="card p-4">
<h2 class="text-sm font-semibold text-slate-300 mb-3">📝 Entwürfe ohne Datum</h2>
<div class="space-y-1">
{% for art in undated_drafts %}
<div class="flex items-center gap-2 px-3 py-2 rounded-lg border border-slate-700/50 bg-slate-800/30 cursor-pointer hover:border-slate-500 transition"
onclick="loadArticle({{ art.id }})">
<span class="text-xs">🤖</span>
<span class="text-xs text-slate-300 flex-1 truncate">{{ (art.title or 'Kein Titel')[:55] }}</span>
<span class="text-xs text-slate-500">Entwurf</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="card p-4">
<h2 class="text-base font-semibold text-white mb-3">📅 Redaktionsplan — 7 Tage</h2>
<p class="text-xs text-slate-600 mb-3">Artikel per Drag &amp; Drop auf einen anderen Tag ziehen zum Umplanen.</p>
{% set type_icons = {'ki':'🤖','rss':'📡'} %}
{% for d in plan_days %}
{% set arts = plan_articles.get(d, []) %}
{% set is_today = (d == today) %}
<!-- Drop-Zone für jeden Tag -->
<div class="drop-zone mb-2" data-date="{{ d }}"
ondragover="event.preventDefault(); this.style.background='rgba(59,130,246,0.08)'"
ondragleave="this.style.background=''"
ondrop="onDrop(event, '{{ d }}')">
<div style="display:flex;align-items:center;gap:6px;padding:4px 4px 2px 4px">
<span style="font-size:0.72rem;font-weight:700;color:{% if is_today %}#60a5fa{% else %}#475569{% endif %}">
{{ d[8:] }}.{{ d[5:7] }}.
</span>
{% if is_today %}
<span style="font-size:0.65rem;background:#1e3a5f;border:1px solid #3b82f6;color:#60a5fa;padding:1px 7px;border-radius:999px">Heute</span>
{% endif %}
{% if not arts %}
<span style="font-size:0.68rem;color:#334155;font-style:italic">— leer</span>
{% endif %}
</div>
{% for art in arts %}
<!-- Artikel-Karte: draggable -->
<div id="plan-row-{{ art.id }}"
draggable="true"
ondragstart="onDragStart(event, {{ art.id }}, '{{ art.post_time }}')"
ondragend="onDragEnd(event)"
style="border-radius:7px;border:1px solid #334155;background:#1e293b;
margin-bottom:5px;cursor:grab;transition:opacity 0.15s">
<!-- Haupt-Zeile -->
<div style="display:flex;align-items:flex-start;gap:8px;padding:8px 10px"
onclick="loadArticle({{ art.id }})" style="cursor:pointer">
<span style="font-size:1rem;flex-shrink:0;margin-top:1px">{{ type_icons.get(art.article_type, '📝') }}</span>
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:3px;flex-wrap:wrap">
<span style="font-size:0.7rem;color:#64748b;font-family:monospace">{{ art.post_time }}</span>
<span style="font-size:0.72rem;padding:1px 7px;border-radius:999px;
{% if art.status == 'published' %}background:#14532d;color:#4ade80;border:1px solid #166534
{% elif art.status == 'scheduled' %}background:#1e3a5f;color:#60a5fa;border:1px solid #1d4ed8
{% else %}background:#292524;color:#a8a29e;border:1px solid #44403c{% endif %}">
{{ {'draft':'Entwurf','scheduled':'Geplant','published':'✓ Live'}.get(art.status, art.status) }}
</span>
</div>
<!-- Titel -->
<div style="font-size:0.8rem;color:#e2e8f0;font-weight:500;margin-bottom:4px;cursor:pointer"
onclick="loadArticle({{ art.id }})">
{{ (art.title or 'Kein Titel')[:70] }}
</div>
<!-- Snippet -->
{% if art.seo_description %}
<div style="font-size:0.7rem;color:#64748b;line-height:1.4">
{{ art.seo_description[:100] }}…
</div>
{% endif %}
<!-- Aktions-Buttons -->
<div style="display:flex;gap:5px;margin-top:6px;flex-wrap:wrap" onclick="event.stopPropagation()">
<button onclick="loadArticle({{ art.id }})"
style="font-size:0.67rem;padding:2px 8px;border-radius:4px;background:#1e293b;
border:1px solid #475569;color:#94a3b8;cursor:pointer">
✏️ Bearbeiten
</button>
{% if art.wp_post_id %}
<a href="{{ wp_admin_direct }}/wp-admin/post.php?post={{ art.wp_post_id }}&action=edit" target="_blank"
style="font-size:0.67rem;padding:2px 8px;border-radius:4px;background:#1e3a5f;
border:1px solid #3b82f6;color:#93c5fd;text-decoration:none">
🌐 WP-Editor
</a>
<a href="{{ wp_url }}/?p={{ art.wp_post_id }}&preview=true" target="_blank"
style="font-size:0.67rem;padding:2px 8px;border-radius:4px;background:#1e293b;
border:1px solid #475569;color:#94a3b8;text-decoration:none">
👁 Vorschau
</a>
{% endif %}
{% if art.status != 'published' %}
<button onclick="openReschedule({{ art.id }}, '{{ d }}', '{{ art.post_time }}')"
style="font-size:0.67rem;padding:2px 8px;border-radius:4px;background:#1e293b;
border:1px solid #475569;color:#94a3b8;cursor:pointer" title="Umplanen">
🗓 Umplanen
</button>
<button onclick="deleteArticle({{ art.id }})"
style="font-size:0.67rem;padding:2px 8px;border-radius:4px;background:#1e293b;
border:1px solid #475569;color:#94a3b8;cursor:pointer" title="Löschen">
🗑
</button>
{% endif %}
</div>
</div>
</div>
<!-- Umplan-Panel -->
<div id="rs-panel-{{ art.id }}" class="hidden"
style="border-top:1px solid #334155;padding:8px 10px;background:#0f172a;border-radius:0 0 7px 7px">
<div style="display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap">
<input type="date" id="rs-date-{{ art.id }}" value="{{ d }}" class="text-xs py-1 px-2">
<input type="time" id="rs-time-{{ art.id }}" value="{{ art.post_time }}" step="900" class="text-xs py-1 px-2">
<button onclick="confirmReschedule({{ art.id }})"
style="font-size:0.72rem;background:#92400e;color:#fff;border:none;padding:4px 10px;border-radius:4px;cursor:pointer">✓ OK</button>
<button onclick="closeReschedule({{ art.id }})"
style="font-size:0.72rem;background:none;color:#64748b;border:none;cursor:pointer"></button>
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- hidden article id -->
<input type="hidden" id="current-article-id" value="">
{% endblock %}
{% block extra_js %}
let currentArticleId = null;
let currentWpPostId = null;
function updatePreview() {
const content = document.getElementById('article-content').value;
const title = document.getElementById('article-title').value;
const preview = document.getElementById('wp-preview');
preview.innerHTML = (title ? `<h1 style="font-size:1.4rem;font-weight:700;margin-bottom:1rem">${title}</h1>` : '') + content;
}
function insertImageAtCursor() {
const url = document.getElementById('inline-image-url').value.trim();
const ta = document.getElementById('article-content');
if (!url) { showToast('⚠️ Bild-URL eingeben'); return; }
const img = `<p><img src="${url.replace(/"/g, '&quot;')}" alt="Bild" style="max-width:100%;height:auto;"></p>`;
const start = ta.selectionStart, end = ta.selectionEnd;
const before = ta.value.substring(0, start), after = ta.value.substring(end);
ta.value = before + img + after;
ta.selectionStart = ta.selectionEnd = start + img.length;
ta.focus();
updatePreview();
showToast('🖼️ Bild eingefügt');
}
function setButtonLoading(btnId, loading) {
const btn = document.getElementById(btnId);
if (!btn) return;
const labels = { 'btn-generate': '🤖 KI generieren', 'btn-polish': 'Verbessern', 'btn-chat': 'Senden' };
if (loading) {
btn.dataset.origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Bitte warten...';
} else {
btn.disabled = false;
btn.textContent = btn.dataset.origText || labels[btnId] || 'OK';
}
}
let chatHistory = [];
let lastSuggested = null;
function renderChatMessages() {
const el = document.getElementById('chat-messages');
if (!el) return;
if (chatHistory.length === 0) {
el.innerHTML = '<span class="text-slate-500 italic text-xs">Schreibe eine Nachricht die KI kennt deinen Artikel.</span>';
return;
}
el.innerHTML = chatHistory.map(m => {
const isUser = m.role === 'user';
return `<div class="flex ${isUser ? 'justify-end' : 'justify-start'}">
<div class="max-w-[85%] rounded-lg px-3 py-2 text-xs ${isUser ? 'bg-blue-900/50 border border-blue-700' : 'bg-slate-800 border border-slate-600'}">
${(m.content || '').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>')}
</div>
</div>`;
}).join('');
el.scrollTop = el.scrollHeight;
}
async function sendChat() {
const input = document.getElementById('chat-input');
const msg = (input?.value || '').trim();
if (!msg) return;
const ctx = getArticleData();
const history = chatHistory.slice(-12).map(m => ({ role: m.role, content: m.content }));
chatHistory.push({ role: 'user', content: msg });
input.value = '';
renderChatMessages();
document.getElementById('chat-apply-row').classList.add('hidden');
lastSuggested = null;
setButtonLoading('btn-chat', true);
try {
const r = await fetch('/api/chat', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
message: msg,
history: history,
context: { title: ctx.title, content: ctx.content, seo_title: ctx.seo_title, seo_description: ctx.seo_description, focus_keyword: ctx.focus_keyword }
})
});
const d = await r.json().catch(() => ({}));
if (d.error) { showToast('❌ ' + d.error); chatHistory.pop(); renderChatMessages(); return; }
chatHistory.push({ role: 'assistant', content: d.reply });
if (chatHistory.length > 12) chatHistory = chatHistory.slice(-12);
if (d.suggested_content) { lastSuggested = d.suggested_content; document.getElementById('chat-apply-row').classList.remove('hidden'); }
renderChatMessages();
} catch (e) {
showToast('❌ ' + (e.message || 'Fehler')); chatHistory.pop(); renderChatMessages();
} finally {
setButtonLoading('btn-chat', false);
}
}
function applyChatSuggestion() {
if (!lastSuggested) return;
if (lastSuggested.title) document.getElementById('article-title').value = lastSuggested.title;
if (lastSuggested.content) document.getElementById('article-content').value = lastSuggested.content;
if (lastSuggested.seo_title) document.getElementById('seo-title').value = lastSuggested.seo_title;
if (lastSuggested.seo_description) document.getElementById('seo-description').value = lastSuggested.seo_description;
if (lastSuggested.focus_keyword) document.getElementById('focus-keyword').value = lastSuggested.focus_keyword;
const c1 = document.getElementById('seo-title-count');
if (c1) c1.textContent = (lastSuggested.seo_title || '').length + '/60';
const c2 = document.getElementById('seo-desc-count');
if (c2) c2.textContent = (lastSuggested.seo_description || '').length + '/155';
updatePreview();
document.getElementById('chat-apply-row').classList.add('hidden');
lastSuggested = null;
showToast('✓ Übernommen');
}
async function polishArticle() {
const instruction = document.getElementById('polish-instruction').value.trim();
const title = document.getElementById('article-title').value;
const content = document.getElementById('article-content').value;
if (!instruction) { showToast('⚠️ Anweisung eingeben'); return; }
if (!content && !title) { showToast('⚠️ Kein Inhalt zum Verbessern'); return; }
setButtonLoading('btn-polish', true);
const backup = setTimeout(() => setButtonLoading('btn-polish', false), 130000);
try {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 120000);
const r = await fetch('/api/polish', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ title, content, instruction, seo_title: document.getElementById('seo-title').value, seo_description: document.getElementById('seo-description').value, focus_keyword: document.getElementById('focus-keyword').value }),
signal: ctrl.signal
});
clearTimeout(t);
const d = await r.json().catch(() => ({}));
if (d.error) { showToast('❌ ' + d.error); return; }
document.getElementById('article-title').value = d.title || '';
document.getElementById('article-content').value = d.content || '';
if (d.seo_title) document.getElementById('seo-title').value = d.seo_title;
if (d.seo_description) document.getElementById('seo-description').value = d.seo_description;
if (d.focus_keyword) document.getElementById('focus-keyword').value = d.focus_keyword;
const c1 = document.getElementById('seo-title-count');
if (c1) c1.textContent = (d.seo_title || '').length + '/60';
const c2 = document.getElementById('seo-desc-count');
if (c2) c2.textContent = (d.seo_description || '').length + '/155';
updatePreview();
document.getElementById('polish-instruction').value = '';
showToast('✨ Text verbessert');
} catch (e) {
showToast('❌ ' + (e.name === 'AbortError' ? 'Timeout' : (e.message || 'Fehler')));
} finally {
clearTimeout(backup);
setButtonLoading('btn-polish', false);
}
}
async function generateArticle() {
const source = document.getElementById('source-input').value.trim();
const tone = document.getElementById('tone-select').value;
const promptId = document.getElementById('prompt-select')?.value || null;
if (!source) { showToast('⚠️ Bitte Quelle eingeben'); return; }
setButtonLoading('btn-generate', true);
const backup = setTimeout(() => setButtonLoading('btn-generate', false), 130000);
try {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 120000);
const r = await fetch('/api/generate', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({source, tone, prompt_id: promptId ? parseInt(promptId, 10) : null}),
signal: ctrl.signal
});
clearTimeout(t);
const d = await r.json().catch(() => ({}));
if (d.error) { showToast('❌ ' + d.error); return; }
document.getElementById('article-title').value = d.title || '';
document.getElementById('article-content').value = d.content || '';
document.getElementById('seo-title').value = d.seo_title || '';
document.getElementById('seo-description').value = d.seo_description || '';
document.getElementById('focus-keyword').value = d.focus_keyword || '';
updatePreview();
if (source.startsWith('http')) fetchOgImage(source);
showToast('✅ Artikel generiert');
} catch (e) {
showToast('❌ ' + (e.name === 'AbortError' ? 'Timeout (>2 Min)' : (e.message || 'Fehler')));
} finally {
clearTimeout(backup);
setButtonLoading('btn-generate', false);
}
}
async function fetchOgImage(url) {
try {
const r = await fetch('/api/og-image', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({url})
});
const d = await r.json();
if (d.image) document.getElementById('featured-image').value = d.image;
} catch(e) {}
}
function getArticleData() {
return {
id: currentArticleId,
title: document.getElementById('article-title').value,
content: document.getElementById('article-content').value,
source_url: document.getElementById('source-input').value,
tone: document.getElementById('tone-select').value,
seo_title: document.getElementById('seo-title').value,
seo_description: document.getElementById('seo-description').value,
focus_keyword: document.getElementById('focus-keyword').value,
featured_image_url: document.getElementById('featured-image').value,
category_id: document.getElementById('category-select').value || null,
article_type: 'ki',
};
}
async function saveDraft() {
const r = await fetch('/api/article/save', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({...getArticleData(), id: currentArticleId, wp_post_id: currentWpPostId, status: 'draft'})
});
const d = await r.json();
if (d.success) {
currentArticleId = d.id;
if (d.wp_post_id) currentWpPostId = d.wp_post_id;
// Vorschau- und Editor-Links anzeigen
const linkBox = document.getElementById('wp-draft-link');
if ((d.wp_preview_url || d.wp_edit_url) && linkBox) {
const links = [];
if (d.wp_edit_url) links.push(`<a href="${d.wp_edit_url}" target="_blank"
style="display:inline-flex;align-items:center;gap:6px;background:#e0f2fe;border:1px solid #7dd3fc;
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none;margin-right:8px">
&#9998; Im WP-Editor bearbeiten &rarr;</a>`);
if (d.wp_preview_url) links.push(`<a href="${d.wp_preview_url}" target="_blank"
style="display:inline-flex;align-items:center;gap:6px;background:#f0f9ff;border:1px solid #bae6fd;
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none">
&#128065; Vorschau ansehen</a>`);
linkBox.innerHTML = links.join('');
linkBox.classList.remove('hidden');
}
showToast('💾 Entwurf gespeichert & nach WordPress gepusht');
}
}
async function publishNow() {
if (!document.getElementById('article-title').value) { showToast('⚠️ Titel fehlt'); return; }
const r = await fetch('/api/article/schedule', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
...getArticleData(),
status: 'scheduled',
post_date: new Date().toISOString().split('T')[0],
post_time: new Date().toTimeString().slice(0,5),
})
});
const d = await r.json();
if (d.success) { currentArticleId = d.id; showToast('🚀 Wird sofort veröffentlicht!'); setTimeout(() => location.reload(), 2000); }
}
function toggleSchedulePanel() {
const p = document.getElementById('schedule-panel');
p.classList.toggle('hidden');
if (!p.classList.contains('hidden')) {
const today = new Date().toISOString().split('T')[0];
document.getElementById('schedule-date').value = today;
}
}
async function checkSlot() {
const date = document.getElementById('schedule-date').value;
const time = document.getElementById('schedule-time').value;
if (!date || !time) return;
const r = await fetch(`/api/slots/${date}`);
const d = await r.json();
const statusEl = document.getElementById('slot-status');
statusEl.classList.remove('hidden');
if (d.taken && d.taken.includes(time)) {
statusEl.className = 'text-xs mt-2 text-red-400';
statusEl.textContent = `❌ Slot ${date} ${time} bereits belegt`;
document.getElementById('schedule-confirm-btn').disabled = true;
} else {
statusEl.className = 'text-xs mt-2 text-green-400';
statusEl.textContent = `✅ Slot ${date} ${time} ist frei`;
document.getElementById('schedule-confirm-btn').disabled = false;
}
}
async function confirmSchedule() {
const post_date = document.getElementById('schedule-date').value;
const post_time = document.getElementById('schedule-time').value;
if (!post_date || !post_time) { showToast('⚠️ Datum und Uhrzeit wählen'); return; }
if (!document.getElementById('article-title').value) { showToast('⚠️ Titel fehlt'); return; }
const r = await fetch('/api/article/schedule', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({...getArticleData(), post_date, post_time})
});
const d = await r.json();
if (d.success) {
showToast(`📅 Eingeplant: ${post_date} ${post_time}`);
setTimeout(() => location.reload(), 1500);
} else {
showToast('❌ Fehler beim Einplanen');
}
}
async function loadArticle(id) {
if (currentArticleId !== id) { chatHistory = []; lastSuggested = null; if (document.getElementById('chat-apply-row')) document.getElementById('chat-apply-row').classList.add('hidden'); renderChatMessages(); }
const r = await fetch(`/api/article/${id}`);
const d = await r.json();
currentArticleId = id;
currentWpPostId = d.wp_post_id || null;
document.getElementById('article-title').value = d.title || '';
document.getElementById('article-content').value = d.content || '';
document.getElementById('source-input').value = d.source_url || '';
document.getElementById('seo-title').value = d.seo_title || '';
document.getElementById('seo-description').value = d.seo_description || '';
document.getElementById('focus-keyword').value = d.focus_keyword || '';
document.getElementById('featured-image').value = d.featured_image_url || '';
if (d.category_id) document.getElementById('category-select').value = d.category_id;
// WP-Links wiederherstellen wenn vorhanden
const linkBox = document.getElementById('wp-draft-link');
if (d.wp_post_id && linkBox) {
const wrap = document.querySelector('[data-wp-admin]');
const wpAdmin = wrap ? wrap.dataset.wpAdmin : '';
const wpBase = wrap ? wrap.dataset.wpUrl : '';
const links = [];
if (wpAdmin) links.push(`<a href="${wpAdmin}/wp-admin/post.php?post=${d.wp_post_id}&action=edit" target="_blank"
style="display:inline-flex;align-items:center;gap:6px;background:#e0f2fe;border:1px solid #7dd3fc;
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none;margin-right:8px">
&#9998; Im WP-Editor bearbeiten &rarr;</a>`);
if (wpBase) links.push(`<a href="${wpBase}/?p=${d.wp_post_id}&preview=true" target="_blank"
style="display:inline-flex;align-items:center;gap:6px;background:#f0f9ff;border:1px solid #bae6fd;
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none">
&#128065; Vorschau ansehen</a>`);
linkBox.innerHTML = links.join('');
linkBox.classList.remove('hidden');
} else if (linkBox) {
linkBox.classList.add('hidden');
}
updatePreview();
loadMirrorStatus(id);
window.scrollTo({top: 0, behavior: 'smooth'});
}
// ── Drag & Drop ──
let dragId = null, dragTime = null;
function onDragStart(event, id, time) {
dragId = id;
dragTime = time;
event.dataTransfer.effectAllowed = 'move';
const el = document.getElementById(`plan-row-${id}`);
setTimeout(() => { if(el) el.style.opacity = '0.4'; }, 0);
}
function onDragEnd(event) {
if (dragId) {
const el = document.getElementById(`plan-row-${dragId}`);
if (el) el.style.opacity = '1';
}
document.querySelectorAll('.drop-zone').forEach(z => z.style.background = '');
}
async function onDrop(event, newDate) {
event.preventDefault();
document.querySelectorAll('.drop-zone').forEach(z => z.style.background = '');
if (!dragId) return;
const r = await fetch(`/api/article/${dragId}/reschedule`, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({post_date: newDate, post_time: dragTime})
});
const d = await r.json();
if (d.success) {
showToast(`📅 Verschoben auf ${newDate}`);
setTimeout(() => location.reload(), 800);
} else {
showToast('❌ ' + (d.error || 'Fehler'));
}
dragId = null;
}
// ── Board: Umplanen ──
function openReschedule(id, date, time) {
document.querySelectorAll('[id^="rs-panel-"]').forEach(el => el.classList.add('hidden'));
document.getElementById(`rs-panel-${id}`).classList.remove('hidden');
}
function closeReschedule(id) {
document.getElementById(`rs-panel-${id}`).classList.add('hidden');
}
async function confirmReschedule(id) {
const date = document.getElementById(`rs-date-${id}`).value;
const time = document.getElementById(`rs-time-${id}`).value;
const r = await fetch(`/api/article/${id}/reschedule`, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({post_date: date, post_time: time})
});
const d = await r.json();
if (d.success) { showToast(`✅ Umgeplant: ${date} ${time}`); setTimeout(() => location.reload(), 1200); }
else showToast('❌ ' + (d.error || 'Fehler'));
}
async function deleteArticle(id) {
if (!confirm('Artikel wirklich löschen?')) return;
const r = await fetch(`/api/article/${id}/delete`, {method: 'POST'});
const d = await r.json();
if (d.success) { showToast('🗑️ Gelöscht'); setTimeout(() => location.reload(), 1000); }
}
// ── Mirror-Status nach Publish anzeigen ──
async function loadMirrorStatus(articleId) {
try {
const r = await fetch(`/api/article/${articleId}/mirrors`);
const mirrors = await r.json();
const box = document.getElementById('mirror-status-box');
if (!mirrors.length) { box.classList.add('hidden'); return; }
box.innerHTML = '<div class="text-slate-500 mb-1">📡 Mirror-Status:</div>';
for (const m of mirrors) {
const ok = m.status === 'ok';
box.innerHTML += `<div class="flex items-center gap-2 ${ok ? 'text-green-400' : 'text-red-400'}">
${ok ? '✅' : '❌'} ${m.mirror_label}
${ok && m.mirror_url ? `<a href="${m.mirror_url}" target="_blank" class="underline text-blue-400">&rarr; ansehen</a>` : ''}
${!ok && m.error ? `<span class="text-slate-500">(${m.error})</span>` : ''}
</div>`;
}
box.classList.remove('hidden');
} catch(e) {}
}
document.addEventListener('DOMContentLoaded', () => {
const today = new Date().toISOString().split('T')[0];
document.getElementById('schedule-date').value = today;
});
{% endblock %}