diff --git a/backend/scheduler.py b/backend/scheduler.py index 69bdbcc..a4ce739 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -100,6 +100,14 @@ def start(): replace_existing=True, misfire_grace_time=1800, ) + # Täglich 12:00 — Moderation-Overdue-Check + _scheduler.add_job( + _job_moderation_overdue, + CronTrigger(hour=12, minute=0), + id="moderation_overdue", + replace_existing=True, + misfire_grace_time=1800, + ) # 1. Feb / Mai / Aug / Nov 07:00 — Quartalsbericht _scheduler.add_job( _job_quarterly_report, @@ -117,7 +125,7 @@ def start(): misfire_grace_time=3600, ) _scheduler.start() - logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00. OSM-Cache: on-demand (kein Prewarm).") + logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00. OSM-Cache: on-demand (kein Prewarm).") def stop(): @@ -650,6 +658,87 @@ async def _job_ki_health_report(): # ------------------------------------------------------------------ +async def _job_moderation_overdue(): + """Sendet Alarm-Mail wenn Moderations-Einträge seit >24h offen sind.""" + import os + from mailer import send_email + + admin = os.getenv("ADMIN_EMAIL", "") + if not admin: + return + + SLA_H = 24 + threshold = f"datetime('now', '-{SLA_H} hours')" + + overdue = {} + try: + with db() as conn: + n = conn.execute(f"SELECT COUNT(*) FROM job_applications WHERE status IN ('pending','reviewing') AND created_at < {threshold}").fetchone()[0] + if n: overdue["Bewerbungen"] = n + n = conn.execute(f"SELECT COUNT(*) FROM users WHERE breeder_status='pending' AND created_at < {threshold}").fetchone()[0] + if n: overdue["Züchter-Anträge"] = n + n = conn.execute(f"SELECT COUNT(*) FROM forum_reports WHERE resolved=0 AND created_at < {threshold}").fetchone()[0] + if n: overdue["Forum-Meldungen"] = n + n = conn.execute(f"SELECT COUNT(*) FROM wiki_foto_submissions WHERE status='pending' AND created_at < {threshold}").fetchone()[0] + if n: overdue["Foto-Einreichungen"] = n + n = conn.execute(f"SELECT COUNT(*) FROM osm_poi_edits WHERE status='pending' AND created_at < {threshold}").fetchone()[0] + if n: overdue["POI-Korrekturen"] = n + n = conn.execute(f"SELECT COUNT(*) FROM wiki_zuchter WHERE verified=0 AND created_at < {threshold}").fetchone()[0] + if n: overdue["Züchter-Einreichungen (Wiki)"] = n + except Exception as e: + logger.error(f"Moderation-Overdue-Check: DB-Fehler: {e}") + return + + if not overdue: + logger.info("Moderation-Overdue-Check: Alles im SLA.") + return + + now_str = datetime.now(tz=_TZ).strftime("%d.%m.%Y %H:%M") + rows_html = "".join( + f'{label}' + f'{count}' + for label, count in overdue.items() + ) + html = f"""\ + + +
+
+
⚠️ Moderation überfällig
+
{now_str} · SLA: {SLA_H}h
+
+
+

Folgende Einträge warten seit mehr als {SLA_H} Stunden auf Bearbeitung:

+ + + + + + {rows_html} +
BereichAnzahl
+
+ + → Admin-Panel öffnen + +
+
+
+ Ban Yaro · banyaro.app +
+
""" + + plain = f"Ban Yaro — Moderation überfällig ({now_str})\n\nSeit >{SLA_H}h offen:\n" + \ + "\n".join(f" • {l}: {c}" for l, c in overdue.items()) + \ + "\n\nhttps://banyaro.app/app/admin" + + try: + await send_email(admin, f"⚠️ Ban Yaro — Moderation überfällig ({', '.join(overdue)})", html, plain) + logger.info(f"Moderation-Overdue-Mail gesendet: {overdue}") + except Exception as e: + logger.error(f"Moderation-Overdue-Mail fehlgeschlagen: {e}") + + def _action_items_html(metrics: dict) -> str: items = [ ("jobs_pending", "Bewerbungen offen"), diff --git a/backend/static/js/app.js b/backend/static/js/app.js index b145576..20021b5 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 = '590'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '591'; // ← 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 65a9b76..4e89f32 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -1460,6 +1460,19 @@ window.Page_admin = (() => { // ------------------------------------------------------------------ // TAB: MODERATION // ------------------------------------------------------------------ + function _ageLabel(createdAt) { + if (!createdAt) return ''; + const h = (Date.now() - new Date(createdAt + 'Z').getTime()) / 3600000; + const overdue = h >= 24; + const label = h < 1 ? '<1h' : h < 24 ? `${Math.floor(h)}h` : `${Math.floor(h/24)}d ${Math.floor(h%24)}h`; + return ` + ${overdue ? '⚠️ ' : ''}${label} + `; + } + function _historySection(label, items, renderItem) { const id = `hist-${label.replace(/\W/g,'').toLowerCase()}`; return ` @@ -1560,7 +1573,7 @@ window.Page_admin = (() => { html += `
- + ${zuchterPending.map((z, i) => ` @@ -1568,6 +1581,7 @@ window.Page_admin = (() => { + + @@ -1692,6 +1709,7 @@ window.Page_admin = (() => { +
RasseName / ZwingernameOrtVDHWebsiteOrtVDHAlterWebsite
${_esc(z.name)}${z.zwingername ? `
${_esc(z.zwingername)}` : ''}
${_esc([z.plz, z.ort, z.bundesland].filter(Boolean).join(' '))} ${z.vdh_mitglied ? ` VDH` : '—'}${_ageLabel(z.created_at)} ${z.website ? `Link` : '—'} @@ -1599,7 +1613,8 @@ window.Page_admin = (() => {
${_esc(f.rasse_name)}
-
von ${_esc(f.user_name)}
+
von ${_esc(f.user_name)}
+
${_ageLabel(f.created_at)}
${f.aktuell_foto ? `Aktuell @@ -1637,8 +1652,9 @@ window.Page_admin = (() => {
-
+
${_esc(r.target_type)} #${r.target_id} · Gemeldet von ${_esc(r.melder_name || '?')} + ${_ageLabel(r.created_at)}
Grund: ${_esc(r.grund)} @@ -1682,6 +1698,7 @@ window.Page_admin = (() => {
Alt Neu VonAlter
${_esc(e.old_value || '—')} ${_esc(e.new_value || '—')} ${_esc(e.einreicher_name || '?')}${_ageLabel(e.created_at)}