SPB Git forge

spb/sorti-ka

Public

Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.

58commits 1branches 0releases
13.7 MBsize
maindefault branch
17 days agolast push
HTML 82.9% Python 15.2% TypeScript 0.9% JavaScript 0.7%

favoris ♥ « Mon univers Ka » (KA ID) : hub groupe-ka.com magasin central

- sortika/hubfav.py : client HMAC du hub (patron food-ka) — toggle synchrone,
  lecture avec cache mémoire 30 s invalidé au toggle, aucun stockage local
- API : GET /api/favorites + POST /api/favorites/toggle (session KA ID, 401
  sinon ; 403 compte non relié ; 502 hub injoignable)
- frontend : cœur ♥ sur les cartes et la fiche (non connecté -> connexion
  KA ID), page /favoris + entrée de nav, toggle optimiste avec rétablissement
- SSR : route /favoris (noindex, hors sitemaps)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent 71919c4

4 changed files +334 −5

modified frontend/index.html +173 −3
@@ -397,6 +397,21 @@ button{font-family:inherit}
397 397 .st-empty{padding:20px;background:var(--surface-2);border-style:dashed}
398 398 .st-empty b{font-family:var(--font-display);font-size:14px}
399 399 .st-loading{padding:40px 0;font-family:var(--font-mono);font-size:12px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.08em}
400 +/* ---- favoris ♥ « Mon univers Ka » (hub groupe-ka.com) ---- */
401 +.fav-btn{position:absolute;top:8px;right:8px;z-index:2;width:34px;height:34px;
402 + display:inline-flex;align-items:center;justify-content:center;cursor:pointer;
403 + background:var(--surface);border:1.5px solid var(--ink);border-radius:50%;
404 + color:var(--ink);padding:0;transition:transform .12s ease,background .12s ease}
405 +.fav-btn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2;stroke-linejoin:round}
406 +.fav-btn:hover{transform:scale(1.08);background:var(--accent-soft)}
407 +.fav-btn.on{background:var(--accent);border-color:var(--accent-deep);color:var(--on-accent)}
408 +.fav-btn.on svg{fill:currentColor}
409 +.fav-btn.big{position:static;flex:none;width:44px;height:44px}
410 +.fav-btn.big svg{width:21px;height:21px}
411 +.d-title-row{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
412 +.d-title-row h1{min-width:0}
413 +.fav-empty{grid-column:1/-1}
414 +.fav-empty .btn-ka{margin-top:12px}
400 415 #ssr{display:none}
401 416 .hidden{display:none!important}
402 417 </style>
@@ -423,6 +438,7 @@ button{font-family:inherit}
423 438 <span class="brand-tag">Sorties &amp; événements — Québec</span>
424 439 <nav class="nav">
425 440 <a href="/#sources">Sources</a>
441 + <a data-nav href="/favoris" title="Mes favoris — Mon univers Ka">♥ Favoris</a>
426 442 <a data-nav href="/stats">Stats</a>
427 443 <a data-nav href="/contact">Contact</a>
428 444 <a href="/docs" target="_blank" rel="noopener">API</a>
@@ -489,7 +505,7 @@ button{font-family:inherit}
489 505 <div class="body">
490 506 <span class="klabel" id="d-cats-label"></span>
491 507 <div id="d-status"></div>
492 − <h1 id="d-title"></h1>
508 + <div class="d-title-row"><h1 id="d-title"></h1><span id="d-fav"></span></div>
493 509 <div class="desc" id="d-desc"></div>
494 510 <div class="detail-cats" id="d-cats"></div>
495 511 </div>
@@ -508,6 +524,19 @@ button{font-family:inherit}
508 524 </div>
509 525 </main>
510 526
527 +<!-- ======== VUE FAVORIS (/favoris) — ♥ « Mon univers Ka » (hub groupe-ka.com) ======== -->
528 +<main id="view-favoris" class="hidden">
529 + <section class="hero container">
530 + <span class="kicker">Mon univers Ka · synchronisés sur toutes les plateformes Groupe KA</span>
531 + <h1>Mes <span class="hl">favoris</span></h1>
532 + <p class="lede" id="fav-lede">Vos sorties et événements favoris, rattachés à votre compte KA ID.</p>
533 + </section>
534 + <section class="container">
535 + <div class="meta-line"><span id="fav-count"></span></div>
536 + <section class="grid" id="fav-grid"></section>
537 + </section>
538 +</main>
539 +
511 540 <!-- ======== VUE STATS (/stats) — module ka-stats (SPEC.md), vanilla + SVG ======== -->
512 541 <main id="view-stats" class="hidden">
513 542 <div class="container stats-page">
@@ -724,7 +753,7 @@ function card(ev){
724 753 const img = ev.image
725 754 ? `<img src="${esc(ev.image)}" alt="" loading="lazy" onerror="this.remove()">` : "";
726 755 return `<a class="card gk-card gk-hover" data-nav href="/evenement/${esc(ev.uid)}">
727 − <div class="card-img">${img}<span class="badge-date">${dateBadge(ev)}</span></div>
756 + <div class="card-img">${img}<span class="badge-date">${dateBadge(ev)}</span>${favBtn(ev)}</div>
728 757 <div class="card-body">
729 758 <div class="card-top"><h3>${esc(ev.title)}</h3>${statusBadge(ev)||priceBadge(ev)}</div>
730 759 ${ev.description?`<div class="desc">${esc(ev.description)}</div>`:""}
@@ -795,6 +824,7 @@ async function showDetail(uid){
795 824 : ev.status==="postponed" ? "Cet événement est reporté — vérifiez la nouvelle date chez la source"
796 825 : "Complet — plus de billets disponibles"}</div>` : "";
797 826 $("d-title").textContent = ev.title;
827 + $("d-fav").innerHTML = favBtn(ev, true);
798 828 $("d-desc").textContent = ev.description || "";
799 829 $("d-cats").innerHTML = (ev.raw_categories||[]).slice(0,5).filter(c=>c.length<40)
800 830 .map(c=>`<span class="cat">${esc(c)}</span>`).join("");
@@ -1598,8 +1628,146 @@ async function sbGenerate(){
1598 1628
1599 1629 $("st-custom-btn").onclick = () => { SB.cat = null; SB.err = ""; sbOpen(); };
1600 1630
1631 +/* ---------- favoris ♥ « Mon univers Ka » ----------
1632 + Le hub Groupe KA (groupe-ka.com) est le magasin central des favoris du
1633 + groupe : AUCUN stockage navigateur. Lecture/écriture via l'API locale
1634 + /api/favorites, qui relaie au hub (session KA ID requise). */
1635 +const FAV = {ready:false, connected:false, ids:new Set(), items:[], meta:{}};
1636 +const FAV_HEART = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>`;
1637 +
1638 +/* événement -> item de favori poussé au hub (titre, lieu · date, prix, image) */
1639 +function favItem(ev){
1640 + let price = "";
1641 + if(ev.is_free === true) price = "Gratuit";
1642 + else if(ev.price_min != null) price = `Dès ${ev.price_min.toFixed(2).replace(".",",")} $`;
1643 + return {
1644 + item_id: ev.uid,
1645 + title: ev.title || "Événement",
1646 + subtitle: [ev.venue || ev.city, dateBadge(ev)].filter(Boolean).join(" · "),
1647 + price_label: price,
1648 + image_url: ev.image || "",
1649 + url: `https://www.sorti-ka.com/evenement/${encodeURIComponent(ev.uid)}`,
1650 + };
1651 +}
1652 +
1653 +/* cœur ♥ — cartes (par défaut) et fiche événement (big) */
1654 +function favBtn(ev, big=false){
1655 + FAV.meta[ev.uid] = favItem(ev);
1656 + const on = FAV.ids.has(ev.uid);
1657 + const label = on ? "Retirer de mes favoris (Mon univers Ka)"
1658 + : "Ajouter à mes favoris (Mon univers Ka)";
1659 + return `<button type="button" class="fav-btn${on?" on":""}${big?" big":""}"
1660 + data-fav="${esc(ev.uid)}" aria-pressed="${on}" aria-label="${label}"
1661 + title="${label}">${FAV_HEART}</button>`;
1662 +}
1663 +
1664 +/* repeint tous les cœurs affichés selon l'état courant */
1665 +function favPaint(){
1666 + document.querySelectorAll(".fav-btn[data-fav]").forEach(b => {
1667 + const on = FAV.ids.has(b.dataset.fav);
1668 + b.classList.toggle("on", on);
1669 + b.setAttribute("aria-pressed", on);
1670 + const label = on ? "Retirer de mes favoris (Mon univers Ka)"
1671 + : "Ajouter à mes favoris (Mon univers Ka)";
1672 + b.setAttribute("aria-label", label); b.title = label;
1673 + });
1674 +}
1675 +
1676 +/* lit la liste au hub (via l'API locale) — 401 = pas connecté, cœurs vides */
1677 +async function favLoad(){
1678 + try{
1679 + const r = await fetch("/api/favorites");
1680 + if(r.status === 401){ FAV.connected = false; return; }
1681 + if(!r.ok) return;
1682 + const d = await r.json();
1683 + FAV.connected = true;
1684 + FAV.items = d.items || [];
1685 + FAV.ids = new Set(d.ids || []);
1686 + favPaint();
1687 + }catch(e){}
1688 + finally{ FAV.ready = true; }
1689 +}
1690 +
1691 +/* toggle optimiste — le hub confirme (sinon on rétablit) */
1692 +async function favToggle(uid){
1693 + if(!FAV.connected){ location.href = "/api/auth/ka/login"; return; }
1694 + const on = !FAV.ids.has(uid);
1695 + const item = FAV.meta[uid] || FAV.items.find(i => i.item_id === uid) || {item_id: uid};
1696 + const apply = v => {
1697 + if(v){ FAV.ids.add(uid); if(!FAV.items.some(i=>i.item_id===uid)) FAV.items.push(item); }
1698 + else { FAV.ids.delete(uid); FAV.items = FAV.items.filter(i => i.item_id !== uid); }
1699 + favPaint();
1700 + if(location.pathname === "/favoris") renderFavs();
1701 + };
1702 + apply(on);
1703 + try{
1704 + const r = await fetch("/api/favorites/toggle", {method:"POST",
1705 + headers:{"Content-Type":"application/json"},
1706 + body: JSON.stringify({on, item})});
1707 + if(!r.ok) throw new Error(r.status);
1708 + }catch(e){ apply(!on); }
1709 +}
1710 +
1711 +/* clic sur un cœur — en phase capture : la carte entière est un lien */
1712 +document.addEventListener("click", e => {
1713 + const b = e.target.closest(".fav-btn[data-fav]");
1714 + if(!b) return;
1715 + e.preventDefault(); e.stopPropagation();
1716 + favToggle(b.dataset.fav);
1717 +}, true);
1718 +
1719 +/* carte d'un favori (données du hub — l'événement peut avoir quitté l'index) */
1720 +function favCard(it){
1721 + FAV.meta[it.item_id] = it;
1722 + const img = it.image_url
1723 + ? `<img src="${esc(it.image_url)}" alt="" loading="lazy" onerror="this.remove()">` : "";
1724 + const local = (it.url || "").replace(/^https?:\/\/[^/]*sorti-ka\.com/i, "")
1725 + || `/evenement/${encodeURIComponent(it.item_id)}`;
1726 + return `<a class="card gk-card gk-hover" data-nav href="${esc(local)}">
1727 + <div class="card-img">${img}
1728 + <button type="button" class="fav-btn on" data-fav="${esc(it.item_id)}" aria-pressed="true"
1729 + aria-label="Retirer de mes favoris (Mon univers Ka)"
1730 + title="Retirer de mes favoris (Mon univers Ka)">${FAV_HEART}</button>
1731 + </div>
1732 + <div class="card-body">
1733 + <div class="card-top"><h3>${esc(it.title)}</h3>${it.price_label?`<span class="badge-price">${esc(it.price_label)}</span>`:""}</div>
1734 + ${it.subtitle?`<div class="where">${esc(it.subtitle)}</div>`:""}
1735 + <div class="src"><span>Mon univers Ka</span><span class="go">Fiche →</span></div>
1736 + </div>
1737 + </a>`;
1738 +}
1739 +
1740 +function renderFavs(){
1741 + if(!FAV.connected){
1742 + $("fav-count").innerHTML = "Connectez-vous pour retrouver vos favoris";
1743 + $("fav-grid").innerHTML = `<div class="empty fav-empty">
1744 + <b>Vos favoris vous suivent partout dans l'univers Ka</b>
1745 + Connectez-vous avec votre KA ID pour ajouter des sorties à vos favoris —
1746 + ils sont synchronisés sur toutes les plateformes du Groupe KA.<br>
1747 + <a class="btn-ka" href="/api/auth/ka/login"><span class="kdot">·Ka</span> Se connecter avec KA ID</a></div>`;
1748 + return;
1749 + }
1750 + const items = FAV.items;
1751 + $("fav-count").innerHTML = items.length
1752 + ? `<b>${items.length.toLocaleString("fr-CA")}</b>&nbsp;favori${items.length>1?"s":""} sur Sorti·Ka`
1753 + : "Aucun favori pour l'instant";
1754 + $("fav-grid").innerHTML = items.map(favCard).join("") ||
1755 + `<div class="empty fav-empty"><b>Aucun favori pour l'instant</b>
1756 + Touchez le cœur ♥ d'un événement pour le garder ici — et le retrouver
1757 + sur toutes les plateformes du Groupe KA.</div>`;
1758 + bindNav($("fav-grid"));
1759 +}
1760 +
1761 +async function showFavoris(){
1762 + document.title = "Mes favoris — Sorti-Ka | Mon univers Ka";
1763 + show("view-favoris");
1764 + window.scrollTo({top:0});
1765 + await favLoad(); // le hub est la source de vérité — relire à l'ouverture
1766 + renderFavs();
1767 +}
1768 +
1601 1769 /* ---------- vues ---------- */
1602 −const VIEWS = ["view-list","view-detail","view-contact","view-stats"];
1770 +const VIEWS = ["view-list","view-detail","view-contact","view-stats","view-favoris"];
1603 1771 function show(id){
1604 1772 VIEWS.forEach(v => $(v).classList.toggle("hidden", v !== id));
1605 1773 }
@@ -1628,6 +1796,7 @@ function route(){
1628 1796 const c = location.pathname.match(/^\/categorie\/([^/]+)$/);
1629 1797 if(m) showDetail(decodeURIComponent(m[1]));
1630 1798 else if(location.pathname === "/contact") showContact();
1799 + else if(location.pathname === "/favoris") showFavoris();
1631 1800 else if(location.pathname === "/stats") showStats();
1632 1801 else if(r){
1633 1802 // page SSR /region/{slug} : la SPA ouvre la recherche filtrée sur la région
@@ -1659,6 +1828,7 @@ $("back").onclick = () => go("/");
1659 1828
1660 1829 /* ---------- init ---------- */
1661 1830 async function init(){
1831 + favLoad(); // favoris ♥ — repeint les cœurs dès la réponse du hub
1662 1832 const regions = await (await fetch("/api/regions")).json();
1663 1833 allRegions = regions;
1664 1834 $("region").insertAdjacentHTML("beforeend",
added sortika/hubfav.py +104 −0
@@ -0,0 +1,104 @@
1 +# -----------------------------------------------------------------------------
2 +# Sorti-Ka — Agrégateur de sorties & événements (province de Québec)
3 +# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 +# hubfav.py : favoris « Mon univers Ka » — le hub Groupe KA (groupe-ka.com)
5 +# est le MAGASIN CENTRAL des favoris du groupe : Sorti·Ka ne stocke rien
6 +# localement. Chaque ♥ est poussé au hub (POST signé HMAC, synchrone : un
7 +# échec remonte à l'appelant) et la liste est lue au hub (GET signé, cache
8 +# mémoire 30 s, invalidé à chaque toggle). Même secret que le SSO.
9 +# Config .env : KA_SSO_SECRET, KA_HUB_URL (optionnel).
10 +# (patron Food-Ka : foodka/hubfav.py)
11 +# -----------------------------------------------------------------------------
12 +from __future__ import annotations
13 +
14 +import hashlib
15 +import hmac
16 +import os
17 +import threading
18 +import time
19 +
20 +import requests
21 +
22 +CLIENT_ID = "sorti-ka"
23 +KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/")
24 +CACHE_TTL = 30 # secondes — liste des favoris
25 +TIMEOUT = 6 # secondes
26 +
27 +_cache: dict[str, tuple[float, list[dict]]] = {}
28 +_lock = threading.Lock()
29 +
30 +# champs d'item acceptés -> longueur maximale (troncature défensive)
31 +_FIELDS = {"item_id": 120, "title": 200, "subtitle": 200,
32 + "price_label": 60, "image_url": 500, "url": 500}
33 +
34 +
35 +def _sig(ka_id: str, ts: int) -> str | None:
36 + """Signature HMAC-SHA256 du hub : hex("sorti-ka.<ka_id>.<ts>")."""
37 + secret = os.environ.get("KA_SSO_SECRET")
38 + if not secret:
39 + return None
40 + return hmac.new(secret.encode(),
41 + f"{CLIENT_ID}.{ka_id}.{ts}".encode(),
42 + hashlib.sha256).hexdigest()
43 +
44 +
45 +def linked(ka_id: str | None) -> bool:
46 + """Vrai si le compte est relié au hub (KA-ID « ka-… » du groupe)."""
47 + return bool(ka_id) and str(ka_id).startswith("ka-")
48 +
49 +
50 +def clean_item(item: dict) -> dict:
51 + """Ne garde que les champs d'item connus, en chaînes tronquées."""
52 + return {k: str(item.get(k) or "")[:n]
53 + for k, n in _FIELDS.items() if item.get(k)}
54 +
55 +
56 +def hub_toggle(ka_id: str, action: str, item: dict) -> bool:
57 + """Pousse un ♥ (« add » / « remove ») au hub — SYNCHRONE, timeout 6 s :
58 + le hub est le magasin des favoris, l'échec doit remonter à l'appelant."""
59 + ts = int(time.time())
60 + sig = _sig(ka_id, ts)
61 + if not sig or not linked(ka_id) or action not in ("add", "remove"):
62 + return False
63 + try:
64 + r = requests.post(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT,
65 + json={"client_id": CLIENT_ID, "ka_id": ka_id,
66 + "ts": str(ts), "sig": sig,
67 + "action": action, "item": item})
68 + ok = r.status_code == 200
69 + except Exception:
70 + ok = False
71 + if ok:
72 + with _lock:
73 + _cache.pop(ka_id, None) # la prochaine lecture reflète le toggle
74 + return ok
75 +
76 +
77 +def hub_list(ka_id: str) -> list[dict] | None:
78 + """Favoris Sorti·Ka du membre, lus au hub (cache mémoire 30 s).
79 + [] = aucun favori ; None = hub injoignable (erreur, jamais mise en cache)."""
80 + if not linked(ka_id):
81 + return [] # compte legacy non relié au hub
82 + now = time.time()
83 + with _lock:
84 + hit = _cache.get(ka_id)
85 + if hit and now - hit[0] < CACHE_TTL:
86 + return hit[1]
87 + sig = _sig(ka_id, int(now))
88 + if not sig:
89 + return None
90 + try:
91 + r = requests.get(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT,
92 + params={"client_id": CLIENT_ID, "ka_id": ka_id,
93 + "ts": int(now), "sig": sig})
94 + if r.status_code != 200:
95 + return None
96 + favs = r.json().get("favorites") or []
97 + if not isinstance(favs, list):
98 + return None
99 + except Exception:
100 + return None
101 + favs = [f for f in favs if isinstance(f, dict)]
102 + with _lock:
103 + _cache[ka_id] = (now, favs)
104 + return favs
modified sortika/seo.py +22 −0
@@ -376,6 +376,28 @@ def contact_page() -> HTMLResponse:
376 376 return _render(head, ssr)
377 377
378 378
379 +@router.get("/favoris", include_in_schema=False)
380 +def favoris_page() -> HTMLResponse:
381 + """Page /favoris (SPA) — favoris ♥ « Mon univers Ka » du membre connecté.
382 + Page privée (session KA ID) : noindex, absente des sitemaps."""
383 + title = f"Mes favoris — {SITE_NAME} | Mon univers Ka"
384 + desc = ("Vos sorties et événements favoris, synchronisés avec votre "
385 + "compte KA ID sur toutes les plateformes du Groupe KA.")
386 + head = (
387 + f"<title>{_e(title)}</title>\n"
388 + f'<meta name="description" content="{_e(desc)}">\n'
389 + '<meta name="robots" content="noindex">\n'
390 + f'<meta property="og:site_name" content="{SITE_NAME}">\n'
391 + '<meta property="og:locale" content="fr_CA">\n'
392 + + OG_IMAGE_TAGS
393 + )
394 + ssr = ("<article><h1>Mes favoris — Mon univers Ka</h1>"
395 + f"<p>{_e(desc)}</p>"
396 + '<p><a href="/">Toutes les sorties et tous les événements du '
397 + "Québec</a></p></article>")
398 + return _render(head, ssr)
399 +
400 +
379 401 @router.get("/stats", include_in_schema=False)
380 402 def stats_page() -> HTMLResponse:
381 403 """Page /stats (SPA) — head SEO + contenu SSR pour les robots."""
modified sortika/web.py +35 −2
@@ -10,13 +10,13 @@ import json
10 10 from datetime import date
11 11 from pathlib import Path
12 12
13 −from fastapi import Body, FastAPI, HTTPException, Query
13 +from fastapi import Body, FastAPI, HTTPException, Query, Request
14 14 from fastapi.middleware.cors import CORSMiddleware
15 15 from fastapi.middleware.gzip import GZipMiddleware
16 16 from fastapi.responses import FileResponse, Response
17 17 from fastapi.staticfiles import StaticFiles
18 18
19 −from . import auth, db, kapdf, seo
19 +from . import auth, db, hubfav, kapdf, seo
20 20 from . import stats as kstats
21 21 from .normalize import CATEGORIES
22 22 from .regions import REGIONS
@@ -291,6 +291,39 @@ def sources() -> dict:
291 291 return reg
292 292
293 293
294 +# --- Favoris ♥ « Mon univers Ka » (magasin central : hub groupe-ka.com) -----
295 +@app.get("/api/favorites")
296 +def favorites(request: Request):
297 + """Favoris du membre connecté, lus au hub Groupe KA (aucun stockage local)."""
298 + user = auth.current_user(request)
299 + if not user:
300 + raise HTTPException(401, "Connexion KA ID requise")
301 + items = hubfav.hub_list(user.get("ka_id") or "")
302 + if items is None:
303 + raise HTTPException(502, "Hub Groupe KA injoignable — réessayez")
304 + return {"ids": [i["item_id"] for i in items if i.get("item_id")],
305 + "items": items}
306 +
307 +
308 +@app.post("/api/favorites/toggle")
309 +def toggle_favorite(request: Request, body: dict = Body(...)):
310 + """Ajoute (on=true) ou retire (on=false) un favori — poussé au hub
311 + Groupe KA de façon synchrone : le hub est la seule source de vérité."""
312 + user = auth.current_user(request)
313 + if not user:
314 + raise HTTPException(401, "Connexion KA ID requise")
315 + ka_id = user.get("ka_id") or ""
316 + if not hubfav.linked(ka_id):
317 + raise HTTPException(403, "Compte non relié au hub Groupe KA")
318 + on = bool(body.get("on"))
319 + item = hubfav.clean_item(body.get("item") or {})
320 + if not item.get("item_id"):
321 + raise HTTPException(422, "item.item_id requis")
322 + if not hubfav.hub_toggle(ka_id, "add" if on else "remove", item):
323 + raise HTTPException(502, "Hub Groupe KA injoignable — favori non enregistré")
324 + return {"ok": True, "on": on}
325 +
326 +
294 327 # --- comptes membres : « Se connecter avec KA » (KA ID) — sortika/auth.py ---
295 328 app.include_router(auth.router)
296 329
297 330