← Method & Data

Internal design document, rendered for transparency. Operational sections (deployment, job recipes, credentials) have been removed. Dated 2026-10-21; may be amended by later work recorded in the changes log.

Conflict incident map — design (2026-10-21)

A new tab in the status dashboard (sahel-status, FastAPI + static pages) that shows UCDP conflict incidents on a Mapbox map with GDELT news-report context, lets the viewer pick a date range, shows three lines on hover, and opens one incident in a new tab with everything we hold about it. A local demo with real data (Mac folder GDELT-MAP-DEMO-2026-10-21/) settled the interaction; this document turns it into the cluster version.

1. Intent and why

The project manager wants to see the two conflict sources together. The earlier map in an old application failed because every fact was crammed into hover panels. The rule here: the map answers "where and when", the hover answers "what, briefly", and the incident page answers everything else. The incident page is also the future home for "what the channels said" (posts and news tied to an incident by the incident study), so it must exist as a page with a stable address, not a popup.

2. Goals, non-goals, anti-goals

Goals

  1. A tab in the dashboard domain (same basic auth as the rest of the site).
  2. Every UCDP incident as a dot (size = best death estimate); GDELT violent reports as region shading; a third, optional layer showing per region whether days were seen by both sources, UCDP only, or GDELT only.
  3. A date range (from / to, plus 30 d / 90 d / 1 y / all presets). Dots use exact dates. Shading and coverage are summed over the exact days in the range (the demo could only do whole months; the cluster version has daily tables).
  4. Hover on a dot: three lines, nothing else. Click: the incident page opens in a new browser tab.
  5. Incident page: what UCDP recorded, the linked GDELT reports, the region's context for that month, and an empty, clearly labelled box reserved for posts and news.
  6. Data comes from the live database; no manual export step; nothing to run by hand after deploy except creating one secret.

Non-goals

Anti-goals (would make it a failure even if it works)

3. Constraints (from the codebase and the environment)

4. What gets built

4.1 Names and addresses

Thing Name
Nav tab label Conflicts on Map (between Graph and Roadmap; user's choice 2026-10-21)
Map page /conflict.html
Incident page /conflict-incident.html?id=<ucdp event id>
Page script assets/conflict.js (map) and assets/conflict-incident.js (page); shared helpers may live in assets/conflict-common.js
Vendored library assets/vendor/mapbox-gl.js, assets/vendor/mapbox-gl.css (v3.33.0, the current release on 2026-10-21)
Region shapes static/data/regions-gadm41-admin1.geojson
API /api/conflict/config, /api/conflict/incidents, /api/conflict/region-days, /api/conflict/incident/{id}
Python module src/sahel_de/status/conflict_map.py (pure payload builders + cache + refresher, modelled on graph3d.py)

4.2 Data path (my call: read the database directly, cache in the process)

4.3 Map page behaviour (as validated in the demo)

4.4 Incident page behaviour

4.5 Dashboard integration

5. Decisions the user made (verbatim intent)

6. Calls I made (check these while reading)

  1. Read the database directly with a 30-minute in-process refresher instead of a nightly export Job. Reason: the dashboard already has the read-only DSN and the pattern (graph3d); an export would add a Job, an S3 prefix and a staleness tile for no gain at this data size.
  2. Daily aggregation for shading and coverage (the demo used months).
  3. Group linked reports by outlet on the incident page, cap the rows at 2,000, show the true total.
  4. The client, not the server, sums the region-days over the chosen range.
  5. Paths and identifiers use conflict, because incidents is taken on this site.
  6. Unpublished UCDP days are a fourth, grey "not yet comparable" class in the coverage layer, excluded from the majority vote (reviewer's finding; my call, since the anti-goals in §2 leave no other honest option).
  7. Payloads are filtered to the current pipeline version and the newest link version, both reported in meta.
  8. unlocated GDELT rows are carried in the payload and shown as a count, never dropped silently.

7. Known data limits the page must state, not hide

8. Left to the builder

Exact SQL and indexes used (read-only; no migration expected — if a query needs an index, that is a separate PR), payload encoding, cache object shape, JS file structure, quantile break computation, how the outlet groups expand, and test fixtures.

9. Questions asked and answered (2026-10-21)

Rendered from 2026-10-21-conflict-map-design.md · changes log