diff --git a/src/central/adapters/eonet.py b/src/central/adapters/eonet.py index 87003c7..ac4d46e 100644 --- a/src/central/adapters/eonet.py +++ b/src/central/adapters/eonet.py @@ -148,9 +148,6 @@ class EONETAdapter(SourceAdapter): wizard_order = None default_cadence_s = 1800 - # Event lat/lon mirrored from Geo.centroid into event.data (see poll()). - enrichment_locations = [("latitude", "longitude")] - def __init__( self, config: AdapterConfig, @@ -373,12 +370,6 @@ class EONETAdapter(SourceAdapter): "latest_geometry_date": latest_date_iso or None, } - # Mirror centroid (lon, lat) into top-level data keys for the flat - # enrichment path (see enrichment_locations). - if centroid is not None: - data["latitude"] = centroid[1] - data["longitude"] = centroid[0] - dedup_key = _dedup_key(event_id, latest_date_iso) if self.is_published(dedup_key): diff --git a/src/central/adapters/gdacs.py b/src/central/adapters/gdacs.py index cff6801..9ff6d6e 100644 --- a/src/central/adapters/gdacs.py +++ b/src/central/adapters/gdacs.py @@ -150,9 +150,6 @@ class GDACSAdapter(SourceAdapter): wizard_order = None default_cadence_s = 600 - # Event lat/lon mirrored from Geo.centroid into event.data (see poll()). - enrichment_locations = [("latitude", "longitude")] - def __init__( self, config: AdapterConfig, @@ -394,12 +391,6 @@ class GDACSAdapter(SourceAdapter): "iscurrent": iscurrent, } - # Mirror centroid (lon, lat) into top-level data keys for the flat - # enrichment path (see enrichment_locations). - if centroid is not None: - data["latitude"] = centroid[1] - data["longitude"] = centroid[0] - if not iscurrent: # Explicit tombstone from upstream. Only emit if we previously observed it. if guid in observed_before: diff --git a/src/central/adapters/inciweb.py b/src/central/adapters/inciweb.py index 853b120..2ae0634 100644 --- a/src/central/adapters/inciweb.py +++ b/src/central/adapters/inciweb.py @@ -171,9 +171,6 @@ class InciWebAdapter(SourceAdapter): wizard_order = None # Ships disabled default_cadence_s = 600 - # Coords parsed from the narrative, mirrored from Geo.centroid into event.data. - enrichment_locations = [("latitude", "longitude")] - def __init__( self, config: AdapterConfig, @@ -464,9 +461,6 @@ class InciWebAdapter(SourceAdapter): "url": item.get("link", ""), "guid": guid, "raw": item, - # Mirror centroid (lon, lat) for the flat enrichment path. - "latitude": centroid[1] if centroid else None, - "longitude": centroid[0] if centroid else None, }, ) diff --git a/src/central/adapters/nwis.py b/src/central/adapters/nwis.py index 6948a69..e2a922d 100644 --- a/src/central/adapters/nwis.py +++ b/src/central/adapters/nwis.py @@ -124,9 +124,6 @@ class NWISAdapter(SourceAdapter): wizard_order = None default_cadence_s = 900 - # Site lat/lon mirrored from Geo.centroid into event.data (see _build_event). - enrichment_locations = [("latitude", "longitude")] - def __init__( self, config: AdapterConfig, @@ -375,12 +372,6 @@ class NWISAdapter(SourceAdapter): "last_modified": props.get("last_modified"), } - # Mirror centroid (lon, lat) into top-level data keys so the flat - # enrichment path can reach them (see enrichment_locations). - if centroid is not None: - data["latitude"] = centroid[1] - data["longitude"] = centroid[0] - return Event( id=f"{monitoring_location_id}:{parameter_code}:{time_iso}", adapter=self.name, diff --git a/src/central/adapters/nws.py b/src/central/adapters/nws.py index e50922e..8205a1f 100644 --- a/src/central/adapters/nws.py +++ b/src/central/adapters/nws.py @@ -212,9 +212,6 @@ class NWSAdapter(SourceAdapter): wizard_order = 1 default_cadence_s = 60 - # Alerts cover forecast zones/counties (polygons), not a single point. - enrichment_locations = [] - def __init__( self, config: AdapterConfig, diff --git a/src/central/adapters/swpc_alerts.py b/src/central/adapters/swpc_alerts.py index 8bd3317..3368824 100644 --- a/src/central/adapters/swpc_alerts.py +++ b/src/central/adapters/swpc_alerts.py @@ -41,9 +41,6 @@ class SWPCAlertsAdapter(SourceAdapter): wizard_order = None default_cadence_s = 300 - # Space weather — no geographic coordinate to enrich. - enrichment_locations = [] - def __init__( self, config: AdapterConfig, diff --git a/src/central/adapters/swpc_kindex.py b/src/central/adapters/swpc_kindex.py index ab5d29f..f05bcbb 100644 --- a/src/central/adapters/swpc_kindex.py +++ b/src/central/adapters/swpc_kindex.py @@ -41,9 +41,6 @@ class SWPCKindexAdapter(SourceAdapter): wizard_order = None default_cadence_s = 600 - # Space weather — no geographic coordinate to enrich. - enrichment_locations = [] - def __init__( self, config: AdapterConfig, diff --git a/src/central/adapters/swpc_protons.py b/src/central/adapters/swpc_protons.py index 7c1d84c..1a3876e 100644 --- a/src/central/adapters/swpc_protons.py +++ b/src/central/adapters/swpc_protons.py @@ -40,9 +40,6 @@ class SWPCProtonsAdapter(SourceAdapter): wizard_order = None default_cadence_s = 600 - # Space weather — no geographic coordinate to enrich. - enrichment_locations = [] - def __init__( self, config: AdapterConfig, diff --git a/src/central/adapters/usgs_quake.py b/src/central/adapters/usgs_quake.py index 4fd8911..63009ee 100644 --- a/src/central/adapters/usgs_quake.py +++ b/src/central/adapters/usgs_quake.py @@ -79,9 +79,6 @@ class USGSQuakeAdapter(SourceAdapter): wizard_order = 3 default_cadence_s = 60 - # Epicenter lat/lon are top-level keys in event.data (see _build_event). - enrichment_locations = [("latitude", "longitude")] - def __init__( self, config: AdapterConfig, diff --git a/src/central/adapters/wfigs_incidents.py b/src/central/adapters/wfigs_incidents.py index be31f20..660d1de 100644 --- a/src/central/adapters/wfigs_incidents.py +++ b/src/central/adapters/wfigs_incidents.py @@ -60,9 +60,6 @@ class WFIGSIncidentsAdapter(SourceAdapter): wizard_order = None # Not in setup wizard default_cadence_s = 300 - # Incident-point lat/lon mirrored from Geo.centroid into event.data. - enrichment_locations = [("latitude", "longitude")] - def __init__( self, config: AdapterConfig, @@ -329,9 +326,6 @@ class WFIGSIncidentsAdapter(SourceAdapter): "POOState_raw": state_raw, "POOCounty": county, "raw": props, - # Mirror centroid (lon, lat) for the flat enrichment path. - "latitude": centroid[1] if centroid else None, - "longitude": centroid[0] if centroid else None, }, ) diff --git a/src/central/adapters/wfigs_perimeters.py b/src/central/adapters/wfigs_perimeters.py index 1a1d336..669d635 100644 --- a/src/central/adapters/wfigs_perimeters.py +++ b/src/central/adapters/wfigs_perimeters.py @@ -60,9 +60,6 @@ class WFIGSPerimetersAdapter(SourceAdapter): wizard_order = None # Not in setup wizard default_cadence_s = 300 - # Perimeters are polygons, not a single point — no coordinate to enrich. - enrichment_locations = [] - def __init__( self, config: AdapterConfig, diff --git a/src/central/gui/routes.py b/src/central/gui/routes.py index a056496..e6bb580 100644 --- a/src/central/gui/routes.py +++ b/src/central/gui/routes.py @@ -4,7 +4,7 @@ import base64 import json import logging import re -from datetime import datetime, timezone +from datetime import datetime from typing import Any logger = logging.getLogger("central.gui.routes") @@ -2870,31 +2870,6 @@ def _geometry_summary(geometry: dict | None) -> str: return geom_type -def _format_event_time(iso: str | None) -> str: - """Format an ISO-8601 timestamp as 'MM-DD-YYYY HH:MM UTC' (24h, no seconds).""" - if not iso: - return "" - try: - dt = datetime.fromisoformat(iso).astimezone(timezone.utc) - except (ValueError, TypeError): - return iso - return dt.strftime("%m-%d-%Y %H:%M") + " UTC" - - -def _decorate_table_events(events: list[dict]) -> None: - """Add display-only fields used by the HTML events table (in place). - - These are for the table chrome only and are deliberately NOT added in - _fetch_events, so the /events.json payload is unchanged. adapter_display - is sourced from the registry (display_name), with the bare name as fallback. - """ - display = {cls.name: cls.display_name for cls in discover_adapters().values()} - for event in events: - event["geometry_summary"] = _geometry_summary(event.get("geometry")) - event["time_human"] = _format_event_time(event.get("time")) - event["adapter_display"] = display.get(event.get("adapter"), event.get("adapter")) - - @router.get("/events.json") async def events_json(request: Request): @@ -2983,15 +2958,9 @@ async def events_list(request: Request) -> HTMLResponse: events = result.events next_cursor = result.next_cursor - # Add table-only display fields (time_human, adapter_display, geometry_summary) - _decorate_table_events(events) - - # Registry-derived adapter list for the filter - {% for a in adapters %} - - {% endfor %} + + +
@@ -170,12 +163,18 @@
- {% for a in adapters %}
-
- {{ a.display_name }} +
+ NWS (Weather) +
+
+
+ FIRMS (Fire) +
+
+
+ USGS (Quake)
- {% endfor %}
@@ -190,57 +189,17 @@ var tileUrl = {{ tile_url | tojson }}; var tileAttr = {{ tile_attribution | tojson }}; - // Adapter color mapping — built from the registry-derived adapter list and - // the same palette the legend uses, keyed by sorted index. No adapter name - // or color is hardcoded here. - var PALETTE = {{ palette | tojson }}; + // Adapter color mapping var ADAPTER_COLORS = { - {% for a in adapters %}{{ a.name | tojson }}: PALETTE[{{ loop.index0 }} % PALETTE.length]{{ "," if not loop.last }} - {% endfor %} + "nws": "#f59e0b", + "firms": "#dc2626", + "usgs_quake": "#7c3aed" }; function getAdapterColor(adapter) { return ADAPTER_COLORS[adapter] || "#3388ff"; } - // Flatten arbitrarily-nested GeoJSON coordinates into a flat [lng, lat] list. - function flattenCoords(coords, out) { - if (coords.length && typeof coords[0] === "number") { - out.push(coords); - return; - } - for (var i = 0; i < coords.length; i++) { - flattenCoords(coords[i], out); - } - } - - // A geometry whose vertices all collapse to a single point (zero extent in - // both dimensions). Enrichment stores some point-like sources as degenerate - // polygons; Leaflet would draw these invisibly, so we plot a marker instead. - function isDegenerate(geom) { - if (!geom || !geom.coordinates) return false; - var pts = []; - flattenCoords(geom.coordinates, pts); - if (pts.length === 0) return false; - var minX = pts[0][0], maxX = pts[0][0], minY = pts[0][1], maxY = pts[0][1]; - for (var i = 1; i < pts.length; i++) { - if (pts[i][0] < minX) minX = pts[i][0]; - if (pts[i][0] > maxX) maxX = pts[i][0]; - if (pts[i][1] < minY) minY = pts[i][1]; - if (pts[i][1] > maxY) maxY = pts[i][1]; - } - return (maxX - minX) < 1e-9 && (maxY - minY) < 1e-9; - } - - // Mean of all vertices, returned as Leaflet [lat, lng]. - function centroidLatLng(geom) { - var pts = []; - flattenCoords(geom.coordinates, pts); - var sx = 0, sy = 0; - for (var i = 0; i < pts.length; i++) { sx += pts[i][0]; sy += pts[i][1]; } - return [sy / pts.length, sx / pts.length]; - } - // Initialize map var map = L.map("events-map").setView([39, -98], 4); @@ -316,33 +275,23 @@ var adapter = row.dataset.adapter || ""; var color = getAdapterColor(adapter); - var markerStyle = { - radius: 8, - color: color, - weight: 2, - fillColor: color, - fillOpacity: 0.25 - }; - - // Real polygons/lines render as geometry; zero-extent geometries - // (degenerate polygons from enrichment) render as a point marker - // so every non-null geometry is actually visible on the map. - var layer; - if (isDegenerate(geom)) { - layer = L.circleMarker(centroidLatLng(geom), markerStyle); - } else { - layer = L.geoJSON(geom, { - style: { + var layer = L.geoJSON(geom, { + style: { + color: color, + weight: 2, + fillColor: color, + fillOpacity: 0.25 + }, + pointToLayer: function(feature, latlng) { + return L.circleMarker(latlng, { + radius: 8, color: color, weight: 2, fillColor: color, fillOpacity: 0.25 - }, - pointToLayer: function(feature, latlng) { - return L.circleMarker(latlng, markerStyle); - } - }); - } + }); + } + }); layer.bindPopup(buildPopup(row)); layer.on("click", function() { @@ -465,80 +414,17 @@ // Fit to results button document.getElementById("fit-to-results").addEventListener("click", fitToAllLayers); - // Client-side sort of the displayed rows; state persists across HTMX swaps. - var sortState = { col: null, dir: 1 }; // dir: 1 asc, -1 desc - - function sortKey(row, col) { - // Time sorts on the ISO timestamp for true chronological order. - if (col === 1) return row.dataset.time || ""; - var cell = row.children[col]; - return cell ? cell.textContent.trim().toLowerCase() : ""; - } - - function applySort() { - if (sortState.col === null) return; - var tbody = document.querySelector("#events-rows table.events-table tbody"); - if (!tbody) return; - var pairs = []; - tbody.querySelectorAll("tr.event-row").forEach(function(r) { - pairs.push([r, r.nextElementSibling]); // main row + its detail row - }); - // Array.prototype.sort is stable (ES2019+), so equal keys keep order. - pairs.sort(function(a, b) { - var ka = sortKey(a[0], sortState.col), kb = sortKey(b[0], sortState.col); - if (ka < kb) return -sortState.dir; - if (ka > kb) return sortState.dir; - return 0; - }); - pairs.forEach(function(p) { - tbody.appendChild(p[0]); - if (p[1]) tbody.appendChild(p[1]); - }); - } - - function updateSortIndicators(ths) { - ths.forEach(function(th, idx) { - th.querySelectorAll(".sort-ind").forEach(function(s) { s.remove(); }); - if (idx === sortState.col) { - var s = document.createElement("span"); - s.className = "sort-ind"; - s.textContent = sortState.dir === 1 ? " ▲" : " ▼"; - th.appendChild(s); - } - }); - } - - function bindSortHandlers() { - var ths = document.querySelectorAll("#events-rows table.events-table thead th"); - ths.forEach(function(th, idx) { - if (idx === 0) return; // expand column is not sortable - th.style.cursor = "pointer"; - th.onclick = function() { - if (sortState.col === idx) { sortState.dir *= -1; } - else { sortState.col = idx; sortState.dir = 1; } - updateSortIndicators(ths); - applySort(); - }; - }); - updateSortIndicators(ths); - } - // Initial load - bind layers and fit bounds rebindEventLayers(); // Initial load only - bindSortHandlers(); if (false) { // DISABLED: map never auto-fits fitToAllLayers(); isInitialLoad = false; } - // Re-bind layers after HTMX swap so the map tracks the current (filtered / - // paginated) result set. Viewport is preserved — we never auto-fit here. + // Re-bind layers after HTMX swap (but do NOT fit bounds) document.body.addEventListener("htmx:afterSwap", function(evt) { if (evt.detail.target.id === "events-rows") { - rebindEventLayers(); - // Re-bind sort handlers to the new rows and re-apply the active sort. - bindSortHandlers(); - applySort(); + // rebindEventLayers(); // DISABLED: map shows all events, only table filters // Do NOT call fitToAllLayers - preserve user viewport } }); diff --git a/src/central/supervisor.py b/src/central/supervisor.py index 26db1da..90913a7 100644 --- a/src/central/supervisor.py +++ b/src/central/supervisor.py @@ -76,12 +76,9 @@ async def apply_enrichment( No-op when the adapter declares no enrichment_locations or no enrichers are registered. Uses the first (lat_path, lon_path) tuple that resolves to - a non-null coordinate pair in event.data. If no declared pair resolves to - coordinates, still attaches an all-null bundle so that every event from an - enriched adapter carries _enriched (consumers get a stable field set). - Each enricher's result is keyed by enricher.name. Mutates the data dict in - place (Event is frozen, but its data dict is not — this avoids a - model_copy on every published event). + a non-null coordinate pair in event.data. Each enricher's result is keyed + by enricher.name. Mutates the data dict in place (Event is frozen, but its + data dict is not — this avoids a model_copy on every published event). """ if not enrichment_locations or not enrichers: return @@ -96,15 +93,6 @@ async def apply_enrichment( enriched[enricher.name] = await enricher.enrich(location) event.data["_enriched"] = enriched return - # No declared pair resolved to coordinates. Still attach _enriched: each - # enricher resolves the null location to its own all-null bundle (per the - # never-raise contract), so coordless events (e.g. removal tombstones) - # carry the same shape as enriched ones. - null_location = {"lat": None, "lon": None} - enriched = {} - for enricher in enrichers: - enriched[enricher.name] = await enricher.enrich(null_location) - event.data["_enriched"] = enriched # Stream subject mappings -- derived from the registry; every stream is included # (META too: supervisor must create it in JetStream even though archive skips it). diff --git a/tests/test_apply_enrichment_coordless.py b/tests/test_apply_enrichment_coordless.py deleted file mode 100644 index 870485d..0000000 --- a/tests/test_apply_enrichment_coordless.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Regression tests for apply_enrichment's coordless path. - -Design principle: every event from an adapter that declares enrichment_locations -must carry data["_enriched"] — populated when coordinates resolve, an all-null -bundle when they don't (e.g. removal tombstones with no lat/lon). Adapters that -declare no enrichment_locations are still skipped entirely. -""" - -from datetime import datetime, timezone -from typing import Any - -import pytest - -from central.config_models import EnrichmentConfig -from central.enrichment.cache import EnrichmentCache -from central.enrichment.geocoder import GeocoderEnricher, all_null_bundle -from central.models import Event, Geo -from central.supervisor import apply_enrichment, build_enrichers - - -def _make_event(data: dict[str, Any]) -> Event: - return Event( - id="evt-1", - adapter="usgs_quake", - category="quake.event.test", - time=datetime(2026, 1, 1, tzinfo=timezone.utc), - geo=Geo(), - data=data, - ) - - -class _PopulatingBackend: - """Deterministic backend that resolves any real coords to a fixed place.""" - - async def reverse(self, lat: float, lon: float) -> dict[str, Any]: - return {**all_null_bundle(), "city": "Boise", "state": "ID"} - - -@pytest.mark.asyncio -async def test_coordless_event_with_declared_locations_gets_null_bundle(tmp_path): - """An event whose declared coord paths are all None still gets _enriched.""" - cache = EnrichmentCache(tmp_path / "enrichment_cache.db") - enrichers = build_enrichers(EnrichmentConfig(), cache) - event = _make_event( - {"latitude": None, "longitude": None, "reason": "fallen_off_current_service"} - ) - assert "_enriched" not in event.data - - await apply_enrichment(event, [("latitude", "longitude")], enrichers) - - assert event.data["_enriched"]["geocoder"] == all_null_bundle() - - -@pytest.mark.asyncio -async def test_event_with_coords_still_enriches_normally(tmp_path): - """The coord-bearing path is unchanged: the backend is consulted and its - resolved fields land in the bundle.""" - cache = EnrichmentCache(tmp_path / "enrichment_cache.db") - enricher = GeocoderEnricher(_PopulatingBackend(), cache=cache) - event = _make_event({"latitude": 43.0, "longitude": -116.0}) - - await apply_enrichment(event, [("latitude", "longitude")], [enricher]) - - bundle = event.data["_enriched"]["geocoder"] - assert bundle["state"] == "ID" - assert bundle["city"] == "Boise" - - -@pytest.mark.asyncio -async def test_adapter_with_no_enrichment_locations_still_skipped(tmp_path): - """Adapters declaring no enrichment_locations are skipped — no _enriched.""" - cache = EnrichmentCache(tmp_path / "enrichment_cache.db") - enrichers = build_enrichers(EnrichmentConfig(), cache) - event = _make_event({"latitude": 43.0, "longitude": -116.0}) - - await apply_enrichment(event, [], enrichers) - - assert "_enriched" not in event.data diff --git a/tests/test_enrichment_locations_coverage.py b/tests/test_enrichment_locations_coverage.py deleted file mode 100644 index 2be4c50..0000000 --- a/tests/test_enrichment_locations_coverage.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Coverage tests for adapter enrichment_locations declarations (PR L-a). - -Every adapter must make a conscious enrichment_locations declaration — a -non-empty [(lat_field, lon_field)] for point adapters, or an explicit [] for -adapters with no point coordinate. Registry-derived (iterates -discover_adapters()), no hardcoded adapter lists. - -Plus synthetic-event tests for the two adapters with isolated event builders -(usgs_quake._feature_to_event, nwis._build_event) proving the declared -latitude/longitude paths actually resolve on event.data. The four inline-build -point adapters (eonet, gdacs, wfigs_incidents, inciweb) construct events only -inside poll() and are covered by the live end-to-end smoke instead. -""" - -from datetime import datetime, timezone -from pathlib import Path -from unittest.mock import MagicMock - -from central.adapter_discovery import discover_adapters -from central.config_models import AdapterConfig - - -def test_every_adapter_explicitly_declares_enrichment_locations(): - """Each adapter declares enrichment_locations in its OWN class body (not - just inheriting the SourceAdapter default) — a conscious choice per adapter.""" - missing = [ - name for name, cls in discover_adapters().items() - if "enrichment_locations" not in cls.__dict__ - ] - assert not missing, f"adapters missing an explicit enrichment_locations: {missing}" - - -def test_enrichment_locations_shape_is_valid(): - """Each declaration is a list of (str, str) tuples (possibly empty).""" - for name, cls in discover_adapters().items(): - locs = cls.enrichment_locations - assert isinstance(locs, list), f"{name}: not a list" - for tup in locs: - assert isinstance(tup, tuple) and len(tup) == 2, f"{name}: bad tuple {tup!r}" - assert all(isinstance(p, str) for p in tup), f"{name}: non-str path {tup!r}" - - -def test_point_adapters_use_canonical_lat_lon_paths(): - """Every point adapter (non-empty declaration) uses the same protocol keys - ('latitude', 'longitude') — the convention FIRMS established.""" - for name, cls in discover_adapters().items(): - if cls.enrichment_locations: - assert cls.enrichment_locations == [("latitude", "longitude")], ( - f"{name} uses non-canonical paths: {cls.enrichment_locations}" - ) - - -def test_at_least_the_known_point_adapters_are_non_empty(): - """Registry-derived sanity: the adapters that carry a point coordinate have - a non-empty declaration. Derived by probing enrichment_locations, not a - hardcoded list — guards against a regression that blanks them all.""" - non_empty = { - name for name, cls in discover_adapters().items() if cls.enrichment_locations - } - # firms is the original; there must be several point adapters now. - assert "firms" in non_empty - assert len(non_empty) >= 5, f"expected several point adapters, got {sorted(non_empty)}" - - -# --- synthetic-event tests for the two isolated builders -------------------- - -def test_usgs_quake_event_exposes_top_level_latlon(): - from central.adapters.usgs_quake import USGSQuakeAdapter - - config = AdapterConfig( - name="usgs_quake", enabled=True, cadence_s=60, - settings={}, updated_at=datetime.now(timezone.utc), - ) - adapter = USGSQuakeAdapter(config, MagicMock(), Path("/tmp/never_used.db")) - feature = { - "type": "Feature", "id": "test_q1", - "properties": {"mag": 2.5, "place": "X", "time": 1715000000000, - "updated": 1715000000000}, - "geometry": {"type": "Point", "coordinates": [-116.2, 43.7, 10.5]}, - } - event = adapter._feature_to_event(feature) - assert event is not None - assert event.data["latitude"] == 43.7 - assert event.data["longitude"] == -116.2 - - -def test_nwis_event_mirrors_centroid_into_data(): - from central.adapters.nwis import NWISAdapter - - config = AdapterConfig( - name="nwis", enabled=True, cadence_s=900, - settings={}, updated_at=datetime.now(timezone.utc), - ) - adapter = NWISAdapter(config, MagicMock(), Path("/tmp/never_used.db")) - feature = { - "geometry": {"type": "Point", "coordinates": [-90.25, 41.78]}, # (lon, lat) - "properties": { - "monitoring_location_id": "USGS-05420500", - "time": "2026-05-20T00:00:00Z", - "value": "123.0", - "unit_of_measure": "ft", - }, - } - event = adapter._build_event(feature, "00060") - assert event is not None - # latitude = centroid[1], longitude = centroid[0]; no axis swap. - assert event.data["latitude"] == 41.78 - assert event.data["longitude"] == -90.25 - # Geo.centroid retained for existing rendering uses. - assert event.geo.centroid == (-90.25, 41.78) diff --git a/tests/test_events_feed_frontend.py b/tests/test_events_feed_frontend.py index a2b557c..2ec1700 100644 --- a/tests/test_events_feed_frontend.py +++ b/tests/test_events_feed_frontend.py @@ -684,404 +684,3 @@ class TestEventRowDataAttributes: assert context["events"][0]["adapter"] == "usgs_quake" assert context["events"][0]["category"] == "quake.event" assert context["events"][0]["subject"] == "M4.2 Earthquake" - - -# --- PR L-b: operator /events tab polish --------------------------------- - - -def _events_context(events): - """Minimal context for rendering _events_rows.html as a standalone fragment.""" - return { - "events": events, - "next_cursor": None, - "filter_error": None, - "filter_values": { - "adapter": "", "category": "", "since": "", "until": "", - "region_north": "", "region_south": "", "region_east": "", - "region_west": "", "limit": "50", - }, - } - - -def _event(adapter, inner=None, geometry=None): - """Build an event dict matching _fetch_events output shape. - - `inner` populates payload->data->data (the adapter-specific payload) at - event["data"]["data"]["data"], which the per-adapter partials read. - """ - return { - "id": "evt-" + adapter, - "time": "2026-05-17T12:00:00+00:00", - "received": "2026-05-17T12:00:00+00:00", - "adapter": adapter, - "category": adapter + ".test", - "subject": "subject", - "geometry": geometry, - "geometry_summary": "", - "data": {"data": {"data": inner or {}}}, - "regions": [], - } - - -def _render_rows(events): - """Render _events_rows.html through the real Jinja environment.""" - from central.gui import templates as gui_templates - return gui_templates.env.get_template("_events_rows.html").render( - **_events_context(events) - ) - - -class TestRegistryDrivenAdapterFilter: - """(A) Adapter filter