diff --git a/backend/routes/admin.py b/backend/routes/admin.py
index c4156da..8bcdff2 100644
--- a/backend/routes/admin.py
+++ b/backend/routes/admin.py
@@ -728,6 +728,65 @@ async def wiki_fetch_photos(limit: int = 50, user=Depends(require_mod)):
# ------------------------------------------------------------------
# DELETE /api/admin/wiki/zuchter/{id} — Züchter-Eintrag löschen (Admin/Mod)
# ------------------------------------------------------------------
+@router.get("/social")
+async def admin_social_stats(user=Depends(require_mod)):
+ """Social-Media-Übersicht für Admins — alle Manager, alle Plattformen."""
+ with db() as conn:
+ # Pro Manager: Name + Counts
+ managers = conn.execute("""
+ SELECT u.id, u.name,
+ COUNT(sc.id) AS total,
+ COUNT(CASE WHEN sc.status='published' THEN 1 END) AS published,
+ COUNT(CASE WHEN sc.status='idea' THEN 1 END) AS ideas,
+ COUNT(CASE WHEN sc.status='scheduled' THEN 1 END) AS scheduled,
+ COUNT(CASE WHEN sc.post_url IS NOT NULL
+ AND sc.post_url != ''
+ AND sc.status='published' THEN 1 END) AS with_link
+ FROM users u
+ JOIN social_content sc ON sc.created_by = u.id
+ GROUP BY u.id
+ ORDER BY published DESC
+ """).fetchall()
+
+ # Veröffentlichte Posts nach Plattform
+ by_platform = conn.execute("""
+ SELECT platform, COUNT(*) AS n
+ FROM social_content
+ WHERE status='published'
+ GROUP BY platform
+ ORDER BY n DESC
+ """).fetchall()
+
+ # Veröffentlichte Posts nach Monat (letzte 6 Monate)
+ by_month = conn.execute("""
+ SELECT strftime('%Y-%m', published_at) AS monat, COUNT(*) AS n
+ FROM social_content
+ WHERE status='published' AND published_at IS NOT NULL
+ GROUP BY monat
+ ORDER BY monat DESC
+ LIMIT 6
+ """).fetchall()
+
+ # Letzte veröffentlichte Posts mit Link (für Abrechnung/Nachweis)
+ recent_published = conn.execute("""
+ SELECT sc.id, sc.topic, sc.platform, sc.category,
+ sc.published_at, sc.post_url, sc.ai_score,
+ u.name AS manager
+ FROM social_content sc
+ LEFT JOIN users u ON u.id = sc.created_by
+ WHERE sc.status = 'published'
+ ORDER BY sc.published_at DESC
+ LIMIT 50
+ """).fetchall()
+
+ return {
+ "managers": [dict(r) for r in managers],
+ "by_platform": [dict(r) for r in by_platform],
+ "by_month": [dict(r) for r in by_month],
+ "recent_published": [dict(r) for r in recent_published],
+ }
+
+
@router.delete("/wiki/zuchter/{zuchter_id}", status_code=204)
async def admin_delete_zuchter(zuchter_id: int, user=Depends(require_mod)):
with db() as conn:
diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js
index 8ed377d..5d41cf4 100644
--- a/backend/static/js/pages/admin.js
+++ b/backend/static/js/pages/admin.js
@@ -14,6 +14,7 @@ window.Page_admin = (() => {
{ id: 'nutzer', label: 'Nutzer', icon: 'users' },
{ id: 'moderation', label: 'Moderation', icon: 'shield-check' },
{ id: 'forum', label: 'Forum & Meldungen', icon: 'chat-circle-dots' },
+ { id: 'social', label: 'Social Media', icon: 'camera' },
{ id: 'analytics', label: 'Analytics', icon: 'target' },
{ id: 'system', label: 'System', icon: 'gear' },
{ id: 'jobs', label: 'Jobs', icon: 'clock' },
@@ -81,6 +82,7 @@ window.Page_admin = (() => {
case 'nutzer': await _renderUsers(el); break;
case 'moderation': await _renderModeration(el); break;
case 'forum': await _renderForum(el); break;
+ case 'social': await _renderSocial(el); break;
case 'analytics': await _renderAnalytics(el); break;
case 'system': await _renderSystem(el); break;
case 'jobs': await _renderJobs(el); break;
@@ -91,6 +93,126 @@ window.Page_admin = (() => {
}
}
+ // ------------------------------------------------------------------
+ // TAB: SOCIAL MEDIA
+ async function _renderSocial(el) {
+ const d = await API.get('/admin/social');
+
+ const _PL = { instagram: '📸 Instagram', tiktok: '🎵 TikTok', both: '📱 Beide' };
+ const _fmt = iso => iso ? new Date(iso).toLocaleDateString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric'}) : '–';
+
+ // Manager-Tabelle
+ const managerRows = d.managers.map(m => `
+
+ | ${_esc(m.name)} |
+ ${m.published} |
+ ${m.with_link}
+ ${m.published > 0 ? `
+ (${Math.round(m.with_link/m.published*100)}%)` : ''} |
+ ${m.scheduled} |
+ ${m.ideas} |
+ ${m.total} |
+
`).join('');
+
+ // Plattform-Balken
+ const maxPlat = Math.max(...d.by_platform.map(p => p.n), 1);
+ const platBars = d.by_platform.map(p => `
+
+
${_PL[p.platform]||p.platform}
+
+
${p.n}
+
`).join('');
+
+ // Monats-Timeline
+ const maxMonth = Math.max(...d.by_month.map(m => m.n), 1);
+ const monthBars = [...d.by_month].reverse().map(m => `
+
+
${m.monat}
+
+
${m.n}
+
`).join('');
+
+ // Veröffentlichte Posts (mit/ohne Link)
+ const postRows = d.recent_published.map(p => `
+
+ | ${_fmt(p.published_at)} |
+ ${_esc(p.topic)} |
+ ${_PL[p.platform]||p.platform||'–'} |
+ ${_esc(p.category||'–')} |
+ ${p.ai_score ? '⭐'.repeat(p.ai_score) : '–'} |
+ ${_esc(p.manager||'–')} |
+ ${p.post_url
+ ? `🔗 Link`
+ : `–`} |
+
`).join('');
+
+ el.innerHTML = `
+
+
+
+
+ Veröffentlicht nach Plattform
+
+ ${platBars || '
Noch keine Posts
'}
+
+
+
+
+ Posts pro Monat
+
+ ${monthBars || '
Noch keine Posts
'}
+
+
+
+
+ ${d.managers.length ? `
+ ` : ''}
+
+
+
+
+ Veröffentlichte Posts (letzte 50)
+
+
+
`;
+ }
+
// ------------------------------------------------------------------
// TAB: ANALYTICS
async function _renderAnalytics(el) {
diff --git a/backend/static/sw.js b/backend/static/sw.js
index 620e75f..93bc520 100644
--- a/backend/static/sw.js
+++ b/backend/static/sw.js
@@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
-const CACHE_VERSION = 'by-v369';
+const CACHE_VERSION = 'by-v370';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten