mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Redesign Home screen with device summary and scanner status
Adds a responsive Home screen combining the notification list with a device summary panel and ARP scanner status, visible side-by-side on wide screens and stacked on narrow ones. Backend: - New GET /api/devices/summary endpoint returning registered device counts and "seen in last 24h / 7 days" breakdowns by registration status - Migration 07 adds indexes on (is_registered) and (last_seen, is_registered) so summary queries use index range scans instead of full table scans Frontend: - HomeScreen replaces NotificationList, embedding notifications, device summary card, and ArpScannerCard in a LayoutBuilder-driven two-column (≥700 px) or single-column layout - ArpScannerCard extracted to a shared public widget reused by both HomeScreen and StatusScreen - Nav rail entry renamed to "Home" with home icon Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
dc8f47e66d
commit
251d8a479e
@@ -1,10 +1,10 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use log::{debug, error};
|
||||
use rusqlite::{params, params_from_iter};
|
||||
|
||||
use crate::{
|
||||
db::{self, error::DbError},
|
||||
model::devices::Device,
|
||||
model::devices::{Device, DeviceSummary},
|
||||
};
|
||||
|
||||
pub fn list_devices(
|
||||
@@ -122,6 +122,47 @@ pub fn insert(device: Device) -> Result<(), DbError> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_summary() -> Result<DeviceSummary, DbError> {
|
||||
debug!("Getting device summary");
|
||||
let conn = db::get_db_connection();
|
||||
let one_day_ago = (Utc::now() - Duration::days(1)).to_rfc3339();
|
||||
let one_week_ago = (Utc::now() - Duration::weeks(1)).to_rfc3339();
|
||||
|
||||
let total_registered: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM devices WHERE is_registered = 1",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
let seen_last_day_registered: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM devices WHERE last_seen >= ?1 AND is_registered = 1",
|
||||
params![one_day_ago],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
let seen_last_day_unregistered: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM devices WHERE last_seen >= ?1 AND is_registered = 0",
|
||||
params![one_day_ago],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
let seen_last_week_registered: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM devices WHERE last_seen >= ?1 AND is_registered = 1",
|
||||
params![one_week_ago],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
let seen_last_week_unregistered: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM devices WHERE last_seen >= ?1 AND is_registered = 0",
|
||||
params![one_week_ago],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
Ok(DeviceSummary {
|
||||
total_registered,
|
||||
seen_last_day_registered,
|
||||
seen_last_day_unregistered,
|
||||
seen_last_week_registered,
|
||||
seen_last_week_unregistered,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(device: Device) -> Result<(), DbError> {
|
||||
let conn = db::get_db_connection();
|
||||
match conn.execute(
|
||||
@@ -432,6 +473,59 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_summary() {
|
||||
tests_common::setup().await;
|
||||
|
||||
// Insert a registered device seen now
|
||||
insert(Device {
|
||||
mac_address: "su:mm:ar:y1:01:01".to_string(),
|
||||
ipv4_address: "192.168.50.1".to_string(),
|
||||
vendor: "Test".to_string(),
|
||||
last_seen: Utc::now(),
|
||||
is_registered: true,
|
||||
owner: "Test".to_string(),
|
||||
device_type: "Server".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Insert an unregistered device seen now
|
||||
insert(Device {
|
||||
mac_address: "su:mm:ar:y1:02:02".to_string(),
|
||||
ipv4_address: "192.168.50.2".to_string(),
|
||||
vendor: "Test".to_string(),
|
||||
last_seen: Utc::now(),
|
||||
is_registered: false,
|
||||
owner: "".to_string(),
|
||||
device_type: "".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let summary = get_summary().unwrap();
|
||||
|
||||
// bb and cc from seed data are registered, plus our new one
|
||||
assert!(
|
||||
summary.total_registered >= 3,
|
||||
"Should have at least 3 registered devices"
|
||||
);
|
||||
assert!(
|
||||
summary.seen_last_day_registered >= 1,
|
||||
"Should have at least 1 registered device seen in last day"
|
||||
);
|
||||
assert!(
|
||||
summary.seen_last_day_unregistered >= 1,
|
||||
"Should have at least 1 unregistered device seen in last day"
|
||||
);
|
||||
assert!(
|
||||
summary.seen_last_week_registered >= 1,
|
||||
"Should have at least 1 registered device seen in last week"
|
||||
);
|
||||
assert!(
|
||||
summary.seen_last_week_unregistered >= 1,
|
||||
"Should have at least 1 unregistered device seen in last week"
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_device(
|
||||
device: Device,
|
||||
mac_address: String,
|
||||
|
||||
@@ -4,6 +4,15 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct DeviceSummary {
|
||||
pub total_registered: i64,
|
||||
pub seen_last_day_registered: i64,
|
||||
pub seen_last_day_unregistered: i64,
|
||||
pub seen_last_week_registered: i64,
|
||||
pub seen_last_week_unregistered: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Device {
|
||||
pub mac_address: String,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::error::Error;
|
||||
|
||||
use crate::model::device_events::{DeviceEvent, DeviceEventType};
|
||||
use crate::model::devices::Device;
|
||||
use crate::model::devices::{Device, DeviceSummary};
|
||||
use crate::model::notifications::{Notification, NotificationType};
|
||||
use crate::settings::get_settings;
|
||||
use crate::web_server::arp_scanner::ArpScannerStatusResponse;
|
||||
@@ -38,6 +38,7 @@ pub mod utils;
|
||||
paths(
|
||||
test_api,
|
||||
devices::list,
|
||||
devices::summary,
|
||||
devices::read,
|
||||
devices::register,
|
||||
devices::unregister,
|
||||
@@ -51,6 +52,7 @@ pub mod utils;
|
||||
),
|
||||
components(schemas(
|
||||
Device,
|
||||
DeviceSummary,
|
||||
Notification,
|
||||
NotificationType,
|
||||
RegisterDevicePayload,
|
||||
@@ -103,6 +105,7 @@ pub async fn serve() -> Result<(), Box<dyn Error>> {
|
||||
.route("/api/test", get(test_api))
|
||||
.route("/api/devices", get(devices::list))
|
||||
.route("/api/devices", put(devices::register))
|
||||
.route("/api/devices/summary", get(devices::summary))
|
||||
.route("/api/devices/{mac_address}", delete(devices::unregister))
|
||||
.route("/api/devices/{mac_address}", get(devices::read))
|
||||
.route("/api/devices/{mac_address}/events", get(device_events::list))
|
||||
|
||||
@@ -8,7 +8,7 @@ use log::{debug, error};
|
||||
use serde::Deserialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{db, model::devices::Device};
|
||||
use crate::{db, model::devices::{Device, DeviceSummary}};
|
||||
|
||||
use crate::web_server::utils;
|
||||
|
||||
@@ -177,6 +177,26 @@ pub async fn unregister(Path(mac_address): Path<String>) -> impl IntoResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/devices/summary",
|
||||
tag = "devices",
|
||||
responses(
|
||||
(status = 200, description = "Device summary counts", body = DeviceSummary),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn summary() -> Result<Json<DeviceSummary>, StatusCode> {
|
||||
match db::devices::get_summary() {
|
||||
Ok(value) => Ok(Json(value)),
|
||||
Err(err) => {
|
||||
error!("Error getting device summary: {}", err);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Payload structs
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct RegisterDevicePayload {
|
||||
|
||||
Reference in New Issue
Block a user