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:
+50
-11
@@ -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 {{ 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: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; }}
|
.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; }}
|
.upload-label {{ display: inline-flex; }}
|
||||||
|
|
||||||
.token-indicator {{ font-size: 0.7rem; margin-left: 0.6rem; opacity: 0.8; }}
|
.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="selectAll()">Alle auswählen</button>
|
||||||
<button class="btn btn-select" onclick="deselectAll()">Auswahl aufheben</button>
|
<button class="btn btn-select" onclick="deselectAll()">Auswahl aufheben</button>
|
||||||
<button class="btn btn-upload" onclick="toggleUpload()">📷 Bild hochladen</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-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>
|
<button class="btn btn-delete" id="deleteBtn" onclick="deleteSelected()" disabled>Markierte löschen</button>
|
||||||
<span class="counter" id="counter">0 markiert</span>
|
<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);
|
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() {{
|
async function publishSelected() {{
|
||||||
const selected = Array.from(document.querySelectorAll('.article-check:checked')).map(cb => cb.value);
|
const selected = Array.from(document.querySelectorAll('.article-check:checked')).map(cb => cb.value);
|
||||||
if (selected.length === 0) return;
|
if (selected.length === 0) return;
|
||||||
|
const live = document.getElementById('liveToggle').checked;
|
||||||
|
|
||||||
// Readiness-Warnung (soft, blockiert nicht)
|
// Readiness-Warnung (soft, blockiert nicht)
|
||||||
const notReady = [];
|
const notReady = [];
|
||||||
@@ -488,11 +511,15 @@ def build_index(articles: list[dict]) -> str:
|
|||||||
notReady.push(row.querySelector('.article-link')?.textContent || cb.value);
|
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).';
|
let confirmMsg;
|
||||||
if (notReady.length > 0) {{
|
if (live) {{
|
||||||
confirmMsg += '\\n\\n⚠️ ' + notReady.length + ' Artikel ohne Meta-Daten/Bild. Upload erfolgt TROTZDEM.';
|
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;
|
if (!confirm(confirmMsg)) return;
|
||||||
|
|
||||||
publishBtn.disabled = true;
|
publishBtn.disabled = true;
|
||||||
@@ -503,11 +530,11 @@ def build_index(articles: list[dict]) -> str:
|
|||||||
const resp = await fetch('/publish', {{
|
const resp = await fetch('/publish', {{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {{'Content-Type': 'application/json', 'Authorization': 'Basic ' + auth}},
|
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();
|
const data = await resp.json();
|
||||||
if (data.ok) {{
|
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);
|
setTimeout(() => location.reload(), 2000);
|
||||||
}} else {{
|
}} else {{
|
||||||
showToast('Fehler: ' + (data.error || 'unbekannt'), true);
|
showToast('Fehler: ' + (data.error || 'unbekannt'), true);
|
||||||
@@ -823,7 +850,11 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
|||||||
|
|
||||||
def _handle_publish(self):
|
def _handle_publish(self):
|
||||||
"""Publish selected articles via Joomla API with VG-Wort pixel injection
|
"""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:
|
if not JOOMLA_TOKEN:
|
||||||
self._json_reply(400, {
|
self._json_reply(400, {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
@@ -838,6 +869,7 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
|||||||
try:
|
try:
|
||||||
data = json.loads(body)
|
data = json.loads(body)
|
||||||
files = data.get("files", [])
|
files = data.get("files", [])
|
||||||
|
allow_live = data.get("live", False) # SAFETY GATE: Nur wenn explizit true
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
self._json_reply(400, {"ok": False, "error": "Invalid JSON"})
|
self._json_reply(400, {"ok": False, "error": "Invalid JSON"})
|
||||||
return
|
return
|
||||||
@@ -847,6 +879,10 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
|||||||
errors = []
|
errors = []
|
||||||
img_map = get_image_map()
|
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 importing requests
|
||||||
try:
|
try:
|
||||||
import requests
|
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)
|
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
|
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)
|
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)
|
cat_match = re.search(r'<meta\s+name="category-id"\s+content="(\d+)"', html_content, re.IGNORECASE)
|
||||||
if cat_match:
|
if cat_match:
|
||||||
catid = int(cat_match.group(1))
|
catid = int(cat_match.group(1))
|
||||||
|
|
||||||
# Korrektur (61) → state=0 (unpublished/Redaktion-only)
|
# 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
|
state = 0 if catid == 61 else 1
|
||||||
access = 6 if catid == 61 else 1 # 6=Redaktion, 1=Public
|
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 = 0
|
||||||
featured_up = None
|
featured_up = None
|
||||||
featured_down = None
|
featured_down = None
|
||||||
if catid != 61:
|
if allow_live and catid != 61:
|
||||||
# Default: 3 Tage featured, via <meta name="featured-days"> anpassbar
|
# Default: 3 Tage featured, via <meta name="featured-days"> anpassbar
|
||||||
featured_days = 3
|
featured_days = 3
|
||||||
fd_match = re.search(
|
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, {
|
self._json_reply(200 if published > 0 else 500, {
|
||||||
"ok": published > 0,
|
"ok": published > 0,
|
||||||
"published": published,
|
"published": published,
|
||||||
|
"live_mode": allow_live,
|
||||||
"images_deleted": images_deleted_total,
|
"images_deleted": images_deleted_total,
|
||||||
"errors": errors,
|
"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):
|
def _json_reply(self, status, data):
|
||||||
|
|||||||
Reference in New Issue
Block a user