feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
// routes/ivantiTodoQueue.js
|
|
|
|
|
|
const express = require('express');
|
2026-05-06 11:44:17 -06:00
|
|
|
|
const pool = require('../db');
|
|
|
|
|
|
const { requireAuth, requireGroup } = require('../middleware/auth');
|
2026-04-09 09:49:40 -06:00
|
|
|
|
const logAudit = require('../helpers/auditLog');
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
const VALID_WORKFLOW_TYPES = ['FP', 'Archer', 'CARD', 'GRANITE', 'DECOM'];
|
|
|
|
|
|
const INVENTORY_TYPES = ['CARD', 'GRANITE', 'DECOM'];
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
const VALID_STATUSES = ['pending', 'complete'];
|
|
|
|
|
|
|
|
|
|
|
|
function isValidVendor(vendor) {
|
2026-04-07 10:23:10 -06:00
|
|
|
|
if (typeof vendor !== 'string') return false;
|
|
|
|
|
|
const trimmed = vendor.trim();
|
|
|
|
|
|
return trimmed.length > 0 && trimmed.length <= 200;
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 11:44:17 -06:00
|
|
|
|
function createIvantiTodoQueueRouter() {
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* GET /api/ivanti/todo-queue
|
|
|
|
|
|
*
|
|
|
|
|
|
* Returns all todo queue items belonging to the authenticated user.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @query None
|
|
|
|
|
|
* @returns {Array<Object>} Array of queue items with parsed `cves` array
|
|
|
|
|
|
* - id {number}
|
|
|
|
|
|
* - user_id {number}
|
|
|
|
|
|
* - finding_id {string}
|
|
|
|
|
|
* - finding_title {string|null}
|
|
|
|
|
|
* - cves {Array<string>}
|
|
|
|
|
|
* - ip_address {string|null}
|
|
|
|
|
|
* - hostname {string|null}
|
|
|
|
|
|
* - vendor {string}
|
|
|
|
|
|
* - workflow_type {string} One of: FP, Archer, CARD, GRANITE, DECOM
|
|
|
|
|
|
* - status {string} pending | complete
|
|
|
|
|
|
* - created_at {string}
|
|
|
|
|
|
* - updated_at {string}
|
|
|
|
|
|
*/
|
2026-05-06 11:44:17 -06:00
|
|
|
|
router.get('/', requireAuth(), async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { rows } = await pool.query(
|
|
|
|
|
|
`SELECT q.*
|
|
|
|
|
|
FROM ivanti_todo_queue q
|
|
|
|
|
|
WHERE q.user_id = $1
|
|
|
|
|
|
ORDER BY q.vendor ASC, q.created_at ASC`,
|
|
|
|
|
|
[req.user.id]
|
|
|
|
|
|
);
|
2026-05-15 17:31:19 -06:00
|
|
|
|
const parsed = rows.map((r) => {
|
|
|
|
|
|
let cves = [];
|
|
|
|
|
|
if (r.cves_json) {
|
|
|
|
|
|
try { cves = JSON.parse(r.cves_json); } catch (e) { cves = []; }
|
|
|
|
|
|
}
|
|
|
|
|
|
return { ...r, cves };
|
|
|
|
|
|
});
|
2026-05-06 11:44:17 -06:00
|
|
|
|
res.json(parsed);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error fetching todo queue:', err);
|
|
|
|
|
|
res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* POST /api/ivanti/todo-queue/batch
|
|
|
|
|
|
*
|
|
|
|
|
|
* Adds multiple findings to the authenticated user's todo queue in a single transaction.
|
|
|
|
|
|
* Requires Admin or Standard_User group.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @body {Object}
|
|
|
|
|
|
* - findings {Array<Object>} 1–200 items, each with:
|
|
|
|
|
|
* - finding_id {string} Required, non-empty
|
|
|
|
|
|
* - finding_title {string} Optional, max 500 chars
|
|
|
|
|
|
* - cves {Array<string>} Optional
|
|
|
|
|
|
* - ip_address {string} Optional, max 64 chars
|
|
|
|
|
|
* - hostname {string} Optional, max 255 chars
|
|
|
|
|
|
* - workflow_type {string} Required. One of: FP, Archer, CARD, GRANITE, DECOM
|
|
|
|
|
|
* - vendor {string} Required for FP, Archer, and DECOM workflows; max 200 chars
|
|
|
|
|
|
* @returns {Object} { items: Array<Object> } — inserted queue items with parsed `cves` array
|
|
|
|
|
|
* @error 400 Invalid input
|
|
|
|
|
|
* @error 500 Internal server error
|
|
|
|
|
|
*/
|
2026-05-06 11:44:17 -06:00
|
|
|
|
router.post('/batch', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
2026-04-09 09:49:40 -06:00
|
|
|
|
const { findings, workflow_type, vendor } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
if (!Array.isArray(findings) || findings.length < 1 || findings.length > 200) {
|
|
|
|
|
|
return res.status(400).json({ error: 'findings array must contain 1-200 items.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < findings.length; i++) {
|
|
|
|
|
|
const f = findings[i];
|
|
|
|
|
|
if (!f || typeof f.finding_id !== 'string' || f.finding_id.trim().length === 0) {
|
|
|
|
|
|
return res.status(400).json({ error: 'Each finding must have a non-empty finding_id string.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) {
|
2026-05-08 14:51:05 -06:00
|
|
|
|
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, GRANITE, or DECOM.' });
|
2026-04-09 09:49:40 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
if (!INVENTORY_TYPES.includes(workflow_type)) {
|
2026-04-09 09:49:40 -06:00
|
|
|
|
if (!isValidVendor(vendor)) {
|
|
|
|
|
|
return res.status(400).json({ error: 'vendor is required for FP and Archer workflows.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (vendor !== undefined && vendor !== '' && typeof vendor === 'string' && vendor.trim().length > 200) {
|
|
|
|
|
|
return res.status(400).json({ error: 'vendor must be under 200 chars.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
const vendorVal = INVENTORY_TYPES.includes(workflow_type) ? '' : vendor.trim();
|
2026-04-09 09:49:40 -06:00
|
|
|
|
const userId = req.user.id;
|
|
|
|
|
|
|
2026-05-06 11:44:17 -06:00
|
|
|
|
const client = await pool.connect();
|
|
|
|
|
|
try {
|
|
|
|
|
|
await client.query('BEGIN');
|
|
|
|
|
|
|
|
|
|
|
|
const insertedIds = [];
|
|
|
|
|
|
for (const f of findings) {
|
|
|
|
|
|
const findingId = f.finding_id.trim();
|
|
|
|
|
|
const title = f.finding_title && typeof f.finding_title === 'string'
|
|
|
|
|
|
? f.finding_title.slice(0, 500) : null;
|
|
|
|
|
|
const cvesJson = Array.isArray(f.cves) ? JSON.stringify(f.cves) : null;
|
|
|
|
|
|
const ipVal = f.ip_address && typeof f.ip_address === 'string'
|
|
|
|
|
|
? f.ip_address.trim().slice(0, 64) : null;
|
|
|
|
|
|
const hostVal = f.hostname && typeof f.hostname === 'string'
|
|
|
|
|
|
? f.hostname.trim().slice(0, 255) : null;
|
|
|
|
|
|
|
|
|
|
|
|
const { rows } = await client.query(
|
2026-04-09 09:49:40 -06:00
|
|
|
|
`INSERT INTO ivanti_todo_queue
|
2026-04-09 11:56:56 -06:00
|
|
|
|
(user_id, finding_id, finding_title, cves_json, ip_address, hostname, vendor, workflow_type)
|
2026-05-06 11:44:17 -06:00
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
|
|
|
|
RETURNING id`,
|
|
|
|
|
|
[userId, findingId, title, cvesJson, ipVal, hostVal, vendorVal, workflow_type]
|
2026-04-09 09:49:40 -06:00
|
|
|
|
);
|
2026-05-06 11:44:17 -06:00
|
|
|
|
insertedIds.push(rows[0].id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await client.query('COMMIT');
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch all inserted rows
|
|
|
|
|
|
const { rows: fetchedRows } = await pool.query(
|
|
|
|
|
|
`SELECT * FROM ivanti_todo_queue WHERE id = ANY($1)`,
|
|
|
|
|
|
[insertedIds]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const items = fetchedRows.map((r) => ({
|
|
|
|
|
|
...r,
|
|
|
|
|
|
cves: r.cves_json ? JSON.parse(r.cves_json) : [],
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
logAudit({
|
|
|
|
|
|
userId: req.user.id,
|
|
|
|
|
|
username: req.user.username,
|
|
|
|
|
|
action: 'batch_add_to_queue',
|
|
|
|
|
|
entityType: 'ivanti_todo_queue',
|
|
|
|
|
|
entityId: null,
|
|
|
|
|
|
details: {
|
|
|
|
|
|
count: insertedIds.length,
|
|
|
|
|
|
workflow_type: workflow_type,
|
|
|
|
|
|
finding_ids: findings.map((f) => f.finding_id.trim()),
|
|
|
|
|
|
},
|
|
|
|
|
|
ipAddress: req.ip,
|
2026-04-09 09:49:40 -06:00
|
|
|
|
});
|
2026-05-06 11:44:17 -06:00
|
|
|
|
|
|
|
|
|
|
return res.status(201).json({ items });
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
await client.query('ROLLBACK');
|
|
|
|
|
|
console.error('Batch insert error:', err);
|
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
client.release();
|
|
|
|
|
|
}
|
2026-04-09 09:49:40 -06:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* POST /api/ivanti/todo-queue
|
|
|
|
|
|
*
|
|
|
|
|
|
* Adds a single finding to the authenticated user's todo queue.
|
|
|
|
|
|
* Requires Admin or Standard_User group.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @body {Object}
|
|
|
|
|
|
* - finding_id {string} Required, non-empty
|
|
|
|
|
|
* - finding_title {string} Optional, max 500 chars
|
|
|
|
|
|
* - cves {Array<string>} Optional
|
|
|
|
|
|
* - ip_address {string} Optional, max 64 chars
|
|
|
|
|
|
* - hostname {string} Optional, max 255 chars
|
|
|
|
|
|
* - vendor {string} Required for FP, Archer, and DECOM workflows; max 200 chars
|
|
|
|
|
|
* - workflow_type {string} Required. One of: FP, Archer, CARD, GRANITE, DECOM
|
|
|
|
|
|
* @returns {Object} The created queue item with parsed `cves` array
|
|
|
|
|
|
* @error 400 Invalid input
|
|
|
|
|
|
* @error 500 Internal server error
|
|
|
|
|
|
*/
|
2026-05-06 11:44:17 -06:00
|
|
|
|
router.post('/', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
2026-04-09 11:56:56 -06:00
|
|
|
|
const { finding_id, finding_title, cves, ip_address, hostname, vendor, workflow_type } = req.body;
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
|
|
|
|
|
|
if (!finding_id || typeof finding_id !== 'string' || finding_id.trim().length === 0) {
|
|
|
|
|
|
return res.status(400).json({ error: 'finding_id is required.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) {
|
2026-05-08 14:51:05 -06:00
|
|
|
|
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, GRANITE, or DECOM.' });
|
2026-03-26 14:52:06 -06:00
|
|
|
|
}
|
2026-05-08 14:51:05 -06:00
|
|
|
|
if (!INVENTORY_TYPES.includes(workflow_type) && !isValidVendor(vendor)) {
|
2026-03-26 14:52:06 -06:00
|
|
|
|
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.' });
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
const vendorVal = INVENTORY_TYPES.includes(workflow_type) ? '' : vendor.trim();
|
2026-03-26 14:52:06 -06:00
|
|
|
|
const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null;
|
2026-03-26 15:01:32 -06:00
|
|
|
|
const ipVal = ip_address && typeof ip_address === 'string' ? ip_address.trim().slice(0, 64) : null;
|
2026-04-09 11:56:56 -06:00
|
|
|
|
const hostVal = hostname && typeof hostname === 'string' ? hostname.trim().slice(0, 255) : null;
|
2026-03-26 14:52:06 -06:00
|
|
|
|
const title = finding_title && typeof finding_title === 'string'
|
2026-05-06 11:44:17 -06:00
|
|
|
|
? finding_title.slice(0, 500) : null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { rows } = await pool.query(
|
|
|
|
|
|
`INSERT INTO ivanti_todo_queue
|
|
|
|
|
|
(user_id, finding_id, finding_title, cves_json, ip_address, hostname, vendor, workflow_type)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
|
|
|
|
RETURNING *`,
|
|
|
|
|
|
[req.user.id, finding_id.trim(), title, cvesJson, ipVal, hostVal, vendorVal, workflow_type]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const result = {
|
|
|
|
|
|
...rows[0],
|
|
|
|
|
|
cves: rows[0].cves_json ? JSON.parse(rows[0].cves_json) : [],
|
|
|
|
|
|
};
|
|
|
|
|
|
res.status(201).json(result);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error adding to queue:', err);
|
|
|
|
|
|
res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* PUT /api/ivanti/todo-queue/:id
|
|
|
|
|
|
*
|
|
|
|
|
|
* Updates an existing queue item owned by the authenticated user.
|
|
|
|
|
|
* Requires Admin or Standard_User group.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} id — Queue item ID (URL parameter)
|
|
|
|
|
|
* @body {Object} At least one field required:
|
|
|
|
|
|
* - vendor {string} Optional, non-empty, max 200 chars
|
|
|
|
|
|
* - workflow_type {string} Optional. One of: FP, Archer, CARD, GRANITE, DECOM
|
|
|
|
|
|
* - status {string} Optional. One of: pending, complete
|
|
|
|
|
|
* @returns {Object} The updated queue item with parsed `cves` array
|
|
|
|
|
|
* @error 400 Invalid input or no fields to update
|
|
|
|
|
|
* @error 404 Queue item not found
|
|
|
|
|
|
* @error 500 Internal server error
|
|
|
|
|
|
*/
|
2026-05-06 11:44:17 -06:00
|
|
|
|
router.put('/:id', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { vendor, workflow_type, status } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
if (vendor !== undefined && !isValidVendor(vendor)) {
|
|
|
|
|
|
return res.status(400).json({ error: 'vendor must be a non-empty string (max 200 chars).' });
|
|
|
|
|
|
}
|
|
|
|
|
|
if (workflow_type !== undefined && !VALID_WORKFLOW_TYPES.includes(workflow_type)) {
|
2026-05-08 14:51:05 -06:00
|
|
|
|
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, GRANITE, or DECOM.' });
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
}
|
|
|
|
|
|
if (status !== undefined && !VALID_STATUSES.includes(status)) {
|
|
|
|
|
|
return res.status(400).json({ error: 'status must be pending or complete.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 11:44:17 -06:00
|
|
|
|
try {
|
|
|
|
|
|
const { rows: existingRows } = await pool.query(
|
|
|
|
|
|
'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2',
|
|
|
|
|
|
[id, req.user.id]
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!existingRows[0]) {
|
|
|
|
|
|
return res.status(404).json({ error: 'Queue item not found.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const updates = [];
|
|
|
|
|
|
const params = [];
|
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
|
|
if (vendor !== undefined) {
|
|
|
|
|
|
updates.push(`vendor = $${paramIndex++}`);
|
|
|
|
|
|
params.push(vendor.trim());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (workflow_type !== undefined) {
|
|
|
|
|
|
updates.push(`workflow_type = $${paramIndex++}`);
|
|
|
|
|
|
params.push(workflow_type);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (status !== undefined) {
|
|
|
|
|
|
updates.push(`status = $${paramIndex++}`);
|
|
|
|
|
|
params.push(status);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (updates.length === 0) {
|
|
|
|
|
|
return res.status(400).json({ error: 'No fields to update.' });
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
}
|
2026-05-06 11:44:17 -06:00
|
|
|
|
|
|
|
|
|
|
updates.push('updated_at = NOW()');
|
|
|
|
|
|
params.push(id, req.user.id);
|
|
|
|
|
|
|
|
|
|
|
|
await pool.query(
|
|
|
|
|
|
`UPDATE ivanti_todo_queue SET ${updates.join(', ')} WHERE id = $${paramIndex++} AND user_id = $${paramIndex}`,
|
|
|
|
|
|
params
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const { rows } = await pool.query(
|
|
|
|
|
|
'SELECT * FROM ivanti_todo_queue WHERE id = $1', [id]
|
|
|
|
|
|
);
|
|
|
|
|
|
const result = {
|
|
|
|
|
|
...rows[0],
|
|
|
|
|
|
cves: rows[0].cves_json ? JSON.parse(rows[0].cves_json) : [],
|
|
|
|
|
|
};
|
|
|
|
|
|
res.json(result);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error(err);
|
|
|
|
|
|
res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* POST /api/ivanti/todo-queue/:id/redirect
|
|
|
|
|
|
*
|
|
|
|
|
|
* Redirects a completed queue item to a different workflow by creating a new
|
|
|
|
|
|
* pending queue item with the same finding data but a new workflow type/vendor.
|
|
|
|
|
|
* Requires Admin or Standard_User group.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} id — Queue item ID of the completed item (URL parameter)
|
|
|
|
|
|
* @body {Object}
|
|
|
|
|
|
* - workflow_type {string} Required. One of: FP, Archer, CARD, GRANITE, DECOM
|
|
|
|
|
|
* - vendor {string} Required for FP, Archer, and DECOM workflows; max 200 chars
|
|
|
|
|
|
* @returns {Object} The newly created queue item with parsed `cves` array
|
|
|
|
|
|
* @error 400 Invalid input or item not in complete status
|
|
|
|
|
|
* @error 404 Queue item not found
|
|
|
|
|
|
* @error 500 Internal server error
|
|
|
|
|
|
*/
|
2026-05-06 11:44:17 -06:00
|
|
|
|
router.post('/:id/redirect', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
2026-04-09 16:01:36 -06:00
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { workflow_type, vendor } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) {
|
2026-05-08 14:51:05 -06:00
|
|
|
|
return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, GRANITE, or DECOM.' });
|
2026-04-09 16:01:36 -06:00
|
|
|
|
}
|
2026-05-08 14:51:05 -06:00
|
|
|
|
if (!INVENTORY_TYPES.includes(workflow_type)) {
|
2026-04-09 16:01:36 -06:00
|
|
|
|
if (!isValidVendor(vendor)) {
|
|
|
|
|
|
return res.status(400).json({ error: 'vendor is required for FP and Archer workflows.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (vendor !== undefined && vendor !== '' && typeof vendor === 'string' && vendor.trim().length > 200) {
|
|
|
|
|
|
return res.status(400).json({ error: 'vendor must be under 200 chars.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
const vendorVal = INVENTORY_TYPES.includes(workflow_type) ? '' : vendor.trim();
|
2026-04-09 16:01:36 -06:00
|
|
|
|
|
2026-05-06 11:44:17 -06:00
|
|
|
|
try {
|
|
|
|
|
|
const { rows: origRows } = await pool.query(
|
|
|
|
|
|
'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2',
|
|
|
|
|
|
[id, req.user.id]
|
|
|
|
|
|
);
|
|
|
|
|
|
const original = origRows[0];
|
|
|
|
|
|
if (!original) {
|
|
|
|
|
|
return res.status(404).json({ error: 'Queue item not found.' });
|
2026-04-09 16:01:36 -06:00
|
|
|
|
}
|
2026-05-06 11:44:17 -06:00
|
|
|
|
if (original.status !== 'complete') {
|
|
|
|
|
|
return res.status(400).json({ error: 'Only completed queue items can be redirected.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { rows } = await pool.query(
|
|
|
|
|
|
`INSERT INTO ivanti_todo_queue
|
|
|
|
|
|
(user_id, finding_id, finding_title, cves_json, ip_address, hostname, vendor, workflow_type)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
|
|
|
|
RETURNING *`,
|
|
|
|
|
|
[req.user.id, original.finding_id, original.finding_title, original.cves_json, original.ip_address, original.hostname, vendorVal, workflow_type]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
logAudit({
|
|
|
|
|
|
userId: req.user.id,
|
|
|
|
|
|
username: req.user.username,
|
|
|
|
|
|
action: 'queue_item_redirected',
|
|
|
|
|
|
entityType: 'ivanti_todo_queue',
|
|
|
|
|
|
entityId: String(original.id),
|
|
|
|
|
|
details: {
|
|
|
|
|
|
original_workflow_type: original.workflow_type,
|
|
|
|
|
|
target_workflow_type: workflow_type,
|
|
|
|
|
|
new_item_id: rows[0].id,
|
|
|
|
|
|
vendor: vendorVal,
|
|
|
|
|
|
},
|
|
|
|
|
|
ipAddress: req.ip,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = {
|
|
|
|
|
|
...rows[0],
|
|
|
|
|
|
cves: rows[0].cves_json ? JSON.parse(rows[0].cves_json) : [],
|
|
|
|
|
|
};
|
|
|
|
|
|
return res.status(201).json(result);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error redirecting queue item:', err);
|
|
|
|
|
|
res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
2026-04-09 16:01:36 -06:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-22 11:12:45 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* GET /api/ivanti/todo-queue/ticket-links
|
|
|
|
|
|
*
|
|
|
|
|
|
* Returns Jira ticket associations for the current user's queue items.
|
|
|
|
|
|
* Joins jira_ticket_queue_items with jira_tickets to get ticket_key and url.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns {Object} { links: { [queue_item_id]: { ticket_key, jira_url } } }
|
|
|
|
|
|
* @error 500 Internal server error
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.get('/ticket-links', requireAuth(), async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { rows } = await pool.query(
|
|
|
|
|
|
`SELECT jtqi.queue_item_id, jt.ticket_key, jt.url AS jira_url
|
|
|
|
|
|
FROM jira_ticket_queue_items jtqi
|
|
|
|
|
|
JOIN jira_tickets jt ON jt.id = jtqi.jira_ticket_id
|
|
|
|
|
|
JOIN ivanti_todo_queue q ON q.id = jtqi.queue_item_id
|
|
|
|
|
|
WHERE q.user_id = $1`,
|
|
|
|
|
|
[req.user.id]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const links = {};
|
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
|
links[row.queue_item_id] = {
|
|
|
|
|
|
ticket_key: row.ticket_key,
|
|
|
|
|
|
jira_url: row.jira_url
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.json({ links });
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error fetching ticket links:', err);
|
|
|
|
|
|
res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* DELETE /api/ivanti/todo-queue/completed
|
|
|
|
|
|
*
|
|
|
|
|
|
* Deletes all completed queue items belonging to the authenticated user.
|
|
|
|
|
|
* Requires Admin or Standard_User group.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns {Object} { message: string, deleted: number }
|
|
|
|
|
|
* @error 500 Internal server error
|
|
|
|
|
|
*/
|
2026-05-06 11:44:17 -06:00
|
|
|
|
router.delete('/completed', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
2026-05-26 14:07:15 -06:00
|
|
|
|
const client = await pool.connect();
|
2026-05-06 11:44:17 -06:00
|
|
|
|
try {
|
2026-05-26 14:07:15 -06:00
|
|
|
|
await client.query('BEGIN');
|
|
|
|
|
|
|
|
|
|
|
|
// Select completed item IDs for this user
|
|
|
|
|
|
const { rows: completedRows } = await client.query(
|
|
|
|
|
|
"SELECT id FROM ivanti_todo_queue WHERE user_id = $1 AND status = 'complete'",
|
2026-05-06 11:44:17 -06:00
|
|
|
|
[req.user.id]
|
|
|
|
|
|
);
|
2026-05-26 14:07:15 -06:00
|
|
|
|
|
|
|
|
|
|
if (completedRows.length === 0) {
|
|
|
|
|
|
await client.query('COMMIT');
|
|
|
|
|
|
return res.json({ message: 'Completed items cleared.', deleted: 0 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ids = completedRows.map(r => r.id);
|
|
|
|
|
|
|
|
|
|
|
|
// Delete junction table references first
|
|
|
|
|
|
await client.query(
|
|
|
|
|
|
'DELETE FROM jira_ticket_queue_items WHERE queue_item_id = ANY($1::int[])',
|
|
|
|
|
|
[ids]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Delete the completed queue items
|
|
|
|
|
|
const deleteResult = await client.query(
|
|
|
|
|
|
'DELETE FROM ivanti_todo_queue WHERE id = ANY($1::int[])',
|
|
|
|
|
|
[ids]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await client.query('COMMIT');
|
|
|
|
|
|
res.json({ message: 'Completed items cleared.', deleted: deleteResult.rowCount });
|
2026-05-06 11:44:17 -06:00
|
|
|
|
} catch (err) {
|
2026-05-26 14:07:15 -06:00
|
|
|
|
await client.query('ROLLBACK');
|
2026-05-06 11:44:17 -06:00
|
|
|
|
console.error('Error clearing completed queue items:', err);
|
|
|
|
|
|
res.status(500).json({ error: 'Internal server error.' });
|
2026-05-26 14:07:15 -06:00
|
|
|
|
} finally {
|
|
|
|
|
|
client.release();
|
2026-05-06 11:44:17 -06:00
|
|
|
|
}
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-08 14:51:05 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* DELETE /api/ivanti/todo-queue/:id
|
|
|
|
|
|
*
|
|
|
|
|
|
* Deletes a single queue item owned by the authenticated user.
|
|
|
|
|
|
* Requires Admin or Standard_User group.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} id — Queue item ID (URL parameter)
|
|
|
|
|
|
* @returns {Object} { message: string }
|
|
|
|
|
|
* @error 404 Queue item not found
|
|
|
|
|
|
* @error 500 Internal server error
|
|
|
|
|
|
*/
|
2026-05-06 11:44:17 -06:00
|
|
|
|
router.delete('/:id', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
|
2026-05-06 11:44:17 -06:00
|
|
|
|
try {
|
|
|
|
|
|
const { rows } = await pool.query(
|
|
|
|
|
|
'SELECT id FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2',
|
|
|
|
|
|
[id, req.user.id]
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!rows[0]) {
|
|
|
|
|
|
return res.status(404).json({ error: 'Queue item not found.' });
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
}
|
2026-05-06 11:44:17 -06:00
|
|
|
|
|
|
|
|
|
|
await pool.query(
|
|
|
|
|
|
'DELETE FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2',
|
|
|
|
|
|
[id, req.user.id]
|
|
|
|
|
|
);
|
|
|
|
|
|
res.json({ message: 'Queue item deleted.' });
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error(err);
|
|
|
|
|
|
res.status(500).json({ error: 'Internal server error.' });
|
|
|
|
|
|
}
|
feat(reporting): add Ivanti queue panel for batch FP/Archer staging
Adds a persistent per-user staging queue so analysts can tag findings
during review and batch-process Ivanti workflows in one focused session.
Backend:
- New ivanti_todo_queue table (user-scoped, vendor, workflow_type, status)
- Table auto-created on server startup via idempotent CREATE IF NOT EXISTS
- New route /api/ivanti/todo-queue: GET, POST, PUT/:id, DELETE/:id,
DELETE/completed — all scoped to req.user.id
Frontend (ReportingPage):
- Fixed checkbox column on findings table; clicking opens an add-to-queue
popover (portal) with vendor input and FP/Archer toggle
- Already-queued rows show checked/disabled checkbox
- Queue slide-out panel (420px fixed, CSS transition) with items grouped
by vendor, per-item complete toggle + delete, Clear Completed footer
- Queue button in header with live pending-count badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:10:53 -06:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return router;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = createIvantiTodoQueueRouter;
|