// File: page.tsx // Path: apps/web/app/ranking/page.tsx // Project: AI Risk Index — airiskindex.io // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // // Description: Full index — every scored occupation ranked, paginated by 50. import Link from "next/link"; import { prisma } from "@airiskindex/db"; import { ScoreBar } from "@/components/score-marks"; import { formatWage } from "@/lib/soc-groups"; export const dynamic = "force-dynamic"; export const metadata = { title: "Full index — AI Risk Index", description: "Every scored occupation ranked by substitution pressure, with confidence intervals.", }; const PER_PAGE = 50; export default async function RankingPage({ searchParams, }: { searchParams: { page?: string }; }): Promise { const run = await prisma.scoreRun.findFirst({ orderBy: { createdAt: "desc" } }); const total = run ? await prisma.occupationScore.count({ where: { runId: run.id } }) : 0; const pages = Math.max(1, Math.ceil(total / PER_PAGE)); const page = Math.min(pages, Math.max(1, Number(searchParams.page ?? "1") || 1)); const rows = run ? await prisma.occupationScore.findMany({ where: { runId: run.id }, orderBy: { substitution: "desc" }, skip: (page - 1) * PER_PAGE, take: PER_PAGE, include: { occupation: { select: { code: true, title: true, medianWageCents: true, employment: true }, }, }, }) : []; const pager = (
← Previous = pages} className={`card px-4 py-2 font-medium ${page >= pages ? "pointer-events-none opacity-40" : "card-hover"}`} > Next →

Page {page} / {pages} · ranks {(page - 1) * PER_PAGE + 1}– {Math.min(total, page * PER_PAGE)} of {total.toLocaleString("en-US")}

); return (

Full index{run ? ` · run ${run.indexVersion} · ${run.createdAt.toISOString().slice(0, 10)}` : ""}

All {total.toLocaleString("en-US")} scored occupations

Ranked by composite substitution pressure (0–100). The whisker is the confidence interval from rater disagreement. Wages are U.S. national medians (BLS OEWS).

{pager}
    {rows.map((row, index) => { const rank = (page - 1) * PER_PAGE + index + 1; const wage = formatWage(row.occupation.medianWageCents); return (
  1. {rank} {row.occupation.title} {row.substitution.toFixed(0)}

    CI {row.substitutionLow.toFixed(0)}–{row.substitutionHigh.toFixed(0)} · exposure {row.exposure.toFixed(0)} · augmentation{" "} {row.augmentation.toFixed(0)} {wage && <> · {wage}} {row.occupation.employment != null && ( <> · {row.occupation.employment.toLocaleString("en-US")} employed )}

  2. ); })}
{pager}
); }