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

@@ -9,7 +9,15 @@ function createIvantiArchiveRouter(db, requireAuth) {
// All routes require authentication
router.use(requireAuth(db));
// GET / — List archive records with optional ?state= filter
/**
* GET /
* List archive records with optional state filtering.
*
* @query {string} [state] - Filter by lifecycle state (ACTIVE, ARCHIVED, RETURNED, CLOSED)
* @returns {Object} 200 - { archives: Array<ArchiveRecord>, total: number }
* @returns {Object} 400 - { error: string } when state param is invalid
* @returns {Object} 500 - { error: string } on database failure
*/
router.get('/', async (req, res) => {
const { state } = req.query;
@@ -44,9 +52,17 @@ function createIvantiArchiveRouter(db, requireAuth) {
}
});
// GET /stats — Summary counts by state
/**
* GET /stats
* Summary counts of archive records by lifecycle state.
* ACTIVE is implicit: live findings in the cache that have no ARCHIVED/RETURNED archive record.
*
* @returns {Object} 200 - { ACTIVE: number, ARCHIVED: number, RETURNED: number, CLOSED: number, total: number }
* @returns {Object} 500 - { error: string } on database failure
*/
router.get('/stats', async (req, res) => {
try {
// Count archive records by state
const rows = await new Promise((resolve, reject) => {
db.all(
`SELECT current_state, COUNT(*) as count
@@ -60,15 +76,31 @@ function createIvantiArchiveRouter(db, requireAuth) {
});
const stats = { ACTIVE: 0, ARCHIVED: 0, RETURNED: 0, CLOSED: 0 };
let total = 0;
for (const row of rows) {
if (stats.hasOwnProperty(row.current_state)) {
stats[row.current_state] = row.count;
}
total += row.count;
}
// Compute ACTIVE: total live findings minus those with ARCHIVED or RETURNED records
const cacheRow = await new Promise((resolve, reject) => {
db.get(
'SELECT total FROM ivanti_findings_cache WHERE id = 1',
(err, row) => {
if (err) reject(err);
else resolve(row);
}
);
});
const liveFindingsCount = (cacheRow && cacheRow.total) || 0;
// Findings that are ARCHIVED or RETURNED are "missing" from the live set,
// so ACTIVE = live count (all findings currently present in sync results)
stats.ACTIVE = liveFindingsCount;
const total = stats.ACTIVE + stats.ARCHIVED + stats.RETURNED + stats.CLOSED;
res.json({ ...stats, total });
} catch (err) {
console.error('Archive stats error:', err);
@@ -76,7 +108,15 @@ function createIvantiArchiveRouter(db, requireAuth) {
}
});
// GET /:findingId/history — Transition history for a finding
/**
* GET /:findingId/history
* Transition history for a specific archived finding, ordered by most recent first.
* Returns an empty transitions array if the finding has no archive record.
*
* @param {string} findingId - Ivanti finding identifier (route param)
* @returns {Object} 200 - { finding_id: string, transitions: Array<TransitionRecord> }
* @returns {Object} 500 - { error: string } on database failure
*/
router.get('/:findingId/history', async (req, res) => {
const { findingId } = req.params;