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:
+53
-14
@@ -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)
|
||||
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))
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user