04/21 Fisrt commit
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
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"<p>Error generating report: {e}</p>"
|
||||
|
||||
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"<tr>"
|
||||
f"<td style='padding:8px 12px'>{r['username']}</td>"
|
||||
f"<td style='padding:8px 12px'>{r['full_name'] or ''}</td>"
|
||||
f"<td style='padding:8px 12px;text-align:center'>{int(r['checked_count'] or 0)}</td>"
|
||||
f"<td style='padding:8px 12px;text-align:center'>{int(r['total_sites'] or 0)}</td>"
|
||||
f"<td style='padding:8px 12px;text-align:center;"
|
||||
f"color:{color};font-weight:bold'>{pct:.0f}%</td>"
|
||||
f"</tr>"
|
||||
)
|
||||
|
||||
return f"""
|
||||
<html><body style="font-family:Segoe UI,Arial,sans-serif;color:#1a1a2e">
|
||||
<h2 style="color:#5b4de8">Website Checker — Daily Report</h2>
|
||||
<p style="color:#6b6b80">{today}</p>
|
||||
<table border="0" cellspacing="0" cellpadding="0"
|
||||
style="border-collapse:collapse;width:100%;max-width:600px">
|
||||
<thead>
|
||||
<tr style="background:#e0dff8">
|
||||
<th style="padding:10px 12px;text-align:left">Username</th>
|
||||
<th style="padding:10px 12px;text-align:left">Full Name</th>
|
||||
<th style="padding:10px 12px;text-align:center">Checked</th>
|
||||
<th style="padding:10px 12px;text-align:center">Total</th>
|
||||
<th style="padding:10px 12px;text-align:center">Completion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{table_rows}</tbody>
|
||||
</table>
|
||||
<p style="color:#6b6b80;font-size:12px;margin-top:24px">
|
||||
Sent automatically by Website Checker at {datetime.datetime.now().strftime('%H:%M')}.
|
||||
</p>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
|
||||
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.")
|
||||
Reference in New Issue
Block a user