adding website

This commit is contained in:
2026-04-28 22:50:53 -04:00
parent e75c077e16
commit 72eb2d8c3d
19 changed files with 3376 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Nornsight{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', path='styles.css') }}">
<script src="{{ url_for('static', path='vendor/htmx.min.js') }}" defer></script>
</head>
<body>
<header class="topbar">
<a class="brand" href="/">
<span class="brand-mark">N</span>
<span>Nornsight</span>
</a>
{% if show_primary_nav|default(true) %}
<nav class="primary-nav" aria-label="Primary">
<a href="/">Issues</a>
<a href="/legislators">Legislators</a>
<a href="/compare">Compare</a>
</nav>
{% endif %}
<nav class="account-nav" aria-label="Account">
<a href="#" aria-disabled="true">Help</a>
{% if is_authenticated|default(true) %}
<details class="account-menu">
<summary>My account</summary>
<div class="account-menu-panel">
<a href="#" aria-disabled="true">Account settings</a>
<a class="sign-out" href="/logout">Sign out</a>
</div>
</details>
{% else %}
<a class="sign-in" href="/login">Sign in</a>
{% endif %}
</nav>
</header>
{% block body %}{% endblock %}
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
{% extends "base.html" %}
{% block title %}Compare Legislators{% endblock %}
{% block body %}
<main class="shell">
<section class="page-heading stacked-heading">
<div>
<h1>Compare legislators</h1>
<p>Up to 4 legislators · up to 8 issue axes · each polygon = one legislator's full issue profile</p>
</div>
</section>
<section class="compare-controls">
<form class="wide-search compare-search" action="/compare" method="get">
<label class="sr-only" for="compare-legislator-search">Search legislators</label>
{% for legislator_id in selected_legislators %}
<input type="hidden" name="legislator_id" value="{{ legislator_id }}">
{% endfor %}
{% for topic in topics %}
<input type="hidden" name="topic" value="{{ topic }}">
{% endfor %}
<input
id="compare-legislator-search"
type="search"
name="q"
value="{{ q }}"
placeholder="Search legislators to add"
autocomplete="off">
<button type="submit">Search</button>
</form>
<h2>Legislators ({{ selected_legislator_options|length }} / 4)</h2>
<div class="result-chips">
{% for legislator in selected_legislator_options %}
{% set without = selected_legislators | reject('equalto', legislator.legislator_id) | list %}
<a href="{{ build_compare_url(legislator_ids=without, topics=topics, q=q) }}"><span class="legend-dot dot-{{ loop.index0 }}"></span>{{ legislator.display_name }}{% if legislator.state %} — {{ legislator.state }}{% endif %} ×</a>
{% endfor %}
{% if selected_legislator_options|length < 4 %}
{% for option in legislator_options %}
{% if option.legislator_id not in selected_legislators %}
<a class="dashed-chip" href="{{ build_compare_url(legislator_ids=selected_legislators + [option.legislator_id], topics=topics, q=q) }}">+ {{ option.display_name }}</a>
{% endif %}
{% endfor %}
{% endif %}
</div>
<h2>Issue axes ({{ topics|length }} / 8)</h2>
<div class="axis-chips">
{% for topic in topics %}
{% set without_topic = topics[:loop.index0] + topics[loop.index:] %}
<a href="{{ build_compare_url(legislator_ids=selected_legislators, topics=without_topic, q=q) }}">{{ topic }} ×</a>
{% endfor %}
{% if topics|length < 8 %}
{% for topic in topic_options %}
{% if topic not in topics %}
<a href="{{ build_compare_url(legislator_ids=selected_legislators, topics=topics + [topic], q=q) }}">{{ topic }}</a>
{% endif %}
{% endfor %}
{% endif %}
</div>
</section>
<section class="compare-card">
<div class="radar-frame">{{ radar_svg | safe }}</div>
<aside class="compare-legend">
<h2>Legend</h2>
{% for item in series %}
<div class="legend-row">
<span class="legend-line line-{{ loop.index0 }}"></span>
<div>
<strong>{{ item.legislator.display_name }}</strong>
<small>{{ item.legislator.state or "US" }} · {{ item.legislator.party or "—" }} · avg {{ item.average_score|round(0) if item.average_score is not none else "—" }}</small>
</div>
</div>
{% endfor %}
<p>Outer ring = 100% support. Each axis is scored independently against full roll-call record.</p>
<p><em>Max 4 legislators · max 8 axes</em></p>
</aside>
</section>
</main>
<footer class="footer">
<span>Actual record, not rhetoric</span>
<span>Source: congressional roll-call votes</span>
<span>Not affiliated with any political party or organization</span>
</footer>
{% endblock %}
+30
View File
@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}Legislative Accountability{% endblock %}
{% block body %}
<main class="shell">
<section class="page-heading">
<div>
<h1>Legislative accountability</h1>
<p>US legislative accountability · precomputed legislator topic scores{% if latest_score_year %} through {{ latest_score_year }}{% endif %}</p>
</div>
<div class="heading-actions">
<a href="#" aria-disabled="true">Methodology</a>
<a href="#" aria-disabled="true">Data sources</a>
<span>Last updated: {{ last_updated.strftime("%b %Y") if last_updated else "Unavailable" }}</span>
</div>
</section>
<div class="notice">Choose one or more score topics, then select lawmakers to compare computed records over time.</div>
<div id="dashboard-body">
{% include "partials/_dashboard.html" %}
</div>
</main>
<footer class="footer">
<span>Actual record, not rhetoric</span>
<span>Source: congressional roll-call votes</span>
<span>Not affiliated with any political party or organization</span>
</footer>
{% endblock %}
+148
View File
@@ -0,0 +1,148 @@
{% extends "base.html" %}
{% block title %}Legislator Profiles{% endblock %}
{% block body %}
<main class="shell">
<section class="page-heading stacked-heading">
<div>
<h1>Legislator profiles</h1>
<p>Full issue taxonomy · search any current legislator</p>
</div>
</section>
<form class="wide-search legislator-search-form" action="/legislators" method="get">
<label class="sr-only" for="legislator-search">Search legislators</label>
<input
id="legislator-search"
type="search"
name="q"
value="{{ q }}"
placeholder="Search by name or state"
autocomplete="off"
hx-get="/partials/legislator-suggestions"
hx-trigger="input changed delay:200ms, search"
hx-target="#legislator-suggestions"
hx-swap="innerHTML">
<label class="sr-only" for="legislator-page-size">Results per page</label>
<select id="legislator-page-size" name="per_page" aria-label="Results per page">
{% for option in per_page_options %}
<option value="{{ option }}" {{ "selected" if option == per_page else "" }}>{{ option }}</option>
{% endfor %}
</select>
<button type="submit">Search</button>
</form>
<div id="legislator-suggestions" aria-live="polite"></div>
{% if q %}
<section class="phonebook-results" aria-label="Matching legislators">
<header>
<h2>Matching legislators</h2>
<span>{{ result_count }} result{{ "" if result_count == 1 else "s" }}</span>
</header>
{% if matches %}
<ol class="phonebook-list" start="{{ ((page - 1) * per_page) + 1 }}">
{% for option in matches %}
<li>
<a href="{{ build_legislator_url(legislator_id=option.legislator_id, q=q, per_page=per_page) }}">
<span class="phonebook-name">{{ option.display_name }}</span>
<span class="phonebook-meta">
{{ option.state or "US" }}{% if option.party %} · {{ option.party }}{% endif %}{% if option.chamber %} · {{ option.chamber }}{% endif %}
</span>
</a>
</li>
{% endfor %}
</ol>
<nav class="pagination" aria-label="Legislator results pages">
{% if previous_page %}
<a href="{{ build_legislator_search_url(q=q, per_page=per_page, page=previous_page) }}">Previous</a>
{% else %}
<span>Previous</span>
{% endif %}
<strong>Page {{ page }} of {{ total_pages }}</strong>
{% if next_page %}
<a href="{{ build_legislator_search_url(q=q, per_page=per_page, page=next_page) }}">Next</a>
{% else %}
<span>Next</span>
{% endif %}
</nav>
{% else %}
<p class="empty-state">No legislators matched this search.</p>
{% endif %}
</section>
{% endif %}
{% if profile %}
<section class="profile-card">
<header class="profile-header">
<div class="profile-identity">
<span class="avatar">{{ profile.legislator.display_name.split(',')[0][:1] }}{{ profile.legislator.display_name.split(',')[-1].strip()[:1] }}</span>
<div>
<h2>{{ profile.legislator.display_name }} <span class="party-pill">{{ profile.legislator.chamber or "LEG" }}</span></h2>
<p>{{ profile.legislator.state or "US" }} · {{ profile.legislator.party or "Unaffiliated" }}{% if profile.serving_since %} · Serving since {{ profile.serving_since }}{% endif %}</p>
</div>
</div>
<div class="overall-score">
<span>Overall avg</span>
<strong>{{ profile.overall_score|round(0) if profile.overall_score is not none else "—" }}</strong>
<small>/ 100</small>
</div>
</header>
{% if profile.top_topics or profile.bottom_topics %}
<div class="topic-panels">
<article>
<h3>Most important issues for</h3>
{% for item in profile.top_topics %}
<a class="topic-row" href="{{ build_legislator_url(legislator_id=profile.legislator.legislator_id, topic=item.topic) }}">
<strong class="score positive">{{ item.score|round(0) }}</strong>
<span>{{ item.topic }}</span>
<i style="width: {{ item.score }}%"></i>
</a>
{% endfor %}
</article>
<article>
<h3 class="opposed-heading">Most important issues against</h3>
{% for item in profile.bottom_topics %}
<a class="topic-row {{ 'active' if item.topic == selected_topic else '' }}" href="{{ build_legislator_url(legislator_id=profile.legislator.legislator_id, topic=item.topic) }}">
<strong class="score negative">{{ item.score|round(0) }}</strong>
<span>{{ item.topic }}</span>
<i style="width: {{ item.score }}%"></i>
</a>
{% endfor %}
</article>
</div>
<section class="profile-history">
<h3>{{ selected_topic or "Topic" }} — score history</h3>
<div class="chart-frame">{{ history_svg | safe }}</div>
{% if history_series %}
<div class="chart-legend compact" aria-label="Chart legend">
{% for item in history_series %}
<div class="chart-legend-row">
<span class="chart-legend-line line-0"></span>
<span class="chart-legend-marker marker-0"></span>
<div class="chart-legend-copy">
<span class="chart-legend-label">{{ item.label }}</span>
<span class="chart-legend-meta">
{% if item.party %}{{ item.party }}{% endif %}{% if item.party and item.state %} · {% endif %}{% if item.state %}{{ item.state }}{% endif %}
</span>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</section>
{% else %}
<p class="empty-state">No issue scores are available for this legislator yet.</p>
{% endif %}
</section>
{% endif %}
</main>
<footer class="footer">
<span>Actual record, not rhetoric</span>
<span>Source: congressional roll-call votes</span>
<span>Not affiliated with any political party or organization</span>
</footer>
{% endblock %}
+45
View File
@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block title %}Sign in | Nornsight{% endblock %}
{% block body %}
<main class="login-shell">
<section class="login-panel" aria-labelledby="login-title">
<div class="login-copy">
<p class="eyebrow">Admin access</p>
<h1 id="login-title">Sign in to Nornsight</h1>
<p>Use the dashboard account to review rankings, profiles, and legislator comparisons.</p>
</div>
<form class="login-form" action="/login" method="post">
<input type="hidden" name="next" value="{{ next_path }}">
{% if error %}
<p class="form-error" role="alert">{{ error }}</p>
{% endif %}
<label for="username">Username</label>
<input
id="username"
name="username"
type="text"
autocomplete="username"
value="{{ username }}"
required
autofocus
>
<label for="password">Password</label>
<input
id="password"
name="password"
type="password"
autocomplete="current-password"
required
>
<button type="submit">Sign in</button>
</form>
</section>
</main>
{% endblock %}
@@ -0,0 +1,30 @@
<section class="chart-card">
<header>
<h2>Score history{% if selected_issue_label %} — {{ selected_issue_label }}{% endif %}</h2>
<a href="{{ build_url(request, compare=[]) }}"
hx-get="/partials/dashboard{{ build_url(request, compare=[])|replace('/', '', 1) }}"
hx-target="#dashboard-body"
hx-push-url="{{ build_url(request, compare=[]) }}">Clear comparison</a>
</header>
<div class="chart-frame">
{{ chart_svg | safe }}
</div>
{% if chart_series %}
<div class="chart-legend" aria-label="Chart legend">
{% for item in chart_series %}
{% set style_index = loop.index0 % 4 %}
<div class="chart-legend-row">
<span class="chart-legend-line line-{{ style_index }}"></span>
<span class="chart-legend-marker marker-{{ style_index }}"></span>
<div class="chart-legend-copy">
<span class="chart-legend-label">{{ item.label }}</span>
<span class="chart-legend-meta">
{% if item.party %}{{ item.party }}{% endif %}{% if item.party and item.state %} · {% endif %}{% if item.state %}{{ item.state }}{% endif %}
</span>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<p class="score-note">Scores reflect averaged precomputed topic rows per year. Sparse years are omitted rather than plotted as zero.</p>
</section>
@@ -0,0 +1,25 @@
<section class="controls-grid">
{% include "partials/_issue_filters.html" %}
<div class="chamber-card">
<a class="segment {{ 'active' if chamber == 'house' else '' }}"
href="{{ build_url(request, chamber='house') }}"
hx-get="/partials/dashboard{{ build_url(request, chamber='house')|replace('/', '', 1) }}"
hx-target="#dashboard-body"
hx-push-url="{{ build_url(request, chamber='house') }}">House</a>
<a class="segment {{ 'active' if chamber == 'senate' else '' }}"
href="{{ build_url(request, chamber='senate') }}"
hx-get="/partials/dashboard{{ build_url(request, chamber='senate')|replace('/', '', 1) }}"
hx-target="#dashboard-body"
hx-push-url="{{ build_url(request, chamber='senate') }}">Senate</a>
<a class="segment {{ 'active' if chamber == 'all' else '' }}"
href="{{ build_url(request, chamber='all') }}"
hx-get="/partials/dashboard{{ build_url(request, chamber='all')|replace('/', '', 1) }}"
hx-target="#dashboard-body"
hx-push-url="{{ build_url(request, chamber='all') }}">All</a>
</div>
</section>
<p class="score-note">Support score: 1-100 precomputed from bill topic stance and roll-call votes. Higher means more aligned with the topic.</p>
{% include "partials/_rankings.html" %}
{% include "partials/_chart.html" %}
@@ -0,0 +1,46 @@
<section class="filter-card">
<h2>Issue filters</h2>
<form class="issue-form"
method="get"
action="/"
hx-get="/"
hx-target="#dashboard-body"
hx-push-url="true">
<input type="hidden" name="chamber" value="{{ chamber }}">
{% if congress %}
<input type="hidden" name="congress" value="{{ congress }}">
{% endif %}
{% for legislator_id in compare %}
<input type="hidden" name="compare" value="{{ legislator_id }}">
{% endfor %}
{% for issue in issues %}
<span class="chip">
{{ issue }}
<a href="{{ build_url(request, issues=issues[:loop.index0] + issues[loop.index:]) }}"
hx-get="/partials/dashboard{{ build_url(request, issues=issues[:loop.index0] + issues[loop.index:])|replace('/', '', 1) }}"
hx-target="#dashboard-body"
hx-push-url="{{ build_url(request, issues=issues[:loop.index0] + issues[loop.index:]) }}"
aria-label="Remove {{ issue }}">×</a>
</span>
<input type="hidden" name="issues" value="{{ issue }}">
{% endfor %}
<label class="search-box">
<span class="sr-only">Search issue areas</span>
<input type="search" name="issues" placeholder="Search issue areas" autocomplete="off">
</label>
<button type="submit">Apply</button>
</form>
{% if suggestions %}
<div class="suggestions" aria-label="Issue suggestions">
{% for suggestion in suggestions %}
{% if suggestion not in issues %}
<a href="{{ build_url(request, issues=issues + [suggestion]) }}"
hx-get="/partials/dashboard{{ build_url(request, issues=issues + [suggestion])|replace('/', '', 1) }}"
hx-target="#dashboard-body"
hx-push-url="{{ build_url(request, issues=issues + [suggestion]) }}">{{ suggestion }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</section>
@@ -0,0 +1,11 @@
{% if matches %}
<div class="result-chips" aria-label="Search suggestions">
{% for option in matches %}
<a href="{{ build_legislator_url(legislator_id=option.legislator_id) }}">
{{ option.display_name }}{% if option.state %} · {{ option.state }}{% endif %}
</a>
{% endfor %}
</div>
{% elif q %}
<p class="suggestion-empty">No matches</p>
{% endif %}
@@ -0,0 +1,61 @@
<section class="rankings-grid">
<article class="ranking-card">
<header>
<h2>Most supportive</h2>
<span>Top 10</span>
</header>
{% if rankings.supportive %}
<ol class="ranking-list">
{% for row in rankings.supportive %}
{% set next_compare = toggle_compare(compare, row.legislator_id) %}
<li class="{{ 'selected' if row.legislator_id in compare else '' }}">
<a href="{{ build_url(request, compare=next_compare) }}"
hx-get="/partials/dashboard{{ build_url(request, compare=next_compare)|replace('/', '', 1) }}"
hx-target="#dashboard-body"
hx-push-url="{{ build_url(request, compare=next_compare) }}">
<span class="rank">{{ loop.index }}</span>
<strong class="score positive">{{ row.score|round(1) }}</strong>
<span class="member">
<strong>{{ row.display_name }}</strong>
<small>{{ row.state or "US" }}{% if row.party %} · {{ row.party[:1] }}{% endif %}</small>
</span>
<span class="votes">{{ row.total }} rows</span>
</a>
</li>
{% endfor %}
</ol>
{% else %}
<p class="empty-state">{{ empty_message }}</p>
{% endif %}
</article>
<article class="ranking-card">
<header>
<h2>Most opposed</h2>
<span>Bottom 10</span>
</header>
{% if rankings.opposed %}
<ol class="ranking-list">
{% for row in rankings.opposed %}
{% set next_compare = toggle_compare(compare, row.legislator_id) %}
<li class="{{ 'selected' if row.legislator_id in compare else '' }}">
<a href="{{ build_url(request, compare=next_compare) }}"
hx-get="/partials/dashboard{{ build_url(request, compare=next_compare)|replace('/', '', 1) }}"
hx-target="#dashboard-body"
hx-push-url="{{ build_url(request, compare=next_compare) }}">
<span class="rank">{{ loop.index }}</span>
<strong class="score negative">{{ row.score|round(1) }}</strong>
<span class="member">
<strong>{{ row.display_name }}</strong>
<small>{{ row.state or "US" }}{% if row.party %} · {{ row.party[:1] }}{% endif %}</small>
</span>
<span class="votes">{{ row.total }} rows</span>
</a>
</li>
{% endfor %}
</ol>
{% else %}
<p class="empty-state">{{ empty_message }}</p>
{% endif %}
</article>
</section>
+15
View File
@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block title %}Database Setup Required{% endblock %}
{% block body %}
<main class="shell">
<section class="page-heading stacked-heading">
<div>
<h1>Database setup required</h1>
<p>Configure DATA_SCIENCE_DEV before opening the dashboard.</p>
</div>
</section>
<pre class="setup-error">{{ error }}</pre>
</main>
{% endblock %}