From e76bb4d9598dcd129eb0c51943a1d6488a57a240 Mon Sep 17 00:00:00 2001 From: nullhack Date: Thu, 23 Jul 2026 01:15:04 -0400 Subject: [PATCH] fix(dashboard): reconstruct GDACS source links for reports without link field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two GDACS reports in production had empty source_link URLs, causing the dashboard to redirect back to itself when clicked: - 20251221-XX-DR Drought Green (degenerate RSS — only alertlevel/severity/ population/episodeid in raw_fields; no link/eventid/eventtype) - 20260702-PT-WF Forest Fire Aveiro (newer RSS shape — URL lives in raw_fields.url as a dict {geometry, report, details}, not in a link field) Root cause: generate_dashboard_data.py:494 read only rf.get('link', ''). Reports without a 'link' field produced url='' which the frontend rendered as href='' (browser navigates to the current page). Fix: - _resolve_gdacs_link(raw_fields, source_id, incident_type) tries three sources in order: (1) rf['link'] (43/45 reports), (2) rf['url']['report'] dict sub-key (Portugal case), (3) construct from source_id + episodeid + eventtype code derived from incident_type via _GDACS_EVENTTYPE_CODES reverse map (drought case — confirmed opens the correct GDACS page). - app.js safety net: render a disabled (⊘) instead of when L.url is empty, so any future empty-URL case can't redirect to self. Tests: 5 new TestResolveGdacsLink covering all three fallback paths plus the empty case. 256 tests pass, pyright 0 errors, ruff clean (4 pre-existing C901). --- dashboard/app.js | 6 +-- scripts/generate_dashboard_data.py | 37 ++++++++++++++++++- .../generate_dashboard_data_test.py | 27 +++++++++++++- .../generate_dashboard_data_test.pyi | 8 ++++ 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/dashboard/app.js b/dashboard/app.js index 2e10fc32..bdb21651 100644 --- a/dashboard/app.js +++ b/dashboard/app.js @@ -817,11 +817,11 @@ function openDrawer(id) { const i = STATE.digest.incidents.find((x) => x.incide

Original source records (${i.source_links.length})

` : `

No deep links available for this incident.

`} ${(i.logs && i.logs.length) ? `

Timeline · ${i.logs.length} log(s)

