Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR implements a smart email alert system that automatically notifies users when air quality readings from their subscribed sensors exceed their configured thresholds. The system uses scheduled tasks to periodically check sensor data and sends personalized health advisories via email.
Key changes include:
- Addition of scheduled alert service with configurable timing
- Enhanced repository methods for efficient subscription and sensor data retrieval
- Email template with personalized health recommendations
- Configuration properties for alert scheduling intervals
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/main/resources/application.properties |
Adds configuration for alert scheduling interval |
src/main/java/com/itp/breathsafe/subscription/repository/SubscriptionRepository.java |
Adds query method to fetch active email subscriptions with user/sensor details |
src/main/java/com/itp/breathsafe/data/repository/SensorDataRepository.java |
Adds method to retrieve latest sensor data by sensor ID |
src/main/java/com/itp/breathsafe/common/smartalert/ScheduledAlertService.java |
Implements core scheduled alert functionality with email notifications |
src/main/java/com/itp/breathsafe/BreatheSafeServerApplication.java |
Enables Spring scheduling support |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| private boolean hasBeenAlertedRecently(Long subscriptionId, LocalDateTime dataTimestamp) { | ||
| LocalDateTime lastAlertTimestamp = recentlyAlerted.get(subscriptionId); | ||
| return lastAlertTimestamp != null && lastAlertTimestamp.equals(dataTimestamp); | ||
| } |
There was a problem hiding this comment.
The current implementation only prevents duplicate alerts for the exact same data timestamp. Consider adding a time-based cooldown period to prevent alert spam when new data points continue to exceed the threshold within a short timeframe.
|
|
||
| // In-memory cache to prevent sending duplicate alerts for the same sensor data point. | ||
| // Key: Subscription ID, Value: Timestamp of the SensorData record that triggered the last alert. | ||
| private final Map<Long, LocalDateTime> recentlyAlerted = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
The in-memory cache will lose data on application restart, potentially causing duplicate alerts. Consider persisting alert history or implementing a time-based cleanup mechanism to prevent unbounded memory growth.
| } catch (CustomException | MessagingException e) { | ||
| logger.error("Failed to process alert for subscription ID {}: {}", sub.getId(), e.getMessage()); | ||
| // Continue to the next subscription even if one fails. | ||
| } | ||
| } |
There was a problem hiding this comment.
[nitpick] Consider catching specific exceptions separately to provide more targeted error handling. For example, messaging failures might warrant retry logic while custom exceptions might indicate data issues.
| } catch (CustomException | MessagingException e) { | |
| logger.error("Failed to process alert for subscription ID {}: {}", sub.getId(), e.getMessage()); | |
| // Continue to the next subscription even if one fails. | |
| } | |
| } | |
| } catch (MessagingException e) { | |
| logger.error("Messaging failure for subscription ID {}: {}. Consider retrying.", sub.getId(), e.getMessage()); | |
| // Could implement retry logic here if desired. | |
| } catch (CustomException e) { | |
| logger.error("Custom exception for subscription ID {}: {}", sub.getId(), e.getMessage()); | |
| // Data issue, skip to next subscription. | |
| } |
Description
Please include a summary of the change and which issue is fixed or feature is added.
Checklist
USE breathsafe;
-- Sample data for the 'user' table
INSERT INTO user (id, address, bio, created_at, date_of_birth, email, first_name, last_name, password, phone, profile_image, role, updated_at, username) VALUES
(1, '', '', '2025-10-04 10:53:40.307693', NULL, 'info@anupa.lk', 'Anupa', 'Prabhasara', '$2a$10$HZdNma4BL8m/Gg6lfcBqYeNLQXgWgI8OhW5Vz2yLr4y1lAMD/R3Bu', '', '', 'USER', NULL, 'user'),
(2, '', '', '2025-10-05 10:53:40.307693', NULL, 'admin@anupa.lk', 'Anupa', 'Prabhasara', '$2a$10$HZdNma4BL8m/Gg6lfcBqYeNLQXgWgI8OhW5Vz2yLr4y1lAMD/R3Bu', '', '', 'ADMIN', NULL, 'admin'),
(3, '', '', '2025-10-06 10:53:40.307693', NULL, 'sadmin@anupa.lk', 'Anupa', 'Prabhasara', '$2a$10$HZdNma4BL8m/Gg6lfcBqYeNLQXgWgI8OhW5Vz2yLr4y1lAMD/R3Bu', '', '', 'SENSOR_ADMIN', NULL, 'sadmin');
-- Sample data for the 'sensor' table
INSERT INTO sensor (name, location, latitude, longitude, status, installation_date, last_maintenance, battery_level, is_active) VALUES
('AQM-Central-01', 'Town Hall, Colombo', 6.9271, 79.8612, 'ONLINE', '2023-01-15 10:00:00', '2023-09-01 14:30:00', 95, true),
('AQM-Kandy-Lake-01', 'Kandy Lake Round', 7.2906, 80.6337, 'ONLINE', '2023-02-20 09:00:00', '2023-08-10 11:00:00', 88, true),
('AQM-Galle-Fort-01', 'Galle Fort Lighthouse', 6.0264, 80.2172, 'OFFLINE', '2022-11-05 15:00:00', '2023-10-25 09:00:00', 15, true),
('AQM-Industrial-Zone-01', 'Katunayake EPZ', 7.1793, 79.8821, 'MAINTENANCE', '2023-03-10 12:00:00', '2023-10-28 08:00:00', 100, true),
('AQM-North-Jaffna-01', 'Nallur Kovil Area', 9.6750, 80.0255, 'ERROR', '2023-05-22 11:30:00', '2023-09-15 16:00:00', 42, true),
('AQM-Decommissioned-01', 'Storage Warehouse', 6.8511, 79.9213, 'OFFLINE', '2022-01-10 14:00:00', '2023-01-01 10:00:00', 0, false);
-- Sample data for the 'subscription' table
INSERT INTO subscription (user_id, sensor_id, alert_threshold, email_notifications, is_active) VALUES
(1, 1, 100, true, true), -- User 1 subscribes to sensor 1 with default settings.
(1, 2, 120, true, true), -- User 1 subscribes to sensor 2 with a custom alert threshold.
(1, 3, 100, true, true), -- User 1 subscribes to sensor 3 but has disabled email notifications.
(1, 4, 100, true, true), -- User 1 has an inactive/paused subscription to sensor 4.
(1, 5, 150, true, true), -- User 1 subscribes to sensor 5 with a high alert threshold.
(1, 6, 80, true, true), -- User 1 subscribes to sensor 6 with a lower, more sensitive threshold.
(2, 1, 100, false, true), -- User 1 subscribes to sensor 1 with default settings.
(2, 2, 120, false, true), -- User 1 subscribes to sensor 2 with a custom alert threshold.
(2, 3, 100, false, true), -- User 1 subscribes to sensor 3 but has disabled email notifications.
(2, 4, 100, false, false), -- User 1 has an inactive/paused subscription to sensor 4.
(2, 5, 150, false, true), -- User 1 subscribes to sensor 5 with a high alert threshold.
(2, 6, 80, false, true), -- User 1 subscribes to sensor 6 with a lower, more sensitive threshold.
(3, 1, 100, false, true), -- User 1 subscribes to sensor 1 with default settings.
(3, 2, 120, false, true), -- User 1 subscribes to sensor 2 with a custom alert threshold.
(3, 3, 100, false, true), -- User 1 subscribes to sensor 3 but has disabled email notifications.
(3, 4, 100, false, false), -- User 1 has an inactive/paused subscription to sensor 4.
(3, 5, 150, false, true), -- User 1 subscribes to sensor 5 with a high alert threshold.
(3, 6, 80, false, true); -- User 1 subscribes to sensor 6 with a lower, more sensitive threshold.
-- Sample data for the 'sensor_installation_requests' table (Enum Corrected)
INSERT INTO sensor_installation_requests (id, requested_location, latitude, longitude, justification, status, admin_comments, approved_at, rejected_at, created_at, updated_at, requester_id, approved_by, assigned_sensor_id) VALUES
(1, 'Viharamahadevi Park, Colombo', 6.9147, 79.8601, 'High foot traffic area with many families and children. Monitoring air quality here would be beneficial for public health awareness.', 'PENDING', NULL, NULL, NULL, '2025-10-01 09:15:00', '2025-10-01 09:15:00', 1, NULL, NULL),
(2, 'University of Moratuwa, Engineering Faculty', 6.7969, 79.9018, 'Request from the university to monitor air quality for a research project on urban pollution.', 'COMPLETED', 'Good initiative. Assigned and installed the new sensor.', '2025-09-25 14:00:00', NULL, '2025-09-20 11:30:00', '2025-10-05 16:00:00', 1, 2, 6),
(3, 'Private Residence, Wellawatte', 6.8773, 79.8631, 'Personal use for home air quality monitoring.', 'REJECTED', 'Requests are currently only approved for public spaces and research institutions.', NULL, '2025-09-28 17:20:00', '2025-09-26 15:00:00', '2025-09-28 17:20:00', 1, 3, NULL),
(4, 'Galle Face Green', 6.9213, 79.8449, 'Popular recreational area, important for monitoring tourist and local exposure to pollutants.', 'IN_PROGRESS', 'Approved. Team dispatched for installation.', '2025-10-06 10:00:00', NULL, '2025-10-02 12:00:00', '2025-10-06 10:00:00', 1, 2, NULL);
-- Sample data for the 'sensor_data' table (Enum Corrected)
-- Sensor 1: AQM-Central-01 (Colombo) - GOOD to MODERATE AQI
INSERT INTO sensor_data (id, temperature, humidity, co2_level, aqi_value, aqi_category, timestamp, sensor_id) VALUES
(1, 29.5, 78.0, 450.0, 45, 'GOOD', '2025-10-07 14:20:00', 1),
(2, 29.7, 79.0, 460.0, 52, 'MODERATE', '2025-10-07 14:25:00', 1),
(3, 29.6, 78.5, 455.0, 48, 'GOOD', '2025-10-07 14:30:00', 1);
-- Sensor 2: AQM-Kandy-Lake-01 - GOOD AQI
INSERT INTO sensor_data (id, temperature, humidity, co2_level, aqi_value, aqi_category, timestamp, sensor_id) VALUES
(4, 26.2, 82.0, 410.0, 35, 'GOOD', '2025-10-07 14:20:00', 2),
(5, 26.3, 82.5, 412.0, 38, 'GOOD', '2025-10-07 14:25:00', 2),
(6, 26.1, 82.2, 411.0, 36, 'GOOD', '2025-10-07 14:30:00', 2);
-- Sensor 4: AQM-Industrial-Zone-01 (Katunayake) - MODERATE to UNHEALTHY_SENSITIVE
INSERT INTO sensor_data (id, temperature, humidity, co2_level, aqi_value, aqi_category, timestamp, sensor_id) VALUES
(7, 30.1, 75.0, 550.0, 95, 'MODERATE', '2025-10-07 14:20:00', 4),
(8, 30.3, 76.0, 580.0, 115, 'UNHEALTHY_SENSITIVE', '2025-10-07 14:25:00', 4),
(9, 30.2, 75.5, 565.0, 102, 'UNHEALTHY_SENSITIVE', '2025-10-07 14:30:00', 4);
-- Sensor 5: AQM-North-Jaffna-01 - UNHEALTHY to VERY_UNHEALTHY
INSERT INTO sensor_data (id, temperature, humidity, co2_level, aqi_value, aqi_category, timestamp, sensor_id) VALUES
(10, 31.0, 70.0, 650.0, 165, 'UNHEALTHY', '2025-10-07 14:20:00', 5),
(11, 31.5, 68.0, 750.0, 205, 'VERY_UNHEALTHY', '2025-10-07 14:25:00', 5),
(12, 31.2, 69.0, 680.0, 175, 'UNHEALTHY', '2025-10-07 14:30:00', 5);
Fixes #[issue_number] (if applicable)