FünfVorAcht: - Redaktionsplan zeigt jetzt mehrere Artikel pro Tag - Umplanen-Button (Inline-Panel mit Slot-Prüfung, 15-Min-Raster) - Löschen-Button mit Sicherheitsabfrage - STATE.md aktualisiert (Changelog 26.02.2026) Redakteur: - STATE.md mit vollständigem Sprint-Plan angelegt - Übernommene Komponenten aus FünfVorAcht dokumentiert - 4 Sprints: Infrastruktur → WP-Anbindung → Editor → Polish Made-with: Cursor
950 lines
44 KiB
HTML
950 lines
44 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>🕗 FünfVorAcht Dashboard</title>
|
||
<link rel="stylesheet" href="/static/tailwind.min.css">
|
||
<style>
|
||
body { background: #0f172a; color: #e2e8f0; }
|
||
.card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; }
|
||
.status-draft { background: #1e293b22; border-color: #475569; color: #94a3b8; }
|
||
.status-pending { background: #92400e22; border-color: #d97706; color: #fbbf24; }
|
||
.status-sent { background: #4c1d9522; border-color: #7c3aed; color: #c4b5fd; }
|
||
.status-approved { background: #14532d22; border-color: #16a34a; color: #4ade80; }
|
||
.status-posted { background: #1e3a5f22; border-color: #3b82f6; color: #60a5fa; }
|
||
.status-skipped { background: #1e293b; border-color: #475569; color: #64748b; }
|
||
.tg-preview { background: #17212b; border-left: 3px solid #3b82f6; font-family: -apple-system,system-ui,sans-serif; white-space: pre-wrap; line-height: 1.7; }
|
||
textarea { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; border-radius: 8px; resize: vertical; font-family: inherit; line-height: 1.6; }
|
||
textarea:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 2px #3b82f620; }
|
||
input[type=text], input[type=time], select {
|
||
background: #0f172a; border: 1px solid #334155; color: #e2e8f0;
|
||
border-radius: 8px; padding: 0.5rem 0.75rem; font-size: 0.875rem;
|
||
}
|
||
input:focus, select:focus { outline: none; border-color: #3b82f6; }
|
||
.btn { border-radius: 8px; font-size: 0.875rem; font-weight: 500; padding: 0.5rem 1.25rem; transition: all .15s; cursor: pointer; }
|
||
.btn-primary { background: #2563eb; color: #fff; }
|
||
.btn-primary:hover { background: #1d4ed8; }
|
||
.btn-success { background: #15803d; color: #fff; }
|
||
.btn-success:hover { background: #166534; }
|
||
.btn-purple { background: #6d28d9; color: #fff; }
|
||
.btn-purple:hover { background: #5b21b6; }
|
||
.btn-ghost { background: #334155; color: #cbd5e1; }
|
||
.btn-ghost:hover { background: #475569; }
|
||
.btn-danger { background: #991b1b; color: #fff; }
|
||
.btn-danger:hover { background: #7f1d1d; }
|
||
.spinner { animation: spin 1s linear infinite; display: inline-block; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
.countdown { font-variant-numeric: tabular-nums; }
|
||
</style>
|
||
</head>
|
||
<body class="min-h-screen">
|
||
|
||
<!-- Nav -->
|
||
<nav class="bg-slate-900 border-b border-slate-700 px-6 py-3 flex items-center justify-between sticky top-0 z-50">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-xl">🕗</span>
|
||
<span class="text-lg font-bold text-white">FünfVorAcht</span>
|
||
</div>
|
||
<div class="flex items-center gap-5 text-sm">
|
||
<a href="/" class="text-blue-400 font-semibold">Studio</a>
|
||
<a href="/history" class="text-slate-400 hover:text-white">History</a>
|
||
<a href="/prompts" class="text-slate-400 hover:text-white">Prompts</a>
|
||
<a href="/settings" class="text-slate-400 hover:text-white">Einstellungen</a>
|
||
<a href="/hilfe" class="text-slate-400 hover:text-white">❓ Hilfe</a>
|
||
|
||
<!-- Aufenthaltsort-Schalter -->
|
||
<div class="relative">
|
||
<button onclick="toggleLocationMenu()"
|
||
class="flex items-center gap-2 bg-slate-800 hover:bg-slate-700 border border-slate-600 px-3 py-1.5 rounded-lg text-sm transition"
|
||
id="location-btn">
|
||
<span id="location-flag">{{ current_location.flag if current_location else '🌍' }}</span>
|
||
<span id="location-name" class="text-slate-200">{{ current_location.name if current_location else 'Ort wählen' }}</span>
|
||
<span class="text-slate-500">▾</span>
|
||
</button>
|
||
<div id="location-menu" class="hidden absolute right-0 top-10 bg-slate-800 border border-slate-600 rounded-xl shadow-xl z-50 w-52 py-1 overflow-hidden">
|
||
{% for loc in locations %}
|
||
<button onclick="setLocation({{ loc.id }}, '{{ loc.flag }}', '{{ loc.name }}')"
|
||
class="w-full text-left px-4 py-2.5 hover:bg-slate-700 flex items-center gap-3 text-sm
|
||
{% if current_location and current_location.id == loc.id %}bg-slate-700 text-blue-400{% else %}text-slate-200{% endif %}">
|
||
<span class="text-base">{{ loc.flag }}</span>
|
||
<div>
|
||
<div class="font-medium">{{ loc.name }}</div>
|
||
<div class="text-xs text-slate-400">{{ loc.timezone }}</div>
|
||
</div>
|
||
{% if current_location and current_location.id == loc.id %}
|
||
<span class="ml-auto text-blue-400">✓</span>
|
||
{% endif %}
|
||
</button>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Live-Uhr + Countdown -->
|
||
<div class="text-right">
|
||
<div class="flex items-center gap-2 justify-end">
|
||
<span class="text-white font-mono text-sm" id="live-clock-berlin">--:--:--</span>
|
||
<span class="text-xs text-slate-500">🇩🇪</span>
|
||
</div>
|
||
<div id="local-clock-row" class="flex items-center gap-2 justify-end" style="display:none">
|
||
<span class="text-slate-400 font-mono text-xs" id="live-clock-local">--:--:--</span>
|
||
<span class="text-xs text-slate-600" id="local-flag"></span>
|
||
</div>
|
||
<div class="text-xs text-green-400 countdown" id="countdown">--</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="max-w-6xl mx-auto px-6 py-6 space-y-6">
|
||
|
||
<!-- ═══ ARTIKEL-STUDIO ═══ -->
|
||
<div class="card p-6">
|
||
<div class="flex items-center justify-between mb-5">
|
||
<h2 class="text-lg font-bold text-white">✏️ Artikel-Studio — <span id="studio-date" class="text-blue-400">{{ today }}</span></h2>
|
||
<div class="flex items-center gap-3">
|
||
{% if article_today %}
|
||
<span id="status-badge" class="border text-xs px-3 py-1 rounded-full
|
||
{% if article_today.status == 'draft' %}status-draft
|
||
{% elif article_today.status == 'sent_to_bot' %}status-sent
|
||
{% elif article_today.status == 'approved' %}status-approved
|
||
{% elif article_today.status == 'posted' %}status-posted
|
||
{% elif article_today.status == 'skipped' %}status-skipped
|
||
{% else %}status-pending{% endif %}">
|
||
{{ {'draft':'📝 Entwurf','sent_to_bot':'📱 Beim Bot','approved':'✅ Freigegeben','posted':'📤 Gepostet','skipped':'⏭️ Übersprungen','pending_review':'⏳ Offen'}.get(article_today.status, article_today.status) }}
|
||
</span>
|
||
{% else %}
|
||
<span id="status-badge" class="border text-xs px-3 py-1 rounded-full status-draft">📝 Neu</span>
|
||
{% endif %}
|
||
<button onclick="clearStudio()" class="text-slate-500 hover:text-white text-xs px-2 py-1 rounded hover:bg-slate-700 transition" title="Editor leeren">✕ Leeren</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
|
||
<!-- Linke Spalte: Eingabe + Editor -->
|
||
<div class="space-y-4">
|
||
|
||
<!-- Quelle -->
|
||
<div>
|
||
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wide mb-1 block">Quelle / Inhaltswunsch</label>
|
||
<div class="flex gap-2">
|
||
<input type="text" id="source-input"
|
||
class="flex-1"
|
||
placeholder="URL einfügen oder Thema beschreiben…"
|
||
value="{{ article_today.source_input if article_today and article_today.source_input else '' }}">
|
||
<!-- Favoriten-Dropdown -->
|
||
<select id="fav-select" class="w-40" onchange="useFavorite(this)">
|
||
<option value="">📌 Favoriten</option>
|
||
{% for f in favorites %}
|
||
<option value="{{ f.url }}">{{ f.label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tag + Prompt -->
|
||
<div class="flex gap-3">
|
||
<div class="flex-1">
|
||
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wide mb-1 block">Thema</label>
|
||
<select id="tag-select" class="w-full">
|
||
{% for t in tags %}
|
||
<option value="{{ t.name }}" {% if article_today and article_today.tag == t.name %}selected{% endif %}>{{ t.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div class="flex-1">
|
||
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wide mb-1 block">Prompt</label>
|
||
<select id="prompt-select" class="w-full">
|
||
{% for p in prompts %}
|
||
<option value="{{ p.id }}" {% if p.is_default %}selected{% endif %}>{{ p.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Generieren -->
|
||
<button onclick="generateArticle()" id="gen-btn" class="btn btn-primary w-full py-2.5">
|
||
⚡ Artikel generieren
|
||
</button>
|
||
|
||
<!-- Editor -->
|
||
<div>
|
||
<div class="flex items-center justify-between mb-1">
|
||
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wide">Artikel bearbeiten</label>
|
||
<span id="char-count" class="text-xs text-slate-500"></span>
|
||
</div>
|
||
<textarea id="article-editor" rows="14"
|
||
class="w-full px-3 py-2.5 text-sm"
|
||
placeholder="Hier erscheint der generierte Artikel — direkt bearbeitbar…"
|
||
oninput="updatePreview()">{{ article_today.content_final if article_today and article_today.content_final else '' }}</textarea>
|
||
</div>
|
||
|
||
<!-- Aktions-Buttons -->
|
||
<div class="flex gap-2 flex-wrap">
|
||
<button onclick="regenerate()" class="btn btn-ghost">🔄 Neu generieren</button>
|
||
<button onclick="togglePlanPanel()" id="plan-btn" class="btn btn-purple flex-1">
|
||
📅 Einplanen & zum Bot senden
|
||
</button>
|
||
<button onclick="skipToday()" class="btn btn-danger">⏭️ Überspringen</button>
|
||
</div>
|
||
|
||
<!-- Einplan-Panel -->
|
||
<div id="plan-panel" class="hidden mt-1 rounded-xl border border-purple-700 bg-purple-950/30 p-4 space-y-3">
|
||
<div class="text-sm font-semibold text-purple-300">📅 Artikel einplanen</div>
|
||
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="text-xs text-slate-400 block mb-1">Datum</label>
|
||
<input type="date" id="plan-date" class="w-full text-sm py-1.5 px-2"
|
||
onchange="checkSlot()">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-slate-400 block mb-1">Uhrzeit (15-Min-Raster)</label>
|
||
<input type="time" id="plan-time" step="900" class="w-full text-sm py-1.5 px-2"
|
||
onchange="checkSlot()">
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="text-xs text-slate-400 block mb-1">Bot-Benachrichtigung</label>
|
||
<select id="plan-notify" class="w-full text-sm py-1.5 px-2">
|
||
<option value="auto">🤖 Automatisch (heute → sofort, sonst Vortag 17:00)</option>
|
||
<option value="sofort">⚡ Sofort zum Review senden</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="slot-status" class="text-xs hidden"></div>
|
||
|
||
<div class="flex gap-2 pt-1">
|
||
<button onclick="confirmPlan()" id="confirm-plan-btn"
|
||
class="btn btn-success flex-1">✅ Einplanen bestätigen</button>
|
||
<button onclick="togglePlanPanel()" class="btn btn-ghost">✕ Abbrechen</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Rechte Spalte: Telegram-Vorschau + Timer -->
|
||
<div class="space-y-4">
|
||
|
||
<!-- Telegram-Vorschau -->
|
||
<div>
|
||
<div class="flex items-center justify-between mb-1">
|
||
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wide">📱 Telegram-Vorschau</label>
|
||
<span id="tg-char" class="text-xs text-slate-500"></span>
|
||
</div>
|
||
<div class="tg-preview rounded-xl p-4 text-sm min-h-[200px]" id="tg-preview-box">
|
||
<span class="text-slate-500 italic">Vorschau erscheint beim Bearbeiten…</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Posting-Timer kompakt -->
|
||
<div class="card p-4 border border-slate-600">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<h3 class="text-white font-semibold text-sm">⏰ Posting</h3>
|
||
<div class="flex items-center gap-2">
|
||
<input type="time" id="post-time-input" value="{{ channel.post_time or '19:55' }}" class="w-28 text-xs py-1">
|
||
<span class="text-xs text-slate-500">🇩🇪</span>
|
||
<button onclick="savePostTime()" class="text-xs text-slate-500 hover:text-white">💾</button>
|
||
<span id="save-time-ok" class="text-green-400 text-xs hidden">✓</span>
|
||
</div>
|
||
</div>
|
||
<div class="text-xs text-slate-400">
|
||
<span class="text-white font-semibold" id="next-post-time">{{ channel.post_time or '19:55' }} Uhr 🇩🇪</span>
|
||
<span id="next-post-local" class="text-slate-500 ml-2"></span>
|
||
</div>
|
||
<div id="studio-status-line" class="mt-1 text-xs text-slate-500"></div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ REDAKTIONSPLAN ═══ -->
|
||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
|
||
<!-- Planungsliste: Nächste 7 Tage -->
|
||
<div class="card p-5 lg:col-span-2">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h2 class="text-base font-semibold text-white">📅 Redaktionsplan — Nächste 7 Tage</h2>
|
||
<span class="text-xs text-slate-500">Posting: {{ channel.post_time or '19:55' }} Uhr 🇩🇪</span>
|
||
</div>
|
||
<div class="space-y-1">
|
||
{% set status_icons = {'draft':'📝','scheduled':'🗓️','sent_to_bot':'📱','approved':'✅','posted':'📤','skipped':'⏭️','pending_review':'⏳'} %}
|
||
{% set status_labels = {'draft':'Entwurf','scheduled':'Eingeplant','sent_to_bot':'Beim Bot','approved':'Freigegeben','posted':'Gepostet','skipped':'Skip','pending_review':'Offen'} %}
|
||
{% set status_css = {'posted':'status-posted','approved':'status-approved','sent_to_bot':'status-sent','scheduled':'status-sent','draft':'status-pending','pending_review':'status-pending','skipped':'status-skipped'} %}
|
||
|
||
{% for d in plan_days %}
|
||
{% set arts = plan_articles.get(d, []) %}
|
||
{% set is_today = (d == today) %}
|
||
|
||
<!-- Tag-Header -->
|
||
<div class="flex items-center gap-2 pt-2 pb-1 px-1
|
||
{% if is_today %}text-blue-400{% else %}text-slate-400{% endif %}">
|
||
<span class="text-xs font-bold">{{ d[8:] }}.{{ d[5:7] }}.</span>
|
||
{% if is_today %}<span class="text-xs bg-blue-900/40 border border-blue-700 text-blue-400 px-2 py-0.5 rounded-full">Heute</span>{% endif %}
|
||
{% if not arts %}<span class="text-xs text-slate-600 italic">— kein Artikel</span>{% endif %}
|
||
</div>
|
||
|
||
{% for art in arts %}
|
||
<!-- Artikel-Slot -->
|
||
<div class="rounded-lg border border-slate-700/50 hover:border-slate-600 transition bg-slate-800/30"
|
||
id="plan-row-{{ art.id }}">
|
||
|
||
<!-- Haupt-Zeile -->
|
||
<div class="flex items-center gap-3 px-3 py-2.5 cursor-pointer"
|
||
onclick="loadDate('{{ d }}')">
|
||
<span class="text-base w-5 shrink-0 text-center">{{ status_icons.get(art.status, '❓') }}</span>
|
||
<span class="text-xs font-mono text-slate-400 w-12 shrink-0">{{ art.post_time }}</span>
|
||
<div class="flex-1 min-w-0">
|
||
{% if art.content_final %}
|
||
<div class="text-sm text-slate-300 truncate">{{ art.content_final[:70] }}</div>
|
||
<div class="text-xs text-slate-500">v{{ art.version }}{% if art.tag %} · {{ art.tag }}{% endif %}</div>
|
||
{% else %}
|
||
<div class="text-sm text-slate-600 italic">Kein Inhalt</div>
|
||
{% endif %}
|
||
</div>
|
||
<span class="text-xs px-2 py-0.5 rounded-full border shrink-0 {{ status_css.get(art.status, 'status-pending') }}">
|
||
{{ status_labels.get(art.status, art.status) }}
|
||
</span>
|
||
|
||
<!-- Aktions-Buttons (nur wenn nicht gepostet) -->
|
||
{% if art.status != 'posted' %}
|
||
<div class="flex items-center gap-1 shrink-0" onclick="event.stopPropagation()">
|
||
<button onclick="openReschedule({{ art.id }}, '{{ d }}', '{{ art.post_time }}')"
|
||
class="text-slate-500 hover:text-yellow-400 text-xs px-1.5 py-1 rounded hover:bg-slate-700 transition"
|
||
title="Umplanen">🔄</button>
|
||
<button onclick="deleteArticle({{ art.id }}, '{{ d }}', '{{ art.post_time }}')"
|
||
class="text-slate-500 hover:text-red-400 text-xs px-1.5 py-1 rounded hover:bg-slate-700 transition"
|
||
title="Löschen">🗑️</button>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Umplan-Panel (eingeklappt) -->
|
||
<div id="reschedule-panel-{{ art.id }}" class="hidden border-t border-slate-700 px-3 py-3 bg-slate-900/50 rounded-b-lg">
|
||
<div class="text-xs text-yellow-400 font-semibold mb-2">🔄 Umplanen</div>
|
||
<div class="flex gap-2 items-end flex-wrap">
|
||
<div>
|
||
<label class="text-xs text-slate-500 block mb-1">Datum</label>
|
||
<input type="date" id="rs-date-{{ art.id }}" value="{{ d }}"
|
||
class="text-xs py-1 px-2 w-36" onchange="checkRescheduleSlot({{ art.id }})">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-slate-500 block mb-1">Uhrzeit</label>
|
||
<input type="time" id="rs-time-{{ art.id }}" value="{{ art.post_time }}" step="900"
|
||
class="text-xs py-1 px-2 w-28" onchange="checkRescheduleSlot({{ art.id }})">
|
||
</div>
|
||
<button onclick="confirmReschedule({{ art.id }})"
|
||
id="rs-confirm-{{ art.id }}"
|
||
class="text-xs bg-yellow-700 hover:bg-yellow-600 text-white px-3 py-1.5 rounded-lg transition">
|
||
✓ Bestätigen
|
||
</button>
|
||
<button onclick="closeReschedule({{ art.id }})"
|
||
class="text-xs text-slate-500 hover:text-white px-2 py-1.5 rounded-lg transition">
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
<div id="rs-status-{{ art.id }}" class="text-xs mt-2 hidden"></div>
|
||
</div>
|
||
|
||
</div>
|
||
{% endfor %}
|
||
|
||
{% if not arts %}
|
||
<div class="h-1"></div>
|
||
{% endif %}
|
||
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Monatskalender + Stats -->
|
||
<div class="space-y-4">
|
||
|
||
<!-- Mini-Monatskalender -->
|
||
<div class="card p-4">
|
||
<h3 class="text-white font-semibold mb-3 text-sm">📆 Monatsübersicht</h3>
|
||
<div class="grid grid-cols-7 gap-1 text-center text-xs mb-2">
|
||
<div class="text-slate-500 font-semibold">Mo</div>
|
||
<div class="text-slate-500 font-semibold">Di</div>
|
||
<div class="text-slate-500 font-semibold">Mi</div>
|
||
<div class="text-slate-500 font-semibold">Do</div>
|
||
<div class="text-slate-500 font-semibold">Fr</div>
|
||
<div class="text-slate-500 font-semibold">Sa</div>
|
||
<div class="text-slate-500 font-semibold">So</div>
|
||
</div>
|
||
<div id="month-grid" class="grid grid-cols-7 gap-1 text-center text-xs"></div>
|
||
<div class="flex items-center gap-4 mt-3 text-xs text-slate-500">
|
||
<span><span class="inline-block w-2 h-2 rounded-full bg-blue-500 mr-1"></span>Gepostet</span>
|
||
<span><span class="inline-block w-2 h-2 rounded-full bg-green-500 mr-1"></span>Geplant</span>
|
||
<span><span class="inline-block w-2 h-2 rounded-full bg-yellow-500 mr-1"></span>Entwurf</span>
|
||
<span><span class="inline-block w-2 h-2 rounded-full bg-slate-600 mr-1"></span>Leer</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stats -->
|
||
<div class="card p-4 border border-slate-700">
|
||
<h3 class="text-slate-300 font-semibold mb-2 text-sm">📊 {{ today[:7] }}</h3>
|
||
<div class="grid grid-cols-3 gap-2 text-center">
|
||
<div>
|
||
<div class="text-2xl font-bold text-green-400">{{ stats.posted }}</div>
|
||
<div class="text-xs text-slate-400">Gepostet</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-2xl font-bold text-slate-400">{{ stats.skipped }}</div>
|
||
<div class="text-xs text-slate-400">Skip</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-2xl font-bold text-blue-400">{{ stats.avg_version }}×</div>
|
||
<div class="text-xs text-slate-400">Ø Regen.</div>
|
||
</div>
|
||
</div>
|
||
<div class="mt-3 pt-3 border-t border-slate-700">
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-xs text-slate-400">💰 OpenRouter</span>
|
||
<button onclick="loadBalance()" class="text-xs text-slate-500 hover:text-white">🔄</button>
|
||
</div>
|
||
<div id="balance-inline" class="text-sm font-semibold text-green-400 mt-1">laden…</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Letzte Posts kompakt -->
|
||
<div class="card p-4">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<h3 class="text-slate-300 font-semibold text-sm">📋 Letzte Posts</h3>
|
||
<a href="/history" class="text-blue-400 text-xs hover:underline">Alle →</a>
|
||
</div>
|
||
{% for art in recent[:4] %}
|
||
<div class="flex items-center gap-2 py-1.5 border-b border-slate-700/30 last:border-0 cursor-pointer hover:bg-slate-700/30 rounded px-1 -mx-1"
|
||
onclick="loadDate('{{ art.date }}')">
|
||
<span class="text-xs text-slate-500 w-16 shrink-0">{{ art.date[5:] }}</span>
|
||
<span class="text-xs text-slate-400 flex-1 truncate">{{ art.content_final[:50] if art.content_final else '—' }}</span>
|
||
<span class="text-xs">{{ {'posted':'📤','approved':'✅','sent_to_bot':'📱','draft':'📝','skipped':'⏭️'}.get(art.status,'?') }}</span>
|
||
</div>
|
||
{% else %}
|
||
<div class="text-slate-500 text-xs py-2">Keine Artikel.</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
const TODAY = "{{ today }}";
|
||
let selectedDate = TODAY;
|
||
let userTimezone = "{{ current_location.timezone if current_location else 'Europe/Berlin' }}";
|
||
let userFlag = "{{ current_location.flag if current_location else '🇩🇪' }}";
|
||
const BERLIN_TZ = "Europe/Berlin";
|
||
const MONTH_ARTICLES = {{ month_articles | tojson }};
|
||
|
||
function getBerlinTime(date) {
|
||
return new Date(date.toLocaleString('en-US', {timeZone: BERLIN_TZ}));
|
||
}
|
||
|
||
function formatTime(date, tz) {
|
||
return date.toLocaleTimeString('de-DE', {hour:'2-digit', minute:'2-digit', second:'2-digit', timeZone: tz});
|
||
}
|
||
|
||
function formatTimeShort(date, tz) {
|
||
return date.toLocaleTimeString('de-DE', {hour:'2-digit', minute:'2-digit', timeZone: tz});
|
||
}
|
||
|
||
function updateClock() {
|
||
const now = new Date();
|
||
|
||
document.getElementById('live-clock-berlin').textContent = formatTime(now, BERLIN_TZ);
|
||
|
||
const isLocal = userTimezone !== BERLIN_TZ;
|
||
const localRow = document.getElementById('local-clock-row');
|
||
if (isLocal) {
|
||
localRow.style.display = 'flex';
|
||
document.getElementById('live-clock-local').textContent = formatTime(now, userTimezone);
|
||
document.getElementById('local-flag').textContent = userFlag;
|
||
} else {
|
||
localRow.style.display = 'none';
|
||
}
|
||
|
||
const postTime = document.getElementById('post-time-input').value || '19:55';
|
||
const [ph, pm] = postTime.split(':').map(Number);
|
||
|
||
const berlinNow = getBerlinTime(now);
|
||
const berlinPost = new Date(berlinNow);
|
||
berlinPost.setHours(ph, pm, 0, 0);
|
||
if (berlinPost <= berlinNow) berlinPost.setDate(berlinPost.getDate() + 1);
|
||
|
||
const diff = Math.floor((berlinPost - berlinNow) / 1000);
|
||
const h = Math.floor(diff / 3600);
|
||
const m = Math.floor((diff % 3600) / 60);
|
||
const s = diff % 60;
|
||
document.getElementById('countdown').textContent =
|
||
`Posting in ${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
|
||
|
||
updateLocalPostTime();
|
||
}
|
||
|
||
function updateLocalPostTime() {
|
||
const el = document.getElementById('next-post-local');
|
||
if (userTimezone === BERLIN_TZ) {
|
||
el.textContent = '';
|
||
return;
|
||
}
|
||
const postTime = document.getElementById('post-time-input').value || '19:55';
|
||
const [ph, pm] = postTime.split(':').map(Number);
|
||
const now = new Date();
|
||
const berlinStr = now.toLocaleDateString('en-US', {timeZone: BERLIN_TZ});
|
||
const berlinDate = new Date(berlinStr);
|
||
berlinDate.setHours(ph, pm, 0, 0);
|
||
const utcOffset = berlinDate.getTime() - new Date(berlinDate.toLocaleString('en-US', {timeZone: BERLIN_TZ})).getTime();
|
||
const postUTC = new Date(berlinDate.getTime() + utcOffset);
|
||
const localTimeStr = postUTC.toLocaleTimeString('de-DE', {hour:'2-digit', minute:'2-digit', timeZone: userTimezone});
|
||
el.textContent = `= ${localTimeStr} Uhr ${userFlag}`;
|
||
}
|
||
|
||
setInterval(updateClock, 1000);
|
||
updateClock();
|
||
|
||
// ── Telegram-Vorschau rendern ──────────────────────────────────────────────
|
||
function renderTelegram(raw) {
|
||
if (!raw) return '<span class="text-slate-500 italic">Vorschau erscheint beim Bearbeiten…</span>';
|
||
let html = raw
|
||
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||
.replace(/<b>([\s\S]*?)<\/b>/g, '<strong>$1</strong>')
|
||
.replace(/<i>([\s\S]*?)<\/i>/g, '<em class="text-slate-300">$1</em>')
|
||
.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<a href="$1" class="text-blue-400 underline">$2</a>')
|
||
.replace(/\n/g, '<br>');
|
||
return html;
|
||
}
|
||
|
||
function updatePreview() {
|
||
const text = document.getElementById('article-editor').value;
|
||
const len = text.length;
|
||
document.getElementById('tg-preview-box').innerHTML = renderTelegram(text);
|
||
document.getElementById('char-count').textContent = `${len} Zeichen`;
|
||
document.getElementById('tg-char').textContent = `${len}/4096 ${len > 4096 ? '⚠️' : '✓'}`;
|
||
}
|
||
|
||
// Init Preview wenn Artikel vorhanden
|
||
const initText = document.getElementById('article-editor').value;
|
||
if (initText) updatePreview();
|
||
|
||
// ── Favorit verwenden ──────────────────────────────────────────────────────
|
||
function useFavorite(sel) {
|
||
if (sel.value) {
|
||
document.getElementById('source-input').value = sel.value;
|
||
sel.value = '';
|
||
}
|
||
}
|
||
|
||
// ── Editor leeren ─────────────────────────────────────────────────────────
|
||
function clearStudio() {
|
||
selectedDate = TODAY;
|
||
document.getElementById('studio-date').textContent = TODAY;
|
||
document.getElementById('source-input').value = '';
|
||
document.getElementById('article-editor').value = '';
|
||
document.getElementById('tg-preview-box').innerHTML = '<span class="text-slate-500 italic">Vorschau erscheint beim Bearbeiten…</span>';
|
||
document.getElementById('char-count').textContent = '';
|
||
document.getElementById('tg-char').textContent = '';
|
||
updateStatusBadge('draft', '📝 Neu');
|
||
document.querySelectorAll('[id^="plan-row-"]').forEach(el => {
|
||
el.classList.remove('ring-2', 'ring-yellow-500');
|
||
});
|
||
}
|
||
|
||
// ── Datum wechseln (Redaktionsplan klick) ─────────────────────────────────
|
||
async function loadDate(dateStr) {
|
||
selectedDate = dateStr;
|
||
document.getElementById('studio-date').textContent = dateStr;
|
||
|
||
document.querySelectorAll('[id^="plan-row-"]').forEach(el => {
|
||
el.classList.remove('ring-2', 'ring-yellow-500');
|
||
});
|
||
const row = document.getElementById('plan-row-' + dateStr);
|
||
if (row) row.classList.add('ring-2', 'ring-yellow-500');
|
||
|
||
try {
|
||
const r = await fetch('/api/article/' + dateStr);
|
||
if (r.ok) {
|
||
const art = await r.json();
|
||
document.getElementById('source-input').value = art.source_input || '';
|
||
document.getElementById('article-editor').value = art.content_final || '';
|
||
const statusMap = {draft:'📝 Entwurf',sent_to_bot:'📱 Beim Bot',approved:'✅ Freigegeben',posted:'📤 Gepostet',skipped:'⏭️ Übersprungen',pending_review:'⏳ Offen'};
|
||
updateStatusBadge(art.status, statusMap[art.status] || art.status);
|
||
updatePreview();
|
||
} else {
|
||
document.getElementById('source-input').value = '';
|
||
document.getElementById('article-editor').value = '';
|
||
updateStatusBadge('draft', '📝 Neu');
|
||
document.getElementById('tg-preview-box').innerHTML = '<span class="text-slate-500 italic">Kein Artikel für dieses Datum</span>';
|
||
document.getElementById('char-count').textContent = '';
|
||
document.getElementById('tg-char').textContent = '';
|
||
}
|
||
} catch(e) {
|
||
console.error('loadDate error:', e);
|
||
}
|
||
}
|
||
|
||
// ── Artikel generieren ─────────────────────────────────────────────────────
|
||
async function generateArticle() {
|
||
const source = document.getElementById('source-input').value.trim();
|
||
if (!source) { alert('Bitte Quelle oder Thema eingeben'); return; }
|
||
const tag = document.getElementById('tag-select').value;
|
||
const promptId = document.getElementById('prompt-select').value;
|
||
const btn = document.getElementById('gen-btn');
|
||
|
||
btn.innerHTML = '<span class="spinner">⚙️</span> Generiere…';
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
const r = await fetch('/api/generate', {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({source, tag, prompt_id: promptId, date: selectedDate})
|
||
});
|
||
const d = await r.json();
|
||
if (d.success) {
|
||
document.getElementById('article-editor').value = d.content;
|
||
updatePreview();
|
||
updateStatusBadge('draft', '📝 Entwurf');
|
||
} else {
|
||
alert('Fehler: ' + d.error);
|
||
}
|
||
} catch(e) {
|
||
alert('Verbindungsfehler: ' + e);
|
||
} finally {
|
||
btn.innerHTML = '⚡ Artikel generieren';
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function regenerate() { await generateArticle(); }
|
||
|
||
// ── Artikel speichern (stiller Zwischenspeicher) ───────────────────────────
|
||
async function saveArticle() {
|
||
const content = document.getElementById('article-editor').value.trim();
|
||
if (!content) return;
|
||
const channel = document.getElementById('post-time-input')?.value || '19:55';
|
||
await fetch('/api/article/' + selectedDate + '/save', {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({content, post_time: channel})
|
||
});
|
||
showToast('💾 Gespeichert (' + selectedDate + ')');
|
||
}
|
||
|
||
// ── Einplan-Panel ──────────────────────────────────────────────────────────
|
||
function togglePlanPanel() {
|
||
const panel = document.getElementById('plan-panel');
|
||
const content = document.getElementById('article-editor').value.trim();
|
||
if (!content) { showToast('⚠️ Bitte erst Artikel generieren oder eingeben.'); return; }
|
||
|
||
const isHidden = panel.classList.contains('hidden');
|
||
panel.classList.toggle('hidden');
|
||
|
||
if (isHidden) {
|
||
// Defaults setzen: ausgewähltes Datum + Standard-Uhrzeit
|
||
const dateInput = document.getElementById('plan-date');
|
||
const timeInput = document.getElementById('plan-time');
|
||
dateInput.value = selectedDate;
|
||
// Nächsten vollen 15-Min-Slot ab jetzt berechnen
|
||
const now = new Date();
|
||
const m = Math.ceil(now.getMinutes() / 15) * 15;
|
||
const h = m === 60 ? now.getHours() + 1 : now.getHours();
|
||
const defaultTime = `${String(h % 24).padStart(2,'0')}:${String(m % 60).padStart(2,'0')}`;
|
||
timeInput.value = defaultTime;
|
||
// Notify-Modus anpassen
|
||
const notifySelect = document.getElementById('plan-notify');
|
||
if (selectedDate === TODAY) {
|
||
notifySelect.value = 'sofort';
|
||
} else {
|
||
notifySelect.value = 'auto';
|
||
}
|
||
checkSlot();
|
||
document.getElementById('plan-date').focus();
|
||
}
|
||
}
|
||
|
||
async function checkSlot() {
|
||
const date = document.getElementById('plan-date').value;
|
||
const time = document.getElementById('plan-time').value;
|
||
const statusEl = document.getElementById('slot-status');
|
||
const confirmBtn = document.getElementById('confirm-plan-btn');
|
||
if (!date || !time) return;
|
||
|
||
// Zeit auf 15-Min-Raster runden
|
||
const [h, m] = time.split(':').map(Number);
|
||
const roundedM = Math.round(m / 15) * 15;
|
||
const roundedH = roundedM === 60 ? h + 1 : h;
|
||
const roundedTime = `${String(roundedH % 24).padStart(2,'0')}:${String(roundedM % 60).padStart(2,'0')}`;
|
||
document.getElementById('plan-time').value = roundedTime;
|
||
|
||
try {
|
||
const r = await fetch(`/api/slots/${date}`);
|
||
const d = await r.json();
|
||
statusEl.classList.remove('hidden');
|
||
if (d.taken && d.taken.includes(roundedTime)) {
|
||
statusEl.className = 'text-xs text-red-400 bg-red-900/20 rounded p-2';
|
||
statusEl.textContent = `❌ Slot ${date} ${roundedTime} Uhr ist bereits belegt. Bitte andere Uhrzeit wählen.`;
|
||
confirmBtn.disabled = true;
|
||
confirmBtn.classList.add('opacity-50');
|
||
} else {
|
||
statusEl.className = 'text-xs text-green-400 bg-green-900/20 rounded p-2';
|
||
statusEl.textContent = `✅ Slot ${date} ${roundedTime} Uhr ist frei.`;
|
||
confirmBtn.disabled = false;
|
||
confirmBtn.classList.remove('opacity-50');
|
||
}
|
||
} catch(e) {
|
||
statusEl.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
async function confirmPlan() {
|
||
const content = document.getElementById('article-editor').value.trim();
|
||
const date = document.getElementById('plan-date').value;
|
||
const time = document.getElementById('plan-time').value;
|
||
const notify = document.getElementById('plan-notify').value;
|
||
const btn = document.getElementById('confirm-plan-btn');
|
||
|
||
if (!content || !date || !time) return;
|
||
|
||
btn.innerHTML = '<span class="spinner">⚙️</span> Wird eingeplant…';
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
// 1. Speichern (inkl. post_time)
|
||
const saveRes = await fetch(`/api/article/${date}/save`, {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({content, post_time: time})
|
||
});
|
||
const saveData = await saveRes.json();
|
||
if (!saveData.success) throw new Error(saveData.error || 'Speichern fehlgeschlagen');
|
||
|
||
// 2. Einplanen + notify_at setzen
|
||
const schedRes = await fetch(`/api/article/${date}/schedule`, {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({post_time: time, notify_mode: notify})
|
||
});
|
||
const schedData = await schedRes.json();
|
||
if (!schedData.success) throw new Error(schedData.error || 'Einplanen fehlgeschlagen');
|
||
|
||
// Panel schließen
|
||
document.getElementById('plan-panel').classList.add('hidden');
|
||
updateStatusBadge('scheduled', '🗓️ Eingeplant');
|
||
|
||
if (notify === 'sofort') {
|
||
showToast(`✅ Eingeplant ${date} ${time} Uhr — Review sofort gesendet!`);
|
||
} else {
|
||
showToast(`✅ Eingeplant ${date} ${time} Uhr — Bot-Benachrichtigung folgt automatisch.`);
|
||
}
|
||
btn.innerHTML = '✅ Einplanen bestätigen';
|
||
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
btn.innerHTML = '✅ Einplanen bestätigen';
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// ── Überspringen ───────────────────────────────────────────────────────────
|
||
async function skipToday() {
|
||
if (!confirm(selectedDate + ' überspringen?')) return;
|
||
await fetch('/api/article/' + selectedDate + '/skip', {method:'POST'});
|
||
updateStatusBadge('skipped', '⏭️ Übersprungen');
|
||
}
|
||
|
||
// ── Posting-Zeit speichern ─────────────────────────────────────────────────
|
||
async function savePostTime() {
|
||
const t = document.getElementById('post-time-input').value;
|
||
await fetch('/api/settings/post-time', {
|
||
method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({post_time: t})
|
||
});
|
||
document.getElementById('next-post-time').textContent = t + ' Uhr';
|
||
const ok = document.getElementById('save-time-ok');
|
||
ok.classList.remove('hidden');
|
||
setTimeout(() => ok.classList.add('hidden'), 2000);
|
||
}
|
||
|
||
// ── Status-Badge aktualisieren ─────────────────────────────────────────────
|
||
function updateStatusBadge(status, label) {
|
||
const badge = document.getElementById('status-badge');
|
||
badge.textContent = label;
|
||
badge.className = 'border text-xs px-3 py-1 rounded-full status-' +
|
||
(status === 'sent_to_bot' ? 'sent' : status === 'pending_review' ? 'pending' : status);
|
||
}
|
||
|
||
// ── Board: Umplanen ────────────────────────────────────────────────────────
|
||
function openReschedule(id, date, time) {
|
||
closeAllReschedule();
|
||
document.getElementById(`reschedule-panel-${id}`).classList.remove('hidden');
|
||
document.getElementById(`rs-date-${id}`).value = date;
|
||
document.getElementById(`rs-time-${id}`).value = time;
|
||
checkRescheduleSlot(id);
|
||
}
|
||
|
||
function closeReschedule(id) {
|
||
document.getElementById(`reschedule-panel-${id}`).classList.add('hidden');
|
||
}
|
||
|
||
function closeAllReschedule() {
|
||
document.querySelectorAll('[id^="reschedule-panel-"]').forEach(el => el.classList.add('hidden'));
|
||
}
|
||
|
||
async function checkRescheduleSlot(id) {
|
||
const date = document.getElementById(`rs-date-${id}`).value;
|
||
const time = document.getElementById(`rs-time-${id}`).value;
|
||
const statusEl = document.getElementById(`rs-status-${id}`);
|
||
const confirmBtn = document.getElementById(`rs-confirm-${id}`);
|
||
if (!date || !time) return;
|
||
|
||
const [h, m] = time.split(':').map(Number);
|
||
const roundedM = Math.round(m / 15) * 15;
|
||
const roundedH = roundedM === 60 ? h + 1 : h;
|
||
const roundedTime = `${String(roundedH % 24).padStart(2,'0')}:${String(roundedM % 60).padStart(2,'0')}`;
|
||
document.getElementById(`rs-time-${id}`).value = roundedTime;
|
||
|
||
const r = await fetch(`/api/slots/${date}`);
|
||
const d = await r.json();
|
||
statusEl.classList.remove('hidden');
|
||
if (d.taken && d.taken.includes(roundedTime)) {
|
||
statusEl.className = 'text-xs mt-2 text-red-400';
|
||
statusEl.textContent = `❌ Slot ${date} ${roundedTime} bereits belegt`;
|
||
confirmBtn.disabled = true;
|
||
confirmBtn.classList.add('opacity-50');
|
||
} else {
|
||
statusEl.className = 'text-xs mt-2 text-green-400';
|
||
statusEl.textContent = `✅ Slot ${date} ${roundedTime} ist frei`;
|
||
confirmBtn.disabled = false;
|
||
confirmBtn.classList.remove('opacity-50');
|
||
}
|
||
}
|
||
|
||
async function confirmReschedule(id) {
|
||
const date = document.getElementById(`rs-date-${id}`).value;
|
||
const time = document.getElementById(`rs-time-${id}`).value;
|
||
const r = await fetch(`/api/article/${id}/reschedule`, {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({date, post_time: time})
|
||
});
|
||
const d = await r.json();
|
||
if (d.success) {
|
||
showToast(`✅ Umgeplant auf ${date} ${time} Uhr`);
|
||
setTimeout(() => location.reload(), 1200);
|
||
} else {
|
||
showToast('❌ ' + (d.error || 'Fehler beim Umplanen'));
|
||
}
|
||
}
|
||
|
||
// ── Board: Löschen ─────────────────────────────────────────────────────────
|
||
async function deleteArticle(id, date, time) {
|
||
if (!confirm(`Artikel vom ${date} ${time} Uhr wirklich löschen?\n\nDieser Vorgang kann nicht rückgängig gemacht werden.`)) return;
|
||
const r = await fetch(`/api/article/${id}/delete`, {method: 'POST'});
|
||
const d = await r.json();
|
||
if (d.success) {
|
||
showToast(`🗑️ Artikel ${date} ${time} gelöscht`);
|
||
setTimeout(() => location.reload(), 1000);
|
||
} else {
|
||
showToast('❌ Fehler beim Löschen');
|
||
}
|
||
}
|
||
|
||
// ── Aufenthaltsort ─────────────────────────────────────────────────────────
|
||
function toggleLocationMenu() {
|
||
const menu = document.getElementById('location-menu');
|
||
menu.classList.toggle('hidden');
|
||
}
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('#location-btn') && !e.target.closest('#location-menu')) {
|
||
document.getElementById('location-menu').classList.add('hidden');
|
||
}
|
||
});
|
||
|
||
async function setLocation(id, flag, name) {
|
||
const r = await fetch('/api/settings/location', {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({location_id: id})
|
||
});
|
||
const d = await r.json();
|
||
if (d.success) {
|
||
document.getElementById('location-flag').textContent = flag;
|
||
document.getElementById('location-name').textContent = name;
|
||
document.getElementById('location-menu').classList.add('hidden');
|
||
userTimezone = d.location.timezone;
|
||
userFlag = flag;
|
||
updateClock();
|
||
showToast(`📍 ${flag} ${name} — Reminder: ${d.reminders_berlin.morning} & ${d.reminders_berlin.afternoon} Uhr (Berlin)`);
|
||
}
|
||
}
|
||
|
||
function showToast(msg) {
|
||
const t = document.createElement('div');
|
||
t.className = 'fixed bottom-6 left-1/2 -translate-x-1/2 bg-slate-700 border border-slate-500 text-white text-sm px-5 py-3 rounded-xl shadow-xl z-50 transition-opacity';
|
||
t.textContent = msg;
|
||
document.body.appendChild(t);
|
||
setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 500); }, 4000);
|
||
}
|
||
|
||
// ── OpenRouter Balance ─────────────────────────────────────────────────────
|
||
async function loadBalance() {
|
||
const el = document.getElementById('balance-inline');
|
||
try {
|
||
const r = await fetch('/api/balance');
|
||
const d = await r.json();
|
||
if (d.remaining !== null && d.remaining !== undefined) {
|
||
el.textContent = '$' + d.remaining.toFixed(4);
|
||
el.className = d.remaining < 0.5
|
||
? 'text-sm font-semibold text-red-400 mt-1'
|
||
: 'text-sm font-semibold text-green-400 mt-1';
|
||
} else if (d.limit === null) {
|
||
el.textContent = '∞ Free Tier';
|
||
el.className = 'text-sm font-semibold text-blue-400 mt-1';
|
||
} else {
|
||
el.textContent = '—';
|
||
}
|
||
} catch { el.textContent = 'Fehler'; }
|
||
}
|
||
// Balance nur auf Knopfdruck laden, nicht automatisch beim Start
|
||
|
||
// ── Monatskalender rendern ────────────────────────────────────────────────
|
||
function renderMonthCalendar() {
|
||
const grid = document.getElementById('month-grid');
|
||
if (!grid) return;
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = now.getMonth();
|
||
const firstDay = new Date(year, month, 1);
|
||
let startWeekday = firstDay.getDay() - 1;
|
||
if (startWeekday < 0) startWeekday = 6;
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
const todayDate = now.getDate();
|
||
|
||
let html = '';
|
||
for (let i = 0; i < startWeekday; i++) {
|
||
html += '<div></div>';
|
||
}
|
||
for (let d = 1; d <= daysInMonth; d++) {
|
||
const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
||
const status = MONTH_ARTICLES[dateStr];
|
||
const isToday = d === todayDate;
|
||
let dotColor = 'bg-slate-700';
|
||
if (status === 'posted') dotColor = 'bg-blue-500';
|
||
else if (status === 'approved' || status === 'sent_to_bot') dotColor = 'bg-green-500';
|
||
else if (status === 'draft' || status === 'pending_review') dotColor = 'bg-yellow-500';
|
||
else if (status === 'skipped') dotColor = 'bg-slate-600';
|
||
|
||
html += `<div onclick="loadDate('${dateStr}')"
|
||
class="py-1 rounded cursor-pointer hover:bg-slate-600/50 transition
|
||
${isToday ? 'ring-1 ring-blue-400 font-bold text-blue-400' : 'text-slate-400'}">
|
||
${d}
|
||
<div class="w-1.5 h-1.5 rounded-full ${dotColor} mx-auto mt-0.5"></div>
|
||
</div>`;
|
||
}
|
||
grid.innerHTML = html;
|
||
}
|
||
renderMonthCalendar();
|
||
</script>
|
||
</body>
|
||
</html>
|