Idea
`scan_history` + `ports.first_seen_scan_id` / `last_seen_scan_id` / `closed_at_scan_id` already track every rescan and which ports came / went / stayed (P1-G PR 1). Today the data is invisible — operators rescan and have no idea what changed unless they manually compare.
A first-class diff UI turns the existing schema into a feature.
Proposal
Engagement header badge
After a rescan, the header shows a chip like `+3 / -1 / changed: 2 since 12:04` referencing the latest scan_history row vs the prior one. Click → diff page.
`/engagements/[id]/diff?from=N&to=M` page
Three columns:
- New ports (`first_seen_scan_id == M`) — green left border, "opened in this scan"
- Closed ports (`closed_at_scan_id == M` AND were observed in N) — red left border, "closed since last scan"
- Reaffirmed with changes (port survived but service / product / version / NSE script output diff) — yellow
Each row links to the port detail pane. Service / version diffs render as inline strikethrough → arrow → new value.
Compare picker
Dropdown on the diff page: `from = scan N`, `to = scan M`. Defaults: `to = latest`, `from = previous`. Listing already lives in `listScanHistory`.
Reopen detection
A port that was closed in scan N-1 and seen again in scan N is its own category — "reopened". Lifecycle columns already cover this (closed_at_scan_id cleared on reaffirm); the diff page just needs to surface it.
Heatmap chip
Each port tile gets a small overlay when it was new / reopened in the latest scan, fading after 24h or when the operator views it.
Out of scope
- Rolling diff across 3+ scans (just pairwise to start)
- Diffing AutoRecon artifacts — only nmap-derived port surface for v1
Acceptance
- Engagement header shows the delta badge after a rescan
- Diff page renders the three buckets correctly against fixture data
- NSE script output diffs use a unified-diff style render (existing scripts pane has the chrome)
- Lifecycle data already in DB is sufficient — no new migrations
Notes
Highest-leverage win for ops who actually rescan. Cost = pure UI + data joins — no schema change, no parser change.
Idea
`scan_history` + `ports.first_seen_scan_id` / `last_seen_scan_id` / `closed_at_scan_id` already track every rescan and which ports came / went / stayed (P1-G PR 1). Today the data is invisible — operators rescan and have no idea what changed unless they manually compare.
A first-class diff UI turns the existing schema into a feature.
Proposal
Engagement header badge
After a rescan, the header shows a chip like `+3 / -1 / changed: 2 since 12:04` referencing the latest scan_history row vs the prior one. Click → diff page.
`/engagements/[id]/diff?from=N&to=M` page
Three columns:
Each row links to the port detail pane. Service / version diffs render as inline strikethrough → arrow → new value.
Compare picker
Dropdown on the diff page: `from = scan N`, `to = scan M`. Defaults: `to = latest`, `from = previous`. Listing already lives in `listScanHistory`.
Reopen detection
A port that was closed in scan N-1 and seen again in scan N is its own category — "reopened". Lifecycle columns already cover this (closed_at_scan_id cleared on reaffirm); the diff page just needs to surface it.
Heatmap chip
Each port tile gets a small overlay when it was new / reopened in the latest scan, fading after 24h or when the operator views it.
Out of scope
Acceptance
Notes
Highest-leverage win for ops who actually rescan. Cost = pure UI + data joins — no schema change, no parser change.