feat(gui-bugs): fix eonet dashboard exception + out-of-range map bbox

Kickoff of the v0.7.x GUI rework arc. Two operator-facing bugs confirmed
live; production code, central-gui + central-supervisor restart required.

Bug 1 (eonet exception leaking to /dashboard):
The supervisor calls adapter.bump_last_seen on every dedup hit, but only
4 of 12 adapters defined it and the base class did not. Adapters that
re-emit already-published events (eonet re-lists open natural events each
poll) raised AttributeError; the supervisor published it as the adapter's
status error, which /dashboard rendered as literal text in the Last Poll
cell. Fix: add bump_last_seen to the SourceAdapter base class (guarded on
getattr(self, "_db", None)); remove the 4 now-redundant identical
overrides. Fixes all 8 affected adapters, not just eonet. Documents the
method in PRODUCER-INTEGRATION.md 4.3 (producer-doc API guard).

Bug 2 (map bbox out of valid range):
applyViewportFilter serialized raw Leaflet getEast()/getWest(), which
exceed [-180,180] when panned past the dateline at low zoom (e.g.
region_east=411.3281, region_west=-608.2031), and _parse_events_params
passed them straight to ST_MakeEnvelope. Fix (JS): normalize longitudes
into [-180,180]; when the visible span exceeds ~350 deg, omit the bbox
entirely. Fix (backend, defense in depth): _parse_events_params treats an
out-of-range or inverted envelope as "no bbox" rather than erroring or
querying a bogus envelope.

Bugs 3 (FIRMS "duplicates") and 4 (missing expand buttons) from the
planning walkthrough were investigated and refuted (FIRMS rows are
distinct fire pixels, not satellite dupes -- dropping satellite collapses
0 rows; the expand button is present and functional on main), so they are
not part of this PR.

Tests: registry-derived guard that every adapter resolves bump_last_seen +
base-method behavior test; 3 bbox-guard unit tests on _parse_events_params.
Full suite: 634 passed, 1 skipped (central and unprivileged zvx).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Johnson 2026-05-24 22:38:13 +00:00
commit 47e7b4f267
10 changed files with 157 additions and 44 deletions

View file

@ -108,3 +108,52 @@ def test_nwis_event_mirrors_centroid_into_data():
assert event.data["longitude"] == -90.25
# Geo.centroid retained for existing rendering uses.
assert event.geo.centroid == (-90.25, 41.78)
# --- bump_last_seen contract (v0.7.0 Bug 1) ---------------------------------
def test_every_adapter_resolves_bump_last_seen():
"""The supervisor calls adapter.bump_last_seen on every dedup hit. Every
registered adapter must resolve a callable (inherited from SourceAdapter or
overridden) -- otherwise the AttributeError leaks to /dashboard (the eonet
bug). Registry-derived, no hardcoded list."""
missing = [
name for name, cls in discover_adapters().items()
if not callable(getattr(cls, "bump_last_seen", None))
]
assert not missing, f"adapters missing bump_last_seen: {missing}"
def test_base_bump_last_seen_updates_and_is_noop_without_db():
"""SourceAdapter.bump_last_seen updates published_ids.last_seen when a _db
handle is present, and is a safe no-op when it is not."""
import sqlite3
from central.adapter import SourceAdapter
class _StubAdapter(SourceAdapter):
name = "stub"
async def poll(self): ... # never called in this test
async def apply_config(self, new_config): ...
def subject_for(self, event): return ""
adapter = _StubAdapter()
adapter.bump_last_seen("e1") # no _db attribute -> must not raise
adapter._db = sqlite3.connect(":memory:")
adapter._db.execute(
"CREATE TABLE published_ids (adapter TEXT, event_id TEXT, "
"first_seen TIMESTAMP, last_seen TIMESTAMP)"
)
adapter._db.execute(
"INSERT INTO published_ids VALUES ('stub', 'e1', "
"'2020-01-01 00:00:00', '2020-01-01 00:00:00')"
)
adapter._db.commit()
adapter.bump_last_seen("e1")
row = adapter._db.execute(
"SELECT last_seen FROM published_ids WHERE event_id = 'e1'"
).fetchone()
assert row[0] != "2020-01-01 00:00:00" # bumped to CURRENT_TIMESTAMP

View file

@ -0,0 +1,47 @@
"""Bug 2 (v0.7.0): out-of-range map bbox must not reach the SQL envelope.
Leaflet returns longitudes outside [-180, 180] when the map is panned past the
dateline at low zoom (e.g. east=411.3281, west=-608.2031). The map JS now
normalizes/omits those, but _parse_events_params also defends in depth: a
degenerate or out-of-range region is treated as "no bbox" rather than erroring
or building a bogus ST_MakeEnvelope that silently matches the wrong rows.
"""
from central.gui.routes import _parse_events_params
def _params(**kw):
base = {"limit": "50"}
base.update(kw)
return base
def test_out_of_range_bbox_is_dropped_not_errored():
"""The exact pan-past-dateline artifact from the bug report -> no bbox."""
parsed, error = _parse_events_params(_params(
region_north="42.0", region_south="31.0",
region_east="411.3281", region_west="-608.2031",
))
assert error is None
assert parsed is not None
assert parsed["bbox"] is None
def test_valid_bbox_is_preserved():
parsed, error = _parse_events_params(_params(
region_north="42.0", region_south="31.0",
region_east="-102.0", region_west="-124.5",
))
assert error is None
assert parsed["bbox"] == {
"north": 42.0, "south": 31.0, "east": -102.0, "west": -124.5,
}
def test_inverted_bbox_is_dropped():
"""west >= east (a wrapped dateline-straddle) is degenerate -> no bbox."""
parsed, error = _parse_events_params(_params(
region_north="42.0", region_south="31.0",
region_east="-124.5", region_west="-102.0",
))
assert error is None
assert parsed["bbox"] is None