diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx
index cfc9ac9..905abca 100644
--- a/frontend/src/pages/DashboardPage.tsx
+++ b/frontend/src/pages/DashboardPage.tsx
@@ -108,17 +108,22 @@ const StatCard = memo(function StatCard({
sub,
accentColor,
icon,
+ to,
}: {
label: string;
value: React.ReactNode;
sub?: string;
accentColor: string;
icon: React.ReactNode;
+ to?: string;
}) {
- return (
+ const inner = (
@@ -140,6 +145,7 @@ const StatCard = memo(function StatCard({
);
+ return to ?
{inner} : inner;
});
export function DashboardPage() {
@@ -208,6 +214,7 @@ export function DashboardPage() {
value={devices.length}
accentColor={STRIPE.devices}
icon={
}
+ to="/devices"
/>
}
+ to="/risks?severity=critical"
/>
}
+ to="/risks?severity=high"
/>
}
+ to="/risks"
/>
diff --git a/frontend/src/pages/DeviceDetailPage.tsx b/frontend/src/pages/DeviceDetailPage.tsx
index dd3496f..17d6e9e 100644
--- a/frontend/src/pages/DeviceDetailPage.tsx
+++ b/frontend/src/pages/DeviceDetailPage.tsx
@@ -2,7 +2,7 @@
* DeviceDetailPage — ports/services table, risk list, timestamps.
* Route: /devices/:id
*/
-import { memo } from "react";
+import { memo, useMemo } from "react";
import { Link, useParams } from "react-router-dom";
import { Card, Badge, SkeletonCard, PageHeader } from "../components";
import { useDevice, useRisks, useDeviceRecommendations } from "../hooks";
@@ -49,6 +49,30 @@ const RecCard = memo(function RecCard({ rec }: { rec: Recommendation }) {
);
});
+/** A risk paired with its linked recommendation (if any). */
+const RiskRecPair = memo(function RiskRecPair({
+ risk,
+ rec,
+}: {
+ risk: Risk;
+ rec: Recommendation | undefined;
+}) {
+ return (
+
+
+ {rec ? (
+
+ ) : (
+
+
+ No remediation available for this risk.
+
+
+ )}
+
+ );
+});
+
export function DeviceDetailPage() {
const { id } = useParams<{ id: string }>();
const deviceId = Number(id);
@@ -57,6 +81,20 @@ export function DeviceDetailPage() {
const { recommendations, loading: recsLoading } =
useDeviceRecommendations(deviceId);
+ // Build a map of risk_id → recommendation for O(1) lookup
+ const recByRiskId = useMemo(() => {
+ const map = new Map();
+ recommendations.forEach((r) => map.set(r.risk_id, r));
+ return map;
+ }, [recommendations]);
+
+ // Recommendations not linked to any risk shown in the list
+ const linkedRiskIds = useMemo(() => new Set(risks.map((r) => r.id)), [risks]);
+ const orphanRecs = useMemo(
+ () => recommendations.filter((r) => !linkedRiskIds.has(r.risk_id)),
+ [recommendations, linkedRiskIds],
+ );
+
if (loading) return ;
if (error)
return Error: {error}
;
@@ -169,61 +207,59 @@ export function DeviceDetailPage() {
)}
- {/* Risks */}
-
- Risks{" "}
- {!risksLoading && (
-
- ({sortedRisks.length})
+ {/* Risks + Remediations paired side by side */}
+
+
Risks & Remediations
+ {!risksLoading && !recsLoading && (
+
+ {sortedRisks.length} risk{sortedRisks.length !== 1 ? "s" : ""}
)}
-
- {risksLoading ? (
-
- {Array.from({ length: 2 }).map((_, i) => (
-
- ))}
-
- ) : sortedRisks.length === 0 ? (
-
-
- No risks detected for this device.
-
-
- ) : (
-
- {sortedRisks.map((risk) => (
-
- ))}
-
- )}
+
- {/* Recommendations */}
-
- Recommendations{" "}
- {!recsLoading && (
-
- ({recommendations.length})
-
- )}
-
- {recsLoading ? (
+ {risksLoading || recsLoading ? (
{Array.from({ length: 2 }).map((_, i) => (
))}
- ) : recommendations.length === 0 ? (
+ ) : sortedRisks.length === 0 && orphanRecs.length === 0 ? (
- No recommendations for this device.
+ No risks or recommendations for this device.
) : (
-
- {recommendations.map((rec) => (
-
+
+ {/* Header row for desktop */}
+ {sortedRisks.length > 0 && (
+
+
+ Risk
+
+
+ Remediation
+
+
+ )}
+ {sortedRisks.map((risk) => (
+
))}
+ {/* Orphan recommendations not linked to any discovered risk */}
+ {orphanRecs.length > 0 && (
+ <>
+
+ Additional Recommendations
+
+ {orphanRecs.map((rec) => (
+
+ ))}
+ >
+ )}
)}