/* 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);
})();