SPB Git forge

spb/qc26

Public
10commits 1branches 0releases
2.8 MBsize
maindefault branch
17 days agolast push
Python 51.6% TypeScript 46.7% CSS 1.7%
12.5 KB · 243 lines typescript
Raw Blame History
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    components?: ForecastComponents;124  };125  diff: {126    previous_run: number;127    previous_at: string | null;128    changes: Record<string, { vote: number; seats: number; p_majority: number; p_most_seats: number }>;129    flips: { code: number; from: string; to: string; p: number }[];130    causes: { kind: string; detail: string | string[] | Record<string, number> }[];131  } | null;132  parties: Record<string, PartyForecast>;133  trend: TrendPoint[] | null;134}135136export interface ComponentParties { parties: Record<string, ForecastSummaryParty & { vote95?: [number, number] }>; p_minority: number }137export interface ForecastComponents {138  pollsOnly: ComponentParties;139  modelNoMarkets: ComponentParties;140  fundamentals: { enabled: boolean; weight: number; daysLeft?: number; satisfaction?: number; satisfactionPolls?: number; incumbent?: string; incumbentPrior?: number; prior?: Record<string, number>; shiftPp?: Record<string, number>; params?: Record<string, number> };141  momentum: { enabled: boolean; shiftPp: Record<string, number>; parts: { leader: Record<string, number>; attention: Record<string, number>; media: Record<string, number> }; params: Record<string, number> };142  markets: { enabled: boolean; weight: number; bandWeight?: number; ess?: number; constraints?: string[]; achieved?: Record<string, Record<string, number>>; marketAsOf?: string | null; marketVolume?: number; mostSeatsMarket?: Record<string, number>; majorityMarket?: Record<string, number>; seatBandsImpliedMean?: Record<string, number> };143  byelections: { enabled: boolean; ridings: { code: number; riding: string; date: string; adjLogit: Record<string, number>; kappa: number }[] };144  signals: { attention: { enabled?: boolean; asOf?: string; share7?: Record<string, number>; share28?: Record<string, number>; delta?: Record<string, number> }; media: { enabled?: boolean; asOf?: string; articles?: number; shareOfVoice?: Record<string, number>; tone?: Record<string, number | null>; counts?: Record<string, number> }; fundamentals: { enabled?: boolean; satisfaction?: number | null; satisfactionNet?: number | null; bestPmShare?: Record<string, number | null>; leaderNetFavorable?: Record<string, number | null>; secondChoice?: Record<string, number | null>; sovereigntyYes?: number | null }; markets: { asOf?: string | null; mostSeats?: Record<string, number>; majority?: Record<string, number>; volume?: number } | null; byelections: { n: number; meanSwing: Record<string, number | null> | null } };145  ess: number;146  nSimulations: number;147}148149export interface MarketRow { id: string; question: string; kind: string; party: string | null; price: number; bid: number | null; ask: number | null; volume: number | null; liquidity: number | null; url: string | null; updatedAt: string | null; band: [number | null, number | null] | null; place: number | null }150export interface MarketsPayload { source: string; asOf: string | null; mostSeats: Record<string, number>; mostSeatsRaw?: Record<string, number>; majority: Record<string, number>; seatBands: Record<string, { bands: { lo: number; hi: number | null; p: number }[]; impliedMean: number }>; place: Record<string, Record<string, number>>; volume: number; markets: MarketRow[]; history: Record<string, { date: string; p: number }[]> }151export interface AttentionPayload { enabled: boolean; asOf?: string; share7?: Record<string, number>; share28?: Record<string, number>; delta?: Record<string, number>; series?: { date: string; share: Record<string, number>; views: Record<string, number> }[]; election?: { date: string; views: number }[]; pages?: Record<string, string[]> }152export interface MediaItem { id: number; title: string; domain: string | null; url: string; publishedAt: string; parties: Record<string, number> | null; topic: string | null; summary: string | null }153export interface MediaPayload { enabled: boolean; window: number; asOf: string; articles: number; shareOfVoice: Record<string, number>; tone: Record<string, number | null>; counts: Record<string, number>; gdelt: Record<string, { volume7: number | null; volume28: number | null; tone7: number | null }>; series: { date: string; articles?: Record<string, number>; share_of_voice?: Record<string, number>; tone?: Record<string, number> }[]; latest: MediaItem[]; topics: Record<string, number> }154export interface FundamentalsPayload { enabled: boolean; asOf: string; incumbent: string; satisfaction: number | null; satisfactionNet: number | null; satisfactionPolls: number; satisfactionBilan: number | null; bestPmShare: Record<string, number | null>; bestPm: Record<string, number | null>; leaderNetFavorable: Record<string, number | null>; leaderFavorable: Record<string, number | null>; changeLeader: Record<string, number | null>; secondChoice: Record<string, number | null>; sovereigntyYes: number | null; sovereigntyPolls: number; series: Record<string, { date: string; party: string | null; value: number; poll: string | null; pollster: string | null }[]>; params: Record<string, number> }155export interface ByElectionsPayload { enabled: boolean; n: number; meanSwing: Record<string, number | null>; byElections: { id: string; date: string; riding: string | null; code: number; url: string; turnout: number | null; winner: string | null; results: { candidate: string; party_label: string; party_id: string | null; votes: number; share: number }[]; previous: Record<string, number> | null; swing: Record<string, number>; archiveSha: string | null }[] }156export interface SignalsPayload { asOf: string; runId: number | null; components: ForecastComponents | null; markets: MarketsPayload; attention: AttentionPayload; media: MediaPayload; fundamentals: FundamentalsPayload; byElections: ByElectionsPayload }157158export interface Party extends RegistryParty {159  leader: string | null;160  leaderSince: string | null;161  dgeqName: string | null;162  spokespersons: string | null;163  candidatesCount: number;164  seats2022: number;165  vote2022: number | null;166  forecast: { voteMean: number; seatsMedian: number; seats80: [number, number]; pMajority: number; pMostSeats: number } | null;167  documents: number;168  proposals: number;169}170171export interface RidingListItem {172  code: number;173  name: string;174  slug: string;175  region: string | null;176  regionName: string | null;177  electors: number | null;178  isNew: boolean;179  changed: boolean;180  incumbent: { party: string | null; name: string | null; running: boolean | null };181  centroid: [number | null, number | null];182  forecast: { favorite: string; p: number; runnerUp: string | null; margin: number; rating: string; swing: number; pWin: Record<string, number> } | null;183  live: { status: string; leader: string | null; bureaux: [number, number]; marginPct: number | null } | null;184}185186export interface Poll {187  id: string;188  pollster: { id: string; name: string };189  sponsor: string | null;190  publicationDate: string | null;191  fieldworkStart: string | null;192  fieldworkEnd: string;193  sampleSize: number | null;194  population: string | null;195  methodology: string | null;196  mode: string | null;197  marginOfError: number | null;198  sourceUrl: string | null;199  sourceTier: string;200  extractionMethod: string;201  confidence: number;202  validated: boolean;203  excluded: boolean;204  exclusionReason: string | null;205  undecided: number | null;206  results: Record<string, number>;207  resultsPage: number | null;208  ingestedAt: string | null;209  document: { id: number; sha256: string; pages: number | null; bytes: number; contentType: string | null; downloadedAt: string; archiveUrl: string } | null;210  weight: { weight: number; recency: number; sample: number; impact: Record<string, number> | null } | null;211  validation: { checks?: string[]; warnings?: string[]; sum?: number } | null;212  nonPrimarySource: boolean;213  subsamples?: Record<string, Record<string, Record<string, number | null>>>;214  leadership?: { question: string; page: number; values: Record<string, number> }[] | null;215  llmNotes?: string[] | null;216  houseEffects?: Record<string, number>;217  audit?: { ts: string; actor: string; field: string | null; old: string | null; new: string | null; reason: string | null }[];218}219220export interface LiveState {221  mode: string;222  simulation: boolean;223  seatsNow: Record<string, { elected: number; leading: number }>;224  lastValid: { snapshot_id: number; at: string; source_updated_at: string | null; reporting: number; simulated: boolean } | null;225  quarantine: boolean;226  quarantineReason: string | null;227  snapshot: { id: number; fetchedAt: string; sourceUpdatedAt: string | null; reporting: number; total: number; bureaux: [number, number]; final: boolean } | null;228  nowcast: {229    id: number;230    ts: string;231    majority: number;232    reporting: number;233    coverage: number;234    p_minority: number;235    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 }>;236    projected_now: number[];237  } | null;238  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 }>;239  nowcastRidings: Record<string, { p: number[]; est: number[]; w: number }> | null;240  seatsTotal: number;241  majority: number;242}243