diff --git a/backend/routes/ivantiTodoQueue.js b/backend/routes/ivantiTodoQueue.js
index 6b83f18..e18395b 100644
--- a/backend/routes/ivantiTodoQueue.js
+++ b/backend/routes/ivantiTodoQueue.js
@@ -42,15 +42,20 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
if (!finding_id || typeof finding_id !== 'string' || finding_id.trim().length === 0) {
return res.status(400).json({ error: 'finding_id is required.' });
}
- if (!isValidVendor(vendor)) {
- return res.status(400).json({ error: 'vendor is required (max 200 chars).' });
- }
if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) {
- return res.status(400).json({ error: 'workflow_type must be FP or Archer.' });
+ return res.status(400).json({ error: 'workflow_type must be FP, Archer, or CARD.' });
+ }
+ // Vendor is required for FP and Archer, optional for CARD
+ if (workflow_type !== 'CARD' && !isValidVendor(vendor)) {
+ return res.status(400).json({ error: 'vendor is required for FP and Archer workflows.' });
+ }
+ if (vendor !== undefined && vendor !== '' && !isValidVendor(vendor)) {
+ return res.status(400).json({ error: 'vendor must be under 200 chars.' });
}
- const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null;
- const title = finding_title && typeof finding_title === 'string'
+ const vendorVal = workflow_type === 'CARD' ? '' : vendor.trim();
+ const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null;
+ const title = finding_title && typeof finding_title === 'string'
? finding_title.slice(0, 500)
: null;
@@ -58,7 +63,7 @@ function createIvantiTodoQueueRouter(db, requireAuth) {
`INSERT INTO ivanti_todo_queue
(user_id, finding_id, finding_title, cves_json, vendor, workflow_type)
VALUES (?, ?, ?, ?, ?, ?)`,
- [req.user.id, finding_id.trim(), title, cvesJson, vendor.trim(), workflow_type],
+ [req.user.id, finding_id.trim(), title, cvesJson, vendorVal, workflow_type],
function (err) {
if (err) {
console.error('Error adding to queue:', err);
diff --git a/frontend/src/components/pages/ReportingPage.js b/frontend/src/components/pages/ReportingPage.js
index d1904ee..0b32740 100644
--- a/frontend/src/components/pages/ReportingPage.js
+++ b/frontend/src/components/pages/ReportingPage.js
@@ -1111,7 +1111,8 @@ function AddToQueuePopover({ finding, anchorRect, queueForm, setQueueForm, onAdd
return () => document.removeEventListener('keydown', handler);
}, [onCancel]);
- const canSubmit = queueForm.vendor.trim().length > 0;
+ const isCard = queueForm.workflowType === 'CARD';
+ const canSubmit = isCard || queueForm.vendor.trim().length > 0;
return ReactDOM.createPortal(
- {/* Vendor input */}
-
+ {/* Vendor input — hidden for CARD */}
+ {isCard ? (
+
+ No vendor required — disposition handled in CARD
+
+ ) : (
+
+ )}
{/* Workflow type toggle */}
@@ -1232,15 +1245,24 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted
const pendingCount = items.filter((i) => i.status === 'pending').length;
const completedCount = items.filter((i) => i.status === 'complete').length;
- // Group by vendor, sorted alphabetically
+ // CARD items are their own top section; everything else groups by vendor
const grouped = useMemo(() => {
+ const cardItems = items.filter((i) => i.workflow_type === 'CARD');
+ const otherItems = items.filter((i) => i.workflow_type !== 'CARD');
+
const map = {};
- items.forEach((item) => {
+ otherItems.forEach((item) => {
const v = item.vendor || 'Unknown';
if (!map[v]) map[v] = [];
map[v].push(item);
});
- return Object.keys(map).sort().map((vendor) => ({ vendor, items: map[vendor] }));
+ const vendorGroups = Object.keys(map).sort().map((vendor) => ({
+ key: vendor, label: vendor, items: map[vendor], isCard: false,
+ }));
+
+ return cardItems.length > 0
+ ? [{ key: '__CARD__', label: 'CARD', items: cardItems, isCard: true }, ...vendorGroups]
+ : vendorGroups;
}, [items]);
return (
@@ -1313,24 +1335,24 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted
Check a row in the findings table to add it.
- ) : grouped.map(({ vendor, items: vendorItems }) => (
-
- {/* Vendor group header */}
+ ) : grouped.map(({ key, label, items: groupItems, isCard }) => (
+
+ {/* Group header */}
-
- {vendor}
+
+ {label}
- {vendorItems.length}
+ {groupItems.length}
{/* Items */}
- {vendorItems.map((item) => {
+ {groupItems.map((item) => {
const done = item.status === 'complete';
const wfColor = item.workflow_type === 'FP' ? { col: '#F59E0B', rgb: '245,158,11' }
: item.workflow_type === 'Archer' ? { col: '#0EA5E9', rgb: '14,165,233' }