Big Sweep: Security + Race-Conditions + Tests + DSGVO + A11y, SW by-v1095
SECURITY (auth.py, routes/auth.py, database.py, main.py) - JWT bekommt jti; Logout trägt in neue jwt_blacklist-Tabelle ein, decode_token() prüft → server-side Invalidierung - JWT-Expiry default 30 → 7 Tage (ENV JWT_EXPIRY_DAYS überschreibt) - Sliding-Refresh-Middleware: erneuert Cookie wenn >50% verbraucht (Schwelle via JWT_REFRESH_FRACTION, Default 2) - Login-Lockout in DB-Tabelle login_attempts (5 Versuche / 15 Min, überlebt Container-Restart) — alte In-Memory-Lockouts ersetzt - SMTP-Versand: alle 'except: pass' durch logger.exception ersetzt; Fehlversuche landen in failed_emails-Tabelle für späteres Retry - Referral-Counter Race gefixt: UPDATE partner_codes SET uses=uses+1 ... WHERE uses<max_uses RETURNING — atomar statt SELECT+UPDATE RACE CONDITIONS (routes/invoices.py, database.py) - Neue invoice_counters-Tabelle für atomare Nummernvergabe - _next_invoice_number nutzt BEGIN IMMEDIATE + atomares UPDATE - Funktioniert für RG- und ST-Prefixe (Stornorechnungen) - Race-Test verifiziert (5 Threads × 20 Calls = 100 eindeutige Nummern) VERSION + TESTS + ERROR-DIGEST (VERSION, Makefile, tests/, scheduler.py) - Neue VERSION-Datei (Single Source of Truth) — main.py liest beim Startup - Makefile-Target 'make bump' propagiert in sw.js, app.js, index.html - Makefile-Target 'make test' setzt venv auf, läuft pytest - 19 Smoke-Tests in tests/ (health, auth, diary, invoice) — alle grün - Scheduler: täglicher _job_error_digest um 06:30 → schickt Error- Zusammenfassung an ADMIN_EMAIL (still wenn keine Errors) DSGVO + A11Y + ERSTE-HILFE - landing.html: 'HTML und ODS' → 'JSON' (tatsächlich implementiert) - datenschutz.js: Sektion Account-Löschung erweitert (sofort gelöscht / anonymisiert / 10 Jahre für Rechnungen) - erste-hilfe.js: prominentes Warning-Banner oben (ersetzt keine Tierarzt-Beratung); Notfallnummern gruppiert nach Land, TODO-Platz- halter für AT-Uni-Klinik, CH Tox Info Suisse, CH Tierspital Zürich - ui.js Modal: ESC schließt, Focus-Trap, Auto-Focus erstes Element, Restore Focus auf vorigen Caller - impressum.js Kontaktformular: Labels mit for=cf-name etc. NEUE DB-TABELLEN (idempotent via CREATE TABLE IF NOT EXISTS) - jwt_blacklist, login_attempts, failed_emails, invoice_counters NEUE ENV-VARS - JWT_REFRESH_FRACTION (Default 2) - JWT_EXPIRY_DAYS Default geändert (30 → 7)
This commit is contained in:
parent
6224044654
commit
9394bab1fb
23 changed files with 1208 additions and 78 deletions
|
|
@ -231,6 +231,15 @@ def start():
|
|||
misfire_grace_time=3600,
|
||||
coalesce=True,
|
||||
)
|
||||
# Täglich 06:30 — Error-Digest-Mail an ADMIN_EMAIL
|
||||
_scheduler.add_job(
|
||||
_job_error_digest,
|
||||
CronTrigger(hour=6, minute=30),
|
||||
id="error_digest",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=1800,
|
||||
coalesce=True,
|
||||
)
|
||||
_scheduler.start()
|
||||
logger.info("Scheduler gestartet (gestaffelt) — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import 1.+2./4./7./10. 02:00, Rassen-Seed 1. 03:00, Wikidata-Seed 1. 04:00, Status-Report 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:10, KI-Gesundheitsbericht Mo 07:05, Streak-Reminder 19:00, Rückruf-Check 08:05, Goldene-Gassi-Stunde 07:00, Jahrestags-Erinnerungen 09:00, Monatlicher-Rückblick 1. 10:00, Foto-Challenge Mo 08:10, Weekly-Praise Mo 09:05, Abo-Check 03:15, Invoice-Reminder 08:30. OSM-Cache: on-demand (kein Prewarm).")
|
||||
|
||||
|
|
@ -2092,3 +2101,133 @@ def _find_best_gassi_window(hourly: list[dict]) -> tuple[int, int, float | None,
|
|||
best_wind = sum(winds) / len(winds) if winds else None
|
||||
|
||||
return best_start, best_score, best_temp, best_wind
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: Error-Digest (täglich 06:30 Uhr)
|
||||
# ------------------------------------------------------------------
|
||||
# Quellen fuer Fehler:
|
||||
# 1. _job_log dieser Datei — Status der einzelnen Scheduler-Jobs
|
||||
# 2. main.log_buffer — In-Memory-Buffer der letzten 500 Log-Zeilen
|
||||
# Limitierung: log_buffer ist nicht persistent. Ein vollstaendiger 24h-Digest
|
||||
# wuerde eine externe Log-Senke (Datei oder DB) brauchen. Bis dahin liefert
|
||||
# der Job das was im Memory verfuegbar ist plus alle Scheduler-Errors.
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_error_digest():
|
||||
"""Schickt eine Zusammenfassung aller bekannten ERROR/EXCEPTION-Eintraege
|
||||
der letzten 24h an ADMIN_EMAIL."""
|
||||
import os
|
||||
import html as _html
|
||||
from collections import Counter
|
||||
from mailer import send_email, email_html
|
||||
|
||||
admin = os.getenv("ADMIN_EMAIL", "")
|
||||
if not admin:
|
||||
logger.info("Error-Digest: ADMIN_EMAIL nicht gesetzt, uebersprungen.")
|
||||
_log_job("error_digest", "ok", "ADMIN_EMAIL nicht gesetzt")
|
||||
return
|
||||
|
||||
now = datetime.now(tz=_TZ)
|
||||
cutoff = now - timedelta(hours=24)
|
||||
|
||||
# ── 1. Scheduler-Job-Errors einsammeln ───────────────────────
|
||||
scheduler_errors = []
|
||||
for jid, log in _job_log.items():
|
||||
last_run = log.get("last_run")
|
||||
if last_run and last_run >= cutoff and log.get("status") == "error":
|
||||
scheduler_errors.append({
|
||||
"job": jid,
|
||||
"ts": last_run.strftime("%d.%m. %H:%M"),
|
||||
"result": log.get("result", ""),
|
||||
})
|
||||
|
||||
# ── 2. In-Memory-Log-Buffer einsammeln (best-effort) ─────────
|
||||
log_errors = []
|
||||
try:
|
||||
from main import log_buffer # type: ignore
|
||||
for entry in list(log_buffer):
|
||||
lvl = entry.get("l", "")
|
||||
if lvl in ("ERROR", "CRITICAL", "EXCEPTION"):
|
||||
log_errors.append({
|
||||
"ts": entry.get("t", ""),
|
||||
"lvl": lvl,
|
||||
"name": entry.get("n", ""),
|
||||
"msg": entry.get("m", ""),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.debug(f"Error-Digest: log_buffer nicht verfuegbar: {e}")
|
||||
|
||||
# Gruppieren: Fehler-Meldungen mit gleichem msg-Prefix zusammenfassen
|
||||
grouped: Counter = Counter()
|
||||
for e in log_errors:
|
||||
# Erste 80 Zeichen als Schluessel — schluckt Variablenwerte am Ende
|
||||
key = (e["name"], e["msg"][:80])
|
||||
grouped[key] += 1
|
||||
|
||||
# ── Wenn nichts zu melden ist: leise raus ────────────────────
|
||||
if not scheduler_errors and not grouped:
|
||||
logger.info("Error-Digest: Keine Errors in den letzten 24h.")
|
||||
_log_job("error_digest", "ok", "0 Errors")
|
||||
return
|
||||
|
||||
# ── HTML-Body bauen ──────────────────────────────────────────
|
||||
parts = []
|
||||
parts.append(f'<p style="margin:0 0 16px;color:#444">Fehler-Zusammenfassung der letzten 24h ({now.strftime("%d.%m.%Y %H:%M")}).</p>')
|
||||
|
||||
if scheduler_errors:
|
||||
rows_html = "".join(
|
||||
f'<tr>'
|
||||
f'<td style="padding:6px 12px;color:#c45000;font-weight:600">{_html.escape(e["job"])}</td>'
|
||||
f'<td style="padding:6px 12px;font-family:monospace;font-size:12px">{e["ts"]}</td>'
|
||||
f'<td style="padding:6px 12px;color:#555">{_html.escape(e["result"])}</td>'
|
||||
f'</tr>'
|
||||
for e in scheduler_errors
|
||||
)
|
||||
parts.append(
|
||||
'<h2 style="font-size:14px;color:#c45000;margin:20px 0 6px">Scheduler-Job-Fehler ({0})</h2>'.format(len(scheduler_errors))
|
||||
+ f'<table style="width:100%;border-collapse:collapse;font-size:13px"><tbody>{rows_html}</tbody></table>'
|
||||
)
|
||||
|
||||
if grouped:
|
||||
sorted_groups = sorted(grouped.items(), key=lambda kv: -kv[1])
|
||||
rows_html = "".join(
|
||||
f'<tr>'
|
||||
f'<td style="padding:6px 12px;text-align:right;font-weight:700;color:#dc2626">{count}x</td>'
|
||||
f'<td style="padding:6px 12px;font-family:monospace;font-size:11px;color:#888">{_html.escape(name)}</td>'
|
||||
f'<td style="padding:6px 12px;color:#444">{_html.escape(msg)}</td>'
|
||||
f'</tr>'
|
||||
for (name, msg), count in sorted_groups[:30]
|
||||
)
|
||||
parts.append(
|
||||
'<h2 style="font-size:14px;color:#c45000;margin:20px 0 6px">In-Memory-Log-Errors ({0} Typen, {1} Eintraege)</h2>'.format(len(grouped), sum(grouped.values()))
|
||||
+ f'<table style="width:100%;border-collapse:collapse;font-size:12px"><tbody>{rows_html}</tbody></table>'
|
||||
)
|
||||
|
||||
parts.append('<p style="margin:24px 0 0;font-size:11px;color:#aaa">Quellen: scheduler._job_log (24h) + main.log_buffer (Memory, max. 500 Eintraege). Fuer vollstaendige 24h-Historie waere eine persistente Log-Senke noetig.</p>')
|
||||
|
||||
body = "\n".join(parts)
|
||||
html = email_html(body, footer_text="Ban Yaro · Error-Digest")
|
||||
plain_lines = [f"Ban Yaro Error-Digest — {now.strftime('%d.%m.%Y %H:%M')}", ""]
|
||||
if scheduler_errors:
|
||||
plain_lines.append("Scheduler-Job-Fehler:")
|
||||
for e in scheduler_errors:
|
||||
plain_lines.append(f" - {e['ts']} {e['job']}: {e['result']}")
|
||||
plain_lines.append("")
|
||||
if grouped:
|
||||
plain_lines.append("Log-Errors (Top 30 Typen, gruppiert):")
|
||||
for (name, msg), count in sorted(grouped.items(), key=lambda kv: -kv[1])[:30]:
|
||||
plain_lines.append(f" {count}x [{name}] {msg}")
|
||||
plain = "\n".join(plain_lines)
|
||||
|
||||
try:
|
||||
await send_email(
|
||||
admin,
|
||||
f"Ban Yaro Error-Digest ({len(scheduler_errors)} Scheduler + {len(grouped)} Log-Errors)",
|
||||
html,
|
||||
plain,
|
||||
)
|
||||
logger.info(f"Error-Digest gesendet an {admin} — {len(scheduler_errors)} scheduler, {len(grouped)} log-types.")
|
||||
_log_job("error_digest", "ok", f"{len(scheduler_errors)} scheduler / {len(grouped)} log-types")
|
||||
except Exception as e:
|
||||
logger.error(f"Error-Digest: Mail-Fehler: {e}")
|
||||
_log_job("error_digest", "error", str(e))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue