SPB Git forge

spb/api-ka

Public

API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com

48commits 1branches 0releases
5.9 MBsize
maindefault branch
19 days agolast push
Python 60.9% HTML 21% TypeScript 7.3% JavaScript 5.2% CSS 4.8% Shell 0.8%
46.2 KB · 737 lines html
Raw Blame History
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&#8217;API produit (<code>/api/v1/&#8230;</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_&#8230;</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,"&amp;").replace(/</g,"&lt;")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://","")}&nbsp;↗</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