Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e96ebf3c5 | ||
|
|
f88c05a053 | ||
|
|
29ce94808d | ||
|
|
6f31275c12 | ||
|
|
f11dc26930 | ||
|
|
3b8402374a | ||
|
|
6635defb2e | ||
|
|
7fb073d462 | ||
|
|
2e59f8786b | ||
|
|
ba6069962a | ||
|
|
375de59e28 | ||
|
|
3b84a86b8f | ||
|
|
a5dac2349d |
+9
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
|||||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.81.2**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.81.3**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,14 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.81.3] — 2026-10-28
|
||||||
|
|
||||||
|
### Modifié
|
||||||
|
|
||||||
|
- **Barre d'apps** — vendange de `dracodev-commons` v0.1.6 : dérivation `-dev` par l'hôte courant, menu « ••• » par défaut, menu compte fonctionnel, spark sans dérivation inventée.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.81.2] — 2026-10-28
|
## [2.81.2] — 2026-10-28
|
||||||
|
|
||||||
### Modifié
|
### Modifié
|
||||||
|
|||||||
+12
-2
@@ -189,15 +189,25 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|||||||
# #223 : l'origine du service d'agents n'est pas figée — elle
|
# #223 : l'origine du service d'agents n'est pas figée — elle
|
||||||
# est dérivée de la configuration (DRACO_AGENT_URL, sinon
|
# est dérivée de la configuration (DRACO_AGENT_URL, sinon
|
||||||
# l'origine d'AGENT_EVENEMENTS_URL, sinon le défaut officiel).
|
# l'origine d'AGENT_EVENEMENTS_URL, sinon le défaut officiel).
|
||||||
from backend.draco_config import agent_origine
|
from backend.draco_config import agent_origine, origine, urls_publiques
|
||||||
|
|
||||||
agent = agent_origine()
|
agent = agent_origine()
|
||||||
|
# Fusion #69 : le panneau Alertes unifiées lit les API
|
||||||
|
# notifications des applications soeurs depuis cet hôte ;
|
||||||
|
# leurs origines rejoignent connect-src (même résolution
|
||||||
|
# draco_config que les liens de la barre : explicite >
|
||||||
|
# dérivation depuis l'hôte de l'agent > défaut officiel).
|
||||||
|
_soeurs69 = " ".join(sorted({
|
||||||
|
origine(v)
|
||||||
|
for k, v in urls_publiques().items()
|
||||||
|
if k in ("obsigate", "flowdeck", "tiktak", "kanbante")
|
||||||
|
} - {""}))
|
||||||
response.headers["Content-Security-Policy"] = (
|
response.headers["Content-Security-Policy"] = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
f"script-src 'self' 'nonce-{nonce}' blob: https://cdnjs.cloudflare.com https://unpkg.com https://esm.sh https://cdn.jsdelivr.net https://static.cloudflareinsights.com; "
|
f"script-src 'self' 'nonce-{nonce}' blob: https://cdnjs.cloudflare.com https://unpkg.com https://esm.sh https://cdn.jsdelivr.net https://static.cloudflareinsights.com; "
|
||||||
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com https://cdn.jsdelivr.net https://esm.sh; "
|
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com https://cdn.jsdelivr.net https://esm.sh; "
|
||||||
"img-src 'self' data: blob:; "
|
"img-src 'self' data: blob:; "
|
||||||
f"connect-src 'self' blob: https://esm.sh https://unpkg.com https://cdnjs.cloudflare.com https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net {agent}; "
|
f"connect-src 'self' blob: https://esm.sh https://unpkg.com https://cdnjs.cloudflare.com https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net {agent} {_soeurs69}; "
|
||||||
"font-src 'self' data: https://fonts.gstatic.com https://esm.sh; "
|
"font-src 'self' data: https://fonts.gstatic.com https://esm.sh; "
|
||||||
"worker-src 'self' blob:; "
|
"worker-src 'self' blob:; "
|
||||||
f"frame-src 'self' blob: {agent}; "
|
f"frame-src 'self' blob: {agent}; "
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.81.2"
|
version = "2.81.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.81.2"
|
version = "2.81.3"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.81.2",
|
"version": "2.81.3",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.81.2 | **Dernière mise à jour :** 2026-10-28
|
> **Version :** 2.81.3 | **Dernière mise à jour :** 2026-10-28
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/* Alertes unifiées dracodev — Fusion #69. Styles neutres, variables
|
||||||
|
* d'hôte avec replis sombres (les 4 hôtes sont à dominante sombre). */
|
||||||
|
.draco-alertes-overlay {
|
||||||
|
position: fixed; inset: 0; z-index: 30000;
|
||||||
|
display: none; align-items: flex-start; justify-content: center;
|
||||||
|
padding: 8vh 16px 16px;
|
||||||
|
background: rgb(0 0 0 / .55); overflow-y: auto;
|
||||||
|
}
|
||||||
|
.draco-alertes-box {
|
||||||
|
width: 720px; max-width: 100%;
|
||||||
|
background: var(--bg-primary, #1f1f1f);
|
||||||
|
color: var(--text, #eceee9);
|
||||||
|
border: 1px solid var(--border, #333);
|
||||||
|
border-radius: 14px; box-shadow: 0 16px 48px rgb(0 0 0 / .5);
|
||||||
|
padding: 16px 18px 18px;
|
||||||
|
}
|
||||||
|
.draco-alertes-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||||
|
.draco-alertes-head h2 { margin: 0; font-size: 18px; }
|
||||||
|
.draco-alertes-close {
|
||||||
|
border: 1px solid var(--border, #333); background: transparent;
|
||||||
|
color: inherit; border-radius: 8px; padding: 4px 10px; cursor: pointer;
|
||||||
|
}
|
||||||
|
.draco-alertes-note { font-size: 12.5px; color: var(--text-dim, #999); margin: 8px 0 14px; }
|
||||||
|
.draco-alertes-group { font-size: 15px; margin: 18px 0 6px; }
|
||||||
|
.draco-alertes-section h3 { font-size: 14px; margin: 12px 0 6px; }
|
||||||
|
.draco-alertes-item {
|
||||||
|
border: 1px solid var(--border, #2a2a2a); border-radius: 10px;
|
||||||
|
padding: 9px 12px; margin: 6px 0;
|
||||||
|
}
|
||||||
|
.draco-alertes-item.is-active { border-color: #e8762c; }
|
||||||
|
.draco-alertes-title { font-weight: 600; color: inherit; text-decoration: none; }
|
||||||
|
.draco-alertes-title:hover { text-decoration: underline; }
|
||||||
|
.draco-alertes-detail {
|
||||||
|
font-size: 13px; color: var(--text-dim, #bbb); margin-top: 2px;
|
||||||
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||||
|
}
|
||||||
|
.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 a { color: inherit; }
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
/* Alertes unifiées dracodev — Fusion #69 (chantier 2).
|
||||||
|
*
|
||||||
|
* Agrégation À LA DEMANDE, pas une fédération temps réel : à chaque
|
||||||
|
* ouverture du panneau (et au chargement, pour la pastille), le module
|
||||||
|
* interroge les API de notifications réellement exposées par chaque
|
||||||
|
* application, avec les identifiants du navigateur (credentials:
|
||||||
|
* "include"). Aucune alerte n'est inventée : une source injoignable
|
||||||
|
* ou non authentifiée affiche son état réel (« Connexion requise »)
|
||||||
|
* avec un lien profond vers sa page native, jamais du vide déguisé.
|
||||||
|
*
|
||||||
|
* Sources réelles (audit #69, 2026-10-28) :
|
||||||
|
* - Flowdeck : GET {base}/api/notifications?limit=50
|
||||||
|
* -> { notifications: [{id, ntype, title, message, url, is_read,
|
||||||
|
* created_at}], unread } ; active = is_read = 0 ; lien = base + url.
|
||||||
|
* - TikTak : GET {base}/api/notifications
|
||||||
|
* -> [{id, kind, title, body, task_id, is_read, created_at}] ;
|
||||||
|
* active = is_read = 0 ; lien = base + "/?task=" + task_id, sinon
|
||||||
|
* base + "/notifications".
|
||||||
|
* - Kanbante : GET {base}/api/events?limit=100
|
||||||
|
* -> { events: [{repo_full_name, issue_number, event_type,
|
||||||
|
* from_column, to_column, created_at}] } ; AUCUN état lu/non lu
|
||||||
|
* n'existe dans Kanbante, donc tout y est historique et rien n'y
|
||||||
|
* est compté comme actif ; lien = base + "/board/" + repo.
|
||||||
|
* - ObsiGate : AUCUNE notification entrante consultable. Ses
|
||||||
|
* /api/notify/* sont des canaux d'envoi sortants (admin, secrets
|
||||||
|
* masqués), pas un journal lisible par l'utilisateur. La section
|
||||||
|
* ObsiGate le dit et renvoie vers l'accueil d'ObsiGate.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* 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) ;
|
||||||
|
* 3. href rendu de l'onglet .draco-apptab[data-app] ;
|
||||||
|
* 4. défaut officiel (production).
|
||||||
|
* ObsiGate n'a pas de .draco-apptab dans tous les cas : l'étape 3 y
|
||||||
|
* lit aussi window.DRACO_URLS (injecté par son backend).
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* pour la pastille (repli : mise à jour directe du badge).
|
||||||
|
* Insertion des données : textContent uniquement, jamais innerHTML
|
||||||
|
* avec une valeur venue du serveur.
|
||||||
|
*/
|
||||||
|
(function (global) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var DEFAULT_URLS = {
|
||||||
|
obsigate: "https://og.dracodev.net/",
|
||||||
|
flowdeck: "https://flowdeck.dracodev.net/",
|
||||||
|
tiktak: "https://tiktak.dracodev.net/",
|
||||||
|
kanbante: "https://kanbante.dracodev.net/"
|
||||||
|
};
|
||||||
|
var DEV_URLS = {
|
||||||
|
obsigate: "https://og-dev.dracodev.net/",
|
||||||
|
flowdeck: "https://flowdeck-dev.dracodev.net/",
|
||||||
|
tiktak: "https://tiktak-dev.dracodev.net/",
|
||||||
|
kanbante: "https://kanbante-dev.dracodev.net/"
|
||||||
|
};
|
||||||
|
var APP_LABELS = {
|
||||||
|
obsigate: "ObsiGate", flowdeck: "Flowdeck",
|
||||||
|
tiktak: "TikTak", kanbante: "Kanbante"
|
||||||
|
};
|
||||||
|
var APP_ORDER = ["obsigate", "flowdeck", "tiktak", "kanbante"];
|
||||||
|
var EVENT_LABELS = {
|
||||||
|
created: "créée", moved: "déplacée", closed: "fermée",
|
||||||
|
reopened: "rouverte", commented: "commentée"
|
||||||
|
};
|
||||||
|
|
||||||
|
function isDevHost(hostname) {
|
||||||
|
if (!hostname) return false;
|
||||||
|
var h = String(hostname).toLowerCase();
|
||||||
|
if (h.slice(-13) !== ".dracodev.net") return false;
|
||||||
|
var first = h.split(".")[0];
|
||||||
|
return first.length > 4 && first.slice(-4) === "-dev";
|
||||||
|
}
|
||||||
|
function cleanUrl(u) {
|
||||||
|
if (!u || typeof u !== "string") return null;
|
||||||
|
if (u.indexOf("{{") !== -1) return null;
|
||||||
|
if (!u.replace(/\s/g, "")) return null;
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
function withSlash(u) {
|
||||||
|
return u.charAt(u.length - 1) !== "/" ? u + "/" : u;
|
||||||
|
}
|
||||||
|
function joinUrl(base, path) {
|
||||||
|
var b = withSlash(base);
|
||||||
|
if (!path) return b;
|
||||||
|
if (/^https?:\/\//.test(path)) return path;
|
||||||
|
return b + String(path).replace(/^\//, "");
|
||||||
|
}
|
||||||
|
function parseDate(s) {
|
||||||
|
if (!s) return null;
|
||||||
|
var t = new Date(String(s).indexOf("T") === -1 && String(s).indexOf("Z") === -1
|
||||||
|
? String(s).replace(" ", "T") + "Z" : s);
|
||||||
|
return isNaN(t.getTime()) ? null : t;
|
||||||
|
}
|
||||||
|
function fmtDate(s) {
|
||||||
|
var t = parseDate(s);
|
||||||
|
if (!t) return s ? String(s).slice(0, 16) : "";
|
||||||
|
try { return t.toLocaleString("fr-FR"); } catch (e) { return t.toISOString().slice(0, 16); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Résolution des bases. `env` permet les tests sans navigateur :
|
||||||
|
* { hostname, barUrls, configUrls, tabHrefs, dracoUrls }. */
|
||||||
|
function resolveUrls(env) {
|
||||||
|
env = env || {};
|
||||||
|
var hostname = env.hostname;
|
||||||
|
if (hostname === undefined) {
|
||||||
|
try { hostname = (global.location && global.location.hostname) || ""; } catch (e) { hostname = ""; }
|
||||||
|
}
|
||||||
|
var dev = isDevHost(hostname);
|
||||||
|
var urls = {};
|
||||||
|
APP_ORDER.forEach(function (app) {
|
||||||
|
var u = cleanUrl(env.barUrls && env.barUrls[app])
|
||||||
|
|| cleanUrl(env.configUrls && env.configUrls[app]);
|
||||||
|
if (!u && dev) u = DEV_URLS[app] || null;
|
||||||
|
if (!u) {
|
||||||
|
var href = null;
|
||||||
|
if (env.tabHrefs && env.tabHrefs[app]) href = env.tabHrefs[app];
|
||||||
|
else if (typeof document !== "undefined" && document.querySelector) {
|
||||||
|
var tab = document.querySelector('.draco-apptab[data-app="' + app + '"]');
|
||||||
|
href = tab ? tab.getAttribute("href") : null;
|
||||||
|
}
|
||||||
|
u = cleanUrl(href);
|
||||||
|
}
|
||||||
|
if (!u && env.dracoUrls && env.dracoUrls[app]) u = cleanUrl(env.dracoUrls[app]);
|
||||||
|
if (!u) {
|
||||||
|
/* ObsiGate : window.DRACO_URLS injecté par le backend. */
|
||||||
|
try {
|
||||||
|
if (global.DRACO_URLS && global.DRACO_URLS[app]) u = cleanUrl(global.DRACO_URLS[app]);
|
||||||
|
} catch (e) { /* rien */ }
|
||||||
|
}
|
||||||
|
if (!u) u = DEFAULT_URLS[app];
|
||||||
|
urls[app] = withSlash(u);
|
||||||
|
});
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
function browserEnv() {
|
||||||
|
var cfg = global.DRACO_BAR_CONFIG || {};
|
||||||
|
var bar = global.DRACO_BAR || {};
|
||||||
|
var tabs = {};
|
||||||
|
try {
|
||||||
|
if (typeof document !== "undefined" && document.querySelectorAll) {
|
||||||
|
var nodes = document.querySelectorAll(".draco-apptab[data-app]");
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
tabs[nodes[i].getAttribute("data-app")] = nodes[i].getAttribute("href");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* rien */ }
|
||||||
|
return {
|
||||||
|
barUrls: bar.urls || null, configUrls: cfg.urls || null,
|
||||||
|
tabHrefs: tabs, dracoUrls: global.DRACO_URLS || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFlowdeck(base, payload) {
|
||||||
|
var list = (payload && payload.notifications) || [];
|
||||||
|
return list.map(function (n) {
|
||||||
|
return {
|
||||||
|
app: "flowdeck", id: n.id, title: n.title || "(sans titre)",
|
||||||
|
detail: n.message || "", date: n.created_at || "",
|
||||||
|
active: !n.is_read, state: n.is_read ? "lue" : "active",
|
||||||
|
kind: n.ntype || "", url: joinUrl(base, n.url || "")
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function normalizeTiktak(base, payload) {
|
||||||
|
var list = Array.isArray(payload) ? payload : (payload && payload.notifications) || [];
|
||||||
|
return list.map(function (n) {
|
||||||
|
return {
|
||||||
|
app: "tiktak", id: n.id, title: n.title || "(sans titre)",
|
||||||
|
detail: n.body || "", date: n.created_at || "",
|
||||||
|
active: !n.is_read, state: n.is_read ? "lue" : "active",
|
||||||
|
kind: n.kind || "",
|
||||||
|
url: n.task_id ? withSlash(base) + "?task=" + n.task_id : joinUrl(base, "notifications")
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function normalizeKanbante(base, payload) {
|
||||||
|
var list = (payload && payload.events) || [];
|
||||||
|
return list.map(function (e) {
|
||||||
|
var label = EVENT_LABELS[e.event_type] || e.event_type || "événement";
|
||||||
|
var detail = (e.from_column ? e.from_column + " → " : "") + (e.to_column || "");
|
||||||
|
return {
|
||||||
|
app: "kanbante", id: e.id,
|
||||||
|
title: (e.repo_full_name || "?") + " #" + e.issue_number + " — " + label,
|
||||||
|
detail: detail, date: e.created_at || "",
|
||||||
|
active: false, state: label, kind: e.event_type || "",
|
||||||
|
url: joinUrl(base, "board/" + (e.repo_full_name || ""))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/* Actives d'abord, puis date décroissante (sans date en dernier). */
|
||||||
|
function sortAlerts(list) {
|
||||||
|
return list.slice().sort(function (a, b) {
|
||||||
|
if (a.active !== b.active) return a.active ? -1 : 1;
|
||||||
|
var da = parseDate(a.date), db = parseDate(b.date);
|
||||||
|
if (da && db) return db - da;
|
||||||
|
if (da) return -1;
|
||||||
|
if (db) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function countActive(list) {
|
||||||
|
var n = 0;
|
||||||
|
list.forEach(function (a) { if (a.active) n++; });
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Rendu (navigateur seulement) ---- */
|
||||||
|
var panel = null, listEl = null, badgeTimer = null;
|
||||||
|
|
||||||
|
function el(tag, cls, text) {
|
||||||
|
var d = document.createElement(tag);
|
||||||
|
if (cls) d.className = cls;
|
||||||
|
if (text !== undefined && text !== null) d.textContent = text;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
function ensurePanel() {
|
||||||
|
if (panel) return panel;
|
||||||
|
var ov = el("div", "draco-alertes-overlay");
|
||||||
|
ov.setAttribute("role", "dialog");
|
||||||
|
ov.setAttribute("aria-label", "Alertes unifiées");
|
||||||
|
var box = el("div", "draco-alertes-box");
|
||||||
|
var head = el("div", "draco-alertes-head");
|
||||||
|
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", "✕");
|
||||||
|
close.setAttribute("aria-label", "Fermer");
|
||||||
|
close.addEventListener("click", closePanel);
|
||||||
|
head.appendChild(close);
|
||||||
|
box.appendChild(head);
|
||||||
|
box.appendChild(note);
|
||||||
|
listEl = el("div", "draco-alertes-list");
|
||||||
|
box.appendChild(listEl);
|
||||||
|
ov.appendChild(box);
|
||||||
|
ov.addEventListener("click", function (ev) { if (ev.target === ov) closePanel(); });
|
||||||
|
document.addEventListener("keydown", function (ev) {
|
||||||
|
if (ev.key === "Escape") closePanel();
|
||||||
|
});
|
||||||
|
document.body.appendChild(ov);
|
||||||
|
panel = ov;
|
||||||
|
return ov;
|
||||||
|
}
|
||||||
|
function closePanel() { if (panel) panel.style.display = "none"; }
|
||||||
|
|
||||||
|
function renderSection(title, items, fallback) {
|
||||||
|
var sec = el("section", "draco-alertes-section");
|
||||||
|
sec.appendChild(el("h3", "", title + " (" + items.length + ")"));
|
||||||
|
if (!items.length) {
|
||||||
|
sec.appendChild(el("p", "draco-alertes-empty", fallback));
|
||||||
|
return sec;
|
||||||
|
}
|
||||||
|
items.forEach(function (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));
|
||||||
|
var meta = el("div", "draco-alertes-meta",
|
||||||
|
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) {
|
||||||
|
ensurePanel();
|
||||||
|
listEl.textContent = "";
|
||||||
|
var all = [];
|
||||||
|
APP_ORDER.forEach(function (app) {
|
||||||
|
(results[app] && results[app].items || []).forEach(function (a) { all.push(a); });
|
||||||
|
});
|
||||||
|
var actives = sortAlerts(all.filter(function (a) { return a.active; }));
|
||||||
|
var history = sortAlerts(all.filter(function (a) { return !a.active; }));
|
||||||
|
listEl.appendChild(el("h3", "draco-alertes-group", "Alertes actives"));
|
||||||
|
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) {
|
||||||
|
if (results[app] && results[app].error) return; /* déjà signalé ci-dessus */
|
||||||
|
if (app === "obsigate") {
|
||||||
|
var link = el("div", "draco-alertes-empty");
|
||||||
|
var a = el("a", "", "Ouvrir ObsiGate");
|
||||||
|
a.href = urls.obsigate;
|
||||||
|
link.appendChild(a);
|
||||||
|
var sec = el("section", "draco-alertes-section");
|
||||||
|
sec.appendChild(el("h3", "", "ObsiGate (0)"));
|
||||||
|
sec.appendChild(link);
|
||||||
|
listEl.appendChild(sec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var items = history.filter(function (x) { return x.app === app; });
|
||||||
|
listEl.appendChild(renderSection(APP_LABELS[app], items,
|
||||||
|
"Aucun historique disponible pour cette application."));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchJson(url, timeoutMs) {
|
||||||
|
var ctrl = null, timer = null;
|
||||||
|
try {
|
||||||
|
if (typeof AbortController !== "undefined") {
|
||||||
|
ctrl = new AbortController();
|
||||||
|
timer = setTimeout(function () { ctrl.abort(); }, timeoutMs || 8000);
|
||||||
|
}
|
||||||
|
} catch (e) { ctrl = null; }
|
||||||
|
var opts = { credentials: "include" };
|
||||||
|
if (ctrl) opts.signal = ctrl.signal;
|
||||||
|
return fetch(url, opts).then(function (r) {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
if (!r.ok) {
|
||||||
|
var err = new Error("HTTP " + r.status);
|
||||||
|
err.status = r.status;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return r.json();
|
||||||
|
}).catch(function (e) {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function loadAll() {
|
||||||
|
var urls = resolveUrls(browserEnv());
|
||||||
|
var jobs = {
|
||||||
|
flowdeck: fetchJson(urls.flowdeck + "api/notifications?limit=50")
|
||||||
|
.then(function (p) { return { items: normalizeFlowdeck(urls.flowdeck, p) }; }),
|
||||||
|
tiktak: fetchJson(urls.tiktak + "api/notifications")
|
||||||
|
.then(function (p) { return { items: normalizeTiktak(urls.tiktak, p) }; }),
|
||||||
|
kanbante: fetchJson(urls.kanbante + "api/events?limit=100")
|
||||||
|
.then(function (p) { return { items: normalizeKanbante(urls.kanbante, p) }; })
|
||||||
|
};
|
||||||
|
var keys = Object.keys(jobs);
|
||||||
|
return Promise.all(keys.map(function (k) {
|
||||||
|
return jobs[k].then(function (v) { return [k, v]; }).catch(function (e) {
|
||||||
|
var msg = (e && e.status === 401)
|
||||||
|
? "Connexion requise sur " + APP_LABELS[k] + " : ouvrez sa page native pour vous y connecter, puis rouvrez ce panneau."
|
||||||
|
: "Application injoignable depuis cet hôte (" + (e && e.message ? e.message : "erreur réseau") + ").";
|
||||||
|
return [k, { items: [], error: msg }];
|
||||||
|
});
|
||||||
|
})).then(function (pairs) {
|
||||||
|
var results = { obsigate: { items: [] } };
|
||||||
|
pairs.forEach(function (kv) { results[kv[0]] = kv[1]; });
|
||||||
|
return { urls: urls, results: results };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function setBadge(n) {
|
||||||
|
try {
|
||||||
|
if (global.DRACO_BAR && typeof global.DRACO_BAR.setCounts === "function") {
|
||||||
|
global.DRACO_BAR.setCounts({ alertes: n });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) { /* repli ci-dessous */ }
|
||||||
|
try {
|
||||||
|
document.querySelectorAll('.draco-badge[data-badge="alertes"]').forEach(function (b) {
|
||||||
|
b.textContent = n;
|
||||||
|
b.hidden = !(n > 0);
|
||||||
|
});
|
||||||
|
} catch (e) { /* rien */ }
|
||||||
|
}
|
||||||
|
function refreshBadge() {
|
||||||
|
return loadAll().then(function (r) {
|
||||||
|
var all = [];
|
||||||
|
APP_ORDER.forEach(function (app) {
|
||||||
|
(r.results[app] && r.results[app].items || []).forEach(function (a) { all.push(a); });
|
||||||
|
});
|
||||||
|
setBadge(countActive(all));
|
||||||
|
return countActive(all);
|
||||||
|
}).catch(function () { /* pastille optionnelle : la barre vit sans */ });
|
||||||
|
}
|
||||||
|
function openPanel() {
|
||||||
|
ensurePanel();
|
||||||
|
panel.style.display = "flex";
|
||||||
|
listEl.textContent = "";
|
||||||
|
listEl.appendChild(el("p", "draco-alertes-empty", "Chargement des alertes…"));
|
||||||
|
loadAll().then(function (r) {
|
||||||
|
render(r.results, r.urls);
|
||||||
|
var all = [];
|
||||||
|
APP_ORDER.forEach(function (app) {
|
||||||
|
(r.results[app] && r.results[app].items || []).forEach(function (a) { all.push(a); });
|
||||||
|
});
|
||||||
|
setBadge(countActive(all));
|
||||||
|
}).catch(function () {
|
||||||
|
render({ obsigate: { items: [] } }, resolveUrls(browserEnv()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var api = {
|
||||||
|
open: openPanel, close: closePanel, refreshBadge: refreshBadge,
|
||||||
|
_test: {
|
||||||
|
resolveUrls: resolveUrls, normalizeFlowdeck: normalizeFlowdeck,
|
||||||
|
normalizeTiktak: normalizeTiktak, normalizeKanbante: normalizeKanbante,
|
||||||
|
sortAlerts: sortAlerts, countActive: countActive,
|
||||||
|
isDevHost: isDevHost, DEFAULT_URLS: DEFAULT_URLS, DEV_URLS: DEV_URLS
|
||||||
|
}
|
||||||
|
};
|
||||||
|
global.DRACO_ALERTES = api;
|
||||||
|
if (typeof module !== "undefined" && module.exports) module.exports = api._test;
|
||||||
|
|
||||||
|
/* Pastille au chargement (après les scripts defer de la barre). */
|
||||||
|
if (typeof document !== "undefined" && document.addEventListener) {
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
if (badgeTimer) clearTimeout(badgeTimer);
|
||||||
|
badgeTimer = setTimeout(refreshBadge, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})(typeof window !== "undefined" ? window : globalThis);
|
||||||
+6
-3
@@ -76,6 +76,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/static/style.css" />
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
<link rel="stylesheet" href="/static/draco-barre.css" />
|
<link rel="stylesheet" href="/static/draco-barre.css" />
|
||||||
|
<link rel="stylesheet" href="/static/draco-alertes.css" />
|
||||||
<link rel="stylesheet" href="/static/draco-panneau.css" />
|
<link rel="stylesheet" href="/static/draco-panneau.css" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@@ -395,8 +396,9 @@
|
|||||||
/* Configuration ObsiGate de la barre commune (Fusion #215).
|
/* Configuration ObsiGate de la barre commune (Fusion #215).
|
||||||
Phase 1 : navigation entre apps et domaine mémorisé
|
Phase 1 : navigation entre apps et domaine mémorisé
|
||||||
localement ; la recherche focus la recherche native
|
localement ; la recherche focus la recherche native
|
||||||
d'ObsiGate ; Alertes unifiées et Spark arrivent avec la
|
d'ObsiGate ; Alertes unifiées : panneau draco-alertes.js
|
||||||
fédération / l'agent commun (phases suivantes). */
|
(Fusion #69, agrégation à la demande, pas une fédération
|
||||||
|
temps réel) ; Spark via le panneau commun. */
|
||||||
/* #223 : les href des onglets portent les défauts officiels ;
|
/* #223 : les href des onglets portent les défauts officiels ;
|
||||||
l'hôte les réécrit depuis window.DRACO_URLS (injecté par le
|
l'hôte les réécrit depuis window.DRACO_URLS (injecté par le
|
||||||
backend depuis sa configuration) quand il est présent. */
|
backend depuis sa configuration) quand il est présent. */
|
||||||
@@ -433,7 +435,7 @@
|
|||||||
}
|
}
|
||||||
if (act === "palette") { var s = document.getElementById("search-input"); if (s) { s.focus(); s.scrollIntoView({ block: "center" }); } return; }
|
if (act === "palette") { var s = document.getElementById("search-input"); if (s) { s.focus(); s.scrollIntoView({ block: "center" }); } return; }
|
||||||
if (act === "menu") { var m = document.getElementById("header-menu-btn"); if (m) m.click(); return; }
|
if (act === "menu") { var m = document.getElementById("header-menu-btn"); if (m) m.click(); return; }
|
||||||
if (act === "alertes") { dire("Alertes unifiées : arrive avec la fédération (programme Fusion)."); return; }
|
if (act === "alertes") { if (window.DRACO_ALERTES) { window.DRACO_ALERTES.open(); } else { dire("Alertes unifiées : le module n'a pas pu se charger."); } return; }
|
||||||
if (act === "spark") {
|
if (act === "spark") {
|
||||||
if (window.DRACO_PANNEAU) { window.DRACO_PANNEAU.toggle(); }
|
if (window.DRACO_PANNEAU) { window.DRACO_PANNEAU.toggle(); }
|
||||||
else { dire("Spark : le panneau n'a pas pu se charger."); }
|
else { dire("Spark : le panneau n'a pas pu se charger."); }
|
||||||
@@ -6065,6 +6067,7 @@ curl -X POST https://votre-serveur.com/webhook \
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="/static/draco-alertes.js"></script>
|
||||||
<script defer src="/static/draco-barre.js"></script>
|
<script defer src="/static/draco-barre.js"></script>
|
||||||
<script defer src="/static/draco-liens.js"></script>
|
<script defer src="/static/draco-liens.js"></script>
|
||||||
<script defer src="/static/draco-panneau.js"></script>
|
<script defer src="/static/draco-panneau.js"></script>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.81.2",
|
"version": "2.81.3",
|
||||||
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
||||||
"main": "patch.js",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* ObsiGate — Alertes unifiées (Fusion #69, chantier 2).
|
||||||
|
*
|
||||||
|
* Tests du module partagé frontend/draco-alertes.js (logique pure,
|
||||||
|
* sans DOM) + vérification du gabarit livré frontend/index.html :
|
||||||
|
* le placeholder « Alertes unifiées : arrive avec la fédération »
|
||||||
|
* a disparu et le module est chargé.
|
||||||
|
*
|
||||||
|
* Usage: node tests/frontend/draco-alertes.test.mjs
|
||||||
|
*/
|
||||||
|
import test from "node:test";
|
||||||
|
import { strict as assert } from "node:assert";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const t = require(path.join(REPO_ROOT, "frontend", "draco-alertes.js"));
|
||||||
|
|
||||||
|
test("resolveUrls: config explicite > hote -dev > href > defaut prod", () => {
|
||||||
|
assert.equal(
|
||||||
|
t.resolveUrls({ hostname: "og.dracodev.net", tabHrefs: {} }).tiktak,
|
||||||
|
"https://tiktak.dracodev.net/"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
t.resolveUrls({ hostname: "og-dev.dracodev.net", tabHrefs: {} }).flowdeck,
|
||||||
|
"https://flowdeck-dev.dracodev.net/"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
t.resolveUrls({
|
||||||
|
hostname: "og-dev.dracodev.net",
|
||||||
|
configUrls: { kanbante: "https://kanbante.dracodev.net/" },
|
||||||
|
}).kanbante,
|
||||||
|
"https://kanbante.dracodev.net/"
|
||||||
|
);
|
||||||
|
assert.equal(t.isDevHost("og-dev.dracodev.net.evil.com"), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeFlowdeck/TikTak/Kanbante: actifs = non lus, Kanbante jamais actif", () => {
|
||||||
|
const fd = t.normalizeFlowdeck("https://flowdeck.dracodev.net/", {
|
||||||
|
notifications: [
|
||||||
|
{ id: 1, ntype: "mention", title: "Mention", message: "m", url: "/collections/3", is_read: 0, created_at: "2026-10-28T10:00:00" },
|
||||||
|
],
|
||||||
|
unread: 1,
|
||||||
|
});
|
||||||
|
assert.equal(fd[0].active, true);
|
||||||
|
assert.equal(fd[0].url, "https://flowdeck.dracodev.net/collections/3");
|
||||||
|
const tt = t.normalizeTiktak("https://tiktak.dracodev.net/", [
|
||||||
|
{ id: 7, kind: "reminder", title: "Rappel", body: "", task_id: 42, is_read: 0, created_at: "2026-10-28T08:00:00" },
|
||||||
|
]);
|
||||||
|
assert.equal(tt[0].url, "https://tiktak.dracodev.net/?task=42");
|
||||||
|
const kb = 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" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(kb[0].active, false);
|
||||||
|
assert.equal(kb[0].url, "https://kanbante.dracodev.net/board/Projets/fusion-trois-apps");
|
||||||
|
assert.equal(t.countActive([...fd, ...tt, ...kb]), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sortAlerts: actives d'abord, date decroissante", () => {
|
||||||
|
const sorted = t.sortAlerts([
|
||||||
|
{ app: "tiktak", active: false, date: "2026-10-28T12:00:00" },
|
||||||
|
{ app: "flowdeck", active: true, date: "2026-10-26T12:00:00" },
|
||||||
|
{ app: "flowdeck", active: true, date: "2026-10-28T12:00:00" },
|
||||||
|
]);
|
||||||
|
assert.equal(sorted[0].active, true);
|
||||||
|
assert.equal(sorted[0].date, "2026-10-28T12:00:00");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("index.html: placeholder Alertes disparu, module charge", () => {
|
||||||
|
const html = readFileSync(path.join(REPO_ROOT, "frontend", "index.html"), "utf8");
|
||||||
|
assert.equal(html.includes("Alertes unifiées : arrive avec la fédération"), false);
|
||||||
|
assert.equal(html.includes("/static/draco-alertes.js"), true);
|
||||||
|
assert.equal(html.includes("/static/draco-alertes.css"), true);
|
||||||
|
assert.equal(html.includes("DRACO_ALERTES.open()"), true);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user