bmc_hub/REMINDER_SYSTEM_IMPLEMENTATION.md
Christian b43e9f797d feat: Add reminder system for sag cases with user preferences and notification channels
- Implemented user notification preferences table for managing default notification settings.
- Created sag_reminders table to define reminder rules with various trigger types and recipient configurations.
- Developed sag_reminder_queue for processing reminder events triggered by status changes or scheduled times.
- Added sag_reminder_logs to track reminder notifications and user interactions.
- Introduced frontend notification system using Bootstrap 5 Toast for displaying reminders.
- Created email template for sending reminders with case details and action links.
- Implemented rate limiting for user notifications to prevent spamming.
- Added triggers and functions for automatic updates and reminder processing.
2026-02-06 10:47:14 +01:00

12 KiB

Reminder System Implementation - BMC Hub

Overview

The Reminder System for BMC Hub's Sag (Case) module provides flexible, multi-channel notification delivery with support for:

  • Time-based reminders: Scheduled at specific times or recurring (daily, weekly, monthly)
  • Status-change triggered reminders: Automatically triggered when case status changes
  • Multi-channel delivery: Mattermost, Email, Frontend popup notifications
  • User preferences: Global defaults with per-reminder overrides
  • Rate limiting: Max 5 notifications per user per hour (global)
  • Smart scheduling: Database triggers for status changes, APScheduler for time-based

Architecture

Database Schema

4 Main Tables (created in migrations/096_reminder_system.sql):

  1. user_notification_preferences - User default notification settings

    • Default channels (mattermost, email, frontend)
    • Quiet hours configuration
    • Email override option
  2. sag_reminders - Reminder rules/templates

    • Trigger configuration (status_change, deadline_approaching, time_based)
    • Recipient configuration (user IDs or email addresses)
    • Recurrence setup (once, daily, weekly, monthly)
    • Scheduling info (scheduled_at, next_check_at)
  3. sag_reminder_queue - Event queue from database triggers

    • Holds events generated by status-change trigger
    • Processing status tracking (pending, processing, sent, failed, rate_limited)
    • Batch processed by scheduler job
  4. sag_reminder_logs - Execution log

    • Every notification sent/failed is logged
    • User interactions (snooze, dismiss, acknowledge)
    • Used for rate limiting verification

Database Triggers:

  • sag_status_change_reminder_trigger() - Fires on status UPDATE, queues relevant reminders

Helper Functions:

  • check_reminder_rate_limit(user_id) - Verifies user hasn't exceeded 5 per hour

Helper Views:

  • v_pending_reminders - Time-based reminders ready to send
  • v_pending_reminder_queue - Queued events ready for processing

Backend Services

app/services/reminder_notification_service.py:

  • Unified notification delivery via Mattermost, Email, Frontend
  • Merges user preferences with per-reminder overrides
  • Rate limit checking
  • Event logging
  • Email template rendering (Jinja2)

app/services/email_service.py (extended):

  • Added send_email() async method using aiosmtplib
  • SMTP configuration from .env
  • Supports plain text + HTML bodies
  • Safety flag: REMINDERS_DRY_RUN=true logs without sending

API Endpoints

User Preferences (in app/modules/sag/backend/reminders.py):

GET  /api/v1/users/me/notification-preferences
PATCH /api/v1/users/me/notification-preferences

Reminder CRUD:

GET    /api/v1/sag/{sag_id}/reminders           - List reminders for case
POST   /api/v1/sag/{sag_id}/reminders           - Create new reminder
PATCH  /api/v1/sag/reminders/{reminder_id}      - Update reminder
DELETE /api/v1/sag/reminders/{reminder_id}      - Soft-delete reminder

Reminder Interactions:

POST /api/v1/sag/reminders/{reminder_id}/snooze   - Snooze for X minutes
POST /api/v1/sag/reminders/{reminder_id}/dismiss  - Permanently dismiss
GET  /api/v1/reminders/pending/me                 - Get pending (for polling)

Scheduler Job

app/jobs/check_reminders.py:

  • Processes time-based reminders (next_check_at <= NOW())
  • Processes queued status-change events
  • Calculates next recurrence (daily +24h, weekly +7d, monthly +30d)
  • Respects rate limiting

Registration in main.py:

backup_scheduler.scheduler.add_job(
    func=check_reminders,
    trigger=IntervalTrigger(minutes=5),
    id='check_reminders',
    name='Check Reminders',
    max_instances=1
)

Runs every 5 minutes (configurable via REMINDERS_CHECK_INTERVAL_MINUTES)

Frontend Notifications

