rss-news/backend/templates/admin_article_detail.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

269 lines
13 KiB
HTML

<!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>Artikel-Detail #{{ article.id }}</h1>
<p>Angemeldet als <strong>{{ user }}</strong></p>
</div>
<div class="row">
<a class="linkbtn" href="/admin/dashboard">Zurück</a>
<a class="linkbtn" href="/admin/freigabe">Freigabe</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="card">
<h2>{{ article.title }}</h2>
<div class="detail-grid">
<div class="detail-item"><span class="k">Status</span><span><span class="badge">{{ status_labels.get(article.status_ui, article.status_ui) }}</span></span></div>
<div class="detail-item"><span class="k">Redaktionelle Freigabe</span><span>
{% if article.editorial_review_at %}
<span class="badge ok">geprüft</span> {{ article.editorial_review_at }} durch {{ article.editorial_review_by }}
{% else %}
<span class="badge bad">offen</span>
{% endif %}
</span></div>
<div class="detail-item"><span class="k">Artikel-Datum</span><span>{{ article.published_at or "-" }}</span></div>
<div class="detail-item"><span class="k">Alter</span><span>{{ article.days_old if article.days_old is not none else "-" }} Tage</span></div>
<div class="detail-item"><span class="k">Relevanz</span><span>{{ article.relevance }}</span></div>
<div class="detail-item"><span class="k">Autor</span><span>{{ article.author or "-" }}</span></div>
<div class="detail-item"><span class="k">Feed</span><span>{{ feed.name if feed else "-" }}</span></div>
<div class="detail-item"><span class="k">Quelle Snapshot</span><span>{{ article.source_name_snapshot or "-" }}</span></div>
<div class="detail-item"><span class="k">Lizenz Snapshot</span><span>{{ article.source_license_name_snapshot or "-" }}</span></div>
<div class="detail-item"><span class="k">Terms Snapshot</span><span>{{ article.source_terms_url_snapshot or "-" }}</span></div>
</div>
<p><strong>Quelle:</strong> <a href="{{ article.source_url }}" target="_blank" rel="noopener">{{ article.source_url }}</a></p>
{% if article.canonical_url %}<p><strong>Canonical:</strong> <a href="{{ article.canonical_url }}" target="_blank" rel="noopener">{{ article.canonical_url }}</a></p>{% endif %}
{% if article.summary %}
<p><strong>Summary:</strong> {{ article.summary }}</p>
{% endif %}
<p><strong>WordPress Post:</strong>
{% if article.wp_post_url %}
<a href="{{ article.wp_post_url }}" target="_blank" rel="noopener">#{{ article.wp_post_id }}</a>
{% elif article.wp_post_id %}
#{{ article.wp_post_id }}
{% else %}
-
{% endif %}
</p>
<p><strong>Publish Attempts:</strong> {{ article.publish_attempts or 0 }} | <strong>Letzter Fehler:</strong> {{ article.publish_last_error or "-" }}</p>
</section>
<section class="card">
<h2>Checkliste</h2>
<table>
<thead>
<tr><th>Kriterium</th><th>Status</th><th>Wert</th></tr>
</thead>
<tbody>
{% for c in checklist %}
<tr>
<td>{{ c.label }}</td>
<td>
{% if c.status == "ok" %}
<span class="badge ok">OK</span>
{% else %}
<span class="badge bad">Fehlt</span>
{% endif %}
</td>
<td>{{ c.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="card">
<h2>Extrahierte Daten</h2>
<p><strong>Bilder:</strong> {{ article.image_entries|length if article.image_entries else 0 }}</p>
{% if article.selected_image_url %}
<p><strong>Ausgewähltes Hauptbild:</strong> <a href="{{ article.selected_image_url }}" target="_blank" rel="noopener">{{ article.selected_image_url }}</a></p>
{% if article.selected_image_proxy_url %}
<img src="{{ article.selected_image_proxy_url }}" alt="Ausgewähltes Hauptbild" class="thumb" loading="lazy" />
{% endif %}
{% endif %}
{% if article.image_entries %}
{% if article.image_selection %}
<details>
<summary>Automatische Bildauswahl (Score + Gründe)</summary>
<div class="subtle">Primärbild (Auto): {{ article.image_selection.primary or "-" }}</div>
<div class="subtle">Ausgewählt: {{ article.image_selection.selected_count or 0 }} / Kandidaten: {{ article.image_selection.total_candidates or 0 }}</div>
{% if article.image_selection.ranked %}
<table>
<thead>
<tr><th>Bild</th><th>Score</th><th>Gründe</th></tr>
</thead>
<tbody>
{% for r in article.image_selection.ranked %}
<tr>
<td><a href="{{ r.url }}" target="_blank" rel="noopener">{{ r.url }}</a></td>
<td>{{ r.score }}</td>
<td>{{ r.reasons|join(", ") if r.reasons else "-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</details>
{% endif %}
<div class="image-grid">
{% for image in article.image_entries %}
<article class="image-card {{ 'image-selected' if image.is_selected else '' }} {{ 'image-excluded' if image.is_excluded else '' }}">
<a href="{{ image.url }}" target="_blank" rel="noopener">
<img src="{{ image.proxy_url }}" data-fallback-src="{{ image.url }}" alt="Artikelbild" loading="lazy" onerror="if(!this.dataset.fallbackUsed){this.dataset.fallbackUsed='1';this.src=this.dataset.fallbackSrc;}else{this.classList.add('img-failed');}" />
</a>
<div class="image-meta">
{% if image.is_selected %}<span class="badge ok">Ausgewählt</span>{% endif %}
{% if image.is_excluded %}<span class="badge bad">Ausgeblendet</span>{% endif %}
{% if image.is_irrelevant_hint %}<span class="badge">evtl. irrelevant</span>{% endif %}
</div>
<div class="image-actions">
<form method="post" action="/admin/articles/{{ article.id }}/images/decision">
<input type="hidden" name="image_url" value="{{ image.url }}" />
<input type="hidden" name="action" value="select" />
<button type="submit">Als Hauptbild</button>
</form>
{% if not image.is_excluded %}
<form method="post" action="/admin/articles/{{ article.id }}/images/decision">
<input type="hidden" name="image_url" value="{{ image.url }}" />
<input type="hidden" name="action" value="exclude" />
<button type="submit" class="secondary">Ausblenden</button>
</form>
{% else %}
<form method="post" action="/admin/articles/{{ article.id }}/images/decision">
<input type="hidden" name="image_url" value="{{ image.url }}" />
<input type="hidden" name="action" value="restore" />
<button type="submit" class="secondary">Einblenden</button>
</form>
{% endif %}
</div>
<div class="subtle"><a href="{{ image.url }}" target="_blank" rel="noopener">{{ image.url }}</a></div>
</article>
{% endfor %}
</div>
{% endif %}
{% if article.press_contact or article.extraction.press_contact %}
<p><strong>Pressekontakt</strong></p>
<div class="pre">{{ article.press_contact or article.extraction.press_contact }}</div>
{% endif %}
{% if article.extraction.extraction_error %}
<p class="subtle">Extraktionsfehler: {{ article.extraction.extraction_error }}</p>
{% endif %}
</section>
<section class="card">
<h2>Volltext</h2>
<div class="pre">{{ article.content_raw or "-" }}</div>
</section>
<section class="card">
<h2>Rewrite-Text (editierbar)</h2>
<form method="post" action="/admin/articles/{{ article.id }}/rewrite-save" class="stack">
<textarea name="content_rewritten" rows="14" style="width:100%;">{{ article.content_rewritten or "" }}</textarea>
<button type="submit">Rewrite-Text speichern</button>
</form>
{% if article.meta.generated_tags %}
<p><strong>Generierte Tags:</strong> {{ article.meta.generated_tags|join("; ") }}</p>
{% endif %}
<p class="subtle">Dieser Text wird für den WordPress-Entwurf verwendet, falls vorhanden.</p>
</section>
<section class="card">
<h2>Redaktionelle Freigabe</h2>
{% if article.editorial_review_at %}
<p>
<span class="badge ok">Redaktionell geprüft</span>
am <strong>{{ article.editorial_review_at }}</strong> durch <strong>{{ article.editorial_review_by }}</strong>
</p>
{% if article.editorial_review_note %}<p class="subtle">Notiz: {{ article.editorial_review_note }}</p>{% endif %}
<p class="subtle">
Dieser Eintrag ist der Nachweis der menschlichen redaktionellen Kontrolle
nach Art. 50 Abs. 4 KI-VO. Zeitpunkt und Prüfer setzt das System, sie sind
nicht nachträglich editierbar.
</p>
{% elif article.status_ui in ["freigabe", "rewrite", "no_image"] %}
<p class="subtle">
Erst nach deiner Freigabe geht der Artikel nach WordPress und bekommt einen
Veröffentlichungs-Slot. Datum und Uhrzeit der Prüfung werden automatisch gesetzt.
</p>
<form method="post" action="/admin/articles/{{ article.id }}/approve" class="row">
<input name="note" placeholder="Notiz zur Prüfung (optional)" style="min-width:260px;" />
<button type="submit">✅ Redaktionell geprüft &amp; freigeben</button>
</form>
<div class="row" style="margin-top:8px;">
<form method="post" action="/admin/articles/{{ article.id }}/rewrite-run" class="inline">
<button type="submit" class="secondary" title="Text mit OpenAI neu schreiben lassen">✏️ Neu schreiben</button>
</form>
<form method="post" action="/admin/articles/{{ article.id }}/transition" class="inline">
<input type="hidden" name="target_status" value="close" />
<input type="hidden" name="note" value="Bei der redaktionellen Prüfung verworfen" />
<button type="submit" class="secondary">❌ Verwerfen</button>
</form>
</div>
{% else %}
<p class="subtle">Für diesen Status ist keine Freigabe vorgesehen.</p>
{% endif %}
</section>
<section class="card">
<h2>Status ändern</h2>
{% if article.status_ui in ["new", "rewrite", "no_image"] %}
<form method="post" action="/admin/articles/{{ article.id }}/rewrite-run" class="row" style="margin-bottom:8px;">
<button type="submit">Rewrite ausführen (OpenAI)</button>
</form>
{% endif %}
{% if article.status_ui == "published" %}
<form method="post" action="/admin/articles/{{ article.id }}/reopen" class="row" style="margin-bottom:8px;">
<button type="submit">Zurück in Rewrite-Workflow</button>
</form>
{% endif %}
<form method="post" action="/admin/articles/{{ article.id }}/transition" class="row">
<select name="target_status">
{% for s in allowed_transitions %}
<option value="{{ s }}">{{ status_labels.get(s, s) }}</option>
{% endfor %}
</select>
<input name="note" placeholder="Notiz" />
<button type="submit" class="secondary">Setzen</button>
</form>
</section>
<section class="card">
<h2>WordPress Publish Queue</h2>
{% if article.publish_ready %}
<p><span class="badge ok">Publish bereit</span></p>
{% else %}
<p><span class="badge bad">Publish blockiert</span></p>
{% if article.publish_blockers %}
<ul>
{% for reason in article.publish_blockers %}
<li class="subtle">{{ reason }}</li>
{% endfor %}
</ul>
{% endif %}
{% endif %}
<p class="subtle">Voraussetzungen: Status `publish` und Hauptbild gesetzt.</p>
<form method="post" action="/admin/articles/{{ article.id }}/publish-enqueue" class="row">
<input name="max_attempts" value="3" />
<button type="submit" {% if not article.publish_ready %}disabled{% endif %}>In Queue einreihen</button>
</form>
</section>
</main>
</body>
</html>