From 1ba325d529e11e6d656d6897fc7a7b0ab7b86a71 Mon Sep 17 00:00:00 2001 From: Echo Date: Mon, 4 May 2026 18:56:52 +0000 Subject: [PATCH] feat: add patches missing filter and count indicator to deploy page --- crates/pm-core/src/models.rs | 1 + crates/pm-web/src/routes/hosts.rs | 16 ++++++++---- frontend/src/pages/PatchDeploymentPage.tsx | 30 ++++++++++++++++++++-- frontend/src/types/index.ts | 1 + 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/crates/pm-core/src/models.rs b/crates/pm-core/src/models.rs index 6b6e71f..da267b3 100644 --- a/crates/pm-core/src/models.rs +++ b/crates/pm-core/src/models.rs @@ -112,6 +112,7 @@ pub struct HostSummary { pub os_name: Option, pub health_status: HostHealthStatus, pub agent_version: Option, + pub patches_missing: i64, pub registered_at: DateTime, } diff --git a/crates/pm-web/src/routes/hosts.rs b/crates/pm-web/src/routes/hosts.rs index 049153f..7aeb72a 100644 --- a/crates/pm-web/src/routes/hosts.rs +++ b/crates/pm-web/src/routes/hosts.rs @@ -109,10 +109,13 @@ async fn list_hosts( let hosts: Vec = if auth.role.is_admin() { sqlx::query_as( r#" - SELECT id, fqdn, host(ip_address)::text AS ip_address, display_name, - os_family, os_name, health_status, agent_version, registered_at - FROM hosts - ORDER BY fqdn + SELECT h.id, h.fqdn, host(h.ip_address)::text AS ip_address, h.display_name, + h.os_family, h.os_name, h.health_status, h.agent_version, + COALESCE(hpd.patch_count, 0) AS patches_missing, + h.registered_at + FROM hosts h + LEFT JOIN host_patch_data hpd ON hpd.host_id = h.id + ORDER BY h.fqdn LIMIT $1 OFFSET $2 "#, ) @@ -125,8 +128,11 @@ async fn list_hosts( r#" SELECT DISTINCT h.id, h.fqdn, host(h.ip_address)::text AS ip_address, h.display_name, h.os_family, h.os_name, - h.health_status, h.agent_version, h.registered_at + h.health_status, h.agent_version, + COALESCE(hpd.patch_count, 0) AS patches_missing, + h.registered_at FROM hosts h + LEFT JOIN host_patch_data hpd ON hpd.host_id = h.id WHERE -- Hosts in operator's groups EXISTS ( diff --git a/frontend/src/pages/PatchDeploymentPage.tsx b/frontend/src/pages/PatchDeploymentPage.tsx index c4694c8..dcbbb93 100644 --- a/frontend/src/pages/PatchDeploymentPage.tsx +++ b/frontend/src/pages/PatchDeploymentPage.tsx @@ -52,6 +52,7 @@ export default function PatchDeploymentPage() { const [hostsError, setHostsError] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [healthFilter, setHealthFilter] = useState('') + const [patchesFilter, setPatchesFilter] = useState<'all' | 'missing' | 'uptodate'>('all') const [selectedIds, setSelectedIds] = useState>(new Set()) // Step 1 state @@ -89,7 +90,11 @@ export default function PatchDeploymentPage() { h.display_name.toLowerCase().includes(searchQuery.toLowerCase()) || h.fqdn.toLowerCase().includes(searchQuery.toLowerCase()) const matchesHealth = healthFilter === '' || h.health_status === healthFilter - return matchesSearch && matchesHealth + const matchesPatches = + patchesFilter === 'all' || + (patchesFilter === 'missing' && h.patches_missing > 0) || + (patchesFilter === 'uptodate' && h.patches_missing === 0) + return matchesSearch && matchesHealth && matchesPatches }) const handleToggleHost = (id: string) => { @@ -209,6 +214,19 @@ export default function PatchDeploymentPage() { + setPatchesFilter(e.target.value as 'all' | 'missing' | 'uptodate')} + SelectProps={{ native: true }} + sx={{ minWidth: 160 }} + > + + + + {hostsLoading ? ( @@ -238,13 +256,14 @@ export default function PatchDeploymentPage() { FQDN IP Address Health + Patches OS {filteredHosts.length === 0 ? ( - + No hosts found @@ -272,6 +291,13 @@ export default function PatchDeploymentPage() { + + 0 ? 'error' : 'success'} + size="small" + /> + {host.os_name ?? host.os_family ?? '—'} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 055689a..e7720a2 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -24,6 +24,7 @@ export interface Host { os_family?: string os_name?: string agent_version?: string + patches_missing: number registered_at: string }