Add VCL compliance reporting: exec report page, device metadata fields, bulk upload

This commit is contained in:
Jordan Ramos
2026-05-11 15:48:10 -06:00
parent 955036145d
commit d093a3d113
10 changed files with 2626 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield, Trash2 } from 'lucide-react';
import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield, Trash2, Calendar, FileText, Save } from 'lucide-react';
import ConfirmModal from '../ConfirmModal';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
@@ -48,6 +48,31 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
const [noteError, setNoteError] = useState(null);
const [pendingConfirm, setPendingConfirm] = useState(null);
// Metadata fields
const [resolutionDate, setResolutionDate] = useState('');
const [remediationPlan, setRemediationPlan] = useState('');
const [metaSaving, setMetaSaving] = useState(false);
const [metaError, setMetaError] = useState(null);
const handleSaveMetadata = async (fields) => {
setMetaSaving(true);
setMetaError(null);
try {
const res = await fetch(`${API_BASE}/compliance/items/${encodeURIComponent(hostname)}/metadata`, {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fields),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to save metadata');
} catch (err) {
setMetaError(err.message);
} finally {
setMetaSaving(false);
}
};
const fetchDetail = useCallback(async () => {
setLoading(true);
setError(null);
@@ -60,6 +85,10 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
// Default selected metrics to first active failing metric
const firstActive = (data.metrics || []).find(m => m.status === 'active');
if (firstActive) setSelectedMetrics([firstActive.metric_id]);
// Populate metadata fields
setResolutionDate(data.resolution_date || '');
setRemediationPlan(data.remediation_plan || '');
} catch (err) {
setError(err.message);
} finally {
@@ -214,6 +243,80 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
</Section>
)}
{/* Resolution Date */}
<Section title="Resolution Date" icon={<Calendar style={{ width: '14px', height: '14px' }} />}>
<input
type="date"
value={resolutionDate}
onChange={e => setResolutionDate(e.target.value)}
onBlur={() => handleSaveMetadata({ resolution_date: resolutionDate || null })}
style={{
width: '100%',
background: 'rgba(15,23,42,0.8)',
border: '1px solid rgba(20,184,166,0.25)',
borderRadius: '0.375rem',
color: '#F8FAFC',
padding: '0.5rem 0.625rem',
fontSize: '0.8rem',
fontFamily: 'monospace',
outline: 'none',
}}
onFocus={e => e.target.style.borderColor = `${TEAL}70`}
/>
</Section>
{/* Remediation Plan */}
<Section title="Remediation Plan" icon={<FileText style={{ width: '14px', height: '14px' }} />}>
<textarea
value={remediationPlan}
onChange={e => {
if (e.target.value.length <= 2000) setRemediationPlan(e.target.value);
}}
placeholder="Describe the remediation plan…"
rows={4}
style={{
width: '100%', resize: 'vertical',
background: 'rgba(15,23,42,0.8)',
border: '1px solid rgba(20,184,166,0.25)',
borderRadius: '0.375rem',
color: '#F8FAFC',
padding: '0.5rem 0.625rem',
fontSize: '0.8rem',
outline: 'none',
boxSizing: 'border-box',
}}
onFocus={e => e.target.style.borderColor = `${TEAL}70`}
onBlur={e => e.target.style.borderColor = 'rgba(20,184,166,0.25)'}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '0.4rem' }}>
<span style={{ fontSize: '0.68rem', fontFamily: 'monospace', color: remediationPlan.length > 1900 ? '#F59E0B' : '#475569' }}>
{remediationPlan.length}/2000
</span>
<button
onClick={() => handleSaveMetadata({ remediation_plan: remediationPlan || null })}
disabled={metaSaving}
style={{
display: 'inline-flex', alignItems: 'center', gap: '0.3rem',
padding: '0.3rem 0.6rem',
background: `${TEAL}15`,
border: `1px solid ${TEAL}60`,
borderRadius: '0.25rem',
color: TEAL,
fontSize: '0.68rem', fontFamily: 'monospace', fontWeight: '600',
cursor: metaSaving ? 'wait' : 'pointer',
opacity: metaSaving ? 0.6 : 1,
transition: 'all 0.15s',
}}
>
{metaSaving
? <Loader style={{ width: '11px', height: '11px', animation: 'spin 1s linear infinite' }} />
: <Save style={{ width: '11px', height: '11px' }} />}
Save
</button>
</div>
{metaError && <div style={{ marginTop: '0.4rem', color: '#F87171', fontSize: '0.72rem', fontFamily: 'monospace' }}>{metaError}</div>}
</Section>
{/* Notes */}
<Section title="Notes" icon={<MessageSquare style={{ width: '14px', height: '14px' }} />} grow>
{detail.notes.length === 0 && (