diff --git a/frontend/src/App.js b/frontend/src/App.js
index b6c493d..4d8f369 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -179,6 +179,7 @@ export default function App() {
const [currentPage, setCurrentPage] = useState('home');
const [navOpen, setNavOpen] = useState(false);
const [calendarFilter, setCalendarFilter] = useState(null);
+ const [reportingExcFilter, setReportingExcFilter] = useState(null);
const [showAddCVE, setShowAddCVE] = useState(false);
const [showUserManagement, setShowUserManagement] = useState(false);
const [showAuditLog, setShowAuditLog] = useState(false);
@@ -963,8 +964,8 @@ export default function App() {
onClose={() => setNavOpen(false)}
currentPage={currentPage}
onNavigate={(page) => {
- // Clear calendar filter when navigating directly via the nav drawer
- if (page === 'reporting') setCalendarFilter(null);
+ // Clear contextual filters when navigating directly via the nav drawer
+ if (page === 'reporting') { setCalendarFilter(null); setReportingExcFilter(null); }
setCurrentPage(page);
}}
/>
@@ -1041,7 +1042,7 @@ export default function App() {
{/* Page content */}
- {currentPage === 'reporting' && }
+ {currentPage === 'reporting' && }
{currentPage === 'knowledge-base' && }
{currentPage === 'exports' && }
@@ -2332,16 +2333,23 @@ export default function App() {
>
{ticket.exc_number}
- {canWrite() && (
-
+
+
+ {canWrite() && (<>
-
- )}
+ >)}
+
{ticket.cve_id}
{ticket.vendor}
diff --git a/frontend/src/components/pages/ReportingPage.js b/frontend/src/components/pages/ReportingPage.js
index e956ab4..e41e966 100644
--- a/frontend/src/components/pages/ReportingPage.js
+++ b/frontend/src/components/pages/ReportingPage.js
@@ -117,6 +117,17 @@ function getExportVal(finding, key) {
}
}
+// ---------------------------------------------------------------------------
+// Action coverage classification — used by chart and filter
+// ---------------------------------------------------------------------------
+const EXC_PATTERN = /EXC-\d+/i;
+
+function classifyFinding(finding) {
+ if (finding.workflow != null) return 'fp';
+ if (EXC_PATTERN.test(finding.note || '')) return 'archer';
+ return 'pending';
+}
+
// ---------------------------------------------------------------------------
// Style helpers
// ---------------------------------------------------------------------------
@@ -265,19 +276,15 @@ function StatusDonut({ open, closed, loading }) {
}
// ---------------------------------------------------------------------------
-// SVG Donut Chart — FP# workflow state distribution
+// SVG Donut Chart — Action Coverage (FP Request | Archer Exception | Pending)
// ---------------------------------------------------------------------------
-const WF_STATE_DEFS = [
- { key: 'expired', label: 'Expired', color: '#EF4444' },
- { key: 'rejected', label: 'Rejected', color: '#F87171' },
- { key: 'reworked', label: 'Reworked', color: '#F59E0B' },
- { key: 'actionable', label: 'Actionable', color: '#FCD34D' },
- { key: 'requested', label: 'Requested', color: '#0EA5E9' },
- { key: 'approved', label: 'Approved', color: '#10B981' },
- { key: 'none', label: 'No FP#', color: '#334155' },
+const ACTION_DEFS = [
+ { key: 'fp', label: 'FP Request', color: '#0EA5E9' },
+ { key: 'archer', label: 'Archer Exception', color: '#F59E0B' },
+ { key: 'pending', label: 'Pending', color: '#EF4444' },
];
-function WorkflowDonut({ findings }) {
+function ActionCoverageDonut({ findings, activeSegment, onSegmentClick }) {
const SIZE = 180;
const CX = SIZE / 2;
const CY = SIZE / 2;
@@ -285,16 +292,12 @@ function WorkflowDonut({ findings }) {
const INNER = 48;
const counts = useMemo(() => {
- const map = Object.fromEntries(WF_STATE_DEFS.map((d) => [d.key, 0]));
- findings.forEach((f) => {
- const state = (f.workflow?.state || '').toLowerCase();
- if (state && state in map) map[state]++;
- else map.none++;
- });
+ const map = { fp: 0, archer: 0, pending: 0 };
+ findings.forEach((f) => { map[classifyFinding(f)]++; });
return map;
}, [findings]);
- const total = Object.values(counts).reduce((a, b) => a + b, 0);
+ const total = findings.length;
if (total === 0) {
return (
@@ -305,29 +308,35 @@ function WorkflowDonut({ findings }) {
}
let cursor = 0;
- const segments = WF_STATE_DEFS
- .map((def) => {
- const count = counts[def.key];
- if (!count) return null;
- const start = cursor;
- const end = cursor + (count / total) * 360;
- cursor = end;
- return { ...def, count, start, end };
- })
- .filter(Boolean);
+ const segments = ACTION_DEFS.map((def) => {
+ const count = counts[def.key];
+ const start = cursor;
+ const end = count > 0 ? cursor + (count / total) * 360 : cursor;
+ if (count > 0) cursor = end;
+ return { ...def, count, start, end };
+ });
+
+ const hasActive = !!activeSegment;
return (
- {/* Legend */}
-
- {segments.map((seg) => (
-
-
-
-
- {seg.label}
-
-
- {seg.count}
-
-
- ({((seg.count / total) * 100).toFixed(0)}%)
-
+ {/* Legend — always shows all 3 categories */}
+
+ {segments.map((seg) => {
+ const isActive = activeSegment === seg.key;
+ const dimmed = hasActive && !isActive;
+ return (
+
onSegmentClick(isActive ? null : seg.key)}
+ style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', opacity: dimmed ? 0.35 : 1, transition: 'opacity 0.2s' }}
+ >
+
+
+
+ {seg.label}
+
+
+ {seg.count}
+
+
+ ({total > 0 ? ((seg.count / total) * 100).toFixed(0) : 0}%)
+
+
-
- ))}
+ );
+ })}
+ {hasActive && (
+
+ )}
);
@@ -835,7 +860,7 @@ function TableCell({ colKey, finding }) {
// ---------------------------------------------------------------------------
// Main ReportingPage
// ---------------------------------------------------------------------------
-export default function ReportingPage({ filterDate }) {
+export default function ReportingPage({ filterDate, filterEXC }) {
const [findings, setFindings] = useState([]);
const [total, setTotal] = useState(null);
const [syncedAt, setSyncedAt] = useState(null);
@@ -852,6 +877,8 @@ export default function ReportingPage({ filterDate }) {
);
const [openFilter, setOpenFilter] = useState(null);
const filterBtnRefs = useRef({});
+ const [actionFilter, setActionFilter] = useState(null);
+ const [excFilter, setExcFilter] = useState(filterEXC || null);
const updateColumns = useCallback((newOrder) => {
setColumnOrder(newOrder);
@@ -925,22 +952,38 @@ export default function ReportingPage({ filterDate }) {
});
}, []);
- // Apply all active column filters to produce the visible row set
+ // Apply all active filters to produce the visible row set
const filtered = useMemo(() => {
+ let result = findings;
+
+ // Column filters
const active = Object.entries(columnFilters);
- if (active.length === 0) return findings;
- return findings.filter((f) =>
- active.every(([key, vals]) => {
- if (!vals || vals.size === 0) return false;
- const def = COLUMN_DEFS[key];
- if (def?.multiValue) {
- // Row matches if ANY of its values is in the selected set
- return (f[key] || []).some((v) => vals.has(String(v).trim()));
- }
- return vals.has(getFilterVal(f, key).trim());
- })
- );
- }, [findings, columnFilters]);
+ if (active.length > 0) {
+ result = result.filter((f) =>
+ active.every(([key, vals]) => {
+ if (!vals || vals.size === 0) return false;
+ const def = COLUMN_DEFS[key];
+ if (def?.multiValue) {
+ return (f[key] || []).some((v) => vals.has(String(v).trim()));
+ }
+ return vals.has(getFilterVal(f, key).trim());
+ })
+ );
+ }
+
+ // Action coverage filter (chart segment click)
+ if (actionFilter) {
+ result = result.filter((f) => classifyFinding(f) === actionFilter);
+ }
+
+ // EXC filter (navigated from home page Archer ticket)
+ if (excFilter) {
+ const upper = excFilter.toUpperCase();
+ result = result.filter((f) => (f.note || '').toUpperCase().includes(upper));
+ }
+
+ return result;
+ }, [findings, columnFilters, actionFilter, excFilter]);
// Visible columns in current order
const visibleCols = columnOrder.filter((c) => c.visible && COLUMN_DEFS[c.key]);
@@ -966,7 +1009,7 @@ export default function ReportingPage({ filterDate }) {
);
};
- const activeFilterCount = Object.keys(columnFilters).length;
+ const activeFilterCount = Object.keys(columnFilters).length + (actionFilter ? 1 : 0) + (excFilter ? 1 : 0);
const [exportMenuOpen, setExportMenuOpen] = useState(false);
const exportBtnRef = useRef(null);
@@ -1073,12 +1116,20 @@ export default function ReportingPage({ filterDate }) {
{/* Divider */}
- {/* FP# Workflow state donut */}
+ {/* Action Coverage donut */}
- FP# Workflow Status
+ Action Coverage
+ {actionFilter && d.key === actionFilter)?.color, fontSize: '0.6rem' }}>● filtered}
-
+
{
+ setExcFilter(null);
+ setActionFilter(key);
+ }}
+ />
@@ -1117,7 +1168,46 @@ export default function ReportingPage({ filterDate }) {
{/* Action buttons */}
- {activeFilterCount > 0 && (
+ {/* EXC filter badge (from home page navigation) */}
+ {excFilter && (
+
+ )}
+ {/* Action coverage filter badge (from chart click) */}
+ {actionFilter && (
+
+ )}
+ {Object.keys(columnFilters).length > 0 && (