API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com
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:84px}4243/* ================= topbar ================= */44.topbar{position:sticky;top:0;z-index:60;background:rgba(245,243,238,.94);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-bottom:1.5px solid var(--ink)}45.topbar-in{display:flex;align-items:center;gap:14px;min-height:68px;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{order:5;flex-basis:100%;margin-left:0}}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.field select:focus,.field input:focus{outline:none;box-shadow:3px 3px 0 var(--accent)}139.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}140.send:hover{transform:translate(-2px,-2px);box-shadow:var(--shadow-off-mid);background:var(--accent-deep);color:#fff}141.send:disabled{opacity:.55;cursor:wait;transform:none;box-shadow:none}142.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)}143.pg-url span{flex:1}144.pg-url .copy{border-color:var(--line-strong);color:var(--ink-3)}145.pg-url .copy:hover{color:var(--ink);border-color:var(--accent)}146.pg-status{display:flex;gap:10px;margin-bottom:12px;flex-wrap:wrap}147.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)}148.pg-status .s-ok{background:var(--accent-soft);color:var(--accent-deep)}149.pg-status .s-err{background:var(--danger-soft);color:var(--danger)}150#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}151152/* ================= conventions ================= */153.conv-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:16px}154.conv{padding:22px}155.conv h3{margin-bottom:8px;display:flex;align-items:center;gap:9px}156.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}157.conv p,.conv li{font-size:13.5px;color:var(--ink-2);margin:0}158.conv ul{padding-left:18px;margin:0}159.conv li+li{margin-top:4px}160161/* reveal on scroll */162.reveal{opacity:0;transform:translateY(14px);transition:opacity .5s ease,transform .5s ease}163.reveal.in{opacity:1;transform:none}164@media(prefers-reduced-motion:reduce){.reveal{opacity:1;transform:none;transition:none}.hero-tile{transform:none}.pulse.ok{animation:none}}165</style>166</head>167<body>168169<div class="topbar"><div class="container topbar-in">170 <a class="brand" href="/" aria-label="API-Ka — accueil">API<span class="ka">·Ka</span></a>171 <a class="gk-badge" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer"172 aria-label="Un service Groupe KA — visiter le portail de l'écosystème">Un service <b>Groupe<span class="ka">KA</span></b></a>173 <ul class="topnav">174 <li><a href="#services">Services</a></li>175 <li><a href="#endpoints">Endpoints</a></li>176 <li><a href="#playground">Playground</a></li>177 <li><a href="#conventions">Conventions</a></li>178 <li><a href="/stats">Stats</a></li>179 <li><a href="/docs">Swagger</a></li>180 <li><a href="/contact">Contact</a></li>181 </ul>182 <div class="topbar-tools">183 <span class="live-chip"><span class="pulse" id="live-dot"></span><span id="live-txt">vérification…</span></span>184 <div id="ka-auth"></div>185 </div>186</div></div>187188<header class="hero"><div class="container"><div class="hero-grid">189 <div>190 <span class="kicker">Plateforme centrale · Node m3u96b</span>191 <h1>Toutes les données <span class="hl">KA</span>, centralisées chaque jour.</h1>192 <p class="lede">API-Ka collecte, sauvegarde et historise quotidiennement les données des huit193 services de l'écosystème Groupe KA — logements, propriétés, épicerie, véhicules, produits194 québécois, restaurants, sorties et créateurs — puis les expose via une API publique,195 versionnée et documentée.</p>196 <div class="hero-cta">197 <a class="btn btn-primary" href="#playground">Essayer le playground</a>198 <a class="btn btn-ghost" href="/docs">Documentation Swagger</a>199 </div>200 <div class="stat-chips">201 <span class="stat-chip"><b>8</b> services collectés</span>202 <span class="stat-chip"><b id="f-records">…</b> enregistrements</span>203 <span class="stat-chip">collecte quotidienne <b>02:00</b></span>204 <span class="stat-chip">dernière collecte <b id="f-last">…</b></span>205 </div>206 </div>207 <div class="hero-tile reveal">208 <div class="tile-bar"><i></i><i></i><i></i><span>GET https://www.api-ka.com/health</span></div>209 <div class="tile-body" id="tile-json"><span class="tc">// chargement de l'état réel de la plateforme…</span></div>210 </div>211</div></div></header>212213<section id="services"><div class="container">214 <span class="kicker">01 — Sources</span>215 <h2>Les huit <span class="hl">services</span></h2>216 <p class="sec-sub">Chaque service est interrogé tous les jours à 02:00. Les données brutes sont217 validées, dédupliquées par empreinte SHA-256, historisées en mode append-only et sauvegardées.</p>218 <div class="svc-grid" id="svc-grid"></div>219</div></section>220221<section id="endpoints" class="alt"><div class="container">222 <span class="kicker">02 — Référence</span>223 <h2>Les <span class="hl">endpoints</span></h2>224 <p class="sec-sub">Base : <code>https://www.api-ka.com</code> — toutes les réponses suivent225 l'enveloppe uniforme <code>{ success, data, meta }</code>.226 <code>{service}</code> ∈ louka · immoka · foodka · autoka · fabrika · restoka · sortika · creaka.</p>227 <div id="ep-list"></div>228</div></section>229230<section id="playground"><div class="container">231 <span class="kicker">03 — Essayer</span>232 <h2>Play<span class="hl">ground</span></h2>233 <p class="sec-sub">Composez une requête réelle contre l'API en production et inspectez la234 réponse — statut, latence et corps JSON.</p>235 <div class="pg reveal"><div class="pg-grid">236 <div>237 <div class="field"><label for="pg-ep">Endpoint</label><select id="pg-ep"></select></div>238 <div id="pg-fields"></div>239 <button class="send" id="pg-send">Envoyer la requête →</button>240 </div>241 <div>242 <div class="pg-url"><span id="pg-url">—</span><button class="copy" id="pg-copy-url">copier</button></div>243 <div class="pg-status"><span id="pg-code"></span><span id="pg-time"></span><span id="pg-size"></span></div>244 <div id="pg-out">La réponse s'affichera ici.</div>245 </div>246 </div></div>247</div></section>248249<section id="conventions" class="alt"><div class="container">250 <span class="kicker">04 — Contrat</span>251 <h2>Conven<span class="hl">tions</span></h2>252 <div class="conv-grid">253 <div class="conv card reveal"><h3><span class="ico">{ }</span>Enveloppe uniforme</h3>254 <p>Toute réponse contient <code>success</code>, <code>data</code> et <code>meta</code>.255 Le <code>meta</code> inclut toujours le node (<code>m3u96b</code>) et, pour les listes,256 <code>page</code>, <code>limit</code> et <code>total</code>.</p></div>257 <div class="conv card reveal"><h3><span class="ico">⇥</span>Pagination</h3>258 <p>Paramètres <code>?page=</code> (≥ 1) et <code>?limit=</code> (1 à 500, défaut 100).259 Les enregistrements sont triés du plus récent au plus ancien.</p></div>260 <div class="conv card reveal"><h3><span class="ico">⏱</span>Limites d'usage</h3>261 <p>120 requêtes / minute / IP. Au-delà : <code>429</code> avec un corps JSON explicite.262 <code>/health</code> n'est pas limité.</p></div>263 <div class="conv card reveal"><h3><span class="ico">!</span>Erreurs</h3>264 <ul><li><code>404</code> — service inconnu</li>265 <li><code>422</code> — paramètre invalide (date, limit…)</li>266 <li><code>429</code> — limite de débit dépassée</li></ul></div>267 <div class="conv card reveal"><h3><span class="ico">#</span>Intégrité des données</h3>268 <p>Historique append-only : rien n'est jamais supprimé. Chaque enregistrement porte un269 <code>checksum</code> SHA-256 et une <code>date_key</code> logique. Déduplication par270 <code>(source, date_key, checksum)</code>.</p></div>271 <div class="conv card reveal"><h3><span class="ico">↻</span>Fiabilité</h3>272 <p>Relance automatique (3 tentatives, backoff 30 s → 2 min → 10 min), rattrapage (backfill)273 des 7 derniers jours, sauvegardes quotidiennes horodatées conservées 90 jours.</p></div>274 <div class="conv card reveal"><h3><span class="ico">ID</span>Compte KA ID</h3>275 <p>La connexion « Se connecter avec KA » est déléguée au hub276 <a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">groupe-ka.com</a>.277 Pour créer un compte : <a href="https://www.groupe-ka.com/connexion" target="_blank"278 rel="noopener noreferrer">groupe-ka.com/connexion</a>.</p></div>279 </div>280</div></section>281282<footer class="ka-footer" id="contact"><div class="container" id="ka-footer-root"></div></footer>283284<script src="/ka/ka-shell.js" defer></script>285<script>286"use strict";287const BASE = location.origin;288const NF = new Intl.NumberFormat("fr-CA");289290/* ---------- catalogue des services ---------- */291const SERVICES = [292 {id:"louka", nom:"Lou·Ka", site:"https://www.lou-ka.com", desc:"Logements locatifs au Québec — Québec, Lévis, Grand Montréal."},293 {id:"immoka", nom:"Immo·Ka", site:"https://www.immo-ka.com", desc:"Propriétés à vendre au Québec, toutes agences confondues."},294 {id:"foodka", nom:"Food·Ka", site:"https://www.food-ka.com", desc:"Produits d'épicerie québécois et suivi des prix."},295 {id:"autoka", nom:"Auto·Ka", site:"https://www.auto-ka.com", desc:"Véhicules usagés des concessionnaires du Québec."},296 {id:"fabrika",nom:"Fabri·Ka", site:"https://www.fabri-ka.com", desc:"Produits des boutiques et fabricants québécois."},297 {id:"restoka",nom:"Resto·Ka", site:"https://www.resto-ka.com", desc:"Restaurants du Québec — menus complets et prix réels."},298 {id:"sortika",nom:"Sorti·Ka", site:"https://www.sorti-ka.com", desc:"Sorties et événements dans les 17 régions du Québec."},299 {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."},300];301302/* ---------- catalogue des endpoints ---------- */303const ENDPOINTS = [304 {id:"root", path:"/", titre:"Statut de la plateforme",305 desc:"Statut global et version. Les clients API reçoivent du JSON ; les navigateurs reçoivent cette plateforme web.",306 params:[], build:()=>"/", fields:[]},307 {id:"health", path:"/health", titre:"Santé de la plateforme",308 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.",309 params:[], build:()=>"/health", fields:[]},310 {id:"list", path:"/api/v1/{service}", titre:"Données paginées d'un service",311 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.",312 params:[313 ["service","path","chemin — louka, immoka, foodka, autoka, fabrika, restoka, sortika ou creaka",true],314 ["page","int","numéro de page, ≥ 1 (défaut 1)",false],315 ["limit","int","taille de page, 1 à 500 (défaut 100)",false]],316 fields:["service","page","limit"],317 build:v=>`/api/v1/${v.service}?page=${v.page||1}&limit=${v.limit||100}`},318 {id:"latest", path:"/api/v1/{service}/latest", titre:"Dernière collecte",319 desc:"Tous les enregistrements de la date de collecte la plus récente du service.",320 params:[["service","path","chemin — service KA",true]],321 fields:["service"], build:v=>`/api/v1/${v.service}/latest`},322 {id:"bydate", path:"/api/v1/{service}/date/{YYYY-MM-DD}", titre:"Données d'une date précise",323 desc:"Les enregistrements collectés pour une date logique donnée, paginés.",324 params:[325 ["service","path","chemin — service KA",true],326 ["date","path","date logique au format YYYY-MM-DD",true],327 ["page","int","numéro de page (défaut 1)",false],328 ["limit","int","taille de page, max 500 (défaut 100)",false]],329 fields:["service","date","page","limit"],330 build:v=>`/api/v1/${v.service}/date/${v.date}?page=${v.page||1}&limit=${v.limit||100}`},331 {id:"stats", path:"/api/v1/{service}/stats", titre:"Statistiques d'un service",332 desc:"Nombre d'enregistrements par jour, total historisé et détail de la dernière collecte réussie.",333 params:[["service","path","chemin — service KA",true]],334 fields:["service"], build:v=>`/api/v1/${v.service}/stats`},335 {id:"runs", path:"/api/v1/runs", titre:"Historique des collectes",336 desc:"Le journal de toutes les exécutions de collecte : statut (success, failed, retried), volumétrie, durée et messages d'erreur éventuels.",337 params:[338 ["service","query","filtrer par service KA",false],339 ["status","query","success, failed ou retried",false],340 ["page","int","numéro de page (défaut 1)",false],341 ["limit","int","taille de page, max 500 (défaut 100)",false]],342 fields:["service_opt","status","page","limit"],343 build:v=>{344 const q=[["service",v.service_opt],["status",v.status],["page",v.page||1],["limit",v.limit||100]]345 .filter(([,x])=>x).map(([k,x])=>`${k}=${x}`).join("&");346 return `/api/v1/runs?${q}`;}},347];348349function snippets(url){350 const full = BASE + url;351 return {352 curl: `curl -s "${full}"`,353 python: `import httpx\n\nr = httpx.get("${full}")\nr.raise_for_status()\nprint(r.json())`,354 js: `const r = await fetch("${full}");\nconst body = await r.json();\nconsole.log(body);`,355 };356}357358/* ---------- rendu des cartes endpoints ---------- */359const epList = document.getElementById("ep-list");360ENDPOINTS.forEach((ep, i) => {361 const ex = ep.build({service:"louka", date:"2026-08-16", page:1, limit:100});362 const sn = snippets(ex);363 const card = document.createElement("article");364 card.className = "ep card reveal";365 card.innerHTML = `366 <div class="ep-top">367 <span class="ep-num">${String(i+1).padStart(2,"0")}</span>368 <span class="method">GET</span>369 <span class="ep-path">${ep.path}</span>370 <a class="try" href="#playground" data-ep="${ep.id}">Essayer ↓</a>371 </div>372 <p class="ep-desc">${ep.desc}</p>373 ${ep.params.length ? `<div class="tbl-wrap"><table class="params"><tr><th>Paramètre</th><th>Type</th><th></th><th>Description</th></tr>374 ${ep.params.map(([n,t,d,req])=>`<tr><td><code>${n}</code></td><td><code>${t}</code></td>375 <td>${req?'<span class="req">requis</span>':'<span class="opt">optionnel</span>'}</td><td>${d}</td></tr>`).join("")}376 </table></div>`:""}377 <div class="codeblock">378 <div class="code-tabs">379 <button class="on" data-lang="curl">curl</button>380 <button data-lang="python">Python</button>381 <button data-lang="js">JavaScript</button>382 <button class="copy">copier</button>383 </div>384 <pre></pre>385 </div>`;386 const pre = card.querySelector("pre");387 pre.textContent = sn.curl;388 card.querySelectorAll(".code-tabs button[data-lang]").forEach(btn => {389 btn.addEventListener("click", () => {390 card.querySelectorAll(".code-tabs button[data-lang]").forEach(b=>b.classList.remove("on"));391 btn.classList.add("on");392 pre.textContent = sn[btn.dataset.lang];393 });394 });395 card.querySelector(".copy").addEventListener("click", e => {396 navigator.clipboard.writeText(pre.textContent);397 e.target.textContent = "copié ✓"; setTimeout(()=>e.target.textContent="copier", 1400);398 });399 card.querySelector(".try").addEventListener("click", () => {400 document.getElementById("pg-ep").value = ep.id; renderFields();401 });402 epList.appendChild(card);403});404405/* ---------- playground ---------- */406const pgEp = document.getElementById("pg-ep");407ENDPOINTS.forEach(ep => {408 const o = document.createElement("option");409 o.value = ep.id; o.textContent = `GET ${ep.path}`;410 pgEp.appendChild(o);411});412pgEp.value = "list";413414const FIELD_DEFS = {415 service: {label:"Service", type:"select", options:SERVICES.map(s=>s.id)},416 service_opt: {label:"Service (optionnel)", type:"select", options:["", ...SERVICES.map(s=>s.id)]},417 status: {label:"Statut (optionnel)", type:"select", options:["","success","failed","retried"]},418 date: {label:"Date (YYYY-MM-DD)", type:"date"},419 page: {label:"Page", type:"number", value:1, min:1},420 limit: {label:"Limit (max 500)", type:"number", value:25, min:1, max:500},421};422423function renderFields(){424 const ep = ENDPOINTS.find(e => e.id === pgEp.value);425 const zone = document.getElementById("pg-fields");426 zone.innerHTML = "";427 ep.fields.forEach(f => {428 const def = FIELD_DEFS[f];429 const div = document.createElement("div");430 div.className = "field";431 const id = "pgf-" + f;432 if (def.type === "select") {433 div.innerHTML = `<label for="${id}">${def.label}</label><select id="${id}">434 ${def.options.map(o=>`<option value="${o}">${o||"— tous —"}</option>`).join("")}</select>`;435 } else if (def.type === "date") {436 const today = new Date().toISOString().slice(0,10);437 div.innerHTML = `<label for="${id}">${def.label}</label><input id="${id}" type="date" value="${today}">`;438 } else {439 div.innerHTML = `<label for="${id}">${def.label}</label>440 <input id="${id}" type="number" value="${def.value}" min="${def.min||0}" ${def.max?`max="${def.max}"`:""}>`;441 }442 zone.appendChild(div);443 });444 updateUrl();445 zone.querySelectorAll("select,input").forEach(el => el.addEventListener("input", updateUrl));446}447448function pgValues(){449 const ep = ENDPOINTS.find(e => e.id === pgEp.value);450 const v = {};451 ep.fields.forEach(f => { v[f] = (document.getElementById("pgf-"+f)||{}).value || ""; });452 return {ep, v};453}454function updateUrl(){455 const {ep, v} = pgValues();456 document.getElementById("pg-url").textContent = BASE + ep.build(v);457}458pgEp.addEventListener("change", renderFields);459renderFields();460461document.getElementById("pg-copy-url").addEventListener("click", e => {462 navigator.clipboard.writeText(document.getElementById("pg-url").textContent);463 e.target.textContent = "copié ✓"; setTimeout(()=>e.target.textContent="copier", 1400);464});465466document.getElementById("pg-send").addEventListener("click", async () => {467 const btn = document.getElementById("pg-send");468 const {ep, v} = pgValues();469 const url = ep.build(v);470 const out = document.getElementById("pg-out");471 const codeEl = document.getElementById("pg-code");472 const timeEl = document.getElementById("pg-time");473 const sizeEl = document.getElementById("pg-size");474 btn.disabled = true; out.textContent = "…"; codeEl.textContent = ""; timeEl.textContent = ""; sizeEl.textContent = "";475 codeEl.className = "";476 const t0 = performance.now();477 try {478 const r = await fetch(BASE + url, {headers:{Accept:"application/json"}});479 const txt = await r.text();480 const ms = Math.round(performance.now() - t0);481 codeEl.textContent = `HTTP ${r.status}`;482 codeEl.className = r.ok ? "s-ok" : "s-err";483 timeEl.textContent = `${ms} ms`;484 sizeEl.textContent = `${NF.format(txt.length)} octets`;485 try { out.textContent = JSON.stringify(JSON.parse(txt), null, 2); }486 catch { out.textContent = txt.slice(0, 20000); }487 } catch (err) {488 codeEl.textContent = "ÉCHEC"; codeEl.className = "s-err";489 out.textContent = String(err);490 } finally { btn.disabled = false; }491});492493/* ---------- coloration JSON du tile héros ---------- */494function colorJson(obj){495 const json = JSON.stringify(obj, null, 2);496 return json497 .replace(/&/g,"&").replace(/</g,"<")498 .replace(/"([^"]+)":/g, '<span class="tk">"$1"</span>:')499 .replace(/: "([^"]*)"/g, ': <span class="ts">"$1"</span>')500 .replace(/: (\d+(\.\d+)?)/g, ': <span class="tn">$1</span>')501 .replace(/: (true|false|null)/g, ': <span class="tn">$1</span>');502}503504/* ---------- données live (santé + stats) ---------- */505function relTime(iso){506 if (!iso) return "—";507 const mins = Math.round((Date.now() - new Date(iso).getTime()) / 60000);508 if (mins < 1) return "à l'instant";509 if (mins < 60) return `il y a ${mins} min`;510 const h = Math.round(mins/60);511 if (h < 48) return `il y a ${h} h`;512 return `il y a ${Math.round(h/24)} j`;513}514515(async () => {516 const dot = document.getElementById("live-dot"), txt = document.getElementById("live-txt");517 try {518 const h = await (await fetch(BASE + "/health")).json();519 const d = h.data;520 dot.className = "pulse " + (d.database === "ok" && d.node_ok ? "ok" : "err");521 txt.textContent = d.database === "ok" ? "API opérationnelle · " + d.node : "API dégradée";522 const lasts = Object.values(d.last_collections || {}).filter(Boolean)523 .map(x => x.finished_at).sort().reverse();524 document.getElementById("f-last").textContent = relTime(lasts[0]);525 document.getElementById("tile-json").innerHTML = colorJson(h);526 } catch {527 dot.className = "pulse err"; txt.textContent = "API injoignable";528 document.getElementById("tile-json").innerHTML = '<span class="tc">// impossible de joindre /health</span>';529 }530531 const grid = document.getElementById("svc-grid");532 let grand = 0, done = 0;533 for (const s of SERVICES) {534 const card = document.createElement("div");535 card.className = "svc card card-hover reveal"; card.id = "svc-" + s.id;536 card.innerHTML = `<h3>${s.nom}</h3><div class="path">/api/v1/${s.id}</div>537 <p>${s.desc}</p><b>…</b><small>enregistrements historisés</small>538 <a href="${s.site}" target="_blank" rel="noopener">${s.site.replace("https://","")} ↗</a>`;539 grid.appendChild(card);540 }541 observeReveals();542 await Promise.all(SERVICES.map(async s => {543 try {544 const st = await (await fetch(`${BASE}/api/v1/${s.id}/stats`)).json();545 const n = st.data.total_records || 0;546 grand += n; done++;547 document.querySelector(`#svc-${s.id} b`).textContent = NF.format(n);548 } catch {549 document.querySelector(`#svc-${s.id} b`).textContent = "—";550 }551 }));552 document.getElementById("f-records").textContent = done ? NF.format(grand) : "—";553})();554555/* ---------- reveal on scroll ---------- */556function observeReveals(){557 const io = new IntersectionObserver(entries => {558 entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } });559 }, {threshold:.12});560 document.querySelectorAll(".reveal:not(.in)").forEach(el => io.observe(el));561}562observeReveals();563</script>564</body>565</html>566