1/* Groupe KA — ka-id.js : SDK client KA ID v2 (personnalisation).2 * SOURCE CANONIQUE : ka-ui.git/kaid/ka-id.js — copié dans frontend/public/3 * (ou public/) de chaque app par sync-kaid.sh. Corriger ICI puis redistribuer.4 *5 * Rôle : signaux navigateur que le serveur ne voit pas — position des clics6 * dans les résultats, temps passé sur une fiche (dwell), visites de retour —7 * plus les aides d'interface : KAID.hide() (« Pas pour moi »),8 * KAID.saveSearch() (recherche sauvegardée), badge « Recommandé pour vous ».9 *10 * Intégration (index.html, AVANT le bundle) :11 * <script>12 * window.KAID_CONF = {13 * card: "a[href^='/emploi/']", // sélecteur des cartes résultat14 * detail: "^/emploi/(.+)$", // regex fiche → capture l'uid15 * entity: "job", // entity_type des événements16 * };17 * </script>18 * <script src="/ka-id.js" defer></script>19 *20 * Vie privée : ne fait RIEN si l'utilisateur n'est pas connecté via KA ID21 * (vérifié auprès de /api/kaid/status) ; le hub respecte ensuite ses22 * réglages Historique/Personnalisation (voir groupe-ka.com/mon-ka).23 */24(function () {25 "use strict";26 var CONF = window.KAID_CONF || {};27 var API = CONF.api || "/api/kaid";28 var connected = null; // null = inconnu, sinon bool29 var queue = [];30 var flushTimer = null;3132 /* ---------- transport (lots de 10, sendBeacon à la fermeture) ---------- */3334 function post(events, useBeacon) {35 if (!events.length) return;36 var body = JSON.stringify({ events: events });37 if (useBeacon && navigator.sendBeacon) {38 navigator.sendBeacon(API + "/events",39 new Blob([body], { type: "application/json" }));40 return;41 }42 fetch(API + "/events", {43 method: "POST",44 headers: { "Content-Type": "application/json" },45 body: body,46 keepalive: true,47 credentials: "same-origin",48 }).catch(function () {});49 }5051 function flush(useBeacon) {52 if (!queue.length) return;53 var batch = queue.splice(0, queue.length);54 post(batch, useBeacon);55 }5657 function track(type, data) {58 if (connected === false) return;59 var e = { type: type };60 if (data) for (var k in data) if (data[k] != null) e[k] = data[k];61 queue.push(e);62 if (queue.length >= 10) flush(false);63 else if (!flushTimer)64 flushTimer = setTimeout(function () { flushTimer = null; flush(false); }, 4000);65 }6667 /* ---------- connexion ---------- */6869 function init(cb) {70 try {71 var cached = sessionStorage.getItem("kaid_status");72 if (cached) {73 var c = JSON.parse(cached);74 if (Date.now() - c.t < 600000) { connected = c.on; cb(); return; }75 }76 } catch (err) { /* stockage indisponible */ }77 fetch(API + "/status", { credentials: "same-origin" })78 .then(function (r) { return r.json(); })79 .then(function (d) {80 connected = !!d.connected;81 try {82 sessionStorage.setItem("kaid_status",83 JSON.stringify({ on: connected, t: Date.now() }));84 } catch (err) { /* stockage indisponible */ }85 cb();86 })87 .catch(function () { connected = false; });88 }8990 /* ---------- clics sur les cartes résultat ---------- */9192 function uidFromHref(el) {93 if (!CONF.detail) return null;94 var href = el.getAttribute("href") || "";95 var path = href.replace(/^https?:\/\/[^/]+/, "").split("?")[0];96 var m = path.match(new RegExp(CONF.detail));97 return m ? decodeURIComponent(m[1]) : null;98 }99100 function watchClicks() {101 if (!CONF.card) return;102 document.addEventListener("click", function (ev) {103 var el = ev.target && ev.target.closest && ev.target.closest(CONF.card);104 if (!el) return;105 var uid = (CONF.uid ? CONF.uid(el) : null) || uidFromHref(el);106 if (!uid) return;107 var cards = document.querySelectorAll(CONF.card);108 var pos = Array.prototype.indexOf.call(cards, el);109 track("click", {110 entity_type: CONF.entity, entity_id: uid,111 position: pos >= 0 ? pos : null,112 });113 }, true);114 }115116 /* ---------- temps passé sur une fiche (dwell) ---------- */117118 var dwell = { uid: null, t0: 0 };119120 function detailUid() {121 if (!CONF.detail) return null;122 var m = location.pathname.match(new RegExp(CONF.detail));123 return m ? decodeURIComponent(m[1]) : null;124 }125126 function endDwell(useBeacon) {127 if (!dwell.uid) return;128 var ms = Date.now() - dwell.t0;129 if (ms >= 8000)130 track("detail_dwell",131 { entity_type: CONF.entity, entity_id: dwell.uid, dwell_ms: ms });132 dwell.uid = null;133 if (useBeacon) flush(true);134 }135136 function checkRoute() {137 var uid = detailUid();138 if (uid === dwell.uid) return;139 endDwell(false);140 if (uid) dwell = { uid: uid, t0: Date.now() };141 }142143 function watchRoutes() {144 ["pushState", "replaceState"].forEach(function (fn) {145 var orig = history[fn];146 history[fn] = function () {147 var out = orig.apply(this, arguments);148 setTimeout(checkRoute, 50);149 return out;150 };151 });152 window.addEventListener("popstate", function () { setTimeout(checkRoute, 50); });153 document.addEventListener("visibilitychange", function () {154 if (document.visibilityState === "hidden") { endDwell(true); }155 else checkRoute();156 });157 window.addEventListener("pagehide", function () { endDwell(true); });158 checkRoute();159 }160161 /* ---------- visite de retour ---------- */162163 function returnVisit() {164 try {165 var last = Number(localStorage.getItem("kaid_last") || 0);166 var now = Date.now();167 if (last && now - last > 72e6) track("return_visit", {}); // > 20 h168 localStorage.setItem("kaid_last", String(now));169 } catch (err) { /* stockage indisponible */ }170 }171172 /* ---------- API publique ---------- */173174 window.KAID = {175 track: track,176 connected: function () { return connected; },177 /** « Pas pour moi » — masque l'annonce partout et nourrit le signal négatif. */178 hide: function (itemId, features) {179 return fetch(API + "/hide", {180 method: "POST",181 headers: { "Content-Type": "application/json" },182 credentials: "same-origin",183 body: JSON.stringify({ item_id: itemId, on: true, features: features || null }),184 }).then(function (r) { return r.json(); });185 },186 /** Sauvegarde la recherche courante (par défaut : URL + titre du document). */187 saveSearch: function (opts) {188 opts = opts || {};189 return fetch(API + "/saved-searches", {190 method: "POST",191 headers: { "Content-Type": "application/json" },192 credentials: "same-origin",193 body: JSON.stringify({194 action: "add",195 label: opts.label || document.title.split("|")[0].split("—")[0].trim(),196 query: opts.query || null,197 filters: opts.filters || null,198 url: opts.url || (location.origin + location.pathname + location.search),199 alert: !!opts.alert,200 frequency: opts.frequency || null,201 }),202 }).then(function (r) { return r.json(); });203 },204 external: function (itemId, features) {205 track("external_click",206 { entity_type: CONF.entity, entity_id: itemId, features: features });207 flush(true);208 },209 };210211 /* ---------- démarrage ---------- */212213 function start() {214 if (!connected) return;215 watchClicks();216 watchRoutes();217 returnVisit();218 }219220 if (document.readyState === "loading")221 document.addEventListener("DOMContentLoaded", function () { init(start); });222 else init(start);223})();224