Python 51.6%
TypeScript 46.7%
CSS 1.7%
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