static/js/notifications.js:

  • Bootstrap 5 Toast-based notification popups
  • Polls /api/v1/reminders/pending/me every 30 seconds
  • Snooze presets: 15min, 30min, 1h, 4h, 1day, custom
  • Dismiss action
  • Auto-removes when hidden
  • Pauses polling when tab not visible

Integration:

  • Loaded in app/shared/frontend/base.html
  • Auto-initializes on page load if user authenticated
  • User ID extracted from JWT token

Configuration

Environment Variables

# Master switches (default: disabled for safety)
REMINDERS_ENABLED=false
REMINDERS_EMAIL_ENABLED=false
REMINDERS_MATTERMOST_ENABLED=false
REMINDERS_DRY_RUN=true              # Log without sending if true

# Scheduler settings
REMINDERS_CHECK_INTERVAL_MINUTES=5   # Frequency of reminder checks
REMINDERS_MAX_PER_USER_PER_HOUR=5    # Rate limit
REMINDERS_QUEUE_BATCH_SIZE=10        # Batch size for queue processing

# SMTP Configuration (for email reminders)
EMAIL_SMTP_HOST=smtp.gmail.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=noreply@bmcnetworks.dk
EMAIL_SMTP_PASSWORD=<secret>
EMAIL_SMTP_USE_TLS=true
EMAIL_SMTP_FROM_ADDRESS=noreply@bmcnetworks.dk
EMAIL_SMTP_FROM_NAME=BMC Hub

Pydantic Configuration

Added to app/core/config.py:

REMINDERS_ENABLED: bool = False
REMINDERS_EMAIL_ENABLED: bool = False
REMINDERS_MATTERMOST_ENABLED: bool = False
REMINDERS_DRY_RUN: bool = True
REMINDERS_CHECK_INTERVAL_MINUTES: int = 5
REMINDERS_MAX_PER_USER_PER_HOUR: int = 5
REMINDERS_QUEUE_BATCH_SIZE: int = 10

EMAIL_SMTP_HOST: str = ""
EMAIL_SMTP_PORT: int = 587
EMAIL_SMTP_USER: str = ""
EMAIL_SMTP_PASSWORD: str = ""
EMAIL_SMTP_USE_TLS: bool = True
EMAIL_SMTP_FROM_ADDRESS: str = "noreply@bmcnetworks.dk"
EMAIL_SMTP_FROM_NAME: str = "BMC Hub"

Safety Features

Rate Limiting

  • Global per user: Max 5 notifications per hour
  • Checked before sending via check_reminder_rate_limit(user_id)
  • Queued events marked as rate_limited if limit exceeded

Dry Run Mode

  • REMINDERS_DRY_RUN=true (default)
  • All operations logged to console/logs
  • No emails actually sent
  • No Mattermost webhooks fired
  • Useful for testing

Soft Delete

  • Reminders never hard-deleted from DB
  • deleted_at timestamp + is_active=false
  • Full audit trail preserved

Per-Reminder Override

  • Reminder can override user's default channels
  • override_user_preferences flag
  • Useful for critical reminders (urgent priority)

Usage Examples

Create a Status-Change Reminder

curl -X POST http://localhost:8000/api/v1/sag/123/reminders \
  -H "Content-Type: application/json" \
  -d {
    "title": "Case entered In Progress",
    "message": "Case #123 has moved to 'i_gang' status",
    "priority": "high",
    "trigger_type": "status_change",
    "trigger_config": {"target_status": "i_gang"},
    "recipient_user_ids": [1, 2],
    "notify_mattermost": true,
    "notify_email": true,
    "recurrence_type": "once"
  }

Create a Scheduled Reminder

curl -X POST http://localhost:8000/api/v1/sag/123/reminders \
  -H "Content-Type: application/json" \
  -d {
    "title": "Follow up needed",
    "message": "Check in with customer",
    "priority": "normal",
    "trigger_type": "time_based",
    "trigger_config": {},
    "scheduled_at": "2026-02-10T14:30:00",
    "recipient_user_ids": [1],
    "recurrence_type": "once"
  }

Create a Daily Recurring Reminder

curl -X POST http://localhost:8000/api/v1/sag/123/reminders \
  -H "Content-Type: application/json" \
  -d {
    "title": "Daily status check",
    "priority": "low",
    "trigger_type": "time_based",
    "trigger_config": {},
    "scheduled_at": "2026-02-04T09:00:00",
    "recipient_user_ids": [1],
    "recurrence_type": "daily"
  }

Update User Preferences

