Design Groupe KA « éditorial sharp » + SSO KA ID
· Site docs refait sur le design system commun (ka-ui/tokens.css, accent indigo #3b5bdb) : header API·Ka + badge « Un service Groupe KA », footer commun .ka-footer alimenté par ecosystem.json, page /contact, responsive mobile-first (360/768/1024/1440, tactile ≥ 44 px). · Package ka-ui vendorisé dans src/api/web/ka/ et servi sur /ka (tokens.css, ecosystem.json, ka-shell.js). · SSO KA ID délégué au hub groupe-ka.com (src/api/routes/auth.py) : /api/auth/ka/login → /sso/authorize, callback JWT HS256 (aud=api-ka), cookie de session signé apika_session, /api/auth/me, /api/auth/logout, bouton « Se connecter avec KA » au header. · /contact : HTML pour navigateurs, JSON (enveloppe) pour clients API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9 changed files +1,057 −174
modified
src/api/main.py
+36 −4
@@ -4,13 +4,14 @@ | ||
| 4 | 4 | # Node : m3u96b |
| 5 | 5 | # Author : Simon-Pierre Boucher |
| 6 | 6 | # Contact : contact@spboucher.ai |
| 7 | −# Date : 2026-08-16 | |
| 7 | +# Date : 2026-08-17 | |
| 8 | 8 | # ============================================ |
| 9 | 9 | """Application FastAPI d'API-KA — servie par uvicorn sur 127.0.0.1:8000, |
| 10 | 10 | exposée publiquement uniquement via le tunnel ngrok www.api-ka.com.""" |
| 11 | 11 | |
| 12 | 12 | from __future__ import annotations |
| 13 | 13 | |
| 14 | +import json | |
| 14 | 15 | from collections.abc import AsyncIterator |
| 15 | 16 | from contextlib import asynccontextmanager |
| 16 | 17 | from pathlib import Path |
@@ -18,10 +19,11 @@ from typing import Any | ||
| 18 | 19 | |
| 19 | 20 | from fastapi import FastAPI, Request |
| 20 | 21 | from fastapi.responses import FileResponse |
| 22 | +from fastapi.staticfiles import StaticFiles | |
| 21 | 23 | |
| 22 | 24 | from src.api.middleware.logging import RequestLoggingMiddleware |
| 23 | 25 | from src.api.middleware.ratelimit import RateLimitMiddleware |
| 24 | −from src.api.routes import envelope, health, runs, services | |
| 26 | +from src.api.routes import auth, envelope, health, runs, services | |
| 25 | 27 | from src.config import SERVICES, get_settings, verify_node |
| 26 | 28 | from src.database.db import init_db |
| 27 | 29 | from src.utils.logger import get_logger |
@@ -45,7 +47,8 @@ app = FastAPI( | ||
| 45 | 47 | title="API-KA", |
| 46 | 48 | description=( |
| 47 | 49 | "Plateforme centrale de l'écosystème KA — données quotidiennes des services " |
| 48 | − "lou-ka, immo-ka, food-ka, auto-ka et fabri-ka. Node : m3u96b." | |
| 50 | + "lou-ka, immo-ka, food-ka, auto-ka et fabri-ka. Node : m3u96b. " | |
| 51 | + "Un service Groupe KA (www.groupe-ka.com)." | |
| 49 | 52 | ), |
| 50 | 53 | version=VERSION, |
| 51 | 54 | lifespan=lifespan, |
@@ -58,11 +61,20 @@ app.add_middleware(RequestLoggingMiddleware) | ||
| 58 | 61 | |
| 59 | 62 | # /api/v1/runs doit être enregistré avant les routes génériques /api/v1/{service}. |
| 60 | 63 | app.include_router(health.router) |
| 64 | +app.include_router(auth.router) | |
| 61 | 65 | app.include_router(runs.router) |
| 62 | 66 | app.include_router(services.router) |
| 63 | 67 | |
| 64 | 68 | |
| 65 | −WEB_INDEX = Path(__file__).resolve().parent / "web" / "index.html" | |
| 69 | +WEB_DIR = Path(__file__).resolve().parent / "web" | |
| 70 | +WEB_INDEX = WEB_DIR / "index.html" | |
| 71 | +WEB_CONTACT = WEB_DIR / "contact.html" | |
| 72 | +KA_DIR = WEB_DIR / "ka" | |
| 73 | + | |
| 74 | +# Package ka-ui vendorisé (tokens.css, ecosystem.json, ka-shell.js) — servi | |
| 75 | +# statiquement pour le site de documentation. Voir frontend → src/api/web/ka. | |
| 76 | +if KA_DIR.is_dir(): | |
| 77 | + app.mount("/ka", StaticFiles(directory=KA_DIR), name="ka") | |
| 66 | 78 | |
| 67 | 79 | |
| 68 | 80 | @app.get("/", tags=["root"]) |
@@ -85,3 +97,23 @@ def root(request: Request) -> Any: | ||
| 85 | 97 | "docs": "/docs", |
| 86 | 98 | } |
| 87 | 99 | ) |
| 100 | + | |
| 101 | + | |
| 102 | +@app.get("/contact", tags=["root"]) | |
| 103 | +def contact(request: Request) -> Any: | |
| 104 | + """Page contact Groupe KA — HTML pour les navigateurs, JSON pour les clients API. | |
| 105 | + | |
| 106 | + Les coordonnées proviennent d'ecosystem.json (source de vérité ka-ui). | |
| 107 | + """ | |
| 108 | + accept = request.headers.get("accept", "") | |
| 109 | + if "text/html" in accept and WEB_CONTACT.is_file(): | |
| 110 | + return FileResponse(WEB_CONTACT, media_type="text/html") | |
| 111 | + eco_path = KA_DIR / "ecosystem.json" | |
| 112 | + eco = json.loads(eco_path.read_text(encoding="utf-8")) if eco_path.is_file() else {} | |
| 113 | + return envelope( | |
| 114 | + { | |
| 115 | + "org": eco.get("org", {}).get("name", "Groupe KA"), | |
| 116 | + "hub": eco.get("hub", {}).get("url", "https://www.groupe-ka.com"), | |
| 117 | + "contacts": eco.get("contacts", []), | |
| 118 | + } | |
| 119 | + ) | |
added
src/api/routes/auth.py
+267 −0
@@ -0,0 +1,267 @@ | ||
| 1 | +# ============================================ | |
| 2 | +# Projet : API-KA | |
| 3 | +# Fichier : src/api/routes/auth.py | |
| 4 | +# Node : m3u96b | |
| 5 | +# Author : Simon-Pierre Boucher | |
| 6 | +# Contact : contact@spboucher.ai | |
| 7 | +# Date : 2026-08-17 | |
| 8 | +# ============================================ | |
| 9 | +"""« Se connecter avec KA ID » — connexion déléguée au hub d'identité du | |
| 10 | +Groupe KA (groupe-ka.com), calquée sur creaka/auth.py (stdlib pur pour la | |
| 11 | +crypto, httpx pour le profil). | |
| 12 | + | |
| 13 | +Flux : GET /api/auth/ka/login → hub {KA_HUB_URL}/sso/authorize (state signé) | |
| 14 | + GET /api/auth/ka/callback → vérifie le JWT HS256 ``ka_token`` du hub | |
| 15 | + (aud="api-ka"), pose le cookie de session signé ``apika_session`` | |
| 16 | + GET /api/auth/me · POST /api/auth/logout · GET /api/auth/config | |
| 17 | + | |
| 18 | +Session SANS stockage local (aucune table users) : le hub est LA source de | |
| 19 | +vérité du profil (fetch signé HMAC, cache 60 s). | |
| 20 | +.env : KA_SSO_SECRET (partagé avec le hub), SESSION_SECRET, | |
| 21 | + KA_HUB_URL, APIKA_BASE_URL (optionnel — sinon dérivé de NGROK_DOMAIN). | |
| 22 | +""" | |
| 23 | + | |
| 24 | +from __future__ import annotations | |
| 25 | + | |
| 26 | +import base64 | |
| 27 | +import hashlib | |
| 28 | +import hmac | |
| 29 | +import json | |
| 30 | +import os | |
| 31 | +import secrets | |
| 32 | +import time | |
| 33 | +from urllib.parse import urlencode | |
| 34 | + | |
| 35 | +import httpx | |
| 36 | +from fastapi import APIRouter, HTTPException, Request | |
| 37 | +from fastapi.responses import JSONResponse, RedirectResponse | |
| 38 | + | |
| 39 | +router = APIRouter(prefix="/api/auth", tags=["auth"]) | |
| 40 | + | |
| 41 | +CLIENT_ID = "api-ka" | |
| 42 | +COOKIE = "apika_session" | |
| 43 | +SESSION_DAYS = 30 | |
| 44 | + | |
| 45 | + | |
| 46 | +def _hub_url() -> str: | |
| 47 | + return os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/") | |
| 48 | + | |
| 49 | + | |
| 50 | +_profile_cache: dict[str, tuple[float, dict | None]] = {} | |
| 51 | +_PROFILE_TTL = 60.0 | |
| 52 | + | |
| 53 | + | |
| 54 | +def _secret() -> bytes: | |
| 55 | + """Secret de session local — SESSION_SECRET, sinon dérivé de KA_SSO_SECRET.""" | |
| 56 | + s = os.environ.get("SESSION_SECRET") or os.environ.get("KA_SSO_SECRET") | |
| 57 | + if not s: | |
| 58 | + raise HTTPException(503, "SESSION_SECRET / KA_SSO_SECRET manquant (voir .env)") | |
| 59 | + return s.encode() | |
| 60 | + | |
| 61 | + | |
| 62 | +def _ka_secret() -> bytes | None: | |
| 63 | + s = os.environ.get("KA_SSO_SECRET") | |
| 64 | + return s.encode() if s else None | |
| 65 | + | |
| 66 | + | |
| 67 | +def _base_url(request: Request) -> str: | |
| 68 | + """URL publique de l'app — APIKA_BASE_URL, sinon https://{NGROK_DOMAIN}.""" | |
| 69 | + explicit = os.environ.get("APIKA_BASE_URL") | |
| 70 | + if explicit: | |
| 71 | + return explicit.rstrip("/") | |
| 72 | + domain = os.environ.get("NGROK_DOMAIN") | |
| 73 | + if domain: | |
| 74 | + return f"https://{domain}" | |
| 75 | + return str(request.base_url).rstrip("/") | |
| 76 | + | |
| 77 | + | |
| 78 | +# -- jeton signé maison (payload JSON -> base64url.signature) ------------------- | |
| 79 | + | |
| 80 | + | |
| 81 | +def _sign(payload: dict) -> str: | |
| 82 | + raw = ( | |
| 83 | + base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode()) | |
| 84 | + .decode() | |
| 85 | + .rstrip("=") | |
| 86 | + ) | |
| 87 | + sig = hmac.new(_secret(), raw.encode(), hashlib.sha256).hexdigest() | |
| 88 | + return f"{raw}.{sig}" | |
| 89 | + | |
| 90 | + | |
| 91 | +def _verify(token: str) -> dict | None: | |
| 92 | + try: | |
| 93 | + raw, sig = token.rsplit(".", 1) | |
| 94 | + expected = hmac.new(_secret(), raw.encode(), hashlib.sha256).hexdigest() | |
| 95 | + if not hmac.compare_digest(sig, expected): | |
| 96 | + return None | |
| 97 | + payload = json.loads(base64.urlsafe_b64decode(raw + "=" * (-len(raw) % 4))) | |
| 98 | + if payload.get("exp", 0) < time.time(): | |
| 99 | + return None | |
| 100 | + return payload | |
| 101 | + except Exception: | |
| 102 | + return None | |
| 103 | + | |
| 104 | + | |
| 105 | +def current_user(request: Request) -> dict | None: | |
| 106 | + """Payload de session ({ka_id, email, name, picture…}) ou None.""" | |
| 107 | + token = request.cookies.get(COOKIE) | |
| 108 | + return _verify(token) if token else None | |
| 109 | + | |
| 110 | + | |
| 111 | +# -- vérification du JWT HS256 émis par le hub (stdlib seulement) --------------- | |
| 112 | + | |
| 113 | + | |
| 114 | +def _b64url(s: str) -> bytes: | |
| 115 | + return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) | |
| 116 | + | |
| 117 | + | |
| 118 | +def _verify_ka_token(token: str) -> dict | None: | |
| 119 | + secret = _ka_secret() | |
| 120 | + if not secret: | |
| 121 | + return None | |
| 122 | + try: | |
| 123 | + h_b64, p_b64, s_b64 = token.split(".") | |
| 124 | + expected = hmac.new(secret, f"{h_b64}.{p_b64}".encode(), hashlib.sha256).digest() | |
| 125 | + if not hmac.compare_digest(_b64url(s_b64), expected): | |
| 126 | + return None | |
| 127 | + if json.loads(_b64url(h_b64)).get("alg") != "HS256": | |
| 128 | + return None | |
| 129 | + claims = json.loads(_b64url(p_b64)) | |
| 130 | + if claims.get("iss") != _hub_url(): | |
| 131 | + return None | |
| 132 | + aud = claims.get("aud") | |
| 133 | + if (aud if isinstance(aud, str) else "") != CLIENT_ID and CLIENT_ID not in ( | |
| 134 | + aud if isinstance(aud, list) else [] | |
| 135 | + ): | |
| 136 | + return None | |
| 137 | + if claims.get("exp", 0) < time.time(): | |
| 138 | + return None | |
| 139 | + return claims | |
| 140 | + except Exception: | |
| 141 | + return None | |
| 142 | + | |
| 143 | + | |
| 144 | +# -- profil dicté par le hub (source de vérité, cache 60 s) --------------------- | |
| 145 | + | |
| 146 | + | |
| 147 | +def fetch_hub_profile(ka_id: str) -> dict | None: | |
| 148 | + """GET signé {hub}/api/sso/profile — sig = HMAC(secret, client.ka_id.ts).""" | |
| 149 | + secret = _ka_secret() | |
| 150 | + if not secret or not ka_id: | |
| 151 | + return None | |
| 152 | + now = time.time() | |
| 153 | + hit = _profile_cache.get(ka_id) | |
| 154 | + if hit and now - hit[0] < _PROFILE_TTL: | |
| 155 | + return hit[1] | |
| 156 | + ts = str(int(now)) | |
| 157 | + sig = hmac.new(secret, f"{CLIENT_ID}.{ka_id}.{ts}".encode(), hashlib.sha256).hexdigest() | |
| 158 | + profile = None | |
| 159 | + try: | |
| 160 | + resp = httpx.get( | |
| 161 | + f"{_hub_url()}/api/sso/profile", | |
| 162 | + params={"client_id": CLIENT_ID, "ka_id": ka_id, "ts": ts, "sig": sig}, | |
| 163 | + timeout=8, | |
| 164 | + ) | |
| 165 | + if resp.status_code == 200: | |
| 166 | + profile = resp.json() | |
| 167 | + except Exception: | |
| 168 | + pass | |
| 169 | + _profile_cache[ka_id] = (now, profile) # cache positif ET négatif | |
| 170 | + return profile | |
| 171 | + | |
| 172 | + | |
| 173 | +# -- routes --------------------------------------------------------------------- | |
| 174 | + | |
| 175 | + | |
| 176 | +@router.get("/config") | |
| 177 | +def auth_config(): | |
| 178 | + return {"ka": bool(_ka_secret())} | |
| 179 | + | |
| 180 | + | |
| 181 | +@router.get("/ka/login") | |
| 182 | +def ka_login(request: Request): | |
| 183 | + """Départ SSO : envoie l'utilisateur au hub KA ID (groupe-ka.com).""" | |
| 184 | + if not _ka_secret(): | |
| 185 | + raise HTTPException(503, "KA_SSO_SECRET manquant (voir .env)") | |
| 186 | + state = _sign({"n": secrets.token_urlsafe(12), "exp": time.time() + 600}) | |
| 187 | + params = { | |
| 188 | + "client_id": CLIENT_ID, | |
| 189 | + "redirect_uri": f"{_base_url(request)}/api/auth/ka/callback", | |
| 190 | + "state": state, | |
| 191 | + } | |
| 192 | + return RedirectResponse(f"{_hub_url()}/sso/authorize?{urlencode(params)}") | |
| 193 | + | |
| 194 | + | |
| 195 | +@router.get("/ka/callback") | |
| 196 | +def ka_callback(request: Request, ka_token: str = "", state: str = ""): | |
| 197 | + """Retour SSO : vérifie le jeton du hub, pose le cookie de session API-KA. | |
| 198 | + | |
| 199 | + Aucune table users locale : le KA ID du hub EST l'identité — la session | |
| 200 | + transporte les claims utiles, le profil riche est relu au hub à la demande. | |
| 201 | + """ | |
| 202 | + if not ka_token or _verify(state) is None: | |
| 203 | + raise HTTPException(400, "state invalide ou expiré") | |
| 204 | + claims = _verify_ka_token(ka_token) | |
| 205 | + if claims is None: | |
| 206 | + raise HTTPException(401, "jeton KA invalide ou expiré") | |
| 207 | + session = _sign( | |
| 208 | + { | |
| 209 | + "ka_id": (claims.get("ka_id") or "").strip(), | |
| 210 | + "sub": str(claims.get("sub") or ""), | |
| 211 | + "email": (claims.get("email") or "").lower(), | |
| 212 | + "name": claims.get("name") or "membre", | |
| 213 | + "picture": claims.get("picture") or "", | |
| 214 | + "role": claims.get("ka_role") or "", | |
| 215 | + "exp": time.time() + SESSION_DAYS * 86400, | |
| 216 | + } | |
| 217 | + ) | |
| 218 | + resp = RedirectResponse("/") | |
| 219 | + resp.set_cookie( | |
| 220 | + COOKIE, | |
| 221 | + session, | |
| 222 | + max_age=SESSION_DAYS * 86400, | |
| 223 | + httponly=True, | |
| 224 | + secure=_base_url(request).startswith("https"), | |
| 225 | + samesite="lax", | |
| 226 | + path="/", | |
| 227 | + ) | |
| 228 | + return resp | |
| 229 | + | |
| 230 | + | |
| 231 | +@router.get("/me") | |
| 232 | +def me(request: Request): | |
| 233 | + user = current_user(request) | |
| 234 | + if user is None: | |
| 235 | + raise HTTPException(401, "non connecté") | |
| 236 | + out = { | |
| 237 | + "provider": "ka-id", | |
| 238 | + **{k: user.get(k) for k in ("ka_id", "email", "name", "picture", "role")}, | |
| 239 | + } | |
| 240 | + profile = fetch_hub_profile(user.get("ka_id") or "") | |
| 241 | + if profile: # le hub dicte le profil (nom/photo/rôle à jour) | |
| 242 | + out.update( | |
| 243 | + { | |
| 244 | + k: profile.get(k) | |
| 245 | + for k in ( | |
| 246 | + "name", | |
| 247 | + "picture", | |
| 248 | + "role", | |
| 249 | + "role_label", | |
| 250 | + "bio", | |
| 251 | + "city", | |
| 252 | + "job_title", | |
| 253 | + "company", | |
| 254 | + "website", | |
| 255 | + "public_url", | |
| 256 | + ) | |
| 257 | + if profile.get(k) not in (None, "") | |
| 258 | + } | |
| 259 | + ) | |
| 260 | + return out | |
| 261 | + | |
| 262 | + | |
| 263 | +@router.post("/logout") | |
| 264 | +def logout(): | |
| 265 | + resp = JSONResponse({"ok": True}) | |
| 266 | + resp.delete_cookie(COOKIE, path="/") | |
| 267 | + return resp | |
added
src/api/web/contact.html
+102 −0
@@ -0,0 +1,102 @@ | ||
| 1 | +<!DOCTYPE html> | |
| 2 | +<!-- | |
| 3 | +============================================ | |
| 4 | +Projet : API-KA | |
| 5 | +Fichier : src/api/web/contact.html | |
| 6 | +Node : m3u96b | |
| 7 | +Author : Simon-Pierre Boucher | |
| 8 | +Contact : contact@spboucher.ai | |
| 9 | +Date : 2026-08-17 | |
| 10 | +============================================ | |
| 11 | +Page /contact — courriels officiels Groupe KA (ecosystem.json) + compte KA ID. | |
| 12 | +--> | |
| 13 | +<html lang="fr"> | |
| 14 | +<head> | |
| 15 | +<meta charset="utf-8"> | |
| 16 | +<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> | |
| 17 | +<title>Contact — API-Ka — Un service Groupe KA</title> | |
| 18 | +<meta name="description" content="Joindre le Groupe KA : contact@groupe-ka.com (projets, partenariats et données), info@groupe-ka.com (médias) et admin@groupe-ka.com (légal, vie privée et Loi 25)."> | |
| 19 | +<meta name="theme-color" content="#f5f3ee"> | |
| 20 | +<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%23141814'/%3E%3Crect x='10' y='10' width='44' height='44' rx='8' fill='none' stroke='%233b5bdb' stroke-width='5'/%3E%3Ccircle cx='32' cy='32' r='7' fill='%233b5bdb'/%3E%3C/svg%3E"> | |
| 21 | +<link rel="preconnect" href="https://fonts.googleapis.com"> | |
| 22 | +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| 23 | +<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"> | |
| 24 | +<link rel="stylesheet" href="/ka/tokens.css"> | |
| 25 | +<style> | |
| 26 | +/* ---- Accent API-Ka : INDIGO ---- */ | |
| 27 | +:root{ | |
| 28 | + --accent:#3b5bdb; --accent-soft:#e4eafb; --accent-deep:#2b44a8; --on-accent:#ffffff; | |
| 29 | +} | |
| 30 | + | |
| 31 | +/* ---- topbar (identique à l'accueil) ---- */ | |
| 32 | +.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)} | |
| 33 | +.topbar-in{display:flex;align-items:center;gap:14px;min-height:68px;flex-wrap:wrap;padding-top:8px;padding-bottom:8px} | |
| 34 | +.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} | |
| 35 | +.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} | |
| 36 | +.brand:hover .ka{transform:rotate(0)} | |
| 37 | +.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%} | |
| 38 | +.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} | |
| 39 | +.topnav a:hover{background:var(--accent-soft);color:var(--accent-deep)} | |
| 40 | +#ka-auth{display:flex;align-items:center;gap:8px} | |
| 41 | +#ka-auth .btn{font-size:12.5px;padding:8px 14px} | |
| 42 | +.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)} | |
| 43 | +.ka-user b{font-family:var(--font-display);font-size:13px;text-transform:none;letter-spacing:-.01em;color:var(--ink)} | |
| 44 | +.ka-avatar{width:28px;height:28px;border-radius:50%;border:1.5px solid var(--ink);object-fit:cover} | |
| 45 | +@media(max-width:1023px){.topnav{order:5;flex-basis:100%;margin-left:0}} | |
| 46 | + | |
| 47 | +/* ---- page contact ---- */ | |
| 48 | +.hero-c{padding:64px 0 56px;border-bottom:1.5px solid var(--ink);background:linear-gradient(rgba(59,91,219,.05),transparent 60%),var(--paper)} | |
| 49 | +.hero-c h1{max-width:640px;margin-top:16px} | |
| 50 | +.lede{margin-top:14px;max-width:60ch;color:var(--ink-2);font-size:16px} | |
| 51 | +section{padding:64px 0} | |
| 52 | +.contact-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:18px} | |
| 53 | +.contact-card{padding:24px 22px;display:flex;flex-direction:column;gap:12px} | |
| 54 | +.contact-mail{display:inline-flex;align-items:center;min-height:var(--touch);font-family:var(--font-mono);font-size:14.5px;font-weight:700;color:var(--accent-deep);text-decoration:none;word-break:break-all} | |
| 55 | +.contact-mail:hover{text-decoration:underline;text-underline-offset:4px} | |
| 56 | +.account{margin-top:56px;background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);box-shadow:var(--shadow-off-soft);padding:28px;display:flex;flex-wrap:wrap;align-items:center;gap:20px;justify-content:space-between} | |
| 57 | +.account p{margin:0;max-width:56ch;color:var(--ink-2);font-size:14.5px} | |
| 58 | +.account b{color:var(--ink)} | |
| 59 | +</style> | |
| 60 | +</head> | |
| 61 | +<body> | |
| 62 | + | |
| 63 | +<div class="topbar"><div class="container topbar-in"> | |
| 64 | + <a class="brand" href="/" aria-label="API-Ka — accueil">API<span class="ka">·Ka</span></a> | |
| 65 | + <a class="gk-badge" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer" | |
| 66 | + aria-label="Un service Groupe KA — visiter le portail de l'écosystème">Un service <b>Groupe<span class="ka">KA</span></b></a> | |
| 67 | + <ul class="topnav"> | |
| 68 | + <li><a href="/#services">Services</a></li> | |
| 69 | + <li><a href="/#endpoints">Endpoints</a></li> | |
| 70 | + <li><a href="/#playground">Playground</a></li> | |
| 71 | + <li><a href="/docs">Swagger</a></li> | |
| 72 | + <li><a href="/contact">Contact</a></li> | |
| 73 | + </ul> | |
| 74 | + <div id="ka-auth"></div> | |
| 75 | +</div></div> | |
| 76 | + | |
| 77 | +<header class="hero-c"><div class="container"> | |
| 78 | + <span class="kicker">Contact · Groupe KA</span> | |
| 79 | + <h1>Nous <span class="hl">joindre</span></h1> | |
| 80 | + <p class="lede">API-Ka est un service du Groupe KA. Toutes les demandes — projets, données, | |
| 81 | + médias, légal — passent par les courriels officiels du groupe.</p> | |
| 82 | +</div></header> | |
| 83 | + | |
| 84 | +<section><div class="container"> | |
| 85 | + <span class="kicker">Courriels officiels</span> | |
| 86 | + <h2>Trois <span class="hl">adresses</span></h2> | |
| 87 | + <div class="contact-grid" id="ka-contacts"></div> | |
| 88 | + | |
| 89 | + <div class="account"> | |
| 90 | + <p><b>Compte KA ID.</b> La connexion « Se connecter avec KA » est déléguée au hub | |
| 91 | + groupe-ka.com : un seul compte pour toutes les plateformes de l'écosystème. | |
| 92 | + La création de compte se fait sur le hub.</p> | |
| 93 | + <a class="btn btn-accent" id="ka-hub-link" href="https://www.groupe-ka.com/connexion" | |
| 94 | + target="_blank" rel="noopener noreferrer">Créer un compte KA ID</a> | |
| 95 | + </div> | |
| 96 | +</div></section> | |
| 97 | + | |
| 98 | +<footer class="ka-footer"><div class="container" id="ka-footer-root"></div></footer> | |
| 99 | + | |
| 100 | +<script src="/ka/ka-shell.js" defer></script> | |
| 101 | +</body> | |
| 102 | +</html> | |
modified
src/api/web/index.html
+161 −170
@@ -6,163 +6,148 @@ Fichier : src/api/web/index.html | ||
| 6 | 6 | Node : m3u96b |
| 7 | 7 | Author : Simon-Pierre Boucher |
| 8 | 8 | Contact : contact@spboucher.ai |
| 9 | −Date : 2026-08-16 | |
| 9 | +Date : 2026-08-17 | |
| 10 | 10 | ============================================ |
| 11 | +Design system Groupe KA « éditorial sharp » — tokens.css (ka-ui) importé en | |
| 12 | +premier, accent indigo propre à API-Ka surchargé en :root ci-dessous. | |
| 11 | 13 | --> |
| 12 | 14 | <html lang="fr"> |
| 13 | 15 | <head> |
| 14 | 16 | <meta charset="utf-8"> |
| 15 | 17 | <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> |
| 16 | −<title>API-KA — La plateforme de données de l'écosystème KA</title> | |
| 17 | −<meta name="description" content="API publique centralisant chaque jour les données des services lou-ka, immo-ka, food-ka, auto-ka et fabri-ka. Documentation complète et playground interactif."> | |
| 18 | −<meta name="theme-color" content="#ffffff"> | |
| 19 | −<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230B1330'/%3E%3Cellipse cx='32' cy='18' rx='17' ry='7' fill='none' stroke='%23FF6A00' stroke-width='5'/%3E%3Cpath d='M15 18v28c0 3.9 7.6 7 17 7s17-3.1 17-7V18' fill='none' stroke='%23FF6A00' stroke-width='5'/%3E%3Cpath d='M15 32c0 3.9 7.6 7 17 7s17-3.1 17-7' fill='none' stroke='%23ffffff' stroke-width='4'/%3E%3C/svg%3E"> | |
| 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" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%23141814'/%3E%3Crect x='10' y='10' width='44' height='44' rx='8' fill='none' stroke='%233b5bdb' stroke-width='5'/%3E%3Ccircle cx='32' cy='32' r='7' fill='%233b5bdb'/%3E%3C/svg%3E"> | |
| 20 | 22 | <link rel="preconnect" href="https://fonts.googleapis.com"> |
| 21 | 23 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| 22 | −<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"> | |
| 24 | +<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"> | |
| 25 | +<link rel="stylesheet" href="/ka/tokens.css"> | |
| 23 | 26 | <style> |
| 27 | +/* ---- Accent API-Ka : INDIGO (seules variables surchargées de tokens.css) ---- */ | |
| 24 | 28 | :root{ |
| 25 | − --navy:#0b1330; --blue:#2258ff; --orange:#ff6a00; --orange-deep:#e55f00; | |
| 26 | − --orange-soft:#fff1e6; --blue-soft:#e9efff; --navy-soft:#eef0f6; --grey:#f2f2f2; | |
| 27 | − --white:#ffffff; --surface:var(--white); --surface-2:#f7f7f9; | |
| 28 | − --ink:var(--navy); --ink-2:#3e4560; --ink-3:#8a90a5; | |
| 29 | − --line:rgba(11,19,48,.1); --line-strong:rgba(11,19,48,.16); --danger:#d64545; | |
| 30 | − --r-card:18px; --r-ctl:12px; --r-pill:999px; | |
| 31 | − --shadow-flat:0 1px 3px rgba(11,19,48,.06); | |
| 32 | − --shadow-off:0 16px 40px rgba(11,19,48,.14); | |
| 33 | − --shadow-off-soft:0 8px 24px rgba(11,19,48,.08); | |
| 34 | − --font:"Inter","SF Pro Display",system-ui,sans-serif; | |
| 35 | − --mono:"JetBrains Mono",ui-monospace,monospace; | |
| 29 | + --accent:#3b5bdb; --accent-soft:#e4eafb; --accent-deep:#2b44a8; --on-accent:#ffffff; | |
| 36 | 30 | } |
| 37 | −*{box-sizing:border-box;margin:0;padding:0} | |
| 38 | −html{scroll-behavior:smooth;scroll-padding-top:86px} | |
| 39 | −body{background:var(--white);color:var(--ink);font-family:var(--font);font-size:15.5px;line-height:1.62;-webkit-font-smoothing:antialiased} | |
| 40 | −a{color:inherit} | |
| 41 | −.wrap{max-width:1120px;margin:0 auto;padding:0 24px} | |
| 31 | +html{scroll-padding-top:84px} | |
| 32 | + | |
| 42 | 33 | /* ================= topbar ================= */ |
| 43 | −.topbar{position:sticky;top:0;z-index:60;background:rgba(255,255,255,.92);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-bottom:1px solid var(--line)} | |
| 44 | −.topbar .wrap{display:flex;align-items:center;gap:26px;height:68px} | |
| 45 | −.brand{font-weight:800;font-size:24px;letter-spacing:-.03em;color:var(--navy);text-decoration:none;display:flex;align-items:center;gap:10px;line-height:1} | |
| 46 | −.brand svg{width:30px;height:30px;flex:none} | |
| 47 | −.brand .hl{color:var(--orange)} | |
| 48 | −.brand>span:first-of-type{white-space:nowrap} | |
| 49 | −.brand-tag{font-size:11px;font-weight:500;color:var(--ink-3);letter-spacing:.02em;margin-top:3px} | |
| 50 | −.topnav{display:flex;gap:4px;list-style:none;margin-left:auto} | |
| 51 | −.topnav a{text-decoration:none;font-size:13.5px;font-weight:600;color:var(--ink-2);padding:8px 14px;border-radius:var(--r-pill);transition:all .13s ease} | |
| 52 | −.topnav a:hover{background:var(--blue-soft);color:var(--blue)} | |
| 53 | −.live-chip{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:var(--r-pill);padding:8px 16px;background:var(--surface);font-size:12.5px;font-weight:600;color:var(--ink-2);box-shadow:var(--shadow-flat);white-space:nowrap} | |
| 34 | +.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)} | |
| 35 | +.topbar-in{display:flex;align-items:center;gap:14px;min-height:68px;flex-wrap:wrap;padding-top:8px;padding-bottom:8px} | |
| 36 | +.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} | |
| 37 | +.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} | |
| 38 | +.brand:hover .ka{transform:rotate(0)} | |
| 39 | +.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%} | |
| 40 | +.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} | |
| 41 | +.topnav a:hover{background:var(--accent-soft);color:var(--accent-deep)} | |
| 42 | +.topbar-tools{display:flex;align-items:center;gap:10px;flex-wrap:wrap} | |
| 43 | +#ka-auth{display:flex;align-items:center;gap:8px} | |
| 44 | +#ka-auth .btn{font-size:12.5px;padding:8px 14px} | |
| 45 | +.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)} | |
| 46 | +.ka-user b{font-family:var(--font-display);font-size:13px;text-transform:none;letter-spacing:-.01em;color:var(--ink)} | |
| 47 | +.ka-avatar{width:28px;height:28px;border-radius:50%;border:1.5px solid var(--ink);object-fit:cover} | |
| 48 | +.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} | |
| 54 | 49 | .pulse{width:8px;height:8px;border-radius:50%;background:var(--ink-3)} |
| 55 | −.pulse.ok{background:var(--orange);box-shadow:0 0 0 4px var(--orange-soft);animation:pulse 2.4s ease infinite} | |
| 56 | −.pulse.err{background:var(--danger);box-shadow:0 0 0 4px #fde8e8} | |
| 57 | −@keyframes pulse{0%,100%{box-shadow:0 0 0 4px var(--orange-soft)}50%{box-shadow:0 0 0 7px #ff6a002e}} | |
| 58 | −@media(max-width:900px){.topnav{display:none}.brand-tag{display:none}} | |
| 50 | +.pulse.ok{background:var(--accent);box-shadow:0 0 0 4px var(--accent-soft);animation:pulse 2.4s ease infinite} | |
| 51 | +.pulse.err{background:var(--danger);box-shadow:0 0 0 4px var(--danger-soft)} | |
| 52 | +@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)}} | |
| 53 | +@media(max-width:1023px){.topnav{order:5;flex-basis:100%;margin-left:0}} | |
| 54 | +@media(max-width:767px){.live-chip{display:none}} | |
| 55 | + | |
| 59 | 56 | /* ================= hero ================= */ |
| 60 | −.hero{padding:64px 0 56px;background: | |
| 61 | − radial-gradient(900px 420px at 85% -10%, var(--blue-soft) 0%, transparent 60%), | |
| 62 | − radial-gradient(700px 380px at -5% 15%, var(--orange-soft) 0%, transparent 55%), var(--white)} | |
| 63 | −.hero-grid{display:grid;grid-template-columns:1.15fr .85fr;gap:48px;align-items:center} | |
| 64 | −@media(max-width:960px){.hero-grid{grid-template-columns:1fr}} | |
| 65 | −.kicker{display:inline-flex;align-items:center;gap:8px;border:1px solid rgba(255,106,0,.35);background:var(--orange-soft);color:var(--orange-deep);border-radius:var(--r-pill);padding:6px 16px;font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase} | |
| 66 | −h1{font-size:clamp(38px,5.4vw,60px);font-weight:800;line-height:1.05;letter-spacing:-.03em;color:var(--navy);margin:18px 0 0;max-width:640px} | |
| 67 | −h1 .hl{color:var(--orange)} | |
| 68 | −.lede{margin-top:20px;max-width:56ch;color:var(--ink-2);font-size:16.5px} | |
| 57 | +.hero{padding:56px 0 64px;border-bottom:1.5px solid var(--ink);background: | |
| 58 | + linear-gradient(rgba(59,91,219,.05),transparent 55%),var(--paper)} | |
| 59 | +.hero-grid{display:grid;grid-template-columns:1.12fr .88fr;gap:48px;align-items:center} | |
| 60 | +@media(max-width:1023px){.hero-grid{grid-template-columns:1fr;gap:36px}} | |
| 61 | +.hero h1{max-width:640px;margin-top:16px} | |
| 62 | +.lede{margin-top:16px;max-width:58ch;color:var(--ink-2);font-size:16px} | |
| 69 | 63 | .hero-cta{display:flex;flex-wrap:wrap;gap:12px;margin-top:28px} |
| 70 | −.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid transparent;border-radius:var(--r-pill);padding:12px 26px;font-family:var(--font);font-weight:700;font-size:14.5px;cursor:pointer;text-decoration:none;min-height:46px;transition:transform .12s ease,box-shadow .12s ease,background .15s ease} | |
| 71 | −.btn-primary{background:var(--orange);color:var(--white);box-shadow:0 6px 18px #ff6a0052} | |
| 72 | −.btn-primary:hover{background:var(--orange-deep);transform:translateY(-1px);box-shadow:0 10px 24px #ff6a0060} | |
| 73 | −.btn-ghost{background:transparent;color:var(--navy);border-color:var(--line-strong)} | |
| 74 | −.btn-ghost:hover{background:var(--navy-soft);transform:translateY(-1px)} | |
| 75 | −.stat-chips{display:flex;flex-wrap:wrap;gap:10px;margin-top:32px} | |
| 76 | −.stat-chip{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-pill);padding:9px 18px;font-size:12.5px;font-weight:500;color:var(--ink-2);display:inline-flex;gap:8px;align-items:center;box-shadow:var(--shadow-flat);white-space:nowrap;transition:background .13s ease} | |
| 77 | −.stat-chip:hover{background:var(--blue-soft)} | |
| 78 | −.stat-chip b{color:var(--navy);font-weight:700;font-variant-numeric:tabular-nums} | |
| 79 | −/* terminal navy */ | |
| 80 | −.hero-tile{background:var(--navy);border-radius:var(--r-card);box-shadow:var(--shadow-off);overflow:hidden;transform:rotate(.6deg)} | |
| 81 | −.tile-bar{display:flex;align-items:center;gap:8px;padding:13px 18px;background:#101a3d;border-bottom:1px solid rgba(255,255,255,.08)} | |
| 82 | −.tile-bar i{width:11px;height:11px;border-radius:50%;background:#26325c;display:inline-block} | |
| 83 | −.tile-bar i:first-child{background:var(--orange)} | |
| 84 | −.tile-bar span{font-family:var(--mono);font-size:11.5px;color:#8a90a5;margin-left:8px} | |
| 85 | −.tile-body{padding:20px 22px;font-family:var(--mono);font-size:12.3px;line-height:1.75;color:#e9efff;white-space:pre-wrap;word-break:break-word;min-height:280px} | |
| 86 | −.tile-body .tk{color:#7ea0ff}.tile-body .ts{color:#ffb277}.tile-body .tn{color:#7dd3a7}.tile-body .tc{color:#8a90a5} | |
| 64 | +.stat-chips{display:flex;flex-wrap:wrap;gap:10px;margin-top:30px} | |
| 65 | +.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)} | |
| 66 | +.stat-chip b{color:var(--ink);font-weight:700;font-variant-numeric:tabular-nums} | |
| 67 | +/* terminal encre */ | |
| 68 | +.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)} | |
| 69 | +.tile-bar{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid rgba(245,243,238,.12)} | |
| 70 | +.tile-bar i{width:10px;height:10px;border-radius:50%;background:rgba(245,243,238,.18);display:inline-block} | |
| 71 | +.tile-bar i:first-child{background:var(--accent)} | |
| 72 | +.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} | |
| 73 | +.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} | |
| 74 | +.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)} | |
| 75 | + | |
| 87 | 76 | /* ================= sections ================= */ |
| 88 | −section{padding:76px 0} | |
| 89 | −section.alt{background:var(--surface-2);border-top:1px solid var(--line);border-bottom:1px solid var(--line)} | |
| 90 | −.sec-kicker{color:var(--orange);font-size:12px;font-weight:800;letter-spacing:.1em;text-transform:uppercase} | |
| 91 | −h2{font-size:clamp(28px,3.6vw,40px);font-weight:800;letter-spacing:-.03em;line-height:1.1;color:var(--navy);margin:8px 0 12px} | |
| 92 | −h2 .hl{color:var(--orange)} | |
| 93 | −.sec-sub{color:var(--ink-2);max-width:66ch;margin-bottom:36px;font-size:15.5px} | |
| 94 | −.sec-sub code, .conv code{font-family:var(--mono);font-size:.82em;background:var(--navy-soft);border-radius:6px;padding:2px 7px;color:var(--navy)} | |
| 77 | +section{padding:72px 0} | |
| 78 | +section.alt{background:var(--surface-2);border-top:1.5px solid var(--ink);border-bottom:1.5px solid var(--ink)} | |
| 79 | +.sec-sub{color:var(--ink-2);max-width:66ch;margin:0 0 34px;font-size:15px} | |
| 80 | +h2{margin-top:10px} | |
| 81 | +h2 .hl{font-size:.92em} | |
| 82 | +.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)} | |
| 83 | + | |
| 95 | 84 | /* ================= services ================= */ |
| 96 | −.svc-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px} | |
| 97 | −.svc{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-card);padding:24px 22px;box-shadow:var(--shadow-flat);transition:transform .16s ease,box-shadow .16s ease;display:flex;flex-direction:column} | |
| 98 | −.svc:hover{transform:translateY(-3px);box-shadow:var(--shadow-off-soft)} | |
| 99 | −.svc h3{font-size:18px;font-weight:800;letter-spacing:-.02em;color:var(--navy)} | |
| 100 | −.svc h3 .hl{color:var(--orange)} | |
| 101 | −.svc .path{font-family:var(--mono);font-size:11.5px;color:var(--blue);margin:5px 0 12px} | |
| 102 | −.svc p{font-size:13px;color:var(--ink-2);line-height:1.55;flex:1} | |
| 103 | −.svc b{display:block;margin-top:16px;font-size:22px;font-weight:800;letter-spacing:-.02em;color:var(--navy);font-variant-numeric:tabular-nums} | |
| 104 | −.svc small{color:var(--ink-3);font-size:11.5px;font-weight:500} | |
| 105 | −.svc a{display:inline-flex;margin-top:12px;font-size:12.5px;font-weight:700;color:var(--orange);text-decoration:none} | |
| 106 | −.svc a:hover{color:var(--orange-deep)} | |
| 85 | +.svc-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px} | |
| 86 | +.svc{padding:22px 20px;display:flex;flex-direction:column} | |
| 87 | +.svc h3{margin-bottom:2px} | |
| 88 | +.svc .path{font-family:var(--font-mono);font-size:11px;color:var(--accent-deep);margin:4px 0 12px} | |
| 89 | +.svc p{font-size:13px;color:var(--ink-2);line-height:1.55;flex:1;margin:0} | |
| 90 | +.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} | |
| 91 | +.svc small{color:var(--ink-3);font-size:11px;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em} | |
| 92 | +.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} | |
| 93 | +.svc a:hover{text-decoration:underline;text-underline-offset:4px} | |
| 94 | + | |
| 107 | 95 | /* ================= endpoints ================= */ |
| 108 | −.ep{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-card);padding:26px 28px;margin-bottom:18px;box-shadow:var(--shadow-flat);transition:transform .16s ease,box-shadow .16s ease} | |
| 109 | −.ep:hover{transform:translateY(-2px);box-shadow:var(--shadow-off-soft)} | |
| 96 | +.ep{padding:24px 26px;margin-bottom:18px} | |
| 110 | 97 | .ep-top{display:flex;flex-wrap:wrap;align-items:center;gap:12px} |
| 111 | −.ep-num{font-family:var(--mono);font-size:11px;font-weight:700;color:var(--ink-3)} | |
| 112 | −.method{border:1px solid rgba(34,88,255,.3);background:var(--blue-soft);color:var(--blue);border-radius:var(--r-pill);padding:4px 13px;font-size:11px;font-weight:800;letter-spacing:.05em} | |
| 113 | −.ep-path{font-family:var(--mono);font-size:15px;font-weight:700;color:var(--navy);letter-spacing:-.01em} | |
| 114 | −.try{margin-left:auto;font-size:12.5px;font-weight:700;color:var(--orange);text-decoration:none;border:1px solid rgba(255,106,0,.35);border-radius:var(--r-pill);padding:6px 16px;background:var(--orange-soft);transition:all .13s ease} | |
| 115 | −.try:hover{background:var(--orange);color:var(--white)} | |
| 98 | +.ep-num{font-family:var(--font-mono);font-size:11px;font-weight:700;color:var(--ink-3)} | |
| 99 | +.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} | |
| 100 | +.ep-path{font-family:var(--font-mono);font-size:14.5px;font-weight:700;color:var(--ink);letter-spacing:-.01em;word-break:break-all} | |
| 101 | +.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} | |
| 102 | +.try:hover{background:var(--accent);color:var(--on-accent)} | |
| 116 | 103 | .ep-desc{margin:12px 0 0;color:var(--ink-2);max-width:72ch;font-size:14px} |
| 117 | −table.params{width:100%;border-collapse:collapse;margin-top:18px;font-size:13.5px} | |
| 118 | −table.params th{text-align:left;font-weight:700;font-size:10.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3);padding:8px 14px 8px 0;border-bottom:1px solid var(--line)} | |
| 119 | −table.params td{padding:10px 14px 10px 0;border-bottom:1px solid rgba(11,19,48,.06);vertical-align:top;color:var(--ink-2)} | |
| 120 | −table.params code{font-family:var(--mono);font-size:12px;color:var(--navy);font-weight:500} | |
| 121 | −.req{font-family:var(--mono);font-size:10px;font-weight:700;border-radius:var(--r-pill);padding:2px 9px;background:var(--orange-soft);color:var(--orange-deep);letter-spacing:.04em;text-transform:uppercase} | |
| 122 | −.opt{font-family:var(--mono);font-size:10px;font-weight:700;border-radius:var(--r-pill);padding:2px 9px;background:var(--navy-soft);color:var(--ink-3);letter-spacing:.04em;text-transform:uppercase} | |
| 123 | −/* code blocks — terminal navy */ | |
| 124 | −.codeblock{margin-top:18px;border-radius:var(--r-ctl);overflow:hidden;background:var(--navy);box-shadow:var(--shadow-flat)} | |
| 125 | −.code-tabs{display:flex;align-items:center;background:#101a3d;padding:0 10px;border-bottom:1px solid rgba(255,255,255,.08)} | |
| 126 | −.code-tabs button{font-family:var(--mono);font-size:11.5px;font-weight:500;background:none;border:none;padding:11px 13px;cursor:pointer;color:#8a90a5;border-bottom:2px solid transparent;transition:color .12s ease} | |
| 127 | −.code-tabs button:hover{color:#e9efff} | |
| 128 | −.code-tabs button.on{color:var(--white);border-bottom-color:var(--orange)} | |
| 129 | −.copy{margin-left:auto;font-family:var(--mono);font-size:10.5px;font-weight:500;background:none;border:1px solid rgba(255,255,255,.18);border-radius:7px;padding:4px 12px;cursor:pointer;color:#8a90a5;transition:all .12s ease} | |
| 130 | −.copy:hover{color:var(--white);border-color:var(--orange)} | |
| 131 | −pre{background:var(--navy);color:#e9efff;padding:17px 20px;overflow-x:auto;font-family:var(--mono);font-size:12.3px;line-height:1.7;white-space:pre} | |
| 104 | +table.params{width:100%;min-width:520px;border-collapse:collapse;margin-top:18px;font-size:13.5px} | |
| 105 | +table.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)} | |
| 106 | +table.params td{padding:10px 14px 10px 0;border-bottom:1px solid var(--line);vertical-align:top;color:var(--ink-2)} | |
| 107 | +table.params code{font-family:var(--font-mono);font-size:12px;color:var(--ink);font-weight:500} | |
| 108 | +.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} | |
| 109 | +.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} | |
| 110 | +/* code blocks — terminal encre */ | |
| 111 | +.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)} | |
| 112 | +.code-tabs{display:flex;align-items:center;padding:0 8px;border-bottom:1px solid rgba(245,243,238,.12);overflow-x:auto} | |
| 113 | +.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} | |
| 114 | +.code-tabs button:hover{color:var(--paper)} | |
| 115 | +.code-tabs button.on{color:var(--paper);border-bottom-color:var(--accent)} | |
| 116 | +.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} | |
| 117 | +.copy:hover{color:var(--paper);border-color:var(--accent)} | |
| 118 | +pre{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} | |
| 119 | + | |
| 132 | 120 | /* ================= playground ================= */ |
| 133 | −.pg{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-card);padding:32px;box-shadow:var(--shadow-off-soft)} | |
| 134 | −.pg-grid{display:grid;grid-template-columns:340px 1fr;gap:34px} | |
| 135 | −@media(max-width:880px){.pg-grid{grid-template-columns:1fr}} | |
| 121 | +.pg{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);padding:28px;box-shadow:var(--shadow-off-soft)} | |
| 122 | +@media(max-width:767px){.pg{padding:18px}} | |
| 123 | +.pg-grid{display:grid;grid-template-columns:330px 1fr;gap:32px} | |
| 124 | +@media(max-width:1023px){.pg-grid{grid-template-columns:1fr}} | |
| 136 | 125 | .field{margin-bottom:16px} |
| 137 | −.field label{display:block;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3);margin-bottom:7px} | |
| 138 | −.field select,.field input{width:100%;font-family:var(--mono);font-size:13px;padding:11px 14px;border:1px solid var(--line-strong);border-radius:var(--r-ctl);background:var(--surface);color:var(--navy);transition:border-color .12s ease,box-shadow .12s ease;appearance:auto} | |
| 139 | −.field select:focus,.field input:focus{outline:none;border-color:var(--orange);box-shadow:0 0 0 4px var(--orange-soft)} | |
| 140 | −.send{width:100%;font-family:var(--font);font-weight:700;font-size:15px;background:var(--orange);color:var(--white);border:none;border-radius:var(--r-pill);padding:13px;cursor:pointer;margin-top:6px;min-height:48px;box-shadow:0 6px 18px #ff6a0052;transition:transform .12s ease,background .15s ease,box-shadow .12s ease} | |
| 141 | −.send:hover{background:var(--orange-deep);transform:translateY(-1px);box-shadow:0 10px 24px #ff6a0060} | |
| 142 | −.send:disabled{opacity:.55;cursor:wait;transform:none} | |
| 143 | −.pg-url{font-family:var(--mono);font-size:12px;background:var(--surface-2);border:1px solid var(--line);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(--navy)} | |
| 126 | +.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} | |
| 127 | +.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} | |
| 128 | +.field select:focus,.field input:focus{outline:none;box-shadow:3px 3px 0 var(--accent)} | |
| 129 | +.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} | |
| 130 | +.send:hover{transform:translate(-2px,-2px);box-shadow:var(--shadow-off-mid);background:var(--accent-deep);color:#fff} | |
| 131 | +.send:disabled{opacity:.55;cursor:wait;transform:none;box-shadow:none} | |
| 132 | +.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)} | |
| 144 | 133 | .pg-url span{flex:1} |
| 145 | 134 | .pg-url .copy{border-color:var(--line-strong);color:var(--ink-3)} |
| 146 | −.pg-url .copy:hover{color:var(--navy);border-color:var(--orange)} | |
| 135 | +.pg-url .copy:hover{color:var(--ink);border-color:var(--accent)} | |
| 147 | 136 | .pg-status{display:flex;gap:10px;margin-bottom:12px;flex-wrap:wrap} |
| 148 | −.pg-status span:not(:empty){font-family:var(--mono);font-size:11.5px;font-weight:700;border-radius:var(--r-pill);padding:4px 13px;background:var(--navy-soft);color:var(--ink-2)} | |
| 149 | −.pg-status .s-ok{background:var(--blue-soft);color:var(--blue)} | |
| 150 | −.pg-status .s-err{background:#fde8e8;color:var(--danger)} | |
| 151 | −#pg-out{background:var(--navy);color:#e9efff;border-radius:var(--r-ctl);padding:18px;font-family:var(--mono);font-size:12.3px;line-height:1.6;max-height:500px;overflow:auto;white-space:pre-wrap;word-break:break-word;min-height:220px;box-shadow:var(--shadow-flat)} | |
| 137 | +.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)} | |
| 138 | +.pg-status .s-ok{background:var(--accent-soft);color:var(--accent-deep)} | |
| 139 | +.pg-status .s-err{background:var(--danger-soft);color:var(--danger)} | |
| 140 | +#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} | |
| 141 | + | |
| 152 | 142 | /* ================= conventions ================= */ |
| 153 | 143 | .conv-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:16px} |
| 154 | −.conv{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-card);padding:24px;box-shadow:var(--shadow-flat);transition:transform .16s ease,box-shadow .16s ease} | |
| 155 | −.conv:hover{transform:translateY(-3px);box-shadow:var(--shadow-off-soft)} | |
| 156 | −.conv h3{font-size:15.5px;font-weight:800;letter-spacing:-.01em;color:var(--navy);margin-bottom:8px;display:flex;align-items:center;gap:9px} | |
| 157 | −.conv h3 .ico{width:26px;height:26px;flex:none;border-radius:8px;background:var(--orange-soft);color:var(--orange);display:inline-flex;align-items:center;justify-content:center;font-size:13px} | |
| 158 | −.conv p,.conv li{font-size:13.5px;color:var(--ink-2)} | |
| 159 | −.conv ul{padding-left:18px} | |
| 160 | −/* ================= footer ================= */ | |
| 161 | −footer{background:var(--navy);color:#8a90a5;padding:44px 0;font-size:13.5px} | |
| 162 | −footer .wrap{display:flex;flex-wrap:wrap;gap:14px;justify-content:space-between;align-items:center} | |
| 163 | −footer b{color:var(--white);font-weight:700} | |
| 164 | −footer a{color:var(--orange);text-decoration:none;font-weight:600} | |
| 165 | −footer a:hover{color:#ffb277} | |
| 144 | +.conv{padding:22px} | |
| 145 | +.conv h3{margin-bottom:8px;display:flex;align-items:center;gap:9px} | |
| 146 | +.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} | |
| 147 | +.conv p,.conv li{font-size:13.5px;color:var(--ink-2);margin:0} | |
| 148 | +.conv ul{padding-left:18px;margin:0} | |
| 149 | +.conv li+li{margin-top:4px} | |
| 150 | + | |
| 166 | 151 | /* reveal on scroll */ |
| 167 | 152 | .reveal{opacity:0;transform:translateY(14px);transition:opacity .5s ease,transform .5s ease} |
| 168 | 153 | .reveal.in{opacity:1;transform:none} |
@@ -171,27 +156,30 @@ footer a:hover{color:#ffb277} | ||
| 171 | 156 | </head> |
| 172 | 157 | <body> |
| 173 | 158 | |
| 174 | −<div class="topbar"><div class="wrap"> | |
| 175 | − <a class="brand" href="#"> | |
| 176 | − <svg viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#0B1330"/><ellipse cx="32" cy="18" rx="17" ry="7" fill="none" stroke="#FF6A00" stroke-width="5"/><path d="M15 18v28c0 3.9 7.6 7 17 7s17-3.1 17-7V18" fill="none" stroke="#FF6A00" stroke-width="5"/><path d="M15 32c0 3.9 7.6 7 17 7s17-3.1 17-7" fill="none" stroke="#ffffff" stroke-width="4"/></svg> | |
| 177 | − <span>API-<span class="hl">KA</span></span><span class="brand-tag">Les données de l'écosystème KA</span> | |
| 178 | − </a> | |
| 159 | +<div class="topbar"><div class="container topbar-in"> | |
| 160 | + <a class="brand" href="/" aria-label="API-Ka — accueil">API<span class="ka">·Ka</span></a> | |
| 161 | + <a class="gk-badge" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer" | |
| 162 | + aria-label="Un service Groupe KA — visiter le portail de l'écosystème">Un service <b>Groupe<span class="ka">KA</span></b></a> | |
| 179 | 163 | <ul class="topnav"> |
| 180 | 164 | <li><a href="#services">Services</a></li> |
| 181 | 165 | <li><a href="#endpoints">Endpoints</a></li> |
| 182 | 166 | <li><a href="#playground">Playground</a></li> |
| 183 | 167 | <li><a href="#conventions">Conventions</a></li> |
| 184 | 168 | <li><a href="/docs">Swagger</a></li> |
| 169 | + <li><a href="/contact">Contact</a></li> | |
| 185 | 170 | </ul> |
| 186 | − <span class="live-chip"><span class="pulse" id="live-dot"></span><span id="live-txt">vérification…</span></span> | |
| 171 | + <div class="topbar-tools"> | |
| 172 | + <span class="live-chip"><span class="pulse" id="live-dot"></span><span id="live-txt">vérification…</span></span> | |
| 173 | + <div id="ka-auth"></div> | |
| 174 | + </div> | |
| 187 | 175 | </div></div> |
| 188 | 176 | |
| 189 | −<header class="hero"><div class="wrap"><div class="hero-grid"> | |
| 177 | +<header class="hero"><div class="container"><div class="hero-grid"> | |
| 190 | 178 | <div> |
| 191 | 179 | <span class="kicker">Plateforme centrale · Node m3u96b</span> |
| 192 | − <h1>Toutes les données <span class="hl">KA</span>,<br>centralisées <span class="hl">chaque jour</span>.</h1> | |
| 193 | − <p class="lede">API-KA collecte, sauvegarde et historise quotidiennement les données des huit | |
| 194 | − services de l'écosystème KA — logements, propriétés, épicerie, véhicules, produits | |
| 180 | + <h1>Toutes les données <span class="hl">KA</span>, centralisées chaque jour.</h1> | |
| 181 | + <p class="lede">API-Ka collecte, sauvegarde et historise quotidiennement les données des huit | |
| 182 | + services de l'écosystème Groupe KA — logements, propriétés, épicerie, véhicules, produits | |
| 195 | 183 | québécois, restaurants, sorties et créateurs — puis les expose via une API publique, |
| 196 | 184 | versionnée et documentée.</p> |
| 197 | 185 | <div class="hero-cta"> |
@@ -211,16 +199,16 @@ footer a:hover{color:#ffb277} | ||
| 211 | 199 | </div> |
| 212 | 200 | </div></div></header> |
| 213 | 201 | |
| 214 | −<section id="services"><div class="wrap"> | |
| 215 | − <span class="sec-kicker">01 — Sources</span> | |
| 202 | +<section id="services"><div class="container"> | |
| 203 | + <span class="kicker">01 — Sources</span> | |
| 216 | 204 | <h2>Les huit <span class="hl">services</span></h2> |
| 217 | 205 | <p class="sec-sub">Chaque service est interrogé tous les jours à 02:00. Les données brutes sont |
| 218 | 206 | validées, dédupliquées par empreinte SHA-256, historisées en mode append-only et sauvegardées.</p> |
| 219 | 207 | <div class="svc-grid" id="svc-grid"></div> |
| 220 | 208 | </div></section> |
| 221 | 209 | |
| 222 | −<section id="endpoints" class="alt"><div class="wrap"> | |
| 223 | − <span class="sec-kicker">02 — Référence</span> | |
| 210 | +<section id="endpoints" class="alt"><div class="container"> | |
| 211 | + <span class="kicker">02 — Référence</span> | |
| 224 | 212 | <h2>Les <span class="hl">endpoints</span></h2> |
| 225 | 213 | <p class="sec-sub">Base : <code>https://www.api-ka.com</code> — toutes les réponses suivent |
| 226 | 214 | l'enveloppe uniforme <code>{ success, data, meta }</code>. |
@@ -228,8 +216,8 @@ footer a:hover{color:#ffb277} | ||
| 228 | 216 | <div id="ep-list"></div> |
| 229 | 217 | </div></section> |
| 230 | 218 | |
| 231 | −<section id="playground"><div class="wrap"> | |
| 232 | − <span class="sec-kicker">03 — Essayer</span> | |
| 219 | +<section id="playground"><div class="container"> | |
| 220 | + <span class="kicker">03 — Essayer</span> | |
| 233 | 221 | <h2>Play<span class="hl">ground</span></h2> |
| 234 | 222 | <p class="sec-sub">Composez une requête réelle contre l'API en production et inspectez la |
| 235 | 223 | réponse — statut, latence et corps JSON.</p> |
@@ -247,39 +235,42 @@ footer a:hover{color:#ffb277} | ||
| 247 | 235 | </div></div> |
| 248 | 236 | </div></section> |
| 249 | 237 | |
| 250 | −<section id="conventions" class="alt"><div class="wrap"> | |
| 251 | − <span class="sec-kicker">04 — Contrat</span> | |
| 238 | +<section id="conventions" class="alt"><div class="container"> | |
| 239 | + <span class="kicker">04 — Contrat</span> | |
| 252 | 240 | <h2>Conven<span class="hl">tions</span></h2> |
| 253 | 241 | <div class="conv-grid"> |
| 254 | − <div class="conv reveal"><h3><span class="ico">{ }</span>Enveloppe uniforme</h3> | |
| 242 | + <div class="conv card reveal"><h3><span class="ico">{ }</span>Enveloppe uniforme</h3> | |
| 255 | 243 | <p>Toute réponse contient <code>success</code>, <code>data</code> et <code>meta</code>. |
| 256 | 244 | Le <code>meta</code> inclut toujours le node (<code>m3u96b</code>) et, pour les listes, |
| 257 | 245 | <code>page</code>, <code>limit</code> et <code>total</code>.</p></div> |
| 258 | − <div class="conv reveal"><h3><span class="ico">⇥</span>Pagination</h3> | |
| 246 | + <div class="conv card reveal"><h3><span class="ico">⇥</span>Pagination</h3> | |
| 259 | 247 | <p>Paramètres <code>?page=</code> (≥ 1) et <code>?limit=</code> (1 à 500, défaut 100). |
| 260 | 248 | Les enregistrements sont triés du plus récent au plus ancien.</p></div> |
| 261 | − <div class="conv reveal"><h3><span class="ico">⏱</span>Limites d'usage</h3> | |
| 249 | + <div class="conv card reveal"><h3><span class="ico">⏱</span>Limites d'usage</h3> | |
| 262 | 250 | <p>120 requêtes / minute / IP. Au-delà : <code>429</code> avec un corps JSON explicite. |
| 263 | 251 | <code>/health</code> n'est pas limité.</p></div> |
| 264 | − <div class="conv reveal"><h3><span class="ico">!</span>Erreurs</h3> | |
| 252 | + <div class="conv card reveal"><h3><span class="ico">!</span>Erreurs</h3> | |
| 265 | 253 | <ul><li><code>404</code> — service inconnu</li> |
| 266 | 254 | <li><code>422</code> — paramètre invalide (date, limit…)</li> |
| 267 | 255 | <li><code>429</code> — limite de débit dépassée</li></ul></div> |
| 268 | − <div class="conv reveal"><h3><span class="ico">#</span>Intégrité des données</h3> | |
| 256 | + <div class="conv card reveal"><h3><span class="ico">#</span>Intégrité des données</h3> | |
| 269 | 257 | <p>Historique append-only : rien n'est jamais supprimé. Chaque enregistrement porte un |
| 270 | 258 | <code>checksum</code> SHA-256 et une <code>date_key</code> logique. Déduplication par |
| 271 | 259 | <code>(source, date_key, checksum)</code>.</p></div> |
| 272 | − <div class="conv reveal"><h3><span class="ico">↻</span>Fiabilité</h3> | |
| 260 | + <div class="conv card reveal"><h3><span class="ico">↻</span>Fiabilité</h3> | |
| 273 | 261 | <p>Relance automatique (3 tentatives, backoff 30 s → 2 min → 10 min), rattrapage (backfill) |
| 274 | 262 | des 7 derniers jours, sauvegardes quotidiennes horodatées conservées 90 jours.</p></div> |
| 263 | + <div class="conv card reveal"><h3><span class="ico">ID</span>Compte KA ID</h3> | |
| 264 | + <p>La connexion « Se connecter avec KA » est déléguée au hub | |
| 265 | + <a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">groupe-ka.com</a>. | |
| 266 | + Pour créer un compte : <a href="https://www.groupe-ka.com/connexion" target="_blank" | |
| 267 | + rel="noopener noreferrer">groupe-ka.com/connexion</a>.</p></div> | |
| 275 | 268 | </div> |
| 276 | 269 | </div></section> |
| 277 | 270 | |
| 278 | −<footer><div class="wrap"> | |
| 279 | − <span>API-<b>KA</b> — plateforme légendaire de l'écosystème KA · Node <b>m3u96b</b></span> | |
| 280 | − <span>Créée par <b>Simon-Pierre Boucher</b> · <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a></span> | |
| 281 | −</div></footer> | |
| 271 | +<footer class="ka-footer" id="contact"><div class="container" id="ka-footer-root"></div></footer> | |
| 282 | 272 | |
| 273 | +<script src="/ka/ka-shell.js" defer></script> | |
| 283 | 274 | <script> |
| 284 | 275 | "use strict"; |
| 285 | 276 | const BASE = location.origin; |
@@ -287,14 +278,14 @@ const NF = new Intl.NumberFormat("fr-CA"); | ||
| 287 | 278 | |
| 288 | 279 | /* ---------- catalogue des services ---------- */ |
| 289 | 280 | const SERVICES = [ |
| 290 | − {id:"louka", nom:"lou-<span class='hl'>ka</span>", site:"https://www.lou-ka.com", desc:"Logements locatifs au Québec — Québec, Lévis, Grand Montréal."}, | |
| 291 | − {id:"immoka", nom:"immo-<span class='hl'>ka</span>", site:"https://www.immo-ka.com", desc:"Propriétés à vendre au Québec, toutes agences confondues."}, | |
| 292 | − {id:"foodka", nom:"food-<span class='hl'>ka</span>", site:"https://www.food-ka.com", desc:"Produits d'épicerie québécois et suivi des prix."}, | |
| 293 | − {id:"autoka", nom:"auto-<span class='hl'>ka</span>", site:"https://www.auto-ka.com", desc:"Véhicules usagés des concessionnaires du Québec."}, | |
| 294 | − {id:"fabrika",nom:"fabri-<span class='hl'>ka</span>", site:"https://www.fabri-ka.com", desc:"Produits des boutiques et fabricants québécois."}, | |
| 295 | − {id:"restoka",nom:"resto-<span class='hl'>ka</span>", site:"https://www.resto-ka.com", desc:"Restaurants du Québec — menus complets et prix réels."}, | |
| 296 | − {id:"sortika",nom:"sorti-<span class='hl'>ka</span>", site:"https://www.sorti-ka.com", desc:"Sorties et événements dans les 17 régions du Québec."}, | |
| 297 | − {id:"creaka", nom:"crea-<span class='hl'>ka</span>", site:"https://www.crea-ka.com", desc:"Annuaire des créateurs de contenu québécois et de leurs comptes."}, | |
| 281 | + {id:"louka", nom:"Lou·Ka", site:"https://www.lou-ka.com", desc:"Logements locatifs au Québec — Québec, Lévis, Grand Montréal."}, | |
| 282 | + {id:"immoka", nom:"Immo·Ka", site:"https://www.immo-ka.com", desc:"Propriétés à vendre au Québec, toutes agences confondues."}, | |
| 283 | + {id:"foodka", nom:"Food·Ka", site:"https://www.food-ka.com", desc:"Produits d'épicerie québécois et suivi des prix."}, | |
| 284 | + {id:"autoka", nom:"Auto·Ka", site:"https://www.auto-ka.com", desc:"Véhicules usagés des concessionnaires du Québec."}, | |
| 285 | + {id:"fabrika",nom:"Fabri·Ka", site:"https://www.fabri-ka.com", desc:"Produits des boutiques et fabricants québécois."}, | |
| 286 | + {id:"restoka",nom:"Resto·Ka", site:"https://www.resto-ka.com", desc:"Restaurants du Québec — menus complets et prix réels."}, | |
| 287 | + {id:"sortika",nom:"Sorti·Ka", site:"https://www.sorti-ka.com", desc:"Sorties et événements dans les 17 régions du Québec."}, | |
| 288 | + {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."}, | |
| 298 | 289 | ]; |
| 299 | 290 | |
| 300 | 291 | /* ---------- catalogue des endpoints ---------- */ |
@@ -359,7 +350,7 @@ ENDPOINTS.forEach((ep, i) => { | ||
| 359 | 350 | const ex = ep.build({service:"louka", date:"2026-08-16", page:1, limit:100}); |
| 360 | 351 | const sn = snippets(ex); |
| 361 | 352 | const card = document.createElement("article"); |
| 362 | − card.className = "ep reveal"; | |
| 353 | + card.className = "ep card reveal"; | |
| 363 | 354 | card.innerHTML = ` |
| 364 | 355 | <div class="ep-top"> |
| 365 | 356 | <span class="ep-num">${String(i+1).padStart(2,"0")}</span> |
@@ -368,10 +359,10 @@ ENDPOINTS.forEach((ep, i) => { | ||
| 368 | 359 | <a class="try" href="#playground" data-ep="${ep.id}">Essayer ↓</a> |
| 369 | 360 | </div> |
| 370 | 361 | <p class="ep-desc">${ep.desc}</p> |
| 371 | − ${ep.params.length ? `<table class="params"><tr><th>Paramètre</th><th>Type</th><th></th><th>Description</th></tr> | |
| 362 | + ${ep.params.length ? `<div class="tbl-wrap"><table class="params"><tr><th>Paramètre</th><th>Type</th><th></th><th>Description</th></tr> | |
| 372 | 363 | ${ep.params.map(([n,t,d,req])=>`<tr><td><code>${n}</code></td><td><code>${t}</code></td> |
| 373 | 364 | <td>${req?'<span class="req">requis</span>':'<span class="opt">optionnel</span>'}</td><td>${d}</td></tr>`).join("")} |
| 374 | − </table>`:""} | |
| 365 | + </table></div>`:""} | |
| 375 | 366 | <div class="codeblock"> |
| 376 | 367 | <div class="code-tabs"> |
| 377 | 368 | <button class="on" data-lang="curl">curl</button> |
@@ -530,7 +521,7 @@ function relTime(iso){ | ||
| 530 | 521 | let grand = 0, done = 0; |
| 531 | 522 | for (const s of SERVICES) { |
| 532 | 523 | const card = document.createElement("div"); |
| 533 | − card.className = "svc reveal"; card.id = "svc-" + s.id; | |
| 524 | + card.className = "svc card card-hover reveal"; card.id = "svc-" + s.id; | |
| 534 | 525 | card.innerHTML = `<h3>${s.nom}</h3><div class="path">/api/v1/${s.id}</div> |
| 535 | 526 | <p>${s.desc}</p><b>…</b><small>enregistrements historisés</small> |
| 536 | 527 | <a href="${s.site}" target="_blank" rel="noopener">${s.site.replace("https://","")} ↗</a>`; |
added
src/api/web/ka/GroupeKaBadge.tsx
+21 −0
@@ -0,0 +1,21 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +// ka-ui/react/GroupeKaBadge.tsx — badge « Un service Groupe KA », cliquable | |
| 3 | +// vers le hub. À placer dans le header de chaque site (à côté du logo de la | |
| 4 | +// marque). Variante dark pour les surfaces encre. Zone de protection : le | |
| 5 | +// padding interne du badge suffit ; ne rien coller à moins de 8 px du badge. | |
| 6 | +export default function GroupeKaBadge({ dark = false }: { dark?: boolean }) { | |
| 7 | + return ( | |
| 8 | + <a | |
| 9 | + className={`gk-badge${dark ? " gk-badge--dark" : ""}`} | |
| 10 | + href="https://www.groupe-ka.com" | |
| 11 | + target="_blank" | |
| 12 | + rel="noopener noreferrer" | |
| 13 | + aria-label="Un service Groupe KA — visiter le portail de l'écosystème" | |
| 14 | + > | |
| 15 | + Un service | |
| 16 | + <b> | |
| 17 | + Groupe<span className="ka">KA</span> | |
| 18 | + </b> | |
| 19 | + </a> | |
| 20 | + ); | |
| 21 | +} | |
added
src/api/web/ka/KaFooter.tsx
+91 −0
@@ -0,0 +1,91 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +// ka-ui/react/KaFooter.tsx — footer commun Groupe KA (fond encre), à vendorer | |
| 3 | +// dans chaque app Vite/React sous frontend/src/ka/. Consomme ecosystem.json : | |
| 4 | +// toute modification des infos de contact se fait dans ka-ui/ecosystem.json | |
| 5 | +// puis se resynchronise sur les 12 sites (voir ka-ui/README.md). | |
| 6 | +import eco from "./ecosystem.json"; | |
| 7 | + | |
| 8 | +type LocalLink = { label: string; href: string }; | |
| 9 | + | |
| 10 | +export default function KaFooter({ | |
| 11 | + siteId, | |
| 12 | + localLegal = [], | |
| 13 | +}: { | |
| 14 | + /** id du site courant dans ecosystem.json (ex. "resto-ka") */ | |
| 15 | + siteId: string; | |
| 16 | + /** pages légales PROPRES au site (ex. /conditions), affichées avant celles du hub */ | |
| 17 | + localLegal?: LocalLink[]; | |
| 18 | +}) { | |
| 19 | + const year = new Date().getFullYear(); | |
| 20 | + const site = eco.sites.find((s) => s.id === siteId); | |
| 21 | + const others = eco.sites.filter((s) => s.id !== siteId); | |
| 22 | + return ( | |
| 23 | + <footer className="ka-footer" id="contact"> | |
| 24 | + <div className="container"> | |
| 25 | + <a | |
| 26 | + className="wordmark" | |
| 27 | + href={eco.hub.url} | |
| 28 | + target={siteId === "groupe-ka" ? undefined : "_blank"} | |
| 29 | + rel="noopener noreferrer" | |
| 30 | + aria-label="Groupe KA — le portail de l'écosystème" | |
| 31 | + > | |
| 32 | + Groupe <span className="ka">KA</span> | |
| 33 | + </a> | |
| 34 | + <p className="desc"> | |
| 35 | + {eco.org.tagline} Des centaines de connecteurs lisent les sites à la | |
| 36 | + source, normalisent la donnée et se resynchronisent seuls.{" "} | |
| 37 | + {site ? ( | |
| 38 | + <> | |
| 39 | + <b style={{ color: "var(--paper)" }}>{site.wordmark}</b> —{" "} | |
| 40 | + {site.tagline} — est un service Groupe KA. | |
| 41 | + </> | |
| 42 | + ) : null} | |
| 43 | + </p> | |
| 44 | + <p className="notice"> | |
| 45 | + <b>{eco.org.disclaimer}</b> | |
| 46 | + </p> | |
| 47 | + <ul className="sites"> | |
| 48 | + {others.map((s) => ( | |
| 49 | + <li key={s.id}> | |
| 50 | + <a href={`https://${s.domain}`} target="_blank" rel="noopener noreferrer"> | |
| 51 | + {s.wordmark} | |
| 52 | + </a> | |
| 53 | + </li> | |
| 54 | + ))} | |
| 55 | + {eco.extraFooterLinks.map((l) => ( | |
| 56 | + <li key={l.href}> | |
| 57 | + <a href={l.href} target="_blank" rel="noopener noreferrer"> | |
| 58 | + {l.label} | |
| 59 | + </a> | |
| 60 | + </li> | |
| 61 | + ))} | |
| 62 | + </ul> | |
| 63 | + <div className="contacts"> | |
| 64 | + {eco.contacts.map((c) => ( | |
| 65 | + <p key={c.email} style={{ margin: 0 }}> | |
| 66 | + <a href={`mailto:${c.email}`}>{c.email}</a> | |
| 67 | + <span>{c.role}</span> | |
| 68 | + </p> | |
| 69 | + ))} | |
| 70 | + </div> | |
| 71 | + <p className="legal"> | |
| 72 | + © {year} {eco.org.copyrightHolder} | |
| 73 | + {localLegal.map((l) => ( | |
| 74 | + <span key={l.href}> | |
| 75 | + {" · "} | |
| 76 | + <a href={l.href}>{l.label}</a> | |
| 77 | + </span> | |
| 78 | + ))} | |
| 79 | + {eco.legal.map((l) => ( | |
| 80 | + <span key={l.href}> | |
| 81 | + {" · "} | |
| 82 | + <a href={l.href} target="_blank" rel="noopener noreferrer"> | |
| 83 | + {l.label} | |
| 84 | + </a> | |
| 85 | + </span> | |
| 86 | + ))} | |
| 87 | + </p> | |
| 88 | + </div> | |
| 89 | + </footer> | |
| 90 | + ); | |
| 91 | +} | |
added
src/api/web/ka/ecosystem.json
+45 −0
@@ -0,0 +1,45 @@ | ||
| 1 | +{ | |
| 2 | + "org": { | |
| 3 | + "name": "Groupe KA", | |
| 4 | + "legalName": "Groupe KA — Simon-Pierre Boucher", | |
| 5 | + "tagline": "Holding québécois d'agrégateurs de produits et services entièrement automatisés.", | |
| 6 | + "disclaimer": "Groupe KA est un agrégateur de contenu : nous ne vendons rien, ne louons rien et ne sommes partie à aucune transaction.", | |
| 7 | + "copyrightHolder": "Groupe KA — Simon-Pierre Boucher" | |
| 8 | + }, | |
| 9 | + "hub": { | |
| 10 | + "url": "https://www.groupe-ka.com", | |
| 11 | + "loginPath": "/connexion", | |
| 12 | + "signupNote": "La création de compte KA ID se fait sur le hub groupe-ka.com ; chaque site délègue sa connexion via /api/auth/ka/login." | |
| 13 | + }, | |
| 14 | + "contacts": [ | |
| 15 | + { "email": "contact@groupe-ka.com", "role": "Projets, partenariats & données" }, | |
| 16 | + { "email": "info@groupe-ka.com", "role": "Médias & questions générales" }, | |
| 17 | + { "email": "admin@groupe-ka.com", "role": "Légal, vie privée & Loi 25" } | |
| 18 | + ], | |
| 19 | + "legal": [ | |
| 20 | + { "label": "Conditions d'utilisation", "href": "https://www.groupe-ka.com/conditions" }, | |
| 21 | + { "label": "Politique de confidentialité", "href": "https://www.groupe-ka.com/confidentialite" }, | |
| 22 | + { "label": "Protection des renseignements personnels (Loi 25)", "href": "https://www.groupe-ka.com/loi-25" }, | |
| 23 | + { "label": "Transparence des robots d'indexation", "href": "https://www.groupe-ka.com/bots" } | |
| 24 | + ], | |
| 25 | + "sites": [ | |
| 26 | + { "id": "groupe-ka", "wordmark": "Groupe KA", "domain": "www.groupe-ka.com", "accent": "#d9f26b", "accentSoft": "#f0f9d2", "accentDeep": "#123f2e", "onAccent": "#141814", "tagline": "Le portail de l'écosystème ·Ka" }, | |
| 27 | + { "id": "trouve-ka", "wordmark": "Trouve·Ka", "domain": "www.trouve-ka.com", "accent": "#1c7ed6", "accentSoft": "#e7f2fd", "accentDeep": "#14508f", "onAccent": "#ffffff", "tagline": "Le moteur de recherche du web québécois" }, | |
| 28 | + { "id": "lou-ka", "wordmark": "Lou·Ka", "domain": "www.lou-ka.com", "accent": "#ff6a00", "accentSoft": "#fff1e6", "accentDeep": "#cc5500", "onAccent": "#ffffff", "tagline": "Tous les logements à louer" }, | |
| 29 | + { "id": "immo-ka", "wordmark": "Immo·Ka", "domain": "www.immo-ka.com", "accent": "#e23744", "accentSoft": "#fbe0e2", "accentDeep": "#a8232e", "onAccent": "#ffffff", "tagline": "Toutes les propriétés à vendre" }, | |
| 30 | + { "id": "vrai-prix", "wordmark": "Vrai-Prix", "domain": "www.vrai-prix.com", "accent": "#ff5148", "accentSoft": "#ffe3e0", "accentDeep": "#9e2a25", "onAccent": "#ffffff", "tagline": "La valeur réelle de chaque propriété" }, | |
| 31 | + { "id": "auto-ka", "wordmark": "Auto·Ka", "domain": "www.auto-ka.com", "accent": "#ff5a2a", "accentSoft": "#ffe8de", "accentDeep": "#cc3f16", "onAccent": "#ffffff", "tagline": "Les voitures usagées du Québec" }, | |
| 32 | + { "id": "fabri-ka", "wordmark": "Fabri·Ka", "domain": "www.fabri-ka.com", "accent": "#c4532e", "accentSoft": "#f7e3da", "accentDeep": "#a94525", "onAccent": "#ffffff", "tagline": "Les produits fabriqués au Québec" }, | |
| 33 | + { "id": "food-ka", "wordmark": "Food·Ka", "domain": "www.food-ka.com", "accent": "#1f9d55", "accentSoft": "#e2f5ea", "accentDeep": "#157a40", "onAccent": "#ffffff", "tagline": "Les prix d'épicerie, suivis à la source" }, | |
| 34 | + { "id": "resto-ka", "wordmark": "Resto·Ka", "domain": "www.resto-ka.com", "accent": "#f08c00", "accentSoft": "#fdeed7", "accentDeep": "#b96a00", "onAccent": "#141814", "tagline": "Chaque resto, chaque plat, chaque prix" }, | |
| 35 | + { "id": "sorti-ka", "wordmark": "Sorti·Ka", "domain": "www.sorti-ka.com", "accent": "#d6336c", "accentSoft": "#fbe0eb", "accentDeep": "#a12551", "onAccent": "#ffffff", "tagline": "Toutes les sorties, dans les 17 régions" }, | |
| 36 | + { "id": "crea-ka", "wordmark": "Créa·Ka", "domain": "www.crea-ka.com", "accent": "#7048e8", "accentSoft": "#ece5fc", "accentDeep": "#5433b8", "onAccent": "#ffffff", "tagline": "Les créateurs d'ici, tous leurs liens" }, | |
| 37 | + { "id": "api-ka", "wordmark": "API·Ka", "domain": "www.api-ka.com", "accent": "#3b5bdb", "accentSoft": "#e4eafb", "accentDeep": "#2b44a8", "onAccent": "#ffffff", "tagline": "La donnée de l'écosystème, par API" } | |
| 38 | + ], | |
| 39 | + "extraFooterLinks": [ | |
| 40 | + { "label": "ValoPlex", "href": "https://www.valoplex.com" }, | |
| 41 | + { "label": "Ka2", "href": "https://www.ka2.bot" }, | |
| 42 | + { "label": "Ka4", "href": "https://www.ka4.bot" }, | |
| 43 | + { "label": "Ka6", "href": "https://www.ka6.bot" } | |
| 44 | + ] | |
| 45 | +} | |
added
src/api/web/ka/ka-shell.js
+105 −0
@@ -0,0 +1,105 @@ | ||
| 1 | +/* ============================================ | |
| 2 | + Projet : API-KA | |
| 3 | + Fichier : src/api/web/ka/ka-shell.js | |
| 4 | + Node : m3u96b | |
| 5 | + Author : Simon-Pierre Boucher | |
| 6 | + Contact : contact@spboucher.ai | |
| 7 | + Date : 2026-08-17 | |
| 8 | + ============================================ | |
| 9 | + Coquille commune Groupe KA pour les pages statiques d'API-KA : | |
| 10 | + · footer commun .ka-footer (équivalent vanilla de ka-ui/KaFooter.tsx), | |
| 11 | + alimenté par /ka/ecosystem.json (source de vérité unique) ; | |
| 12 | + · cartes contact (page /contact, conteneur #ka-contacts) ; | |
| 13 | + · état de connexion KA ID dans le header (conteneur #ka-auth). */ | |
| 14 | +"use strict"; | |
| 15 | + | |
| 16 | +const KA_SITE_ID = "api-ka"; | |
| 17 | + | |
| 18 | +async function kaEco() { | |
| 19 | + const r = await fetch("/ka/ecosystem.json"); | |
| 20 | + return r.json(); | |
| 21 | +} | |
| 22 | + | |
| 23 | +function el(tag, cls, html) { | |
| 24 | + const e = document.createElement(tag); | |
| 25 | + if (cls) e.className = cls; | |
| 26 | + if (html !== undefined) e.innerHTML = html; | |
| 27 | + return e; | |
| 28 | +} | |
| 29 | + | |
| 30 | +function esc(s) { | |
| 31 | + return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); | |
| 32 | +} | |
| 33 | + | |
| 34 | +/* ---------- footer commun (structure .ka-footer de tokens.css) ---------- */ | |
| 35 | +function kaRenderFooter(eco) { | |
| 36 | + const root = document.getElementById("ka-footer-root"); | |
| 37 | + if (!root) return; | |
| 38 | + const site = eco.sites.find((s) => s.id === KA_SITE_ID); | |
| 39 | + const others = eco.sites.filter((s) => s.id !== KA_SITE_ID); | |
| 40 | + const year = new Date().getFullYear(); | |
| 41 | + root.innerHTML = ` | |
| 42 | + <a class="wordmark" href="${eco.hub.url}" target="_blank" rel="noopener noreferrer" | |
| 43 | + aria-label="Groupe KA — le portail de l'écosystème">Groupe <span class="ka">KA</span></a> | |
| 44 | + <p class="desc">${esc(eco.org.tagline)} Des centaines de connecteurs lisent les sites à la | |
| 45 | + source, normalisent la donnée et se resynchronisent seuls. | |
| 46 | + ${site ? `<b style="color:var(--paper)">${esc(site.wordmark)}</b> — ${esc(site.tagline)} — est un service Groupe KA.` : ""}</p> | |
| 47 | + <p class="notice"><b>${esc(eco.org.disclaimer)}</b></p> | |
| 48 | + <ul class="sites"> | |
| 49 | + ${others.map((s) => `<li><a href="https://${s.domain}" target="_blank" rel="noopener noreferrer">${esc(s.wordmark)}</a></li>`).join("")} | |
| 50 | + ${(eco.extraFooterLinks || []).map((l) => `<li><a href="${l.href}" target="_blank" rel="noopener noreferrer">${esc(l.label)}</a></li>`).join("")} | |
| 51 | + </ul> | |
| 52 | + <div class="contacts"> | |
| 53 | + ${eco.contacts.map((c) => `<p style="margin:0"><a href="mailto:${c.email}">${c.email}</a><span>${esc(c.role)}</span></p>`).join("")} | |
| 54 | + </div> | |
| 55 | + <p class="legal">© ${year} ${esc(eco.org.copyrightHolder)} | |
| 56 | + ${eco.legal.map((l) => ` · <a href="${l.href}" target="_blank" rel="noopener noreferrer">${esc(l.label)}</a>`).join("")} | |
| 57 | + </p>`; | |
| 58 | +} | |
| 59 | + | |
| 60 | +/* ---------- cartes contact (page /contact) ---------- */ | |
| 61 | +function kaRenderContacts(eco) { | |
| 62 | + const zone = document.getElementById("ka-contacts"); | |
| 63 | + if (!zone) return; | |
| 64 | + zone.innerHTML = eco.contacts | |
| 65 | + .map( | |
| 66 | + (c) => `<div class="card contact-card"> | |
| 67 | + <span class="klabel">${esc(c.role)}</span> | |
| 68 | + <a class="contact-mail" href="mailto:${c.email}">${c.email}</a> | |
| 69 | + </div>` | |
| 70 | + ) | |
| 71 | + .join(""); | |
| 72 | + const hub = document.getElementById("ka-hub-link"); | |
| 73 | + if (hub) hub.href = eco.hub.url + eco.hub.loginPath; | |
| 74 | +} | |
| 75 | + | |
| 76 | +/* ---------- connexion KA ID dans le header (#ka-auth) ---------- */ | |
| 77 | +async function kaRenderAuth() { | |
| 78 | + const zone = document.getElementById("ka-auth"); | |
| 79 | + if (!zone) return; | |
| 80 | + try { | |
| 81 | + const r = await fetch("/api/auth/me", { headers: { Accept: "application/json" } }); | |
| 82 | + if (r.ok) { | |
| 83 | + const u = await r.json(); | |
| 84 | + zone.innerHTML = `<span class="ka-user" title="${esc(u.email || "")}"> | |
| 85 | + ${u.picture ? `<img src="${u.picture}" alt="" class="ka-avatar">` : ""} | |
| 86 | + <b>${esc(u.name || "membre")}</b></span> | |
| 87 | + <button class="btn btn-ghost ka-logout" type="button">Déconnexion</button>`; | |
| 88 | + zone.querySelector(".ka-logout").addEventListener("click", async () => { | |
| 89 | + await fetch("/api/auth/logout", { method: "POST" }); | |
| 90 | + location.reload(); | |
| 91 | + }); | |
| 92 | + return; | |
| 93 | + } | |
| 94 | + } catch {} | |
| 95 | + zone.innerHTML = `<a class="btn btn-accent ka-login" href="/api/auth/ka/login">Se connecter avec KA</a>`; | |
| 96 | +} | |
| 97 | + | |
| 98 | +(async () => { | |
| 99 | + kaRenderAuth(); | |
| 100 | + try { | |
| 101 | + const eco = await kaEco(); | |
| 102 | + kaRenderFooter(eco); | |
| 103 | + kaRenderContacts(eco); | |
| 104 | + } catch {} | |
| 105 | +})(); | |
added
src/api/web/ka/tokens.css
+229 −0
@@ -0,0 +1,229 @@ | ||
| 1 | +/* ----------------------------------------------------------------------------- | |
| 2 | + Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 3 | + Fichier : ka-ui/tokens.css — SOURCE CANONIQUE (repo ka-ui sur spbgit) | |
| 4 | + Desc. : Design system commun GROUPE KA « éditorial sharp » — tokens + socle | |
| 5 | + de composants partagés par les 12 plateformes. Chaque site importe ce | |
| 6 | + fichier PUIS surcharge uniquement son accent (voir accents.css). | |
| 7 | + | |
| 8 | + · Typo : display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono | |
| 9 | + · Palette: papier #f5f3ee · encre #141814 · vert profond #1c5c41 + ACCENT du site | |
| 10 | + · Signature : bordures encre 1,5–2 px + ombres décalées dures, grain de film, | |
| 11 | + surlignés d'accent inclinés (néo-brutalisme raffiné) | |
| 12 | + · Breakpoints communs : 360 / 768 / 1024 / 1440 px (mobile-first) | |
| 13 | + · Zones tactiles ≥ 44×44 px, safe-areas iOS, typographie fluide (clamp) | |
| 14 | +----------------------------------------------------------------------------- */ | |
| 15 | + | |
| 16 | +:root { | |
| 17 | + /* ---- Palette fixe Groupe KA (identique sur les 12 sites) ---- */ | |
| 18 | + --paper: #f5f3ee; | |
| 19 | + --surface: #ffffff; | |
| 20 | + --surface-2: #faf9f5; | |
| 21 | + --ink: #141814; | |
| 22 | + --ink-2: #4d5551; | |
| 23 | + --ink-3: #8b928c; | |
| 24 | + --line: rgba(20, 24, 20, 0.14); | |
| 25 | + --line-strong: rgba(20, 24, 20, 0.85); | |
| 26 | + --green: #1c5c41; | |
| 27 | + --green-deep: #123f2e; | |
| 28 | + --amber: #e8a33d; | |
| 29 | + --amber-soft: #fdf3e2; | |
| 30 | + --danger: #b3423a; | |
| 31 | + --danger-soft: #fbe9e7; | |
| 32 | + | |
| 33 | + /* ---- ACCENT du site — SEULES variables surchargées par marque ---- */ | |
| 34 | + --accent: #d9f26b; /* défaut : lime Groupe KA */ | |
| 35 | + --accent-soft: #f0f9d2; | |
| 36 | + --accent-deep: #123f2e; | |
| 37 | + --on-accent: var(--ink); /* couleur du texte posé SUR l'accent */ | |
| 38 | + | |
| 39 | + /* alias rétro-compatibles (les satellites historiques utilisent --lime) */ | |
| 40 | + --lime: var(--accent); | |
| 41 | + --lime-soft: var(--accent-soft); | |
| 42 | + | |
| 43 | + /* ---- Géométrie sharp ---- */ | |
| 44 | + --r-card: 10px; | |
| 45 | + --r-ctl: 6px; | |
| 46 | + --r-pill: 999px; | |
| 47 | + | |
| 48 | + /* ---- Ombres décalées — la signature du groupe ---- */ | |
| 49 | + --shadow-flat: 0 1px 2px rgba(20, 24, 20, 0.05); | |
| 50 | + --shadow-off: 6px 6px 0 var(--ink); | |
| 51 | + --shadow-off-soft: 8px 8px 0 rgba(20, 24, 20, 0.08); | |
| 52 | + --shadow-off-mid: 4px 4px 0 rgba(20, 24, 20, 0.18); | |
| 53 | + | |
| 54 | + /* ---- Typographie ---- */ | |
| 55 | + --font-display: "Space Grotesk", system-ui, sans-serif; | |
| 56 | + --font-body: "Inter", system-ui, sans-serif; | |
| 57 | + --font-mono: "JetBrains Mono", ui-monospace, monospace; | |
| 58 | + | |
| 59 | + /* Échelle fluide (mobile-first, clamp) */ | |
| 60 | + --fs-h1: clamp(30px, 3.2vw + 18px, 54px); | |
| 61 | + --fs-h2: clamp(23px, 1.8vw + 14px, 34px); | |
| 62 | + --fs-h3: clamp(17px, 0.9vw + 12px, 22px); | |
| 63 | + --fs-body: 15px; | |
| 64 | + --fs-small: 13px; | |
| 65 | + | |
| 66 | + /* Espacements */ | |
| 67 | + --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; | |
| 68 | + --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px; | |
| 69 | + | |
| 70 | + /* Cible tactile minimale */ | |
| 71 | + --touch: 44px; | |
| 72 | +} | |
| 73 | + | |
| 74 | +/* ---- Socle ---- */ | |
| 75 | +* { box-sizing: border-box; } | |
| 76 | +html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; } | |
| 77 | +body { | |
| 78 | + margin: 0; | |
| 79 | + background: var(--paper); | |
| 80 | + color: var(--ink); | |
| 81 | + font-family: var(--font-body); | |
| 82 | + font-size: var(--fs-body); | |
| 83 | + line-height: 1.55; | |
| 84 | + -webkit-font-smoothing: antialiased; | |
| 85 | + padding-bottom: env(safe-area-inset-bottom); | |
| 86 | + overflow-x: clip; /* jamais de débordement horizontal */ | |
| 87 | +} | |
| 88 | +img, svg, video { max-width: 100%; height: auto; display: block; } | |
| 89 | +h1, h2, h3, h4 { font-family: var(--font-display); letter-spacing: -0.03em; margin: 0 0 0.5em; } | |
| 90 | +h1 { font-size: var(--fs-h1); line-height: 1.06; } | |
| 91 | +h2 { font-size: var(--fs-h2); line-height: 1.15; } | |
| 92 | +h3 { font-size: var(--fs-h3); line-height: 1.25; } | |
| 93 | +a { color: inherit; } | |
| 94 | +::selection { background: var(--accent); color: var(--on-accent); } | |
| 95 | +:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; } | |
| 96 | + | |
| 97 | +/* Grain de film pleine page — ⚠️ jamais de z-index sur body > * (position: | |
| 98 | + relative seulement), sinon les utilitaires z-* sont écrasés. */ | |
| 99 | +body::before { | |
| 100 | + content: ""; | |
| 101 | + position: fixed; inset: 0; pointer-events: none; opacity: 0.35; z-index: 9999; | |
| 102 | + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.06'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E"); | |
| 103 | +} | |
| 104 | +body > * { position: relative; } | |
| 105 | + | |
| 106 | +/* ---- Conteneur commun ---- */ | |
| 107 | +.container { max-width: 1152px; margin: 0 auto; padding: 0 var(--sp-4); } | |
| 108 | +@media (min-width: 768px) { .container { padding: 0 var(--sp-5); } } | |
| 109 | + | |
| 110 | +/* ---- Micro-typographie signature ---- */ | |
| 111 | +.kicker { | |
| 112 | + display: inline-flex; align-items: center; gap: 10px; | |
| 113 | + font-family: var(--font-mono); font-size: 11.5px; font-weight: 500; | |
| 114 | + text-transform: uppercase; letter-spacing: 0.12em; color: var(--green); | |
| 115 | +} | |
| 116 | +.kicker::before { content: ""; width: 22px; height: 2px; background: var(--green); } | |
| 117 | +.klabel { | |
| 118 | + font-family: var(--font-mono); font-size: 10px; font-weight: 700; | |
| 119 | + text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); | |
| 120 | +} | |
| 121 | +.hl { | |
| 122 | + display: inline-block; background: var(--accent); color: var(--on-accent); | |
| 123 | + border-radius: 8px; padding: 0 10px 2px; transform: rotate(-1deg); | |
| 124 | +} | |
| 125 | +.outline-txt { color: transparent; -webkit-text-stroke: 2px var(--ink); } | |
| 126 | + | |
| 127 | +/* ---- Boutons (cible ≥ 44 px) ---- */ | |
| 128 | +.btn { | |
| 129 | + display: inline-flex; align-items: center; justify-content: center; gap: 8px; | |
| 130 | + min-height: var(--touch); padding: 10px 18px; | |
| 131 | + font-family: var(--font-display); font-weight: 700; font-size: 14px; | |
| 132 | + border: 1.5px solid var(--ink); border-radius: var(--r-ctl); | |
| 133 | + background: var(--surface); color: var(--ink); | |
| 134 | + cursor: pointer; text-decoration: none; | |
| 135 | + transition: transform 0.15s, box-shadow 0.15s, background 0.15s, color 0.15s; | |
| 136 | +} | |
| 137 | +.btn:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off-mid); } | |
| 138 | +.btn:active { transform: translate(0, 0); box-shadow: none; } | |
| 139 | +.btn:disabled { opacity: 0.45; pointer-events: none; } | |
| 140 | +.btn-primary { background: var(--ink); color: var(--accent); } | |
| 141 | +.btn-primary:hover { background: var(--accent-deep); } | |
| 142 | +.btn-accent { background: var(--accent); color: var(--on-accent); } | |
| 143 | +.btn-ghost { background: transparent; border-color: var(--line); } | |
| 144 | +.btn-ghost:hover { border-color: var(--ink); } | |
| 145 | + | |
| 146 | +/* ---- Cartes ---- */ | |
| 147 | +.card { | |
| 148 | + background: var(--surface); border: 1.5px solid var(--ink); | |
| 149 | + border-radius: var(--r-card); box-shadow: var(--shadow-off-soft); | |
| 150 | + overflow: hidden; | |
| 151 | +} | |
| 152 | +.card-hover { transition: transform 0.15s, box-shadow 0.15s; } | |
| 153 | +.card-hover:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); } | |
| 154 | + | |
| 155 | +/* ---- Chips / badges ---- */ | |
| 156 | +.chip { | |
| 157 | + display: inline-flex; align-items: center; gap: 6px; | |
| 158 | + padding: 4px 11px; font-family: var(--font-mono); font-size: 11px; font-weight: 700; | |
| 159 | + text-transform: uppercase; letter-spacing: 0.06em; | |
| 160 | + border: 1.5px solid var(--ink); border-radius: var(--r-pill); | |
| 161 | + background: var(--surface); color: var(--ink); | |
| 162 | +} | |
| 163 | +.chip-accent { background: var(--accent); color: var(--on-accent); } | |
| 164 | +.chip-soft { background: var(--accent-soft); border-color: var(--line); } | |
| 165 | + | |
| 166 | +/* ---- Champs ---- */ | |
| 167 | +.input, .select, .textarea { | |
| 168 | + width: 100%; min-height: var(--touch); padding: 10px 14px; | |
| 169 | + font: inherit; color: var(--ink); background: var(--surface); | |
| 170 | + border: 1.5px solid var(--ink); border-radius: var(--r-ctl); | |
| 171 | +} | |
| 172 | +.input:focus, .select:focus, .textarea:focus { | |
| 173 | + outline: none; box-shadow: 3px 3px 0 var(--accent); border-color: var(--ink); | |
| 174 | +} | |
| 175 | +.input::placeholder { color: var(--ink-3); } | |
| 176 | + | |
| 177 | +/* ---- Badge « Un service Groupe KA » (header, cliquable → hub) ---- */ | |
| 178 | +.gk-badge { | |
| 179 | + display: inline-flex; align-items: center; gap: 7px; | |
| 180 | + min-height: 30px; padding: 3px 10px 4px; | |
| 181 | + font-family: var(--font-mono); font-size: 10px; font-weight: 700; | |
| 182 | + letter-spacing: 0.08em; text-transform: uppercase; text-decoration: none; | |
| 183 | + border: 1.5px solid var(--ink); border-radius: var(--r-pill); | |
| 184 | + background: var(--surface); color: var(--ink-2); white-space: nowrap; | |
| 185 | +} | |
| 186 | +.gk-badge b { | |
| 187 | + font-family: var(--font-display); font-size: 12px; letter-spacing: -0.02em; | |
| 188 | + text-transform: none; color: var(--ink); | |
| 189 | +} | |
| 190 | +.gk-badge b .ka { | |
| 191 | + display: inline-block; background: var(--ink); color: var(--accent); | |
| 192 | + border-radius: 5px; padding: 0 5px 1px; margin-left: 3px; transform: rotate(-2deg); | |
| 193 | +} | |
| 194 | +.gk-badge:hover .ka { transform: rotate(0); } | |
| 195 | +.gk-badge--dark { background: transparent; border-color: rgba(245,243,238,0.4); color: rgba(245,243,238,0.75); } | |
| 196 | +.gk-badge--dark b { color: var(--paper); } | |
| 197 | + | |
| 198 | +/* ---- Footer commun (fond encre — voir react/KaFooter.tsx) ---- */ | |
| 199 | +.ka-footer { margin-top: var(--sp-8); background: var(--ink); padding: 44px 0; font-size: 13px; color: rgba(245,243,238,0.75); } | |
| 200 | +.ka-footer a { text-decoration: none; } | |
| 201 | +.ka-footer .wordmark { font-family: var(--font-display); font-weight: 700; font-size: 30px; letter-spacing: -0.04em; color: var(--paper); text-decoration: none; } | |
| 202 | +.ka-footer .wordmark .ka { color: var(--accent); } | |
| 203 | +.ka-footer .desc { max-width: 640px; margin-top: var(--sp-4); } | |
| 204 | +.ka-footer .notice { max-width: 640px; margin-top: var(--sp-4); border-left: 2px solid var(--accent); padding-left: var(--sp-4); } | |
| 205 | +.ka-footer .notice b { color: var(--paper); } | |
| 206 | +.ka-footer .sites { list-style: none; display: flex; flex-wrap: wrap; gap: 10px 26px; margin: 26px 0 0; padding: 0; font-family: var(--font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; } | |
| 207 | +.ka-footer .sites a { color: rgba(245,243,238,0.65); } | |
| 208 | +.ka-footer .sites a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 4px; } | |
| 209 | +.ka-footer .contacts { display: grid; gap: 14px 32px; border-top: 1px solid rgba(245,243,238,0.15); margin-top: 30px; padding-top: 26px; } | |
| 210 | +@media (min-width: 768px) { .ka-footer .contacts { grid-template-columns: repeat(3, 1fr); } } | |
| 211 | +.ka-footer .contacts a { font-family: var(--font-mono); font-weight: 700; font-size: 12px; color: rgba(245,243,238,0.85); } | |
| 212 | +.ka-footer .contacts a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 4px; } | |
| 213 | +.ka-footer .contacts span { display: block; margin-top: 3px; font-size: 11px; color: rgba(245,243,238,0.5); } | |
| 214 | +.ka-footer .legal { margin-top: 30px; font-family: var(--font-mono); font-size: 11px; color: rgba(245,243,238,0.45); } | |
| 215 | +.ka-footer .legal a { color: inherit; } | |
| 216 | +.ka-footer .legal a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 4px; } | |
| 217 | + | |
| 218 | +/* ---- Tableaux → cartes empilées sous 768 px ---- */ | |
| 219 | +.tbl-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; } | |
| 220 | +@media (max-width: 767px) { | |
| 221 | + .tbl-stack thead { display: none; } | |
| 222 | + .tbl-stack tr { display: block; border: 1.5px solid var(--ink); border-radius: var(--r-card); margin-bottom: var(--sp-3); background: var(--surface); } | |
| 223 | + .tbl-stack td { display: flex; justify-content: space-between; gap: var(--sp-3); padding: 8px 12px; border: 0; } | |
| 224 | + .tbl-stack td::before { content: attr(data-label); font-family: var(--font-mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); } | |
| 225 | +} | |
| 226 | + | |
| 227 | +/* ---- Utilitaires responsive ---- */ | |
| 228 | +.only-mobile { display: initial; } .only-desktop { display: none; } | |
| 229 | +@media (min-width: 768px) { .only-mobile { display: none; } .only-desktop { display: initial; } } | |
| 230 | ||