Each server's detail page now has a Ports card. "Scan…" runs a TCP connect
scan of a chosen range from the app and shows what's open, along with the
ranges that were actually confirmed free; clicking a free range starts a
reservation. Any port can carry a service name and a comment, so the page
also answers "what is this port for". A port with a note counts as taken
even when nothing is listening, which is what makes a reservation work.
Operators can scan and edit; everyone can read. Scans and note changes are
audit-logged.
Details that matter for correctness:
- "Free" means the host actively refused the connection AND nobody has
claimed the port. A port that never answers (firewall drop, host down)
is reported as not answering, not as free.
- A scan from elsewhere can't see services bound to localhost only, so the
agent now also reports what is bound on the host (ss -tulnp) and those
ports are treated as taken. They show as "local only". Existing agents
keep working; re-run the install one-liner to add this. The field is
validated leniently so one odd line can never cost an agent its whole
report, tasks included.
- If nothing answers at all during a scan, existing results are left
alone instead of being marked all-closed.
- Scan targets are limited to private addresses (RFC1918, Tailscale
100.64/10, link-local, IPv6 ULA/link-local); loopback and public
addresses are refused. Ranges are capped at 20,000 ports, and only one
scan runs per server at a time.
- Rows exist only while they carry information: an open port, or one with
a note. A closed port with no note disappears on the next scan; one with
a note stays as "reserved".
New table server_ports plus two columns on servers (migration 0009).
Verified with 76 backend checks (scanner open/refused/filtered, address
rules, agent report leniency, note/reserve/clear semantics, free-range
calculation including the localhost-only case, roles, concurrency lock,
no-response guard, audit entries, cascade delete) and by driving the real
component against the real router in a browser. Real dev database mtime
untouched.
Not verified: the agent's ss/awk/jq pipeline on a real host — the awk step
was checked against sample ss output and the script passes bash -n, but
jq isn't available here to run the whole thing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>