diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index 3c028aa..a20c012 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
-const APP_VER = '267'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '268'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => {
diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js
index ecd08ca..0dbd689 100644
--- a/backend/static/js/pages/admin.js
+++ b/backend/static/js/pages/admin.js
@@ -10,13 +10,14 @@ window.Page_admin = (() => {
let _tab = 'uebersicht';
const TABS = [
- { id: 'uebersicht', label: 'Übersicht', icon: 'house-line' },
- { id: 'nutzer', label: 'Nutzer', icon: 'users' },
- { id: 'forum', label: 'Forum & Meldungen', icon: 'chat-circle-dots' },
- { id: 'analytics', label: 'Analytics', icon: 'target' },
- { id: 'system', label: 'System', icon: 'gear' },
- { id: 'jobs', label: 'Jobs', icon: 'clock' },
- { id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' },
+ { id: 'uebersicht', label: 'Übersicht', icon: 'house-line' },
+ { id: 'nutzer', label: 'Nutzer', icon: 'users' },
+ { id: 'moderation', label: 'Moderation', icon: 'shield-check' },
+ { id: 'forum', label: 'Forum & Meldungen', icon: 'chat-circle-dots' },
+ { id: 'analytics', label: 'Analytics', icon: 'target' },
+ { id: 'system', label: 'System', icon: 'gear' },
+ { id: 'jobs', label: 'Jobs', icon: 'clock' },
+ { id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' },
];
// ------------------------------------------------------------------
@@ -74,13 +75,14 @@ window.Page_admin = (() => {
el.innerHTML = `
Lade…
`;
try {
switch (_tab) {
- case 'uebersicht': await _renderStats(el); break;
- case 'nutzer': await _renderUsers(el); break;
- case 'forum': await _renderForum(el); break;
- case 'analytics': await _renderAnalytics(el); break;
- case 'system': await _renderSystem(el); break;
- case 'jobs': await _renderJobs(el); break;
- case 'audit': await _renderAudit(el); break;
+ case 'uebersicht': await _renderStats(el); break;
+ case 'nutzer': await _renderUsers(el); break;
+ case 'moderation': await _renderModeration(el); break;
+ case 'forum': await _renderForum(el); break;
+ case 'analytics': await _renderAnalytics(el); break;
+ case 'system': await _renderSystem(el); break;
+ case 'jobs': await _renderJobs(el); break;
+ case 'audit': await _renderAudit(el); break;
}
} catch (e) {
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
@@ -737,6 +739,133 @@ window.Page_admin = (() => {
// ------------------------------------------------------------------
// TAB: JOBS
+ // ------------------------------------------------------------------
+ // TAB: MODERATION
+ // ------------------------------------------------------------------
+ async function _renderModeration(el) {
+ el.innerHTML = `
+
+
+
+ Lade…
+ `;
+ el.querySelector('#adm-mod-refresh').addEventListener('click', () => _loadModeration(el.querySelector('#adm-mod-content')));
+ await _loadModeration(el.querySelector('#adm-mod-content'));
+ }
+
+ async function _loadModeration(el) {
+ el.innerHTML = `Lade…
`;
+
+ const [zuchter, fotos] = await Promise.all([
+ API.get('/wiki/zuchter/pending').catch(() => []),
+ API.get('/wiki/foto-submissions').catch(() => []),
+ ]);
+
+ let html = '';
+
+ // --- Züchter-Einreichungen ---
+ html += `
+ Züchter-Einreichungen
+ ${zuchter.length}
+
`;
+
+ if (!zuchter.length) {
+ html += `Keine ausstehenden Einreichungen.
`;
+ } else {
+ html += ``;
+ }
+
+ // --- Wiki-Foto-Einreichungen ---
+ html += `
+ Wiki-Foto-Einreichungen
+ ${fotos.length}
+
`;
+
+ if (!fotos.length) {
+ html += `Keine ausstehenden Foto-Einreichungen.
`;
+ } else {
+ html += `
+ ${fotos.map(f => `
+
+
})
+
${_esc(f.rasse_name)}
+
von ${_esc(f.user_name)}
+ ${f.aktuell_foto ? `
})
+
↑ aktuelles Foto
` : ''}
+
+
+
+
+
`).join('')}
+
`;
+ }
+
+ el.innerHTML = html;
+
+ // Züchter freigeben
+ el.querySelectorAll('.adm-zuchter-approve').forEach(btn => {
+ btn.addEventListener('click', async () => {
+ btn.disabled = true;
+ await API.patch(`/wiki/zuchter/${btn.dataset.id}/verify`, {});
+ await _loadModeration(el);
+ });
+ });
+
+ // Züchter löschen
+ el.querySelectorAll('.adm-zuchter-delete').forEach(btn => {
+ btn.addEventListener('click', async () => {
+ if (!window.confirm('Eintrag löschen?')) return;
+ btn.disabled = true;
+ await API.delete(`/admin/wiki/zuchter/${btn.dataset.id}`);
+ await _loadModeration(el);
+ });
+ });
+
+ // Foto freigeben
+ el.querySelectorAll('.adm-foto-approve').forEach(btn => {
+ btn.addEventListener('click', async () => {
+ btn.disabled = true;
+ await API.patch(`/wiki/foto-submissions/${btn.dataset.id}`, {action: 'approve'});
+ await _loadModeration(el);
+ });
+ });
+
+ // Foto ablehnen
+ el.querySelectorAll('.adm-foto-reject').forEach(btn => {
+ btn.addEventListener('click', async () => {
+ btn.disabled = true;
+ await API.patch(`/wiki/foto-submissions/${btn.dataset.id}`, {action: 'reject', reject_reason: 'Nicht geeignet.'});
+ await _loadModeration(el);
+ });
+ });
+ }
+
// ------------------------------------------------------------------
async function _renderJobs(el) {
el.innerHTML = `
diff --git a/backend/static/sw.js b/backend/static/sw.js
index e59302e..b38cdaf 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-v279';
+const CACHE_VERSION = 'by-v280';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten