mirror of
https://github.com/zvx-echo6/central.git
synced 2026-06-10 11:54:37 +02:00
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:
parent
76c5e94b39
commit
47e7b4f267
10 changed files with 157 additions and 44 deletions
|
|
@ -86,6 +86,27 @@ class SourceAdapter(ABC):
|
|||
"""Optional lifecycle hook called on graceful shutdown."""
|
||||
pass
|
||||
|
||||
def bump_last_seen(self, event_id: str) -> None:
|
||||
"""Refresh the dedup ``last_seen`` for an already-published event.
|
||||
|
||||
The supervisor calls this on every dedup hit so the periodic
|
||||
``sweep_old_ids`` purge doesn't expire long-lived events that upstream
|
||||
keeps re-listing (e.g. EONET open events, NWS active alerts). Adapters
|
||||
that maintain the standard ``published_ids`` table inherit this; it is a
|
||||
safe no-op for any adapter without a ``_db`` handle. Previously only
|
||||
four adapters defined it, so adapters that re-emit (eonet, etc.) raised
|
||||
``AttributeError`` here and the supervisor surfaced it on /dashboard.
|
||||
"""
|
||||
db = getattr(self, "_db", None)
|
||||
if db is None:
|
||||
return
|
||||
db.execute(
|
||||
"UPDATE published_ids SET last_seen = CURRENT_TIMESTAMP "
|
||||
"WHERE adapter = ? AND event_id = ?",
|
||||
(self.name, event_id),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
async def preview_for_settings(self, settings: BaseModel) -> list[dict] | None:
|
||||
"""Optional. Override to surface a settings-driven preview on the edit page.
|
||||
|
||||
|
|
|
|||
|
|
@ -271,16 +271,6 @@ class InciWebAdapter(SourceAdapter):
|
|||
)
|
||||
self._db.commit()
|
||||
|
||||
def bump_last_seen(self, event_id: str) -> None:
|
||||
"""Bump the last_seen timestamp for an event."""
|
||||
if not self._db:
|
||||
return
|
||||
self._db.execute(
|
||||
"UPDATE published_ids SET last_seen = CURRENT_TIMESTAMP WHERE adapter = ? AND event_id = ?",
|
||||
(self.name, event_id),
|
||||
)
|
||||
self._db.commit()
|
||||
|
||||
def sweep_old_ids(self) -> int:
|
||||
"""Remove published_ids older than 14 days. Returns count deleted."""
|
||||
if not self._db:
|
||||
|
|
|
|||
|
|
@ -418,16 +418,6 @@ class NWSAdapter(SourceAdapter):
|
|||
)
|
||||
self._db.commit()
|
||||
|
||||
def bump_last_seen(self, event_id: str) -> None:
|
||||
"""Bump the last_seen timestamp for an event."""
|
||||
if not self._db:
|
||||
return
|
||||
self._db.execute(
|
||||
"UPDATE published_ids SET last_seen = CURRENT_TIMESTAMP WHERE adapter = ? AND event_id = ?",
|
||||
(self.name, event_id)
|
||||
)
|
||||
self._db.commit()
|
||||
|
||||
def sweep_old_ids(self) -> int:
|
||||
"""Remove published_ids older than 8 days. Returns count deleted."""
|
||||
if not self._db:
|
||||
|
|
|
|||
|
|
@ -158,16 +158,6 @@ class WFIGSIncidentsAdapter(SourceAdapter):
|
|||
)
|
||||
self._db.commit()
|
||||
|
||||
def bump_last_seen(self, event_id: str) -> None:
|
||||
"""Bump the last_seen timestamp for an event."""
|
||||
if not self._db:
|
||||
return
|
||||
self._db.execute(
|
||||
"UPDATE published_ids SET last_seen = CURRENT_TIMESTAMP WHERE adapter = ? AND event_id = ?",
|
||||
(self.name, event_id),
|
||||
)
|
||||
self._db.commit()
|
||||
|
||||
def sweep_old_ids(self) -> int:
|
||||
"""Remove published_ids older than 14 days. Returns count deleted."""
|
||||
if not self._db:
|
||||
|
|
|
|||
|
|
@ -158,16 +158,6 @@ class WFIGSPerimetersAdapter(SourceAdapter):
|
|||
)
|
||||
self._db.commit()
|
||||
|
||||
def bump_last_seen(self, event_id: str) -> None:
|
||||
"""Bump the last_seen timestamp for an event."""
|
||||
if not self._db:
|
||||
return
|
||||
self._db.execute(
|
||||
"UPDATE published_ids SET last_seen = CURRENT_TIMESTAMP WHERE adapter = ? AND event_id = ?",
|
||||
(self.name, event_id),
|
||||
)
|
||||
self._db.commit()
|
||||
|
||||
def sweep_old_ids(self) -> int:
|
||||
"""Remove published_ids older than 14 days. Returns count deleted."""
|
||||
if not self._db:
|
||||
|
|
|
|||
|
|
@ -2700,6 +2700,17 @@ def _parse_events_params(params) -> tuple[dict | None, str | None]:
|
|||
except ValueError:
|
||||
return None, "Region parameters must be valid numbers"
|
||||
|
||||
# Defense in depth: the map JS normalizes coordinates, but a degenerate
|
||||
# or out-of-range bbox (e.g. Leaflet pan-past-dateline artifacts like
|
||||
# east=411.3, west=-608.2) must never reach ST_MakeEnvelope. Treat an
|
||||
# invalid envelope as "no bbox" rather than erroring or querying a bogus
|
||||
# envelope that silently matches the wrong rows.
|
||||
if not (
|
||||
-90 <= bbox["south"] < bbox["north"] <= 90
|
||||
and -180 <= bbox["west"] < bbox["east"] <= 180
|
||||
):
|
||||
bbox = None
|
||||
|
||||
# Parse cursor
|
||||
cursor_time = None
|
||||
cursor_id = None
|
||||
|
|
|
|||
|
|
@ -274,12 +274,30 @@
|
|||
viewportDebounceTimer = setTimeout(applyViewportFilter, 400);
|
||||
});
|
||||
|
||||
// Normalize a longitude into [-180, 180]. Leaflet returns out-of-range
|
||||
// values (e.g. east=411, west=-608) when the map is panned past the
|
||||
// dateline / wrapped at low zoom; those must not reach the bbox filter.
|
||||
function wrapLon(lon) {
|
||||
return ((((lon + 180) % 360) + 360) % 360) - 180;
|
||||
}
|
||||
|
||||
function applyViewportFilter() {
|
||||
var bounds = map.getBounds();
|
||||
northInput.value = bounds.getNorth().toFixed(4);
|
||||
southInput.value = bounds.getSouth().toFixed(4);
|
||||
eastInput.value = bounds.getEast().toFixed(4);
|
||||
westInput.value = bounds.getWest().toFixed(4);
|
||||
var rawWest = bounds.getWest();
|
||||
var rawEast = bounds.getEast();
|
||||
// When the visible map spans more than ~350 deg of longitude (zoomed
|
||||
// far out / wrapped), a bbox filter is meaningless — treat as no bbox.
|
||||
if (rawEast - rawWest > 350) {
|
||||
northInput.value = "";
|
||||
southInput.value = "";
|
||||
eastInput.value = "";
|
||||
westInput.value = "";
|
||||
} else {
|
||||
northInput.value = Math.min(90, bounds.getNorth()).toFixed(4);
|
||||
southInput.value = Math.max(-90, bounds.getSouth()).toFixed(4);
|
||||
eastInput.value = wrapLon(rawEast).toFixed(4);
|
||||
westInput.value = wrapLon(rawWest).toFixed(4);
|
||||
}
|
||||
htmx.trigger(document.getElementById("filter-form"), "submit");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue