diff --git a/backend/routes/vclMultiVertical.js b/backend/routes/vclMultiVertical.js
index 5450908..d73ef5b 100644
--- a/backend/routes/vclMultiVertical.js
+++ b/backend/routes/vclMultiVertical.js
@@ -796,16 +796,19 @@ function createVCLMultiVerticalRouter(upload) {
/**
* GET /vertical/:code/metric/:metricId/devices
* Returns the list of non-compliant devices for a specific vertical + metric.
+ * Optionally filters by team when the query parameter is provided.
*
* @method GET
* @route /vertical/:code/metric/:metricId/devices
* @param {string} code — vertical code (e.g., "NTS_AEO")
* @param {string} metricId — metric identifier (e.g., "VM-001")
+ * @query {string} [team] — optional team name to filter devices (e.g., "STEAM", "ACCESS-ENG")
*
* @response 200
* {
* vertical: string,
* metric_id: string,
+ * team: string|null,
* devices: Array<{
* hostname: string,
* ip_address: string,
@@ -828,19 +831,26 @@ function createVCLMultiVerticalRouter(upload) {
if (!metricId || metricId.length > 50) return res.status(400).json({ error: 'Invalid metric ID' });
try {
- const { rows } = await pool.query(
- `SELECT ci.hostname, ci.ip_address, ci.device_type, ci.team, ci.seen_count,
+ const team = req.query.team || null;
+ let query = `SELECT ci.hostname, ci.ip_address, ci.device_type, ci.team, ci.seen_count,
ci.resolution_date, ci.remediation_plan,
fu.report_date AS first_seen, lu.report_date AS last_seen
FROM compliance_items ci
LEFT JOIN compliance_uploads fu ON ci.first_seen_upload_id = fu.id
LEFT JOIN compliance_uploads lu ON ci.upload_id = lu.id
- WHERE ci.vertical = $1 AND ci.metric_id = $2 AND ci.status = 'active'
- ORDER BY ci.hostname`,
- [vertical, metricId]
- );
+ WHERE ci.vertical = $1 AND ci.metric_id = $2 AND ci.status = 'active'`;
+ const params = [vertical, metricId];
- res.json({ vertical, metric_id: metricId, devices: rows });
+ if (team) {
+ query += ` AND ci.team = $3`;
+ params.push(team);
+ }
+
+ query += ` ORDER BY ci.hostname`;
+
+ const { rows } = await pool.query(query, params);
+
+ res.json({ vertical, metric_id: metricId, team: team || null, devices: rows });
} catch (err) {
console.error('[VCL Multi] GET /vertical/:code/metric/:metricId/devices error:', err.message);
res.status(500).json({ error: 'Database error' });
diff --git a/docs/vcl-multi-vertical-design-brief.md b/docs/vcl-multi-vertical-design-brief.md
index ecc3a5f..75e133f 100644
--- a/docs/vcl-multi-vertical-design-brief.md
+++ b/docs/vcl-multi-vertical-design-brief.md
@@ -110,8 +110,15 @@ Executive Overview (all verticals aggregated)
│ │ │ ├── └ INTELDEV: 233 compliant, 11 NC, 244 total — 95.0%
│ │ │ └── └ STEAM: 123 compliant, 4 NC, 127 total — 97.0%
│ │ │
- │ │ ├── ▸ 7.1.1 (Logging & Monitoring) — 72.0% — 149 NC → click metric
- │ │ │ └── Device list: hostname, IP, type, team, seen_count, resolution_date
+ │ │ ├── Click metric ID → Metric Sub-Team View
+ │ │ │ ├── Stats: total 66,176 | compliant 64,414 | NC 1,762 | 97% | target 80%
+ │ │ │ └── Sub-Team Table:
+ │ │ │ ├── ACCESS-ENG — 8 total — 88.0% → click
+ │ │ │ │ └── Device list (filtered to ACCESS-ENG)
+ │ │ │ ├── ACCESS-OPS — 65,797 total — 97.0% → click
+ │ │ │ │ └── Device list (filtered to ACCESS-OPS)
+ │ │ │ ├── INTELDEV — 244 total — 95.0% → click
+ │ │ │ └── STEAM — 127 total — 97.0% → click
│ │ └── ...
│ │
│ └── Burndown: blockers, with dates, projected clear date
@@ -197,7 +204,7 @@ Some metrics have a team value of `(Other)` in the Summary sheet. This represent
### Device-Level Drill-Down
-Clicking a metric ID navigates to the device list — individual non-compliant hostnames for that vertical + metric combination. This data comes from the detail sheets (not the Summary sheet) and shows:
+Clicking a sub-team row in the metric sub-team view navigates to the device list — individual non-compliant hostnames for that vertical + metric + team combination. The device list is filtered to only show devices belonging to the selected team. This data comes from the detail sheets (not the Summary sheet) and shows:
- Hostname, IP address, device type, team
- Seen count (how many consecutive uploads this device has been non-compliant)
@@ -205,6 +212,14 @@ Clicking a metric ID navigates to the device list — individual non-compliant h
- Resolution date (if set)
- Remediation plan (if documented)
+If a metric has no sub-team breakdown (e.g., only an "(Other)" team), a "View All Devices" button is shown instead, which loads the full unfiltered device list for that metric.
+
+The full navigation path is:
+
+```
+Overview → Vertical → Metric (sub-team totals) → Team (device list)
+```
+
---
## Burndown Forecast
diff --git a/frontend/src/components/pages/CCPMetricsPage.js b/frontend/src/components/pages/CCPMetricsPage.js
index 2ac5fa6..df6deb2 100644
--- a/frontend/src/components/pages/CCPMetricsPage.js
+++ b/frontend/src/components/pages/CCPMetricsPage.js
@@ -417,7 +417,7 @@ function VerticalDetailView({ vertical, onBack, onSelectMetric }) {
onSelectMetric(m.metric_id)}
+ onClick={() => onSelectMetric(m.metric_id, m)}
>
{m.metric_id}
@@ -461,22 +461,17 @@ function VerticalDetailView({ vertical, onBack, onSelectMetric }) {
}
// ---------------------------------------------------------------------------
-// Metric Device List (deepest drill-down)
+// Metric Sub-Team View (intermediate drill-down: metric → sub-teams)
// ---------------------------------------------------------------------------
-function MetricDeviceList({ vertical, metricId, onBack }) {
- const [devices, setDevices] = useState(null);
- const [loading, setLoading] = useState(true);
- // ⚠️ CONVENTION: Missing error state — .catch() silently swallows errors without displaying them to the user. Add an error state and render an error message (see main CCPMetricsPage pattern).
+function MetricSubTeamView({ vertical, metricId, metricData, onBack, onSelectTeam }) {
+ // metricData contains the metric's sub_teams from the parent view
+ const pctColor = (pct, target) => {
+ const p = Number(pct || 0);
+ const t = Number(target || 0);
+ return p >= t ? '#10B981' : p >= t * 0.85 ? '#F59E0B' : '#EF4444';
+ };
- useEffect(() => {
- setLoading(true);
- fetch(`${API_BASE}/compliance/vcl-multi/vertical/${encodeURIComponent(vertical)}/metric/${encodeURIComponent(metricId)}/devices`, { credentials: 'include' })
- .then(r => r.json())
- .then(data => { setDevices(data.devices || []); setLoading(false); })
- .catch(() => setLoading(false));
- }, [vertical, metricId]);
-
- if (loading) return Loading...
;
+ const targetVal = Number(metricData?.target || 0);
return (
@@ -487,8 +482,126 @@ function MetricDeviceList({ vertical, metricId, onBack }) {
Back to Metrics
+
+ {vertical} / Metric {metricId}
+
+ {metricData?.metric_desc && (
+
{metricData.metric_desc}
+ )}
+
+ {/* Metric rollup stats */}
+
+
+
Total
+
{(metricData?.total || 0).toLocaleString()}
+
+
+
Compliant
+
{(metricData?.compliant || 0).toLocaleString()}
+
+
+
Non-Compliant
+
{(metricData?.non_compliant || 0).toLocaleString()}
+
+
+
Compliance
+
+ {(Number(metricData?.compliance_pct || 0) * 100).toFixed(1)}%
+
+
+
+
Target
+
{(targetVal * 100).toFixed(0)}%
+
+
+
+ {/* Sub-team breakdown table */}
+
+
+ Sub-Team Breakdown
+
+ {metricData?.sub_teams && metricData.sub_teams.length > 0 ? (
+
+
+
+ Team
+ Compliant
+ Non-Compliant
+ Total
+ Compliance %
+
+
+
+ {metricData.sub_teams.map(st => {
+ const stPct = Number(st.compliance_pct || 0);
+ const stPctColor = pctColor(st.compliance_pct, metricData.target);
+ return (
+ onSelectTeam(st.team)}
+ style={{ cursor: 'pointer', transition: 'background 0.15s' }}
+ onMouseEnter={e => e.currentTarget.style.background = 'rgba(20, 184, 166, 0.08)'}
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
+ >
+ {st.team}
+ {(st.compliant || 0).toLocaleString()}
+ {(st.non_compliant || 0).toLocaleString()}
+ {(st.total || 0).toLocaleString()}
+ {(stPct * 100).toFixed(1)}%
+
+ );
+ })}
+
+
+ ) : (
+
+ No sub-team breakdown available for this metric.
+
+ onSelectTeam(null)}
+ style={{ padding: '0.5rem 1rem', background: `${PURPLE}20`, border: `1px solid ${PURPLE}60`, borderRadius: '0.5rem', color: PURPLE, fontSize: '0.75rem', cursor: 'pointer', fontWeight: '600' }}
+ >
+ View All Devices
+
+
+
+ )}
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Metric Device List (deepest drill-down — filtered by team)
+// ---------------------------------------------------------------------------
+function MetricDeviceList({ vertical, metricId, team, onBack }) {
+ const [devices, setDevices] = useState(null);
+ const [loading, setLoading] = useState(true);
+ // ⚠️ CONVENTION: Missing error state — .catch() below silently swallows errors without displaying them to the user. Add an error state and render an error message.
+
+ useEffect(() => {
+ setLoading(true);
+ let url = `${API_BASE}/compliance/vcl-multi/vertical/${encodeURIComponent(vertical)}/metric/${encodeURIComponent(metricId)}/devices`;
+ if (team) url += `?team=${encodeURIComponent(team)}`;
+ fetch(url, { credentials: 'include' })
+ .then(r => r.json())
+ .then(data => { setDevices(data.devices || []); setLoading(false); })
+ .catch(() => setLoading(false));
+ }, [vertical, metricId, team]);
+
+ if (loading) return Loading...
;
+
+ return (
+
+
+ Back to Sub-Teams
+
+
- {vertical} / Metric {metricId} — {devices ? devices.length : 0} non-compliant devices
+ {vertical} / Metric {metricId}{team ? ` / ${team}` : ''} — {devices ? devices.length : 0} non-compliant devices
@@ -611,6 +724,7 @@ function DataManagementPanel({ onClose, onDataChanged }) {
Manage Data
{/* ⚠️ CONVENTION: Use lucide-react icon instead of raw Unicode character */}
+ {/* ⚠️ CONVENTION: Use lucide-react icon instead of raw Unicode character */}
✕
@@ -706,6 +820,8 @@ export default function CCPMetricsPage() {
// Drill-down state
const [selectedVertical, setSelectedVertical] = useState(null);
const [selectedMetric, setSelectedMetric] = useState(null);
+ const [selectedMetricData, setSelectedMetricData] = useState(null);
+ const [selectedTeam, setSelectedTeam] = useState(null);
const fetchData = useCallback(() => {
setLoading(true);
@@ -731,13 +847,28 @@ export default function CCPMetricsPage() {
};
// Render drill-down views
- if (selectedMetric && selectedVertical) {
+ if (selectedTeam !== null && selectedMetric && selectedVertical) {
return (
setSelectedMetric(null)}
+ team={selectedTeam}
+ onBack={() => setSelectedTeam(null)}
+ />
+
+ );
+ }
+
+ if (selectedMetric && selectedVertical) {
+ return (
+
+ { setSelectedMetric(null); setSelectedMetricData(null); }}
+ onSelectTeam={(team) => setSelectedTeam(team)}
/>
);
@@ -749,7 +880,7 @@ export default function CCPMetricsPage() {
setSelectedVertical(null)}
- onSelectMetric={(metricId) => setSelectedMetric(metricId)}
+ onSelectMetric={(metricId, metricData) => { setSelectedMetric(metricId); setSelectedMetricData(metricData); }}
/>
);