Skip to main content

Overview

The Automated Email Reminders feature sends scheduled notifications to employees who have not checked in by 8:00 AM each workday. Using APScheduler’s cron-based scheduling, the system automatically identifies absent employees and sends reminder emails via SMTP, helping ensure accountability and timely attendance.
Reminders are sent only to employees who have not checked in on the current date. Admin users are excluded from notifications.

How It Works

1

Scheduler Triggers at 8:00 AM

APScheduler’s background cron job activates daily at exactly 8:00 AM server time
2

System Checks Today's Attendance

The system queries all check-ins from the current date and creates a list of employees who have logged in
3

Identify Missing Employees

Compares the user database against today’s check-ins to find employees without attendance records
4

Send Reminder Emails

Sends bulk email notifications to all missing employees via Gmail SMTP server

Technical Implementation

Scheduler Configuration

The system uses APScheduler’s BackgroundScheduler with cron triggers in app.py:87-91:
from apscheduler.schedulers.background import BackgroundScheduler

# Configure the scheduler
scheduler = BackgroundScheduler(daemon=True)
# Set to trigger daily at 8:00 AM
scheduler.add_job(enviar_recordatorio_automatizado, 'cron', hour=8, minute=0)
scheduler.start()
Important: The Flask app must run with use_reloader=False to prevent the scheduler from running twice in development mode:
app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=False)

Email Logic

The core reminder function in app.py:65-85 handles all notification logic:
def enviar_recordatorio_automatizado():
    """Esta función la llama el Scheduler a las 8:00 AM"""
    with app.app_context():
        print(f"[{datetime.datetime.now()}] Iniciando envío automático de las 8:00 AM...")
        usuarios = leer_json(USUARIOS_FILE)
        registros = leer_json(REGISTROS_FILE)
        hoy = datetime.datetime.now().strftime("%Y-%m-%d")
        
        # Find who has already checked in today
        quienes_registraron = {r['usuario'] for r in registros if r['fecha'] == hoy}
        
        # Build recipient list (exclude admin and those who checked in)
        destinatarios = [
            u['email'] for u in usuarios 
            if u['username'] != 'admin' and u['username'] not in quienes_registraron
        ]
        
        if destinatarios:
            try:
                msg = Message(
                    "⚠️ Recordatorio Automático: Inicia tu Turno", 
                    sender=app.config['MAIL_USERNAME'], 
                    recipients=destinatarios
                )
                msg.body = "Buenos días. Son las 8:00 AM y el sistema aún no detecta tu registro de hoy. Por favor inicia tu monitoreo."
                mail.send(msg)
                print(f"✅ Correos enviados a: {destinatarios}")
            except Exception as e:
                print(f"❌ Error en envío automático: {e}")

Email Configuration

SMTP Setup

The system uses Gmail’s SMTP server configured in app.py:15-21:
from flask_mail import Mail, Message

app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = '[email protected]'
app.config['MAIL_PASSWORD'] = 'mcgc unmv wkci dbrr'  # App-specific password
mail = Mail(app)
Security Alert: The code example contains hardcoded credentials for demonstration purposes. In production:
  • Store credentials in environment variables
  • Use .env files with .gitignore
  • Enable 2-factor authentication on Gmail
  • Generate app-specific passwords instead of using main password

Gmail App Password Setup

1

Enable 2-Factor Authentication

Go to Google Account settings and enable 2FA on the sender Gmail account
2

Generate App Password

Navigate to Security → App Passwords and create a new app password for “Mail”
3

Update Configuration

Replace MAIL_PASSWORD with the 16-character app password (spaces optional)
4

Test Email

Use the manual reminder button on admin dashboard to verify SMTP connection

Who Gets Notified?

Recipient Selection Logic

The system applies three filtering rules:

Rule 1: Exclude Admin

Admin users (username == 'admin') never receive reminder emails

Rule 2: Check Today's Date

Only checks registrations with fecha == today to determine attendance

Rule 3: No Duplicate Emails

Employees who already checked in today are excluded from recipients list

Example Scenario

User Database:
[
  {"username": "admin", "email": "[email protected]"},
  {"username": "empleado1", "email": "[email protected]"},
  {"username": "empleado2", "email": "[email protected]"},
  {"username": "empleado3", "email": "[email protected]"}
]
Today’s Check-ins (2026-03-05):
[
  {"usuario": "empleado1", "fecha": "2026-03-05", "hora": "07:45:00"},
  {"usuario": "empleado1", "fecha": "2026-03-05", "hora": "08:35:00"}
]
Result at 8:00 AM:
  • empleado1: Already checked in → No email
  • empleado2: No check-in → Receives reminder
  • empleado3: No check-in → Receives reminder
  • admin: Excluded by rule → No email

Email Content

Message Template

⚠️ Recordatorio Automático: Inicia tu Turno
  • Emoji draws attention in inbox
  • Clear action required: “Inicia tu Turno”
  • “Automático” indicates system-generated

Manual Override

Admin-Triggered Reminders

Administrators can manually send reminders outside the 8:00 AM schedule:
@app.route('/send-reminders')
@login_required
def send_reminders():
    # Reuse the automated function for manual trigger
    enviar_recordatorio_automatizado()
    return "Proceso de recordatorios ejecutado"
