SPB Git

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

34.4 KB · 566 lines html
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,"&amp;").replace(/</g,"&lt;")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://","")}&nbsp;↗</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
Files · 792661b70946e1e0f47211608f696c2270459551
Menu

Theme