Generate SH5-style HTML statistics pages from amateur radio contest ADIF logs.
Parses an ADIF file, enriches QSOs with country/prefix/distance data from cty.dat, detects duplicates, and renders a full set of interactive HTML statistics pages with charts, maps, and tables.
- ADIF parsing with mode-group classification (CW/PH/DIG)
- Country, continent, CQ/ITU zone lookup via
cty.dat - WPX prefix extraction
- Duplicate detection (per-band-mode, per-band, or all-band-mode); POTA-aware (N-fer hunts at different parks are not dupes)
- Distance and beam heading in km or miles (defaults to mi for US callsigns)
- Operator privacy/anonymization mode
- MASTER.SCP / LOTW / FCC ULS / Census ZCTA / BigCTY downloaded on demand via
--update-data - POTA park-grid override for portable ops; Parks page with per-park stats; clickable
pota.applinks on the map - LCR (Log Checking Report) error overlay (HTML and PDF formats)
- Configurable callsign lookup hyperlinks (QRZ or HamQTH)
- KMZ export for Google Earth
- 30+ HTML output pages with charts and interactive maps
Requires Python 3.8+.
pip install -r requirements.txtOptional, for PDF LCR support:
pip install pypdfAll data files live in ./databases/. Fetch them once:
python generate_stats.py --update-dataThis downloads BigCTY (cty.dat + cty.csv), MASTER.SCP, the LOTW user list, the FCC ULS amateur database, and Census ZCTA centroids into ./databases/. cty.dat is required; the script faults with a pointer to --update-data if it is missing.
python generate_stats.py <adif_file> [options]Options:
| Option | Description |
|---|---|
--update-data |
Download/refresh all data files into ./databases/ and exit |
--output-dir, -o |
Output directory (default: auto-named under ./stats_output/) |
--callsign, -c |
Station callsign (overrides ADIF header) |
--locator, -l |
Station Maidenhead locator (overrides ADIF header) |
--contest |
Contest name (overrides ADIF header) |
--cty-dat |
Path to cty.dat file (overrides ./databases/cty.dat) |
--hide-operators |
Anonymize operator names in output |
--dupe-rule |
Dupe detection: per_band_mode (default), per_band, or all_band_mode |
--lcr |
Path to LCR file for error overlay |
--master-scp |
Path to MASTER.SCP file |
--lotw |
Path to LOTW user activity CSV file |
--no-download |
Skip auto-download of MASTER.SCP and LOTW when missing |
--callsign-lookup |
Callsign lookup service: qrz (default) or hamqth |
--no-qrz |
Skip QRZ.com lookups for grid squares |
--qrz-cfg |
Path to QRZ settings file (default: ~/qrz_settings.cfg) |
--units |
Distance units km or mi (default: mi for US callsigns, else km) |
--photos |
Directory of operator portraits (matched by callsign stem; prefers .jpg over .png) |
--remote |
Remote user@host:/path for a hint-only scp command in the output |
--post-script |
Path to an executable invoked after generation with the absolute output dir as $1; see examples/post-script.sh.sample |
Example:
python generate_stats.py mylog.adi -c W4TA -l EM73 --lcr errors.lcrOutput directory is auto-named (e.g. stats_output/2026-APR-18-POTA-US-1880-W4AFC/ for a POTA activation, or stats_output/2026-APR-W4TA-ARRL-FIELD-DAY/ otherwise). Override with -o.
QRZ lookups require a QRZ.com XML Logbook Data subscription. Create ~/qrz_settings.cfg in INI format:
[qrz]
username=YOURCALL
password=YOURPASSWORDThe file is read by default; override the location with --qrz-cfg. Skip QRZ lookups entirely with --no-qrz. Keep this file out of version control — credentials are cached per-callsign in ~/.publicLogProcessor/qrz_cache.json so the password is only transmitted on the first lookup per call.
Standalone tool to analyze Log Checking Report errors against your ADIF log by operator, band, and time of day.
python analyze_lcr.py <lcr_file> <adif_file>Supports .lcr, .html, and .pdf LCR formats.
All data files live in ./databases/ (gitignored). Populate with python generate_stats.py --update-data.
| File | Source | Required |
|---|---|---|
cty.dat, cty.csv |
country-files.com BigCTY | Yes — script faults without cty.dat |
MASTER.SCP |
supercheckpartial.com | No (used for "Not in master") |
| LOTW users | ARRL LOTW | No (LOTW check page skipped when absent) |
fcc_amateur.db |
Built from FCC ULS by fcc_uls_to_sqlite.py |
No (US callsigns fall through to QRZ) |
zcta_centroids.csv |
U.S. Census Gazetteer | No (used by FCC zip→grid) |
pota_parks.csv |
pota.app — lazy-loaded only when the ADIF has POTA refs | No (auto-downloaded on first POTA log) |
vuccgrids.dat |
TQSL (manual placement) | No (DXCC grid validation disabled when absent) |
Core: Index, Summary, Full Log, Operators, Dupes
Geographic & Rate Analysis: All Callsigns, Rates, Countries, Countries by Time (all + per-band), QSOs per Station, Passed QSOs, QSOs by Hour (sheet + graphs, all + per-band), QSOs by Minute, One Minute Rates, Prefixes, Distance, Beam Heading, Break Time, Continents, Fields Map, Callsign Length, Callsign Structure, CQ Zones, ITU Zones, Not in Master, Possible Errors, LOTW Check
Charts & Maps: QSOs by Band, QSOs by Mode, Top 10 Countries, Continents, Beam Heading, Frequencies, Interactive Map, KMZ Export
POTA (only when the ADIF contains POTA references): Parks page (activator- and hunter-side tables with park name / grid / state / QSO count), pota.app links on map popups, park-grid overrides for portable ops, POTA-aware dupe detection, auto-named output directory with date + activated park
Optional:
LCR Error Summary (when --lcr provided)
Note: These are hardening suggestions. You assume all the risk to ensure your website is well-protected. WHile every effort has been extended to ensure these steps will help protect the pages generated by these scripts, you assume the ultimate responsibility to ensure they are safe.
The generated pages already include a CSP meta tag, rel="noopener noreferrer" on external links, and a referrer policy. Clickjacking protection (frame-ancestors / X-Frame-Options) must be set as an HTTP response header — meta-tag equivalents are ignored by browsers. Set these once at your webserver level; they'll cover every log you publish.
Apache (site config or .htaccess with AllowOverride All):
Header always set X-Frame-Options "DENY"
Header always set Content-Security-Policy "frame-ancestors 'none';"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"nginx (inside the server block):
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none';" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;If you serve over HTTPS, also set HSTS (Strict-Transport-Security: max-age=63072000; includeSubDomains) once you're confident the cert is stable.
Ready-to-deploy .htaccess and matching stylesheet for the directory index are in websiteFormattingTools/ — see its README.
pip install pytest
pytest tests/The test suite includes a bundled cty.dat in tests/data/ so no external files are needed.
GPL-3.0. See LICENSE.