80 lines
3.4 KiB
Python
80 lines
3.4 KiB
Python
"""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)
|