# 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`**: ```python 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 ```env # 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= 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`: ```python 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 ```bash 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 ```bash 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 ```bash 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 ```bash 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