2.x had `recon_note.TodoNote` — a title, a body and an optional scan. 3.0
dropped it and replaced it with nothing, so the operator's own reasoning had
nowhere to live: the lead to come back to, the dead end not worth retesting,
the reason behind a conclusion.
A note anchors to a durable asset identity rather than to a scan row. A note
written on api.example.com during scan 2 is still on that host in scan 9,
because the anchor is the host name, not the `subdomains.id` scan 2 created.
Identity per dimension: host name, endpoint signature, `ip:port`, address, and
vulnerability fingerprint.
That gives the rollup for free. An asset sheet shows its own notes across every
scan; a scan shows notes for assets it observed, plus what was written on it; a
target shows everything under it; /notes holds the project. The scan reach is a
per-dimension EXISTS over the `TABLES` map `surface_scope` already holds.
Every note carries at least one tag, enforced by `min_length=1` on the create
model and a disabled Save in the composer. Tags are the project vocabulary from
`shared/models/tag.py`, so a tag means the same thing on a note as on a target.
`dimension` is a `SurfaceDimension` value, so notes inherit the labels, nouns
and ordering the rest of the product uses. No second asset-kind enum.
Surfaces: a Notes section in the web asset, endpoint, service and vulnerability
sheets; a Notes tab on a scan and on a target; and /notes with tag, dimension,
status and free-text filters. The IP sheet is left out — an address group folds
across targets, so it has no single rollup anchor.
Also gives the two ScrollAreas added with the focused-scan work a definite
height; `max-h` alone does not clip, and a short list now renders without a
scroller rather than showing an empty track.
Row markers on the result tables are not built.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01799buekQvDvH41ns8EdhdH