diff --git a/backend/database.py b/backend/database.py
index 8ef5362..b7b5114 100644
--- a/backend/database.py
+++ b/backend/database.py
@@ -1072,6 +1072,19 @@ def _migrate(conn_factory):
pass
logger.info("Migration: wiki_rassen Anreicherungs-Felder bereit.")
+ # Moderation-Logging: resolved_by/at für forum_reports, verified_by/at/reject für wiki_zuchter
+ for table, col, typedef in [
+ ("forum_reports", "resolved_by", "INTEGER"),
+ ("forum_reports", "resolved_at", "TEXT"),
+ ("wiki_zuchter", "verified_by", "INTEGER"),
+ ("wiki_zuchter", "verified_at", "TEXT"),
+ ("wiki_zuchter", "reject_reason", "TEXT"),
+ ]:
+ try:
+ conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}")
+ except Exception:
+ pass
+
# Wiki: Züchter-Verzeichnis
conn.executescript("""
CREATE TABLE IF NOT EXISTS wiki_zuchter (
diff --git a/backend/routes/moderation.py b/backend/routes/moderation.py
index 95b33a9..1357a85 100644
--- a/backend/routes/moderation.py
+++ b/backend/routes/moderation.py
@@ -1,4 +1,5 @@
"""BAN YARO — Moderations-Panel Backend"""
+from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from database import db
from auth import get_current_user
@@ -69,17 +70,19 @@ async def mod_stats(user=Depends(require_moderator)):
async def mod_reports(user=Depends(require_moderator)):
with db() as conn:
rows = conn.execute("""
- SELECT r.id, r.target_type, r.target_id, r.grund, r.resolved, r.created_at,
- u.name AS melder_name,
+ SELECT r.id, r.target_type, r.target_id, r.grund, r.resolved,
+ r.created_at, r.resolved_at,
+ u.name AS melder_name,
+ m.name AS resolved_by_name,
CASE r.target_type
WHEN 'thread' THEN (SELECT t.titel FROM forum_threads t WHERE t.id=r.target_id)
WHEN 'post' THEN (SELECT SUBSTR(p.text,1,80) FROM forum_posts p WHERE p.id=r.target_id)
END AS content_preview
FROM forum_reports r
LEFT JOIN users u ON u.id=r.user_id
- WHERE r.resolved=0
- ORDER BY r.created_at DESC
- LIMIT 100
+ LEFT JOIN users m ON m.id=r.resolved_by
+ ORDER BY r.resolved ASC, r.created_at DESC
+ LIMIT 200
""").fetchall()
return [dict(r) for r in rows]
@@ -97,8 +100,12 @@ async def mod_resolve_report(rid: int, user=Depends(require_moderator)):
raise HTTPException(404, "Meldung nicht gefunden.")
new_state = 0 if r["resolved"] else 1
conn.execute(
- "UPDATE forum_reports SET resolved=? WHERE id=?",
- (new_state, rid)
+ """UPDATE forum_reports SET resolved=?, resolved_by=?, resolved_at=?
+ WHERE id=?""",
+ (new_state,
+ user["id"] if new_state else None,
+ datetime.utcnow().isoformat() if new_state else None,
+ rid)
)
return {"ok": True}
@@ -189,17 +196,19 @@ async def mod_patch_user(uid: int, data: dict, user=Depends(require_moderator)):
async def mod_fotos(user=Depends(require_moderator)):
with db() as conn:
rows = conn.execute("""
- SELECT s.id, s.foto_url, s.created_at,
+ SELECT s.id, s.foto_url, s.status, s.created_at,
+ s.reviewed_at, s.reject_reason,
COALESCE(s.rights_confirmed, 0) AS rights_confirmed,
- u.name AS user_name,
- r.name AS rasse_name, r.slug AS rasse_slug,
+ u.name AS user_name,
+ m.name AS reviewed_by_name,
+ r.name AS rasse_name, r.slug AS rasse_slug,
r.foto_url AS aktuell_foto
FROM wiki_foto_submissions s
LEFT JOIN users u ON u.id = s.user_id
+ LEFT JOIN users m ON m.id = s.reviewed_by
LEFT JOIN wiki_rassen r ON r.id = s.rasse_id
- WHERE s.status = 'pending'
- ORDER BY s.created_at ASC
- LIMIT 50
+ ORDER BY s.status ASC, s.created_at ASC
+ LIMIT 200
""").fetchall()
return [dict(r) for r in rows]
@@ -228,11 +237,13 @@ async def mod_poi_edits(user=Depends(require_moderator)):
SELECT e.id, e.osm_id, e.poi_name, e.field,
e.old_value, e.new_value, e.status,
e.created_at, e.resolved_at,
- u.name AS einreicher_name
+ u.name AS einreicher_name,
+ m.name AS mod_name
FROM osm_poi_edits e
JOIN users u ON u.id = e.user_id
+ LEFT JOIN users m ON m.id = e.mod_id
ORDER BY e.status ASC, e.created_at DESC
- LIMIT 100
+ LIMIT 200
""").fetchall()
return [dict(r) for r in rows]
diff --git a/backend/routes/wiki.py b/backend/routes/wiki.py
index 83093d7..bf3c19c 100644
--- a/backend/routes/wiki.py
+++ b/backend/routes/wiki.py
@@ -694,11 +694,12 @@ async def list_zuchter_pending(user=Depends(get_current_user)):
raise HTTPException(403, "Nur Moderatoren.")
with db() as conn:
rows = conn.execute(
- """SELECT z.*, u.name AS user_name
+ """SELECT z.*, u.name AS user_name, m.name AS verified_by_name
FROM wiki_zuchter z
LEFT JOIN users u ON u.id = z.user_id
- WHERE z.verified=0
- ORDER BY z.created_at ASC""",
+ LEFT JOIN users m ON m.id = z.verified_by
+ ORDER BY z.verified ASC, z.created_at ASC
+ LIMIT 200""",
).fetchall()
return [dict(r) for r in rows]
@@ -716,8 +717,10 @@ async def verify_zuchter(zuchter_id: int, user=Depends(get_current_user)):
).fetchone()
if not row:
raise HTTPException(404, "Züchter nicht gefunden.")
+ from datetime import datetime
conn.execute(
- "UPDATE wiki_zuchter SET verified=1 WHERE id=?", (zuchter_id,)
+ "UPDATE wiki_zuchter SET verified=1, verified_by=?, verified_at=? WHERE id=?",
+ (user["id"], datetime.utcnow().isoformat(), zuchter_id)
)
result = conn.execute(
"SELECT * FROM wiki_zuchter WHERE id=?", (zuchter_id,)
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index bdb4e29..b145576 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 = '589'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '590'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.2.1'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js
index 6809ee0..65a9b76 100644
--- a/backend/static/js/pages/admin.js
+++ b/backend/static/js/pages/admin.js
@@ -1460,6 +1460,30 @@ window.Page_admin = (() => {
// ------------------------------------------------------------------
// TAB: MODERATION
// ------------------------------------------------------------------
+ function _historySection(label, items, renderItem) {
+ const id = `hist-${label.replace(/\W/g,'').toLowerCase()}`;
+ return `
+
+ ${UI.icon('clock-countdown')} ${items.length} erledigte ${label}
+
+
+
Keine ausstehenden Einreichungen.
`; + if (!zuchterPending.length) { + html += `Keine ausstehenden Einreichungen.
`; } else { - html += `| Rasse | Name / Zwingername | Ort | VDH | Website | |
|---|---|---|---|---|---|
| ${_esc(z.rasse_slug)} | ${_esc(z.name)}${z.zwingername ? ` ${_esc(z.zwingername)}` : ''} |
@@ -1545,6 +1576,10 @@ window.Page_admin = (() => {
Keine ausstehenden Foto-Einreichungen.
`; + if (!fotosPending.length) { + html += `Keine ausstehenden Foto-Einreichungen.
`; } else { - html += `Keine offenen Meldungen.
`; + if (!reportsPending.length) { + html += `Keine offenen Meldungen.
`; } else { - html += `${_esc(e.field)}:
+ ${_esc(e.old_value||'—')} →
+ ${_esc(e.new_value||'—')} ·
+ ${e.status==='approved' ? `${UI.icon('check-circle')} freigegeben` : `${UI.icon('x-circle')} abgelehnt`}
+ ${e.mod_name ? ` von ${_esc(e.mod_name)}` : ''} · ${(e.resolved_at||'').slice(0,10)}`);
el.innerHTML = html;
diff --git a/backend/static/sw.js b/backend/static/sw.js
index 48d8c56..2f97913 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-v589';
+const CACHE_VERSION = 'by-v590';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache