# Implementation Plan: Finding Archive Tracking ## Overview Implement the Finding Archive Tracking system by creating the database migration, archive detection logic within the existing sync pipeline, three API endpoints via a new route module, and an Archive Summary Bar UI component. Each task builds incrementally — schema first, then detection logic, then API, then frontend. ## Tasks - [x] 1. Create database migration and archive tables - [x] 1.1 Create `backend/migrations/add_finding_archive_tables.js` migration script - Create `ivanti_finding_archives` table with columns: id, finding_id (UNIQUE), finding_title, host_name, ip_address, current_state (CHECK constraint for ACTIVE/ARCHIVED/RETURNED/CLOSED), last_severity, first_archived_at, last_transition_at, created_at - Create `ivanti_archive_transitions` table with columns: id, archive_id (FK), from_state, to_state, severity_at_transition, reason, transitioned_at - Create indexes: idx_archive_finding_id, idx_archive_current_state, idx_transition_archive_id - Use `CREATE TABLE IF NOT EXISTS` and `CREATE INDEX IF NOT EXISTS` for idempotency - Follow existing migration pattern: open db, `db.serialize()`, log progress, close db - _Requirements: 3.1, 3.2, 3.3, 3.4, 6.1, 6.2, 6.3_ - [ ]* 1.2 Write property test for migration idempotency - **Property 9: Migration idempotency** - Run migration logic multiple times against in-memory SQLite, verify no errors and schema is consistent - **Validates: Requirements 6.2** - [x] 2. Implement archive detection logic in sync pipeline - [x] 2.1 Add `initArchiveTables(db)` function to `backend/routes/ivantiFindings.js` - Create both archive tables inline (same pattern as existing `initTables`) so they exist on startup - Call from `createIvantiFindingsRouter` during init alongside existing `initTables` - _Requirements: 3.1, 3.2, 3.3, 3.4_ - [x] 2.2 Implement `detectArchiveChanges(db, previousFindings, currentFindings)` function - Build ID sets from previous and current findings - For disappeared findings (in previous, not in current): upsert archive record with state ARCHIVED, insert transition history - For returned findings (in current, has ARCHIVED record): update to RETURNED, insert transition history - For re-disappeared findings (has RETURNED record, not in current): update to ARCHIVED, insert transition history - Use `db.run` with callbacks wrapped in promises (matching existing `dbRun` helper pattern) - _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 2.2_ - [x] 2.3 Implement `detectClosedFindings(db, closedFindingIds)` function - Query archive records with state ARCHIVED or RETURNED - For any that appear in the closed findings set, update to CLOSED with reason "remediated_in_ivanti" - Insert transition history for each state change - _Requirements: 2.3_ - [x] 2.4 Integrate archive detection into `syncFindings()` flow - Before updating the cache, read the current findings from `ivanti_findings_cache` as `previousFindings` - After successful cache update, call `detectArchiveChanges(db, previousFindings, currentFindings)` - Skip archive detection if sync encountered an error (requirement 1.5) - Call `detectClosedFindings` during `syncClosedCount` with closed finding IDs - _Requirements: 1.1, 1.5, 2.3_ - [ ]* 2.5 Write property test for archive detection — disappeared findings - **Property 1: Disappeared findings are archived with complete metadata** - Generate random previous/current finding sets using fast-check, run detectArchiveChanges against in-memory SQLite, verify all disappeared findings have ARCHIVED records with correct metadata - **Validates: Requirements 1.1, 1.2, 2.2** - [ ]* 2.6 Write property test for archive detection — returned findings - **Property 2: Returned findings transition from ARCHIVED to RETURNED** - Generate archived findings, add some back to current set, verify RETURNED state and updated severity - **Validates: Requirements 1.3** - [ ]* 2.7 Write property test for archive detection — re-disappeared findings - **Property 3: Re-disappeared findings transition from RETURNED to ARCHIVED** - Generate returned findings, remove some from current set, verify ARCHIVED state - **Validates: Requirements 1.4** - [ ]* 2.8 Write property test for transition history completeness - **Property 4: Every state transition produces a history record with all required fields** - Generate random state transitions, verify each produces a complete history row with archive_id, from_state, to_state, severity_at_transition, reason, transitioned_at - **Validates: Requirements 2.1** - [ ]* 2.9 Write property test for closed finding detection - **Property 5: Closed findings transition to CLOSED state** - Generate archived/returned findings, mark some as closed, verify CLOSED state and reason "remediated_in_ivanti" - **Validates: Requirements 2.3** - [x] 3. Checkpoint — Verify archive detection logic - Ensure all tests pass, ask the user if questions arise. - [x] 4. Implement Archive API endpoints - [x] 4.1 Create `backend/routes/ivantiArchive.js` route module - Export factory function `createIvantiArchiveRouter(db, requireAuth)` returning Express Router - Apply `requireAuth(db)` middleware to all routes - Implement GET `/` — list archive records with optional `?state=` filter, return `{ archives: [...], total: N }`. Return 400 with message "Invalid state parameter. Valid values: ACTIVE, ARCHIVED, RETURNED, CLOSED" if an unrecognized state value is provided. - Implement GET `/stats` — return `{ ACTIVE: N, ARCHIVED: N, RETURNED: N, CLOSED: N, total: N }` - Implement GET `/:findingId/history` — return `{ finding_id, transitions: [...] }` ordered by transitioned_at DESC, return empty array for unknown finding_id - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ - [x] 4.2 Register archive router in `backend/server.js` - Import `createIvantiArchiveRouter` from `./routes/ivantiArchive` - Mount at `/api/ivanti/archive` with `requireAuth` middleware - _Requirements: 4.1_ - [ ]* 4.3 Write property test for state filtering - **Property 6: State filter returns only matching records** - Generate archive records with random states, query with filter, verify only matching records returned - **Validates: Requirements 4.1** - [ ]* 4.4 Write property test for history ordering - **Property 7: Transition history is ordered by timestamp descending** - Generate multiple transitions for a finding, query history, verify descending timestamp order - **Validates: Requirements 4.2** - [ ]* 4.5 Write property test for stats accuracy - **Property 8: Stats counts match actual record distribution** - Generate archive records with random states, query stats, verify counts match actual distribution - **Validates: Requirements 4.3** - [x] 5. Checkpoint — Verify API endpoints - Ensure all tests pass, ask the user if questions arise. - [x] 6. Implement Archive Summary Bar UI component - [x] 6.1 Create `frontend/src/components/pages/ArchiveSummaryBar.js` - Fetch stats from `/api/ivanti/archive/stats` on mount - Render four stat cards: ACTIVE (sky blue #0EA5E9), ARCHIVED (amber #F59E0B), RETURNED (emerald #10B981), CLOSED (red #EF4444) - Each card shows the count and state label with Lucide icons and monospace typography - Accept `onStateClick` callback prop and `activeFilter` prop for highlighting the selected state - Use inline style objects matching the existing design system (dark gradients, glows, hover effects) - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_ - [x] 6.2 Integrate Archive Summary Bar into the Ivanti findings page - Import and render `ArchiveSummaryBar` in the Ivanti findings section of `App.js` (or the relevant page component) - Wire `onStateClick` to manage a state filter for the archive list display - _Requirements: 5.3_ - [x] 7. Final checkpoint — Verify full integration - Ensure all tests pass, ask the user if questions arise. ## Notes - Tasks marked with `*` are optional and can be skipped for faster MVP - Each task references specific requirements for traceability - Checkpoints ensure incremental validation - Property tests use `fast-check` library with minimum 100 iterations per test - All backend code uses callback-based SQLite API wrapped in promises (matching existing patterns) - All frontend code uses plain JavaScript (no TypeScript)