"""Reads audience composition straight from assets/follower_directory.html's DATA array, so the dashboard and PDF reports show the same numbers as the hand-maintained Follower Study/Directory instead of a second, independently hand-computed set that can drift out of sync with it. Deliberately has zero import-time dependency on app.py, same reasoning as report.py's module docstring: app.py runs as "__main__", so importing it here would re-execute it as a second, still-mid-import copy. """ import json import os import re from collections import Counter def load_directory(assets_dir): """Parses the `const DATA = [...];` array out of follower_directory.html into a list of dicts. Returns an empty list if the file is missing rather than raising, since a fresh checkout or a stripped-down deployment may not have it yet.""" path = os.path.join(assets_dir, "follower_directory.html") if not os.path.exists(path): return [] text = open(path, encoding="utf-8").read() match = re.search(r"const DATA = (\[.*?\]);", text, re.S) if not match: return [] return json.loads(match.group(1)) def composition_stats(data, month_value=None, top_countries=None): """Sector/seniority/function/country breakdowns, either over the whole audited set (month_value=None) or restricted to the entries whose `followedSort` matches month_value (e.g. "2026-07"), i.e. that month's own follower cohort as profiled so far. A month's count only ever grows as later audit passes catch up, it is a coverage figure, not a final one. top_countries defaults to unbounded (None = every country shown): this was previously hardcoded to 10, which silently dropped tail countries from a monthly cohort's breakdown while the "total" count kept counting them, the table's own sum fell short of the total it was supposed to explain (caught when adding Sabine E. Herlitschka's July correction pushed the cohort to 11 distinct countries).""" rows = data if month_value is None else [d for d in data if d.get("followedSort") == month_value] return { "total": len(rows), "sector": Counter(d.get("sector", "Unknown") for d in rows), "seniority": Counter(d.get("seniority", "Unknown") for d in rows), "function": Counter(d.get("function", "Unknown") for d in rows), "country": Counter(d.get("country") or "Unknown/blank" for d in rows).most_common(top_countries), }