v_2.3
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user