homelab-brain/redax-wp/src/templates/feeds.html
root 064ae085b5 redax-wp: Sprint 1+2 — vollständiger Stack
Infrastruktur:
- CT 113 auf pve-hetzner erstellt (Docker, Tailscale)
- Forgejo-Repo redax-wp angelegt

Code (Sprint 2):
- docker-compose.yml: wordpress + db + redax-web
- .env.example mit allen Variablen
- database.py: articles, feeds, feed_items, prompts, settings
- wordpress.py: WP REST API Client (create/update post, media upload, Yoast SEO)
- rss_fetcher.py: Feed-Import, Blacklist, Teaser-Modus, KI-Rewrite
- app.py: Flask Dashboard, Scheduler (publish/rss/briefing), alle API-Routen
- templates: base, login, index (Zwei-Spalten-Editor), feeds, history, prompts, settings, hilfe
- README.md + .gitignore

Made-with: Cursor
2026-02-27 07:52:31 +07:00

182 lines
8.4 KiB
HTML

{% extends "base.html" %}
{% block title %}Redax-WP — Feeds{% endblock %}
{% block content %}
<div class="max-w-6xl mx-auto px-6 py-6">
<div class="flex items-center justify-between mb-6">
<h1 class="text-xl font-bold text-white">📡 RSS-Feeds</h1>
<button onclick="showAddFeed()" class="btn btn-primary text-sm">+ Feed hinzufügen</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Feed-Liste -->
<div>
<h2 class="text-sm font-semibold text-slate-400 mb-3">Aktive Feeds ({{ feeds|length }})</h2>
<div class="space-y-2">
{% for feed in feeds %}
<div class="card p-4">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full {% if feed.active %}bg-green-400{% else %}bg-slate-600{% endif %}"></span>
<span class="text-sm font-semibold text-white">{{ feed.name }}</span>
</div>
<div class="flex gap-1">
<button onclick="fetchFeedNow({{ feed.id }}, this)"
class="text-xs text-slate-500 hover:text-blue-400 px-2 py-1 rounded hover:bg-slate-700 transition">
🔄 Abrufen
</button>
<button onclick="deleteFeed({{ feed.id }}, '{{ feed.name }}')"
class="text-xs text-slate-500 hover:text-red-400 px-2 py-1 rounded hover:bg-slate-700 transition">
🗑️
</button>
</div>
</div>
<div class="text-xs text-slate-500 truncate mb-2">{{ feed.url }}</div>
<div class="flex gap-2 flex-wrap text-xs">
<span class="bg-slate-700 text-slate-300 px-2 py-0.5 rounded">{{ feed.schedule }}</span>
{% if feed.auto_publish %}<span class="bg-green-900/40 text-green-400 px-2 py-0.5 rounded border border-green-800">Auto-Publish</span>{% endif %}
{% if feed.ki_rewrite %}<span class="bg-purple-900/40 text-purple-400 px-2 py-0.5 rounded border border-purple-800">KI-Rewrite</span>{% endif %}
{% if feed.teaser_only %}<span class="bg-slate-700/50 text-slate-400 px-2 py-0.5 rounded">Teaser</span>{% endif %}
</div>
{% if feed.last_error %}
<div class="text-xs text-red-400 mt-2">⚠️ {{ feed.last_error[:80] }}</div>
{% endif %}
{% if feed.last_fetched_at %}
<div class="text-xs text-slate-600 mt-1">Zuletzt: {{ feed.last_fetched_at[:16] }}</div>
{% endif %}
</div>
{% else %}
<div class="card p-6 text-center text-slate-600 text-sm">Noch keine Feeds konfiguriert.</div>
{% endfor %}
</div>
</div>
<!-- Artikel-Queue -->
<div>
<h2 class="text-sm font-semibold text-slate-400 mb-3">Artikel-Queue ({{ queue|length }} neu)</h2>
<div class="space-y-2">
{% for item in queue %}
<div class="card p-3" id="queue-item-{{ item.id }}">
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<div class="text-xs text-slate-500 mb-1">📡 {{ item.feed_name }}</div>
<div class="text-sm text-slate-200 truncate font-medium">{{ item.title }}</div>
<a href="{{ item.url }}" target="_blank" class="text-xs text-blue-400 hover:text-blue-300 truncate block">{{ item.url[:60] }}...</a>
</div>
<div class="flex flex-col gap-1 shrink-0">
<button onclick="approveItem({{ item.id }})"
class="text-xs bg-green-800 hover:bg-green-700 text-white px-2 py-1 rounded">✓ Übernehmen</button>
<button onclick="rejectItem({{ item.id }})"
class="text-xs text-slate-600 hover:text-red-400 px-2 py-1 rounded hover:bg-slate-700">✗ Ablehnen</button>
</div>
</div>
</div>
{% else %}
<div class="card p-6 text-center text-slate-600 text-sm">Queue ist leer — alle Artikel verarbeitet.</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Add Feed Modal -->
<div id="add-feed-modal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-slate-800 border border-slate-700 rounded-xl p-6 w-full max-w-md mx-4">
<h3 class="text-white font-semibold mb-4">+ Feed hinzufügen</h3>
<div class="space-y-3">
<div>
<label class="text-xs text-slate-400 block mb-1">Name</label>
<input type="text" id="new-feed-name" class="w-full" placeholder="z.B. Heise Online">
</div>
<div>
<label class="text-xs text-slate-400 block mb-1">RSS-URL</label>
<input type="url" id="new-feed-url" class="w-full" placeholder="https://...">
</div>
<div>
<label class="text-xs text-slate-400 block mb-1">Abruf-Intervall (Minuten)</label>
<select id="new-feed-schedule" class="w-full">
<option value="*/30 * * * *">Alle 30 Minuten</option>
<option value="*/60 * * * *">Stündlich</option>
<option value="*/120 * * * *">Alle 2 Stunden</option>
<option value="*/180 * * * *">Alle 3 Stunden</option>
<option value="0 8,14,20 * * *">3x täglich (8/14/20 Uhr)</option>
</select>
</div>
<div class="flex gap-4">
<label class="flex items-center gap-2 text-sm text-slate-300 cursor-pointer">
<input type="checkbox" id="new-feed-auto" class="rounded"> Auto-Publish
</label>
<label class="flex items-center gap-2 text-sm text-slate-300 cursor-pointer">
<input type="checkbox" id="new-feed-ki" class="rounded"> KI-Rewrite
</label>
<label class="flex items-center gap-2 text-sm text-slate-300 cursor-pointer">
<input type="checkbox" id="new-feed-teaser" class="rounded" checked> Nur Teaser
</label>
</div>
<div>
<label class="text-xs text-slate-400 block mb-1">Blacklist (kommagetrennt)</label>
<input type="text" id="new-feed-blacklist" class="w-full text-xs" placeholder="Anzeige:, Sponsored, Werbung">
</div>
</div>
<div class="flex gap-2 mt-5">
<button onclick="submitAddFeed()" class="btn btn-primary flex-1">Feed hinzufügen</button>
<button onclick="hideAddFeed()" class="btn btn-ghost">Abbrechen</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
function showAddFeed() { document.getElementById('add-feed-modal').classList.remove('hidden'); }
function hideAddFeed() { document.getElementById('add-feed-modal').classList.add('hidden'); }
async function submitAddFeed() {
const data = {
name: document.getElementById('new-feed-name').value,
url: document.getElementById('new-feed-url').value,
schedule: document.getElementById('new-feed-schedule').value,
active: 1,
auto_publish: document.getElementById('new-feed-auto').checked ? 1 : 0,
ki_rewrite: document.getElementById('new-feed-ki').checked ? 1 : 0,
teaser_only: document.getElementById('new-feed-teaser').checked ? 1 : 0,
blacklist: document.getElementById('new-feed-blacklist').value,
};
if (!data.name || !data.url) { showToast('⚠️ Name und URL erforderlich'); return; }
const r = await fetch('/api/feeds/add', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
const d = await r.json();
if (d.success) { showToast('✅ Feed hinzugefügt'); setTimeout(() => location.reload(), 1000); }
else showToast('❌ Fehler');
}
async function fetchFeedNow(id, btn) {
btn.textContent = '⟳ ...';
const r = await fetch(`/api/feeds/${id}/fetch`, {method:'POST'});
const d = await r.json();
if (d.success) showToast(`✅ ${d.new_items} neue Artikel`);
btn.textContent = '🔄 Abrufen';
}
async function deleteFeed(id, name) {
if (!confirm(`Feed "${name}" wirklich löschen?`)) return;
const r = await fetch(`/api/feeds/${id}/delete`, {method:'POST'});
const d = await r.json();
if (d.success) { showToast('🗑️ Feed gelöscht'); setTimeout(() => location.reload(), 1000); }
}
async function approveItem(id) {
const r = await fetch(`/api/queue/${id}/approve`, {method:'POST'});
const d = await r.json();
if (d.success) {
showToast('✅ Artikel übernommen');
document.getElementById(`queue-item-${id}`).remove();
}
}
async function rejectItem(id) {
const r = await fetch(`/api/queue/${id}/reject`, {method:'POST'});
const d = await r.json();
if (d.success) document.getElementById(`queue-item-${id}`).remove();
}
{% endblock %}