feat(devices): add rename and forget controls and clarify fleet labels

This commit is contained in:
lda committed 2026-10-10 17:28:44 +07:00 Verified
1 parent acecb57b50
commit 7caba52e57
15 files changed
+411 -21

No files matched your search

@@ -25,6 +25,38 @@ pub struct DeviceIdentifierRequest {
pub value: String,
}
#[derive(Debug, Deserialize)]
pub struct RenameKnownDeviceRequest {
pub display_name: String,
}
pub async fn rename_known_device(
State(state): State<AppState>,
AxumPath(device_id): AxumPath<String>,
Json(req): Json<RenameKnownDeviceRequest>,
) -> Result<impl IntoResponse, ApiError> {
match state
.store
.rename_known_device(&device_id, &req.display_name)
.await
{
Ok(Some(device)) => Ok((StatusCode::OK, Json(known_device_response(device)))),
Ok(None) => Err(ApiError::new(
StatusCode::NOT_FOUND,
"known_device_not_found",
"known device not found",
)),
Err(err) => {
warn!(error = %err, "failed to rename known device");
Err(ApiError::new(
StatusCode::BAD_REQUEST,
"rename_known_device_failed",
err.to_string(),
))
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KnownDeviceResponse {
pub device_id: String,
@@ -26,6 +26,7 @@ pub(crate) struct AgentRuntimeStatus {
struct FleetAccumulator {
device_key: String,
display_name: Option<String>,
label_ip: Option<IpAddr>,
known_device: Option<KnownDeviceSummary>,
pinned: bool,
ips: BTreeSet<IpAddr>,
@@ -45,6 +46,7 @@ impl Default for FleetAccumulator {
Self {
device_key: String::new(),
display_name: None,
label_ip: None,
known_device: None,
pinned: false,
ips: BTreeSet::new(),
@@ -55,7 +57,8 @@ impl Default for FleetAccumulator {
endpoints: Vec::new(),
first_seen_unix: None,
last_seen_unix: None,
presence: Presence::Offline,
// Start at the lowest signal so unknown-only evidence stays unknown.
presence: Presence::Unknown,
routes: BTreeMap::new(),
}
}
@@ -194,6 +197,20 @@ fn add_agent_device_to_entry(
agent_device: AgentDeviceWithChildren,
context: &FleetBuildContext,
) {
// Retain address evidence for naming only, without promoting stale IPs into
// the active summary or wake-route candidates.
let label_ip = agent_device
.ips
.iter()
.copied()
.chain(
agent_device
.endpoints
.iter()
.filter_map(|endpoint| endpoint.key.ip),
)
.min();
entry.label_ip = entry.label_ip.into_iter().chain(label_ip).min();
let device_offline = agent_device.device.presence() == Presence::Offline;
if let Some(summary) = device_known_device(&agent_device, context)
&& entry.known_device.is_none()
@@ -473,8 +490,38 @@ impl FleetAccumulator {
let display_name = self
.display_name
.or_else(|| self.hostnames.iter().next().cloned())
.or_else(|| self.macs.iter().next().map(|mac| mac.to_string()))
.or_else(|| self.ips.iter().next().map(|ip| ip.to_string()))
.or_else(|| {
// Summary IPs omit offline endpoints; labels must not discard
// their address evidence or imply that unknown means offline.
let address = self
.ips
.iter()
.next()
.copied()
.or(self.label_ip)
.or_else(|| {
self.endpoints
.iter()
.filter_map(|endpoint| endpoint.ip)
.min()
})
.map(|ip| ip.to_string())
.or_else(|| self.macs.iter().next().map(|mac| mac.to_string()))
.or_else(|| {
self.endpoints
.iter()
.find_map(|endpoint| endpoint.mac)
.map(|mac| mac.to_string())
});
address.map(|address| {
let prefix = if self.presence == Presence::Offline {
"Offline device"
} else {
"Device"
};
format!("{prefix}: {address}")
})
})
.unwrap_or_else(|| "(unknown device)".to_string());
FleetDevice {
@@ -447,11 +447,48 @@ fn unknown_ip_only_offline_device_is_hidden_by_default() {
);
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].display_name, "(unknown device)");
assert_eq!(devices[0].display_name, "Offline device: 192.168.1.2");
filter_fleet_devices(&mut devices, &ListFleetDevicesQuery::default());
assert!(devices.is_empty());
}
#[test]
fn unnamed_endpoint_labels_preserve_presence_and_inactive_ips() {
for (presence, mac, ip, expected) in [
(
Presence::Offline,
None,
Some("192.168.1.2"),
"Offline device: 192.168.1.2",
),
(
Presence::Unknown,
None,
Some("192.168.1.2"),
"Device: 192.168.1.2",
),
(
Presence::Offline,
Some("aa:bb:cc:dd:ee:01"),
None,
"Offline device: AA:BB:CC:DD:EE:01",
),
] {
let mut row = offline_ip_only_unknown("agent-a", "test", "192.168.1.2", 20);
row.ips.clear();
row.device.presence = presence.as_str().into();
let mut evidence = endpoint_with_presence(EndpointSource::Neighbor, mac, ip, presence, 20);
evidence.hostname = None;
row.endpoints = vec![evidence];
let devices = build_fleet_devices(Vec::new(), vec![row], &context(&["agent-a"]));
assert_eq!(devices[0].display_name, expected);
assert_eq!(devices[0].presence, presence);
if presence == Presence::Offline {
assert!(devices[0].ips.is_empty());
}
}
}
#[test]
fn visibility_all_keeps_unknown_ip_only_offline_device() {
let mut devices = build_fleet_devices(
@@ -473,7 +510,7 @@ fn visibility_all_keeps_unknown_ip_only_offline_device() {
},
);
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].display_name, "(unknown device)");
assert_eq!(devices[0].display_name, "Offline device: 192.168.1.2");
}
#[test]