docs: add Postgres migration plan and Kiro spec

- docs/guides/postgres-migration-plan.md: full migration manual with
  phases, port allocation, rollback plan, and timeline
- .kiro/specs/postgres-migration/: requirements, design, and tasks
- Replaces findings_json blob with individual indexed rows
- Enables per-BU closed counts via SQL queries
- Uses existing Postgres instance (port 5432), new cve_dashboard DB
- Testing on port 3003, cutover to 3001 with 30s downtime
This commit is contained in:
Jordan Ramos
2026-05-05 15:04:14 -06:00
parent bd5fcccacf
commit 5cdca09f40
3 changed files with 295 additions and 3 deletions

View File

@@ -781,6 +781,9 @@ async function syncFindings(db) {
[allFindings.length, JSON.stringify(allFindings)]
);
// Invalidate in-memory cache so next read parses fresh data
invalidateFindingsCache();
console.log(`[Ivanti Findings] Sync complete — ${allFindings.length} findings`);
// Archive detection — compare previous vs current to detect disappeared/returned findings
@@ -890,6 +893,18 @@ function dbAll(db, sql, params = []) {
});
}
// ---------------------------------------------------------------------------
// In-memory findings cache — avoids re-parsing the large JSON blob on every request.
// Invalidated on sync (when findings_json is updated).
// ---------------------------------------------------------------------------
let _findingsCache = null; // { findings, total, synced_at, sync_status, error_message }
let _findingsCacheAt = null; // synced_at value when cache was built
function invalidateFindingsCache() {
_findingsCache = null;
_findingsCacheAt = null;
}
function readState(db) {
return new Promise((resolve, reject) => {
db.get(
@@ -897,9 +912,21 @@ function readState(db) {
(err, row) => {
if (err) return reject(err);
if (!row) return resolve({ total: 0, findings: [], synced_at: null, sync_status: 'never', error_message: null });
// Return cached if synced_at hasn't changed
if (_findingsCache && _findingsCacheAt === row.synced_at) {
return resolve({ ..._findingsCache });
}
let findings = [];
try { findings = JSON.parse(row.findings_json || '[]'); } catch (_) { /* leave empty */ }
resolve({ total: row.total || 0, findings, synced_at: row.synced_at, sync_status: row.sync_status, error_message: row.error_message });
const state = { total: row.total || 0, findings, synced_at: row.synced_at, sync_status: row.sync_status, error_message: row.error_message };
// Store in cache
_findingsCache = state;
_findingsCacheAt = row.synced_at;
resolve({ ...state });
}
);
});