diff --git a/article_server.py b/article_server.py index ea23202..27621a5 100644 --- a/article_server.py +++ b/article_server.py @@ -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: + 0 markiert @@ -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;} 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;} 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;} 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;} jid_match = re.search(r' featured = 0 featured_up = None featured_down = None - if catid != 61: + if allow_live and catid != 61: # Default: 3 Tage featured, via anpassbar featured_days = 3 fd_match = re.search( @@ -1074,9 +1112,10 @@ h1{color:#c0392b;}p{color:#888;} 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):