fix: resolve 5 pre-merge issues in finding archive tracking

1. ACTIVE state never populated — stats endpoint now computes ACTIVE from live findings cache count instead of querying archive table

2. CHECK constraint mismatch — migration now uses 3-state constraint (ARCHIVED, RETURNED, CLOSED) matching runtime initArchiveTables()

3. Archive filter click non-functional — handleArchiveStateClick now fetches and renders filtered archive list below summary bar

4. Hook glob pattern mismatch — changed **/migrate*.js to **/migrations/*.js so hook fires for actual migration filenames

5. Stale stats after sync — ArchiveSummaryBar polls every 60s and refreshes immediately after workflow sync via refreshKey prop
This commit is contained in:
jramos
2026-04-03 15:51:18 -06:00
parent 3f7887eba6
commit d1fe0bf455
5 changed files with 112 additions and 14 deletions

View File

@@ -236,6 +236,9 @@ export default function App() {
// Archive filter state
const [archiveFilter, setArchiveFilter] = useState(null);
const [archiveRefreshKey, setArchiveRefreshKey] = useState(0);
const [archiveList, setArchiveList] = useState([]);
const [archiveListLoading, setArchiveListLoading] = useState(false);
const toggleCVEExpand = (cveId) => {
setExpandedCVEs(prev => ({ ...prev, [cveId]: !prev[cveId] }));
@@ -370,11 +373,23 @@ export default function App() {
console.error('Error syncing Ivanti workflows:', err);
} finally {
setIvantiSyncing(false);
setArchiveRefreshKey(k => k + 1);
}
};
};
const handleArchiveStateClick = (state) => {
setArchiveFilter(prev => prev === state ? null : state);
const newFilter = archiveFilter === state ? null : state;
setArchiveFilter(newFilter);
if (newFilter) {
setArchiveListLoading(true);
fetch(`${API_BASE}/ivanti/archive?state=${newFilter}`, { credentials: 'include' })
.then(res => res.ok ? res.json() : Promise.reject())
.then(data => setArchiveList(data.archives || []))
.catch(() => setArchiveList([]))
.finally(() => setArchiveListLoading(false));
} else {
setArchiveList([]);
}
};
const fetchDocuments = async (cveId, vendor) => {
@@ -2260,7 +2275,47 @@ export default function App() {
</div>
{/* Archive Summary Bar */}
<ArchiveSummaryBar onStateClick={handleArchiveStateClick} activeFilter={archiveFilter} />
<ArchiveSummaryBar onStateClick={handleArchiveStateClick} activeFilter={archiveFilter} refreshKey={archiveRefreshKey} />
{/* Archive list — shown when a state card is clicked */}
{archiveFilter && (
<div style={{ marginBottom: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#94A3B8', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{archiveFilter} findings
</span>
<button
onClick={() => { setArchiveFilter(null); setArchiveList([]); }}
style={{ background: 'none', border: 'none', color: '#94A3B8', cursor: 'pointer', fontFamily: 'monospace', fontSize: '0.7rem' }}
>
Clear
</button>
</div>
{archiveListLoading ? (
<div style={{ textAlign: 'center', padding: '1rem', color: '#94A3B8', fontFamily: 'monospace', fontSize: '0.75rem' }}>Loading</div>
) : archiveList.length === 0 ? (
<div style={{ textAlign: 'center', padding: '1rem', color: '#64748B', fontFamily: 'monospace', fontSize: '0.72rem', border: '1px dashed rgba(100, 116, 139, 0.3)', borderRadius: '0.375rem' }}>
No {archiveFilter.toLowerCase()} findings
</div>
) : (
<div style={{ maxHeight: '240px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '0.375rem' }}>
{archiveList.map((a) => (
<div key={a.id} style={{ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85), rgba(51, 65, 85, 0.75))', border: '1px solid rgba(100, 116, 139, 0.25)', borderRadius: '0.375rem', padding: '0.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', gap: '0.5rem', marginBottom: '0.25rem' }}>
<span style={{ fontFamily: 'monospace', fontSize: '0.7rem', fontWeight: '600', color: '#E2E8F0' }}>{a.finding_title || a.finding_id}</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.6rem', padding: '0.15rem 0.35rem', borderRadius: '0.25rem', background: 'rgba(100, 116, 139, 0.2)', border: '1px solid rgba(100, 116, 139, 0.4)', color: '#94A3B8', whiteSpace: 'nowrap' }}>
{a.last_severity?.toFixed(1) ?? '—'}
</span>
</div>
<div style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#64748B' }}>
{a.host_name}{a.ip_address ? ` (${a.ip_address})` : ''}
</div>
</div>
))}
</div>
)}
</div>
)}
{ivantiLoading ? (
<div className="text-center py-8">

View File

@@ -121,7 +121,7 @@ function hexToRgb(hex) {
return `${r}, ${g}, ${b}`;
}
export default function ArchiveSummaryBar({ onStateClick, activeFilter }) {
export default function ArchiveSummaryBar({ onStateClick, activeFilter, refreshKey }) {
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
@@ -146,8 +146,11 @@ export default function ArchiveSummaryBar({ onStateClick, activeFilter }) {
}
};
load();
return () => { cancelled = true; };
}, []);
// Re-fetch every 60s so stats stay reasonably fresh after syncs
const interval = setInterval(load, 60000);
return () => { cancelled = true; clearInterval(interval); };
}, [refreshKey]);
if (loading) {
return (