Add multi-metric note selection to compliance detail panel
This commit is contained in:
@@ -8,12 +8,13 @@
|
||||
// GET /summary — metric health cards for a team (from latest upload)
|
||||
// GET /items — non-compliant devices grouped by hostname (?team=X&status=active)
|
||||
// GET /items/:hostname — detail panel: all metrics + notes + upload history for a device
|
||||
// POST /notes — add a note to a (hostname, metric_id) pair
|
||||
// POST /notes — add a note to one or more (hostname, metric_id) pairs
|
||||
// GET /notes/:hostname/:metricId — notes for a specific device+metric
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const PARSER_SCRIPT = path.join(__dirname, '../scripts/parse_compliance_xlsx.py');
|
||||
@@ -488,7 +489,7 @@ function createComplianceRouter(db, upload, requireAuth, requireGroup) {
|
||||
|
||||
// Notes (all metrics for this hostname, sorted newest first)
|
||||
const notes = await dbAll(db,
|
||||
`SELECT cn.id, cn.metric_id, cn.note, cn.created_at,
|
||||
`SELECT cn.id, cn.metric_id, cn.note, cn.group_id, cn.created_at,
|
||||
u.username AS created_by
|
||||
FROM compliance_notes cn
|
||||
LEFT JOIN users u ON cn.created_by = u.id
|
||||
@@ -517,42 +518,82 @@ function createComplianceRouter(db, upload, requireAuth, requireGroup) {
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// POST /notes
|
||||
// Add a note to a (hostname, metric_id) pair.
|
||||
// Body: { hostname, metric_id, note }
|
||||
// Add a note to one or more (hostname, metric_id) pairs.
|
||||
// Body: { hostname, metric_ids: [...], note } — or legacy { hostname, metric_id, note }
|
||||
// -----------------------------------------------------------------------
|
||||
router.post('/notes', requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
||||
const { hostname, metric_id, note } = req.body;
|
||||
const { hostname, metric_id, metric_ids, note } = req.body;
|
||||
|
||||
if (!hostname || typeof hostname !== 'string' || hostname.length > 300 || !/^[a-zA-Z0-9._-]+$/.test(hostname)) {
|
||||
return res.status(400).json({ error: 'Invalid hostname format' });
|
||||
}
|
||||
if (!metric_id || typeof metric_id !== 'string' || metric_id.length > 50) {
|
||||
return res.status(400).json({ error: 'Invalid metric_id' });
|
||||
|
||||
// --- Resolve metric IDs: metric_ids takes precedence over metric_id ---
|
||||
let resolvedIds;
|
||||
if (metric_ids !== undefined) {
|
||||
if (!Array.isArray(metric_ids)) {
|
||||
return res.status(400).json({ error: 'metric_ids must be an array' });
|
||||
}
|
||||
resolvedIds = metric_ids;
|
||||
} else if (metric_id !== undefined && metric_id !== null && metric_id !== '') {
|
||||
if (typeof metric_id !== 'string' || metric_id.length > 50) {
|
||||
return res.status(400).json({ error: 'Invalid metric_id' });
|
||||
}
|
||||
resolvedIds = [metric_id];
|
||||
} else {
|
||||
return res.status(400).json({ error: 'metric_id or metric_ids is required' });
|
||||
}
|
||||
|
||||
// --- Validate resolved metric IDs ---
|
||||
if (resolvedIds.length === 0) {
|
||||
return res.status(400).json({ error: 'At least one metric ID is required' });
|
||||
}
|
||||
for (let i = 0; i < resolvedIds.length; i++) {
|
||||
const mid = resolvedIds[i];
|
||||
if (!mid || typeof mid !== 'string' || mid.length === 0 || mid.length > 50) {
|
||||
return res.status(400).json({ error: `Invalid metric_id at index ${i}` });
|
||||
}
|
||||
}
|
||||
|
||||
const noteText = String(note || '').trim().slice(0, 1000);
|
||||
if (!noteText) {
|
||||
return res.status(400).json({ error: 'Note cannot be empty' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { lastID } = await dbRun(db,
|
||||
`INSERT INTO compliance_notes (hostname, metric_id, note, created_by, created_at)
|
||||
VALUES (?, ?, ?, ?, datetime('now'))`,
|
||||
[hostname, metric_id, noteText, req.user?.id || null]
|
||||
);
|
||||
const groupId = crypto.randomUUID();
|
||||
const userId = req.user?.id || null;
|
||||
|
||||
const created = await dbGet(db,
|
||||
`SELECT cn.id, cn.hostname, cn.metric_id, cn.note, cn.created_at,
|
||||
try {
|
||||
await dbRun(db, 'BEGIN TRANSACTION');
|
||||
|
||||
const insertedIds = [];
|
||||
for (const mid of resolvedIds) {
|
||||
const { lastID } = await dbRun(db,
|
||||
`INSERT INTO compliance_notes (hostname, metric_id, note, group_id, created_by, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
||||
[hostname, mid, noteText, groupId, userId]
|
||||
);
|
||||
insertedIds.push(lastID);
|
||||
}
|
||||
|
||||
await dbRun(db, 'COMMIT');
|
||||
|
||||
// Fetch all created rows with username
|
||||
const placeholders = insertedIds.map(() => '?').join(', ');
|
||||
const notes = await dbAll(db,
|
||||
`SELECT cn.id, cn.hostname, cn.metric_id, cn.note, cn.group_id, cn.created_at,
|
||||
u.username AS created_by
|
||||
FROM compliance_notes cn
|
||||
LEFT JOIN users u ON cn.created_by = u.id
|
||||
WHERE cn.id = ?`,
|
||||
[lastID]
|
||||
WHERE cn.id IN (${placeholders})
|
||||
ORDER BY cn.id ASC`,
|
||||
insertedIds
|
||||
);
|
||||
|
||||
res.status(201).json(created);
|
||||
res.status(201).json({ notes });
|
||||
|
||||
} catch (err) {
|
||||
await dbRun(db, 'ROLLBACK').catch(() => {});
|
||||
console.error('[Compliance] POST /notes error:', err.message);
|
||||
res.status(500).json({ error: 'Failed to save note' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user