feat: one-click prune nodes outside geofilter (#669 M4)#738
feat: one-click prune nodes outside geofilter (#669 M4)#738efiten wants to merge 5 commits intoKpa-clawbot:masterfrom
Conversation
Kpa-clawbot
left a comment
There was a problem hiding this comment.
Review: one-click prune nodes outside geofilter (M4)
Good overall design: dry-run by default, API key required, no-GPS nodes always kept. Tests are thorough. A few items to address:
Must-fix
1. TOCTOU race between preview and confirm delete
The dry-run and confirm are two separate API calls. Between them, nodes could be ingested (or coordinates updated). The confirm call re-evaluates the filter independently — it doesn't use the preview list. This means:
- Nodes not shown in preview could be deleted on confirm (ingested between calls, with out-of-bounds coords)
- Nodes shown in preview might not be deleted (if their coords were updated to be inside)
The user sees a preview list, clicks "Delete N nodes", but a different set gets deleted. This violates the principle of least surprise for a destructive operation.
Fix: Either (a) pass the pubkey list from the preview to the confirm call (server-side or client-side), so only previewed nodes are deleted, or (b) return the actual deleted list in the confirm response so the UI shows what really happened, and document this behavior.
2. No cascade/cleanup of related data
DeleteNodesByPubkeys does DELETE FROM nodes WHERE public_key IN (...). If other tables reference these pubkeys (packets with source/dest fields, any future neighbor/edge table), orphaned references will remain. This isn't breaking today (no FK constraints), but it's technical debt that will bite when the schema evolves.
Fix: At minimum, add a code comment documenting that this only deletes from the nodes table and does not cascade. Ideally, also delete from any tables that reference the node pubkey (check transmissions/observations).
Should-fix
3. Irreversibility — no safety net
This is a permanent DELETE with no backup, no soft-delete, no undo. For a one-click operation in a UI panel, that's aggressive. The confirm() dialog is the only guard.
Suggestion: Log the deleted pubkeys + names at INFO level (not just the count) so an operator can at least see what was removed. The nodes will re-appear if the mesh re-advertises them, but historical metadata (first_seen, advert_count) is lost permanently.
4. Frontend prune confirm doesn't show the re-evaluated list
_gfPruneConfirm sends confirm=true but the response only contains { deleted: N }, not the list of what was deleted. If the set changed between preview and confirm (per issue #1), the user has no visibility into what actually happened.
Nits
handlePruneGeoFiltercallsNodePassesGeoFilter(lat, lon, ...)wherelat/lonare concretefloat64(defaulting to0when nil). This works correctly becausePassesFilterhas thelat == 0 && lon == 0escape hatch, but it's semantically confusing — a nil coordinate is being passed as0.0wrapped ininterface{}. Consider checkingn.Lat == nildirectly and skipping the filter call entirely for no-GPS nodes.geofilter-builder.htmlloads Leaflet from unpkg CDN — consider using the same CDN/version as the rest of the app for consistency and offline scenarios.
What's good
- Dry-run default is the right call — safe by default
requireAPIKeymiddleware +IsWeakAPIKeygate on the UI controls- Tests cover all the important paths: dry-run, confirm, no-filter-400, no-key-401
- No-GPS nodes correctly preserved (verified through the full call chain)
- Atomic config write (temp file + rename)
- Documentation is comprehensive
Verdict: Requesting changes for the TOCTOU issue (#1). The rest can be addressed in follow-up, but #1 is a data safety concern for a destructive endpoint.
- Fix TOCTOU race: confirm now requires pubkeys from preview in request body; server intersects with still-outside nodes so exactly the previewed set is deleted (no more, no less) - Add cascade comment to DeleteNodesByPubkeys documenting that only the nodes table is affected (no FK constraints today) - Log each deleted node by name + pubkey for operator visibility - Return deleted node list in confirm response so UI shows what happened - Check lat/lon nil directly instead of passing 0.0 to NodePassesGeoFilter - Update confirm test to send pubkeys body; add test for missing body → 400 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ebcfc18 to
b3d29be
Compare
- Fix TOCTOU race: confirm now requires pubkeys from preview in request body; server intersects with still-outside nodes so exactly the previewed set is deleted (no more, no less) - Add cascade comment to DeleteNodesByPubkeys documenting that only the nodes table is affected (no FK constraints today) - Log each deleted node by name + pubkey for operator visibility - Return deleted node list in confirm response so UI shows what happened - Check lat/lon nil directly instead of passing 0.0 to NodePassesGeoFilter - Update confirm test to send pubkeys body; add test for missing body → 400 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-review: PR #738 — geo-prune M4 (post
|
| # | Item | Status |
|---|---|---|
| 1 | TOCTOU race between preview and confirm — different node set could be deleted | ✅ Fixed. Confirm now requires pubkeys in request body; server intersects with current outside-filter set (routes.go:2565-2580). Frontend sends preview pubkeys in _gfPruneConfirm. Solid fix. |
| 2 | No cascade/cleanup comment on DeleteNodesByPubkeys | ✅ Fixed. Comment added in db.go:2355-2357 documenting no FK cascade. |
| 3 | Irreversibility — no audit trail | ✅ Fixed. Each deleted node logged individually at INFO: log.Printf("[geo-prune] deleted node %q (%s)") (routes.go:2596). |
| 4 | Confirm response didn't show what was deleted | ✅ Fixed. Response now includes "nodes": toDelete with full list (routes.go:2600). |
| 5 | Nil coords passed as 0.0 to filter | ✅ Fixed. NodeForGeoPrune uses *float64 pointers; handler checks n.Lat == nil before calling NodePassesGeoFilter (routes.go:2554). |
| 6 | Leaflet CDN in geofilter-builder.html | Minor — still uses unpkg, rest of app uses CDN link in index.html. Non-blocking. |
New findings (djb + munger)
No must-fix items found. The TOCTOU fix is well-designed — the intersection approach means confirm can never delete more than what was previewed, even if the polygon or GPS data changes between calls.
Observations (informational, not blocking):
confirm=trueis a query param on a POST — acceptable since the body carries the pubkey payload and the endpoint requiresX-API-Key. A future improvement could move it to the body.- Single
DELETE FROM nodes WHERE public_key IN (...)is atomic — no partial-delete risk. - Same
X-API-Keygates all admin endpoints. Acceptable for a single-operator tool; if multi-operator access is added later, consider per-endpoint permissions. - No soft-delete/undo — nodes re-appear when the mesh re-advertises them, but historical metadata (first_seen, advert_count) is lost. Documented behavior; operator is warned via
confirm()dialog.
Merge verdict
Ready after rebase. PR is currently CONFLICTING against master. Depends on PR #736 (M3) — merge that first, then rebase this. Zero must-fix items remain.
|
@efiten same comment as elsewhere - please clean up this PR and get it ready for merging or abandon it. |
…bot#669 M3) Backend: - Add PUT /api/config/geo-filter (requires X-API-Key) — saves geo_filter back to config.json atomically and updates in-memory config immediately, no restart needed - Add SaveGeoFilter() to config.go: reads config as raw map (preserving _comment fields), updates geo_filter key, writes back via temp+rename - Add writeEnabled field to GET /api/config/geo-filter response so the frontend can gate editing controls on server write capability - Add Server.configDir field; wired from -config-dir flag in main.go - Tests: TestPutConfigGeoFilter (4 cases) + TestSaveGeoFilter (3 cases) Frontend: - Add GeoFilter tab (🗺️) to the customizer between Display and Export - Tab shows current polygon on a Leaflet map (read-only for all users) - Editing controls (undo, clear, buffer km, API key input, save/remove) are only revealed when the server reports writeEnabled=true — i.e. the deployment has a write-capable apiKey configured. Public instances see a read-only polygon view. - Save calls PUT /api/config/geo-filter; Remove clears the filter - Map is destroyed on tab switch and panel close to avoid Leaflet leaks Docs: - Add docs/user-guide/geofilter.md (full guide: config, customizer, builder, prune script, API) - Update configuration.md and customization.md with geo_filter section - Update config.example.json _comment to mention the Customizer tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking the small inline map in the customizer GeoFilter tab now opens a full-screen modal (92vw × 86vh) with Undo/Clear/Done/Cancel controls. The inline map becomes a read-only preview. Both maps and the standalone geofilter-builder.html now use CartoDB Positron (light) instead of dark. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix data race on s.cfg.GeoFilter: add cfgMu RWMutex with getGeoFilter/ setGeoFilter accessors used in all handler goroutines - Add 1 MB MaxBytesReader cap on PUT /api/config/geo-filter request body - Validate polygon coordinate ranges (lat ∈ [-90,90], lon ∈ [-180,180]) and reject NaN/Inf values - Cap polygon at 1000 points to bound storage and computation - Document writeEnabled information-leak trade-off with a comment - Add tests for coordinate range rejection and oversized polygon rejection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds POST /api/admin/prune-geo-filter endpoint (dry-run by default, ?confirm=true to delete). Wires a Prune section into the GeoFilter customizer tab — preview lists affected nodes, confirm deletes them. Requires write-capable apiKey (writeEnabled gate, same as PUT). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix TOCTOU race: confirm now requires pubkeys from preview in request body; server intersects with still-outside nodes so exactly the previewed set is deleted (no more, no less) - Add cascade comment to DeleteNodesByPubkeys documenting that only the nodes table is affected (no FK constraints today) - Log each deleted node by name + pubkey for operator visibility - Return deleted node list in confirm response so UI shows what happened - Check lat/lon nil directly instead of passing 0.0 to NodePassesGeoFilter - Update confirm test to send pubkeys body; add test for missing body → 400 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
b3d29be to
0f565e4
Compare
Summary
POST /api/admin/prune-geo-filterendpoint — dry-run by default,?confirm=trueto permanently delete nodes outside the current geofilter polygon + buffer. RequiresX-API-Keyheader.writeEnabledgate as PUT). Preview lists affected nodes; Confirm delete removes them.GetNodesForGeoPruneandDeleteNodesByPubkeysDB helpers.docs/user-guide/geofilter.md— documents the UI button as primary workflow, CLI script as alternative.Test plan
cd cmd/server && go test ./...— all passapiKey— Prune section not visibleapiKey+ polygon active — Prune section visiblePOST /api/admin/prune-geo-filterwithoutX-API-Key→ 401POST /api/admin/prune-geo-filterwith no polygon configured → 400🤖 Generated with Claude Code