diff --git a/scripts/generate_dashboard_data.py b/scripts/generate_dashboard_data.py index 1328234b..0abbed4e 100644 --- a/scripts/generate_dashboard_data.py +++ b/scripts/generate_dashboard_data.py @@ -345,6 +345,41 @@ def _resolve_region(name: str, latest_summary: str | None, is_disease: bool) -> return "Global" if is_disease else "Unknown" +_GDACS_EVENTTYPE_CODES = { + "Tropical Cyclone": "TC", + "Earthquake": "EQ", + "Flood": "FL", + "Forest Fire": "WF", + "Drought": "DR", + "Tsunami": "TS", + "Volcano": "VO", +} + + +def _resolve_gdacs_link( + raw_fields: dict[str, object], + source_id: str, + incident_type: str, +) -> str: + link = raw_fields.get("link", "") + if link: + return str(link) + url = raw_fields.get("url") + if isinstance(url, dict): + report = url.get("report", "") + if report: + return str(report) + eventid = str(raw_fields.get("eventid") or source_id or "") + episodeid = str(raw_fields.get("episodeid") or "") + eventtype = _GDACS_EVENTTYPE_CODES.get(incident_type, "") + if eventid and episodeid and eventtype: + return ( + f"https://www.gdacs.org/report.aspx?eventid={eventid}" + f"&episodeid={episodeid}&eventtype={eventtype}" + ) + return "" + + def build_incident_object(store: ContentStore, inc: dict, as_of_date: datetime) -> dict | None: incident_id = inc["incident_id"] reports = load_reports_for_incident(store, incident_id) @@ -491,7 +526,7 @@ def build_incident_object(store: ContentStore, inc: dict, as_of_date: datetime) source_links.append({ "type": "GDACS", "label": f"{alert_label} · {rf.get('severitytext', r['name'])}" if alert_label else r["name"], - "url": rf.get("link", ""), + "url": _resolve_gdacs_link(rf, r["source_id"], r["incident_type"]), "meta": " · ".join(meta_parts) if meta_parts else "", }) elif r["source"] == "WHO": diff --git a/tests/integration/generate_dashboard_data_test.py b/tests/integration/generate_dashboard_data_test.py index 31b8f17f..69925ff4 100644 --- a/tests/integration/generate_dashboard_data_test.py +++ b/tests/integration/generate_dashboard_data_test.py @@ -2,7 +2,7 @@ from datetime import datetime, timezone -from scripts.generate_dashboard_data import _resolve_region, generate_md_report +from scripts.generate_dashboard_data import _resolve_gdacs_link, _resolve_region, generate_md_report class TestResolveRegion: @@ -24,6 +24,31 @@ def test_disease_with_country_in_summary_returns_that_region(self) -> None: assert _resolve_region("Disease Japan 2025-01-01", summary, True) == "Asia" +class TestResolveGdacsLink: + def test_link_field_used_directly_when_present(self) -> None: + rf: dict[str, object] = {"link": "https://www.gdacs.org/report.aspx?eventid=1&episodeid=2&eventtype=EQ"} + assert _resolve_gdacs_link(rf, "1", "Earthquake") == rf["link"] + + def test_url_dict_report_subkey_used_when_no_link(self) -> None: + rf: dict[str, object] = {"url": {"report": "https://www.gdacs.org/report.aspx?eventid=2&episodeid=3&eventtype=WF"}} + assert _resolve_gdacs_link(rf, "2", "Forest Fire") == "https://www.gdacs.org/report.aspx?eventid=2&episodeid=3&eventtype=WF" + + def test_constructed_from_source_id_and_episodeid_for_degenerate_drought(self) -> None: + rf: dict[str, object] = {"alertlevel": "Green", "severity": "Minor", "population": 0, "episodeid": "14"} + url = _resolve_gdacs_link(rf, "1018332", "Drought") + assert url == "https://www.gdacs.org/report.aspx?eventid=1018332&episodeid=14&eventtype=DR" + + def test_eventid_in_raw_fields_preferred_over_source_id(self) -> None: + rf: dict[str, object] = {"eventid": "999", "episodeid": "5", "eventtype": "FL"} + url = _resolve_gdacs_link(rf, "ignored", "Flood") + assert "eventid=999" in url + assert "episodeid=5" in url + + def test_returns_empty_when_nothing_reconstructable(self) -> None: + rf: dict[str, object] = {} + assert _resolve_gdacs_link(rf, "", "Drought") == "" + + def _make_incident( incident_id: str = "x", name: str = "Earthquake M7.5 Yumare, Venezuela 2026-06-24", diff --git a/tests/integration/generate_dashboard_data_test.pyi b/tests/integration/generate_dashboard_data_test.pyi index 00882369..78442207 100644 --- a/tests/integration/generate_dashboard_data_test.pyi +++ b/tests/integration/generate_dashboard_data_test.pyi @@ -6,6 +6,14 @@ class TestResolveRegion: def test_disease_with_country_in_summary_returns_that_region(self) -> None: ... +class TestResolveGdacsLink: + def test_link_field_used_directly_when_present(self) -> None: ... + def test_url_dict_report_subkey_used_when_no_link(self) -> None: ... + def test_constructed_from_source_id_and_episodeid_for_degenerate_drought(self) -> None: ... + def test_eventid_in_raw_fields_preferred_over_source_id(self) -> None: ... + def test_returns_empty_when_nothing_reconstructable(self) -> None: ... + + class TestGenerateMdReportDayPrimary: def test_day_primary_shows_3_most_recent_days(self) -> None: ... def test_each_incident_shows_only_that_day_log(self) -> None: ...