feat(pipeline): redaktionelle Freigabe vor der Veroeffentlichung

Die Pipeline liess GPT Artikel umschreiben und legte sie direkt als
geplanten WordPress-Beitrag an - ohne dass ein Mensch sie gesehen hat.
Der KI-Hinweis auf dem Blog sagt aber redaktionelle Pruefung zu, und
genau daran haengt die Ausnahme in Art. 50 Abs. 4 KI-VO.

Neuer Status `pending_review` zwischen Rewrite und Publish: Die Pipeline
endet beim Rewrite, ohne WordPress-Beitrag und ohne Publish-Slot. Erst
die Freigabe im Portal stempelt Pruefer und Systemzeit, reserviert den
Slot und legt den Beitrag an.

- Migration: editorial_review_at/_by/_note, Status-CHECK erweitert
- Spalten-Migration laeuft nach den Tabellen-Neubauten erneut, sonst
  verwirft der aeltere no_image-Rebuild die frisch angelegten Spalten
- Jeder Weg nach `approved` stempelt (Button, Statuswechsel, API)
- Jeder maschinelle Rewrite loescht einen alten Stempel
- Telegram: Info mit Portal-Link statt Draft-Meldung, kein Freigabe-Button
- Altbestand bleibt unberuehrt und veroeffentlicht weiter
- EDITORIAL_REVIEW_REQUIRED=false stellt den alten Ablauf wieder her
- 14 neue Tests, docs/KI-VO.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Oliver 2026-08-24 10:35:49 +02:00
parent 682755c6a0
commit a233012887
No known key found for this signature in database
21 changed files with 1151 additions and 64 deletions

View file

@ -30,7 +30,14 @@
<section class="card">
<h2>{{ article.title }}</h2>
<div class="detail-grid">
<div class="detail-item"><span class="k">Status</span><span><span class="badge">{{ article.status_ui }}</span></span></div>
<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>
@ -176,9 +183,46 @@
<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"] %}
{% 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>
@ -191,7 +235,7 @@
<form method="post" action="/admin/articles/{{ article.id }}/transition" class="row">
<select name="target_status">
{% for s in allowed_transitions %}
<option value="{{ s }}">{{ s }}</option>
<option value="{{ s }}">{{ status_labels.get(s, s) }}</option>
{% endfor %}
</select>
<input name="note" placeholder="Notiz" />