| from __future__ import annotations | |
| import gzip | |
| import json | |
| import re | |
| from html import escape | |
| from pathlib import Path | |
| import gradio as gr | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| ROOT = Path(__file__).resolve().parent | |
| DATA = ROOT / "data" | |
| AUDIT = DATA / "audit" | |
| DATASET = DATA / "dataset_bundle" | |
| MANIFEST = json.loads((DATA / "public_release_manifest.json").read_text(encoding="utf-8")) | |
| CLAIM_BOUNDARY = json.loads((AUDIT / "claim_boundary.json").read_text(encoding="utf-8")) | |
| SOURCE_GAPS = json.loads((AUDIT / "source_gap_report.json").read_text(encoding="utf-8")) | |
| FONT_FAMILY = "Arial, Helvetica, sans-serif" | |
| def style_figure(fig): | |
| fig.update_layout( | |
| template="plotly_white", | |
| font=dict(family=FONT_FAMILY, size=14, color="#1f2933"), | |
| title_font=dict(family=FONT_FAMILY, size=18, color="#0f172a"), | |
| legend=dict(font=dict(family=FONT_FAMILY, size=13, color="#334155")), | |
| hoverlabel=dict(font_size=13, font_family=FONT_FAMILY), | |
| paper_bgcolor="#ffffff", | |
| plot_bgcolor="#ffffff", | |
| ) | |
| fig.update_xaxes( | |
| title_font=dict(family=FONT_FAMILY, size=14, color="#334155"), | |
| tickfont=dict(family=FONT_FAMILY, size=13, color="#334155"), | |
| gridcolor="#e7edf5", | |
| linecolor="#cbd5e1", | |
| ) | |
| fig.update_yaxes( | |
| title_font=dict(family=FONT_FAMILY, size=14, color="#334155"), | |
| tickfont=dict(family=FONT_FAMILY, size=13, color="#334155"), | |
| gridcolor="#e7edf5", | |
| linecolor="#cbd5e1", | |
| ) | |
| return fig | |
| def read_jsonl_gz(name: str, limit: int | None = None) -> pd.DataFrame: | |
| rows = [] | |
| with gzip.open(DATASET / name, "rt", encoding="utf-8") as fh: | |
| for idx, line in enumerate(fh): | |
| if limit is not None and idx >= limit: | |
| break | |
| rows.append(json.loads(line)) | |
| return pd.DataFrame(rows) | |
| def pct(value) -> float | None: | |
| try: | |
| return float(value) * 100.0 | |
| except (TypeError, ValueError): | |
| return None | |
| def fmt_int(value) -> str: | |
| try: | |
| return f"{int(value):,}" | |
| except (TypeError, ValueError): | |
| return str(value or "") | |
| def html_escape(value) -> str: | |
| if value is None: | |
| return "" | |
| try: | |
| if pd.isna(value): | |
| return "" | |
| except (TypeError, ValueError): | |
| pass | |
| return escape(str(value)) | |
| def fmt_money(value) -> str: | |
| if value is None: | |
| return "" | |
| try: | |
| if pd.isna(value): | |
| return "" | |
| except (TypeError, ValueError): | |
| pass | |
| text = str(value).strip() | |
| if not text: | |
| return "" | |
| try: | |
| number = float(value) | |
| except (TypeError, ValueError): | |
| return html_escape(value) | |
| if pd.isna(number): | |
| return "" | |
| prefix = "-$" if number < 0 else "$" | |
| number = abs(number) | |
| if number >= 1_000_000_000: | |
| return f"{prefix}{number / 1_000_000_000:.2f}B" | |
| if number >= 1_000_000: | |
| return f"{prefix}{number / 1_000_000:.2f}M" | |
| if number >= 1_000: | |
| return f"{prefix}{number / 1_000:.0f}K" | |
| return f"{prefix}{number:,.0f}" | |
| def present_text(value, fallback: str = "not available") -> str: | |
| text = html_escape(value).strip() | |
| return text if text else fallback | |
| def normalize_text(value) -> str: | |
| text = str(value or "").lower() | |
| text = re.sub(r"[^a-z0-9]+", " ", text) | |
| return re.sub(r"\s+", " ", text).strip() | |
| COMMON_NAME_ALIASES = { | |
| "bill": ["william"], | |
| "bob": ["robert"], | |
| "chuck": ["charles"], | |
| "dick": ["richard"], | |
| "jim": ["james"], | |
| "jack": ["john"], | |
| "jeff": ["jeffrey"], | |
| "jon": ["john", "jonathan", "thomas"], | |
| "maggie": ["margaret"], | |
| "mitch": ["mitchell"], | |
| "rick": ["richard"], | |
| "ted": ["theodore", "edward", "rafael"], | |
| "tom": ["thomas"], | |
| "tommy": ["thomas"], | |
| "bernie": ["bernard"], | |
| } | |
| def query_variants(query: str) -> list[str]: | |
| base = normalize_text(query) | |
| if not base: | |
| return [] | |
| variants = {base} | |
| tokens = base.split() | |
| for idx, token in enumerate(tokens): | |
| for alias in COMMON_NAME_ALIASES.get(token, []): | |
| changed = tokens.copy() | |
| changed[idx] = alias | |
| variants.add(" ".join(changed)) | |
| return sorted(variants, key=lambda value: (value != base, value)) | |
| def last_name_from_display(value: str) -> str: | |
| tokens = [ | |
| token | |
| for token in normalize_text(value).split() | |
| if token not in {"jr", "sr", "ii", "iii", "iv", "v"} | |
| ] | |
| return tokens[-1] if tokens else "" | |
| def has_all_tokens(text: str, tokens: list[str]) -> bool: | |
| padded = f" {text} " | |
| return all(f" {token} " in padded for token in tokens if token) | |
| def data_status(row) -> str: | |
| filings = int(row.get("filings", 0) or 0) | |
| transactions = int(row.get("transactions", 0) or 0) | |
| wealth = int(row.get("wealth_ranges", 0) or 0) | |
| if filings or transactions or wealth: | |
| return "disclosure rows found" | |
| if bool(row.get("in_roster", False)): | |
| return "roster row; no disclosure rows in this public bundle" | |
| return "disclosure filer row; no transaction or wealth rows" | |
| def public_empty_html(title: str, body: str) -> str: | |
| return ( | |
| "<div class='result-card empty-state'>" | |
| f"<h2>{html_escape(title)}</h2>" | |
| f"<p>{html_escape(body)}</p>" | |
| "</div>" | |
| ) | |
| def public_table_html( | |
| frame: pd.DataFrame | None, | |
| title: str, | |
| empty_title: str, | |
| empty_body: str, | |
| max_rows: int = 12, | |
| ) -> str: | |
| if frame is None or frame.empty: | |
| return public_empty_html(empty_title, empty_body) | |
| display = frame.head(int(max_rows)).fillna("").copy() | |
| headers = "".join(f"<th>{html_escape(col)}</th>" for col in display.columns) | |
| body_rows = [] | |
| for _, row in display.iterrows(): | |
| cells = "".join(f"<td>{html_escape(row[col])}</td>" for col in display.columns) | |
| body_rows.append(f"<tr>{cells}</tr>") | |
| more = "" | |
| if len(frame) > max_rows: | |
| more = f"<p class='table-note'>Showing {fmt_int(max_rows)} of {fmt_int(len(frame))} matching rows.</p>" | |
| return ( | |
| "<div class='result-card'>" | |
| f"<h2>{html_escape(title)}</h2>" | |
| "<div class='public-table-wrap'>" | |
| "<table class='public-table'>" | |
| f"<thead><tr>{headers}</tr></thead>" | |
| f"<tbody>{''.join(body_rows)}</tbody>" | |
| "</table>" | |
| "</div>" | |
| f"{more}" | |
| "</div>" | |
| ) | |
| def public_filer_cards_html( | |
| frame: pd.DataFrame | None, | |
| empty_title: str, | |
| empty_body: str, | |
| max_rows: int = 8, | |
| ) -> str: | |
| if frame is None or frame.empty: | |
| return public_empty_html(empty_title, empty_body) | |
| cards = [] | |
| display = frame.head(int(max_rows)).fillna("").copy() | |
| for _, row in display.iterrows(): | |
| status = str(row.get("Record status", "")) | |
| if "roster row; no disclosure rows" in status: | |
| status_text = "Roster match only; no disclosure rows under this exact ID" | |
| elif "disclosure rows found" in status: | |
| status_text = "Disclosure rows found" | |
| else: | |
| status_text = status or "Record status unavailable" | |
| lag = str(row.get("Typical filing lag (days)", "")).strip() | |
| lag_text = f"{lag} day typical filing lag" if lag else "filing lag not available" | |
| counts = [ | |
| f"{fmt_int(row.get('Public forms', 0))} public forms", | |
| f"{fmt_int(row.get('Trade rows', 0))} trade rows", | |
| f"{fmt_int(row.get('Wealth summaries', 0))} wealth summaries", | |
| lag_text, | |
| ] | |
| count_html = "".join(f"<span>{html_escape(item)}</span>" for item in counts) | |
| meta_parts = [part for part in [row.get("Party", ""), row.get("State", "")] if str(part).strip()] | |
| meta = f"<span class='filer-meta'>{html_escape(' - '.join(str(part) for part in meta_parts))}</span>" if meta_parts else "" | |
| why = str(row.get("Why shown", "")).strip() | |
| why_html = f"<p>{html_escape(why)}</p>" if why else "" | |
| cards.append( | |
| "<article class='filer-match-card'>" | |
| f"<div><h3>{html_escape(row.get('Matching record', ''))}</h3>{meta}</div>" | |
| f"<strong class='filer-status'>{html_escape(status_text)}</strong>" | |
| f"<div class='filer-counts'>{count_html}</div>" | |
| f"{why_html}" | |
| "</article>" | |
| ) | |
| more = "" | |
| if len(frame) > max_rows: | |
| more = f"<p class='table-note'>Showing {fmt_int(max_rows)} of {fmt_int(len(frame))} matching public records.</p>" | |
| return ( | |
| "<div class='result-card filer-results-card'>" | |
| "<h2>Matching records</h2>" | |
| "<p>If more than one record appears, use the one that says disclosure rows were found.</p>" | |
| f"<div class='filer-card-list'>{''.join(cards)}</div>" | |
| f"{more}" | |
| "</div>" | |
| ) | |
| def basis_label(value: str) -> str: | |
| labels = { | |
| "filing_date": "after public filing date", | |
| "transaction_date": "after reported transaction date", | |
| } | |
| return labels.get(str(value), str(value).replace("_", " ")) | |
| def window_label(value: str) -> str: | |
| return str(value).replace("_trading_days", " trading days").replace("_", " ") | |
| def claim_label(value: str) -> str: | |
| labels = { | |
| "blocked_market_data_policy": "Not enough public market data for a market-beating claim", | |
| "blocked": "Not enough public data for a market-beating claim", | |
| } | |
| return labels.get(str(value), str(value).replace("_", " ")) | |
| RETURN_TESTS = pd.read_csv(DATASET / "return_tests.csv") | |
| RETURN_TESTS["equal_weight_alpha_pct"] = RETURN_TESTS["alpha_equal_weight"].map(pct) | |
| RETURN_TESTS["midpoint_weight_alpha_pct"] = RETURN_TESTS["alpha_midpoint"].map(pct) | |
| RETURN_TESTS["ci_low_pct"] = RETURN_TESTS["bootstrap_ci_low"].map(pct) | |
| RETURN_TESTS["ci_high_pct"] = RETURN_TESTS["bootstrap_ci_high"].map(pct) | |
| RETURN_TESTS["date_basis_label"] = RETURN_TESTS["event_date_source"].map(basis_label) | |
| RETURN_TESTS["window_label"] = RETURN_TESTS["window"].map(window_label) | |
| RETURN_TESTS["claim_status_label"] = RETURN_TESTS["claim_status"].map(claim_label) | |
| RETURN_HANDOFF = pd.read_csv(DATA / "reporter_handoff" / "return_test_summary_handoff.csv") | |
| TRADES = read_jsonl_gz("trades.jsonl.gz") | |
| WEALTH = read_jsonl_gz("net_worth_intervals.jsonl.gz") | |
| FILINGS = read_jsonl_gz("filings.jsonl.gz") | |
| SENATORS = pd.read_csv(DATASET / "senators.csv") | |
| TRADES["transaction_year"] = pd.to_datetime(TRADES["transaction_date"], errors="coerce").dt.year | |
| TRADES["filing_lag_days_num"] = pd.to_numeric(TRADES.get("filing_lag_days"), errors="coerce") | |
| TRADES["amount_midpoint_num"] = pd.to_numeric(TRADES.get("amount_midpoint"), errors="coerce") | |
| TRADES["ticker_display"] = TRADES["ticker_candidate"].fillna("").replace("", "(no ticker)") | |
| WEALTH["report_year_num"] = pd.to_numeric(WEALTH.get("report_year"), errors="coerce") | |
| for col in ["net_worth_min", "net_worth_midpoint", "net_worth_max", "uncertainty_width"]: | |
| WEALTH[col] = pd.to_numeric(WEALTH.get(col), errors="coerce") | |
| def title_name(value: str) -> str: | |
| text = str(value or "").replace("-", " ").strip() | |
| return " ".join(part.capitalize() for part in text.split()) | |
| def build_filer_index() -> pd.DataFrame: | |
| filing_counts = FILINGS.groupby("senator_id").size().rename("filings") | |
| trade_counts = TRADES.groupby("senator_id").size().rename("transactions") | |
| wealth_counts = WEALTH.groupby("senator_id").size().rename("wealth_ranges") | |
| median_lag = TRADES.groupby("senator_id")["filing_lag_days_num"].median().round().rename("median_filing_lag_days") | |
| ids = sorted(set(FILINGS["senator_id"]) | set(TRADES["senator_id"]) | set(WEALTH["senator_id"]) | set(SENATORS["senator_id"])) | |
| frame = pd.DataFrame({"senator_id": ids}) | |
| frame = frame.merge(filing_counts, on="senator_id", how="left") | |
| frame = frame.merge(trade_counts, on="senator_id", how="left") | |
| frame = frame.merge(wealth_counts, on="senator_id", how="left") | |
| frame = frame.merge(median_lag, on="senator_id", how="left") | |
| roster = SENATORS[["senator_id", "display_name", "state", "party"]].copy() | |
| frame = frame.merge(roster, on="senator_id", how="left") | |
| filing_names = ( | |
| FILINGS.assign( | |
| filing_display=( | |
| FILINGS.get("filer_first_name", "").fillna("").map(title_name) | |
| + " " | |
| + FILINGS.get("filer_last_name", "").fillna("").map(title_name) | |
| ).str.strip() | |
| ) | |
| .groupby("senator_id")["filing_display"] | |
| .agg(lambda values: values.dropna().iloc[0] if len(values.dropna()) else "") | |
| ) | |
| frame = frame.merge(filing_names.rename("filing_display"), on="senator_id", how="left") | |
| frame["display_name"] = frame["display_name"].fillna(frame["filing_display"]).fillna("") | |
| frame.loc[frame["display_name"].eq(""), "display_name"] = frame.loc[frame["display_name"].eq(""), "senator_id"] | |
| frame["in_roster"] = frame["senator_id"].isin(set(SENATORS["senator_id"])) | |
| for col in ["filings", "transactions", "wealth_ranges"]: | |
| frame[col] = frame[col].fillna(0).astype(int) | |
| frame["median_filing_lag_days"] = frame["median_filing_lag_days"].fillna("") | |
| frame["state"] = frame["state"].fillna("") | |
| frame["party"] = frame["party"].fillna("") | |
| frame["display_norm"] = frame["display_name"].map(normalize_text) | |
| frame["last_name"] = frame["display_name"].map(last_name_from_display) | |
| frame["search_text"] = ( | |
| frame["display_name"].astype(str) | |
| + " " | |
| + frame["senator_id"].astype(str) | |
| + " " | |
| + frame["state"].astype(str) | |
| + " " | |
| + frame["party"].astype(str) | |
| ).str.lower() | |
| frame["search_norm"] = frame["search_text"].map(normalize_text) | |
| frame["total_rows"] = frame[["filings", "transactions", "wealth_ranges"]].sum(axis=1) | |
| frame["has_parsed_rows"] = frame["total_rows"] > 0 | |
| return frame.sort_values(["transactions", "filings", "display_name"], ascending=[False, False, True]) | |
| FILER_INDEX = build_filer_index() | |
| def name_for_id(senator_id: str) -> str: | |
| match = FILER_INDEX[FILER_INDEX["senator_id"] == senator_id] | |
| if match.empty: | |
| return str(senator_id) | |
| row = match.iloc[0] | |
| suffix = "" | |
| if row.get("state") or row.get("party"): | |
| suffix = f" ({row.get('party', '')}-{row.get('state', '')})".replace(" (-", " (").replace("-)", ")") | |
| return f"{row['display_name']}{suffix}" | |
| PUBLIC_FILER_COLUMNS = [ | |
| "Matching record", | |
| "State", | |
| "Party", | |
| "Public forms", | |
| "Trade rows", | |
| "Wealth summaries", | |
| "Typical filing lag (days)", | |
| "Record status", | |
| "Why shown", | |
| ] | |
| PUBLIC_WEALTH_COLUMNS = [ | |
| "Year", | |
| "What the form can say", | |
| "Low number", | |
| "Middle of range", | |
| "High number", | |
| "Missing number", | |
| "Filing date", | |
| "Plain-English note", | |
| ] | |
| PUBLIC_TRADE_COLUMNS = [ | |
| "Filer", | |
| "Reported transaction date", | |
| "Public filing date", | |
| "Filing lag days", | |
| "Disclosure transaction type", | |
| "Reported owner", | |
| "Asset name from filing", | |
| "Ticker text from filing", | |
| "Amount low", | |
| "Amount high", | |
| "What this row does not prove", | |
| ] | |
| def resolve_filer(query: str) -> str: | |
| matches = filer_candidates(query, max_rows=25) | |
| if not matches.empty: | |
| with_wealth = matches[matches["wealth_ranges"].fillna(0).astype(int) > 0] | |
| if not with_wealth.empty: | |
| return str(with_wealth.iloc[0]["senator_id"]) | |
| with_data = matches[matches["has_parsed_rows"]] | |
| if not with_data.empty: | |
| return str(with_data.iloc[0]["senator_id"]) | |
| return str(matches.iloc[0]["senator_id"]) | |
| if str(query or "").strip(): | |
| return "" | |
| wealth_ids = set(WEALTH["senator_id"]) | |
| matches = FILER_INDEX[FILER_INDEX["senator_id"].isin(wealth_ids)].sort_values(["wealth_ranges", "transactions"], ascending=[False, False]) | |
| return str(matches.iloc[0]["senator_id"]) if not matches.empty else str(FILER_INDEX.iloc[0]["senator_id"]) | |
| def filer_candidates(query: str, max_rows: int = 12) -> pd.DataFrame: | |
| variants = query_variants(query) | |
| if not variants: | |
| return FILER_INDEX.head(0).copy() | |
| frames = [] | |
| for variant in variants: | |
| tokens = variant.split() | |
| direct = FILER_INDEX[ | |
| FILER_INDEX["search_norm"].str.contains(variant, regex=False) | |
| | FILER_INDEX["display_norm"].str.contains(variant, regex=False) | |
| | FILER_INDEX["search_norm"].map(lambda value: has_all_tokens(value, tokens)) | |
| ].copy() | |
| if not direct.empty: | |
| direct["match_score"] = 100 if variant == variants[0] else 82 | |
| direct["why_shown"] = "direct name, state, party, or filer-ID match" if variant == variants[0] else "nickname or legal-name variant match" | |
| frames.append(direct) | |
| if frames: | |
| direct_all = pd.concat(frames, ignore_index=True) | |
| roster_last_names = direct_all[direct_all["in_roster"]]["last_name"].dropna().unique().tolist() | |
| if roster_last_names: | |
| related = FILER_INDEX[ | |
| FILER_INDEX["last_name"].isin(roster_last_names) | |
| & ~FILER_INDEX["senator_id"].isin(set(direct_all["senator_id"])) | |
| ].copy() | |
| if not related.empty: | |
| related["match_score"] = 74 | |
| related["why_shown"] = "related disclosure filer name with the same last name" | |
| frames.append(related) | |
| if not frames: | |
| return FILER_INDEX.head(0).copy() | |
| result = pd.concat(frames, ignore_index=True) | |
| result = result.sort_values( | |
| ["has_parsed_rows", "match_score", "wealth_ranges", "transactions", "filings", "display_name"], | |
| ascending=[False, False, False, False, False, True], | |
| ) | |
| result = result.drop_duplicates("senator_id", keep="first") | |
| return result.head(int(max_rows)).copy() | |
| def empty_state_figure(message: str): | |
| fig = go.Figure() | |
| fig.add_annotation( | |
| text=message, | |
| x=0.5, | |
| y=0.5, | |
| xref="paper", | |
| yref="paper", | |
| showarrow=False, | |
| font=dict(size=16, color="#334155", family=FONT_FAMILY), | |
| ) | |
| fig.update_layout(height=360, margin=dict(l=24, r=24, t=34, b=24)) | |
| return style_figure(fig) | |
| def hero_html() -> str: | |
| return """ | |
| <section class="hero-panel"> | |
| <div class="eyebrow">Official Senate disclosure forms</div> | |
| <h1>See what senators reported</h1> | |
| <p class="hero-answer"> | |
| Search a name. The app turns public Senate financial disclosure forms into plain answers: reported wealth range, disclosed transaction rows, filing dates, and the source filing. | |
| </p> | |
| <p class="hero-why"> | |
| People might care because lawmakers file public financial disclosures. This makes those official records easier to read without turning them into accusations. | |
| </p> | |
| <div class="reader-model"> | |
| <span>Forms, not accusations</span> | |
| <span>Ranges, not exact net worth</span> | |
| <span>Receipts, not guesses</span> | |
| </div> | |
| <div class="not-verdict">No claim of wrongdoing or market-beating performance.</div> | |
| </section> | |
| """ | |
| def plain_english_cards_html() -> str: | |
| cards = [ | |
| ( | |
| "Start with a person", | |
| "Search a senator and read the latest public form summary before opening charts or tables.", | |
| "#e8f7f3", | |
| "#0f766e", | |
| ), | |
| ( | |
| "Read money as a range", | |
| "Forms report dollar ranges. A result like 'at least $363K' is a disclosure floor, not a precise fortune.", | |
| "#eef4ff", | |
| "#3155b7", | |
| ), | |
| ( | |
| "Open the receipt", | |
| "Every useful row points back to the public filing so readers can check the source record.", | |
| "#fff7e8", | |
| "#8a4b00", | |
| ), | |
| ] | |
| cells = "".join( | |
| f""" | |
| <div class="plain-card" style="background:{bg};border-color:{accent};"> | |
| <div class="plain-card-title">{title}</div> | |
| <div class="plain-card-body" style="color:#111827 !important; opacity:1 !important;">{body}</div> | |
| </div> | |
| """ | |
| for title, body, bg, accent in cards | |
| ) | |
| return f"<div class='plain-cards'>{cells}</div>" | |
| def metric_html() -> str: | |
| counts = MANIFEST.get("counts", {}) | |
| dataset = counts.get("dataset", {}) | |
| items = [ | |
| (fmt_int(dataset.get("filings", "")), "public filings"), | |
| (fmt_int(dataset.get("trades", "")), "reported trades"), | |
| (fmt_int(dataset.get("net_worth_intervals", "")), "wealth summaries"), | |
| ] | |
| cells = "".join( | |
| f"<span class='count-pill'><strong>{value}</strong> {label}</span>" | |
| for value, label in items | |
| ) | |
| return f"<div class='count-strip'><span class='count-label'>Behind the app:</span>{cells}<span class='count-note'>More technical tables live in Advanced.</span></div>" | |
| def boundary_html() -> str: | |
| return """ | |
| <div class='boundary' style='color:#2b1700 !important; opacity:1 !important;'> | |
| <strong>Plain-English boundary:</strong> Public disclosure records can show what was reported and when. | |
| They cannot, by themselves, prove why someone traded or whether a private return was earned. | |
| </div> | |
| """ | |
| def overview_intro_html() -> str: | |
| return """ | |
| <div class="simple-start"> | |
| <h2>Understand it in five seconds</h2> | |
| <p>Use this like a receipt-backed reader for public Senate financial forms.</p> | |
| <div class="question-grid"> | |
| <div><strong>1. Who?</strong><span>Search the senator or filer name.</span></div> | |
| <div><strong>2. What was reported?</strong><span>Read the latest wealth range and disclosed transaction rows.</span></div> | |
| <div><strong>3. Where is the proof?</strong><span>Open the Senate filing linked beside the result.</span></div> | |
| </div> | |
| </div> | |
| """ | |
| def public_source_summary_html() -> str: | |
| return """ | |
| <div class="path-panel"> | |
| <strong>Best first step:</strong> search a senator by name, then open the original filing when you need the source. | |
| <p>Public disclosure forms show what was reported and when. They usually do not show intent, exact dollar amounts, or the reason for a trade.</p> | |
| </div> | |
| """ | |
| def coverage_summary_html() -> str: | |
| roster_count = len(SENATORS) | |
| parsed_roster_like_count = int((FILER_INDEX["has_parsed_rows"]).sum()) | |
| zero_roster_count = int((FILER_INDEX["in_roster"] & ~FILER_INDEX["has_parsed_rows"]).sum()) | |
| trade_filer_count = int((FILER_INDEX["transactions"].fillna(0).astype(int) > 0).sum()) | |
| wealth_filer_count = int((FILER_INDEX["wealth_ranges"].fillna(0).astype(int) > 0).sum()) | |
| return f""" | |
| <div class='coverage-panel'> | |
| <h2>What the search covers</h2> | |
| <p> | |
| The roster table includes <strong>{fmt_int(roster_count)}</strong> Senate roster records. | |
| This public bundle has disclosure rows for <strong>{fmt_int(parsed_roster_like_count)}</strong> filer-name records, | |
| transaction rows for <strong>{fmt_int(trade_filer_count)}</strong>, and wealth summaries for <strong>{fmt_int(wealth_filer_count)}</strong>. | |
| </p> | |
| <p> | |
| <strong>{fmt_int(zero_roster_count)}</strong> roster records currently show no filings, trades, or wealth rows under that exact roster ID. | |
| That can mean no data under this ID, a legal-name/filing-name split, or no matching rows in this public bundle. | |
| It is not a claim that the senator never filed or never traded. | |
| </p> | |
| </div> | |
| """ | |
| def market_note_html() -> str: | |
| return """ | |
| <div class="section-intro" style="background:#f8fafc;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:12px 0;"> | |
| <div style="color:#0f172a;font-size:1.08rem;font-weight:800;margin-bottom:5px;">How to read this benchmark-context chart</div> | |
| <div style="color:#334155;font-size:1rem;line-height:1.55;"> | |
| Bars above zero mean the disclosed transaction set moved more than a simple benchmark in that window under one assumption. | |
| Bars below zero mean it moved less. Because this public bundle uses no-cost adjusted-close data, every row remains exploratory. | |
| </div> | |
| </div> | |
| """ | |
| def return_chart(): | |
| fig = go.Figure() | |
| for date_basis, frame in RETURN_TESTS.groupby("date_basis_label"): | |
| fig.add_trace( | |
| go.Bar( | |
| name=str(date_basis).capitalize(), | |
| x=frame["window_label"], | |
| y=frame["equal_weight_alpha_pct"], | |
| customdata=frame[["claim_status_label", "n_events"]], | |
| hovertemplate=( | |
| "%{x}<br>" | |
| "Benchmark difference: %{y:.3f}%<br>" | |
| "Rows: %{customdata[1]}<br>" | |
| "%{customdata[0]}<extra></extra>" | |
| ), | |
| ) | |
| ) | |
| fig.add_hline(y=0, line_width=1, line_color="#555") | |
| fig.update_layout( | |
| title="Advanced benchmark context", | |
| yaxis_title="Market move vs benchmark (%)", | |
| xaxis_title="Market window after date", | |
| barmode="group", | |
| margin=dict(l=64, r=20, t=70, b=62), | |
| legend_title_text="Measured from", | |
| ) | |
| return style_figure(fig) | |
| def filing_delay_chart(): | |
| frame = TRADES.dropna(subset=["filing_lag_days_num"]).copy() | |
| frame = frame[(frame["filing_lag_days_num"] >= 0) & (frame["filing_lag_days_num"] <= 800)] | |
| bins = [-1, 30, 45, 90, 180, 365, 800] | |
| labels = ["0-30 days", "31-45 days", "46-90 days", "91-180 days", "181-365 days", "366+ days"] | |
| frame["lag_bucket"] = pd.cut(frame["filing_lag_days_num"], bins=bins, labels=labels) | |
| counts = frame["lag_bucket"].value_counts().reindex(labels).reset_index() | |
| counts.columns = ["lag_bucket", "rows"] | |
| fig = px.bar(counts, x="lag_bucket", y="rows", color="lag_bucket", color_discrete_sequence=px.colors.qualitative.Safe) | |
| fig.update_layout( | |
| title="How long after a transaction was it filed?", | |
| xaxis_title="Days between transaction and filing", | |
| yaxis_title="Disclosed trade rows", | |
| showlegend=False, | |
| margin=dict(l=40, r=20, t=60, b=50), | |
| ) | |
| return style_figure(fig) | |
| def trade_type_chart(): | |
| frame = TRADES.copy() | |
| frame["kind"] = frame["transaction_type"].fillna("").str.lower().str.extract(r"(purchase|sale|exchange)", expand=False).fillna("other") | |
| yearly = frame.dropna(subset=["transaction_year"]).groupby(["transaction_year", "kind"]).size().reset_index(name="rows") | |
| yearly = yearly[(yearly["transaction_year"] >= 2012) & (yearly["transaction_year"] <= 2026)] | |
| fig = px.area(yearly, x="transaction_year", y="rows", color="kind", color_discrete_sequence=px.colors.qualitative.Safe) | |
| fig.update_layout( | |
| title="Disclosed transaction rows by year and type", | |
| xaxis_title="Transaction year", | |
| yaxis_title="Rows", | |
| margin=dict(l=40, r=20, t=60, b=50), | |
| ) | |
| return style_figure(fig) | |
| def top_ticker_chart(): | |
| frame = TRADES[TRADES["ticker_display"] != "(no ticker)"] | |
| counts = frame["ticker_display"].value_counts().head(20).sort_values().reset_index() | |
| counts.columns = ["ticker", "rows"] | |
| fig = px.bar(counts, x="rows", y="ticker", orientation="h", color="rows", color_continuous_scale="Viridis") | |
| fig.update_layout( | |
| title="Stock-symbol text found most often in disclosures", | |
| xaxis_title="Disclosure rows", | |
| yaxis_title="Symbol text from filing", | |
| margin=dict(l=80, r=20, t=60, b=50), | |
| coloraxis_showscale=False, | |
| ) | |
| return style_figure(fig) | |
| def wealth_senators() -> list[str]: | |
| counts = WEALTH["senator_id"].value_counts() | |
| return counts.index.tolist() | |
| def wealth_chart(senator_id: str): | |
| frame = WEALTH[WEALTH["senator_id"] == senator_id].sort_values("report_year_num") | |
| if frame.empty: | |
| fig = go.Figure() | |
| fig.add_annotation( | |
| text="No wealth form summary found for this filer.", | |
| x=0.5, | |
| y=0.5, | |
| xref="paper", | |
| yref="paper", | |
| showarrow=False, | |
| font=dict(size=16, color="#334155", family=FONT_FAMILY), | |
| ) | |
| fig.update_layout(height=360, margin=dict(l=20, r=20, t=40, b=20)) | |
| return style_figure(fig) | |
| fig = go.Figure() | |
| fig.add_trace( | |
| go.Scatter( | |
| x=frame["report_year"], | |
| y=frame["net_worth_max"], | |
| mode="lines", | |
| line=dict(width=0), | |
| showlegend=False, | |
| hoverinfo="skip", | |
| ) | |
| ) | |
| fig.add_trace( | |
| go.Scatter( | |
| x=frame["report_year"], | |
| y=frame["net_worth_min"], | |
| mode="lines", | |
| fill="tonexty", | |
| fillcolor="rgba(56, 142, 129, 0.22)", | |
| line=dict(width=0), | |
| name="disclosed interval", | |
| hovertemplate="Year %{x}<br>Low bound: $%{y:,.0f}<extra></extra>", | |
| ) | |
| ) | |
| fig.add_trace( | |
| go.Scatter( | |
| x=frame["report_year"], | |
| y=frame["net_worth_midpoint"], | |
| mode="lines+markers", | |
| line=dict(color="#1b6b63", width=3), | |
| name="midpoint", | |
| hovertemplate="Year %{x}<br>Midpoint: $%{y:,.0f}<extra></extra>", | |
| ) | |
| ) | |
| fig.update_layout( | |
| title=f"Reported wealth range over time: {name_for_id(senator_id)}", | |
| xaxis_title="Report year", | |
| yaxis_title="Estimated range from disclosure brackets ($)", | |
| margin=dict(l=70, r=20, t=60, b=50), | |
| ) | |
| return style_figure(fig) | |
| def wealth_table(senator_id: str) -> pd.DataFrame: | |
| cols = [ | |
| "report_year", | |
| "net_worth_min", | |
| "net_worth_midpoint", | |
| "net_worth_max", | |
| "top_coded", | |
| "filing_date", | |
| "calculation_note", | |
| ] | |
| frame = WEALTH[WEALTH["senator_id"] == senator_id][cols].sort_values("report_year") | |
| display = frame.copy() | |
| display["reported_range"] = display.apply( | |
| lambda row: f"{fmt_money(row.get('net_worth_min')) or 'not available'} to {fmt_money(row.get('net_worth_max')) or 'not available'}", | |
| axis=1, | |
| ) | |
| display["top_coded"] = display.apply(wealth_missing_number_text, axis=1) | |
| display = display[ | |
| [ | |
| "report_year", | |
| "reported_range", | |
| "net_worth_min", | |
| "net_worth_midpoint", | |
| "net_worth_max", | |
| "top_coded", | |
| "filing_date", | |
| "calculation_note", | |
| ] | |
| ] | |
| return display.rename( | |
| columns={ | |
| "report_year": "Year", | |
| "reported_range": "What the form can say", | |
| "net_worth_min": "Low number", | |
| "net_worth_midpoint": "Middle of range", | |
| "net_worth_max": "High number", | |
| "top_coded": "Missing number", | |
| "filing_date": "Filing date", | |
| "calculation_note": "Plain-English note", | |
| } | |
| ) | |
| def filer_search(query: str, max_rows: int = 12) -> pd.DataFrame: | |
| if not str(query or "").strip(): | |
| return pd.DataFrame(columns=PUBLIC_FILER_COLUMNS) | |
| frame = filer_candidates(query, max_rows=max_rows) | |
| if frame.empty: | |
| return pd.DataFrame(columns=PUBLIC_FILER_COLUMNS) | |
| display = frame[ | |
| [ | |
| "display_name", | |
| "state", | |
| "party", | |
| "filings", | |
| "transactions", | |
| "wealth_ranges", | |
| "median_filing_lag_days", | |
| "why_shown", | |
| ] | |
| ].copy() | |
| display["median_filing_lag_days"] = display["median_filing_lag_days"].map( | |
| lambda value: "" if not str(value).strip() else f"{int(float(value))}" | |
| ) | |
| display["record_status"] = frame.apply(data_status, axis=1).values | |
| return display.rename( | |
| columns={ | |
| "display_name": "Matching record", | |
| "state": "State", | |
| "party": "Party", | |
| "filings": "Public forms", | |
| "transactions": "Trade rows", | |
| "wealth_ranges": "Wealth summaries", | |
| "median_filing_lag_days": "Typical filing lag (days)", | |
| "record_status": "Record status", | |
| "why_shown": "Why shown", | |
| } | |
| ) | |
| def filer_search_html(query: str) -> str: | |
| if not str(query or "").strip(): | |
| return public_empty_html( | |
| "Type a senator name and press Search", | |
| "Try a name, state, party, or filer ID. Results stay plain because this is the public reading path.", | |
| ) | |
| results = filer_search(query) | |
| note = "" | |
| if not results.empty: | |
| has_roster_only = results["Record status"].astype(str).str.contains("roster row; no disclosure rows", regex=False).any() | |
| has_data = results["Record status"].astype(str).str.contains("disclosure rows found", regex=False).any() | |
| if has_roster_only or has_data: | |
| note = ( | |
| "<div class='coverage-note'>" | |
| "<strong>Read this first:</strong> Senate roster names and disclosure filing names do not always use the same ID or name text. " | |
| "If a roster row says no disclosure rows, that means this public bundle has no rows under that exact roster ID. " | |
| "It does not prove the senator had no filings or no trades; check any related disclosure-name rows shown with it." | |
| "</div>" | |
| ) | |
| return note + public_filer_cards_html( | |
| results, | |
| "No matching public records found", | |
| "Try a last name, state abbreviation, party, filer ID, or common/legal-name variant.", | |
| max_rows=12, | |
| ) | |
| def filer_profile_html(query: str) -> str: | |
| if not str(query or "").strip(): | |
| return """ | |
| <div class='profile-card empty-state'> | |
| <h2>Search a senator to start</h2> | |
| <p>Type a name, state, party, or filer ID. The result shows what appears in the public disclosure dataset, not a judgment about the person.</p> | |
| </div> | |
| """ | |
| senator_id = resolve_filer(query) | |
| if not senator_id: | |
| return "<div class='profile-card'><h2>No matching senator or filer record found</h2><p>Try a last name, state abbreviation, party, filer ID, or legal-name variant.</p></div>" | |
| match = FILER_INDEX[FILER_INDEX["senator_id"] == senator_id] | |
| if match.empty: | |
| return "<div class='profile-card'><strong>No match found.</strong> Try a name, state, or filer ID.</div>" | |
| row = match.iloc[0] | |
| lag = row.get("median_filing_lag_days", "") | |
| lag_text = f"{int(lag)} days" if str(lag).strip() else "not enough transaction rows" | |
| status = data_status(row) | |
| status_note = "" | |
| if "no disclosure rows" in status: | |
| status_note = ( | |
| "<p class='status-note'><strong>Coverage note:</strong> This exact public record has no filings, trades, or wealth rows in this bundle. " | |
| "That is a data-coverage statement, not a claim that nothing exists or nothing happened.</p>" | |
| ) | |
| elif "disclosure rows found" in status: | |
| status_note = ( | |
| "<p class='status-note'><strong>Coverage note:</strong> Disclosure rows were found for this public filing-name record. " | |
| "Names may differ from the official roster name because Senate forms can use legal or filer names.</p>" | |
| ) | |
| return f""" | |
| <div class='profile-card'> | |
| <div class='profile-kicker'>Selected public filer</div> | |
| <h2>{name_for_id(senator_id)}</h2> | |
| {status_note} | |
| <div class='profile-grid'> | |
| <span><strong>{fmt_int(row.get('filings', 0))}</strong> public forms</span> | |
| <span><strong>{fmt_int(row.get('transactions', 0))}</strong> trade rows</span> | |
| <span><strong>{fmt_int(row.get('wealth_ranges', 0))}</strong> wealth summaries</span> | |
| <span><strong>{lag_text}</strong> typical filing lag</span> | |
| </div> | |
| </div> | |
| """ | |
| def filer_wealth_chart(query: str): | |
| if not str(query or "").strip(): | |
| return empty_state_figure("Search a senator to see the rough wealth range from public forms.") | |
| senator_id = resolve_filer(query) | |
| if not senator_id: | |
| return empty_state_figure("No matching senator or filer record found.") | |
| return wealth_chart(senator_id) | |
| def filer_wealth_table(query: str) -> pd.DataFrame: | |
| if not str(query or "").strip(): | |
| return pd.DataFrame(columns=PUBLIC_WEALTH_COLUMNS) | |
| senator_id = resolve_filer(query) | |
| if not senator_id: | |
| return pd.DataFrame(columns=PUBLIC_WEALTH_COLUMNS) | |
| return wealth_table(senator_id) | |
| def numeric_or_none(value): | |
| try: | |
| number = float(value) | |
| except (TypeError, ValueError): | |
| return None | |
| try: | |
| if pd.isna(number): | |
| return None | |
| except (TypeError, ValueError): | |
| pass | |
| return number | |
| def floor_money_text(value: float) -> str: | |
| if value < 0: | |
| return f"No lower than {fmt_money(value)}" | |
| return f"At least {fmt_money(value)}" | |
| def disclosure_math_plain_text() -> str: | |
| return "subtracting debts from assets reported on the public form" | |
| def wealth_plain_result(row) -> dict[str, str]: | |
| low_num = numeric_or_none(row.get("net_worth_min")) | |
| high_num = numeric_or_none(row.get("net_worth_max")) | |
| year = present_text(row.get("report_year", "latest"), "latest") | |
| if low_num is not None and high_num is not None: | |
| short = f"{fmt_money(low_num)} to {fmt_money(high_num)}" | |
| sentence = f"For {year}, the public form puts {disclosure_math_plain_text()} between {short}." | |
| missing = "Exact net worth is not reported." | |
| note = "That is a reported range, not an exact private valuation." | |
| elif low_num is not None: | |
| short = floor_money_text(low_num) | |
| sentence_value = short[:1].lower() + short[1:] | |
| sentence = f"For {year}, the public form shows {sentence_value} after {disclosure_math_plain_text()}." | |
| missing = "No calculable maximum from this filing." | |
| note = "The exact amount is not public here, and the real number may be higher." | |
| elif high_num is not None: | |
| short = f"No more than {fmt_money(high_num)}" | |
| sentence = f"For {year}, the public form shows no more than {fmt_money(high_num)} after {disclosure_math_plain_text()}." | |
| missing = "No calculable minimum from this filing." | |
| note = "The exact amount is not public here, and the real number may be lower." | |
| else: | |
| short = "Not enough dollar detail" | |
| sentence = f"The {year} public form does not give enough dollar detail for a wealth summary." | |
| missing = "Low and high numbers not available." | |
| note = "Open the source filing to inspect the underlying disclosure rows." | |
| return { | |
| "short": short, | |
| "sentence": sentence, | |
| "missing": missing, | |
| "note": note, | |
| } | |
| def wealth_range_text(row) -> str: | |
| return wealth_plain_result(row)["short"] | |
| def wealth_range_sentence(row) -> str: | |
| return wealth_plain_result(row)["sentence"] | |
| def wealth_year_explanation(row) -> str: | |
| year = present_text(row.get("report_year", "this year"), "this year") | |
| low_num = numeric_or_none(row.get("net_worth_min")) | |
| high_num = numeric_or_none(row.get("net_worth_max")) | |
| if low_num is not None and high_num is not None: | |
| return ( | |
| f"For {year}, the reported assets-minus-debts number lands somewhere from {fmt_money(low_num)} to {fmt_money(high_num)}. " | |
| "It is a public disclosure range, not an appraisal or exact net worth." | |
| ) | |
| if low_num is not None: | |
| sentence = ( | |
| f"For {year}, the public form supports at least {fmt_money(low_num)}. " | |
| "This bundle cannot calculate a top end from that filing, so the real number could be higher." | |
| ) | |
| if low_num < 0: | |
| sentence += " A negative floor can happen when reported debt ranges can exceed reported asset ranges." | |
| return sentence | |
| if high_num is not None: | |
| return ( | |
| f"For {year}, the public form supports no more than {fmt_money(high_num)}. " | |
| "This bundle cannot calculate a bottom end from that filing, so the real number could be lower." | |
| ) | |
| return f"For {year}, the filing rows do not have enough dollar detail here to summarize assets minus debts." | |
| def wealth_movement_sentence(ordered: pd.DataFrame) -> str: | |
| if len(ordered) < 2: | |
| return "Only one wealth report is available for this filer in the public bundle." | |
| latest = ordered.iloc[0] | |
| previous = ordered.iloc[1] | |
| latest_low = numeric_or_none(latest.get("net_worth_min")) | |
| latest_high = numeric_or_none(latest.get("net_worth_max")) | |
| previous_low = numeric_or_none(previous.get("net_worth_min")) | |
| previous_high = numeric_or_none(previous.get("net_worth_max")) | |
| if None in {latest_low, latest_high, previous_low, previous_high}: | |
| return "The forms are too broad for a clean year-to-year comparison." | |
| latest_year = present_text(latest.get("report_year", "latest")) | |
| previous_year = present_text(previous.get("report_year", "previous")) | |
| if latest_low > previous_high: | |
| direction = "is above" | |
| elif latest_high < previous_low: | |
| direction = "is below" | |
| else: | |
| return f"Compared with {previous_year}, the {latest_year} reported range overlaps the earlier one, so the forms do not show a clear move." | |
| return f"Compared with {previous_year}, the {latest_year} reported range {direction} the earlier reported range." | |
| def wealth_missing_number_text(row) -> str: | |
| low_num = numeric_or_none(row.get("net_worth_min")) | |
| high_num = numeric_or_none(row.get("net_worth_max")) | |
| if low_num is not None and high_num is None: | |
| return "No calculable maximum from this filing." | |
| if low_num is None and high_num is not None: | |
| return "No calculable minimum from this filing." | |
| if low_num is None and high_num is None: | |
| return "Low and high numbers not available." | |
| return "Low and high numbers available." | |
| def filer_wealth_summary_html(query: str) -> str: | |
| if not str(query or "").strip(): | |
| return public_empty_html( | |
| "Search a senator to see what the forms can say about wealth", | |
| "Senate records use dollar brackets, so this page shows a reported range rather than exact net worth.", | |
| ) | |
| senator_id = resolve_filer(query) | |
| if not senator_id: | |
| return public_empty_html( | |
| "No matching senator or filer record found", | |
| "Try a last name, state abbreviation, party, filer ID, or legal-name variant.", | |
| ) | |
| table = wealth_table(senator_id) | |
| if table.empty: | |
| return public_empty_html( | |
| "No wealth summary in this public bundle", | |
| "This does not mean no filing, no assets, or no disclosure. It only means this bundle does not have a wealth summary for the selected public filer record.", | |
| ) | |
| raw = WEALTH[WEALTH["senator_id"] == senator_id].copy() | |
| raw["_year_sort"] = pd.to_numeric(raw["report_year"], errors="coerce") | |
| ordered = raw.sort_values("_year_sort", ascending=False).drop(columns=["_year_sort"]) | |
| latest = ordered.iloc[0] | |
| latest_year = present_text(latest.get("report_year", "")) | |
| latest_filing_date = present_text(latest.get("filing_date", ""), "filing date not available") | |
| latest_url = str(latest.get("source_url", "") or "").strip() | |
| source_link = ( | |
| f"<a class='source-link' href='{html_escape(latest_url)}' target='_blank' rel='noopener noreferrer'>Open Senate filing</a>" | |
| if latest_url | |
| else "<span class='source-missing'>Source link not available in this row</span>" | |
| ) | |
| latest_plain = wealth_plain_result(latest) | |
| index_match = FILER_INDEX[FILER_INDEX["senator_id"] == senator_id] | |
| index_row = index_match.iloc[0] if not index_match.empty else {} | |
| filing_count = int(index_row.get("filings", 0) or 0) if hasattr(index_row, "get") else 0 | |
| transaction_count = int(index_row.get("transactions", 0) or 0) if hasattr(index_row, "get") else 0 | |
| summary_cards = [ | |
| ("Year covered", latest_year), | |
| ("Filed with Senate", latest_filing_date), | |
| ("Records found", f"{fmt_int(filing_count)} public forms; {fmt_int(transaction_count)} trade rows"), | |
| ] | |
| card_html = "".join( | |
| f"<div><strong>{html_escape(label)}</strong><span>{html_escape(value)}</span></div>" | |
| for label, value in summary_cards | |
| ) | |
| year_items = [] | |
| for _, row in ordered.head(6).iterrows(): | |
| row_url = str(row.get("source_url", "") or "").strip() | |
| row_source = ( | |
| f"<a href='{html_escape(row_url)}' target='_blank' rel='noopener noreferrer'>View filing</a>" | |
| if row_url | |
| else "<span>source link not available</span>" | |
| ) | |
| year_items.append( | |
| "<li>" | |
| f"<strong>{html_escape(present_text(row.get('report_year', '')))}</strong>" | |
| "<div class='year-meaning'>" | |
| f"<span>{html_escape(wealth_range_text(row))}</span>" | |
| f"<p>{html_escape(wealth_year_explanation(row))}</p>" | |
| "</div>" | |
| f"<em>Filed {html_escape(present_text(row.get('filing_date', ''), 'date not available'))}.</em>" | |
| f"{row_source}" | |
| "</li>" | |
| ) | |
| more_note = "" | |
| if len(ordered) > 6: | |
| more_note = f"<p class='table-note'>Showing 6 of {fmt_int(len(ordered))} past filing summaries. Download files and technical rows live in Advanced.</p>" | |
| return ( | |
| "<div class='result-card wealth-result wealth-reader'>" | |
| f"<div class='profile-kicker'>Latest filing in plain English</div>" | |
| f"<h2>{html_escape(name_for_id(senator_id))}</h2>" | |
| "<p class='profile-lens'>This is a summary of public Senate disclosure forms, not a judgment about the person.</p>" | |
| f"<div class='wealth-answer-card'><span>What the latest financial form says</span><strong>{html_escape(latest_plain['short'])}</strong><p>{html_escape(latest_plain['sentence'])} {html_escape(latest_plain['note'])}</p><p class='why-care-line'>Why someone might care: it shows the financial picture this official publicly reported for that year, and lets readers compare filings over time with source receipts.</p></div>" | |
| f"<div class='wealth-plain-grid'>{card_html}</div>" | |
| "<details class='reader-details'>" | |
| "<summary>How to read this number</summary>" | |
| "<p>Senate forms report assets and debts in dollar ranges. This app reads those ranges and summarizes the public form as a low-to-high range, or as a floor when the top end cannot be calculated.</p>" | |
| f"<p>{html_escape(latest_plain['missing'])} The number cannot show exact net worth, taxable income, trading profit, intent, wrongdoing, or whether any trade made money.</p>" | |
| "</details>" | |
| f"<p class='wealth-movement'>{html_escape(wealth_movement_sentence(ordered))}</p>" | |
| f"<div class='receipt-strip'><span>Latest filing date: <strong>{html_escape(latest_filing_date)}</strong></span>{source_link}</div>" | |
| "</div>" | |
| "<details class='result-card wealth-years-card history-drawer'>" | |
| "<summary>Show year-by-year filings</summary>" | |
| "<div class='wealth-year-guide'><strong>What each year means</strong><span>The year is the report year named by the public filing. The dollar phrase is what that filing supports for reported assets minus reported debts. A floor is not a ranking, and changes over time can reflect broad form ranges, purchases, sales, debts, filing differences, or parser limits.</span></div>" | |
| f"<ul class='wealth-year-list'>{''.join(year_items)}</ul>" | |
| f"{more_note}" | |
| "</details>" | |
| ) | |
| def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame: | |
| frame = TRADES.copy() | |
| if query: | |
| q = query.lower().strip() | |
| matching_ids = filer_candidates(query, max_rows=50)["senator_id"].tolist() | |
| frame = frame[ | |
| frame["asset_name"].fillna("").str.lower().str.contains(q, regex=False) | |
| | frame["ticker_candidate"].fillna("").str.lower().str.contains(q, regex=False) | |
| | frame["senator_id"].fillna("").str.lower().str.contains(q, regex=False) | |
| | frame["senator_id"].isin(matching_ids) | |
| ] | |
| frame = frame[frame["filing_lag_days_num"].fillna(-1) >= min_lag] | |
| frame = frame.head(int(max_rows)).copy() | |
| frame["filer_name"] = frame["senator_id"].fillna("").map(name_for_id) | |
| cols = [ | |
| "filer_name", | |
| "transaction_date", | |
| "filing_date", | |
| "filing_lag_days", | |
| "transaction_type", | |
| "owner", | |
| "asset_name", | |
| "ticker_candidate", | |
| "amount_min", | |
| "amount_max", | |
| "what_this_does_not_prove", | |
| ] | |
| return frame[cols].rename( | |
| columns={ | |
| "filer_name": "Filer", | |
| "transaction_date": "Reported transaction date", | |
| "filing_date": "Public filing date", | |
| "filing_lag_days": "Filing lag days", | |
| "transaction_type": "Disclosure transaction type", | |
| "owner": "Reported owner", | |
| "asset_name": "Asset name from filing", | |
| "ticker_candidate": "Ticker text from filing", | |
| "amount_min": "Amount low", | |
| "amount_max": "Amount high", | |
| "what_this_does_not_prove": "What this row does not prove", | |
| } | |
| ) | |
| def public_trade_search(query: str) -> pd.DataFrame: | |
| if not str(query or "").strip(): | |
| return pd.DataFrame(columns=PUBLIC_TRADE_COLUMNS) | |
| return trade_search(query, 0, 50) | |
| def public_trade_search_html(query: str) -> str: | |
| if not str(query or "").strip(): | |
| return public_empty_html( | |
| "Type a senator, company, ticker, asset, or filer ID and press Search", | |
| "Try Ted Cruz, AAPL, Microsoft, SPY, or a filer ID. Rows show public disclosure text, not exact private returns.", | |
| ) | |
| table = public_table_html( | |
| public_trade_search(query), | |
| "Matching public transaction disclosure rows", | |
| "No matching transaction rows found", | |
| "Try a shorter ticker, company name, asset name, senator name, or filer ID.", | |
| max_rows=20, | |
| ) | |
| if not filer_candidates(query, max_rows=1).empty: | |
| note = ( | |
| "<div class='coverage-note'><strong>Name matching note:</strong> " | |
| "Senate roster names and disclosure filing names do not always use the same text. " | |
| "A senator-name search may include related disclosure-filer names when the records line up by common name or last name.</div>" | |
| ) | |
| return note + table | |
| return table | |
| def update_filer_public(query: str): | |
| return filer_wealth_summary_html(query), filer_search_html(query), filer_profile_html(query) | |
| def update_trade_public(query: str): | |
| return public_trade_search_html(query) | |
| def return_handoff_display() -> pd.DataFrame: | |
| frame = RETURN_HANDOFF.copy() | |
| if "date_basis" in frame: | |
| frame["date_basis"] = frame["date_basis"].map(basis_label) | |
| if "window" in frame: | |
| frame["window"] = frame["window"].map(window_label) | |
| if "claim_status" in frame: | |
| frame["claim_status"] = frame["claim_status"].map(claim_label) | |
| cols = [ | |
| "date_basis", | |
| "window", | |
| "events", | |
| "senators", | |
| "tickers", | |
| "equal_weight_alpha_pct", | |
| "midpoint_weight_alpha_pct", | |
| "bootstrap_ci_low_pct", | |
| "bootstrap_ci_high_pct", | |
| "claim_status", | |
| "plain_english_status", | |
| ] | |
| frame = frame[[col for col in cols if col in frame.columns]] | |
| return frame.rename( | |
| columns={ | |
| "date_basis": "Measured from", | |
| "window": "Window", | |
| "events": "Trade windows checked", | |
| "senators": "Filers", | |
| "tickers": "Stock symbols found", | |
| "equal_weight_alpha_pct": "Equal-weight benchmark difference (%)", | |
| "midpoint_weight_alpha_pct": "Midpoint-weight benchmark difference (%)", | |
| "bootstrap_ci_low_pct": "Bootstrap low (%)", | |
| "bootstrap_ci_high_pct": "Bootstrap high (%)", | |
| "claim_status": "Status", | |
| "plain_english_status": "Plain English status", | |
| } | |
| ) | |
| def source_gap_table() -> pd.DataFrame: | |
| return pd.DataFrame(SOURCE_GAPS.get("source_gaps", [])) | |
| def file_inventory() -> pd.DataFrame: | |
| rows = [] | |
| for row in MANIFEST.get("files", []): | |
| rows.append( | |
| { | |
| "path": row.get("path", "").replace(str(DATA.parent), ""), | |
| "bytes": row.get("byte_count", ""), | |
| "sha256": row.get("sha256", ""), | |
| "compressed": row.get("compressed", False), | |
| } | |
| ) | |
| return pd.DataFrame(rows) | |
| def audit_summary_html() -> str: | |
| counts = MANIFEST.get("counts", {}) | |
| dataset = counts.get("dataset", {}) | |
| source_gaps = SOURCE_GAPS.get("source_gaps", []) | |
| return f""" | |
| <div class="audit-summary"> | |
| <h2>Source summary</h2> | |
| <div class="audit-grid"> | |
| <div><strong>{fmt_int(dataset.get('source_artifacts', ''))}</strong><span>source artifacts tracked</span></div> | |
| <div><strong>{fmt_int(len(source_gaps))}</strong><span>known source-gap notes</span></div> | |
| <div><strong>No</strong><span>raw market-price file redistributed</span></div> | |
| </div> | |
| <p>Use the downloads below if you want to audit the package. Most visitors do not need this section.</p> | |
| </div> | |
| """ | |
| CSS = """ | |
| :root { | |
| --accent: #1b6b63; | |
| --ink: #0b1220; | |
| --muted: #1f2937; | |
| --warn: #8a4b00; | |
| --line: #cbd5e1; | |
| --soft: #f8fafc; | |
| --font: Arial, Helvetica, sans-serif; | |
| color-scheme: light; | |
| } | |
| html, | |
| body, | |
| #root, | |
| .gradio-container, | |
| .gradio-container .main, | |
| .gradio-container .wrap, | |
| .gradio-container .contain { | |
| background: #f6f8fb !important; | |
| color: var(--ink) !important; | |
| color-scheme: light !important; | |
| } | |
| .dark .gradio-container, | |
| .dark .gradio-container .main, | |
| .dark .gradio-container .wrap, | |
| .dark .gradio-container .contain { | |
| background: #f6f8fb !important; | |
| color: var(--ink) !important; | |
| color-scheme: light !important; | |
| } | |
| .gradio-container, | |
| .gradio-container * { | |
| font-family: var(--font) !important; | |
| text-rendering: optimizeLegibility; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| letter-spacing: 0; | |
| } | |
| .gradio-container { | |
| max-width: 1080px !important; | |
| margin: auto; | |
| color: var(--ink) !important; | |
| font-size: 16px; | |
| line-height: 1.48; | |
| padding: 0 18px 28px; | |
| } | |
| .gradio-container .hero-panel, | |
| .gradio-container .hero-panel *, | |
| .gradio-container .plain-card, | |
| .gradio-container .plain-card *, | |
| .gradio-container .count-strip, | |
| .gradio-container .count-strip *, | |
| .gradio-container .boundary, | |
| .gradio-container .boundary *, | |
| .gradio-container .simple-start, | |
| .gradio-container .simple-start *, | |
| .gradio-container .path-panel, | |
| .gradio-container .path-panel *, | |
| .gradio-container .section-intro, | |
| .gradio-container .section-intro *, | |
| .gradio-container .profile-card, | |
| .gradio-container .profile-card *, | |
| .gradio-container .coverage-panel, | |
| .gradio-container .coverage-panel *, | |
| .gradio-container .coverage-note, | |
| .gradio-container .coverage-note *, | |
| .gradio-container .result-card, | |
| .gradio-container .result-card *, | |
| .gradio-container .public-table, | |
| .gradio-container .public-table *, | |
| .gradio-container .audit-summary, | |
| .gradio-container .audit-summary * { | |
| font-family: Arial, Helvetica, sans-serif !important; | |
| opacity: 1 !important; | |
| filter: none !important; | |
| mix-blend-mode: normal !important; | |
| text-shadow: none !important; | |
| } | |
| .hero-panel { | |
| background: #ffffff; | |
| border: 1px solid #cbd5e1; | |
| border-radius: 14px; | |
| padding: 26px 28px; | |
| margin: 14px 0 14px; | |
| box-shadow: 0 14px 34px rgba(15,23,42,.08); | |
| } | |
| .hero-panel .eyebrow { | |
| color: #006b5d !important; | |
| font-size: .86rem; | |
| font-weight: 800; | |
| letter-spacing: .04em; | |
| text-transform: uppercase; | |
| margin-bottom: 8px; | |
| } | |
| .hero-panel h1 { | |
| color: #0f172a !important; | |
| font-size: 2.55rem !important; | |
| line-height: 1.04 !important; | |
| font-weight: 800 !important; | |
| margin: 0 0 10px !important; | |
| } | |
| .hero-answer { | |
| color: #111827 !important; | |
| font-size: 1.12rem !important; | |
| line-height: 1.55 !important; | |
| max-width: 830px; | |
| margin: 0 !important; | |
| } | |
| .hero-why { | |
| color: #111827 !important; | |
| font-size: 1rem !important; | |
| line-height: 1.5 !important; | |
| max-width: 860px; | |
| margin: 10px 0 0 !important; | |
| font-weight: 700 !important; | |
| } | |
| .reader-model { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-top: 14px; | |
| } | |
| .reader-model span { | |
| background: #eef8f6 !important; | |
| border: 1px solid #99d4cb !important; | |
| border-radius: 999px; | |
| color: #073d38 !important; | |
| font-weight: 800 !important; | |
| padding: 8px 11px; | |
| } | |
| .not-verdict { | |
| display: inline-block; | |
| margin-top: 14px; | |
| color: #4d2a00 !important; | |
| background: #fff7e8; | |
| border: 1px solid #e6bd7a; | |
| border-radius: 999px; | |
| padding: 8px 12px; | |
| font-weight: 800; | |
| } | |
| .not-verdict, | |
| .not-verdict * { | |
| color: #4d2a00 !important; | |
| opacity: 1 !important; | |
| } | |
| .plain-cards { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 12px; | |
| margin: 14px 0; | |
| } | |
| .plain-card { | |
| border: 1px solid; | |
| border-radius: 12px; | |
| padding: 16px; | |
| min-height: 120px; | |
| } | |
| .plain-card-title { | |
| color: #0b1220 !important; | |
| font-size: 1.08rem; | |
| font-weight: 800; | |
| margin-bottom: 8px; | |
| } | |
| .plain-card-body { | |
| color: #111827 !important; | |
| font-size: 1rem; | |
| line-height: 1.45; | |
| font-weight: 600; | |
| opacity: 1 !important; | |
| } | |
| .count-strip { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| align-items: center; | |
| background: #ffffff; | |
| border: 1px solid #d7dee8; | |
| border-radius: 12px; | |
| padding: 12px 14px; | |
| margin: 12px 0; | |
| } | |
| .count-label { | |
| color: #111827 !important; | |
| font-weight: 800; | |
| } | |
| .count-pill { | |
| background: #eef7f5; | |
| color: #111827 !important; | |
| border: 1px solid #b8dcd6; | |
| border-radius: 999px; | |
| padding: 6px 10px; | |
| font-weight: 700; | |
| } | |
| .count-pill strong { | |
| color: #0b1220 !important; | |
| font-weight: 800 !important; | |
| } | |
| .count-note { | |
| color: #111827 !important; | |
| font-weight: 700; | |
| } | |
| .simple-start, | |
| .path-panel, | |
| .section-intro, | |
| .profile-card, | |
| .coverage-panel, | |
| .coverage-note { | |
| background: #ffffff; | |
| border: 1px solid #cbd5e1; | |
| border-radius: 12px; | |
| padding: 16px 18px; | |
| margin: 10px 0 14px; | |
| color: #111827 !important; | |
| } | |
| .simple-start *, | |
| .path-panel *, | |
| .section-intro *, | |
| .profile-card *, | |
| .coverage-panel *, | |
| .coverage-note *, | |
| .audit-summary * { | |
| color: #111827 !important; | |
| opacity: 1 !important; | |
| } | |
| .simple-start h2, | |
| .profile-card h2, | |
| .coverage-panel h2 { | |
| color: #0f172a !important; | |
| font-size: 1.45rem !important; | |
| font-weight: 800 !important; | |
| line-height: 1.2 !important; | |
| margin: 0 0 8px !important; | |
| } | |
| .question-grid, | |
| .profile-grid, | |
| .audit-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 10px; | |
| margin-top: 12px; | |
| } | |
| .question-grid div, | |
| .profile-grid span, | |
| .audit-grid div { | |
| background: #f8fafc; | |
| border: 1px solid #d7dee8; | |
| border-radius: 10px; | |
| padding: 12px; | |
| color: #1f2937; | |
| opacity: 1 !important; | |
| } | |
| .question-grid strong, | |
| .profile-grid strong, | |
| .audit-grid strong { | |
| display: block; | |
| color: #0f172a !important; | |
| font-weight: 800 !important; | |
| margin-bottom: 4px; | |
| } | |
| .question-grid span, | |
| .profile-grid span, | |
| .audit-grid span { | |
| color: #334155; | |
| opacity: 1 !important; | |
| font-weight: 700; | |
| } | |
| .audit-summary h2 { | |
| color: #0f172a !important; | |
| font-size: 1.35rem !important; | |
| font-weight: 800 !important; | |
| margin: 0 0 8px !important; | |
| } | |
| .coverage-note { | |
| background: #fff7e8 !important; | |
| border-left: 5px solid #8a4b00 !important; | |
| } | |
| .coverage-note strong, | |
| .status-note strong { | |
| color: #0f172a !important; | |
| } | |
| .status-note { | |
| color: #111827 !important; | |
| background: #f8fafc !important; | |
| border: 1px solid #d7dee8 !important; | |
| border-radius: 10px !important; | |
| padding: 10px 12px !important; | |
| margin: 8px 0 12px !important; | |
| } | |
| .result-card { | |
| background: #ffffff !important; | |
| border: 1px solid #cbd5e1 !important; | |
| border-radius: 12px !important; | |
| padding: 18px 20px !important; | |
| margin: 12px 0 16px !important; | |
| color: #111827 !important; | |
| box-shadow: none !important; | |
| } | |
| .result-card h2 { | |
| color: #0f172a !important; | |
| font-size: 1.42rem !important; | |
| line-height: 1.2 !important; | |
| font-weight: 800 !important; | |
| margin: 0 0 8px !important; | |
| } | |
| .result-card p, | |
| .result-card .table-note { | |
| color: #111827 !important; | |
| font-size: 1rem !important; | |
| line-height: 1.55 !important; | |
| margin: 0 0 10px !important; | |
| } | |
| .filer-card-list { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 10px; | |
| margin-top: 12px; | |
| } | |
| .filer-match-card { | |
| background: #f8fafc; | |
| border: 1px solid #d7dee8; | |
| border-radius: 10px; | |
| padding: 14px; | |
| } | |
| .filer-match-card h3 { | |
| color: #0f172a !important; | |
| font-size: 1.08rem !important; | |
| line-height: 1.25 !important; | |
| font-weight: 800 !important; | |
| margin: 0 0 4px !important; | |
| } | |
| .filer-meta { | |
| color: #334155 !important; | |
| font-size: .92rem !important; | |
| font-weight: 800 !important; | |
| } | |
| .filer-status { | |
| display: inline-block; | |
| color: #0f766e !important; | |
| background: #e8f7f3; | |
| border: 1px solid #b8dcd6; | |
| border-radius: 999px; | |
| padding: 5px 9px; | |
| font-size: .9rem !important; | |
| margin: 10px 0; | |
| } | |
| .filer-counts { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 7px; | |
| } | |
| .filer-counts span { | |
| background: #ffffff; | |
| border: 1px solid #e5edf5; | |
| border-radius: 8px; | |
| color: #111827 !important; | |
| font-weight: 800 !important; | |
| padding: 8px; | |
| } | |
| .filer-match-card p { | |
| color: #334155 !important; | |
| font-size: .95rem !important; | |
| font-weight: 700 !important; | |
| margin-top: 10px !important; | |
| } | |
| .empty-state { | |
| background: #ffffff !important; | |
| } | |
| .public-table-wrap { | |
| overflow-x: auto; | |
| border: 1px solid #d7dee8; | |
| border-radius: 10px; | |
| background: #ffffff; | |
| } | |
| .public-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| min-width: 720px; | |
| background: #ffffff !important; | |
| } | |
| .public-table th, | |
| .public-table td { | |
| color: #111827 !important; | |
| background: #ffffff !important; | |
| border-bottom: 1px solid #e5edf5 !important; | |
| padding: 10px 11px !important; | |
| text-align: left !important; | |
| vertical-align: top !important; | |
| font-size: .95rem !important; | |
| line-height: 1.45 !important; | |
| } | |
| .public-table th { | |
| color: #0f172a !important; | |
| background: #eef4f8 !important; | |
| font-weight: 800 !important; | |
| white-space: nowrap; | |
| } | |
| .wealth-answer-card { | |
| background: #effaf8 !important; | |
| border: 1px solid #99d4cb !important; | |
| border-radius: 10px; | |
| color: #06211f !important; | |
| padding: 16px 18px; | |
| margin: 16px 0 12px; | |
| } | |
| .wealth-answer-card span { | |
| display: block; | |
| color: #0f766e !important; | |
| font-size: .86rem !important; | |
| font-weight: 800 !important; | |
| letter-spacing: .03em; | |
| text-transform: uppercase; | |
| margin-bottom: 6px; | |
| } | |
| .wealth-answer-card strong { | |
| display: block; | |
| color: #061a18 !important; | |
| font-size: 2rem !important; | |
| line-height: 1.08 !important; | |
| font-weight: 800 !important; | |
| margin-bottom: 8px; | |
| } | |
| .wealth-answer-card p { | |
| color: #111827 !important; | |
| font-size: 1.03rem !important; | |
| line-height: 1.55 !important; | |
| margin: 0 !important; | |
| } | |
| .wealth-answer-card .why-care-line { | |
| margin-top: 10px !important; | |
| font-weight: 800 !important; | |
| } | |
| .wealth-plain-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 10px; | |
| margin-top: 12px; | |
| } | |
| .wealth-plain-grid div { | |
| background: #f8fafc; | |
| border: 1px solid #d7dee8; | |
| border-radius: 10px; | |
| padding: 12px; | |
| } | |
| .wealth-plain-grid strong { | |
| display: block; | |
| color: #334155 !important; | |
| font-size: .84rem !important; | |
| font-weight: 800 !important; | |
| margin-bottom: 5px; | |
| } | |
| .wealth-plain-grid span { | |
| color: #0f172a !important; | |
| font-size: 1.1rem !important; | |
| font-weight: 800 !important; | |
| } | |
| .why-matter-card, | |
| .wealth-year-guide { | |
| background: #fff7e8 !important; | |
| border: 1px solid #e4b56d !important; | |
| border-left: 5px solid #8a4b00 !important; | |
| border-radius: 10px; | |
| color: #2b1700 !important; | |
| padding: 12px 14px; | |
| margin-top: 12px; | |
| } | |
| .why-matter-card strong, | |
| .wealth-year-guide strong { | |
| display: block; | |
| color: #1f1200 !important; | |
| font-weight: 800 !important; | |
| margin-bottom: 4px; | |
| } | |
| .why-matter-card p { | |
| color: #2b1700 !important; | |
| margin: 0 !important; | |
| } | |
| .wealth-year-guide span { | |
| color: #2b1700 !important; | |
| display: block; | |
| font-weight: 700 !important; | |
| line-height: 1.45 !important; | |
| } | |
| .wealth-reader h2 { | |
| max-width: 860px; | |
| } | |
| .profile-lens { | |
| color: #334155 !important; | |
| font-weight: 800 !important; | |
| margin: 0 0 10px !important; | |
| } | |
| .wealth-lede { | |
| font-size: 1.12rem !important; | |
| line-height: 1.5 !important; | |
| margin-bottom: 16px !important; | |
| } | |
| .wealth-range-bar { | |
| position: relative; | |
| height: 18px; | |
| border-radius: 999px; | |
| background: #e5edf3; | |
| border: 1px solid #cbd5e1; | |
| margin: 18px 0 8px; | |
| overflow: visible; | |
| } | |
| .wealth-range-fill { | |
| position: absolute; | |
| top: 3px; | |
| bottom: 3px; | |
| border-radius: 999px; | |
| background: #0f766e; | |
| } | |
| .wealth-range-dot { | |
| position: absolute; | |
| top: 50%; | |
| width: 16px; | |
| height: 16px; | |
| transform: translate(-50%, -50%); | |
| border-radius: 50%; | |
| background: #ffffff; | |
| border: 4px solid #0f172a; | |
| box-shadow: 0 0 0 2px #ffffff; | |
| } | |
| .wealth-range-labels { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 10px; | |
| color: #334155 !important; | |
| font-size: .9rem !important; | |
| font-weight: 800 !important; | |
| margin-bottom: 12px; | |
| } | |
| .wealth-range-labels span { | |
| color: #334155 !important; | |
| } | |
| .wealth-open-range { | |
| display: grid; | |
| grid-template-columns: minmax(180px, .8fr) minmax(180px, 1fr) minmax(220px, 1fr); | |
| gap: 12px; | |
| align-items: center; | |
| background: #f8fafc; | |
| border: 1px solid #d7dee8; | |
| border-radius: 10px; | |
| padding: 12px; | |
| margin: 16px 0 10px; | |
| } | |
| .wealth-open-range span, | |
| .wealth-open-range strong { | |
| color: #111827 !important; | |
| font-weight: 800 !important; | |
| } | |
| .wealth-open-line { | |
| position: relative; | |
| height: 12px; | |
| border-radius: 999px; | |
| background: linear-gradient(90deg, #0f766e 0%, #0f766e 72%, rgba(15, 118, 110, .08) 100%); | |
| } | |
| .wealth-open-line i { | |
| position: absolute; | |
| right: -2px; | |
| top: 50%; | |
| width: 0; | |
| height: 0; | |
| transform: translateY(-50%); | |
| border-left: 12px solid #0f766e; | |
| border-top: 8px solid transparent; | |
| border-bottom: 8px solid transparent; | |
| } | |
| .wealth-explain, | |
| .receipt-strip { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px 12px; | |
| align-items: center; | |
| background: #f8fafc; | |
| border: 1px solid #d7dee8; | |
| border-radius: 10px; | |
| color: #111827 !important; | |
| padding: 12px; | |
| margin-top: 12px; | |
| } | |
| .wealth-explain span { | |
| display: block; | |
| flex-basis: 100%; | |
| color: #334155 !important; | |
| font-weight: 700 !important; | |
| } | |
| .reader-details, | |
| .history-drawer { | |
| color: #111827 !important; | |
| } | |
| .reader-details { | |
| background: #f8fafc !important; | |
| border: 1px solid #d7dee8 !important; | |
| border-radius: 10px; | |
| padding: 12px; | |
| margin-top: 12px; | |
| } | |
| .reader-details summary, | |
| .history-drawer summary { | |
| color: #0f172a !important; | |
| cursor: pointer; | |
| font-size: 1.05rem !important; | |
| font-weight: 800 !important; | |
| list-style-position: inside; | |
| } | |
| .reader-details p { | |
| color: #111827 !important; | |
| margin: 8px 0 0 !important; | |
| } | |
| .wealth-movement { | |
| margin-top: 12px !important; | |
| font-weight: 700 !important; | |
| } | |
| .source-link, | |
| .wealth-year-list a { | |
| color: #0f766e !important; | |
| font-weight: 800 !important; | |
| text-decoration: underline; | |
| text-underline-offset: 2px; | |
| } | |
| .wealth-year-list { | |
| list-style: none !important; | |
| padding: 0 !important; | |
| margin: 12px 0 0 !important; | |
| display: grid; | |
| gap: 10px; | |
| } | |
| .wealth-year-list li { | |
| display: grid; | |
| grid-template-columns: minmax(70px, .45fr) minmax(340px, 2.4fr) minmax(120px, .7fr) minmax(100px, .55fr); | |
| gap: 10px; | |
| align-items: start; | |
| background: #f8fafc; | |
| border: 1px solid #d7dee8; | |
| border-radius: 10px; | |
| padding: 12px; | |
| margin: 0 !important; | |
| } | |
| .wealth-year-list strong { | |
| color: #0f172a !important; | |
| font-size: 1.02rem !important; | |
| } | |
| .wealth-year-list .year-meaning span { | |
| display: block; | |
| color: #111827 !important; | |
| font-weight: 800 !important; | |
| margin-bottom: 4px; | |
| } | |
| .wealth-year-list .year-meaning p { | |
| color: #334155 !important; | |
| font-size: .96rem !important; | |
| font-weight: 700 !important; | |
| line-height: 1.45 !important; | |
| margin: 0 !important; | |
| } | |
| .wealth-year-list em { | |
| color: #334155 !important; | |
| font-style: normal !important; | |
| font-weight: 700 !important; | |
| } | |
| .profile-kicker { | |
| color: #0f766e; | |
| font-size: .84rem; | |
| font-weight: 800; | |
| letter-spacing: .04em; | |
| text-transform: uppercase; | |
| margin-bottom: 6px; | |
| } | |
| .gradio-container a, | |
| .gradio-container a *, | |
| .gradio-container strong, | |
| .gradio-container b { | |
| color: #0f172a !important; | |
| } | |
| .gradio-container h1, | |
| .gradio-container .prose h1, | |
| .gradio-container .markdown h1 { | |
| color: #0f172a !important; | |
| font-size: 2rem !important; | |
| line-height: 1.15 !important; | |
| font-weight: 800 !important; | |
| margin: 10px 0 8px !important; | |
| } | |
| .gradio-container p, | |
| .gradio-container li, | |
| .gradio-container .prose { | |
| color: #111827 !important; | |
| font-size: 1rem !important; | |
| line-height: 1.58 !important; | |
| } | |
| .gradio-container .hero-answer, | |
| .gradio-container .simple-start p, | |
| .gradio-container .path-panel p, | |
| .gradio-container .section-intro, | |
| .gradio-container .profile-card p, | |
| .gradio-container .audit-summary p { | |
| color: #111827 !important; | |
| opacity: 1 !important; | |
| } | |
| .gradio-container label, | |
| .gradio-container .label-wrap, | |
| .gradio-container .label-wrap span, | |
| .gradio-container button[role="tab"] { | |
| color: #0f172a !important; | |
| font-size: 1rem !important; | |
| font-weight: 700 !important; | |
| } | |
| .gradio-container button[role="tab"][aria-selected="true"] { | |
| color: var(--accent) !important; | |
| font-weight: 800 !important; | |
| } | |
| .gradio-container button[role="tab"] { | |
| min-height: 42px !important; | |
| } | |
| .gradio-container [role="tablist"] { | |
| flex-wrap: wrap !important; | |
| gap: 4px 10px !important; | |
| } | |
| .gradio-container input, | |
| .gradio-container textarea, | |
| .gradio-container select { | |
| color: #111827 !important; | |
| font-size: 1rem !important; | |
| background: #ffffff !important; | |
| } | |
| .gradio-container button { | |
| color: #0f172a !important; | |
| font-weight: 800 !important; | |
| font-family: Arial, Helvetica, sans-serif !important; | |
| } | |
| .gradio-container button.primary, | |
| .gradio-container button[variant="primary"] { | |
| background: #0f766e !important; | |
| border-color: #0f766e !important; | |
| color: #ffffff !important; | |
| } | |
| .gradio-container button.primary *, | |
| .gradio-container button[variant="primary"] * { | |
| color: #ffffff !important; | |
| } | |
| #filer-search-button, | |
| #trade-search-button { | |
| max-width: 220px !important; | |
| min-height: 44px !important; | |
| margin: 6px 0 14px !important; | |
| } | |
| .gradio-container code { | |
| color: #0f172a !important; | |
| background: #eef4f8 !important; | |
| border: 1px solid #d7dee8 !important; | |
| border-radius: 6px !important; | |
| padding: 1px 5px !important; | |
| } | |
| [role="listbox"], | |
| [role="option"], | |
| .gradio-container .options, | |
| .gradio-container .option, | |
| .gradio-container .svelte-select-list, | |
| .gradio-container .dropdown-options { | |
| background: #ffffff !important; | |
| color: #111827 !important; | |
| } | |
| .gradio-container .block, | |
| .gradio-container .form, | |
| .gradio-container .panel, | |
| .gradio-container .plot-container, | |
| .gradio-container .table-wrap, | |
| .gradio-container .dataframe, | |
| .gradio-container .input-container, | |
| .gradio-container .container { | |
| background: #ffffff !important; | |
| color: #111827 !important; | |
| } | |
| .metrics { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin: 14px 0 18px; } | |
| .metric { border: 1px solid var(--line); border-radius: 8px; padding: 13px 14px; background: #ffffff; } | |
| .metric-value { font-size: 1.45rem; font-weight: 800; color: var(--accent); line-height: 1.18; overflow-wrap: anywhere; } | |
| .metric-label { font-size: .92rem; color: var(--muted); font-weight: 700; margin-top: 5px; } | |
| .boundary { | |
| border-left: 5px solid var(--warn); | |
| background: #fff7e8; | |
| color: #2b1700 !important; | |
| padding: 13px 15px; | |
| border-radius: 6px; | |
| margin: 12px 0 20px; | |
| font-size: 1rem; | |
| line-height: 1.55; | |
| } | |
| .boundary, | |
| .boundary * { | |
| color: #2b1700 !important; | |
| opacity: 1 !important; | |
| } | |
| .boundary strong { color: #1f1200 !important; font-weight: 800; } | |
| .small-note { color: #111827 !important; font-size: .98rem; opacity: 1 !important; } | |
| .gradio-container table, | |
| .gradio-container th, | |
| .gradio-container td { | |
| color: #111827 !important; | |
| font-size: .94rem !important; | |
| line-height: 1.45 !important; | |
| } | |
| .gradio-container th { | |
| background: #eef4f8 !important; | |
| color: #0f172a !important; | |
| font-weight: 700 !important; | |
| } | |
| .gradio-container td { | |
| background: #ffffff !important; | |
| } | |
| .gradio-container .dataframe, | |
| .gradio-container .table-wrap { | |
| border-color: var(--line) !important; | |
| } | |
| .gradio-container .wealth-reader, | |
| .gradio-container .wealth-reader *, | |
| .gradio-container .wealth-years-card, | |
| .gradio-container .wealth-years-card * { | |
| color: #111827 !important; | |
| font-family: Arial, Helvetica, sans-serif !important; | |
| opacity: 1 !important; | |
| } | |
| .gradio-container .wealth-answer-card, | |
| .gradio-container .wealth-answer-card * { | |
| color: #061a18 !important; | |
| opacity: 1 !important; | |
| } | |
| .gradio-container .wealth-answer-card span, | |
| .gradio-container .profile-kicker { | |
| color: #0f766e !important; | |
| } | |
| .gradio-container .wealth-explain, | |
| .gradio-container .wealth-explain *, | |
| .gradio-container .receipt-strip, | |
| .gradio-container .receipt-strip * { | |
| color: #111827 !important; | |
| opacity: 1 !important; | |
| } | |
| .gradio-container .source-link, | |
| .gradio-container .wealth-year-list a { | |
| color: #0f766e !important; | |
| } | |
| @media (max-width: 900px) { | |
| .plain-cards, .question-grid, .profile-grid, .audit-grid, .wealth-plain-grid, .filer-card-list { grid-template-columns: 1fr !important; } | |
| .wealth-year-list li { grid-template-columns: 1fr !important; } | |
| .wealth-open-range { grid-template-columns: 1fr !important; } | |
| .hero-panel h1 { font-size: 2rem !important; } | |
| } | |
| @media (max-width: 620px) { | |
| .plain-cards { grid-template-columns: 1fr !important; } | |
| .hero-panel { | |
| padding: 20px 18px !important; | |
| } | |
| .hero-panel h1 { | |
| font-size: 2rem !important; | |
| line-height: 1.08 !important; | |
| } | |
| .not-verdict { | |
| display: block !important; | |
| border-radius: 12px !important; | |
| padding: 10px 12px !important; | |
| } | |
| .count-strip { | |
| align-items: flex-start !important; | |
| } | |
| .gradio-container [role="tablist"] { | |
| display: flex !important; | |
| flex-wrap: wrap !important; | |
| } | |
| .gradio-container button[role="tab"] { | |
| flex: 1 1 44% !important; | |
| min-width: 0 !important; | |
| min-height: 38px !important; | |
| padding: 6px 8px !important; | |
| font-size: .92rem !important; | |
| } | |
| #filer-search-button, | |
| #trade-search-button { | |
| width: 100% !important; | |
| max-width: none !important; | |
| } | |
| } | |
| """ | |
| with gr.Blocks(title="Senate Disclosure Reader") as demo: | |
| gr.HTML(hero_html()) | |
| with gr.Tabs(): | |
| with gr.Tab("Search Senator"): | |
| gr.HTML("<div class='section-intro'><strong>Search a senator.</strong> The first result gives the latest filing in plain English. If this bundle has no usable rows for that exact record, it says that directly.</div>") | |
| filer_query = gr.Textbox( | |
| label="Name, state, or filer ID", | |
| value="", | |
| placeholder="Type a senator name, state, party, or filer ID", | |
| show_label=True, | |
| elem_id="filer-search-input", | |
| ) | |
| filer_button = gr.Button("Search", variant="primary", elem_id="filer-search-button") | |
| wealth_summary = gr.HTML(filer_wealth_summary_html("")) | |
| filer_results = gr.HTML(filer_search_html("")) | |
| filer_profile = gr.HTML(filer_profile_html("")) | |
| filer_query.submit( | |
| update_filer_public, | |
| inputs=filer_query, | |
| outputs=[wealth_summary, filer_results, filer_profile], | |
| show_progress="minimal", | |
| ) | |
| filer_button.click( | |
| update_filer_public, | |
| inputs=filer_query, | |
| outputs=[wealth_summary, filer_results, filer_profile], | |
| show_progress="minimal", | |
| ) | |
| with gr.Tab("Search Trades"): | |
| gr.HTML("<div class='section-intro'><strong>Search transaction disclosures.</strong> These rows show what was reported on forms. They do not show exact execution price or private profit.</div>") | |
| search_box = gr.Textbox(label="Senator, company, ticker, asset, or filer ID", value="", placeholder="Try: Ted Cruz, AAPL, Microsoft, SPY", elem_id="trade-search-input") | |
| trade_button = gr.Button("Search", variant="primary", elem_id="trade-search-button") | |
| trade_rows = gr.HTML(public_trade_search_html("")) | |
| search_box.submit(update_trade_public, inputs=search_box, outputs=trade_rows, show_progress="minimal") | |
| trade_button.click(update_trade_public, inputs=search_box, outputs=trade_rows, show_progress="minimal") | |
| with gr.Tab("Advanced"): | |
| gr.HTML("<div class='section-intro'><strong>Advanced area.</strong> This is where the heavier charts, market assumptions, and receipts live.</div>") | |
| with gr.Accordion("What this app can and cannot say", open=False): | |
| gr.HTML(overview_intro_html()) | |
| gr.HTML(metric_html()) | |
| gr.HTML(boundary_html()) | |
| gr.HTML(coverage_summary_html()) | |
| with gr.Accordion("Filing-delay chart", open=False): | |
| gr.Plot(value=filing_delay_chart(), show_label=False) | |
| with gr.Accordion("Stock-symbol text chart", open=False): | |
| gr.HTML("<div class='small-note'>Ticker text is read from disclosure forms. It is not always a verified security identifier.</div>") | |
| gr.Plot(value=top_ticker_chart(), show_label=False) | |
| with gr.Accordion("Benchmark context", open=False): | |
| gr.HTML(market_note_html()) | |
| gr.Plot(value=return_chart(), show_label=False) | |
| gr.Dataframe(value=return_handoff_display(), label="Market-check rows", interactive=False, wrap=True) | |
| gr.HTML("<div class='boundary'><strong>Bottom line:</strong> these rows are not public alpha proof. A positive cell is not proof of intent, illegality, causality, insider trading, or realized private return.</div>") | |
| with gr.Accordion("Audit trail and downloads", open=False): | |
| gr.HTML(audit_summary_html()) | |
| gr.Dataframe(value=source_gap_table(), label="Source-gap report", interactive=False, wrap=True) | |
| gr.Dataframe(value=file_inventory(), label="Bundle inventory", interactive=False, wrap=True) | |
| gr.File( | |
| value=str(DATA / "public_release_manifest.json"), | |
| label="Public release manifest", | |
| interactive=False, | |
| ) | |
| gr.File( | |
| value=str(DATA / "dataset_bundle" / "return_tests.csv"), | |
| label="Return-test summary CSV", | |
| interactive=False, | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(theme=gr.themes.Soft(), css=CSS) | |