This commit is contained in:
jpmvaz
2026-09-13 19:59:54 +01:00
commit d86f1ba099
46 changed files with 4138 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
"""Background scheduler that scans alerts and dispatches reminder emails."""
import logging
from datetime import date, timezone
from apscheduler.schedulers.background import BackgroundScheduler
from .models import Alert, User, db
from .mailer import send_mail
logger = logging.getLogger(__name__)
_scheduler: BackgroundScheduler | None = None
def _check_and_send(app):
"""Iterate active alerts; for each reminder threshold reached, email all users."""
with app.app_context():
today = date.today()
active_alerts = Alert.query.filter_by(is_active=True).all()
recipients = [u.email for u in User.query.filter_by(is_active=True).all() if u.email]
if not recipients:
logger.info("Scheduler tick: no recipients.")
return
for alert in active_alerts:
days_left = (alert.expiration_date - today).days
already_sent = set(alert.reminders_sent)
newly_sent = []
for threshold in alert.reminder_days:
if days_left <= threshold and threshold not in already_sent:
subject = f"[AlertHub] Reminder: {alert.title} expires in {days_left} day(s)"
body_text = (
f"Alert: {alert.title}\n"
f"Category: {alert.category or ''}\n"
f"Expiration date: {alert.expiration_date.isoformat()}\n"
f"Days remaining: {days_left}\n\n"
f"{alert.description or ''}\n"
)
body_html = f"""
<div style="font-family: -apple-system, sans-serif; max-width: 600px;">
<h2 style="color: #00d4aa;">⏰ AlertHub Reminder</h2>
<h3>{alert.title}</h3>
<p><strong>Category:</strong> {alert.category or ''}<br>
<strong>Expiration date:</strong> {alert.expiration_date.isoformat()}<br>
<strong>Days remaining:</strong> <span style="color:#ff6b6b;font-size:1.2em;">{days_left}</span></p>
<p>{alert.description or ''}</p>
</div>
"""
ok, info = send_mail(recipients, subject, body_text, body_html,
kind="reminder", triggered_by="system")
logger.info("Alert %s threshold %d: send=%s (%s)", alert.id, threshold, ok, info)
if ok:
newly_sent.append(threshold)
if newly_sent:
alert.reminders_sent = list(already_sent.union(newly_sent))
db.session.commit()
def init_scheduler(app):
"""Start the scheduler if not already running. Called once at app startup."""
global _scheduler
if _scheduler is not None and _scheduler.running:
return _scheduler
interval = max(1, int(app.config.get("SCHEDULER_INTERVAL_MINUTES", 60)))
_scheduler = BackgroundScheduler(timezone="UTC")
_scheduler.add_job(
_check_and_send,
"interval",
minutes=interval,
args=[app],
id="alert_reminder_job",
replace_existing=True,
next_run_time=None, # don't run immediately on boot
)
_scheduler.start()
logger.info("Scheduler started; reminder check every %d minute(s).", interval)
return _scheduler
def trigger_now(app):
"""Manual trigger (for the 'Run check now' button)."""
_check_and_send(app)