SPB Git

spb/qc26

Public
7.3 KB · 220 lines typescript
1/** Accès serveur → moteur QC26 (FastAPI). Côté client, les appels passent par le rewrite /api. */2export const ENGINE_URL = process.env.QC26_ENGINE_URL || "http://127.0.0.1:8201";3export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://www.qc26.ca";45export class ApiError extends Error {6  constructor(public status: number, message: string) {7    super(message);8  }9}1011export async function api<T>(path: string, opts: { revalidate?: number; cache?: RequestCache } = {}): Promise<T> {12  const url = `${ENGINE_URL}${path}`;13  const res = await fetch(url, {14    next: { revalidate: opts.revalidate ?? 60 },15    cache: opts.cache,16    headers: { accept: "application/json" },17  });18  if (!res.ok) throw new ApiError(res.status, `${res.status} ${path}`);19  return res.json() as Promise<T>;20}2122export async function apiSafe<T>(path: string, opts: { revalidate?: number } = {}): Promise<T | null> {23  try {24    return await api<T>(path, opts);25  } catch {26    return null;27  }28}2930// ------------------------------------------------------------------ types31export type PartyId = "caq" | "plq" | "pq" | "qs" | "pcq";32export type PartyKey = PartyId | "aut";3334export interface Meta {35  electionDate: string;36  electionName: string;37  campaignStart: string;38  pollsCloseLocal: string;39  seatsTotal: number;40  majority: number;41  daysToElection: number;42  mode: "forecast" | "live" | "archive";43  simulation: boolean;44  modelVersion: string;45  latestRun: { id: number; completedAt: string | null; pollCount: number; modelVersion: string } | null;46  parties: RegistryParty[];47  others: { id: string; label: string; color: string };48  pollRegions: Record<string, string>;49  topics: Record<string, string>;50  axes: Record<string, { label: string; left: string; right: string }>;51  disclaimer: string;52}5354export interface RegistryParty {55  id: PartyId;56  abbreviation: string;57  officialName: string;58  shortName: string;59  color: string;60  secondaryColor: string;61  website: string;62  dgeqId: string;63  colorNote: string;64  order: number;65}6667export interface PartyForecast {68  voteMean: number;69  voteSd: number;70  vote80: [number, number];71  vote95: [number, number];72  seatsMean: number;73  seatsMedian: number;74  seats80: [number, number];75  seats95: [number, number];76  pMostSeats: number;77  pMajority: number;78  seatDistribution: number[] | null;79}8081export interface TrendPoint {82  date: string;83  mean: Record<string, number>;84  sd: Record<string, number>;85  n_polls: number;86}8788export interface ForecastSummaryParty {89  vote: number;90  vote_sd: number;91  vote80: [number, number];92  seats: number;93  seats_mean: number;94  seats80: [number, number];95  seats95: [number, number];96  p_most_seats: number;97  p_majority: number;98}99100export interface Forecast {101  id: number;102  modelVersion: string;103  dataCutoff: string;104  completedAt: string | null;105  pollCount: number;106  nSimulations: number;107  seed: number;108  codeCommit: string | null;109  trigger: string | null;110  summary: {111    seats_total: number;112    majority: number;113    parties: Record<string, ForecastSummaryParty>;114    p_minority: number;115    leader: string;116    poll_count: number;117    latest_poll: string;118    national_now: Record<string, number>;119    cutoff: string;120    days_to_election: number;121    battlegrounds: number[];122    ridings: Record<string, { fav: string; p: number; second: string; margin: number }>;123  };124  diff: {125    previous_run: number;126    previous_at: string | null;127    changes: Record<string, { vote: number; seats: number; p_majority: number; p_most_seats: number }>;128    flips: { code: number; from: string; to: string; p: number }[];129    causes: { kind: string; detail: string | string[] }[];130  } | null;131  parties: Record<string, PartyForecast>;132  trend: TrendPoint[] | null;133}134135export interface Party extends RegistryParty {136  leader: string | null;137  leaderSince: string | null;138  dgeqName: string | null;139  spokespersons: string | null;140  candidatesCount: number;141  seats2022: number;142  vote2022: number | null;143  forecast: { voteMean: number; seatsMedian: number; seats80: [number, number]; pMajority: number; pMostSeats: number } | null;144  documents: number;145  proposals: number;146}147148export interface RidingListItem {149  code: number;150  name: string;151  slug: string;152  region: string | null;153  regionName: string | null;154  electors: number | null;155  isNew: boolean;156  changed: boolean;157  incumbent: { party: string | null; name: string | null; running: boolean | null };158  centroid: [number | null, number | null];159  forecast: { favorite: string; p: number; runnerUp: string | null; margin: number; rating: string; swing: number; pWin: Record<string, number> } | null;160  live: { status: string; leader: string | null; bureaux: [number, number]; marginPct: number | null } | null;161}162163export interface Poll {164  id: string;165  pollster: { id: string; name: string };166  sponsor: string | null;167  publicationDate: string | null;168  fieldworkStart: string | null;169  fieldworkEnd: string;170  sampleSize: number | null;171  population: string | null;172  methodology: string | null;173  mode: string | null;174  marginOfError: number | null;175  sourceUrl: string | null;176  sourceTier: string;177  extractionMethod: string;178  confidence: number;179  validated: boolean;180  excluded: boolean;181  exclusionReason: string | null;182  undecided: number | null;183  results: Record<string, number>;184  resultsPage: number | null;185  ingestedAt: string | null;186  document: { id: number; sha256: string; pages: number | null; bytes: number; contentType: string | null; downloadedAt: string; archiveUrl: string } | null;187  weight: { weight: number; recency: number; sample: number; impact: Record<string, number> | null } | null;188  validation: { checks?: string[]; warnings?: string[]; sum?: number } | null;189  nonPrimarySource: boolean;190  subsamples?: Record<string, Record<string, Record<string, number | null>>>;191  leadership?: { question: string; page: number; values: Record<string, number> }[] | null;192  llmNotes?: string[] | null;193  houseEffects?: Record<string, number>;194  audit?: { ts: string; actor: string; field: string | null; old: string | null; new: string | null; reason: string | null }[];195}196197export interface LiveState {198  mode: string;199  simulation: boolean;200  seatsNow: Record<string, { elected: number; leading: number }>;201  lastValid: { snapshot_id: number; at: string; source_updated_at: string | null; reporting: number; simulated: boolean } | null;202  quarantine: boolean;203  quarantineReason: string | null;204  snapshot: { id: number; fetchedAt: string; sourceUpdatedAt: string | null; reporting: number; total: number; bureaux: [number, number]; final: boolean } | null;205  nowcast: {206    id: number;207    ts: string;208    majority: number;209    reporting: number;210    coverage: number;211    p_minority: number;212    parties: Record<string, { elected: number; leading: number; total_now: number; seats_mean: number; seats_median: number; seats80: [number, number]; p_majority: number; p_most_seats: number; vote_est: number | null }>;213    projected_now: number[];214  } | null;215  ridings: Record<string, { status: string; leader: string | null; leaderCandidate: string | null; bureaux: [number, number]; votes: number; marginVotes: number | null; marginPct: number | null; turnout: number | null; candidates: { name: string; party_id: string; votes: number; share: number }[] | null; final: boolean }>;216  nowcastRidings: Record<string, { p: number[]; est: number[]; w: number }> | null;217  seatsTotal: number;218  majority: number;219}220
Files · 08f5606551574077c8600a5faf68d3daef5dbc10
Menu

Theme