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 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-05-28 10:51:19 -04:00
co-authored by Claude Sonnet 4.6
parent 2e8604a354
commit e1287e5a0f
9 changed files with 42 additions and 49 deletions
@@ -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 '____-__-__ %';
+2 -2
View File
@@ -13,7 +13,7 @@ pub fn insert(event: DeviceEvent) -> Result<i64, DbError> {
"INSERT INTO device_events (mac_address, created_on, event_type, ipv4_address, vendor) VALUES (?1, ?2, ?3, ?4, ?5)", "INSERT INTO device_events (mac_address, created_on, event_type, ipv4_address, vendor) VALUES (?1, ?2, ?3, ?4, ?5)",
params![ params![
event.mac_address, event.mac_address,
event.created_on, event.created_on.to_rfc3339_opts(chrono::SecondsFormat::Nanos, false),
event.event_type, event.event_type,
event.ipv4_address, event.ipv4_address,
event.vendor event.vendor
@@ -92,7 +92,7 @@ pub fn purge_older_than(cutoff: DateTime<Utc>) -> Result<usize, DbError> {
match conn.execute( match conn.execute(
"DELETE FROM device_events WHERE created_on < ?1", "DELETE FROM device_events WHERE created_on < ?1",
params![cutoff], params![cutoff.to_rfc3339()],
) { ) {
Ok(count) => { Ok(count) => {
debug!("Purged {} device event(s) older than {}", count, cutoff); debug!("Purged {} device event(s) older than {}", count, cutoff);
+6 -22
View File
@@ -27,30 +27,14 @@ pub fn list_devices(
params.push(is_registered.into()); params.push(is_registered.into());
}; };
if let Some(last_seen_from) = last_seen_from { if let Some(last_seen_from) = last_seen_from {
debug!( debug!("Adding filter last_seen>={}", last_seen_from.to_rfc3339());
"Adding filter last_seen>={}",
last_seen_from.format("%Y-%m-%d %H:%M:%S")
);
sql_statement.push_str("AND last_seen>=? "); sql_statement.push_str("AND last_seen>=? ");
params.push( params.push(last_seen_from.to_rfc3339().into());
last_seen_from
.format("%Y-%m-%d %H:%M:%S")
.to_string()
.into(),
);
}; };
if let Some(last_seen_to) = last_seen_to { if let Some(last_seen_to) = last_seen_to {
debug!( debug!("Adding filter last_seen<={}", last_seen_to.to_rfc3339());
"Adding filter last_seen<={}",
last_seen_to.format("%Y-%m-%d %H:%M:%S")
);
sql_statement.push_str("AND last_seen<=? "); sql_statement.push_str("AND last_seen<=? ");
params.push( params.push(last_seen_to.to_rfc3339().into());
last_seen_to
.format("%Y-%m-%d %H:%M:%S")
.to_string()
.into(),
);
}; };
if let Some(owner) = owner { if let Some(owner) = owner {
debug!("Adding filter owner={}", owner); debug!("Adding filter owner={}", owner);
@@ -126,7 +110,7 @@ pub fn insert(device: Device) -> Result<(), DbError> {
match conn.execute( 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)", "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(_) => { Ok(_) => {
debug!("Device inserted into database: {}", device); debug!("Device inserted into database: {}", device);
Ok(()) Ok(())
@@ -145,7 +129,7 @@ pub fn update(device: Device) -> Result<(), DbError> {
params![ params![
device.ipv4_address, device.ipv4_address,
device.vendor, device.vendor,
device.last_seen, device.last_seen.to_rfc3339_opts(chrono::SecondsFormat::Nanos, false),
device.is_registered, device.is_registered,
device.owner, device.owner,
device.device_type, device.device_type,
+2 -2
View File
@@ -67,7 +67,7 @@ pub fn insert(notification: Notification) -> Result<i64, DbError> {
match conn.execute( match conn.execute(
"INSERT INTO notifications (created_on, notification_type, title, body, is_new, mac_address) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", "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(_) => { Ok(_) => {
debug!("Notification inserted into database: {}", notification); debug!("Notification inserted into database: {}", notification);
Ok(conn.last_insert_rowid()) Ok(conn.last_insert_rowid())
@@ -129,7 +129,7 @@ pub fn purge_older_than(cutoff: DateTime<Utc>) -> Result<usize, DbError> {
match conn.execute( match conn.execute(
"DELETE FROM notifications WHERE created_on < ?1", "DELETE FROM notifications WHERE created_on < ?1",
params![cutoff], params![cutoff.to_rfc3339()],
) { ) {
Ok(count) => { Ok(count) => {
debug!("Purged {} notification(s) older than {}", count, cutoff); debug!("Purged {} notification(s) older than {}", count, cutoff);
@@ -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 (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-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 (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-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 (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-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 (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-01 11:11:11', 'Other', 'Unread other', 'Body other', 1); 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-03 13:13:13', 'Other', 'Read other', 'Body other', 0); 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);
+3 -3
View File
@@ -1,6 +1,6 @@
INSERT INTO DEVICES (mac_address, ipv4_address, vendor, last_seen, is_registered, owner, device_type) 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) 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) 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');
@@ -1,6 +1,6 @@
INSERT INTO device_events (id, mac_address, created_on, event_type, ipv4_address, vendor) 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) 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) 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');
@@ -70,7 +70,7 @@ class DeviceEventHistory extends StatefulWidget {
} }
class _DeviceEventHistoryState extends State<DeviceEventHistory> { class _DeviceEventHistoryState extends State<DeviceEventHistory> {
List<DeviceEvent>? _allEvents; List<DeviceEvent>? _events;
bool _isLoading = true; bool _isLoading = true;
String? _error; String? _error;
_TimeRange _selectedRange = _TimeRange.lastWeek; _TimeRange _selectedRange = _TimeRange.lastWeek;
@@ -89,10 +89,11 @@ class _DeviceEventHistoryState extends State<DeviceEventHistory> {
try { try {
final events = await BackendAPI.instance.getDeviceEvents( final events = await BackendAPI.instance.getDeviceEvents(
widget.device.macAddress, widget.device.macAddress,
createdFrom: _selectedRange.cutoff,
); );
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_allEvents = events; _events = events;
_isLoading = false; _isLoading = false;
}); });
} catch (e) { } catch (e) {
@@ -104,12 +105,6 @@ class _DeviceEventHistoryState extends State<DeviceEventHistory> {
} }
} }
List<DeviceEvent> get _filteredEvents {
if (_allEvents == null) return [];
final cutoff = _selectedRange.cutoff;
return _allEvents!.where((e) => e.createdOn.isAfter(cutoff)).toList();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading) { if (_isLoading) {
@@ -126,7 +121,7 @@ class _DeviceEventHistoryState extends State<DeviceEventHistory> {
); );
} }
final events = _filteredEvents; final events = _events ?? [];
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -143,6 +138,7 @@ class _DeviceEventHistoryState extends State<DeviceEventHistory> {
selected: {_selectedRange}, selected: {_selectedRange},
onSelectionChanged: (selection) { onSelectionChanged: (selection) {
setState(() => _selectedRange = selection.first); setState(() => _selectedRange = selection.first);
_loadEvents();
}, },
), ),
), ),
+12 -2
View File
@@ -137,9 +137,19 @@ class BackendAPI {
await _dio.delete('/devices/$macAddress'); await _dio.delete('/devices/$macAddress');
} }
Future<List<DeviceEvent>> getDeviceEvents(String macAddress) async { Future<List<DeviceEvent>> getDeviceEvents(
String macAddress, {
DateTime? createdFrom,
}) async {
debugPrint('About to call GET /devices/$macAddress/events'); debugPrint('About to call GET /devices/$macAddress/events');
final response = await _dio.get('/devices/$macAddress/events'); final queryParams = <String, dynamic>{};
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}'); debugPrint('Received: ${response.data}');
return (response.data as List) return (response.data as List)
.map((item) => DeviceEvent.fromJson(item as Map<String, dynamic>)) .map((item) => DeviceEvent.fromJson(item as Map<String, dynamic>))