feat(postgres): data migration + per-BU closed counts in frontend

- Create backend/scripts/migrate-to-postgres.js (one-time SQLite→Postgres copy)
- Successfully migrated: 6 users, 21 CVEs, 6307 findings, 20965 compliance items,
  138 archives, 67 atlas plans, all notes/overrides merged
- All 22 tables verified with matching row counts
- Frontend StatusDonut now uses server-provided per-BU counts (no more N/A)
- Counts endpoint called with teams param on scope change
- Re-fetch counts when admin scope toggle changes
This commit is contained in:
Jordan Ramos
2026-05-06 12:26:54 -06:00
parent e30ad79f2a
commit 8cd73c126e
2 changed files with 942 additions and 10 deletions

View File

@@ -5052,8 +5052,12 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
const fetchCounts = async () => {
setCountsLoading(true);
try {
// Fetch global counts — open count is overridden by client-side scoped findings
const res = await fetch(`${API_BASE}/ivanti/findings/counts`, { credentials: 'include' });
// Fetch counts from server — Postgres provides per-BU open+closed counts
const teamsParam = getActiveTeamsParam();
const url = teamsParam
? `${API_BASE}/ivanti/findings/counts?teams=${encodeURIComponent(teamsParam)}`
: `${API_BASE}/ivanti/findings/counts`;
const res = await fetch(url, { credentials: 'include' });
const data = await res.json();
if (res.ok) setStatusCounts({ open: data.open ?? 0, closed: data.closed ?? 0 });
} catch (e) {
@@ -5182,6 +5186,11 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
fetchCardStatus();
}, []); // eslint-disable-line
// Re-fetch counts when admin scope changes (per-BU counts from Postgres)
useEffect(() => {
fetchCounts();
}, [adminScope]); // eslint-disable-line
// Set/clear a single column filter
const setColFilter = useCallback((colKey, vals) => {
setColumnFilters((prev) => {
@@ -5662,21 +5671,16 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
<div role="tabpanel">
{metricsTab === 'ivanti' && (
<div style={{ display: 'flex', gap: '3rem', flexWrap: 'wrap', alignItems: 'flex-start' }}>
{/* Open vs Closed donut — open from scoped findings; closed only meaningful when unscoped */}
{/* Open vs Closed donut — per-BU counts from Postgres */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
Open vs Closed
</div>
<StatusDonut
open={scopedFindings.length}
closed={getActiveTeamsParam() ? 0 : statusCounts.closed}
open={statusCounts.open}
closed={statusCounts.closed}
loading={countsLoading}
/>
{getActiveTeamsParam() && (
<div style={{ fontFamily: 'monospace', fontSize: '0.6rem', color: '#475569', marginTop: '0.5rem' }}>
Closed count unavailable when filtered by BU
</div>
)}
</div>
{/* Divider */}