Triggered from the Admin Dashboard button:
<button class="btn btn-email" onclick="enviarRecordatorio()">
    📧 Notificar Faltantes
</button>
Use the manual trigger button to send reminders at any time, such as after a network outage or if the 8 AM automated run failed.

Scheduling Details

Cron Expression Breakdown

scheduler.add_job(enviar_recordatorio_automatizado, 'cron', hour=8, minute=0)

Trigger Type

'cron' - Use cron-style scheduling (not interval or date-based)

Hour

hour=8 - Execute at 8 AM (24-hour format, server timezone)

Minute

minute=0 - Execute at exactly :00 (top of the hour)

Frequency

Runs every day at 08:00:00 (implicit daily recurrence)

Timezone Considerations

APScheduler uses the server’s system timezone by default. Ensure your server time is configured correctly:
# Check current timezone
timedatectl

# Set timezone if needed
sudo timedatectl set-timezone America/Mexico_City
To specify an explicit timezone:
from pytz import timezone

scheduler.add_job(
    enviar_recordatorio_automatizado, 
    'cron', 
    hour=8, 
    minute=0,
    timezone=timezone('America/Mexico_City')
)

Monitoring & Logs

Console Output

The system prints detailed logs to stdout:
print(f"[{datetime.datetime.now()}] Iniciando envío automático de las 8:00 AM...")
print(f"✅ Correos enviados a: {destinatarios}")
print(f"❌ Error en envío automático: {e}")
Example log output:
[2026-03-05 08:00:00.123456] Iniciando envío automático de las 8:00 AM...
✅ Correos enviados a: ['[email protected]', '[email protected]']

Error Handling

Common causes:
  • Invalid Gmail credentials
  • 2FA not enabled or app password not generated
  • SMTP port 587 blocked by firewall
  • Gmail rate limiting (too many emails)
Error message:
❌ Error en envío automático: (535, b'5.7.8 Username and Password not accepted')
Solution: Verify credentials, regenerate app password, check network connectivity
If all employees have checked in, destinatarios will be empty and no emails are sent.Expected behavior: Silent success (no error thrown)Log output: Function executes but mail.send() is skipped due to if destinatarios: check

Testing the System

Development Testing

1

Adjust Trigger Time

Temporarily modify the cron schedule to trigger in the next few minutes:
# Test at 2:35 PM instead of 8:00 AM
scheduler.add_job(enviar_recordatorio_automatizado, 'cron', hour=14, minute=35)
2

Create Test Users

Add test email addresses in usuarios.json:
{
  "username": "test_employee",
  "pass": "123",
  "email": "[email protected]"
}
3

Start Application

Run Flask app and monitor console for scheduler output:
python app.py
4

Wait for Trigger

Wait until the scheduled time and check:
  • Console logs for confirmation message
  • Test email inbox for reminder
5

Verify Manual Trigger

Test the manual button by logging in as admin and clicking ”📧 Notificar Faltantes”

Best Practices

For Reliable Email Delivery:✅ Use dedicated email account for system notifications
✅ Enable Gmail app passwords (never use main password)
✅ Monitor server logs daily for SMTP errors
✅ Test manually after any configuration changes
✅ Set up SPF/DKIM records for custom domain senders
✅ Keep recipient lists under 100 per batch (Gmail limits)
❌ Don’t hardcode credentials in version control
❌ Don’t send to personal emails in production tests
❌ Don’t ignore repeated SMTP failures

Production Recommendations

Environment Variables

import os
app.config['MAIL_USERNAME'] = os.getenv('SMTP_USER')
app.config['MAIL_PASSWORD'] = os.getenv('SMTP_PASS')

Email Service Provider

Consider SendGrid, Mailgun, or AWS SES for better deliverability and higher limits

Rate Limiting

Implement delays between bulk sends to avoid Gmail spam filters

Logging System

Replace print() statements with proper logging framework (e.g., Python logging module)

Integration with Other Features

The reminder system works seamlessly with:
  • Employee Tracking: Checks GPS check-in timestamps to determine attendance
  • Admin Dashboard: Provides manual trigger button for on-demand reminders
  • PDF Reports: Email logs can be referenced when reviewing attendance patterns

Troubleshooting

Symptoms: No logs appear at 8:00 AMPossible causes:
  • Flask app crashed or restarted before trigger time
  • scheduler.start() not called
  • use_reloader=True causing duplicate schedulers
Solutions:
  • Check app uptime with process monitoring
  • Verify scheduler.start() in code
  • Set use_reloader=False in app.run()
Symptoms: Logs show success but no emails receivedPossible causes:
  • Emails in spam/junk folder
  • Invalid recipient email addresses
  • Gmail blocked delivery (suspicious activity)
Solutions:
  • Check spam folders
  • Verify email addresses in usuarios.json
  • Review Gmail account security notifications
  • Test with mail.send() directly in Python shell

Customization Options

Modify hour and minute parameters:
# Send at 7:30 AM instead
scheduler.add_job(
    enviar_recordatorio_automatizado, 
    'cron', 
    hour=7, 
    minute=30
)

Build docs developers (and LLMs) love