A local web dashboard that shows a sanctions snapshot by country from OFAC's Sanctions List Service (SLS) API. It downloads OFAC's consolidated sanctions list, caches it locally in SQLite, and renders KPIs and charts in a dark "finance" theme with Chart.js.
On load, the dashboard automatically focuses on the visitor's country (detected from their IP address), with a country selector to browse any other country's exposure.
- Country snapshot — for any country: total entities, breakdown by entity type (individual / organization / vessel / aircraft), top sanctions programs, list membership, and a table of notable entities.
- IP-based initial country — the page opens on the visitor's country, detected client-side with a 3-provider geolocation fallback chain and a 24h local cache. If detection fails, it falls back to the country with the most entities.
- Global overview — top 15 countries by sanctioned entities and the sanctions-list distribution.
- Dark finance theme — pure-black background, navy cards, flat blue primary, per-panel loading skeletons and error/retry states.
- Refreshable data — a Refresh button re-downloads the latest consolidated list from OFAC in the background; the UI polls until it is live again.
app.py Flask server: serves the static frontend + JSON API
fetch.py Downloads the consolidated XML (curl) and parses it (iterparse) into SQLite
db.py SQLite schema and all read queries
static/
index.html Dashboard markup
app.js Frontend logic: data loading, charts, geolocation, refresh
countrymatch.js UMD module: matches geolocated country names to OFAC country names
style.css Dark-finance theme
data/ofac.db Generated SQLite cache (created on first run)
tests/ pytest (Python) + node:test (JS) suites, plus a small XML fixture
Data flow: app.py → on first run (or Refresh), fetch.py downloads the
full consolidated list from https://sanctionslistservice.ofac.treas.gov/entities
(~100 MB XML, takes 1–2 minutes) and streams it into SQLite with
xml.etree.iterparse (memory-safe). All API endpoints then answer from the
SQLite cache, so page loads are instant.
- Python 3.10+ with Flask (
pip install flask) curl(used for the OFAC download)- Node.js 18+ (only for the JS unit tests)
- Internet access to OFAC (data) and a CDN (Chart.js)
pip install flask # if not already installed
python app.py # serves on http://127.0.0.1:8080Override the port or database location with environment variables:
PORT=8787 OFAC_DB=/tmp/ofac.db python app.pyOn the very first request the server kicks off a background download of the
consolidated list. The page shows a "Fetching consolidated OFAC data" state and
polls until it is ready. Subsequent requests read straight from data/ofac.db.
| Endpoint | Description |
|---|---|
GET / |
Dashboard frontend |
GET /api/meta |
ready flag, refreshing state, as-of date, entity/country/program/list totals |
GET /api/countries |
All countries with entity/individual/organization/vessel counts |
GET /api/country/<name> |
Country snapshot: type breakdown, top programs, lists, notable entities |
GET /api/programs |
Top 20 sanctions programs by entity count |
GET /api/lists |
Sanctions-list distribution |
POST /api/refresh |
Start a background re-download of the consolidated list (single-flight) |
All responses are JSON. GET /api/country/<name> returns 404 for unknown
countries.
- On load,
app.jsbuilds the country dropdown from/api/countries. - It asks for the visitor's country via a fallback chain of free geolocation
endpoints (
ip-api.com→ipwho.is→ipapi.co), each with an 8s timeout. - The result is cached in
localStoragefor 24h so free-API rate limits are never hit on repeat visits. countrymatch.jsmatches the geolocated name against OFAC's country names (exact match, token-set matching for reversed names like "Korea, North" vs "North Korea", plus aliases like Myanmar → Burma).- If the user manually picks a country, that choice persists across a data refresh; a full page reload returns to the visitor's country.
- If geolocation fails entirely, the dashboard falls back to the top country.
OFAC updates the list frequently. Click Refresh in the header — or
curl -X POST http://127.0.0.1:8080/api/refresh — to re-download the latest
consolidated list in the background. A progress modal appears while it runs,
showing a live progress bar plus a byte counter in MB during the download
("Downloading… 12.0 of 100.0 MB") and an entity count while parsing
("Parsing… 15,000 entities"). It disappears once the data is live and the
dashboard re-renders with the new as-of date. The same modal is shown on the
very first fetch.
python3 -m pytest tests/ -v # parser, DB queries, API endpoints
node --test tests/countrymatch.test.js # country-name matchingThe Python tests parse a small fixture (tests/fixtures/sample.xml) and use a
seeded temp database — no live network calls. To run all checks:
python3 -m pytest tests/ -q && node --test tests/countrymatch.test.js- Official source: the data comes from OFAC's public SLS API. Use it for research and awareness — always screen against OFAC's official list publications for compliance decisions.
- Consolidated list: the dashboard ingests the full consolidated dataset (SDN + Non-SDN lists together).
- Refresh is a full re-download (~100 MB, 1–2 min). Incremental delta pulls
via
/changes/latestare a possible future improvement. - Static files are served with
Cache-Control: no-cacheso the dashboard always runs the latest frontend code after an update.