From e1287e5a0f39bfdacbfac7dad205c809e391b351 Mon Sep 17 00:00:00 2001 From: rzuasti Date: Thu, 28 May 2026 10:51:19 -0400 Subject: [PATCH] Use server-side date filtering for device event history chart The device event history chart previously fetched all events and filtered client-side. This wires the existing created_from API parameter to the frontend so filtering happens in the backend. Also fixes a bug where the Today filter returned no results: rusqlite was storing datetimes with a space separator ("YYYY-MM-DD HH:MM:SS+00:00") while SQL filters used RFC 3339 with a T separator, causing string comparisons to fail for same-day events. All datetime storage across device_events, devices, and notifications is now consistently RFC 3339. A migration converts existing records. Co-Authored-By: Claude Sonnet 4.6 --- .../06-rfc3339_datetimes/up.sql | 3 ++ backend/src/db/device_events.rs | 4 +-- backend/src/db/devices.rs | 28 ++++--------------- backend/src/db/notifications.rs | 4 +-- .../tests/database_setup/01-notifications.sql | 12 ++++---- backend/tests/database_setup/02-devices.sql | 6 ++-- .../tests/database_setup/03-device_events.sql | 6 ++-- .../lib/devices/device_event_history.dart | 14 ++++------ frontend/lib/utils/oott_api.dart | 14 ++++++++-- 9 files changed, 42 insertions(+), 49 deletions(-) create mode 100644 backend/database_migrations/06-rfc3339_datetimes/up.sql diff --git a/backend/database_migrations/06-rfc3339_datetimes/up.sql b/backend/database_migrations/06-rfc3339_datetimes/up.sql new file mode 100644 index 0000000..1387450 --- /dev/null +++ b/backend/database_migrations/06-rfc3339_datetimes/up.sql @@ -0,0 +1,3 @@ +UPDATE device_events SET created_on = REPLACE(created_on, ' ', 'T') WHERE created_on LIKE '____-__-__ %'; +UPDATE devices SET last_seen = REPLACE(last_seen, ' ', 'T') WHERE last_seen LIKE '____-__-__ %'; +UPDATE notifications SET created_on = REPLACE(created_on, ' ', 'T') WHERE created_on LIKE '____-__-__ %'; diff --git a/backend/src/db/device_events.rs b/backend/src/db/device_events.rs index 2cfe1f8..9b708b4 100644 --- a/backend/src/db/device_events.rs +++ b/backend/src/db/device_events.rs @@ -13,7 +13,7 @@ pub fn insert(event: DeviceEvent) -> Result { "INSERT INTO device_events (mac_address, created_on, event_type, ipv4_address, vendor) VALUES (?1, ?2, ?3, ?4, ?5)", params![ event.mac_address, - event.created_on, + event.created_on.to_rfc3339_opts(chrono::SecondsFormat::Nanos, false), event.event_type, event.ipv4_address, event.vendor @@ -92,7 +92,7 @@ pub fn purge_older_than(cutoff: DateTime) -> Result { match conn.execute( "DELETE FROM device_events WHERE created_on < ?1", - params![cutoff], + params![cutoff.to_rfc3339()], ) { Ok(count) => { debug!("Purged {} device event(s) older than {}", count, cutoff); diff --git a/backend/src/db/devices.rs b/backend/src/db/devices.rs index 22dbcbd..58231bf 100644 --- a/backend/src/db/devices.rs +++ b/backend/src/db/devices.rs @@ -27,30 +27,14 @@ pub fn list_devices( params.push(is_registered.into()); }; if let Some(last_seen_from) = last_seen_from { - debug!( - "Adding filter last_seen>={}", - last_seen_from.format("%Y-%m-%d %H:%M:%S") - ); + debug!("Adding filter last_seen>={}", last_seen_from.to_rfc3339()); sql_statement.push_str("AND last_seen>=? "); - params.push( - last_seen_from - .format("%Y-%m-%d %H:%M:%S") - .to_string() - .into(), - ); + params.push(last_seen_from.to_rfc3339().into()); }; if let Some(last_seen_to) = last_seen_to { - debug!( - "Adding filter last_seen<={}", - last_seen_to.format("%Y-%m-%d %H:%M:%S") - ); + debug!("Adding filter last_seen<={}", last_seen_to.to_rfc3339()); sql_statement.push_str("AND last_seen<=? "); - params.push( - last_seen_to - .format("%Y-%m-%d %H:%M:%S") - .to_string() - .into(), - ); + params.push(last_seen_to.to_rfc3339().into()); }; if let Some(owner) = owner { debug!("Adding filter owner={}", owner); @@ -126,7 +110,7 @@ pub fn insert(device: Device) -> Result<(), DbError> { match conn.execute( "INSERT INTO devices (mac_address, ipv4_address, vendor, last_seen, is_registered, owner, device_type) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - params![device.mac_address, device.ipv4_address, device.vendor, device.last_seen, device.is_registered, device.owner, device.device_type]) { + params![device.mac_address, device.ipv4_address, device.vendor, device.last_seen.to_rfc3339_opts(chrono::SecondsFormat::Nanos, false), device.is_registered, device.owner, device.device_type]) { Ok(_) => { debug!("Device inserted into database: {}", device); Ok(()) @@ -145,7 +129,7 @@ pub fn update(device: Device) -> Result<(), DbError> { params![ device.ipv4_address, device.vendor, - device.last_seen, + device.last_seen.to_rfc3339_opts(chrono::SecondsFormat::Nanos, false), device.is_registered, device.owner, device.device_type, diff --git a/backend/src/db/notifications.rs b/backend/src/db/notifications.rs index d45e547..1a84af1 100644 --- a/backend/src/db/notifications.rs +++ b/backend/src/db/notifications.rs @@ -67,7 +67,7 @@ pub fn insert(notification: Notification) -> Result { match conn.execute( "INSERT INTO notifications (created_on, notification_type, title, body, is_new, mac_address) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![notification.created_on, notification.notification_type, notification.title, notification.body, notification.is_new, notification.mac_address]) { + params![notification.created_on.to_rfc3339_opts(chrono::SecondsFormat::Nanos, false), notification.notification_type, notification.title, notification.body, notification.is_new, notification.mac_address]) { Ok(_) => { debug!("Notification inserted into database: {}", notification); Ok(conn.last_insert_rowid()) @@ -129,7 +129,7 @@ pub fn purge_older_than(cutoff: DateTime) -> Result { match conn.execute( "DELETE FROM notifications WHERE created_on < ?1", - params![cutoff], + params![cutoff.to_rfc3339()], ) { Ok(count) => { debug!("Purged {} notification(s) older than {}", count, cutoff); diff --git a/backend/tests/database_setup/01-notifications.sql b/backend/tests/database_setup/01-notifications.sql index a63e9d2..1b0e67b 100644 --- a/backend/tests/database_setup/01-notifications.sql +++ b/backend/tests/database_setup/01-notifications.sql @@ -1,6 +1,6 @@ -INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (1, '2026-01-03 14:13:12', 'NewDeviceFound', 'Unread new device found', 'Body unread new device found', 1); -INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (2, '2026-01-04 08:10:13', 'NewDeviceFound', 'Read new device found', 'Body read new device found', 0); -INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (3, '2026-01-05 17:20:01', 'DeviceOnlineAfterTime', 'Unread device online after time', 'Body unread device online after time', 1); -INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (4, '2026-01-06 02:11:12', 'DeviceOnlineAfterTime', 'Read device online after time', 'Body read device online after time', 0); -INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (5, '2026-02-01 11:11:11', 'Other', 'Unread other', 'Body other', 1); -INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (6, '2026-02-03 13:13:13', 'Other', 'Read other', 'Body other', 0); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (1, '2026-01-03T14:13:12+00:00', 'NewDeviceFound', 'Unread new device found', 'Body unread new device found', 1); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (2, '2026-01-04T08:10:13+00:00', 'NewDeviceFound', 'Read new device found', 'Body read new device found', 0); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (3, '2026-01-05T17:20:01+00:00', 'DeviceOnlineAfterTime', 'Unread device online after time', 'Body unread device online after time', 1); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (4, '2026-01-06T02:11:12+00:00', 'DeviceOnlineAfterTime', 'Read device online after time', 'Body read device online after time', 0); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (5, '2026-02-01T11:11:11+00:00', 'Other', 'Unread other', 'Body other', 1); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (6, '2026-02-03T13:13:13+00:00', 'Other', 'Read other', 'Body other', 0); diff --git a/backend/tests/database_setup/02-devices.sql b/backend/tests/database_setup/02-devices.sql index 2752ab6..968c819 100644 --- a/backend/tests/database_setup/02-devices.sql +++ b/backend/tests/database_setup/02-devices.sql @@ -1,6 +1,6 @@ INSERT INTO DEVICES (mac_address, ipv4_address, vendor, last_seen, is_registered, owner, device_type) - VALUES ('aa:aa:aa:aa:aa:aa', '192.168.0.1', 'Vendor 1', '2026-01-01 11:11:11', 0, '', ''); + VALUES ('aa:aa:aa:aa:aa:aa', '192.168.0.1', 'Vendor 1', '2026-01-01T11:11:11+00:00', 0, '', ''); INSERT INTO DEVICES (mac_address, ipv4_address, vendor, last_seen, is_registered, owner, device_type) - VALUES ('bb:bb:bb:bb:bb:bb', '192.168.0.2', 'Vendor 2', '2026-02-03 13:14:15', 1, 'John', 'Phone'); + VALUES ('bb:bb:bb:bb:bb:bb', '192.168.0.2', 'Vendor 2', '2026-02-03T13:14:15+00:00', 1, 'John', 'Phone'); INSERT INTO DEVICES (mac_address, ipv4_address, vendor, last_seen, is_registered, owner, device_type) - VALUES ('cc:cc:cc:cc:cc:cc', '192.168.0.3', 'Vendor 3', '2026-02-17 20:11:00', 1, 'Sarah', 'Laptop'); + VALUES ('cc:cc:cc:cc:cc:cc', '192.168.0.3', 'Vendor 3', '2026-02-17T20:11:00+00:00', 1, 'Sarah', 'Laptop'); diff --git a/backend/tests/database_setup/03-device_events.sql b/backend/tests/database_setup/03-device_events.sql index 19b9e58..6bb9999 100644 --- a/backend/tests/database_setup/03-device_events.sql +++ b/backend/tests/database_setup/03-device_events.sql @@ -1,6 +1,6 @@ INSERT INTO device_events (id, mac_address, created_on, event_type, ipv4_address, vendor) - VALUES (1, 'aa:aa:aa:aa:aa:aa', '2026-01-01 11:11:11', 'NewDevice', '192.168.0.1', 'Vendor 1'); + VALUES (1, 'aa:aa:aa:aa:aa:aa', '2026-01-01T11:11:11+00:00', 'NewDevice', '192.168.0.1', 'Vendor 1'); INSERT INTO device_events (id, mac_address, created_on, event_type, ipv4_address, vendor) - VALUES (2, 'bb:bb:bb:bb:bb:bb', '2026-02-03 13:14:15', 'DeviceSeen', '192.168.0.2', 'Vendor 2'); + VALUES (2, 'bb:bb:bb:bb:bb:bb', '2026-02-03T13:14:15+00:00', 'DeviceSeen', '192.168.0.2', 'Vendor 2'); INSERT INTO device_events (id, mac_address, created_on, event_type, ipv4_address, vendor) - VALUES (3, 'aa:aa:aa:aa:aa:aa', '2026-03-10 09:00:00', 'DeviceSeen', '192.168.0.1', 'Vendor 1'); + VALUES (3, 'aa:aa:aa:aa:aa:aa', '2026-03-10T09:00:00+00:00', 'DeviceSeen', '192.168.0.1', 'Vendor 1'); diff --git a/frontend/lib/devices/device_event_history.dart b/frontend/lib/devices/device_event_history.dart index 6ba6919..38d22d6 100644 --- a/frontend/lib/devices/device_event_history.dart +++ b/frontend/lib/devices/device_event_history.dart @@ -70,7 +70,7 @@ class DeviceEventHistory extends StatefulWidget { } class _DeviceEventHistoryState extends State { - List? _allEvents; + List? _events; bool _isLoading = true; String? _error; _TimeRange _selectedRange = _TimeRange.lastWeek; @@ -89,10 +89,11 @@ class _DeviceEventHistoryState extends State { try { final events = await BackendAPI.instance.getDeviceEvents( widget.device.macAddress, + createdFrom: _selectedRange.cutoff, ); if (!mounted) return; setState(() { - _allEvents = events; + _events = events; _isLoading = false; }); } catch (e) { @@ -104,12 +105,6 @@ class _DeviceEventHistoryState extends State { } } - List get _filteredEvents { - if (_allEvents == null) return []; - final cutoff = _selectedRange.cutoff; - return _allEvents!.where((e) => e.createdOn.isAfter(cutoff)).toList(); - } - @override Widget build(BuildContext context) { if (_isLoading) { @@ -126,7 +121,7 @@ class _DeviceEventHistoryState extends State { ); } - final events = _filteredEvents; + final events = _events ?? []; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -143,6 +138,7 @@ class _DeviceEventHistoryState extends State { selected: {_selectedRange}, onSelectionChanged: (selection) { setState(() => _selectedRange = selection.first); + _loadEvents(); }, ), ), diff --git a/frontend/lib/utils/oott_api.dart b/frontend/lib/utils/oott_api.dart index 5d0e0c9..bbf572d 100644 --- a/frontend/lib/utils/oott_api.dart +++ b/frontend/lib/utils/oott_api.dart @@ -137,9 +137,19 @@ class BackendAPI { await _dio.delete('/devices/$macAddress'); } - Future> getDeviceEvents(String macAddress) async { + Future> getDeviceEvents( + String macAddress, { + DateTime? createdFrom, + }) async { debugPrint('About to call GET /devices/$macAddress/events'); - final response = await _dio.get('/devices/$macAddress/events'); + final queryParams = {}; + if (createdFrom != null) { + queryParams['created_from'] = createdFrom.toUtc().toIso8601String(); + } + final response = await _dio.get( + '/devices/$macAddress/events', + queryParameters: queryParams.isEmpty ? null : queryParams, + ); debugPrint('Received: ${response.data}'); return (response.data as List) .map((item) => DeviceEvent.fromJson(item as Map))