/* Groupe KA — ka-id.js : SDK client KA ID v2 (personnalisation). * SOURCE CANONIQUE : ka-ui.git/kaid/ka-id.js — copié dans frontend/public/ * (ou public/) de chaque app par sync-kaid.sh. Corriger ICI puis redistribuer. * * Rôle : signaux navigateur que le serveur ne voit pas — position des clics * dans les résultats, temps passé sur une fiche (dwell), visites de retour — * plus les aides d'interface : KAID.hide() (« Pas pour moi »), * KAID.saveSearch() (recherche sauvegardée), badge « Recommandé pour vous ». * * Intégration (index.html, AVANT le bundle) : * * * * Vie privée : ne fait RIEN si l'utilisateur n'est pas connecté via KA ID * (vérifié auprès de /api/kaid/status) ; le hub respecte ensuite ses * réglages Historique/Personnalisation (voir groupe-ka.com/mon-ka). */ (function () { "use strict"; var CONF = window.KAID_CONF || {}; var API = CONF.api || "/api/kaid"; var connected = null; // null = inconnu, sinon bool var queue = []; var flushTimer = null; /* ---------- transport (lots de 10, sendBeacon à la fermeture) ---------- */ function post(events, useBeacon) { if (!events.length) return; var body = JSON.stringify({ events: events }); if (useBeacon && navigator.sendBeacon) { navigator.sendBeacon(API + "/events", new Blob([body], { type: "application/json" })); return; } fetch(API + "/events", { method: "POST", headers: { "Content-Type": "application/json" }, body: body, keepalive: true, credentials: "same-origin", }).catch(function () {}); } function flush(useBeacon) { if (!queue.length) return; var batch = queue.splice(0, queue.length); post(batch, useBeacon); } function track(type, data) { if (connected === false) return; var e = { type: type }; if (data) for (var k in data) if (data[k] != null) e[k] = data[k]; queue.push(e); if (queue.length >= 10) flush(false); else if (!flushTimer) flushTimer = setTimeout(function () { flushTimer = null; flush(false); }, 4000); } /* ---------- connexion ---------- */ function init(cb) { try { var cached = sessionStorage.getItem("kaid_status"); if (cached) { var c = JSON.parse(cached); if (Date.now() - c.t < 600000) { connected = c.on; cb(); return; } } } catch (err) { /* stockage indisponible */ } fetch(API + "/status", { credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (d) { connected = !!d.connected; try { sessionStorage.setItem("kaid_status", JSON.stringify({ on: connected, t: Date.now() })); } catch (err) { /* stockage indisponible */ } cb(); }) .catch(function () { connected = false; }); } /* ---------- clics sur les cartes résultat ---------- */ function uidFromHref(el) { if (!CONF.detail) return null; var href = el.getAttribute("href") || ""; var path = href.replace(/^https?:\/\/[^/]+/, "").split("?")[0]; var m = path.match(new RegExp(CONF.detail)); return m ? decodeURIComponent(m[1]) : null; } function watchClicks() { if (!CONF.card) return; document.addEventListener("click", function (ev) { var el = ev.target && ev.target.closest && ev.target.closest(CONF.card); if (!el) return; var uid = (CONF.uid ? CONF.uid(el) : null) || uidFromHref(el); if (!uid) return; var cards = document.querySelectorAll(CONF.card); var pos = Array.prototype.indexOf.call(cards, el); track("click", { entity_type: CONF.entity, entity_id: uid, position: pos >= 0 ? pos : null, }); }, true); } /* ---------- temps passé sur une fiche (dwell) ---------- */ var dwell = { uid: null, t0: 0 }; function detailUid() { if (!CONF.detail) return null; var m = location.pathname.match(new RegExp(CONF.detail)); return m ? decodeURIComponent(m[1]) : null; } function endDwell(useBeacon) { if (!dwell.uid) return; var ms = Date.now() - dwell.t0; if (ms >= 8000) track("detail_dwell", { entity_type: CONF.entity, entity_id: dwell.uid, dwell_ms: ms }); dwell.uid = null; if (useBeacon) flush(true); } function checkRoute() { var uid = detailUid(); if (uid !== dwell.uid) { endDwell(false); if (uid) dwell = { uid: uid, t0: Date.now() }; } updateUi(); } function watchRoutes() { ["pushState", "replaceState"].forEach(function (fn) { var orig = history[fn]; history[fn] = function () { var out = orig.apply(this, arguments); setTimeout(checkRoute, 50); return out; }; }); window.addEventListener("popstate", function () { setTimeout(checkRoute, 50); }); document.addEventListener("visibilitychange", function () { if (document.visibilityState === "hidden") { endDwell(true); } else checkRoute(); }); window.addEventListener("pagehide", function () { endDwell(true); }); checkRoute(); } /* ---------- interface : sauvegarder la recherche / pas pour moi ---------- */ var INK = "#141814"; var PAPER = "#f5f3ee"; function injectCss() { if (document.getElementById("kaid-ui-css")) return; var st = document.createElement("style"); st.id = "kaid-ui-css"; st.textContent = ".kaid-pill{position:fixed;left:14px;bottom:14px;z-index:560;display:inline-flex;" + "align-items:center;gap:8px;padding:9px 14px;border-radius:999px;border:1.5px solid " + INK + ";" + "background:" + PAPER + ";color:" + INK + ";font:600 12.5px/1 system-ui,sans-serif;" + "box-shadow:0 2px 10px rgba(20,24,20,.18);cursor:pointer;max-width:78vw}" + ".kaid-pill b{font-weight:700}" + ".kaid-pill .kaid-x{margin-left:2px;opacity:.55;font-weight:700;cursor:pointer}" + ".kaid-pill a{color:inherit;text-decoration:underline}" + "@media (max-width:768px){.kaid-pill{bottom:calc(84px + env(safe-area-inset-bottom))}}"; document.head.appendChild(st); } function removePill() { var el = document.getElementById("kaid-pill"); if (el) el.remove(); } function pillDismissed() { try { return Date.now() - Number(localStorage.getItem("kaid_pill_off") || 0) < 6048e5; } catch (err) { return false; } } function showSavePill() { if (pillDismissed() || document.getElementById("kaid-pill")) return; injectCss(); var el = document.createElement("div"); el.id = "kaid-pill"; el.className = "kaid-pill"; el.innerHTML = "☆ Sauvegarder cette recherche" + "✕"; el.querySelector(".kaid-x").addEventListener("click", function (ev) { ev.stopPropagation(); try { localStorage.setItem("kaid_pill_off", String(Date.now())); } catch (err) { /* privé */ } removePill(); }); el.addEventListener("click", function () { el.innerHTML = "…"; var filters = {}; new URLSearchParams(location.search).forEach(function (v, k) { if (v) filters[k] = v; }); window.KAID.saveSearch({ alert: true, filters: filters, query: filters.q || null }) .then(function (r) { el.innerHTML = r && r.ok !== false && !r.error ? "✓ Sauvegardée · alerte activée — Mon KA" : "Connexion KA ID requise"; setTimeout(removePill, 6000); }) .catch(function () { removePill(); }); }); document.body.appendChild(el); } function showHideLink(uid) { if (document.getElementById("kaid-pill")) return; injectCss(); var el = document.createElement("div"); el.id = "kaid-pill"; el.className = "kaid-pill"; el.innerHTML = "✕ Pas pour moi" + "✕"; el.querySelector(".kaid-x").addEventListener("click", function (ev) { ev.stopPropagation(); try { localStorage.setItem("kaid_pill_off", String(Date.now())); } catch (err) { /* privé */ } removePill(); }); el.addEventListener("click", function () { el.innerHTML = "…"; window.KAID.hide(uid).then(function () { el.innerHTML = "Masquée — elle ne réapparaîtra plus dans vos résultats."; setTimeout(removePill, 5000); }).catch(function () { removePill(); }); }); document.body.appendChild(el); } function updateUi() { if (!connected || CONF.ui === false) return; removePill(); if (pillDismissed()) return; var uid = detailUid(); if (uid) showHideLink(uid); else if (location.search.length > 1 && CONF.savePill !== false) showSavePill(); } /* ---------- visite de retour ---------- */ function returnVisit() { try { var last = Number(localStorage.getItem("kaid_last") || 0); var now = Date.now(); if (last && now - last > 72e6) track("return_visit", {}); // > 20 h localStorage.setItem("kaid_last", String(now)); } catch (err) { /* stockage indisponible */ } } /* ---------- API publique ---------- */ window.KAID = { track: track, connected: function () { return connected; }, /** « Pas pour moi » — masque l'annonce partout et nourrit le signal négatif. */ hide: function (itemId, features) { return fetch(API + "/hide", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "same-origin", body: JSON.stringify({ item_id: itemId, on: true, features: features || null }), }).then(function (r) { return r.json(); }); }, /** Sauvegarde la recherche courante (par défaut : URL + titre du document). */ saveSearch: function (opts) { opts = opts || {}; return fetch(API + "/saved-searches", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "same-origin", body: JSON.stringify({ action: "add", label: opts.label || document.title.split("|")[0].split("—")[0].trim(), query: opts.query || null, filters: opts.filters || null, url: opts.url || (location.origin + location.pathname + location.search), alert: !!opts.alert, frequency: opts.frequency || null, }), }).then(function (r) { return r.json(); }); }, external: function (itemId, features) { track("external_click", { entity_type: CONF.entity, entity_id: itemId, features: features }); flush(true); }, }; /* ---------- démarrage ---------- */ function start() { if (!connected) return; watchClicks(); watchRoutes(); returnVisit(); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", function () { init(start); }); else init(start); })();