From d4fef9c1bf2f067d95285e18af16849404df9f4b Mon Sep 17 00:00:00 2001 From: bruno Date: Wed, 28 Oct 2026 14:44:30 -0400 Subject: [PATCH] recherche: onglet Console Spark sur GET /api/v1/recherche (alignement) --- static/js/recherche.js | 159 +++++++++++++++++++++++++++++++++-------- 1 file changed, 128 insertions(+), 31 deletions(-) diff --git a/static/js/recherche.js b/static/js/recherche.js index 6a6f442..acd34c3 100644 --- a/static/js/recherche.js +++ b/static/js/recherche.js @@ -46,23 +46,35 @@ * -> { groups: [{ full_name, rows: [{ issue, column }] }] } * Lien profond : {base}board/ (la carte n'a pas d'URL propre * adressable — même limite que la page /search native de Kanbante). - * - Console Spark : AUCUN endpoint de recherche mergé au moment de - * cette livraison (agent-commun : seule la PR #12 est ouverte, autre - * programme — ce dépôt n'est pas touché ici). Le code tente quand - * même l'appel conventionnel GET {base-console}/api/search?q= (même - * convention que les autres hôtes ; chemin À CONFIRMER avec le - * travailleur console dès que son endpoint atterrit) et, en cas - * d'échec, de HTTP non-OK ou de format non reconnu, affiche - * honnêtement « Console Spark : recherche indisponible pour - * l'instant » — jamais de faux résultats, jamais de faux 0. La base - * console vient, dans l'ordre, de data-base-console rendu par l'hôte, - * de window.DRACO_URLS.agent (ObsiGate) / window.DRACO_AGENT_URL - * (TikTak), de la dérivation -dev si l'hôte courant est -dev, du - * href d'un onglet de barre data-app="console"/"spark" s'il existe, - * sinon du défaut prod agent.dracodev.net. L'onglet console n'est - * interrogé que lorsqu'il est affiché (ou quand une recherche a lieu - * pendant qu'il est affiché) : « Tout » ne le contient pas, pour ne - * pas polluer la vue groupée d'un message d'indisponibilité. + * - Console Spark : GET {base-console}/api/v1/recherche?q=&limite= + * (agent-commun, authentifié par le cookie SSO draco_session) -> + * { q, domain, limite_par_type, resultats[], par_type, + * non_cherchables[] } ; chaque résultat porte type, id, titre, + * extrait, statut, domaine, section, objet et lien (relatif à + * l'origine console). q < 2 caractères -> 422. Types cherchables : + * discussion, mission, approbation, livrable, activite, + * planification, objectif, identite (aucun secret). Liens profonds + * (préfixés par la base console) : discussion + * /?section=conversation&disc=, mission + * /?section=missions&objet=, approbation + * /?section=approbations&objet=, objectif + * /?section=objectifs&objet=, activite /?activite= ; + * livrable /?section=livrables, planification /?section=planifie + * et identite /?section=identites ouvrent la vue sans sélection + * d'objet. L'onglet appelle avec limite=20 (plafond par type) car + * il affiche la liste complète, comme les onglets d'applications. + * En cas d'échec, de HTTP non-OK ou de format non reconnu, la + * section affiche honnêtement « Console Spark : recherche + * indisponible pour l'instant » — jamais de faux résultats, + * jamais de faux 0. La base console vient, dans l'ordre, de + * data-base-console rendu par l'hôte, de window.DRACO_URLS.agent + * (ObsiGate) / window.DRACO_AGENT_URL (TikTak), de la dérivation + * -dev si l'hôte courant est -dev, du href d'un onglet de barre + * data-app="console"/"spark"/"agent" s'il existe, sinon du défaut + * prod agent.dracodev.net. L'onglet console n'est interrogé que + * lorsqu'il est affiché (ou quand une recherche a lieu pendant + * qu'il est affiché) : « Tout » ne le contient pas, pour ne pas + * polluer la vue groupée d'un message d'indisponibilité. * * Résolution des bases sœurs (même ordre que la barre v0.1.6) : * 1. data-base-* rendus par l'hôte (son propre résolveur : explicite > @@ -76,12 +88,14 @@ * credentials:"include". Le navigateur envoie alors les cookies de * chaque origine, dont le cookie SSO partagé draco_session * (Domain=.dracodev.net), accepté par les 4 API de recherche - * (ObsiGate et TikTak nativement, Flowdeck et Kanbante via #68). - * Chaque sœur n'autorise en CORS que les origines sœurs - * (GET sur l'endpoint de recherche uniquement, pas de "*"). + * (ObsiGate et TikTak nativement, Flowdeck et Kanbante via #68) + * et par GET /api/v1/recherche de la console (identity() accepte + * draco_session). Chaque sœur n'autorise en CORS que les origines + * sœurs (GET sur l'endpoint de recherche uniquement, pas de "*") ; + * côté console, un middleware borné à /api/v1/recherche autorise + * de même les 8 origines hôtes prod ET dev, avec credentials. * Une sœur injoignable / non autorisée ne casse pas les autres : * sa section affiche l'erreur + un lien vers sa recherche native. - * Le correctif CORS lui-même est chez un autre travailleur. */ (function (global) { "use strict"; @@ -104,7 +118,8 @@ }; var PROD_AGENT = "https://agent.dracodev.net/"; var DEV_AGENT = "https://agent-dev.dracodev.net/"; - var CONSOLE_SEARCH_PATH = "api/search"; + var CONSOLE_SEARCH_PATH = "api/v1/recherche"; + var CONSOLE_SEARCH_LIMIT = 20; var APPS = [ { key: "obsigate", label: "ObsiGate" }, { key: "flowdeck", label: "Flowdeck" }, @@ -112,6 +127,24 @@ { key: "kanbante", label: "Kanbante" }, ]; var CONSOLE_APP = { key: "console", label: "Console Spark" }; + var CONSOLE_TYPE_LABELS = { + discussion: "Discussion", + mission: "Mission", + approbation: "Approbation", + livrable: "Livrable", + activite: "Activité", + planification: "Planification", + objectif: "Objectif", + identite: "Identité", + }; + /* Lien profond console (relatif à l'origine console) quand le + * résultat n'en porte pas : même règle que _lien_recherche + * d'agent-commun (section + objet, activite en section=null). */ + var CONSOLE_VIEW_LINKS = { + livrable: "/?section=livrables", + planification: "/?section=planifie", + identite: "/?section=identites", + }; var TABS = ["all", "obsigate", "flowdeck", "tiktak", "kanbante", "console"]; var COLUMN_LABELS = { backlog: "Backlog", @@ -182,7 +215,8 @@ if (!u && isDevHost(opts.host)) u = DEV_AGENT; if (!u) u = cleanUrl( - opts.tabHrefs && (opts.tabHrefs.console || opts.tabHrefs.spark) + opts.tabHrefs && + (opts.tabHrefs.console || opts.tabHrefs.spark || opts.tabHrefs.agent) ); if (!u) u = PROD_AGENT; return withSlash(u); @@ -336,19 +370,71 @@ return []; } - /* Normalisation console, défensive : accepte results/discussions/ - * items. Un élément sans URL exploitable (http(s), /relatif, #ancre) - * garde url=null et sera rendu en , pas en faux lien profond. - * Retourne { recognized, items } : recognized=false -> l'appelant - * affiche « indisponible », jamais un vide mensonger. */ + /* Normalisation console : format réel GET /api/v1/recherche, + * { resultats: [{ type, id, titre, extrait, statut, domaine, + * section, objet, lien }] }. Le lien relatif est préfixé par la + * base console ; sans lien, il est reconstruit depuis type/id + * (même règle que la console) ; sans lien reconstructible, + * url=null et le résultat est rendu en , pas en faux lien. + * Les anciennes formes tolérées (results/discussions/items, avec + * url/href/link) restent acceptées en repli, pour ne pas casser + * un déploiement console antérieur. Retourne { recognized, + * items } : recognized=false -> l'appelant affiche + * « indisponible », jamais un vide mensonger. */ + function consoleLinkFor(r) { + var lien = r.lien || ""; + if (lien) return lien; + var id = r.id != null ? String(r.id) : ""; + if (r.type === "discussion") + return "/?section=conversation&disc=" + encodeURIComponent(id); + if (r.type === "mission") + return "/?section=missions&objet=" + encodeURIComponent(id); + if (r.type === "approbation") + return "/?section=approbations&objet=" + encodeURIComponent(id); + if (r.type === "objectif") + return "/?section=objectifs&objet=" + encodeURIComponent(id); + if (r.type === "activite") return "/?activite=" + encodeURIComponent(id); + if (CONSOLE_VIEW_LINKS[r.type]) return CONSOLE_VIEW_LINKS[r.type]; + if (r.section) + return ( + "/?section=" + + encodeURIComponent(r.section) + + (r.objet != null + ? "&objet=" + encodeURIComponent(String(r.objet)) + : "") + ); + return ""; + } + function normalizeConsole(data, base) { data = data || {}; + if (Array.isArray(data.resultats)) { + var items = data.resultats.map(function (r) { + r = r || {}; + var rel = consoleLinkFor(r); + var url = null; + if (/^https?:\/\//.test(rel)) url = rel; + else if (rel.charAt(0) === "/" || rel.charAt(0) === "#") + url = base + String(rel).replace(/^\//, ""); + var label = CONSOLE_TYPE_LABELS[r.type] || r.type || "Console"; + var context = label; + if (r.statut) context += " · " + r.statut; + if (r.domaine) context += " · " + r.domaine; + return { + title: r.titre || r.title || "Sans titre", + snippet: snippetOf(r.extrait || r.snippet || ""), + context: context, + url: url, + }; + }); + return { recognized: true, items: items }; + } var list = null; if (Array.isArray(data.results)) list = data.results; else if (Array.isArray(data.discussions)) list = data.discussions; else if (Array.isArray(data.items)) list = data.items; if (!list) return { recognized: false, items: [] }; - var items = list.map(function (r) { + var legacy = list.map(function (r) { r = r || {}; var raw = r.url || r.href || r.link || ""; var url = null; @@ -361,7 +447,7 @@ url: url, }; }); - return { recognized: true, items: items }; + return { recognized: true, items: legacy }; } /* Recherche native de l'app (repli quand la section est en erreur). */ @@ -408,6 +494,9 @@ PROD_AGENT: PROD_AGENT, DEV_AGENT: DEV_AGENT, CONSOLE_SEARCH_PATH: CONSOLE_SEARCH_PATH, + CONSOLE_SEARCH_LIMIT: CONSOLE_SEARCH_LIMIT, + CONSOLE_TYPE_LABELS: CONSOLE_TYPE_LABELS, + consoleLinkFor: consoleLinkFor, PER_APP_LIMIT: PER_APP_LIMIT, FULL_LIMIT: FULL_LIMIT, }; @@ -671,12 +760,20 @@ }; renderSection(sec, states.console, true); renderTabs(); - var url = consoleBase + CONSOLE_SEARCH_PATH + "?q=" + encodeURIComponent(q); + var url = + consoleBase + + CONSOLE_SEARCH_PATH + + "?q=" + + encodeURIComponent(q) + + "&limite=" + + CONSOLE_SEARCH_LIMIT; fetchSection("console", url, function (err, data) { if (myRun !== runId) return; if (err) { var msg; if (err.name === "AbortError") msg = "délai dépassé (8 s)"; + else if (err.status === 422) + msg = "au moins 2 caractères requis"; else if (err.status) msg = "HTTP " + err.status; else msg = "injoignable (CORS ou réseau)"; states.console = {