const { useState, useMemo, useEffect } = React; /* ============================================================ i18n — lightweight EN/FR. t(en, fr) picks by current lang; provided via context so every component can localise. ============================================================ */ const LangContext = React.createContext({ lang: "en", t: (en) => en, setLang: () => {} }); const useL = () => React.useContext(LangContext); const STATUS_DEFS = [ { id: "kept", label: "Kept", label_fr: "Tenue", short: "Kept", short_fr: "Tenue", def: "substantially or fully delivered", def_fr: "réalisée en grande partie ou entièrement" }, { id: "progress", label: "In Progress", label_fr: "En cours", short: "In progress", short_fr: "En cours", def: "real movement, not yet delivered", def_fr: "progrès réels, pas encore réalisée" }, { id: "stalled", label: "Stalled", label_fr: "Au point mort", short: "Stalled", short_fr: "Au pt mort", def: "started, then stuck or paused", def_fr: "amorcée, puis bloquée ou suspendue" }, { id: "broken", label: "Broken", label_fr: "Rompue", short: "Broken", short_fr: "Rompue", def: "abandoned or reversed", def_fr: "abandonnée ou annulée" }, { id: "notstarted", label: "Not Started", label_fr: "Non commencée", short: "Not started", short_fr: "Non comm.", def: "no action, or insufficient sourced reporting", def_fr: "aucune action, ou sources insuffisantes" }, ]; const TOPICS = ["Affordability", "Housing", "Workers", "Trade", "Defence", "Public Safety", "Culture", "Environment", "Energy"]; const TOPIC_FR = { "Affordability": "Abordabilité", "Housing": "Logement", "Workers": "Travailleurs", "Trade": "Commerce", "Defence": "Défense", "Public Safety": "Sécurité publique", "Culture": "Culture", "Environment": "Environnement", "Energy": "Énergie", }; const topicLabel = (topic, lang) => (lang === "fr" ? (TOPIC_FR[topic] || topic) : topic); // Pledge → most relevant live macro indicator (the "context" layer). const CONTEXT_MAP = { "tax-cut": "cpi", housing: "rate", apprentices: "jobs", auto: "fx", "trade-diversify": "fx", agrifood: "cpi", energy: "fx" }; function daysBetween(a, b) { return Math.round((new Date(b) - new Date(a)) / (1000 * 60 * 60 * 24)); } function fmtDate(s, lang = "en") { const loc = lang === "fr" ? "fr-CA" : "en-CA"; if (/^\d{4}$/.test(s)) return s; if (/^\d{4}-\d{2}$/.test(s)) return new Date(s + "-01T00:00:00").toLocaleDateString(loc, { year: "numeric", month: "short" }); return new Date(s + "T00:00:00").toLocaleDateString(loc, { year: "numeric", month: "short", day: "numeric" }); } const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "density": "compact", "sortBy": "default", "theme": "cream" }/*EDITMODE-END*/; const THEMES = { cream: { "--bg": "#efece4", "--bg-elev": "#f7f4ec", "--ink": "#14161d", "--ink-2": "#2c3040", "--ink-3": "#696d7c", "--rule": "#d2cebf", "--rule-soft": "#e1ddcd", "--accent": "#ee2737", "--accent-deep": "#b51625" }, paper: { "--bg": "#f4f3ef", "--bg-elev": "#ffffff", "--ink": "#0c0d11", "--ink-2": "#2a2c34", "--ink-3": "#73767f", "--rule": "#dedcd5", "--rule-soft": "#ecebe5", "--accent": "#ee2737", "--accent-deep": "#b51625" }, ink: { "--bg": "#0e1014", "--bg-elev": "#171a21", "--ink": "#ece9e0", "--ink-2": "#b9b6ab", "--ink-3": "#7e7c74", "--rule": "#2a2d36", "--rule-soft": "#1f222a", "--accent": "#ff4d5b", "--accent-deep": "#ee2737" }, }; function App() { const data = window.PLEDGE_DATA; const [lang, setLang] = useState("en"); const t = (en, fr) => (lang === "fr" ? fr : en); const [activeStatuses, setActiveStatuses] = useState(new Set()); const [activeTopics, setActiveTopics] = useState(new Set()); const [search, setSearch] = useState(""); const [tw, setTweak] = window.useTweaks(TWEAK_DEFAULTS); // Apply theme variables useEffect(() => { const theme = THEMES[tw.theme] || THEMES.cream; const root = document.documentElement; Object.entries(theme).forEach(([k, v]) => root.style.setProperty(k, v)); }, [tw.theme]); useEffect(() => { document.documentElement.lang = lang; }, [lang]); // Topline aggregates const counts = useMemo(() => { const c = { kept: 0, progress: 0, stalled: 0, broken: 0, notstarted: 0 }; data.pledges.forEach(p => c[p.status]++); return c; }, [data]); const total = data.pledges.length; const avgProgress = useMemo(() => Math.round(data.pledges.reduce((s, p) => s + p.progress, 0) / total), [data, total] ); // Filter + sort const visible = useMemo(() => { let list = data.pledges.filter(p => { if (activeStatuses.size && !activeStatuses.has(p.status)) return false; if (activeTopics.size && !activeTopics.has(p.topic)) return false; if (search.trim()) { const q = search.toLowerCase(); const hay = `${p.title} ${p.title_fr || ""} ${p.pledge} ${p.pledge_fr || ""} ${p.note} ${p.note_fr || ""} ${p.topic}`.toLowerCase(); if (!hay.includes(q)) return false; } return true; }); if (tw.sortBy === "progress") { list = [...list].sort((a, b) => b.progress - a.progress); } else if (tw.sortBy === "recent") { list = [...list].sort((a, b) => new Date(b.lastUpdate) - new Date(a.lastUpdate)); } else if (tw.sortBy === "status") { const order = ["broken", "stalled", "notstarted", "progress", "kept"]; list = [...list].sort((a, b) => order.indexOf(a.status) - order.indexOf(b.status)); } return list; }, [data, activeStatuses, activeTopics, search, tw.sortBy]); const toggle = (set, setter) => (id) => { const next = new Set(set); if (next.has(id)) next.delete(id); else next.add(id); setter(next); }; const daysInOffice = daysBetween(data.meta.sworn_in, data.meta.as_of); return (
{window.PollingSection ? React.createElement(window.PollingSection, { lang }) : null} setTweak("sortBy", v)} />
); } function Masthead({ asOf, daysInOffice }) { const { lang, t, setLang } = useL(); return (
C
The Carney Tracker
{t("Day", "Jour")} {daysInOffice} {t("of mandate", "du mandat")} {t("Data as of", "Données au")} {fmtDate(asOf, lang)}
); } function Hero() { const { t } = useL(); return (
{t("A Liberal pledge tracker · 45th Parliament", "Suivi des promesses libérales · 45e législature")}

{t( <>Eleven promises. One year in. Where are we?, <>Onze promesses. Un an plus tard. Où en sommes-nous ? )}

{t( "Tracking the Liberal Party's 2025 platform commitments — what's been delivered, what's stuck, and what's quietly drifting off the agenda.", "Suivi des engagements de la plateforme libérale de 2025 — ce qui a été réalisé, ce qui est bloqué, et ce qui s'éloigne discrètement de l'ordre du jour." )}

); } /* ============================================================ MacroStrip — live Canadian macro indicators (BoC + StatCan), seeded from build-time macro.js; rate/fx refresh live. ============================================================ */ const MACRO_INDICATORS = [ { id: "rate", label: "BoC Policy Rate", label_fr: "Taux directeur BdC", series: "V39079", unit: "%", fmt: (v) => v.toFixed(2), source: "Bank of Canada", source_fr: "Banque du Canada", sourceUrl: "https://www.bankofcanada.ca/core-functions/monetary-policy/key-interest-rate/", fallback: { value: 2.75, date: "2026-04-16", prev: 3.00 } }, { id: "fx", label: "USD / CAD", label_fr: "USD / CAD", series: "FXUSDCAD", unit: "", fmt: (v) => v.toFixed(4), source: "Bank of Canada", source_fr: "Banque du Canada", sourceUrl: "https://www.bankofcanada.ca/rates/exchange/", fallback: { value: 1.3645, date: "2026-05-22", prev: 1.3702 } }, { id: "cpi", label: "CPI Inflation (y/y)", label_fr: "Inflation IPC (a/a)", series: null, unit: "%", fmt: (v) => v.toFixed(1), source: "Statistics Canada", source_fr: "Statistique Canada", sourceUrl: "https://www150.statcan.gc.ca/n1/daily-quotidien/240416/dq240416a-eng.htm", fallback: { value: 2.1, date: "2026-04-15", prev: 2.3 } }, { id: "jobs", label: "Unemployment", label_fr: "Chômage", series: null, unit: "%", fmt: (v) => v.toFixed(1), source: "Statistics Canada", source_fr: "Statistique Canada", sourceUrl: "https://www150.statcan.gc.ca/n1/daily-quotidien/240510/dq240510a-eng.htm", fallback: { value: 6.4, date: "2026-05-09", prev: 6.5 } }, ]; async function fetchValet(series) { const url = `https://www.bankofcanada.ca/valet/observations/${series}/json?recent=2`; const r = await fetch(url); if (!r.ok) throw new Error("HTTP " + r.status); const j = await r.json(); // Valet returns observations newest-first; normalise to chronological. const obs = (j.observations || []).slice().sort((a, b) => a.d.localeCompare(b.d)); if (!obs.length) throw new Error("no observations"); const key = Object.keys(obs[obs.length - 1]).find(k => k !== "d"); const last = obs[obs.length - 1]; const prev = obs[obs.length - 2]; return { value: parseFloat(last[key].v), date: last.d, prev: prev ? parseFloat(prev[key].v) : null }; } function MacroStrip() { const { lang, t } = useL(); const [data, setData] = useState(() => Object.fromEntries(MACRO_INDICATORS.map(i => { const seed = (window.MACRO_DATA && window.MACRO_DATA[i.id]) || i.fallback; return [i.id, { ...seed, live: false, loading: !!i.series }]; })) ); useEffect(() => { let cancelled = false; (async () => { const live = {}; await Promise.all(MACRO_INDICATORS.filter(i => i.series).map(async (ind) => { try { const res = await fetchValet(ind.series); if (!cancelled) live[ind.id] = { ...res, live: true, loading: false }; } catch (e) { if (!cancelled) live[ind.id] = { ...((window.MACRO_DATA && window.MACRO_DATA[ind.id]) || ind.fallback), live: false, loading: false }; } })); if (!cancelled && Object.keys(live).length) setData(prev => ({ ...prev, ...live })); })(); return () => { cancelled = true; }; }, []); const anyLive = Object.values(data).some(d => d.live); return (
Canada
{anyLive ? t("Live data", "Données en direct") : t("Latest data", "Dernières données")}
{MACRO_INDICATORS.map(ind => { const d = data[ind.id]; const delta = d.prev != null ? d.value - d.prev : null; const dir = delta == null ? null : delta > 0 ? "up" : delta < 0 ? "down" : null; const src = lang === "fr" ? ind.source_fr : ind.source; return (
{lang === "fr" ? ind.label_fr : ind.label}
{d.loading ? "—" : ind.fmt(d.value)} {ind.unit && {ind.unit}}
{delta != null && ( {delta > 0 ? "▲" : delta < 0 ? "▼" : "·"} {Math.abs(delta).toFixed(ind.unit === "%" ? 2 : 4)} )} · {src.split(" ").map(w => w[0]).join("")} · {d.date}
); })}
); } function Dashboard({ counts, total, avgProgress, activeStatuses, toggleStatus, data }) { const { lang, t } = useL(); const segments = STATUS_DEFS.filter(s => counts[s.id] > 0); const kept = counts.kept; const keptPct = Math.round((kept / total) * 100); const latest = data.pledges.reduce((a, b) => new Date(a.lastUpdate) > new Date(b.lastUpdate) ? a : b); const latestTitle = lang === "fr" ? (latest.title_fr || latest.title) : latest.title; return (
{t("Overall Pledge Score", "Score global des promesses")}
{avgProgress}%
{segments.map(s => (
))}
{STATUS_DEFS.map(s => (
toggleStatus(s.id)}> {lang === "fr" ? s.label_fr : s.label} {counts[s.id]}
))}
{t("Pledges Tracked", "Promesses suivies")}
{total}
{t("From the Liberal 2025 platform", "Plateforme libérale 2025")}
{t("Kept", "Tenues")}
{kept} · {keptPct}%
{t("Substantially or fully delivered", "Réalisées en grande partie ou entièrement")}
{t("At Risk", "À risque")}
{counts.stalled + counts.broken}
{t("Stalled or broken", "Au point mort ou rompues")}
{t("Last Movement", "Dernier mouvement")}
{fmtDate(latest.lastUpdate, lang)}
{latestTitle}
); } function Controls({ activeStatuses, toggleStatus, activeTopics, toggleTopic, search, setSearch, sortBy, setSortBy }) { const { lang, t } = useL(); const totalActive = activeStatuses.size + activeTopics.size; const clearAll = () => { activeStatuses.forEach(s => toggleStatus(s)); activeTopics.forEach(s => toggleTopic(s)); }; return (
setSearch(e.target.value)} />
{t("Status", "Statut")}
{STATUS_DEFS.map(s => ( ))}
{t("Topic", "Sujet")}
{TOPICS.map(topic => ( ))}
{totalActive > 0 && ( )}
); } function PledgeList({ visible, total }) { const { t } = useL(); return (
{t("Showing", "Affichage de")} {visible.length} {t("of", "sur")} {total}
{visible.length === 0 && (
{t("No pledges match these filters.", "Aucune promesse ne correspond à ces filtres.")}
)} {visible.map((p, i) => )}
); } function PledgeRow({ pledge, num }) { const { lang, t } = useL(); const def = STATUS_DEFS.find(s => s.id === pledge.status); const title = lang === "fr" ? (pledge.title_fr || pledge.title) : pledge.title; const text = lang === "fr" ? (pledge.pledge_fr || pledge.pledge) : pledge.pledge; const note = lang === "fr" ? (pledge.note_fr || pledge.note) : pledge.note; // Live macro context tied to this pledge (the integrated-context differentiator). const ctxId = CONTEXT_MAP[pledge.id]; let ctx = null; if (ctxId) { const ind = MACRO_INDICATORS.find(i => i.id === ctxId); const md = (window.MACRO_DATA && window.MACRO_DATA[ctxId]) || (ind && ind.fallback); if (ind && md) ctx = `${lang === "fr" ? ind.label_fr : ind.label} ${ind.fmt(md.value)}${ind.unit}`; } return (
{String(num).padStart(2, "0")}
{topicLabel(pledge.topic, lang)}

{title}

{text}

{note}
{t("Updated", "Mise à jour")} {fmtDate(pledge.lastUpdate, lang)} · Source {ctx && <>·{t("Context", "Contexte")}: {ctx}}
{lang === "fr" ? def.label_fr : def.label}
{t("Progress", "Progrès")} {pledge.progress}%
); } /* ============================================================ Methodology — the trust layer (status defs, AI role, sources, non-partisan statement, open data). Bilingual, inline-styled. ============================================================ */ function Methodology({ asOf }) { const { lang, t } = useL(); const col = { fontSize: 13.5, color: "var(--ink-2)", lineHeight: 1.6 }; const h = { fontFamily: "var(--mono)", fontSize: 10, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--ink-3)", margin: "0 0 10px" }; return (
{t("Methodology & sources", "Méthodologie et sources")}

{t("How these verdicts are made", "Comment ces verdicts sont établis")}

{t("Status definitions", "Définitions des statuts")}

{STATUS_DEFS.map(s => (
{lang === "fr" ? s.label_fr : s.label} — {lang === "fr" ? s.def_fr : s.def}
))}

{t("How a status is set", "Comment un statut est établi")}

{t( "Each pledge is classified from a dossier of cited public reporting (government releases, Statistics Canada, Parliament's bill tracker, and major outlets). An AI model (DeepSeek) analyses only that dossier — never its own training memory — and the reasoning behind every verdict, with source quotes, is kept in an open audit file.", "Chaque promesse est classée à partir d'un dossier de sources publiques citées (communiqués gouvernementaux, Statistique Canada, le suivi des projets de loi du Parlement et grands médias). Un modèle d'IA (DeepSeek) analyse uniquement ce dossier — jamais sa mémoire d'entraînement — et le raisonnement de chaque verdict, avec citations, est conservé dans un fichier d'audit ouvert." )}

{t( "A pledge's progress % is an analytic estimate derived from the cited facts — not a measured value. Where sourced reporting is insufficient, a pledge is marked Not Started rather than guessed.", "Le pourcentage de progrès d'une promesse est une estimation analytique fondée sur les faits cités — non une valeur mesurée. Faute de sources suffisantes, une promesse est marquée « Non commencée » plutôt que devinée." )}

{t("Independence & data", "Indépendance et données")}

{t( "This is a non-partisan accountability tool — it tracks delivery against the published platform, not whether a promise was wise. Kept and Broken verdicts are shown on equal footing.", "Outil de reddition de comptes non partisan — il mesure la réalisation par rapport à la plateforme publiée, et non la pertinence d'une promesse. Les verdicts « Tenue » et « Rompue » sont présentés sur un pied d'égalité." )}

{t( "Vote share is a monthly average of published polls; approval is the Angus Reid Institute approve rating; macro indicators are live from the Bank of Canada and Statistics Canada.", "L'intention de vote est une moyenne mensuelle des sondages publiés; le taux d'approbation provient de l'Institut Angus Reid; les indicateurs macro sont en direct de la Banque du Canada et de Statistique Canada." )}

{t("Open data", "Données ouvertes")}: data.json · {t("Updated", "Mis à jour")} {fmtDate(asOf, lang)}

); } function Footnote({ asOf, sample }) { const { lang, t } = useL(); return (

{sample && <>{t("Sample data.", "Données d'exemple.")}{" "}} {t( <>Pledge text is from the Liberal Party of Canada's 2025 platform at liberal.ca/plan. Status and progress are analyzed from cited public reporting — each pledge links its source. See the methodology above; progress is an analytic estimate, not a measured value., <>Le texte des promesses provient de la plateforme 2025 du Parti libéral du Canada (liberal.ca/plan). Le statut et le progrès sont analysés à partir de sources publiques citées — chaque promesse renvoie à sa source. Voir la méthodologie ci-dessus; le progrès est une estimation analytique, non une valeur mesurée. )}

{t("Data as of", "Données au")} · {fmtDate(asOf, lang)}
); } function TrackerTweaks({ t, setTweak }) { const { TweaksPanel: Panel, TweakSection, TweakRadio, TweakSelect } = window; if (!Panel) return null; return ( setTweak("theme", v)} options={[{ value: "cream", label: "Cream" }, { value: "paper", label: "Paper" }, { value: "ink", label: "Ink" }]} /> setTweak("density", v)} options={[{ value: "comfortable", label: "Comfortable" }, { value: "compact", label: "Compact" }]} /> setTweak("sortBy", v)} options={[{ value: "default", label: "Original order" }, { value: "progress", label: "Most progress" }, { value: "recent", label: "Most recent" }, { value: "status", label: "At-risk first" }]} /> ); } ReactDOM.createRoot(document.getElementById("root")).render();