diff --git a/web/app/(app)/servers/[id]/page.tsx b/web/app/(app)/servers/[id]/page.tsx index 36be8ea..af34417 100644 --- a/web/app/(app)/servers/[id]/page.tsx +++ b/web/app/(app)/servers/[id]/page.tsx @@ -130,7 +130,13 @@ export default function ServerDetailPage() { const { mutate: applyUpdates, isPending: isApplying } = useMutation({ mutationFn: () => api.applyUpdates(serverId), - onSuccess: () => toast.success("Update command sent. Patching runs in the background and may take several minutes."), + onSuccess: (res) => { + if (res.run_id) { + router.push(`/patching/runs/${res.run_id}`); + } else { + toast.success("Update command sent."); + } + }, onError: toast.error, }); diff --git a/web/app/(app)/vulnerabilities/page.tsx b/web/app/(app)/vulnerabilities/page.tsx index 2da6ee3..c57ca0c 100644 --- a/web/app/(app)/vulnerabilities/page.tsx +++ b/web/app/(app)/vulnerabilities/page.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; import { api, vulnerabilities, type FindingState, type Severity, type VulnFinding } from "@/lib/api"; import { useAuth } from "@/components/AuthProvider"; import { Button, Card, Pagination, usePagination, useToast } from "@/components/ui"; @@ -42,6 +43,7 @@ const FIX_FILTERS: { key: string; label: string; hasFix: boolean | undefined }[] export default function VulnerabilitiesPage() { const { isAdmin } = useAuth(); const qc = useQueryClient(); + const router = useRouter(); const [state, setState] = useState("open"); const [severity, setSeverity] = useState(""); @@ -112,10 +114,10 @@ export default function VulnerabilitiesPage() { }); const applyUpdates = useMutation({ - mutationFn: (serverId: string) => api.applyUpdates(serverId), - // This one had no feedback of any kind: the button dispatched a patch - // run to a whole server and the page did not change in any way. - onSuccess: (_data, serverId) => toast.success(`Update command sent to ${serverName(serverId)}.`), + mutationFn: (serverId: string) => api.applyUpdates(serverId, "vulnerabilities"), + onSuccess: (res) => { + if (res.run_id) router.push(`/patching/runs/${res.run_id}`); + }, onError: toast.error, }); diff --git a/web/components/servers/tabs/MaintenanceTab.tsx b/web/components/servers/tabs/MaintenanceTab.tsx index 99c6e18..6e36712 100644 --- a/web/components/servers/tabs/MaintenanceTab.tsx +++ b/web/components/servers/tabs/MaintenanceTab.tsx @@ -1,8 +1,12 @@ "use client"; import { useState } from "react"; +import Link from "next/link"; +import { useQuery } from "@tanstack/react-query"; import { api, ServerWithKeys } from "@/lib/api"; import { Badge, Button, Card, ConfirmDialog, Table, Tbody, Td, Th, Thead, Tr } from "@/components/ui"; +import { matchesTags } from "@/lib/targets"; +import { RUN_STATUS } from "@/components/patching/status"; /* * Everything that changes what is installed on the machine: its OS packages, @@ -42,6 +46,17 @@ export function MaintenanceTab({ const isWindows = server.os_info?.toLowerCase().includes("windows"); const agentCurrent = !!latestVersion && !!server.agent_version && server.agent_version === latestVersion; + const { data: policies } = useQuery({ queryKey: ["patch-policies"], queryFn: () => api.listPatchPolicies() }); + const { data: windows } = useQuery({ queryKey: ["maintenance-windows"], queryFn: () => api.listMaintenanceWindows() }); + const { data: lastRuns } = useQuery({ queryKey: ["patch-runs", "server", server.server_id], queryFn: () => api.listPatchRuns({ server_id: server.server_id, limit: 1 }) }); + // Same selector rule as the scheduler: named, or carrying every tag. + const covering = (policies ?? []).filter((p) => p.enabled && (p.target_server_ids.includes(server.server_id) || matchesTags(server, p.target_tags ?? {}))); + const next = covering + .filter((p) => p.next_run_at) + .sort((a, b) => (a.next_run_at! < b.next_run_at! ? -1 : 1))[0]; + const nextWindow = next && windows?.find((w) => w.window_id === next.window_id); + const lastRun = lastRuns?.[0]; + return (
@@ -55,6 +70,24 @@ export function MaintenanceTab({
+
+ {next && nextWindow ? ( + + Covered by {next.name}, next window{" "} + {new Date(next.next_run_at!).toLocaleString(undefined, { timeZone: nextWindow.tz, dateStyle: "medium", timeStyle: "short" })} ({nextWindow.tz}) + + ) : ( + + Not covered by any patch policy. Set one up + + )} + {lastRun && ( + + Last run {RUN_STATUS[lastRun.status].label} + + )} +
+ {updates.length === 0 ? (

{isWindows ? "No pending Windows updates. The agent checks hourly." : "No pending package updates. The agent checks hourly."} @@ -94,7 +127,7 @@ export function MaintenanceTab({ -

Upgrade runs in the background and may take several minutes.

+

Installs all pending updates now, without rebooting, and records a run.

)}