curl -X PATCH http://localhost:8000/api/v1/users/me/notification-preferences \
  -H "Content-Type: application/json" \
  -d {
    "notify_mattermost": true,
    "notify_email": false,
    "notify_frontend": true,
    "quiet_hours_enabled": true,
    "quiet_hours_start": "18:00",
    "quiet_hours_end": "08:00"
  }

Testing Checklist

Database

  • Run migration: docker-compose exec -T postgres psql -U bmc_hub -d bmc_hub -f /migrations/096_reminder_system.sql
  • Verify tables created: SELECT * FROM sag_reminders LIMIT 0;
  • Verify trigger exists: SELECT * FROM information_schema.triggers WHERE trigger_name LIKE 'sag%reminder%';

API

  • Test create reminder endpoint
  • Test list reminders endpoint
  • Test update reminder endpoint
  • Test snooze endpoint
  • Test dismiss endpoint
  • Test user preferences endpoints

Scheduler

  • Enable REMINDERS_ENABLED=true in .env
  • Restart container
  • Check logs for "Reminder job scheduled" message
  • Verify job runs every 5 minutes: ✅ Checking for pending reminders...

Status Change Trigger

  • Create reminder with trigger_type: status_change
  • Change case status
  • Verify event inserted in sag_reminder_queue
  • Wait for scheduler to process
  • Verify log entry in sag_reminder_logs

Email Sending

  • Configure SMTP in .env
  • Set REMINDERS_EMAIL_ENABLED=true
  • Set REMINDERS_DRY_RUN=false
  • Create reminder with notify_email=true
  • Verify email sent or check logs

Frontend Popup

  • Ensure static/js/notifications.js included in base.html
  • Open browser console
  • Log in to system
  • Should see " Reminder system initialized"
  • Create a pending reminder
  • Should see popup toast within 30 seconds
  • Test snooze dropdown
  • Test dismiss button

Rate Limiting

  • Create 6 reminders for user with trigger_type=time_based
  • Manually trigger scheduler job
  • Verify only 5 sent, 1 marked rate_limited
  • Check sag_reminder_logs for status

Deployment Notes

Local Development

  • All safety switches OFF by default (_ENABLED=false, DRY_RUN=true)
  • No SMTP configured - reminders won't send
  • No Mattermost webhook - notifications go to logs only
  • Test via dry-run mode

Production Deployment

  1. Configure SMTP credentials in .env
  2. Set REMINDERS_ENABLED=true
  3. Set REMINDERS_EMAIL_ENABLED=true if using email
  4. Set REMINDERS_MATTERMOST_ENABLED=true if using Mattermost
  5. Set REMINDERS_DRY_RUN=false to actually send
  6. Deploy with docker-compose -f docker-compose.prod.yml up -d --build
  7. Monitor logs for errors: docker-compose logs -f api

Files Modified/Created

New Files

  • migrations/096_reminder_system.sql - Database schema
  • app/services/reminder_notification_service.py - Notification service
  • app/jobs/check_reminders.py - Scheduler job
  • app/modules/sag/backend/reminders.py - API endpoints
  • static/js/notifications.js - Frontend notification system
  • templates/emails/reminder.html - Email template

Modified Files

  • app/core/config.py - Added reminder settings
  • app/services/email_service.py - Added send_email() method
  • main.py - Imported reminders router, registered scheduler job
  • app/shared/frontend/base.html - Added notifications.js script tag
  • requirements.txt - Added aiosmtplib dependency
  • .env - Added reminder configuration

Troubleshooting

Reminders not sending

  1. Check REMINDERS_ENABLED=true in .env
  2. Check scheduler logs: "Reminder check complete"
  3. Verify next_check_at <= NOW() for reminders
  4. Check rate limit: count in sag_reminder_logs last hour

Frontend popups not showing

  1. Check browser console for errors
  2. Verify JWT token contains sub (user_id)
  3. Check GET /api/v1/reminders/pending/me returns data
  4. Ensure static/js/notifications.js loaded

Email not sending

  1. Verify SMTP credentials in .env
  2. Check REMINDERS_EMAIL_ENABLED=true
  3. Check REMINDERS_DRY_RUN=false
  4. Review application logs for SMTP errors
  5. Test SMTP connection separately

Database trigger not working

  1. Verify migration applied successfully
  2. Check sag_status_change_reminder_trigger_exec trigger exists
  3. Update case status manually
  4. Check sag_reminder_queue for new events
  5. Review PostgreSQL logs if needed

Future Enhancements

  • Escalation rules (auto-escalate if not acknowledged)
  • SMS/WhatsApp integration (Twilio)
  • Calendar integration (iCal export)
  • User notification history/statistics
  • Webhook support for external services
  • AI-powered reminder suggestions
  • Mobile app push notifications