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:
parent
682755c6a0
commit
a233012887
21 changed files with 1151 additions and 64 deletions
|
|
@ -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 & 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" />
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
.badge-error { background: #fee2e2; color: #991b1b; }
|
||||
.badge-published { background: #ede9fe; color: #5b21b6; }
|
||||
.badge-review { background: #fef3c7; color: #92400e; }
|
||||
.badge-pending_review { background: #ffedd5; color: #9a3412; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
<label>Status</label>
|
||||
<select name="status_filter">
|
||||
<option value="">Alle</option>
|
||||
{% for s in ["new","review","approved","published","error","no_image"] %}
|
||||
{% for s in ["new","review","pending_review","approved","published","error","no_image"] %}
|
||||
<option value="{{ s }}" {% if status_filter == s %}selected{% endif %}>{{ s }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -236,12 +236,19 @@
|
|||
|
||||
<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/dashboard?status_filter=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 %}>{{ s }}</option>
|
||||
<option value="{{ s }}" {% if status_filter == s %}selected{% endif %}>{{ status_labels.get(s, s) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="secondary">Filtern</button>
|
||||
|
|
@ -267,7 +274,7 @@
|
|||
<br /><a href="{{ a.canonical_url }}" target="_blank" rel="noopener">Canonical öffnen</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="badge">{{ a.status_ui }}</span></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 %}
|
||||
|
|
@ -323,7 +330,7 @@
|
|||
<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 }}">{{ s }}</option>
|
||||
<option value="{{ s }}">{{ status_labels.get(s, s) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if allowed_transitions.get(a.status_ui, []) %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue