""" utils/scheduler.py — Daily completion report email scheduler. Runs a background daemon thread that wakes every minute, checks whether the configured send_time (HH:MM) has been reached today, and sends the summary report via SMTP if it hasn't been sent yet. Configuration (config.ini [email] section): enabled = true/false smtp_host = smtp.example.com smtp_port = 587 smtp_user = sender@example.com smtp_password= secret use_tls = true recipients = admin@example.com, manager@example.com send_time = 18:00 (24-hour HH:MM, local time) Call start() once after login succeeds (admin only). Call stop() on logout/shutdown. """ import configparser import datetime import logging import os import smtplib import threading from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText logger = logging.getLogger("scheduler") CONFIG_FILE = "config.ini" _scheduler_thread: "threading.Thread | None" = None _stop_event = threading.Event() # ─── Config helpers ─────────────────────────────────────────────────────────── def load_email_config() -> dict: cfg = configparser.ConfigParser() if not os.path.exists(CONFIG_FILE): return {} cfg.read(CONFIG_FILE, encoding="utf-8") if "email" not in cfg: return {} s = cfg["email"] return { "enabled": s.getboolean("enabled", fallback=False), "smtp_host": s.get("smtp_host", ""), "smtp_port": s.getint("smtp_port", fallback=587), "smtp_user": s.get("smtp_user", ""), "smtp_password": s.get("smtp_password", ""), "use_tls": s.getboolean("use_tls", fallback=True), "recipients": [r.strip() for r in s.get("recipients", "").split(",") if r.strip()], "send_time": s.get("send_time", "18:00"), } def save_email_config(enabled: bool, smtp_host: str, smtp_port: int, smtp_user: str, smtp_password: str, use_tls: bool, recipients: str, send_time: str): cfg = configparser.ConfigParser() cfg.read(CONFIG_FILE, encoding="utf-8") cfg["email"] = { "enabled": str(enabled).lower(), "smtp_host": smtp_host, "smtp_port": str(smtp_port), "smtp_user": smtp_user, "smtp_password": smtp_password, "use_tls": str(use_tls).lower(), "recipients": recipients, "send_time": send_time, } with open(CONFIG_FILE, "w", encoding="utf-8") as fh: cfg.write(fh) logger.info("Email configuration saved.") def test_smtp_connection(smtp_host, smtp_port, smtp_user, smtp_password, use_tls) -> tuple: """ Attempt a connection without sending mail. Returns (success: bool, message: str). """ try: if use_tls: server = smtplib.SMTP(smtp_host, smtp_port, timeout=8) server.starttls() else: server = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=8) server.login(smtp_user, smtp_password) server.quit() return True, "Connection successful." except Exception as e: return False, str(e) # ─── Report builder ─────────────────────────────────────────────────────────── def _build_html_report() -> str: """Build a simple HTML daily summary table.""" try: from models import get_admin_dashboard_stats stats = get_admin_dashboard_stats() except Exception as e: return f"
Error generating report: {e}
" today = datetime.date.today().strftime("%A, %d %B %Y") rows = stats.get("user_stats", []) table_rows = "" for r in rows: pct = float(r.get("pct_complete") or 0) color = "#388e3c" if pct >= 100 else "#f57c00" if pct > 0 else "#d32f2f" table_rows += ( f"{today}
| Username | Full Name | Checked | Total | Completion |
|---|
Sent automatically by Website Checker at {datetime.datetime.now().strftime('%H:%M')}.
""" def _send_report(cfg: dict): """Build and send the daily report email.""" html = _build_html_report() today = datetime.date.today().strftime("%d %b %Y") subject = f"Website Checker — Daily Report {today}" msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = cfg["smtp_user"] msg["To"] = ", ".join(cfg["recipients"]) msg.attach(MIMEText(html, "html", "utf-8")) try: if cfg["use_tls"]: server = smtplib.SMTP(cfg["smtp_host"], cfg["smtp_port"], timeout=10) server.starttls() else: server = smtplib.SMTP_SSL(cfg["smtp_host"], cfg["smtp_port"], timeout=10) server.login(cfg["smtp_user"], cfg["smtp_password"]) server.sendmail(cfg["smtp_user"], cfg["recipients"], msg.as_string()) server.quit() logger.info(f"Daily report emailed to: {cfg['recipients']}") except Exception as e: logger.error(f"Failed to send daily report email: {e}") # ─── Scheduler loop ─────────────────────────────────────────────────────────── def _scheduler_loop(): last_sent_date = None while not _stop_event.is_set(): _stop_event.wait(60) # sleep 60 seconds between checks if _stop_event.is_set(): break cfg = load_email_config() if not cfg.get("enabled") or not cfg.get("smtp_host") or not cfg.get("recipients"): continue try: send_h, send_m = map(int, cfg["send_time"].split(":")) except Exception: continue now = datetime.datetime.now() today = now.date() if (now.hour > send_h or (now.hour == send_h and now.minute >= send_m)): if last_sent_date != today: last_sent_date = today logger.info(f"Scheduler: sending daily report at {now.strftime('%H:%M')}.") _send_report(cfg) def start(): """Start the background scheduler thread. Call once after successful login.""" global _scheduler_thread, _stop_event _stop_event.clear() _scheduler_thread = threading.Thread(target=_scheduler_loop, name="EmailScheduler", daemon=True) _scheduler_thread.start() logger.info("Email scheduler started.") def stop(): """Signal the scheduler to stop cleanly.""" global _stop_event _stop_event.set() logger.info("Email scheduler stopped.")