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):