API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com
Python 60.9%
HTML 21%
TypeScript 7.3%
JavaScript 5.2%
CSS 4.8%
Shell 0.8%
1<!DOCTYPE html>2<!--3============================================4Projet : API-KA5Fichier : src/api/web/index.html6Node : m3u96b7Author : Simon-Pierre Boucher8Contact : contact@spboucher.ai9Date : 2026-08-1710============================================11Design system Groupe KA « éditorial sharp » — tokens.css (ka-ui) importé en12premier, accent indigo propre à API-Ka surchargé en :root ci-dessous.13-->14<html lang="fr">15<head>16<meta charset="utf-8">17<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">18<title>API-Ka — Un service Groupe KA</title>19<meta name="description" content="API publique centralisant chaque jour les données des services de l'écosystème Groupe KA — logements, propriétés, épicerie, véhicules, produits québécois, restaurants, sorties et créateurs. Documentation complète et playground interactif.">20<meta name="theme-color" content="#f5f3ee">21<link rel="icon" type="image/svg+xml" href="/favicon.svg">22<link rel="apple-touch-icon" href="/apple-touch-icon.png">23<meta property="og:title" content="API-Ka — Un service Groupe KA">24<meta property="og:description" content="API publique centralisant chaque jour les données des services de l'écosystème Groupe KA — logements, propriétés, épicerie, véhicules, produits québécois, restaurants, sorties et créateurs. Documentation complète et playground interactif.">25<meta property="og:url" content="https://www.api-ka.com/">26<meta property="og:type" content="website">27<meta property="og:image" content="https://www.api-ka.com/og.png">28<meta property="og:image:width" content="1200">29<meta property="og:image:height" content="630">30<meta name="twitter:card" content="summary_large_image">31<meta name="twitter:image" content="https://www.api-ka.com/og.png">32<link rel="preconnect" href="https://fonts.googleapis.com">33<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>34<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">35<link rel="stylesheet" href="/ka/tokens.css">36<style>37/* ---- Accent API-Ka : INDIGO (seules variables surchargées de tokens.css) ---- */38:root{39 --accent:#3b5bdb; --accent-soft:#e4eafb; --accent-deep:#2b44a8; --on-accent:#ffffff;40}41html{scroll-padding-top:80px}4243/* ================= topbar ================= */44.topbar{position:sticky;top:0;z-index:var(--z-header,500);background:var(--paper);border-bottom:1.5px solid var(--ink)}45.topbar-in{display:flex;align-items:center;gap:14px;min-height:64px;flex-wrap:wrap;padding-top:8px;padding-bottom:8px}46.brand{font-family:var(--font-display);font-weight:700;font-size:24px;letter-spacing:-.03em;color:var(--ink);text-decoration:none;display:inline-flex;align-items:center;min-height:var(--touch);white-space:nowrap}47.brand .ka{display:inline-block;background:var(--ink);color:var(--accent);border-radius:6px;padding:0 7px 2px;margin-left:5px;transform:rotate(-2deg);transition:transform .15s}48.brand:hover .ka{transform:rotate(0)}49.topnav{display:flex;gap:2px;list-style:none;margin:0 0 0 auto;padding:0;overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}50.topnav a{display:inline-flex;align-items:center;min-height:var(--touch);text-decoration:none;font-family:var(--font-mono);font-size:11.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-2);padding:0 12px;border-radius:var(--r-ctl);white-space:nowrap;transition:background .13s,color .13s}51.topnav a:hover{background:var(--accent-soft);color:var(--accent-deep)}52.topbar-tools{display:flex;align-items:center;gap:10px;flex-wrap:wrap}53#ka-auth{display:flex;align-items:center;gap:8px}54#ka-auth .btn{font-size:12.5px;padding:8px 14px}55.ka-user{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-2)}56.ka-user b{font-family:var(--font-display);font-size:13px;text-transform:none;letter-spacing:-.01em;color:var(--ink)}57.ka-avatar{width:28px;height:28px;border-radius:50%;border:1.5px solid var(--ink);object-fit:cover}58.live-chip{display:inline-flex;align-items:center;gap:8px;border:1.5px solid var(--ink);border-radius:var(--r-pill);padding:6px 14px;min-height:34px;background:var(--surface);font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--ink-2);white-space:nowrap}59.pulse{width:8px;height:8px;border-radius:50%;background:var(--ink-3)}60.pulse.ok{background:var(--accent);box-shadow:0 0 0 4px var(--accent-soft);animation:pulse 2.4s ease infinite}61.pulse.err{background:var(--danger);box-shadow:0 0 0 4px var(--danger-soft)}62@keyframes pulse{0%,100%{box-shadow:0 0 0 4px var(--accent-soft)}50%{box-shadow:0 0 0 7px rgba(59,91,219,.22)}}63@media(max-width:1023px){.topnav{display:none}.ka-mnav-btn{display:flex}}64@media(max-width:767px){.live-chip{display:none}}6566/* ================= hero ================= */67.hero{padding:56px 0 64px;border-bottom:1.5px solid var(--ink);background:68 linear-gradient(rgba(59,91,219,.05),transparent 55%),var(--paper)}69.hero-grid{display:grid;grid-template-columns:1.12fr .88fr;gap:48px;align-items:center}70@media(max-width:1023px){.hero-grid{grid-template-columns:1fr;gap:36px}}71.hero h1{max-width:640px;margin-top:16px}72.lede{margin-top:16px;max-width:58ch;color:var(--ink-2);font-size:16px}73.hero-cta{display:flex;flex-wrap:wrap;gap:12px;margin-top:28px}74.stat-chips{display:flex;flex-wrap:wrap;gap:10px;margin-top:30px}75.stat-chip{display:inline-flex;gap:8px;align-items:center;background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-pill);padding:8px 16px;font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.02em;color:var(--ink-2);white-space:nowrap;box-shadow:var(--shadow-flat)}76.stat-chip b{color:var(--ink);font-weight:700;font-variant-numeric:tabular-nums}77/* terminal encre */78.hero-tile{background:var(--ink);border:1.5px solid var(--ink);border-radius:var(--r-card);box-shadow:var(--shadow-off);overflow:hidden;transform:rotate(.5deg)}79.tile-bar{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid rgba(245,243,238,.12)}80.tile-bar i{width:10px;height:10px;border-radius:50%;background:rgba(245,243,238,.18);display:inline-block}81.tile-bar i:first-child{background:var(--accent)}82.tile-bar span{font-family:var(--font-mono);font-size:11px;color:rgba(245,243,238,.5);margin-left:8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}83.tile-body{padding:18px 20px;font-family:var(--font-mono);font-size:12.2px;line-height:1.75;color:var(--paper);white-space:pre-wrap;word-break:break-word;min-height:270px}84.tile-body .tk{color:#9db1f5}.tile-body .ts{color:var(--amber)}.tile-body .tn{color:#7dd3a7}.tile-body .tc{color:rgba(245,243,238,.45)}8586/* ================= sections ================= */87section{padding:72px 0}88section.alt{background:var(--surface-2);border-top:1.5px solid var(--ink);border-bottom:1.5px solid var(--ink)}89.sec-sub{color:var(--ink-2);max-width:66ch;margin:0 0 34px;font-size:15px}90h2{margin-top:10px}91h2 .hl{font-size:.92em}92.sec-sub code,.conv code,.ep-desc code{font-family:var(--font-mono);font-size:.82em;background:var(--accent-soft);border-radius:5px;padding:2px 7px;color:var(--accent-deep)}9394/* ================= services ================= */95.svc-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px}96.svc{padding:22px 20px;display:flex;flex-direction:column}97.svc h3{margin-bottom:2px}98.svc .path{font-family:var(--font-mono);font-size:11px;color:var(--accent-deep);margin:4px 0 12px}99.svc p{font-size:13px;color:var(--ink-2);line-height:1.55;flex:1;margin:0}100.svc b{display:block;margin-top:16px;font-family:var(--font-display);font-size:24px;font-weight:700;letter-spacing:-.02em;color:var(--ink);font-variant-numeric:tabular-nums}101.svc small{color:var(--ink-3);font-size:11px;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em}102.svc a{display:inline-flex;align-items:center;min-height:32px;margin-top:10px;font-family:var(--font-mono);font-size:11.5px;font-weight:700;color:var(--accent-deep);text-decoration:none}103.svc a:hover{text-decoration:underline;text-underline-offset:4px}104105/* ================= endpoints ================= */106.ep{padding:24px 26px;margin-bottom:18px}107.ep-top{display:flex;flex-wrap:wrap;align-items:center;gap:12px}108.ep-num{font-family:var(--font-mono);font-size:11px;font-weight:700;color:var(--ink-3)}109.method{border:1.5px solid var(--ink);background:var(--accent);color:var(--on-accent);border-radius:var(--r-pill);padding:3px 12px;font-family:var(--font-mono);font-size:10.5px;font-weight:700;letter-spacing:.06em}110.ep-path{font-family:var(--font-mono);font-size:14.5px;font-weight:700;color:var(--ink);letter-spacing:-.01em;word-break:break-all}111.try{margin-left:auto;display:inline-flex;align-items:center;min-height:36px;font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--accent-deep);text-decoration:none;border:1.5px solid var(--ink);border-radius:var(--r-pill);padding:0 15px;background:var(--accent-soft);transition:background .13s,color .13s}112.try:hover{background:var(--accent);color:var(--on-accent)}113.ep-desc{margin:12px 0 0;color:var(--ink-2);max-width:72ch;font-size:14px}114table.params{width:100%;min-width:520px;border-collapse:collapse;margin-top:18px;font-size:13.5px}115table.params th{text-align:left;font-family:var(--font-mono);font-weight:700;font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3);padding:8px 14px 8px 0;border-bottom:1.5px solid var(--ink)}116table.params td{padding:10px 14px 10px 0;border-bottom:1px solid var(--line);vertical-align:top;color:var(--ink-2)}117table.params code{font-family:var(--font-mono);font-size:12px;color:var(--ink);font-weight:500}118.req{font-family:var(--font-mono);font-size:10px;font-weight:700;border-radius:var(--r-pill);padding:2px 9px;background:var(--accent);color:var(--on-accent);letter-spacing:.04em;text-transform:uppercase}119.opt{font-family:var(--font-mono);font-size:10px;font-weight:700;border-radius:var(--r-pill);padding:2px 9px;background:var(--surface-2);border:1px solid var(--line);color:var(--ink-3);letter-spacing:.04em;text-transform:uppercase}120/* code blocks — terminal encre */121.codeblock{margin-top:18px;border:1.5px solid var(--ink);border-radius:var(--r-ctl);overflow:hidden;background:var(--ink);box-shadow:var(--shadow-off-mid)}122.code-tabs{display:flex;align-items:center;padding:0 8px;border-bottom:1px solid rgba(245,243,238,.12);overflow-x:auto}123.code-tabs button{font-family:var(--font-mono);font-size:11.5px;font-weight:500;background:none;border:none;padding:12px 13px;min-height:var(--touch);cursor:pointer;color:rgba(245,243,238,.5);border-bottom:2px solid transparent;transition:color .12s;white-space:nowrap}124.code-tabs button:hover{color:var(--paper)}125.code-tabs button.on{color:var(--paper);border-bottom-color:var(--accent)}126.copy{margin-left:auto;font-family:var(--font-mono);font-size:10.5px;font-weight:500;background:none;border:1px solid rgba(245,243,238,.25);border-radius:6px;padding:6px 12px;min-height:32px;cursor:pointer;color:rgba(245,243,238,.6);transition:color .12s,border-color .12s;white-space:nowrap}127.copy:hover{color:var(--paper);border-color:var(--accent)}128pre{margin:0;background:var(--ink);color:var(--paper);padding:16px 18px;overflow-x:auto;font-family:var(--font-mono);font-size:12.2px;line-height:1.7;white-space:pre}129130/* ================= playground ================= */131.pg{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);padding:28px;box-shadow:var(--shadow-off-soft)}132@media(max-width:767px){.pg{padding:18px}}133.pg-grid{display:grid;grid-template-columns:330px 1fr;gap:32px}134@media(max-width:1023px){.pg-grid{grid-template-columns:1fr}}135.field{margin-bottom:16px}136.field label{display:block;font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3);margin-bottom:7px}137.field select,.field input{width:100%;min-height:var(--touch);font-family:var(--font-mono);font-size:13px;padding:10px 14px;border:1.5px solid var(--ink);border-radius:var(--r-ctl);background:var(--surface);color:var(--ink);appearance:auto}138/* Anti-zoom iOS : champs du playground >= 16 px sur mobile */139@media(max-width:767px){.field select,.field input{font-size:16px}}140.field select:focus,.field input:focus{outline:none;box-shadow:3px 3px 0 var(--accent)}141.send{width:100%;font-family:var(--font-display);font-weight:700;font-size:15px;background:var(--ink);color:var(--accent);border:1.5px solid var(--ink);border-radius:var(--r-ctl);padding:12px;cursor:pointer;margin-top:6px;min-height:48px;transition:transform .15s,box-shadow .15s,background .15s}142.send:hover{transform:translate(-2px,-2px);box-shadow:var(--shadow-off-mid);background:var(--accent-deep);color:#fff}143.send:disabled{opacity:.55;cursor:wait;transform:none;box-shadow:none}144.pg-url{font-family:var(--font-mono);font-size:12px;background:var(--surface-2);border:1.5px solid var(--ink);border-radius:var(--r-ctl);padding:12px 14px;word-break:break-all;margin-bottom:12px;display:flex;gap:12px;align-items:flex-start;color:var(--ink)}145.pg-url span{flex:1}146.pg-url .copy{border-color:var(--line-strong);color:var(--ink-3)}147.pg-url .copy:hover{color:var(--ink);border-color:var(--accent)}148.pg-status{display:flex;gap:10px;margin-bottom:12px;flex-wrap:wrap}149.pg-status span:not(:empty){font-family:var(--font-mono);font-size:11px;font-weight:700;border-radius:var(--r-pill);border:1.5px solid var(--ink);padding:4px 13px;background:var(--surface);color:var(--ink-2)}150.pg-status .s-ok{background:var(--accent-soft);color:var(--accent-deep)}151.pg-status .s-err{background:var(--danger-soft);color:var(--danger)}152#pg-out{background:var(--ink);color:var(--paper);border:1.5px solid var(--ink);border-radius:var(--r-ctl);padding:16px;font-family:var(--font-mono);font-size:12.2px;line-height:1.6;max-height:500px;overflow:auto;white-space:pre-wrap;word-break:break-word;min-height:220px}153154/* ================= conventions ================= */155.conv-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:16px}156.conv{padding:22px}157.conv h3{margin-bottom:8px;display:flex;align-items:center;gap:9px}158.conv h3 .ico{width:26px;height:26px;flex:none;border-radius:7px;background:var(--accent);color:var(--on-accent);display:inline-flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-size:12px}159.conv p,.conv li{font-size:13.5px;color:var(--ink-2);margin:0}160.conv ul{padding-left:18px;margin:0}161.conv li+li{margin-top:4px}162163/* reveal on scroll */164.reveal{opacity:0;transform:translateY(14px);transition:opacity .5s ease,transform .5s ease}165.reveal.in{opacity:1;transform:none}166@media(prefers-reduced-motion:reduce){.reveal{opacity:1;transform:none;transition:none}.hero-tile{transform:none}.pulse.ok{animation:none}}167168/* ===== KA Nav v2 - menu mobile plein ecran (standard 2026-08-25) ===== */169.ka-mnav-btn{display:none;width:44px;height:44px;margin-left:auto;flex:none;170 flex-direction:column;align-items:center;justify-content:center;gap:5px;171 background:var(--paper);border:1.5px solid var(--ink);border-radius:999px;172 cursor:pointer;padding:0}173.ka-mnav-btn span{display:block;width:18px;height:2px;background:var(--ink);174 border-radius:2px;transition:transform .18s ease,opacity .14s ease}175.ka-mnav-btn[aria-expanded="true"] span:nth-child(1){transform:translateY(7px) rotate(45deg)}176.ka-mnav-btn[aria-expanded="true"] span:nth-child(2){opacity:0}177.ka-mnav-btn[aria-expanded="true"] span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}178.ka-mnav{position:fixed;inset:0;z-index:var(--z-modal,900);background:var(--paper);179 display:none;flex-direction:column;overflow-y:auto;overscroll-behavior:contain}180.ka-mnav.open{display:flex;animation:ka-mnav-in .16s ease}181@keyframes ka-mnav-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}182.ka-mnav-top{display:flex;align-items:center;justify-content:space-between;flex:none;183 min-height:64px;padding:0 18px;border-bottom:2px solid var(--ink);background:var(--paper)}184.ka-mnav-close{width:44px;height:44px;flex:none;display:flex;align-items:center;185 justify-content:center;border:1.5px solid var(--ink);border-radius:999px;186 background:var(--ink);color:var(--paper);font-size:17px;font-weight:700;187 cursor:pointer;padding:0;line-height:1}188.ka-mnav-links{display:flex;flex-direction:column;justify-content:center;flex:1;189 padding:28px 24px;counter-reset:mnav}190.ka-mnav-links a{display:flex;align-items:baseline;gap:14px;padding:16px 0;191 border-bottom:1.5px dashed rgba(20,24,20,.18);color:var(--ink);text-decoration:none}192.ka-mnav-links a:last-child{border-bottom:0}193.ka-mnav-links a::before{counter-increment:mnav;content:"0" counter(mnav);194 font-family:var(--font-mono,ui-monospace,monospace);font-size:12px;font-weight:700;195 color:var(--accent-deep,var(--ink-3))}196.ka-mnav-links a b{font-family:var(--font-display,system-ui);font-weight:700;197 font-size:25px;line-height:1;letter-spacing:-.03em;text-transform:uppercase}198.ka-mnav-links a[aria-current] b{background:var(--ink);color:var(--paper);199 border-radius:6px;padding:4px 10px 6px}200.ka-mnav-foot{flex:none;border-top:2px solid var(--ink);padding:16px 24px;201 padding-bottom:max(20px,env(safe-area-inset-bottom))}202html.ka-menu-locked,html.ka-menu-locked body{overflow:hidden}203204/* ============================================================================205 KA ÉDITORIAL v2 (2026-08-25) — aligné sur la refonte lou-ka / vrai-prix :206 typo géante, soulignés « brush » SVG, données vivantes en mono + filets,207 grilles magazine en hairlines. Bloc volontairement en FIN de style : il208 surcharge la v1 « boîtes + ombres dures » sans toucher au markup existant.209 ========================================================================= */210:root{--hairline:rgba(20,24,20,.14);--hairline-strong:rgba(20,24,20,.6)}211212/* .hl : la pastille tournée devient un souligné brush irrégulier (indigo) */213.hl{background:transparent;color:inherit;border-radius:0;transform:none;position:relative;display:inline-block;padding:0 .06em;z-index:0}214.hl::after{content:"";position:absolute;left:-3%;right:-3%;bottom:.05em;height:.34em;z-index:-1;transform:rotate(-.6deg);background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 26' preserveAspectRatio='none'%3E%3Cpath d='M5 17 C 60 8, 118 21, 168 13 S 258 10, 295 15' fill='none' stroke='%233b5bdb' stroke-width='11' stroke-linecap='round' opacity='.4'/%3E%3C/svg%3E") no-repeat center/100% 100%}215216/* héro : composition typographique pleine page, plus de bande teintée */217.hero{border-bottom:1px solid var(--hairline-strong);background:var(--paper);padding:64px 0 72px}218.hero-grid{align-items:start}219/* piège min-content : la live-line nowrap ne doit pas gonfler la colonne */220.hero-grid > *{min-width:0}221.hero h1{max-width:960px;font-size:clamp(38px,4.6vw,64px);line-height:1.02;letter-spacing:-.045em}222.hero-tile{transform:none;box-shadow:var(--shadow-off-mid)}223.hero-tile .tile-body{max-height:420px;overflow:auto;overscroll-behavior:contain}224225/* ligne de données vivantes (remplace les stat-chips) */226.live-line{display:flex;flex-wrap:wrap;align-items:baseline;margin-top:32px;font-family:var(--font-mono);font-size:12px;color:var(--ink-2);letter-spacing:.02em;row-gap:10px}227.live-flag{display:inline-flex;align-items:center;gap:7px;font-weight:700;font-size:10.5px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent-deep);padding-right:14px;margin-right:14px;border-right:1px solid var(--hairline-strong)}228.live-line .pulse{width:7px;height:7px}229.live-item{padding-right:14px;margin-right:14px;border-right:1px solid var(--hairline)}230.live-item:last-child{border-right:0;margin-right:0;padding-right:0}231.live-item b{color:var(--ink);font-weight:700;font-variant-numeric:tabular-nums}232@media(max-width:640px){.live-line{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;white-space:nowrap;margin-right:-16px;padding-right:16px}.live-line::-webkit-scrollbar{display:none}}233234/* sections : filets fins, plus de bandes alternées bordées d'encre */235section{border-top:1px solid var(--hairline-strong)}236section.alt{background:transparent;border-bottom:0}237238/* grilles magazine : les cartes deviennent des colonnes à filet */239.svc-grid{gap:30px 34px}240.svc.card{background:transparent;border:0;border-radius:0;box-shadow:none;border-top:2px solid var(--ink);padding:16px 0 0}241.svc.card-hover:hover{transform:none;box-shadow:none}242.svc b{font-size:30px}243.ep.card{background:transparent;border:0;border-radius:0;box-shadow:none;border-top:1px solid var(--hairline-strong);padding:26px 0 30px;margin-bottom:0}244#ep-list{border-bottom:1px solid var(--hairline-strong)}245.conv-grid{gap:30px 34px}246.conv.card{background:transparent;border:0;border-radius:0;box-shadow:none;border-top:2px solid var(--ink);padding:16px 0 0}247.try{background:transparent;border-color:var(--hairline-strong)}248.try:hover{background:var(--accent);border-color:var(--accent)}249.pg{border-width:1px;border-color:var(--hairline-strong);box-shadow:none;background:transparent}250.live-chip{border-width:1px;border-color:var(--hairline-strong);background:transparent}251252/* boutons : plus de fond encre (l'accent indigo y était illisible) */253.btn-primary{background:var(--accent);color:var(--on-accent)}254.btn-primary:hover{background:var(--accent-deep);color:#fff}255.send{background:var(--accent);color:var(--on-accent)}256.send:hover{background:var(--accent-deep);color:#fff}257</style>258<link rel="canonical" href="https://www.api-ka.com/">259<link rel="alternate" hreflang="fr-ca" href="https://www.api-ka.com/">260<link rel="alternate" hreflang="x-default" href="https://www.api-ka.com/">261<meta property="og:site_name" content="API-Ka">262<meta property="og:locale" content="fr_CA">263<script type="application/ld+json">{"@context": "https://schema.org", "@type": "WebSite", "name": "API-Ka", "url": "https://www.api-ka.com/", "description": "API publique des données quotidiennes de l'écosystème Groupe KA (Québec)", "inLanguage": "fr-CA"}</script>264<script type="application/ld+json">{"@context": "https://schema.org", "@type": "Organization", "name": "API-Ka (Groupe KA)", "url": "https://www.api-ka.com/", "logo": "https://www.api-ka.com/og.png"}</script>265<script type="application/ld+json">{"@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [{"@type": "ListItem", "position": 1, "name": "Accueil", "item": "https://www.api-ka.com/"}]}</script>266</head>267<body>268269<div class="topbar"><div class="container topbar-in">270 <a class="brand" href="/" aria-label="API-Ka — accueil">API<span class="ka">·Ka</span></a>271 <a class="gk-badge" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer"272 aria-label="Un service Groupe KA — visiter le portail de l'écosystème">Un service <b>Groupe<span class="ka">KA</span></b></a>273 <ul class="topnav">274 <li><a href="#services">Services</a></li>275 <li><a href="#endpoints">Endpoints</a></li>276 <li><a href="#playground">Playground</a></li>277 <li><a href="#conventions">Conventions</a></li>278 <li><a href="/stats">Stats</a></li>279 <li><a href="/docs">Swagger</a></li>280 <li><a href="/contact">Contact</a></li>281 </ul>282 <button id="ka-mnav-btn" class="ka-mnav-btn" aria-label="Ouvrir le menu"283 aria-expanded="false" aria-controls="ka-mnav">284 <span></span><span></span><span></span>285 </button>286 <div class="topbar-tools">287 <span class="live-chip"><span class="pulse" id="live-dot"></span><span id="live-txt">vérification…</span></span>288 <div id="ka-auth"></div>289 </div>290</div></div>291292<!-- KA Nav v2 : panneau mobile plein ecran (fixed inset:0) -->293<div id="ka-mnav" class="ka-mnav" role="dialog" aria-modal="true" aria-label="Menu">294 <div class="ka-mnav-top">295 <a class="brand" href="/" aria-label="API-Ka — accueil">API<span class="ka">·Ka</span></a>296 <button class="ka-mnav-close" data-ka-mnav-close aria-label="Fermer le menu">✕</button>297 </div>298 <nav class="ka-mnav-links">299 <a href="#services"><b>Services</b></a>300 <a href="#endpoints"><b>Endpoints</b></a>301 <a href="#playground"><b>Playground</b></a>302 <a href="#conventions"><b>Conventions</b></a>303 <a href="/stats"><b>Stats</b></a>304 <a href="/docs"><b>Swagger</b></a>305 <a href="/contact"><b>Contact</b></a>306 </nav>307 <div class="ka-mnav-foot">308 <a class="gk-badge" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer"309 aria-label="Un service Groupe KA — visiter le portail de l'écosystème">310 Un service <b>Groupe<span class="ka">KA</span></b></a>311 </div>312</div>313314<header class="hero"><div class="container"><div class="hero-grid">315 <div>316 <span class="kicker">Plateforme centrale · Node m3u96b</span>317 <h1>Toutes les données <span class="hl">KA</span>, centralisées chaque jour.</h1>318 <p class="lede">API-Ka collecte, sauvegarde et historise quotidiennement les données des huit319 services de l'écosystème Groupe KA — logements, propriétés, épicerie, véhicules, produits320 québécois, restaurants, sorties et créateurs — puis les expose via une API publique,321 versionnée et documentée.</p>322 <div class="hero-cta">323 <a class="btn btn-primary" href="#playground">Essayer le playground</a>324 <a class="btn btn-ghost" href="/docs">Documentation Swagger</a>325 </div>326 <div class="live-line">327 <span class="live-flag"><span class="pulse ok"></span> en direct</span>328 <span class="live-item"><b>8</b> services collectés</span>329 <span class="live-item"><b id="f-records">…</b> enregistrements</span>330 <span class="live-item">collecte quotidienne <b>02:00</b></span>331 <span class="live-item">dernière collecte <b id="f-last">…</b></span>332 </div>333 </div>334 <div class="hero-tile reveal">335 <div class="tile-bar"><i></i><i></i><i></i><span>GET https://www.api-ka.com/health</span></div>336 <div class="tile-body" id="tile-json"><span class="tc">// chargement de l'état réel de la plateforme…</span></div>337 </div>338</div></div></header>339340<section id="services"><div class="container">341 <span class="kicker">01 — Sources</span>342 <h2>Les huit <span class="hl">services</span></h2>343 <p class="sec-sub">Chaque service est interrogé tous les jours à 02:00. Les données brutes sont344 validées, dédupliquées par empreinte SHA-256, historisées en mode append-only et sauvegardées.</p>345 <div class="svc-grid" id="svc-grid"></div>346</div></section>347348<section id="endpoints" class="alt"><div class="container">349 <span class="kicker">02 — Référence</span>350 <h2>Les <span class="hl">endpoints</span></h2>351 <p class="sec-sub">Base : <code>https://www.api-ka.com</code> — toutes les réponses suivent352 l'enveloppe uniforme <code>{ success, data, meta }</code>.353 <code>{service}</code> ∈ louka · immoka · foodka · autoka · fabrika · restoka · sortika · creaka.</p>354 <div id="ep-list"></div>355</div></section>356357<section id="playground"><div class="container">358 <span class="kicker">03 — Essayer</span>359 <h2>Play<span class="hl">ground</span></h2>360 <p class="sec-sub">Composez une requête réelle contre l'API en production et inspectez la361 réponse — statut, latence et corps JSON.</p>362 <div class="pg reveal"><div class="pg-grid">363 <div>364 <div class="field"><label for="pg-ep">Endpoint</label><select id="pg-ep"></select></div>365 <div id="pg-fields"></div>366 <button class="send" id="pg-send">Envoyer la requête →</button>367 </div>368 <div>369 <div class="pg-url"><span id="pg-url">—</span><button class="copy" id="pg-copy-url">copier</button></div>370 <div class="pg-status"><span id="pg-code"></span><span id="pg-time"></span><span id="pg-size"></span></div>371 <div id="pg-out">La réponse s'affichera ici.</div>372 </div>373 </div></div>374</div></section>375376<section id="conventions" class="alt"><div class="container">377 <span class="kicker">04 — Contrat</span>378 <h2>Conven<span class="hl">tions</span></h2>379 <div class="conv-grid">380 <div class="conv card reveal"><h3><span class="ico">{ }</span>Enveloppe uniforme</h3>381 <p>Toute réponse contient <code>success</code>, <code>data</code> et <code>meta</code>.382 Le <code>meta</code> inclut toujours le node (<code>m3u96b</code>) et, pour les listes,383 <code>page</code>, <code>limit</code> et <code>total</code>.</p></div>384 <div class="conv card reveal"><h3><span class="ico">⇥</span>Pagination</h3>385 <p>Paramètres <code>?page=</code> (≥ 1) et <code>?limit=</code> (1 à 500, défaut 100).386 Les enregistrements sont triés du plus récent au plus ancien.</p></div>387 <div class="conv card reveal"><h3><span class="ico">⏱</span>Limites d'usage</h3>388 <p>120 requêtes / minute / IP. Au-delà : <code>429</code> avec un corps JSON explicite.389 <code>/health</code> n'est pas limité.</p></div>390 <div class="conv card reveal"><h3><span class="ico">!</span>Erreurs</h3>391 <ul><li><code>401</code> — authentification requise (KA ID ou jeton Bearer)</li>392 <li><code>404</code> — service inconnu</li>393 <li><code>422</code> — paramètre invalide (date, limit…)</li>394 <li><code>429</code> — limite de débit dépassée</li></ul></div>395 <div class="conv card reveal"><h3><span class="ico">#</span>Intégrité des données</h3>396 <p>Historique append-only : rien n'est jamais supprimé. Chaque enregistrement porte un397 <code>checksum</code> SHA-256 et une <code>date_key</code> logique. Déduplication par398 <code>(source, date_key, checksum)</code>.</p></div>399 <div class="conv card reveal"><h3><span class="ico">↻</span>Fiabilité</h3>400 <p>Relance automatique (3 tentatives, backoff 30 s → 2 min → 10 min), rattrapage (backfill)401 des 7 derniers jours, sauvegardes quotidiennes horodatées conservées 90 jours.</p></div>402 <div class="conv card reveal"><h3><span class="ico">ID</span>Authentification (requise)</h3>403 <p>L’API produit (<code>/api/v1/…</code>) exige un compte <strong>KA ID</strong> :404 connectez-vous ici (« Se connecter avec KA ») pour utiliser le playground, ou envoyez votre405 <strong>jeton personnel</strong> — généré sur406 <a href="https://www.groupe-ka.com/compte" target="_blank" rel="noopener noreferrer">groupe-ka.com/compte</a> —407 en en-tête <code>Authorization: Bearer kapi_…</code>. Sans authentification : <code>401</code>.408 Pour créer un compte : <a href="https://www.groupe-ka.com/connexion" target="_blank"409 rel="noopener noreferrer">groupe-ka.com/connexion</a>.</p></div>410 </div>411</div></section>412413<footer class="ka-footer" id="contact"><div class="container" id="ka-footer-root"></div></footer>414415<script src="/ka/ka-shell.js" defer></script>416<script>417"use strict";418const BASE = location.origin;419const NF = new Intl.NumberFormat("fr-CA");420421/* ---------- catalogue des services ---------- */422const SERVICES = [423 {id:"louka", nom:"Lou·Ka", site:"https://www.lou-ka.com", desc:"Logements locatifs au Québec — Québec, Lévis, Grand Montréal."},424 {id:"immoka", nom:"Immo·Ka", site:"https://www.immo-ka.com", desc:"Propriétés à vendre au Québec, toutes agences confondues."},425 {id:"foodka", nom:"Food·Ka", site:"https://www.food-ka.com", desc:"Produits d'épicerie québécois et suivi des prix."},426 {id:"autoka", nom:"Auto·Ka", site:"https://www.auto-ka.com", desc:"Véhicules usagés des concessionnaires du Québec."},427 {id:"fabrika",nom:"Fabri·Ka", site:"https://www.fabri-ka.com", desc:"Produits des boutiques et fabricants québécois."},428 {id:"restoka",nom:"Resto·Ka", site:"https://www.resto-ka.com", desc:"Restaurants du Québec — menus complets et prix réels."},429 {id:"sortika",nom:"Sorti·Ka", site:"https://www.sorti-ka.com", desc:"Sorties et événements dans les 17 régions du Québec."},430 {id:"creaka", nom:"Créa·Ka", site:"https://www.crea-ka.com", desc:"Annuaire des créateurs de contenu québécois et de leurs comptes."},431];432433/* ---------- catalogue des endpoints ---------- */434const ENDPOINTS = [435 {id:"root", path:"/", titre:"Statut de la plateforme",436 desc:"Statut global et version. Les clients API reçoivent du JSON ; les navigateurs reçoivent cette plateforme web.",437 params:[], build:()=>"/", fields:[]},438 {id:"health", path:"/health", titre:"Santé de la plateforme",439 desc:"Node (m3u96b), état de la base de données et horodatage de la dernière collecte réussie de chaque service. Non soumis au rate limiting.",440 params:[], build:()=>"/health", fields:[]},441 {id:"list", path:"/api/v1/{service}", titre:"Données paginées d'un service",442 desc:"L'historique complet d'un service, paginé, du plus récent au plus ancien. Chaque enregistrement contient le payload brut, sa date logique et son checksum.",443 params:[444 ["service","path","chemin — louka, immoka, foodka, autoka, fabrika, restoka, sortika ou creaka",true],445 ["page","int","numéro de page, ≥ 1 (défaut 1)",false],446 ["limit","int","taille de page, 1 à 500 (défaut 100)",false]],447 fields:["service","page","limit"],448 build:v=>`/api/v1/${v.service}?page=${v.page||1}&limit=${v.limit||100}`},449 {id:"latest", path:"/api/v1/{service}/latest", titre:"Dernière collecte",450 desc:"Tous les enregistrements de la date de collecte la plus récente du service.",451 params:[["service","path","chemin — service KA",true]],452 fields:["service"], build:v=>`/api/v1/${v.service}/latest`},453 {id:"bydate", path:"/api/v1/{service}/date/{YYYY-MM-DD}", titre:"Données d'une date précise",454 desc:"Les enregistrements collectés pour une date logique donnée, paginés.",455 params:[456 ["service","path","chemin — service KA",true],457 ["date","path","date logique au format YYYY-MM-DD",true],458 ["page","int","numéro de page (défaut 1)",false],459 ["limit","int","taille de page, max 500 (défaut 100)",false]],460 fields:["service","date","page","limit"],461 build:v=>`/api/v1/${v.service}/date/${v.date}?page=${v.page||1}&limit=${v.limit||100}`},462 {id:"stats", path:"/api/v1/{service}/stats", titre:"Statistiques d'un service",463 desc:"Nombre d'enregistrements par jour, total historisé et détail de la dernière collecte réussie.",464 params:[["service","path","chemin — service KA",true]],465 fields:["service"], build:v=>`/api/v1/${v.service}/stats`},466 {id:"runs", path:"/api/v1/runs", titre:"Historique des collectes",467 desc:"Le journal de toutes les exécutions de collecte : statut (success, failed, retried), volumétrie, durée et messages d'erreur éventuels.",468 params:[469 ["service","query","filtrer par service KA",false],470 ["status","query","success, failed ou retried",false],471 ["page","int","numéro de page (défaut 1)",false],472 ["limit","int","taille de page, max 500 (défaut 100)",false]],473 fields:["service_opt","status","page","limit"],474 build:v=>{475 const q=[["service",v.service_opt],["status",v.status],["page",v.page||1],["limit",v.limit||100]]476 .filter(([,x])=>x).map(([k,x])=>`${k}=${x}`).join("&");477 return `/api/v1/runs?${q}`;}},478 {id:"search", path:"/api/v1/search", titre:"Recherche Trouve·Ka",479 desc:"Recherche web québécoise propulsée par le moteur Trouve·Ka (www.trouve-ka.com) : résultats classés (sémantique + rerank), filtrables par site KA, langue et fraîcheur.",480 params:[481 ["q","query","termes de recherche",true],482 ["site","query","limiter à un site KA (ex. www.lou-ka.com)",false],483 ["page","query","page (défaut 1)",false],484 ["limit","query","résultats par page (max 50)",false]],485 fields:["q","site","page","limit50"],486 build:v=>{487 const p=[["q",encodeURIComponent(v.q||"montreal")],["site",v.site?encodeURIComponent(v.site):""],["page",v.page||1],["limit",v.limit50||10]]488 .filter(([,x])=>x).map(([k,x])=>`${k}=${x}`).join("&");489 return `/api/v1/search?${p}`;}},490 {id:"suggest", path:"/api/v1/suggest", titre:"Suggestions de recherche",491 desc:"Autocomplétion du moteur Trouve·Ka : suggestions de requêtes à partir d'un préfixe (2 caractères minimum).",492 params:[["q","query","préfixe de recherche (min. 2 caractères)",true]],493 fields:["q"], build:v=>`/api/v1/suggest?q=${encodeURIComponent(v.q||"logem")}`},494];495496function snippets(url){497 const full = BASE + url;498 return {499 curl: `curl -s "${full}"`,500 python: `import httpx\n\nr = httpx.get("${full}")\nr.raise_for_status()\nprint(r.json())`,501 js: `const r = await fetch("${full}");\nconst body = await r.json();\nconsole.log(body);`,502 };503}504505/* ---------- rendu des cartes endpoints ---------- */506const epList = document.getElementById("ep-list");507ENDPOINTS.forEach((ep, i) => {508 const ex = ep.build({service:"louka", date:"2026-08-16", page:1, limit:100});509 const sn = snippets(ex);510 const card = document.createElement("article");511 card.className = "ep card reveal";512 card.innerHTML = `513 <div class="ep-top">514 <span class="ep-num">${String(i+1).padStart(2,"0")}</span>515 <span class="method">GET</span>516 <span class="ep-path">${ep.path}</span>517 <a class="try" href="#playground" data-ep="${ep.id}">Essayer ↓</a>518 </div>519 <p class="ep-desc">${ep.desc}</p>520 ${ep.params.length ? `<div class="tbl-wrap"><table class="params"><tr><th>Paramètre</th><th>Type</th><th></th><th>Description</th></tr>521 ${ep.params.map(([n,t,d,req])=>`<tr><td><code>${n}</code></td><td><code>${t}</code></td>522 <td>${req?'<span class="req">requis</span>':'<span class="opt">optionnel</span>'}</td><td>${d}</td></tr>`).join("")}523 </table></div>`:""}524 <div class="codeblock">525 <div class="code-tabs">526 <button class="on" data-lang="curl">curl</button>527 <button data-lang="python">Python</button>528 <button data-lang="js">JavaScript</button>529 <button class="copy">copier</button>530 </div>531 <pre></pre>532 </div>`;533 const pre = card.querySelector("pre");534 pre.textContent = sn.curl;535 card.querySelectorAll(".code-tabs button[data-lang]").forEach(btn => {536 btn.addEventListener("click", () => {537 card.querySelectorAll(".code-tabs button[data-lang]").forEach(b=>b.classList.remove("on"));538 btn.classList.add("on");539 pre.textContent = sn[btn.dataset.lang];540 });541 });542 card.querySelector(".copy").addEventListener("click", e => {543 navigator.clipboard.writeText(pre.textContent);544 e.target.textContent = "copié ✓"; setTimeout(()=>e.target.textContent="copier", 1400);545 });546 card.querySelector(".try").addEventListener("click", () => {547 document.getElementById("pg-ep").value = ep.id; renderFields();548 });549 epList.appendChild(card);550});551552/* ---------- playground ---------- */553const pgEp = document.getElementById("pg-ep");554ENDPOINTS.forEach(ep => {555 const o = document.createElement("option");556 o.value = ep.id; o.textContent = `GET ${ep.path}`;557 pgEp.appendChild(o);558});559pgEp.value = "list";560561const FIELD_DEFS = {562 service: {label:"Service", type:"select", options:SERVICES.map(s=>s.id)},563 service_opt: {label:"Service (optionnel)", type:"select", options:["", ...SERVICES.map(s=>s.id)]},564 status: {label:"Statut (optionnel)", type:"select", options:["","success","failed","retried"]},565 date: {label:"Date (YYYY-MM-DD)", type:"date"},566 page: {label:"Page", type:"number", value:1, min:1},567 limit: {label:"Limit (max 500)", type:"number", value:25, min:1, max:500},568 limit50: {label:"Limit (max 50)", type:"number", value:10, min:1, max:50},569 q: {label:"Recherche (q)", type:"text", value:"montreal"},570 site: {label:"Site (optionnel, ex. www.lou-ka.com)", type:"text", value:""},571};572573function renderFields(){574 const ep = ENDPOINTS.find(e => e.id === pgEp.value);575 const zone = document.getElementById("pg-fields");576 zone.innerHTML = "";577 ep.fields.forEach(f => {578 const def = FIELD_DEFS[f];579 const div = document.createElement("div");580 div.className = "field";581 const id = "pgf-" + f;582 if (def.type === "select") {583 div.innerHTML = `<label for="${id}">${def.label}</label><select id="${id}">584 ${def.options.map(o=>`<option value="${o}">${o||"— tous —"}</option>`).join("")}</select>`;585 } else if (def.type === "date") {586 const today = new Date().toISOString().slice(0,10);587 div.innerHTML = `<label for="${id}">${def.label}</label><input id="${id}" type="date" value="${today}">`;588 } else if (def.type === "text") {589 div.innerHTML = `<label for="${id}">${def.label}</label><input id="${id}" type="text" value="${def.value||""}">`;590 } else {591 div.innerHTML = `<label for="${id}">${def.label}</label>592 <input id="${id}" type="number" value="${def.value}" min="${def.min||0}" ${def.max?`max="${def.max}"`:""}>`;593 }594 zone.appendChild(div);595 });596 updateUrl();597 zone.querySelectorAll("select,input").forEach(el => el.addEventListener("input", updateUrl));598}599600function pgValues(){601 const ep = ENDPOINTS.find(e => e.id === pgEp.value);602 const v = {};603 ep.fields.forEach(f => { v[f] = (document.getElementById("pgf-"+f)||{}).value || ""; });604 return {ep, v};605}606function updateUrl(){607 const {ep, v} = pgValues();608 document.getElementById("pg-url").textContent = BASE + ep.build(v);609}610pgEp.addEventListener("change", renderFields);611renderFields();612613document.getElementById("pg-copy-url").addEventListener("click", e => {614 navigator.clipboard.writeText(document.getElementById("pg-url").textContent);615 e.target.textContent = "copié ✓"; setTimeout(()=>e.target.textContent="copier", 1400);616});617618document.getElementById("pg-send").addEventListener("click", async () => {619 const btn = document.getElementById("pg-send");620 const {ep, v} = pgValues();621 const url = ep.build(v);622 const out = document.getElementById("pg-out");623 const codeEl = document.getElementById("pg-code");624 const timeEl = document.getElementById("pg-time");625 const sizeEl = document.getElementById("pg-size");626 btn.disabled = true; out.textContent = "…"; codeEl.textContent = ""; timeEl.textContent = ""; sizeEl.textContent = "";627 codeEl.className = "";628 const t0 = performance.now();629 try {630 // Jeton API optionnel (localStorage apika_token) — sinon la session KA ID (cookie) fait foi.631 const pgHeaders = {Accept:"application/json"};632 try { const tk = localStorage.getItem("apika_token"); if (tk) pgHeaders.Authorization = "Bearer " + tk; } catch {}633 const r = await fetch(BASE + url, {headers: pgHeaders});634 const txt = await r.text();635 const ms = Math.round(performance.now() - t0);636 codeEl.textContent = `HTTP ${r.status}`;637 codeEl.className = r.ok ? "s-ok" : "s-err";638 timeEl.textContent = `${ms} ms`;639 sizeEl.textContent = `${NF.format(txt.length)} octets`;640 try { out.textContent = JSON.stringify(JSON.parse(txt), null, 2); }641 catch { out.textContent = txt.slice(0, 20000); }642 } catch (err) {643 codeEl.textContent = "ÉCHEC"; codeEl.className = "s-err";644 out.textContent = String(err);645 } finally { btn.disabled = false; }646});647648/* ---------- coloration JSON du tile héros ---------- */649function colorJson(obj){650 const json = JSON.stringify(obj, null, 2);651 return json652 .replace(/&/g,"&").replace(/</g,"<")653 .replace(/"([^"]+)":/g, '<span class="tk">"$1"</span>:')654 .replace(/: "([^"]*)"/g, ': <span class="ts">"$1"</span>')655 .replace(/: (\d+(\.\d+)?)/g, ': <span class="tn">$1</span>')656 .replace(/: (true|false|null)/g, ': <span class="tn">$1</span>');657}658659/* ---------- données live (santé + stats) ---------- */660function relTime(iso){661 if (!iso) return "—";662 const mins = Math.round((Date.now() - new Date(iso).getTime()) / 60000);663 if (mins < 1) return "à l'instant";664 if (mins < 60) return `il y a ${mins} min`;665 const h = Math.round(mins/60);666 if (h < 48) return `il y a ${h} h`;667 return `il y a ${Math.round(h/24)} j`;668}669670(async () => {671 const dot = document.getElementById("live-dot"), txt = document.getElementById("live-txt");672 try {673 const h = await (await fetch(BASE + "/health")).json();674 const d = h.data;675 dot.className = "pulse " + (d.database === "ok" && d.node_ok ? "ok" : "err");676 txt.textContent = d.database === "ok" ? "API opérationnelle · " + d.node : "API dégradée";677 const lasts = Object.values(d.last_collections || {}).filter(Boolean)678 .map(x => x.finished_at).sort().reverse();679 document.getElementById("f-last").textContent = relTime(lasts[0]);680 document.getElementById("tile-json").innerHTML = colorJson(h);681 } catch {682 dot.className = "pulse err"; txt.textContent = "API injoignable";683 document.getElementById("tile-json").innerHTML = '<span class="tc">// impossible de joindre /health</span>';684 }685686 const grid = document.getElementById("svc-grid");687 let grand = 0, done = 0;688 for (const s of SERVICES) {689 const card = document.createElement("div");690 card.className = "svc card card-hover reveal"; card.id = "svc-" + s.id;691 card.innerHTML = `<h3>${s.nom}</h3><div class="path">/api/v1/${s.id}</div>692 <p>${s.desc}</p><b>…</b><small>enregistrements historisés</small>693 <a href="${s.site}" target="_blank" rel="noopener">${s.site.replace("https://","")} ↗</a>`;694 grid.appendChild(card);695 }696 observeReveals();697 await Promise.all(SERVICES.map(async s => {698 try {699 const st = await (await fetch(`${BASE}/api/v1/${s.id}/stats`)).json();700 const n = st.data.total_records || 0;701 grand += n; done++;702 document.querySelector(`#svc-${s.id} b`).textContent = NF.format(n);703 } catch {704 document.querySelector(`#svc-${s.id} b`).textContent = "—";705 }706 }));707 document.getElementById("f-records").textContent = done ? NF.format(grand) : "—";708})();709710/* ---------- reveal on scroll ---------- */711function observeReveals(){712 const io = new IntersectionObserver(entries => {713 entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } });714 }, {threshold:.12});715 document.querySelectorAll(".reveal:not(.in)").forEach(el => io.observe(el));716}717observeReveals();718</script>719<script>window.KA_AGENT={site:"api-ka"}</script>720<script src="/ka-agent.js" defer></script>721<script>722/* KA Nav v2 : ouverture/fermeture du panneau mobile plein ecran (2026-08-25) */723(function(){724 var btn=document.getElementById("ka-mnav-btn"),panel=document.getElementById("ka-mnav");725 if(!btn||!panel)return;726 function set(o){panel.classList.toggle("open",o);727 btn.setAttribute("aria-expanded",o?"true":"false");728 document.documentElement.classList.toggle("ka-menu-locked",o);}729 btn.addEventListener("click",function(){set(!panel.classList.contains("open"))});730 panel.addEventListener("click",function(e){731 if(e.target.closest("a")||e.target.closest("[data-ka-mnav-close]"))set(false)});732 document.addEventListener("keydown",function(e){if(e.key==="Escape")set(false)});733})();734</script>735</body>736</html>737