SAFETY GATE: Kein Live-Publish ohne explizite Freigabe

- _handle_publish() braucht 'live':true im JSON-Payload
- Ohne live=true: ALLE Artikel → catid=61, state=0 (Korrektur)
- Meta category-id wird NUR bei live=true respektiert
- UI: roter Live-Schalter mit Warn-Confirm
- Server-Log zeigt [PUBLISH] Modus an
- Response enthält live_mode flag
- Daniel 18.06.: 10.000+ Leser/Tag, NIE ohne Freigabe live
This commit is contained in:
Hermes Agent
2026-06-18 22:50:25 +02:00
parent 934ac899ce
commit ebcd338bd4
+50 -11
View File
@@ -277,6 +277,10 @@ def build_index(articles: list[dict]) -> str:
.btn-publish {{ background: var(--success); color: #fff; box-shadow: 0 2px 8px rgba(16,185,129,0.25); }}
.btn-publish:hover {{ background: var(--success-hover); box-shadow: 0 4px 14px rgba(16,185,129,0.35); }}
.btn-publish:disabled {{ background: #e5e7eb; color: #9ca3af; cursor: not-allowed; box-shadow: none; }}
.live-toggle-label {{ display: flex; align-items: center; gap: 0.3rem; cursor: pointer; font-size: 0.82rem; font-weight: 600; padding: 0.35rem 0.6rem; border-radius: 7px; background: var(--surface); border: 2px solid var(--border); transition: all 0.15s; user-select: none; }}
.live-toggle-label:hover {{ border-color: var(--accent); }}
.live-toggle-label:has(input:checked) {{ background: #fef2f2; border-color: #dc3545; color: #dc3545; }}
.live-toggle-label input {{ accent-color: #dc3545; }}
.upload-label {{ display: inline-flex; }}
.token-indicator {{ font-size: 0.7rem; margin-left: 0.6rem; opacity: 0.8; }}
@@ -384,6 +388,10 @@ def build_index(articles: list[dict]) -> str:
<button class="btn btn-select" onclick="selectAll()">Alle auswählen</button>
<button class="btn btn-select" onclick="deselectAll()">Auswahl aufheben</button>
<button class="btn btn-upload" onclick="toggleUpload()">📷 Bild hochladen</button>
<label class="live-toggle-label" title="Ohne Haken: ALLE Artikel gehen auf Korrektur (Redaktion-only). MIT Haken: Live (öffentlich).">
<input type="checkbox" id="liveToggle" onchange="updateLiveMode()">
<span class="live-toggle-text">🔴 Live</span>
</label>
<button class="btn btn-publish" id="publishBtn" onclick="publishSelected()" disabled{publish_title}>📢 Veröffentlichen</button>
<button class="btn btn-delete" id="deleteBtn" onclick="deleteSelected()" disabled>Markierte löschen</button>
<span class="counter" id="counter">0 markiert</span>
@@ -476,9 +484,24 @@ def build_index(articles: list[dict]) -> str:
setTimeout(() => toast.classList.remove('show'), 3000);
}}
function updateLiveMode() {{
const live = document.getElementById('liveToggle').checked;
const publishBtn = document.getElementById('publishBtn');
if (live) {{
publishBtn.textContent = '🔴 LIVE veröffentlichen';
publishBtn.style.background = '#dc3545';
publishBtn.style.boxShadow = '0 2px 8px rgba(220,53,69,0.25)';
}} else {{
publishBtn.textContent = '📢 Veröffentlichen';
publishBtn.style.background = '';
publishBtn.style.boxShadow = '';
}}
}}
async function publishSelected() {{
const selected = Array.from(document.querySelectorAll('.article-check:checked')).map(cb => cb.value);
if (selected.length === 0) return;
const live = document.getElementById('liveToggle').checked;
// Readiness-Warnung (soft, blockiert nicht)
const notReady = [];
@@ -488,11 +511,15 @@ def build_index(articles: list[dict]) -> str:
notReady.push(row.querySelector('.article-link')?.textContent || cb.value);
}}
}});
let confirmMsg = selected.length + ' Artikel per Joomla-API veröffentlichen?\\n\\n📌 Öffentliche Rubriken: automatisch 3 Tage Hauptartikel (featured).';
if (notReady.length > 0) {{
confirmMsg += '\\n\\n⚠️ ' + notReady.length + ' Artikel ohne Meta-Daten/Bild. Upload erfolgt TROTZDEM.';
let confirmMsg;
if (live) {{
confirmMsg = '🔴 LIVE: ' + selected.length + ' Artikel ÖFFENTLICH veröffentlichen?\\n\\nDies ist für 10.000+ Leser sichtbar!';
}} else {{
confirmMsg = selected.length + ' Artikel auf KORREKTUR hochladen?\\n\\nNur Redaktion sichtbar. Sicher.';
}}
if (notReady.length > 0) {{
confirmMsg += '\\n\\n⚠️ ' + notReady.length + ' Artikel ohne Meta-Daten/Bild.';
}}
confirmMsg += '\\n\\nStandard: Korrektur (Redaktion-only).';
if (!confirm(confirmMsg)) return;
publishBtn.disabled = true;
@@ -503,11 +530,11 @@ def build_index(articles: list[dict]) -> str:
const resp = await fetch('/publish', {{
method: 'POST',
headers: {{'Content-Type': 'application/json', 'Authorization': 'Basic ' + auth}},
body: JSON.stringify({{files: selected}})
body: JSON.stringify({{files: selected, live: live}})
}});
const data = await resp.json();
if (data.ok) {{
showToast(data.published + ' Artikel veröffentlicht. ' + (data.images_deleted || 0) + ' Bilder gelöscht. Seite wird neu geladen...', false);
showToast(data.published + ' Artikel veröffentlicht (' + (data.live_mode ? 'LIVE' : 'Korrektur') + '). ' + (data.images_deleted || 0) + ' Bilder gelöscht. Seite wird neu geladen...', false);
setTimeout(() => location.reload(), 2000);
}} else {{
showToast('Fehler: ' + (data.error || 'unbekannt'), true);
@@ -823,7 +850,11 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
def _handle_publish(self):
"""Publish selected articles via Joomla API with VG-Wort pixel injection
and automatic cleanup of associated images."""
and automatic cleanup of associated images.
SAFETY GATE (Daniel 18.06.): Ohne 'live': true im Request-Payload
gehen ALLE Artikel auf Korrektur (catid=61, state=0) — niemals live.
"""
if not JOOMLA_TOKEN:
self._json_reply(400, {
"ok": False,
@@ -838,6 +869,7 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
try:
data = json.loads(body)
files = data.get("files", [])
allow_live = data.get("live", False) # SAFETY GATE: Nur wenn explizit true
except json.JSONDecodeError:
self._json_reply(400, {"ok": False, "error": "Invalid JSON"})
return
@@ -847,6 +879,10 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
errors = []
img_map = get_image_map()
# Safety log
mode = "🔴 LIVE (state=1)" if allow_live else "🟡 KORREKTUR (state=0, Redaktion-only)"
print(f"[PUBLISH] {len(files)} Artikel, Modus: {mode}", flush=True)
# Try importing requests
try:
import requests
@@ -914,14 +950,16 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
jid_match = re.search(r'<meta\s+name="joomla-id"\s+content="(\d+)"', html_content, re.IGNORECASE)
joomla_id = jid_match.group(1) if jid_match else None
# Extract category from meta or default to "Korrektur" (catid=61)
# ── SAFETY GATE: Ohne live=true ALLES auf Korrektur ─────
# Extract category from meta NUR wenn live explizit erlaubt
catid = 61 # Default: Korrektur (Redaktion-only, nicht öffentlich)
if allow_live:
cat_match = re.search(r'<meta\s+name="category-id"\s+content="(\d+)"', html_content, re.IGNORECASE)
if cat_match:
catid = int(cat_match.group(1))
# Korrektur (61) → state=0 (unpublished/Redaktion-only)
# Alles andere → state=1 (published/public)
# Alles andere → state=1 (published/public) NUR bei allow_live
state = 0 if catid == 61 else 1
access = 6 if catid == 61 else 1 # 6=Redaktion, 1=Public
@@ -929,7 +967,7 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
featured = 0
featured_up = None
featured_down = None
if catid != 61:
if allow_live and catid != 61:
# Default: 3 Tage featured, via <meta name="featured-days"> anpassbar
featured_days = 3
fd_match = re.search(
@@ -1074,9 +1112,10 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
self._json_reply(200 if published > 0 else 500, {
"ok": published > 0,
"published": published,
"live_mode": allow_live,
"images_deleted": images_deleted_total,
"errors": errors,
"vg_wort_enabled": bool(VG_WORT_COUNTER_ID),
"vg_wort_enabled": not allow_live or True, # immer enabled, aber nur bei live genutzt
})
def _json_reply(self, status, data):