rss-news/backend/templates/admin_dashboard.html
Oliver G de1d62c6f7
feat(admin): Freigabe-Seite und Aufraeumen der Status-Altlasten
Die Freigabe ist jetzt der taegliche Arbeitsschritt und bekommt eine
eigene Seite: /admin/freigabe listet alle wartenden Artikel mit Bild,
Score, Tags und dem umgeschriebenen Text lesbar gerendert, dazu die drei
Aktionen Freigeben, Neu schreiben, Verwerfen. Nach jeder Aktion geht es
zurueck in die Liste, damit sich eine Warteschlange am Stueck abarbeiten
laesst.

Der Vorschautext ist Modell-Ausgabe ueber fremde Webseiten und laeuft
deshalb durch einen Tag-Whitelist-Filter statt roh ins Template.

Aufgeraeumt:
- Artikelliste zeigte interne Kuerzel statt Klartext
- Status `review` (Relevanz-Warnzone) wurde als "Rewrite" ausgegeben
- `Rewrite -> Freigegeben` entfernt: zweiter Weg an der Freigabe vorbei
- `Freigegeben -> Veroeffentlicht` entfernt: das macht der WP-Sync
- API-Statusliste wird abgeleitet statt handgepflegt, sie kannte den
  neuen Status nicht und lehnte den Wechsel mit 422 ab

Behoben: list_articles las content_rewritten nicht mit, die neue Seite
haette nie einen Text angezeigt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:32:48 +02:00

413 lines
17 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.

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ title }}</title>
<link rel="stylesheet" href="/admin/static/admin.css" />
</head>
<body>
<header class="topbar">
<div>
<h1>rss-news Admin Dashboard</h1>
<p>Angemeldet als <strong>{{ user }}</strong></p>
</div>
<div class="row">
<a class="linkbtn" href="/admin/freigabe">Freigabe{% if pending_review_count %} ({{ pending_review_count }}){% endif %}</a>
<a class="linkbtn" href="/admin/article-list">Artikelliste</a>
<a class="linkbtn" href="/admin/schedule">Veröffentlichungsplan</a>
<a class="linkbtn" href="/admin/connectivity">Connectivity Check</a>
<form method="post" action="/admin/logout">
<button type="submit" class="secondary">Logout</button>
</form>
</div>
</header>
<main class="container">
{% if flash_msg %}
<section class="card flash {{ 'flash-error' if flash_type == 'error' else 'flash-success' }}">
{{ flash_msg }}
</section>
{% endif %}
<section class="stats">
<article class="stat">
<div class="label">Quellen</div>
<div class="value">{{ sources|length }}</div>
</article>
<article class="stat">
<div class="label">Feeds</div>
<div class="value">{{ feeds|length }}</div>
</article>
<article class="stat">
<div class="label">Artikel</div>
<div class="value">{{ articles|length }}</div>
</article>
<article class="stat">
<div class="label">Runs</div>
<div class="value">{{ runs|length }}</div>
</article>
</section>
<section class="grid two">
<article class="card">
<h2>Quelle anlegen</h2>
<form method="post" action="/admin/sources/create" class="stack">
<input name="name" placeholder="Name" required />
<input name="base_url" placeholder="Base URL" />
<input name="terms_url" placeholder="Terms URL" />
<input name="license_name" placeholder="Lizenzname" />
<select name="risk_level">
<option value="green">green</option>
<option value="yellow" selected>yellow</option>
<option value="red">red</option>
</select>
<input name="last_reviewed_at" placeholder="last_reviewed_at (ISO)" />
<button type="submit">Quelle speichern</button>
</form>
</article>
<article class="card">
<h2>Feed anlegen</h2>
<form method="post" action="/admin/feeds/create" class="stack">
<input name="name" placeholder="Feed Name" required />
<input name="url" placeholder="https://..." required />
<label>Quelle</label>
<select name="source_id">
<option value="">-- keine --</option>
{% for s in sources %}
<option value="{{ s.id }}">{{ s.name }} (#{{ s.id }})</option>
{% endfor %}
</select>
<button type="submit">Feed speichern</button>
</form>
</article>
</section>
<section class="card">
<h2>Ingestion starten</h2>
<form method="post" action="/admin/ingestion/run" class="row">
<select name="feed_id">
<option value="">Alle aktivierten Feeds</option>
{% for f in feeds %}
<option value="{{ f.id }}">{{ f.name }} (#{{ f.id }})</option>
{% endfor %}
</select>
<button type="submit">Ingestion starten</button>
</form>
</section>
<section class="card">
<h2>Publisher ausführen</h2>
<form method="post" action="/admin/publisher/run" class="row">
<input name="max_jobs" value="10" />
<button type="submit">Publisher Run starten</button>
</form>
</section>
<section class="card">
<h2>Rewrite Run (geplante Artikel)</h2>
<p class="subtle">Verarbeitet alle Artikel im Status <code>rewrite</code> und setzt sie auf <code>publish</code>.</p>
<form method="post" action="/admin/rewrite/run" class="row">
<input name="max_jobs" value="10" />
<button type="submit">Rewrite Run starten</button>
</form>
</section>
<section class="card">
<h2>Quellen + Policy</h2>
<table>
<thead>
<tr><th>ID</th><th>Name</th><th>Risk</th><th>Lizenz</th><th>Terms</th><th>Policy</th></tr>
</thead>
<tbody>
{% for s in sources %}
<tr>
<td>{{ s.id }}</td>
<td>{{ s.name }}</td>
<td>{{ s.risk_level }}</td>
<td>{{ s.license_name or "-" }}</td>
<td>{{ s.terms_url or "-" }}</td>
<td>
{% if source_policy[s.id] %}
<span class="badge bad">BLOCKED ({{ source_policy[s.id]|length }})</span>
<div class="subtle">{{ source_policy[s.id]|join(", ") }}</div>
{% else %}
<span class="badge ok">OK</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="card">
<h2>Quellen verwalten</h2>
<table>
<thead>
<tr><th>ID</th><th>Name</th><th>URLs</th><th>Meta</th><th>Aktionen</th></tr>
</thead>
<tbody>
{% for s in sources %}
{% set source_form_id = 'source-update-' ~ s.id %}
<tr>
<td>#{{ s.id }}</td>
<td>
<input form="{{ source_form_id }}" name="name" value="{{ s.name }}" required />
</td>
<td>
<input form="{{ source_form_id }}" name="base_url" value="{{ s.base_url or '' }}" placeholder="Base URL" />
<input form="{{ source_form_id }}" name="terms_url" value="{{ s.terms_url or '' }}" placeholder="Terms URL" />
<input form="{{ source_form_id }}" name="license_name" value="{{ s.license_name or '' }}" placeholder="Lizenz" />
</td>
<td>
<select form="{{ source_form_id }}" name="risk_level">
<option value="green" {% if s.risk_level == 'green' %}selected{% endif %}>green</option>
<option value="yellow" {% if s.risk_level == 'yellow' %}selected{% endif %}>yellow</option>
<option value="red" {% if s.risk_level == 'red' %}selected{% endif %}>red</option>
</select>
<select form="{{ source_form_id }}" name="is_enabled">
<option value="1" {% if s.is_enabled %}selected{% endif %}>aktiv</option>
<option value="0" {% if not s.is_enabled %}selected{% endif %}>inaktiv</option>
</select>
<input form="{{ source_form_id }}" name="last_reviewed_at" value="{{ s.last_reviewed_at or '' }}" placeholder="last_reviewed_at" />
<input form="{{ source_form_id }}" name="notes" value="{{ s.notes or '' }}" placeholder="Notiz" />
</td>
<td>
<div class="inline">
<form method="post" action="/admin/sources/{{ s.id }}/update" id="{{ source_form_id }}" class="inline">
<button type="submit" class="secondary">Speichern</button>
</form>
<form method="post" action="/admin/sources/{{ s.id }}/delete" class="inline" onsubmit="return confirm('Quelle wirklich löschen?');">
<button type="submit">Löschen</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="card">
<h2>Feeds verwalten</h2>
<table>
<thead>
<tr><th>ID</th><th>Name</th><th>URL</th><th>Quelle</th><th>Status</th><th>Aktionen</th></tr>
</thead>
<tbody>
{% for f in feeds %}
{% set feed_form_id = 'feed-update-' ~ f.id %}
<tr>
<td>#{{ f.id }}</td>
<td>
<input form="{{ feed_form_id }}" name="name" value="{{ f.name }}" required />
</td>
<td><input form="{{ feed_form_id }}" name="url" value="{{ f.url }}" required /></td>
<td>
<select form="{{ feed_form_id }}" name="source_id">
<option value="">-- keine --</option>
{% for s in sources %}
<option value="{{ s.id }}" {% if f.source_id == s.id %}selected{% endif %}>{{ s.name }} (#{{ s.id }})</option>
{% endfor %}
</select>
</td>
<td>
<select form="{{ feed_form_id }}" name="is_enabled">
<option value="1" {% if f.is_enabled %}selected{% endif %}>aktiv</option>
<option value="0" {% if not f.is_enabled %}selected{% endif %}>inaktiv</option>
</select>
</td>
<td>
<div class="inline">
<form method="post" action="/admin/feeds/{{ f.id }}/update" id="{{ feed_form_id }}" class="inline">
<button type="submit" class="secondary">Speichern</button>
</form>
<form method="post" action="/admin/feeds/{{ f.id }}/delete" class="inline" onsubmit="return confirm('Feed wirklich löschen?');">
<button type="submit">Löschen</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="card">
<h2>Artikel (Review)</h2>
{% if pending_review_count %}
<p class="flash flash-success" style="padding:8px 12px;">
📝 <strong>{{ pending_review_count }}</strong> Artikel warten auf deine redaktionelle Freigabe —
<a href="/admin/freigabe">jetzt prüfen</a>.
Erst nach der Freigabe gehen sie nach WordPress.
</p>
{% endif %}
<form method="get" action="/admin/dashboard" class="row filter-row">
<label>Status-Filter</label>
<select name="status_filter">
<option value="" {% if not status_filter %}selected{% endif %}>alle</option>
{% for s in status_options %}
<option value="{{ s }}" {% if status_filter == s %}selected{% endif %}>{{ status_labels.get(s, s) }}</option>
{% endfor %}
</select>
<button type="submit" class="secondary">Filtern</button>
<a href="/admin/dashboard" class="linkbtn">Reset</a>
<a href="/api/articles/export?format=json{% if status_filter %}&status_filter={{ status_filter }}{% endif %}" class="linkbtn">Export JSON</a>
<a href="/api/articles/export?format=csv{% if status_filter %}&status_filter={{ status_filter }}{% endif %}" class="linkbtn">Export CSV</a>
</form>
<table>
<thead>
<tr><th>ID</th><th>Artikel</th><th>Status</th><th>Details</th><th>Rewrite</th><th>Transition</th></tr>
</thead>
<tbody>
{% for a in articles %}
<tr>
<td>{{ a.id }}</td>
<td>
<strong>{{ a.title }}</strong><br />
<span class="subtle">Autor: {{ a.author or "-" }}</span><br />
<span class="subtle">Datum: {{ a.published_at or "-" }} | Alter: {{ a.days_old if a.days_old is not none else "-" }} Tage | Relevanz: {{ a.relevance }}</span><br />
<a href="{{ a.source_url }}" target="_blank" rel="noopener">Original öffnen</a>
<br /><a href="/admin/articles/{{ a.id }}">Details anzeigen</a>
{% if a.canonical_url and a.canonical_url != a.source_url %}
<br /><a href="{{ a.canonical_url }}" target="_blank" rel="noopener">Canonical öffnen</a>
{% endif %}
</td>
<td><span class="badge">{{ status_labels.get(a.status_ui, a.status_ui) }}</span></td>
<td>
<div class="subtle">Publish: {{ "bereit" if a.publish_ready else "blockiert" }}</div>
{% if not a.publish_ready and a.publish_blockers %}
<div class="subtle">{{ a.publish_blockers|join(", ") }}</div>
{% endif %}
{% if a.selected_image_url %}
<div class="subtle">Hauptbild gesetzt</div>
<a href="{{ a.selected_image_url }}" target="_blank" rel="noopener"><img src="{{ a.selected_image_proxy_url }}" data-fallback-src="{{ a.selected_image_url }}" alt="Hauptbild" class="thumb" loading="lazy" onerror="if(!this.dataset.fallbackUsed){this.dataset.fallbackUsed='1';this.src=this.dataset.fallbackSrc;}else{this.classList.add('img-failed');}" /></a>
{% endif %}
{% if a.summary %}
<div><strong>Summary:</strong> {{ a.summary }}</div>
{% endif %}
{% if a.generated_tags %}
<div><strong>Tags:</strong> {{ a.generated_tags|join("; ") }}</div>
{% endif %}
{% if a.content_raw %}
<details>
<summary>Volltext anzeigen</summary>
<div class="pre">{{ a.content_raw }}</div>
</details>
{% endif %}
<div class="subtle">Bilder: {{ a.extracted_images|length }}</div>
{% if a.extracted_images %}
<details>
<summary>Bild-URLs</summary>
<ul>
{% for img in a.extracted_images %}
<li><a href="{{ img }}" target="_blank" rel="noopener">{{ img }}</a></li>
{% endfor %}
</ul>
</details>
{% endif %}
{% if a.press_contact %}
<details>
<summary>Pressekontakt</summary>
<div class="pre">{{ a.press_contact }}</div>
</details>
{% endif %}
{% if a.extraction_error %}
<div class="subtle">Extraktionsfehler: {{ a.extraction_error }}</div>
{% endif %}
</td>
<td>
{% if a.status_ui in ["new", "rewrite"] %}
<form method="post" action="/admin/articles/{{ a.id }}/rewrite-run" class="inline">
<button type="submit">Rewrite ausführen</button>
</form>
{% else %}
-
{% endif %}
</td>
<td>
<form method="post" action="/admin/articles/{{ a.id }}/transition" class="inline">
<select name="target_status">
{% for s in allowed_transitions.get(a.status_ui, []) %}
<option value="{{ s }}">{{ status_labels.get(s, s) }}</option>
{% endfor %}
</select>
{% if allowed_transitions.get(a.status_ui, []) %}
<button type="submit" class="secondary">Setzen</button>
{% else %}
<span class="subtle">keine Aktion</span>
{% endif %}
</form>
{% if a.status_ui == 'close' %}
<form method="post" action="/admin/articles/{{ a.id }}/retry" class="inline" style="margin-top:4px;">
<button type="submit" title="Artikel auf 'neu' zurücksetzen wird beim nächsten Pipeline-Lauf erneut verarbeitet">🔄 Wiederholen</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="card">
<h2>Runs</h2>
<table>
<thead>
<tr><th>ID</th><th>Typ</th><th>Status</th><th>Start</th><th>Ende</th></tr>
</thead>
<tbody>
{% for r in runs %}
<tr>
<td>{{ r.id }}</td>
<td>{{ r.run_type }}</td>
<td>{{ r.status }}</td>
<td>{{ r.started_at }}</td>
<td>{{ r.finished_at or "-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="card">
<h2>Publish Jobs</h2>
<table>
<thead>
<tr><th>ID</th><th>Artikel</th><th>Status</th><th>Attempts</th><th>WP Post</th><th>Fehler</th><th>Hinweis</th></tr>
</thead>
<tbody>
{% for j in publish_jobs %}
<tr>
<td>{{ j.id }}</td>
<td>#{{ j.article_id }} {{ j.article_title or "-" }}</td>
<td>{{ j.status }}</td>
<td>{{ j.attempts }}/{{ j.max_attempts }}</td>
<td>
{% if j.wp_post_url %}
<a href="{{ j.wp_post_url }}" target="_blank" rel="noopener">#{{ j.wp_post_id }}</a>
{% elif j.wp_post_id %}
#{{ j.wp_post_id }}
{% else %}
-
{% endif %}
</td>
<td>
{% if j.error_message %}
<span class="badge errcat errcat-{{ j.error_category }}">{{ j.error_category }}</span>
<div>{{ j.error_message }}</div>
{% else %}
-
{% endif %}
</td>
<td>{{ j.error_hint or "-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
</main>
</body>
</html>