- 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.
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):
-
user_notification_preferences- User default notification settings- Default channels (mattermost, email, frontend)
- Quiet hours configuration
- Email override option
-
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)
-
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
-
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 sendv_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 usingaiosmtplib - SMTP configuration from
.env - Supports plain text + HTML bodies
- Safety flag:
REMINDERS_DRY_RUN=truelogs 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/meevery 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_limitedif 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_attimestamp +is_active=false- Full audit trail preserved
Per-Reminder Override
- Reminder can override user's default channels
override_user_preferencesflag- 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=truein.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.jsincluded 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_logsfor 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
- Configure SMTP credentials in
.env - Set
REMINDERS_ENABLED=true - Set
REMINDERS_EMAIL_ENABLED=trueif using email - Set
REMINDERS_MATTERMOST_ENABLED=trueif using Mattermost - Set
REMINDERS_DRY_RUN=falseto actually send - Deploy with
docker-compose -f docker-compose.prod.yml up -d --build - Monitor logs for errors:
docker-compose logs -f api
Files Modified/Created
New Files
migrations/096_reminder_system.sql- Database schemaapp/services/reminder_notification_service.py- Notification serviceapp/jobs/check_reminders.py- Scheduler jobapp/modules/sag/backend/reminders.py- API endpointsstatic/js/notifications.js- Frontend notification systemtemplates/emails/reminder.html- Email template
Modified Files
app/core/config.py- Added reminder settingsapp/services/email_service.py- Addedsend_email()methodmain.py- Imported reminders router, registered scheduler jobapp/shared/frontend/base.html- Added notifications.js script tagrequirements.txt- Addedaiosmtplibdependency.env- Added reminder configuration
Troubleshooting
Reminders not sending
- Check
REMINDERS_ENABLED=truein.env - Check scheduler logs: "Reminder check complete"
- Verify
next_check_at<= NOW() for reminders - Check rate limit: count in
sag_reminder_logslast hour
Frontend popups not showing
- Check browser console for errors
- Verify JWT token contains
sub(user_id) - Check
GET /api/v1/reminders/pending/mereturns data - Ensure
static/js/notifications.jsloaded
Email not sending
- Verify SMTP credentials in
.env - Check
REMINDERS_EMAIL_ENABLED=true - Check
REMINDERS_DRY_RUN=false - Review application logs for SMTP errors
- Test SMTP connection separately
Database trigger not working
- Verify migration applied successfully
- Check
sag_status_change_reminder_trigger_exectrigger exists - Update case status manually
- Check
sag_reminder_queuefor new events - 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