diff --git a/web/app/(app)/vulnerabilities/page.tsx b/web/app/(app)/vulnerabilities/page.tsx
index 6f15535..1ed48d3 100644
--- a/web/app/(app)/vulnerabilities/page.tsx
+++ b/web/app/(app)/vulnerabilities/page.tsx
@@ -4,7 +4,7 @@ import { useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api, vulnerabilities, type FindingState, type Severity, type VulnFinding } from "@/lib/api";
import { useAuth } from "@/components/AuthProvider";
-import { Button } from "@/components/ui";
+import { Button, Card } from "@/components/ui";
import { AcceptDialog } from "@/components/vulnerabilities/AcceptDialog";
import { DBFreshness } from "@/components/vulnerabilities/DBFreshness";
import { FindingRow } from "@/components/vulnerabilities/FindingRow";
@@ -78,12 +78,12 @@ export default function VulnerabilitiesPage() {
const total = SEVERITY_ORDER.reduce((n, s) => n + (counts[s] ?? 0), 0);
return (
-
-
+
+
-
Vulnerabilities
+
Vulnerabilities
- Installed packages matched against distribution security advisories.
+ {total} open finding{total !== 1 ? "s" : ""} · installed packages matched against distribution security advisories
{isAdmin && (
@@ -93,32 +93,36 @@ export default function VulnerabilitiesPage() {
)}
-
+
+
+
-
+ {/* Severity counts double as the filter. They are always the whole
+ fleet's open counts, never the filtered view's, so switching
+ state cannot make the fleet look better than it is. */}
+
{SEVERITY_ORDER.map((s) => (
setSeverity(severity === s ? "" : s)}
- className={`flex items-center gap-2 rounded px-2 py-1 text-left transition-colors ${
- severity === s ? "bg-surface-2" : "hover:bg-surface-2"
+ aria-pressed={severity === s}
+ className={`flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-left transition-colors ${
+ severity === s ? "border-accent bg-surface-2" : "border-transparent hover:bg-surface-2"
}`}
>
{counts[s] ?? 0}
))}
-
- {total} open
-
-
+
{STATES.map((s) => (
setState(s)}
- className={`rounded border px-3 py-1.5 text-sm capitalize transition-colors ${
+ aria-pressed={state === s}
+ className={`rounded-lg border px-3 py-1.5 text-sm capitalize transition-colors ${
state === s ? "border-accent text-accent" : "border-border text-text-secondary hover:text-text-primary"
}`}
>
@@ -127,33 +131,42 @@ export default function VulnerabilitiesPage() {
))}
- {groups.isLoading &&
Loading…
}
- {groups.error &&
{(groups.error as Error).message}
}
-
- {groups.data && groups.data.length === 0 && (
-
-
No {state} findings.
-
- Servers report packages hourly. A server whose distribution has no advisory feed is reported as unsupported on its own
- page rather than counted here.
-
+ {groups.error && (
+
+ {(groups.error as Error).message}
)}
-
- {groups.data?.map((g) => (
- unaccept.mutate(f.id)}
- onApplyUpdates={(serverId) => applyUpdates.mutate(serverId)}
- applying={applyUpdates.isPending ? (applyUpdates.variables as string) : undefined}
- />
- ))}
-
+
+ {groups.isLoading ? (
+
+ ) : groups.data && groups.data.length > 0 ? (
+ groups.data.map((g) => (
+ unaccept.mutate(f.id)}
+ onApplyUpdates={(serverId) => applyUpdates.mutate(serverId)}
+ applying={applyUpdates.isPending ? (applyUpdates.variables as string) : undefined}
+ />
+ ))
+ ) : (
+
+
+ No {state} findings{severity ? ` at ${severity} severity` : ""}.
+
+
+ Servers report their packages hourly. A server whose distribution has no advisory feed is reported as unsupported
+ on its own page rather than counted as clean here.
+
+
+ )}
+
{accepting && (
f.fixed_in);
return (
-
+ // A row inside the page's one bordered container, not a card of its
+ // own — the same stack idiom as the monitors and workflows lists.
+
setOpen((v) => !v)}
- className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-surface-2"
+ className="flex w-full items-center gap-3 px-4 py-3.5 text-left transition-colors hover:bg-surface-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent sm:px-5"
aria-expanded={open}
>
{open ? "▾" : "▸"}
{group.cve_id}
{group.title && {group.title} }
-
+
{group.server_count} {group.server_count === 1 ? "server" : "servers"}
- {!anyFix && no fix published }
+ {!anyFix && (
+
+ no fix
+
+ )}
{open && (
-
+
{group.findings.map((f) => (
-
+
{serverName(f.server_id)}