Initial import of Politrack, a self-hosted service that tracks Austrian politicians from federal to municipal level - roles, votes, money flows, statements, meetings, social media and legal proceedings - with every claim linked to its sources. - backend: FastAPI + Postgres/pgvector; adapter framework (fetch -> parse -> load -> enrich) with robots.txt/TDM-aware fetching, a raw-response archive for reparsing, and a claim/evidence model with corroboration - sources: Parliament, Wikidata, EU Parliament/Commission, Council of Ministers, Rechnungshof party reports and donations, procurement, RTR media transparency, political ad libraries, WKStA, news feeds, Wikipedia edits, Bluesky, Mastodon, public Telegram channels, YouTube and X (official API) - web: Next.js frontend in German and English with typed API client - mcp: MCP server for AI agents and RAG - docs: architecture, data model, sources, legal notes, Unraid deployment and contributor guides; Docker Compose setup for Unraid Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3.3 KiB
Extending the web app
Next.js 15 (App Router, React Server Components) with next-intl (German default, English). Pages fetch from the API on the server; there is almost no client-side JavaScript.
web/
messages/de.json, en.json all UI text (keys must exist in both)
src/app/[locale]/<route>/page.tsx pages (German route names: /personen, /parteien, /geld …)
src/components/ shared server components
src/lib/api.ts typed API client (server-side only; reads the filter cookie)
src/lib/api-types.ts GENERATED from the backend's response models – do not edit
src/lib/format.ts dates, money, localized names
src/lib/prefs.ts the user's source/topic filter (cookie)
Types come from the backend
After any change to backend/politrack/api/schemas.py:
cd backend && politrack openapi --ts ../web/src/lib/api-types.ts
cd ../web && npm run typecheck
api.ts re-exports the generated types (Claim, MoneyFlow, …) and adds one function per endpoint.
api.claims(), api.money(), api.social() … automatically apply the visitor's source filter
(withPrefs), so every list respects "which sources do I trust".
Adding a page
- Create
src/app/[locale]/<route>/page.tsx– an async server component:Filters are plainexport default async function Page({params, searchParams}: Props) { const {locale} = await params; setRequestLocale(locale); const t = await getTranslations('MyPage'); const rows = await api.something({...}); return <>…</>; }<form method="get">– shareable URLs, no client state. - Add the texts to both
messages/de.jsonandmessages/en.json. - Link it from the navigation (
src/app/[locale]/layout.tsx) or the home page's "Entdecken" cards. - Page modules may only export Next.js page fields (
default,generateMetadata, …); keep helpers and constants module-local or insrc/components.
Showing a new claim type
Claims render through ClaimList; type-specific details come from the registry in
src/components/ClaimDetails.tsx:
const ambassador: Renderer = (claim, {t}) => (
<p className="claim-detail small">{t('country')}: {String(claim.data.country ?? '')}</p>
);
const DETAILS: Partial<Record<ClaimType, Renderer[]>> = {
…,
career: [counterpart, ambassador],
};
Also add the label under Claims.type.<claim_type> in both message files, and (if it should appear) the
type to the person page's TABS.
Non-negotiables in the UI
- Every fact shows its sources (the "Quellen" disclosure under each claim,
[source]links in tables). Don't render a claim without its evidence. - Presumption of innocence:
needsInnocenceNotice()inClaimList.tsxdecides; never remove the notice for anything but an undisputed final conviction. - Connections are not accusations: pages that show networks or money of connected organisations
carry the note (
Network.note,Organizations.connectionNote). - Unverified social accounts are marked (Wikidata-only handles).
Styling
One global stylesheet (src/app/globals.css) with CSS variables for light/dark mode. Reuse the existing
classes (claims, claim-detail, chip, tag, notice, grid-2, compact, num, muted, small)
before adding new ones.