Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce2ac08dd3 | ||
|
|
b64efb8674 | ||
|
|
2841847188 | ||
|
|
8b84306f61 | ||
|
|
ba995ebe1c | ||
|
|
2b1f7808fd | ||
|
|
8827b3dec8 |
@@ -6,6 +6,9 @@
|
|||||||
v7.82.0 (dev/prod) : les href des onglets ne sont plus figés —
|
v7.82.0 (dev/prod) : les href des onglets ne sont plus figés —
|
||||||
l'hôte les injecte depuis sa config (draco_urls(), noms officiels
|
l'hôte les injecte depuis sa config (draco_urls(), noms officiels
|
||||||
par défaut, noms en -dev déclarés dans le .env de l'environnement). #}
|
par défaut, noms en -dev déclarés dans le .env de l'environnement). #}
|
||||||
|
Chantier 5 (fusion #69/#71, sans bump) : 5e onglet Console Spark
|
||||||
|
(data-app="agent", draco_urls().agent), après Kanbante ; l'entrée
|
||||||
|
« Console Spark » du menu de la marque est conservée.
|
||||||
<link rel="stylesheet" href="/static/css/draco-alertes.css?v={{ asset_version }}">
|
<link rel="stylesheet" href="/static/css/draco-alertes.css?v={{ asset_version }}">
|
||||||
<header class="draco-bar" data-current="flowdeck">
|
<header class="draco-bar" data-current="flowdeck">
|
||||||
<div class="draco-bar-zone draco-bar-gauche">
|
<div class="draco-bar-zone draco-bar-gauche">
|
||||||
@@ -32,6 +35,10 @@
|
|||||||
<span class="draco-apptab-txt"><strong>Kanbante</strong><small>Kanban</small></span>
|
<span class="draco-apptab-txt"><strong>Kanbante</strong><small>Kanban</small></span>
|
||||||
<span class="draco-badge" data-badge="kanbante" hidden>0</span>
|
<span class="draco-badge" data-badge="kanbante" hidden>0</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="draco-apptab" data-app="agent" href="{{ draco_urls().agent }}/">
|
||||||
|
<span class="draco-apptab-ico" style="--app:#c2437e">✦</span>
|
||||||
|
<span class="draco-apptab-txt"><strong>Console Spark</strong><small>Agent commun</small></span>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="draco-bar-zone draco-bar-centre">
|
<div class="draco-bar-zone draco-bar-centre">
|
||||||
|
|||||||
@@ -37,3 +37,22 @@
|
|||||||
.draco-alertes-meta { font-size: 11.5px; color: var(--text-tertiary, #777); margin-top: 4px; }
|
.draco-alertes-meta { font-size: 11.5px; color: var(--text-tertiary, #777); margin-top: 4px; }
|
||||||
.draco-alertes-empty { font-size: 13px; color: var(--text-dim, #999); padding: 6px 0; }
|
.draco-alertes-empty { font-size: 13px; color: var(--text-dim, #999); padding: 6px 0; }
|
||||||
.draco-alertes-empty a { color: inherit; }
|
.draco-alertes-empty a { color: inherit; }
|
||||||
|
/* Chantier 5 — onglets + filtre (mêmes variables d'hôte, mêmes replis). */
|
||||||
|
.draco-alertes-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin: 2px 0 12px; }
|
||||||
|
.draco-alertes-tab {
|
||||||
|
border: 1px solid var(--border, #333); background: transparent;
|
||||||
|
color: inherit; border-radius: 999px; padding: 5px 11px;
|
||||||
|
cursor: pointer; font-size: 13px; line-height: 1.3;
|
||||||
|
}
|
||||||
|
.draco-alertes-tab[aria-selected="true"] { background: #e8762c; border-color: #e8762c; color: #fff; }
|
||||||
|
.draco-alertes-tab:focus-visible { outline: 2px solid #e8762c; outline-offset: 2px; }
|
||||||
|
.draco-alertes-filtre { display: flex; gap: 8px; align-items: center; margin: 0 0 4px; }
|
||||||
|
.draco-alertes-filtre label { font-size: 13px; color: var(--text-dim, #999); }
|
||||||
|
.draco-alertes-filtre input {
|
||||||
|
flex: 1; min-width: 0; background: transparent; color: inherit;
|
||||||
|
border: 1px solid var(--border, #333); border-radius: 8px; padding: 6px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.draco-alertes-compte { font-size: 12.5px; color: var(--text-dim, #999); margin: 4px 0 10px; min-height: 1em; }
|
||||||
|
.draco-alertes-compte:empty { display: none; }
|
||||||
|
.draco-alertes-item.is-error { border-color: #c2437e; }
|
||||||
|
|||||||
@@ -192,5 +192,9 @@
|
|||||||
.draco-bar[data-current="obsigate"] .draco-apptab[data-app="obsigate"],
|
.draco-bar[data-current="obsigate"] .draco-apptab[data-app="obsigate"],
|
||||||
.draco-bar[data-current="flowdeck"] .draco-apptab[data-app="flowdeck"],
|
.draco-bar[data-current="flowdeck"] .draco-apptab[data-app="flowdeck"],
|
||||||
.draco-bar[data-current="tiktak"] .draco-apptab[data-app="tiktak"],
|
.draco-bar[data-current="tiktak"] .draco-apptab[data-app="tiktak"],
|
||||||
.draco-bar[data-current="kanbante"] .draco-apptab[data-app="kanbante"] { display: flex; }
|
.draco-bar[data-current="kanbante"] .draco-apptab[data-app="kanbante"],
|
||||||
|
/* Chantier 5 : sur la console elle-même (data-current="agent",
|
||||||
|
programme console #71), c'est l'onglet Console Spark qui reste
|
||||||
|
visible en mobile — les 4 hôtes gardent leur propre courant. */
|
||||||
|
.draco-bar[data-current="agent"] .draco-apptab[data-app="agent"] { display: flex; }
|
||||||
}
|
}
|
||||||
|
|||||||
+431
-109
@@ -1,14 +1,25 @@
|
|||||||
/* Alertes unifiées dracodev — Fusion #69 (chantier 2).
|
/* Alertes unifiées dracodev — Fusion #69 (chantier 2) + chantier 5
|
||||||
|
* (onglets + composant partagé + onglet Console Spark).
|
||||||
|
*
|
||||||
|
* SOURCE CANONIQUE : Projets/dracodev-commons, ui/alertes/. Les copies
|
||||||
|
* dans les 4 hôtes (TikTak, Kanbante, Flowdeck, ObsiGate) sont des
|
||||||
|
* vendoring byte-identiques de ce fichier — ne pas les modifier sur
|
||||||
|
* place, toute évolution passe par commons puis re-vendoring.
|
||||||
*
|
*
|
||||||
* Agrégation À LA DEMANDE, pas une fédération temps réel : à chaque
|
* Agrégation À LA DEMANDE, pas une fédération temps réel : à chaque
|
||||||
* ouverture du panneau (et au chargement, pour la pastille), le module
|
* ouverture du panneau (et au chargement, pour la pastille), le module
|
||||||
* interroge les API de notifications réellement exposées par chaque
|
* interroge les API de notifications réellement exposées par chaque
|
||||||
* application, avec les identifiants du navigateur (credentials:
|
* application, avec les identifiants du navigateur (credentials:
|
||||||
* "include"). Aucune alerte n'est inventée : une source injoignable
|
* "include"). Aucune alerte n'est inventée : une source injoignable
|
||||||
* ou non authentifiée affiche son état réel (« Connexion requise »)
|
* ou non authentifiée affiche son état réel par section (une section
|
||||||
* avec un lien profond vers sa page native, jamais du vide déguisé.
|
* en échec n'empêche jamais de consulter les autres), jamais du vide
|
||||||
|
* déguisé. En attendant le correctif CORS des allowlists (autre
|
||||||
|
* chantier), une section inter-origines peut afficher cet état
|
||||||
|
* d'erreur : c'est un état d'erreur transitoire, pas un comportement
|
||||||
|
* normal — aucun état « Injoignable » systématique n'est présenté
|
||||||
|
* comme normal.
|
||||||
*
|
*
|
||||||
* Sources réelles (audit #69, 2026-10-28) :
|
* Sources réelles (audit #69, 2026-10-28, inchangé) :
|
||||||
* - Flowdeck : GET {base}/api/notifications?limit=50
|
* - Flowdeck : GET {base}/api/notifications?limit=50
|
||||||
* -> { notifications: [{id, ntype, title, message, url, is_read,
|
* -> { notifications: [{id, ntype, title, message, url, is_read,
|
||||||
* created_at}], unread } ; active = is_read = 0 ; lien = base + url.
|
* created_at}], unread } ; active = is_read = 0 ; lien = base + url.
|
||||||
@@ -23,20 +34,55 @@
|
|||||||
* est compté comme actif ; lien = base + "/board/" + repo.
|
* est compté comme actif ; lien = base + "/board/" + repo.
|
||||||
* - ObsiGate : AUCUNE notification entrante consultable. Ses
|
* - ObsiGate : AUCUNE notification entrante consultable. Ses
|
||||||
* /api/notify/* sont des canaux d'envoi sortants (admin, secrets
|
* /api/notify/* sont des canaux d'envoi sortants (admin, secrets
|
||||||
* masqués), pas un journal lisible par l'utilisateur. La section
|
* masqués), pas un journal lisible par l'utilisateur. L'onglet
|
||||||
* ObsiGate le dit et renvoie vers l'accueil d'ObsiGate.
|
* ObsiGate le dit et renvoie vers l'accueil d'ObsiGate.
|
||||||
|
* - Console Spark : AUCUN endpoint d'alertes console n'est publié
|
||||||
|
* sur agent-commun main au moment de ce chantier (autre chantier
|
||||||
|
* en cours). L'onglet Console Spark affiche donc un état vide
|
||||||
|
* HONNÊTE (« source indisponible », lien vers la console) et ne
|
||||||
|
* fabrique aucune alerte. Branchement prévu, sans deviner d'URL :
|
||||||
|
* poser window.DRACO_ALERTES_CONSOLE_URL (ou
|
||||||
|
* DRACO_BAR_CONFIG.consoleAlertsUrl) vers l'endpoint livré par
|
||||||
|
* l'autre chantier ; forme attendue : tableau, ou
|
||||||
|
* {alerts:[...]} / {items:[...]}, chaque alerte portant title,
|
||||||
|
* detail|message|context, date|created_at, active (booléen) ou
|
||||||
|
* status/state (pause budget, accès requis, en attente, fermée…),
|
||||||
|
* url|link. Les missions en pause budget / accès requis et les
|
||||||
|
* approbations en attente y apparaîtront avec leurs états réels.
|
||||||
*
|
*
|
||||||
* Résolution des bases (même ordre que barre-apps v0.1.6) :
|
* Résolution des bases (même ordre que barre-apps v0.1.6) :
|
||||||
* 1. window.DRACO_BAR.urls déjà résolue par la barre, sinon
|
* 1. window.DRACO_BAR.urls déjà résolue par la barre, sinon
|
||||||
* window.DRACO_BAR_CONFIG.urls (config explicite de l'hôte) ;
|
* window.DRACO_BAR_CONFIG.urls (config explicite de l'hôte) ;
|
||||||
* 2. dérivation -dev par l'hôte courant (hôte -dev dracodev.net) ;
|
* 2. dérivation -dev par l'hôte courant (hôte -dev dracodev.net) ;
|
||||||
* 3. href rendu de l'onglet .draco-apptab[data-app] ;
|
* 3. href rendu de l'onglet .draco-apptab[data-app] (l'onglet
|
||||||
|
* « Console Spark » ajouté par le chantier 5 porte data-app
|
||||||
|
* "agent", donc la base agent suit le même ordre) ;
|
||||||
* 4. défaut officiel (production).
|
* 4. défaut officiel (production).
|
||||||
* ObsiGate n'a pas de .draco-apptab dans tous les cas : l'étape 3 y
|
* ObsiGate n'a pas de .draco-apptab dans tous les cas : l'étape 3 y
|
||||||
* lit aussi window.DRACO_URLS (injecté par son backend).
|
* lit aussi window.DRACO_URLS (injecté par son backend, clé "agent"
|
||||||
|
* incluse).
|
||||||
* Aucun nom en -dev n'est un défaut : la dérivée -dev ne s'applique
|
* Aucun nom en -dev n'est un défaut : la dérivée -dev ne s'applique
|
||||||
* que sur un hôte -dev, exactement comme la barre.
|
* que sur un hôte -dev, exactement comme la barre.
|
||||||
*
|
*
|
||||||
|
* Présentation (identique partout, imposée par ce seul fichier) :
|
||||||
|
* champs par alerte = application, titre, date, état, lien profond
|
||||||
|
* (environnement -dev respecté via la résolution ci-dessus) ;
|
||||||
|
* ordre = date décroissante dans chaque onglet (sans date en
|
||||||
|
* dernier) ; format de date = toLocaleString("fr-FR"), « sans date »
|
||||||
|
* si absente ou illisible ; libellés d'état affichés = « Active »,
|
||||||
|
* « Lue », ou le libellé d'événement Kanbante (ex. « Fermée »),
|
||||||
|
* ou l'état réel console quand la source existe. Le type brut
|
||||||
|
* Kanbante (« moved »…) n'est plus affiché : il dupliquait le
|
||||||
|
* libellé d'état en anglais.
|
||||||
|
* Onglets : Tout, ObsiGate, Flowdeck, TikTak, Kanbante,
|
||||||
|
* Console Spark, Historique — dans cet ordre. Tout et chaque onglet
|
||||||
|
* par application ne montrent QUE les alertes actives ; Historique
|
||||||
|
* ne montre QUE les alertes fermées (lues / événements Kanbante /
|
||||||
|
* console fermées). Le filtre texte (titre + contexte) s'applique
|
||||||
|
* APRÈS le périmètre de l'onglet courant : il ne mélange jamais
|
||||||
|
* actifs et historique. L'onglet courant est dans l'URL
|
||||||
|
* (#alertes=<onglet>, partageable, retour arrière via pushState).
|
||||||
|
*
|
||||||
* Le fichier ne touche à AUCUN fichier ui/barre-apps.* : il consomme
|
* Le fichier ne touche à AUCUN fichier ui/barre-apps.* : il consomme
|
||||||
* seulement l'action "alertes" relayée par l'hôte et DRACO_BAR.setCounts
|
* seulement l'action "alertes" relayée par l'hôte et DRACO_BAR.setCounts
|
||||||
* pour la pastille (repli : mise à jour directe du badge).
|
* pour la pastille (repli : mise à jour directe du badge).
|
||||||
@@ -50,23 +96,39 @@
|
|||||||
obsigate: "https://og.dracodev.net/",
|
obsigate: "https://og.dracodev.net/",
|
||||||
flowdeck: "https://flowdeck.dracodev.net/",
|
flowdeck: "https://flowdeck.dracodev.net/",
|
||||||
tiktak: "https://tiktak.dracodev.net/",
|
tiktak: "https://tiktak.dracodev.net/",
|
||||||
kanbante: "https://kanbante.dracodev.net/"
|
kanbante: "https://kanbante.dracodev.net/",
|
||||||
|
agent: "https://agent.dracodev.net/"
|
||||||
};
|
};
|
||||||
var DEV_URLS = {
|
var DEV_URLS = {
|
||||||
obsigate: "https://og-dev.dracodev.net/",
|
obsigate: "https://og-dev.dracodev.net/",
|
||||||
flowdeck: "https://flowdeck-dev.dracodev.net/",
|
flowdeck: "https://flowdeck-dev.dracodev.net/",
|
||||||
tiktak: "https://tiktak-dev.dracodev.net/",
|
tiktak: "https://tiktak-dev.dracodev.net/",
|
||||||
kanbante: "https://kanbante-dev.dracodev.net/"
|
kanbante: "https://kanbante-dev.dracodev.net/",
|
||||||
|
agent: "https://agent-dev.dracodev.net/"
|
||||||
};
|
};
|
||||||
var APP_LABELS = {
|
var APP_LABELS = {
|
||||||
obsigate: "ObsiGate", flowdeck: "Flowdeck",
|
obsigate: "ObsiGate", flowdeck: "Flowdeck",
|
||||||
tiktak: "TikTak", kanbante: "Kanbante"
|
tiktak: "TikTak", kanbante: "Kanbante", console: "Console Spark"
|
||||||
};
|
};
|
||||||
var APP_ORDER = ["obsigate", "flowdeck", "tiktak", "kanbante"];
|
var APP_ORDER = ["obsigate", "flowdeck", "tiktak", "kanbante"];
|
||||||
|
var ALL_APPS = ["obsigate", "flowdeck", "tiktak", "kanbante", "console"];
|
||||||
|
var TABS = ["tout", "obsigate", "flowdeck", "tiktak", "kanbante", "console", "historique"];
|
||||||
|
var TAB_LABELS = {
|
||||||
|
tout: "Tout", obsigate: "ObsiGate", flowdeck: "Flowdeck",
|
||||||
|
tiktak: "TikTak", kanbante: "Kanbante",
|
||||||
|
console: "Console Spark", historique: "Historique"
|
||||||
|
};
|
||||||
var EVENT_LABELS = {
|
var EVENT_LABELS = {
|
||||||
created: "créée", moved: "déplacée", closed: "fermée",
|
created: "créée", moved: "déplacée", closed: "fermée",
|
||||||
reopened: "rouverte", commented: "commentée"
|
reopened: "rouverte", commented: "commentée"
|
||||||
};
|
};
|
||||||
|
var CONSOLE_ACTIVE_STATUSES = [
|
||||||
|
"active", "pending", "pause", "pause_budget", "acces_requis",
|
||||||
|
"en_attente", "en attente", "requise", "awaiting"
|
||||||
|
];
|
||||||
|
var CONSOLE_CLOSED_STATUSES = [
|
||||||
|
"closed", "resolved", "resolue", "lue", "terminee", "done", "fermee"
|
||||||
|
];
|
||||||
|
|
||||||
function isDevHost(hostname) {
|
function isDevHost(hostname) {
|
||||||
if (!hostname) return false;
|
if (!hostname) return false;
|
||||||
@@ -98,12 +160,16 @@
|
|||||||
}
|
}
|
||||||
function fmtDate(s) {
|
function fmtDate(s) {
|
||||||
var t = parseDate(s);
|
var t = parseDate(s);
|
||||||
if (!t) return s ? String(s).slice(0, 16) : "";
|
if (!t) return "";
|
||||||
try { return t.toLocaleString("fr-FR"); } catch (e) { return t.toISOString().slice(0, 16); }
|
try { return t.toLocaleString("fr-FR"); } catch (e) { return t.toISOString().slice(0, 16); }
|
||||||
}
|
}
|
||||||
|
function capitalize(s) {
|
||||||
|
s = String(s || "");
|
||||||
|
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
||||||
|
}
|
||||||
|
|
||||||
/* Résolution des bases. `env` permet les tests sans navigateur :
|
/* Résolution des bases. `env` permet les tests sans navigateur :
|
||||||
* { hostname, barUrls, configUrls, tabHrefs, dracoUrls }. */
|
* { hostname, barUrls, configUrls, tabHrefs, dracoUrls, consoleUrl }. */
|
||||||
function resolveUrls(env) {
|
function resolveUrls(env) {
|
||||||
env = env || {};
|
env = env || {};
|
||||||
var hostname = env.hostname;
|
var hostname = env.hostname;
|
||||||
@@ -112,28 +178,31 @@
|
|||||||
}
|
}
|
||||||
var dev = isDevHost(hostname);
|
var dev = isDevHost(hostname);
|
||||||
var urls = {};
|
var urls = {};
|
||||||
APP_ORDER.forEach(function (app) {
|
ALL_APPS.forEach(function (key) {
|
||||||
var u = cleanUrl(env.barUrls && env.barUrls[app])
|
var app = key === "console" ? "agent" : key;
|
||||||
|| cleanUrl(env.configUrls && env.configUrls[app]);
|
var out = key === "console" ? "agent" : key;
|
||||||
|
var u = cleanUrl(env.barUrls && (env.barUrls[key] || env.barUrls[app]))
|
||||||
|
|| cleanUrl(env.configUrls && (env.configUrls[key] || env.configUrls[app]));
|
||||||
if (!u && dev) u = DEV_URLS[app] || null;
|
if (!u && dev) u = DEV_URLS[app] || null;
|
||||||
if (!u) {
|
if (!u) {
|
||||||
var href = null;
|
var href = null;
|
||||||
if (env.tabHrefs && env.tabHrefs[app]) href = env.tabHrefs[app];
|
if (env.tabHrefs && (env.tabHrefs[key] || env.tabHrefs[app])) href = env.tabHrefs[key] || env.tabHrefs[app];
|
||||||
else if (typeof document !== "undefined" && document.querySelector) {
|
else if (typeof document !== "undefined" && document.querySelector) {
|
||||||
var tab = document.querySelector('.draco-apptab[data-app="' + app + '"]');
|
var tab = document.querySelector('.draco-apptab[data-app="' + app + '"]');
|
||||||
href = tab ? tab.getAttribute("href") : null;
|
href = tab ? tab.getAttribute("href") : null;
|
||||||
}
|
}
|
||||||
u = cleanUrl(href);
|
u = cleanUrl(href);
|
||||||
}
|
}
|
||||||
if (!u && env.dracoUrls && env.dracoUrls[app]) u = cleanUrl(env.dracoUrls[app]);
|
if (!u && env.dracoUrls && (env.dracoUrls[key] || env.dracoUrls[app])) u = cleanUrl(env.dracoUrls[key] || env.dracoUrls[app]);
|
||||||
if (!u) {
|
if (!u) {
|
||||||
/* ObsiGate : window.DRACO_URLS injecté par le backend. */
|
/* ObsiGate : window.DRACO_URLS injecté par le backend. */
|
||||||
try {
|
try {
|
||||||
if (global.DRACO_URLS && global.DRACO_URLS[app]) u = cleanUrl(global.DRACO_URLS[app]);
|
if (global.DRACO_URLS && (global.DRACO_URLS[key] || global.DRACO_URLS[app])) u = cleanUrl(global.DRACO_URLS[key] || global.DRACO_URLS[app]);
|
||||||
} catch (e) { /* rien */ }
|
} catch (e) { /* rien */ }
|
||||||
}
|
}
|
||||||
if (!u) u = DEFAULT_URLS[app];
|
if (!u) u = DEFAULT_URLS[app];
|
||||||
urls[app] = withSlash(u);
|
urls[out] = withSlash(u);
|
||||||
|
if (key === "console") urls.console = withSlash(u);
|
||||||
});
|
});
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
@@ -151,9 +220,23 @@
|
|||||||
} catch (e) { /* rien */ }
|
} catch (e) { /* rien */ }
|
||||||
return {
|
return {
|
||||||
barUrls: bar.urls || null, configUrls: cfg.urls || null,
|
barUrls: bar.urls || null, configUrls: cfg.urls || null,
|
||||||
tabHrefs: tabs, dracoUrls: global.DRACO_URLS || null
|
tabHrefs: tabs, dracoUrls: global.DRACO_URLS || null,
|
||||||
|
consoleUrl: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/* URL de la source console, si l'hôte l'a configurée. AUCUNE URL
|
||||||
|
* n'est devinée : sans configuration, la source est indisponible
|
||||||
|
* (état vide honnête, voir renderCurrent). */
|
||||||
|
function consoleSourceUrl(env) {
|
||||||
|
env = env || {};
|
||||||
|
if (env.consoleUrl) return env.consoleUrl;
|
||||||
|
try {
|
||||||
|
if (global.DRACO_ALERTES_CONSOLE_URL) return global.DRACO_ALERTES_CONSOLE_URL;
|
||||||
|
var cfg = global.DRACO_BAR_CONFIG || {};
|
||||||
|
if (cfg.consoleAlertsUrl) return cfg.consoleAlertsUrl;
|
||||||
|
} catch (e) { /* rien */ }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFlowdeck(base, payload) {
|
function normalizeFlowdeck(base, payload) {
|
||||||
var list = (payload && payload.notifications) || [];
|
var list = (payload && payload.notifications) || [];
|
||||||
@@ -192,7 +275,48 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/* Actives d'abord, puis date décroissante (sans date en dernier). */
|
function normalizeConsole(base, payload) {
|
||||||
|
var list = Array.isArray(payload) ? payload
|
||||||
|
: (payload && (payload.alerts || payload.items)) || [];
|
||||||
|
return list.map(function (n) {
|
||||||
|
var status = String(n.status || "").toLowerCase().replace(/_/g, " ").trim();
|
||||||
|
var stateRaw = String(n.state || status || "");
|
||||||
|
var active;
|
||||||
|
if (typeof n.active === "boolean") active = n.active;
|
||||||
|
else if (status && CONSOLE_ACTIVE_STATUSES.indexOf(status.replace(/ /g, "_")) !== -1) active = true;
|
||||||
|
else if (status && CONSOLE_CLOSED_STATUSES.indexOf(status.replace(/ /g, "_")) !== -1) active = false;
|
||||||
|
else if (/pause|acc[eè]s|attente/.test(stateRaw.toLowerCase())) active = true;
|
||||||
|
else if (/ferm|résol|resol|termin/.test(stateRaw.toLowerCase())) active = false;
|
||||||
|
else active = true; /* source réelle, statut inconnu : on l'expose en active */
|
||||||
|
return {
|
||||||
|
app: "console", id: n.id, title: n.title || "(sans titre)",
|
||||||
|
detail: n.detail || n.message || n.context || "", date: n.date || n.created_at || "",
|
||||||
|
active: active, state: stateRaw || (active ? "en attente" : "fermée"),
|
||||||
|
kind: n.kind || n.type || "",
|
||||||
|
url: n.url || n.link ? joinUrl(base, n.url || n.link) : withSlash(base)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/* Libellé d'état AFFICHÉ, uniforme partout (les valeurs brutes de
|
||||||
|
* normalize* sont inchangées, pour compatibilité des tests #69). */
|
||||||
|
function displayState(a) {
|
||||||
|
if (a.active) return "Active";
|
||||||
|
if (a.app === "kanbante") return capitalize(a.state);
|
||||||
|
if (a.app === "console") return capitalize(a.state);
|
||||||
|
return "Lue";
|
||||||
|
}
|
||||||
|
function metaLine(a) {
|
||||||
|
var date = fmtDate(a.date) || "sans date";
|
||||||
|
/* Kanbante : le type brut (« moved ») dupliquait le libellé
|
||||||
|
* d'état ; il n'est plus affiché. Les autres apps gardent leur
|
||||||
|
* type quand il apporte une information. */
|
||||||
|
if (a.app === "kanbante") return APP_LABELS[a.app] + " · " + date + " · " + displayState(a);
|
||||||
|
var kind = a.kind ? " · " + a.kind : "";
|
||||||
|
return APP_LABELS[a.app] + kind + " · " + date + " · " + displayState(a);
|
||||||
|
}
|
||||||
|
/* Actives d'abord, puis date décroissante (sans date en dernier).
|
||||||
|
* Dans un onglet, le périmètre est déjà homogène (que des actives
|
||||||
|
* ou que des fermées) : l'ordre effectif est donc la date décroissante. */
|
||||||
function sortAlerts(list) {
|
function sortAlerts(list) {
|
||||||
return list.slice().sort(function (a, b) {
|
return list.slice().sort(function (a, b) {
|
||||||
if (a.active !== b.active) return a.active ? -1 : 1;
|
if (a.active !== b.active) return a.active ? -1 : 1;
|
||||||
@@ -208,9 +332,74 @@
|
|||||||
list.forEach(function (a) { if (a.active) n++; });
|
list.forEach(function (a) { if (a.active) n++; });
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
function allItems(results) {
|
||||||
|
var all = [];
|
||||||
|
ALL_APPS.forEach(function (app) {
|
||||||
|
(results[app] && results[app].items || []).forEach(function (a) { all.push(a); });
|
||||||
|
});
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
/* Périmètre d'un onglet. Retour : { items, errors, consoleMissing }.
|
||||||
|
* Tout et onglets par application = actives seulement ; Historique =
|
||||||
|
* fermées seulement. Les erreurs de section sont remontées dans
|
||||||
|
* chaque onglet concerné (jamais silencieusement avalées). */
|
||||||
|
function scopeAlerts(all, tab, results) {
|
||||||
|
results = results || {};
|
||||||
|
var items = [], errors = [];
|
||||||
|
function collect(app, pred) {
|
||||||
|
if (results[app] && results[app].error) errors.push({ app: app, error: results[app].error });
|
||||||
|
all.forEach(function (a) { if (a.app === app && pred(a)) items.push(a); });
|
||||||
|
}
|
||||||
|
if (tab === "historique") {
|
||||||
|
ALL_APPS.forEach(function (app) { collect(app, function (a) { return !a.active; }); });
|
||||||
|
} else if (tab === "tout") {
|
||||||
|
ALL_APPS.forEach(function (app) { collect(app, function (a) { return a.active; }); });
|
||||||
|
} else if (ALL_APPS.indexOf(tab) !== -1) {
|
||||||
|
collect(tab, function (a) { return a.active; });
|
||||||
|
} else {
|
||||||
|
ALL_APPS.forEach(function (app) { collect(app, function (a) { return a.active; }); });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
items: sortAlerts(items), errors: errors,
|
||||||
|
consoleMissing: !!(results.console && results.console.sourceMissing)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function countByTab(all, results) {
|
||||||
|
var counts = {};
|
||||||
|
TABS.forEach(function (tab) { counts[tab] = scopeAlerts(all, tab, results).items.length; });
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
/* Filtre texte, appliqué APRÈS le périmètre de l'onglet : titre ou
|
||||||
|
* contexte, insensible à la casse, sans jamais mélanger actifs et
|
||||||
|
* historique (le périmètre est déjà fixé par scopeAlerts). */
|
||||||
|
function filterAlerts(list, q) {
|
||||||
|
q = String(q || "").trim().toLowerCase();
|
||||||
|
if (!q) return list.slice();
|
||||||
|
return list.filter(function (a) {
|
||||||
|
return (String(a.title || "").toLowerCase().indexOf(q) !== -1)
|
||||||
|
|| (String(a.detail || "").toLowerCase().indexOf(q) !== -1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/* Onglet depuis le hash d'URL : "#alertes=historique" -> "historique",
|
||||||
|
* "#alertes" -> "tout", autre -> null. */
|
||||||
|
function parseAlertTab(hash) {
|
||||||
|
if (!hash) return null;
|
||||||
|
var h = String(hash).replace(/^#/, "");
|
||||||
|
if (h === "alertes") return "tout";
|
||||||
|
var m = /^alertes=([a-z]+)$/.exec(h);
|
||||||
|
if (m && TABS.indexOf(m[1]) !== -1) return m[1];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function hashForTab(tab) {
|
||||||
|
if (TABS.indexOf(tab) === -1) tab = "tout";
|
||||||
|
return "#alertes=" + tab;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Rendu (navigateur seulement) ---- */
|
/* ---- Rendu (navigateur seulement) ---- */
|
||||||
var panel = null, listEl = null, badgeTimer = null;
|
var panel = null, listEl = null, tabsEl = null, filterEl = null,
|
||||||
|
countEl = null, badgeTimer = null;
|
||||||
|
var currentTab = "tout", currentData = null, currentFilter = "";
|
||||||
|
var panelPushed = false, closingViaBack = false, suppressHash = false;
|
||||||
|
|
||||||
function el(tag, cls, text) {
|
function el(tag, cls, text) {
|
||||||
var d = document.createElement(tag);
|
var d = document.createElement(tag);
|
||||||
@@ -226,96 +415,204 @@
|
|||||||
var box = el("div", "draco-alertes-box");
|
var box = el("div", "draco-alertes-box");
|
||||||
var head = el("div", "draco-alertes-head");
|
var head = el("div", "draco-alertes-head");
|
||||||
head.appendChild(el("h2", "", "Alertes unifiées"));
|
head.appendChild(el("h2", "", "Alertes unifiées"));
|
||||||
var note = el("p", "draco-alertes-note",
|
|
||||||
"Agrégation à la demande, pas une fédération temps réel : " +
|
|
||||||
"les alertes sont relues dans chaque application à l'ouverture.");
|
|
||||||
var close = el("button", "draco-alertes-close", "✕");
|
var close = el("button", "draco-alertes-close", "✕");
|
||||||
close.setAttribute("aria-label", "Fermer");
|
close.setAttribute("aria-label", "Fermer");
|
||||||
close.addEventListener("click", closePanel);
|
close.addEventListener("click", function () { closePanel(); });
|
||||||
head.appendChild(close);
|
head.appendChild(close);
|
||||||
box.appendChild(head);
|
box.appendChild(head);
|
||||||
box.appendChild(note);
|
box.appendChild(el("p", "draco-alertes-note",
|
||||||
|
"Agrégation à la demande, pas une fédération temps réel : " +
|
||||||
|
"les alertes sont relues dans chaque application à l'ouverture. " +
|
||||||
|
"Une section en échec n'empêche pas de consulter les autres."));
|
||||||
|
tabsEl = el("div", "draco-alertes-tabs");
|
||||||
|
tabsEl.setAttribute("role", "tablist");
|
||||||
|
tabsEl.setAttribute("aria-label", "Alertes par application");
|
||||||
|
tabsEl.addEventListener("keydown", onTabsKeydown);
|
||||||
|
box.appendChild(tabsEl);
|
||||||
|
var filtre = el("div", "draco-alertes-filtre");
|
||||||
|
var lbl = el("label", "", "Filtrer");
|
||||||
|
lbl.setAttribute("for", "draco-alertes-filtre-input");
|
||||||
|
filtre.appendChild(lbl);
|
||||||
|
filterEl = el("input", "");
|
||||||
|
filterEl.type = "search";
|
||||||
|
filterEl.id = "draco-alertes-filtre-input";
|
||||||
|
filterEl.setAttribute("placeholder", "Filtrer par titre ou contexte, dans cet onglet…");
|
||||||
|
filterEl.setAttribute("aria-label", "Filtrer les alertes de l'onglet courant");
|
||||||
|
filterEl.addEventListener("input", function () {
|
||||||
|
currentFilter = filterEl.value;
|
||||||
|
renderCurrent();
|
||||||
|
});
|
||||||
|
filtre.appendChild(filterEl);
|
||||||
|
box.appendChild(filtre);
|
||||||
|
countEl = el("p", "draco-alertes-compte", "");
|
||||||
|
box.appendChild(countEl);
|
||||||
listEl = el("div", "draco-alertes-list");
|
listEl = el("div", "draco-alertes-list");
|
||||||
|
listEl.setAttribute("role", "tabpanel");
|
||||||
box.appendChild(listEl);
|
box.appendChild(listEl);
|
||||||
ov.appendChild(box);
|
ov.appendChild(box);
|
||||||
ov.addEventListener("click", function (ev) { if (ev.target === ov) closePanel(); });
|
ov.addEventListener("click", function (ev) { if (ev.target === ov) closePanel(); });
|
||||||
document.addEventListener("keydown", function (ev) {
|
document.addEventListener("keydown", function (ev) {
|
||||||
if (ev.key === "Escape") closePanel();
|
if (ev.key === "Escape" && panel && panel.style.display !== "none") closePanel();
|
||||||
});
|
});
|
||||||
document.body.appendChild(ov);
|
document.body.appendChild(ov);
|
||||||
panel = ov;
|
panel = ov;
|
||||||
return ov;
|
return ov;
|
||||||
}
|
}
|
||||||
function closePanel() { if (panel) panel.style.display = "none"; }
|
function closePanel() {
|
||||||
|
if (!panel) return;
|
||||||
function renderSection(title, items, fallback) {
|
panel.style.display = "none";
|
||||||
var sec = el("section", "draco-alertes-section");
|
if (panelPushed) {
|
||||||
sec.appendChild(el("h3", "", title + " (" + items.length + ")"));
|
panelPushed = false;
|
||||||
if (!items.length) {
|
closingViaBack = true;
|
||||||
sec.appendChild(el("p", "draco-alertes-empty", fallback));
|
try { history.back(); } catch (e) { closingViaBack = false; }
|
||||||
return sec;
|
return;
|
||||||
}
|
}
|
||||||
items.forEach(function (a) {
|
try {
|
||||||
var row = el("div", "draco-alertes-item" + (a.active ? " is-active" : ""));
|
var h = parseAlertTab(window.location.hash);
|
||||||
var main = el("div", "draco-alertes-main");
|
if (h) {
|
||||||
var link = el("a", "draco-alertes-title", a.title);
|
suppressHash = true;
|
||||||
link.href = a.url;
|
history.replaceState(null, "", window.location.pathname + window.location.search);
|
||||||
main.appendChild(link);
|
suppressHash = false;
|
||||||
if (a.detail) main.appendChild(el("div", "draco-alertes-detail", a.detail));
|
}
|
||||||
var meta = el("div", "draco-alertes-meta",
|
} catch (e) { /* rien */ }
|
||||||
APP_LABELS[a.app] + (a.kind ? " · " + a.kind : "") +
|
|
||||||
" · " + (a.date ? fmtDate(a.date) : "sans date") + " · " + a.state);
|
|
||||||
main.appendChild(meta);
|
|
||||||
row.appendChild(main);
|
|
||||||
sec.appendChild(row);
|
|
||||||
});
|
|
||||||
return sec;
|
|
||||||
}
|
}
|
||||||
function render(results, urls) {
|
function onHashChange() {
|
||||||
ensurePanel();
|
if (suppressHash) return;
|
||||||
listEl.textContent = "";
|
if (closingViaBack) { closingViaBack = false; return; }
|
||||||
var all = [];
|
var tab = parseAlertTab(window.location.hash);
|
||||||
APP_ORDER.forEach(function (app) {
|
if (tab) {
|
||||||
(results[app] && results[app].items || []).forEach(function (a) { all.push(a); });
|
if (!panel || panel.style.display === "none") openPanel(tab);
|
||||||
});
|
else setTab(tab, false);
|
||||||
var actives = sortAlerts(all.filter(function (a) { return a.active; }));
|
} else if (panel && panel.style.display !== "none") {
|
||||||
var history = sortAlerts(all.filter(function (a) { return !a.active; }));
|
panelPushed = false;
|
||||||
listEl.appendChild(el("h3", "draco-alertes-group", "Alertes actives"));
|
panel.style.display = "none";
|
||||||
APP_ORDER.forEach(function (app) {
|
|
||||||
var items = actives.filter(function (a) { return a.app === app; });
|
|
||||||
if (results[app] && results[app].error) {
|
|
||||||
var sec = renderSection(APP_LABELS[app], [], results[app].error);
|
|
||||||
listEl.appendChild(sec);
|
|
||||||
} else if (app === "obsigate") {
|
|
||||||
listEl.appendChild(renderSection(APP_LABELS[app], [],
|
|
||||||
"ObsiGate n'expose aucune notification entrante consultable " +
|
|
||||||
"(ses canaux /api/notify sont des envois sortants, réservés aux administrateurs)."));
|
|
||||||
} else if (items.length) {
|
|
||||||
listEl.appendChild(renderSection(APP_LABELS[app], items, ""));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!actives.length) {
|
|
||||||
listEl.appendChild(el("p", "draco-alertes-empty", "Aucune alerte active dans les applications joignables."));
|
|
||||||
}
|
}
|
||||||
listEl.appendChild(el("h3", "draco-alertes-group", "Historique"));
|
}
|
||||||
APP_ORDER.forEach(function (app) {
|
function onTabsKeydown(ev) {
|
||||||
if (results[app] && results[app].error) return; /* déjà signalé ci-dessus */
|
var keys = ["ArrowLeft", "ArrowRight", "Home", "End"];
|
||||||
if (app === "obsigate") {
|
if (keys.indexOf(ev.key) === -1) return;
|
||||||
var link = el("div", "draco-alertes-empty");
|
var btns = tabsEl.querySelectorAll(".draco-alertes-tab");
|
||||||
var a = el("a", "", "Ouvrir ObsiGate");
|
var idx = TABS.indexOf(currentTab);
|
||||||
a.href = urls.obsigate;
|
if (ev.key === "ArrowLeft") idx = (idx + TABS.length - 1) % TABS.length;
|
||||||
link.appendChild(a);
|
else if (ev.key === "ArrowRight") idx = (idx + 1) % TABS.length;
|
||||||
var sec = el("section", "draco-alertes-section");
|
else if (ev.key === "Home") idx = 0;
|
||||||
sec.appendChild(el("h3", "", "ObsiGate (0)"));
|
else if (ev.key === "End") idx = TABS.length - 1;
|
||||||
sec.appendChild(link);
|
ev.preventDefault();
|
||||||
listEl.appendChild(sec);
|
setTab(TABS[idx], true);
|
||||||
return;
|
if (btns[idx] && btns[idx].focus) btns[idx].focus();
|
||||||
}
|
}
|
||||||
var items = history.filter(function (x) { return x.app === app; });
|
function renderTabs() {
|
||||||
listEl.appendChild(renderSection(APP_LABELS[app], items,
|
if (!tabsEl || !currentData) return;
|
||||||
"Aucun historique disponible pour cette application."));
|
tabsEl.textContent = "";
|
||||||
|
var all = allItems(currentData.results);
|
||||||
|
var counts = countByTab(all, currentData.results);
|
||||||
|
TABS.forEach(function (tab) {
|
||||||
|
var b = el("button", "draco-alertes-tab",
|
||||||
|
TAB_LABELS[tab] + " (" + counts[tab] + ")");
|
||||||
|
b.type = "button";
|
||||||
|
b.setAttribute("role", "tab");
|
||||||
|
b.setAttribute("aria-selected", tab === currentTab ? "true" : "false");
|
||||||
|
b.tabIndex = tab === currentTab ? 0 : -1;
|
||||||
|
b.addEventListener("click", function () { setTab(tab, true); });
|
||||||
|
tabsEl.appendChild(b);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function renderRow(a) {
|
||||||
|
var row = el("div", "draco-alertes-item" + (a.active ? " is-active" : ""));
|
||||||
|
var main = el("div", "draco-alertes-main");
|
||||||
|
var link = el("a", "draco-alertes-title", a.title);
|
||||||
|
link.href = a.url;
|
||||||
|
main.appendChild(link);
|
||||||
|
if (a.detail) main.appendChild(el("div", "draco-alertes-detail", a.detail));
|
||||||
|
main.appendChild(el("div", "draco-alertes-meta", metaLine(a)));
|
||||||
|
row.appendChild(main);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
function renderErrorRow(app, msg) {
|
||||||
|
var row = el("div", "draco-alertes-item is-error");
|
||||||
|
var main = el("div", "draco-alertes-main");
|
||||||
|
main.appendChild(el("div", "draco-alertes-title", APP_LABELS[app] + " — section en échec"));
|
||||||
|
main.appendChild(el("div", "draco-alertes-detail", msg));
|
||||||
|
main.appendChild(el("div", "draco-alertes-meta",
|
||||||
|
APP_LABELS[app] + " · section en échec — les autres sections restent consultables"));
|
||||||
|
row.appendChild(main);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
function renderCurrent() {
|
||||||
|
if (!listEl || !currentData) return;
|
||||||
|
renderTabs();
|
||||||
|
var scope = scopeAlerts(allItems(currentData.results), currentTab, currentData.results);
|
||||||
|
var filtered = filterAlerts(scope.items, currentFilter);
|
||||||
|
var q = String(currentFilter || "").trim();
|
||||||
|
if (countEl) {
|
||||||
|
countEl.textContent = q
|
||||||
|
? filtered.length + " résultat(s) sur " + scope.items.length + " dans cet onglet — filtre « " + q + " »"
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
listEl.textContent = "";
|
||||||
|
/* Onglet Console sans source : état vide honnête, jamais d'alertes
|
||||||
|
* inventées, avec le lien profond vers la console. */
|
||||||
|
if (currentTab === "console" && scope.consoleMissing) {
|
||||||
|
var sec = el("section", "draco-alertes-section");
|
||||||
|
sec.appendChild(el("h3", "", "Console Spark (0)"));
|
||||||
|
sec.appendChild(el("p", "draco-alertes-empty",
|
||||||
|
"Aucune alerte console — source indisponible : l'endpoint " +
|
||||||
|
"d'alertes console (agent-commun, autre chantier) n'est pas " +
|
||||||
|
"encore publié. Les missions en pause budget / accès requis " +
|
||||||
|
"et les approbations en attente apparaîtront ici, avec leurs " +
|
||||||
|
"états réels, dès sa livraison."));
|
||||||
|
var link = el("div", "draco-alertes-empty");
|
||||||
|
var a = el("a", "", "Ouvrir la Console Spark");
|
||||||
|
a.href = currentData.urls.agent || currentData.urls.console || "#";
|
||||||
|
link.appendChild(a);
|
||||||
|
sec.appendChild(link);
|
||||||
|
listEl.appendChild(sec);
|
||||||
|
scope.errors.forEach(function (e) { listEl.appendChild(renderErrorRow(e.app, e.error)); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentTab === "obsigate" && !scope.items.length && !scope.errors.length) {
|
||||||
|
var secOg = el("section", "draco-alertes-section");
|
||||||
|
secOg.appendChild(el("h3", "", "ObsiGate (0)"));
|
||||||
|
secOg.appendChild(el("p", "draco-alertes-empty",
|
||||||
|
"ObsiGate n'expose aucune notification entrante consultable " +
|
||||||
|
"(ses canaux /api/notify sont des envois sortants, réservés aux administrateurs)."));
|
||||||
|
var linkOg = el("div", "draco-alertes-empty");
|
||||||
|
var aOg = el("a", "", "Ouvrir ObsiGate");
|
||||||
|
aOg.href = currentData.urls.obsigate;
|
||||||
|
linkOg.appendChild(aOg);
|
||||||
|
secOg.appendChild(linkOg);
|
||||||
|
listEl.appendChild(secOg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scope.errors.forEach(function (e) { listEl.appendChild(renderErrorRow(e.app, e.error)); });
|
||||||
|
if (!scope.items.length && !scope.errors.length) {
|
||||||
|
var empty = currentTab === "historique"
|
||||||
|
? "Aucune alerte fermée dans les applications joignables."
|
||||||
|
: currentTab === "tout"
|
||||||
|
? "Aucune alerte active dans les applications joignables."
|
||||||
|
: "Aucune alerte active pour " + TAB_LABELS[currentTab] + ".";
|
||||||
|
if (currentTab === "console") empty = "Aucune alerte console.";
|
||||||
|
listEl.appendChild(el("p", "draco-alertes-empty", empty));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (q && !filtered.length) {
|
||||||
|
listEl.appendChild(el("p", "draco-alertes-empty",
|
||||||
|
"Aucune alerte dans cet onglet pour ce filtre."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filtered.forEach(function (a) { listEl.appendChild(renderRow(a)); });
|
||||||
|
}
|
||||||
|
function setTab(tab, push) {
|
||||||
|
if (TABS.indexOf(tab) === -1) tab = "tout";
|
||||||
|
currentTab = tab;
|
||||||
|
renderCurrent();
|
||||||
|
if (push) {
|
||||||
|
try {
|
||||||
|
history.pushState(null, "", hashForTab(tab));
|
||||||
|
panelPushed = true;
|
||||||
|
} catch (e) { /* URL partageable : repli silencieux */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function fetchJson(url, timeoutMs) {
|
function fetchJson(url, timeoutMs) {
|
||||||
var ctrl = null, timer = null;
|
var ctrl = null, timer = null;
|
||||||
@@ -342,6 +639,7 @@
|
|||||||
}
|
}
|
||||||
function loadAll() {
|
function loadAll() {
|
||||||
var urls = resolveUrls(browserEnv());
|
var urls = resolveUrls(browserEnv());
|
||||||
|
var consoleUrl = consoleSourceUrl(browserEnv());
|
||||||
var jobs = {
|
var jobs = {
|
||||||
flowdeck: fetchJson(urls.flowdeck + "api/notifications?limit=50")
|
flowdeck: fetchJson(urls.flowdeck + "api/notifications?limit=50")
|
||||||
.then(function (p) { return { items: normalizeFlowdeck(urls.flowdeck, p) }; }),
|
.then(function (p) { return { items: normalizeFlowdeck(urls.flowdeck, p) }; }),
|
||||||
@@ -350,6 +648,11 @@
|
|||||||
kanbante: fetchJson(urls.kanbante + "api/events?limit=100")
|
kanbante: fetchJson(urls.kanbante + "api/events?limit=100")
|
||||||
.then(function (p) { return { items: normalizeKanbante(urls.kanbante, p) }; })
|
.then(function (p) { return { items: normalizeKanbante(urls.kanbante, p) }; })
|
||||||
};
|
};
|
||||||
|
if (consoleUrl) {
|
||||||
|
var agentBase = urls.agent || urls.console;
|
||||||
|
jobs.console = fetchJson(consoleUrl)
|
||||||
|
.then(function (p) { return { items: normalizeConsole(agentBase, p) }; });
|
||||||
|
}
|
||||||
var keys = Object.keys(jobs);
|
var keys = Object.keys(jobs);
|
||||||
return Promise.all(keys.map(function (k) {
|
return Promise.all(keys.map(function (k) {
|
||||||
return jobs[k].then(function (v) { return [k, v]; }).catch(function (e) {
|
return jobs[k].then(function (v) { return [k, v]; }).catch(function (e) {
|
||||||
@@ -361,6 +664,7 @@
|
|||||||
})).then(function (pairs) {
|
})).then(function (pairs) {
|
||||||
var results = { obsigate: { items: [] } };
|
var results = { obsigate: { items: [] } };
|
||||||
pairs.forEach(function (kv) { results[kv[0]] = kv[1]; });
|
pairs.forEach(function (kv) { results[kv[0]] = kv[1]; });
|
||||||
|
if (!consoleUrl) results.console = { items: [], sourceMissing: true };
|
||||||
return { urls: urls, results: results };
|
return { urls: urls, results: results };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -380,28 +684,35 @@
|
|||||||
}
|
}
|
||||||
function refreshBadge() {
|
function refreshBadge() {
|
||||||
return loadAll().then(function (r) {
|
return loadAll().then(function (r) {
|
||||||
var all = [];
|
var n = countActive(allItems(r.results));
|
||||||
APP_ORDER.forEach(function (app) {
|
setBadge(n);
|
||||||
(r.results[app] && r.results[app].items || []).forEach(function (a) { all.push(a); });
|
return n;
|
||||||
});
|
|
||||||
setBadge(countActive(all));
|
|
||||||
return countActive(all);
|
|
||||||
}).catch(function () { /* pastille optionnelle : la barre vit sans */ });
|
}).catch(function () { /* pastille optionnelle : la barre vit sans */ });
|
||||||
}
|
}
|
||||||
function openPanel() {
|
function openPanel(initialTab) {
|
||||||
ensurePanel();
|
ensurePanel();
|
||||||
panel.style.display = "flex";
|
panel.style.display = "flex";
|
||||||
|
var tab = initialTab || parseAlertTab(window.location.hash) || "tout";
|
||||||
|
currentTab = tab;
|
||||||
|
currentFilter = "";
|
||||||
|
if (filterEl) filterEl.value = "";
|
||||||
listEl.textContent = "";
|
listEl.textContent = "";
|
||||||
listEl.appendChild(el("p", "draco-alertes-empty", "Chargement des alertes…"));
|
listEl.appendChild(el("p", "draco-alertes-empty", "Chargement des alertes…"));
|
||||||
|
if (tabsEl) tabsEl.textContent = "";
|
||||||
loadAll().then(function (r) {
|
loadAll().then(function (r) {
|
||||||
render(r.results, r.urls);
|
currentData = r;
|
||||||
var all = [];
|
/* Ouverture par le bouton (pas de hash) : l'onglet entre dans
|
||||||
APP_ORDER.forEach(function (app) {
|
* l'URL pour être partageable ; ouverture par lien partagé : le
|
||||||
(r.results[app] && r.results[app].items || []).forEach(function (a) { all.push(a); });
|
* hash est déjà le bon, on ne pousse pas d'entrée en double. */
|
||||||
});
|
var fromHash = parseAlertTab(window.location.hash);
|
||||||
setBadge(countActive(all));
|
if (!fromHash) {
|
||||||
|
try { history.pushState(null, "", hashForTab(tab)); panelPushed = true; } catch (e) { /* rien */ }
|
||||||
|
}
|
||||||
|
renderCurrent();
|
||||||
|
setBadge(countActive(allItems(r.results)));
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
render({ obsigate: { items: [] } }, resolveUrls(browserEnv()));
|
currentData = { urls: resolveUrls(browserEnv()), results: { obsigate: { items: [] }, console: { items: [], sourceMissing: true } } };
|
||||||
|
renderCurrent();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,18 +721,29 @@
|
|||||||
_test: {
|
_test: {
|
||||||
resolveUrls: resolveUrls, normalizeFlowdeck: normalizeFlowdeck,
|
resolveUrls: resolveUrls, normalizeFlowdeck: normalizeFlowdeck,
|
||||||
normalizeTiktak: normalizeTiktak, normalizeKanbante: normalizeKanbante,
|
normalizeTiktak: normalizeTiktak, normalizeKanbante: normalizeKanbante,
|
||||||
|
normalizeConsole: normalizeConsole, consoleSourceUrl: consoleSourceUrl,
|
||||||
sortAlerts: sortAlerts, countActive: countActive,
|
sortAlerts: sortAlerts, countActive: countActive,
|
||||||
isDevHost: isDevHost, DEFAULT_URLS: DEFAULT_URLS, DEV_URLS: DEV_URLS
|
filterAlerts: filterAlerts, scopeAlerts: scopeAlerts,
|
||||||
|
countByTab: countByTab, parseAlertTab: parseAlertTab,
|
||||||
|
hashForTab: hashForTab, displayState: displayState,
|
||||||
|
metaLine: metaLine, fmtDate: fmtDate,
|
||||||
|
isDevHost: isDevHost, DEFAULT_URLS: DEFAULT_URLS, DEV_URLS: DEV_URLS,
|
||||||
|
TABS: TABS, TAB_LABELS: TAB_LABELS, APP_LABELS: APP_LABELS
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
global.DRACO_ALERTES = api;
|
global.DRACO_ALERTES = api;
|
||||||
if (typeof module !== "undefined" && module.exports) module.exports = api._test;
|
if (typeof module !== "undefined" && module.exports) module.exports = api._test;
|
||||||
|
|
||||||
/* Pastille au chargement (après les scripts defer de la barre). */
|
/* Pastille au chargement (après les scripts defer de la barre) ;
|
||||||
|
* un lien partagé #alertes=<onglet> ouvre directement le panneau
|
||||||
|
* sur cet onglet. */
|
||||||
if (typeof document !== "undefined" && document.addEventListener) {
|
if (typeof document !== "undefined" && document.addEventListener) {
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
if (badgeTimer) clearTimeout(badgeTimer);
|
if (badgeTimer) clearTimeout(badgeTimer);
|
||||||
badgeTimer = setTimeout(refreshBadge, 300);
|
badgeTimer = setTimeout(refreshBadge, 300);
|
||||||
|
var tab = parseAlertTab(window.location.hash);
|
||||||
|
if (tab) setTimeout(function () { openPanel(tab); }, 350);
|
||||||
});
|
});
|
||||||
|
window.addEventListener("hashchange", onHashChange);
|
||||||
}
|
}
|
||||||
})(typeof window !== "undefined" ? window : globalThis);
|
})(typeof window !== "undefined" ? window : globalThis);
|
||||||
|
|||||||
Vendored
+16
-6
@@ -51,9 +51,13 @@
|
|||||||
* Cas spark (v0.1.6) : AUCUNE dérivation inventée — aucun hôte
|
* Cas spark (v0.1.6) : AUCUNE dérivation inventée — aucun hôte
|
||||||
* spark-dev connu ; sur un hôte -dev, spark retombe sur l'étape 3
|
* spark-dev connu ; sur un hôte -dev, spark retombe sur l'étape 3
|
||||||
* (href rendu) puis 4 (défaut prod), sauf config explicite (étape 1).
|
* (href rendu) puis 4 (défaut prod), sauf config explicite (étape 1).
|
||||||
* Les clés agent et spark ne correspondent à aucun onglet de la barre :
|
* La clé spark ne correspond à aucun onglet de la barre : elle est
|
||||||
* elles sont résolues et exposées via DRACO_BAR.urls pour le panneau
|
* résolue et exposée via DRACO_BAR.urls pour le panneau Spark
|
||||||
* Spark (panneau-agent), qui suit le même contrat d'injection.
|
* (panneau-agent), qui suit le même contrat d'injection. La clé agent,
|
||||||
|
* elle, a un onglet depuis le chantier 5 (ordre : ObsiGate, Flowdeck,
|
||||||
|
* TikTak, Kanbante, Console Spark) : elle suit donc l'étape « href
|
||||||
|
* rendu » comme les autres onglets, en plus d'alimenter le menu de
|
||||||
|
* la marque et DRACO_BAR.urls.
|
||||||
* Actions émises (data-act) : rail, palette, domaine (data-dom),
|
* Actions émises (data-act) : rail, palette, domaine (data-dom),
|
||||||
* alertes, spark, rappel-entete. L'app intégratrice décide de chacune
|
* alertes, spark, rappel-entete. L'app intégratrice décide de chacune
|
||||||
* (ouvrir la palette, le panneau Spark, changer de domaine…).
|
* (ouvrir la palette, le panneau Spark, changer de domaine…).
|
||||||
@@ -79,9 +83,10 @@
|
|||||||
* onglets présents dans la barre, puis une entrée « Console Spark »
|
* onglets présents dans la barre, puis une entrée « Console Spark »
|
||||||
* (logo ✦, rôle « Agent commun ») APRÈS Kanbante, vers l'URL agent
|
* (logo ✦, rôle « Agent commun ») APRÈS Kanbante, vers l'URL agent
|
||||||
* résolue : 1) config explicite DRACO_BAR_CONFIG.urls.agent,
|
* résolue : 1) config explicite DRACO_BAR_CONFIG.urls.agent,
|
||||||
* 2) dérivation -dev agent-dev sur hôte -dev, 3) défaut officiel
|
* 2) dérivation -dev agent-dev sur hôte -dev, 3) href rendu de
|
||||||
* prod https://agent.dracodev.net/ (il n'y a pas d'onglet agent, donc
|
* l'onglet « Console Spark » (chantier 5, data-app="agent"),
|
||||||
* pas d'étape « href rendu »). AUCUN hôte spark-dev n'est inventé :
|
* 4) défaut officiel prod https://agent.dracodev.net/.
|
||||||
|
* AUCUN hôte spark-dev n'est inventé :
|
||||||
* la clé spark, elle, suit toujours son propre contrat (explicite >
|
* la clé spark, elle, suit toujours son propre contrat (explicite >
|
||||||
* href rendu > défaut prod) et ne sert qu'au panneau Spark.
|
* href rendu > défaut prod) et ne sert qu'au panneau Spark.
|
||||||
* Focus (correctif #70) — ouverture au clavier : Entrée sur la marque
|
* Focus (correctif #70) — ouverture au clavier : Entrée sur la marque
|
||||||
@@ -329,6 +334,11 @@
|
|||||||
appsMenu.setAttribute("role", "menu");
|
appsMenu.setAttribute("role", "menu");
|
||||||
appsMenu.setAttribute("aria-label", "Applications");
|
appsMenu.setAttribute("aria-label", "Applications");
|
||||||
document.querySelectorAll(".draco-apptab").forEach(function (t) {
|
document.querySelectorAll(".draco-apptab").forEach(function (t) {
|
||||||
|
/* Chantier 5 : l'onglet agent (« Console Spark ») n'est PAS
|
||||||
|
* repris ici — l'entrée dédiée ci-dessous le représente déjà
|
||||||
|
* (après Kanbante, avec séparateur) ; sans ce saut, le menu
|
||||||
|
* listerait « Console Spark » deux fois. */
|
||||||
|
if (t.dataset.app === "agent") return;
|
||||||
var a = document.createElement("a");
|
var a = document.createElement("a");
|
||||||
a.className = "draco-apps-item";
|
a.className = "draco-apps-item";
|
||||||
a.href = resolvedUrls[t.dataset.app] || t.getAttribute("href");
|
a.href = resolvedUrls[t.dataset.app] || t.getAttribute("href");
|
||||||
|
|||||||
+157
-5
@@ -1,18 +1,61 @@
|
|||||||
// Tests unitaires (node:test) du module partagé draco-alertes.js.
|
// Tests unitaires (node:test) du module partagé draco-alertes.js.
|
||||||
// Exécutés dans la sandbox : node --test draco-alertes.test.js
|
// Exécutés dans la sandbox : node --test draco-alertes.test.js
|
||||||
|
// Copie vendangée byte-identique de Projets/dracodev-commons
|
||||||
|
// ui/alertes/ (chantier 5) — seul le require change selon l'hôte.
|
||||||
|
// Hôte : Flowdeck
|
||||||
// Aucune donnée inventée : les payloads reprennent la forme exacte
|
// Aucune donnée inventée : les payloads reprennent la forme exacte
|
||||||
// des API réelles (Flowdeck /api/notifications, TikTak
|
// des API réelles (Flowdeck /api/notifications, TikTak
|
||||||
// /api/notifications, Kanbante /api/events).
|
// /api/notifications, Kanbante /api/events). Le payload console
|
||||||
|
// n'est qu'un EXEMPLE DE FORMAT (voir README commons) : aucun
|
||||||
|
// endpoint console n'est publié sur agent-commun main au moment
|
||||||
|
// du chantier.
|
||||||
const test = require("node:test");
|
const test = require("node:test");
|
||||||
const assert = require("node:assert/strict");
|
const assert = require("node:assert/strict");
|
||||||
const t = require("../static/js/draco-alertes.js");
|
const t = require("../static/js/draco-alertes.js");
|
||||||
|
|
||||||
test("resolveUrls: config explicite > hote -dev > href > defaut prod", () => {
|
function sampleResults() {
|
||||||
|
return {
|
||||||
|
flowdeck: { items: t.normalizeFlowdeck("https://flowdeck.dracodev.net/", {
|
||||||
|
notifications: [
|
||||||
|
{ id: 1, ntype: "mention", title: "Mention budget", message: "voir page", url: "/collections/3", is_read: 0, created_at: "2026-10-28T10:00:00" },
|
||||||
|
{ id: 2, ntype: "reminder", title: "Rappel lu", message: "", url: "", is_read: 1, created_at: "2026-10-27T09:00:00" },
|
||||||
|
], unread: 1,
|
||||||
|
}) },
|
||||||
|
tiktak: { items: t.normalizeTiktak("https://tiktak.dracodev.net/", [
|
||||||
|
{ id: 7, kind: "reminder", title: "Rappel tâche", body: "tache", task_id: 42, is_read: 0, created_at: "2026-10-28T08:00:00" },
|
||||||
|
{ id: 8, kind: "share", title: "Partage lu", body: "", task_id: null, is_read: 1, created_at: "2026-10-26T08:00:00" },
|
||||||
|
]) },
|
||||||
|
kanbante: { items: t.normalizeKanbante("https://kanbante.dracodev.net/", {
|
||||||
|
events: [
|
||||||
|
{ id: 9, repo_full_name: "Projets/fusion-trois-apps", issue_number: 69, event_type: "commented", from_column: "", to_column: "", created_at: "2026-10-28T11:50:00" },
|
||||||
|
{ id: 10, repo_full_name: "Projets/fusion-trois-apps", issue_number: 69, event_type: "moved", from_column: "todo", to_column: "review", created_at: "2026-10-28T12:00:00" },
|
||||||
|
],
|
||||||
|
}) },
|
||||||
|
obsigate: { items: [] },
|
||||||
|
console: { items: [], sourceMissing: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function allOf(results) {
|
||||||
|
const all = [];
|
||||||
|
for (const app of ["obsigate", "flowdeck", "tiktak", "kanbante", "console"]) {
|
||||||
|
(results[app] && results[app].items || []).forEach((a) => all.push(a));
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("ordre des onglets : Tout, apps, Console Spark, Historique", () => {
|
||||||
|
assert.deepEqual(t.TABS, ["tout", "obsigate", "flowdeck", "tiktak", "kanbante", "console", "historique"]);
|
||||||
|
assert.equal(t.TAB_LABELS.console, "Console Spark");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveUrls: config explicite > hote -dev > href > defaut prod, agent inclus", () => {
|
||||||
const prod = t.resolveUrls({ hostname: "tiktak.dracodev.net", tabHrefs: {} });
|
const prod = t.resolveUrls({ hostname: "tiktak.dracodev.net", tabHrefs: {} });
|
||||||
assert.equal(prod.tiktak, "https://tiktak.dracodev.net/");
|
assert.equal(prod.tiktak, "https://tiktak.dracodev.net/");
|
||||||
|
assert.equal(prod.agent, "https://agent.dracodev.net/");
|
||||||
const dev = t.resolveUrls({ hostname: "tiktak-dev.dracodev.net", tabHrefs: {} });
|
const dev = t.resolveUrls({ hostname: "tiktak-dev.dracodev.net", tabHrefs: {} });
|
||||||
assert.equal(dev.flowdeck, "https://flowdeck-dev.dracodev.net/");
|
assert.equal(dev.flowdeck, "https://flowdeck-dev.dracodev.net/");
|
||||||
assert.equal(dev.obsigate, "https://og-dev.dracodev.net/");
|
assert.equal(dev.obsigate, "https://og-dev.dracodev.net/");
|
||||||
|
assert.equal(dev.agent, "https://agent-dev.dracodev.net/");
|
||||||
const explicit = t.resolveUrls({
|
const explicit = t.resolveUrls({
|
||||||
hostname: "tiktak-dev.dracodev.net",
|
hostname: "tiktak-dev.dracodev.net",
|
||||||
configUrls: { flowdeck: "https://flowdeck.dracodev.net/" },
|
configUrls: { flowdeck: "https://flowdeck.dracodev.net/" },
|
||||||
@@ -20,9 +63,10 @@ test("resolveUrls: config explicite > hote -dev > href > defaut prod", () => {
|
|||||||
assert.equal(explicit.flowdeck, "https://flowdeck.dracodev.net/");
|
assert.equal(explicit.flowdeck, "https://flowdeck.dracodev.net/");
|
||||||
const href = t.resolveUrls({
|
const href = t.resolveUrls({
|
||||||
hostname: "example.org",
|
hostname: "example.org",
|
||||||
tabHrefs: { kanbante: "https://kanbante-dev.dracodev.net/" },
|
tabHrefs: { kanbante: "https://kanbante-dev.dracodev.net/", agent: "https://agent.dracodev.net/" },
|
||||||
});
|
});
|
||||||
assert.equal(href.kanbante, "https://kanbante-dev.dracodev.net/");
|
assert.equal(href.kanbante, "https://kanbante-dev.dracodev.net/");
|
||||||
|
assert.equal(href.agent, "https://agent.dracodev.net/");
|
||||||
const token = t.resolveUrls({ hostname: "x", tabHrefs: { tiktak: "{{URL_TIKTAK}}" } });
|
const token = t.resolveUrls({ hostname: "x", tabHrefs: { tiktak: "{{URL_TIKTAK}}" } });
|
||||||
assert.equal(token.tiktak, "https://tiktak.dracodev.net/");
|
assert.equal(token.tiktak, "https://tiktak.dracodev.net/");
|
||||||
assert.equal(t.isDevHost("tiktak-dev.dracodev.net.evil.com"), false);
|
assert.equal(t.isDevHost("tiktak-dev.dracodev.net.evil.com"), false);
|
||||||
@@ -34,8 +78,7 @@ test("normalizeFlowdeck: non lues actives, lien = base + url", () => {
|
|||||||
notifications: [
|
notifications: [
|
||||||
{ id: 1, ntype: "mention", title: "Mention", message: "voir page", url: "/collections/3", is_read: 0, created_at: "2026-10-28T10:00:00" },
|
{ id: 1, ntype: "mention", title: "Mention", message: "voir page", url: "/collections/3", is_read: 0, created_at: "2026-10-28T10:00:00" },
|
||||||
{ id: 2, ntype: "reminder", title: "Rappel", message: "", url: "", is_read: 1, created_at: "2026-10-27T09:00:00" },
|
{ id: 2, ntype: "reminder", title: "Rappel", message: "", url: "", is_read: 1, created_at: "2026-10-27T09:00:00" },
|
||||||
],
|
], unread: 1,
|
||||||
unread: 1,
|
|
||||||
});
|
});
|
||||||
assert.equal(items.length, 2);
|
assert.equal(items.length, 2);
|
||||||
assert.equal(items[0].active, true);
|
assert.equal(items[0].active, true);
|
||||||
@@ -82,3 +125,112 @@ test("sortAlerts + countActive: actives d'abord, date decroissante", () => {
|
|||||||
assert.equal(sorted[3].date, "");
|
assert.equal(sorted[3].date, "");
|
||||||
assert.equal(t.countActive(list), 2);
|
assert.equal(t.countActive(list), 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("scopeAlerts: Tout et onglets apps = actives seulement ; Historique = fermees seulement", () => {
|
||||||
|
const r = sampleResults();
|
||||||
|
const all = allOf(r);
|
||||||
|
const tout = t.scopeAlerts(all, "tout", r);
|
||||||
|
assert.equal(tout.items.length, 2);
|
||||||
|
assert.ok(tout.items.every((a) => a.active));
|
||||||
|
assert.equal(tout.items[0].date, "2026-10-28T10:00:00"); // date decroissante
|
||||||
|
const fd = t.scopeAlerts(all, "flowdeck", r);
|
||||||
|
assert.equal(fd.items.length, 1);
|
||||||
|
assert.equal(fd.items[0].app, "flowdeck");
|
||||||
|
const hist = t.scopeAlerts(all, "historique", r);
|
||||||
|
assert.equal(hist.items.length, 4); // 1 FD lue + 1 TK lue + 2 KB
|
||||||
|
assert.ok(hist.items.every((a) => !a.active));
|
||||||
|
const og = t.scopeAlerts(all, "obsigate", r);
|
||||||
|
assert.equal(og.items.length, 0);
|
||||||
|
const cs = t.scopeAlerts(all, "console", r);
|
||||||
|
assert.equal(cs.items.length, 0);
|
||||||
|
assert.equal(cs.consoleMissing, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("scopeAlerts: une section en echec remonte aussi dans Historique (pas avalee)", () => {
|
||||||
|
const r = sampleResults();
|
||||||
|
r.flowdeck = { items: [], error: "Application injoignable depuis cet hôte (TypeError)." };
|
||||||
|
const hist = t.scopeAlerts(allOf(r), "historique", r);
|
||||||
|
assert.equal(hist.errors.length, 1);
|
||||||
|
assert.equal(hist.errors[0].app, "flowdeck");
|
||||||
|
const tout = t.scopeAlerts(allOf(r), "tout", r);
|
||||||
|
assert.equal(tout.errors.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("countByTab: compteurs par onglet sur l'echantillon", () => {
|
||||||
|
const r = sampleResults();
|
||||||
|
const c = t.countByTab(allOf(r), r);
|
||||||
|
assert.deepEqual(c, {
|
||||||
|
tout: 2, obsigate: 0, flowdeck: 1, tiktak: 1,
|
||||||
|
kanbante: 0, console: 0, historique: 4,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filterAlerts: titre + contexte, insensible a la casse, sans melanger actifs/historique", () => {
|
||||||
|
const r = sampleResults();
|
||||||
|
const all = allOf(r);
|
||||||
|
const tout = t.scopeAlerts(all, "tout", r).items;
|
||||||
|
assert.equal(t.filterAlerts(tout, "budget").length, 1);
|
||||||
|
assert.equal(t.filterAlerts(tout, "BUDGET").length, 1);
|
||||||
|
assert.equal(t.filterAlerts(tout, "voir page").length, 1); // contexte (detail)
|
||||||
|
assert.equal(t.filterAlerts(tout, "tache").length, 1);
|
||||||
|
// « Rappel lu » est une alerte fermee : absente de Tout meme filtree.
|
||||||
|
assert.equal(t.filterAlerts(tout, "Rappel lu").length, 0);
|
||||||
|
const hist = t.scopeAlerts(all, "historique", r).items;
|
||||||
|
assert.equal(t.filterAlerts(hist, "Rappel lu").length, 1);
|
||||||
|
assert.equal(t.filterAlerts(tout, "").length, tout.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("presentation identique: meta Kanbante sans type brut anglais ; date illisible = sans date", () => {
|
||||||
|
const kb = t.normalizeKanbante("https://kanbante.dracodev.net/", {
|
||||||
|
events: [{ id: 1, repo_full_name: "R", issue_number: 1, event_type: "moved", from_column: "todo", to_column: "review", created_at: "2026-10-28T12:00:00" }],
|
||||||
|
})[0];
|
||||||
|
assert.ok(!t.metaLine(kb).includes("moved"));
|
||||||
|
assert.match(t.metaLine(kb), /Kanbante · .* · Déplacée/);
|
||||||
|
assert.equal(t.displayState({ active: true }), "Active");
|
||||||
|
assert.equal(t.displayState({ active: false, app: "flowdeck" }), "Lue");
|
||||||
|
const sansDate = { app: "tiktak", active: true, title: "x", detail: "", date: "nimporte-quoi", kind: "", url: "https://x/" };
|
||||||
|
assert.match(t.metaLine(sansDate), /sans date/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hash URL: parse + round-trip, onglet inconnu rejete", () => {
|
||||||
|
assert.equal(t.parseAlertTab("#alertes=historique"), "historique");
|
||||||
|
assert.equal(t.parseAlertTab("#alertes=console"), "console");
|
||||||
|
assert.equal(t.parseAlertTab("#alertes"), "tout");
|
||||||
|
assert.equal(t.parseAlertTab("#autre"), null);
|
||||||
|
assert.equal(t.parseAlertTab("#alertes=nimporte"), null);
|
||||||
|
assert.equal(t.parseAlertTab(""), null);
|
||||||
|
for (const tab of t.TABS) assert.equal(t.parseAlertTab(t.hashForTab(tab)), tab);
|
||||||
|
assert.equal(t.hashForTab("nimporte"), "#alertes=tout");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeConsole (FORMAT EXEMPLE): statuts reels -> actif/ferme, lien console par defaut", () => {
|
||||||
|
const items = t.normalizeConsole("https://agent.dracodev.net/", {
|
||||||
|
alerts: [
|
||||||
|
{ id: "m1", title: "Mission en pause budget", detail: "budget épuisé", status: "pause_budget", created_at: "2026-10-28T09:00:00" },
|
||||||
|
{ id: "m2", title: "Accès requis", detail: "jeton manquant", status: "acces_requis", created_at: "2026-10-28T09:30:00" },
|
||||||
|
{ id: "a1", title: "Approbation en attente", status: "en_attente", url: "/approbations", created_at: "2026-10-28T10:00:00" },
|
||||||
|
{ id: "m3", title: "Mission terminée", status: "terminee", created_at: "2026-10-27T10:00:00" },
|
||||||
|
{ id: "m4", title: "Sans statut", created_at: "2026-10-27T11:00:00" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(items.length, 5);
|
||||||
|
assert.equal(items[0].active, true);
|
||||||
|
assert.equal(items[1].active, true);
|
||||||
|
assert.equal(items[2].active, true);
|
||||||
|
assert.equal(items[2].url, "https://agent.dracodev.net/approbations");
|
||||||
|
assert.equal(items[3].active, false);
|
||||||
|
assert.equal(items[4].active, true); // statut inconnu : expose en active, jamais masque
|
||||||
|
assert.equal(items[0].url, "https://agent.dracodev.net/");
|
||||||
|
const r = sampleResults();
|
||||||
|
r.console = { items };
|
||||||
|
const c = t.countByTab(allOf(r), r);
|
||||||
|
assert.equal(c.console, 4);
|
||||||
|
assert.equal(c.tout, 6);
|
||||||
|
assert.equal(c.historique, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("consoleSourceUrl: null sans configuration (aucune URL devineee)", () => {
|
||||||
|
assert.equal(t.consoleSourceUrl({}), null);
|
||||||
|
assert.equal(t.consoleSourceUrl({ consoleUrl: "https://agent-dev.dracodev.net/api/alertes" }),
|
||||||
|
"https://agent-dev.dracodev.net/api/alertes");
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Fusion #69 — Alertes unifiées (chantier 2, Flowdeck).
|
"""Fusion #69 — Alertes unifiées (chantier 2 + chantier 5, Flowdeck).
|
||||||
|
|
||||||
Source réelle de Flowdeck : GET /api/notifications (non lues =
|
Source réelle de Flowdeck : GET /api/notifications (non lues =
|
||||||
actives, `url` relative pour le lien profond), déjà exposée et
|
actives, `url` relative pour le lien profond), déjà exposée et
|
||||||
@@ -61,3 +61,24 @@ def test_notifications_api_et_cors_soeur(client):
|
|||||||
headers={"Origin": "https://evil.example",
|
headers={"Origin": "https://evil.example",
|
||||||
"Access-Control-Request-Method": "GET"})
|
"Access-Control-Request-Method": "GET"})
|
||||||
assert "access-control-allow-origin" not in r4.headers
|
assert "access-control-allow-origin" not in r4.headers
|
||||||
|
|
||||||
|
|
||||||
|
def test_chantier5_onglet_console_spark_et_alertes_onglets(client):
|
||||||
|
"""Chantier 5 : onglet Console Spark en dernier dans la barre
|
||||||
|
(gabarit lu, pages authentifiées inatteignables en sandbox comme
|
||||||
|
pour le test #69), et vue Alertes en onglets (module + styles)."""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
gabarit = Path("app/templates/_draco_bar.html").read_text()
|
||||||
|
nav = gabarit.split('<nav class="draco-apptabs"')[1].split("</nav>")[0]
|
||||||
|
assert nav.index('data-app="kanbante"') < nav.index('data-app="agent"')
|
||||||
|
assert "Console Spark" in nav
|
||||||
|
js = client.get("/static/js/draco-alertes.js")
|
||||||
|
assert js.status_code == 200
|
||||||
|
assert "draco-alertes-tab" in js.text
|
||||||
|
assert "historique" in js.text
|
||||||
|
assert "source indisponible" in js.text
|
||||||
|
css = client.get("/static/css/draco-alertes.css")
|
||||||
|
assert css.status_code == 200
|
||||||
|
assert "draco-alertes-tab" in css.text
|
||||||
|
assert "draco-alertes-filtre" in css.text
|
||||||
|
|||||||
Reference in New Issue
Block a user